-
Notifications
You must be signed in to change notification settings - Fork 76
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
Refactor Docker Init with ContainerRuntime
Interface
#1748
Conversation
airflow/runtimes/podman_runtime.go
Outdated
package runtimes | ||
|
||
type PodmanRuntime struct{} | ||
|
||
func (p PodmanRuntime) Initialize() error { | ||
return nil | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Coming in the next PR.
airflow/runtimes/docker.go
Outdated
func InitializeDocker(d DockerInitializer, timeoutSeconds int) error { | ||
// Initialize spinner. | ||
timeout := time.After(time.Duration(timeoutSeconds) * time.Second) | ||
ticker := time.NewTicker(time.Duration(tickNum) * time.Millisecond) | ||
s := spinner.New(spinnerCharSet, spinnerRefresh) | ||
s.Suffix = containerRuntimeInitMessage | ||
defer s.Stop() | ||
|
||
// Execute `docker ps` to check if Docker is running. | ||
_, err := d.CheckDockerCmd() | ||
|
||
// If we didn't get an error, Docker is running, so we can return. | ||
if err == nil { | ||
return nil | ||
} | ||
|
||
// If we got an error, Docker is not running, so we attempt to start it. | ||
_, err = d.OpenDockerCmd() | ||
if err != nil { | ||
return fmt.Errorf(dockerOpenNotice) | ||
} | ||
|
||
// Wait for Docker to start. | ||
s.Start() | ||
for { | ||
select { | ||
case <-timeout: | ||
return fmt.Errorf(timeoutErrMsg) | ||
case <-ticker.C: | ||
_, err := d.CheckDockerCmd() | ||
if err != nil { | ||
continue | ||
} | ||
return nil | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This function is a refactored, more testable version of the previous startDocker()
and waitForDocker()
functions that we've deleted above.
func (s *Suite) TestStartDocker() { | ||
s.Run("start docker success", func() { | ||
counter := 0 | ||
cmdExec = func(cmd string, stdout, stderr io.Writer, args ...string) error { | ||
switch cmd { | ||
case "open": | ||
return nil | ||
case "docker": | ||
if counter == 0 { | ||
counter++ | ||
return errExecMock | ||
} | ||
return nil | ||
default: | ||
return errExecMock | ||
} | ||
} | ||
|
||
err := startDocker() | ||
s.NoError(err) | ||
}) | ||
|
||
s.Run("start docker fail", func() { | ||
timeoutNum = 5 | ||
|
||
cmdExec = func(cmd string, stdout, stderr io.Writer, args ...string) error { | ||
switch cmd { | ||
case "open": | ||
return nil | ||
case "docker": | ||
return errExecMock | ||
default: | ||
return errExecMock | ||
} | ||
} | ||
err := startDocker() | ||
s.Contains(err.Error(), "timed out waiting for docker") | ||
}) | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Refactored and replaced below.
|
||
func startDocker() error { | ||
containerRuntime, err := GetContainerRuntimeBinary() | ||
if err != nil { | ||
return err | ||
} | ||
|
||
buf := new(bytes.Buffer) | ||
err = cmdExec(containerRuntime, buf, buf, "ps") | ||
if err != nil { | ||
// open docker | ||
fmt.Println("\nDocker is not running. Starting up the Docker engine…") | ||
err = cmdExec(OpenCmd, buf, os.Stderr, "-a", dockerCmd) | ||
if err != nil { | ||
return err | ||
} | ||
fmt.Println("\nIf you don't see Docker Desktop starting, exit this command and start it manually.") | ||
fmt.Println("If you don't have Docker Desktop installed, install it (https://www.docker.com/products/docker-desktop/) and try again.") | ||
fmt.Println("If you are using Colima or another Docker alternative, start the engine manually.") | ||
// poll for docker | ||
err = waitForDocker() | ||
if err != nil { | ||
return err | ||
} | ||
} | ||
return nil | ||
} | ||
|
||
func waitForDocker() error { | ||
containerRuntime, err := GetContainerRuntimeBinary() | ||
if err != nil { | ||
return err | ||
} | ||
|
||
buf := new(bytes.Buffer) | ||
timeout := time.After(time.Duration(timeoutNum) * time.Second) | ||
ticker := time.NewTicker(time.Duration(tickNum) * time.Millisecond) | ||
for { | ||
select { | ||
// Got a timeout! fail with a timeout error | ||
case <-timeout: | ||
return errors.New("timed out waiting for docker") | ||
// Got a tick, we should check if docker is up & running | ||
case <-ticker.C: | ||
buf.Reset() | ||
err := cmdExec(containerRuntime, buf, buf, "ps") | ||
if err != nil { | ||
continue | ||
} else { | ||
return nil | ||
} | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This block of code (startDocker()
) was previously called directly by the astro dev start
handler. It's now been moved and ultimately called from our new cobra hooks - EnsureRuntime
. This hook then ultimately calls the docker initialization code which now does this setup work. The refactored version is below.
// ContainerRuntime interface defines the methods that manage | ||
// the container runtime lifecycle. | ||
type ContainerRuntime interface { | ||
Initialize() error | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This interface will grow to include more lifecycle functions when we follow up with podman support in the next PR.
// Most astro dev sub-commands require the container runtime, | ||
// so we set that configuration in this persistent pre-run hook. | ||
// A few sub-commands don't require this, so they explicitly | ||
// clobber it with a no-op function. | ||
PersistentPreRunE: ConfigureContainerRuntime, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here we insert a new PersistentPreRunE
function to setup the runtime for all the sub commands. This is a quick function and doesn't actually run any initialization steps that take time. When needed, the value set from this function is referenced and we run any lifecycle hooks that interact with the runtime.
With this PR, we're only running this on the astro dev start
command, as the other commands explicitly clobber this hook. In the following PR for podman support, we'll utilize this much more.
The methods that are not yet using this functionality explicitly define their own PersistentPreRunE
function of DoNothing
. This was already happening previously with an inline function definiton. This is really just cleaning that up and defining all our hooks in a single place.
// ignore PersistentPreRunE of root command | ||
PersistentPreRunE: func(cmd *cobra.Command, args []string) error { | ||
return nil | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All of these blocks are have been replaced with the same function - DoNothing
.
package runtimes | ||
|
||
import ( | ||
"bytes" | ||
"os/exec" | ||
) | ||
|
||
// Command represents a command to be executed. | ||
type Command struct { | ||
Command string | ||
Args []string | ||
} | ||
|
||
// Execute runs the Podman command and returns the output. | ||
func (p *Command) Execute() (string, error) { | ||
cmd := exec.Command(p.Command, p.Args...) //nolint:gosec | ||
var out bytes.Buffer | ||
cmd.Stdout = &out | ||
cmd.Stderr = &out | ||
err := cmd.Run() | ||
return out.String(), err | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just a generic command execution structure we'll use for shelling out docker
, podman
, etc commands.
ec87aef
to
98016bf
Compare
98016bf
to
0ad0bbc
Compare
// check if docker is up for macOS | ||
containerRuntime, err := GetContainerRuntimeBinary() | ||
if err != nil { | ||
return err | ||
} | ||
if runtime.GOOS == "darwin" && containerRuntime == dockerCmd { | ||
err := startDocker() | ||
if err != nil { | ||
return err | ||
} | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This has been moved to a cobra PersistentPreRunE
function named ConfiguredContainerRuntime
and a PreRunE
function named EnsureRuntime
. These will be rolled out more broadly across the astro dev
commands in the next PR.
"github.com/astronomer/astro-cli/airflow/runtimes" | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just bumped all this new container runtime management stuff into a sub package runtimes
, so that's just getting updated below.
588de54
to
a5c314d
Compare
airflow/container_runtime_test.go
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is just being moved and expanded within the sub-package runtimes
.
a5c314d
to
da610b6
Compare
da610b6
to
5097786
Compare
func (s *AirflowSuite) TestNewAirflowInitCmd() { | ||
cmd := newAirflowInitCmd() | ||
func (s *AirflowSuite) TestNewAirflowDevRootCmd() { | ||
cmd := newDevRootCmd(nil, nil) | ||
s.Nil(cmd.PersistentPreRunE(new(cobra.Command), []string{})) | ||
} | ||
|
||
func (s *AirflowSuite) TestNewAirflowStartCmd() { | ||
cmd := newAirflowStartCmd(nil) | ||
func (s *AirflowSuite) TestNewAirflowInitCmd() { | ||
cmd := newAirflowInitCmd() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Weird looking change here, but I've:
- removed the test for
TestNewAirflowStartRootCmd
, thePersistentPreRunE
function has been moved to the parent command so this test fails and really isn't necessary any more. - added the test for the
TestNewAirflowDevRootCmd
since it now holds the sharedPersistentPreRunE
function. - left the test for
TestNewAirflowInitCmd
in place.
ContainerRuntime
Interface
ContainerRuntime
InterfaceContainerRuntime
Interface
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is excellent, I really like the new structure as a way of treating the runtime options equally. Left a couple of small comments.
|
||
"github.com/astronomer/astro-cli/config" | ||
"github.com/astronomer/astro-cli/pkg/fileutil" | ||
"github.com/astronomer/astro-cli/pkg/util" | ||
"github.com/pkg/errors" | ||
) | ||
|
||
var spinnerCharSet = spinner.CharSets[14] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we maybe want to match (or reuse? or change?) the charset in pkg/ansi/spinner.go
for consistency?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like we created a custom one for login, etc. I think standardizing would be good but I'm trying to keep my changes to a minimum and keep them highly correlated. I've started and stopped this PR a few times because I've gone too far down the refactor path. I do plan to keep going, and intend to standardize as much as possible as we go it'll just be in targeted chunks.
This particular spinner charset actually mimics the ones that docker-compose uses so it looks somewhat intentional at the moment since we're using it for the messages where we're dealing with containers and the runtimes, etc.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok fair enough. I appreciate the refactor considering it can feel like a bit of a never-ending rabbit hole.
var spinnerCharSet = spinner.CharSets[14] | ||
|
||
const ( | ||
docker = "docker" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need these verbatim constants? I'd think if the value changed we'd probably just rename the constant anyway.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What are you thinking? Change them back to dockerCmd
, etc?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could just use the literal string.
airflow/runtimes/docker.go
Outdated
// DockerInitializer is a struct that contains the functions needed to initialize Docker. | ||
// The concrete implementation that we use is DefaultDockerInitializer below. | ||
// When running the tests, we substitute the default implementation with a mock implementation. | ||
type DockerInitializer struct { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks like it should be an interface, what do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah yea good call, meant to do that. It's now an interface and I've updated the mock implementation and tests accordingly.
634a68e
to
43c8a93
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tested the PR changes locally by setting up local astro-project using docker and changes work as expected.
Thanks @schnie ! 🚀
Description
Functionally, this really doesn't change much of anything. It is really just refactoring the
startDocker()
command to a new place. We've defined a newContainerRuntime
interface that will grow a bit in the next PR to cover container runtime lifecycle management functions. This places the original, directly in-line code into this new lifecycle management structure. We then call these functions from cobra pre-run hooks on ourastro dev
commands.My original podman PR grew too large, so this is part 1 to get some of the basic structure in place for expansion, while not changing any behavior quite yet. A follow up PR will expand on the
ContainerRuntime
interface and add podman support.🎟 Issue(s)
Related to https://github.com/astronomer/astro/issues/24344
🧪 Functional Testing
astro dev
subcommands have been tested manually and confirmed to be working as expected. (we should automate this as one of our next steps)📸 Screenshots
📋 Checklist
make test
before taking out of draftmake lint
before taking out of draft