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

Signed-off-by: CrazyMax <crazy-max@users.noreply.github.com>
This commit is contained in:
CrazyMax 2021-11-09 10:16:12 +01:00
parent da0eb138d0
commit a18829f837
No known key found for this signature in database
GPG Key ID: 3248E46B6BB8C7F7
25 changed files with 721 additions and 436 deletions

2
go.mod
View File

@ -8,7 +8,7 @@ require (
github.com/bugsnag/panicwrap v1.2.0 // indirect
github.com/cenkalti/backoff v2.1.1+incompatible // indirect
github.com/cloudflare/cfssl v0.0.0-20181213083726-b94e044bb51e // indirect
github.com/compose-spec/compose-go v0.0.0-20210729195839-de56f4f0cb3c
github.com/compose-spec/compose-go v1.0.5
github.com/containerd/console v1.0.3
github.com/containerd/containerd v1.5.5
github.com/docker/cli v20.10.8+incompatible

10
go.sum
View File

@ -262,8 +262,10 @@ github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnht
github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=
github.com/codahale/hdrhistogram v0.0.0-20160425231609-f8ad88b59a58/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI=
github.com/compose-spec/compose-go v0.0.0-20210729195839-de56f4f0cb3c h1:lSR4wokZlq+Q8uJpgZuFMs3VoLaYVV07cJOZHa1zRBg=
github.com/compose-spec/compose-go v0.0.0-20210729195839-de56f4f0cb3c/go.mod h1:5V65rPnTvvQagtoMxTneJ2QicLq6ZRQQ7fOgPN226fo=
github.com/compose-spec/compose-go v1.0.5 h1:WtfK7tJsk5C8h12iggum7p28kTxeXH7Xi5c/pLfnBwk=
github.com/compose-spec/compose-go v1.0.5/go.mod h1:LQ/JAjSIyh8bTu4RV6nkyf0Ow/Yf3qpvzrdEigxduiw=
github.com/compose-spec/godotenv v1.1.0 h1:wzShe5P6L/Aw3wsV357eWlZdMcPaOe2V2+3+qGwMEL4=
github.com/compose-spec/godotenv v1.1.0/go.mod h1:zF/3BOa18Z24tts5qnO/E9YURQanJTBUf7nlcCTNsyc=
github.com/containerd/aufs v0.0.0-20200908144142-dab0cbea06f4/go.mod h1:nukgQABAEopAHvB6j7cnP5zJ+/3aVcE7hCYqvIwAHyE=
github.com/containerd/aufs v0.0.0-20201003224125-76a6863f2989/go.mod h1:AkGGQs9NM2vtYHaUen+NljV0/baGCAPELGm2q9ZXpWU=
github.com/containerd/aufs v0.0.0-20210316121734-20793ff83c97/go.mod h1:kL5kd6KM5TzQjR79jljyi4olc1Vrx6XBlcyj3gNv2PU=
@ -794,7 +796,6 @@ github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht
github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik=
github.com/jmoiron/sqlx v1.2.1-0.20190826204134-d7d95172beb5/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks=
github.com/joefitzgerald/rainbow-reporter v0.1.0/go.mod h1:481CNgqmVHQZzdIbN52CupLJyoVwB10FQ/IQlF1pdL8=
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7/go.mod h1:2iMrUgbbvHEiQClaW2NsSzMyGHqN+rDFqY705q49KG0=
@ -903,8 +904,9 @@ github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0Qu
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.3.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag=
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/mapstructure v1.4.2 h1:6h7AQ0yhTcIsmFmnAwQls75jp2Gzs4iB8W7pjMO+rqo=
github.com/mitchellh/mapstructure v1.4.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f/go.mod h1:OkQIRizQZAeMln+1tSwduZz7+Af5oFlKirV/MSYes2A=
github.com/moby/buildkit v0.8.1/go.mod h1:/kyU1hKy/aYCuP39GZA9MaKioovHku57N6cqlKZIaiQ=
github.com/moby/buildkit v0.9.1-0.20211019185819-8778943ac3da h1:DJ6zT0cdxXgUf17GmYAWqCGv47cJXx7nZ1CTHojZ3A4=

View File

@ -21,14 +21,21 @@ import (
"strings"
interp "github.com/compose-spec/compose-go/interpolation"
"github.com/compose-spec/compose-go/types"
"github.com/pkg/errors"
)
var interpolateTypeCastMapping = map[interp.Path]interp.Cast{
servicePath("configs", interp.PathMatchList, "mode"): toInt,
servicePath("secrets", interp.PathMatchList, "mode"): toInt,
servicePath("healthcheck", "retries"): toInt,
servicePath("healthcheck", "disable"): toBoolean,
servicePath("cpu_count"): toInt64,
servicePath("cpu_percent"): toFloat,
servicePath("cpu_period"): toInt64,
servicePath("cpu_quota"): toInt64,
servicePath("cpu_rt_period"): toInt64,
servicePath("cpu_rt_runtime"): toInt64,
servicePath("cpus"): toFloat32,
servicePath("cpu_shares"): toInt64,
servicePath("init"): toBoolean,
servicePath("deploy", "replicas"): toInt,
servicePath("deploy", "update_config", "parallelism"): toInt,
servicePath("deploy", "update_config", "max_failure_ratio"): toFloat,
@ -36,20 +43,35 @@ var interpolateTypeCastMapping = map[interp.Path]interp.Cast{
servicePath("deploy", "rollback_config", "max_failure_ratio"): toFloat,
servicePath("deploy", "restart_policy", "max_attempts"): toInt,
servicePath("deploy", "placement", "max_replicas_per_node"): toInt,
servicePath("healthcheck", "retries"): toInt,
servicePath("healthcheck", "disable"): toBoolean,
servicePath("mem_limit"): toUnitBytes,
servicePath("mem_reservation"): toUnitBytes,
servicePath("memswap_limit"): toUnitBytes,
servicePath("mem_swappiness"): toUnitBytes,
servicePath("oom_kill_disable"): toBoolean,
servicePath("oom_score_adj"): toInt64,
servicePath("pids_limit"): toInt64,
servicePath("ports", interp.PathMatchList, "target"): toInt,
servicePath("ports", interp.PathMatchList, "published"): toInt,
servicePath("privileged"): toBoolean,
servicePath("read_only"): toBoolean,
servicePath("scale"): toInt,
servicePath("secrets", interp.PathMatchList, "mode"): toInt,
servicePath("shm_size"): toUnitBytes,
servicePath("stdin_open"): toBoolean,
servicePath("stop_grace_period"): toDuration,
servicePath("tty"): toBoolean,
servicePath("ulimits", interp.PathMatchAll): toInt,
servicePath("ulimits", interp.PathMatchAll, "hard"): toInt,
servicePath("ulimits", interp.PathMatchAll, "soft"): toInt,
servicePath("privileged"): toBoolean,
servicePath("read_only"): toBoolean,
servicePath("stdin_open"): toBoolean,
servicePath("tty"): toBoolean,
servicePath("volumes", interp.PathMatchList, "read_only"): toBoolean,
servicePath("volumes", interp.PathMatchList, "volume", "nocopy"): toBoolean,
servicePath("volumes", interp.PathMatchList, "tmpfs", "size"): toUnitBytes,
iPath("networks", interp.PathMatchAll, "external"): toBoolean,
iPath("networks", interp.PathMatchAll, "internal"): toBoolean,
iPath("networks", interp.PathMatchAll, "attachable"): toBoolean,
iPath("networks", interp.PathMatchAll, "enable_ipv6"): toBoolean,
iPath("volumes", interp.PathMatchAll, "external"): toBoolean,
iPath("secrets", interp.PathMatchAll, "external"): toBoolean,
iPath("configs", interp.PathMatchAll, "external"): toBoolean,
@ -67,10 +89,38 @@ func toInt(value string) (interface{}, error) {
return strconv.Atoi(value)
}
func toInt64(value string) (interface{}, error) {
return strconv.ParseInt(value, 10, 64)
}
func toUnitBytes(value string) (interface{}, error) {
i, err := strconv.ParseInt(value, 10, 64)
if err != nil {
return nil, err
}
return types.UnitBytes(i), nil
}
func toDuration(value string) (interface{}, error) {
i, err := strconv.ParseInt(value, 10, 64)
if err != nil {
return nil, err
}
return types.Duration(i), nil
}
func toFloat(value string) (interface{}, error) {
return strconv.ParseFloat(value, 64)
}
func toFloat32(value string) (interface{}, error) {
f, err := strconv.ParseFloat(value, 32)
if err != nil {
return nil, err
}
return float32(f), nil
}
// should match http://yaml.org/type/bool.html
func toBoolean(value string) (interface{}, error) {
switch strings.ToLower(value) {

View File

@ -31,14 +31,13 @@ import (
"github.com/compose-spec/compose-go/schema"
"github.com/compose-spec/compose-go/template"
"github.com/compose-spec/compose-go/types"
units "github.com/docker/go-units"
"github.com/imdario/mergo"
"github.com/joho/godotenv"
shellwords "github.com/mattn/go-shellwords"
"github.com/compose-spec/godotenv"
"github.com/docker/go-units"
"github.com/mattn/go-shellwords"
"github.com/mitchellh/mapstructure"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
yaml "gopkg.in/yaml.v2"
"gopkg.in/yaml.v2"
)
// Options supported by Load
@ -49,6 +48,8 @@ type Options struct {
SkipInterpolation bool
// Skip normalization
SkipNormalization bool
// Resolve paths
ResolvePaths bool
// Skip consistency check
SkipConsistencyCheck bool
// Skip extends
@ -103,6 +104,11 @@ func WithDiscardEnvFiles(opts *Options) {
opts.discardEnvFiles = true
}
// WithSkipValidation sets the Options to skip validation when loading sections
func WithSkipValidation(opts *Options) {
opts.SkipValidation = true
}
// ParseYAML reads the bytes from a file, parses the bytes into a mapping
// structure, and returns it.
func ParseYAML(source []byte) (map[string]interface{}, error) {
@ -199,7 +205,7 @@ func Load(configDetails types.ConfigDetails, options ...func(*Options)) (*types.
}
if !opts.SkipNormalization {
err = normalize(project)
err = normalize(project, opts.ResolvePaths)
if err != nil {
return nil, err
}
@ -216,34 +222,14 @@ func Load(configDetails types.ConfigDetails, options ...func(*Options)) (*types.
}
func parseConfig(b []byte, opts *Options) (map[string]interface{}, error) {
yaml, err := ParseYAML(b)
if err != nil {
return nil, err
}
if !opts.SkipInterpolation {
withoutComments, err := removeYamlComments(b)
if err != nil {
return nil, err
}
substituted, err := opts.Interpolate.Substitute(string(withoutComments), template.Mapping(opts.Interpolate.LookupValue))
if err != nil {
return nil, err
}
b = []byte(substituted)
return interp.Interpolate(yaml, *opts.Interpolate)
}
return ParseYAML(b)
}
// removeYamlComments drop all comments from the yaml file, so we don't try to apply string substitutions on irrelevant places
func removeYamlComments(b []byte) ([]byte, error) {
var cfg interface{}
err := yaml.Unmarshal(b, &cfg)
if err != nil {
return nil, err
}
b, err = yaml.Marshal(cfg)
if err != nil {
return nil, err
}
return b, nil
return yaml, err
}
func groupXFieldsIntoExtensions(dict map[string]interface{}) map[string]interface{} {
@ -274,7 +260,7 @@ func loadSections(filename string, config map[string]interface{}, configDetails
return nil, err
}
cfg.Networks, err = LoadNetworks(getSection(config, "networks"), configDetails.Version)
cfg.Networks, err = LoadNetworks(getSection(config, "networks"))
if err != nil {
return nil, err
}
@ -282,11 +268,11 @@ func loadSections(filename string, config map[string]interface{}, configDetails
if err != nil {
return nil, err
}
cfg.Secrets, err = LoadSecrets(getSection(config, "secrets"), configDetails)
cfg.Secrets, err = LoadSecrets(getSection(config, "secrets"), configDetails, opts.ResolvePaths)
if err != nil {
return nil, err
}
cfg.Configs, err = LoadConfigObjs(getSection(config, "configs"), configDetails)
cfg.Configs, err = LoadConfigObjs(getSection(config, "configs"), configDetails, opts.ResolvePaths)
if err != nil {
return nil, err
}
@ -439,6 +425,14 @@ 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"]
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{}) {
servicesDict[k] = v
}
}
for name := range servicesDict {
serviceConfig, err := loadServiceWithExtends(filename, name, servicesDict, workingDir, lookupEnv, opts, &cycleTracker{})
if err != nil {
@ -456,7 +450,12 @@ func loadServiceWithExtends(filename, name string, servicesDict map[string]inter
return nil, err
}
serviceConfig, err := LoadService(name, servicesDict[name].(map[string]interface{}), workingDir, lookupEnv)
target, ok := servicesDict[name]
if !ok {
return nil, fmt.Errorf("cannot extend service %q in %s: service not found", name, filename)
}
serviceConfig, err := LoadService(name, target.(map[string]interface{}), workingDir, lookupEnv, opts.ResolvePaths)
if err != nil {
return nil, err
}
@ -478,15 +477,7 @@ func loadServiceWithExtends(filename, name string, servicesDict map[string]inter
return nil, err
}
if !opts.SkipInterpolation {
substitute, err := opts.Interpolate.Substitute(string(bytes), template.Mapping(opts.Interpolate.LookupValue))
if err != nil {
return nil, err
}
bytes = []byte(substitute)
}
baseFile, err := ParseYAML(bytes)
baseFile, err := parseConfig(bytes, opts)
if err != nil {
return nil, err
}
@ -516,10 +507,10 @@ func loadServiceWithExtends(filename, name string, servicesDict map[string]inter
}
}
if err := mergo.Merge(baseService, serviceConfig, mergo.WithAppendSlice, mergo.WithOverride, mergo.WithTransformers(serviceSpecials)); err != nil {
return nil, errors.Wrapf(err, "cannot merge service %s", name)
serviceConfig, err = _merge(baseService, serviceConfig)
if err != nil {
return nil, err
}
serviceConfig = baseService
}
return serviceConfig, nil
@ -527,8 +518,10 @@ func loadServiceWithExtends(filename, name string, servicesDict map[string]inter
// LoadService produces a single ServiceConfig from a compose file Dict
// the serviceDict is not validated if directly used. Use Load() to enable validation
func LoadService(name string, serviceDict map[string]interface{}, workingDir string, lookupEnv template.Mapping) (*types.ServiceConfig, error) {
serviceConfig := &types.ServiceConfig{}
func LoadService(name string, serviceDict map[string]interface{}, workingDir string, lookupEnv template.Mapping, resolvePaths bool) (*types.ServiceConfig, error) {
serviceConfig := &types.ServiceConfig{
Scale: 1,
}
if err := Transform(serviceDict, serviceConfig); err != nil {
return nil, err
}
@ -538,8 +531,18 @@ func LoadService(name string, serviceDict map[string]interface{}, workingDir str
return nil, err
}
if err := resolveVolumePaths(serviceConfig.Volumes, workingDir, lookupEnv); err != nil {
return nil, err
for i, volume := range serviceConfig.Volumes {
if volume.Type != "bind" {
continue
}
if volume.Source == "" {
return nil, errors.New(`invalid mount config for type "bind": field Source must not be empty`)
}
if resolvePaths {
serviceConfig.Volumes[i] = resolveVolumePath(volume, workingDir, lookupEnv)
}
}
return serviceConfig, nil
@ -574,30 +577,19 @@ func resolveEnvironment(serviceConfig *types.ServiceConfig, workingDir string, l
return nil
}
func resolveVolumePaths(volumes []types.ServiceVolumeConfig, workingDir string, lookupEnv template.Mapping) error {
for i, volume := range volumes {
if volume.Type != "bind" {
continue
}
if volume.Source == "" {
return errors.New(`invalid mount config for type "bind": field Source must not be empty`)
}
filePath := expandUser(volume.Source, lookupEnv)
// Check if source is an absolute path (either Unix or Windows), to
// handle a Windows client with a Unix daemon or vice-versa.
//
// 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) {
filePath = absPath(workingDir, filePath)
}
volume.Source = filePath
volumes[i] = volume
func resolveVolumePath(volume types.ServiceVolumeConfig, workingDir string, lookupEnv template.Mapping) types.ServiceVolumeConfig {
filePath := expandUser(volume.Source, lookupEnv)
// Check if source is an absolute path (either Unix or Windows), to
// handle a Windows client with a Unix daemon or vice-versa.
//
// 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) {
filePath = absPath(workingDir, filePath)
}
return nil
volume.Source = filePath
return volume
}
// TODO: make this more robust
@ -633,7 +625,7 @@ func transformUlimits(data interface{}) (interface{}, error) {
// LoadNetworks produces a NetworkConfig map from a compose file Dict
// the source Dict is not validated if directly used. Use Load() to enable validation
func LoadNetworks(source map[string]interface{}, version string) (map[string]types.NetworkConfig, error) {
func LoadNetworks(source map[string]interface{}) (map[string]types.NetworkConfig, error) {
networks := make(map[string]types.NetworkConfig)
err := Transform(source, &networks)
if err != nil {
@ -701,13 +693,13 @@ func LoadVolumes(source map[string]interface{}) (map[string]types.VolumeConfig,
// LoadSecrets produces a SecretConfig map from a compose file Dict
// the source Dict is not validated if directly used. Use Load() to enable validation
func LoadSecrets(source map[string]interface{}, details types.ConfigDetails) (map[string]types.SecretConfig, error) {
func LoadSecrets(source map[string]interface{}, details types.ConfigDetails, resolvePaths bool) (map[string]types.SecretConfig, error) {
secrets := make(map[string]types.SecretConfig)
if err := Transform(source, &secrets); err != nil {
return secrets, err
}
for name, secret := range secrets {
obj, err := loadFileObjectConfig(name, "secret", types.FileObjectConfig(secret), details)
obj, err := loadFileObjectConfig(name, "secret", types.FileObjectConfig(secret), details, resolvePaths)
if err != nil {
return nil, err
}
@ -719,13 +711,13 @@ func LoadSecrets(source map[string]interface{}, details types.ConfigDetails) (ma
// LoadConfigObjs produces a ConfigObjConfig map from a compose file Dict
// the source Dict is not validated if directly used. Use Load() to enable validation
func LoadConfigObjs(source map[string]interface{}, details types.ConfigDetails) (map[string]types.ConfigObjConfig, error) {
func LoadConfigObjs(source map[string]interface{}, details types.ConfigDetails, resolvePaths bool) (map[string]types.ConfigObjConfig, error) {
configs := make(map[string]types.ConfigObjConfig)
if err := Transform(source, &configs); err != nil {
return configs, err
}
for name, config := range configs {
obj, err := loadFileObjectConfig(name, "config", types.FileObjectConfig(config), details)
obj, err := loadFileObjectConfig(name, "config", types.FileObjectConfig(config), details, resolvePaths)
if err != nil {
return nil, err
}
@ -735,7 +727,7 @@ func LoadConfigObjs(source map[string]interface{}, details types.ConfigDetails)
return configs, nil
}
func loadFileObjectConfig(name string, objType string, obj types.FileObjectConfig, details types.ConfigDetails) (types.FileObjectConfig, error) {
func loadFileObjectConfig(name string, objType string, obj types.FileObjectConfig, details types.ConfigDetails, resolvePaths bool) (types.FileObjectConfig, error) {
// if "external: true"
switch {
case obj.External.External:
@ -758,7 +750,9 @@ func loadFileObjectConfig(name string, objType string, obj types.FileObjectConfi
return obj, errors.Errorf("%[1]s %[2]s: %[1]s.driver and %[1]s.file conflict; only use %[1]s.driver", objType, name)
}
default:
obj.File = absPath(details.WorkingDir, obj.File)
if resolvePaths {
obj.File = absPath(details.WorkingDir, obj.File)
}
}
return obj, nil
@ -1018,10 +1012,13 @@ var transformSize TransformerFunc = func(value interface{}) (interface{}, error)
switch value := value.(type) {
case int:
return int64(value), nil
case int64, types.UnitBytes:
return value, nil
case string:
return units.RAMInBytes(value)
default:
return value, errors.Errorf("invalid type for size %T", value)
}
panic(errors.Errorf("invalid type for size %T", value))
}
var transformStringToDuration TransformerFunc = func(value interface{}) (interface{}, error) {

View File

@ -33,6 +33,7 @@ var serviceSpecials = &specials{
m: map[reflect.Type]func(dst, src reflect.Value) error{
reflect.TypeOf(&types.LoggingConfig{}): safelyMerge(mergeLoggingConfig),
reflect.TypeOf(&types.UlimitsConfig{}): safelyMerge(mergeUlimitsConfig),
reflect.TypeOf([]types.ServiceVolumeConfig{}): mergeSlice(toServiceVolumeConfigsMap, toServiceVolumeConfigsSlice),
reflect.TypeOf([]types.ServicePortConfig{}): mergeSlice(toServicePortConfigsMap, toServicePortConfigsSlice),
reflect.TypeOf([]types.ServiceSecretConfig{}): mergeSlice(toServiceSecretConfigsMap, toServiceSecretConfigsSlice),
reflect.TypeOf([]types.ServiceConfigObjConfig{}): mergeSlice(toServiceConfigObjConfigsMap, toSServiceConfigObjConfigsSlice),
@ -86,13 +87,11 @@ func mergeServices(base, override []types.ServiceConfig) ([]types.ServiceConfig,
for name, overrideService := range overrideServices {
overrideService := overrideService
if baseService, ok := baseServices[name]; ok {
if err := mergo.Merge(&baseService, &overrideService, mergo.WithAppendSlice, mergo.WithOverride, mergo.WithTransformers(serviceSpecials)); err != nil {
return base, errors.Wrapf(err, "cannot merge service %s", name)
merged, err := _merge(&baseService, &overrideService)
if err != nil {
return nil, errors.Wrapf(err, "cannot merge service %s", name)
}
if len(overrideService.Command) > 0 {
baseService.Command = overrideService.Command
}
baseServices[name] = baseService
baseServices[name] = *merged
continue
}
baseServices[name] = overrideService
@ -105,6 +104,19 @@ func mergeServices(base, override []types.ServiceConfig) ([]types.ServiceConfig,
return services, nil
}
func _merge(baseService *types.ServiceConfig, overrideService *types.ServiceConfig) (*types.ServiceConfig, error) {
if err := mergo.Merge(baseService, overrideService, mergo.WithAppendSlice, mergo.WithOverride, mergo.WithTransformers(serviceSpecials)); err != nil {
return nil, err
}
if overrideService.Command != nil {
baseService.Command = overrideService.Command
}
if overrideService.Entrypoint != nil {
baseService.Entrypoint = overrideService.Entrypoint
}
return baseService, nil
}
func toServiceSecretConfigsMap(s interface{}) (map[interface{}]interface{}, error) {
secrets, ok := s.([]types.ServiceSecretConfig)
if !ok {
@ -135,8 +147,33 @@ func toServicePortConfigsMap(s interface{}) (map[interface{}]interface{}, error)
return nil, errors.Errorf("not a servicePortConfig slice: %v", s)
}
m := map[interface{}]interface{}{}
type port struct {
target uint32
published uint32
ip string
protocol string
}
for _, p := range ports {
m[p.Published] = p
mergeKey := port{
target: p.Target,
published: p.Published,
ip: p.HostIP,
protocol: p.Protocol,
}
m[mergeKey] = p
}
return m, nil
}
func toServiceVolumeConfigsMap(s interface{}) (map[interface{}]interface{}, error) {
volumes, ok := s.([]types.ServiceVolumeConfig)
if !ok {
return nil, errors.Errorf("not a ServiceVolumeConfig slice: %v", s)
}
m := map[interface{}]interface{}{}
for _, v := range volumes {
m[v.Target] = v
}
return m, nil
}
@ -166,7 +203,28 @@ func toServicePortConfigsSlice(dst reflect.Value, m map[interface{}]interface{})
for _, v := range m {
s = append(s, v.(types.ServicePortConfig))
}
sort.Slice(s, func(i, j int) bool { return s[i].Published < s[j].Published })
sort.Slice(s, func(i, j int) bool {
if s[i].Target != s[j].Target {
return s[i].Target < s[j].Target
}
if s[i].Published != s[j].Published {
return s[i].Published < s[j].Published
}
if s[i].HostIP != s[j].HostIP {
return s[i].HostIP < s[j].HostIP
}
return s[i].Protocol < s[j].Protocol
})
dst.Set(reflect.ValueOf(s))
return nil
}
func toServiceVolumeConfigsSlice(dst reflect.Value, m map[interface{}]interface{}) error {
s := []types.ServiceVolumeConfig{}
for _, v := range m {
s = append(s, v.(types.ServiceVolumeConfig))
}
sort.Slice(s, func(i, j int) bool { return s[i].Target < s[j].Target })
dst.Set(reflect.ValueOf(s))
return nil
}

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) error {
func normalize(project *types.Project, resolvePaths bool) error {
absWorkingDir, err := filepath.Abs(project.WorkingDir)
if err != nil {
return err
@ -41,6 +41,10 @@ func normalize(project *types.Project) error {
}
project.ComposeFiles = absComposeFiles
if project.Networks == nil {
project.Networks = make(map[string]types.NetworkConfig)
}
// If not declared explicitly, Compose model involves an implicit "default" network
if _, ok := project.Networks["default"]; !ok {
project.Networks["default"] = types.NetworkConfig{}
@ -72,8 +76,9 @@ func normalize(project *types.Project) error {
}
localContext := absPath(project.WorkingDir, s.Build.Context)
if _, err := os.Stat(localContext); err == nil {
s.Build.Context = localContext
s.Build.Dockerfile = absPath(localContext, s.Build.Dockerfile)
if resolvePaths {
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
@ -82,17 +87,22 @@ func normalize(project *types.Project) error {
}
s.Environment = s.Environment.Resolve(fn)
err := relocateLogDriver(s)
err := relocateLogDriver(&s)
if err != nil {
return err
}
err = relocateLogOpt(s)
err = relocateLogOpt(&s)
if err != nil {
return err
}
err = relocateDockerfile(s)
err = relocateDockerfile(&s)
if err != nil {
return err
}
err = relocateScale(&s)
if err != nil {
return err
}
@ -105,6 +115,21 @@ func normalize(project *types.Project) error {
return nil
}
func relocateScale(s *types.ServiceConfig) error {
scale := uint64(s.Scale)
if scale != 1 {
logrus.Warn("`scale` is deprecated. Use the `deploy.replicas` element")
if s.Deploy == nil {
s.Deploy = &types.DeployConfig{}
}
if s.Deploy.Replicas != nil && *s.Deploy.Replicas != scale {
return errors.Wrap(errdefs.ErrInvalid, "can't use both 'scale' (deprecated) and 'deploy.replicas'")
}
s.Deploy.Replicas = &scale
}
return nil
}
func absComposeFiles(composeFiles []string) ([]string, error) {
absComposeFiles := make([]string, len(composeFiles))
for i, composeFile := range composeFiles {
@ -191,7 +216,7 @@ func relocateExternalName(project *types.Project) error {
return nil
}
func relocateLogOpt(s types.ServiceConfig) error {
func relocateLogOpt(s *types.ServiceConfig) error {
if len(s.LogOpt) != 0 {
logrus.Warn("`log_opts` is deprecated. Use the `logging` element")
if s.Logging == nil {
@ -208,7 +233,7 @@ func relocateLogOpt(s types.ServiceConfig) error {
return nil
}
func relocateLogDriver(s types.ServiceConfig) error {
func relocateLogDriver(s *types.ServiceConfig) error {
if s.LogDriver != "" {
logrus.Warn("`log_driver` is deprecated. Use the `logging` element")
if s.Logging == nil {
@ -223,7 +248,7 @@ func relocateLogDriver(s types.ServiceConfig) error {
return nil
}
func relocateDockerfile(s types.ServiceConfig) error {
func relocateDockerfile(s *types.ServiceConfig) error {
if s.Dockerfile != "" {
logrus.Warn("`dockerfile` is deprecated. Use the `build` element")
if s.Build == nil {

View File

@ -38,20 +38,13 @@ func checkConsistency(project *types.Project) error {
}
}
if strings.HasPrefix(s.NetworkMode, types.NetworkModeServicePrefix) {
serviceName := s.NetworkMode[len(types.NetworkModeServicePrefix):]
if strings.HasPrefix(s.NetworkMode, types.ServicePrefix) {
serviceName := s.NetworkMode[len(types.ServicePrefix):]
if _, err := project.GetServices(serviceName); err != nil {
return fmt.Errorf("service %q not found for network_mode 'service:%s'", serviceName, serviceName)
}
}
if strings.HasPrefix(s.NetworkMode, types.NetworkModeContainerPrefix) {
containerName := s.NetworkMode[len(types.NetworkModeContainerPrefix):]
if _, err := project.GetByContainerName(containerName); err != nil {
return fmt.Errorf("service with container_name %q not found for network_mode 'container:%s'", containerName, containerName)
}
}
for _, volume := range s.Volumes {
switch volume.Type {
case types.VolumeTypeVolume:

View File

@ -331,7 +331,7 @@
"privileged": {"type": "boolean"},
"profiles": {"$ref": "#/definitions/list_of_strings"},
"pull_policy": {"type": "string", "enum": [
"always", "never", "if_not_present", "build"
"always", "never", "if_not_present", "build", "missing"
]},
"read_only": {"type": "boolean"},
"restart": {"type": "string"},
@ -367,6 +367,7 @@
"stdin_open": {"type": "boolean"},
"stop_grace_period": {"type": "string", "format": "duration"},
"stop_signal": {"type": "string"},
"storage_opt": {"type": "object"},
"tmpfs": {"$ref": "#/definitions/string_or_list"},
"tty": {"type": "boolean"},
"ulimits": {
@ -426,8 +427,10 @@
"type": "object",
"properties": {
"size": {
"type": "integer",
"minimum": 0
"oneOf": [
{"type": "integer", "minimum": 0},
{"type": "string"}
]
}
},
"additionalProperties": false,
@ -599,12 +602,12 @@
"items": {
"type": "object",
"properties": {
"capabilities": {"$ref": "#/definitions/list_of_strings"},
"count": {"type": ["string", "integer"]},
"device_ids": {"$ref": "#/definitions/list_of_strings"},
"driver":{"type": "string"},
"options":{"$ref": "#/definitions/list_or_dict"}
},
"capabilities": {"$ref": "#/definitions/list_of_strings"},
"count": {"type": ["string", "integer"]},
"device_ids": {"$ref": "#/definitions/list_of_strings"},
"driver":{"type": "string"},
"options":{"$ref": "#/definitions/list_or_dict"}
},
"additionalProperties": false,
"patternProperties": {"^x-": {}}
}
@ -769,7 +772,7 @@
"type": "object",
"patternProperties": {
".+": {
"type": ["string", "number", "null"]
"type": ["string", "number", "boolean", "null"]
}
},
"additionalProperties": false
@ -810,4 +813,4 @@
}
}
}
}
}

View File

@ -25,11 +25,12 @@ import (
)
var delimiter = "\\$"
var substitution = "[_a-z][_a-z0-9]*(?::?[-?][^}]*)?"
var substitutionNamed = "[_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>))",
delimiter, delimiter, substitution, substitution,
delimiter, delimiter, substitutionNamed, substitutionBraced,
)
var defaultPattern = regexp.MustCompile(patternString)
@ -74,9 +75,11 @@ func SubstituteWith(template string, mapping Mapping, pattern *regexp.Regexp, su
return escaped
}
braced := false
substitution := groups["named"]
if substitution == "" {
substitution = groups["braced"]
braced = true
}
if substitution == "" {
@ -84,19 +87,21 @@ func SubstituteWith(template string, mapping Mapping, pattern *regexp.Regexp, su
return ""
}
for _, f := range subsFuncs {
var (
value string
applied bool
)
value, applied, err = f(substitution, mapping)
if err != nil {
return ""
if braced {
for _, f := range subsFuncs {
var (
value string
applied bool
)
value, applied, err = f(substitution, mapping)
if err != nil {
return ""
}
if !applied {
continue
}
return value
}
if !applied {
continue
}
return value
}
value, ok := mapping(substitution)

View File

@ -94,24 +94,6 @@ func (p Project) ConfigNames() []string {
return names
}
func (p Project) GetByContainerName(names ...string) (Services, error) {
if len(names) == 0 {
return p.Services, nil
}
services := Services{}
outLoop:
for _, name := range names {
for _, s := range p.Services {
if name == s.ContainerName {
services = append(services, s)
continue outLoop
}
}
return nil, fmt.Errorf("service with container_name %q could not be found", name)
}
return services, nil
}
// GetServices retrieve services by names, or return all services if no name specified
func (p Project) GetServices(names ...string) (Services, error) {
if len(names) == 0 {
@ -228,6 +210,11 @@ func (s Services) GetProfiles() []string {
// ApplyProfiles disables service which don't match selected profiles
func (p *Project) ApplyProfiles(profiles []string) {
for _, p := range profiles {
if p == "*" {
return
}
}
var enabled, disabled Services
for _, service := range p.Services {
if service.HasProfile(profiles) {

View File

@ -89,7 +89,7 @@ type ServiceConfig struct {
Profiles []string `mapstructure:"profiles" yaml:"profiles,omitempty" json:"profiles,omitempty"`
Build *BuildConfig `yaml:",omitempty" json:"build,omitempty"`
BlkioConfig *BlkioConfig `yaml:",omitempty" json:"blkio_config,omitempty"`
BlkioConfig *BlkioConfig `mapstructure:"blkio_config" yaml:",omitempty" json:"blkio_config,omitempty"`
CapAdd []string `mapstructure:"cap_add" yaml:"cap_add,omitempty" json:"cap_add,omitempty"`
CapDrop []string `mapstructure:"cap_drop" yaml:"cap_drop,omitempty" json:"cap_drop,omitempty"`
CgroupParent string `mapstructure:"cgroup_parent" yaml:"cgroup_parent,omitempty" json:"cgroup_parent,omitempty"`
@ -152,7 +152,7 @@ type ServiceConfig struct {
ReadOnly bool `mapstructure:"read_only" yaml:"read_only,omitempty" json:"read_only,omitempty"`
Restart string `yaml:",omitempty" json:"restart,omitempty"`
Runtime string `yaml:",omitempty" json:"runtime,omitempty"`
Scale int `yaml:",omitempty" json:"scale,omitempty"`
Scale int `yaml:"-" json:"-"`
Secrets []ServiceSecretConfig `yaml:",omitempty" json:"secrets,omitempty"`
SecurityOpt []string `mapstructure:"security_opt" yaml:"security_opt,omitempty" json:"security_opt,omitempty"`
ShmSize UnitBytes `mapstructure:"shm_size" yaml:"shm_size,omitempty" json:"shm_size,omitempty"`
@ -226,10 +226,17 @@ const (
)
const (
// ServicePrefix is the prefix for references pointing to a service
ServicePrefix = "service:"
// ContainerPrefix is the prefix for references pointing to a container
ContainerPrefix = "container:"
// NetworkModeServicePrefix is the prefix for network_mode pointing to a service
NetworkModeServicePrefix = "service:"
// Deprecated prefer ServicePrefix
NetworkModeServicePrefix = ServicePrefix
// NetworkModeContainerPrefix is the prefix for network_mode pointing to a container
NetworkModeContainerPrefix = "container:"
// Deprecated prefer ContainerPrefix
NetworkModeContainerPrefix = ContainerPrefix
)
// GetDependencies retrieve all services this service depends on
@ -246,9 +253,21 @@ func (s ServiceConfig) GetDependencies() []string {
dependencies.append(link)
}
}
if strings.HasPrefix(s.NetworkMode, NetworkModeServicePrefix) {
dependencies.append(s.NetworkMode[len(NetworkModeServicePrefix):])
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) {
dependencies.append(vol)
}
}
return dependencies.toSlice()
}
@ -352,7 +371,7 @@ func (e MappingWithEquals) OverrideBy(other MappingWithEquals) MappingWithEquals
// Resolve update a MappingWithEquals for keys without value (`key`, but not `key=`)
func (e MappingWithEquals) Resolve(lookupFn func(string) (string, bool)) MappingWithEquals {
for k, v := range e {
if v == nil || *v == "" {
if v == nil {
if value, ok := lookupFn(k); ok {
e[k] = &value
}
@ -558,7 +577,7 @@ type ServiceNetworkConfig struct {
// ServicePortConfig is the port configuration for a service
type ServicePortConfig struct {
Mode string `yaml:",omitempty" json:"mode,omitempty"`
HostIP string `yaml:"host_ip,omitempty" json:"host_ip,omitempty"`
HostIP string `mapstructure:"host_ip" yaml:"host_ip,omitempty" json:"host_ip,omitempty"`
Target uint32 `yaml:",omitempty" json:"target,omitempty"`
Published uint32 `yaml:",omitempty" json:"published,omitempty"`
Protocol string `yaml:",omitempty" json:"protocol,omitempty"`
@ -671,7 +690,7 @@ type ServiceVolumeVolume struct {
// ServiceVolumeTmpfs are options for a service volume of type tmpfs
type ServiceVolumeTmpfs struct {
Size int64 `yaml:",omitempty" json:"size,omitempty"`
Size UnitBytes `yaml:",omitempty" json:"size,omitempty"`
Extensions map[string]interface{} `yaml:",inline" json:"-"`
}
@ -729,6 +748,7 @@ type NetworkConfig struct {
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:"-"`
}

27
vendor/github.com/compose-spec/godotenv/Earthfile generated vendored Normal file
View File

@ -0,0 +1,27 @@
ARG GOLANG_VERSION=1.17.1
ARG ALPINE_VERSION=3.14
FROM golang:${GOLANG_VERSION}-alpine${ALPINE_VERSION}
WORKDIR /code
code:
FROM +base
COPY . .
golangci:
ARG GOLANGCI_VERSION=v1.40.1
FROM golangci/golangci-lint:${GOLANGCI_VERSION}-alpine
SAVE ARTIFACT /usr/bin/golangci-lint
lint:
FROM +code
COPY +golangci/golangci-lint /usr/bin/golangci-lint
RUN golangci-lint run --timeout 5m ./...
test:
FROM +code
RUN go test ./...
all:
BUILD +lint
BUILD +test

18
vendor/github.com/compose-spec/godotenv/README.md generated vendored Normal file
View File

@ -0,0 +1,18 @@
# GoDotEnv
A Go (golang) port of the Ruby dotenv project (which loads env vars from a .env file)
From the original Library:
> Storing configuration in the environment is one of the tenets of a twelve-factor app. Anything that is likely to change between deployment environmentssuch as resource handles for databases or credentials for external servicesshould be extracted from the code into environment variables.
>
> But it is not always practical to set environment variables on development machines or continuous integration servers where multiple projects are run. Dotenv load variables from a .env file into ENV when the environment is bootstrapped.
This is a fork of [joho/godotenv](https://github.com/joho/godotenv) focussing on `.env` file support by the compose specification
To run linter and tests, please install [Earthly](https://earthly.dev/get-earthly) and run:
```sh
earthly +all
```

3
vendor/github.com/compose-spec/godotenv/go.mod generated vendored Normal file
View File

@ -0,0 +1,3 @@
module github.com/compose-spec/godotenv
go 1.16

0
vendor/github.com/compose-spec/godotenv/go.sum generated vendored Normal file
View File

View File

@ -14,19 +14,42 @@
package godotenv
import (
"bufio"
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"os/exec"
"regexp"
"sort"
"strconv"
"strings"
)
const doubleQuoteSpecialChars = "\\\n\r\"!$`"
// LookupFn represents a lookup function to resolve variables from
type LookupFn func(string) (string, bool)
var noLookupFn = func(s string) (string, bool) {
return "", false
}
// Parse reads an env file from io.Reader, returning a map of keys and values.
func Parse(r io.Reader) (map[string]string, error) {
return ParseWithLookup(r, nil)
}
// 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)
if err != nil {
return nil, err
}
return UnmarshalBytesWithLookup(data, lookupFn)
}
// Load will read your env file(s) and load them into ENV for this process.
//
// Call this function as close as possible to the start of your program (ideally in main)
@ -39,15 +62,7 @@ const doubleQuoteSpecialChars = "\\\n\r\"!$`"
//
// 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) {
filenames = filenamesOrDefault(filenames)
for _, filename := range filenames {
err = loadFile(filename, false)
if err != nil {
return // return early on a spazout
}
}
return
return load(false, filenames...)
}
// Overload will read your env file(s) and load them into ENV for this process.
@ -62,10 +77,14 @@ func Load(filenames ...string) (err error) {
//
// 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) {
return load(true, filenames...)
}
func load(overload bool, filenames ...string) (err error) {
filenames = filenamesOrDefault(filenames)
for _, filename := range filenames {
err = loadFile(filename, true)
err = loadFile(filename, overload)
if err != nil {
return // return early on a spazout
}
@ -73,14 +92,14 @@ func Overload(filenames ...string) (err error) {
return
}
// Read all env (with same file loading semantics as Load) but return values as
// 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 Read(filenames ...string) (envMap map[string]string, err error) {
func ReadWithLookup(lookupFn LookupFn, filenames ...string) (envMap map[string]string, err error) {
filenames = filenamesOrDefault(filenames)
envMap = make(map[string]string)
for _, filename := range filenames {
individualEnvMap, individualErr := readFile(filename)
individualEnvMap, individualErr := readFile(filename, lookupFn)
if individualErr != nil {
err = individualErr
@ -95,37 +114,27 @@ func Read(filenames ...string) (envMap map[string]string, err error) {
return
}
// Parse reads an env file from io.Reader, returning a map of keys and values.
func Parse(r io.Reader) (envMap map[string]string, err error) {
envMap = make(map[string]string)
var lines []string
scanner := bufio.NewScanner(r)
for scanner.Scan() {
lines = append(lines, scanner.Text())
}
if err = scanner.Err(); err != nil {
return
}
for _, fullLine := range lines {
if !isIgnoredLine(fullLine) {
var key, value string
key, value, err = parseLine(fullLine, envMap)
if err != nil {
return
}
envMap[key] = value
}
}
return
// 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) {
return ReadWithLookup(nil, filenames...)
}
//Unmarshal reads an env file from a string, returning a map of keys and values.
// 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) {
return Parse(strings.NewReader(str))
return UnmarshalBytes([]byte(str))
}
// UnmarshalBytes parses env file from byte slice of chars, returning a map of keys and values.
func UnmarshalBytes(src []byte) (map[string]string, error) {
return UnmarshalBytesWithLookup(src, nil)
}
// 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) {
out := make(map[string]string)
err := parseBytes(src, out, lookupFn)
return out, err
}
// Exec loads env vars from the specified filenames (empty map falls back to default)
@ -136,7 +145,9 @@ func Unmarshal(str string) (envMap map[string]string, err error) {
// If you want more fine grained control over your command it's recommended
// that you use `Load()` or `Read()` and the `os/exec` package yourself.
func Exec(filenames []string, cmd string, cmdArgs []string) error {
Load(filenames...)
if err := Load(filenames...); err != nil {
return err
}
command := exec.Command(cmd, cmdArgs...)
command.Stdin = os.Stdin
@ -147,16 +158,20 @@ func Exec(filenames []string, cmd string, cmdArgs []string) error {
// Write serializes the given environment and writes it to a file
func Write(envMap map[string]string, filename string) error {
content, error := Marshal(envMap)
if error != nil {
return error
content, err := Marshal(envMap)
if err != nil {
return err
}
file, error := os.Create(filename)
if error != nil {
return error
file, err := os.Create(filename)
if err != nil {
return err
}
_, err := file.WriteString(content)
return err
defer file.Close()
_, err = file.WriteString(content + "\n")
if err != nil {
return err
}
return file.Sync()
}
// Marshal outputs the given environment as a dotenv-formatted environment file.
@ -164,7 +179,11 @@ func Write(envMap map[string]string, filename string) error {
func Marshal(envMap map[string]string) (string, error) {
lines := make([]string, 0, len(envMap))
for k, v := range envMap {
lines = append(lines, fmt.Sprintf(`%s="%s"`, k, doubleQuoteEscape(v)))
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)))
}
}
sort.Strings(lines)
return strings.Join(lines, "\n"), nil
@ -178,7 +197,7 @@ func filenamesOrDefault(filenames []string) []string {
}
func loadFile(filename string, overload bool) error {
envMap, err := readFile(filename)
envMap, err := readFile(filename, nil)
if err != nil {
return err
}
@ -192,24 +211,29 @@ func loadFile(filename string, overload bool) error {
for key, value := range envMap {
if !currentEnv[key] || overload {
os.Setenv(key, value)
_ = os.Setenv(key, value)
}
}
return nil
}
func readFile(filename string) (envMap map[string]string, err error) {
func readFile(filename string, lookupFn LookupFn) (envMap map[string]string, err error) {
file, err := os.Open(filename)
if err != nil {
return
}
defer file.Close()
return Parse(file)
return ParseWithLookup(file, lookupFn)
}
var exportRegex = regexp.MustCompile(`^\s*(?:export\s+)?(.*?)\s*$`)
func parseLine(line string, envMap map[string]string) (key string, value string, err 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
@ -250,31 +274,30 @@ func parseLine(line string, envMap map[string]string) (key string, value string,
err = errors.New("Can't separate key from value")
return
}
// Parse the key
key = splitString[0]
if strings.HasPrefix(key, "export") {
key = strings.TrimPrefix(key, "export")
}
key = strings.Trim(key, " ")
key = exportRegex.ReplaceAllString(splitString[0], "$1")
// Parse the value
value = parseValue(splitString[1], envMap)
value = parseValue(splitString[1], envMap, lookupFn)
return
}
func parseValue(value string, envMap map[string]string) string {
var (
singleQuotesRegex = regexp.MustCompile(`\A'(.*)'\z`)
doubleQuotesRegex = regexp.MustCompile(`\A"(.*)"\z`)
escapeRegex = regexp.MustCompile(`\\.`)
unescapeCharsRegex = regexp.MustCompile(`\\([^$])`)
)
func parseValue(value string, envMap map[string]string, lookupFn LookupFn) string {
// trim
value = strings.Trim(value, " ")
// check if we've got quoted values or possible escapes
if len(value) > 1 {
rs := regexp.MustCompile(`\A'(.*)'\z`)
singleQuotes := rs.FindStringSubmatch(value)
singleQuotes := singleQuotesRegex.FindStringSubmatch(value)
rd := regexp.MustCompile(`\A"(.*)"\z`)
doubleQuotes := rd.FindStringSubmatch(value)
doubleQuotes := doubleQuotesRegex.FindStringSubmatch(value)
if singleQuotes != nil || doubleQuotes != nil {
// pull the quotes off the edges
@ -283,7 +306,6 @@ func parseValue(value string, envMap map[string]string) string {
if doubleQuotes != nil {
// expand newlines
escapeRegex := regexp.MustCompile(`\\.`)
value = escapeRegex.ReplaceAllStringFunc(value, func(match string) string {
c := strings.TrimPrefix(match, `\`)
switch c {
@ -296,23 +318,22 @@ func parseValue(value string, envMap map[string]string) string {
}
})
// unescape characters
e := regexp.MustCompile(`\\([^$])`)
value = e.ReplaceAllString(value, "$1")
value = unescapeCharsRegex.ReplaceAllString(value, "$1")
}
if singleQuotes == nil {
value = expandVariables(value, envMap)
value = expandVariables(value, envMap, lookupFn)
}
}
return value
}
func expandVariables(v string, m map[string]string) string {
r := regexp.MustCompile(`(\\)?(\$)(\()?\{?([A-Z0-9_]+)?\}?`)
var expandVarRegex = regexp.MustCompile(`(\\)?(\$)(\()?\{?([A-Z0-9_]+)?\}?`)
return r.ReplaceAllStringFunc(v, func(s string) string {
submatch := r.FindStringSubmatch(s)
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
@ -320,17 +341,25 @@ func expandVariables(v string, m map[string]string) string {
if submatch[1] == "\\" || submatch[2] == "(" {
return submatch[0][1:]
} else if submatch[4] != "" {
return m[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
})
}
func isIgnoredLine(line string) bool {
trimmedLine := strings.Trim(line, " \n\t")
return len(trimmedLine) == 0 || strings.HasPrefix(trimmedLine, "#")
}
func doubleQuoteEscape(line string) string {
for _, c := range doubleQuoteSpecialChars {
toReplace := "\\" + string(c)

223
vendor/github.com/compose-spec/godotenv/parser.go generated vendored Normal file
View File

@ -0,0 +1,223 @@
package godotenv
import (
"bytes"
"errors"
"fmt"
"strings"
"unicode"
)
const (
charComment = '#'
prefixSingleQuote = '\''
prefixDoubleQuote = '"'
exportPrefix = "export"
)
func parseBytes(src []byte, out map[string]string, lookupFn LookupFn) error {
cutset := src
for {
cutset = getStatementStart(cutset)
if cutset == nil {
// reached end of file
break
}
key, left, inherited, err := locateKeyName(cutset)
if err != nil {
return err
}
if strings.Contains(key, " ") {
return errors.New("key cannot contain a space")
}
if inherited {
if lookupFn == nil {
lookupFn = noLookupFn
}
value, ok := lookupFn(key)
if ok {
out[key] = value
cutset = left
continue
}
}
value, left, err := extractVarValue(left, out, lookupFn)
if err != nil {
return err
}
out[key] = value
cutset = left
}
return nil
}
// getStatementPosition returns position of statement begin.
//
// It skips any comment line or non-whitespace character.
func getStatementStart(src []byte) []byte {
pos := indexOfNonSpaceChar(src)
if pos == -1 {
return nil
}
src = src[pos:]
if src[0] != charComment {
return src
}
// skip comment section
pos = bytes.IndexFunc(src, isCharFunc('\n'))
if pos == -1 {
return nil
}
return getStatementStart(src[pos:])
}
// locateKeyName locates and parses key name and returns rest of slice
func locateKeyName(src []byte) (key string, cutset []byte, inherited bool, err error) {
// trim "export" and space at beginning
src = bytes.TrimLeftFunc(bytes.TrimPrefix(src, []byte(exportPrefix)), 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) {
continue
}
switch char {
case '=', ':', '\n':
// library also supports yaml-style value declaration
key = string(src[0:i])
offset = i + 1
inherited = char == '\n'
break loop
case '_':
default:
// variable name should match [A-Za-z0-9_]
if unicode.IsLetter(rchar) || unicode.IsNumber(rchar) {
continue
}
return "", nil, inherited, fmt.Errorf(
`unexpected character %q in variable name near %q`,
string(char), string(src))
}
}
if len(src) == 0 {
return "", nil, inherited, errors.New("zero length string")
}
// trim whitespace
key = strings.TrimRightFunc(key, unicode.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) {
quote, hasPrefix := hasQuotePrefix(src)
if !hasPrefix {
// unquoted value - read until whitespace
end := bytes.IndexFunc(src, unicode.IsSpace)
if end == -1 {
return expandVariables(string(src), envMap, lookupFn), nil, nil
}
return expandVariables(string(src[0:end]), envMap, lookupFn), src[end:], nil
}
// lookup quoted string terminator
for i := 1; i < len(src); i++ {
if char := src[i]; char != quote {
continue
}
// skip escaped quote symbol (\" or \', depends on quote)
if prevChar := src[i-1]; prevChar == '\\' {
continue
}
// trim quotes
trimFunc := isCharFunc(rune(quote))
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)
}
return value, src[i+1:], nil
}
// return formatted error if quoted string is not terminated
valEndIndex := bytes.IndexFunc(src, isCharFunc('\n'))
if valEndIndex == -1 {
valEndIndex = len(src)
}
return "", nil, fmt.Errorf("unterminated quoted value %s", src[:valEndIndex])
}
func expandEscapes(str string) string {
out := escapeRegex.ReplaceAllStringFunc(str, func(match string) string {
c := strings.TrimPrefix(match, `\`)
switch c {
case "n":
return "\n"
case "r":
return "\r"
default:
return match
}
})
return unescapeCharsRegex.ReplaceAllString(out, "$1")
}
func indexOfNonSpaceChar(src []byte) int {
return bytes.IndexFunc(src, func(r rune) bool {
return !unicode.IsSpace(r)
})
}
// hasQuotePrefix reports whether charset starts with single or double quote and returns quote character
func hasQuotePrefix(src []byte) (prefix byte, isQuored bool) {
if len(src) == 0 {
return 0, false
}
switch prefix := src[0]; prefix {
case prefixDoubleQuote, prefixSingleQuote:
return prefix, true
default:
return 0, false
}
}
func isCharFunc(char rune) func(rune) bool {
return func(v rune) bool {
return v == char
}
}
// isSpace reports whether the rune is a space character but not line break character
//
// this differs from unicode.IsSpace, which also applies line break as space
func isSpace(r rune) bool {
switch r {
case '\t', '\v', '\f', '\r', ' ', 0x85, 0xA0:
return true
}
return false
}

View File

@ -1,8 +0,0 @@
language: go
go:
- 1.x
os:
- linux
- osx

View File

@ -1,163 +0,0 @@
# GoDotEnv [![Build Status](https://travis-ci.org/joho/godotenv.svg?branch=master)](https://travis-ci.org/joho/godotenv) [![Build status](https://ci.appveyor.com/api/projects/status/9v40vnfvvgde64u4?svg=true)](https://ci.appveyor.com/project/joho/godotenv) [![Go Report Card](https://goreportcard.com/badge/github.com/joho/godotenv)](https://goreportcard.com/report/github.com/joho/godotenv)
A Go (golang) port of the Ruby dotenv project (which loads env vars from a .env file)
From the original Library:
> Storing configuration in the environment is one of the tenets of a twelve-factor app. Anything that is likely to change between deployment environmentssuch as resource handles for databases or credentials for external servicesshould be extracted from the code into environment variables.
>
> But it is not always practical to set environment variables on development machines or continuous integration servers where multiple projects are run. Dotenv load variables from a .env file into ENV when the environment is bootstrapped.
It can be used as a library (for loading in env for your own daemons etc) or as a bin command.
There is test coverage and CI for both linuxish and windows environments, but I make no guarantees about the bin version working on windows.
## Installation
As a library
```shell
go get github.com/joho/godotenv
```
or if you want to use it as a bin command
```shell
go get github.com/joho/godotenv/cmd/godotenv
```
## Usage
Add your application configuration to your `.env` file in the root of your project:
```shell
S3_BUCKET=YOURS3BUCKET
SECRET_KEY=YOURSECRETKEYGOESHERE
```
Then in your Go app you can do something like
```go
package main
import (
"github.com/joho/godotenv"
"log"
"os"
)
func main() {
err := godotenv.Load()
if err != nil {
log.Fatal("Error loading .env file")
}
s3Bucket := os.Getenv("S3_BUCKET")
secretKey := os.Getenv("SECRET_KEY")
// now do something with s3 or whatever
}
```
If you're even lazier than that, you can just take advantage of the autoload package which will read in `.env` on import
```go
import _ "github.com/joho/godotenv/autoload"
```
While `.env` in the project root is the default, you don't have to be constrained, both examples below are 100% legit
```go
_ = godotenv.Load("somerandomfile")
_ = godotenv.Load("filenumberone.env", "filenumbertwo.env")
```
If you want to be really fancy with your env file you can do comments and exports (below is a valid env file)
```shell
# I am a comment and that is OK
SOME_VAR=someval
FOO=BAR # comments at line end are OK too
export BAR=BAZ
```
Or finally you can do YAML(ish) style
```yaml
FOO: bar
BAR: baz
```
as a final aside, if you don't want godotenv munging your env you can just get a map back instead
```go
var myEnv map[string]string
myEnv, err := godotenv.Read()
s3Bucket := myEnv["S3_BUCKET"]
```
... or from an `io.Reader` instead of a local file
```go
reader := getRemoteFile()
myEnv, err := godotenv.Parse(reader)
```
... or from a `string` if you so desire
```go
content := getRemoteFileContent()
myEnv, err := godotenv.Unmarshal(content)
```
### Command Mode
Assuming you've installed the command as above and you've got `$GOPATH/bin` in your `$PATH`
```
godotenv -f /some/path/to/.env some_command with some args
```
If you don't specify `-f` it will fall back on the default of loading `.env` in `PWD`
### Writing Env Files
Godotenv can also write a map representing the environment to a correctly-formatted and escaped file
```go
env, err := godotenv.Unmarshal("KEY=value")
err := godotenv.Write(env, "./.env")
```
... or to a string
```go
env, err := godotenv.Unmarshal("KEY=value")
content, err := godotenv.Marshal(env)
```
## Contributing
Contributions are most welcome! The parser itself is pretty stupidly naive and I wouldn't be surprised if it breaks with edge cases.
*code changes without tests will not be accepted*
1. Fork it
2. Create your feature branch (`git checkout -b my-new-feature`)
3. Commit your changes (`git commit -am 'Added some feature'`)
4. Push to the branch (`git push origin my-new-feature`)
5. Create new Pull Request
## Releases
Releases should follow [Semver](http://semver.org/) though the first couple of releases are `v1` and `v1.1`.
Use [annotated tags for all releases](https://github.com/joho/godotenv/issues/30). Example `git tag -a v1.2.1`
## CI
Linux: [![Build Status](https://travis-ci.org/joho/godotenv.svg?branch=master)](https://travis-ci.org/joho/godotenv) Windows: [![Build status](https://ci.appveyor.com/api/projects/status/9v40vnfvvgde64u4)](https://ci.appveyor.com/project/joho/godotenv)
## Who?
The original library [dotenv](https://github.com/bkeepers/dotenv) was written by [Brandon Keepers](http://opensoul.org/), and this port was done by [John Barton](https://johnbarton.co/) based off the tests/fixtures in the original library.

View File

@ -1,6 +1,12 @@
## unreleased
## 1.4.2
* Fix regression where `*time.Time` value would be set to empty and not be sent
* Custom name matchers to support any sort of casing, formatting, etc. for
field names. [GH-250]
* Fix possible panic in ComposeDecodeHookFunc [GH-251]
## 1.4.1
* Fix regression where `*time.Time` value would be set to empty and not be sent
to decode hooks properly [GH-232]
## 1.4.0

View File

@ -62,7 +62,8 @@ func DecodeHookExec(
func ComposeDecodeHookFunc(fs ...DecodeHookFunc) DecodeHookFunc {
return func(f reflect.Value, t reflect.Value) (interface{}, error) {
var err error
var data interface{}
data := f.Interface()
newFrom := f
for _, f1 := range fs {
data, err = DecodeHookExec(f1, newFrom, t)

View File

@ -192,7 +192,7 @@ type DecodeHookFuncType func(reflect.Type, reflect.Type, interface{}) (interface
// source and target types.
type DecodeHookFuncKind func(reflect.Kind, reflect.Kind, interface{}) (interface{}, error)
// DecodeHookFuncRaw is a DecodeHookFunc which has complete access to both the source and target
// DecodeHookFuncValue is a DecodeHookFunc which has complete access to both the source and target
// values.
type DecodeHookFuncValue func(from reflect.Value, to reflect.Value) (interface{}, error)
@ -258,6 +258,11 @@ type DecoderConfig struct {
// The tag name that mapstructure reads for field names. This
// defaults to "mapstructure"
TagName string
// MatchName is the function used to match the map key to the struct
// field name or tag. Defaults to `strings.EqualFold`. This can be used
// to implement case-sensitive tag values, support snake casing, etc.
MatchName func(mapKey, fieldName string) bool
}
// A Decoder takes a raw interface value and turns it into structured
@ -376,6 +381,10 @@ func NewDecoder(config *DecoderConfig) (*Decoder, error) {
config.TagName = "mapstructure"
}
if config.MatchName == nil {
config.MatchName = strings.EqualFold
}
result := &Decoder{
config: config,
}
@ -1340,7 +1349,7 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e
continue
}
if strings.EqualFold(mK, fieldName) {
if d.config.MatchName(mK, fieldName) {
rawMapKey = dataValKey
rawMapVal = dataVal.MapIndex(dataValKey)
break

8
vendor/modules.txt vendored
View File

@ -30,7 +30,7 @@ github.com/cenkalti/backoff/v4
github.com/cespare/xxhash/v2
# github.com/cloudflare/cfssl v0.0.0-20181213083726-b94e044bb51e
## explicit
# github.com/compose-spec/compose-go v0.0.0-20210729195839-de56f4f0cb3c
# github.com/compose-spec/compose-go v1.0.5
## explicit
github.com/compose-spec/compose-go/errdefs
github.com/compose-spec/compose-go/interpolation
@ -38,6 +38,8 @@ github.com/compose-spec/compose-go/loader
github.com/compose-spec/compose-go/schema
github.com/compose-spec/compose-go/template
github.com/compose-spec/compose-go/types
# github.com/compose-spec/godotenv v1.1.0
github.com/compose-spec/godotenv
# github.com/containerd/console v1.0.3
## explicit
github.com/containerd/console
@ -263,8 +265,6 @@ github.com/inconshreveable/mousetrap
## explicit
# github.com/jinzhu/inflection v0.0.0-20180308033659-04140366298a
## explicit
# github.com/joho/godotenv v1.3.0
github.com/joho/godotenv
# github.com/json-iterator/go v1.1.11
github.com/json-iterator/go
# github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0
@ -284,7 +284,7 @@ github.com/matttproud/golang_protobuf_extensions/pbutil
github.com/miekg/pkcs11
# github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7
github.com/mitchellh/go-wordwrap
# github.com/mitchellh/mapstructure v1.4.1
# github.com/mitchellh/mapstructure v1.4.2
github.com/mitchellh/mapstructure
# github.com/moby/buildkit v0.9.1-0.20211019185819-8778943ac3da
## explicit