Skip to content

Add depsolve to cloudapi #4564

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jan 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 11 additions & 7 deletions internal/cloudapi/v2/compose.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,18 +35,18 @@ func (bcpm BlueprintCustomizationsPartitioningMode) String() string {
}
}

// GetCustomizationsFromBlueprintRequest populates a blueprint customization struct
// with the data from the blueprint section of a ComposeRequest, which is similar but
// GetCustomizationsFromBlueprint populates a blueprint customization struct
// with the data from request Blueprint, which is similar but
// slightly different from the Cloudapi's Customizations section
// This starts with a new empty blueprint.Customization object
// If there are no customizations, it returns nil
func (request *ComposeRequest) GetCustomizationsFromBlueprintRequest() (*blueprint.Customizations, error) {
if request.Blueprint.Customizations == nil {
func (rbp *Blueprint) GetCustomizationsFromBlueprintRequest() (*blueprint.Customizations, error) {
if rbp.Customizations == nil {
return nil, nil
}

c := &blueprint.Customizations{}
rbpc := request.Blueprint.Customizations
rbpc := rbp.Customizations

if rbpc.Hostname != nil {
c.Hostname = rbpc.Hostname
Expand Down Expand Up @@ -499,8 +499,12 @@ func (request *ComposeRequest) GetBlueprintFromCompose() (blueprint.Blueprint, e
return bp, err
}

return ConvertRequestBP(*request.Blueprint)
}

// ConvertRequestBP takes a request Blueprint and returns a composer blueprint.Blueprint
func ConvertRequestBP(rbp Blueprint) (blueprint.Blueprint, error) {
var bp blueprint.Blueprint
rbp := request.Blueprint

// Copy all the parts from the OpenAPI Blueprint into a blueprint.Blueprint
// NOTE: Openapi fields may be nil, test for that first.
Expand Down Expand Up @@ -553,7 +557,7 @@ func (request *ComposeRequest) GetBlueprintFromCompose() (blueprint.Blueprint, e
}
}

customizations, err := request.GetCustomizationsFromBlueprintRequest()
customizations, err := rbp.GetCustomizationsFromBlueprintRequest()
if err != nil {
return bp, err
}
Expand Down
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
51 changes: 50 additions & 1 deletion internal/cloudapi/v2/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -674,7 +674,7 @@ func stagesToPackageMetadata(stages []osbuild.RPMStageMetadata) []PackageMetadat
Release: rpm.Release,
Epoch: rpm.Epoch,
Arch: rpm.Arch,
Sigmd5: rpm.SigMD5,
Sigmd5: common.ToPtr(rpm.SigMD5),
Signature: osbuild.RPMPackageMetadataToSignature(rpm),
},
)
Expand Down 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
}
Loading
Loading