Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add podman remote support #61

Merged
merged 13 commits into from
May 4, 2024
6 changes: 5 additions & 1 deletion config.go
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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.
Expand All @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
89 changes: 50 additions & 39 deletions internal/cliwrapper/cliwrapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
}

Expand All @@ -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 {
Expand All @@ -93,22 +83,43 @@ 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)
} else {
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
}
91 changes: 87 additions & 4 deletions internal/cliwrapper/cliwrapper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package cliwrapper

import (
"fmt"
"os"
"os/exec"
"runtime"
"testing"

log "go.arcalot.io/log/v2"
Expand All @@ -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())
}
Expand Down Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -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](
Expand Down
Loading