Repository: openshift/kube-compare
Pull request #172: CNF-14495: Add ability to retrieve references directly from container
Author: login: adhil0
Labels: name: lgtm
description: Indicates that a PR is ready to be merged.
name: jira/valid-reference
description: Indicates that this PR references a valid Jira ticket of any type.
name: approved
description: Indicates a PR has been approved by an approver from all required OWNERS files.
Created: 2025-02-12T01:18:14Z | Merged: 2025-03-12T17:41:00Z

In the future, it will be preferable for us to deliver references to users via a container. Give users the ability to refer to a
metadata.yaml in an image directly from the kube-compare command-line. 

Use `exec.Command()` to run podman and docker operations rather than official go bindings because quick experimentation showed that it would increase the binary size from ~90 MB to ~120 MB. 

Still need to address:

- [x] podman/docker login to registry; Don't need to login, just need to make sure error messages are descriptive
- [x] cleaning up files created in tmp directory
- [x] add more tests


Diff:
diff --git a/README.md b/README.md
index d9bb66eb..d47a6f0f 100644
--- a/README.md
+++ b/README.md
@@ -56,6 +56,12 @@ Run a known valid reference configuration with an `oc must-gather` output:
 kubectl cluster-compare -r ./reference/metadata.yaml -f "must-gather*/*/cluster-scoped-resources","must-gather*/*/namespaces" -R
 ```
 
+Extract a reference configuration from a container image and compare with a local set of CRs:
+
+```bash
+kubectl cluster-compare -r container://<IMAGE>:<TAG>:/home/ztp/reference/metadata.yaml -f ./crsdir -R
+```
+
 Run `kubectl cluster-compare --help` for a more extensive usage description.
 
 ## Output
diff --git a/addon-tools/helm-convert/convert/convert.go b/addon-tools/helm-convert/convert/convert.go
index 23167c4d..4d8369a1 100644
--- a/addon-tools/helm-convert/convert/convert.go
+++ b/addon-tools/helm-convert/convert/convert.go
@@ -62,7 +62,8 @@ func convertToHelm(o *Options) error {
 	var preValues map[string]any
 	crsWithDefaults := make(map[string]map[string]interface{})
 
-	cfs, err := compare.GetRefFS(o.refPath)
+	compareOptions := compare.Options{ReferenceConfig: o.refPath, TmpDir: ""}
+	cfs, err := compareOptions.GetRefFS()
 	if err != nil {
 		return fmt.Errorf("failed to get filesystem of cluster-compare reference %w", err)
 	}
diff --git a/pkg/compare/compare.go b/pkg/compare/compare.go
index 9b871fc5..2a9fc243 100644
--- a/pkg/compare/compare.go
+++ b/pkg/compare/compare.go
@@ -88,6 +88,9 @@ var (
 
 		# Run a known valid reference configuration with a must-gather output:
 		kubectl cluster-compare -r ./reference/metadata.yaml -f "must-gather*/*/cluster-scoped-resources","must-gather*/*/namespaces" -R
+
+		# Extract a reference configuration from a container image and compare with a local set of CRs:
+		kubectl cluster-compare -r container://<IMAGE>:<TAG>:/home/ztp/reference/metadata.yaml -f ./crsdir -R
 	`)
 )
 
