Skip to content

Commit

Permalink
cloudapi: Implement the package search
Browse files Browse the repository at this point in the history
This takes the same input as depsolving (a blueprint and an optional
list of repositories), searches the package metadata and returns the
info about the matching packages in a structure that is similar to that
returned by the weldrapi projects API.

If the distro and/or arch are set in the blueprint they override the
host's distro and arch.

Resolves: RHEL-60136
  • Loading branch information
bcl committed Jan 11, 2025
1 parent d10ea2d commit ea59f30
Show file tree
Hide file tree
Showing 4 changed files with 262 additions and 0 deletions.
75 changes: 75 additions & 0 deletions internal/cloudapi/v2/depsolve.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,78 @@ func (request *DepsolveRequest) DepsolveBlueprint(df *distrofactory.Factory, rr

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
}
64 changes: 64 additions & 0 deletions internal/cloudapi/v2/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -1397,3 +1397,67 @@ func packageSpecToPackageMetadata(pkgspecs []rpmmd.PackageSpec) []PackageMetadat
}
return packages
}

// PostPackageSearch searches for one or more packages and returns their details
// as a list of PackageDetails
func (h *apiHandlers) PostPackageSearch(ctx echo.Context) error {
var request PackageSearchRequest
err := ctx.Bind(&request)
if err != nil {
return err
}

pkgs, err := request.PackageSearch(h.server.distros, h.server.repos, h.server.solver)
if err != nil {
return err
}

return ctx.JSON(http.StatusOK,
PackageSearchResponse{
Packages: packageInfoToPackageInfo(pkgs),
})
}

// packageInfoToPackageInfo converts []rpmmd.PackageInfo to []PackageInfo
func packageInfoToPackageInfo(pkgs []rpmmd.PackageInfo) []PackageInfo {
info := []PackageInfo{}
for _, p := range pkgs {
i := PackageInfo{
Name: p.Name,
Summary: p.Summary,
}
if len(p.Description) > 0 {
i.Description = common.ToPtr(p.Description)
}
if len(p.Homepage) > 0 {
i.Homepage = common.ToPtr(p.Homepage)
}
var builds []PackageBuild
if len(p.Builds) > 0 {
for _, b := range p.Builds {
build := PackageBuild{
Arch: b.Arch,
Release: b.Release,
Source: PackageSource{
License: b.Source.License,
Version: b.Source.Version,
},
}
if b.Epoch > 0 {
build.Epoch = common.ToPtr(strconv.FormatUint(uint64(b.Epoch), 10))
}
if len(b.BuildTime) > 0 {
build.BuildTime = common.ToPtr(b.BuildTime)
}

builds = append(builds, build)
}

i.Builds = &builds
}

info = append(info, i)
}

return info
}
56 changes: 56 additions & 0 deletions internal/cloudapi/v2/v2_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -405,3 +405,59 @@ func TestPackageSpecToPackageMetadata(t *testing.T) {
assert.Equal(tc.pkgs, packageSpecToPackageMetadata(tc.specs), "mismatch in test case %d", idx)
}
}

func TestPackageInfoToPackageInfo(t *testing.T) {
assert := assert.New(t)
type testCase struct {
pkgs []rpmmd.PackageInfo
info []PackageInfo
}
testCases := []testCase{
{
pkgs: []rpmmd.PackageInfo{
{
Name: "vim-enhanced",
Summary: "A version of the VIM editor which includes recent enhancements",
Description: "VIM (VIsual editor iMproved) is an updated and improved ...",
Homepage: "http://www.vim.org/",
Builds: []rpmmd.PackageBuild{
{
Arch: "x86_64",
BuildTime: "2024-09-06T16:14:20",
Epoch: 2,
Release: "1.fc40",
Source: rpmmd.PackageSource{
Version: "9.1.719",
License: "Vim AND LGPL-2.1-or-later AND ...",
},
},
},
},
},
info: []PackageInfo{
{
Name: "vim-enhanced",
Summary: "A version of the VIM editor which includes recent enhancements",
Description: common.ToPtr("VIM (VIsual editor iMproved) is an updated and improved ..."),
Homepage: common.ToPtr("http://www.vim.org/"),
Builds: &[]PackageBuild{
{
Arch: "x86_64",
BuildTime: common.ToPtr("2024-09-06T16:14:20"),
Epoch: common.ToPtr("2"),
Release: "1.fc40",
Source: PackageSource{
Version: "9.1.719",
License: "Vim AND LGPL-2.1-or-later AND ...",
},
},
},
},
},
},
}

for idx, tc := range testCases {
assert.Equal(tc.info, packageInfoToPackageInfo(tc.pkgs), "mismatch in test case %d", idx)
}
}
67 changes: 67 additions & 0 deletions internal/cloudapi/v2/v2_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1623,6 +1623,73 @@ func TestDepsolveBlueprint(t *testing.T) {
}`)
}

func TestPackagesSearch(t *testing.T) {
srv, _, _, cancel := newV2Server(t, t.TempDir(), []string{""}, false, false)
defer cancel()

test.TestRoute(t, srv.Handler("/api/image-builder-composer/v2"), false, "POST",
"/api/image-builder-composer/v2/packages/search", fmt.Sprintf(`
{"blueprint": {
"name": "searchtest1",
"version": "0.0.1",
"distro": "%s",
"architecture": "%s",
"packages": [
{ "name": "package1" }
]}
}`, test_distro.TestDistro1Name, test_distro.TestArchName),
http.StatusOK,
`{
"packages": [
{
"description": "pkg1 desc",
"homepage": "https://pkg1.example.com",
"name": "package1",
"summary": "pkg1 sum",
"builds": [
{
"arch": "x86_64",
"build_time": "2006-02-02T15:04:05",
"release": "1.fc30",
"source": {
"license": "MIT",
"version": "1.0"
}
},
{
"arch": "x86_64",
"build_time": "2006-02-03T15:04:05",
"release": "1.fc30",
"source": {
"license": "MIT",
"version": "1.1"
}
}]
}]
}`)
}

func TestPackagesSearchError(t *testing.T) {
srv, _, _, cancel := newV2Server(t, t.TempDir(), []string{""}, false, false)
defer cancel()

test.TestRoute(t, srv.Handler("/api/image-builder-composer/v2"), false, "POST",
"/api/image-builder-composer/v2/packages/search", fmt.Sprintf(`
{"blueprint": {
"name": "searchtest2",
"version": "0.0.1",
"distro": "%s",
"architecture": "%s",
"packages": [
{ "name": "nonexistingpkg" }
]}
}`, test_distro.TestDistro1Name, test_distro.TestArchName),
http.StatusOK,
`{
"packages": []
}`)
}

// TestMain builds the mock dnf json binary and cleans it up on exit
func TestMain(m *testing.M) {
setupDNFJSON()
Expand Down

0 comments on commit ea59f30

Please sign in to comment.