Skip to content

Commit d482796

Browse files
Merge pull request #5345 from pablintino/ocpbugs-62714
OCPBUGS-62714: Temporary policy.json for PIS rpm-ostree rebasing
2 parents 06e7b70 + b938d41 commit d482796

File tree

9 files changed

+701
-161
lines changed

9 files changed

+701
-161
lines changed

pkg/daemon/command_runner.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package daemon
2+
3+
import (
4+
"fmt"
5+
"os/exec"
6+
"strings"
7+
8+
"k8s.io/klog/v2"
9+
)
10+
11+
// CommandRunner abstracts command execution for testing and flexibility.
12+
type CommandRunner interface {
13+
// RunGetOut executes a command and returns its stdout output.
14+
// On error, stderr is included in the error message.
15+
RunGetOut(command string, args ...string) ([]byte, error)
16+
}
17+
18+
// CommandRunnerOS is the production implementation that executes real OS commands.
19+
type CommandRunnerOS struct{}
20+
21+
// RunGetOut executes a command, logs it, and returns stdout.
22+
// On error, stderr is included in the error message (truncated to 256 chars).
23+
func (r *CommandRunnerOS) RunGetOut(command string, args ...string) ([]byte, error) {
24+
klog.Infof("Running captured: %s %s", command, strings.Join(args, " "))
25+
cmd := exec.Command(command, args...)
26+
rawOut, err := cmd.Output()
27+
if err != nil {
28+
errtext := ""
29+
if e, ok := err.(*exec.ExitError); ok {
30+
// Trim to max of 256 characters
31+
errtext = fmt.Sprintf("\n%s", truncate(string(e.Stderr), 256))
32+
}
33+
return nil, fmt.Errorf("error running %s %s: %s%s", command, strings.Join(args, " "), err, errtext)
34+
}
35+
return rawOut, nil
36+
}
37+
38+
// TODO: Delete this function to always consume the interface instance
39+
// Tracking story: https://issues.redhat.com/browse/MCO-1924
40+
//
41+
// Conserved the old signature to avoid a big footprint bugfix with this change
42+
func runGetOut(command string, args ...string) ([]byte, error) {
43+
return (&CommandRunnerOS{}).RunGetOut(command, args...)
44+
}
45+
46+
// MockCommandRunner is a test implementation that returns pre-configured outputs.
47+
type MockCommandRunner struct {
48+
outputs map[string][]byte
49+
errors map[string]error
50+
}
51+
52+
// RunGetOut returns pre-configured output or error based on the command string.
53+
// It matches commands using "command arg1 arg2..." as the key.
54+
// Returns an error if no matching output or error is found.
55+
func (m *MockCommandRunner) RunGetOut(command string, args ...string) ([]byte, error) {
56+
key := command + " " + strings.Join(args, " ")
57+
if out, ok := m.outputs[key]; ok {
58+
return out, nil
59+
}
60+
if err, ok := m.errors[key]; ok {
61+
return nil, err
62+
}
63+
return nil, fmt.Errorf("no output for command %s found", command)
64+
}

pkg/daemon/command_runner_test.go

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package daemon
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
)
8+
9+
func TestCommandRunnerOS_RunGetOut(t *testing.T) {
10+
runner := &CommandRunnerOS{}
11+
12+
// Test successful command with no output
13+
o, err := runner.RunGetOut("true")
14+
assert.Nil(t, err)
15+
assert.Equal(t, len(o), 0)
16+
17+
// Test failed command
18+
o, err = runner.RunGetOut("false")
19+
assert.NotNil(t, err)
20+
21+
// Test successful command with output
22+
o, err = runner.RunGetOut("echo", "hello")
23+
assert.Nil(t, err)
24+
assert.Equal(t, string(o), "hello\n")
25+
26+
// Test command that outputs to stderr and exits with error
27+
// base64 encode "oops" so we can't match on the command arguments
28+
o, err = runner.RunGetOut("/bin/sh", "-c", "echo hello; echo b29vcwo== | base64 -d 1>&2; exit 1")
29+
assert.Error(t, err)
30+
errtext := err.Error()
31+
assert.Contains(t, errtext, "exit status 1\nooos\n")
32+
33+
// Test command that doesn't exist
34+
o, err = runner.RunGetOut("/usr/bin/test-failure-to-exec-this-should-not-exist", "arg")
35+
assert.Error(t, err)
36+
}
37+
38+
func TestMockCommandRunner_RunGetOut(t *testing.T) {
39+
// Test mock with pre-configured output
40+
mock := &MockCommandRunner{
41+
outputs: map[string][]byte{
42+
"echo hello": []byte("hello\n"),
43+
"rpm-ostree status": []byte("State: idle\n"),
44+
},
45+
errors: map[string]error{},
46+
}
47+
48+
o, err := mock.RunGetOut("echo", "hello")
49+
assert.Nil(t, err)
50+
assert.Equal(t, []byte("hello\n"), o)
51+
52+
o, err = mock.RunGetOut("rpm-ostree", "status")
53+
assert.Nil(t, err)
54+
assert.Equal(t, []byte("State: idle\n"), o)
55+
56+
// Test mock with pre-configured error
57+
mock = &MockCommandRunner{
58+
outputs: map[string][]byte{},
59+
errors: map[string]error{
60+
"cmd --fail": assert.AnError,
61+
},
62+
}
63+
64+
o, err = mock.RunGetOut("cmd", "--fail")
65+
assert.Error(t, err)
66+
assert.Equal(t, assert.AnError, err)
67+
68+
// Test mock with no matching command (should return error)
69+
mock = &MockCommandRunner{
70+
outputs: map[string][]byte{},
71+
errors: map[string]error{},
72+
}
73+
74+
o, err = mock.RunGetOut("unknown", "command")
75+
assert.Error(t, err)
76+
assert.Contains(t, err.Error(), "no output for command")
77+
}
78+
79+
func TestMockCommandRunner_Priority(t *testing.T) {
80+
// Test that outputs take priority over errors
81+
mock := &MockCommandRunner{
82+
outputs: map[string][]byte{
83+
"test cmd": []byte("output\n"),
84+
},
85+
errors: map[string]error{
86+
"test cmd": assert.AnError,
87+
},
88+
}
89+
90+
o, err := mock.RunGetOut("test", "cmd")
91+
assert.Nil(t, err)
92+
assert.Equal(t, []byte("output\n"), o)
93+
}

