cloudapi: Request depsolve from osbuild-worker

and return the response to the client. This uses the worker to depsolve
the requested packages. The result is returned to the client as a list
of packages using the same PackageMetadata schema as the ComposeStatus
response.  It will also time out after 5 minutes and return an error,
using the same timeout constant as depsolving during manifest
generation.

Related: RHEL-60125
This commit is contained in:
Brian C. Lane 2025-01-14 10:02:59 -08:00 committed by Brian C. Lane
parent e06e62ca03
commit 02d0b8ec01
4 changed files with 165 additions and 3 deletions

View file

@ -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
}

View file

@ -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"},

View file

@ -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
}

View file

@ -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",