Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds Podman Container Runtime #1750

Merged
merged 20 commits into from
Dec 11, 2024
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Comment on lines +34 to +36
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding 3 new lifecycle methods here. The airflow hooks call these methods depending on the situation.

}

// 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