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:
parent
e06e62ca03
commit
02d0b8ec01
4 changed files with 165 additions and 3 deletions
107
internal/cloudapi/v2/depsolve.go
Normal file
107
internal/cloudapi/v2/depsolve.go
Normal 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
|
||||
}
|
||||
|
|
@ -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"},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue