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
|
ErrorInvalidPartitioningMode ServiceErrorCode = 37
|
||||||
ErrorInvalidUploadTarget ServiceErrorCode = 38
|
ErrorInvalidUploadTarget ServiceErrorCode = 38
|
||||||
ErrorBlueprintOrCustomNotBoth ServiceErrorCode = 39
|
ErrorBlueprintOrCustomNotBoth ServiceErrorCode = 39
|
||||||
|
ErrorMismatchedDistribution ServiceErrorCode = 40
|
||||||
|
ErrorMismatchedArchitecture ServiceErrorCode = 41
|
||||||
|
|
||||||
// Internal errors, these are bugs
|
// Internal errors, these are bugs
|
||||||
ErrorFailedToInitializeBlueprint ServiceErrorCode = 1000
|
ErrorFailedToInitializeBlueprint ServiceErrorCode = 1000
|
||||||
|
|
@ -131,6 +133,8 @@ func getServiceErrors() serviceErrors {
|
||||||
serviceError{ErrorInvalidPartitioningMode, http.StatusBadRequest, "Requested partitioning mode is invalid"},
|
serviceError{ErrorInvalidPartitioningMode, http.StatusBadRequest, "Requested partitioning mode is invalid"},
|
||||||
serviceError{ErrorInvalidUploadTarget, http.StatusBadRequest, "Invalid upload target for image type"},
|
serviceError{ErrorInvalidUploadTarget, http.StatusBadRequest, "Invalid upload target for image type"},
|
||||||
serviceError{ErrorBlueprintOrCustomNotBoth, http.StatusBadRequest, "Invalid request, include blueprint or customizations, not both"},
|
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{ErrorFailedToInitializeBlueprint, http.StatusInternalServerError, "Failed to initialize blueprint"},
|
||||||
serviceError{ErrorFailedToGenerateManifestSeed, http.StatusInternalServerError, "Failed to generate manifest seed"},
|
serviceError{ErrorFailedToGenerateManifestSeed, http.StatusInternalServerError, "Failed to generate manifest seed"},
|
||||||
|
|
|
||||||
|
|
@ -1354,3 +1354,52 @@ func uploadStatusFromJobStatus(js *worker.JobStatus, je *clienterrors.Error) Upl
|
||||||
}
|
}
|
||||||
return UploadStatusValueSuccess
|
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"
|
"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
|
// Server represents the state of the cloud Server
|
||||||
type Server struct {
|
type Server struct {
|
||||||
workers *worker.Server
|
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) {
|
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
|
// prepared to become a config variable
|
||||||
const depsolveTimeout = 5
|
ctx, cancel := context.WithTimeout(ctx, time.Minute*depsolveTimeoutMin)
|
||||||
ctx, cancel := context.WithTimeout(ctx, time.Minute*depsolveTimeout)
|
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
jobResult := &worker.ManifestJobByIDResult{
|
jobResult := &worker.ManifestJobByIDResult{
|
||||||
|
|
@ -515,7 +517,7 @@ func serializeManifest(ctx context.Context, manifestSource *manifest.Manifest, w
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
logWithId.Warning(fmt.Sprintf("Manifest job dependencies took longer than %d minutes to finish,"+
|
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,
|
jobResult.JobError = clienterrors.New(clienterrors.ErrorDepsolveTimeout,
|
||||||
"Timeout while waiting for package dependency resolution",
|
"Timeout while waiting for package dependency resolution",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue