diff --git a/internal/cloudapi/v2/handler.go b/internal/cloudapi/v2/handler.go index 0d65e08df..a9ea10303 100644 --- a/internal/cloudapi/v2/handler.go +++ b/internal/cloudapi/v2/handler.go @@ -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), + }) +} diff --git a/internal/cloudapi/v2/search.go b/internal/cloudapi/v2/search.go new file mode 100644 index 000000000..9d84b1c94 --- /dev/null +++ b/internal/cloudapi/v2/search.go @@ -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 +}