|
17 | 17 | package compose
|
18 | 18 |
|
19 | 19 | import (
|
| 20 | + "context" |
20 | 21 | "fmt"
|
| 22 | + "io" |
| 23 | + "os" |
| 24 | + "sort" |
| 25 | + "strings" |
| 26 | + "text/tabwriter" |
21 | 27 |
|
| 28 | + "github.com/compose-spec/compose-go/v2/cli" |
| 29 | + "github.com/compose-spec/compose-go/v2/template" |
22 | 30 | "github.com/compose-spec/compose-go/v2/types"
|
| 31 | + "github.com/docker/cli/cli/command" |
| 32 | + ui "github.com/docker/compose/v2/pkg/progress" |
| 33 | + "github.com/docker/compose/v2/pkg/prompt" |
23 | 34 | "github.com/docker/compose/v2/pkg/utils"
|
24 | 35 | )
|
25 | 36 |
|
@@ -72,3 +83,165 @@ func applyPlatforms(project *types.Project, buildForSinglePlatform bool) error {
|
72 | 83 | }
|
73 | 84 | return nil
|
74 | 85 | }
|
| 86 | + |
| 87 | +// isRemoteConfig checks if the main compose file is from a remote source (OCI or Git) |
| 88 | +func isRemoteConfig(dockerCli command.Cli, options buildOptions) bool { |
| 89 | + if len(options.ConfigPaths) == 0 { |
| 90 | + return false |
| 91 | + } |
| 92 | + remoteLoaders := options.remoteLoaders(dockerCli) |
| 93 | + for _, loader := range remoteLoaders { |
| 94 | + if loader.Accept(options.ConfigPaths[0]) { |
| 95 | + return true |
| 96 | + } |
| 97 | + } |
| 98 | + return false |
| 99 | +} |
| 100 | + |
| 101 | +// checksForRemoteStack handles environment variable prompts for remote configurations |
| 102 | +func checksForRemoteStack(ctx context.Context, dockerCli command.Cli, project *types.Project, options buildOptions, assumeYes bool, cmdEnvs []string) error { |
| 103 | + if !isRemoteConfig(dockerCli, options) { |
| 104 | + return nil |
| 105 | + } |
| 106 | + displayLocationRemoteStack(dockerCli, project, options) |
| 107 | + return promptForInterpolatedVariables(ctx, dockerCli, options.ProjectOptions, assumeYes, cmdEnvs) |
| 108 | +} |
| 109 | + |
| 110 | +// Prepare the values map and collect all variables info |
| 111 | +type varInfo struct { |
| 112 | + name string |
| 113 | + value string |
| 114 | + source string |
| 115 | + required bool |
| 116 | + defaultValue string |
| 117 | +} |
| 118 | + |
| 119 | +// promptForInterpolatedVariables displays all variables and their values at once, |
| 120 | +// then prompts for confirmation |
| 121 | +func promptForInterpolatedVariables(ctx context.Context, dockerCli command.Cli, projectOptions *ProjectOptions, assumeYes bool, cmdEnvs []string) error { |
| 122 | + if assumeYes { |
| 123 | + return nil |
| 124 | + } |
| 125 | + |
| 126 | + varsInfo, noVariables, err := extractInterpolationVariablesFromModel(ctx, dockerCli, projectOptions, cmdEnvs) |
| 127 | + if err != nil { |
| 128 | + return err |
| 129 | + } |
| 130 | + |
| 131 | + if noVariables { |
| 132 | + return nil |
| 133 | + } |
| 134 | + |
| 135 | + displayInterpolationVariables(dockerCli.Out(), varsInfo) |
| 136 | + |
| 137 | + // Prompt for confirmation |
| 138 | + userInput := prompt.NewPrompt(dockerCli.In(), dockerCli.Out()) |
| 139 | + msg := "\nDo you want to proceed with these variables? [Y/n]: " |
| 140 | + confirmed, err := userInput.Confirm(msg, true) |
| 141 | + if err != nil { |
| 142 | + return err |
| 143 | + } |
| 144 | + |
| 145 | + if !confirmed { |
| 146 | + return fmt.Errorf("operation cancelled by user") |
| 147 | + } |
| 148 | + |
| 149 | + return nil |
| 150 | +} |
| 151 | + |
| 152 | +func extractInterpolationVariablesFromModel(ctx context.Context, dockerCli command.Cli, projectOptions *ProjectOptions, cmdEnvs []string) ([]varInfo, bool, error) { |
| 153 | + cmdEnvMap := extractEnvCLIDefined(cmdEnvs) |
| 154 | + |
| 155 | + // Create a model without interpolation to extract variables |
| 156 | + opts := configOptions{ |
| 157 | + noInterpolate: true, |
| 158 | + ProjectOptions: projectOptions, |
| 159 | + } |
| 160 | + |
| 161 | + model, err := opts.ToModel(ctx, dockerCli, nil, cli.WithoutEnvironmentResolution) |
| 162 | + if err != nil { |
| 163 | + return nil, false, err |
| 164 | + } |
| 165 | + |
| 166 | + // Extract variables that need interpolation |
| 167 | + variables := template.ExtractVariables(model, template.DefaultPattern) |
| 168 | + if len(variables) == 0 { |
| 169 | + return nil, true, nil |
| 170 | + } |
| 171 | + |
| 172 | + var varsInfo []varInfo |
| 173 | + proposedValues := make(map[string]string) |
| 174 | + |
| 175 | + for name, variable := range variables { |
| 176 | + info := varInfo{ |
| 177 | + name: name, |
| 178 | + required: variable.Required, |
| 179 | + defaultValue: variable.DefaultValue, |
| 180 | + } |
| 181 | + |
| 182 | + // Determine value and source based on priority |
| 183 | + if value, exists := cmdEnvMap[name]; exists { |
| 184 | + info.value = value |
| 185 | + info.source = "command-line" |
| 186 | + proposedValues[name] = value |
| 187 | + } else if value, exists := os.LookupEnv(name); exists { |
| 188 | + info.value = value |
| 189 | + info.source = "environment" |
| 190 | + proposedValues[name] = value |
| 191 | + } else if variable.DefaultValue != "" { |
| 192 | + info.value = variable.DefaultValue |
| 193 | + info.source = "compose file" |
| 194 | + proposedValues[name] = variable.DefaultValue |
| 195 | + } else { |
| 196 | + info.value = "<unset>" |
| 197 | + info.source = "none" |
| 198 | + } |
| 199 | + |
| 200 | + varsInfo = append(varsInfo, info) |
| 201 | + } |
| 202 | + return varsInfo, false, nil |
| 203 | +} |
| 204 | + |
| 205 | +func extractEnvCLIDefined(cmdEnvs []string) map[string]string { |
| 206 | + // Parse command-line environment variables |
| 207 | + cmdEnvMap := make(map[string]string) |
| 208 | + for _, env := range cmdEnvs { |
| 209 | + parts := strings.SplitN(env, "=", 2) |
| 210 | + if len(parts) == 2 { |
| 211 | + cmdEnvMap[parts[0]] = parts[1] |
| 212 | + } |
| 213 | + } |
| 214 | + return cmdEnvMap |
| 215 | +} |
| 216 | + |
| 217 | +func displayInterpolationVariables(writer io.Writer, varsInfo []varInfo) { |
| 218 | + // Display all variables in a table format |
| 219 | + _, _ = fmt.Fprintln(writer, "\nFound the following variables in configuration:") |
| 220 | + |
| 221 | + w := tabwriter.NewWriter(writer, 0, 0, 3, ' ', 0) |
| 222 | + _, _ = fmt.Fprintln(w, "VARIABLE\tVALUE\tSOURCE\tREQUIRED\tDEFAULT") |
| 223 | + sort.Slice(varsInfo, func(a, b int) bool { |
| 224 | + return varsInfo[a].name < varsInfo[b].name |
| 225 | + }) |
| 226 | + for _, info := range varsInfo { |
| 227 | + required := "no" |
| 228 | + if info.required { |
| 229 | + required = "yes" |
| 230 | + } |
| 231 | + _, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", |
| 232 | + info.name, |
| 233 | + info.value, |
| 234 | + info.source, |
| 235 | + required, |
| 236 | + info.defaultValue, |
| 237 | + ) |
| 238 | + } |
| 239 | + _ = w.Flush() |
| 240 | +} |
| 241 | + |
| 242 | +func displayLocationRemoteStack(dockerCli command.Cli, project *types.Project, options buildOptions) { |
| 243 | + mainComposeFile := options.ProjectOptions.ConfigPaths[0] |
| 244 | + if ui.Mode != ui.ModeQuiet && ui.Mode != ui.ModeJSON { |
| 245 | + _, _ = fmt.Fprintf(dockerCli.Out(), "Your compose stack %q is stored in %q\n", mainComposeFile, project.WorkingDir) |
| 246 | + } |
| 247 | +} |
0 commit comments