loader.go 20 KB

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