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