diff --git a/pkg/container/container_podman.go b/pkg/container/container_podman.go index f504871e..0e497783 100644 --- a/pkg/container/container_podman.go +++ b/pkg/container/container_podman.go @@ -1,15 +1,40 @@ package container import ( + "bytes" "encoding/base64" "fmt" "os" "path/filepath" + "runtime" "strings" logger "github.com/sirupsen/logrus" ) +// checkRosettaEnabled verifies if Rosetta is enabled in Podman on macOS +// This is a non-blocking check that provides a hint to the user if Rosetta is not configured +func checkRosettaEnabled() { + // Rosetta is only relevant on Apple Silicon (arm64); skip on Intel Macs + if runtime.GOARCH != "arm64" { + return + } + + checkCmd := createCommand(PODMAN, "machine", "ssh", "ls /proc/sys/fs/binfmt_misc/") + var out bytes.Buffer + checkCmd.Stdout = &out + checkCmd.Stderr = nil + + if err := checkCmd.Run(); err != nil { + // Silently skip if we can't check + return + } + + if !strings.Contains(out.String(), "rosetta") { + logger.Warnf("Rosetta does not appear to be enabled in Podman. For better compatibility with x86_64 images on Apple Silicon, please configure Rosetta. See https://github.com/openshift/backplane-cli/blob/main/docs/macOS.md for setup instructions.") + } +} + type podmanLinux struct { fileMountDir string } @@ -82,6 +107,8 @@ func podmanRunConsoleContainer(containerName string, port string, consoleArgs [] } func (ce *podmanMac) RunConsoleContainer(containerName string, port string, consoleArgs []string, envVars []EnvVar) error { + // Check if Rosetta is enabled for better compatibility + checkRosettaEnabled() return podmanRunConsoleContainer(containerName, port, consoleArgs, envVars) } diff --git a/pkg/container/container_test.go b/pkg/container/container_test.go index 22f27e17..acaf9938 100644 --- a/pkg/container/container_test.go +++ b/pkg/container/container_test.go @@ -3,6 +3,7 @@ package container import ( "os" "os/exec" + "runtime" "strings" "testing" @@ -100,6 +101,79 @@ var _ = Describe("console container implementation", func() { }) }) + Context("when checking Rosetta on macOS Podman", func() { + It("should execute podman machine ssh command on arm64", func() { + if os.Getenv("GOARCH") == "arm64" || (os.Getenv("GOARCH") == "" && runtime.GOARCH == "arm64") { + capturedCommands = nil + checkRosettaEnabled() + Expect(len(capturedCommands)).To(Equal(1)) + command := capturedCommands[0] + Expect(command[0]).To(Equal(PODMAN)) + Expect(command[1]).To(Equal("machine")) + Expect(command[2]).To(Equal("ssh")) + Expect(strings.Join(command[3:], " ")).To(Equal("ls /proc/sys/fs/binfmt_misc/")) + } else { + Skip("Rosetta check only runs on arm64 architecture") + } + }) + It("should skip the check on non-arm64 architectures", func() { + if os.Getenv("GOARCH") != "arm64" && (os.Getenv("GOARCH") != "" || runtime.GOARCH != "arm64") { + capturedCommands = nil + checkRosettaEnabled() + Expect(len(capturedCommands)).To(Equal(0)) + } else { + Skip("This test only runs on non-arm64 architectures") + } + }) + }) + + Context("when running console container on macOS", func() { + ce := podmanMac{} + It("should check Rosetta before running the container on arm64", func() { + if os.Getenv("GOARCH") == "arm64" || (os.Getenv("GOARCH") == "" && runtime.GOARCH == "arm64") { + mockOcmInterface.EXPECT().GetPullSecret().Return(pullSecret, nil).AnyTimes() + capturedCommands = nil + args := []string{"arg1"} + envvars := []EnvVar{{Key: "testkey", Value: "testval"}} + err := ce.RunConsoleContainer("console", "8888", args, envvars) + Expect(err).To(BeNil()) + // Should have 2 commands: 1 for Rosetta check, 1 for running container + Expect(len(capturedCommands)).To(BeNumerically(">=", 2)) + // First command should be Rosetta check + rosettaCheckCmd := capturedCommands[0] + Expect(rosettaCheckCmd[0]).To(Equal(PODMAN)) + Expect(rosettaCheckCmd[1]).To(Equal("machine")) + Expect(rosettaCheckCmd[2]).To(Equal("ssh")) + // Last command should be the actual container run + runCmd := capturedCommands[len(capturedCommands)-1] + fullCommand := strings.Join(runCmd, " ") + Expect(fullCommand).To(ContainSubstring("arg1")) + Expect(fullCommand).To(ContainSubstring("--env")) + Expect(fullCommand).To(ContainSubstring("testkey=testval")) + } else { + Skip("Rosetta check only runs on arm64 architecture") + } + }) + It("should skip Rosetta check on non-arm64 architectures", func() { + if os.Getenv("GOARCH") != "arm64" && (os.Getenv("GOARCH") != "" || runtime.GOARCH != "arm64") { + mockOcmInterface.EXPECT().GetPullSecret().Return(pullSecret, nil).AnyTimes() + capturedCommands = nil + args := []string{"arg1"} + envvars := []EnvVar{{Key: "testkey", Value: "testval"}} + err := ce.RunConsoleContainer("console", "8888", args, envvars) + Expect(err).To(BeNil()) + // Should only have 1 command for running container (no Rosetta check) + Expect(len(capturedCommands)).To(Equal(1)) + fullCommand := strings.Join(capturedCommands[0], " ") + Expect(fullCommand).To(ContainSubstring("arg1")) + Expect(fullCommand).To(ContainSubstring("--env")) + Expect(fullCommand).To(ContainSubstring("testkey=testval")) + } else { + Skip("This test only runs on non-arm64 architectures") + } + }) + }) + Context("when running console container", func() { ce := podmanMac{} It("should pass argments and environment variable if specified", func() { @@ -109,8 +183,9 @@ var _ = Describe("console container implementation", func() { envvars := []EnvVar{{Key: "testkey", Value: "testval"}} err := ce.RunConsoleContainer("console", "8888", args, envvars) Expect(err).To(BeNil()) - Expect(len(capturedCommands)).To(Equal(1)) - fullCommand := strings.Join(capturedCommands[0], " ") + Expect(len(capturedCommands)).To(BeNumerically(">=", 1)) + // Find the run command (should be the last one) + fullCommand := strings.Join(capturedCommands[len(capturedCommands)-1], " ") // arg Expect(fullCommand).To(ContainSubstring("arg1")) // env var