github.com/khulnasoft/cli@v0.0.0-20240402070845-01bcad7beefa/cli/compose/loader/loader.go (about) 1 // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: 2 //go:build go1.19 3 4 package loader 5 6 import ( 7 "fmt" 8 "path" 9 "path/filepath" 10 "reflect" 11 "sort" 12 "strconv" 13 "strings" 14 "time" 15 16 "github.com/docker/docker/api/types/versions" 17 "github.com/google/shlex" 18 "github.com/khulnasoft-lab/go-connections/nat" 19 units "github.com/khulnasoft-lab/go-units" 20 interp "github.com/khulnasoft/cli/cli/compose/interpolation" 21 "github.com/khulnasoft/cli/cli/compose/schema" 22 "github.com/khulnasoft/cli/cli/compose/template" 23 "github.com/khulnasoft/cli/cli/compose/types" 24 "github.com/khulnasoft/cli/opts" 25 "github.com/mitchellh/mapstructure" 26 "github.com/pkg/errors" 27 "github.com/sirupsen/logrus" 28 yaml "gopkg.in/yaml.v2" 29 ) 30 31 // Options supported by Load 32 type Options struct { 33 // Skip schema validation 34 SkipValidation bool 35 // Skip interpolation 36 SkipInterpolation bool 37 // Interpolation options 38 Interpolate *interp.Options 39 // Discard 'env_file' entries after resolving to 'environment' section 40 discardEnvFiles bool 41 } 42 43 // WithDiscardEnvFiles sets the Options to discard the `env_file` section after resolving to 44 // the `environment` section 45 func WithDiscardEnvFiles(options *Options) { 46 options.discardEnvFiles = true 47 } 48 49 // ParseYAML reads the bytes from a file, parses the bytes into a mapping 50 // structure, and returns it. 51 func ParseYAML(source []byte) (map[string]any, error) { 52 var cfg any 53 if err := yaml.Unmarshal(source, &cfg); err != nil { 54 return nil, err 55 } 56 cfgMap, ok := cfg.(map[any]any) 57 if !ok { 58 return nil, errors.Errorf("top-level object must be a mapping") 59 } 60 converted, err := convertToStringKeysRecursive(cfgMap, "") 61 if err != nil { 62 return nil, err 63 } 64 return converted.(map[string]any), nil 65 } 66 67 // Load reads a ConfigDetails and returns a fully loaded configuration 68 func Load(configDetails types.ConfigDetails, opt ...func(*Options)) (*types.Config, error) { 69 if len(configDetails.ConfigFiles) < 1 { 70 return nil, errors.Errorf("No files specified") 71 } 72 73 options := &Options{ 74 Interpolate: &interp.Options{ 75 Substitute: template.Substitute, 76 LookupValue: configDetails.LookupEnv, 77 TypeCastMapping: interpolateTypeCastMapping, 78 }, 79 } 80 81 for _, op := range opt { 82 op(options) 83 } 84 85 configs := []*types.Config{} 86 var err error 87 88 for _, file := range configDetails.ConfigFiles { 89 configDict := file.Config 90 version := schema.Version(configDict) 91 if configDetails.Version == "" { 92 configDetails.Version = version 93 } 94 if configDetails.Version != version { 95 return nil, errors.Errorf("version mismatched between two composefiles : %v and %v", configDetails.Version, version) 96 } 97 98 if err := validateForbidden(configDict); err != nil { 99 return nil, err 100 } 101 102 if !options.SkipInterpolation { 103 configDict, err = interpolateConfig(configDict, *options.Interpolate) 104 if err != nil { 105 return nil, err 106 } 107 } 108 109 if !options.SkipValidation { 110 if err := schema.Validate(configDict, configDetails.Version); err != nil { 111 return nil, err 112 } 113 } 114 115 cfg, err := loadSections(configDict, configDetails) 116 if err != nil { 117 return nil, err 118 } 119 cfg.Filename = file.Filename 120 if options.discardEnvFiles { 121 for i := range cfg.Services { 122 cfg.Services[i].EnvFile = nil 123 } 124 } 125 126 configs = append(configs, cfg) 127 } 128 129 return merge(configs) 130 } 131 132 func validateForbidden(configDict map[string]any) error { 133 servicesDict, ok := configDict["services"].(map[string]any) 134 if !ok { 135 return nil 136 } 137 forbidden := getProperties(servicesDict, types.ForbiddenProperties) 138 if len(forbidden) > 0 { 139 return &ForbiddenPropertiesError{Properties: forbidden} 140 } 141 return nil 142 } 143 144 func loadSections(config map[string]any, configDetails types.ConfigDetails) (*types.Config, error) { 145 var err error 146 cfg := types.Config{ 147 Version: schema.Version(config), 148 } 149 150 loaders := []struct { 151 key string 152 fnc func(config map[string]any) error 153 }{ 154 { 155 key: "services", 156 fnc: func(config map[string]any) error { 157 cfg.Services, err = LoadServices(config, configDetails.WorkingDir, configDetails.LookupEnv) 158 return err 159 }, 160 }, 161 { 162 key: "networks", 163 fnc: func(config map[string]any) error { 164 cfg.Networks, err = LoadNetworks(config, configDetails.Version) 165 return err 166 }, 167 }, 168 { 169 key: "volumes", 170 fnc: func(config map[string]any) error { 171 cfg.Volumes, err = LoadVolumes(config, configDetails.Version) 172 return err 173 }, 174 }, 175 { 176 key: "secrets", 177 fnc: func(config map[string]any) error { 178 cfg.Secrets, err = LoadSecrets(config, configDetails) 179 return err 180 }, 181 }, 182 { 183 key: "configs", 184 fnc: func(config map[string]any) error { 185 cfg.Configs, err = LoadConfigObjs(config, configDetails) 186 return err 187 }, 188 }, 189 } 190 for _, loader := range loaders { 191 if err := loader.fnc(getSection(config, loader.key)); err != nil { 192 return nil, err 193 } 194 } 195 cfg.Extras = getExtras(config) 196 return &cfg, nil 197 } 198 199 func getSection(config map[string]any, key string) map[string]any { 200 section, ok := config[key] 201 if !ok { 202 return make(map[string]any) 203 } 204 return section.(map[string]any) 205 } 206 207 // GetUnsupportedProperties returns the list of any unsupported properties that are 208 // used in the Compose files. 209 func GetUnsupportedProperties(configDicts ...map[string]any) []string { 210 unsupported := map[string]bool{} 211 212 for _, configDict := range configDicts { 213 for _, service := range getServices(configDict) { 214 serviceDict := service.(map[string]any) 215 for _, property := range types.UnsupportedProperties { 216 if _, isSet := serviceDict[property]; isSet { 217 unsupported[property] = true 218 } 219 } 220 } 221 } 222 223 return sortedKeys(unsupported) 224 } 225 226 func sortedKeys(set map[string]bool) []string { 227 keys := make([]string, 0, len(set)) 228 for key := range set { 229 keys = append(keys, key) 230 } 231 sort.Strings(keys) 232 return keys 233 } 234 235 // GetDeprecatedProperties returns the list of any deprecated properties that 236 // are used in the compose files. 237 func GetDeprecatedProperties(configDicts ...map[string]any) map[string]string { 238 deprecated := map[string]string{} 239 240 for _, configDict := range configDicts { 241 deprecatedProperties := getProperties(getServices(configDict), types.DeprecatedProperties) 242 for key, value := range deprecatedProperties { 243 deprecated[key] = value 244 } 245 } 246 247 return deprecated 248 } 249 250 func getProperties(services map[string]any, propertyMap map[string]string) map[string]string { 251 output := map[string]string{} 252 253 for _, service := range services { 254 if serviceDict, ok := service.(map[string]any); ok { 255 for property, description := range propertyMap { 256 if _, isSet := serviceDict[property]; isSet { 257 output[property] = description 258 } 259 } 260 } 261 } 262 263 return output 264 } 265 266 // ForbiddenPropertiesError is returned when there are properties in the Compose 267 // file that are forbidden. 268 type ForbiddenPropertiesError struct { 269 Properties map[string]string 270 } 271 272 func (e *ForbiddenPropertiesError) Error() string { 273 return "Configuration contains forbidden properties" 274 } 275 276 func getServices(configDict map[string]any) map[string]any { 277 if services, ok := configDict["services"]; ok { 278 if servicesDict, ok := services.(map[string]any); ok { 279 return servicesDict 280 } 281 } 282 283 return map[string]any{} 284 } 285 286 // Transform converts the source into the target struct with compose types transformer 287 // and the specified transformers if any. 288 func Transform(source any, target any, additionalTransformers ...Transformer) error { 289 data := mapstructure.Metadata{} 290 config := &mapstructure.DecoderConfig{ 291 DecodeHook: mapstructure.ComposeDecodeHookFunc( 292 createTransformHook(additionalTransformers...), 293 mapstructure.StringToTimeDurationHookFunc()), 294 Result: target, 295 Metadata: &data, 296 } 297 decoder, err := mapstructure.NewDecoder(config) 298 if err != nil { 299 return err 300 } 301 return decoder.Decode(source) 302 } 303 304 // TransformerFunc defines a function to perform the actual transformation 305 type TransformerFunc func(any) (any, error) 306 307 // Transformer defines a map to type transformer 308 type Transformer struct { 309 TypeOf reflect.Type 310 Func TransformerFunc 311 } 312 313 func createTransformHook(additionalTransformers ...Transformer) mapstructure.DecodeHookFuncType { 314 transforms := map[reflect.Type]func(any) (any, error){ 315 reflect.TypeOf(types.External{}): transformExternal, 316 reflect.TypeOf(types.HealthCheckTest{}): transformHealthCheckTest, 317 reflect.TypeOf(types.ShellCommand{}): transformShellCommand, 318 reflect.TypeOf(types.StringList{}): transformStringList, 319 reflect.TypeOf(map[string]string{}): transformMapStringString, 320 reflect.TypeOf(types.UlimitsConfig{}): transformUlimits, 321 reflect.TypeOf(types.UnitBytes(0)): transformSize, 322 reflect.TypeOf([]types.ServicePortConfig{}): transformServicePort, 323 reflect.TypeOf(types.ServiceSecretConfig{}): transformStringSourceMap, 324 reflect.TypeOf(types.ServiceConfigObjConfig{}): transformStringSourceMap, 325 reflect.TypeOf(types.StringOrNumberList{}): transformStringOrNumberList, 326 reflect.TypeOf(map[string]*types.ServiceNetworkConfig{}): transformServiceNetworkMap, 327 reflect.TypeOf(types.Mapping{}): transformMappingOrListFunc("=", false), 328 reflect.TypeOf(types.MappingWithEquals{}): transformMappingOrListFunc("=", true), 329 reflect.TypeOf(types.Labels{}): transformMappingOrListFunc("=", false), 330 reflect.TypeOf(types.MappingWithColon{}): transformMappingOrListFunc(":", false), 331 reflect.TypeOf(types.HostsList{}): transformHostsList, 332 reflect.TypeOf(types.ServiceVolumeConfig{}): transformServiceVolumeConfig, 333 reflect.TypeOf(types.BuildConfig{}): transformBuildConfig, 334 reflect.TypeOf(types.Duration(0)): transformStringToDuration, 335 } 336 337 for _, transformer := range additionalTransformers { 338 transforms[transformer.TypeOf] = transformer.Func 339 } 340 341 return func(_ reflect.Type, target reflect.Type, data any) (any, error) { 342 transform, ok := transforms[target] 343 if !ok { 344 return data, nil 345 } 346 return transform(data) 347 } 348 } 349 350 // keys needs to be converted to strings for jsonschema 351 func convertToStringKeysRecursive(value any, keyPrefix string) (any, error) { 352 if mapping, ok := value.(map[any]any); ok { 353 dict := make(map[string]any) 354 for key, entry := range mapping { 355 str, ok := key.(string) 356 if !ok { 357 return nil, formatInvalidKeyError(keyPrefix, key) 358 } 359 var newKeyPrefix string 360 if keyPrefix == "" { 361 newKeyPrefix = str 362 } else { 363 newKeyPrefix = fmt.Sprintf("%s.%s", keyPrefix, str) 364 } 365 convertedEntry, err := convertToStringKeysRecursive(entry, newKeyPrefix) 366 if err != nil { 367 return nil, err 368 } 369 dict[str] = convertedEntry 370 } 371 return dict, nil 372 } 373 if list, ok := value.([]any); ok { 374 var convertedList []any 375 for index, entry := range list { 376 newKeyPrefix := fmt.Sprintf("%s[%d]", keyPrefix, index) 377 convertedEntry, err := convertToStringKeysRecursive(entry, newKeyPrefix) 378 if err != nil { 379 return nil, err 380 } 381 convertedList = append(convertedList, convertedEntry) 382 } 383 return convertedList, nil 384 } 385 return value, nil 386 } 387 388 func formatInvalidKeyError(keyPrefix string, key any) error { 389 var location string 390 if keyPrefix == "" { 391 location = "at top level" 392 } else { 393 location = "in " + keyPrefix 394 } 395 return errors.Errorf("non-string key %s: %#v", location, key) 396 } 397 398 // LoadServices produces a ServiceConfig map from a compose file Dict 399 // the servicesDict is not validated if directly used. Use Load() to enable validation 400 func LoadServices(servicesDict map[string]any, workingDir string, lookupEnv template.Mapping) ([]types.ServiceConfig, error) { 401 services := make([]types.ServiceConfig, 0, len(servicesDict)) 402 403 for name, serviceDef := range servicesDict { 404 serviceConfig, err := LoadService(name, serviceDef.(map[string]any), workingDir, lookupEnv) 405 if err != nil { 406 return nil, err 407 } 408 services = append(services, *serviceConfig) 409 } 410 411 return services, nil 412 } 413 414 // LoadService produces a single ServiceConfig from a compose file Dict 415 // the serviceDict is not validated if directly used. Use Load() to enable validation 416 func LoadService(name string, serviceDict map[string]any, workingDir string, lookupEnv template.Mapping) (*types.ServiceConfig, error) { 417 serviceConfig := &types.ServiceConfig{} 418 if err := Transform(serviceDict, serviceConfig); err != nil { 419 return nil, err 420 } 421 serviceConfig.Name = name 422 423 if err := resolveEnvironment(serviceConfig, workingDir, lookupEnv); err != nil { 424 return nil, err 425 } 426 427 if err := resolveVolumePaths(serviceConfig.Volumes, workingDir, lookupEnv); err != nil { 428 return nil, err 429 } 430 431 serviceConfig.Extras = getExtras(serviceDict) 432 433 return serviceConfig, nil 434 } 435 436 func loadExtras(name string, source map[string]any) map[string]any { 437 if dict, ok := source[name].(map[string]any); ok { 438 return getExtras(dict) 439 } 440 return nil 441 } 442 443 func getExtras(dict map[string]any) map[string]any { 444 extras := map[string]any{} 445 for key, value := range dict { 446 if strings.HasPrefix(key, "x-") { 447 extras[key] = value 448 } 449 } 450 if len(extras) == 0 { 451 return nil 452 } 453 return extras 454 } 455 456 func updateEnvironment(environment map[string]*string, vars map[string]*string, lookupEnv template.Mapping) { 457 for k, v := range vars { 458 interpolatedV, ok := lookupEnv(k) 459 if (v == nil || *v == "") && ok { 460 // lookupEnv is prioritized over vars 461 environment[k] = &interpolatedV 462 } else { 463 environment[k] = v 464 } 465 } 466 } 467 468 func resolveEnvironment(serviceConfig *types.ServiceConfig, workingDir string, lookupEnv template.Mapping) error { 469 environment := make(map[string]*string) 470 471 if len(serviceConfig.EnvFile) > 0 { 472 var envVars []string 473 474 for _, file := range serviceConfig.EnvFile { 475 filePath := absPath(workingDir, file) 476 fileVars, err := opts.ParseEnvFile(filePath) 477 if err != nil { 478 return err 479 } 480 envVars = append(envVars, fileVars...) 481 } 482 updateEnvironment(environment, 483 opts.ConvertKVStringsToMapWithNil(envVars), lookupEnv) 484 } 485 486 updateEnvironment(environment, serviceConfig.Environment, lookupEnv) 487 serviceConfig.Environment = environment 488 return nil 489 } 490 491 func resolveVolumePaths(volumes []types.ServiceVolumeConfig, workingDir string, lookupEnv template.Mapping) error { 492 for i, volume := range volumes { 493 if volume.Type != "bind" { 494 continue 495 } 496 497 if volume.Source == "" { 498 return errors.New(`invalid mount config for type "bind": field Source must not be empty`) 499 } 500 501 filePath := expandUser(volume.Source, lookupEnv) 502 // Check if source is an absolute path (either Unix or Windows), to 503 // handle a Windows client with a Unix daemon or vice-versa. 504 // 505 // Note that this is not required for Docker for Windows when specifying 506 // a local Windows path, because Docker for Windows translates the Windows 507 // path into a valid path within the VM. 508 if !path.IsAbs(filePath) && !isAbs(filePath) { 509 filePath = absPath(workingDir, filePath) 510 } 511 volume.Source = filePath 512 volumes[i] = volume 513 } 514 return nil 515 } 516 517 // TODO: make this more robust 518 func expandUser(srcPath string, lookupEnv template.Mapping) string { 519 if strings.HasPrefix(srcPath, "~") { 520 home, ok := lookupEnv("HOME") 521 if !ok { 522 logrus.Warn("cannot expand '~', because the environment lacks HOME") 523 return srcPath 524 } 525 return strings.Replace(srcPath, "~", home, 1) 526 } 527 return srcPath 528 } 529 530 func transformUlimits(data any) (any, error) { 531 switch value := data.(type) { 532 case int: 533 return types.UlimitsConfig{Single: value}, nil 534 case map[string]any: 535 ulimit := types.UlimitsConfig{} 536 ulimit.Soft = value["soft"].(int) 537 ulimit.Hard = value["hard"].(int) 538 return ulimit, nil 539 default: 540 return data, errors.Errorf("invalid type %T for ulimits", value) 541 } 542 } 543 544 // LoadNetworks produces a NetworkConfig map from a compose file Dict 545 // the source Dict is not validated if directly used. Use Load() to enable validation 546 func LoadNetworks(source map[string]any, version string) (map[string]types.NetworkConfig, error) { 547 networks := make(map[string]types.NetworkConfig) 548 err := Transform(source, &networks) 549 if err != nil { 550 return networks, err 551 } 552 for name, network := range networks { 553 if !network.External.External { 554 continue 555 } 556 switch { 557 case network.External.Name != "": 558 if network.Name != "" { 559 return nil, errors.Errorf("network %s: network.external.name and network.name conflict; only use network.name", name) 560 } 561 if versions.GreaterThanOrEqualTo(version, "3.5") { 562 logrus.Warnf("network %s: network.external.name is deprecated in favor of network.name", name) 563 } 564 network.Name = network.External.Name 565 network.External.Name = "" 566 case network.Name == "": 567 network.Name = name 568 } 569 network.Extras = loadExtras(name, source) 570 networks[name] = network 571 } 572 return networks, nil 573 } 574 575 func externalVolumeError(volume, key string) error { 576 return errors.Errorf( 577 "conflicting parameters \"external\" and %q specified for volume %q", 578 key, volume) 579 } 580 581 // LoadVolumes produces a VolumeConfig map from a compose file Dict 582 // the source Dict is not validated if directly used. Use Load() to enable validation 583 func LoadVolumes(source map[string]any, version string) (map[string]types.VolumeConfig, error) { 584 volumes := make(map[string]types.VolumeConfig) 585 if err := Transform(source, &volumes); err != nil { 586 return volumes, err 587 } 588 589 for name, volume := range volumes { 590 if !volume.External.External { 591 continue 592 } 593 switch { 594 case volume.Driver != "": 595 return nil, externalVolumeError(name, "driver") 596 case len(volume.DriverOpts) > 0: 597 return nil, externalVolumeError(name, "driver_opts") 598 case len(volume.Labels) > 0: 599 return nil, externalVolumeError(name, "labels") 600 case volume.External.Name != "": 601 if volume.Name != "" { 602 return nil, errors.Errorf("volume %s: volume.external.name and volume.name conflict; only use volume.name", name) 603 } 604 if versions.GreaterThanOrEqualTo(version, "3.4") { 605 logrus.Warnf("volume %s: volume.external.name is deprecated in favor of volume.name", name) 606 } 607 volume.Name = volume.External.Name 608 volume.External.Name = "" 609 case volume.Name == "": 610 volume.Name = name 611 } 612 volume.Extras = loadExtras(name, source) 613 volumes[name] = volume 614 } 615 return volumes, nil 616 } 617 618 // LoadSecrets produces a SecretConfig map from a compose file Dict 619 // the source Dict is not validated if directly used. Use Load() to enable validation 620 func LoadSecrets(source map[string]any, details types.ConfigDetails) (map[string]types.SecretConfig, error) { 621 secrets := make(map[string]types.SecretConfig) 622 if err := Transform(source, &secrets); err != nil { 623 return secrets, err 624 } 625 for name, secret := range secrets { 626 obj, err := loadFileObjectConfig(name, "secret", types.FileObjectConfig(secret), details) 627 if err != nil { 628 return nil, err 629 } 630 secretConfig := types.SecretConfig(obj) 631 secretConfig.Extras = loadExtras(name, source) 632 secrets[name] = secretConfig 633 } 634 return secrets, nil 635 } 636 637 // LoadConfigObjs produces a ConfigObjConfig map from a compose file Dict 638 // the source Dict is not validated if directly used. Use Load() to enable validation 639 func LoadConfigObjs(source map[string]any, details types.ConfigDetails) (map[string]types.ConfigObjConfig, error) { 640 configs := make(map[string]types.ConfigObjConfig) 641 if err := Transform(source, &configs); err != nil { 642 return configs, err 643 } 644 for name, config := range configs { 645 obj, err := loadFileObjectConfig(name, "config", types.FileObjectConfig(config), details) 646 if err != nil { 647 return nil, err 648 } 649 configConfig := types.ConfigObjConfig(obj) 650 configConfig.Extras = loadExtras(name, source) 651 configs[name] = configConfig 652 } 653 return configs, nil 654 } 655 656 func loadFileObjectConfig(name string, objType string, obj types.FileObjectConfig, details types.ConfigDetails) (types.FileObjectConfig, error) { 657 // if "external: true" 658 switch { 659 case obj.External.External: 660 // handle deprecated external.name 661 if obj.External.Name != "" { 662 if obj.Name != "" { 663 return obj, errors.Errorf("%[1]s %[2]s: %[1]s.external.name and %[1]s.name conflict; only use %[1]s.name", objType, name) 664 } 665 if versions.GreaterThanOrEqualTo(details.Version, "3.5") { 666 logrus.Warnf("%[1]s %[2]s: %[1]s.external.name is deprecated in favor of %[1]s.name", objType, name) 667 } 668 obj.Name = obj.External.Name 669 obj.External.Name = "" 670 } else if obj.Name == "" { 671 obj.Name = name 672 } 673 // if not "external: true" 674 case obj.Driver != "": 675 if obj.File != "" { 676 return obj, errors.Errorf("%[1]s %[2]s: %[1]s.driver and %[1]s.file conflict; only use %[1]s.driver", objType, name) 677 } 678 default: 679 obj.File = absPath(details.WorkingDir, obj.File) 680 } 681 682 return obj, nil 683 } 684 685 func absPath(workingDir string, filePath string) string { 686 if filepath.IsAbs(filePath) { 687 return filePath 688 } 689 return filepath.Join(workingDir, filePath) 690 } 691 692 var transformMapStringString TransformerFunc = func(data any) (any, error) { 693 switch value := data.(type) { 694 case map[string]any: 695 return toMapStringString(value, false), nil 696 case map[string]string: 697 return value, nil 698 default: 699 return data, errors.Errorf("invalid type %T for map[string]string", value) 700 } 701 } 702 703 var transformExternal TransformerFunc = func(data any) (any, error) { 704 switch value := data.(type) { 705 case bool: 706 return map[string]any{"external": value}, nil 707 case map[string]any: 708 return map[string]any{"external": true, "name": value["name"]}, nil 709 default: 710 return data, errors.Errorf("invalid type %T for external", value) 711 } 712 } 713 714 var transformServicePort TransformerFunc = func(data any) (any, error) { 715 switch entries := data.(type) { 716 case []any: 717 // We process the list instead of individual items here. 718 // The reason is that one entry might be mapped to multiple ServicePortConfig. 719 // Therefore we take an input of a list and return an output of a list. 720 ports := []any{} 721 for _, entry := range entries { 722 switch value := entry.(type) { 723 case int: 724 v, err := toServicePortConfigs(strconv.Itoa(value)) 725 if err != nil { 726 return data, err 727 } 728 ports = append(ports, v...) 729 case string: 730 v, err := toServicePortConfigs(value) 731 if err != nil { 732 return data, err 733 } 734 ports = append(ports, v...) 735 case map[string]any: 736 ports = append(ports, value) 737 default: 738 return data, errors.Errorf("invalid type %T for port", value) 739 } 740 } 741 return ports, nil 742 default: 743 return data, errors.Errorf("invalid type %T for port", entries) 744 } 745 } 746 747 var transformStringSourceMap TransformerFunc = func(data any) (any, error) { 748 switch value := data.(type) { 749 case string: 750 return map[string]any{"source": value}, nil 751 case map[string]any: 752 return data, nil 753 default: 754 return data, errors.Errorf("invalid type %T for secret", value) 755 } 756 } 757 758 var transformBuildConfig TransformerFunc = func(data any) (any, error) { 759 switch value := data.(type) { 760 case string: 761 return map[string]any{"context": value}, nil 762 case map[string]any: 763 return data, nil 764 default: 765 return data, errors.Errorf("invalid type %T for service build", value) 766 } 767 } 768 769 var transformServiceVolumeConfig TransformerFunc = func(data any) (any, error) { 770 switch value := data.(type) { 771 case string: 772 return ParseVolume(value) 773 case map[string]any: 774 return data, nil 775 default: 776 return data, errors.Errorf("invalid type %T for service volume", value) 777 } 778 } 779 780 var transformServiceNetworkMap TransformerFunc = func(value any) (any, error) { 781 if list, ok := value.([]any); ok { 782 mapValue := map[any]any{} 783 for _, name := range list { 784 mapValue[name] = nil 785 } 786 return mapValue, nil 787 } 788 return value, nil 789 } 790 791 var transformStringOrNumberList TransformerFunc = func(value any) (any, error) { 792 list := value.([]any) 793 result := make([]string, len(list)) 794 for i, item := range list { 795 result[i] = fmt.Sprint(item) 796 } 797 return result, nil 798 } 799 800 var transformStringList TransformerFunc = func(data any) (any, error) { 801 switch value := data.(type) { 802 case string: 803 return []string{value}, nil 804 case []any: 805 return value, nil 806 default: 807 return data, errors.Errorf("invalid type %T for string list", value) 808 } 809 } 810 811 var transformHostsList TransformerFunc = func(data any) (any, error) { 812 hl := transformListOrMapping(data, ":", false, []string{"=", ":"}) 813 814 // Remove brackets from IP addresses if present (for example "[::1]" -> "::1"). 815 result := make([]string, 0, len(hl)) 816 for _, hip := range hl { 817 host, ip, _ := strings.Cut(hip, ":") 818 if len(ip) > 2 && ip[0] == '[' && ip[len(ip)-1] == ']' { 819 ip = ip[1 : len(ip)-1] 820 } 821 result = append(result, fmt.Sprintf("%s:%s", host, ip)) 822 } 823 return result, nil 824 } 825 826 // transformListOrMapping transforms pairs of strings that may be represented as 827 // a map, or a list of '=' or ':' separated strings, into a list of ':' separated 828 // strings. 829 func transformListOrMapping(listOrMapping any, sep string, allowNil bool, allowSeps []string) []string { 830 switch value := listOrMapping.(type) { 831 case map[string]any: 832 return toStringList(value, sep, allowNil) 833 case []any: 834 result := make([]string, 0, len(value)) 835 for _, entry := range value { 836 for i, allowSep := range allowSeps { 837 entry := fmt.Sprint(entry) 838 k, v, ok := strings.Cut(entry, allowSep) 839 if ok { 840 // Entry uses this allowed separator. Add it to the result, using 841 // sep as a separator. 842 result = append(result, fmt.Sprintf("%s%s%s", k, sep, v)) 843 break 844 } else if i == len(allowSeps)-1 { 845 // No more separators to try, keep the entry if allowNil. 846 if allowNil { 847 result = append(result, k) 848 } 849 } 850 } 851 } 852 return result 853 } 854 panic(errors.Errorf("expected a map or a list, got %T: %#v", listOrMapping, listOrMapping)) 855 } 856 857 func transformMappingOrListFunc(sep string, allowNil bool) TransformerFunc { 858 return func(data any) (any, error) { 859 return transformMappingOrList(data, sep, allowNil), nil 860 } 861 } 862 863 func transformMappingOrList(mappingOrList any, sep string, allowNil bool) any { 864 switch values := mappingOrList.(type) { 865 case map[string]any: 866 return toMapStringString(values, allowNil) 867 case []any: 868 result := make(map[string]any) 869 for _, v := range values { 870 key, val, hasValue := strings.Cut(v.(string), sep) 871 switch { 872 case !hasValue && allowNil: 873 result[key] = nil 874 case !hasValue && !allowNil: 875 result[key] = "" 876 default: 877 result[key] = val 878 } 879 } 880 return result 881 } 882 panic(errors.Errorf("expected a map or a list, got %T: %#v", mappingOrList, mappingOrList)) 883 } 884 885 var transformShellCommand TransformerFunc = func(value any) (any, error) { 886 if str, ok := value.(string); ok { 887 return shlex.Split(str) 888 } 889 return value, nil 890 } 891 892 var transformHealthCheckTest TransformerFunc = func(data any) (any, error) { 893 switch value := data.(type) { 894 case string: 895 return append([]string{"CMD-SHELL"}, value), nil 896 case []any: 897 return value, nil 898 default: 899 return value, errors.Errorf("invalid type %T for healthcheck.test", value) 900 } 901 } 902 903 var transformSize TransformerFunc = func(value any) (any, error) { 904 switch value := value.(type) { 905 case int: 906 return int64(value), nil 907 case string: 908 return units.RAMInBytes(value) 909 } 910 panic(errors.Errorf("invalid type for size %T", value)) 911 } 912 913 var transformStringToDuration TransformerFunc = func(value any) (any, error) { 914 switch value := value.(type) { 915 case string: 916 d, err := time.ParseDuration(value) 917 if err != nil { 918 return value, err 919 } 920 return types.Duration(d), nil 921 default: 922 return value, errors.Errorf("invalid type %T for duration", value) 923 } 924 } 925 926 func toServicePortConfigs(value string) ([]any, error) { 927 var portConfigs []any 928 929 ports, portBindings, err := nat.ParsePortSpecs([]string{value}) 930 if err != nil { 931 return nil, err 932 } 933 // We need to sort the key of the ports to make sure it is consistent 934 keys := []string{} 935 for port := range ports { 936 keys = append(keys, string(port)) 937 } 938 sort.Strings(keys) 939 940 for _, key := range keys { 941 // Reuse ConvertPortToPortConfig so that it is consistent 942 portConfig, err := opts.ConvertPortToPortConfig(nat.Port(key), portBindings) 943 if err != nil { 944 return nil, err 945 } 946 for _, p := range portConfig { 947 portConfigs = append(portConfigs, types.ServicePortConfig{ 948 Protocol: string(p.Protocol), 949 Target: p.TargetPort, 950 Published: p.PublishedPort, 951 Mode: string(p.PublishMode), 952 }) 953 } 954 } 955 956 return portConfigs, nil 957 } 958 959 func toMapStringString(value map[string]any, allowNil bool) map[string]any { 960 output := make(map[string]any) 961 for key, value := range value { 962 output[key] = toString(value, allowNil) 963 } 964 return output 965 } 966 967 func toString(value any, allowNil bool) any { 968 switch { 969 case value != nil: 970 return fmt.Sprint(value) 971 case allowNil: 972 return nil 973 default: 974 return "" 975 } 976 } 977 978 func toStringList(value map[string]any, separator string, allowNil bool) []string { 979 output := []string{} 980 for key, value := range value { 981 if value == nil && !allowNil { 982 continue 983 } 984 output = append(output, fmt.Sprintf("%s%s%s", key, separator, value)) 985 } 986 sort.Strings(output) 987 return output 988 }