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
6 changes: 5 additions & 1 deletion factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,11 @@ 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)
connectionName := ""
if config.Podman.ConnectionName != nil {
connectionName = *config.Podman.ConnectionName
}
podman := cliwrapper.NewCliWrapper(podmanPath, logger, 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 != "" {
connection = []string{"--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
}
55 changes: 52 additions & 3 deletions internal/cliwrapper/cliwrapper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import (
"fmt"
"os/exec"
"runtime"
"testing"

log "go.arcalot.io/log/v2"
Expand All @@ -11,11 +12,11 @@
"go.flow.arcalot.io/podmandeployer/tests"
)

func TestPodman_ImageExists(t *testing.T) {
func Podman_ImageExists(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())

Expand Down Expand Up @@ -48,11 +49,59 @@
tests.RemoveImage(logger, tests.TestImage)
}

func TestPodman_ImageExists(t *testing.T) {
Podman_ImageExists(t, "")
}

func TestPodman_Remote_ImageExists(t *testing.T) {
var tmpPodmanSocketCmd *exec.Cmd

// Check if there is an existing connection of `podman-machine-default` since this is included when installing
// podman desktop for macOS.
ewchong marked this conversation as resolved.
Show resolved Hide resolved
connectionName := "podman-machine-default"
chkDefaultConnectionCmd := exec.Command(tests.GetPodmanPath(), "--connection", connectionName, "system", "info") //nolint:gosec
ewchong marked this conversation as resolved.
Show resolved Hide resolved
if err := chkDefaultConnectionCmd.Run(); err == nil {

Check failure on line 63 in internal/cliwrapper/cliwrapper_test.go

View workflow job for this annotation

GitHub Actions / golangci-lint

`if err == nil` has complex nested blocks (complexity: 6) (nestif)
Podman_ImageExists(t, connectionName)
} else if runtime.GOOS == "linux" {
ewchong marked this conversation as resolved.
Show resolved Hide resolved
// The podman-machine-default doesn't exist then for Linux, create a temporary socket

// Setup
connectionName = "arcaflow-engine-deployer-podman-test"
podmanSocketPath := "unix:///var/tmp/" + connectionName + ".sock"

tmpPodmanSocketCmd = exec.Command(tests.GetPodmanPath(), "system", "service", "--time=0", podmanSocketPath)

Check failure on line 72 in internal/cliwrapper/cliwrapper_test.go

View workflow job for this annotation

GitHub Actions / golangci-lint

G204: Subprocess launched with a potential tainted input or cmd arguments (gosec)
ewchong marked this conversation as resolved.
Show resolved Hide resolved
if err := tmpPodmanSocketCmd.Start(); err != nil {
t.Fatalf("Failed to create temporary podman socket")
}
webbnh marked this conversation as resolved.
Show resolved Hide resolved

addConnectionCmd := exec.Command(tests.GetPodmanPath(), "system", "connection", "add", connectionName, podmanSocketPath)

Check failure on line 77 in internal/cliwrapper/cliwrapper_test.go

View workflow job for this annotation

GitHub Actions / golangci-lint

G204: Subprocess launched with a potential tainted input or cmd arguments (gosec)
if err := addConnectionCmd.Run(); err != nil {
t.Fatalf("Failed to add connection: " + connectionName)
}

// Run test
Podman_ImageExists(t, connectionName)

// Clean up
if err := tmpPodmanSocketCmd.Process.Kill(); err != nil {
ewchong marked this conversation as resolved.
Show resolved Hide resolved
t.Fatalf("Failed to kill temporary socket")
}

delConnectionCmd := exec.Command(tests.GetPodmanPath(), "system", "connection", "remove", connectionName)

Check failure on line 90 in internal/cliwrapper/cliwrapper_test.go

View workflow job for this annotation

GitHub Actions / golangci-lint

G204: Subprocess launched with a potential tainted input or cmd arguments (gosec)
if err := delConnectionCmd.Run(); err != nil {
t.Fatalf("Failed to delete connection: " + connectionName)
}
// Unexpected setup, force user to add podman-machine-default
} else {
t.Fatalf("Unsupported configuration")
ewchong marked this conversation as resolved.
Show resolved Hide resolved
}
}

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, "")
ewchong marked this conversation as resolved.
Show resolved Hide resolved
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