diff --git a/src/cmd/cli/command/commands.go b/src/cmd/cli/command/commands.go index 1f8999a1d..dd207bbf4 100644 --- a/src/cmd/cli/command/commands.go +++ b/src/cmd/cli/command/commands.go @@ -298,6 +298,8 @@ func SetupCommands(ctx context.Context, version string) { configCmd.AddCommand(configListCmd) + configCmd.AddCommand(configResolveCmd) + RootCmd.AddCommand(configCmd) RootCmd.AddCommand(setupComposeCommand()) @@ -742,9 +744,7 @@ func collectUnsetEnvVars(project *composeTypes.Project) []string { if project == nil { return nil // in case loading failed } - err := compose.ValidateProjectConfig(context.TODO(), project, func(ctx context.Context) ([]string, error) { - return nil, nil // assume no config - }) + err := compose.ValidateProjectConfig(context.TODO(), project, []string{}) var missingConfig compose.ErrMissingConfig if errors.As(err, &missingConfig) { return missingConfig @@ -970,6 +970,39 @@ var configListCmd = &cobra.Command{ }, } +var configResolveCmd = &cobra.Command{ + Use: "resolve", + Annotations: authNeededAnnotation, + Args: cobra.NoArgs, + Aliases: []string{"final"}, + Short: "Show the final resolved config for the project", + RunE: func(cmd *cobra.Command, args []string) error { + loader := configureLoader(cmd) + + provider, err := newProviderChecked(cmd.Context(), loader) + if err != nil { + return err + } + + project, err := loader.LoadProject(cmd.Context()) + if err != nil { + return err + } + + config, err := provider.ListConfig(cmd.Context(), &defangv1.ListConfigsRequest{Project: project.Name}) + if err != nil { + return err + } + + err = compose.ValidateProjectConfig(cmd.Context(), project, config.Names) + if err != nil { + return err + } + + return cli.PrintConfigResolutionSummary(project, config.Names) + }, +} + var debugCmd = &cobra.Command{ Use: "debug [SERVICE...]", Annotations: authNeededAnnotation, diff --git a/src/pkg/cli/compose/validation.go b/src/pkg/cli/compose/validation.go index 41c41b2f9..8f3adb011 100644 --- a/src/pkg/cli/compose/validation.go +++ b/src/pkg/cli/compose/validation.go @@ -435,7 +435,21 @@ func getResourceReservations(r composeTypes.Resources) *composeTypes.Resource { // Copied from shared/utils.ts but slightly modified to remove the negative-lookahead assertion var interpolationRegex = regexp.MustCompile(`(?i)\$(\$)|\$(?:{([^}]+)}|([_a-z][_a-z0-9]*))|([^$]+)`) // [1] escaped dollar, [2] curly braces, [3] variable name, [4] literal -func ValidateProjectConfig(ctx context.Context, composeProject *composeTypes.Project, listConfigNamesFunc ListConfigNamesFunc) error { +func DetectInterpolationVariables(value string) []string { + var names []string + // check for variables used during interpolation + for _, match := range interpolationRegex.FindAllStringSubmatch(value, -1) { + if match[2] != "" { + names = append(names, match[2]) + } + if match[3] != "" { + names = append(names, match[3]) + } + } + return names +} + +func ValidateProjectConfig(ctx context.Context, composeProject *composeTypes.Project, listConfigNames []string) error { var names []string // make list of secrets for _, service := range composeProject.Services { @@ -444,15 +458,8 @@ func ValidateProjectConfig(ctx context.Context, composeProject *composeTypes.Pro names = append(names, key) continue } - // check for variables used during interpolation - for _, match := range interpolationRegex.FindAllStringSubmatch(*value, -1) { - if match[2] != "" { - names = append(names, match[2]) - } - if match[3] != "" { - names = append(names, match[3]) - } - } + detectedNames := DetectInterpolationVariables(*value) + names = append(names, detectedNames...) } } @@ -460,18 +467,13 @@ func ValidateProjectConfig(ctx context.Context, composeProject *composeTypes.Pro return nil // no secrets to check } - configs, err := listConfigNamesFunc(ctx) - if err != nil { - return err - } - // Deduplicate (sort + uniq) slices.Sort(names) names = slices.Compact(names) errMissingConfig := ErrMissingConfig{} for _, name := range names { - if !slices.Contains(configs, name) { + if !slices.Contains(listConfigNames, name) { errMissingConfig = append(errMissingConfig, name) } } diff --git a/src/pkg/cli/compose/validation_test.go b/src/pkg/cli/compose/validation_test.go index d64b210b3..7ccecedba 100644 --- a/src/pkg/cli/compose/validation_test.go +++ b/src/pkg/cli/compose/validation_test.go @@ -56,7 +56,11 @@ func TestValidationAndConvert(t *testing.T) { logs.WriteString("Error: " + err.Error() + "\n") // no coverage! } - if err := ValidateProjectConfig(t.Context(), project, listConfigNamesFunc); err != nil { + listConfigNames, err := listConfigNamesFunc(t.Context()) + if err != nil { + t.Fatal(err) + } + if err := ValidateProjectConfig(t.Context(), project, listConfigNames); err != nil { t.Logf("Project config validation failed: %v", err) logs.WriteString("Error: " + err.Error() + "\n") } @@ -82,12 +86,6 @@ func TestValidationAndConvert(t *testing.T) { }) } -func makeListConfigNamesFunc(configs ...string) func(context.Context) ([]string, error) { - return func(context.Context) ([]string, error) { - return configs, nil - } -} - func TestValidateConfig(t *testing.T) { ctx := t.Context() @@ -101,7 +99,7 @@ func TestValidateConfig(t *testing.T) { } testProject.Services["service1"] = composeTypes.ServiceConfig{Environment: env} - if err := ValidateProjectConfig(ctx, &testProject, makeListConfigNamesFunc()); err != nil { + if err := ValidateProjectConfig(ctx, &testProject, []string{}); err != nil { t.Fatal(err) } }) @@ -116,7 +114,7 @@ func TestValidateConfig(t *testing.T) { testProject.Services["service1"] = composeTypes.ServiceConfig{Environment: env} var missing ErrMissingConfig - if err := ValidateProjectConfig(ctx, &testProject, makeListConfigNamesFunc()); !errors.As(err, &missing) { + if err := ValidateProjectConfig(ctx, &testProject, []string{}); !errors.As(err, &missing) { t.Fatalf("expected ErrMissingConfig, got: %v", err) } else { if len(missing) != 3 { @@ -139,7 +137,7 @@ func TestValidateConfig(t *testing.T) { } testProject.Services["service1"] = composeTypes.ServiceConfig{Environment: env} - if err := ValidateProjectConfig(ctx, &testProject, makeListConfigNamesFunc(CONFIG_VAR)); err != nil { + if err := ValidateProjectConfig(ctx, &testProject, []string{CONFIG_VAR}); err != nil { t.Fatal(err) } }) @@ -151,7 +149,7 @@ func TestValidateConfig(t *testing.T) { testProject.Services["service1"] = composeTypes.ServiceConfig{Environment: env} var missing ErrMissingConfig - if err := ValidateProjectConfig(ctx, &testProject, makeListConfigNamesFunc()); !errors.As(err, &missing) { + if err := ValidateProjectConfig(ctx, &testProject, []string{}); !errors.As(err, &missing) { t.Fatalf("expected ErrMissingConfig, got: %v", err) } else { if len(missing) != 1 { diff --git a/src/pkg/cli/composeUp.go b/src/pkg/cli/composeUp.go index 4a31c8a0d..271666da1 100644 --- a/src/pkg/cli/composeUp.go +++ b/src/pkg/cli/composeUp.go @@ -50,20 +50,22 @@ func ComposeUp(ctx context.Context, fabric client.FabricClient, provider cliClie // Validate the project configuration against the provider's configuration, but only if we are going to deploy. // FIXME: should not need to validate configs if we are doing preview, but preview will fail on missing configs. if upload != compose.UploadModeIgnore { - listConfigNamesFunc := func(ctx context.Context) ([]string, error) { + // Ignore missing configs in preview mode, because we don't want to fail the preview if some configs are missing. + if upload != compose.UploadModeEstimate { configs, err := provider.ListConfig(ctx, &defangv1.ListConfigsRequest{Project: project.Name}) if err != nil { - return nil, err + return nil, project, err } - return configs.Names, nil - } - - // Ignore missing configs in preview mode, because we don't want to fail the preview if some configs are missing. - if upload != compose.UploadModeEstimate { - if err := compose.ValidateProjectConfig(ctx, project, listConfigNamesFunc); err != nil { + if err := compose.ValidateProjectConfig(ctx, project, configs.Names); err != nil { return nil, project, &ComposeError{err} } + + // Print config resolution summary + err = PrintConfigResolutionSummary(project, configs.Names) + if err != nil { + return nil, project, err + } } } diff --git a/src/pkg/cli/configResolution.go b/src/pkg/cli/configResolution.go new file mode 100644 index 000000000..e89750f63 --- /dev/null +++ b/src/pkg/cli/configResolution.go @@ -0,0 +1,104 @@ +package cli + +import ( + "slices" + "sort" + + "github.com/DefangLabs/defang/src/pkg/cli/compose" + "github.com/DefangLabs/defang/src/pkg/term" + "github.com/compose-spec/compose-go/v2/types" +) + +type configOutput struct { + Service string `json:"service"` + Name string `json:"name"` + Value string `json:"value,omitempty"` + Source Source `json:"source,omitempty"` +} + +const configMaskedValue = "*****" + +type Source int + +const ( + SourceUnknown Source = iota + SourceComposeFile + SourceDefangConfig + SourceDefangAndComposeFile +) + +var sourceNames = map[Source]string{ + SourceUnknown: "unknown", + SourceComposeFile: "compose_file", + SourceDefangConfig: "defang_config", + SourceDefangAndComposeFile: "compose_file and defang_config", +} + +func (s Source) String() string { + if name, ok := sourceNames[s]; ok { + return name + } + return sourceNames[SourceUnknown] +} + +// determineConfigSource determines the source of an environment variable +// and returns the appropriate source type and value to display +func determineConfigSource(envKey string, envValue *string, defangConfigs map[string]struct{}) (Source, string) { + // If the key itself is a defang config, mask it + if _, isDefangConfig := defangConfigs[envKey]; isDefangConfig { + return SourceDefangConfig, configMaskedValue + } + + // If value is nil, it's from the compose file with empty value + if envValue == nil { + return SourceComposeFile, "" + } + + // Check if the value contains references to defang configs + interpolatedVariables := compose.DetectInterpolationVariables(*envValue) + if len(interpolatedVariables) > 0 { + for _, varName := range interpolatedVariables { + if _, isDefangConfig := defangConfigs[varName]; isDefangConfig { + return SourceDefangAndComposeFile, *envValue + } + } + } + + // Otherwise, it's from the compose file + return SourceComposeFile, *envValue +} + +func PrintConfigResolutionSummary(project *types.Project, defangConfig []string) error { + configset := make(map[string]struct{}) + for _, name := range defangConfig { + configset[name] = struct{}{} + } + + projectEnvVars := []configOutput{} + + for serviceName, service := range project.Services { + for envKey, envValue := range service.Environment { + source, value := determineConfigSource(envKey, envValue, configset) + projectEnvVars = append(projectEnvVars, configOutput{ + Service: serviceName, + Name: envKey, + Value: value, + Source: source, + }) + } + } + + // Sort by Service, then by Name within each service + sort.Slice(projectEnvVars, func(i, j int) bool { + if projectEnvVars[i].Service != projectEnvVars[j].Service { + return projectEnvVars[i].Service < projectEnvVars[j].Service + } + return projectEnvVars[i].Name < projectEnvVars[j].Name + }) + + projectEnvVars = slices.Compact(projectEnvVars) + + term.Println("\033[1mENVIRONMENT VARIABLES RESOLUTION SUMMARY:\033[0m") + + return term.Table(projectEnvVars, "Service", "Name", "Value", "Source") +} diff --git a/src/pkg/cli/configResolution_test.go b/src/pkg/cli/configResolution_test.go new file mode 100644 index 000000000..4ca7b3a0d --- /dev/null +++ b/src/pkg/cli/configResolution_test.go @@ -0,0 +1,71 @@ +package cli + +import ( + "os" + "path/filepath" + "regexp" + "testing" + + "github.com/DefangLabs/defang/src/pkg" + "github.com/DefangLabs/defang/src/pkg/cli/compose" + "github.com/DefangLabs/defang/src/pkg/term" +) + +func TestPrintConfigResolutionSummary(t *testing.T) { + testAllConfigResolutionFiles(t, func(t *testing.T, name, path string) { + stdout, _ := term.SetupTestTerm(t) + + loader := compose.NewLoader(compose.WithPath(path)) + proj, err := loader.LoadProject(t.Context()) + if err != nil { + t.Fatal(err) + } + + // Determine which config variables should be treated as defang configs based on the test case + var defangConfigs []string + switch name { + case "defang-config-only": + defangConfigs = []string{"SECRET_KEY", "API_TOKEN"} + case "mixed-sources": + defangConfigs = []string{"SECRET_KEY"} + case "interpolated-values": + defangConfigs = []string{"DB_USER", "DB_PASSWORD", "API_TOKEN"} + case "multiple-services": + defangConfigs = []string{"REDIS_PASSWORD", "DATABASE_URL"} + default: + defangConfigs = []string{} + } + + err = PrintConfigResolutionSummary(proj, defangConfigs) + if err != nil { + t.Fatalf("PrintConfigResolutionSummary() error = %v", err) + } + + output := stdout.Bytes() + + // Compare the output with the golden file + if err := pkg.Compare(output, path+".golden"); err != nil { + t.Error(err) + } + }) +} + +func testAllConfigResolutionFiles(t *testing.T, f func(t *testing.T, name, path string)) { + t.Helper() + + composeRegex := regexp.MustCompile(`^(?i)(docker-)?compose.ya?ml$`) + err := filepath.WalkDir("testdata/configresolution", func(path string, d os.DirEntry, err error) error { + if err != nil || d.IsDir() || !composeRegex.MatchString(d.Name()) { + return err + } + + t.Run(path, func(t *testing.T) { + t.Log(path) + f(t, filepath.Base(filepath.Dir(path)), path) + }) + return nil + }) + if err != nil { + t.Error(err) + } +} diff --git a/src/pkg/cli/testdata/configresolution/compose-file-only/compose.yaml b/src/pkg/cli/testdata/configresolution/compose-file-only/compose.yaml new file mode 100644 index 000000000..3e9fd9196 --- /dev/null +++ b/src/pkg/cli/testdata/configresolution/compose-file-only/compose.yaml @@ -0,0 +1,6 @@ +services: + service1: + image: nginx + environment: + ENV_VAR1: value1 + ENV_VAR2: value2 diff --git a/src/pkg/cli/testdata/configresolution/compose-file-only/compose.yaml.fixup b/src/pkg/cli/testdata/configresolution/compose-file-only/compose.yaml.fixup new file mode 100644 index 000000000..e3e9a2dfc --- /dev/null +++ b/src/pkg/cli/testdata/configresolution/compose-file-only/compose.yaml.fixup @@ -0,0 +1,14 @@ +{ + "service1": { + "command": null, + "entrypoint": null, + "environment": { + "ENV_VAR1": "value1", + "ENV_VAR2": "value2" + }, + "image": "nginx", + "networks": { + "default": null + } + } +} \ No newline at end of file diff --git a/src/pkg/cli/testdata/configresolution/compose-file-only/compose.yaml.golden b/src/pkg/cli/testdata/configresolution/compose-file-only/compose.yaml.golden new file mode 100644 index 000000000..235defe3b --- /dev/null +++ b/src/pkg/cli/testdata/configresolution/compose-file-only/compose.yaml.golden @@ -0,0 +1,5 @@ +ENVIRONMENT VARIABLES RESOLUTION SUMMARY: + +SERVICE NAME VALUE SOURCE +service1 ENV_VAR1 value1 compose_file +service1 ENV_VAR2 value2 compose_file diff --git a/src/pkg/cli/testdata/configresolution/defang-config-only/compose.yaml b/src/pkg/cli/testdata/configresolution/defang-config-only/compose.yaml new file mode 100644 index 000000000..6108ea444 --- /dev/null +++ b/src/pkg/cli/testdata/configresolution/defang-config-only/compose.yaml @@ -0,0 +1,6 @@ +services: + service1: + image: nginx + environment: + SECRET_KEY: ${SECRET_KEY} + API_TOKEN: ${API_TOKEN} diff --git a/src/pkg/cli/testdata/configresolution/defang-config-only/compose.yaml.fixup b/src/pkg/cli/testdata/configresolution/defang-config-only/compose.yaml.fixup new file mode 100644 index 000000000..5ce433a89 --- /dev/null +++ b/src/pkg/cli/testdata/configresolution/defang-config-only/compose.yaml.fixup @@ -0,0 +1,14 @@ +{ + "service1": { + "command": null, + "entrypoint": null, + "environment": { + "API_TOKEN": "${API_TOKEN}", + "SECRET_KEY": "${SECRET_KEY}" + }, + "image": "nginx", + "networks": { + "default": null + } + } +} \ No newline at end of file diff --git a/src/pkg/cli/testdata/configresolution/defang-config-only/compose.yaml.golden b/src/pkg/cli/testdata/configresolution/defang-config-only/compose.yaml.golden new file mode 100644 index 000000000..0a588ce0c --- /dev/null +++ b/src/pkg/cli/testdata/configresolution/defang-config-only/compose.yaml.golden @@ -0,0 +1,5 @@ +ENVIRONMENT VARIABLES RESOLUTION SUMMARY: + +SERVICE NAME VALUE SOURCE +service1 API_TOKEN ***** defang_config +service1 SECRET_KEY ***** defang_config diff --git a/src/pkg/cli/testdata/configresolution/empty-values/compose.yaml b/src/pkg/cli/testdata/configresolution/empty-values/compose.yaml new file mode 100644 index 000000000..2c0ee5946 --- /dev/null +++ b/src/pkg/cli/testdata/configresolution/empty-values/compose.yaml @@ -0,0 +1,6 @@ +services: + service1: + image: nginx + environment: + EMPTY_VAR: + WITH_VALUE: some_value diff --git a/src/pkg/cli/testdata/configresolution/empty-values/compose.yaml.fixup b/src/pkg/cli/testdata/configresolution/empty-values/compose.yaml.fixup new file mode 100644 index 000000000..00ac3ac8f --- /dev/null +++ b/src/pkg/cli/testdata/configresolution/empty-values/compose.yaml.fixup @@ -0,0 +1,14 @@ +{ + "service1": { + "command": null, + "entrypoint": null, + "environment": { + "EMPTY_VAR": null, + "WITH_VALUE": "some_value" + }, + "image": "nginx", + "networks": { + "default": null + } + } +} \ No newline at end of file diff --git a/src/pkg/cli/testdata/configresolution/empty-values/compose.yaml.golden b/src/pkg/cli/testdata/configresolution/empty-values/compose.yaml.golden new file mode 100644 index 000000000..bee640c2a --- /dev/null +++ b/src/pkg/cli/testdata/configresolution/empty-values/compose.yaml.golden @@ -0,0 +1,5 @@ +ENVIRONMENT VARIABLES RESOLUTION SUMMARY: + +SERVICE NAME VALUE SOURCE +service1 EMPTY_VAR compose_file +service1 WITH_VALUE some_value compose_file diff --git a/src/pkg/cli/testdata/configresolution/interpolated-values/compose.yaml b/src/pkg/cli/testdata/configresolution/interpolated-values/compose.yaml new file mode 100644 index 000000000..6e0fab81f --- /dev/null +++ b/src/pkg/cli/testdata/configresolution/interpolated-values/compose.yaml @@ -0,0 +1,7 @@ +services: + service1: + image: nginx + environment: + DATABASE_URL: postgres://${DB_USER}:${DB_PASSWORD}@localhost/mydb + API_ENDPOINT: https://api.example.com + AUTH_HEADER: Bearer ${API_TOKEN} diff --git a/src/pkg/cli/testdata/configresolution/interpolated-values/compose.yaml.fixup b/src/pkg/cli/testdata/configresolution/interpolated-values/compose.yaml.fixup new file mode 100644 index 000000000..1e175f86b --- /dev/null +++ b/src/pkg/cli/testdata/configresolution/interpolated-values/compose.yaml.fixup @@ -0,0 +1,15 @@ +{ + "service1": { + "command": null, + "entrypoint": null, + "environment": { + "API_ENDPOINT": "https://api.example.com", + "AUTH_HEADER": "Bearer ${API_TOKEN}", + "DATABASE_URL": "postgres://${DB_USER}:${DB_PASSWORD}@localhost/mydb" + }, + "image": "nginx", + "networks": { + "default": null + } + } +} \ No newline at end of file diff --git a/src/pkg/cli/testdata/configresolution/interpolated-values/compose.yaml.golden b/src/pkg/cli/testdata/configresolution/interpolated-values/compose.yaml.golden new file mode 100644 index 000000000..259d92414 --- /dev/null +++ b/src/pkg/cli/testdata/configresolution/interpolated-values/compose.yaml.golden @@ -0,0 +1,6 @@ +ENVIRONMENT VARIABLES RESOLUTION SUMMARY: + +SERVICE NAME VALUE SOURCE +service1 API_ENDPOINT https://api.example.com compose_file +service1 AUTH_HEADER Bearer ${API_TOKEN} compose_file and defang_config +service1 DATABASE_URL postgres://${DB_USER}:${DB_PASSWORD}@localhost/mydb compose_file and defang_config diff --git a/src/pkg/cli/testdata/configresolution/mixed-sources/compose.yaml b/src/pkg/cli/testdata/configresolution/mixed-sources/compose.yaml new file mode 100644 index 000000000..87288cf29 --- /dev/null +++ b/src/pkg/cli/testdata/configresolution/mixed-sources/compose.yaml @@ -0,0 +1,7 @@ +services: + service1: + image: nginx + environment: + PLAIN_VAR: plain_value + SECRET_KEY: ${SECRET_KEY} + PUBLIC_URL: https://example.com diff --git a/src/pkg/cli/testdata/configresolution/mixed-sources/compose.yaml.fixup b/src/pkg/cli/testdata/configresolution/mixed-sources/compose.yaml.fixup new file mode 100644 index 000000000..96943b8e6 --- /dev/null +++ b/src/pkg/cli/testdata/configresolution/mixed-sources/compose.yaml.fixup @@ -0,0 +1,15 @@ +{ + "service1": { + "command": null, + "entrypoint": null, + "environment": { + "PLAIN_VAR": "plain_value", + "PUBLIC_URL": "https://example.com", + "SECRET_KEY": "${SECRET_KEY}" + }, + "image": "nginx", + "networks": { + "default": null + } + } +} \ No newline at end of file diff --git a/src/pkg/cli/testdata/configresolution/mixed-sources/compose.yaml.golden b/src/pkg/cli/testdata/configresolution/mixed-sources/compose.yaml.golden new file mode 100644 index 000000000..dda3a5722 --- /dev/null +++ b/src/pkg/cli/testdata/configresolution/mixed-sources/compose.yaml.golden @@ -0,0 +1,6 @@ +ENVIRONMENT VARIABLES RESOLUTION SUMMARY: + +SERVICE NAME VALUE SOURCE +service1 PLAIN_VAR plain_value compose_file +service1 PUBLIC_URL https://example.com compose_file +service1 SECRET_KEY ***** defang_config diff --git a/src/pkg/cli/testdata/configresolution/multiple-services/compose.yaml b/src/pkg/cli/testdata/configresolution/multiple-services/compose.yaml new file mode 100644 index 000000000..2928bcc40 --- /dev/null +++ b/src/pkg/cli/testdata/configresolution/multiple-services/compose.yaml @@ -0,0 +1,12 @@ +services: + service2: + image: redis + environment: + REDIS_PORT: "6379" + REDIS_PASSWORD: ${REDIS_PASSWORD} + service1: + image: nginx + environment: + APP_ENV: production + DATABASE_URL: ${DATABASE_URL} + LOG_LEVEL: info diff --git a/src/pkg/cli/testdata/configresolution/multiple-services/compose.yaml.fixup b/src/pkg/cli/testdata/configresolution/multiple-services/compose.yaml.fixup new file mode 100644 index 000000000..147384972 --- /dev/null +++ b/src/pkg/cli/testdata/configresolution/multiple-services/compose.yaml.fixup @@ -0,0 +1,34 @@ +{ + "service1": { + "command": null, + "entrypoint": null, + "environment": { + "APP_ENV": "production", + "DATABASE_URL": "${DATABASE_URL}", + "LOG_LEVEL": "info" + }, + "image": "nginx", + "networks": { + "default": null + } + }, + "service2": { + "command": null, + "entrypoint": null, + "environment": { + "REDIS_PASSWORD": "${REDIS_PASSWORD}", + "REDIS_PORT": "6379" + }, + "image": "redis", + "networks": { + "default": null + }, + "ports": [ + { + "mode": "host", + "target": 6379, + "protocol": "tcp" + } + ] + } +} \ No newline at end of file diff --git a/src/pkg/cli/testdata/configresolution/multiple-services/compose.yaml.golden b/src/pkg/cli/testdata/configresolution/multiple-services/compose.yaml.golden new file mode 100644 index 000000000..cb46823da --- /dev/null +++ b/src/pkg/cli/testdata/configresolution/multiple-services/compose.yaml.golden @@ -0,0 +1,8 @@ +ENVIRONMENT VARIABLES RESOLUTION SUMMARY: + +SERVICE NAME VALUE SOURCE +service1 APP_ENV production compose_file +service1 DATABASE_URL ***** defang_config +service1 LOG_LEVEL info compose_file +service2 REDIS_PASSWORD ***** defang_config +service2 REDIS_PORT 6379 compose_file