update github.com/compose-spec/compose-go to v1.3.0

Signed-off-by: CrazyMax <crazy-max@users.noreply.github.com>
This commit is contained in:
CrazyMax
2022-08-01 14:12:33 +02:00
parent 7b25e2cffc
commit 52fd555bdd
145 changed files with 10391 additions and 2854 deletions

View File

@ -17,13 +17,14 @@ import (
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"os/exec"
"regexp"
"sort"
"strconv"
"strings"
"github.com/compose-spec/compose-go/template"
)
const doubleQuoteSpecialChars = "\\\n\r\"!$`"
@ -42,7 +43,7 @@ func Parse(r io.Reader) (map[string]string, error) {
// ParseWithLookup reads an env file from io.Reader, returning a map of keys and values.
func ParseWithLookup(r io.Reader, lookupFn LookupFn) (map[string]string, error) {
data, err := ioutil.ReadAll(r)
data, err := io.ReadAll(r)
if err != nil {
return nil, err
}
@ -61,7 +62,7 @@ func ParseWithLookup(r io.Reader, lookupFn LookupFn) (map[string]string, error)
// godotenv.Load("fileone", "filetwo")
//
// It's important to note that it WILL NOT OVERRIDE an env variable that already exists - consider the .env file to set dev vars or sensible defaults
func Load(filenames ...string) (err error) {
func Load(filenames ...string) error {
return load(false, filenames...)
}
@ -76,52 +77,55 @@ func Load(filenames ...string) (err error) {
// godotenv.Overload("fileone", "filetwo")
//
// It's important to note this WILL OVERRIDE an env variable that already exists - consider the .env file to forcefilly set all vars.
func Overload(filenames ...string) (err error) {
func Overload(filenames ...string) error {
return load(true, filenames...)
}
func load(overload bool, filenames ...string) (err error) {
func load(overload bool, filenames ...string) error {
filenames = filenamesOrDefault(filenames)
for _, filename := range filenames {
err = loadFile(filename, overload)
err := loadFile(filename, overload)
if err != nil {
return // return early on a spazout
return err
}
}
return
return nil
}
var startsWithDigitRegex = regexp.MustCompile(`^\s*\d.*`) // Keys starting with numbers are ignored
// ReadWithLookup gets all env vars from the files and/or lookup function and return values as
// a map rather than automatically writing values into env
func ReadWithLookup(lookupFn LookupFn, filenames ...string) (envMap map[string]string, err error) {
func ReadWithLookup(lookupFn LookupFn, filenames ...string) (map[string]string, error) {
filenames = filenamesOrDefault(filenames)
envMap = make(map[string]string)
envMap := make(map[string]string)
for _, filename := range filenames {
individualEnvMap, individualErr := readFile(filename, lookupFn)
if individualErr != nil {
err = individualErr
return // return early on a spazout
return envMap, individualErr
}
for key, value := range individualEnvMap {
if startsWithDigitRegex.MatchString(key) {
continue
}
envMap[key] = value
}
}
return
return envMap, nil
}
// Read all env (with same file loading semantics as Load) but return values as
// a map rather than automatically writing values into env
func Read(filenames ...string) (envMap map[string]string, err error) {
func Read(filenames ...string) (map[string]string, error) {
return ReadWithLookup(nil, filenames...)
}
// Unmarshal reads an env file from a string, returning a map of keys and values.
func Unmarshal(str string) (envMap map[string]string, err error) {
func Unmarshal(str string) (map[string]string, error) {
return UnmarshalBytes([]byte(str))
}
@ -182,7 +186,7 @@ func Marshal(envMap map[string]string) (string, error) {
if d, err := strconv.Atoi(v); err == nil {
lines = append(lines, fmt.Sprintf(`%s=%d`, k, d))
} else {
lines = append(lines, fmt.Sprintf(`%s="%s"`, k, doubleQuoteEscape(v)))
lines = append(lines, fmt.Sprintf(`%s="%s"`, k, doubleQuoteEscape(v))) // nolint // Cannot use %q here
}
}
sort.Strings(lines)
@ -218,10 +222,10 @@ func loadFile(filename string, overload bool) error {
return nil
}
func readFile(filename string, lookupFn LookupFn) (envMap map[string]string, err error) {
func readFile(filename string, lookupFn LookupFn) (map[string]string, error) {
file, err := os.Open(filename)
if err != nil {
return
return nil, err
}
defer file.Close()
@ -230,28 +234,25 @@ func readFile(filename string, lookupFn LookupFn) (envMap map[string]string, err
var exportRegex = regexp.MustCompile(`^\s*(?:export\s+)?(.*?)\s*$`)
func parseLine(line string, envMap map[string]string) (key string, value string, err error) {
func parseLine(line string, envMap map[string]string) (string, string, error) {
return parseLineWithLookup(line, envMap, nil)
}
func parseLineWithLookup(line string, envMap map[string]string, lookupFn LookupFn) (key string, value string, err error) {
if len(line) == 0 {
err = errors.New("zero length string")
return
func parseLineWithLookup(line string, envMap map[string]string, lookupFn LookupFn) (string, string, error) {
if line == "" {
return "", "", errors.New("zero length string")
}
// ditch the comments (but keep quoted hashes)
if strings.Contains(line, "#") {
if strings.HasPrefix(strings.TrimSpace(line), "#") || strings.Contains(line, " #") {
segmentsBetweenHashes := strings.Split(line, "#")
quotesAreOpen := false
var segmentsToKeep []string
for _, segment := range segmentsBetweenHashes {
if strings.Count(segment, "\"") == 1 || strings.Count(segment, "'") == 1 {
if quotesAreOpen {
quotesAreOpen = false
segmentsToKeep = append(segmentsToKeep, segment)
} else {
quotesAreOpen = true
}
quotesAreOpen = !quotesAreOpen
}
if len(segmentsToKeep) == 0 || quotesAreOpen {
@ -271,14 +272,14 @@ func parseLineWithLookup(line string, envMap map[string]string, lookupFn LookupF
}
if len(splitString) != 2 {
err = errors.New("can't separate key from value")
return
return "", "", errors.New("can't separate key from value")
}
key = exportRegex.ReplaceAllString(splitString[0], "$1")
key := exportRegex.ReplaceAllString(splitString[0], "$1")
// Parse the value
value = parseValue(splitString[1], envMap, lookupFn)
return
value := parseValue(splitString[1], envMap, lookupFn)
return key, value, nil
}
var (
@ -322,42 +323,24 @@ func parseValue(value string, envMap map[string]string, lookupFn LookupFn) strin
}
if singleQuotes == nil {
value = expandVariables(value, envMap, lookupFn)
value, _ = expandVariables(value, envMap, lookupFn)
}
}
return value
}
var expandVarRegex = regexp.MustCompile(`(\\)?(\$)(\()?\{?([A-Z0-9_]+)?\}?`)
func expandVariables(v string, envMap map[string]string, lookupFn LookupFn) string {
return expandVarRegex.ReplaceAllStringFunc(v, func(s string) string {
submatch := expandVarRegex.FindStringSubmatch(s)
if submatch == nil {
return s
func expandVariables(value string, envMap map[string]string, lookupFn LookupFn) (string, error) {
retVal, err := template.Substitute(value, func(k string) (string, bool) {
if v, ok := envMap[k]; ok {
return v, ok
}
if submatch[1] == "\\" || submatch[2] == "(" {
return submatch[0][1:]
} else if submatch[4] != "" {
// first check if we have defined this already earlier
if envMap[submatch[4]] != "" {
return envMap[submatch[4]]
}
if lookupFn == nil {
return ""
}
// if we have not defined it, check the lookup function provided
// by the user
s2, ok := lookupFn(submatch[4])
if ok {
return s2
}
return ""
}
return s
return lookupFn(k)
})
if err != nil {
return value, err
}
return retVal, nil
}
func doubleQuoteEscape(line string) string {
@ -369,7 +352,7 @@ func doubleQuoteEscape(line string) string {
if c == '\r' {
toReplace = `\r`
}
line = strings.Replace(line, string(c), toReplace, -1)
line = strings.ReplaceAll(line, string(c), toReplace)
}
return line
}

View File

@ -37,7 +37,6 @@ func parseBytes(src []byte, out map[string]string, lookupFn LookupFn) error {
}
if inherited {
value, ok := lookupFn(key)
if ok {
out[key] = value
@ -50,9 +49,6 @@ func parseBytes(src []byte, out map[string]string, lookupFn LookupFn) error {
if err != nil {
return err
}
if lookUpValue, ok := lookupFn(key); ok {
value = lookUpValue
}
out[key] = value
cutset = left
@ -85,7 +81,9 @@ func getStatementStart(src []byte) []byte {
}
// locateKeyName locates and parses key name and returns rest of slice
func locateKeyName(src []byte) (key string, cutset []byte, inherited bool, err error) {
func locateKeyName(src []byte) (string, []byte, bool, error) {
var key string
var inherited bool
// trim "export" and space at beginning
src = bytes.TrimLeftFunc(bytes.TrimPrefix(src, []byte(exportPrefix)), isSpace)
@ -105,9 +103,9 @@ loop:
offset = i + 1
inherited = char == '\n'
break loop
case '_':
case '_', '.':
default:
// variable name should match [A-Za-z0-9_]
// variable name should match [A-Za-z0-9_.]
if unicode.IsLetter(rchar) || unicode.IsNumber(rchar) {
continue
}
@ -124,12 +122,12 @@ loop:
// trim whitespace
key = strings.TrimRightFunc(key, unicode.IsSpace)
cutset = bytes.TrimLeftFunc(src[offset:], isSpace)
cutset := bytes.TrimLeftFunc(src[offset:], isSpace)
return key, cutset, inherited, nil
}
// extractVarValue extracts variable value and returns rest of slice
func extractVarValue(src []byte, envMap map[string]string, lookupFn LookupFn) (value string, rest []byte, err error) {
func extractVarValue(src []byte, envMap map[string]string, lookupFn LookupFn) (string, []byte, error) {
quote, isQuoted := hasQuotePrefix(src)
if !isQuoted {
// unquoted value - read until new line
@ -138,13 +136,17 @@ func extractVarValue(src []byte, envMap map[string]string, lookupFn LookupFn) (v
if end < 0 {
value := strings.Split(string(src), "#")[0] // Remove inline comments on unquoted lines
value = strings.TrimRightFunc(value, unicode.IsSpace)
return expandVariables(value, envMap, lookupFn), nil, nil
retVal, err := expandVariables(value, envMap, lookupFn)
return retVal, nil, err
}
value := strings.Split(string(src[0:end]), "#")[0]
value = strings.TrimRightFunc(value, unicode.IsSpace)
rest = src[end:]
return expandVariables(value, envMap, lookupFn), rest, nil
retVal, err := expandVariables(value, envMap, lookupFn)
return retVal, rest, err
}
// lookup quoted string terminator
@ -160,11 +162,16 @@ func extractVarValue(src []byte, envMap map[string]string, lookupFn LookupFn) (v
// trim quotes
trimFunc := isCharFunc(rune(quote))
value = string(bytes.TrimLeftFunc(bytes.TrimRightFunc(src[0:i], trimFunc), trimFunc))
value := string(bytes.TrimLeftFunc(bytes.TrimRightFunc(src[0:i], trimFunc), trimFunc))
if quote == prefixDoubleQuote {
// unescape newlines for double quote (this is compat feature)
// and expand environment variables
value = expandVariables(expandEscapes(value), envMap, lookupFn)
retVal, err := expandVariables(expandEscapes(value), envMap, lookupFn)
if err != nil {
return "", nil, err
}
value = retVal
}
return value, src[i+1:], nil
@ -187,6 +194,8 @@ func expandEscapes(str string) string {
return "\n"
case "r":
return "\r"
case "$":
return "$$"
default:
return match
}
@ -201,14 +210,14 @@ func indexOfNonSpaceChar(src []byte) int {
}
// hasQuotePrefix reports whether charset starts with single or double quote and returns quote character
func hasQuotePrefix(src []byte) (quote byte, isQuoted bool) {
func hasQuotePrefix(src []byte) (byte, bool) {
if len(src) == 0 {
return 0, false
}
switch prefix := src[0]; prefix {
switch quote := src[0]; quote {
case prefixDoubleQuote, prefixSingleQuote:
return prefix, true
return quote, true // isQuoted
default:
return 0, false
}

View File

@ -115,7 +115,7 @@ func newPathError(path Path, err error) error {
return nil
case *template.InvalidTemplateError:
return errors.Errorf(
"invalid interpolation format for %s: %#v. You may need to escape any $ with another $",
"invalid interpolation format for %s.\nYou may need to escape any $ with another $.\n%s",
path, err.Template)
default:
return errors.Wrapf(err, "error while interpolating %s", path)

View File

@ -1,5 +1,7 @@
# passed through
FOO=foo_from_env_file
ENV.WITH.DOT=ok
ENV_WITH_UNDERSCORE=ok
# overridden in example2.env
BAR=bar_from_env_file

View File

@ -25,6 +25,7 @@ services:
tags:
- foo:v1.0.0
- docker.io/username/foo:my-other-tag
- ${COMPOSE_PROJECT_NAME}:1.0.0
cap_add:

View File

@ -17,10 +17,11 @@
package loader
import (
"bytes"
"fmt"
"io/ioutil"
"io"
"os"
"path"
paths "path"
"path/filepath"
"reflect"
"regexp"
@ -42,6 +43,11 @@ import (
"gopkg.in/yaml.v2"
)
const (
DefaultSeparator = "-"
CompatibilitySeparator = "_"
)
// Options supported by Load
type Options struct {
// Skip schema validation
@ -66,6 +72,8 @@ type Options struct {
projectName string
// Indicates when the projectName was imperatively set or guessed from path
projectNameImperativelySet bool
// Set separator used for naming resources
Separator string
}
func (o *Options) SetProjectName(name string, imperativelySet bool) {
@ -154,12 +162,15 @@ func Load(configDetails types.ConfigDetails, options ...func(*Options)) (*types.
LookupValue: configDetails.LookupEnv,
TypeCastMapping: interpolateTypeCastMapping,
},
Separator: DefaultSeparator,
}
for _, op := range options {
op(opts)
}
projectName := projectName(configDetails, opts)
var configs []*types.Config
for i, file := range configDetails.ConfigFiles {
configDict := file.Config
@ -207,15 +218,6 @@ func Load(configDetails types.ConfigDetails, options ...func(*Options)) (*types.
s.EnvFile = newEnvFiles
}
projectName, projectNameImperativelySet := opts.GetProjectName()
model.Name = NormalizeProjectName(model.Name)
if !projectNameImperativelySet && model.Name != "" {
projectName = model.Name
}
if projectName != "" {
configDetails.Environment[consts.ComposeProjectName] = projectName
}
project := &types.Project{
Name: projectName,
WorkingDir: configDetails.WorkingDir,
@ -229,7 +231,7 @@ func Load(configDetails types.ConfigDetails, options ...func(*Options)) (*types.
}
if !opts.SkipNormalization {
err = normalize(project, opts.ResolvePaths)
err = normalize(project, opts.ResolvePaths, opts.Separator)
if err != nil {
return nil, err
}
@ -245,6 +247,30 @@ func Load(configDetails types.ConfigDetails, options ...func(*Options)) (*types.
return project, nil
}
func projectName(details types.ConfigDetails, opts *Options) string {
projectName, projectNameImperativelySet := opts.GetProjectName()
var pjNameFromConfigFile string
for _, configFile := range details.ConfigFiles {
yml, err := ParseYAML(configFile.Content)
if err != nil {
return ""
}
if val, ok := yml["name"]; ok && val != "" {
pjNameFromConfigFile = yml["name"].(string)
}
}
pjNameFromConfigFile = NormalizeProjectName(pjNameFromConfigFile)
if !projectNameImperativelySet && pjNameFromConfigFile != "" {
projectName = pjNameFromConfigFile
}
if _, ok := details.Environment[consts.ComposeProjectName]; !ok && projectName != "" {
details.Environment[consts.ComposeProjectName] = projectName
}
return projectName
}
func NormalizeProjectName(s string) string {
r := regexp.MustCompile("[a-z0-9_-]")
s = strings.ToLower(s)
@ -507,12 +533,12 @@ func loadServiceWithExtends(filename, name string, servicesDict map[string]inter
// Resolve the path to the imported file, and load it.
baseFilePath := absPath(workingDir, *file)
bytes, err := ioutil.ReadFile(baseFilePath)
b, err := os.ReadFile(baseFilePath)
if err != nil {
return nil, err
}
baseFile, err := parseConfig(bytes, opts)
baseFile, err := parseConfig(b, opts)
if err != nil {
return nil, err
}
@ -606,14 +632,25 @@ func resolveEnvironment(serviceConfig *types.ServiceConfig, workingDir string, l
environment := types.MappingWithEquals{}
if len(serviceConfig.EnvFile) > 0 {
if serviceConfig.Environment == nil {
serviceConfig.Environment = types.MappingWithEquals{}
}
for _, envFile := range serviceConfig.EnvFile {
filePath := absPath(workingDir, envFile)
file, err := os.Open(filePath)
if err != nil {
return err
}
defer file.Close()
fileVars, err := dotenv.ParseWithLookup(file, dotenv.LookupFn(lookupEnv))
b, err := io.ReadAll(file)
if err != nil {
return err
}
// Do not defer to avoid it inside a loop
file.Close() //nolint:errcheck
fileVars, err := dotenv.ParseWithLookup(bytes.NewBuffer(b), dotenv.LookupFn(lookupEnv))
if err != nil {
return err
}
@ -639,7 +676,7 @@ func resolveVolumePath(volume types.ServiceVolumeConfig, workingDir string, look
// Note that this is not required for Docker for Windows when specifying
// a local Windows path, because Docker for Windows translates the Windows
// path into a valid path within the VM.
if !path.IsAbs(filePath) && !isAbs(filePath) {
if !paths.IsAbs(filePath) && !isAbs(filePath) {
filePath = absPath(workingDir, filePath)
}
volume.Source = filePath
@ -793,10 +830,8 @@ func loadFileObjectConfig(name string, objType string, obj types.FileObjectConfi
logrus.Warnf("%[1]s %[2]s: %[1]s.external.name is deprecated in favor of %[1]s.name", objType, name)
obj.Name = obj.External.Name
obj.External.Name = ""
} else {
if obj.Name == "" {
obj.Name = name
}
} else if obj.Name == "" {
obj.Name = name
}
// if not "external: true"
case obj.Driver != "":
@ -928,7 +963,7 @@ func cleanTarget(target string) string {
if target == "" {
return ""
}
return path.Clean(target)
return paths.Clean(target)
}
var transformBuildConfig TransformerFunc = func(data interface{}) (interface{}, error) {

View File

@ -299,7 +299,7 @@ func mergeLoggingConfig(dst, src reflect.Value) error {
return nil
}
// nolint: unparam
//nolint: unparam
func mergeUlimitsConfig(dst, src reflect.Value) error {
if src.Interface() != reflect.Zero(reflect.TypeOf(src.Interface())).Interface() {
dst.Elem().Set(src.Elem())
@ -307,7 +307,7 @@ func mergeUlimitsConfig(dst, src reflect.Value) error {
return nil
}
// nolint: unparam
//nolint: unparam
func mergeServiceNetworkConfig(dst, src reflect.Value) error {
if src.Interface() != reflect.Zero(reflect.TypeOf(src.Interface())).Interface() {
dst.Elem().FieldByName("Aliases").Set(src.Elem().FieldByName("Aliases"))

View File

@ -28,7 +28,7 @@ import (
)
// normalize compose project by moving deprecated attributes to their canonical position and injecting implicit defaults
func normalize(project *types.Project, resolvePaths bool) error {
func normalize(project *types.Project, resolvePaths bool, separator string) error {
absWorkingDir, err := filepath.Abs(project.WorkingDir)
if err != nil {
return err
@ -110,7 +110,7 @@ func normalize(project *types.Project, resolvePaths bool) error {
project.Services[i] = s
}
setNameFromKey(project)
setNameFromKey(project, separator)
return nil
}
@ -143,31 +143,31 @@ func absComposeFiles(composeFiles []string) ([]string, error) {
}
// Resources with no explicit name are actually named by their key in map
func setNameFromKey(project *types.Project) {
func setNameFromKey(project *types.Project, separator string) {
for i, n := range project.Networks {
if n.Name == "" {
n.Name = fmt.Sprintf("%s_%s", project.Name, i)
n.Name = fmt.Sprintf("%s%s%s", project.Name, separator, i)
project.Networks[i] = n
}
}
for i, v := range project.Volumes {
if v.Name == "" {
v.Name = fmt.Sprintf("%s_%s", project.Name, i)
v.Name = fmt.Sprintf("%s%s%s", project.Name, separator, i)
project.Volumes[i] = v
}
}
for i, c := range project.Configs {
if c.Name == "" {
c.Name = fmt.Sprintf("%s_%s", project.Name, i)
c.Name = fmt.Sprintf("%s%s%s", project.Name, separator, i)
project.Configs[i] = c
}
}
for i, s := range project.Secrets {
if s.Name == "" {
s.Name = fmt.Sprintf("%s_%s", project.Name, i)
s.Name = fmt.Sprintf("%s%s%s", project.Name, separator, i)
project.Secrets[i] = s
}
}

View File

@ -38,6 +38,12 @@ func checkConsistency(project *types.Project) error {
}
}
for dependedService := range s.DependsOn {
if _, err := project.GetService(dependedService); err != nil {
return errors.Wrap(errdefs.ErrInvalid, fmt.Sprintf("service %q depends on undefined service %s", s.Name, dependedService))
}
}
if strings.HasPrefix(s.NetworkMode, types.ServicePrefix) {
serviceName := s.NetworkMode[len(types.ServicePrefix):]
if _, err := project.GetServices(serviceName); err != nil {
@ -46,12 +52,9 @@ func checkConsistency(project *types.Project) error {
}
for _, volume := range s.Volumes {
switch volume.Type {
case types.VolumeTypeVolume:
if volume.Source != "" { // non anonymous volumes
if _, ok := project.Volumes[volume.Source]; !ok {
return errors.Wrap(errdefs.ErrInvalid, fmt.Sprintf("service %q refers to undefined volume %s", s.Name, volume.Source))
}
if volume.Type == types.VolumeTypeVolume && volume.Source != "" { // non anonymous volumes
if _, ok := project.Volumes[volume.Source]; !ok {
return errors.Wrap(errdefs.ErrInvalid, fmt.Sprintf("service %q refers to undefined volume %s", s.Name, volume.Source))
}
}
}

View File

@ -44,7 +44,7 @@ func isAbs(path string) (b bool) {
// volumeNameLen returns length of the leading volume name on Windows.
// It returns 0 elsewhere.
// nolint: gocyclo
//nolint: gocyclo
func volumeNameLen(path string) int {
if len(path) < 2 {
return 0

View File

@ -19,6 +19,7 @@ package template
import (
"fmt"
"regexp"
"sort"
"strings"
"github.com/sirupsen/logrus"
@ -27,7 +28,7 @@ import (
var delimiter = "\\$"
var substitutionNamed = "[_a-z][_a-z0-9]*"
var substitutionBraced = "[_a-z][_a-z0-9]*(?::?[-?](.*}|[^}]*))?"
var substitutionBraced = "[_a-z][_a-z0-9]*(?::?[-+?](.*}|[^}]*))?"
var patternString = fmt.Sprintf(
"%s(?i:(?P<escaped>%s)|(?P<named>%s)|{(?P<braced>%s)}|(?P<invalid>))",
@ -60,10 +61,12 @@ type SubstituteFunc func(string, Mapping) (string, bool, error)
// SubstituteWith substitute variables in the string with their values.
// It accepts additional substitute function.
func SubstituteWith(template string, mapping Mapping, pattern *regexp.Regexp, subsFuncs ...SubstituteFunc) (string, error) {
if len(subsFuncs) == 0 {
subsFuncs = getDefaultSortedSubstitutionFunctions(template)
}
var err error
if len(subsFuncs) == 0 {
_, subsFunc := getSubstitutionFunctionForTemplate(template)
subsFuncs = []SubstituteFunc{subsFunc}
}
result := pattern.ReplaceAllStringFunc(template, func(substring string) string {
closingBraceIndex := getFirstBraceClosingIndex(substring)
rest := ""
@ -121,23 +124,31 @@ func SubstituteWith(template string, mapping Mapping, pattern *regexp.Regexp, su
return result, err
}
func getDefaultSortedSubstitutionFunctions(template string, fns ...SubstituteFunc) []SubstituteFunc {
hyphenIndex := strings.IndexByte(template, '-')
questionIndex := strings.IndexByte(template, '?')
if hyphenIndex < 0 || hyphenIndex > questionIndex {
return []SubstituteFunc{
requiredNonEmpty,
required,
softDefault,
hardDefault,
func getSubstitutionFunctionForTemplate(template string) (string, SubstituteFunc) {
interpolationMapping := []struct {
string
SubstituteFunc
}{
{":?", requiredErrorWhenEmptyOrUnset},
{"?", requiredErrorWhenUnset},
{":-", defaultWhenEmptyOrUnset},
{"-", defaultWhenUnset},
{":+", defaultWhenNotEmpty},
{"+", defaultWhenSet},
}
sort.Slice(interpolationMapping, func(i, j int) bool {
idxI := strings.Index(template, interpolationMapping[i].string)
idxJ := strings.Index(template, interpolationMapping[j].string)
if idxI < 0 {
return false
}
}
return []SubstituteFunc{
softDefault,
hardDefault,
requiredNonEmpty,
required,
}
if idxJ < 0 {
return true
}
return idxI < idxJ
})
return interpolationMapping[0].string, interpolationMapping[0].SubstituteFunc
}
func getFirstBraceClosingIndex(s string) int {
@ -203,9 +214,10 @@ func recurseExtract(value interface{}, pattern *regexp.Regexp) map[string]Variab
}
type Variable struct {
Name string
DefaultValue string
Required bool
Name string
DefaultValue string
PresenceValue string
Required bool
}
func extractVariable(value interface{}, pattern *regexp.Regexp) ([]Variable, bool) {
@ -229,6 +241,7 @@ func extractVariable(value interface{}, pattern *regexp.Regexp) ([]Variable, boo
}
name := val
var defaultValue string
var presenceValue string
var required bool
switch {
case strings.Contains(val, ":?"):
@ -241,37 +254,52 @@ func extractVariable(value interface{}, pattern *regexp.Regexp) ([]Variable, boo
name, defaultValue = partition(val, ":-")
case strings.Contains(val, "-"):
name, defaultValue = partition(val, "-")
case strings.Contains(val, ":+"):
name, presenceValue = partition(val, ":+")
case strings.Contains(val, "+"):
name, presenceValue = partition(val, "+")
}
values = append(values, Variable{
Name: name,
DefaultValue: defaultValue,
Required: required,
Name: name,
DefaultValue: defaultValue,
PresenceValue: presenceValue,
Required: required,
})
}
return values, len(values) > 0
}
// Soft default (fall back if unset or empty)
func softDefault(substitution string, mapping Mapping) (string, bool, error) {
sep := ":-"
if !strings.Contains(substitution, sep) {
return "", false, nil
}
name, defaultValue := partition(substitution, sep)
defaultValue, err := Substitute(defaultValue, mapping)
if err != nil {
return "", false, err
}
value, ok := mapping(name)
if !ok || value == "" {
return defaultValue, true, nil
}
return value, true, nil
func defaultWhenEmptyOrUnset(substitution string, mapping Mapping) (string, bool, error) {
return withDefaultWhenAbsence(substitution, mapping, true)
}
// Hard default (fall back if-and-only-if empty)
func hardDefault(substitution string, mapping Mapping) (string, bool, error) {
sep := "-"
func defaultWhenUnset(substitution string, mapping Mapping) (string, bool, error) {
return withDefaultWhenAbsence(substitution, mapping, false)
}
func defaultWhenNotEmpty(substitution string, mapping Mapping) (string, bool, error) {
return withDefaultWhenPresence(substitution, mapping, true)
}
func defaultWhenSet(substitution string, mapping Mapping) (string, bool, error) {
return withDefaultWhenPresence(substitution, mapping, false)
}
func requiredErrorWhenEmptyOrUnset(substitution string, mapping Mapping) (string, bool, error) {
return withRequired(substitution, mapping, ":?", func(v string) bool { return v != "" })
}
func requiredErrorWhenUnset(substitution string, mapping Mapping) (string, bool, error) {
return withRequired(substitution, mapping, "?", func(_ string) bool { return true })
}
func withDefaultWhenPresence(substitution string, mapping Mapping, notEmpty bool) (string, bool, error) {
sep := "+"
if notEmpty {
sep = ":+"
}
if !strings.Contains(substitution, sep) {
return "", false, nil
}
@ -281,18 +309,30 @@ func hardDefault(substitution string, mapping Mapping) (string, bool, error) {
return "", false, err
}
value, ok := mapping(name)
if !ok {
if ok && (!notEmpty || (notEmpty && value != "")) {
return defaultValue, true, nil
}
return value, true, nil
}
func requiredNonEmpty(substitution string, mapping Mapping) (string, bool, error) {
return withRequired(substitution, mapping, ":?", func(v string) bool { return v != "" })
}
func required(substitution string, mapping Mapping) (string, bool, error) {
return withRequired(substitution, mapping, "?", func(_ string) bool { return true })
func withDefaultWhenAbsence(substitution string, mapping Mapping, emptyOrUnset bool) (string, bool, error) {
sep := "-"
if emptyOrUnset {
sep = ":-"
}
if !strings.Contains(substitution, sep) {
return "", false, nil
}
name, defaultValue := partition(substitution, sep)
defaultValue, err := Substitute(defaultValue, mapping)
if err != nil {
return "", false, err
}
value, ok := mapping(name)
if !ok || (emptyOrUnset && value == "") {
return defaultValue, true, nil
}
return value, true, nil
}
func withRequired(substitution string, mapping Mapping, sep string, valid func(string) bool) (string, bool, error) {

View File

@ -23,7 +23,7 @@ import (
"sort"
"github.com/distribution/distribution/v3/reference"
"github.com/opencontainers/go-digest"
godigest "github.com/opencontainers/go-digest"
"golang.org/x/sync/errgroup"
)
@ -142,18 +142,19 @@ func (p Project) WithServices(names []string, fn ServiceFunc) error {
return p.withServices(names, fn, map[string]bool{})
}
func (p Project) withServices(names []string, fn ServiceFunc, done map[string]bool) error {
func (p Project) withServices(names []string, fn ServiceFunc, seen map[string]bool) error {
services, err := p.GetServices(names...)
if err != nil {
return err
}
for _, service := range services {
if done[service.Name] {
if seen[service.Name] {
continue
}
seen[service.Name] = true
dependencies := service.GetDependencies()
if len(dependencies) > 0 {
err := p.withServices(dependencies, fn, done)
err := p.withServices(dependencies, fn, seen)
if err != nil {
return err
}
@ -161,7 +162,6 @@ func (p Project) withServices(names []string, fn ServiceFunc, done map[string]bo
if err := fn(service); err != nil {
return err
}
done[service.Name] = true
}
return nil
}
@ -311,7 +311,7 @@ func (p *Project) ForServices(names []string) error {
}
// ResolveImages updates services images to include digest computed by a resolver function
func (p *Project) ResolveImages(resolver func(named reference.Named) (digest.Digest, error)) error {
func (p *Project) ResolveImages(resolver func(named reference.Named) (godigest.Digest, error)) error {
eg := errgroup.Group{}
for i, s := range p.Services {
idx := i

View File

@ -204,26 +204,26 @@ func (s *ServiceConfig) NetworksByPriority() []string {
}
const (
//PullPolicyAlways always pull images
// PullPolicyAlways always pull images
PullPolicyAlways = "always"
//PullPolicyNever never pull images
// PullPolicyNever never pull images
PullPolicyNever = "never"
//PullPolicyIfNotPresent pull missing images
// PullPolicyIfNotPresent pull missing images
PullPolicyIfNotPresent = "if_not_present"
//PullPolicyMissing pull missing images
// PullPolicyMissing pull missing images
PullPolicyMissing = "missing"
//PullPolicyBuild force building images
// PullPolicyBuild force building images
PullPolicyBuild = "build"
)
const (
//RestartPolicyAlways always restart the container if it stops
// RestartPolicyAlways always restart the container if it stops
RestartPolicyAlways = "always"
//RestartPolicyOnFailure restart the container if it exits due to an error
// RestartPolicyOnFailure restart the container if it exits due to an error
RestartPolicyOnFailure = "on-failure"
//RestartPolicyNo do not automatically restart the container
// RestartPolicyNo do not automatically restart the container
RestartPolicyNo = "no"
//RestartPolicyUnlessStopped always restart the container unless the container is stopped (manually or otherwise)
// RestartPolicyUnlessStopped always restart the container unless the container is stopped (manually or otherwise)
RestartPolicyUnlessStopped = "unless-stopped"
)
@ -275,8 +275,8 @@ func (s ServiceConfig) GetDependencies() []string {
type set map[string]struct{}
func (s set) append(strings ...string) {
for _, str := range strings {
func (s set) append(strs ...string) {
for _, str := range strs {
s[str] = struct{}{}
}
}
@ -459,9 +459,9 @@ func (s SSHKey) MarshalYAML() (interface{}, error) {
// MarshalJSON makes SSHKey implement json.Marshaller
func (s SSHKey) MarshalJSON() ([]byte, error) {
if s.Path == "" {
return []byte(fmt.Sprintf(`"%s"`, s.ID)), nil
return []byte(fmt.Sprintf(`%q`, s.ID)), nil
}
return []byte(fmt.Sprintf(`"%s": %s`, s.ID, s.Path)), nil
return []byte(fmt.Sprintf(`%q: %s`, s.ID, s.Path)), nil
}
// MappingWithColon is a mapping type that can be converted from a list of
@ -617,10 +617,11 @@ type PlacementPreferences struct {
// ServiceNetworkConfig is the network configuration for a service
type ServiceNetworkConfig struct {
Priority int `yaml:",omitempty" json:"priotirt,omitempty"`
Aliases []string `yaml:",omitempty" json:"aliases,omitempty"`
Ipv4Address string `mapstructure:"ipv4_address" yaml:"ipv4_address,omitempty" json:"ipv4_address,omitempty"`
Ipv6Address string `mapstructure:"ipv6_address" yaml:"ipv6_address,omitempty" json:"ipv6_address,omitempty"`
Priority int `yaml:",omitempty" json:"priority,omitempty"`
Aliases []string `yaml:",omitempty" json:"aliases,omitempty"`
Ipv4Address string `mapstructure:"ipv4_address" yaml:"ipv4_address,omitempty" json:"ipv4_address,omitempty"`
Ipv6Address string `mapstructure:"ipv6_address" yaml:"ipv6_address,omitempty" json:"ipv6_address,omitempty"`
LinkLocalIPs []string `mapstructure:"link_local_ips" yaml:"link_local_ips,omitempty" json:"link_local_ips,omitempty"`
Extensions map[string]interface{} `yaml:",inline" json:"-"`
}