loader.go 20 KB

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