introduce WithPrompt to configure compose backend to use a plugable UI component for user interaction

Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
This commit is contained in:
Nicolas De Loof
2025-10-22 14:26:46 +02:00
committed by Nicolas De loof
parent da5c57c29d
commit 9b4fcce034
46 changed files with 152 additions and 136 deletions

View File

@@ -21,7 +21,7 @@ import (
)
// alphaCommand groups all experimental subcommands
func alphaCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
func alphaCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose) *cobra.Command {
cmd := &cobra.Command{
Short: "Experimental commands",
Use: "alpha [COMMAND]",

View File

@@ -35,7 +35,7 @@ type attachOpts struct {
proxy bool
}
func attachCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
func attachCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose) *cobra.Command {
opts := attachOpts{
composeOptions: &composeOptions{
ProjectOptions: p,
@@ -63,7 +63,7 @@ func attachCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service
return runCmd
}
func runAttach(ctx context.Context, dockerCli command.Cli, backend api.Service, opts attachOpts) error {
func runAttach(ctx context.Context, dockerCli command.Cli, backend api.Compose, opts attachOpts) error {
projectName, err := opts.toProjectName(ctx, dockerCli)
if err != nil {
return err

View File

@@ -90,7 +90,7 @@ func (opts buildOptions) toAPIBuildOptions(services []string) (api.BuildOptions,
}, nil
}
func buildCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
func buildCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose) *cobra.Command {
opts := buildOptions{
ProjectOptions: p,
}
@@ -148,7 +148,7 @@ func buildCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
return cmd
}
func runBuild(ctx context.Context, dockerCli command.Cli, backend api.Service, opts buildOptions, services []string) error {
func runBuild(ctx context.Context, dockerCli command.Cli, backend api.Compose, opts buildOptions, services []string) error {
opts.All = true // do not drop resources as build may involve some dependencies by additional_contexts
project, _, err := opts.ToProject(ctx, dockerCli, nil, cli.WithResolvedPaths(true), cli.WithoutEnvironmentResolution)
if err != nil {

View File

@@ -39,7 +39,7 @@ type commitOptions struct {
index int
}
func commitCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
func commitCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose) *cobra.Command {
options := commitOptions{
ProjectOptions: p,
}
@@ -73,7 +73,7 @@ func commitCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service
return cmd
}
func runCommit(ctx context.Context, dockerCli command.Cli, backend api.Service, options commitOptions) error {
func runCommit(ctx context.Context, dockerCli command.Cli, backend api.Compose, options commitOptions) error {
projectName, err := options.toProjectName(ctx, dockerCli)
if err != nil {
return err

View File

@@ -52,7 +52,7 @@ func completeServiceNames(dockerCli command.Cli, p *ProjectOptions) validArgsFn
}
}
func completeProjectNames(backend api.Service) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
func completeProjectNames(backend api.Compose) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
list, err := backend.List(cmd.Context(), api.ListOptions{
All: true,

View File

@@ -416,7 +416,7 @@ func RunningAsStandalone() bool {
}
// RootCommand returns the compose command with its child commands
func RootCommand(dockerCli command.Cli, backend api.Service) *cobra.Command { //nolint:gocyclo
func RootCommand(dockerCli command.Cli, backend api.Compose) *cobra.Command { //nolint:gocyclo
// filter out useless commandConn.CloseWrite warning message that can occur
// when using a remote context that is unreachable: "commandConn.CloseWrite: commandconn: failed to wait: signal: killed"
// https://github.com/docker/cli/blob/e1f24d3c93df6752d3c27c8d61d18260f141310c/cli/connhelper/commandconn/commandconn.go#L203-L215

View File

@@ -38,7 +38,7 @@ type copyOptions struct {
copyUIDGID bool
}
func copyCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
func copyCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose) *cobra.Command {
opts := copyOptions{
ProjectOptions: p,
}
@@ -73,7 +73,7 @@ func copyCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
return copyCmd
}
func runCopy(ctx context.Context, dockerCli command.Cli, backend api.Service, opts copyOptions) error {
func runCopy(ctx context.Context, dockerCli command.Cli, backend api.Compose, opts copyOptions) error {
name, err := opts.toProjectName(ctx, dockerCli)
if err != nil {
return err

View File

@@ -51,7 +51,7 @@ type createOptions struct {
AssumeYes bool
}
func createCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
func createCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose) *cobra.Command {
opts := createOptions{}
buildOpts := buildOptions{
ProjectOptions: p,
@@ -95,7 +95,7 @@ func createCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service
return cmd
}
func runCreate(ctx context.Context, _ command.Cli, backend api.Service, createOpts createOptions, buildOpts buildOptions, project *types.Project, services []string) error {
func runCreate(ctx context.Context, _ command.Cli, backend api.Compose, createOpts createOptions, buildOpts buildOptions, project *types.Project, services []string) error {
if err := createOpts.Apply(project); err != nil {
return err
}

View File

@@ -40,7 +40,7 @@ type downOptions struct {
images string
}
func downCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
func downCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose) *cobra.Command {
opts := downOptions{
ProjectOptions: p,
}
@@ -77,7 +77,7 @@ func downCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
return downCmd
}
func runDown(ctx context.Context, dockerCli command.Cli, backend api.Service, opts downOptions, services []string) error {
func runDown(ctx context.Context, dockerCli command.Cli, backend api.Compose, opts downOptions, services []string) error {
project, name, err := opts.projectOrName(ctx, dockerCli, services...)
if err != nil {
return err

View File

@@ -34,7 +34,7 @@ type eventsOpts struct {
until string
}
func eventsCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
func eventsCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose) *cobra.Command {
opts := eventsOpts{
composeOptions: &composeOptions{
ProjectOptions: p,
@@ -55,7 +55,7 @@ func eventsCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service
return cmd
}
func runEvents(ctx context.Context, dockerCli command.Cli, backend api.Service, opts eventsOpts, services []string) error {
func runEvents(ctx context.Context, dockerCli command.Cli, backend api.Compose, opts eventsOpts, services []string) error {
name, err := opts.toProjectName(ctx, dockerCli)
if err != nil {
return err

View File

@@ -48,7 +48,7 @@ type execOpts struct {
interactive bool
}
func execCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
func execCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose) *cobra.Command {
opts := execOpts{
composeOptions: &composeOptions{
ProjectOptions: p,
@@ -100,7 +100,7 @@ func execCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
return runCmd
}
func runExec(ctx context.Context, dockerCli command.Cli, backend api.Service, opts execOpts) error {
func runExec(ctx context.Context, dockerCli command.Cli, backend api.Compose, opts execOpts) error {
projectName, err := opts.toProjectName(ctx, dockerCli)
if err != nil {
return err

View File

@@ -33,7 +33,7 @@ type exportOptions struct {
index int
}
func exportCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
func exportCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose) *cobra.Command {
options := exportOptions{
ProjectOptions: p,
}
@@ -58,7 +58,7 @@ func exportCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service
return cmd
}
func runExport(ctx context.Context, dockerCli command.Cli, backend api.Service, options exportOptions) error {
func runExport(ctx context.Context, dockerCli command.Cli, backend api.Compose, options exportOptions) error {
projectName, err := options.toProjectName(ctx, dockerCli)
if err != nil {
return err

View File

@@ -30,7 +30,7 @@ type generateOptions struct {
Format string
}
func generateCommand(p *ProjectOptions, backend api.Service) *cobra.Command {
func generateCommand(p *ProjectOptions, backend api.Compose) *cobra.Command {
opts := generateOptions{
ProjectOptions: p,
}
@@ -52,7 +52,7 @@ func generateCommand(p *ProjectOptions, backend api.Service) *cobra.Command {
return cmd
}
func runGenerate(ctx context.Context, backend api.Service, opts generateOptions, containers []string) error {
func runGenerate(ctx context.Context, backend api.Compose, opts generateOptions, containers []string) error {
_, _ = fmt.Fprintln(os.Stderr, "generate command is EXPERIMENTAL")
if len(containers) == 0 {
return fmt.Errorf("at least one container must be specified")

View File

@@ -41,7 +41,7 @@ type imageOptions struct {
Format string
}
func imagesCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
func imagesCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose) *cobra.Command {
opts := imageOptions{
ProjectOptions: p,
}
@@ -58,7 +58,7 @@ func imagesCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service
return imgCmd
}
func runImages(ctx context.Context, dockerCli command.Cli, backend api.Service, opts imageOptions, services []string) error {
func runImages(ctx context.Context, dockerCli command.Cli, backend api.Compose, opts imageOptions, services []string) error {
projectName, err := opts.toProjectName(ctx, dockerCli)
if err != nil {
return err

View File

@@ -33,7 +33,7 @@ type killOptions struct {
signal string
}
func killCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
func killCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose) *cobra.Command {
opts := killOptions{
ProjectOptions: p,
}
@@ -54,7 +54,7 @@ func killCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
return cmd
}
func runKill(ctx context.Context, dockerCli command.Cli, backend api.Service, opts killOptions, services []string) error {
func runKill(ctx context.Context, dockerCli command.Cli, backend api.Compose, opts killOptions, services []string) error {
project, name, err := opts.projectOrName(ctx, dockerCli, services...)
if err != nil {
return err

View File

@@ -38,7 +38,7 @@ type lsOptions struct {
Filter opts.FilterOpt
}
func listCommand(dockerCli command.Cli, backend api.Service) *cobra.Command {
func listCommand(dockerCli command.Cli, backend api.Compose) *cobra.Command {
lsOpts := lsOptions{Filter: opts.NewFilterOpt()}
lsCmd := &cobra.Command{
Use: "ls [OPTIONS]",
@@ -61,7 +61,7 @@ var acceptedListFilters = map[string]bool{
"name": true,
}
func runList(ctx context.Context, dockerCli command.Cli, backend api.Service, lsOpts lsOptions) error {
func runList(ctx context.Context, dockerCli command.Cli, backend api.Compose, lsOpts lsOptions) error {
filters := lsOpts.Filter.Value()
err := filters.Validate(acceptedListFilters)
if err != nil {

View File

@@ -40,7 +40,7 @@ type logsOptions struct {
timestamps bool
}
func logsCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
func logsCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose) *cobra.Command {
opts := logsOptions{
ProjectOptions: p,
}
@@ -70,7 +70,7 @@ func logsCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
return logsCmd
}
func runLogs(ctx context.Context, dockerCli command.Cli, backend api.Service, opts logsOptions, services []string) error {
func runLogs(ctx context.Context, dockerCli command.Cli, backend api.Compose, opts logsOptions, services []string) error {
project, name, err := opts.projectOrName(ctx, dockerCli, services...)
if err != nil {
return err

View File

@@ -30,9 +30,9 @@ 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/cmd/prompt"
"github.com/docker/compose/v2/internal/tracing"
ui "github.com/docker/compose/v2/pkg/progress"
"github.com/docker/compose/v2/pkg/prompt"
)
func applyPlatforms(project *types.Project, buildForSinglePlatform bool) error {

View File

@@ -29,7 +29,7 @@ type pauseOptions struct {
*ProjectOptions
}
func pauseCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
func pauseCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose) *cobra.Command {
opts := pauseOptions{
ProjectOptions: p,
}
@@ -44,7 +44,7 @@ func pauseCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
return cmd
}
func runPause(ctx context.Context, dockerCli command.Cli, backend api.Service, opts pauseOptions, services []string) error {
func runPause(ctx context.Context, dockerCli command.Cli, backend api.Compose, opts pauseOptions, services []string) error {
project, name, err := opts.projectOrName(ctx, dockerCli, services...)
if err != nil {
return err
@@ -60,7 +60,7 @@ type unpauseOptions struct {
*ProjectOptions
}
func unpauseCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
func unpauseCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose) *cobra.Command {
opts := unpauseOptions{
ProjectOptions: p,
}
@@ -75,7 +75,7 @@ func unpauseCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Servic
return cmd
}
func runUnPause(ctx context.Context, dockerCli command.Cli, backend api.Service, opts unpauseOptions, services []string) error {
func runUnPause(ctx context.Context, dockerCli command.Cli, backend api.Compose, opts unpauseOptions, services []string) error {
project, name, err := opts.projectOrName(ctx, dockerCli, services...)
if err != nil {
return err

View File

@@ -35,7 +35,7 @@ type portOptions struct {
index int
}
func portCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
func portCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose) *cobra.Command {
opts := portOptions{
ProjectOptions: p,
}
@@ -62,7 +62,7 @@ func portCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
return cmd
}
func runPort(ctx context.Context, dockerCli command.Cli, backend api.Service, opts portOptions, service string) error {
func runPort(ctx context.Context, dockerCli command.Cli, backend api.Compose, opts portOptions, service string) error {
projectName, err := opts.toProjectName(ctx, dockerCli)
if err != nil {
return err

View File

@@ -64,7 +64,7 @@ func (p *psOptions) parseFilter() error {
return nil
}
func psCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
func psCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose) *cobra.Command {
opts := psOptions{
ProjectOptions: p,
}
@@ -91,7 +91,7 @@ func psCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *c
return psCmd
}
func runPs(ctx context.Context, dockerCli command.Cli, backend api.Service, services []string, opts psOptions) error {
func runPs(ctx context.Context, dockerCli command.Cli, backend api.Compose, services []string, opts psOptions) error {
project, name, err := opts.projectOrName(ctx, dockerCli, services...)
if err != nil {
return err

View File

@@ -37,7 +37,7 @@ type publishOptions struct {
app bool
}
func publishCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
func publishCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose) *cobra.Command {
opts := publishOptions{
ProjectOptions: p,
}
@@ -67,7 +67,7 @@ func publishCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Servic
return cmd
}
func runPublish(ctx context.Context, dockerCli command.Cli, backend api.Service, opts publishOptions, repository string) error {
func runPublish(ctx context.Context, dockerCli command.Cli, backend api.Compose, opts publishOptions, repository string) error {
project, metrics, err := opts.ToProject(ctx, dockerCli, nil)
if err != nil {
return err

View File

@@ -42,7 +42,7 @@ type pullOptions struct {
policy string
}
func pullCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
func pullCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose) *cobra.Command {
opts := pullOptions{
ProjectOptions: p,
}
@@ -97,7 +97,7 @@ func (opts pullOptions) apply(project *types.Project, services []string) (*types
return project, nil
}
func runPull(ctx context.Context, dockerCli command.Cli, backend api.Service, opts pullOptions, services []string) error {
func runPull(ctx context.Context, dockerCli command.Cli, backend api.Compose, opts pullOptions, services []string) error {
project, _, err := opts.ToProject(ctx, dockerCli, services, cli.WithoutEnvironmentResolution)
if err != nil {
return err

View File

@@ -34,7 +34,7 @@ type pushOptions struct {
Quiet bool
}
func pushCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
func pushCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose) *cobra.Command {
opts := pushOptions{
ProjectOptions: p,
}
@@ -53,7 +53,7 @@ func pushCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
return pushCmd
}
func runPush(ctx context.Context, dockerCli command.Cli, backend api.Service, opts pushOptions, services []string) error {
func runPush(ctx context.Context, dockerCli command.Cli, backend api.Compose, opts pushOptions, services []string) error {
project, _, err := opts.ToProject(ctx, dockerCli, services)
if err != nil {
return err

View File

@@ -31,7 +31,7 @@ type removeOptions struct {
volumes bool
}
func removeCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
func removeCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose) *cobra.Command {
opts := removeOptions{
ProjectOptions: p,
}
@@ -59,7 +59,7 @@ Any data which is not in a volume will be lost.`,
return cmd
}
func runRemove(ctx context.Context, dockerCli command.Cli, backend api.Service, opts removeOptions, services []string) error {
func runRemove(ctx context.Context, dockerCli command.Cli, backend api.Compose, opts removeOptions, services []string) error {
project, name, err := opts.projectOrName(ctx, dockerCli, services...)
if err != nil {
return err

View File

@@ -33,7 +33,7 @@ type restartOptions struct {
noDeps bool
}
func restartCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
func restartCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose) *cobra.Command {
opts := restartOptions{
ProjectOptions: p,
}
@@ -55,7 +55,7 @@ func restartCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Servic
return restartCmd
}
func runRestart(ctx context.Context, dockerCli command.Cli, backend api.Service, opts restartOptions, services []string) error {
func runRestart(ctx context.Context, dockerCli command.Cli, backend api.Compose, opts restartOptions, services []string) error {
project, name, err := opts.projectOrName(ctx, dockerCli)
if err != nil {
return err

View File

@@ -142,7 +142,7 @@ func (options runOptions) getEnvironment(resolve func(string) (string, bool)) (t
return environment, nil
}
func runCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
func runCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose) *cobra.Command {
options := runOptions{
composeOptions: &composeOptions{
ProjectOptions: p,
@@ -266,7 +266,7 @@ func normalizeRunFlags(f *pflag.FlagSet, name string) pflag.NormalizedName {
return pflag.NormalizedName(name)
}
func runRun(ctx context.Context, backend api.Service, project *types.Project, options runOptions, createOpts createOptions, buildOpts buildOptions, dockerCli command.Cli) error {
func runRun(ctx context.Context, backend api.Compose, project *types.Project, options runOptions, createOpts createOptions, buildOpts buildOptions, dockerCli command.Cli) error {
project, err := options.apply(project)
if err != nil {
return err

View File

@@ -35,7 +35,7 @@ type scaleOptions struct {
noDeps bool
}
func scaleCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
func scaleCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose) *cobra.Command {
opts := scaleOptions{
ProjectOptions: p,
}
@@ -58,7 +58,7 @@ func scaleCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
return scaleCmd
}
func runScale(ctx context.Context, dockerCli command.Cli, backend api.Service, opts scaleOptions, serviceReplicaTuples map[string]int) error {
func runScale(ctx context.Context, dockerCli command.Cli, backend api.Compose, opts scaleOptions, serviceReplicaTuples map[string]int) error {
services := slices.Sorted(maps.Keys(serviceReplicaTuples))
project, _, err := opts.ToProject(ctx, dockerCli, services)
if err != nil {

View File

@@ -28,7 +28,7 @@ type startOptions struct {
*ProjectOptions
}
func startCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
func startCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose) *cobra.Command {
opts := startOptions{
ProjectOptions: p,
}
@@ -43,7 +43,7 @@ func startCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
return startCmd
}
func runStart(ctx context.Context, dockerCli command.Cli, backend api.Service, opts startOptions, services []string) error {
func runStart(ctx context.Context, dockerCli command.Cli, backend api.Compose, opts startOptions, services []string) error {
project, name, err := opts.projectOrName(ctx, dockerCli, services...)
if err != nil {
return err

View File

@@ -32,7 +32,7 @@ type stopOptions struct {
timeout int
}
func stopCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
func stopCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose) *cobra.Command {
opts := stopOptions{
ProjectOptions: p,
}
@@ -53,7 +53,7 @@ func stopCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
return cmd
}
func runStop(ctx context.Context, dockerCli command.Cli, backend api.Service, opts stopOptions, services []string) error {
func runStop(ctx context.Context, dockerCli command.Cli, backend api.Compose, opts stopOptions, services []string) error {
project, name, err := opts.projectOrName(ctx, dockerCli, services...)
if err != nil {
return err

View File

@@ -34,7 +34,7 @@ type topOptions struct {
*ProjectOptions
}
func topCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
func topCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose) *cobra.Command {
opts := topOptions{
ProjectOptions: p,
}
@@ -54,7 +54,7 @@ type (
topEntries map[string]string
)
func runTop(ctx context.Context, dockerCli command.Cli, backend api.Service, opts topOptions, services []string) error {
func runTop(ctx context.Context, dockerCli command.Cli, backend api.Compose, opts topOptions, services []string) error {
projectName, err := opts.toProjectName(ctx, dockerCli)
if err != nil {
return err

View File

@@ -202,7 +202,7 @@ var topTestCases = []struct {
}
// TestRunTopCore only tests the core functionality of runTop: formatting
// and printing of the output of (api.Service).Top().
// and printing of the output of (api.Compose).Top().
func TestRunTopCore(t *testing.T) {
t.Parallel()

View File

@@ -109,7 +109,7 @@ func (opts upOptions) OnExit() api.Cascade {
}
}
func upCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
func upCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose) *cobra.Command {
up := upOptions{}
create := createOptions{}
build := buildOptions{ProjectOptions: p}
@@ -228,7 +228,7 @@ func validateFlags(up *upOptions, create *createOptions) error {
func runUp(
ctx context.Context,
dockerCli command.Cli,
backend api.Service,
backend api.Compose,
createOptions createOptions,
upOptions upOptions,
buildOptions buildOptions,

View File

@@ -35,7 +35,7 @@ type vizOptions struct {
indentationStr string
}
func vizCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
func vizCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose) *cobra.Command {
opts := vizOptions{
ProjectOptions: p,
}
@@ -63,7 +63,7 @@ func vizCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *
return cmd
}
func runViz(ctx context.Context, dockerCli command.Cli, backend api.Service, opts *vizOptions) error {
func runViz(ctx context.Context, dockerCli command.Cli, backend api.Compose, opts *vizOptions) error {
_, _ = fmt.Fprintln(os.Stderr, "viz command is EXPERIMENTAL")
project, _, err := opts.ToProject(ctx, dockerCli, nil)
if err != nil {

View File

@@ -34,7 +34,7 @@ type volumesOptions struct {
Format string
}
func volumesCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
func volumesCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose) *cobra.Command {
options := volumesOptions{
ProjectOptions: p,
}
@@ -54,7 +54,7 @@ func volumesCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Servic
return cmd
}
func runVol(ctx context.Context, dockerCli command.Cli, backend api.Service, services []string, options volumesOptions) error {
func runVol(ctx context.Context, dockerCli command.Cli, backend api.Compose, services []string, options volumesOptions) error {
project, name, err := options.projectOrName(ctx, dockerCli, services...)
if err != nil {
return err

View File

@@ -34,7 +34,7 @@ type waitOptions struct {
downProject bool
}
func waitCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
func waitCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose) *cobra.Command {
opts := waitOptions{
ProjectOptions: p,
}
@@ -60,7 +60,7 @@ func waitCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
return cmd
}
func runWait(ctx context.Context, dockerCli command.Cli, backend api.Service, opts *waitOptions) (int64, error) {
func runWait(ctx context.Context, dockerCli command.Cli, backend api.Compose, opts *waitOptions) (int64, error) {
_, name, err := opts.projectOrName(ctx, dockerCli)
if err != nil {
return 0, err

View File

@@ -36,7 +36,7 @@ type watchOptions struct {
noUp bool
}
func watchCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
func watchCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose) *cobra.Command {
watchOpts := watchOptions{
ProjectOptions: p,
}
@@ -64,7 +64,7 @@ func watchCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
return cmd
}
func runWatch(ctx context.Context, dockerCli command.Cli, backend api.Service, watchOpts watchOptions, buildOpts buildOptions, services []string) error {
func runWatch(ctx context.Context, dockerCli command.Cli, backend api.Compose, watchOpts watchOptions, buildOpts buildOptions, services []string) error {
project, _, err := watchOpts.ToProject(ctx, dockerCli, services)
if err != nil {
return err

View File

@@ -24,6 +24,7 @@ import (
"github.com/docker/cli/cli-plugins/plugin"
"github.com/docker/cli/cli/command"
"github.com/docker/compose/v2/cmd/cmdtrace"
"github.com/docker/compose/v2/cmd/prompt"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
@@ -35,16 +36,18 @@ import (
func pluginMain() {
plugin.Run(
func(dockerCli command.Cli) *cobra.Command {
backend := compose.NewComposeService(dockerCli)
cmd := commands.RootCommand(dockerCli, backend)
func(cli command.Cli) *cobra.Command {
backend := compose.NewComposeService(cli,
compose.WithPrompt(prompt.NewPrompt(cli.In(), cli.Out()).Confirm),
)
cmd := commands.RootCommand(cli, backend)
originalPreRunE := cmd.PersistentPreRunE
cmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error {
// initialize the dockerCli instance
// initialize the cli instance
if err := plugin.PersistentPreRunE(cmd, args); err != nil {
return err
}
if err := cmdtrace.Setup(cmd, dockerCli, os.Args[1:]); err != nil {
if err := cmdtrace.Setup(cmd, cli, os.Args[1:]); err != nil {
logrus.Debugf("failed to enable tracing: %v", err)
}

View File

@@ -30,8 +30,9 @@ import (
"github.com/docker/docker/api/types/volume"
)
// Service manages a compose project
type Service interface {
// Compose is the API interface one can use to programmatically use docker/compose in a third-party software
// Use [compose.NewComposeService] to get an actual instance
type Compose interface {
// Build executes the equivalent to a `compose build`
Build(ctx context.Context, project *types.Project, options BuildOptions) error
// Push executes the equivalent to a `compose push`

View File

@@ -50,18 +50,36 @@ func init() {
}
}
// NewComposeService create a local implementation of the compose.Service API
func NewComposeService(dockerCli command.Cli) api.Service {
return &composeService{
type Option func(service *composeService)
// NewComposeService create a local implementation of the compose.Compose API
func NewComposeService(dockerCli command.Cli, options ...Option) api.Compose {
s := &composeService{
dockerCli: dockerCli,
clock: clockwork.NewRealClock(),
maxConcurrency: -1,
dryRun: false,
}
for _, option := range options {
option(s)
}
return s
}
// WithPrompt configure a UI component for Compose service to interact with user and confirm actions
func WithPrompt(prompt Prompt) Option {
return func(s *composeService) {
s.prompt = prompt
}
}
type Prompt func(message string, defaultValue bool) (bool, error)
type composeService struct {
dockerCli command.Cli
dockerCli command.Cli
// prompt is used to interact with user and confirm actions
prompt Prompt
clock clockwork.Clock
maxConcurrency int
dryRun bool

View File

@@ -44,7 +44,6 @@ import (
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/progress"
"github.com/docker/compose/v2/pkg/prompt"
)
type createOptions struct {
@@ -1566,7 +1565,7 @@ func (s *composeService) ensureVolume(ctx context.Context, name string, volume t
confirm := assumeYes
if !assumeYes {
msg := fmt.Sprintf("Volume %q exists but doesn't match configuration in compose file. Recreate (data will be lost)?", volume.Name)
confirm, err = prompt.NewPrompt(s.stdin(), s.stdout()).Confirm(msg, false)
confirm, err = s.prompt(msg, false)
if err != nil {
return "", err
}

View File

@@ -25,18 +25,17 @@ import (
"fmt"
"io"
"os"
"strings"
"github.com/DefangLabs/secret-detector/pkg/scanner"
"github.com/DefangLabs/secret-detector/pkg/secrets"
"github.com/compose-spec/compose-go/v2/loader"
"github.com/compose-spec/compose-go/v2/types"
"github.com/distribution/reference"
"github.com/docker/cli/cli/command"
"github.com/docker/compose/v2/internal/oci"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/compose/transform"
"github.com/docker/compose/v2/pkg/progress"
"github.com/docker/compose/v2/pkg/prompt"
"github.com/opencontainers/go-digest"
"github.com/opencontainers/image-spec/specs-go"
v1 "github.com/opencontainers/image-spec/specs-go/v1"
@@ -280,16 +279,20 @@ func (s *composeService) preChecks(project *types.Project, options api.PublishOp
}
bindMounts := s.checkForBindMount(project)
if len(bindMounts) > 0 {
fmt.Println("you are about to publish bind mounts declaration within your OCI artifact.\n" +
b := strings.Builder{}
b.WriteString("you are about to publish bind mounts declaration within your OCI artifact.\n" +
"only the bind mount declarations will be added to the OCI artifact (not content)\n" +
"please double check that you are not mounting potential user's sensitive directories or data")
"please double check that you are not mounting potential user's sensitive directories or data\n")
for key, val := range bindMounts {
_, _ = fmt.Fprintln(s.dockerCli.Out(), key)
b.WriteString(key)
for _, v := range val {
_, _ = fmt.Fprintf(s.dockerCli.Out(), "%s\n", v.String())
b.WriteString(v.String())
b.WriteRune('\n')
}
}
if ok, err := acceptPublishBindMountDeclarations(s.dockerCli); err != nil || !ok {
b.WriteString("Are you ok to publish these bind mount declarations?")
confirm, err := s.prompt(b.String(), false)
if err != nil || !confirm {
return false, err
}
}
@@ -298,13 +301,17 @@ func (s *composeService) preChecks(project *types.Project, options api.PublishOp
return false, err
}
if len(detectedSecrets) > 0 {
fmt.Println("you are about to publish sensitive data within your OCI artifact.\n" +
"please double check that you are not leaking sensitive data")
b := strings.Builder{}
b.WriteString("you are about to publish sensitive data within your OCI artifact.\n" +
"please double check that you are not leaking sensitive data\n")
for _, val := range detectedSecrets {
_, _ = fmt.Fprintln(s.dockerCli.Out(), val.Type)
_, _ = fmt.Fprintf(s.dockerCli.Out(), "%q: %s\n", val.Key, val.Value)
b.WriteString(val.Type)
b.WriteRune('\n')
b.WriteString(fmt.Sprintf("%q: %s\n", val.Key, val.Value))
}
if ok, err := acceptPublishSensitiveData(s.dockerCli); err != nil || !ok {
b.WriteString("Are you ok to publish these sensitive data?")
confirm, err := s.prompt(b.String(), false)
if err != nil || !confirm {
return false, err
}
}
@@ -313,15 +320,20 @@ func (s *composeService) preChecks(project *types.Project, options api.PublishOp
return false, err
}
if len(envVariables) > 0 {
fmt.Println("you are about to publish environment variables within your OCI artifact.\n" +
"please double check that you are not leaking sensitive data")
b := strings.Builder{}
b.WriteString("you are about to publish environment variables within your OCI artifact.\n" +
"please double check that you are not leaking sensitive data\n")
for key, val := range envVariables {
_, _ = fmt.Fprintln(s.dockerCli.Out(), "Service/Config ", key)
b.WriteString("Service/Config ")
b.WriteString(key)
b.WriteRune('\n')
for k, v := range val {
_, _ = fmt.Fprintf(s.dockerCli.Out(), "%s=%v\n", k, *v)
b.WriteString(fmt.Sprintf("%s=%v\n", k, *v))
}
}
if ok, err := acceptPublishEnvVariables(s.dockerCli); err != nil || !ok {
b.WriteString("Are you ok to publish these environment variables?")
confirm, err := s.prompt(b.String(), false)
if err != nil || !confirm {
return false, err
}
}
@@ -364,24 +376,6 @@ func (s *composeService) checkEnvironmentVariables(project *types.Project, optio
return envVarList, nil
}
func acceptPublishEnvVariables(cli command.Cli) (bool, error) {
msg := "Are you ok to publish these environment variables? [y/N]: "
confirm, err := prompt.NewPrompt(cli.In(), cli.Out()).Confirm(msg, false)
return confirm, err
}
func acceptPublishSensitiveData(cli command.Cli) (bool, error) {
msg := "Are you ok to publish these sensitive data? [y/N]: "
confirm, err := prompt.NewPrompt(cli.In(), cli.Out()).Confirm(msg, false)
return confirm, err
}
func acceptPublishBindMountDeclarations(cli command.Cli) (bool, error) {
msg := "Are you ok to publish these bind mount declarations? [y/N]: "
confirm, err := prompt.NewPrompt(cli.In(), cli.Out()).Confirm(msg, false)
return confirm, err
}
func envFileLayers(project *types.Project) []v1.Descriptor {
var layers []v1.Descriptor
for _, service := range project.Services {

View File

@@ -26,7 +26,6 @@ import (
"golang.org/x/sync/errgroup"
"github.com/docker/compose/v2/pkg/progress"
"github.com/docker/compose/v2/pkg/prompt"
)
func (s *composeService) Remove(ctx context.Context, projectName string, options api.RemoveOptions) error {
@@ -85,7 +84,7 @@ func (s *composeService) Remove(ctx context.Context, projectName string, options
if options.Force {
_, _ = fmt.Fprintln(s.stdout(), msg)
} else {
confirm, err := prompt.NewPrompt(s.stdin(), s.stdout()).Confirm(msg, false)
confirm, err := s.prompt(msg, false)
if err != nil {
return err
}

View File

@@ -75,7 +75,7 @@ or remove sensitive data from your Compose configuration
cmd.Stdin = strings.NewReader("y\n")
res := icmd.RunCmd(cmd)
res.Assert(t, icmd.Expected{ExitCode: 0})
assert.Assert(t, strings.Contains(res.Combined(), "Are you ok to publish these environment variables? [y/N]:"), res.Combined())
assert.Assert(t, strings.Contains(res.Combined(), "Are you ok to publish these environment variables?"), res.Combined())
assert.Assert(t, strings.Contains(res.Combined(), "test/test publishing"), res.Combined())
assert.Assert(t, strings.Contains(res.Combined(), "test/test published"), res.Combined())
})
@@ -86,7 +86,7 @@ or remove sensitive data from your Compose configuration
cmd.Stdin = strings.NewReader("n\n")
res := icmd.RunCmd(cmd)
res.Assert(t, icmd.Expected{ExitCode: 0})
assert.Assert(t, strings.Contains(res.Combined(), "Are you ok to publish these environment variables? [y/N]:"), res.Combined())
assert.Assert(t, strings.Contains(res.Combined(), "Are you ok to publish these environment variables?"), res.Combined())
assert.Assert(t, !strings.Contains(res.Combined(), "test/test publishing"), res.Combined())
assert.Assert(t, !strings.Contains(res.Combined(), "test/test published"), res.Combined())
})
@@ -103,15 +103,16 @@ or remove sensitive data from your Compose configuration
cmd.Stdin = strings.NewReader("n\n")
res := icmd.RunCmd(cmd)
res.Assert(t, icmd.Expected{ExitCode: 0})
assert.Assert(t, strings.Contains(res.Combined(), `you are about to publish environment variables within your OCI artifact.
please double check that you are not leaking sensitive data`), res.Combined())
assert.Assert(t, strings.Contains(res.Combined(), `Service/Config serviceA
FOO=bar`), res.Combined())
assert.Assert(t, strings.Contains(res.Combined(), `Service/Config serviceB`), res.Combined())
out := res.Combined()
assert.Assert(t, strings.Contains(out, `you are about to publish environment variables within your OCI artifact.
please double check that you are not leaking sensitive data`), out)
assert.Assert(t, strings.Contains(out, `Service/Config serviceA
FOO=bar`), out)
assert.Assert(t, strings.Contains(out, `Service/Config serviceB`), out)
// we don't know in which order the env variables will be loaded
assert.Assert(t, strings.Contains(res.Combined(), `FOO=bar`), res.Combined())
assert.Assert(t, strings.Contains(res.Combined(), `BAR=baz`), res.Combined())
assert.Assert(t, strings.Contains(res.Combined(), `QUIX=`), res.Combined())
assert.Assert(t, strings.Contains(out, `FOO=bar`), out)
assert.Assert(t, strings.Contains(out, `BAR=baz`), out)
assert.Assert(t, strings.Contains(out, `QUIX=`), out)
})
t.Run("refuse to publish with bind mount", func(t *testing.T) {
@@ -120,10 +121,11 @@ FOO=bar`), res.Combined())
cmd.Stdin = strings.NewReader("n\n")
res := icmd.RunCmd(cmd)
res.Assert(t, icmd.Expected{ExitCode: 0})
assert.Assert(t, strings.Contains(res.Combined(), "you are about to publish bind mounts declaration within your OCI artifact."), res.Combined())
assert.Assert(t, strings.Contains(res.Combined(), "e2e/fixtures/publish:/user-data"), res.Combined())
assert.Assert(t, strings.Contains(res.Combined(), "Are you ok to publish these bind mount declarations? [y/N]:"), res.Combined())
assert.Assert(t, !strings.Contains(res.Combined(), "serviceA published"), res.Combined())
out := res.Combined()
assert.Assert(t, strings.Contains(out, "you are about to publish bind mounts declaration within your OCI artifact."), out)
assert.Assert(t, strings.Contains(out, "e2e/fixtures/publish:/user-data"), out)
assert.Assert(t, strings.Contains(out, "Are you ok to publish these bind mount declarations?"), out)
assert.Assert(t, !strings.Contains(out, "serviceA published"), out)
})
t.Run("publish with bind mount", func(t *testing.T) {
@@ -133,7 +135,7 @@ FOO=bar`), res.Combined())
res := icmd.RunCmd(cmd)
res.Assert(t, icmd.Expected{ExitCode: 0})
assert.Assert(t, strings.Contains(res.Combined(), "you are about to publish bind mounts declaration within your OCI artifact."), res.Combined())
assert.Assert(t, strings.Contains(res.Combined(), "Are you ok to publish these bind mount declarations? [y/N]:"), res.Combined())
assert.Assert(t, strings.Contains(res.Combined(), "Are you ok to publish these bind mount declarations?"), res.Combined())
assert.Assert(t, strings.Contains(res.Combined(), "e2e/fixtures/publish:/user-data"), res.Combined())
assert.Assert(t, strings.Contains(res.Combined(), "test/test published"), res.Combined())
})