diff options
Diffstat (limited to 'pkg/specgen')
-rw-r--r-- | pkg/specgen/generate/config_linux.go | 11 | ||||
-rw-r--r-- | pkg/specgen/generate/kube/kube.go | 130 | ||||
-rw-r--r-- | pkg/specgen/generate/kube/play_test.go | 398 | ||||
-rw-r--r-- | pkg/specgen/generate/kube/volume.go | 2 | ||||
-rw-r--r-- | pkg/specgen/generate/pod_create.go | 6 | ||||
-rw-r--r-- | pkg/specgen/generate/security.go | 2 | ||||
-rw-r--r-- | pkg/specgen/namespaces.go | 4 | ||||
-rw-r--r-- | pkg/specgen/podspecgen.go | 2 | ||||
-rw-r--r-- | pkg/specgen/specgen.go | 4 |
9 files changed, 472 insertions, 87 deletions
diff --git a/pkg/specgen/generate/config_linux.go b/pkg/specgen/generate/config_linux.go index fe220b9e8..a5772bc6a 100644 --- a/pkg/specgen/generate/config_linux.go +++ b/pkg/specgen/generate/config_linux.go @@ -47,17 +47,6 @@ func addPrivilegedDevices(g *generate.Generator) error { if _, found := mounts[d.Path]; found { continue } - st, err := os.Stat(d.Path) - if err != nil { - if err == unix.EPERM { - continue - } - return err - } - // Skip devices that the user has not access to. - if st.Mode()&0007 == 0 { - continue - } newMounts = append(newMounts, devMnt) } g.Config.Mounts = append(newMounts, g.Config.Mounts...) diff --git a/pkg/specgen/generate/kube/kube.go b/pkg/specgen/generate/kube/kube.go index 2fd149b49..475401016 100644 --- a/pkg/specgen/generate/kube/kube.go +++ b/pkg/specgen/generate/kube/kube.go @@ -4,7 +4,10 @@ import ( "context" "encoding/json" "fmt" + "math" "net" + "regexp" + "strconv" "strings" "time" @@ -291,9 +294,9 @@ func ToSpecGen(ctx context.Context, opts *CtrSpecGenOptions) (*specgen.SpecGener return nil, err } - // Only set the env if the value is not "" - if value != "" { - envs[env.Name] = value + // Only set the env if the value is not nil + if value != nil { + envs[env.Name] = *value } } for _, envFrom := range opts.Container.EnvFrom { @@ -609,7 +612,7 @@ func envVarsFrom(envFrom v1.EnvFromSource, opts *CtrSpecGenOptions) (map[string] // envVarValue returns the environment variable value configured within the container's env setting. // It gets the value from a configMap or secret if specified, otherwise returns env.Value -func envVarValue(env v1.EnvVar, opts *CtrSpecGenOptions) (string, error) { +func envVarValue(env v1.EnvVar, opts *CtrSpecGenOptions) (*string, error) { if env.ValueFrom != nil { if env.ValueFrom.ConfigMapKeyRef != nil { cmKeyRef := env.ValueFrom.ConfigMapKeyRef @@ -618,16 +621,16 @@ func envVarValue(env v1.EnvVar, opts *CtrSpecGenOptions) (string, error) { for _, c := range opts.ConfigMaps { if cmKeyRef.Name == c.Name { if value, ok := c.Data[cmKeyRef.Key]; ok { - return value, nil + return &value, nil } err = errors.Errorf("Cannot set env %v: key %s not found in configmap %v", env.Name, cmKeyRef.Key, cmKeyRef.Name) break } } if cmKeyRef.Optional == nil || !*cmKeyRef.Optional { - return "", err + return nil, err } - return "", nil + return nil, nil } if env.ValueFrom.SecretKeyRef != nil { @@ -635,18 +638,123 @@ func envVarValue(env v1.EnvVar, opts *CtrSpecGenOptions) (string, error) { secret, err := k8sSecretFromSecretManager(secKeyRef.Name, opts.SecretsManager) if err == nil { if val, ok := secret[secKeyRef.Key]; ok { - return string(val), nil + value := string(val) + return &value, nil } err = errors.Errorf("Secret %v has not %v key", secKeyRef.Name, secKeyRef.Key) } if secKeyRef.Optional == nil || !*secKeyRef.Optional { - return "", errors.Errorf("Cannot set env %v: %v", env.Name, err) + return nil, errors.Errorf("Cannot set env %v: %v", env.Name, err) } - return "", nil + return nil, nil + } + + if env.ValueFrom.FieldRef != nil { + return envVarValueFieldRef(env, opts) + } + + if env.ValueFrom.ResourceFieldRef != nil { + return envVarValueResourceFieldRef(env, opts) } } - return env.Value, nil + return &env.Value, nil +} + +func envVarValueFieldRef(env v1.EnvVar, opts *CtrSpecGenOptions) (*string, error) { + fieldRef := env.ValueFrom.FieldRef + + fieldPathLabelPattern := `^metadata.labels\['(.+)'\]$` + fieldPathLabelRegex := regexp.MustCompile(fieldPathLabelPattern) + fieldPathAnnotationPattern := `^metadata.annotations\['(.+)'\]$` + fieldPathAnnotationRegex := regexp.MustCompile(fieldPathAnnotationPattern) + + fieldPath := fieldRef.FieldPath + + if fieldPath == "metadata.name" { + return &opts.PodName, nil + } + if fieldPath == "metadata.uid" { + return &opts.PodID, nil + } + fieldPathMatches := fieldPathLabelRegex.FindStringSubmatch(fieldPath) + if len(fieldPathMatches) == 2 { // 1 for entire regex and 1 for subexp + labelValue := opts.Labels[fieldPathMatches[1]] // not existent label is OK + return &labelValue, nil + } + fieldPathMatches = fieldPathAnnotationRegex.FindStringSubmatch(fieldPath) + if len(fieldPathMatches) == 2 { // 1 for entire regex and 1 for subexp + annotationValue := opts.Annotations[fieldPathMatches[1]] // not existent annotation is OK + return &annotationValue, nil + } + + return nil, errors.Errorf( + "Can not set env %v. Reason: fieldPath %v is either not valid or not supported", + env.Name, fieldPath, + ) +} + +func envVarValueResourceFieldRef(env v1.EnvVar, opts *CtrSpecGenOptions) (*string, error) { + divisor := env.ValueFrom.ResourceFieldRef.Divisor + if divisor.IsZero() { // divisor not set, use default + divisor.Set(1) + } + + var value *resource.Quantity + resources := opts.Container.Resources + resourceName := env.ValueFrom.ResourceFieldRef.Resource + var isValidDivisor bool + + switch resourceName { + case "limits.memory": + value = resources.Limits.Memory() + isValidDivisor = isMemoryDivisor(divisor) + case "limits.cpu": + value = resources.Limits.Cpu() + isValidDivisor = isCPUDivisor(divisor) + case "requests.memory": + value = resources.Requests.Memory() + isValidDivisor = isMemoryDivisor(divisor) + case "requests.cpu": + value = resources.Requests.Cpu() + isValidDivisor = isCPUDivisor(divisor) + default: + return nil, errors.Errorf( + "Can not set env %v. Reason: resource %v is either not valid or not supported", + env.Name, resourceName, + ) + } + + if !isValidDivisor { + return nil, errors.Errorf( + "Can not set env %s. Reason: divisor value %s is not valid", + env.Name, divisor.String(), + ) + } + + // k8s rounds up the result to the nearest integer + intValue := int(math.Ceil(value.AsApproximateFloat64() / divisor.AsApproximateFloat64())) + stringValue := strconv.Itoa(intValue) + + return &stringValue, nil +} + +func isMemoryDivisor(divisor resource.Quantity) bool { + switch divisor.String() { + case "1", "1k", "1M", "1G", "1T", "1P", "1E", "1Ki", "1Mi", "1Gi", "1Ti", "1Pi", "1Ei": + return true + default: + return false + } +} + +func isCPUDivisor(divisor resource.Quantity) bool { + switch divisor.String() { + case "1", "1m": + return true + default: + return false + } } // getPodPorts converts a slice of kube container descriptions to an diff --git a/pkg/specgen/generate/kube/play_test.go b/pkg/specgen/generate/kube/play_test.go index f714826f0..282324310 100644 --- a/pkg/specgen/generate/kube/play_test.go +++ b/pkg/specgen/generate/kube/play_test.go @@ -2,13 +2,17 @@ package kube import ( "encoding/json" + "fmt" "io/ioutil" + "math" "os" + "strconv" "testing" "github.com/containers/common/pkg/secrets" "github.com/stretchr/testify/assert" v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" v12 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -233,7 +237,7 @@ func TestEnvVarValue(t *testing.T) { ConfigMaps: configMapList, }, false, - "", + nilString, }, { "OptionalContainerKeyDoesNotExistInConfigMap", @@ -253,7 +257,7 @@ func TestEnvVarValue(t *testing.T) { ConfigMaps: configMapList, }, true, - "", + nilString, }, { "ConfigMapDoesNotExist", @@ -272,7 +276,7 @@ func TestEnvVarValue(t *testing.T) { ConfigMaps: configMapList, }, false, - "", + nilString, }, { "OptionalConfigMapDoesNotExist", @@ -292,7 +296,7 @@ func TestEnvVarValue(t *testing.T) { ConfigMaps: configMapList, }, true, - "", + nilString, }, { "EmptyConfigMapList", @@ -311,7 +315,7 @@ func TestEnvVarValue(t *testing.T) { ConfigMaps: []v1.ConfigMap{}, }, false, - "", + nilString, }, { "OptionalEmptyConfigMapList", @@ -331,7 +335,7 @@ func TestEnvVarValue(t *testing.T) { ConfigMaps: []v1.ConfigMap{}, }, true, - "", + nilString, }, { "SecretExists", @@ -369,7 +373,7 @@ func TestEnvVarValue(t *testing.T) { SecretsManager: secretsManager, }, false, - "", + nilString, }, { "OptionalContainerKeyDoesNotExistInSecret", @@ -389,7 +393,7 @@ func TestEnvVarValue(t *testing.T) { SecretsManager: secretsManager, }, true, - "", + nilString, }, { "SecretDoesNotExist", @@ -408,7 +412,7 @@ func TestEnvVarValue(t *testing.T) { SecretsManager: secretsManager, }, false, - "", + nilString, }, { "OptionalSecretDoesNotExist", @@ -428,8 +432,268 @@ func TestEnvVarValue(t *testing.T) { SecretsManager: secretsManager, }, true, + nilString, + }, + { + "FieldRefMetadataName", + v1.EnvVar{ + Name: "FOO", + ValueFrom: &v1.EnvVarSource{ + FieldRef: &v1.ObjectFieldSelector{ + FieldPath: "metadata.name", + }, + }, + }, + CtrSpecGenOptions{ + PodName: "test", + }, + true, + "test", + }, + { + "FieldRefMetadataUID", + v1.EnvVar{ + Name: "FOO", + ValueFrom: &v1.EnvVarSource{ + FieldRef: &v1.ObjectFieldSelector{ + FieldPath: "metadata.uid", + }, + }, + }, + CtrSpecGenOptions{ + PodID: "ec71ff37c67b688598c0008187ab0960dc34e1dfdcbf3a74e3d778bafcfe0977", + }, + true, + "ec71ff37c67b688598c0008187ab0960dc34e1dfdcbf3a74e3d778bafcfe0977", + }, + { + "FieldRefMetadataLabelsExist", + v1.EnvVar{ + Name: "FOO", + ValueFrom: &v1.EnvVarSource{ + FieldRef: &v1.ObjectFieldSelector{ + FieldPath: "metadata.labels['label']", + }, + }, + }, + CtrSpecGenOptions{ + Labels: map[string]string{"label": "label"}, + }, + true, + "label", + }, + { + "FieldRefMetadataLabelsEmpty", + v1.EnvVar{ + Name: "FOO", + ValueFrom: &v1.EnvVarSource{ + FieldRef: &v1.ObjectFieldSelector{ + FieldPath: "metadata.labels['label']", + }, + }, + }, + CtrSpecGenOptions{ + Labels: map[string]string{"label": ""}, + }, + true, + "", + }, + { + "FieldRefMetadataLabelsNotExist", + v1.EnvVar{ + Name: "FOO", + ValueFrom: &v1.EnvVarSource{ + FieldRef: &v1.ObjectFieldSelector{ + FieldPath: "metadata.labels['label']", + }, + }, + }, + CtrSpecGenOptions{}, + true, + "", + }, + { + "FieldRefMetadataAnnotationsExist", + v1.EnvVar{ + Name: "FOO", + ValueFrom: &v1.EnvVarSource{ + FieldRef: &v1.ObjectFieldSelector{ + FieldPath: "metadata.annotations['annotation']", + }, + }, + }, + CtrSpecGenOptions{ + Annotations: map[string]string{"annotation": "annotation"}, + }, + true, + "annotation", + }, + { + "FieldRefMetadataAnnotationsEmpty", + v1.EnvVar{ + Name: "FOO", + ValueFrom: &v1.EnvVarSource{ + FieldRef: &v1.ObjectFieldSelector{ + FieldPath: "metadata.annotations['annotation']", + }, + }, + }, + CtrSpecGenOptions{ + Annotations: map[string]string{"annotation": ""}, + }, + true, "", }, + { + "FieldRefMetadataAnnotationsNotExist", + v1.EnvVar{ + Name: "FOO", + ValueFrom: &v1.EnvVarSource{ + FieldRef: &v1.ObjectFieldSelector{ + FieldPath: "metadata.annotations['annotation']", + }, + }, + }, + CtrSpecGenOptions{}, + true, + "", + }, + { + "FieldRefInvalid1", + v1.EnvVar{ + Name: "FOO", + ValueFrom: &v1.EnvVarSource{ + FieldRef: &v1.ObjectFieldSelector{ + FieldPath: "metadata.annotations['annotation]", + }, + }, + }, + CtrSpecGenOptions{}, + false, + nilString, + }, + { + "FieldRefInvalid2", + v1.EnvVar{ + Name: "FOO", + ValueFrom: &v1.EnvVarSource{ + FieldRef: &v1.ObjectFieldSelector{ + FieldPath: "metadata.dummy['annotation']", + }, + }, + }, + CtrSpecGenOptions{}, + false, + nilString, + }, + { + "FieldRefNotSupported", + v1.EnvVar{ + Name: "FOO", + ValueFrom: &v1.EnvVarSource{ + FieldRef: &v1.ObjectFieldSelector{ + FieldPath: "metadata.namespace", + }, + }, + }, + CtrSpecGenOptions{}, + false, + nilString, + }, + { + "ResourceFieldRefNotSupported", + v1.EnvVar{ + Name: "FOO", + ValueFrom: &v1.EnvVarSource{ + ResourceFieldRef: &v1.ResourceFieldSelector{ + Resource: "limits.dummy", + }, + }, + }, + CtrSpecGenOptions{}, + false, + nilString, + }, + { + "ResourceFieldRefMemoryDivisorNotValid", + v1.EnvVar{ + Name: "FOO", + ValueFrom: &v1.EnvVarSource{ + ResourceFieldRef: &v1.ResourceFieldSelector{ + Resource: "limits.memory", + Divisor: resource.MustParse("2M"), + }, + }, + }, + CtrSpecGenOptions{}, + false, + nilString, + }, + { + "ResourceFieldRefCpuDivisorNotValid", + v1.EnvVar{ + Name: "FOO", + ValueFrom: &v1.EnvVarSource{ + ResourceFieldRef: &v1.ResourceFieldSelector{ + Resource: "limits.cpu", + Divisor: resource.MustParse("2m"), + }, + }, + }, + CtrSpecGenOptions{}, + false, + nilString, + }, + { + "ResourceFieldRefNoDivisor", + v1.EnvVar{ + Name: "FOO", + ValueFrom: &v1.EnvVarSource{ + ResourceFieldRef: &v1.ResourceFieldSelector{ + Resource: "limits.memory", + }, + }, + }, + CtrSpecGenOptions{ + Container: container, + }, + true, + memoryString, + }, + { + "ResourceFieldRefMemoryDivisor", + v1.EnvVar{ + Name: "FOO", + ValueFrom: &v1.EnvVarSource{ + ResourceFieldRef: &v1.ResourceFieldSelector{ + Resource: "limits.memory", + Divisor: resource.MustParse("1Mi"), + }, + }, + }, + CtrSpecGenOptions{ + Container: container, + }, + true, + strconv.Itoa(int(math.Ceil(float64(memoryInt) / 1024 / 1024))), + }, + { + "ResourceFieldRefCpuDivisor", + v1.EnvVar{ + Name: "FOO", + ValueFrom: &v1.EnvVarSource{ + ResourceFieldRef: &v1.ResourceFieldSelector{ + Resource: "requests.cpu", + Divisor: resource.MustParse("1m"), + }, + }, + }, + CtrSpecGenOptions{ + Container: container, + }, + true, + strconv.Itoa(int(float64(cpuInt) / 0.001)), + }, } for _, test := range tests { @@ -437,59 +701,85 @@ func TestEnvVarValue(t *testing.T) { t.Run(test.name, func(t *testing.T) { result, err := envVarValue(test.envVar, &test.options) assert.Equal(t, err == nil, test.succeed) - assert.Equal(t, test.expected, result) + if test.expected == nilString { + assert.Nil(t, result) + } else { + fmt.Println(*result, test.expected) + assert.Equal(t, &(test.expected), result) + } }) } } -var configMapList = []v1.ConfigMap{ - { - TypeMeta: v12.TypeMeta{ - Kind: "ConfigMap", - }, - ObjectMeta: v12.ObjectMeta{ - Name: "bar", - }, - Data: map[string]string{ - "myvar": "bar", - }, - }, - { - TypeMeta: v12.TypeMeta{ - Kind: "ConfigMap", - }, - ObjectMeta: v12.ObjectMeta{ - Name: "foo", +var ( + nilString = "<nil>" + configMapList = []v1.ConfigMap{ + { + TypeMeta: v12.TypeMeta{ + Kind: "ConfigMap", + }, + ObjectMeta: v12.ObjectMeta{ + Name: "bar", + }, + Data: map[string]string{ + "myvar": "bar", + }, }, - Data: map[string]string{ - "myvar": "foo", + { + TypeMeta: v12.TypeMeta{ + Kind: "ConfigMap", + }, + ObjectMeta: v12.ObjectMeta{ + Name: "foo", + }, + Data: map[string]string{ + "myvar": "foo", + }, }, - }, -} + } -var optional = true + optional = true -var k8sSecrets = []v1.Secret{ - { - TypeMeta: v12.TypeMeta{ - Kind: "Secret", - }, - ObjectMeta: v12.ObjectMeta{ - Name: "bar", - }, - Data: map[string][]byte{ - "myvar": []byte("bar"), - }, - }, - { - TypeMeta: v12.TypeMeta{ - Kind: "Secret", + k8sSecrets = []v1.Secret{ + { + TypeMeta: v12.TypeMeta{ + Kind: "Secret", + }, + ObjectMeta: v12.ObjectMeta{ + Name: "bar", + }, + Data: map[string][]byte{ + "myvar": []byte("bar"), + }, }, - ObjectMeta: v12.ObjectMeta{ - Name: "foo", + { + TypeMeta: v12.TypeMeta{ + Kind: "Secret", + }, + ObjectMeta: v12.ObjectMeta{ + Name: "foo", + }, + Data: map[string][]byte{ + "myvar": []byte("foo"), + }, }, - Data: map[string][]byte{ - "myvar": []byte("foo"), + } + + cpuInt = 4 + cpuString = strconv.Itoa(cpuInt) + memoryInt = 30000000 + memoryString = strconv.Itoa(memoryInt) + container = v1.Container{ + Name: "test", + Resources: v1.ResourceRequirements{ + Limits: v1.ResourceList{ + v1.ResourceCPU: resource.MustParse(cpuString), + v1.ResourceMemory: resource.MustParse(memoryString), + }, + Requests: v1.ResourceList{ + v1.ResourceCPU: resource.MustParse(cpuString), + v1.ResourceMemory: resource.MustParse(memoryString), + }, }, - }, -} + } +) diff --git a/pkg/specgen/generate/kube/volume.go b/pkg/specgen/generate/kube/volume.go index e52d70092..01f731b60 100644 --- a/pkg/specgen/generate/kube/volume.go +++ b/pkg/specgen/generate/kube/volume.go @@ -122,7 +122,7 @@ func VolumeFromConfigMap(configMapVolumeSource *v1.ConfigMapVolumeSource, config if configMap == nil { // If the volumeSource was optional, move on even if a matching configmap wasn't found - if *configMapVolumeSource.Optional { + if configMapVolumeSource.Optional != nil && *configMapVolumeSource.Optional { kv.Source = configMapVolumeSource.Name kv.Optional = *configMapVolumeSource.Optional return kv, nil diff --git a/pkg/specgen/generate/pod_create.go b/pkg/specgen/generate/pod_create.go index 672a53eea..68fda3ad7 100644 --- a/pkg/specgen/generate/pod_create.go +++ b/pkg/specgen/generate/pod_create.go @@ -82,7 +82,7 @@ func pullOrBuildInfraImage(p *entities.PodSpec, rt *libpod.Runtime) error { imageName = rtConfig.Engine.InfraImage } - if imageName != config.DefaultInfraImage { + if imageName != "" { _, err := rt.LibimageRuntime().Pull(context.Background(), imageName, config.PullPolicyMissing, nil) if err != nil { return err @@ -281,8 +281,6 @@ func MapSpec(p *specgen.PodSpecGenerator) (*specgen.SpecGenerator, error) { p.InfraContainerSpec.ConmonPidFile = p.InfraConmonPidFile } - if p.InfraImage != config.DefaultInfraImage { - p.InfraContainerSpec.Image = p.InfraImage - } + p.InfraContainerSpec.Image = p.InfraImage return p.InfraContainerSpec, nil } diff --git a/pkg/specgen/generate/security.go b/pkg/specgen/generate/security.go index 2eaf71897..9c6709905 100644 --- a/pkg/specgen/generate/security.go +++ b/pkg/specgen/generate/security.go @@ -246,7 +246,7 @@ func securityConfigureGenerator(s *specgen.SpecGenerator, g *generate.Generator, // Ignore net sysctls if --net=host if s.NetNS.IsHost() && strings.HasPrefix(sysctlKey, "net.") { - return errors.Wrapf(define.ErrInvalidArg, "sysctl %s=%s can't be set since Host Namespace set to host", sysctlKey, sysctlVal) + return errors.Wrapf(define.ErrInvalidArg, "sysctl %s=%s can't be set since Network Namespace set to host", sysctlKey, sysctlVal) } // Ignore uts sysctls if --uts=host diff --git a/pkg/specgen/namespaces.go b/pkg/specgen/namespaces.go index c1356bef4..e672bc65f 100644 --- a/pkg/specgen/namespaces.go +++ b/pkg/specgen/namespaces.go @@ -353,11 +353,11 @@ func ParseNetworkFlag(networks []string) (Namespace, map[string]types.PerNetwork toReturn.NSMode = FromPod case ns == "" || ns == string(Default) || ns == string(Private): // Net defaults to Slirp on rootless - if rootless.IsRootless() && containerConfig.Containers.RootlessNetworking != "cni" { + if rootless.IsRootless() { toReturn.NSMode = Slirp break } - // if not slirp we use bridge + // if root we use bridge fallthrough case ns == string(Bridge), strings.HasPrefix(ns, string(Bridge)+":"): toReturn.NSMode = Bridge diff --git a/pkg/specgen/podspecgen.go b/pkg/specgen/podspecgen.go index 7b1115f5d..759caa0c0 100644 --- a/pkg/specgen/podspecgen.go +++ b/pkg/specgen/podspecgen.go @@ -96,7 +96,7 @@ type PodNetworkConfig struct { // Only available if NetNS is set to Bridge or Slirp. // Optional. PortMappings []types.PortMapping `json:"portmappings,omitempty"` - // Map of networks names ot ids the container should join to. + // Map of networks names to ids the container should join to. // You can request additional settings for each network, you can // set network aliases, static ips, static mac address and the // network interface name for this container on the specific network. diff --git a/pkg/specgen/specgen.go b/pkg/specgen/specgen.go index 750fc875d..7f6f79b87 100644 --- a/pkg/specgen/specgen.go +++ b/pkg/specgen/specgen.go @@ -223,7 +223,7 @@ type ContainerStorageConfig struct { // Conflicts with Image. // At least one of Image or Rootfs must be specified. Rootfs string `json:"rootfs,omitempty"` - // RootfsOverlay tells if rootfs is actuall an overlay on top of base path + // RootfsOverlay tells if rootfs is actually an overlay on top of base path RootfsOverlay bool `json:"rootfs_overlay,omitempty"` // ImageVolumeMode indicates how image volumes will be created. // Supported modes are "ignore" (do not create), "tmpfs" (create as @@ -423,7 +423,7 @@ type ContainerNetworkConfig struct { // PublishExposedPorts is set. // Optional. Expose map[uint16]string `json:"expose,omitempty"` - // Map of networks names ot ids the container should join to. + // Map of networks names or ids that the container should join. // You can request additional settings for each network, you can // set network aliases, static ips, static mac address and the // network interface name for this container on the specific network. |