Skip to content

Commit

Permalink
Add podman remote support (#61)
Browse files Browse the repository at this point in the history
* Add podman remote support

Close #27

---------

Co-authored-by: Edwin Chong <8726446-ewchong@users.noreply.gitlab.com>
Co-authored-by: Webb Scales <wscales@redhat.com>
  • Loading branch information
3 people authored May 4, 2024
1 parent 22103e0 commit a2333c1
Show file tree
Hide file tree
Showing 5 changed files with 153 additions and 45 deletions.
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

0 comments on commit a2333c1

Please sign in to comment.