@@ -114,7 +117,7 @@ var OutputFormats = []string{Json, Yaml, PatchYaml}
 
 type Options struct {
 	CRs                resource.FilenameOptions
-	referenceConfig    string
+	ReferenceConfig    string
 	diffConfigFileName string
 	diffAll            bool
 	verboseOutput      bool
@@ -138,6 +141,8 @@ type Options struct {
 	templatesToGenerateOverridesFor []string
 	overrideReason                  string
 
+	TmpDir string
+
 	diff *diff.DiffProgram
 	genericiooptions.IOStreams
 }
@@ -158,6 +163,17 @@ func NewCmd(f kcmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Comma
 		Long:                  compareLong,
 		Example:               example,
 		Run: func(cmd *cobra.Command, args []string) {
+
+			// FIXME: Handle creation of temporary directory more gracefully. Right now,
+			// kcmdutil.CheckDiffErr calls os.exit(), which does not run defer statements.
+			// Maybe we can create an error handler of some kind, and run os.exit() in a PostRun() block.
+			tmpDir, err := os.MkdirTemp("", "kube-compare")
+			if err != nil {
+				klog.Warningf("temporary directory could not be created %s", err)
+			} else {
+				options.TmpDir = tmpDir
+				defer os.RemoveAll(options.TmpDir)
+			}
 			kcmdutil.CheckDiffErr(options.Complete(f, cmd, args))
 			// `kubectl cluster-compare` propagates the error code from
 			// `kubectl diff` that propagates the error code from
@@ -167,6 +183,9 @@ func NewCmd(f kcmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Comma
 			// were found. We also don't want kubectl to
 			// return 1 if there was a problem.
 			if err := options.Run(); err != nil {
+				// FIXME: Handle clean up of temporary directory more gracefully.
+				// See above FIXME for details
+				os.RemoveAll(options.TmpDir)
 				if exitErr := diffError(err); exitErr != nil {
 					kcmdutil.CheckErr(kcmdutil.ErrExit)
 				}
@@ -187,7 +206,7 @@ func NewCmd(f kcmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Comma
 			" but more memory, I/O and CPU over that shorter period of time.")
 	kcmdutil.AddFilenameOptionFlags(cmd, &options.CRs, "contains the configuration to diff")
 	cmd.Flags().StringVarP(&options.diffConfigFileName, "diff-config", "c", "", "Path to the user config file")
-	cmd.Flags().StringVarP(&options.referenceConfig, "reference", "r", "", "Path to reference config file.")
+	cmd.Flags().StringVarP(&options.ReferenceConfig, "reference", "r", "", "Path to reference config file.")
 	cmd.Flags().BoolVar(&options.ShowManagedFields, "show-managed-fields", options.ShowManagedFields, "If true, include managed fields in the diff.")
 	cmd.Flags().BoolVarP(&options.diffAll, "all-resources", "A", options.diffAll,
 		"If present, In live mode will try to match all resources that are from the types mentioned in the reference. "+
@@ -235,13 +254,27 @@ func diffError(err error) exec.ExitError {
 	return nil
 }
 
-func GetRefFS(refConfig string) (fs.FS, error) {
-	referenceDir := filepath.Dir(refConfig)
-	if isURL(refConfig) {
+func (o *Options) GetRefFS() (fs.FS, error) {
+	referenceDir := filepath.Dir(o.ReferenceConfig)
+	if isURL(o.ReferenceConfig) {
 		// filepath.Dir removes one / from http://
 		referenceDir = strings.Replace(referenceDir, "/", "//", 1)
 		return HTTPFS{baseURL: referenceDir, httpGet: httpgetImpl}, nil
 	}
+	if isContainer(o.ReferenceConfig) {
+		// filepath.Dir removes one / from container://
+		referenceDir = strings.Replace(referenceDir, "/", "//", 1)
+		if o.TmpDir != "" {
+			if info, err := os.Stat(o.TmpDir); err == nil && info.IsDir() { // Does directory exist?
+				containerPath, err := getReferencesFromContainer(referenceDir, o.TmpDir)
+				if err != nil {
+					return nil, err
+				}
+				return os.DirFS(containerPath), nil
+			}
+		}
+		return nil, fmt.Errorf("temporary directory could not be accessed, see logs for details")
+	}
 	rootPath, err := filepath.Abs(referenceDir)
 	if err != nil {
 		return nil, fmt.Errorf("failed to get absolute path: %w", err)
@@ -262,19 +295,19 @@ func (o *Options) Complete(f kcmdutil.Factory, cmd *cobra.Command, args []string
 		}
 	}
 
-	if o.referenceConfig == "" {
+	if o.ReferenceConfig == "" {
 		return kcmdutil.UsageErrorf(cmd, noRefFileWasPassed)
 	}
-	if _, err := os.Stat(o.referenceConfig); os.IsNotExist(err) && !isURL(o.referenceConfig) {
+	if _, err := os.Stat(o.ReferenceConfig); os.IsNotExist(err) && !isURL(o.ReferenceConfig) && !isContainer(o.ReferenceConfig) {
 		return errors.New(refFileNotExistsError)
 	}
 
-	cfs, err := GetRefFS(o.referenceConfig)
+	cfs, err := o.GetRefFS()
 	if err != nil {
 		return err
 	}
 
-	referenceFileName := filepath.Base(o.referenceConfig)
+	referenceFileName := filepath.Base(o.ReferenceConfig)
 	o.ref, err = GetReference(cfs, referenceFileName)
 	if err != nil {
 		return err
diff --git a/pkg/compare/container.go b/pkg/compare/container.go
new file mode 100644
index 00000000..77271c14
--- /dev/null
+++ b/pkg/compare/container.go
@@ -0,0 +1,146 @@
+package compare
+
+import (
+	"fmt"
+	"os/exec"
+	"path/filepath"
+	"strings"
+
+	"k8s.io/klog/v2"
+)
+
+type engine struct {
+	name         string
+	requiresSudo bool
+	containerID  string
+	tempDir      string
+}
+
+// isContainer reports whether the given path is a reference to a file in a container by verifying if it starts with "container://".
+func isContainer(path string) bool {
+	return strings.HasPrefix(path, "container://")
+}
+
+type parsedPath struct {
+	image string
+	path  string
+}
+
+// parsePath returns the image and referencePath (path to the directory for metadata.yaml), given a path
+// of the form container://<IMAGE>:<TAG>:/path_to_metadata.yaml
+func parsePath(path string) (parsedPath, error) {
+	path = strings.TrimPrefix(path, "container://")
+
+	// Split on ':', removing empty strings from slice. Removes errant colons from string,
+	// so container://<IMAGE>:::<TAG>::::::/path/to/metadata.yaml will still work, but
+	// paths with leading and trailing colons won't.
+	f := func(c rune) bool {
+		return c == ':'
+	}
+	sections := strings.FieldsFunc(path, f)
+
+	if len(sections) == 3 {
+		image := sections[0] + ":" + sections[1]
+		referencePath := sections[2]
+		return parsedPath{image: image, path: referencePath}, nil
+	}
+	return parsedPath{image: "", path: ""}, fmt.Errorf("incorrect path passed to -r, it must follow this format: container://<IMAGE>:<TAG>:/path/to/metadata.yaml")
+}
+
+// Use var's so that we can mock functions in tests.
+var execCommand = exec.Command
+var lookPath = exec.LookPath
+
+// runEngineCommand runs a podman/docker command with sudo if necessary.
+// Returns the stdout (out) and stderr (err) of the command.
+func (engine *engine) runEngineCommand(args ...string) ([]byte, error) {
+	var out []byte
+	var err error
+	if engine.requiresSudo {
+		args = append([]string{engine.name}, args...) // Prepend engine name to args
+		out, err = execCommand("sudo", args...).CombinedOutput()
+	} else {
+		out, err = execCommand(engine.name, args...).CombinedOutput()
+	}
+
+	return out, err //nolint:wrapcheck  // We want to return unaltered errors, this is a wrapper around exec.Command()
+}
+
+// newEngine checks if Podman or Docker are in the system's PATH, and returns an engine with a name and a boolean
+// that indicates if sudo is needed for future commands. Prefers Podman. Returns an error if neither engine is found.
+var newEngine = func() (*engine, error) {
+	if _, err := lookPath("podman"); err == nil {
+		return &engine{name: "podman", requiresSudo: false}, nil
+	}
+
+	if _, err := lookPath("docker"); err == nil {
+		_, err = execCommand("docker", "images").Output() // If this errors out, we need to use sudo, return true.
+		return &engine{name: "docker", requiresSudo: err != nil}, nil
+	}
+	return &engine{name: "", requiresSudo: false}, fmt.Errorf("you do not have Podman or Docker on your PATH")
+}
+
+// pullContainer pulls an image, runs it using the provided engine, and stores the corresponding containerID in the engine struct
+func (engine *engine) pullAndRunContainer(image string) error {
+	// run because copy requires a running or stopped container
+	// -d to output container ID
+	out, err := engine.runEngineCommand("run", "-d", image)
+	if err != nil {
+		return fmt.Errorf("could not pull/run container: %s", out)
+	}
+	engine.containerID = strings.TrimSpace(string(out)) // Convert bytes to string and trim new line
+	return nil
+}
+
+// extractReferences copies the directory in the container that contains the reference configs into a temporary directory,
+// and stores the path to the new directory in the engine struct.
+func (engine *engine) extractReferences(pathToMetadata, dname string) error {
+
+	out, err := engine.runEngineCommand("cp", engine.containerID+":"+pathToMetadata, dname)
+	if err != nil {
+		return fmt.Errorf("could not copy templates from container: %s", out)
+	}
+	engine.tempDir = filepath.Join(dname, filepath.Base(pathToMetadata))
+	return nil
+}
+
+// cleanup stops and removes the container used to extract the reference configs.
+func (engine *engine) cleanup() {
+	out, err := engine.runEngineCommand("stop", engine.containerID)
+	if err != nil {
+		// Print errors as warnings rather than returning, since stopping and removing the container is not vital.
+		klog.Warningf("Warning: Could not stop container: %s", out)
+	}
+	out, err = engine.runEngineCommand("rm", engine.containerID)
+	if err != nil {
+		klog.Warningf("Warning: Could not remove container: %s", out)
+	}
+}
+
+// getReferencesFromContainer uses a path to an image and a metadata.yaml within that image, and extracts the reference configs
+// to a local temporary directory. Returns the path to this directory.
+func getReferencesFromContainer(path, tempContainerRefDir string) (string, error) {
+	engine, err := newEngine()
+	if err != nil {
+		return "", err
+	}
+
+	parsedPath, err := parsePath(path)
+	if err != nil {
+		return "", err
+	}
+
+	err = engine.pullAndRunContainer(parsedPath.image)
+	if err != nil {
+		return "", err
+	}
+
+	defer engine.cleanup()
+
+	err = engine.extractReferences(parsedPath.path, tempContainerRefDir)
+	if err != nil {
+		return "", err
+	}
+
+	return engine.tempDir, nil
+}
diff --git a/pkg/compare/container_test.go b/pkg/compare/container_test.go
new file mode 100644
index 00000000..ef1ac3af
--- /dev/null
+++ b/pkg/compare/container_test.go
@@ -0,0 +1,352 @@
+package compare
+
+import (
+	"errors"
+	"fmt"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"strings"
+	"testing"
+)
+
+const podman = "podman"
+const docker = "docker"
+
+func TestIsContainer(t *testing.T) {
+	tests := []struct {
+		input    string
+		expected bool
+	}{
+		{"container://my-image:latest:/metadata.yaml", true},
+		{"file://local/path/to/file.yaml", false},
+		{"http://example.com/data.yaml", false},
+		{"randomstringwithoutprefix", false},
+		{"container:/only-one-slash:tag:/metadata.yaml", false},
+		{"container/with/no/colon/metadata.yaml", false},
+		{"container://name:tag", true},
+		{"container://name", true},
+		{"container://", true},
+		{"container", false},
+		{"", false},
+	}
+
+	for _, test := range tests {
+		result := isContainer(test.input)
+		if result != test.expected {
+			t.Errorf("For input '%s', expected %v, got %v", test.input, test.expected, result)
+		}
+	}
+}
+
+func TestParsePath(t *testing.T) {
+	tests := []struct {
+		input         string
+		expectedImage string
+		expectedPath  string
+		expectError   bool
+	}{
+		{"container://my-image:latest:/metadata.yaml", "my-image:latest", "/metadata.yaml", false},
+		{"container://repo/image:tag:/data.yaml", "repo/image:tag", "/data.yaml", false},
+		{"container://repo/image:::tag::::/data.yaml", "repo/image:tag", "/data.yaml", false},
+		{"container://wrongformat", "", "", true},
+		{":missingprefix:/file.yaml", "", "", true},
+		{"container://only:two", "", "", true},
+		{"container://", "", "", true},
+		{"container:/", "", "", true},
+		{"container:", "", "", true},
+		{"container", "", "", true},
+		{"", "", "", true},
+	}
+
+	for _, test := range tests {
+		parsedPath, err := parsePath(test.input)
+		if test.expectError {
+			if err == nil {
+				t.Errorf("Expected error for input '%s', but got none", test.input)
+			}
+		} else {
+			if err != nil {
+				t.Errorf("Unexpected error for input '%s': %v", test.input, err)
+			}
+			if parsedPath.image != test.expectedImage || parsedPath.path != test.expectedPath {
+				t.Errorf("For input '%s', expected ('%s', '%s'), got ('%s', '%s')", test.input, test.expectedImage, test.expectedPath, parsedPath.image, parsedPath.path)
+			}
+		}
+	}
+}
+
+// fakeExecCommand() and TestHelperProcess() are derived from https://npf.io/2015/06/testing-exec-command/,
+// The MIT License (MIT)
+
+// Copyright (c) 2014 Nate Finch
+
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+func fakeExecCommand(command string, args ...string) *exec.Cmd {
+	for _, arg := range args {
+		if strings.Contains(arg, "invalid") {
+			cmd := exec.Command("false")
+			return cmd
+		}
+	}
+	cs := []string{"-test.run=TestHelperProcess", "--", command}
+	cs = append(cs, args...)
+	cmd := exec.Command(os.Args[0], cs...) // nolint:gosec
+	cmd.Env = []string{"GO_WANT_HELPER_PROCESS=1"}
+	return cmd
+}
+
+const dockerRunResult = "fake output"
+
+func TestRunEngineCommand(t *testing.T) {
+	// Override execCommand with fakes
+	execCommand = fakeExecCommand
+	defer func() { execCommand = exec.Command }()
+	tests := []struct {
+		engine engine
+		args   []string
+	}{
+		{engine{podman, false, "container123", "/tmp/dir/"}, []string{"run", "hello-world"}},
+		{engine{docker, true, "container123", "/tmp/dir/"}, []string{"run", "hello-world"}},
+	}
+
+	for _, test := range tests {
+		out, err := test.engine.runEngineCommand(test.args...)
+
+		if err != nil {
+			t.Errorf("Expected nil error, got %#v", err)
+		}
+		if string(out) != dockerRunResult {
+			t.Errorf("Expected %q, got %q", dockerRunResult, out)
+		}
+	}
+}
+
+func TestNewEngine(t *testing.T) {
+	// Override execCommand and LookPath with fakes
+	execCommand = fakeExecCommand
+
+	// reset execCommand and lookPath after the test
+	defer func() {
+		execCommand = exec.Command
+		lookPath = exec.LookPath // lookPath is reassigned below
+	}()
+
+	tests := []struct {
+		available    string
+		expectedName string
+		expectedSudo bool
+		expectError  bool
+	}{
+		{podman, podman, false, false},       // Podman is available
+		{docker, docker, false, false},       // Docker available, no sudo needed
+		{"docker-sudo", docker, true, false}, // Docker requires sudo
+		{"both", podman, false, false},       // Both available, podman should be chosen
+		{"none", "", false, true},            // Neither Podman nor Docker is available
+	}
+
+	for _, test := range tests {
+		// Customize behavior based on test case
+		lookPath = func(cmd string) (string, error) {
+			if (test.available == podman || test.available == "both") && cmd == podman {
+				return "/usr/bin/podman", nil
+			}
+			if (test.available == docker || test.available == "docker-sudo" || test.available == "both") && cmd == docker {
+				return "/usr/bin/docker", nil
+			}
+			return "", errors.New("not found")
+		}
+
+		execCommand = func(command string, args ...string) *exec.Cmd {
+			cs := []string{"-test.run=TestHelperProcess", "--", command}
+			cs = append(cs, args...)
+			cmd := exec.Command(os.Args[0], cs...) // nolint:gosec
+			cmd.Env = []string{"GO_WANT_HELPER_PROCESS=1"}
+			if test.available == "docker-sudo" {
+				cmd.Env = append(cmd.Env, "MOCK_DOCKER_FAIL=1") // Simulate `docker images` failing
+			}
+			return cmd
+		}
+
+		eng, err := newEngine()
+
+		if test.expectError {
+			if err == nil {
+				t.Errorf("Expected error but got nil")
+			}
+		} else {
+			if err != nil {
+				t.Errorf("Unexpected error: %v", err)
+			}
+			if eng.name != test.expectedName {
+				t.Errorf("Expected engine name %q, got %q", test.expectedName, eng.name)
+			}
+			if eng.requiresSudo != test.expectedSudo {
+				t.Errorf("Expected requiresSudo %v, got %v", test.expectedSudo, eng.requiresSudo)
+			}
+		}
+	}
+}
+
+func TestHelperProcess(t *testing.T) {
+	if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" {
+		return
+	}
+
+	if os.Getenv("MOCK_DOCKER_FAIL") == "1" {
+		os.Exit(1)
+	}
+	fmt.Fprint(os.Stdout, dockerRunResult)
+	os.Exit(0)
+}
+
+func TestPullAndRunContainer(t *testing.T) {
+	// Override execCommand with a fake function
+	execCommand = fakeExecCommand
+	defer func() { execCommand = exec.Command }()
+
+	tests := []struct {
+		engine      engine
+		image       string
+		expectError bool
+	}{
+		{engine{podman, false, "", ""}, "hello-world", false},
+		{engine{docker, true, "", ""}, "hello-world", false},
+		{engine{docker, true, "", ""}, "invalid-image", true},
+	}
+
+	for _, test := range tests {
+		err := test.engine.pullAndRunContainer(test.image)
+		if test.expectError {
+			if err == nil {
+				t.Errorf("Expected error but got nil")
+			}
+		} else {
+			if err != nil {
+				t.Errorf("Expected nil error, got %#v", err)
+			}
+			if test.engine.containerID != dockerRunResult {
+				t.Errorf("Expected container ID %q, got %q", dockerRunResult, test.engine.containerID)
+			}
+		}
+	}
+}
+
+func TestExtractReferences(t *testing.T) {
+	// Override execCommand with a fake function
+	execCommand = fakeExecCommand
+	defer func() { execCommand = exec.Command }()
+
+	tests := []struct {
+		engine         engine
+		pathToMetadata string
+		dname          string
+		expectError    bool
+	}{
+		{engine{podman, false, "container123", ""}, "/etc/configs", "/tmp/extracted", false},
+		{engine{docker, true, "container456", ""}, "/var/lib/data", "/tmp/extracted", false},
+		{engine{docker, true, "container456", ""}, "/invalid_path", "/tmp/extracted", true},
+		{engine{podman, true, "container456", ""}, "/etc/configs", "/invalid/path", true},
+	}
+
+	for _, test := range tests {
+		err := test.engine.extractReferences(test.pathToMetadata, test.dname)
+		if test.expectError {
+			if err == nil {
+				t.Errorf("Expected error but got nil")
+			}
+		} else {
+			if err != nil {
+				t.Errorf("Expected nil error, got %#v", err)
+			}
+			expectedTempDir := filepath.Join(test.dname, filepath.Base(test.pathToMetadata))
+			if test.engine.tempDir != expectedTempDir {
+				t.Errorf("Expected tempDir %q, got %q", expectedTempDir, test.engine.tempDir)
+			}
+		}
+	}
+}
+
+func TestCleanup(t *testing.T) {
+	// Override execCommand with a fake function
+	execCommand = fakeExecCommand
+	defer func() { execCommand = exec.Command }()
+
+	tests := []struct {
+		engine engine
+	}{
+		{engine{podman, false, "container123", ""}},
+		{engine{docker, true, "container456", ""}},
+	}
+
+	for _, test := range tests {
+		test.engine.cleanup()
+	}
+}
+
+func TestGetReferencesFromContainer(t *testing.T) {
+	// Override execCommand with a fake function
+	execCommand = fakeExecCommand
+
+	// reset execCommand and lookPath after the test
+	defer func() {
+		execCommand = exec.Command
+		lookPath = exec.LookPath // lookPath is reassigned below
+	}()
+
+	tests := []struct {
+		path                    string
+		tempContainerRefDir     string
+		podmanOrDockerAvailable bool
+		expectError             bool
+	}{
+		{"container://image:tag:/etc/configs", "/tmp/refdir", true, false},
+		{"invalid-path", "/tmp/refdir", true, true},
+
+		// fakeExecCommand will fail when provided with arguments that contain "invalid"
+		{"container://image:tag:/etc/configs", "/invalid-tmp/refdir", true, true}, // Trigger extractReferences() failure
+		{"container://invalid-image:tag:/etc/configs", "/tmp/refdir", true, true}, // Trigger pullAndRunContainer() failure
+
+		{"container://image:tag:/etc/configs", "/tmp/refdir", false, true}, // Trigger newEngine() failure
+	}
+
+	for _, test := range tests {
+		lookPath = func(cmd string) (string, error) {
+			if test.podmanOrDockerAvailable == false {
+				return "", errors.New("not found")
+			}
+			return "/usr/bin/podman", nil
+
+		}
+		tempDir, err := getReferencesFromContainer(test.path, test.tempContainerRefDir)
+		if test.expectError {
+			if err == nil {
+				t.Errorf("Expected error but got nil")
+			}
+		} else {
+			if err != nil {
+				t.Errorf("Expected nil error, got %#v", err)
+			}
+			if tempDir == "" {
+				t.Errorf("Expected a valid tempDir, got an empty string")
+			}
+		}
+	}
+}


Reviews:
user: login: lack
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-13T21:05:39Z
user: login: lack
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-13T21:10:53Z
user: login: lack
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-13T21:12:41Z
user: login: lack
body: This is awesome!

Some minor cleanup suggestions in comments.
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-03-03T22:31:40Z
user: login: lack
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-12T17:33:43Z

Review comments:
diff_hunk: @@ -0,0 +1,139 @@
+package compare
+
+import (
+	"fmt"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"strings"
+)
+
+type engine struct {
+	name         string
+	requiresSudo bool
+}
+
+// isContainer reports whether the given path is a reference to a file in a container by verifying if it starts with "container://".
+func isContainer(path string) bool {
+	return strings.HasPrefix(path, "container://")
+}
+
+// parsePath returns the image and referencePath (path to the directory for metadata.yaml), given a path
+// of the form container://<IMAGE>:<TAG>:/path_to_metadata.yaml
+func parsePath(path string) (string, string, error) {
+	path = strings.TrimPrefix(path, "container://")
+
+	// Split on ':', removing empty strings from slice. Removes errant colons from string,
+	// so container://<IMAGE>:::<TAG>::::::/path/to/metadata.yaml will still work, but
+	// paths with leading and trailing colons won't.
+	f := func(c rune) bool {
+		return c == ':'
+	}
+	sections := strings.FieldsFunc(path, f)
+
+	if len(sections) == 3 {
+		image := sections[0] + ":" + sections[1]
+		referencePath := sections[2]
+		return image, referencePath, nil
+	}
+	return "", "", fmt.Errorf("Incorrect path passed into -r, it should follow this format: container://<IMAGE>:<TAG>:/path_to_metadata.yaml")
path: pkg/compare/container.go
user: login: lack
body: ```suggestion
	return "", "", fmt.Errorf("Incorrect container path passed to -r, it must follow this format: container://<IMAGE>:<TAG>:/path/to/metadata.yaml")
```
created_at: 2025-02-13T21:05:38Z
author_association: MEMBER
diff_hunk: @@ -56,6 +56,12 @@ Run a known valid reference configuration with an `oc must-gather` output:
 kubectl cluster-compare -r ./reference/metadata.yaml -f "must-gather*/*/cluster-scoped-resources","must-gather*/*/namespaces" -R
 ```
 
+Retrieve a known valid reference configuration from a container and compare with a local set of CRs
path: README.md
user: login: lack
body: ```suggestion
Extract a known valid reference configuration from a container image and compare with a local set of CRs:
```
created_at: 2025-02-13T21:10:53Z
author_association: MEMBER
diff_hunk: @@ -87,6 +87,9 @@ var (
 
 		# Run a known valid reference configuration with a must-gather output:
 		kubectl cluster-compare -r ./reference/metadata.yaml -f "must-gather*/*/cluster-scoped-resources","must-gather*/*/namespaces" -R
+
+		# Retrieve a known valid reference configuration from a container and compare with a local set of CRs:
path: pkg/compare/compare.go
user: login: lack
body: ```suggestion
		# Extract a known valid reference configuration from a container image and compare with a local set of CRs:
```
created_at: 2025-02-13T21:12:41Z
author_association: MEMBER
diff_hunk: @@ -265,11 +280,20 @@ func (o *Options) Complete(f kcmdutil.Factory, cmd *cobra.Command, args []string
 	if o.referenceConfig == "" {
 		return kcmdutil.UsageErrorf(cmd, noRefFileWasPassed)
 	}
-	if _, err := os.Stat(o.referenceConfig); os.IsNotExist(err) && !isURL(o.referenceConfig) {
+	if _, err := os.Stat(o.referenceConfig); os.IsNotExist(err) && !isURL(o.referenceConfig) && !isContainer(o.referenceConfig) {
 		return errors.New(refFileNotExistsError)
 	}
 
-	cfs, err := GetRefFS(o.referenceConfig)
+	var tempContainerRefDir = ""
+	if isContainer(o.referenceConfig) {
+		tempContainerRefDir, err = os.MkdirTemp("", "kube-compare")
path: pkg/compare/compare.go
user: login: lack
body: I see that we're creating the temporary directory here in `Complete`, but cleaning it up inside `Run` which seems a bit odd.  I think given we always call both `Complete` and then `Run` it will work, but what would you think about putting both in the inline function definition on line 165.  That parent function would then be in charge of both creation and cleanup of the tmpdir, still storing the value in `o.tmpContainerRefDir`.  For simplicity you could call it `o.tmpDir` instead, and just create it and clean it up every run, without ever checking `isContainer`.

In order to maintain sanity, you should probably have someone eventually check that `o.tmpDir` is set and that it is an existing directory.  Or even leave that check to be done way down in `getReferencesFromContainer` or `extractReferences`, closer to where it's used.
created_at: 2025-03-03T21:48:29Z
author_association: MEMBER
diff_hunk: @@ -235,13 +240,23 @@ func diffError(err error) exec.ExitError {
 	return nil
 }
 
-func GetRefFS(refConfig string) (fs.FS, error) {
+func GetRefFS(refConfig string, tempContainerRefDir string) (fs.FS, error) {
path: pkg/compare/compare.go
user: login: lack
body: If you set this up to be an Option reciever instead, I think it would encapsulate both the refConfig and the tmpContainerRefDir arguments instead of having to pass them in:
```suggestion
func (o *Options) GetRefFS() (fs.FS, error) {
```
created_at: 2025-03-03T21:50:11Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,140 @@
+package compare
+
+import (
+	"fmt"
+	"os/exec"
+	"path/filepath"
+	"strings"
+)
+
+type engine struct {
+	name         string
+	requiresSudo bool
+	containerID  string
+	tempDir      string
+}
+
+// isContainer reports whether the given path is a reference to a file in a container by verifying if it starts with "container://".
+func isContainer(path string) bool {
+	return strings.HasPrefix(path, "container://")
+}
+
+// parsePath returns the image and referencePath (path to the directory for metadata.yaml), given a path
+// of the form container://<IMAGE>:<TAG>:/path_to_metadata.yaml
+func parsePath(path string) (string, string, error) {
path: pkg/compare/container.go
user: login: lack
body: Returning a tuple can sometimes be confusing, because you have to remember which string is which.  Sometimes it's valuable to create a struct with named arguments instead:

```suggestion
type parsedPath struct {
    image string
    path  string
}

func parsePath(path string) (parsedPath, error) {
```
created_at: 2025-03-03T22:01:38Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,140 @@
+package compare
+
+import (
+	"fmt"
+	"os/exec"
+	"path/filepath"
+	"strings"
+)
+
+type engine struct {
+	name         string
+	requiresSudo bool
+	containerID  string
+	tempDir      string
+}
+
+// isContainer reports whether the given path is a reference to a file in a container by verifying if it starts with "container://".
+func isContainer(path string) bool {
+	return strings.HasPrefix(path, "container://")
+}
+
+// parsePath returns the image and referencePath (path to the directory for metadata.yaml), given a path
+// of the form container://<IMAGE>:<TAG>:/path_to_metadata.yaml
+func parsePath(path string) (string, string, error) {
+	path = strings.TrimPrefix(path, "container://")
+
+	// Split on ':', removing empty strings from slice. Removes errant colons from string,
+	// so container://<IMAGE>:::<TAG>::::::/path/to/metadata.yaml will still work, but
+	// paths with leading and trailing colons won't.
+	f := func(c rune) bool {
+		return c == ':'
+	}
+	sections := strings.FieldsFunc(path, f)
+
+	if len(sections) == 3 {
+		image := sections[0] + ":" + sections[1]
+		referencePath := sections[2]
+		return image, referencePath, nil
+	}
+	return "", "", fmt.Errorf("incorrect path passed to -r, it must follow this format: container://<IMAGE>:<TAG>:/path/to/metadata.yaml")
+}
+
+// Use var's so that we can mock functions in tests.
path: pkg/compare/container.go
user: login: lack
body: Spectacular!
created_at: 2025-03-03T22:02:23Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,140 @@
+package compare
+
+import (
+	"fmt"
+	"os/exec"
+	"path/filepath"
+	"strings"
+)
+
+type engine struct {
+	name         string
+	requiresSudo bool
+	containerID  string
+	tempDir      string
+}
+
+// isContainer reports whether the given path is a reference to a file in a container by verifying if it starts with "container://".
+func isContainer(path string) bool {
+	return strings.HasPrefix(path, "container://")
+}
+
+// parsePath returns the image and referencePath (path to the directory for metadata.yaml), given a path
+// of the form container://<IMAGE>:<TAG>:/path_to_metadata.yaml
+func parsePath(path string) (string, string, error) {
+	path = strings.TrimPrefix(path, "container://")
+
+	// Split on ':', removing empty strings from slice. Removes errant colons from string,
+	// so container://<IMAGE>:::<TAG>::::::/path/to/metadata.yaml will still work, but
+	// paths with leading and trailing colons won't.
+	f := func(c rune) bool {
+		return c == ':'
+	}
+	sections := strings.FieldsFunc(path, f)
+
+	if len(sections) == 3 {
+		image := sections[0] + ":" + sections[1]
+		referencePath := sections[2]
+		return image, referencePath, nil
+	}
+	return "", "", fmt.Errorf("incorrect path passed to -r, it must follow this format: container://<IMAGE>:<TAG>:/path/to/metadata.yaml")
+}
+
+// Use var's so that we can mock functions in tests.
+var execCommand = exec.Command
+var lookPath = exec.LookPath
+
+// runEngineCommand runs a podman/docker command with sudo if necessary.
+// Returns the stdout (out) and stderr (err) of the command.
+func (engine *engine) runEngineCommand(args ...string) ([]byte, error) {
+	var out []byte
+	var err error
+	if engine.requiresSudo {
+		args = append([]string{engine.name}, args...) // Prepend engine name to args
+		out, err = execCommand("sudo", args...).CombinedOutput()
+	} else {
+		out, err = execCommand(engine.name, args...).CombinedOutput()
+	}
+
+	return out, err
+}
+
+// newEngine checks if Podman or Docker are in the system's PATH, and returns an engine with a name and a boolean
+// that indicates if sudo is needed for future commands. Prefers Podman. Returns an error if neither engine is found.
+var newEngine = func() (*engine, error) {
+	if _, err := lookPath("podman"); err == nil {
+		return &engine{name: "podman", requiresSudo: false}, nil
+	}
+
+	if _, err := lookPath("docker"); err == nil {
+		_, err = execCommand("docker", "images").Output() // If this errors out, we need to use sudo, return true.
+		return &engine{name: "docker", requiresSudo: err != nil}, nil
+	}
+	return &engine{name: "", requiresSudo: false}, fmt.Errorf("you do not have Podman or Docker on your PATH")
+}
+
+// pullContainer pulls an image, runs it using the provided engine, and stores the corresponding containerID in the engine struct
+func (engine *engine) pullAndRunContainer(image string) error {
+	// run because copy requires a running or stopped container
+	// -d to output container ID
+	out, err := engine.runEngineCommand("run", "-d", image)
+	if err != nil {
+		return fmt.Errorf("could not pull/run container: %s", out)
+	}
+	engine.containerID = strings.TrimSpace(string(out)) // Convert bytes to string and trim new line
+	return nil
+}
+
+// extractReferences copies the directory in the container that contains the reference configs into a temporary directory,
+// and stores the path to the new directory in the engine struct.
+func (engine *engine) extractReferences(pathToMetadata string, dname string) error {
+
+	out, err := engine.runEngineCommand("cp", engine.containerID+":"+pathToMetadata, dname)
+	if err != nil {
+		return fmt.Errorf("could not copy templates from container: %s", out)
+	}
+	engine.tempDir = filepath.Join(dname, filepath.Base(pathToMetadata))
+	return nil
+}
+
+// cleanup stops and removes the container used to extract the reference configs.
+func (engine *engine) cleanup() error {
+	out, err := engine.runEngineCommand("stop", engine.containerID)
+	if err != nil {
+		// Print errors as warnings rather than returning, since stopping and removing the container is not vital.
+		fmt.Printf("Warning: Could not stop container: %s", out)
path: pkg/compare/container.go
user: login: lack
body: Change these fmt.Printf lines to klog.Warningf for more consistent output:
```suggestion
		klog.Warningf("Could not stop container: %s", out)
```
created_at: 2025-03-03T22:04:36Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,140 @@
+package compare
+
+import (
+	"fmt"
+	"os/exec"
+	"path/filepath"
+	"strings"
+)
+
+type engine struct {
+	name         string
+	requiresSudo bool
+	containerID  string
+	tempDir      string
+}
+
+// isContainer reports whether the given path is a reference to a file in a container by verifying if it starts with "container://".
+func isContainer(path string) bool {
+	return strings.HasPrefix(path, "container://")
+}
+
+// parsePath returns the image and referencePath (path to the directory for metadata.yaml), given a path
+// of the form container://<IMAGE>:<TAG>:/path_to_metadata.yaml
+func parsePath(path string) (string, string, error) {
+	path = strings.TrimPrefix(path, "container://")
+
+	// Split on ':', removing empty strings from slice. Removes errant colons from string,
+	// so container://<IMAGE>:::<TAG>::::::/path/to/metadata.yaml will still work, but
+	// paths with leading and trailing colons won't.
+	f := func(c rune) bool {
+		return c == ':'
+	}
+	sections := strings.FieldsFunc(path, f)
+
+	if len(sections) == 3 {
+		image := sections[0] + ":" + sections[1]
+		referencePath := sections[2]
+		return image, referencePath, nil
+	}
+	return "", "", fmt.Errorf("incorrect path passed to -r, it must follow this format: container://<IMAGE>:<TAG>:/path/to/metadata.yaml")
+}
+
+// Use var's so that we can mock functions in tests.
+var execCommand = exec.Command
+var lookPath = exec.LookPath
+
+// runEngineCommand runs a podman/docker command with sudo if necessary.
+// Returns the stdout (out) and stderr (err) of the command.
+func (engine *engine) runEngineCommand(args ...string) ([]byte, error) {
+	var out []byte
+	var err error
+	if engine.requiresSudo {
+		args = append([]string{engine.name}, args...) // Prepend engine name to args
+		out, err = execCommand("sudo", args...).CombinedOutput()
+	} else {
+		out, err = execCommand(engine.name, args...).CombinedOutput()
+	}
+
+	return out, err
+}
+
+// newEngine checks if Podman or Docker are in the system's PATH, and returns an engine with a name and a boolean
+// that indicates if sudo is needed for future commands. Prefers Podman. Returns an error if neither engine is found.
+var newEngine = func() (*engine, error) {
+	if _, err := lookPath("podman"); err == nil {
+		return &engine{name: "podman", requiresSudo: false}, nil
+	}
+
+	if _, err := lookPath("docker"); err == nil {
+		_, err = execCommand("docker", "images").Output() // If this errors out, we need to use sudo, return true.
+		return &engine{name: "docker", requiresSudo: err != nil}, nil
+	}
+	return &engine{name: "", requiresSudo: false}, fmt.Errorf("you do not have Podman or Docker on your PATH")
+}
+
+// pullContainer pulls an image, runs it using the provided engine, and stores the corresponding containerID in the engine struct
+func (engine *engine) pullAndRunContainer(image string) error {
+	// run because copy requires a running or stopped container
+	// -d to output container ID
+	out, err := engine.runEngineCommand("run", "-d", image)
+	if err != nil {
+		return fmt.Errorf("could not pull/run container: %s", out)
+	}
+	engine.containerID = strings.TrimSpace(string(out)) // Convert bytes to string and trim new line
+	return nil
+}
+
+// extractReferences copies the directory in the container that contains the reference configs into a temporary directory,
+// and stores the path to the new directory in the engine struct.
+func (engine *engine) extractReferences(pathToMetadata string, dname string) error {
+
+	out, err := engine.runEngineCommand("cp", engine.containerID+":"+pathToMetadata, dname)
+	if err != nil {
+		return fmt.Errorf("could not copy templates from container: %s", out)
+	}
+	engine.tempDir = filepath.Join(dname, filepath.Base(pathToMetadata))
+	return nil
+}
+
+// cleanup stops and removes the container used to extract the reference configs.
+func (engine *engine) cleanup() error {
+	out, err := engine.runEngineCommand("stop", engine.containerID)
+	if err != nil {
+		// Print errors as warnings rather than returning, since stopping and removing the container is not vital.
+		fmt.Printf("Warning: Could not stop container: %s", out)
+	}
+	out, err = engine.runEngineCommand("rm", engine.containerID)
+	if err != nil {
+		fmt.Printf("Warning: Could not remove container: %s", out)
path: pkg/compare/container.go
user: login: lack
body: ```suggestion
		klog.Warningf("Could not remove container: %s", out)
```
created_at: 2025-03-03T22:04:50Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,140 @@
+package compare
+
+import (
+	"fmt"
+	"os/exec"
+	"path/filepath"
+	"strings"
+)
+
+type engine struct {
+	name         string
+	requiresSudo bool
+	containerID  string
+	tempDir      string
+}
+
+// isContainer reports whether the given path is a reference to a file in a container by verifying if it starts with "container://".
+func isContainer(path string) bool {
+	return strings.HasPrefix(path, "container://")
+}
+
+// parsePath returns the image and referencePath (path to the directory for metadata.yaml), given a path
+// of the form container://<IMAGE>:<TAG>:/path_to_metadata.yaml
+func parsePath(path string) (string, string, error) {
+	path = strings.TrimPrefix(path, "container://")
+
+	// Split on ':', removing empty strings from slice. Removes errant colons from string,
+	// so container://<IMAGE>:::<TAG>::::::/path/to/metadata.yaml will still work, but
+	// paths with leading and trailing colons won't.
+	f := func(c rune) bool {
+		return c == ':'
+	}
+	sections := strings.FieldsFunc(path, f)
+
+	if len(sections) == 3 {
+		image := sections[0] + ":" + sections[1]
+		referencePath := sections[2]
+		return image, referencePath, nil
+	}
+	return "", "", fmt.Errorf("incorrect path passed to -r, it must follow this format: container://<IMAGE>:<TAG>:/path/to/metadata.yaml")
+}
+
+// Use var's so that we can mock functions in tests.
+var execCommand = exec.Command
+var lookPath = exec.LookPath
+
+// runEngineCommand runs a podman/docker command with sudo if necessary.
+// Returns the stdout (out) and stderr (err) of the command.
+func (engine *engine) runEngineCommand(args ...string) ([]byte, error) {
+	var out []byte
+	var err error
+	if engine.requiresSudo {
+		args = append([]string{engine.name}, args...) // Prepend engine name to args
+		out, err = execCommand("sudo", args...).CombinedOutput()
+	} else {
+		out, err = execCommand(engine.name, args...).CombinedOutput()
+	}
+
+	return out, err
+}
+
+// newEngine checks if Podman or Docker are in the system's PATH, and returns an engine with a name and a boolean
+// that indicates if sudo is needed for future commands. Prefers Podman. Returns an error if neither engine is found.
+var newEngine = func() (*engine, error) {
+	if _, err := lookPath("podman"); err == nil {
+		return &engine{name: "podman", requiresSudo: false}, nil
+	}
+
+	if _, err := lookPath("docker"); err == nil {
+		_, err = execCommand("docker", "images").Output() // If this errors out, we need to use sudo, return true.
+		return &engine{name: "docker", requiresSudo: err != nil}, nil
+	}
+	return &engine{name: "", requiresSudo: false}, fmt.Errorf("you do not have Podman or Docker on your PATH")
+}
+
+// pullContainer pulls an image, runs it using the provided engine, and stores the corresponding containerID in the engine struct
+func (engine *engine) pullAndRunContainer(image string) error {
+	// run because copy requires a running or stopped container
+	// -d to output container ID
+	out, err := engine.runEngineCommand("run", "-d", image)
+	if err != nil {
+		return fmt.Errorf("could not pull/run container: %s", out)
+	}
+	engine.containerID = strings.TrimSpace(string(out)) // Convert bytes to string and trim new line
+	return nil
+}
+
+// extractReferences copies the directory in the container that contains the reference configs into a temporary directory,
+// and stores the path to the new directory in the engine struct.
+func (engine *engine) extractReferences(pathToMetadata string, dname string) error {
+
+	out, err := engine.runEngineCommand("cp", engine.containerID+":"+pathToMetadata, dname)
+	if err != nil {
+		return fmt.Errorf("could not copy templates from container: %s", out)
+	}
+	engine.tempDir = filepath.Join(dname, filepath.Base(pathToMetadata))
+	return nil
+}
+
+// cleanup stops and removes the container used to extract the reference configs.
+func (engine *engine) cleanup() error {
+	out, err := engine.runEngineCommand("stop", engine.containerID)
+	if err != nil {
+		// Print errors as warnings rather than returning, since stopping and removing the container is not vital.
+		fmt.Printf("Warning: Could not stop container: %s", out)
+	}
+	out, err = engine.runEngineCommand("rm", engine.containerID)
+	if err != nil {
+		fmt.Printf("Warning: Could not remove container: %s", out)
+	}
+	return nil
+}
+
+// getReferencesFromContainer uses a path to an image and a metadata.yaml within that image, and extracts the reference configs
+// to a local temporary directory. Returns the path to this directory.
+func getReferencesFromContainer(path string, tempContainerRefDir string) (string, error) {
path: pkg/compare/container.go
user: login: lack
body: Minor nit, but some code linters may complain about this - If you have 2 args of the same type in a row, you can combine them like this:
```suggestion
func getReferencesFromContainer(path, tempContainerRefDir string) (string, error) {
```
created_at: 2025-03-03T22:06:27Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,326 @@
+package compare
+
+import (
+	"errors"
+	"fmt"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"strings"
+	"testing"
+)
+
+func TestIsContainer(t *testing.T) {
+	tests := []struct {
+		input    string
+		expected bool
+	}{
+		{"container://my-image:latest:/metadata.yaml", true},
+		{"file://local/path/to/file.yaml", false},
+		{"http://example.com/data.yaml", false},
+		{"randomstringwithoutprefix", false},
path: pkg/compare/container_test.go
user: login: lack
body: We could consider adding other potential edge cases such as:
- `"container:/only-one-slash:tag:/metadata.yaml"`
- `"container/with/no/colon/metadata.yaml"`
- `"container://name:tag"`
- `"container://name"`
- `"container://"`
- `"container`
- `""`
created_at: 2025-03-03T22:09:36Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,326 @@
+package compare
+
+import (
+	"errors"
+	"fmt"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"strings"
+	"testing"
+)
+
+func TestIsContainer(t *testing.T) {
+	tests := []struct {
+		input    string
+		expected bool
+	}{
+		{"container://my-image:latest:/metadata.yaml", true},
+		{"file://local/path/to/file.yaml", false},
+		{"http://example.com/data.yaml", false},
+		{"randomstringwithoutprefix", false},
+	}
+
+	for _, test := range tests {
+		result := isContainer(test.input)
+		if result != test.expected {
+			t.Errorf("For input '%s', expected %v, got %v", test.input, test.expected, result)
+		}
+	}
+}
+
+func TestParsePath(t *testing.T) {
+	tests := []struct {
+		input         string
+		expectedImage string
+		expectedPath  string
+		expectError   bool
+	}{
+		{"container://my-image:latest:/metadata.yaml", "my-image:latest", "/metadata.yaml", false},
+		{"container://repo/image:tag:/data.yaml", "repo/image:tag", "/data.yaml", false},
+		{"container://repo/image:::tag::::/data.yaml", "repo/image:tag", "/data.yaml", false},
+		{"container://wrongformat", "", "", true},
+		{":missingprefix:/file.yaml", "", "", true},
+		{"container://only:two", "", "", true},
path: pkg/compare/container_test.go
user: login: lack
body: As above, try to think like a user who has a 2-year-old helping them type commands in ways that would potentially confuse the parser:
- `"container://"`
- `"container:/"`
- `"container:"`
- `"container"`
- `""`
created_at: 2025-03-03T22:11:41Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,326 @@
+package compare
+
+import (
+	"errors"
+	"fmt"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"strings"
+	"testing"
+)
+
+func TestIsContainer(t *testing.T) {
+	tests := []struct {
+		input    string
+		expected bool
+	}{
+		{"container://my-image:latest:/metadata.yaml", true},
+		{"file://local/path/to/file.yaml", false},
+		{"http://example.com/data.yaml", false},
+		{"randomstringwithoutprefix", false},
+	}
+
+	for _, test := range tests {
+		result := isContainer(test.input)
+		if result != test.expected {
+			t.Errorf("For input '%s', expected %v, got %v", test.input, test.expected, result)
+		}
+	}
+}
+
+func TestParsePath(t *testing.T) {
+	tests := []struct {
+		input         string
+		expectedImage string
+		expectedPath  string
+		expectError   bool
+	}{
+		{"container://my-image:latest:/metadata.yaml", "my-image:latest", "/metadata.yaml", false},
+		{"container://repo/image:tag:/data.yaml", "repo/image:tag", "/data.yaml", false},
+		{"container://repo/image:::tag::::/data.yaml", "repo/image:tag", "/data.yaml", false},
+		{"container://wrongformat", "", "", true},
+		{":missingprefix:/file.yaml", "", "", true},
+		{"container://only:two", "", "", true},
+	}
+
+	for _, test := range tests {
+		image, path, err := parsePath(test.input)
+		if test.expectError {
+			if err == nil {
+				t.Errorf("Expected error for input '%s', but got none", test.input)
+			}
+		} else {
+			if err != nil {
+				t.Errorf("Unexpected error for input '%s': %v", test.input, err)
+			}
+			if image != test.expectedImage || path != test.expectedPath {
+				t.Errorf("For input '%s', expected ('%s', '%s'), got ('%s', '%s')", test.input, test.expectedImage, test.expectedPath, image, path)
+			}
+		}
+	}
+}
+
+// fakeExecCommand() and TestHelperProcess() are derived from https://npf.io/2015/06/testing-exec-command/,
+// The MIT License (MIT)
+
+// Copyright (c) 2014 Nate Finch
+
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+func fakeExecCommand(command string, args ...string) *exec.Cmd {
+	for _, arg := range args {
+		if strings.Contains(arg, "invalid") {
+			cmd := exec.Command("false")
+			return cmd
+		}
+	}
+	cs := []string{"-test.run=TestHelperProcess", "--", command}
+	cs = append(cs, args...)
+	cmd := exec.Command(os.Args[0], cs...)
+	cmd.Env = []string{"GO_WANT_HELPER_PROCESS=1"}
+	return cmd
+}
+
+func fakeLookPath(command string) (string, error) {
+	switch command {
+	case "podman":
+		return "/usr/bin/podman", nil
+	case "docker":
+		return "/usr/bin/docker", nil
+	default:
+		return "", errors.New("not found")
+	}
+}
+
+const dockerRunResult = "fake output"
+
+func TestRunEngineCommand(t *testing.T) {
+	// Override execCommand with fakes
+	execCommand = fakeExecCommand
+	defer func() { execCommand = exec.Command }()
+	tests := []struct {
+		engine engine
+		args   []string
+	}{
+		{engine{"podman", false, "container123", "/tmp/dir/"}, []string{"run", "hello-world"}},
+		{engine{"docker", true, "container123", "/tmp/dir/"}, []string{"run", "hello-world"}},
+	}
+
+	for _, test := range tests {
+		out, err := test.engine.runEngineCommand(test.args...)
+
+		if err != nil {
+			t.Errorf("Expected nil error, got %#v", err)
+		}
+		if string(out) != dockerRunResult {
+			t.Errorf("Expected %q, got %q", dockerRunResult, out)
+		}
+	}
+}
+
+func TestHasPodmanOrDocker(t *testing.T) {
+	// Override execCommand and LookPath with fakes
+	execCommand = fakeExecCommand
+	lookPath = fakeLookPath
+	defer func() {
+		execCommand = exec.Command
+		lookPath = exec.LookPath
+	}()
+
+	tests := []struct {
+		available    string
+		expectedName string
+		expectedSudo bool
+		expectError  bool
+	}{
+		{"podman", "podman", false, false},     // Podman is available
path: pkg/compare/container_test.go
user: login: lack
body: One more case I thought of - What if both `podman` and `docker` are available?
created_at: 2025-03-03T22:13:44Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,326 @@
+package compare
+
+import (
+	"errors"
+	"fmt"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"strings"
+	"testing"
+)
+
+func TestIsContainer(t *testing.T) {
+	tests := []struct {
+		input    string
+		expected bool
+	}{
+		{"container://my-image:latest:/metadata.yaml", true},
+		{"file://local/path/to/file.yaml", false},
+		{"http://example.com/data.yaml", false},
+		{"randomstringwithoutprefix", false},
+	}
+
+	for _, test := range tests {
+		result := isContainer(test.input)
+		if result != test.expected {
+			t.Errorf("For input '%s', expected %v, got %v", test.input, test.expected, result)
+		}
+	}
+}
+
+func TestParsePath(t *testing.T) {
+	tests := []struct {
+		input         string
+		expectedImage string
+		expectedPath  string
+		expectError   bool
+	}{
+		{"container://my-image:latest:/metadata.yaml", "my-image:latest", "/metadata.yaml", false},
+		{"container://repo/image:tag:/data.yaml", "repo/image:tag", "/data.yaml", false},
+		{"container://repo/image:::tag::::/data.yaml", "repo/image:tag", "/data.yaml", false},
+		{"container://wrongformat", "", "", true},
+		{":missingprefix:/file.yaml", "", "", true},
+		{"container://only:two", "", "", true},
+	}
+
+	for _, test := range tests {
+		image, path, err := parsePath(test.input)
+		if test.expectError {
+			if err == nil {
+				t.Errorf("Expected error for input '%s', but got none", test.input)
+			}
+		} else {
+			if err != nil {
+				t.Errorf("Unexpected error for input '%s': %v", test.input, err)
+			}
+			if image != test.expectedImage || path != test.expectedPath {
+				t.Errorf("For input '%s', expected ('%s', '%s'), got ('%s', '%s')", test.input, test.expectedImage, test.expectedPath, image, path)
+			}
+		}
+	}
+}
+
+// fakeExecCommand() and TestHelperProcess() are derived from https://npf.io/2015/06/testing-exec-command/,
+// The MIT License (MIT)
+
+// Copyright (c) 2014 Nate Finch
+
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+func fakeExecCommand(command string, args ...string) *exec.Cmd {
+	for _, arg := range args {
+		if strings.Contains(arg, "invalid") {
+			cmd := exec.Command("false")
+			return cmd
+		}
+	}
+	cs := []string{"-test.run=TestHelperProcess", "--", command}
+	cs = append(cs, args...)
+	cmd := exec.Command(os.Args[0], cs...)
+	cmd.Env = []string{"GO_WANT_HELPER_PROCESS=1"}
+	return cmd
+}
+
+func fakeLookPath(command string) (string, error) {
+	switch command {
+	case "podman":
+		return "/usr/bin/podman", nil
+	case "docker":
+		return "/usr/bin/docker", nil
+	default:
+		return "", errors.New("not found")
+	}
+}
+
+const dockerRunResult = "fake output"
+
+func TestRunEngineCommand(t *testing.T) {
+	// Override execCommand with fakes
+	execCommand = fakeExecCommand
+	defer func() { execCommand = exec.Command }()
+	tests := []struct {
+		engine engine
+		args   []string
+	}{
+		{engine{"podman", false, "container123", "/tmp/dir/"}, []string{"run", "hello-world"}},
+		{engine{"docker", true, "container123", "/tmp/dir/"}, []string{"run", "hello-world"}},
+	}
+
+	for _, test := range tests {
+		out, err := test.engine.runEngineCommand(test.args...)
+
+		if err != nil {
+			t.Errorf("Expected nil error, got %#v", err)
+		}
+		if string(out) != dockerRunResult {
+			t.Errorf("Expected %q, got %q", dockerRunResult, out)
+		}
+	}
+}
+
+func TestHasPodmanOrDocker(t *testing.T) {
+	// Override execCommand and LookPath with fakes
+	execCommand = fakeExecCommand
+	lookPath = fakeLookPath
+	defer func() {
+		execCommand = exec.Command
+		lookPath = exec.LookPath
+	}()
+
+	tests := []struct {
+		available    string
+		expectedName string
+		expectedSudo bool
+		expectError  bool
+	}{
+		{"podman", "podman", false, false},     // Podman is available
+		{"docker", "docker", false, false},     // Docker available, no sudo needed
+		{"docker-sudo", "docker", true, false}, // Docker requires sudo
+		{"none", "", false, true},              // Neither Podman nor Docker is available
+	}
+
+	for _, test := range tests {
+		// Customize behavior based on test case
+		lookPath = func(cmd string) (string, error) {
+			if (test.available == "podman" && cmd == "podman") || (test.available == "docker" || test.available == "docker-sudo") && cmd == "docker" {
+				return "/usr/bin/" + cmd, nil
+			}
+			return "", errors.New("not found")
+		}
+
+		execCommand = func(command string, args ...string) *exec.Cmd {
+			cs := []string{"-test.run=TestHelperProcess", "--", command}
+			cs = append(cs, args...)
+			cmd := exec.Command(os.Args[0], cs...)
+			cmd.Env = []string{"GO_WANT_HELPER_PROCESS=1"}
+			if test.available == "docker-sudo" {
+				cmd.Env = append(cmd.Env, "MOCK_DOCKER_FAIL=1") // Simulate `docker images` failing
+			}
+			return cmd
+		}
+
+		eng, err := newEngine()
+
+		if test.expectError {
+			if err == nil {
+				t.Errorf("Expected error but got nil")
+			}
+		} else {
+			if err != nil {
+				t.Errorf("Unexpected error: %v", err)
+			}
+			if eng.name != test.expectedName {
+				t.Errorf("Expected engine name %q, got %q", test.expectedName, eng.name)
+			}
+			if eng.requiresSudo != test.expectedSudo {
+				t.Errorf("Expected requiresSudo %v, got %v", test.expectedSudo, eng.requiresSudo)
+			}
+		}
+	}
+}
+
+func TestHelperProcess(t *testing.T) {
+	if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" {
+		return
+	}
+
+	if os.Getenv("MOCK_DOCKER_FAIL") == "1" {
+		os.Exit(1)
+	}
+	fmt.Fprintf(os.Stdout, dockerRunResult)
+	os.Exit(0)
+}
+
+func TestPullAndRunContainer(t *testing.T) {
+	// Override execCommand with a fake function
+	execCommand = fakeExecCommand
+	defer func() { execCommand = exec.Command }()
+
+	tests := []struct {
+		engine      engine
+		image       string
+		expectError bool
+	}{
+		{engine{"podman", false, "", ""}, "hello-world", false},
+		{engine{"docker", true, "", ""}, "hello-world", false},
+		{engine{"docker", true, "", ""}, "invalid-image", true},
+	}
+
+	for _, test := range tests {
+		err := test.engine.pullAndRunContainer(test.image)
+		if test.expectError {
+			if err == nil {
+				t.Errorf("Expected error but got nil")
+			}
+		} else {
+			if err != nil {
+				t.Errorf("Expected nil error, got %#v", err)
+			}
+			if test.engine.containerID != dockerRunResult {
+				t.Errorf("Expected container ID %q, got %q", dockerRunResult, test.engine.containerID)
+			}
+		}
+	}
+}
+
+func TestExtractReferences(t *testing.T) {
+	// Override execCommand with a fake function
+	execCommand = fakeExecCommand
+	defer func() { execCommand = exec.Command }()
+
+	tests := []struct {
+		engine         engine
+		pathToMetadata string
+		dname          string
+		expectError    bool
+	}{
+		{engine{"podman", false, "container123", ""}, "/etc/configs", "/tmp/extracted", false},
+		{engine{"docker", true, "container456", ""}, "/var/lib/data", "/tmp/extracted", false},
+		{engine{"docker", true, "container456", ""}, "/invalid_path", "/tmp/extracted", true},
path: pkg/compare/container_test.go
user: login: lack
body: Other test case ideas:
- What if tmpdir does not exist (Or is that relevant at this level?)
created_at: 2025-03-03T22:25:01Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,326 @@
+package compare
+
+import (
+	"errors"
+	"fmt"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"strings"
+	"testing"
+)
+
+func TestIsContainer(t *testing.T) {
+	tests := []struct {
+		input    string
+		expected bool
+	}{
+		{"container://my-image:latest:/metadata.yaml", true},
+		{"file://local/path/to/file.yaml", false},
+		{"http://example.com/data.yaml", false},
+		{"randomstringwithoutprefix", false},
+	}
+
+	for _, test := range tests {
+		result := isContainer(test.input)
+		if result != test.expected {
+			t.Errorf("For input '%s', expected %v, got %v", test.input, test.expected, result)
+		}
+	}
+}
+
+func TestParsePath(t *testing.T) {
+	tests := []struct {
+		input         string
+		expectedImage string
+		expectedPath  string
+		expectError   bool
+	}{
+		{"container://my-image:latest:/metadata.yaml", "my-image:latest", "/metadata.yaml", false},
+		{"container://repo/image:tag:/data.yaml", "repo/image:tag", "/data.yaml", false},
+		{"container://repo/image:::tag::::/data.yaml", "repo/image:tag", "/data.yaml", false},
+		{"container://wrongformat", "", "", true},
+		{":missingprefix:/file.yaml", "", "", true},
+		{"container://only:two", "", "", true},
+	}
+
+	for _, test := range tests {
+		image, path, err := parsePath(test.input)
+		if test.expectError {
+			if err == nil {
+				t.Errorf("Expected error for input '%s', but got none", test.input)
+			}
+		} else {
+			if err != nil {
+				t.Errorf("Unexpected error for input '%s': %v", test.input, err)
+			}
+			if image != test.expectedImage || path != test.expectedPath {
+				t.Errorf("For input '%s', expected ('%s', '%s'), got ('%s', '%s')", test.input, test.expectedImage, test.expectedPath, image, path)
+			}
+		}
+	}
+}
+
+// fakeExecCommand() and TestHelperProcess() are derived from https://npf.io/2015/06/testing-exec-command/,
+// The MIT License (MIT)
+
+// Copyright (c) 2014 Nate Finch
+
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+func fakeExecCommand(command string, args ...string) *exec.Cmd {
+	for _, arg := range args {
+		if strings.Contains(arg, "invalid") {
+			cmd := exec.Command("false")
+			return cmd
+		}
+	}
+	cs := []string{"-test.run=TestHelperProcess", "--", command}
+	cs = append(cs, args...)
+	cmd := exec.Command(os.Args[0], cs...)
+	cmd.Env = []string{"GO_WANT_HELPER_PROCESS=1"}
+	return cmd
+}
+
+func fakeLookPath(command string) (string, error) {
+	switch command {
+	case "podman":
+		return "/usr/bin/podman", nil
+	case "docker":
+		return "/usr/bin/docker", nil
+	default:
+		return "", errors.New("not found")
+	}
+}
+
+const dockerRunResult = "fake output"
+
+func TestRunEngineCommand(t *testing.T) {
+	// Override execCommand with fakes
+	execCommand = fakeExecCommand
+	defer func() { execCommand = exec.Command }()
+	tests := []struct {
+		engine engine
+		args   []string
+	}{
+		{engine{"podman", false, "container123", "/tmp/dir/"}, []string{"run", "hello-world"}},
+		{engine{"docker", true, "container123", "/tmp/dir/"}, []string{"run", "hello-world"}},
+	}
+
+	for _, test := range tests {
+		out, err := test.engine.runEngineCommand(test.args...)
+
+		if err != nil {
+			t.Errorf("Expected nil error, got %#v", err)
+		}
+		if string(out) != dockerRunResult {
+			t.Errorf("Expected %q, got %q", dockerRunResult, out)
+		}
+	}
+}
+
+func TestHasPodmanOrDocker(t *testing.T) {
+	// Override execCommand and LookPath with fakes
+	execCommand = fakeExecCommand
+	lookPath = fakeLookPath
+	defer func() {
+		execCommand = exec.Command
+		lookPath = exec.LookPath
+	}()
+
+	tests := []struct {
+		available    string
+		expectedName string
+		expectedSudo bool
+		expectError  bool
+	}{
+		{"podman", "podman", false, false},     // Podman is available
+		{"docker", "docker", false, false},     // Docker available, no sudo needed
+		{"docker-sudo", "docker", true, false}, // Docker requires sudo
+		{"none", "", false, true},              // Neither Podman nor Docker is available
+	}
+
+	for _, test := range tests {
+		// Customize behavior based on test case
+		lookPath = func(cmd string) (string, error) {
+			if (test.available == "podman" && cmd == "podman") || (test.available == "docker" || test.available == "docker-sudo") && cmd == "docker" {
+				return "/usr/bin/" + cmd, nil
+			}
+			return "", errors.New("not found")
+		}
+
+		execCommand = func(command string, args ...string) *exec.Cmd {
+			cs := []string{"-test.run=TestHelperProcess", "--", command}
+			cs = append(cs, args...)
+			cmd := exec.Command(os.Args[0], cs...)
+			cmd.Env = []string{"GO_WANT_HELPER_PROCESS=1"}
+			if test.available == "docker-sudo" {
+				cmd.Env = append(cmd.Env, "MOCK_DOCKER_FAIL=1") // Simulate `docker images` failing
+			}
+			return cmd
+		}
+
+		eng, err := newEngine()
+
+		if test.expectError {
+			if err == nil {
+				t.Errorf("Expected error but got nil")
+			}
+		} else {
+			if err != nil {
+				t.Errorf("Unexpected error: %v", err)
+			}
+			if eng.name != test.expectedName {
+				t.Errorf("Expected engine name %q, got %q", test.expectedName, eng.name)
+			}
+			if eng.requiresSudo != test.expectedSudo {
+				t.Errorf("Expected requiresSudo %v, got %v", test.expectedSudo, eng.requiresSudo)
+			}
+		}
+	}
+}
+
+func TestHelperProcess(t *testing.T) {
+	if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" {
+		return
+	}
+
+	if os.Getenv("MOCK_DOCKER_FAIL") == "1" {
+		os.Exit(1)
+	}
+	fmt.Fprintf(os.Stdout, dockerRunResult)
+	os.Exit(0)
+}
+
+func TestPullAndRunContainer(t *testing.T) {
+	// Override execCommand with a fake function
+	execCommand = fakeExecCommand
+	defer func() { execCommand = exec.Command }()
+
+	tests := []struct {
+		engine      engine
+		image       string
+		expectError bool
+	}{
+		{engine{"podman", false, "", ""}, "hello-world", false},
+		{engine{"docker", true, "", ""}, "hello-world", false},
+		{engine{"docker", true, "", ""}, "invalid-image", true},
+	}
+
+	for _, test := range tests {
+		err := test.engine.pullAndRunContainer(test.image)
+		if test.expectError {
+			if err == nil {
+				t.Errorf("Expected error but got nil")
+			}
+		} else {
+			if err != nil {
+				t.Errorf("Expected nil error, got %#v", err)
+			}
+			if test.engine.containerID != dockerRunResult {
+				t.Errorf("Expected container ID %q, got %q", dockerRunResult, test.engine.containerID)
+			}
+		}
+	}
+}
+
+func TestExtractReferences(t *testing.T) {
+	// Override execCommand with a fake function
+	execCommand = fakeExecCommand
+	defer func() { execCommand = exec.Command }()
+
+	tests := []struct {
+		engine         engine
+		pathToMetadata string
+		dname          string
+		expectError    bool
+	}{
+		{engine{"podman", false, "container123", ""}, "/etc/configs", "/tmp/extracted", false},
+		{engine{"docker", true, "container456", ""}, "/var/lib/data", "/tmp/extracted", false},
+		{engine{"docker", true, "container456", ""}, "/invalid_path", "/tmp/extracted", true},
+	}
+
+	for _, test := range tests {
+		err := test.engine.extractReferences(test.pathToMetadata, test.dname)
+		if test.expectError {
+			if err == nil {
+				t.Errorf("Expected error but got nil")
+			}
+		} else {
+			if err != nil {
+				t.Errorf("Expected nil error, got %#v", err)
+			}
+			expectedTempDir := filepath.Join(test.dname, filepath.Base(test.pathToMetadata))
+			if test.engine.tempDir != expectedTempDir {
+				t.Errorf("Expected tempDir %q, got %q", expectedTempDir, test.engine.tempDir)
+			}
+		}
+	}
+}
+
+func TestCleanup(t *testing.T) {
+	// Override execCommand with a fake function
+	execCommand = fakeExecCommand
+	defer func() { execCommand = exec.Command }()
+
+	tests := []struct {
+		engine engine
+	}{
+		{engine{"podman", false, "container123", ""}},
+		{engine{"docker", true, "container456", ""}},
+	}
+
+	for _, test := range tests {
+		err := test.engine.cleanup()
+		if err != nil {
+			t.Errorf("Expected nil error, got %#v", err)
+		}
+	}
+}
+
+func TestGetReferencesFromContainer(t *testing.T) {
+	// Override execCommand with a fake function
+	execCommand = fakeExecCommand
+	defer func() { execCommand = exec.Command }()
+
+	tests := []struct {
+		path                string
+		tempContainerRefDir string
+		expectError         bool
+	}{
+		{"container://image:tag:/etc/configs", "/tmp/refdir", false},
+		{"invalid-path", "/tmp/refdir", true},
path: pkg/compare/container_test.go
user: login: lack
body: We should think about adding a code coverage measurement to our unit testing - But think about how many of the interesting if clauses in the function-under-test are being exercised by the test cases...
created_at: 2025-03-03T22:25:27Z
author_association: MEMBER
diff_hunk: @@ -56,6 +56,12 @@ Run a known valid reference configuration with an `oc must-gather` output:
 kubectl cluster-compare -r ./reference/metadata.yaml -f "must-gather*/*/cluster-scoped-resources","must-gather*/*/namespaces" -R
 ```
 
+Extract a known valid reference configuration from a container image and compare with a local set of CRs:
path: README.md
user: login: lack
body: ```suggestion
Extract a reference configuration from a container image and compare with a local set of CRs:
```
created_at: 2025-03-03T22:26:52Z
author_association: MEMBER
diff_hunk: @@ -88,6 +88,9 @@ var (
 
 		# Run a known valid reference configuration with a must-gather output:
 		kubectl cluster-compare -r ./reference/metadata.yaml -f "must-gather*/*/cluster-scoped-resources","must-gather*/*/namespaces" -R
+
+		# Extract a known valid reference configuration from a container image and compare with a local set of CRs:
path: pkg/compare/compare.go
user: login: lack
body: ```suggestion
		# Extract a reference configuration from a container image and compare with a local set of CRs:
```
created_at: 2025-03-03T22:27:13Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,140 @@
+package compare
+
+import (
+	"fmt"
+	"os/exec"
+	"path/filepath"
+	"strings"
+)
+
+type engine struct {
+	name         string
+	requiresSudo bool
+	containerID  string
+	tempDir      string
+}
+
+// isContainer reports whether the given path is a reference to a file in a container by verifying if it starts with "container://".
+func isContainer(path string) bool {
+	return strings.HasPrefix(path, "container://")
+}
+
+// parsePath returns the image and referencePath (path to the directory for metadata.yaml), given a path
+// of the form container://<IMAGE>:<TAG>:/path_to_metadata.yaml
+func parsePath(path string) (string, string, error) {
+	path = strings.TrimPrefix(path, "container://")
+
+	// Split on ':', removing empty strings from slice. Removes errant colons from string,
+	// so container://<IMAGE>:::<TAG>::::::/path/to/metadata.yaml will still work, but
+	// paths with leading and trailing colons won't.
+	f := func(c rune) bool {
+		return c == ':'
+	}
+	sections := strings.FieldsFunc(path, f)
+
+	if len(sections) == 3 {
+		image := sections[0] + ":" + sections[1]
+		referencePath := sections[2]
+		return image, referencePath, nil
+	}
+	return "", "", fmt.Errorf("incorrect path passed to -r, it must follow this format: container://<IMAGE>:<TAG>:/path/to/metadata.yaml")
+}
+
+// Use var's so that we can mock functions in tests.
+var execCommand = exec.Command
+var lookPath = exec.LookPath
+
+// runEngineCommand runs a podman/docker command with sudo if necessary.
+// Returns the stdout (out) and stderr (err) of the command.
+func (engine *engine) runEngineCommand(args ...string) ([]byte, error) {
+	var out []byte
+	var err error
+	if engine.requiresSudo {
+		args = append([]string{engine.name}, args...) // Prepend engine name to args
+		out, err = execCommand("sudo", args...).CombinedOutput()
+	} else {
+		out, err = execCommand(engine.name, args...).CombinedOutput()
+	}
+
+	return out, err
+}
+
+// newEngine checks if Podman or Docker are in the system's PATH, and returns an engine with a name and a boolean
+// that indicates if sudo is needed for future commands. Prefers Podman. Returns an error if neither engine is found.
+var newEngine = func() (*engine, error) {
+	if _, err := lookPath("podman"); err == nil {
+		return &engine{name: "podman", requiresSudo: false}, nil
+	}
+
+	if _, err := lookPath("docker"); err == nil {
+		_, err = execCommand("docker", "images").Output() // If this errors out, we need to use sudo, return true.
+		return &engine{name: "docker", requiresSudo: err != nil}, nil
+	}
+	return &engine{name: "", requiresSudo: false}, fmt.Errorf("you do not have Podman or Docker on your PATH")
+}
+
+// pullContainer pulls an image, runs it using the provided engine, and stores the corresponding containerID in the engine struct
+func (engine *engine) pullAndRunContainer(image string) error {
+	// run because copy requires a running or stopped container
+	// -d to output container ID
+	out, err := engine.runEngineCommand("run", "-d", image)
+	if err != nil {
+		return fmt.Errorf("could not pull/run container: %s", out)
+	}
+	engine.containerID = strings.TrimSpace(string(out)) // Convert bytes to string and trim new line
+	return nil
+}
+
+// extractReferences copies the directory in the container that contains the reference configs into a temporary directory,
+// and stores the path to the new directory in the engine struct.
+func (engine *engine) extractReferences(pathToMetadata string, dname string) error {
+
+	out, err := engine.runEngineCommand("cp", engine.containerID+":"+pathToMetadata, dname)
+	if err != nil {
+		return fmt.Errorf("could not copy templates from container: %s", out)
+	}
+	engine.tempDir = filepath.Join(dname, filepath.Base(pathToMetadata))
+	return nil
+}
+
+// cleanup stops and removes the container used to extract the reference configs.
+func (engine *engine) cleanup() error {
path: pkg/compare/container.go
user: login: lack
body: This function can only return `nil` so maybe just
```suggestion
func (engine *engine) cleanup() {
```
created_at: 2025-03-03T22:30:23Z
author_association: MEMBER

PR comments:
user: login: openshift-ci[bot]
created_at: 2025-02-12T01:18:17Z
body: Skipping CI for Draft Pull Request.
If you want CI signal for your change, please convert it to an actual PR.
You can still manually trigger a test run with `/test all`
author_association: NONE
user: login: adhil0
created_at: 2025-03-10T23:48:56Z
body: /retest
author_association: CONTRIBUTOR
user: login: openshift-ci[bot]
created_at: 2025-03-12T17:04:45Z
body: @adhil0: all tests **passed!**


[Full PR test history](https://prow.ci.openshift.org/pr-history?org=openshift&repo=kube-compare&pr=172). [Your PR dashboard](https://prow.ci.openshift.org/pr?query=is:pr+state:open+author:adhil0).

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes-sigs/prow](https://github.com/kubernetes-sigs/prow/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
author_association: NONE
user: login: openshift-ci-robot
created_at: 2025-03-12T17:10:16Z
body: @adhil0: No Jira issue with key CNF-144495 exists in the tracker at https://issues.redhat.com/.
Once a valid jira issue is referenced in the title of this pull request, request a refresh with <code>/jira refresh</code>.

<details>

In response to [this](https://github.com/openshift/kube-compare/pull/172):

>In the future, it will be preferable for us to deliver references to users via a container. Give users the ability to refer to a
>metadata.yaml in an image directly from the kube-compare command-line. 
>
>Use `exec.Command()` to run podman and docker operations rather than official go bindings because quick experimentation showed that it would increase the binary size from ~90 MB to ~120 MB. 
>
>Still need to address:
>
>- [x] podman/docker login to registry; Don't need to login, just need to make sure error messages are descriptive
>- [x] cleaning up files created in tmp directory
>- [x] add more tests


Instructions for interacting with me using PR comments are available [here](https://prow.ci.openshift.org/command-help?repo=openshift%2Fkube-compare).  If you have questions or suggestions related to my behavior, please file an issue against the [openshift-eng/jira-lifecycle-plugin](https://github.com/openshift-eng/jira-lifecycle-plugin/issues/new) repository.
</details>
author_association: NONE
user: login: openshift-ci-robot
created_at: 2025-03-12T17:13:17Z
body: @adhil0: This pull request references [CNF-14495](https://issues.redhat.com//browse/CNF-14495) which is a valid jira issue.

Warning: The referenced jira issue has an invalid target version for the target branch this PR targets: expected the story to target the "4.19.0" version, but no target version was set.

<details>

In response to [this](https://github.com/openshift/kube-compare/pull/172):

>In the future, it will be preferable for us to deliver references to users via a container. Give users the ability to refer to a
>metadata.yaml in an image directly from the kube-compare command-line. 
>
>Use `exec.Command()` to run podman and docker operations rather than official go bindings because quick experimentation showed that it would increase the binary size from ~90 MB to ~120 MB. 
>
>Still need to address:
>
>- [x] podman/docker login to registry; Don't need to login, just need to make sure error messages are descriptive
>- [x] cleaning up files created in tmp directory
>- [x] add more tests


Instructions for interacting with me using PR comments are available [here](https://prow.ci.openshift.org/command-help?repo=openshift%2Fkube-compare).  If you have questions or suggestions related to my behavior, please file an issue against the [openshift-eng/jira-lifecycle-plugin](https://github.com/openshift-eng/jira-lifecycle-plugin/issues/new) repository.
</details>
author_association: NONE
user: login: lack
created_at: 2025-03-12T17:35:02Z
body: /approve
/lgtm
author_association: MEMBER
user: login: openshift-ci[bot]
created_at: 2025-03-12T17:36:26Z
body: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/openshift/kube-compare/pull/172#issuecomment-2718628639" title="LGTM">lack</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=openshift%2Fkube-compare).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/openshift/kube-compare/blob/main/OWNERS)~~ [lack]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
author_association: NONE
user: login: openshift-bot
created_at: 2025-03-12T20:57:31Z
body: **[ART PR BUILD NOTIFIER]**

Distgit: kube-compare-artifacts
This PR has been included in build [kube-compare-artifacts-container-v4.19.0-202503121913.p0.g54e6ab0.assembly.stream.el9](https://brewweb.engineering.redhat.com/brew/buildinfo?buildID=3552280).
All builds following this will include this PR.
author_association: NONE