From 2621d49606ec58f96f356af3adbe25706da1dffd Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Thu, 30 Jan 2025 16:54:12 +0100 Subject: [PATCH] dnfjson: support redirecting dnfjsons `stderr` This commit tweaks the dnfjson.Solver to also have a `Stderr` attribute. This is a minimal change to prevent `image-builder-cli` from showing: ```console image-builder build qcow2 --distro centos-9 --progress=verbose Manifest generation step Building manifest for qcow2-centos-9 No match for group package "iwl5150-firmware" No match for group package "iwl6050-firmware" No match for group package "firewalld" No match for group package "iwl7260-firmware" No match for group package "dracut-config-rescue" No match for group package "iwl105-firmware" No match for group package "iwl2000-firmware" No match for group package "iwl100-firmware" No match for group package "iwl6000g2a-firmware" No match for group package "iwl1000-firmware" No match for group package "iwl3160-firmware" No match for group package "iwl135-firmware" No match for group package "iwl2030-firmware" No match for group package "iwl5000-firmware" ... ``` which seems to be resulting from the "exclude" directive of the transaction. When qcow2 in centos-9 is build it contains the following: ```go ps := rpmmd.PackageSet{ Include: []string{ "@core", ... }, Exclude: []string{ ... "firewalld", "iwl7260-firmware", ... }, ``` and each package that is both in @core and in the exclude seems to trigger a message like the above, e.g.: ``` No match for group package "firewalld" ``` This is just confusing for our users so image-builder-cli will just discard this stderr output from dnfjson. I'm not sure if there is a better way to handle this though, ideas (very) welcome. --- pkg/dnfjson/dnfjson.go | 24 ++++++++++++++++++------ pkg/dnfjson/dnfjson_test.go | 6 +++++- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/pkg/dnfjson/dnfjson.go b/pkg/dnfjson/dnfjson.go index 5b1e8a6eff..c20f299275 100644 --- a/pkg/dnfjson/dnfjson.go +++ b/pkg/dnfjson/dnfjson.go @@ -18,6 +18,7 @@ import ( "crypto/sha256" "encoding/json" "fmt" + "io" "net/url" "os" "os/exec" @@ -154,6 +155,14 @@ type Solver struct { proxy string subscriptions *rhsm.Subscriptions + + // Stderr is the stderr output from dnfjson, if unset os.Stderr + // will be used. + // + // XXX: ideally this would not be public but just passed via + // NewSolver() but it already has 5 args so ideally we would + // add a SolverOptions struct here with "CacheDir" and "Stderr"? + Stderr io.Writer } // DepsolveResult contains the results of a depsolve operation. @@ -212,7 +221,7 @@ func (s *Solver) Depsolve(pkgSets []rpmmd.PackageSet, sbomType sbom.StandardType s.cache.locker.RLock() defer s.cache.locker.RUnlock() - output, err := run(s.dnfJsonCmd, req) + output, err := run(s.dnfJsonCmd, req, s.Stderr) if err != nil { return nil, fmt.Errorf("running osbuild-depsolve-dnf failed:\n%w", err) } @@ -266,7 +275,7 @@ func (s *Solver) FetchMetadata(repos []rpmmd.RepoConfig) (rpmmd.PackageList, err return pkgs, nil } - result, err := run(s.dnfJsonCmd, req) + result, err := run(s.dnfJsonCmd, req, s.Stderr) if err != nil { return nil, err } @@ -312,7 +321,7 @@ func (s *Solver) SearchMetadata(repos []rpmmd.RepoConfig, packages []string) (rp return pkgs, nil } - result, err := run(s.dnfJsonCmd, req) + result, err := run(s.dnfJsonCmd, req, s.Stderr) if err != nil { return nil, err } @@ -835,7 +844,7 @@ func ParseError(data []byte) Error { return e } -func run(dnfJsonCmd []string, req *Request) ([]byte, error) { +func run(dnfJsonCmd []string, req *Request, stderr io.Writer) ([]byte, error) { if len(dnfJsonCmd) == 0 { dnfJsonCmd = []string{findDepsolveDnf()} } @@ -853,7 +862,11 @@ func run(dnfJsonCmd []string, req *Request) ([]byte, error) { return nil, fmt.Errorf("creating stdin pipe for %s failed: %w", ex, err) } - cmd.Stderr = os.Stderr + if stderr != nil { + cmd.Stderr = stderr + } else { + cmd.Stderr = os.Stderr + } stdout := new(bytes.Buffer) cmd.Stdout = stdout @@ -873,6 +886,5 @@ func run(dnfJsonCmd []string, req *Request) ([]byte, error) { if runError, ok := err.(*exec.ExitError); ok && runError.ExitCode() != 0 { return nil, parseError(output, req.Arguments.Repos) } - return output, nil } diff --git a/pkg/dnfjson/dnfjson_test.go b/pkg/dnfjson/dnfjson_test.go index a62da8194e..58b09d2ae2 100644 --- a/pkg/dnfjson/dnfjson_test.go +++ b/pkg/dnfjson/dnfjson_test.go @@ -858,7 +858,7 @@ exit 1 err := os.WriteFile(fakeDnfJsonPath, []byte(fakeDnfJsonNoOutput), 0o755) assert.NoError(t, err) - _, err = run([]string{fakeDnfJsonPath}, &Request{}) + _, err = run([]string{fakeDnfJsonPath}, &Request{}, nil) assert.EqualError(t, err, `DNF error occurred: InternalError: dnf-json output was empty`) } @@ -867,16 +867,20 @@ func TestSolverRunWithSolverNoError(t *testing.T) { fakeSolver := `#!/bin/sh -e cat - > "$0".stdin echo '{"solver": "zypper"}' +>&2 echo "output-on-stderr" ` fakeSolverPath := filepath.Join(tmpdir, "fake-solver") err := os.WriteFile(fakeSolverPath, []byte(fakeSolver), 0755) //nolint:gosec assert.NoError(t, err) + var capturedStderr bytes.Buffer solver := NewSolver("platform:f38", "38", "x86_64", "fedora-38", "/tmp/cache") + solver.Stderr = &capturedStderr solver.dnfJsonCmd = []string{fakeSolverPath} res, err := solver.Depsolve(nil, sbom.StandardTypeNone) assert.NoError(t, err) assert.NotNil(t, res) + assert.Equal(t, "output-on-stderr\n", capturedStderr.String()) // prerequisite check, i.e. ensure our fake was called in the right way stdin, err := os.ReadFile(fakeSolverPath + ".stdin")