diff --git a/pkg/compose/helper.go b/pkg/compose/helper.go index 1b8de3774..6e78ba104 100644 --- a/pkg/compose/helper.go +++ b/pkg/compose/helper.go @@ -22,6 +22,8 @@ import ( const ( ProjectLabel = "com.docker.compose.project" ServiceLabel = "com.docker.compose.service" + podmanCmd = "podman" + composeArg = "compose" ) func LoadDockerComposeProject( @@ -56,7 +58,8 @@ type ComposeHelper struct { } // NewComposeHelper creates a new ComposeHelper instance after detecting whether Docker -// Compose V1 or V2 is installed. It returns an error if neither is found. +// Compose V2, Podman Compose, or Docker Compose V1 is installed. It returns an error +// if none are found. func NewComposeHelper(dockerHelper *docker.DockerHelper) (*ComposeHelper, error) { dockerCmd := dockerHelper.DockerCommand if dockerCmd == "" { @@ -68,12 +71,17 @@ func NewComposeHelper(dockerHelper *docker.DockerHelper) (*ComposeHelper, error) return helper, nil } + if helper, err := tryPodmanCompose(); err == nil { + helper.Docker = dockerHelper + return helper, nil + } + if helper, err := tryDockerComposeV1(); err == nil { helper.Docker = dockerHelper return helper, nil } - return nil, fmt.Errorf("docker compose not installed") + return nil, fmt.Errorf("docker compose or podman compose not installed") } // tryDockerComposeV2 checks if Docker Compose V2 is available and returns a ComposeHelper if so. @@ -120,6 +128,44 @@ func tryDockerComposeV2(dockerCmd string) (*ComposeHelper, error) { return helper, nil } +func tryPodmanCompose() (*ComposeHelper, error) { + if _, err := exec.LookPath(podmanCmd); err != nil { + return nil, fmt.Errorf("podman not found in PATH") + } + + if exec.Command(podmanCmd, composeArg).Run() != nil { + return nil, fmt.Errorf("podman compose not available") + } + + cmd := exec.Command(podmanCmd, composeArg, "version", "--short") + out, stderr, err := runCmdCapture(cmd) + if len(stderr) > 0 { + log.Warnf("%s: %s", strings.TrimSpace(string(stderr)), strings.TrimSpace(string(out))) + } + if err != nil { + return nil, fmt.Errorf( + "failed to get podman compose version %s: %w", + strings.TrimSpace(string(stderr)), + err, + ) + } + + parsed, parseErr := parseVersion(strings.TrimSpace(string(out))) + if parseErr != nil { + return nil, fmt.Errorf( + "failed to parse podman compose version %q: %w", + strings.TrimSpace(string(out)), + parseErr, + ) + } + + return &ComposeHelper{ + Command: podmanCmd, + Version: parsed.String(), + Args: []string{composeArg}, + }, nil +} + func tryDockerComposeV1() (*ComposeHelper, error) { if _, err := exec.LookPath("docker-compose"); err != nil { return nil, fmt.Errorf("docker-compose not found in PATH") diff --git a/pkg/compose/helper_test.go b/pkg/compose/helper_test.go index bcbcb03e1..102dae17b 100644 --- a/pkg/compose/helper_test.go +++ b/pkg/compose/helper_test.go @@ -1,11 +1,19 @@ package compose import ( + "context" + "strings" "testing" "github.com/stretchr/testify/suite" ) +const ( + testPodmanCmd = "podman" + testComposeArg = "compose" + testPodmanVersion = "2.32.4" +) + type HelperTestSuite struct { suite.Suite } @@ -80,3 +88,79 @@ func (s *HelperTestSuite) TestParseVersionWithPodmanWarning() { s.NoError(err) s.Equal("5.1.0", v.String()) } + +func (s *HelperTestSuite) TestParseVersionPodmanCompose() { + tests := []struct { + name string + version string + want string + wantErr bool + }{ + { + name: "podman compose standard version", + version: testPodmanVersion, + want: testPodmanVersion, + }, + { + name: "podman compose with v prefix", + version: "v" + testPodmanVersion, + want: testPodmanVersion, + }, + { + name: "podman-compose python variant", + version: "1.0.6", + want: "1.0.6", + }, + { + name: "podman compose with trailing newline", + version: testPodmanVersion + "\n", + want: testPodmanVersion, + }, + { + name: "podman compose with external provider warning", + version: ">>>> Executing external compose provider." + + " Please see podman-compose(1) <<<<\n\n" + testPodmanVersion + "\n", + want: testPodmanVersion, + }, + } + + for _, tt := range tests { + s.Run(tt.name, func() { + got, err := parseVersion(tt.version) + if tt.wantErr { + s.Error(err) + } else { + s.NoError(err) + s.Equal(tt.want, got.String()) + } + }) + } +} + +func (s *HelperTestSuite) TestComposeHelperPodmanFields() { + helper := &ComposeHelper{ + Command: testPodmanCmd, + Version: testPodmanVersion, + Args: []string{testComposeArg}, + } + + s.Equal(testPodmanCmd, helper.Command) + s.Equal(testPodmanVersion, helper.Version) + s.Equal([]string{testComposeArg}, helper.Args) +} + +func (s *HelperTestSuite) TestComposeHelperBuildCmdPodman() { + helper := &ComposeHelper{ + Command: testPodmanCmd, + Version: testPodmanVersion, + Args: []string{testComposeArg}, + } + + cmd := helper.buildCmd(context.TODO(), "--project-name", "test", "up", "-d") + s.True(strings.HasSuffix(cmd.Path, testPodmanCmd)) + s.Contains(cmd.Args, testComposeArg) + s.Contains(cmd.Args, "--project-name") + s.Contains(cmd.Args, "test") + s.Contains(cmd.Args, "up") + s.Contains(cmd.Args, "-d") +}