diff --git a/components/cli/cli/compose/template/template.go b/components/cli/cli/compose/template/template.go index 81d579078f..1762ab11a9 100644 --- a/components/cli/cli/compose/template/template.go +++ b/components/cli/cli/compose/template/template.go @@ -14,7 +14,7 @@ var patternString = fmt.Sprintf( delimiter, delimiter, substitution, substitution, ) -var pattern = regexp.MustCompile(patternString) +var defaultPattern = regexp.MustCompile(patternString) // DefaultSubstituteFuncs contains the default SubstitueFunc used by the docker cli var DefaultSubstituteFuncs = []SubstituteFunc{ @@ -51,7 +51,7 @@ func SubstituteWith(template string, mapping Mapping, pattern *regexp.Regexp, su var err error result := pattern.ReplaceAllStringFunc(template, func(substring string) string { matches := pattern.FindStringSubmatch(substring) - groups := matchGroups(matches) + groups := matchGroups(matches, pattern) if escaped := groups["escaped"]; escaped != "" { return escaped } @@ -90,26 +90,31 @@ func SubstituteWith(template string, mapping Mapping, pattern *regexp.Regexp, su // Substitute variables in the string with their values func Substitute(template string, mapping Mapping) (string, error) { - return SubstituteWith(template, mapping, pattern, DefaultSubstituteFuncs...) + return SubstituteWith(template, mapping, defaultPattern, DefaultSubstituteFuncs...) } // ExtractVariables returns a map of all the variables defined in the specified // composefile (dict representation) and their default value if any. -func ExtractVariables(configDict map[string]interface{}) map[string]string { - return recurseExtract(configDict) +func ExtractVariables(configDict map[string]interface{}, pattern *regexp.Regexp) map[string]string { + if pattern == nil { + pattern = defaultPattern + } + return recurseExtract(configDict, pattern) } -func recurseExtract(value interface{}) map[string]string { +func recurseExtract(value interface{}, pattern *regexp.Regexp) map[string]string { m := map[string]string{} switch value := value.(type) { case string: - if v, is := extractVariable(value); is { - m[v.name] = v.value + if values, is := extractVariable(value, pattern); is { + for _, v := range values { + m[v.name] = v.value + } } case map[string]interface{}: for _, elem := range value { - submap := recurseExtract(elem) + submap := recurseExtract(elem, pattern) for key, value := range submap { m[key] = value } @@ -117,8 +122,10 @@ func recurseExtract(value interface{}) map[string]string { case []interface{}: for _, elem := range value { - if v, is := extractVariable(elem); is { - m[v.name] = v.value + if values, is := extractVariable(elem, pattern); is { + for _, v := range values { + m[v.name] = v.value + } } } } @@ -131,36 +138,40 @@ type extractedValue struct { value string } -func extractVariable(value interface{}) (extractedValue, bool) { +func extractVariable(value interface{}, pattern *regexp.Regexp) ([]extractedValue, bool) { sValue, ok := value.(string) if !ok { - return extractedValue{}, false + return []extractedValue{}, false } - matches := pattern.FindStringSubmatch(sValue) + matches := pattern.FindAllStringSubmatch(sValue, -1) if len(matches) == 0 { - return extractedValue{}, false + return []extractedValue{}, false } - groups := matchGroups(matches) - if escaped := groups["escaped"]; escaped != "" { - return extractedValue{}, false + values := []extractedValue{} + for _, match := range matches { + groups := matchGroups(match, pattern) + if escaped := groups["escaped"]; escaped != "" { + continue + } + val := groups["named"] + if val == "" { + val = groups["braced"] + } + name := val + var defaultValue string + switch { + case strings.Contains(val, ":?"): + name, _ = partition(val, ":?") + case strings.Contains(val, "?"): + name, _ = partition(val, "?") + case strings.Contains(val, ":-"): + name, defaultValue = partition(val, ":-") + case strings.Contains(val, "-"): + name, defaultValue = partition(val, "-") + } + values = append(values, extractedValue{name: name, value: defaultValue}) } - val := groups["named"] - if val == "" { - val = groups["braced"] - } - name := val - var defaultValue string - switch { - case strings.Contains(val, ":?"): - name, _ = partition(val, ":?") - case strings.Contains(val, "?"): - name, _ = partition(val, "?") - case strings.Contains(val, ":-"): - name, defaultValue = partition(val, ":-") - case strings.Contains(val, "-"): - name, defaultValue = partition(val, "-") - } - return extractedValue{name: name, value: defaultValue}, true + return values, len(values) > 0 } // Soft default (fall back if unset or empty) @@ -207,7 +218,7 @@ func withRequired(substitution string, mapping Mapping, sep string, valid func(s return value, true, nil } -func matchGroups(matches []string) map[string]string { +func matchGroups(matches []string, pattern *regexp.Regexp) map[string]string { groups := make(map[string]string) for i, name := range pattern.SubexpNames()[1:] { groups[name] = matches[i+1] diff --git a/components/cli/cli/compose/template/template_test.go b/components/cli/cli/compose/template/template_test.go index 48c588c702..abbc810c3c 100644 --- a/components/cli/cli/compose/template/template_test.go +++ b/components/cli/cli/compose/template/template_test.go @@ -161,15 +161,15 @@ func TestSubstituteWithCustomFunc(t *testing.T) { return value, true, nil } - result, err := SubstituteWith("ok ${FOO}", defaultMapping, pattern, errIsMissing) + result, err := SubstituteWith("ok ${FOO}", defaultMapping, defaultPattern, errIsMissing) assert.NilError(t, err) assert.Check(t, is.Equal("ok first", result)) - result, err = SubstituteWith("ok ${BAR}", defaultMapping, pattern, errIsMissing) + result, err = SubstituteWith("ok ${BAR}", defaultMapping, defaultPattern, errIsMissing) assert.NilError(t, err) assert.Check(t, is.Equal("ok ", result)) - _, err = SubstituteWith("ok ${NOTHERE}", defaultMapping, pattern, errIsMissing) + _, err = SubstituteWith("ok ${NOTHERE}", defaultMapping, defaultPattern, errIsMissing) assert.Check(t, is.ErrorContains(err, "required variable")) } @@ -245,18 +245,21 @@ func TestExtractVariables(t *testing.T) { }, "baz": []interface{}{ "foo", + "$docker:${project:-cli}", "$toto", }, }, expected: map[string]string{ - "bar": "foo", - "fruit": "banana", - "toto": "", + "bar": "foo", + "fruit": "banana", + "toto": "", + "docker": "", + "project": "cli", }, }, } for _, tc := range testCases { - actual := ExtractVariables(tc.dict) + actual := ExtractVariables(tc.dict, defaultPattern) assert.Check(t, is.DeepEqual(actual, tc.expected)) } } diff --git a/components/cli/contrib/completion/bash/docker b/components/cli/contrib/completion/bash/docker index 3fdb905cb5..e31467077c 100644 --- a/components/cli/contrib/completion/bash/docker +++ b/components/cli/contrib/completion/bash/docker @@ -584,6 +584,31 @@ __docker_daemon_os_is() { [ "$actual_os" = "$expected_os" ] } +# __docker_stack_orchestrator_is tests whether the client is configured to use +# the orchestrator that is passed in as the first argument. +__docker_stack_orchestrator_is() { + case "$1" in + kubernetes) + if [ -z "$stack_orchestrator_is_kubernetes" ] ; then + __docker_q stack ls --help | grep -qe --namespace + stack_orchestrator_is_kubernetes=$? + fi + return $stack_orchestrator_is_kubernetes + ;; + swarm) + if [ -z "$stack_orchestrator_is_swarm" ] ; then + __docker_q stack deploy --help | grep -qe "with-registry-auth" + stack_orchestrator_is_swarm=$? + fi + return $stack_orchestrator_is_swarm + ;; + *) + return 1 + ;; + + esac +} + # __docker_pos_first_nonflag finds the position of the first word that is neither # option nor an option's argument. If there are options that require arguments, # you should pass a glob describing those options, e.g. "--option1|-o|--option2" @@ -1050,6 +1075,23 @@ __docker_complete_signals() { COMPREPLY=( $( compgen -W "${signals[*]} ${signals[*]#SIG}" -- "$( echo "$cur" | tr '[:lower:]' '[:upper:]')" ) ) } +__docker_complete_stack_orchestrator_options() { + case "$prev" in + --kubeconfig) + _filedir + return 0 + ;; + --namespace) + return 0 + ;; + --orchestrator) + COMPREPLY=( $( compgen -W "all kubernetes swarm" -- "$cur") ) + return 0 + ;; + esac + return 1 +} + __docker_complete_user_group() { if [[ $cur == *:* ]] ; then COMPREPLY=( $(compgen -g -- "${cur#*:}") ) @@ -4378,11 +4420,15 @@ _docker_stack() { remove up " + + __docker_complete_stack_orchestrator_options && return __docker_subcommands "$subcommands $aliases" && return case "$cur" in -*) - COMPREPLY=( $( compgen -W "--help" -- "$cur" ) ) + local options="--help --orchestrator" + __docker_stack_orchestrator_is kubernetes && options+=" --kubeconfig" + COMPREPLY=( $( compgen -W "$options" -- "$cur" ) ) ;; *) COMPREPLY=( $( compgen -W "$subcommands" -- "$cur" ) ) @@ -4391,12 +4437,12 @@ _docker_stack() { } _docker_stack_deploy() { + __docker_complete_stack_orchestrator_options && return + case "$prev" in --bundle-file) - if __docker_daemon_is_experimental ; then - _filedir dab - return - fi + _filedir dab + return ;; --compose-file|-c) _filedir yml @@ -4410,12 +4456,14 @@ _docker_stack_deploy() { case "$cur" in -*) - local options="--compose-file -c --help --prune --resolve-image --with-registry-auth" - __docker_daemon_is_experimental && options+=" --bundle-file" + local options="--compose-file -c --help --orchestrator" + __docker_daemon_is_experimental && __docker_stack_orchestrator_is swarm && options+=" --bundle-file" + __docker_stack_orchestrator_is kubernetes && options+=" --kubeconfig --namespace" + __docker_stack_orchestrator_is swarm && options+=" --prune --resolve-image --with-registry-auth" COMPREPLY=( $( compgen -W "$options" -- "$cur" ) ) ;; *) - local counter=$(__docker_pos_first_nonflag '--bundle-file|--compose-file|-c|--resolve-image') + local counter=$(__docker_pos_first_nonflag '--bundle-file|--compose-file|-c|--kubeconfig|--namespace|--orchestrator|--resolve-image') if [ "$cword" -eq "$counter" ]; then __docker_complete_stacks fi @@ -4432,6 +4480,8 @@ _docker_stack_list() { } _docker_stack_ls() { + __docker_complete_stack_orchestrator_options && return + case "$prev" in --format) return @@ -4440,7 +4490,9 @@ _docker_stack_ls() { case "$cur" in -*) - COMPREPLY=( $( compgen -W "--format --help" -- "$cur" ) ) + local options="--format --help --orchestrator" + __docker_stack_orchestrator_is kubernetes && options+=" --all-namespaces --kubeconfig --namespace" + COMPREPLY=( $( compgen -W "$options" -- "$cur" ) ) ;; esac } @@ -4462,6 +4514,8 @@ _docker_stack_ps() { ;; esac + __docker_complete_stack_orchestrator_options && return + case "$prev" in --filter|-f) COMPREPLY=( $( compgen -S = -W "id name desired-state" -- "$cur" ) ) @@ -4475,10 +4529,12 @@ _docker_stack_ps() { case "$cur" in -*) - COMPREPLY=( $( compgen -W "--filter -f --format --help --no-resolve --no-trunc --quiet -q" -- "$cur" ) ) + local options="--filter -f --format --help --no-resolve --no-trunc --orchestrator --quiet -q" + __docker_stack_orchestrator_is kubernetes && options+=" --all-namespaces --kubeconfig --namespace" + COMPREPLY=( $( compgen -W "$options" -- "$cur" ) ) ;; *) - local counter=$(__docker_pos_first_nonflag '--filter|-f') + local counter=$(__docker_pos_first_nonflag '--all-namespaces|--filter|-f|--format|--kubeconfig|--namespace') if [ "$cword" -eq "$counter" ]; then __docker_complete_stacks fi @@ -4491,9 +4547,13 @@ _docker_stack_remove() { } _docker_stack_rm() { + __docker_complete_stack_orchestrator_options && return + case "$cur" in -*) - COMPREPLY=( $( compgen -W "--help" -- "$cur" ) ) + local options="--help --orchestrator" + __docker_stack_orchestrator_is kubernetes && options+=" --kubeconfig --namespace" + COMPREPLY=( $( compgen -W "$options" -- "$cur" ) ) ;; *) __docker_complete_stacks @@ -4517,6 +4577,8 @@ _docker_stack_services() { ;; esac + __docker_complete_stack_orchestrator_options && return + case "$prev" in --filter|-f) COMPREPLY=( $( compgen -S = -W "id label name" -- "$cur" ) ) @@ -4530,10 +4592,12 @@ _docker_stack_services() { case "$cur" in -*) - COMPREPLY=( $( compgen -W "--filter -f --format --help --quiet -q" -- "$cur" ) ) + local options="--filter -f --format --help --orchestrator --quiet -q" + __docker_stack_orchestrator_is kubernetes && options+=" --kubeconfig --namespace" + COMPREPLY=( $( compgen -W "$options" -- "$cur" ) ) ;; *) - local counter=$(__docker_pos_first_nonflag '--filter|-f|--format') + local counter=$(__docker_pos_first_nonflag '--filter|-f|--format|--kubeconfig|--namespace|--orchestrator') if [ "$cword" -eq "$counter" ]; then __docker_complete_stacks fi @@ -4800,6 +4864,8 @@ _docker_top() { } _docker_version() { + __docker_complete_stack_orchestrator_options && return + case "$prev" in --format|-f) return @@ -4808,7 +4874,9 @@ _docker_version() { case "$cur" in -*) - COMPREPLY=( $( compgen -W "--format -f --help" -- "$cur" ) ) + local options="--format -f --help" + __docker_stack_orchestrator_is kubernetes && options+=" --kubeconfig" + COMPREPLY=( $( compgen -W "$options" -- "$cur" ) ) ;; esac } @@ -5038,6 +5106,9 @@ _docker() { local host config daemon_os + # variables to cache client info, populated on demand for performance reasons + local stack_orchestrator_is_kubernetes stack_orchestrator_is_swarm + COMPREPLY=() local cur prev words cword _get_comp_words_by_ref -n : cur prev words cword