2016-12-20 16:26:49 -05:00
|
|
|
package interpolation
|
|
|
|
|
|
|
|
import (
|
2017-10-03 16:22:02 -04:00
|
|
|
"os"
|
2017-10-03 18:03:20 -04:00
|
|
|
"strings"
|
|
|
|
|
2017-04-17 18:07:56 -04:00
|
|
|
"github.com/docker/cli/cli/compose/template"
|
2017-03-23 11:05:24 -04:00
|
|
|
"github.com/pkg/errors"
|
2016-12-20 16:26:49 -05:00
|
|
|
)
|
|
|
|
|
2017-10-03 16:22:02 -04:00
|
|
|
// Options supported by Interpolate
|
|
|
|
type Options struct {
|
|
|
|
// LookupValue from a key
|
|
|
|
LookupValue LookupValue
|
2017-10-03 18:03:20 -04:00
|
|
|
// TypeCastMapping maps key paths to functions to cast to a type
|
|
|
|
TypeCastMapping map[Path]Cast
|
2018-06-25 11:15:26 -04:00
|
|
|
// Substitution function to use
|
|
|
|
Substitute func(string, template.Mapping) (string, error)
|
2017-10-03 16:22:02 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
// LookupValue is a function which maps from variable names to values.
|
|
|
|
// Returns the value as a string and a bool indicating whether
|
|
|
|
// the value is present, to distinguish between an empty string
|
|
|
|
// and the absence of a value.
|
|
|
|
type LookupValue func(key string) (string, bool)
|
|
|
|
|
2017-10-03 18:03:20 -04:00
|
|
|
// Cast a value to a new type, or return an error if the value can't be cast
|
2023-11-20 12:04:36 -05:00
|
|
|
type Cast func(value string) (any, error)
|
2017-10-03 18:03:20 -04:00
|
|
|
|
2016-12-20 16:26:49 -05:00
|
|
|
// Interpolate replaces variables in a string with the values from a mapping
|
2023-11-20 12:04:36 -05:00
|
|
|
func Interpolate(config map[string]any, opts Options) (map[string]any, error) {
|
2017-10-03 16:22:02 -04:00
|
|
|
if opts.LookupValue == nil {
|
|
|
|
opts.LookupValue = os.LookupEnv
|
|
|
|
}
|
2017-10-03 18:03:20 -04:00
|
|
|
if opts.TypeCastMapping == nil {
|
|
|
|
opts.TypeCastMapping = make(map[Path]Cast)
|
|
|
|
}
|
2018-06-25 11:15:26 -04:00
|
|
|
if opts.Substitute == nil {
|
|
|
|
opts.Substitute = template.Substitute
|
|
|
|
}
|
2017-10-03 16:22:02 -04:00
|
|
|
|
2023-11-20 12:04:36 -05:00
|
|
|
out := map[string]any{}
|
2016-12-20 16:26:49 -05:00
|
|
|
|
2017-10-04 16:51:48 -04:00
|
|
|
for key, value := range config {
|
|
|
|
interpolatedValue, err := recursiveInterpolate(value, NewPath(key), opts)
|
|
|
|
if err != nil {
|
|
|
|
return out, err
|
2016-12-20 16:26:49 -05:00
|
|
|
}
|
|
|
|
out[key] = interpolatedValue
|
|
|
|
}
|
|
|
|
|
|
|
|
return out, nil
|
|
|
|
}
|
|
|
|
|
2023-11-20 12:04:36 -05:00
|
|
|
func recursiveInterpolate(value any, path Path, opts Options) (any, error) {
|
2016-12-20 16:26:49 -05:00
|
|
|
switch value := value.(type) {
|
|
|
|
case string:
|
2018-06-25 11:15:26 -04:00
|
|
|
newValue, err := opts.Substitute(value, template.Mapping(opts.LookupValue))
|
2017-10-03 18:03:20 -04:00
|
|
|
if err != nil || newValue == value {
|
2017-10-04 16:51:48 -04:00
|
|
|
return value, newPathError(path, err)
|
2017-10-03 18:03:20 -04:00
|
|
|
}
|
|
|
|
caster, ok := opts.getCasterForPath(path)
|
|
|
|
if !ok {
|
|
|
|
return newValue, nil
|
|
|
|
}
|
2017-10-04 16:51:48 -04:00
|
|
|
casted, err := caster(newValue)
|
|
|
|
return casted, newPathError(path, errors.Wrap(err, "failed to cast to expected type"))
|
2016-12-20 16:26:49 -05:00
|
|
|
|
2023-11-20 12:04:36 -05:00
|
|
|
case map[string]any:
|
|
|
|
out := map[string]any{}
|
2016-12-20 16:26:49 -05:00
|
|
|
for key, elem := range value {
|
2017-10-03 18:03:20 -04:00
|
|
|
interpolatedElem, err := recursiveInterpolate(elem, path.Next(key), opts)
|
2016-12-20 16:26:49 -05:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
out[key] = interpolatedElem
|
|
|
|
}
|
|
|
|
return out, nil
|
|
|
|
|
2023-11-20 12:04:36 -05:00
|
|
|
case []any:
|
|
|
|
out := make([]any, len(value))
|
2016-12-20 16:26:49 -05:00
|
|
|
for i, elem := range value {
|
2017-10-04 16:51:48 -04:00
|
|
|
interpolatedElem, err := recursiveInterpolate(elem, path.Next(PathMatchList), opts)
|
2016-12-20 16:26:49 -05:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
out[i] = interpolatedElem
|
|
|
|
}
|
|
|
|
return out, nil
|
|
|
|
|
|
|
|
default:
|
|
|
|
return value, nil
|
|
|
|
}
|
|
|
|
}
|
2017-10-03 18:03:20 -04:00
|
|
|
|
2017-10-04 16:51:48 -04:00
|
|
|
func newPathError(path Path, err error) error {
|
|
|
|
switch err := err.(type) {
|
|
|
|
case nil:
|
|
|
|
return nil
|
|
|
|
case *template.InvalidTemplateError:
|
|
|
|
return errors.Errorf(
|
linting: fix incorrectly formatted errors (revive)
cli/compose/interpolation/interpolation.go:102:4: error-strings: error strings should not be capitalized or end with punctuation or a newline (revive)
"invalid interpolation format for %s: %#v. You may need to escape any $ with another $.",
^
cli/command/stack/loader/loader.go:30:30: error-strings: error strings should not be capitalized or end with punctuation or a newline (revive)
return nil, errors.Errorf("Compose file contains unsupported options:\n\n%s\n",
^
cli/command/formatter/formatter.go:76:30: error-strings: error strings should not be capitalized or end with punctuation or a newline (revive)
return tmpl, errors.Errorf("Template parsing error: %v\n", err)
^
cli/command/formatter/formatter.go:97:24: error-strings: error strings should not be capitalized or end with punctuation or a newline (revive)
return errors.Errorf("Template parsing error: %v\n", err)
^
cli/command/image/build.go:257:25: error-strings: error strings should not be capitalized or end with punctuation or a newline (revive)
return errors.Errorf("error checking context: '%s'.", err)
^
cli/command/volume/create.go:35:27: error-strings: error strings should not be capitalized or end with punctuation or a newline (revive)
return errors.Errorf("Conflicting options: either specify --name or provide positional arg, not both\n")
^
cli/command/container/create.go:160:24: error-strings: error strings should not be capitalized or end with punctuation or a newline (revive)
return errors.Errorf("failed to remove the CID file '%s': %s \n", cid.path, err)
^
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2022-03-27 15:13:03 -04:00
|
|
|
"invalid interpolation format for %s: %#v; you may need to escape any $ with another $",
|
2017-10-04 16:51:48 -04:00
|
|
|
path, err.Template)
|
|
|
|
default:
|
|
|
|
return errors.Wrapf(err, "error while interpolating %s", path)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-10-03 18:03:20 -04:00
|
|
|
const pathSeparator = "."
|
|
|
|
|
|
|
|
// PathMatchAll is a token used as part of a Path to match any key at that level
|
|
|
|
// in the nested structure
|
|
|
|
const PathMatchAll = "*"
|
|
|
|
|
2017-10-04 16:51:48 -04:00
|
|
|
// PathMatchList is a token used as part of a Path to match items in a list
|
|
|
|
const PathMatchList = "[]"
|
|
|
|
|
2017-10-03 18:03:20 -04:00
|
|
|
// Path is a dotted path of keys to a value in a nested mapping structure. A *
|
|
|
|
// section in a path will match any key in the mapping structure.
|
|
|
|
type Path string
|
|
|
|
|
|
|
|
// NewPath returns a new Path
|
|
|
|
func NewPath(items ...string) Path {
|
|
|
|
return Path(strings.Join(items, pathSeparator))
|
|
|
|
}
|
|
|
|
|
|
|
|
// Next returns a new path by append part to the current path
|
|
|
|
func (p Path) Next(part string) Path {
|
|
|
|
return Path(string(p) + pathSeparator + part)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (p Path) parts() []string {
|
|
|
|
return strings.Split(string(p), pathSeparator)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (p Path) matches(pattern Path) bool {
|
|
|
|
patternParts := pattern.parts()
|
|
|
|
parts := p.parts()
|
|
|
|
|
|
|
|
if len(patternParts) != len(parts) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
for index, part := range parts {
|
|
|
|
switch patternParts[index] {
|
|
|
|
case PathMatchAll, part:
|
|
|
|
continue
|
|
|
|
default:
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
func (o Options) getCasterForPath(path Path) (Cast, bool) {
|
|
|
|
for pattern, caster := range o.TypeCastMapping {
|
|
|
|
if path.matches(pattern) {
|
|
|
|
return caster, true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil, false
|
|
|
|
}
|