diff --git a/e2e/commands_test.go b/e2e/commands_test.go index ec235d308..69bc8763b 100644 --- a/e2e/commands_test.go +++ b/e2e/commands_test.go @@ -103,6 +103,56 @@ func TestRenderFormatters(t *testing.T) { }) } +func checkFileWarning(t *testing.T, goldenFile, composeData string) { + cmd, cleanup := dockerCli.createTestCmd() + defer cleanup() + + tmpDir := fs.NewDir(t, "app_input", + fs.WithFile(internal.ComposeFileName, composeData), + ) + defer tmpDir.Remove() + + cmd.Dir = tmpDir.Path() + cmd.Command = dockerCli.Command("app", "init", "app-test", + "--compose-file", tmpDir.Join(internal.ComposeFileName)) + stdErr := icmd.RunCmd(cmd).Assert(t, icmd.Success).Stderr() + golden.Assert(t, stdErr, goldenFile) +} + +func TestInitWarningEnvFiles(t *testing.T) { + testCases := []struct { + name string + golden string + compose string + }{ + { + name: "initWarningMultipleEnvFilesTest", + golden: "init-output-warning-single-envfile.golden", + compose: `version: "3.2" +services: + nginx: + image: nginx:latest + env_file: myenv1.env`, + }, + { + name: "initWarningMultipleEnvFilesTest", + golden: "init-output-warning-multiple-envfiles.golden", + compose: `version: "3.2" +services: + nginx: + image: nginx:latest + env_file: + - myenv1.env + - myenv2.env`, + }, + } + for _, test := range testCases { + t.Run(test.name, func(t *testing.T) { + checkFileWarning(t, test.golden, test.compose) + }) + } +} + func TestInit(t *testing.T) { cmd, cleanup := dockerCli.createTestCmd() defer cleanup() diff --git a/e2e/testdata/init-output-warning-multiple-envfiles.golden b/e2e/testdata/init-output-warning-multiple-envfiles.golden new file mode 100644 index 000000000..f3deff231 --- /dev/null +++ b/e2e/testdata/init-output-warning-multiple-envfiles.golden @@ -0,0 +1,2 @@ +nginx.env_file "myenv1.env" will not be copied into app-test.dockerapp. Please copy it manually and update the path accordingly in the compose file. +nginx.env_file "myenv2.env" will not be copied into app-test.dockerapp. Please copy it manually and update the path accordingly in the compose file. diff --git a/e2e/testdata/init-output-warning-single-envfile.golden b/e2e/testdata/init-output-warning-single-envfile.golden new file mode 100644 index 000000000..1a3cab519 --- /dev/null +++ b/e2e/testdata/init-output-warning-single-envfile.golden @@ -0,0 +1 @@ +nginx.env_file "myenv1.env" will not be copied into app-test.dockerapp. Please copy it manually and update the path accordingly in the compose file. diff --git a/internal/commands/init.go b/internal/commands/init.go index bcfa976eb..9a02f3ea3 100644 --- a/internal/commands/init.go +++ b/internal/commands/init.go @@ -21,7 +21,7 @@ func initCmd(dockerCli command.Cli) *cobra.Command { $ docker app init myapp --compose-file docker-compose.yml`, Args: cli.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - created, err := packager.Init(args[0], initComposeFile) + created, err := packager.Init(dockerCli.Err(), args[0], initComposeFile) if err != nil { return err } diff --git a/internal/packager/init.go b/internal/packager/init.go index 8a71a24ed..1f3f02bce 100644 --- a/internal/packager/init.go +++ b/internal/packager/init.go @@ -3,6 +3,7 @@ package packager import ( "bytes" "fmt" + "io" "io/ioutil" "os" "os/user" @@ -27,7 +28,7 @@ import ( // Init is the entrypoint initialization function. // It generates a new application definition based on the provided parameters // and returns the path to the created application definition. -func Init(name string, composeFile string) (string, error) { +func Init(errWriter io.Writer, name string, composeFile string) (string, error) { if err := internal.ValidateAppName(name); err != nil { return "", err } @@ -48,7 +49,7 @@ func Init(name string, composeFile string) (string, error) { if composeFile == "" { err = initFromScratch(name) } else { - err = initFromComposeFile(name, composeFile) + err = initFromComposeFile(errWriter, name, composeFile) } if err != nil { return "", err @@ -79,22 +80,52 @@ func checkComposeFileVersion(compose map[string]interface{}) error { return schema.Validate(compose, fmt.Sprintf("%v", version)) } -func initFromComposeFile(name string, composeFile string) error { - logrus.Debugf("Initializing from compose file %s", composeFile) - - dirName := internal.DirNameFromAppName(name) - - composeRaw, err := ioutil.ReadFile(composeFile) - if err != nil { - return errors.Wrap(err, "failed to read compose file") +func getEnvFiles(svcName string, envFileEntry interface{}) ([]string, error) { + var envFiles []string + switch envFileEntry.(type) { + case string: + envFiles = append(envFiles, envFileEntry.(string)) + case []interface{}: + for _, env := range envFileEntry.([]interface{}) { + envFiles = append(envFiles, env.(string)) + } + default: + return nil, fmt.Errorf("unknown entries in 'env_file' for service %s -> %v", + svcName, envFileEntry) } - cfgMap, err := composeloader.ParseYAML(composeRaw) - if err != nil { - return errors.Wrap(err, "failed to parse compose file") + return envFiles, nil +} + +func checkEnvFiles(errWriter io.Writer, appName string, cfgMap map[string]interface{}) error { + services := cfgMap["services"] + servicesMap, ok := services.(map[string]interface{}) + if !ok { + return fmt.Errorf("invalid Compose file") } - if err := checkComposeFileVersion(cfgMap); err != nil { - return err + for svcName, svc := range servicesMap { + svcContent, ok := svc.(map[string]interface{}) + if !ok { + return fmt.Errorf("invalid service %q", svcName) + } + envFileEntry, ok := svcContent["env_file"] + if !ok { + continue + } + envFiles, err := getEnvFiles(svcName, envFileEntry) + if err != nil { + return errors.Wrap(err, "invalid Compose file") + } + for _, envFilePath := range envFiles { + fmt.Fprintf(errWriter, + "%s.env_file %q will not be copied into %s.dockerapp. "+ + "Please copy it manually and update the path accordingly in the compose file.\n", + svcName, envFilePath, appName) + } } + return nil +} + +func getParamsFromDefaultEnvFile(composeFile string, composeRaw []byte) (map[string]string, bool, error) { params := make(map[string]string) envs, err := opts.ParseEnvFile(filepath.Join(filepath.Dir(composeFile), ".env")) if err == nil { @@ -107,7 +138,7 @@ func initFromComposeFile(name string, composeFile string) error { } vars, err := compose.ExtractVariables(composeRaw, compose.ExtrapolationPattern) if err != nil { - return errors.Wrap(err, "failed to parse compose file") + return nil, false, errors.Wrap(err, "failed to parse compose file") } needsFilling := false for k, v := range vars { @@ -120,6 +151,32 @@ func initFromComposeFile(name string, composeFile string) error { } } } + return params, needsFilling, nil +} + +func initFromComposeFile(errWriter io.Writer, name string, composeFile string) error { + logrus.Debugf("Initializing from compose file %s", composeFile) + + dirName := internal.DirNameFromAppName(name) + + composeRaw, err := ioutil.ReadFile(composeFile) + if err != nil { + return errors.Wrapf(err, "failed to read compose file %q", composeFile) + } + cfgMap, err := composeloader.ParseYAML(composeRaw) + if err != nil { + return errors.Wrap(err, "failed to parse compose file") + } + if err := checkComposeFileVersion(cfgMap); err != nil { + return err + } + if err := checkEnvFiles(errWriter, name, cfgMap); err != nil { + return err + } + params, needsFilling, err := getParamsFromDefaultEnvFile(composeFile, composeRaw) + if err != nil { + return err + } expandedParams, err := parameters.FromFlatten(params) if err != nil { return errors.Wrap(err, "failed to expand parameters") diff --git a/internal/packager/init_test.go b/internal/packager/init_test.go index 767670bb0..b809b3fff 100644 --- a/internal/packager/init_test.go +++ b/internal/packager/init_test.go @@ -32,7 +32,7 @@ services: ) defer dir.Remove() - err := initFromComposeFile(dir.Join(appName), inputDir.Join(internal.ComposeFileName)) + err := initFromComposeFile(nil, dir.Join(appName), inputDir.Join(internal.ComposeFileName)) assert.NilError(t, err) manifest := fs.Expected( @@ -73,7 +73,7 @@ services: ) defer dir.Remove() - err := initFromComposeFile(dir.Join(appName), inputDir.Join(internal.ComposeFileName)) + err := initFromComposeFile(nil, dir.Join(appName), inputDir.Join(internal.ComposeFileName)) assert.NilError(t, err) const expectedParameters = `ports: @@ -108,7 +108,7 @@ services: } func TestInitFromInvalidComposeFile(t *testing.T) { - err := initFromComposeFile("my.dockerapp", "doesnotexist") + err := initFromComposeFile(nil, "my.dockerapp", "doesnotexist") assert.ErrorContains(t, err, "failed to read") } @@ -131,7 +131,7 @@ services: ) defer dir.Remove() - err := initFromComposeFile(dir.Join(appName), inputDir.Join(internal.ComposeFileName)) + err := initFromComposeFile(nil, dir.Join(appName), inputDir.Join(internal.ComposeFileName)) assert.ErrorContains(t, err, "unsupported Compose file version") } @@ -151,7 +151,7 @@ nginx: ) defer dir.Remove() - err := initFromComposeFile(dir.Join(appName), inputDir.Join(internal.ComposeFileName)) + err := initFromComposeFile(nil, dir.Join(appName), inputDir.Join(internal.ComposeFileName)) assert.ErrorContains(t, err, "unsupported Compose file version") }