Skip to content
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

Add depsolving and package search to cloudapi #4390

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
2 changes: 1 addition & 1 deletion cmd/osbuild-composer/composer.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ func (c *Composer) InitAPI(cert, key string, enableTLS bool, enableMTLS bool, en
TenantProviderFields: c.config.Koji.JWTTenantProviderFields,
}

c.api = cloudapi.NewServer(c.workers, c.distros, c.repos, config)
c.api = cloudapi.NewServer(c.workers, c.distros, c.repos, c.solver, config)

if !enableTLS {
c.apiListener = l
Expand Down
5 changes: 3 additions & 2 deletions internal/cloudapi/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"net/http"

"github.com/osbuild/images/pkg/distrofactory"
"github.com/osbuild/images/pkg/dnfjson"
"github.com/osbuild/images/pkg/reporegistry"

v2 "github.com/osbuild/osbuild-composer/internal/cloudapi/v2"
Expand All @@ -14,9 +15,9 @@ type Server struct {
v2 *v2.Server
}

func NewServer(workers *worker.Server, distros *distrofactory.Factory, repos *reporegistry.RepoRegistry, config v2.ServerConfig) *Server {
func NewServer(workers *worker.Server, distros *distrofactory.Factory, repos *reporegistry.RepoRegistry, solver *dnfjson.BaseSolver, config v2.ServerConfig) *Server {
server := &Server{
v2: v2.NewServer(workers, distros, repos, config),
v2: v2.NewServer(workers, distros, repos, solver, config),
}
return server
}
Expand Down
28 changes: 20 additions & 8 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 All @@ -514,6 +518,9 @@ func (request *ComposeRequest) GetBlueprintFromCompose() (blueprint.Blueprint, e
if rbp.Distro != nil {
bp.Distro = *rbp.Distro
}
if rbp.Architecture != nil {
bp.Arch = *rbp.Architecture
}

if rbp.Packages != nil {
for _, pkg := range *rbp.Packages {
Expand Down Expand Up @@ -553,7 +560,7 @@ func (request *ComposeRequest) GetBlueprintFromCompose() (blueprint.Blueprint, e
}
}

customizations, err := request.GetCustomizationsFromBlueprintRequest()
customizations, err := rbp.GetCustomizationsFromBlueprintRequest()
if err != nil {
return bp, err
}
Expand Down Expand Up @@ -1144,7 +1151,12 @@ func (request *ComposeRequest) GetImageRequests(distroFactory *distrofactory.Fac
}
var irs []imageRequest
for _, ir := range *request.ImageRequests {
arch, err := distribution.GetArch(ir.Architecture)
reqArch := ir.Architecture
// If there is an architecture in the blueprint it overrides the request's arch
if len(bp.Arch) > 0 {
reqArch = bp.Arch
}
arch, err := distribution.GetArch(reqArch)
if err != nil {
return nil, HTTPError(ErrorUnsupportedArchitecture)
}
Expand Down
27 changes: 27 additions & 0 deletions internal/cloudapi/v2/compose_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -848,6 +848,33 @@ func TestGetImageRequests_BlueprintDistro(t *testing.T) {
assert.Equal(t, got[0].blueprint.Distro, "fedora-39")
}

// TestGetImageRequests_BlueprintArch test to make sure blueprint architecture overrides
// the request arch
func TestGetImageRequests_BlueprintArch(t *testing.T) {
uo := UploadOptions(struct{}{})
request := &ComposeRequest{
Distribution: "fedora-40",
ImageRequest: &ImageRequest{
Architecture: "x86_64",
ImageType: ImageTypesAws,
UploadOptions: &uo,
Repositories: []Repository{},
},
Blueprint: &Blueprint{
Name: "arch-test",
Architecture: common.ToPtr("aarch64"),
},
}
// NOTE: current directory is the location of this file, back up so it can use ./repositories/
rr, err := reporegistry.New([]string{"../../../"})
require.NoError(t, err)
got, err := request.GetImageRequests(distrofactory.NewDefault(), rr)
assert.NoError(t, err)
require.Len(t, got, 1)
require.Greater(t, len(got[0].repositories), 0)
assert.Equal(t, got[0].blueprint.Arch, "aarch64")
}

func TestOpenSCAPTailoringOptions(t *testing.T) {
cr := ComposeRequest{
Customizations: &Customizations{
Expand Down
157 changes: 157 additions & 0 deletions internal/cloudapi/v2/depsolve.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
package v2

import (
"log"

"github.com/osbuild/images/pkg/arch"
"github.com/osbuild/images/pkg/distro"
"github.com/osbuild/images/pkg/distrofactory"
"github.com/osbuild/images/pkg/dnfjson"
"github.com/osbuild/images/pkg/reporegistry"
"github.com/osbuild/images/pkg/rpmmd"
"github.com/osbuild/images/pkg/sbom"
)

func (request *DepsolveRequest) DepsolveBlueprint(df *distrofactory.Factory, rr *reporegistry.RepoRegistry, solver *dnfjson.BaseSolver) ([]rpmmd.PackageSpec, error) {
bp, err := ConvertRequestBP(request.Blueprint)
if err != nil {
return nil, err
}

// Distro name, in order of priority
// bp.Distro
// host distro
var originalDistroName string
if len(bp.Distro) > 0 {
originalDistroName = bp.Distro
} else {
originalDistroName, err = distro.GetHostDistroName()
if err != nil {
return nil, HTTPErrorWithInternal(ErrorUnsupportedDistribution, err)
}
}

distribution := df.GetDistro(originalDistroName)
if distribution == nil {
return nil, HTTPError(ErrorUnsupportedDistribution)
}

var originalArchName string
if len(bp.Arch) > 0 {
originalArchName = bp.Arch
} else {
originalArchName = arch.Current().String()
}
distroArch, err := distribution.GetArch(originalArchName)
if err != nil {
return nil, HTTPErrorWithInternal(ErrorUnsupportedArchitecture, err)
}

// Get the repositories to use for depsolving
// Either the list passed in with the request, or the defaults for the distro+arch
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(originalDistroName, distroArch.Name(), false)
if err != nil {
return nil, HTTPErrorWithInternal(ErrorInvalidRepository, err)
}
}

s := solver.NewWithConfig(
distribution.ModulePlatformID(),
distribution.Releasever(),
distroArch.Name(),
distribution.Name())
solved, err := s.Depsolve([]rpmmd.PackageSet{{Include: bp.GetPackages(), Repositories: repos}}, sbom.StandardTypeNone)
if err != nil {
return nil, HTTPErrorWithInternal(ErrorFailedToDepsolve, err)
}

if err := solver.CleanCache(); err != nil {
// log and ignore
log.Printf("Error during rpm repo cache cleanup: %s", err.Error())
}

return solved.Packages, nil
}

// PackageSearch uses the solver to search for the details of one or more packages
// It will search for exact package name matches, and for searches using globs on the name
// or version.
// If a specific distro and/or arch are included in the blueprint it will use that for the
// search, otherwise it will use the host's distro and arch.
// If separate repositories are included in the request they are used instead.
func (request *PackageSearchRequest) PackageSearch(df *distrofactory.Factory, rr *reporegistry.RepoRegistry, solver *dnfjson.BaseSolver) ([]rpmmd.PackageInfo, error) {
bp, err := ConvertRequestBP(request.Blueprint)
if err != nil {
return nil, err
}

// Distro name, in order of priority
// bp.Distro
// host distro
var originalDistroName string
if len(bp.Distro) > 0 {
originalDistroName = bp.Distro
} else {
originalDistroName, err = distro.GetHostDistroName()
if err != nil {
return nil, HTTPErrorWithInternal(ErrorUnsupportedDistribution, err)
}
}

distribution := df.GetDistro(originalDistroName)
if distribution == nil {
return nil, HTTPError(ErrorUnsupportedDistribution)
}

var originalArchName string
if len(bp.Arch) > 0 {
originalArchName = bp.Arch
} else {
originalArchName = arch.Current().String()
}
distroArch, err := distribution.GetArch(originalArchName)
if err != nil {
return nil, HTTPErrorWithInternal(ErrorUnsupportedArchitecture, err)
}

// Get the repositories to use for depsolving
// Either the list passed in with the request, or the defaults for the distro+arch
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(originalDistroName, distroArch.Name(), false)
if err != nil {
return nil, HTTPErrorWithInternal(ErrorInvalidRepository, err)
}
}

s := solver.NewWithConfig(
distribution.ModulePlatformID(),
distribution.Releasever(),
distroArch.Name(),
distribution.Name())
pkgs, err := s.SearchMetadata(repos, bp.GetPackagesEx(false))
if err != nil {
return nil, HTTPErrorWithInternal(ErrorFailedToDepsolve, err)
}

if err := solver.CleanCache(); err != nil {
// log and ignore
log.Printf("Error during rpm repo cache cleanup: %s", err.Error())
}

return pkgs.ToPackageInfos(), nil
}
Loading
Loading