diff --git a/cmd/airflow_hooks.go b/cmd/airflow_hooks.go index 9649e440e..b7feeb69e 100644 --- a/cmd/airflow_hooks.go +++ b/cmd/airflow_hooks.go @@ -6,8 +6,10 @@ import ( "path/filepath" "github.com/astronomer/astro-cli/airflow/runtimes" - "github.com/astronomer/astro-cli/cmd/utils" "github.com/astronomer/astro-cli/config" + "github.com/astronomer/astro-cli/pkg/ansi" + "github.com/astronomer/astro-cli/pkg/input" + "github.com/pkg/errors" "github.com/spf13/cobra" ) @@ -28,7 +30,7 @@ func ConfigureContainerRuntime(_ *cobra.Command, _ []string) error { // EnsureRuntime is a pre-run hook that ensures that the project directory exists // and starts the container runtime if necessary. func EnsureRuntime(cmd *cobra.Command, args []string) error { - if err := utils.EnsureProjectDir(cmd, args); err != nil { + if err := EnsureProjectDirOrInit(cmd, args); err != nil { return err } @@ -50,7 +52,7 @@ func EnsureRuntime(cmd *cobra.Command, args []string) error { // SetRuntimeIfExists is a pre-run hook that ensures the project directory exists // and sets the container runtime if its running, otherwise we bail with an error message. func SetRuntimeIfExists(cmd *cobra.Command, args []string) error { - if err := utils.EnsureProjectDir(cmd, args); err != nil { + if err := EnsureProjectDirOrInit(cmd, args); err != nil { return err } return containerRuntime.Configure() @@ -59,7 +61,7 @@ func SetRuntimeIfExists(cmd *cobra.Command, args []string) error { // KillPreRunHook sets the container runtime if its running, // otherwise we bail with an error message. func KillPreRunHook(cmd *cobra.Command, args []string) error { - if err := utils.EnsureProjectDir(cmd, args); err != nil { + if err := EnsureProjectDirOrInit(cmd, args); err != nil { return err } return containerRuntime.ConfigureOrKill() @@ -71,3 +73,26 @@ func KillPostRunHook(_ *cobra.Command, _ []string) error { // Kill the runtime. return containerRuntime.Kill() } + +// EnsureProjectDirOrInit checks if the current directory is an Astro project. +// If it is not, it prompts the user to initialize one. If the user confirms, +// it runs astro dev init in the current directory. +func EnsureProjectDirOrInit(cmd *cobra.Command, args []string) error { + isProjectDir, err := config.IsProjectDir(config.WorkingPath) + if err != nil { + return errors.Wrap(err, ansi.Red("failed to verify that your working directory is an Astro project.\nTry running astro dev init to turn your working directory into an Astro project")) + } + + if !isProjectDir { + confirmed, err := input.Confirm("This is not an Astro project directory. Would you like to initialize one here?") + if err != nil { + return errors.New(ansi.Red("this is not an Astro project directory.\nChange to another directory or run astro dev init to turn your working directory into an Astro project\n")) + } + if !confirmed { + return errors.New(ansi.Red("this is not an Astro project directory.\nChange to another directory or run astro dev init to turn your working directory into an Astro project\n")) + } + return airflowInit(newAirflowInitCmd(), []string{}) + } + + return nil +} diff --git a/cmd/airflow_hooks_test.go b/cmd/airflow_hooks_test.go new file mode 100644 index 000000000..0de3fb237 --- /dev/null +++ b/cmd/airflow_hooks_test.go @@ -0,0 +1,103 @@ +package cmd + +import ( + "os" + "testing" + + "github.com/astronomer/astro-cli/config" + testUtil "github.com/astronomer/astro-cli/pkg/testing" + + "github.com/spf13/cobra" + "github.com/stretchr/testify/suite" +) + +type AirflowHooksSuite struct { + suite.Suite + tempDir string +} + +func TestAirflowHooks(t *testing.T) { + suite.Run(t, new(AirflowHooksSuite)) +} + +func (s *AirflowHooksSuite) SetupTest() { + testUtil.InitTestConfig(testUtil.LocalPlatform) + dir, err := os.MkdirTemp("", "test_hooks_temp_dir_*") + if err != nil { + s.T().Fatalf("failed to create temp dir: %v", err) + } + s.tempDir = dir + config.WorkingPath = s.tempDir +} + +func (s *AirflowHooksSuite) SetupSubTest() { + testUtil.InitTestConfig(testUtil.LocalPlatform) + dir, err := os.MkdirTemp("", "test_hooks_temp_dir_*") + if err != nil { + s.T().Fatalf("failed to create temp dir: %v", err) + } + s.tempDir = dir + config.WorkingPath = s.tempDir +} + +func (s *AirflowHooksSuite) TearDownTest() { + os.RemoveAll(s.tempDir) +} + +func (s *AirflowHooksSuite) TearDownSubTest() { + os.RemoveAll(s.tempDir) +} + +var ( + _ suite.SetupSubTest = (*AirflowHooksSuite)(nil) + _ suite.TearDownSubTest = (*AirflowHooksSuite)(nil) +) + +func (s *AirflowHooksSuite) TestEnsureProjectDirOrInit_AlreadyProjectDir() { + s.Run("returns nil when already an Astro project directory", func() { + // Create .astro/config.yaml to mark this as a valid Astro project + astroDir := s.tempDir + "/.astro" + s.NoError(os.MkdirAll(astroDir, 0o755)) + configFile, err := os.Create(astroDir + "/config.yaml") + s.NoError(err) + configFile.Close() + + err = EnsureProjectDirOrInit(&cobra.Command{}, []string{}) + s.NoError(err) + }) +} + +func (s *AirflowHooksSuite) TestEnsureProjectDirOrInit_NotProjectDir_UserConfirms() { + s.Run("initializes project when user confirms", func() { + // tempDir is not an Astro project dir; user inputs "y" to confirm init + defer testUtil.MockUserInput(s.T(), "y\n")() + + err := EnsureProjectDirOrInit(&cobra.Command{}, []string{}) + s.NoError(err) + + // Verify the project was initialized (Dockerfile should exist) + _, statErr := os.Stat(s.tempDir + "/Dockerfile") + s.NoError(statErr, "Dockerfile should have been created by init") + }) +} + +func (s *AirflowHooksSuite) TestEnsureProjectDirOrInit_NotProjectDir_UserDeclines() { + s.Run("returns error when user declines initialization", func() { + // tempDir is not an Astro project dir; user inputs "n" to decline + defer testUtil.MockUserInput(s.T(), "n\n")() + + err := EnsureProjectDirOrInit(&cobra.Command{}, []string{}) + s.Error(err) + s.Contains(err.Error(), "this is not an Astro project directory") + }) +} + +func (s *AirflowHooksSuite) TestEnsureProjectDirOrInit_InvalidPath() { + s.Run("returns error when path is not resolvable", func() { + config.WorkingPath = "./\000x" + + err := EnsureProjectDirOrInit(&cobra.Command{}, []string{}) + s.Error(err) + s.Contains(err.Error(), "failed to verify that your working directory is an Astro project") + }) +}