@ -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 ) {
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 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 )
yaml , err := ParseYAML ( b )
if err != nil {
return nil , err
}
b , err = yaml . Marshal ( cfg )
if err != nil {
return nil , err
if ! opts . SkipInterpolation {
return interp . Interpolate ( yaml , * opts . Interpolate )
}
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
}
return nil
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 )
}
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 ) {