Skip to content

Commit

Permalink
Adds Podman Container Runtime (#1750)
Browse files Browse the repository at this point in the history
Co-authored-by: Pritesh Arora <[email protected]>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Neel Dalsania <[email protected]>
  • Loading branch information
4 people committed Dec 13, 2024
1 parent 315d0bf commit d42a292
Show file tree
Hide file tree
Showing 29 changed files with 1,908 additions and 323 deletions.
9 changes: 9 additions & 0 deletions .mockery.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,12 @@ packages:
dir: pkg/azure/mocks
interfaces:
Azure:
github.com/astronomer/astro-cli/airflow/runtimes:
config:
dir: airflow/runtimes/mocks
outpkg: mocks
interfaces:
OSChecker:
ContainerRuntime:
PodmanEngine:
DockerEngine:
3 changes: 3 additions & 0 deletions airflow/docker_image.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@ func (d *DockerImage) Build(dockerfilePath, buildSecretString string, buildConfi
if err != nil {
return fmt.Errorf("reading dockerfile: %w", err)
}
if runtimes.IsPodman(containerRuntime) {
args = append(args, "--format", "docker")
}
if addPullFlag {
args = append(args, "--pull")
}
Expand Down
13 changes: 12 additions & 1 deletion airflow/runtimes/command_test.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
package runtimes

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
)

func (s *ContainerRuntimeSuite) TestCommandExecution() {
type ContainerRuntimeCommandSuite struct {
suite.Suite
}

func TestContainerRuntimeCommand(t *testing.T) {
suite.Run(t, new(ContainerRuntimeCommandSuite))
}

func (s *ContainerRuntimeCommandSuite) TestCommandExecution() {
s.Run("Command executes successfully", func() {
cmd := &Command{
Command: "echo",
Expand Down
39 changes: 15 additions & 24 deletions airflow/runtimes/container_runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import (
"github.com/briandowns/spinner"

"github.com/astronomer/astro-cli/config"
"github.com/astronomer/astro-cli/pkg/fileutil"
"github.com/astronomer/astro-cli/pkg/util"
"github.com/pkg/errors"
)
Expand All @@ -32,6 +31,9 @@ const (
// the container runtime lifecycle.
type ContainerRuntime interface {
Initialize() error
Configure() error
ConfigureOrKill() error
Kill() error
}

// GetContainerRuntime creates a new container runtime based on the runtime string
Expand All @@ -46,44 +48,26 @@ func GetContainerRuntime() (ContainerRuntime, error) {
// Return the appropriate container runtime based on the binary discovered.
switch containerRuntime {
case docker:
return DockerRuntime{}, nil
return CreateDockerRuntime(new(dockerEngine), new(osChecker)), nil
case podman:
return PodmanRuntime{}, nil
return CreatePodmanRuntime(new(podmanEngine), new(osChecker)), nil
default:
return nil, errors.New(containerRuntimeNotFoundErrMsg)
}
}

// FileChecker interface defines a method to check if a file exists.
// This is here mostly for testing purposes. This allows us to mock
// around actually checking for binaries on a live system as that
// would create inconsistencies across developer machines when
// working with the unit tests.
type FileChecker interface {
Exists(path string) bool
}

// OSFileChecker is a concrete implementation of FileChecker.
type OSFileChecker struct{}

// Exists checks if the file exists in the file system.
func (f OSFileChecker) Exists(path string) bool {
exists, _ := fileutil.Exists(path, nil)
return exists
}

// FindBinary searches for the specified binary name in the provided $PATH directories,
// using the provided FileChecker. It searches each specific path within the systems
// $PATH environment variable for the binary concurrently and returns a boolean result
// indicating if the binary was found or not.
func FindBinary(pathEnv, binaryName string, checker FileChecker) bool {
func FindBinary(pathEnv, binaryName string, checker FileChecker, osChecker OSChecker) bool {
// Split the $PATH variable into it's individual paths,
// using the OS specific path separator character.
paths := strings.Split(pathEnv, string(os.PathListSeparator))

// Although programs can be called without the .exe extension,
// we need to append it here when searching the file system.
if isWindows() {
if osChecker.IsWindows() {
binaryName += ".exe"
}

Expand Down Expand Up @@ -147,7 +131,7 @@ var GetContainerRuntimeBinary = func() (string, error) {
// Get the $PATH environment variable.
pathEnv := os.Getenv("PATH")
for _, binary := range binaries {
if found := FindBinary(pathEnv, binary, OSFileChecker{}); found {
if found := FindBinary(pathEnv, binary, CreateFileChecker(), CreateOSChecker()); found {
return binary, nil
}
}
Expand All @@ -158,3 +142,10 @@ var GetContainerRuntimeBinary = func() (string, error) {
"See the Astro CLI prerequisites for more information. " +
"https://www.astronomer.io/docs/astro/cli/install-cli")
}

// IsPodman is just a small helper to avoid exporting the podman constant,
// and used in other places that haven't been refactored to use the runtime package.
// This could probably be removed in the future.
func IsPodman(binaryName string) bool {
return binaryName == podman
}
37 changes: 8 additions & 29 deletions airflow/runtimes/container_runtime_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import (
"errors"
"testing"

"github.com/stretchr/testify/assert"
"github.com/astronomer/astro-cli/airflow/runtimes/mocks"

"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/assert"

"github.com/stretchr/testify/suite"
)
Expand All @@ -15,23 +15,13 @@ type ContainerRuntimeSuite struct {
suite.Suite
}

func TestConfig(t *testing.T) {
func TestContainerRuntime(t *testing.T) {
suite.Run(t, new(ContainerRuntimeSuite))
}

// Mock for GetContainerRuntimeBinary
type MockRuntimeChecker struct {
mock.Mock
}

func (m *MockRuntimeChecker) GetContainerRuntimeBinary() (string, error) {
args := m.Called()
return args.String(0), args.Error(1)
}

func (s *ContainerRuntimeSuite) TestGetContainerRuntime() {
s.Run("GetContainerRuntime_Docker", func() {
mockChecker := new(MockRuntimeChecker)
mockChecker := new(mocks.RuntimeChecker)
mockChecker.On("GetContainerRuntimeBinary").Return(docker, nil)

// Inject the mock and make sure we restore after the test.
Expand All @@ -47,7 +37,7 @@ func (s *ContainerRuntimeSuite) TestGetContainerRuntime() {
})

s.Run("GetContainerRuntime_Podman", func() {
mockChecker := new(MockRuntimeChecker)
mockChecker := new(mocks.RuntimeChecker)
mockChecker.On("GetContainerRuntimeBinary").Return(podman, nil)

// Inject the mock and make sure we restore after the test.
Expand All @@ -63,7 +53,7 @@ func (s *ContainerRuntimeSuite) TestGetContainerRuntime() {
})

s.Run("GetContainerRuntime_Error", func() {
mockChecker := new(MockRuntimeChecker)
mockChecker := new(mocks.RuntimeChecker)
mockChecker.On("GetContainerRuntimeBinary").Return("", errors.New(containerRuntimeNotFoundErrMsg))

// Inject the mock and make sure we restore after the test.
Expand All @@ -80,17 +70,6 @@ func (s *ContainerRuntimeSuite) TestGetContainerRuntime() {
})
}

// MockFileChecker is a mock implementation of FileChecker for tests.
type MockFileChecker struct {
existingFiles map[string]bool
}

// Exists is just a mock for os.Stat(). In our test implementation, we just check
// if the file exists in the list of mocked files for a given test.
func (m MockFileChecker) Exists(path string) bool {
return m.existingFiles[path]
}

// TestGetContainerRuntimeBinary runs a suite of tests against GetContainerRuntimeBinary,
// using the MockFileChecker defined above.
func (s *ContainerRuntimeSuite) TestGetContainerRuntimeBinary() {
Expand Down Expand Up @@ -163,8 +142,8 @@ func (s *ContainerRuntimeSuite) TestGetContainerRuntimeBinary() {

for _, tt := range tests {
s.Run(tt.name, func() {
mockChecker := MockFileChecker{existingFiles: tt.mockFiles}
result := FindBinary(tt.pathEnv, tt.binary, mockChecker)
mockChecker := mocks.FileChecker{ExistingFiles: tt.mockFiles}
result := FindBinary(tt.pathEnv, tt.binary, mockChecker, new(osChecker))
s.Equal(tt.expected, result)
})
}
Expand Down
84 changes: 0 additions & 84 deletions airflow/runtimes/docker.go

This file was deleted.

33 changes: 33 additions & 0 deletions airflow/runtimes/docker_engine.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package runtimes

const (
defaultTimeoutSeconds = 60
tickNum = 500
open = "open"
timeoutErrMsg = "timed out waiting for docker"
dockerOpenNotice = "We couldn't start the docker engine automatically. Please start it manually and try again."
)

// dockerEngine is the default implementation of DockerEngine.
type dockerEngine struct{}

func (d dockerEngine) IsRunning() (string, error) {
checkDockerCmd := Command{
Command: docker,
Args: []string{
"ps",
},
}
return checkDockerCmd.Execute()
}

func (d dockerEngine) Start() (string, error) {
openDockerCmd := Command{
Command: open,
Args: []string{
"-a",
docker,
},
}
return openDockerCmd.Execute()
}
Loading

0 comments on commit d42a292

Please sign in to comment.