Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions cmd/compose/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (
"github.com/compose-spec/compose-go/v2/template"
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/cli/cli/command"
"github.com/docker/compose/v2/internal/tracing"
ui "github.com/docker/compose/v2/pkg/progress"
"github.com/docker/compose/v2/pkg/prompt"
"github.com/docker/compose/v2/pkg/utils"
Expand Down Expand Up @@ -103,6 +104,11 @@ func checksForRemoteStack(ctx context.Context, dockerCli command.Cli, project *t
if !isRemoteConfig(dockerCli, options) {
return nil
}
if metrics, ok := ctx.Value(tracing.MetricsKey{}).(tracing.Metrics); ok && metrics.CountIncludesRemote > 0 {
if err := confirmRemoteIncludes(dockerCli, options, assumeYes); err != nil {
return err
}
}
displayLocationRemoteStack(dockerCli, project, options)
return promptForInterpolatedVariables(ctx, dockerCli, options.ProjectOptions, assumeYes, cmdEnvs)
}
Expand Down Expand Up @@ -245,3 +251,41 @@ func displayLocationRemoteStack(dockerCli command.Cli, project *types.Project, o
_, _ = fmt.Fprintf(dockerCli.Out(), "Your compose stack %q is stored in %q\n", mainComposeFile, project.WorkingDir)
}
}

func confirmRemoteIncludes(dockerCli command.Cli, options buildOptions, assumeYes bool) error {
if assumeYes {
return nil
}

var remoteIncludes []string
remoteLoaders := options.ProjectOptions.remoteLoaders(dockerCli)
for _, cf := range options.ProjectOptions.ConfigPaths {
for _, loader := range remoteLoaders {
if loader.Accept(cf) {
remoteIncludes = append(remoteIncludes, cf)
break
}
}
}

if len(remoteIncludes) == 0 {
return nil
}

_, _ = fmt.Fprintln(dockerCli.Out(), "\nWarning: This Compose project includes files from remote sources:")
for _, include := range remoteIncludes {
_, _ = fmt.Fprintf(dockerCli.Out(), " - %s\n", include)
}
_, _ = fmt.Fprintln(dockerCli.Out(), "\nRemote includes could potentially be malicious. Make sure you trust the source.")

msg := "Do you want to continue? [y/N]: "
confirmed, err := prompt.NewPrompt(dockerCli.In(), dockerCli.Out()).Confirm(msg, false)
if err != nil {
return err
}
if !confirmed {
return fmt.Errorf("operation cancelled by user")
}

return nil
}
112 changes: 112 additions & 0 deletions cmd/compose/options_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"bytes"
"context"
"fmt"
"io"
"os"
"path/filepath"
"strings"
Expand Down Expand Up @@ -280,3 +281,114 @@ services:
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()
})
}
}
2 changes: 1 addition & 1 deletion pkg/prompt/prompt.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,6 @@ type Pipe struct {
func (u Pipe) Confirm(message string, defaultValue bool) (bool, error) {
_, _ = fmt.Fprint(u.stdout, message)
var answer string
_, _ = fmt.Scanln(&answer)
_, _ = fmt.Fscanln(u.stdin, &answer)
return utils.StringToBool(answer), nil
}