diff --git a/cmd/compose/compose.go b/cmd/compose/compose.go index ce019e77d0..1338d2802a 100644 --- a/cmd/compose/compose.go +++ b/cmd/compose/compose.go @@ -138,17 +138,18 @@ func Adapt(fn Command) func(cmd *cobra.Command, args []string) error { } type ProjectOptions struct { - ProjectName string - Profiles []string - ConfigPaths []string - WorkDir string - ProjectDir string - EnvFiles []string - Compatibility bool - Progress string - Offline bool - All bool - insecureRegistries []string + ProjectName string + Profiles []string + ConfigPaths []string + WorkDir string + ProjectDir string + EnvFiles []string + Compatibility bool + Progress string + Offline bool + All bool + insecureRegistries []string + remoteLoadersOverride []loader.ResourceLoader } // ProjectFunc does stuff within a types.Project @@ -361,6 +362,9 @@ func (o *ProjectOptions) ToProject(ctx context.Context, dockerCli command.Cli, b } func (o *ProjectOptions) remoteLoaders(dockerCli command.Cli) []loader.ResourceLoader { + if o.remoteLoadersOverride != nil { + return o.remoteLoadersOverride + } if o.Offline { return nil } diff --git a/cmd/compose/config.go b/cmd/compose/config.go index 646ecd8180..cdf53f4b58 100644 --- a/cmd/compose/config.go +++ b/cmd/compose/config.go @@ -28,6 +28,7 @@ import ( "strings" "github.com/compose-spec/compose-go/v2/cli" + "github.com/compose-spec/compose-go/v2/loader" "github.com/compose-spec/compose-go/v2/template" "github.com/compose-spec/compose-go/v2/types" "github.com/docker/cli/cli/command" @@ -519,7 +520,7 @@ func runConfigImages(ctx context.Context, dockerCli command.Cli, opts configOpti func runVariables(ctx context.Context, dockerCli command.Cli, opts configOptions, services []string) error { opts.noInterpolate = true - model, err := opts.ToModel(ctx, dockerCli, services, cli.WithoutEnvironmentResolution) + model, err := opts.ToModel(ctx, dockerCli, services, cli.WithoutEnvironmentResolution, cli.WithLoadOptions(loader.WithSkipValidation)) if err != nil { return err } diff --git a/cmd/compose/options.go b/cmd/compose/options.go index 6454f6aed9..df2848a6bf 100644 --- a/cmd/compose/options.go +++ b/cmd/compose/options.go @@ -27,6 +27,7 @@ import ( "text/tabwriter" "github.com/compose-spec/compose-go/v2/cli" + "github.com/compose-spec/compose-go/v2/loader" "github.com/compose-spec/compose-go/v2/template" "github.com/compose-spec/compose-go/v2/types" "github.com/docker/cli/cli/command" @@ -165,7 +166,7 @@ func extractInterpolationVariablesFromModel(ctx context.Context, dockerCli comma ProjectOptions: projectOptions, } - model, err := opts.ToModel(ctx, dockerCli, nil, cli.WithoutEnvironmentResolution) + model, err := opts.ToModel(ctx, dockerCli, nil, cli.WithoutEnvironmentResolution, cli.WithLoadOptions(loader.WithSkipValidation)) if err != nil { return nil, false, err } diff --git a/cmd/compose/options_test.go b/cmd/compose/options_test.go index b681c22137..13a33d7994 100644 --- a/cmd/compose/options_test.go +++ b/cmd/compose/options_test.go @@ -271,6 +271,77 @@ services: "\nExpected:\n%s\nGot:\n%s", expected, actualOutput) } +func TestExtractInterpolationVariablesFromModelAllowsTemplatedPortFields(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + cli := mocks.NewMockCli(ctrl) + + dir := t.TempDir() + composePath := filepath.Join(dir, "compose.yaml") + assert.NilError(t, os.WriteFile(composePath, []byte(` +name: remote-defaults +services: + web: + image: nginx + ports: + - host_ip: "${LXKNS_ADDRESS:-127.0.0.1}" + published: "${LXKNS_PORT:-5010}" + target: 80 + protocol: tcp +`), 0o600)) + + projectOptions := &ProjectOptions{ + ConfigPaths: []string{composePath}, + ProjectDir: dir, + } + info, noVariables, err := extractInterpolationVariablesFromModel(t.Context(), cli, projectOptions, []string{}) + assert.NilError(t, err) + assert.Assert(t, noVariables == false) + + values := map[string]string{} + for _, variable := range info { + values[variable.name] = variable.defaultValue + } + assert.Equal(t, values["LXKNS_ADDRESS"], "127.0.0.1") + assert.Equal(t, values["LXKNS_PORT"], "5010") +} + +func TestRunVariablesAllowsTemplatedPortFields(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + dir := t.TempDir() + composePath := filepath.Join(dir, "compose.yaml") + assert.NilError(t, os.WriteFile(composePath, []byte(` +name: remote-defaults +services: + web: + image: nginx + ports: + - host_ip: "${LXKNS_ADDRESS:-127.0.0.1}" + published: "${LXKNS_PORT:-5010}" + target: 80 + protocol: tcp +`), 0o600)) + + buf := new(bytes.Buffer) + cli := mocks.NewMockCli(ctrl) + cli.EXPECT().Out().Return(streams.NewOut(buf)).AnyTimes() + + opts := configOptions{ + Format: "json", + ProjectOptions: &ProjectOptions{ + ConfigPaths: []string{composePath}, + ProjectDir: dir, + }, + } + assert.NilError(t, runVariables(t.Context(), cli, opts, nil)) + + output := buf.String() + assert.Assert(t, strings.Contains(output, `"LXKNS_ADDRESS"`), output) + assert.Assert(t, strings.Contains(output, `"LXKNS_PORT"`), output) +} + func TestConfirmRemoteIncludes(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() diff --git a/cmd/compose/up_test.go b/cmd/compose/up_test.go index f567e97ad4..e6e7fd2224 100644 --- a/cmd/compose/up_test.go +++ b/cmd/compose/up_test.go @@ -17,14 +17,43 @@ package compose import ( + "bytes" + "context" + "fmt" + "io" + "os" + "path/filepath" + "strings" "testing" + "github.com/compose-spec/compose-go/v2/loader" "github.com/compose-spec/compose-go/v2/types" + "github.com/docker/cli/cli/streams" + "go.uber.org/mock/gomock" "gotest.tools/v3/assert" "github.com/docker/compose/v5/pkg/api" + "github.com/docker/compose/v5/pkg/mocks" ) +type testRemoteLoader struct { + localPath string +} + +func (l testRemoteLoader) Accept(path string) bool { + return strings.HasPrefix(path, "test://") +} + +func (l testRemoteLoader) Load(context.Context, string) (string, error) { + return l.localPath, nil +} + +func (l testRemoteLoader) Dir(string) string { + return filepath.Dir(l.localPath) +} + +var _ loader.ResourceLoader = testRemoteLoader{} + func TestApplyScaleOpt(t *testing.T) { p := types.Project{ Services: types.Services{ @@ -89,3 +118,64 @@ func TestUpOptions_OnExit(t *testing.T) { }) } } + +func TestRunUpAllowsTemplatedPortFieldsInRemoteStackPrompt(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + dir := t.TempDir() + composePath := filepath.Join(dir, "compose.yaml") + assert.NilError(t, os.WriteFile(composePath, []byte(` +name: remote-defaults +services: + web: + image: nginx + ports: + - host_ip: "${LXKNS_ADDRESS:-127.0.0.1}" + published: "${LXKNS_PORT:-5010}" + target: 80 + protocol: tcp +`), 0o600)) + + in := io.NopCloser(bytes.NewBufferString("n\n")) + out := new(bytes.Buffer) + errOut := new(bytes.Buffer) + cli := mocks.NewMockCli(ctrl) + cli.EXPECT().In().Return(streams.NewIn(in)).AnyTimes() + cli.EXPECT().Out().Return(streams.NewOut(out)).AnyTimes() + cli.EXPECT().Err().Return(streams.NewOut(errOut)).AnyTimes() + + projectOptions := &ProjectOptions{ + ConfigPaths: []string{"test://remote/compose.yaml"}, + ProjectDir: dir, + remoteLoadersOverride: []loader.ResourceLoader{testRemoteLoader{localPath: composePath}}, + } + project := &types.Project{ + Name: "remote-defaults", + WorkingDir: dir, + Services: types.Services{ + "web": { + Name: "web", + Image: "nginx", + }, + }, + } + + err := runUp( + t.Context(), + cli, + &BackendOptions{}, + createOptions{}, + upOptions{}, + buildOptions{ProjectOptions: projectOptions}, + project, + nil, + ) + + assert.Error(t, err, "operation cancelled by user") + output := out.String() + assert.Assert(t, strings.Contains(output, `Your compose stack "test://remote/compose.yaml"`), output) + assert.Assert(t, strings.Contains(output, "LXKNS_ADDRESS"), output) + assert.Assert(t, strings.Contains(output, "LXKNS_PORT"), output) + assert.Assert(t, !strings.Contains(fmt.Sprint(err), "invalid ip address"), fmt.Sprint(err)) +}