diff --git a/internal/cloudapi/v2/errors.go b/internal/cloudapi/v2/errors.go index bfdf05476..b5aed0f14 100644 --- a/internal/cloudapi/v2/errors.go +++ b/internal/cloudapi/v2/errors.go @@ -76,6 +76,7 @@ const ( ErrorGettingJobType ServiceErrorCode = 1019 ErrorTenantNotInContext ServiceErrorCode = 1020 ErrorGettingComposeList ServiceErrorCode = 1021 + ErrorArtifactNotFound ServiceErrorCode = 1022 // Errors contained within this file ErrorUnspecified ServiceErrorCode = 10000 @@ -123,6 +124,7 @@ func getServiceErrors() serviceErrors { serviceError{ErrorInvalidNumberOfImageBuilds, http.StatusBadRequest, "Compose request has unsupported number of image builds"}, serviceError{ErrorInvalidOSTreeParams, http.StatusBadRequest, "Invalid OSTree parameters or parameter combination"}, serviceError{ErrorTenantNotFound, http.StatusBadRequest, "Tenant not found in JWT claims"}, + serviceError{ErrorArtifactNotFound, http.StatusBadRequest, "Artifact not found"}, serviceError{ErrorNoGPGKey, http.StatusBadRequest, "Invalid repository, when check_gpg is set, gpgkey must be specified"}, serviceError{ErrorValidationFailed, http.StatusBadRequest, "Request could not be validated"}, serviceError{ErrorComposeBadState, http.StatusBadRequest, "Compose is running or has failed"}, diff --git a/internal/cloudapi/v2/handler.go b/internal/cloudapi/v2/handler.go index b4cb6e899..e3f66576a 100644 --- a/internal/cloudapi/v2/handler.go +++ b/internal/cloudapi/v2/handler.go @@ -1514,3 +1514,54 @@ func (h *apiHandlers) GetDistributionList(ctx echo.Context) error { return ctx.JSON(http.StatusOK, distros) } + +// GetComposeDownload downloads a compose artifact +func (h *apiHandlers) GetComposeDownload(ctx echo.Context, id string) error { + return h.server.EnsureJobChannel(h.getComposeDownloadImpl)(ctx, id) +} + +func (h *apiHandlers) getComposeDownloadImpl(ctx echo.Context, id string) error { + jobId, err := uuid.Parse(id) + if err != nil { + return HTTPError(ErrorInvalidComposeId) + } + + jobType, err := h.server.workers.JobType(jobId) + if err != nil { + return HTTPError(ErrorComposeNotFound) + } + if jobType != worker.JobTypeOSBuild { + return HTTPError(ErrorInvalidJobType) + } + + var osbuildResult worker.OSBuildJobResult + jobInfo, err := h.server.workers.OSBuildJobInfo(jobId, &osbuildResult) + if err != nil { + return HTTPErrorWithInternal(ErrorGettingOSBuildJobStatus, err) + } + + // Is it finished? + if jobInfo.JobStatus.Finished.IsZero() { + err := fmt.Errorf("Cannot access artifacts before job is finished: %s", jobId) + return HTTPErrorWithInternal(ErrorArtifactNotFound, err) + } + + // Building only supports one target, but that may change, so make sure to check. + // NOTE: TargetResults isn't populated until it is finished + if len(osbuildResult.TargetResults) != 1 { + msg := fmt.Errorf("%#v", osbuildResult.TargetResults) + //return HTTPError(ErrorSeveralUploadTargets) + return HTTPErrorWithInternal(ErrorSeveralUploadTargets, msg) + } + tr := osbuildResult.TargetResults[0] + if tr.OsbuildArtifact == nil { + return HTTPError(ErrorArtifactNotFound) + } + + // NOTE: This also returns an error if the job isn't finished or it cannot find the file + file, err := h.server.workers.JobArtifactLocation(jobId, tr.OsbuildArtifact.ExportFilename) + if err != nil { + return HTTPErrorWithInternal(ErrorArtifactNotFound, err) + } + return ctx.Attachment(file, fmt.Sprintf("%s-%s", jobId, tr.OsbuildArtifact.ExportFilename)) +}