Skip to content

Commit 7f9da4d

Browse files
authored
Merge pull request #467 from Caesarsage/feat/dry-run-podman
feat(test): podman driver support for microcks test --dry-run
2 parents b2ba821 + 337308d commit 7f9da4d

3 files changed

Lines changed: 107 additions & 18 deletions

File tree

cmd/test.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ func NewTestCommand(globalClientOpts *connectors.ClientOptions) *cobra.Command {
4444
image string
4545
readyTimeout time.Duration
4646
watch bool
47+
driver string
4748
)
4849
var testCmd = &cobra.Command{
4950

@@ -131,6 +132,10 @@ func NewTestCommand(globalClientOpts *connectors.ClientOptions) *cobra.Command {
131132
fmt.Println("--watch is only valid together with --dry-run")
132133
os.Exit(1)
133134
}
135+
if driver != "" {
136+
fmt.Println("--driver is only valid together with --dry-run")
137+
os.Exit(1)
138+
}
134139
}
135140

136141
if dryRun {
@@ -140,6 +145,7 @@ func NewTestCommand(globalClientOpts *connectors.ClientOptions) *cobra.Command {
140145
image: image,
141146
readyTimeout: readyTimeout,
142147
watch: watch,
148+
driver: driver,
143149
params: params,
144150
}) {
145151
os.Exit(1)
@@ -230,6 +236,7 @@ func NewTestCommand(globalClientOpts *connectors.ClientOptions) *cobra.Command {
230236
testCmd.Flags().StringVar(&image, "image", defaultDryRunImage, "Microcks uber-native image used for --dry-run")
231237
testCmd.Flags().DurationVar(&readyTimeout, "ready-timeout", 90*time.Second, "How long to wait for the ephemeral container to be ready (--dry-run only)")
232238
testCmd.Flags().BoolVar(&watch, "watch", false, "Watch the artifact file and re-run the test on change (--dry-run only)")
239+
testCmd.Flags().StringVar(&driver, "driver", "", "Container runtime for --dry-run: 'docker' or 'podman' (default: auto-detect)")
233240

234241
return testCmd
235242
}

cmd/testDryRun.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"fmt"
2121
"net/url"
2222
"os"
23+
"os/exec"
2324
"os/signal"
2425
"path/filepath"
2526
"strconv"
@@ -44,9 +45,58 @@ type dryRunOptions struct {
4445
image string
4546
readyTimeout time.Duration
4647
watch bool
48+
driver string
4749
params testParams
4850
}
4951

52+
// configureDriver points testcontainers-go at the right container runtime.
53+
// Docker is its default (honoring DOCKER_HOST); Podman needs its socket wired
54+
// into DOCKER_HOST and Ryuk disabled. An empty driver auto-detects.
55+
func configureDriver(driver string) error {
56+
switch driver {
57+
case "podman":
58+
return setupPodman()
59+
case "docker":
60+
return nil // testcontainers-go's default, via DOCKER_HOST
61+
case "":
62+
if shouldUsePodman() {
63+
return setupPodman()
64+
}
65+
return nil
66+
default:
67+
return fmt.Errorf("unsupported --driver %q (use 'docker' or 'podman')", driver)
68+
}
69+
}
70+
71+
// shouldUsePodman auto-detects podman only when it's clearly the intended
72+
// runtime: no explicit DOCKER_HOST, podman on PATH, and docker absent.
73+
func shouldUsePodman() bool {
74+
if os.Getenv("DOCKER_HOST") != "" {
75+
return false // respect an explicitly configured endpoint
76+
}
77+
_, podErr := exec.LookPath("podman")
78+
_, dockErr := exec.LookPath("docker")
79+
return podErr == nil && dockErr != nil
80+
}
81+
82+
func setupPodman() error {
83+
if err := connectors.ConfigurePodmanHost(); err != nil {
84+
return err
85+
}
86+
// testcontainers-go silently falls back to Docker when the podman endpoint
87+
// isn't reachable, which would make "--driver podman" a lie. Verify the
88+
// connection now and fail loudly instead.
89+
if err := connectors.PingDockerHost(); err != nil {
90+
return fmt.Errorf("--driver podman selected but the podman endpoint is not reachable. "+
91+
"Start it with 'podman machine start' (macOS/Windows) or "+
92+
"'systemctl --user start podman.socket' (Linux). Underlying error: %w", err)
93+
}
94+
// Ryuk (Testcontainers' reaper) needs privileges rootless Podman doesn't
95+
// grant; our signal-driven Terminate already guarantees cleanup, so disable
96+
// it for the Podman path.
97+
return os.Setenv("TESTCONTAINERS_RYUK_DISABLED", "true")
98+
}
99+
50100
func validateDryRunOptions(opts dryRunOptions) error {
51101
if opts.artifact == "" {
52102
return fmt.Errorf("--artifact is required with --dry-run")
@@ -97,6 +147,12 @@ func runDryRunTest(opts dryRunOptions) bool {
97147
return false
98148
}
99149

150+
// Select the container runtime (docker default, podman wired via DOCKER_HOST).
151+
if err := configureDriver(opts.driver); err != nil {
152+
fmt.Println(err)
153+
return false
154+
}
155+
100156
// Ctrl+C / SIGTERM cancels the context so teardown still runs.
101157
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
102158
defer cancel()

pkg/connectors/container_client.go

Lines changed: 44 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@ import (
77
"os/exec"
88
"runtime"
99
"strings"
10+
"time"
1011

1112
"github.com/docker/docker/api/types/container"
1213
"github.com/docker/docker/api/types/image"
1314
"github.com/docker/docker/client"
1415
"github.com/docker/docker/pkg/jsonmessage"
1516
"github.com/docker/go-connections/nat"
16-
"github.com/microcks/microcks-cli/pkg/errors"
1717
"github.com/moby/term"
1818
)
1919

@@ -64,26 +64,52 @@ func NewDockerClient() (*containerClient, error) {
6464
return &containerClient{cli: cli}, nil
6565
}
6666

67-
func NewPodmanClient() (*containerClient, error) {
68-
osName := runtime.GOOS
69-
switch osName {
70-
case "windows":
71-
remoteSocket, err := exec.Command("podman", "machine", "inspect", "--format", "{{.ConnectionInfo.PodmanPipe.Path}}").Output()
72-
errors.CheckError(err)
73-
socketPath := strings.TrimSpace(string(remoteSocket))
74-
err = os.Setenv("DOCKER_HOST", "npipe:////"+strings.TrimPrefix(socketPath, "\\\\"))
75-
errors.CheckError(err)
67+
// PingDockerHost verifies the Docker-API endpoint at DOCKER_HOST actually
68+
// answers. The raw client honors DOCKER_HOST with no fallback, so this catches
69+
// an unreachable endpoint (e.g. a stopped Podman machine) before testcontainers-go
70+
// silently resolves to a different runtime.
71+
func PingDockerHost() error {
72+
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
73+
if err != nil {
74+
return err
75+
}
76+
defer cli.Close()
7677

78+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
79+
defer cancel()
80+
81+
_, err = cli.Ping(ctx)
82+
return err
83+
}
84+
85+
func ConfigurePodmanHost() error {
86+
switch runtime.GOOS {
87+
case "windows":
88+
out, err := exec.Command("podman", "machine", "inspect", "--format", "{{.ConnectionInfo.PodmanPipe.Path}}").Output()
89+
if err != nil {
90+
return fmt.Errorf("resolving podman pipe path (is the podman machine running?): %w", err)
91+
}
92+
socketPath := strings.TrimSpace(string(out))
93+
return os.Setenv("DOCKER_HOST", "npipe:////"+strings.TrimPrefix(socketPath, "\\\\"))
7794
case "darwin":
78-
remoteSocket, err := exec.Command("podman", "machine", "inspect", "--format", "{{.ConnectionInfo.PodmanSocket.Path}}").Output()
79-
errors.CheckError(err)
80-
err = os.Setenv("DOCKER_HOST", "unix://"+strings.TrimSpace(string(remoteSocket)))
81-
errors.CheckError(err)
95+
out, err := exec.Command("podman", "machine", "inspect", "--format", "{{.ConnectionInfo.PodmanSocket.Path}}").Output()
96+
if err != nil {
97+
return fmt.Errorf("resolving podman socket path (is the podman machine running?): %w", err)
98+
}
99+
return os.Setenv("DOCKER_HOST", "unix://"+strings.TrimSpace(string(out)))
82100
case "linux":
83-
remoteSocket, err := exec.Command("podman", "info", "--format", "{{.Host.RemoteSocket.Path}}").Output()
84-
errors.CheckError(err)
85-
err = os.Setenv("DOCKER_HOST", "unix://"+strings.TrimSpace(string(remoteSocket)))
86-
errors.CheckError(err)
101+
out, err := exec.Command("podman", "info", "--format", "{{.Host.RemoteSocket.Path}}").Output()
102+
if err != nil {
103+
return fmt.Errorf("resolving podman socket path: %w", err)
104+
}
105+
return os.Setenv("DOCKER_HOST", "unix://"+strings.TrimSpace(string(out)))
106+
}
107+
return nil
108+
}
109+
110+
func NewPodmanClient() (*containerClient, error) {
111+
if err := ConfigurePodmanHost(); err != nil {
112+
return nil, err
87113
}
88114

89115
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())

0 commit comments

Comments
 (0)