Skip to content

Commit

Permalink
cloudapi: Request depsolve from osbuild-worker
Browse files Browse the repository at this point in the history
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
  • Loading branch information
bcl committed Jan 23, 2025
1 parent be75588 commit 645d76b
Show file tree
Hide file tree
Showing 4 changed files with 165 additions and 3 deletions.
107 changes: 107 additions & 0 deletions internal/cloudapi/v2/depsolve.go
Original file line number Diff line number Diff line change
@@ -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
}
4 changes: 4 additions & 0 deletions internal/cloudapi/v2/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"},
Expand Down
49 changes: 49 additions & 0 deletions internal/cloudapi/v2/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
8 changes: 5 additions & 3 deletions internal/cloudapi/v2/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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{
Expand Down Expand Up @@ -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",
Expand Down

0 comments on commit 645d76b

Please sign in to comment.