Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 136 additions & 6 deletions cmd/api/api/volumes.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,9 @@ func (s *ApiService) ListVolumes(ctx context.Context, request oapi.ListVolumesRe

// CreateVolume creates a new volume
// Supports two modes:
// - JSON body: Creates an empty volume of the specified size
// - Multipart form: Creates a volume pre-populated with content from a tar.gz archive
// - JSON body: Creates an empty volume of the specified size
// - Multipart form (DEPRECATED): Creates a volume from a tar.gz archive
// New integrations should use CreateVolumeFromArchive instead
func (s *ApiService) CreateVolume(ctx context.Context, request oapi.CreateVolumeRequestObject) (oapi.CreateVolumeResponseObject, error) {
log := logger.FromContext(ctx)

Expand Down Expand Up @@ -66,9 +67,9 @@ func (s *ApiService) CreateVolume(ctx context.Context, request oapi.CreateVolume
return oapi.CreateVolume201JSONResponse(volumeToOAPI(*vol)), nil
}

// Handle multipart request (volume with archive content)
// Handle multipart request (DEPRECATED - volume with archive content)
if request.MultipartBody != nil {
return s.createVolumeFromMultipart(ctx, request.MultipartBody)
return s.createVolumeFromMultipartDeprecated(ctx, request.MultipartBody)
}

return oapi.CreateVolume400JSONResponse{
Expand All @@ -77,8 +78,9 @@ func (s *ApiService) CreateVolume(ctx context.Context, request oapi.CreateVolume
}, nil
}

// createVolumeFromMultipart handles creating a volume from multipart form data with archive content
func (s *ApiService) createVolumeFromMultipart(ctx context.Context, multipartReader *multipart.Reader) (oapi.CreateVolumeResponseObject, error) {
// createVolumeFromMultipartDeprecated handles the deprecated multipart form on POST /volumes
// New integrations should use POST /volumes/from-archive instead
func (s *ApiService) createVolumeFromMultipartDeprecated(ctx context.Context, multipartReader *multipart.Reader) (oapi.CreateVolumeResponseObject, error) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of deprecating it, just delete it I think.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it is technically a breaking change in staging and prod (for kernel org) if we simply delete this

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but I guess that is fine

log := logger.FromContext(ctx)

var name string
Expand Down Expand Up @@ -198,6 +200,134 @@ func (s *ApiService) createVolumeFromMultipart(ctx context.Context, multipartRea
}, nil
}

// CreateVolumeFromArchive creates a new volume pre-populated with content from a tar.gz archive
func (s *ApiService) CreateVolumeFromArchive(ctx context.Context, request oapi.CreateVolumeFromArchiveRequestObject) (oapi.CreateVolumeFromArchiveResponseObject, error) {
log := logger.FromContext(ctx)

if request.Body == nil {
return oapi.CreateVolumeFromArchive400JSONResponse{
Code: "invalid_request",
Message: "multipart body is required",
}, nil
}

var name string
var sizeGb int
var id *string
var archiveReader io.Reader

for {
part, err := request.Body.NextPart()
if err == io.EOF {
break
}
if err != nil {
return oapi.CreateVolumeFromArchive400JSONResponse{
Code: "invalid_form",
Message: "failed to parse multipart form: " + err.Error(),
}, nil
}

switch part.FormName() {
case "name":
data, err := io.ReadAll(part)
if err != nil {
return oapi.CreateVolumeFromArchive400JSONResponse{
Code: "invalid_field",
Message: "failed to read name field",
}, nil
}
name = string(data)
case "size_gb":
data, err := io.ReadAll(part)
if err != nil {
return oapi.CreateVolumeFromArchive400JSONResponse{
Code: "invalid_field",
Message: "failed to read size_gb field",
}, nil
}
sizeGb, err = strconv.Atoi(string(data))
if err != nil || sizeGb <= 0 {
return oapi.CreateVolumeFromArchive400JSONResponse{
Code: "invalid_field",
Message: "size_gb must be a positive integer",
}, nil
}
case "id":
data, err := io.ReadAll(part)
if err != nil {
return oapi.CreateVolumeFromArchive400JSONResponse{
Code: "invalid_field",
Message: "failed to read id field",
}, nil
}
idStr := string(data)
if idStr != "" {
id = &idStr
}
case "content":
archiveReader = part
// Process the archive immediately while we have the reader
if name == "" {
return oapi.CreateVolumeFromArchive400JSONResponse{
Code: "missing_field",
Message: "name is required",
}, nil
}
if sizeGb <= 0 {
return oapi.CreateVolumeFromArchive400JSONResponse{
Code: "missing_field",
Message: "size_gb is required",
}, nil
}

// Create the volume from archive
domainReq := volumes.CreateVolumeFromArchiveRequest{
Name: name,
SizeGb: sizeGb,
Id: id,
}

vol, err := s.VolumeManager.CreateVolumeFromArchive(ctx, domainReq, archiveReader)
if err != nil {
if errors.Is(err, volumes.ErrArchiveTooLarge) {
return oapi.CreateVolumeFromArchive400JSONResponse{
Code: "archive_too_large",
Message: err.Error(),
}, nil
}
if errors.Is(err, volumes.ErrAlreadyExists) {
return oapi.CreateVolumeFromArchive409JSONResponse{
Code: "already_exists",
Message: "volume with this ID already exists",
}, nil
}
log.ErrorContext(ctx, "failed to create volume from archive", "error", err, "name", name)
return oapi.CreateVolumeFromArchive500JSONResponse{
Code: "internal_error",
Message: "failed to create volume",
}, nil
}

return oapi.CreateVolumeFromArchive201JSONResponse(volumeToOAPI(*vol)), nil
}
}

// If we get here without processing content, it means content was not provided
if archiveReader == nil {
return oapi.CreateVolumeFromArchive400JSONResponse{
Code: "missing_file",
Message: "content file is required",
}, nil
}

// Should not reach here
return oapi.CreateVolumeFromArchive500JSONResponse{
Code: "internal_error",
Message: "unexpected error processing request",
}, nil
}

// GetVolume gets volume details
// The id parameter can be either a volume ID or name
// Note: Resolution is handled by ResolveResource middleware
Expand Down
Loading