-
Notifications
You must be signed in to change notification settings - Fork 76
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
4 changed files
with
188 additions
and
143 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
package airflow | ||
|
||
import ( | ||
"os" | ||
"path/filepath" | ||
"strings" | ||
"sync" | ||
|
||
"github.com/astronomer/astro-cli/pkg/util" | ||
|
||
"github.com/astronomer/astro-cli/config" | ||
"github.com/astronomer/astro-cli/pkg/fileutil" | ||
"github.com/pkg/errors" | ||
) | ||
|
||
// 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 { | ||
// Split the $PATH variable into it's individual path, | ||
// 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() { | ||
binaryName += ".exe" | ||
} | ||
|
||
var wg sync.WaitGroup | ||
found := make(chan string, 1) | ||
|
||
// Search each individual path concurrently. | ||
for _, dir := range paths { | ||
wg.Add(1) | ||
go func(dir string) { | ||
defer wg.Done() | ||
binaryPath := filepath.Join(dir, binaryName) | ||
if exists := checker.Exists(binaryPath); exists { | ||
found <- binaryPath | ||
} | ||
}(dir) | ||
} | ||
|
||
// Wait for the concurrent checks to finish and close the channel. | ||
wg.Wait() | ||
close(found) | ||
|
||
// If we found the binary in one of the paths, return true. | ||
if _, ok := <-found; ok { | ||
return true | ||
} | ||
|
||
// Otherwise the binary was not found, return false. | ||
return false | ||
} | ||
|
||
// GetContainerRuntimeBinary will return the manually configured container runtime, | ||
// or search the $PATH for an acceptable runtime binary to use. This allows users | ||
// to use alternative container runtimes without needing to explicitly configure it. | ||
// Manual configuration should only be needed when both runtimes are installed and | ||
// need to override to use one or the other and not use the auto-detection. | ||
func GetContainerRuntimeBinary() (string, error) { | ||
// Supported container runtime binaries | ||
binaries := []string{dockerCmd, podmanCmd} | ||
|
||
// If the binary is manually configured to an acceptable runtime, return it directly. | ||
// If a manual configuration exists, but it's not an appropriate runtime, we'll still | ||
// search the $PATH for an acceptable one before completely bailing out. | ||
configuredBinary := config.CFG.DockerCommand.GetString() | ||
if util.Contains(binaries, configuredBinary) { | ||
return configuredBinary, nil | ||
} | ||
|
||
// Get the PATH environment variable. | ||
pathEnv := os.Getenv("PATH") | ||
for _, binary := range binaries { | ||
if found := FindBinary(pathEnv, binary, OSFileChecker{}); found { | ||
return binary, nil | ||
} | ||
} | ||
|
||
// If we made it here, no runtime was found, so we show a helpful error message | ||
// and halt the command execution. | ||
return "", errors.New("Failed to find a container runtime. " + | ||
"See the Astro CLI prerequisites for more information. " + | ||
"https://www.astronomer.io/docs/astro/cli/install-cli") | ||
} | ||
|
||
// isWindows is a utility function to determine if the CLI host machine | ||
// is running on Microsoft Windows OS. | ||
func isWindows() bool { | ||
return strings.Contains(strings.ToLower(os.Getenv("OS")), "windows") | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
package airflow | ||
|
||
// 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 *Suite) TestGetContainerRuntimeBinary() { | ||
tests := []struct { | ||
name string | ||
pathEnv string | ||
binary string | ||
mockFiles map[string]bool | ||
expected bool | ||
}{ | ||
{ | ||
name: "Find docker", | ||
pathEnv: "/usr/local/bin:/usr/bin:/bin", | ||
binary: "docker", | ||
mockFiles: map[string]bool{ | ||
"/usr/local/bin/docker": true, | ||
}, | ||
expected: true, | ||
}, | ||
{ | ||
name: "Find docker - doesn't exist", | ||
pathEnv: "/usr/local/bin:/usr/bin:/bin", | ||
binary: "docker", | ||
mockFiles: map[string]bool{}, | ||
expected: false, | ||
}, | ||
{ | ||
name: "Find podman", | ||
pathEnv: "/usr/local/bin:/usr/bin:/bin", | ||
binary: "podman", | ||
mockFiles: map[string]bool{ | ||
"/usr/local/bin/podman": true, | ||
}, | ||
expected: true, | ||
}, | ||
{ | ||
name: "Find podman - doesn't exist", | ||
pathEnv: "/usr/local/bin:/usr/bin:/bin", | ||
binary: "podman", | ||
mockFiles: map[string]bool{}, | ||
expected: false, | ||
}, | ||
{ | ||
name: "Binary not found", | ||
pathEnv: "/usr/local/bin:/usr/bin:/bin", | ||
binary: "notarealbinary", | ||
mockFiles: map[string]bool{ | ||
"/usr/local/bin/docker": true, | ||
"/usr/local/bin/podman": true, | ||
}, | ||
expected: false, | ||
}, | ||
} | ||
|
||
for _, tt := range tests { | ||
s.Run(tt.name, func() { | ||
mockChecker := MockFileChecker{existingFiles: tt.mockFiles} | ||
result := FindBinary(tt.pathEnv, tt.binary, mockChecker) | ||
s.Equal(tt.expected, result) | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters