| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394 |
- /*
- Copyright 2023 Docker Compose CLI authors
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
- http://www.apache.org/licenses/LICENSE-2.0
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- */
- package compose
- import (
- "bytes"
- "context"
- "fmt"
- "io"
- "os"
- "path/filepath"
- "strings"
- "testing"
- "github.com/compose-spec/compose-go/v2/types"
- "github.com/docker/cli/cli/streams"
- "github.com/docker/compose/v2/pkg/mocks"
- "github.com/stretchr/testify/require"
- "go.uber.org/mock/gomock"
- )
- func TestApplyPlatforms_InferFromRuntime(t *testing.T) {
- makeProject := func() *types.Project {
- return &types.Project{
- Services: types.Services{
- "test": {
- Name: "test",
- Image: "foo",
- Build: &types.BuildConfig{
- Context: ".",
- Platforms: []string{
- "linux/amd64",
- "linux/arm64",
- "alice/32",
- },
- },
- Platform: "alice/32",
- },
- },
- }
- }
- t.Run("SinglePlatform", func(t *testing.T) {
- project := makeProject()
- require.NoError(t, applyPlatforms(project, true))
- require.EqualValues(t, []string{"alice/32"}, project.Services["test"].Build.Platforms)
- })
- t.Run("MultiPlatform", func(t *testing.T) {
- project := makeProject()
- require.NoError(t, applyPlatforms(project, false))
- require.EqualValues(t, []string{"linux/amd64", "linux/arm64", "alice/32"},
- project.Services["test"].Build.Platforms)
- })
- }
- func TestApplyPlatforms_DockerDefaultPlatform(t *testing.T) {
- makeProject := func() *types.Project {
- return &types.Project{
- Environment: map[string]string{
- "DOCKER_DEFAULT_PLATFORM": "linux/amd64",
- },
- Services: types.Services{
- "test": {
- Name: "test",
- Image: "foo",
- Build: &types.BuildConfig{
- Context: ".",
- Platforms: []string{
- "linux/amd64",
- "linux/arm64",
- },
- },
- },
- },
- }
- }
- t.Run("SinglePlatform", func(t *testing.T) {
- project := makeProject()
- require.NoError(t, applyPlatforms(project, true))
- require.EqualValues(t, []string{"linux/amd64"}, project.Services["test"].Build.Platforms)
- })
- t.Run("MultiPlatform", func(t *testing.T) {
- project := makeProject()
- require.NoError(t, applyPlatforms(project, false))
- require.EqualValues(t, []string{"linux/amd64", "linux/arm64"},
- project.Services["test"].Build.Platforms)
- })
- }
- func TestApplyPlatforms_UnsupportedPlatform(t *testing.T) {
- makeProject := func() *types.Project {
- return &types.Project{
- Environment: map[string]string{
- "DOCKER_DEFAULT_PLATFORM": "commodore/64",
- },
- Services: types.Services{
- "test": {
- Name: "test",
- Image: "foo",
- Build: &types.BuildConfig{
- Context: ".",
- Platforms: []string{
- "linux/amd64",
- "linux/arm64",
- },
- },
- },
- },
- }
- }
- t.Run("SinglePlatform", func(t *testing.T) {
- project := makeProject()
- require.EqualError(t, applyPlatforms(project, true),
- `service "test" build.platforms does not support value set by DOCKER_DEFAULT_PLATFORM: commodore/64`)
- })
- t.Run("MultiPlatform", func(t *testing.T) {
- project := makeProject()
- require.EqualError(t, applyPlatforms(project, false),
- `service "test" build.platforms does not support value set by DOCKER_DEFAULT_PLATFORM: commodore/64`)
- })
- }
- func TestIsRemoteConfig(t *testing.T) {
- ctrl := gomock.NewController(t)
- defer ctrl.Finish()
- cli := mocks.NewMockCli(ctrl)
- tests := []struct {
- name string
- configPaths []string
- want bool
- }{
- {
- name: "empty config paths",
- configPaths: []string{},
- want: false,
- },
- {
- name: "local file",
- configPaths: []string{"docker-compose.yaml"},
- want: false,
- },
- {
- name: "OCI reference",
- configPaths: []string{"oci://registry.example.com/stack:latest"},
- want: true,
- },
- {
- name: "GIT reference",
- configPaths: []string{"git://github.com/user/repo.git"},
- want: true,
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- opts := buildOptions{
- ProjectOptions: &ProjectOptions{
- ConfigPaths: tt.configPaths,
- },
- }
- got := isRemoteConfig(cli, opts)
- require.Equal(t, tt.want, got)
- })
- }
- }
- func TestDisplayLocationRemoteStack(t *testing.T) {
- ctrl := gomock.NewController(t)
- defer ctrl.Finish()
- cli := mocks.NewMockCli(ctrl)
- buf := new(bytes.Buffer)
- cli.EXPECT().Out().Return(streams.NewOut(buf)).AnyTimes()
- project := &types.Project{
- Name: "test-project",
- WorkingDir: "/tmp/test",
- }
- options := buildOptions{
- ProjectOptions: &ProjectOptions{
- ConfigPaths: []string{"oci://registry.example.com/stack:latest"},
- },
- }
- displayLocationRemoteStack(cli, project, options)
- output := buf.String()
- require.Equal(t, output, fmt.Sprintf("Your compose stack %q is stored in %q\n", "oci://registry.example.com/stack:latest", "/tmp/test"))
- }
- func TestDisplayInterpolationVariables(t *testing.T) {
- ctrl := gomock.NewController(t)
- defer ctrl.Finish()
- // Create a temporary directory for the test
- tmpDir, err := os.MkdirTemp("", "compose-test")
- require.NoError(t, err)
- defer func() { _ = os.RemoveAll(tmpDir) }()
- // Create a temporary compose file
- composeContent := `
- services:
- app:
- image: nginx
- environment:
- - TEST_VAR=${TEST_VAR:?required} # required with default
- - API_KEY=${API_KEY:?} # required without default
- - DEBUG=${DEBUG:-true} # optional with default
- - UNSET_VAR # optional without default
- `
- composePath := filepath.Join(tmpDir, "docker-compose.yml")
- err = os.WriteFile(composePath, []byte(composeContent), 0o644)
- require.NoError(t, err)
- buf := new(bytes.Buffer)
- cli := mocks.NewMockCli(ctrl)
- cli.EXPECT().Out().Return(streams.NewOut(buf)).AnyTimes()
- // Create ProjectOptions with the temporary compose file
- projectOptions := &ProjectOptions{
- ConfigPaths: []string{composePath},
- }
- // Set up the context with necessary environment variables
- ctx := context.Background()
- _ = os.Setenv("TEST_VAR", "test-value")
- _ = os.Setenv("API_KEY", "123456")
- defer func() {
- _ = os.Unsetenv("TEST_VAR")
- _ = os.Unsetenv("API_KEY")
- }()
- // Extract variables from the model
- info, noVariables, err := extractInterpolationVariablesFromModel(ctx, cli, projectOptions, []string{})
- require.NoError(t, err)
- require.False(t, noVariables)
- // Display the variables
- displayInterpolationVariables(cli.Out(), info)
- // Expected output format with proper spacing
- expected := "\nFound the following variables in configuration:\n" +
- "VARIABLE VALUE SOURCE REQUIRED DEFAULT\n" +
- "API_KEY 123456 environment yes \n" +
- "DEBUG true compose file no true\n" +
- "TEST_VAR test-value environment yes \n"
- // Normalize spaces and newlines for comparison
- normalizeSpaces := func(s string) string {
- // Replace multiple spaces with a single space
- s = strings.Join(strings.Fields(strings.TrimSpace(s)), " ")
- return s
- }
- actualOutput := buf.String()
- // Compare normalized strings
- require.Equal(t,
- normalizeSpaces(expected),
- normalizeSpaces(actualOutput),
- "\nExpected:\n%s\nGot:\n%s", expected, actualOutput)
- }
- func TestConfirmRemoteIncludes(t *testing.T) {
- ctrl := gomock.NewController(t)
- defer ctrl.Finish()
- cli := mocks.NewMockCli(ctrl)
- tests := []struct {
- name string
- opts buildOptions
- assumeYes bool
- userInput string
- wantErr bool
- errMessage string
- wantPrompt bool
- wantOutput string
- }{
- {
- name: "no remote includes",
- opts: buildOptions{
- ProjectOptions: &ProjectOptions{
- ConfigPaths: []string{
- "docker-compose.yaml",
- "./local/path/compose.yaml",
- },
- },
- },
- assumeYes: false,
- wantErr: false,
- wantPrompt: false,
- },
- {
- name: "assume yes with remote includes",
- opts: buildOptions{
- ProjectOptions: &ProjectOptions{
- ConfigPaths: []string{
- "oci://registry.example.com/stack:latest",
- "git://github.com/user/repo.git",
- },
- },
- },
- assumeYes: true,
- wantErr: false,
- wantPrompt: false,
- },
- {
- name: "user confirms remote includes",
- opts: buildOptions{
- ProjectOptions: &ProjectOptions{
- ConfigPaths: []string{
- "oci://registry.example.com/stack:latest",
- "git://github.com/user/repo.git",
- },
- },
- },
- assumeYes: false,
- userInput: "y\n",
- wantErr: false,
- wantPrompt: true,
- wantOutput: "\nWarning: This Compose project includes files from remote sources:\n" +
- " - oci://registry.example.com/stack:latest\n" +
- " - git://github.com/user/repo.git\n" +
- "\nRemote includes could potentially be malicious. Make sure you trust the source.\n" +
- "Do you want to continue? [y/N]: ",
- },
- {
- name: "user rejects remote includes",
- opts: buildOptions{
- ProjectOptions: &ProjectOptions{
- ConfigPaths: []string{
- "oci://registry.example.com/stack:latest",
- },
- },
- },
- assumeYes: false,
- userInput: "n\n",
- wantErr: true,
- errMessage: "operation cancelled by user",
- wantPrompt: true,
- wantOutput: "\nWarning: This Compose project includes files from remote sources:\n" +
- " - oci://registry.example.com/stack:latest\n" +
- "\nRemote includes could potentially be malicious. Make sure you trust the source.\n" +
- "Do you want to continue? [y/N]: ",
- },
- }
- buf := new(bytes.Buffer)
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- cli.EXPECT().Out().Return(streams.NewOut(buf)).AnyTimes()
- if tt.wantPrompt {
- inbuf := io.NopCloser(bytes.NewBufferString(tt.userInput))
- cli.EXPECT().In().Return(streams.NewIn(inbuf)).AnyTimes()
- }
- err := confirmRemoteIncludes(cli, tt.opts, tt.assumeYes)
- if tt.wantErr {
- require.Error(t, err)
- require.Equal(t, tt.errMessage, err.Error())
- } else {
- require.NoError(t, err)
- }
- if tt.wantOutput != "" {
- require.Equal(t, tt.wantOutput, buf.String())
- }
- buf.Reset()
- })
- }
- }
|