pkg/daemon/daemon.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,12 @@ type Daemon struct {
165165

166166
// Ensures that only a single syncOSImagePullSecrets call can run at a time.
167167
osImageMux *sync.Mutex
168+
169+
// Abstraction for running commands against the OS
170+
cmdRunner CommandRunner
171+
172+
// Bare minimal podman client
173+
podmanInterface PodmanInterface
168174
}
169175

170176
// CoreOSDaemon protects the methods that should only be called on CoreOS variants
@@ -307,10 +313,11 @@ func New(
307313
}
308314

309315
var nodeUpdaterClient *RpmOstreeClient
310-
316+
cmdRunner := &CommandRunnerOS{}
317+
podmanInterface := NewPodmanExec(cmdRunner)
311318
// Only pull the osImageURL from OSTree when we are on RHCOS or FCOS
312319
if hostos.IsCoreOSVariant() {
313-
nodeUpdaterClientVal := NewNodeUpdaterClient()
320+
nodeUpdaterClientVal := NewNodeUpdaterClient(cmdRunner, podmanInterface)
314321
nodeUpdaterClient = &nodeUpdaterClientVal
315322
err := nodeUpdaterClient.Initialize()
316323
if err != nil {
@@ -348,6 +355,8 @@ func New(
348355
configDriftMonitor: NewConfigDriftMonitor(),
349356
osImageMux: &sync.Mutex{},
350357
irreconcilableReporter: NewNoOpIrreconcilableReporterImpl(),
358+
cmdRunner: cmdRunner,
359+
podmanInterface: podmanInterface,
351360
}, nil
352361
}
353362

pkg/daemon/image_manager_helper.go

Lines changed: 0 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,8 @@ import (
55
"errors"
66
"fmt"
77
"os"
8-
"os/exec"
98
"path/filepath"
10-
"strings"
11-
"time"
129

13-
"github.com/opencontainers/go-digest"
14-
pivotutils "github.com/openshift/machine-config-operator/pkg/daemon/pivot/utils"
1510
"k8s.io/apimachinery/pkg/util/sets"
1611
"k8s.io/klog/v2"
1712
)
@@ -33,76 +28,13 @@ const (
3328

3429
type imageSystem string
3530

36-
// imageInspection is a public implementation of
37-
// https://github.com/containers/skopeo/blob/82186b916faa9c8c70cfa922229bafe5ae024dec/cmd/skopeo/inspect.go#L20-L31
38-
type imageInspection struct {
39-
Name string `json:",omitempty"`
40-
Tag string `json:",omitempty"`
41-
Digest digest.Digest
42-
RepoDigests []string
43-
Created *time.Time
44-
DockerVersion string
45-
Labels map[string]string
46-
Architecture string
47-
Os string
48-
Layers []string
49-
}
50-
5131
// BootedImageInfo stores MCO interested bootec image info
5232
type BootedImageInfo struct {
5333
OSImageURL string
5434
ImageVersion string
5535
BaseChecksum string
5636
}
5737

58-
func podmanInspect(imgURL string) (imgdata *imageInspection, err error) {
59-
// Pull the container image if not already available
60-
var authArgs []string
61-
if _, err := os.Stat(kubeletAuthFile); err == nil {
62-
authArgs = append(authArgs, "--authfile", kubeletAuthFile)
63-
}
64-
args := []string{"pull", "-q"}
65-
args = append(args, authArgs...)
66-
args = append(args, imgURL)
67-
_, err = pivotutils.RunExt(numRetriesNetCommands, "podman", args...)
68-
if err != nil {
69-
return
70-
}
71-
72-
inspectArgs := []string{"inspect", "--type=image"}
73-
inspectArgs = append(inspectArgs, fmt.Sprintf("%s", imgURL))
74-
var output []byte
75-
output, err = runGetOut("podman", inspectArgs...)
76-
if err != nil {
77-
return
78-
}
79-
var imagedataArray []imageInspection
80-
err = json.Unmarshal(output, &imagedataArray)
81-
if err != nil {
82-
err = fmt.Errorf("unmarshaling podman inspect: %w", err)
83-
return
84-
}
85-
imgdata = &imagedataArray[0]
86-
return
87-
88-
}
89-
90-
// runGetOut executes a command, logging it, and return the stdout output.
91-
func runGetOut(command string, args ...string) ([]byte, error) {
92-
klog.Infof("Running captured: %s %s", command, strings.Join(args, " "))
93-
cmd := exec.Command(command, args...)
94-
rawOut, err := cmd.Output()
95-
if err != nil {
96-
errtext := ""
97-
if e, ok := err.(*exec.ExitError); ok {
98-
// Trim to max of 256 characters
99-
errtext = fmt.Sprintf("\n%s", truncate(string(e.Stderr), 256))
100-
}
101-
return nil, fmt.Errorf("error running %s %s: %s%s", command, strings.Join(args, " "), err, errtext)
102-
}
103-
return rawOut, nil
104-
}
105-
10638
// truncate a string using runes/codepoints as limits.
10739
// This specifically will avoid breaking a UTF-8 value.
10840
func truncate(input string, limit int) string {

pkg/daemon/podman.go

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package daemon
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
)
7+
8+
// PodmanStorageConfig contains storage configuration from Podman.
9+
type PodmanStorageConfig struct {
10+
GraphDriverName string `json:"graphDriverName"`
11+
GraphRoot string `json:"graphRoot"`
12+
}
13+
14+
// PodmanInfo contains system information from Podman.
15+
type PodmanInfo struct {
16+
Store PodmanStorageConfig `json:"store"`
17+
}
18+
19+
// PodmanImageInfo contains image metadata from Podman.
20+
type PodmanImageInfo struct {
21+
ID string `json:"Id"`
22+
Digest string `json:"Digest"`
23+
RepoDigests []string `json:"RepoDigests"`
24+
RepoDigest string `json:"-"` // Filled with matching digest from RepoDigests
25+
}
26+
27+
// PodmanInterface abstracts podman operations for testing and flexibility.
28+
type PodmanInterface interface {
29+
// GetPodmanImageInfoByReference retrieves image info for the given reference.
30+
// Returns nil if no image is found.
31+
GetPodmanImageInfoByReference(reference string) (*PodmanImageInfo, error)
32+
// GetPodmanInfo retrieves Podman system information.
33+
GetPodmanInfo() (*PodmanInfo, error)
34+
}
35+
36+
// PodmanExecInterface is the production implementation that executes real podman commands.
37+
type PodmanExecInterface struct {
38+
cmdRunner CommandRunner
39+
}
40+
41+
// NewPodmanExec creates a new PodmanExecInterface with the given command runner.
42+
func NewPodmanExec(commandRunner CommandRunner) *PodmanExecInterface {
43+
return &PodmanExecInterface{cmdRunner: commandRunner}
44+
}
45+
46+
// GetPodmanImageInfoByReference retrieves image info for the given reference.
47+
// It executes 'podman images --format=json --filter reference=<reference>'.
48+
// Returns nil if no image is found.
49+
// The RepoDigest field is populated if the reference matches one of the RepoDigests.
50+
func (p *PodmanExecInterface) GetPodmanImageInfoByReference(reference string) (*PodmanImageInfo, error) {
51+
output, err := p.cmdRunner.RunGetOut("podman", "images", "--format=json", "--filter", "reference="+reference)
52+
if err != nil {
53+
return nil, err
54+
}
55+
var podmanInfos []PodmanImageInfo
56+
if err := json.Unmarshal(output, &podmanInfos); err != nil {
57+
return nil, fmt.Errorf("failed to decode podman image ls output: %v", err)
58+
}
59+
if len(podmanInfos) == 0 {
60+
61+
return nil, nil
62+
}
63+
64+
info := &podmanInfos[0]
65+
// Fill the custom RepoDigest field with the digest that matches the
66+
// requested reference as it's convenient to be used by the caller
67+
for _, digest := range info.RepoDigests {
68+
if digest == reference {
69+
info.RepoDigest = digest
70+
break
71+
}
72+
}
73+
74+
return info, nil
75+
}
76+
77+
// GetPodmanInfo retrieves Podman system information.
78+
// It executes 'podman system info --format=json'.
79+
func (p *PodmanExecInterface) GetPodmanInfo() (*PodmanInfo, error) {
80+
output, err := p.cmdRunner.RunGetOut("podman", "system", "info", "--format=json")
81+
if err != nil {
82+
return nil, err
83+
}
84+
var podmanInfo PodmanInfo
85+
if err := json.Unmarshal(output, &podmanInfo); err != nil {
86+
return nil, fmt.Errorf("failed to decode podman system info output: %v", err)
87+
}
88+
return &podmanInfo, nil
89+
}

0 commit comments

Comments
 (0)