github.com/fabiokung/docker@v0.11.2-0.20170222101415-4534dcd49497/cli/compose/loader/loader.go (about) 1 package loader 2 3 import ( 4 "fmt" 5 "os" 6 "path" 7 "reflect" 8 "regexp" 9 "sort" 10 "strings" 11 12 "github.com/docker/docker/cli/compose/interpolation" 13 "github.com/docker/docker/cli/compose/schema" 14 "github.com/docker/docker/cli/compose/types" 15 "github.com/docker/docker/opts" 16 runconfigopts "github.com/docker/docker/runconfig/opts" 17 "github.com/docker/go-connections/nat" 18 units "github.com/docker/go-units" 19 shellwords "github.com/mattn/go-shellwords" 20 "github.com/mitchellh/mapstructure" 21 yaml "gopkg.in/yaml.v2" 22 ) 23 24 var ( 25 fieldNameRegexp = regexp.MustCompile("[A-Z][a-z0-9]+") 26 ) 27 28 // ParseYAML reads the bytes from a file, parses the bytes into a mapping 29 // structure, and returns it. 30 func ParseYAML(source []byte) (types.Dict, error) { 31 var cfg interface{} 32 if err := yaml.Unmarshal(source, &cfg); err != nil { 33 return nil, err 34 } 35 cfgMap, ok := cfg.(map[interface{}]interface{}) 36 if !ok { 37 return nil, fmt.Errorf("Top-level object must be a mapping") 38 } 39 converted, err := convertToStringKeysRecursive(cfgMap, "") 40 if err != nil { 41 return nil, err 42 } 43 return converted.(types.Dict), nil 44 } 45 46 // Load reads a ConfigDetails and returns a fully loaded configuration 47 func Load(configDetails types.ConfigDetails) (*types.Config, error) { 48 if len(configDetails.ConfigFiles) < 1 { 49 return nil, fmt.Errorf("No files specified") 50 } 51 if len(configDetails.ConfigFiles) > 1 { 52 return nil, fmt.Errorf("Multiple files are not yet supported") 53 } 54 55 configDict := getConfigDict(configDetails) 56 57 if services, ok := configDict["services"]; ok { 58 if servicesDict, ok := services.(types.Dict); ok { 59 forbidden := getProperties(servicesDict, types.ForbiddenProperties) 60 61 if len(forbidden) > 0 { 62 return nil, &ForbiddenPropertiesError{Properties: forbidden} 63 } 64 } 65 } 66 67 if err := schema.Validate(configDict, schema.Version(configDict)); err != nil { 68 return nil, err 69 } 70 71 cfg := types.Config{} 72 if services, ok := configDict["services"]; ok { 73 servicesConfig, err := interpolation.Interpolate(services.(types.Dict), "service", os.LookupEnv) 74 if err != nil { 75 return nil, err 76 } 77 78 servicesList, err := loadServices(servicesConfig, configDetails.WorkingDir) 79 if err != nil { 80 return nil, err 81 } 82 83 cfg.Services = servicesList 84 } 85 86 if networks, ok := configDict["networks"]; ok { 87 networksConfig, err := interpolation.Interpolate(networks.(types.Dict), "network", os.LookupEnv) 88 if err != nil { 89 return nil, err 90 } 91 92 networksMapping, err := loadNetworks(networksConfig) 93 if err != nil { 94 return nil, err 95 } 96 97 cfg.Networks = networksMapping 98 } 99 100 if volumes, ok := configDict["volumes"]; ok { 101 volumesConfig, err := interpolation.Interpolate(volumes.(types.Dict), "volume", os.LookupEnv) 102 if err != nil { 103 return nil, err 104 } 105 106 volumesMapping, err := loadVolumes(volumesConfig) 107 if err != nil { 108 return nil, err 109 } 110 111 cfg.Volumes = volumesMapping 112 } 113 114 if secrets, ok := configDict["secrets"]; ok { 115 secretsConfig, err := interpolation.Interpolate(secrets.(types.Dict), "secret", os.LookupEnv) 116 if err != nil { 117 return nil, err 118 } 119 120 secretsMapping, err := loadSecrets(secretsConfig, configDetails.WorkingDir) 121 if err != nil { 122 return nil, err 123 } 124 125 cfg.Secrets = secretsMapping 126 } 127 128 return &cfg, nil 129 } 130 131 // GetUnsupportedProperties returns the list of any unsupported properties that are 132 // used in the Compose files. 133 func GetUnsupportedProperties(configDetails types.ConfigDetails) []string { 134 unsupported := map[string]bool{} 135 136 for _, service := range getServices(getConfigDict(configDetails)) { 137 serviceDict := service.(types.Dict) 138 for _, property := range types.UnsupportedProperties { 139 if _, isSet := serviceDict[property]; isSet { 140 unsupported[property] = true 141 } 142 } 143 } 144 145 return sortedKeys(unsupported) 146 } 147 148 func sortedKeys(set map[string]bool) []string { 149 var keys []string 150 for key := range set { 151 keys = append(keys, key) 152 } 153 sort.Strings(keys) 154 return keys 155 } 156 157 // GetDeprecatedProperties returns the list of any deprecated properties that 158 // are used in the compose files. 159 func GetDeprecatedProperties(configDetails types.ConfigDetails) map[string]string { 160 return getProperties(getServices(getConfigDict(configDetails)), types.DeprecatedProperties) 161 } 162 163 func getProperties(services types.Dict, propertyMap map[string]string) map[string]string { 164 output := map[string]string{} 165 166 for _, service := range services { 167 if serviceDict, ok := service.(types.Dict); ok { 168 for property, description := range propertyMap { 169 if _, isSet := serviceDict[property]; isSet { 170 output[property] = description 171 } 172 } 173 } 174 } 175 176 return output 177 } 178 179 // ForbiddenPropertiesError is returned when there are properties in the Compose 180 // file that are forbidden. 181 type ForbiddenPropertiesError struct { 182 Properties map[string]string 183 } 184 185 func (e *ForbiddenPropertiesError) Error() string { 186 return "Configuration contains forbidden properties" 187 } 188 189 // TODO: resolve multiple files into a single config 190 func getConfigDict(configDetails types.ConfigDetails) types.Dict { 191 return configDetails.ConfigFiles[0].Config 192 } 193 194 func getServices(configDict types.Dict) types.Dict { 195 if services, ok := configDict["services"]; ok { 196 if servicesDict, ok := services.(types.Dict); ok { 197 return servicesDict 198 } 199 } 200 201 return types.Dict{} 202 } 203 204 func transform(source map[string]interface{}, target interface{}) error { 205 data := mapstructure.Metadata{} 206 config := &mapstructure.DecoderConfig{ 207 DecodeHook: mapstructure.ComposeDecodeHookFunc( 208 transformHook, 209 mapstructure.StringToTimeDurationHookFunc()), 210 Result: target, 211 Metadata: &data, 212 } 213 decoder, err := mapstructure.NewDecoder(config) 214 if err != nil { 215 return err 216 } 217 err = decoder.Decode(source) 218 // TODO: log unused keys 219 return err 220 } 221 222 func transformHook( 223 source reflect.Type, 224 target reflect.Type, 225 data interface{}, 226 ) (interface{}, error) { 227 switch target { 228 case reflect.TypeOf(types.External{}): 229 return transformExternal(data) 230 case reflect.TypeOf(types.HealthCheckTest{}): 231 return transformHealthCheckTest(data) 232 case reflect.TypeOf(types.ShellCommand{}): 233 return transformShellCommand(data) 234 case reflect.TypeOf(types.StringList{}): 235 return transformStringList(data) 236 case reflect.TypeOf(map[string]string{}): 237 return transformMapStringString(data) 238 case reflect.TypeOf(types.UlimitsConfig{}): 239 return transformUlimits(data) 240 case reflect.TypeOf(types.UnitBytes(0)): 241 return transformSize(data) 242 case reflect.TypeOf([]types.ServicePortConfig{}): 243 return transformServicePort(data) 244 case reflect.TypeOf(types.ServiceSecretConfig{}): 245 return transformServiceSecret(data) 246 case reflect.TypeOf(types.StringOrNumberList{}): 247 return transformStringOrNumberList(data) 248 case reflect.TypeOf(map[string]*types.ServiceNetworkConfig{}): 249 return transformServiceNetworkMap(data) 250 case reflect.TypeOf(types.MappingWithEquals{}): 251 return transformMappingOrList(data, "="), nil 252 case reflect.TypeOf(types.MappingWithColon{}): 253 return transformMappingOrList(data, ":"), nil 254 } 255 return data, nil 256 } 257 258 // keys needs to be converted to strings for jsonschema 259 // TODO: don't use types.Dict 260 func convertToStringKeysRecursive(value interface{}, keyPrefix string) (interface{}, error) { 261 if mapping, ok := value.(map[interface{}]interface{}); ok { 262 dict := make(types.Dict) 263 for key, entry := range mapping { 264 str, ok := key.(string) 265 if !ok { 266 return nil, formatInvalidKeyError(keyPrefix, key) 267 } 268 var newKeyPrefix string 269 if keyPrefix == "" { 270 newKeyPrefix = str 271 } else { 272 newKeyPrefix = fmt.Sprintf("%s.%s", keyPrefix, str) 273 } 274 convertedEntry, err := convertToStringKeysRecursive(entry, newKeyPrefix) 275 if err != nil { 276 return nil, err 277 } 278 dict[str] = convertedEntry 279 } 280 return dict, nil 281 } 282 if list, ok := value.([]interface{}); ok { 283 var convertedList []interface{} 284 for index, entry := range list { 285 newKeyPrefix := fmt.Sprintf("%s[%d]", keyPrefix, index) 286 convertedEntry, err := convertToStringKeysRecursive(entry, newKeyPrefix) 287 if err != nil { 288 return nil, err 289 } 290 convertedList = append(convertedList, convertedEntry) 291 } 292 return convertedList, nil 293 } 294 return value, nil 295 } 296 297 func formatInvalidKeyError(keyPrefix string, key interface{}) error { 298 var location string 299 if keyPrefix == "" { 300 location = "at top level" 301 } else { 302 location = fmt.Sprintf("in %s", keyPrefix) 303 } 304 return fmt.Errorf("Non-string key %s: %#v", location, key) 305 } 306 307 func loadServices(servicesDict types.Dict, workingDir string) ([]types.ServiceConfig, error) { 308 var services []types.ServiceConfig 309 310 for name, serviceDef := range servicesDict { 311 serviceConfig, err := loadService(name, serviceDef.(types.Dict), workingDir) 312 if err != nil { 313 return nil, err 314 } 315 services = append(services, *serviceConfig) 316 } 317 318 return services, nil 319 } 320 321 func loadService(name string, serviceDict types.Dict, workingDir string) (*types.ServiceConfig, error) { 322 serviceConfig := &types.ServiceConfig{} 323 if err := transform(serviceDict, serviceConfig); err != nil { 324 return nil, err 325 } 326 serviceConfig.Name = name 327 328 if err := resolveEnvironment(serviceConfig, workingDir); err != nil { 329 return nil, err 330 } 331 332 if err := resolveVolumePaths(serviceConfig.Volumes, workingDir); err != nil { 333 return nil, err 334 } 335 336 return serviceConfig, nil 337 } 338 339 func resolveEnvironment(serviceConfig *types.ServiceConfig, workingDir string) error { 340 environment := make(map[string]string) 341 342 if len(serviceConfig.EnvFile) > 0 { 343 var envVars []string 344 345 for _, file := range serviceConfig.EnvFile { 346 filePath := absPath(workingDir, file) 347 fileVars, err := runconfigopts.ParseEnvFile(filePath) 348 if err != nil { 349 return err 350 } 351 envVars = append(envVars, fileVars...) 352 } 353 354 for k, v := range runconfigopts.ConvertKVStringsToMap(envVars) { 355 environment[k] = v 356 } 357 } 358 359 for k, v := range serviceConfig.Environment { 360 environment[k] = v 361 } 362 363 serviceConfig.Environment = environment 364 365 return nil 366 } 367 368 func resolveVolumePaths(volumes []string, workingDir string) error { 369 for i, mapping := range volumes { 370 parts := strings.SplitN(mapping, ":", 2) 371 if len(parts) == 1 { 372 continue 373 } 374 375 if strings.HasPrefix(parts[0], ".") { 376 parts[0] = absPath(workingDir, parts[0]) 377 } 378 parts[0] = expandUser(parts[0]) 379 380 volumes[i] = strings.Join(parts, ":") 381 } 382 383 return nil 384 } 385 386 // TODO: make this more robust 387 func expandUser(path string) string { 388 if strings.HasPrefix(path, "~") { 389 return strings.Replace(path, "~", os.Getenv("HOME"), 1) 390 } 391 return path 392 } 393 394 func transformUlimits(data interface{}) (interface{}, error) { 395 switch value := data.(type) { 396 case int: 397 return types.UlimitsConfig{Single: value}, nil 398 case types.Dict: 399 ulimit := types.UlimitsConfig{} 400 ulimit.Soft = value["soft"].(int) 401 ulimit.Hard = value["hard"].(int) 402 return ulimit, nil 403 default: 404 return data, fmt.Errorf("invalid type %T for ulimits", value) 405 } 406 } 407 408 func loadNetworks(source types.Dict) (map[string]types.NetworkConfig, error) { 409 networks := make(map[string]types.NetworkConfig) 410 err := transform(source, &networks) 411 if err != nil { 412 return networks, err 413 } 414 for name, network := range networks { 415 if network.External.External && network.External.Name == "" { 416 network.External.Name = name 417 networks[name] = network 418 } 419 } 420 return networks, nil 421 } 422 423 func loadVolumes(source types.Dict) (map[string]types.VolumeConfig, error) { 424 volumes := make(map[string]types.VolumeConfig) 425 err := transform(source, &volumes) 426 if err != nil { 427 return volumes, err 428 } 429 for name, volume := range volumes { 430 if volume.External.External && volume.External.Name == "" { 431 volume.External.Name = name 432 volumes[name] = volume 433 } 434 } 435 return volumes, nil 436 } 437 438 func loadSecrets(source types.Dict, workingDir string) (map[string]types.SecretConfig, error) { 439 secrets := make(map[string]types.SecretConfig) 440 if err := transform(source, &secrets); err != nil { 441 return secrets, err 442 } 443 for name, secret := range secrets { 444 if secret.External.External && secret.External.Name == "" { 445 secret.External.Name = name 446 secrets[name] = secret 447 } 448 if secret.File != "" { 449 secret.File = absPath(workingDir, secret.File) 450 } 451 } 452 return secrets, nil 453 } 454 455 func absPath(workingDir string, filepath string) string { 456 if path.IsAbs(filepath) { 457 return filepath 458 } 459 return path.Join(workingDir, filepath) 460 } 461 462 func transformMapStringString(data interface{}) (interface{}, error) { 463 switch value := data.(type) { 464 case map[string]interface{}: 465 return toMapStringString(value), nil 466 case types.Dict: 467 return toMapStringString(value), nil 468 case map[string]string: 469 return value, nil 470 default: 471 return data, fmt.Errorf("invalid type %T for map[string]string", value) 472 } 473 } 474 475 func transformExternal(data interface{}) (interface{}, error) { 476 switch value := data.(type) { 477 case bool: 478 return map[string]interface{}{"external": value}, nil 479 case types.Dict: 480 return map[string]interface{}{"external": true, "name": value["name"]}, nil 481 case map[string]interface{}: 482 return map[string]interface{}{"external": true, "name": value["name"]}, nil 483 default: 484 return data, fmt.Errorf("invalid type %T for external", value) 485 } 486 } 487 488 func transformServicePort(data interface{}) (interface{}, error) { 489 switch entries := data.(type) { 490 case []interface{}: 491 // We process the list instead of individual items here. 492 // The reason is that one entry might be mapped to multiple ServicePortConfig. 493 // Therefore we take an input of a list and return an output of a list. 494 ports := []interface{}{} 495 for _, entry := range entries { 496 switch value := entry.(type) { 497 case int: 498 v, err := toServicePortConfigs(fmt.Sprint(value)) 499 if err != nil { 500 return data, err 501 } 502 ports = append(ports, v...) 503 case string: 504 v, err := toServicePortConfigs(value) 505 if err != nil { 506 return data, err 507 } 508 ports = append(ports, v...) 509 case types.Dict: 510 ports = append(ports, value) 511 case map[string]interface{}: 512 ports = append(ports, value) 513 default: 514 return data, fmt.Errorf("invalid type %T for port", value) 515 } 516 } 517 return ports, nil 518 default: 519 return data, fmt.Errorf("invalid type %T for port", entries) 520 } 521 } 522 523 func transformServiceSecret(data interface{}) (interface{}, error) { 524 switch value := data.(type) { 525 case string: 526 return map[string]interface{}{"source": value}, nil 527 case types.Dict: 528 return data, nil 529 case map[string]interface{}: 530 return data, nil 531 default: 532 return data, fmt.Errorf("invalid type %T for external", value) 533 } 534 } 535 536 func transformServiceNetworkMap(value interface{}) (interface{}, error) { 537 if list, ok := value.([]interface{}); ok { 538 mapValue := map[interface{}]interface{}{} 539 for _, name := range list { 540 mapValue[name] = nil 541 } 542 return mapValue, nil 543 } 544 return value, nil 545 } 546 547 func transformStringOrNumberList(value interface{}) (interface{}, error) { 548 list := value.([]interface{}) 549 result := make([]string, len(list)) 550 for i, item := range list { 551 result[i] = fmt.Sprint(item) 552 } 553 return result, nil 554 } 555 556 func transformStringList(data interface{}) (interface{}, error) { 557 switch value := data.(type) { 558 case string: 559 return []string{value}, nil 560 case []interface{}: 561 return value, nil 562 default: 563 return data, fmt.Errorf("invalid type %T for string list", value) 564 } 565 } 566 567 func transformMappingOrList(mappingOrList interface{}, sep string) map[string]string { 568 if mapping, ok := mappingOrList.(types.Dict); ok { 569 return toMapStringString(mapping) 570 } 571 if list, ok := mappingOrList.([]interface{}); ok { 572 result := make(map[string]string) 573 for _, value := range list { 574 parts := strings.SplitN(value.(string), sep, 2) 575 if len(parts) == 1 { 576 result[parts[0]] = "" 577 } else { 578 result[parts[0]] = parts[1] 579 } 580 } 581 return result 582 } 583 panic(fmt.Errorf("expected a map or a slice, got: %#v", mappingOrList)) 584 } 585 586 func transformShellCommand(value interface{}) (interface{}, error) { 587 if str, ok := value.(string); ok { 588 return shellwords.Parse(str) 589 } 590 return value, nil 591 } 592 593 func transformHealthCheckTest(data interface{}) (interface{}, error) { 594 switch value := data.(type) { 595 case string: 596 return append([]string{"CMD-SHELL"}, value), nil 597 case []interface{}: 598 return value, nil 599 default: 600 return value, fmt.Errorf("invalid type %T for healthcheck.test", value) 601 } 602 } 603 604 func transformSize(value interface{}) (int64, error) { 605 switch value := value.(type) { 606 case int: 607 return int64(value), nil 608 case string: 609 return units.RAMInBytes(value) 610 } 611 panic(fmt.Errorf("invalid type for size %T", value)) 612 } 613 614 func toServicePortConfigs(value string) ([]interface{}, error) { 615 var portConfigs []interface{} 616 617 ports, portBindings, err := nat.ParsePortSpecs([]string{value}) 618 if err != nil { 619 return nil, err 620 } 621 // We need to sort the key of the ports to make sure it is consistent 622 keys := []string{} 623 for port := range ports { 624 keys = append(keys, string(port)) 625 } 626 sort.Strings(keys) 627 628 for _, key := range keys { 629 // Reuse ConvertPortToPortConfig so that it is consistent 630 portConfig, err := opts.ConvertPortToPortConfig(nat.Port(key), portBindings) 631 if err != nil { 632 return nil, err 633 } 634 for _, p := range portConfig { 635 portConfigs = append(portConfigs, types.ServicePortConfig{ 636 Protocol: string(p.Protocol), 637 Target: p.TargetPort, 638 Published: p.PublishedPort, 639 Mode: string(p.PublishMode), 640 }) 641 } 642 } 643 644 return portConfigs, nil 645 } 646 647 func toMapStringString(value map[string]interface{}) map[string]string { 648 output := make(map[string]string) 649 for key, value := range value { 650 output[key] = toString(value) 651 } 652 return output 653 } 654 655 func toString(value interface{}) string { 656 if value == nil { 657 return "" 658 } 659 return fmt.Sprint(value) 660 }