github.com/ijc/docker-app@v0.6.1-0.20181012090447-c7ca8bc483fb/internal/helm/templateloader/loader.go (about) 1 package templateloader 2 3 import ( 4 "fmt" 5 "path" 6 "path/filepath" 7 "reflect" 8 "sort" 9 "strconv" 10 "strings" 11 "time" 12 13 "github.com/docker/app/internal/helm/templatetypes" 14 "github.com/docker/cli/cli/compose/loader" 15 "github.com/docker/cli/cli/compose/schema" 16 "github.com/docker/cli/cli/compose/template" 17 "github.com/docker/cli/cli/compose/types" 18 "github.com/docker/cli/opts" 19 "github.com/docker/go-connections/nat" 20 units "github.com/docker/go-units" 21 shellwords "github.com/mattn/go-shellwords" 22 "github.com/pkg/errors" 23 "github.com/sirupsen/logrus" 24 ) 25 26 var ( 27 transformers = []loader.Transformer{ 28 {TypeOf: reflect.TypeOf(templatetypes.MappingWithEqualsTemplate{}), Func: transformMappingOrListFunc("=", true)}, 29 {TypeOf: reflect.TypeOf(templatetypes.LabelsTemplate{}), Func: transformMappingOrListFunc("=", false)}, 30 {TypeOf: reflect.TypeOf(templatetypes.HostsListTemplate{}), Func: transformHostsListTemplate}, 31 {TypeOf: reflect.TypeOf(templatetypes.ShellCommandTemplate{}), Func: transformShellCommandTemplate}, 32 {TypeOf: reflect.TypeOf(templatetypes.StringTemplateList{}), Func: transformStringTemplateList}, 33 {TypeOf: reflect.TypeOf(templatetypes.StringTemplate{}), Func: transformStringTemplate}, 34 {TypeOf: reflect.TypeOf(templatetypes.UnitBytesOrTemplate{}), Func: transformSize}, 35 {TypeOf: reflect.TypeOf([]templatetypes.ServicePortConfig{}), Func: transformServicePort}, 36 {TypeOf: reflect.TypeOf(templatetypes.ServiceSecretConfig{}), Func: transformStringSourceMap}, 37 {TypeOf: reflect.TypeOf(templatetypes.ServiceConfigObjConfig{}), Func: transformStringSourceMap}, 38 {TypeOf: reflect.TypeOf(templatetypes.ServiceVolumeConfig{}), Func: transformServiceVolumeConfig}, 39 {TypeOf: reflect.TypeOf(templatetypes.BoolOrTemplate{}), Func: transformBoolOrTemplate}, 40 {TypeOf: reflect.TypeOf(templatetypes.UInt64OrTemplate{}), Func: transformUInt64OrTemplate}, 41 {TypeOf: reflect.TypeOf(templatetypes.DurationOrTemplate{}), Func: transformDurationOrTemplate}, 42 } 43 ) 44 45 // LoadTemplate loads a config without resolving the variables 46 func LoadTemplate(configDict map[string]interface{}) (*templatetypes.Config, error) { 47 if err := validateForbidden(configDict); err != nil { 48 return nil, err 49 } 50 return loadSections(configDict, types.ConfigDetails{}) 51 } 52 53 func validateForbidden(configDict map[string]interface{}) error { 54 servicesDict, ok := configDict["services"].(map[string]interface{}) 55 if !ok { 56 return nil 57 } 58 forbidden := getProperties(servicesDict, types.ForbiddenProperties) 59 if len(forbidden) > 0 { 60 return &ForbiddenPropertiesError{Properties: forbidden} 61 } 62 return nil 63 } 64 65 func loadSections(config map[string]interface{}, configDetails types.ConfigDetails) (*templatetypes.Config, error) { 66 var err error 67 cfg := templatetypes.Config{ 68 Version: schema.Version(config), 69 } 70 71 var loaders = []struct { 72 key string 73 fnc func(config map[string]interface{}) error 74 }{ 75 { 76 key: "services", 77 fnc: func(config map[string]interface{}) error { 78 cfg.Services, err = LoadServices(config, configDetails.WorkingDir, configDetails.LookupEnv) 79 return err 80 }, 81 }, 82 { 83 key: "networks", 84 fnc: func(config map[string]interface{}) error { 85 cfg.Networks, err = loader.LoadNetworks(config, configDetails.Version) 86 return err 87 }, 88 }, 89 { 90 key: "volumes", 91 fnc: func(config map[string]interface{}) error { 92 cfg.Volumes, err = loader.LoadVolumes(config, configDetails.Version) 93 return err 94 }, 95 }, 96 { 97 key: "secrets", 98 fnc: func(config map[string]interface{}) error { 99 cfg.Secrets, err = loader.LoadSecrets(config, configDetails) 100 return err 101 }, 102 }, 103 { 104 key: "configs", 105 fnc: func(config map[string]interface{}) error { 106 cfg.Configs, err = loader.LoadConfigObjs(config, configDetails) 107 return err 108 }, 109 }, 110 } 111 for _, loader := range loaders { 112 if err := loader.fnc(getSection(config, loader.key)); err != nil { 113 return nil, err 114 } 115 } 116 return &cfg, nil 117 } 118 119 func getSection(config map[string]interface{}, key string) map[string]interface{} { 120 section, ok := config[key] 121 if !ok { 122 return make(map[string]interface{}) 123 } 124 return section.(map[string]interface{}) 125 } 126 127 // GetUnsupportedProperties returns the list of any unsupported properties that are 128 // used in the Compose files. 129 func GetUnsupportedProperties(configDicts ...map[string]interface{}) []string { 130 unsupported := map[string]bool{} 131 132 for _, configDict := range configDicts { 133 for _, service := range getServices(configDict) { 134 serviceDict := service.(map[string]interface{}) 135 for _, property := range types.UnsupportedProperties { 136 if _, isSet := serviceDict[property]; isSet { 137 unsupported[property] = true 138 } 139 } 140 } 141 } 142 143 return sortedKeys(unsupported) 144 } 145 146 func sortedKeys(set map[string]bool) []string { 147 var keys []string 148 for key := range set { 149 keys = append(keys, key) 150 } 151 sort.Strings(keys) 152 return keys 153 } 154 155 // GetDeprecatedProperties returns the list of any deprecated properties that 156 // are used in the compose files. 157 func GetDeprecatedProperties(configDicts ...map[string]interface{}) map[string]string { 158 deprecated := map[string]string{} 159 160 for _, configDict := range configDicts { 161 deprecatedProperties := getProperties(getServices(configDict), types.DeprecatedProperties) 162 for key, value := range deprecatedProperties { 163 deprecated[key] = value 164 } 165 } 166 167 return deprecated 168 } 169 170 func getProperties(services map[string]interface{}, propertyMap map[string]string) map[string]string { 171 output := map[string]string{} 172 173 for _, service := range services { 174 if serviceDict, ok := service.(map[string]interface{}); ok { 175 for property, description := range propertyMap { 176 if _, isSet := serviceDict[property]; isSet { 177 output[property] = description 178 } 179 } 180 } 181 } 182 183 return output 184 } 185 186 // ForbiddenPropertiesError is returned when there are properties in the Compose 187 // file that are forbidden. 188 type ForbiddenPropertiesError struct { 189 Properties map[string]string 190 } 191 192 func (e *ForbiddenPropertiesError) Error() string { 193 return "Configuration contains forbidden properties" 194 } 195 196 func getServices(configDict map[string]interface{}) map[string]interface{} { 197 if services, ok := configDict["services"]; ok { 198 if servicesDict, ok := services.(map[string]interface{}); ok { 199 return servicesDict 200 } 201 } 202 203 return map[string]interface{}{} 204 } 205 206 // LoadServices produces a ServiceConfig map from a compose file Dict 207 // the servicesDict is not validated if directly used. Use Load() to enable validation 208 func LoadServices(servicesDict map[string]interface{}, workingDir string, lookupEnv template.Mapping) ([]templatetypes.ServiceConfig, error) { 209 var services []templatetypes.ServiceConfig 210 211 for name, serviceDef := range servicesDict { 212 serviceConfig, err := LoadService(name, serviceDef.(map[string]interface{}), workingDir, lookupEnv) 213 if err != nil { 214 return nil, err 215 } 216 services = append(services, *serviceConfig) 217 } 218 219 return services, nil 220 } 221 222 // LoadService produces a single ServiceConfig from a compose file Dict 223 // the serviceDict is not validated if directly used. Use Load() to enable validation 224 func LoadService(name string, serviceDict map[string]interface{}, workingDir string, lookupEnv template.Mapping) (*templatetypes.ServiceConfig, error) { 225 serviceConfig := &templatetypes.ServiceConfig{} 226 if err := loader.Transform(serviceDict, serviceConfig, transformers...); err != nil { 227 return nil, err 228 } 229 serviceConfig.Name = name 230 231 if err := resolveEnvironment(serviceConfig, workingDir, lookupEnv); err != nil { 232 return nil, err 233 } 234 235 if err := resolveVolumePaths(serviceConfig.Volumes, workingDir, lookupEnv); err != nil { 236 return nil, err 237 } 238 return serviceConfig, nil 239 } 240 241 func updateEnvironmentMap(environment templatetypes.MappingWithEqualsTemplate, vars map[string]*string, lookupEnv template.Mapping) { 242 for k, v := range vars { 243 interpolatedV, ok := lookupEnv(k) 244 if (v == nil || *v == "") && ok { 245 // lookupEnv is prioritized over vars 246 environment[templatetypes.StringTemplate{Value:k}] = &templatetypes.StringTemplate{Value: interpolatedV} 247 } else if v == nil { 248 environment[templatetypes.StringTemplate{Value:k}] = nil 249 } else { 250 environment[templatetypes.StringTemplate{Value:k}] = &templatetypes.StringTemplate{Value: *v} 251 } 252 } 253 } 254 func updateEnvironmentMapTemplate(environment, vars templatetypes.MappingWithEqualsTemplate, lookupEnv template.Mapping) { 255 for k, v := range vars { 256 interpolatedV, ok := lookupEnv(k.Value) 257 if (v == nil || v.Value == "") && ok { 258 // lookupEnv is prioritized over vars 259 environment[k] = &templatetypes.StringTemplate{Value: interpolatedV} 260 } else { 261 environment[k] = v 262 } 263 } 264 } 265 266 267 func resolveEnvironment(serviceConfig *templatetypes.ServiceConfig, workingDir string, lookupEnv template.Mapping) error { 268 environment := templatetypes.MappingWithEqualsTemplate{} 269 270 if len(serviceConfig.EnvFile) > 0 { 271 var envVars []string 272 273 for _, file := range serviceConfig.EnvFile { 274 filePath := absPath(workingDir, file.Value) 275 fileVars, err := opts.ParseEnvFile(filePath) 276 if err != nil { 277 return err 278 } 279 envVars = append(envVars, fileVars...) 280 } 281 updateEnvironmentMap(environment, 282 opts.ConvertKVStringsToMapWithNil(envVars), lookupEnv) 283 } 284 285 updateEnvironmentMapTemplate(environment, serviceConfig.Environment, lookupEnv) 286 serviceConfig.Environment = environment 287 return nil 288 } 289 290 func resolveVolumePaths(volumes []templatetypes.ServiceVolumeConfig, workingDir string, lookupEnv template.Mapping) error { 291 for i, volume := range volumes { 292 if volume.Type != "bind" { 293 continue 294 } 295 296 if volume.Source.Value == "" { 297 return errors.New(`invalid mount config for type "bind": field Source must not be empty`) 298 } 299 300 filePath := expandUser(volume.Source.Value, lookupEnv) 301 // Check for a Unix absolute path first, to handle a Windows client 302 // with a Unix daemon. This handles a Windows client connecting to a 303 // Unix daemon. Note that this is not required for Docker for Windows 304 // when specifying a local Windows path, because Docker for Windows 305 // translates the Windows path into a valid path within the VM. 306 if !path.IsAbs(filePath) { 307 filePath = absPath(workingDir, filePath) 308 } 309 volume.Source.Value = filePath 310 volumes[i] = volume 311 } 312 return nil 313 } 314 315 // TODO: make this more robust 316 func expandUser(path string, lookupEnv template.Mapping) string { 317 if strings.HasPrefix(path, "~") { 318 home, ok := lookupEnv("HOME") 319 if !ok { 320 logrus.Warn("cannot expand '~', because the environment lacks HOME") 321 return path 322 } 323 return strings.Replace(path, "~", home, 1) 324 } 325 return path 326 } 327 328 func absPath(workingDir string, filePath string) string { 329 if filepath.IsAbs(filePath) { 330 return filePath 331 } 332 return filepath.Join(workingDir, filePath) 333 } 334 335 func transformServicePort(data interface{}) (interface{}, error) { 336 switch entries := data.(type) { 337 case []interface{}: 338 // We process the list instead of individual items here. 339 // The reason is that one entry might be mapped to multiple ServicePortConfig. 340 // Therefore we take an input of a list and return an output of a list. 341 ports := []interface{}{} 342 for _, entry := range entries { 343 switch value := entry.(type) { 344 case int: 345 v, err := toServicePortConfigs(fmt.Sprint(value)) 346 if err != nil { 347 return data, err 348 } 349 ports = append(ports, v...) 350 case string: 351 v, err := toServicePortConfigs(value) 352 if err != nil { 353 return data, err 354 } 355 ports = append(ports, v...) 356 case map[string]interface{}: 357 ports = append(ports, value) 358 default: 359 return data, errors.Errorf("invalid type %T for port", value) 360 } 361 } 362 return ports, nil 363 default: 364 return data, errors.Errorf("invalid type %T for port", entries) 365 } 366 } 367 368 func transformStringSourceMap(data interface{}) (interface{}, error) { 369 switch value := data.(type) { 370 case string: 371 return map[string]interface{}{"source": value}, nil 372 case map[string]interface{}: 373 return data, nil 374 default: 375 return data, errors.Errorf("invalid type %T for secret", value) 376 } 377 } 378 379 func transformServiceVolumeConfig(data interface{}) (interface{}, error) { 380 switch value := data.(type) { 381 case string: 382 return ParseVolume(value) 383 case map[string]interface{}: 384 return data, nil 385 default: 386 return data, errors.Errorf("invalid type %T for service volume", value) 387 } 388 } 389 390 func transformBoolOrTemplate(value interface{}) (interface{}, error) { 391 switch value := value.(type) { 392 case int: 393 return templatetypes.BoolOrTemplate{Value: value != 0}, nil 394 case bool: 395 return templatetypes.BoolOrTemplate{Value: value}, nil 396 case string: 397 b, err := toBoolean(value) 398 if err == nil { 399 return templatetypes.BoolOrTemplate{Value: b.(bool)}, nil 400 } 401 return templatetypes.BoolOrTemplate{ValueTemplate: value}, nil 402 default: 403 return value, errors.Errorf("invali type %T for boolean", value) 404 } 405 } 406 407 func transformUInt64OrTemplate(value interface{}) (interface{}, error) { 408 switch value := value.(type) { 409 case int: 410 v := uint64(value) 411 return templatetypes.UInt64OrTemplate{Value: &v}, nil 412 case string: 413 v, err := strconv.ParseUint(value, 0, 64) 414 if err == nil { 415 return templatetypes.UInt64OrTemplate{Value: &v}, nil 416 } 417 return templatetypes.UInt64OrTemplate{ValueTemplate: value}, nil 418 default: 419 return value, errors.Errorf("invali type %T for boolean", value) 420 } 421 } 422 423 func transformDurationOrTemplate(value interface{}) (interface{}, error) { 424 switch value := value.(type) { 425 case int: 426 d := time.Duration(value) 427 return templatetypes.DurationOrTemplate{Value: &d}, nil 428 case string: 429 d, err := time.ParseDuration(value) 430 if err == nil { 431 return templatetypes.DurationOrTemplate{Value: &d}, nil 432 } 433 return templatetypes.DurationOrTemplate{ValueTemplate: value}, nil 434 default: 435 return nil, errors.Errorf("invalid type for duration %T", value) 436 } 437 } 438 439 func transformSize(value interface{}) (interface{}, error) { 440 switch value := value.(type) { 441 case int: 442 return templatetypes.UnitBytesOrTemplate{Value: int64(value)}, nil 443 case string: 444 v, err := units.RAMInBytes(value) 445 if err == nil { 446 return templatetypes.UnitBytesOrTemplate{Value: int64(v)}, nil 447 } 448 return templatetypes.UnitBytesOrTemplate{ValueTemplate: value}, nil 449 } 450 return nil, errors.Errorf("invalid type for size %T", value) 451 } 452 453 func transformStringTemplate(value interface{}) (interface{}, error) { 454 return templatetypes.StringTemplate{Value: fmt.Sprintf("%v", value)}, nil 455 } 456 457 func transformStringTemplateList(data interface{}) (interface{}, error) { 458 switch value := data.(type) { 459 case string: 460 return templatetypes.StringTemplateList{templatetypes.StringTemplate{Value: value}}, nil 461 case []interface{}: 462 res := templatetypes.StringTemplateList{} 463 for _, v := range value { 464 res = append(res, templatetypes.StringTemplate{ Value: fmt.Sprintf("%v", v)}) 465 } 466 return res, nil 467 default: 468 return data, errors.Errorf("invalid type %T for string list", value) 469 } 470 } 471 472 func transformShellCommandTemplate(value interface{}) (interface{}, error) { 473 if str, ok := value.(string); ok { 474 return shellwords.Parse(str) 475 } 476 return value, nil 477 } 478 479 func transformHostsListTemplate(data interface{}) (interface{}, error) { 480 return transformListOrMapping(data, ":", false), nil 481 } 482 483 func toStringList(value map[string]interface{}, separator string, allowNil bool) []string { 484 output := []string{} 485 for key, value := range value { 486 if value == nil && !allowNil { 487 continue 488 } 489 output = append(output, fmt.Sprintf("%s%s%s", key, separator, value)) 490 } 491 sort.Strings(output) 492 return output 493 } 494 495 func toString(value interface{}, allowNil bool) interface{} { 496 switch { 497 case value != nil: 498 return fmt.Sprint(value) 499 case allowNil: 500 return nil 501 default: 502 return "" 503 } 504 } 505 506 func toMapStringString(value map[string]interface{}, allowNil bool) map[string]interface{} { 507 output := make(map[string]interface{}) 508 for key, value := range value { 509 output[key] = toString(value, allowNil) 510 } 511 return output 512 } 513 514 func transformListOrMapping(listOrMapping interface{}, sep string, allowNil bool) interface{} { 515 switch value := listOrMapping.(type) { 516 case map[string]interface{}: 517 return toStringList(value, sep, allowNil) 518 case []interface{}: 519 return listOrMapping 520 } 521 panic(errors.Errorf("expected a map or a list, got %T: %#v", listOrMapping, listOrMapping)) 522 } 523 524 func transformMappingOrList(mappingOrList interface{}, sep string, allowNil bool) interface{} { 525 switch value := mappingOrList.(type) { 526 case map[string]interface{}: 527 return toMapStringString(value, allowNil) 528 case ([]interface{}): 529 result := make(map[string]interface{}) 530 for _, value := range value { 531 parts := strings.SplitN(value.(string), sep, 2) 532 key := parts[0] 533 switch { 534 case len(parts) == 1 && allowNil: 535 result[key] = nil 536 case len(parts) == 1 && !allowNil: 537 result[key] = "" 538 default: 539 result[key] = parts[1] 540 } 541 } 542 return result 543 } 544 panic(errors.Errorf("expected a map or a list, got %T: %#v", mappingOrList, mappingOrList)) 545 } 546 547 func transformMappingOrListFunc(sep string, allowNil bool) func(interface{}) (interface{}, error) { 548 return func(data interface{}) (interface{}, error) { 549 return transformMappingOrList(data, sep, allowNil), nil 550 } 551 } 552 553 func toServicePortConfigs(value string) ([]interface{}, error) { 554 var portConfigs []interface{} 555 if strings.Contains(value, "$") { 556 // template detected 557 if strings.Contains(value, "-") { 558 return nil, fmt.Errorf("port range not supported with templated values") 559 } 560 portsProtocol := strings.Split(value, "/") 561 protocol := "tcp" 562 if len(portsProtocol) > 1 { 563 protocol = portsProtocol[1] 564 } 565 portPort := strings.Split(portsProtocol[0], ":") 566 tgt, _ := transformUInt64OrTemplate(portPort[0]) // can't fail on string 567 pub := templatetypes.UInt64OrTemplate{} 568 if len(portPort) > 1 { 569 ipub, _ := transformUInt64OrTemplate(portPort[1]) 570 pub = ipub.(templatetypes.UInt64OrTemplate) 571 } 572 portConfigs = append(portConfigs, templatetypes.ServicePortConfig{ 573 Protocol: templatetypes.StringTemplate{Value: protocol}, 574 Target: tgt.(templatetypes.UInt64OrTemplate), 575 Published: pub, 576 Mode: templatetypes.StringTemplate{Value: "ingress"}, 577 }) 578 return portConfigs, nil 579 } 580 581 ports, portBindings, err := nat.ParsePortSpecs([]string{value}) 582 if err != nil { 583 return nil, err 584 } 585 // We need to sort the key of the ports to make sure it is consistent 586 keys := []string{} 587 for port := range ports { 588 keys = append(keys, string(port)) 589 } 590 sort.Strings(keys) 591 592 for _, key := range keys { 593 // Reuse ConvertPortToPortConfig so that it is consistent 594 portConfig, err := opts.ConvertPortToPortConfig(nat.Port(key), portBindings) 595 if err != nil { 596 return nil, err 597 } 598 for _, p := range portConfig { 599 tp := uint64(p.TargetPort) 600 pp := uint64(p.PublishedPort) 601 portConfigs = append(portConfigs, templatetypes.ServicePortConfig{ 602 Protocol: templatetypes.StringTemplate{Value: string(p.Protocol)}, 603 Target: templatetypes.UInt64OrTemplate{Value: &tp}, 604 Published: templatetypes.UInt64OrTemplate{Value: &pp}, 605 Mode: templatetypes.StringTemplate{Value: string(p.PublishMode)}, 606 }) 607 } 608 } 609 610 return portConfigs, nil 611 }