vendor: update to compose-go 1.13.4

Signed-off-by: Nick Sieger <nick@nicksieger.com>
This commit is contained in:
Nick Sieger
2023-04-21 10:52:58 -05:00
parent afcaa8df5f
commit 956a1be656
34 changed files with 1130 additions and 770 deletions

View File

@ -17,29 +17,65 @@
package cli
import (
"fmt"
"bytes"
"io"
"os"
"path/filepath"
"strings"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/compose-spec/compose-go/consts"
"github.com/compose-spec/compose-go/dotenv"
"github.com/compose-spec/compose-go/errdefs"
"github.com/compose-spec/compose-go/loader"
"github.com/compose-spec/compose-go/types"
"github.com/compose-spec/compose-go/utils"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
// ProjectOptions groups the command line options recommended for a Compose implementation
// ProjectOptions provides common configuration for loading a project.
type ProjectOptions struct {
Name string
WorkingDir string
// Name is a valid Compose project name to be used or empty.
//
// If empty, the project loader will automatically infer a reasonable
// project name if possible.
Name string
// WorkingDir is a file path to use as the project directory or empty.
//
// If empty, the project loader will automatically infer a reasonable
// working directory if possible.
WorkingDir string
// ConfigPaths are file paths to one or more Compose files.
//
// These are applied in order by the loader following the merge logic
// as described in the spec.
//
// The first entry is required and is the primary Compose file.
// For convenience, WithConfigFileEnv and WithDefaultConfigPath
// are provided to populate this in a predictable manner.
ConfigPaths []string
// Environment are additional environment variables to make available
// for interpolation.
//
// NOTE: For security, the loader does not automatically expose any
// process environment variables. For convenience, WithOsEnv can be
// used if appropriate.
Environment map[string]string
EnvFile string
// EnvFiles are file paths to ".env" files with additional environment
// variable data.
//
// These are loaded in-order, so it is possible to override variables or
// in subsequent files.
//
// This field is optional, but any file paths that are included here must
// exist or an error will be returned during load.
EnvFiles []string
loadOptions []func(*loader.Options)
}
@ -63,8 +99,12 @@ func NewProjectOptions(configs []string, opts ...ProjectOptionsFn) (*ProjectOpti
// WithName defines ProjectOptions' name
func WithName(name string) ProjectOptionsFn {
return func(o *ProjectOptions) error {
// a project (once loaded) cannot have an empty name
// however, on the options object, the name is optional: if unset,
// a name will be inferred by the loader, so it's legal to set the
// name to an empty string here
if name != loader.NormalizeProjectName(name) {
return fmt.Errorf("%q is not a valid project name", name)
return loader.InvalidProjectNameErr(name)
}
o.Name = name
return nil
@ -187,9 +227,19 @@ func WithOsEnv(o *ProjectOptions) error {
}
// WithEnvFile set an alternate env file
// deprecated - use WithEnvFiles
func WithEnvFile(file string) ProjectOptionsFn {
var files []string
if file != "" {
files = []string{file}
}
return WithEnvFiles(files...)
}
// WithEnvFiles set alternate env files
func WithEnvFiles(file ...string) ProjectOptionsFn {
return func(options *ProjectOptions) error {
options.EnvFile = file
options.EnvFiles = file
return nil
}
}
@ -200,7 +250,7 @@ func WithDotEnv(o *ProjectOptions) error {
if err != nil {
return err
}
envMap, err := GetEnvFromFile(o.Environment, wd, o.EnvFile)
envMap, err := GetEnvFromFile(o.Environment, wd, o.EnvFiles)
if err != nil {
return err
}
@ -213,55 +263,63 @@ func WithDotEnv(o *ProjectOptions) error {
return nil
}
func GetEnvFromFile(currentEnv map[string]string, workingDir string, filename string) (map[string]string, error) {
func GetEnvFromFile(currentEnv map[string]string, workingDir string, filenames []string) (map[string]string, error) {
envMap := make(map[string]string)
dotEnvFile := filename
if dotEnvFile == "" {
dotEnvFile = filepath.Join(workingDir, ".env")
dotEnvFiles := filenames
if len(dotEnvFiles) == 0 {
dotEnvFiles = append(dotEnvFiles, filepath.Join(workingDir, ".env"))
}
abs, err := filepath.Abs(dotEnvFile)
if err != nil {
return envMap, err
}
dotEnvFile = abs
s, err := os.Stat(dotEnvFile)
if os.IsNotExist(err) {
if filename != "" {
return nil, errors.Errorf("Couldn't find env file: %s", filename)
for _, dotEnvFile := range dotEnvFiles {
abs, err := filepath.Abs(dotEnvFile)
if err != nil {
return envMap, err
}
return envMap, nil
}
if err != nil {
return envMap, err
}
dotEnvFile = abs
if s.IsDir() {
if filename == "" {
return envMap, nil
s, err := os.Stat(dotEnvFile)
if os.IsNotExist(err) {
if len(filenames) == 0 {
return envMap, nil
}
return envMap, errors.Errorf("Couldn't find env file: %s", dotEnvFile)
}
return envMap, errors.Errorf("%s is a directory", dotEnvFile)
}
file, err := os.Open(dotEnvFile)
if err != nil {
return envMap, errors.Wrapf(err, "failed to read %s", dotEnvFile)
}
defer file.Close()
env, err := dotenv.ParseWithLookup(file, func(k string) (string, bool) {
v, ok := currentEnv[k]
if !ok {
return "", false
if err != nil {
return envMap, err
}
if s.IsDir() {
if len(filenames) == 0 {
return envMap, nil
}
return envMap, errors.Errorf("%s is a directory", dotEnvFile)
}
b, err := os.ReadFile(dotEnvFile)
if os.IsNotExist(err) {
return nil, errors.Errorf("Couldn't read env file: %s", dotEnvFile)
}
if err != nil {
return envMap, err
}
env, err := dotenv.ParseWithLookup(bytes.NewReader(b), func(k string) (string, bool) {
v, ok := envMap[k]
if ok {
return v, true
}
v, ok = currentEnv[k]
if !ok {
return "", false
}
return v, true
})
if err != nil {
return envMap, errors.Wrapf(err, "failed to read %s", dotEnvFile)
}
for k, v := range env {
envMap[k] = v
}
return v, true
})
if err != nil {
return envMap, errors.Wrapf(err, "failed to read %s", dotEnvFile)
}
for k, v := range env {
envMap[k] = v
}
return envMap, nil
@ -393,7 +451,10 @@ func withNamePrecedenceLoad(absWorkingDir string, options *ProjectOptions) func(
} else if nameFromEnv, ok := options.Environment[consts.ComposeProjectName]; ok && nameFromEnv != "" {
opts.SetProjectName(nameFromEnv, true)
} else {
opts.SetProjectName(filepath.Base(absWorkingDir), false)
opts.SetProjectName(
loader.NormalizeProjectName(filepath.Base(absWorkingDir)),
false,
)
}
}
}

View File

@ -20,4 +20,5 @@ const (
ComposeProjectName = "COMPOSE_PROJECT_NAME"
ComposePathSeparator = "COMPOSE_PATH_SEPARATOR"
ComposeFilePath = "COMPOSE_FILE"
ComposeProfiles = "COMPOSE_PROFILES"
)

View File

@ -111,8 +111,13 @@ func Read(filenames ...string) (map[string]string, error) {
// UnmarshalBytesWithLookup parses env file from byte slice of chars, returning a map of keys and values.
func UnmarshalBytesWithLookup(src []byte, lookupFn LookupFn) (map[string]string, error) {
return UnmarshalWithLookup(string(src), lookupFn)
}
// UnmarshalWithLookup parses env file from string, returning a map of keys and values.
func UnmarshalWithLookup(src string, lookupFn LookupFn) (map[string]string, error) {
out := make(map[string]string)
err := newParser().parseBytes(src, out, lookupFn)
err := newParser().parse(src, out, lookupFn)
return out, err
}

View File

@ -1,7 +1,6 @@
package dotenv
import (
"bytes"
"errors"
"fmt"
"regexp"
@ -31,14 +30,14 @@ func newParser() *parser {
}
}
func (p *parser) parseBytes(src []byte, out map[string]string, lookupFn LookupFn) error {
func (p *parser) parse(src string, out map[string]string, lookupFn LookupFn) error {
cutset := src
if lookupFn == nil {
lookupFn = noLookupFn
}
for {
cutset = p.getStatementStart(cutset)
if cutset == nil {
if cutset == "" {
// reached end of file
break
}
@ -75,10 +74,10 @@ func (p *parser) parseBytes(src []byte, out map[string]string, lookupFn LookupFn
// getStatementPosition returns position of statement begin.
//
// It skips any comment line or non-whitespace character.
func (p *parser) getStatementStart(src []byte) []byte {
func (p *parser) getStatementStart(src string) string {
pos := p.indexOfNonSpaceChar(src)
if pos == -1 {
return nil
return ""
}
src = src[pos:]
@ -87,70 +86,69 @@ func (p *parser) getStatementStart(src []byte) []byte {
}
// skip comment section
pos = bytes.IndexFunc(src, isCharFunc('\n'))
pos = strings.IndexFunc(src, isCharFunc('\n'))
if pos == -1 {
return nil
return ""
}
return p.getStatementStart(src[pos:])
}
// locateKeyName locates and parses key name and returns rest of slice
func (p *parser) locateKeyName(src []byte) (string, []byte, bool, error) {
func (p *parser) locateKeyName(src string) (string, string, bool, error) {
var key string
var inherited bool
// trim "export" and space at beginning
src = bytes.TrimLeftFunc(exportRegex.ReplaceAll(src, nil), isSpace)
src = strings.TrimLeftFunc(exportRegex.ReplaceAllString(src, ""), isSpace)
// locate key name end and validate it in single loop
offset := 0
loop:
for i, char := range src {
rchar := rune(char)
if isSpace(rchar) {
for i, rune := range src {
if isSpace(rune) {
continue
}
switch char {
switch rune {
case '=', ':', '\n':
// library also supports yaml-style value declaration
key = string(src[0:i])
offset = i + 1
inherited = char == '\n'
inherited = rune == '\n'
break loop
case '_', '.', '-', '[', ']':
default:
// variable name should match [A-Za-z0-9_.-]
if unicode.IsLetter(rchar) || unicode.IsNumber(rchar) {
if unicode.IsLetter(rune) || unicode.IsNumber(rune) {
continue
}
return "", nil, inherited, fmt.Errorf(
return "", "", inherited, fmt.Errorf(
`line %d: unexpected character %q in variable name`,
p.line, string(char))
p.line, string(rune))
}
}
if len(src) == 0 {
return "", nil, inherited, errors.New("zero length string")
if src == "" {
return "", "", inherited, errors.New("zero length string")
}
// trim whitespace
key = strings.TrimRightFunc(key, unicode.IsSpace)
cutset := bytes.TrimLeftFunc(src[offset:], isSpace)
cutset := strings.TrimLeftFunc(src[offset:], isSpace)
return key, cutset, inherited, nil
}
// extractVarValue extracts variable value and returns rest of slice
func (p *parser) extractVarValue(src []byte, envMap map[string]string, lookupFn LookupFn) (string, []byte, error) {
func (p *parser) extractVarValue(src string, envMap map[string]string, lookupFn LookupFn) (string, string, error) {
quote, isQuoted := hasQuotePrefix(src)
if !isQuoted {
// unquoted value - read until new line
value, rest, _ := bytes.Cut(src, []byte("\n"))
value, rest, _ := strings.Cut(src, "\n")
p.line++
// Remove inline comments on unquoted lines
value, _, _ = bytes.Cut(value, []byte(" #"))
value = bytes.TrimRightFunc(value, unicode.IsSpace)
value, _, _ = strings.Cut(value, " #")
value = strings.TrimRightFunc(value, unicode.IsSpace)
retVal, err := expandVariables(string(value), envMap, lookupFn)
return retVal, rest, err
}
@ -176,7 +174,7 @@ func (p *parser) extractVarValue(src []byte, envMap map[string]string, lookupFn
// variables on the result
retVal, err := expandVariables(expandEscapes(value), envMap, lookupFn)
if err != nil {
return "", nil, err
return "", "", err
}
value = retVal
}
@ -185,12 +183,12 @@ func (p *parser) extractVarValue(src []byte, envMap map[string]string, lookupFn
}
// return formatted error if quoted string is not terminated
valEndIndex := bytes.IndexFunc(src, isCharFunc('\n'))
valEndIndex := strings.IndexFunc(src, isCharFunc('\n'))
if valEndIndex == -1 {
valEndIndex = len(src)
}
return "", nil, fmt.Errorf("line %d: unterminated quoted value %s", p.line, src[:valEndIndex])
return "", "", fmt.Errorf("line %d: unterminated quoted value %s", p.line, src[:valEndIndex])
}
func expandEscapes(str string) string {
@ -225,8 +223,8 @@ func expandEscapes(str string) string {
return out
}
func (p *parser) indexOfNonSpaceChar(src []byte) int {
return bytes.IndexFunc(src, func(r rune) bool {
func (p *parser) indexOfNonSpaceChar(src string) int {
return strings.IndexFunc(src, func(r rune) bool {
if r == '\n' {
p.line++
}
@ -235,8 +233,8 @@ func (p *parser) indexOfNonSpaceChar(src []byte) int {
}
// hasQuotePrefix reports whether charset starts with single or double quote and returns quote character
func hasQuotePrefix(src []byte) (byte, bool) {
if len(src) == 0 {
func hasQuotePrefix(src string) (byte, bool) {
if src == "" {
return 0, false
}

View File

@ -72,7 +72,7 @@ func recursiveInterpolate(value interface{}, path Path, opts Options) (interface
switch value := value.(type) {
case string:
newValue, err := opts.Substitute(value, template.Mapping(opts.LookupValue))
if err != nil || newValue == value {
if err != nil {
return value, newPathError(path, err)
}
caster, ok := opts.getCasterForPath(path)

View File

@ -1,7 +1,13 @@
name: Full_Example_project_name
name: full_example_project_name
services:
foo:
bar:
build:
dockerfile_inline: |
FROM alpine
RUN echo "hello" > /world.txt
foo:
build:
context: ./dir
dockerfile: Dockerfile
@ -15,6 +21,8 @@ services:
- foo
- bar
labels: [FOO=BAR]
additional_contexts:
foo: /bar
secrets:
- secret1
- source: secret2

View File

@ -22,6 +22,7 @@ import (
interp "github.com/compose-spec/compose-go/interpolation"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
var interpolateTypeCastMapping = map[interp.Path]interp.Cast{
@ -114,9 +115,15 @@ func toFloat32(value string) (interface{}, error) {
// should match http://yaml.org/type/bool.html
func toBoolean(value string) (interface{}, error) {
switch strings.ToLower(value) {
case "y", "yes", "true", "on":
case "true":
return true, nil
case "n", "no", "false", "off":
case "false":
return false, nil
case "y", "yes", "on":
logrus.Warnf("%q for boolean is not supported by YAML 1.2, please use `true`", value)
return true, nil
case "n", "no", "off":
logrus.Warnf("%q for boolean is not supported by YAML 1.2, please use `false`", value)
return false, nil
default:
return nil, errors.Errorf("invalid boolean: %s", value)

View File

@ -37,7 +37,7 @@ import (
"github.com/mitchellh/mapstructure"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"gopkg.in/yaml.v2"
"gopkg.in/yaml.v3"
)
// Options supported by Load
@ -69,7 +69,7 @@ type Options struct {
}
func (o *Options) SetProjectName(name string, imperativelySet bool) {
o.projectName = NormalizeProjectName(name)
o.projectName = name
o.projectNameImperativelySet = imperativelySet
}
@ -138,6 +138,14 @@ func ParseYAML(source []byte) (map[string]interface{}, error) {
if err := yaml.Unmarshal(source, &cfg); err != nil {
return nil, err
}
stringMap, ok := cfg.(map[string]interface{})
if ok {
converted, err := convertToStringKeysRecursive(stringMap, "")
if err != nil {
return nil, err
}
return converted.(map[string]interface{}), nil
}
cfgMap, ok := cfg.(map[interface{}]interface{})
if !ok {
return nil, errors.Errorf("Top-level object must be a mapping")
@ -185,7 +193,7 @@ func Load(configDetails types.ConfigDetails, options ...func(*Options)) (*types.
}
dict, err := parseConfig(file.Content, opts)
if err != nil {
return nil, err
return nil, fmt.Errorf("parsing %s: %w", file.Filename, err)
}
configDict = dict
file.Config = dict
@ -194,7 +202,7 @@ func Load(configDetails types.ConfigDetails, options ...func(*Options)) (*types.
if !opts.SkipValidation {
if err := schema.Validate(configDict); err != nil {
return nil, err
return nil, fmt.Errorf("validating %s: %w", file.Filename, err)
}
}
@ -233,7 +241,7 @@ func Load(configDetails types.ConfigDetails, options ...func(*Options)) (*types.
}
if !opts.SkipNormalization {
err = normalize(project, opts.ResolvePaths)
err = Normalize(project, opts.ResolvePaths)
if err != nil {
return nil, err
}
@ -246,40 +254,82 @@ func Load(configDetails types.ConfigDetails, options ...func(*Options)) (*types.
}
}
if len(opts.Profiles) > 0 {
project.ApplyProfiles(opts.Profiles)
if profiles, ok := project.Environment[consts.ComposeProfiles]; ok && len(opts.Profiles) == 0 {
opts.Profiles = strings.Split(profiles, ",")
}
project.ApplyProfiles(opts.Profiles)
err = project.ResolveServicesEnvironment(opts.discardEnvFiles)
return project, err
}
func InvalidProjectNameErr(v string) error {
return fmt.Errorf(
"%q is not a valid project name: it must contain only "+
"characters from [a-z0-9_-] and start with [a-z0-9]", v,
)
}
// projectName determines the canonical name to use for the project considering
// the loader Options as well as `name` fields in Compose YAML fields (which
// also support interpolation).
//
// TODO(milas): restructure loading so that we don't need to re-parse the YAML
// here, as it's both wasteful and makes this code error-prone.
func projectName(details types.ConfigDetails, opts *Options) (string, error) {
projectName, projectNameImperativelySet := opts.GetProjectName()
var pjNameFromConfigFile string
for _, configFile := range details.ConfigFiles {
yml, err := ParseYAML(configFile.Content)
if err != nil {
return "", nil
// if user did NOT provide a name explicitly, then see if one is defined
// in any of the config files
if !projectNameImperativelySet {
var pjNameFromConfigFile string
for _, configFile := range details.ConfigFiles {
yml, err := ParseYAML(configFile.Content)
if err != nil {
// HACK: the way that loading is currently structured, this is
// a duplicative parse just for the `name`. if it fails, we
// give up but don't return the error, knowing that it'll get
// caught downstream for us
return "", nil
}
if val, ok := yml["name"]; ok && val != "" {
sVal, ok := val.(string)
if !ok {
// HACK: see above - this is a temporary parsed version
// that hasn't been schema-validated, but we don't want
// to be the ones to actually report that, so give up,
// knowing that it'll get caught downstream for us
return "", nil
}
pjNameFromConfigFile = sVal
}
}
if val, ok := yml["name"]; ok && val != "" {
pjNameFromConfigFile = yml["name"].(string)
if !opts.SkipInterpolation {
interpolated, err := interp.Interpolate(
map[string]interface{}{"name": pjNameFromConfigFile},
*opts.Interpolate,
)
if err != nil {
return "", err
}
pjNameFromConfigFile = interpolated["name"].(string)
}
}
if !opts.SkipInterpolation {
interpolated, err := interp.Interpolate(map[string]interface{}{"name": pjNameFromConfigFile}, *opts.Interpolate)
if err != nil {
return "", err
pjNameFromConfigFile = NormalizeProjectName(pjNameFromConfigFile)
if pjNameFromConfigFile != "" {
projectName = pjNameFromConfigFile
}
pjNameFromConfigFile = interpolated["name"].(string)
}
pjNameFromConfigFile = NormalizeProjectName(pjNameFromConfigFile)
if !projectNameImperativelySet && pjNameFromConfigFile != "" {
projectName = pjNameFromConfigFile
}
if projectName == "" {
return "", errors.New("project name must not be empty")
}
if NormalizeProjectName(projectName) != projectName {
return "", InvalidProjectNameErr(projectName)
}
// TODO(milas): this should probably ALWAYS set (overriding any existing)
if _, ok := details.Environment[consts.ComposeProjectName]; !ok && projectName != "" {
details.Environment[consts.ComposeProjectName] = projectName
}
@ -304,6 +354,8 @@ func parseConfig(b []byte, opts *Options) (map[string]interface{}, error) {
return yml, err
}
const extensions = "#extensions" // Using # prefix, we prevent risk to conflict with an actual yaml key
func groupXFieldsIntoExtensions(dict map[string]interface{}) map[string]interface{} {
extras := map[string]interface{}{}
for key, value := range dict {
@ -316,7 +368,7 @@ func groupXFieldsIntoExtensions(dict map[string]interface{}) map[string]interfac
}
}
if len(extras) > 0 {
dict["extensions"] = extras
dict[extensions] = extras
}
return dict
}
@ -355,7 +407,7 @@ func loadSections(filename string, config map[string]interface{}, configDetails
if err != nil {
return nil, err
}
extensions := getSection(config, "extensions")
extensions := getSection(config, extensions)
if len(extensions) > 0 {
cfg.Extensions = extensions
}
@ -450,6 +502,22 @@ func createTransformHook(additionalTransformers ...Transformer) mapstructure.Dec
// keys need to be converted to strings for jsonschema
func convertToStringKeysRecursive(value interface{}, keyPrefix string) (interface{}, error) {
if mapping, ok := value.(map[string]interface{}); ok {
for key, entry := range mapping {
var newKeyPrefix string
if keyPrefix == "" {
newKeyPrefix = key
} else {
newKeyPrefix = fmt.Sprintf("%s.%s", keyPrefix, key)
}
convertedEntry, err := convertToStringKeysRecursive(entry, newKeyPrefix)
if err != nil {
return nil, err
}
mapping[key] = convertedEntry
}
return mapping, nil
}
if mapping, ok := value.(map[interface{}]interface{}); ok {
dict := make(map[string]interface{})
for key, entry := range mapping {
@ -501,7 +569,7 @@ func formatInvalidKeyError(keyPrefix string, key interface{}) error {
func LoadServices(filename string, servicesDict map[string]interface{}, workingDir string, lookupEnv template.Mapping, opts *Options) ([]types.ServiceConfig, error) {
var services []types.ServiceConfig
x, ok := servicesDict["extensions"]
x, ok := servicesDict[extensions]
if ok {
// as a top-level attribute, "services" doesn't support extensions, and a service can be named `x-foo`
for k, v := range x.(map[string]interface{}) {
@ -541,16 +609,17 @@ func loadServiceWithExtends(filename, name string, servicesDict map[string]inter
}
if serviceConfig.Extends != nil && !opts.SkipExtends {
baseServiceName := *serviceConfig.Extends["service"]
baseServiceName := serviceConfig.Extends.Service
var baseService *types.ServiceConfig
if file := serviceConfig.Extends["file"]; file == nil {
file := serviceConfig.Extends.File
if file == "" {
baseService, err = loadServiceWithExtends(filename, baseServiceName, servicesDict, workingDir, lookupEnv, opts, ct)
if err != nil {
return nil, err
}
} else {
// Resolve the path to the imported file, and load it.
baseFilePath := absPath(workingDir, *file)
baseFilePath := absPath(workingDir, file)
b, err := os.ReadFile(baseFilePath)
if err != nil {
@ -569,10 +638,10 @@ func loadServiceWithExtends(filename, name string, servicesDict map[string]inter
}
// Make paths relative to the importing Compose file. Note that we
// make the paths relative to `*file` rather than `baseFilePath` so
// that the resulting paths won't be absolute if `*file` isn't an
// make the paths relative to `file` rather than `baseFilePath` so
// that the resulting paths won't be absolute if `file` isn't an
// absolute path.
baseFileParent := filepath.Dir(*file)
baseFileParent := filepath.Dir(file)
if baseService.Build != nil {
baseService.Build.Context = resolveBuildContextPath(baseFileParent, baseService.Build.Context)
}
@ -583,12 +652,17 @@ func loadServiceWithExtends(filename, name string, servicesDict map[string]inter
}
baseService.Volumes[i].Source = resolveMaybeUnixPath(vol.Source, baseFileParent, lookupEnv)
}
for i, envFile := range baseService.EnvFile {
baseService.EnvFile[i] = resolveMaybeUnixPath(envFile, baseFileParent, lookupEnv)
}
}
serviceConfig, err = _merge(baseService, serviceConfig)
if err != nil {
return nil, err
}
serviceConfig.Extends = nil
}
return serviceConfig, nil
@ -996,14 +1070,15 @@ var transformDependsOnConfig TransformerFunc = func(data interface{}) (interface
}
}
var transformExtendsConfig TransformerFunc = func(data interface{}) (interface{}, error) {
switch data.(type) {
var transformExtendsConfig TransformerFunc = func(value interface{}) (interface{}, error) {
switch value.(type) {
case string:
data = map[string]interface{}{
"service": data,
}
return map[string]interface{}{"service": value}, nil
case map[string]interface{}:
return value, nil
default:
return value, errors.Errorf("invalid type %T for extends", value)
}
return transformMappingOrListFunc("=", true)(data)
}
var transformServiceVolumeConfig TransformerFunc = func(data interface{}) (interface{}, error) {

View File

@ -130,7 +130,7 @@ func _merge(baseService *types.ServiceConfig, overrideService *types.ServiceConf
if overrideService.Command != nil {
baseService.Command = overrideService.Command
}
if overrideService.HealthCheck != nil {
if overrideService.HealthCheck != nil && overrideService.HealthCheck.Test != nil {
baseService.HealthCheck.Test = overrideService.HealthCheck.Test
}
if overrideService.Entrypoint != nil {

View File

@ -20,6 +20,7 @@ import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/compose-spec/compose-go/errdefs"
"github.com/compose-spec/compose-go/types"
@ -27,8 +28,8 @@ import (
"github.com/sirupsen/logrus"
)
// normalize compose project by moving deprecated attributes to their canonical position and injecting implicit defaults
func normalize(project *types.Project, resolvePaths bool) error {
// Normalize compose project by moving deprecated attributes to their canonical position and injecting implicit defaults
func Normalize(project *types.Project, resolvePaths bool) error {
absWorkingDir, err := filepath.Abs(project.WorkingDir)
if err != nil {
return err
@ -71,17 +72,26 @@ func normalize(project *types.Project, resolvePaths bool) error {
}
if s.Build != nil {
if s.Build.Dockerfile == "" {
if s.Build.Dockerfile == "" && s.Build.DockerfileInline == "" {
s.Build.Dockerfile = "Dockerfile"
}
localContext := absPath(project.WorkingDir, s.Build.Context)
if _, err := os.Stat(localContext); err == nil {
if resolvePaths {
if resolvePaths {
// Build context might be a remote http/git context. Unfortunately supported "remote"
// syntax is highly ambiguous in moby/moby and not defined by compose-spec,
// so let's assume runtime will check
localContext := absPath(project.WorkingDir, s.Build.Context)
if _, err := os.Stat(localContext); err == nil {
s.Build.Context = localContext
}
// } else {
// might be a remote http/git context. Unfortunately supported "remote" syntax is highly ambiguous
// in moby/moby and not defined by compose-spec, so let's assume runtime will check
for name, path := range s.Build.AdditionalContexts {
if strings.Contains(path, "://") { // `docker-image://` or any builder specific context type
continue
}
path = absPath(project.WorkingDir, path)
if _, err := os.Stat(path); err == nil {
s.Build.AdditionalContexts[name] = path
}
}
}
s.Build.Args = s.Build.Args.Resolve(fn)
}
@ -90,6 +100,41 @@ func normalize(project *types.Project, resolvePaths bool) error {
}
s.Environment = s.Environment.Resolve(fn)
if s.Extends != nil && s.Extends.File != "" {
s.Extends.File = absPath(project.WorkingDir, s.Extends.File)
}
for _, link := range s.Links {
parts := strings.Split(link, ":")
if len(parts) == 2 {
link = parts[0]
}
s.DependsOn = setIfMissing(s.DependsOn, link, types.ServiceDependency{
Condition: types.ServiceConditionStarted,
Restart: true,
})
}
for _, namespace := range []string{s.NetworkMode, s.Ipc, s.Pid, s.Uts, s.Cgroup} {
if strings.HasPrefix(namespace, types.ServicePrefix) {
name := namespace[len(types.ServicePrefix):]
s.DependsOn = setIfMissing(s.DependsOn, name, types.ServiceDependency{
Condition: types.ServiceConditionStarted,
Restart: true,
})
}
}
for _, vol := range s.VolumesFrom {
if !strings.HasPrefix(vol, types.ContainerPrefix) {
spec := strings.Split(vol, ":")
s.DependsOn = setIfMissing(s.DependsOn, spec[0], types.ServiceDependency{
Condition: types.ServiceConditionStarted,
Restart: false,
})
}
}
err := relocateLogDriver(&s)
if err != nil {
return err
@ -126,9 +171,20 @@ func normalize(project *types.Project, resolvePaths bool) error {
return nil
}
// setIfMissing adds a ServiceDependency for service if not already defined
func setIfMissing(d types.DependsOnConfig, service string, dep types.ServiceDependency) types.DependsOnConfig {
if d == nil {
d = types.DependsOnConfig{}
}
if _, ok := d[service]; !ok {
d[service] = dep
}
return d
}
func relocateScale(s *types.ServiceConfig) error {
scale := uint64(s.Scale)
if scale != 1 {
if scale > 1 {
logrus.Warn("`scale` is deprecated. Use the `deploy.replicas` element")
if s.Deploy == nil {
s.Deploy = &types.DeployConfig{}

View File

@ -32,6 +32,28 @@ func checkConsistency(project *types.Project) error {
return errors.Wrapf(errdefs.ErrInvalid, "service %q has neither an image nor a build context specified", s.Name)
}
if s.Build != nil {
if s.Build.DockerfileInline != "" && s.Build.Dockerfile != "" {
return errors.Wrapf(errdefs.ErrInvalid, "service %q declares mutualy exclusive dockerfile and dockerfile_inline", s.Name)
}
if len(s.Build.Platforms) > 0 && s.Platform != "" {
var found bool
for _, platform := range s.Build.Platforms {
if platform == s.Platform {
found = true
break
}
}
if !found {
return errors.Wrapf(errdefs.ErrInvalid, "service.build.platforms MUST include service.platform %q ", s.Platform)
}
}
}
if s.NetworkMode != "" && len(s.Networks) > 0 {
return errors.Wrap(errdefs.ErrInvalid, fmt.Sprintf("service %s declares mutually exclusive `network_mode` and `networks`", s.Name))
}
for network := range s.Networks {
if _, ok := project.Networks[network]; !ok {
return errors.Wrap(errdefs.ErrInvalid, fmt.Sprintf("service %q refers to undefined network %s", s.Name, network))

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

@ -13,6 +13,7 @@
"name": {
"type": "string",
"pattern": "^[a-z0-9][a-z0-9_-]*$",
"description": "define the Compose project name, until user defines one explicitly."
},
@ -90,12 +91,14 @@
"properties": {
"context": {"type": "string"},
"dockerfile": {"type": "string"},
"dockerfile_inline": {"type": "string"},
"args": {"$ref": "#/definitions/list_or_dict"},
"ssh": {"$ref": "#/definitions/list_or_dict"},
"labels": {"$ref": "#/definitions/list_or_dict"},
"cache_from": {"type": "array", "items": {"type": "string"}},
"cache_to": {"type": "array", "items": {"type": "string"}},
"no_cache": {"type": "boolean"},
"additional_contexts": {"$ref": "#/definitions/list_or_dict"},
"network": {"type": "string"},
"pull": {"type": "boolean"},
"target": {"type": "string"},
@ -143,12 +146,7 @@
"cap_drop": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"cgroup": {"type": "string", "enum": ["host", "private"]},
"cgroup_parent": {"type": "string"},
"command": {
"oneOf": [
{"type": "string"},
{"type": "array", "items": {"type": "string"}}
]
},
"command": {"$ref": "#/definitions/command"},
"configs": {"$ref": "#/definitions/service_config_or_secret"},
"container_name": {"type": "string"},
"cpu_count": {"type": "integer", "minimum": 0},
@ -181,6 +179,7 @@
"type": "object",
"additionalProperties": false,
"properties": {
"restart": {"type": "boolean"},
"condition": {
"type": "string",
"enum": ["service_started", "service_healthy", "service_completed_successfully"]
@ -198,12 +197,7 @@
"dns_opt": {"type": "array","items": {"type": "string"}, "uniqueItems": true},
"dns_search": {"$ref": "#/definitions/string_or_list"},
"domainname": {"type": "string"},
"entrypoint": {
"oneOf": [
{"type": "string"},
{"type": "array", "items": {"type": "string"}}
]
},
"entrypoint": {"$ref": "#/definitions/command"},
"env_file": {"$ref": "#/definitions/string_or_list"},
"environment": {"$ref": "#/definitions/list_or_dict"},
@ -734,6 +728,14 @@
"patternProperties": {"^x-": {}}
},
"command": {
"oneOf": [
{"type": "null"},
{"type": "string"},
{"type": "array","items": {"type": "string"}}
]
},
"string_or_list": {
"oneOf": [
{"type": "string"},

View File

@ -52,6 +52,7 @@ func init() {
}
// Schema is the compose-spec JSON schema
//
//go:embed compose-spec.json
var Schema string

View File

@ -47,6 +47,19 @@ func (e InvalidTemplateError) Error() string {
return fmt.Sprintf("Invalid template: %#v", e.Template)
}
// MissingRequiredError is returned when a variable template is missing
type MissingRequiredError struct {
Variable string
Reason string
}
func (e MissingRequiredError) Error() string {
if e.Reason != "" {
return fmt.Sprintf("required variable %s is missing a value: %s", e.Variable, e.Reason)
}
return fmt.Sprintf("required variable %s is missing a value", e.Variable)
}
// Mapping is a user-supplied 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
@ -351,8 +364,9 @@ func withRequired(substitution string, mapping Mapping, sep string, valid func(s
}
value, ok := mapping(name)
if !ok || !valid(value) {
return "", true, &InvalidTemplateError{
Template: fmt.Sprintf("required variable %s is missing a value: %s", name, errorMessage),
return "", true, &MissingRequiredError{
Reason: errorMessage,
Variable: name,
}
}
return value, true, nil

View File

@ -18,6 +18,7 @@ package types
import (
"bytes"
"encoding/json"
"fmt"
"os"
"path/filepath"
@ -28,6 +29,7 @@ import (
godigest "github.com/opencontainers/go-digest"
"github.com/pkg/errors"
"golang.org/x/sync/errgroup"
"gopkg.in/yaml.v3"
)
// Project is the result of loading a set of compose files
@ -39,16 +41,17 @@ type Project struct {
Volumes Volumes `yaml:",omitempty" json:"volumes,omitempty"`
Secrets Secrets `yaml:",omitempty" json:"secrets,omitempty"`
Configs Configs `yaml:",omitempty" json:"configs,omitempty"`
Extensions Extensions `yaml:",inline" json:"-"` // https://github.com/golang/go/issues/6213
Extensions Extensions `mapstructure:"#extensions" yaml:",inline" json:"-"` // https://github.com/golang/go/issues/6213
ComposeFiles []string `yaml:"-" json:"-"`
Environment Mapping `yaml:"-" json:"-"`
// DisabledServices track services which have been disable as profile is not active
DisabledServices Services `yaml:"-" json:"-"`
Profiles []string `yaml:"-" json:"-"`
}
// ServiceNames return names for all services in this Compose config
func (p Project) ServiceNames() []string {
func (p *Project) ServiceNames() []string {
var names []string
for _, s := range p.Services {
names = append(names, s.Name)
@ -58,7 +61,7 @@ func (p Project) ServiceNames() []string {
}
// VolumeNames return names for all volumes in this Compose config
func (p Project) VolumeNames() []string {
func (p *Project) VolumeNames() []string {
var names []string
for k := range p.Volumes {
names = append(names, k)
@ -68,7 +71,7 @@ func (p Project) VolumeNames() []string {
}
// NetworkNames return names for all volumes in this Compose config
func (p Project) NetworkNames() []string {
func (p *Project) NetworkNames() []string {
var names []string
for k := range p.Networks {
names = append(names, k)
@ -78,7 +81,7 @@ func (p Project) NetworkNames() []string {
}
// SecretNames return names for all secrets in this Compose config
func (p Project) SecretNames() []string {
func (p *Project) SecretNames() []string {
var names []string
for k := range p.Secrets {
names = append(names, k)
@ -88,7 +91,7 @@ func (p Project) SecretNames() []string {
}
// ConfigNames return names for all configs in this Compose config
func (p Project) ConfigNames() []string {
func (p *Project) ConfigNames() []string {
var names []string
for k := range p.Configs {
names = append(names, k)
@ -98,7 +101,7 @@ func (p Project) ConfigNames() []string {
}
// GetServices retrieve services by names, or return all services if no name specified
func (p Project) GetServices(names ...string) (Services, error) {
func (p *Project) GetServices(names ...string) (Services, error) {
if len(names) == 0 {
return p.Services, nil
}
@ -119,8 +122,18 @@ func (p Project) GetServices(names ...string) (Services, error) {
return services, nil
}
// GetDisabledService retrieve disabled service by name
func (p Project) GetDisabledService(name string) (ServiceConfig, error) {
for _, config := range p.DisabledServices {
if config.Name == name {
return config, nil
}
}
return ServiceConfig{}, fmt.Errorf("no such service: %s", name)
}
// GetService retrieve a specific service by name
func (p Project) GetService(name string) (ServiceConfig, error) {
func (p *Project) GetService(name string) (ServiceConfig, error) {
services, err := p.GetServices(name)
if err != nil {
return ServiceConfig{}, err
@ -131,7 +144,7 @@ func (p Project) GetService(name string) (ServiceConfig, error) {
return services[0], nil
}
func (p Project) AllServices() Services {
func (p *Project) AllServices() Services {
var all Services
all = append(all, p.Services...)
all = append(all, p.DisabledServices...)
@ -140,12 +153,16 @@ func (p Project) AllServices() Services {
type ServiceFunc func(service ServiceConfig) error
// WithServices run ServiceFunc on each service and dependencies in dependency order
func (p Project) WithServices(names []string, fn ServiceFunc) error {
return p.withServices(names, fn, map[string]bool{})
// WithServices run ServiceFunc on each service and dependencies according to DependencyPolicy
func (p *Project) WithServices(names []string, fn ServiceFunc, options ...DependencyOption) error {
if len(options) == 0 {
// backward compatibility
options = []DependencyOption{IncludeDependencies}
}
return p.withServices(names, fn, map[string]bool{}, options)
}
func (p Project) withServices(names []string, fn ServiceFunc, seen map[string]bool) error {
func (p *Project) withServices(names []string, fn ServiceFunc, seen map[string]bool, options []DependencyOption) error {
services, err := p.GetServices(names...)
if err != nil {
return err
@ -155,9 +172,21 @@ func (p Project) withServices(names []string, fn ServiceFunc, seen map[string]bo
continue
}
seen[service.Name] = true
dependencies := service.GetDependencies()
var dependencies []string
for _, policy := range options {
switch policy {
case IncludeDependents:
dependencies = append(dependencies, p.GetDependentsForService(service)...)
case IncludeDependencies:
dependencies = append(dependencies, service.GetDependencies()...)
case IgnoreDependencies:
// Noop
default:
return fmt.Errorf("unsupported dependency policy %d", policy)
}
}
if len(dependencies) > 0 {
err := p.withServices(dependencies, fn, seen)
err := p.withServices(dependencies, fn, seen, options)
if err != nil {
return err
}
@ -169,6 +198,18 @@ func (p Project) withServices(names []string, fn ServiceFunc, seen map[string]bo
return nil
}
func (p *Project) GetDependentsForService(s ServiceConfig) []string {
var dependent []string
for _, service := range p.Services {
for name := range service.DependsOn {
if name == s.Name {
dependent = append(dependent, service.Name)
}
}
}
return dependent
}
// RelativePath resolve a relative path based project's working directory
func (p *Project) RelativePath(path string) string {
if path[0] == '~' {
@ -219,7 +260,7 @@ func (p *Project) ApplyProfiles(profiles []string) {
}
}
var enabled, disabled Services
for _, service := range p.Services {
for _, service := range p.AllServices() {
if service.HasProfile(profiles) {
enabled = append(enabled, service)
} else {
@ -228,6 +269,41 @@ func (p *Project) ApplyProfiles(profiles []string) {
}
p.Services = enabled
p.DisabledServices = disabled
p.Profiles = profiles
}
// EnableServices ensure services are enabled and activate profiles accordingly
func (p *Project) EnableServices(names ...string) error {
if len(names) == 0 {
return nil
}
var enabled []string
for _, name := range names {
_, err := p.GetService(name)
if err == nil {
// already enabled
continue
}
def, err := p.GetDisabledService(name)
if err != nil {
return err
}
enabled = append(enabled, def.Profiles...)
}
profiles := p.Profiles
PROFILES:
for _, profile := range enabled {
for _, p := range profiles {
if p == profile {
continue PROFILES
}
}
profiles = append(profiles, profile)
}
p.ApplyProfiles(profiles)
return p.ResolveServicesEnvironment(true)
}
// WithoutUnnecessaryResources drops networks/volumes/secrets/configs that are not referenced by active services
@ -292,8 +368,16 @@ func (p *Project) WithoutUnnecessaryResources() {
p.Configs = configs
}
// ForServices restrict the project model to a subset of services
func (p *Project) ForServices(names []string) error {
type DependencyOption int
const (
IncludeDependencies = iota
IncludeDependents
IgnoreDependencies
)
// ForServices restrict the project model to selected services and dependencies
func (p *Project) ForServices(names []string, options ...DependencyOption) error {
if len(names) == 0 {
// All services
return nil
@ -303,7 +387,7 @@ func (p *Project) ForServices(names []string) error {
err := p.WithServices(names, func(service ServiceConfig) error {
set[service.Name] = struct{}{}
return nil
})
}, options...)
if err != nil {
return err
}
@ -357,6 +441,44 @@ func (p *Project) ResolveImages(resolver func(named reference.Named) (godigest.D
return eg.Wait()
}
// MarshalYAML marshal Project into a yaml tree
func (p *Project) MarshalYAML() ([]byte, error) {
buf := bytes.NewBuffer([]byte{})
encoder := yaml.NewEncoder(buf)
encoder.SetIndent(2)
// encoder.CompactSeqIndent() FIXME https://github.com/go-yaml/yaml/pull/753
err := encoder.Encode(p)
if err != nil {
return nil, err
}
return buf.Bytes(), nil
}
// MarshalJSON makes Config implement json.Marshaler
func (p *Project) MarshalJSON() ([]byte, error) {
m := map[string]interface{}{
"name": p.Name,
"services": p.Services,
}
if len(p.Networks) > 0 {
m["networks"] = p.Networks
}
if len(p.Volumes) > 0 {
m["volumes"] = p.Volumes
}
if len(p.Secrets) > 0 {
m["secrets"] = p.Secrets
}
if len(p.Configs) > 0 {
m["configs"] = p.Configs
}
for k, v := range p.Extensions {
m[k] = v
}
return json.Marshal(m)
}
// ResolveServicesEnvironment parse env_files set for services to resolve the actual environment map for services
func (p Project) ResolveServicesEnvironment(discardEnvFiles bool) error {
for i, service := range p.Services {

View File

@ -107,7 +107,7 @@ type ServiceConfig struct {
// Command for the service containers.
// If set, overrides COMMAND from the image.
//
// Set to `[]` or `''` to clear the command from the image.
// Set to `[]` or an empty string to clear the command from the image.
Command ShellCommand `yaml:",omitempty" json:"command"` // NOTE: we can NOT omitempty for JSON! see ShellCommand type for details.
Configs []ServiceConfigObjConfig `yaml:",omitempty" json:"configs,omitempty"`
@ -126,13 +126,13 @@ type ServiceConfig struct {
// Entrypoint for the service containers.
// If set, overrides ENTRYPOINT from the image.
//
// Set to `[]` or `''` to clear the entrypoint from the image.
// Set to `[]` or an empty string to clear the entrypoint from the image.
Entrypoint ShellCommand `yaml:"entrypoint,omitempty" json:"entrypoint"` // NOTE: we can NOT omitempty for JSON! see ShellCommand type for details.
Environment MappingWithEquals `yaml:",omitempty" json:"environment,omitempty"`
EnvFile StringList `mapstructure:"env_file" yaml:"env_file,omitempty" json:"env_file,omitempty"`
Expose StringOrNumberList `yaml:",omitempty" json:"expose,omitempty"`
Extends ExtendsConfig `yaml:"extends,omitempty" json:"extends,omitempty"`
Extends *ExtendsConfig `yaml:"extends,omitempty" json:"extends,omitempty"`
ExternalLinks []string `mapstructure:"external_links" yaml:"external_links,omitempty" json:"external_links,omitempty"`
ExtraHosts HostsList `mapstructure:"extra_hosts" yaml:"extra_hosts,omitempty" json:"extra_hosts,omitempty"`
GroupAdd []string `mapstructure:"group_add" yaml:"group_add,omitempty" json:"group_add,omitempty"`
@ -186,7 +186,7 @@ type ServiceConfig struct {
VolumesFrom []string `mapstructure:"volumes_from" yaml:"volumes_from,omitempty" json:"volumes_from,omitempty"`
WorkingDir string `mapstructure:"working_dir" yaml:"working_dir,omitempty" json:"working_dir,omitempty"`
Extensions map[string]interface{} `yaml:",inline" json:"-"`
Extensions Extensions `mapstructure:"#extensions" yaml:",inline" json:"-"`
}
// NetworksByPriority return the service networks IDs sorted according to Priority
@ -254,37 +254,26 @@ const (
NetworkModeContainerPrefix = ContainerPrefix
)
// GetDependencies retrieve all services this service depends on
// GetDependencies retrieves all services this service depends on
func (s ServiceConfig) GetDependencies() []string {
dependencies := make(set)
for dependency := range s.DependsOn {
dependencies.append(dependency)
}
for _, link := range s.Links {
parts := strings.Split(link, ":")
if len(parts) == 2 {
dependencies.append(parts[0])
} else {
dependencies.append(link)
}
}
if strings.HasPrefix(s.NetworkMode, ServicePrefix) {
dependencies.append(s.NetworkMode[len(ServicePrefix):])
}
if strings.HasPrefix(s.Ipc, ServicePrefix) {
dependencies.append(s.Ipc[len(ServicePrefix):])
}
if strings.HasPrefix(s.Pid, ServicePrefix) {
dependencies.append(s.Pid[len(ServicePrefix):])
}
for _, vol := range s.VolumesFrom {
if !strings.HasPrefix(s.Pid, ContainerPrefix) {
spec := strings.Split(vol, ":")
dependencies.append(spec[0])
}
var dependencies []string
for service := range s.DependsOn {
dependencies = append(dependencies, service)
}
return dependencies
}
return dependencies.toSlice()
// GetDependents retrieves all services which depend on this service
func (s ServiceConfig) GetDependents(p *Project) []string {
var dependent []string
for _, service := range p.Services {
for name := range service.DependsOn {
if name == s.Name {
dependent = append(dependent, service.Name)
}
}
}
return dependent
}
type set map[string]struct{}
@ -305,25 +294,27 @@ func (s set) toSlice() []string {
// BuildConfig is a type for build
type BuildConfig struct {
Context string `yaml:",omitempty" json:"context,omitempty"`
Dockerfile string `yaml:",omitempty" json:"dockerfile,omitempty"`
Args MappingWithEquals `yaml:",omitempty" json:"args,omitempty"`
SSH SSHConfig `yaml:"ssh,omitempty" json:"ssh,omitempty"`
Labels Labels `yaml:",omitempty" json:"labels,omitempty"`
CacheFrom StringList `mapstructure:"cache_from" yaml:"cache_from,omitempty" json:"cache_from,omitempty"`
CacheTo StringList `mapstructure:"cache_to" yaml:"cache_to,omitempty" json:"cache_to,omitempty"`
NoCache bool `mapstructure:"no_cache" yaml:"no_cache,omitempty" json:"no_cache,omitempty"`
Pull bool `mapstructure:"pull" yaml:"pull,omitempty" json:"pull,omitempty"`
ExtraHosts HostsList `mapstructure:"extra_hosts" yaml:"extra_hosts,omitempty" json:"extra_hosts,omitempty"`
Isolation string `yaml:",omitempty" json:"isolation,omitempty"`
Network string `yaml:",omitempty" json:"network,omitempty"`
Target string `yaml:",omitempty" json:"target,omitempty"`
Secrets []ServiceSecretConfig `yaml:",omitempty" json:"secrets,omitempty"`
Tags StringList `mapstructure:"tags" yaml:"tags,omitempty" json:"tags,omitempty"`
Platforms StringList `mapstructure:"platforms" yaml:"platforms,omitempty" json:"platforms,omitempty"`
Privileged bool `yaml:",omitempty" json:"privileged,omitempty"`
Context string `yaml:",omitempty" json:"context,omitempty"`
Dockerfile string `yaml:",omitempty" json:"dockerfile,omitempty"`
DockerfileInline string `mapstructure:"dockerfile_inline,omitempty" yaml:"dockerfile_inline,omitempty" json:"dockerfile_inline,omitempty"`
Args MappingWithEquals `yaml:",omitempty" json:"args,omitempty"`
SSH SSHConfig `yaml:"ssh,omitempty" json:"ssh,omitempty"`
Labels Labels `yaml:",omitempty" json:"labels,omitempty"`
CacheFrom StringList `mapstructure:"cache_from" yaml:"cache_from,omitempty" json:"cache_from,omitempty"`
CacheTo StringList `mapstructure:"cache_to" yaml:"cache_to,omitempty" json:"cache_to,omitempty"`
NoCache bool `mapstructure:"no_cache" yaml:"no_cache,omitempty" json:"no_cache,omitempty"`
AdditionalContexts Mapping `mapstructure:"additional_contexts" yaml:"additional_contexts,omitempty" json:"additional_contexts,omitempty"`
Pull bool `mapstructure:"pull" yaml:"pull,omitempty" json:"pull,omitempty"`
ExtraHosts HostsList `mapstructure:"extra_hosts" yaml:"extra_hosts,omitempty" json:"extra_hosts,omitempty"`
Isolation string `yaml:",omitempty" json:"isolation,omitempty"`
Network string `yaml:",omitempty" json:"network,omitempty"`
Target string `yaml:",omitempty" json:"target,omitempty"`
Secrets []ServiceSecretConfig `yaml:",omitempty" json:"secrets,omitempty"`
Tags StringList `mapstructure:"tags" yaml:"tags,omitempty" json:"tags,omitempty"`
Platforms StringList `mapstructure:"platforms" yaml:"platforms,omitempty" json:"platforms,omitempty"`
Privileged bool `yaml:",omitempty" json:"privileged,omitempty"`
Extensions map[string]interface{} `yaml:",inline" json:"-"`
Extensions Extensions `mapstructure:"#extensions" yaml:",inline" json:"-"`
}
// BlkioConfig define blkio config
@ -335,7 +326,7 @@ type BlkioConfig struct {
DeviceWriteBps []ThrottleDevice `mapstructure:"device_write_bps" yaml:",omitempty" json:"device_write_bps,omitempty"`
DeviceWriteIOps []ThrottleDevice `mapstructure:"device_write_iops" yaml:",omitempty" json:"device_write_iops,omitempty"`
Extensions map[string]interface{} `yaml:",inline" json:"-"`
Extensions Extensions `mapstructure:"#extensions" yaml:",inline" json:"-"`
}
// WeightDevice is a structure that holds device:weight pair
@ -343,34 +334,34 @@ type WeightDevice struct {
Path string
Weight uint16
Extensions map[string]interface{} `yaml:",inline" json:"-"`
Extensions Extensions `mapstructure:"#extensions" yaml:",inline" json:"-"`
}
// ThrottleDevice is a structure that holds device:rate_per_second pair
type ThrottleDevice struct {
Path string
Rate uint64
Rate UnitBytes
Extensions map[string]interface{} `yaml:",inline" json:"-"`
Extensions Extensions `mapstructure:"#extensions" yaml:",inline" json:"-"`
}
// ShellCommand is a string or list of string args.
//
// When marshaled to YAML, nil command fields will be omitted if `omitempty`
// is specified as a struct tag. Explicitly empty commands (i.e. `[]` or `''`)
// will serialize to an empty array (`[]`).
// is specified as a struct tag. Explicitly empty commands (i.e. `[]` or
// empty string will serialize to an empty array (`[]`).
//
// When marshaled to JSON, the `omitempty` struct must NOT be specified.
// If the command field is nil, it will be serialized as `null`.
// Explicitly empty commands (i.e. `[]` or `''`) will serialize to an empty
// array (`[]`).
// Explicitly empty commands (i.e. `[]` or empty string) will serialize to
// an empty array (`[]`).
//
// The distinction between nil and explicitly empty is important to distinguish
// between an unset value and a provided, but empty, value, which should be
// preserved so that it can override any base value (e.g. container entrypoint).
//
// The different semantics between YAML and JSON are due to limitations with
// JSON marshaling + `omitempty` in the Go stdlib, while gopkg.in/yaml.v2 gives
// JSON marshaling + `omitempty` in the Go stdlib, while gopkg.in/yaml.v3 gives
// us more flexibility via the yaml.IsZeroer interface.
//
// In the future, it might make sense to make fields of this type be
@ -394,7 +385,7 @@ func (s ShellCommand) IsZero() bool {
// accurately if the `omitempty` struct tag is omitted/forgotten.
//
// A similar MarshalJSON() implementation is not needed because the Go stdlib
// already serializes nil slices to `null`, whereas gopkg.in/yaml.v2 by default
// already serializes nil slices to `null`, whereas gopkg.in/yaml.v3 by default
// serializes nil slices to `[]`.
func (s ShellCommand) MarshalYAML() (interface{}, error) {
if s == nil {
@ -574,7 +565,7 @@ type LoggingConfig struct {
Driver string `yaml:",omitempty" json:"driver,omitempty"`
Options map[string]string `yaml:",omitempty" json:"options,omitempty"`
Extensions map[string]interface{} `yaml:",inline" json:"-"`
Extensions Extensions `mapstructure:"#extensions" yaml:",inline" json:"-"`
}
// DeployConfig the deployment configuration for a service
@ -589,7 +580,7 @@ type DeployConfig struct {
Placement Placement `yaml:",omitempty" json:"placement,omitempty"`
EndpointMode string `mapstructure:"endpoint_mode" yaml:"endpoint_mode,omitempty" json:"endpoint_mode,omitempty"`
Extensions map[string]interface{} `yaml:",inline" json:"-"`
Extensions Extensions `mapstructure:"#extensions" yaml:",inline" json:"-"`
}
// HealthCheckConfig the healthcheck configuration for a service
@ -601,7 +592,7 @@ type HealthCheckConfig struct {
StartPeriod *Duration `mapstructure:"start_period" yaml:"start_period,omitempty" json:"start_period,omitempty"`
Disable bool `yaml:",omitempty" json:"disable,omitempty"`
Extensions map[string]interface{} `yaml:",inline" json:"-"`
Extensions Extensions `mapstructure:"#extensions" yaml:",inline" json:"-"`
}
// HealthCheckTest is the command run to test the health of a service
@ -616,7 +607,7 @@ type UpdateConfig struct {
MaxFailureRatio float32 `mapstructure:"max_failure_ratio" yaml:"max_failure_ratio,omitempty" json:"max_failure_ratio,omitempty"`
Order string `yaml:",omitempty" json:"order,omitempty"`
Extensions map[string]interface{} `yaml:",inline" json:"-"`
Extensions Extensions `mapstructure:"#extensions" yaml:",inline" json:"-"`
}
// Resources the resource limits and reservations
@ -624,7 +615,7 @@ type Resources struct {
Limits *Resource `yaml:",omitempty" json:"limits,omitempty"`
Reservations *Resource `yaml:",omitempty" json:"reservations,omitempty"`
Extensions map[string]interface{} `yaml:",inline" json:"-"`
Extensions Extensions `mapstructure:"#extensions" yaml:",inline" json:"-"`
}
// Resource is a resource to be limited or reserved
@ -636,7 +627,7 @@ type Resource struct {
Devices []DeviceRequest `mapstructure:"devices" yaml:"devices,omitempty" json:"devices,omitempty"`
GenericResources []GenericResource `mapstructure:"generic_resources" yaml:"generic_resources,omitempty" json:"generic_resources,omitempty"`
Extensions map[string]interface{} `yaml:",inline" json:"-"`
Extensions Extensions `mapstructure:"#extensions" yaml:",inline" json:"-"`
}
type DeviceRequest struct {
@ -651,7 +642,7 @@ type DeviceRequest struct {
type GenericResource struct {
DiscreteResourceSpec *DiscreteGenericResource `mapstructure:"discrete_resource_spec" yaml:"discrete_resource_spec,omitempty" json:"discrete_resource_spec,omitempty"`
Extensions map[string]interface{} `yaml:",inline" json:"-"`
Extensions Extensions `mapstructure:"#extensions" yaml:",inline" json:"-"`
}
// DiscreteGenericResource represents a "user defined" resource which is defined
@ -662,7 +653,7 @@ type DiscreteGenericResource struct {
Kind string `json:"kind"`
Value int64 `json:"value"`
Extensions map[string]interface{} `yaml:",inline" json:"-"`
Extensions Extensions `mapstructure:"#extensions" yaml:",inline" json:"-"`
}
// UnitBytes is the bytes type
@ -685,7 +676,7 @@ type RestartPolicy struct {
MaxAttempts *uint64 `mapstructure:"max_attempts" yaml:"max_attempts,omitempty" json:"max_attempts,omitempty"`
Window *Duration `yaml:",omitempty" json:"window,omitempty"`
Extensions map[string]interface{} `yaml:",inline" json:"-"`
Extensions Extensions `mapstructure:"#extensions" yaml:",inline" json:"-"`
}
// Placement constraints for the service
@ -694,14 +685,14 @@ type Placement struct {
Preferences []PlacementPreferences `yaml:",omitempty" json:"preferences,omitempty"`
MaxReplicas uint64 `mapstructure:"max_replicas_per_node" yaml:"max_replicas_per_node,omitempty" json:"max_replicas_per_node,omitempty"`
Extensions map[string]interface{} `yaml:",inline" json:"-"`
Extensions Extensions `mapstructure:"#extensions" yaml:",inline" json:"-"`
}
// PlacementPreferences is the preferences for a service placement
type PlacementPreferences struct {
Spread string `yaml:",omitempty" json:"spread,omitempty"`
Extensions map[string]interface{} `yaml:",inline" json:"-"`
Extensions Extensions `mapstructure:"#extensions" yaml:",inline" json:"-"`
}
// ServiceNetworkConfig is the network configuration for a service
@ -712,7 +703,7 @@ type ServiceNetworkConfig struct {
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:"-"`
Extensions Extensions `mapstructure:"#extensions" yaml:",inline" json:"-"`
}
// ServicePortConfig is the port configuration for a service
@ -723,7 +714,7 @@ type ServicePortConfig struct {
Published string `yaml:",omitempty" json:"published,omitempty"`
Protocol string `yaml:",omitempty" json:"protocol,omitempty"`
Extensions map[string]interface{} `yaml:",inline" json:"-"`
Extensions Extensions `mapstructure:"#extensions" yaml:",inline" json:"-"`
}
// ParsePortConfig parse short syntax for service port configuration
@ -776,7 +767,7 @@ type ServiceVolumeConfig struct {
Volume *ServiceVolumeVolume `yaml:",omitempty" json:"volume,omitempty"`
Tmpfs *ServiceVolumeTmpfs `yaml:",omitempty" json:"tmpfs,omitempty"`
Extensions map[string]interface{} `yaml:",inline" json:"-"`
Extensions Extensions `mapstructure:"#extensions" yaml:",inline" json:"-"`
}
// String render ServiceVolumeConfig as a volume string, one can parse back using loader.ParseVolume
@ -820,7 +811,7 @@ type ServiceVolumeBind struct {
Propagation string `yaml:",omitempty" json:"propagation,omitempty"`
CreateHostPath bool `mapstructure:"create_host_path" yaml:"create_host_path,omitempty" json:"create_host_path,omitempty"`
Extensions map[string]interface{} `yaml:",inline" json:"-"`
Extensions Extensions `mapstructure:"#extensions" yaml:",inline" json:"-"`
}
// SELinux represents the SELinux re-labeling options.
@ -851,7 +842,7 @@ const (
type ServiceVolumeVolume struct {
NoCopy bool `mapstructure:"nocopy" yaml:"nocopy,omitempty" json:"nocopy,omitempty"`
Extensions map[string]interface{} `yaml:",inline" json:"-"`
Extensions Extensions `mapstructure:"#extensions" yaml:",inline" json:"-"`
}
// ServiceVolumeTmpfs are options for a service volume of type tmpfs
@ -860,7 +851,7 @@ type ServiceVolumeTmpfs struct {
Mode uint32 `yaml:",omitempty" json:"mode,omitempty"`
Extensions map[string]interface{} `yaml:",inline" json:"-"`
Extensions Extensions `mapstructure:"#extensions" yaml:",inline" json:"-"`
}
// FileReferenceConfig for a reference to a swarm file object
@ -871,7 +862,7 @@ type FileReferenceConfig struct {
GID string `yaml:",omitempty" json:"gid,omitempty"`
Mode *uint32 `yaml:",omitempty" json:"mode,omitempty"`
Extensions map[string]interface{} `yaml:",inline" json:"-"`
Extensions Extensions `mapstructure:"#extensions" yaml:",inline" json:"-"`
}
// ServiceConfigObjConfig is the config obj configuration for a service
@ -886,7 +877,7 @@ type UlimitsConfig struct {
Soft int `yaml:",omitempty" json:"soft,omitempty"`
Hard int `yaml:",omitempty" json:"hard,omitempty"`
Extensions map[string]interface{} `yaml:",inline" json:"-"`
Extensions Extensions `mapstructure:"#extensions" yaml:",inline" json:"-"`
}
// MarshalYAML makes UlimitsConfig implement yaml.Marshaller
@ -894,7 +885,13 @@ func (u *UlimitsConfig) MarshalYAML() (interface{}, error) {
if u.Single != 0 {
return u.Single, nil
}
return u, nil
return struct {
Soft int
Hard int
}{
Soft: u.Soft,
Hard: u.Hard,
}, nil
}
// MarshalJSON makes UlimitsConfig implement json.Marshaller
@ -908,23 +905,23 @@ func (u *UlimitsConfig) MarshalJSON() ([]byte, error) {
// NetworkConfig for a network
type NetworkConfig struct {
Name string `yaml:",omitempty" json:"name,omitempty"`
Driver string `yaml:",omitempty" json:"driver,omitempty"`
DriverOpts map[string]string `mapstructure:"driver_opts" yaml:"driver_opts,omitempty" json:"driver_opts,omitempty"`
Ipam IPAMConfig `yaml:",omitempty" json:"ipam,omitempty"`
External External `yaml:",omitempty" json:"external,omitempty"`
Internal bool `yaml:",omitempty" json:"internal,omitempty"`
Attachable bool `yaml:",omitempty" json:"attachable,omitempty"`
Labels Labels `yaml:",omitempty" json:"labels,omitempty"`
EnableIPv6 bool `mapstructure:"enable_ipv6" yaml:"enable_ipv6,omitempty" json:"enable_ipv6,omitempty"`
Extensions map[string]interface{} `yaml:",inline" json:"-"`
Name string `yaml:",omitempty" json:"name,omitempty"`
Driver string `yaml:",omitempty" json:"driver,omitempty"`
DriverOpts map[string]string `mapstructure:"driver_opts" yaml:"driver_opts,omitempty" json:"driver_opts,omitempty"`
Ipam IPAMConfig `yaml:",omitempty" json:"ipam,omitempty"`
External External `yaml:",omitempty" json:"external,omitempty"`
Internal bool `yaml:",omitempty" json:"internal,omitempty"`
Attachable bool `yaml:",omitempty" json:"attachable,omitempty"`
Labels Labels `yaml:",omitempty" json:"labels,omitempty"`
EnableIPv6 bool `mapstructure:"enable_ipv6" yaml:"enable_ipv6,omitempty" json:"enable_ipv6,omitempty"`
Extensions Extensions `mapstructure:"#extensions" yaml:",inline" json:"-"`
}
// IPAMConfig for a network
type IPAMConfig struct {
Driver string `yaml:",omitempty" json:"driver,omitempty"`
Config []*IPAMPool `yaml:",omitempty" json:"config,omitempty"`
Extensions map[string]interface{} `yaml:",inline" json:"-"`
Driver string `yaml:",omitempty" json:"driver,omitempty"`
Config []*IPAMPool `yaml:",omitempty" json:"config,omitempty"`
Extensions Extensions `mapstructure:"#extensions" yaml:",inline" json:"-"`
}
// IPAMPool for a network
@ -938,21 +935,21 @@ type IPAMPool struct {
// VolumeConfig for a volume
type VolumeConfig struct {
Name string `yaml:",omitempty" json:"name,omitempty"`
Driver string `yaml:",omitempty" json:"driver,omitempty"`
DriverOpts map[string]string `mapstructure:"driver_opts" yaml:"driver_opts,omitempty" json:"driver_opts,omitempty"`
External External `yaml:",omitempty" json:"external,omitempty"`
Labels Labels `yaml:",omitempty" json:"labels,omitempty"`
Extensions map[string]interface{} `yaml:",inline" json:"-"`
Name string `yaml:",omitempty" json:"name,omitempty"`
Driver string `yaml:",omitempty" json:"driver,omitempty"`
DriverOpts map[string]string `mapstructure:"driver_opts" yaml:"driver_opts,omitempty" json:"driver_opts,omitempty"`
External External `yaml:",omitempty" json:"external,omitempty"`
Labels Labels `yaml:",omitempty" json:"labels,omitempty"`
Extensions Extensions `mapstructure:"#extensions" yaml:",inline" json:"-"`
}
// External identifies a Volume or Network as a reference to a resource that is
// not managed, and should already exist.
// External.name is deprecated and replaced by Volume.name
type External struct {
Name string `yaml:",omitempty" json:"name,omitempty"`
External bool `yaml:",omitempty" json:"external,omitempty"`
Extensions map[string]interface{} `yaml:",inline" json:"-"`
Name string `yaml:",omitempty" json:"name,omitempty"`
External bool `yaml:",omitempty" json:"external,omitempty"`
Extensions Extensions `mapstructure:"#extensions" yaml:",inline" json:"-"`
}
// MarshalYAML makes External implement yaml.Marshaller
@ -973,23 +970,23 @@ func (e External) MarshalJSON() ([]byte, error) {
// CredentialSpecConfig for credential spec on Windows
type CredentialSpecConfig struct {
Config string `yaml:",omitempty" json:"config,omitempty"` // Config was added in API v1.40
File string `yaml:",omitempty" json:"file,omitempty"`
Registry string `yaml:",omitempty" json:"registry,omitempty"`
Extensions map[string]interface{} `yaml:",inline" json:"-"`
Config string `yaml:",omitempty" json:"config,omitempty"` // Config was added in API v1.40
File string `yaml:",omitempty" json:"file,omitempty"`
Registry string `yaml:",omitempty" json:"registry,omitempty"`
Extensions Extensions `mapstructure:"#extensions" yaml:",inline" json:"-"`
}
// FileObjectConfig is a config type for a file used by a service
type FileObjectConfig struct {
Name string `yaml:",omitempty" json:"name,omitempty"`
File string `yaml:",omitempty" json:"file,omitempty"`
Environment string `yaml:",omitempty" json:"environment,omitempty"`
External External `yaml:",omitempty" json:"external,omitempty"`
Labels Labels `yaml:",omitempty" json:"labels,omitempty"`
Driver string `yaml:",omitempty" json:"driver,omitempty"`
DriverOpts map[string]string `mapstructure:"driver_opts" yaml:"driver_opts,omitempty" json:"driver_opts,omitempty"`
TemplateDriver string `mapstructure:"template_driver" yaml:"template_driver,omitempty" json:"template_driver,omitempty"`
Extensions map[string]interface{} `yaml:",inline" json:"-"`
Name string `yaml:",omitempty" json:"name,omitempty"`
File string `yaml:",omitempty" json:"file,omitempty"`
Environment string `yaml:",omitempty" json:"environment,omitempty"`
External External `yaml:",omitempty" json:"external,omitempty"`
Labels Labels `yaml:",omitempty" json:"labels,omitempty"`
Driver string `yaml:",omitempty" json:"driver,omitempty"`
DriverOpts map[string]string `mapstructure:"driver_opts" yaml:"driver_opts,omitempty" json:"driver_opts,omitempty"`
TemplateDriver string `mapstructure:"template_driver" yaml:"template_driver,omitempty" json:"template_driver,omitempty"`
Extensions Extensions `mapstructure:"#extensions" yaml:",inline" json:"-"`
}
const (
@ -1006,11 +1003,15 @@ const (
type DependsOnConfig map[string]ServiceDependency
type ServiceDependency struct {
Condition string `yaml:",omitempty" json:"condition,omitempty"`
Extensions map[string]interface{} `yaml:",inline" json:"-"`
Condition string `yaml:",omitempty" json:"condition,omitempty"`
Restart bool `yaml:",omitempty" json:"restart,omitempty"`
Extensions Extensions `mapstructure:"#extensions" yaml:",inline" json:"-"`
}
type ExtendsConfig MappingWithEquals
type ExtendsConfig struct {
File string `yaml:",omitempty" json:"file,omitempty"`
Service string `yaml:",omitempty" json:"service,omitempty"`
}
// SecretConfig for a secret
type SecretConfig FileObjectConfig