debian-forge-composer/cmd/osbuild-worker/jobimpl-depsolve.go
Sanne Raymaekers 22a0452ea9 osbuild-worker: handle error wrapping from dnfjson package
osbuild/images#751 wrapped the errors in the images/dnfjson package to
provide more details, the depsolve job should take this into account to
map the dnfjson error to the correct worker client error.

This caused user input errors errors to be misclassified as internal
errors, triggering depsolve job failure alerts.
2024-09-02 14:39:03 +02:00

159 lines
5 KiB
Go

package main
import (
"errors"
"fmt"
"net/url"
"strings"
"github.com/sirupsen/logrus"
"github.com/osbuild/images/pkg/dnfjson"
"github.com/osbuild/images/pkg/rpmmd"
"github.com/osbuild/osbuild-composer/internal/worker"
"github.com/osbuild/osbuild-composer/internal/worker/clienterrors"
)
// Used by both depsolve and osbuild jobs
type RepositoryMTLSConfig struct {
BaseURL *url.URL
CA string
MTLSClientKey string
MTLSClientCert string
Proxy *url.URL
}
func (rmc *RepositoryMTLSConfig) CompareBaseURL(baseURLStr string) (bool, error) {
baseURL, err := url.Parse(baseURLStr)
if err != nil {
return false, err
}
if baseURL.Scheme != rmc.BaseURL.Scheme {
return false, nil
}
if baseURL.Host != rmc.BaseURL.Host {
return false, nil
}
if !strings.HasPrefix(baseURL.Path, rmc.BaseURL.Path) {
return false, nil
}
return true, nil
}
type DepsolveJobImpl struct {
Solver *dnfjson.BaseSolver
RepositoryMTLSConfig *RepositoryMTLSConfig
}
// depsolve each package set in the pacakgeSets map. The repositories defined
// in repos are used for all package sets, whereas the repositories in
// packageSetsRepos are only used for the package set with the same name
// (matching map keys).
func (impl *DepsolveJobImpl) depsolve(packageSets map[string][]rpmmd.PackageSet, modulePlatformID, arch, releasever string) (map[string][]rpmmd.PackageSpec, map[string][]rpmmd.RepoConfig, error) {
solver := impl.Solver.NewWithConfig(modulePlatformID, releasever, arch, "")
if impl.RepositoryMTLSConfig != nil && impl.RepositoryMTLSConfig.Proxy != nil {
err := solver.SetProxy(impl.RepositoryMTLSConfig.Proxy.String())
if err != nil {
return nil, nil, err
}
}
depsolvedSets := make(map[string][]rpmmd.PackageSpec)
repoConfigs := make(map[string][]rpmmd.RepoConfig)
for name, pkgSet := range packageSets {
res, repos, err := solver.Depsolve(pkgSet)
if err != nil {
return nil, nil, err
}
depsolvedSets[name] = res
repoConfigs[name] = repos
}
return depsolvedSets, repoConfigs, nil
}
func workerClientErrorFrom(err error, logWithId *logrus.Entry) *clienterrors.Error {
if err == nil {
logWithId.Errorf("workerClientErrorFrom expected an error to be processed. Not nil")
}
var dnfjsonErr dnfjson.Error
if errors.As(err, &dnfjsonErr) {
// Error originates from dnf-json
reason := fmt.Sprintf("DNF error occurred: %s", dnfjsonErr.Kind)
details := dnfjsonErr.Reason
switch dnfjsonErr.Kind {
case "DepsolveError":
return clienterrors.New(clienterrors.ErrorDNFDepsolveError, reason, details)
case "MarkingErrors":
return clienterrors.New(clienterrors.ErrorDNFMarkingErrors, reason, details)
case "RepoError":
return clienterrors.New(clienterrors.ErrorDNFRepoError, reason, details)
default:
logWithId.Errorf("Unhandled dnf-json error in depsolve job: %v", err)
// This still has the kind/reason format but a kind that's returned
// by dnf-json and not explicitly handled here.
return clienterrors.New(clienterrors.ErrorDNFOtherError, reason, details)
}
} else {
reason := "rpmmd error in depsolve job"
details := fmt.Sprintf("%v", err)
// Error originates from internal/rpmmd, not from dnf-json
//
// XXX: it seems slightly dangerous to assume that any
// "error" we get there is coming from rpmmd, can we
// generate a more typed error from dnfjson here for
// rpmmd errors?
return clienterrors.New(clienterrors.ErrorRPMMDError, reason, details)
}
}
func (impl *DepsolveJobImpl) Run(job worker.Job) error {
logWithId := logrus.WithField("jobId", job.Id())
var args worker.DepsolveJob
err := job.Args(&args)
if err != nil {
return err
}
var result worker.DepsolveJobResult
if impl.RepositoryMTLSConfig != nil {
for pkgsetsi, pkgsets := range args.PackageSets {
for pkgseti, pkgset := range pkgsets {
for repoi, repo := range pkgset.Repositories {
for _, baseurlstr := range repo.BaseURLs {
match, err := impl.RepositoryMTLSConfig.CompareBaseURL(baseurlstr)
if err != nil {
result.JobError = clienterrors.New(clienterrors.ErrorInvalidRepositoryURL, "Repository URL is malformed", err.Error())
return err
}
if match {
args.PackageSets[pkgsetsi][pkgseti].Repositories[repoi].SSLCACert = impl.RepositoryMTLSConfig.CA
args.PackageSets[pkgsetsi][pkgseti].Repositories[repoi].SSLClientKey = impl.RepositoryMTLSConfig.MTLSClientKey
args.PackageSets[pkgsetsi][pkgseti].Repositories[repoi].SSLClientCert = impl.RepositoryMTLSConfig.MTLSClientCert
}
}
}
}
}
}
result.PackageSpecs, result.RepoConfigs, err = impl.depsolve(args.PackageSets, args.ModulePlatformID, args.Arch, args.Releasever)
if err != nil {
result.JobError = workerClientErrorFrom(err, logWithId)
}
if err := impl.Solver.CleanCache(); err != nil {
// log and ignore
logWithId.Errorf("Error during rpm repo cache cleanup: %s", err.Error())
}
err = job.Update(&result)
if err != nil {
return fmt.Errorf("Error reporting job result: %v", err)
}
return nil
}