cloudapi: Hook up the /search/packages handler

This connects all the pieces needed to implement the search.

If you POST a request to /search/packages like this:

    {
      "packages": [
        "tmux"
      ],
      "distribution": "fedora-41",
      "architecture": "x86_64"
    }

It will return details about the tmux packages that looks like this:

{
  "packages": [
    {
      "arch": "x86_64",
      "buildtime": "2024-10-10T00:19:06Z",
      "description": "tmux is ...",
      "license": "ISC AND BSD-2-Clause AND BSD-3-Clause AND SSH-short AND LicenseRef-Fedora-Public-Domain",
      "name": "tmux",
      "release": "2.fc41",
      "summary": "A terminal multiplexer",
      "url": "https://tmux.github.io/",
      "version": "3.5a"
    }
  ]
}

Resolves: RHEL-60136
This commit is contained in:
Brian C. Lane 2025-01-24 14:18:03 -08:00 committed by Tomáš Hozza
parent 234e8a09eb
commit 532f1b0396
2 changed files with 146 additions and 0 deletions

View file

@ -9,6 +9,7 @@ import (
"sort"
"strconv"
"strings"
"time"
"github.com/google/uuid"
"github.com/labstack/echo/v4"
@ -1383,3 +1384,63 @@ func packageSpecToPackageMetadata(pkgspecs []rpmmd.PackageSpec) []PackageMetadat
}
return packages
}
// packageListToPackageDetails converts the rpmmd.PackageList to PackageDetails
// This is used to return detailed package information from the package search
func packageListToPackageDetails(packages rpmmd.PackageList) []PackageDetails {
details := make([]PackageDetails, 0)
for _, rpm := range packages {
d := PackageDetails{
Name: rpm.Name,
Version: rpm.Version,
Release: rpm.Release,
Arch: rpm.Arch,
}
// Set epoch if it is not 0
if rpm.Epoch > 0 {
d.Epoch = common.ToPtr(strconv.FormatUint(uint64(rpm.Epoch), 10))
}
// Set buildtime to a RFC3339 string
d.Buildtime = common.ToPtr(rpm.BuildTime.Format(time.RFC3339))
if len(rpm.Summary) > 0 {
d.Summary = common.ToPtr(rpm.Summary)
}
if len(rpm.Description) > 0 {
d.Description = common.ToPtr(rpm.Description)
}
if len(rpm.URL) > 0 {
d.Url = common.ToPtr(rpm.URL)
}
if len(rpm.License) > 0 {
d.License = common.ToPtr(rpm.License)
}
details = append(details, d)
}
return details
}
// PostSearchPackages searches for packages and returns detailed
// information about the matches.
func (h *apiHandlers) PostSearchPackages(ctx echo.Context) error {
var request SearchPackagesRequest
err := ctx.Bind(&request)
if err != nil {
return err
}
// Search for the listed packages
// Any errors returned are suitable as a response
packages, err := request.Search(h.server.distros, h.server.repos, h.server.workers)
if err != nil {
return err
}
return ctx.JSON(http.StatusOK,
SearchPackagesResponse{
Packages: packageListToPackageDetails(packages),
})
}

View file

@ -0,0 +1,85 @@
package v2
// SearchPackagesRequest 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/osbuild-composer/internal/worker"
)
func (request *SearchPackagesRequest) Search(df *distrofactory.Factory, rr *reporegistry.RepoRegistry, workers *worker.Server) (rpmmd.PackageList, error) {
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 search request to the worker
searchJobID, err := workers.EnqueueSearchPackages(&worker.SearchPackagesJob{
Packages: request.Packages,
Repositories: repos,
ModulePlatformID: distro.ModulePlatformID(),
Arch: distroArch.Name(),
Releasever: distro.Releasever(),
}, "")
if err != nil {
return nil, HTTPErrorWithInternal(ErrorEnqueueingJob, err)
}
// Limit how long a search can take
ctx, cancel := context.WithTimeout(context.Background(), time.Minute*depsolveTimeoutMin)
defer cancel()
// Wait until search job is finished, fails, or is canceled
var result worker.SearchPackagesJobResult
for {
time.Sleep(time.Millisecond * 50)
info, err := workers.SearchPackagesJobInfo(searchJobID, &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("Search job %q timed out", searchJobID))
default:
}
}
return result.Packages, nil
}