From a2333c11a3ec26d01672c2abdbfb80d008021622 Mon Sep 17 00:00:00 2001 From: Ed Chong <5728235+ewchong@users.noreply.github.com> Date: Fri, 3 May 2024 21:28:25 -0400 Subject: [PATCH] Add podman remote support (#61) * Add podman remote support Close #27 --------- Co-authored-by: Edwin Chong <8726446-ewchong@users.noreply.gitlab.com> Co-authored-by: Webb Scales --- config.go | 6 +- factory.go | 2 +- internal/cliwrapper/cliwrapper.go | 89 ++++++++++++++----------- internal/cliwrapper/cliwrapper_test.go | 91 ++++++++++++++++++++++++-- schema.go | 10 +++ 5 files changed, 153 insertions(+), 45 deletions(-) diff --git a/config.go b/config.go index e980e45..5f49881 100644 --- a/config.go +++ b/config.go @@ -1,8 +1,9 @@ package podman import ( - "github.com/docker/docker/api/types/container" "time" + + "github.com/docker/docker/api/types/container" ) type Config struct { @@ -37,6 +38,8 @@ type Podman struct { // The initial integer that is the starting point for a // Random Number Generator's algorithm. RngSeed int64 `json:"rngSeed"` + // Specify the optional --connection parameter for podman + ConnectionName *string `json:"connectionName"` } // Deployment contains the information about deploying the plugin. @@ -45,6 +48,7 @@ type Deployment struct { HostConfig *container.HostConfig `json:"host"` ImagePullPolicy ImagePullPolicy `json:"imagePullPolicy"` ImagePlatform *string `json:"imagePlatform"` + ConnectionName *string `json:"connectionName"` } // Timeouts drive the timeouts for various interactions in relation to Docker. diff --git a/factory.go b/factory.go index e8323a5..768bbeb 100644 --- a/factory.go +++ b/factory.go @@ -40,7 +40,7 @@ func (f factory) Create(config *Config, logger log.Logger) (deployer.Connector, if err != nil { return &Connector{}, fmt.Errorf("podman binary check failed with error: %w", err) } - podman := cliwrapper.NewCliWrapper(podmanPath, logger) + podman := cliwrapper.NewCliWrapper(podmanPath, logger, config.Podman.ConnectionName) var rngSeed int64 if config.Podman.RngSeed == 0 { diff --git a/internal/cliwrapper/cliwrapper.go b/internal/cliwrapper/cliwrapper.go index 063c47e..6bad8ca 100644 --- a/internal/cliwrapper/cliwrapper.go +++ b/internal/cliwrapper/cliwrapper.go @@ -15,12 +15,20 @@ import ( type cliWrapper struct { podmanFullPath string logger log.Logger + connectionName []string } -func NewCliWrapper(fullPath string, logger log.Logger) CliWrapper { +func NewCliWrapper(fullPath string, logger log.Logger, connectionName *string) CliWrapper { + // Specify podman --connection string if provided + connection := []string{} + if connectionName != nil { + connection = append(connection, "--connection="+*connectionName) + } + return &cliWrapper{ podmanFullPath: fullPath, logger: logger, + connectionName: connection, } } @@ -33,50 +41,32 @@ func (p *cliWrapper) decorateImageName(image string) string { } func (p *cliWrapper) ImageExists(image string) (*bool, error) { - image = p.decorateImageName(image) - cmd := exec.Command(p.podmanFullPath, "image", "ls", "--format", "{{.Repository}}:{{.Tag}}") //nolint:gosec - var out bytes.Buffer - var errOut bytes.Buffer - cmd.Stdout = &out - cmd.Stderr = &errOut - p.logger.Debugf("Checking whether image exists with command %v", cmd.Args) - if err := cmd.Run(); err != nil { - return nil, fmt.Errorf( - "error while determining if image exists. Stdout: '%s', Stderr: '%s', Cmd error: (%w)", - out.String(), errOut.String(), err) + outStr, err := p.runPodmanCmd( + "checking whether image exists", + "image", "ls", "--format", "{{.Repository}}:{{.Tag}}", + ) + if err != nil { + return nil, err } - outStr := out.String() outSlice := strings.Split(outStr, "\n") - exists := util.SliceContains(outSlice, image) + exists := util.SliceContains(outSlice, p.decorateImageName(image)) return &exists, nil } func (p *cliWrapper) PullImage(image string, platform *string) error { commandArgs := []string{"pull"} if platform != nil { - commandArgs = append(commandArgs, []string{"--platform", *platform}...) - } - image = p.decorateImageName(image) - commandArgs = append(commandArgs, image) - cmd := exec.Command(p.podmanFullPath, commandArgs...) //nolint:gosec - p.logger.Debugf("Pulling image with command %v", cmd.Args) - var out bytes.Buffer - var errOut bytes.Buffer - cmd.Stdout = &out - cmd.Stderr = &errOut - if err := cmd.Run(); err != nil { - return fmt.Errorf( - "error while pulling image. Stdout: '%s', Stderr: '%s', Cmd error: (%w)", - out.String(), errOut.String(), err) + commandArgs = append(commandArgs, "--platform", *platform) } - return nil + commandArgs = append(commandArgs, p.decorateImageName(image)) + _, err := p.runPodmanCmd("pulling image", commandArgs...) + return err } func (p *cliWrapper) Deploy(image string, podmanArgs []string, containerArgs []string) (io.WriteCloser, io.ReadCloser, error) { - image = p.decorateImageName(image) - podmanArgs = append(podmanArgs, image) + podmanArgs = append(podmanArgs, p.decorateImageName(image)) podmanArgs = append(podmanArgs, containerArgs...) - deployCommand := exec.Command(p.podmanFullPath, podmanArgs...) //nolint:gosec + deployCommand := p.getPodmanCmd(podmanArgs...) p.logger.Debugf("Deploying with command %v", deployCommand.Args) stdin, err := deployCommand.StdinPipe() if err != nil { @@ -93,7 +83,7 @@ func (p *cliWrapper) Deploy(image string, podmanArgs []string, containerArgs []s } func (p *cliWrapper) KillAndClean(containerName string) error { - cmdKill := exec.Command(p.podmanFullPath, "kill", containerName) //nolint:gosec + cmdKill := p.getPodmanCmd("kill", containerName) p.logger.Debugf("Killing with command %v", cmdKill.Args) if err := cmdKill.Run(); err != nil { p.logger.Warningf("failed to kill pod %s, probably the execution terminated earlier", containerName) @@ -101,14 +91,35 @@ func (p *cliWrapper) KillAndClean(containerName string) error { p.logger.Warningf("successfully killed container %s", containerName) } - var cmdRmContainerStderr bytes.Buffer - cmdRmContainer := exec.Command(p.podmanFullPath, "rm", "--force", containerName) //nolint:gosec - p.logger.Debugf("Removing container with command %v", cmdRmContainer.Args) - cmdRmContainer.Stderr = &cmdRmContainerStderr - if err := cmdRmContainer.Run(); err != nil { - p.logger.Errorf("failed to remove container %s: %s", containerName, cmdRmContainerStderr.String()) + msg := "removing container " + containerName + _, err := p.runPodmanCmd(msg, "rm", "--force", containerName) + if err != nil { + p.logger.Errorf(err.Error()) } else { p.logger.Infof("successfully removed container %s", containerName) } return nil } + +func (p *cliWrapper) getPodmanCmd(cmdArgs ...string) *exec.Cmd { + var commandArgs []string + commandArgs = append(commandArgs, p.connectionName...) + commandArgs = append(commandArgs, cmdArgs...) + return exec.Command(p.podmanFullPath, commandArgs...) //#nosec G204 -- command line is internally generated +} + +func (p *cliWrapper) runPodmanCmd(msg string, cmdArgs ...string) (string, error) { + var out bytes.Buffer + var errOut bytes.Buffer + + cmd := p.getPodmanCmd(cmdArgs...) + cmd.Stdout = &out + cmd.Stderr = &errOut + p.logger.Debugf(msg+" with command %v", cmd.Args) + if err := cmd.Run(); err != nil { + return "", fmt.Errorf( + "error while %s. Stdout: '%s', Stderr: '%s', Cmd error: (%w)", + msg, strings.TrimSpace(out.String()), strings.TrimSpace(errOut.String()), err) + } + return out.String(), nil +} diff --git a/internal/cliwrapper/cliwrapper_test.go b/internal/cliwrapper/cliwrapper_test.go index 76bfe75..4124ef0 100644 --- a/internal/cliwrapper/cliwrapper_test.go +++ b/internal/cliwrapper/cliwrapper_test.go @@ -2,7 +2,9 @@ package cliwrapper import ( "fmt" + "os" "os/exec" + "runtime" "testing" log "go.arcalot.io/log/v2" @@ -11,15 +13,15 @@ import ( "go.flow.arcalot.io/podmandeployer/tests" ) -func TestPodman_ImageExists(t *testing.T) { +func podmanImageExists(t *testing.T, connectionName *string) { logger := log.NewTestLogger(t) tests.RemoveImage(logger, tests.TestImage) - podman := NewCliWrapper(tests.GetPodmanPath(), logger) + podman := NewCliWrapper(tests.GetPodmanPath(), logger, connectionName) assert.NotNil(t, tests.GetPodmanPath()) - cmd := exec.Command(tests.GetPodmanPath(), "pull", tests.TestImage) //nolint:gosec + cmd := exec.Command(tests.GetPodmanPath(), "pull", tests.TestImage) //nolint:gosec // Command line is trusted if err := cmd.Run(); err != nil { t.Fatalf(err.Error()) } @@ -48,11 +50,92 @@ func TestPodman_ImageExists(t *testing.T) { tests.RemoveImage(logger, tests.TestImage) } +func TestPodman_ImageExists(t *testing.T) { + podmanImageExists(t, nil) +} + +func TestPodman_Remote_ImageExists(t *testing.T) { + // Check if there is an existing connection of `podman-machine-default` + // since this is included when installing podman desktop for macOS. + connectionName := "podman-machine-default" + chkDefaultConnectionCmd := exec.Command(tests.GetPodmanPath(), "--connection", connectionName, "system", "info") //nolint:gosec // Command line is trusted + if err := chkDefaultConnectionCmd.Run(); err != nil { + // The podman-machine-default connection doesn't exist, so try to create + // an alternative connection service. For now, only try this on Linux. + // + //goland:noinspection GoBoolExpressions // The linter cannot tell that this expression is not constant. + if runtime.GOOS != "linux" { + t.Skipf("There is no default Podman connection and no support for creating it on %s.", runtime.GOOS) + } + + connectionName = createPodmanConnection(t) + } + + // Run the test + podmanImageExists(t, &connectionName) +} + +// createPodmanConnection creates a Podman API service process and configures +// a Podman "connection" to allow it to be used for remote Podman invocations. +func createPodmanConnection(t *testing.T) (connectionName string) { + // Setup: create a temporary directory with a random name, to avoid + // collisions with other concurrently-running tests; use the resulting + // path as the name of the Podman service connection and put the service + // socket in the directory. Start a listener on that socket and configure + // a connection to it. Declare cleanup functions which will remove the + // connection, kill the listener, and remove the temporary directory and + // socket. + t.Logf("Adding a local Podman API service and connection.") + sockDir, err := os.MkdirTemp("", "arcaflow-engine-deployer-podman-test-*") + if err != nil { + t.Fatalf("Unable to create socket directory: %q", err) + } + + t.Cleanup(func() { + t.Logf("Removing socket directory, %q.", sockDir) + if err := os.RemoveAll(sockDir); err != nil { + t.Logf("Unable to remove socket directory, %q: %q", sockDir, err) + } + }) + + t.Logf("Local Podman API service connection is %q.", sockDir) + + connectionName = sockDir + podmanSocketPath := "unix://" + sockDir + "/podman.sock" + + podmanApiServiceCmd := exec.Command(tests.GetPodmanPath(), "system", "service", "--time=0", podmanSocketPath) //nolint:gosec // Command line is trusted + if err := podmanApiServiceCmd.Start(); err != nil { + t.Fatal("Failed to create temporary Podman API service process") + } + + t.Cleanup(func() { + t.Logf("Killing the Podman API service process.") + if err := podmanApiServiceCmd.Process.Kill(); err != nil { + t.Fatal("Failed to kill Podman API service process.") + } + }) + + addConnectionCmd := exec.Command(tests.GetPodmanPath(), "system", "connection", "add", connectionName, podmanSocketPath) //nolint:gosec // Command line is trusted + if err := addConnectionCmd.Run(); err != nil { + t.Fatalf("Failed to add connection %q.", connectionName) + } + + t.Cleanup(func() { + t.Logf("Removing the Podman connection.") + delConnectionCmd := exec.Command(tests.GetPodmanPath(), "system", "connection", "remove", connectionName) //nolint:gosec // Command line is trusted + if err := delConnectionCmd.Run(); err != nil { + t.Fatalf("Failed to delete connection %q.", connectionName) + } + }) + + return connectionName +} + func TestPodman_PullImage(t *testing.T) { logger := log.NewTestLogger(t) tests.RemoveImage(logger, tests.TestImageMultiPlatform) - podman := NewCliWrapper(tests.GetPodmanPath(), logger) + podman := NewCliWrapper(tests.GetPodmanPath(), logger, nil) assert.NotNil(t, tests.GetPodmanPath()) // pull without platform diff --git a/schema.go b/schema.go index 90d8c94..11cd71c 100644 --- a/schema.go +++ b/schema.go @@ -86,6 +86,16 @@ var Schema = schema.NewTypedScopeSchema[*Config]( nil, nil, ), + "connectionName": schema.NewPropertySchema( + schema.NewStringSchema(nil, nil, nil), + schema.NewDisplayValue(schema.PointerTo("Connection"), schema.PointerTo("Connection name to use for remote podman"), nil), + false, + nil, + nil, + nil, + nil, + nil, + ), }, ), schema.NewStructMappedObjectSchema[Deployment](