diff --git a/internal/cloudapi/v2/depsolve.go b/internal/cloudapi/v2/depsolve.go new file mode 100644 index 000000000..25d8388f0 --- /dev/null +++ b/internal/cloudapi/v2/depsolve.go @@ -0,0 +1,107 @@ +package v2 + +// DepsolveRequest methods + +import ( + "context" + "fmt" + "time" + + "github.com/osbuild/images/pkg/distrofactory" + "github.com/osbuild/images/pkg/reporegistry" + "github.com/osbuild/images/pkg/rpmmd" + "github.com/osbuild/images/pkg/sbom" + "github.com/osbuild/osbuild-composer/internal/worker" +) + +func (request *DepsolveRequest) Depsolve(df *distrofactory.Factory, rr *reporegistry.RepoRegistry, workers *worker.Server) ([]rpmmd.PackageSpec, error) { + // Convert the requested blueprint to a composer blueprint + bp, err := ConvertRequestBP(request.Blueprint) + if err != nil { + return nil, err + } + + // If the blueprint include distro and/or architecture they must match the ones + // in request -- otherwise the results may not be what is expected. + if len(bp.Distro) > 0 && bp.Distro != request.Distribution { + return nil, HTTPError(ErrorMismatchedDistribution) + } + + // XXX CloudAPI Blueprint needs to have missing Architecture added first + /* + if len(bp.Architecture) > 0 && bp.Architecture != request.Architecture { + return nil, HTTPError(ErrorMismatchedArchitecture) + } + */ + distro := df.GetDistro(request.Distribution) + if distro == nil { + return nil, HTTPError(ErrorUnsupportedDistribution) + } + distroArch, err := distro.GetArch(request.Architecture) + if err != nil { + return nil, HTTPErrorWithInternal(ErrorUnsupportedArchitecture, err) + } + + var repos []rpmmd.RepoConfig + if request.Repositories != nil { + repos, err = convertRepos(*request.Repositories, []Repository{}, []string{}) + if err != nil { + // Error comes from genRepoConfig and is already an HTTPError + return nil, err + } + } else { + repos, err = rr.ReposByArchName(request.Distribution, distroArch.Name(), false) + if err != nil { + return nil, HTTPErrorWithInternal(ErrorInvalidRepository, err) + } + } + + // Send the depsolve request to the worker + packageSet := make(map[string][]rpmmd.PackageSet, 1) + packageSet["depsolve"] = []rpmmd.PackageSet{{Include: bp.GetPackages(), Repositories: repos}} + + depsolveJobID, err := workers.EnqueueDepsolve(&worker.DepsolveJob{ + PackageSets: packageSet, + ModulePlatformID: distro.ModulePlatformID(), + Arch: distroArch.Name(), + Releasever: distro.Releasever(), + SbomType: sbom.StandardTypeNone, + }, "") + if err != nil { + return nil, HTTPErrorWithInternal(ErrorEnqueueingJob, err) + } + + // Limit how long a depsolve can take + ctx, cancel := context.WithTimeout(context.Background(), time.Minute*depsolveTimeoutMin) + defer cancel() + + // Wait until depsolve job is finished, fails, or is canceled + var result worker.DepsolveJobResult + for { + time.Sleep(time.Millisecond * 50) + info, err := workers.DepsolveJobInfo(depsolveJobID, &result) + if err != nil { + return nil, HTTPErrorWithInternal(ErrorFailedToDepsolve, err) + } + if result.JobError != nil { + return nil, HTTPErrorWithInternal(ErrorFailedToDepsolve, err) + } + if info.JobStatus != nil { + if info.JobStatus.Canceled { + return nil, HTTPErrorWithInternal(ErrorFailedToDepsolve, err) + } + + if !info.JobStatus.Finished.IsZero() { + break + } + } + + select { + case <-ctx.Done(): + return nil, HTTPErrorWithInternal(ErrorFailedToDepsolve, fmt.Errorf("Depsolve job %q timed out", depsolveJobID)) + default: + } + } + + return result.PackageSpecs["depsolve"], nil +} diff --git a/internal/cloudapi/v2/errors.go b/internal/cloudapi/v2/errors.go index 4f1cbd083..914d5dc96 100644 --- a/internal/cloudapi/v2/errors.go +++ b/internal/cloudapi/v2/errors.go @@ -50,6 +50,8 @@ const ( ErrorInvalidPartitioningMode ServiceErrorCode = 37 ErrorInvalidUploadTarget ServiceErrorCode = 38 ErrorBlueprintOrCustomNotBoth ServiceErrorCode = 39 + ErrorMismatchedDistribution ServiceErrorCode = 40 + ErrorMismatchedArchitecture ServiceErrorCode = 41 // Internal errors, these are bugs ErrorFailedToInitializeBlueprint ServiceErrorCode = 1000 @@ -131,6 +133,8 @@ func getServiceErrors() serviceErrors { serviceError{ErrorInvalidPartitioningMode, http.StatusBadRequest, "Requested partitioning mode is invalid"}, serviceError{ErrorInvalidUploadTarget, http.StatusBadRequest, "Invalid upload target for image type"}, serviceError{ErrorBlueprintOrCustomNotBoth, http.StatusBadRequest, "Invalid request, include blueprint or customizations, not both"}, + serviceError{ErrorMismatchedDistribution, http.StatusBadRequest, "Invalid request, Blueprint and Cloud API request Distribution must match."}, + serviceError{ErrorMismatchedArchitecture, http.StatusBadRequest, "Invalid request, Blueprint and Cloud API request Architecture must match."}, serviceError{ErrorFailedToInitializeBlueprint, http.StatusInternalServerError, "Failed to initialize blueprint"}, serviceError{ErrorFailedToGenerateManifestSeed, http.StatusInternalServerError, "Failed to generate manifest seed"}, diff --git a/internal/cloudapi/v2/handler.go b/internal/cloudapi/v2/handler.go index 378bf55b0..06502fa81 100644 --- a/internal/cloudapi/v2/handler.go +++ b/internal/cloudapi/v2/handler.go @@ -1354,3 +1354,52 @@ func uploadStatusFromJobStatus(js *worker.JobStatus, je *clienterrors.Error) Upl } return UploadStatusValueSuccess } + +// PostDepsolveBlueprint depsolves the packages in a blueprint and returns +// the results as a list of rpmmd.PackageSpecs +func (h *apiHandlers) PostDepsolveBlueprint(ctx echo.Context) error { + var request DepsolveRequest + err := ctx.Bind(&request) + if err != nil { + return err + } + + // Depsolve the requested blueprint + // Any errors returned are suitable as a response + deps, err := request.Depsolve(h.server.distros, h.server.repos, h.server.workers) + if err != nil { + return err + } + + return ctx.JSON(http.StatusOK, + DepsolveResponse{ + Packages: packageSpecToPackageMetadata(deps), + }) +} + +// packageSpecToPackageMetadata converts the rpmmd.PackageSpec to PackageMetadata +// This is used to return package information from the blueprint depsolve request +// using the common PackageMetadata format from the openapi schema. +func packageSpecToPackageMetadata(pkgspecs []rpmmd.PackageSpec) []PackageMetadata { + packages := make([]PackageMetadata, 0) + for _, rpm := range pkgspecs { + // Set epoch if it is not 0 + + var epoch *string + if rpm.Epoch > 0 { + epoch = common.ToPtr(strconv.FormatUint(uint64(rpm.Epoch), 10)) + } + packages = append(packages, + PackageMetadata{ + Type: "rpm", + Name: rpm.Name, + Version: rpm.Version, + Release: rpm.Release, + Epoch: epoch, + Arch: rpm.Arch, + Checksum: common.ToPtr(rpm.Checksum), + }, + ) + } + return packages +} diff --git a/internal/cloudapi/v2/server.go b/internal/cloudapi/v2/server.go index 245ff4459..80ad5927d 100644 --- a/internal/cloudapi/v2/server.go +++ b/internal/cloudapi/v2/server.go @@ -37,6 +37,9 @@ import ( "github.com/osbuild/osbuild-composer/internal/worker/clienterrors" ) +// How long to wait for a depsolve job to finish +const depsolveTimeoutMin = 5 + // Server represents the state of the cloud Server type Server struct { workers *worker.Server @@ -447,8 +450,7 @@ func (s *Server) enqueueKojiCompose(taskID uint64, server, name, version, releas func serializeManifest(ctx context.Context, manifestSource *manifest.Manifest, workers *worker.Server, depsolveJobID, containerResolveJobID, ostreeResolveJobID, manifestJobID uuid.UUID, seed int64) { // prepared to become a config variable - const depsolveTimeout = 5 - ctx, cancel := context.WithTimeout(ctx, time.Minute*depsolveTimeout) + ctx, cancel := context.WithTimeout(ctx, time.Minute*depsolveTimeoutMin) defer cancel() jobResult := &worker.ManifestJobByIDResult{ @@ -515,7 +517,7 @@ func serializeManifest(ctx context.Context, manifestSource *manifest.Manifest, w select { case <-ctx.Done(): logWithId.Warning(fmt.Sprintf("Manifest job dependencies took longer than %d minutes to finish,"+ - " or the server is shutting down, returning to avoid dangling routines", depsolveTimeout)) + " or the server is shutting down, returning to avoid dangling routines", depsolveTimeoutMin)) jobResult.JobError = clienterrors.New(clienterrors.ErrorDepsolveTimeout, "Timeout while waiting for package dependency resolution",