|
|
@@ -17,9 +17,20 @@
|
|
|
package compose
|
|
|
|
|
|
import (
|
|
|
+ "context"
|
|
|
"fmt"
|
|
|
+ "io"
|
|
|
+ "os"
|
|
|
+ "sort"
|
|
|
+ "strings"
|
|
|
+ "text/tabwriter"
|
|
|
|
|
|
+ "github.com/compose-spec/compose-go/v2/cli"
|
|
|
+ "github.com/compose-spec/compose-go/v2/template"
|
|
|
"github.com/compose-spec/compose-go/v2/types"
|
|
|
+ "github.com/docker/cli/cli/command"
|
|
|
+ ui "github.com/docker/compose/v2/pkg/progress"
|
|
|
+ "github.com/docker/compose/v2/pkg/prompt"
|
|
|
"github.com/docker/compose/v2/pkg/utils"
|
|
|
)
|
|
|
|
|
|
@@ -72,3 +83,165 @@ func applyPlatforms(project *types.Project, buildForSinglePlatform bool) error {
|
|
|
}
|
|
|
return nil
|
|
|
}
|
|
|
+
|
|
|
+// isRemoteConfig checks if the main compose file is from a remote source (OCI or Git)
|
|
|
+func isRemoteConfig(dockerCli command.Cli, options buildOptions) bool {
|
|
|
+ if len(options.ConfigPaths) == 0 {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ remoteLoaders := options.remoteLoaders(dockerCli)
|
|
|
+ for _, loader := range remoteLoaders {
|
|
|
+ if loader.Accept(options.ConfigPaths[0]) {
|
|
|
+ return true
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return false
|
|
|
+}
|
|
|
+
|
|
|
+// checksForRemoteStack handles environment variable prompts for remote configurations
|
|
|
+func checksForRemoteStack(ctx context.Context, dockerCli command.Cli, project *types.Project, options buildOptions, assumeYes bool, cmdEnvs []string) error {
|
|
|
+ if !isRemoteConfig(dockerCli, options) {
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+ displayLocationRemoteStack(dockerCli, project, options)
|
|
|
+ return promptForInterpolatedVariables(ctx, dockerCli, options.ProjectOptions, assumeYes, cmdEnvs)
|
|
|
+}
|
|
|
+
|
|
|
+// Prepare the values map and collect all variables info
|
|
|
+type varInfo struct {
|
|
|
+ name string
|
|
|
+ value string
|
|
|
+ source string
|
|
|
+ required bool
|
|
|
+ defaultValue string
|
|
|
+}
|
|
|
+
|
|
|
+// promptForInterpolatedVariables displays all variables and their values at once,
|
|
|
+// then prompts for confirmation
|
|
|
+func promptForInterpolatedVariables(ctx context.Context, dockerCli command.Cli, projectOptions *ProjectOptions, assumeYes bool, cmdEnvs []string) error {
|
|
|
+ if assumeYes {
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+
|
|
|
+ varsInfo, noVariables, err := extractInterpolationVariablesFromModel(ctx, dockerCli, projectOptions, cmdEnvs)
|
|
|
+ if err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
+
|
|
|
+ if noVariables {
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+
|
|
|
+ displayInterpolationVariables(dockerCli.Out(), varsInfo)
|
|
|
+
|
|
|
+ // Prompt for confirmation
|
|
|
+ userInput := prompt.NewPrompt(dockerCli.In(), dockerCli.Out())
|
|
|
+ msg := "\nDo you want to proceed with these variables? [Y/n]: "
|
|
|
+ confirmed, err := userInput.Confirm(msg, true)
|
|
|
+ if err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
+
|
|
|
+ if !confirmed {
|
|
|
+ return fmt.Errorf("operation cancelled by user")
|
|
|
+ }
|
|
|
+
|
|
|
+ return nil
|
|
|
+}
|
|
|
+
|
|
|
+func extractInterpolationVariablesFromModel(ctx context.Context, dockerCli command.Cli, projectOptions *ProjectOptions, cmdEnvs []string) ([]varInfo, bool, error) {
|
|
|
+ cmdEnvMap := extractEnvCLIDefined(cmdEnvs)
|
|
|
+
|
|
|
+ // Create a model without interpolation to extract variables
|
|
|
+ opts := configOptions{
|
|
|
+ noInterpolate: true,
|
|
|
+ ProjectOptions: projectOptions,
|
|
|
+ }
|
|
|
+
|
|
|
+ model, err := opts.ToModel(ctx, dockerCli, nil, cli.WithoutEnvironmentResolution)
|
|
|
+ if err != nil {
|
|
|
+ return nil, false, err
|
|
|
+ }
|
|
|
+
|
|
|
+ // Extract variables that need interpolation
|
|
|
+ variables := template.ExtractVariables(model, template.DefaultPattern)
|
|
|
+ if len(variables) == 0 {
|
|
|
+ return nil, true, nil
|
|
|
+ }
|
|
|
+
|
|
|
+ var varsInfo []varInfo
|
|
|
+ proposedValues := make(map[string]string)
|
|
|
+
|
|
|
+ for name, variable := range variables {
|
|
|
+ info := varInfo{
|
|
|
+ name: name,
|
|
|
+ required: variable.Required,
|
|
|
+ defaultValue: variable.DefaultValue,
|
|
|
+ }
|
|
|
+
|
|
|
+ // Determine value and source based on priority
|
|
|
+ if value, exists := cmdEnvMap[name]; exists {
|
|
|
+ info.value = value
|
|
|
+ info.source = "command-line"
|
|
|
+ proposedValues[name] = value
|
|
|
+ } else if value, exists := os.LookupEnv(name); exists {
|
|
|
+ info.value = value
|
|
|
+ info.source = "environment"
|
|
|
+ proposedValues[name] = value
|
|
|
+ } else if variable.DefaultValue != "" {
|
|
|
+ info.value = variable.DefaultValue
|
|
|
+ info.source = "compose file"
|
|
|
+ proposedValues[name] = variable.DefaultValue
|
|
|
+ } else {
|
|
|
+ info.value = "<unset>"
|
|
|
+ info.source = "none"
|
|
|
+ }
|
|
|
+
|
|
|
+ varsInfo = append(varsInfo, info)
|
|
|
+ }
|
|
|
+ return varsInfo, false, nil
|
|
|
+}
|
|
|
+
|
|
|
+func extractEnvCLIDefined(cmdEnvs []string) map[string]string {
|
|
|
+ // Parse command-line environment variables
|
|
|
+ cmdEnvMap := make(map[string]string)
|
|
|
+ for _, env := range cmdEnvs {
|
|
|
+ parts := strings.SplitN(env, "=", 2)
|
|
|
+ if len(parts) == 2 {
|
|
|
+ cmdEnvMap[parts[0]] = parts[1]
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return cmdEnvMap
|
|
|
+}
|
|
|
+
|
|
|
+func displayInterpolationVariables(writer io.Writer, varsInfo []varInfo) {
|
|
|
+ // Display all variables in a table format
|
|
|
+ _, _ = fmt.Fprintln(writer, "\nFound the following variables in configuration:")
|
|
|
+
|
|
|
+ w := tabwriter.NewWriter(writer, 0, 0, 3, ' ', 0)
|
|
|
+ _, _ = fmt.Fprintln(w, "VARIABLE\tVALUE\tSOURCE\tREQUIRED\tDEFAULT")
|
|
|
+ sort.Slice(varsInfo, func(a, b int) bool {
|
|
|
+ return varsInfo[a].name < varsInfo[b].name
|
|
|
+ })
|
|
|
+ for _, info := range varsInfo {
|
|
|
+ required := "no"
|
|
|
+ if info.required {
|
|
|
+ required = "yes"
|
|
|
+ }
|
|
|
+ _, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n",
|
|
|
+ info.name,
|
|
|
+ info.value,
|
|
|
+ info.source,
|
|
|
+ required,
|
|
|
+ info.defaultValue,
|
|
|
+ )
|
|
|
+ }
|
|
|
+ _ = w.Flush()
|
|
|
+}
|
|
|
+
|
|
|
+func displayLocationRemoteStack(dockerCli command.Cli, project *types.Project, options buildOptions) {
|
|
|
+ mainComposeFile := options.ProjectOptions.ConfigPaths[0]
|
|
|
+ if ui.Mode != ui.ModeQuiet && ui.Mode != ui.ModeJSON {
|
|
|
+ _, _ = fmt.Fprintf(dockerCli.Out(), "Your compose stack %q is stored in %q\n", mainComposeFile, project.WorkingDir)
|
|
|
+ }
|
|
|
+}
|