diff --git a/docs/conversion.md b/docs/conversion.md index 27b73136..6c038984 100644 --- a/docs/conversion.md +++ b/docs/conversion.md @@ -31,7 +31,7 @@ __Glossary:__ | deploy | - | - | ✓ | | | | deploy: mode | - | - | ✓ | | | | deploy: replicas | - | - | ✓ | Deployment.Spec.Replicas / DeploymentConfig.Spec.Replicas | | -| deploy: placement | - | - | ✓ | Pod.Spec.NodeSelector | | +| deploy: placement | - | - | ✓ | Pod.Spec.Affinity | | | deploy: update_config | - | - | ✓ | Workload.Spec.Strategy | Deployment / DeploymentConfig | | deploy: resources | - | - | ✓ | Containers.Resources.Limits.Memory / Containers.Resources.Limits.CPU | Support for memory as well as cpu | | deploy: restart_policy | - | - | ✓ | Pod generation | This generated a Pod, see the [user guide on restart](http://kompose.io/user-guide/#restart) | diff --git a/pkg/kobject/kobject.go b/pkg/kobject/kobject.go index c20b20a7..35b7b112 100644 --- a/pkg/kobject/kobject.go +++ b/pkg/kobject/kobject.go @@ -145,8 +145,8 @@ type ServiceConfig struct { GroupAdd []int64 `compose:"group_add"` Volumes []Volumes `compose:""` Secrets []dockerCliTypes.ServiceSecretConfig - HealthChecks HealthChecks `compose:""` - Placement map[string]string `compose:""` + HealthChecks HealthChecks `compose:""` + Placement Placement `compose:""` //This is for long LONG SYNTAX link(https://docs.docker.com/compose/compose-file/#long-syntax) Configs []dockerCliTypes.ServiceConfigObjConfig `compose:""` //This is for SHORT SYNTAX link(https://docs.docker.com/compose/compose-file/#configs) @@ -203,6 +203,12 @@ type Volumes struct { SelectorValue string // Value of the label selector } +// Placement holds the placement struct of container +type Placement struct { + PositiveConstraints map[string]string + NegativeConstraints map[string]string +} + // GetConfigMapKeyFromMeta ... // given a source name ,find the file and extract the filename which will be act as ConfigMap key // return "" if not found diff --git a/pkg/loader/compose/compose_test.go b/pkg/loader/compose/compose_test.go index a3fecd1b..07653c61 100644 --- a/pkg/loader/compose/compose_test.go +++ b/pkg/loader/compose/compose_test.go @@ -500,11 +500,29 @@ func TestCheckPlacementCustomLabels(t *testing.T) { "node.labels.monitor != xxx", }, } - output := loadV3Placement(placement.Constraints) + output := loadV3Placement(placement) - expected := map[string]string{"something": "anything"} + expected := kobject.Placement{ + PositiveConstraints: map[string]string{ + "something": "anything", + }, + NegativeConstraints: map[string]string{ + "monitor": "xxx", + }, + } - if output["something"] != expected["something"] { - t.Errorf("Expected %s, got %s", expected, output) + checkConstraints(t, "positive", output.PositiveConstraints, expected.PositiveConstraints) + checkConstraints(t, "negative", output.NegativeConstraints, expected.NegativeConstraints) +} + +func checkConstraints(t *testing.T, caseName string, output, expected map[string]string) { + t.Log("Test case:", caseName) + if len(output) != len(expected) { + t.Errorf("constraints len is not equal, expected %d, got %d", len(expected), len(output)) + } + for key := range output { + if output[key] != expected[key] { + t.Errorf("%s constraint is not equal, expected %s, got %s", key, expected[key], output[key]) + } } } diff --git a/pkg/loader/compose/v3.go b/pkg/loader/compose/v3.go index a7f7907c..1e00a948 100644 --- a/pkg/loader/compose/v3.go +++ b/pkg/loader/compose/v3.go @@ -33,7 +33,7 @@ import ( "github.com/docker/cli/cli/compose/loader" "github.com/docker/cli/cli/compose/types" - shlex "github.com/google/shlex" + "github.com/google/shlex" "github.com/kubernetes/kompose/pkg/kobject" "github.com/pkg/errors" log "github.com/sirupsen/logrus" @@ -133,27 +133,43 @@ func parseV3(files []string) (kobject.KomposeObject, error) { return komposeObject, nil } -func loadV3Placement(constraints []string) map[string]string { - placement := make(map[string]string) +func loadV3Placement(placement types.Placement) kobject.Placement { + komposePlacement := kobject.Placement{ + PositiveConstraints: make(map[string]string), + NegativeConstraints: make(map[string]string), + } + equal, notEqual := " == ", " != " errMsg := " constraints in placement is not supported, only 'node.hostname', 'engine.labels.operatingsystem' and 'node.labels.xxx' (ex: node.labels.something == anything) is supported as a constraint " - for _, j := range constraints { - p := strings.Split(j, " == ") + for _, j := range placement.Constraints { + operator := equal + if strings.Contains(j, notEqual) { + operator = notEqual + } + p := strings.Split(j, operator) if len(p) < 2 { log.Warn(p[0], errMsg) continue } + + var key string if p[0] == "node.hostname" { - placement["kubernetes.io/hostname"] = p[1] + key = "kubernetes.io/hostname" } else if p[0] == "engine.labels.operatingsystem" { - placement["beta.kubernetes.io/os"] = p[1] + key = "beta.kubernetes.io/os" } else if strings.HasPrefix(p[0], "node.labels.") { - label := strings.TrimPrefix(p[0], "node.labels.") - placement[label] = p[1] + key = strings.TrimPrefix(p[0], "node.labels.") } else { log.Warn(p[0], errMsg) + continue + } + + if operator == equal { + komposePlacement.PositiveConstraints[key] = p[1] + } else if operator == notEqual { + komposePlacement.NegativeConstraints[key] = p[1] } } - return placement + return komposePlacement } // Convert the Docker Compose v3 volumes to []string (the old way) @@ -434,7 +450,7 @@ func dockerComposeToKomposeMapping(composeObject *types.Config) (kobject.Kompose } // placement: - serviceConfig.Placement = loadV3Placement(composeServiceConfig.Deploy.Placement.Constraints) + serviceConfig.Placement = loadV3Placement(composeServiceConfig.Deploy.Placement) if composeServiceConfig.Deploy.UpdateConfig != nil { serviceConfig.DeployUpdateConfig = *composeServiceConfig.Deploy.UpdateConfig diff --git a/pkg/transformer/kubernetes/k8sutils.go b/pkg/transformer/kubernetes/k8sutils.go index 9c08c64f..dc42f048 100644 --- a/pkg/transformer/kubernetes/k8sutils.go +++ b/pkg/transformer/kubernetes/k8sutils.go @@ -534,7 +534,7 @@ func (k *Kubernetes) UpdateKubernetesObjects(name string, service kobject.Servic template.Spec.Containers[0].Stdin = service.Stdin template.Spec.Containers[0].TTY = service.Tty template.Spec.Volumes = append(template.Spec.Volumes, volumes...) - template.Spec.NodeSelector = service.Placement + template.Spec.Affinity = ConfigAffinity(service) // Configure the HealthCheck // We check to see if it's blank if !reflect.DeepEqual(service.HealthChecks.Liveness, kobject.HealthCheck{}) { diff --git a/pkg/transformer/kubernetes/kubernetes.go b/pkg/transformer/kubernetes/kubernetes.go index 917e0115..f6f716e7 100644 --- a/pkg/transformer/kubernetes/kubernetes.go +++ b/pkg/transformer/kubernetes/kubernetes.go @@ -1020,6 +1020,43 @@ func ConfigEnvs(name string, service kobject.ServiceConfig, opt kobject.ConvertO return envs, nil } +// ConfigAffinity configures the Affinity. +func ConfigAffinity(service kobject.ServiceConfig) *api.Affinity { + positiveConstraints := configConstrains(service.Placement.PositiveConstraints, api.NodeSelectorOpIn) + negativeConstraints := configConstrains(service.Placement.NegativeConstraints, api.NodeSelectorOpNotIn) + if len(positiveConstraints) == 0 && len(negativeConstraints) == 0 { + return nil + } + return &api.Affinity{ + NodeAffinity: &api.NodeAffinity{ + RequiredDuringSchedulingIgnoredDuringExecution: &api.NodeSelector{ + NodeSelectorTerms: []api.NodeSelectorTerm{ + { + MatchExpressions: append(positiveConstraints, negativeConstraints...), + }, + }, + }, + }, + } +} + +func configConstrains(constrains map[string]string, operator api.NodeSelectorOperator) []api.NodeSelectorRequirement { + constraintsLen := len(constrains) + rs := make([]api.NodeSelectorRequirement, 0, constraintsLen) + if constraintsLen == 0 { + return rs + } + for k, v := range constrains { + r := api.NodeSelectorRequirement{ + Key: k, + Operator: operator, + Values: []string{v}, + } + rs = append(rs, r) + } + return rs +} + // CreateKubernetesObjects generates a Kubernetes artifact for each input type service func (k *Kubernetes) CreateKubernetesObjects(name string, service kobject.ServiceConfig, opt kobject.ConvertOptions) []runtime.Object { var objects []runtime.Object diff --git a/pkg/transformer/kubernetes/kubernetes_test.go b/pkg/transformer/kubernetes/kubernetes_test.go index 8209192b..54941369 100644 --- a/pkg/transformer/kubernetes/kubernetes_test.go +++ b/pkg/transformer/kubernetes/kubernetes_test.go @@ -554,6 +554,52 @@ func TestConfigCapabilities(t *testing.T) { } } +func TestConfigAffinity(t *testing.T) { + testCases := map[string]struct { + service kobject.ServiceConfig + result *api.Affinity + }{ + "ConfigAffinity": { + service: kobject.ServiceConfig{ + Placement: kobject.Placement{ + PositiveConstraints: map[string]string{ + "foo": "bar", + }, + NegativeConstraints: map[string]string{ + "baz": "qux", + }, + }, + }, + result: &api.Affinity{ + NodeAffinity: &api.NodeAffinity{ + RequiredDuringSchedulingIgnoredDuringExecution: &api.NodeSelector{ + NodeSelectorTerms: []api.NodeSelectorTerm{ + { + MatchExpressions: []api.NodeSelectorRequirement{ + {Key: "foo", Operator: api.NodeSelectorOpIn, Values: []string{"bar"}}, + {Key: "baz", Operator: api.NodeSelectorOpNotIn, Values: []string{"qux"}}, + }, + }, + }, + }, + }, + }, + }, + "ConfigAffinity (nil)": { + kobject.ServiceConfig{}, + nil, + }, + } + + for name, test := range testCases { + t.Log("Test case:", name) + result := ConfigAffinity(test.service) + if !reflect.DeepEqual(result, test.result) { + t.Errorf("Not expected result for ConfigAffinity") + } + } +} + func TestMultipleContainersInPod(t *testing.T) { groupName := "pod_group" containerName := "" diff --git a/pkg/transformer/kubernetes/podspec.go b/pkg/transformer/kubernetes/podspec.go index 74def137..00f7b0e0 100644 --- a/pkg/transformer/kubernetes/podspec.go +++ b/pkg/transformer/kubernetes/podspec.go @@ -48,7 +48,8 @@ func AddContainer(service kobject.ServiceConfig, opt kobject.ConvertOptions) Pod Stdin: service.Stdin, TTY: service.Tty, }) - podSpec.NodeSelector = service.Placement + + podSpec.Affinity = ConfigAffinity(service) } } diff --git a/script/test/cmd/tests_new.sh b/script/test/cmd/tests_new.sh index 0aa3395d..db4d9aaf 100755 --- a/script/test/cmd/tests_new.sh +++ b/script/test/cmd/tests_new.sh @@ -115,3 +115,12 @@ k8s_output="$KOMPOSE_ROOT/script/test/fixtures/volume-mounts/windows/output-k8s. os_output="$KOMPOSE_ROOT/script/test/fixtures/volume-mounts/windows/output-os.json" convert::expect_success "$k8s_cmd" "$k8s_output" convert::expect_success "$os_cmd" "$os_output" + +# TEST the placement +# should convert placement to affinity +k8s_cmd="kompose -f $KOMPOSE_ROOT/script/test/fixtures/deploy/placement/docker-compose-placement.yaml convert --stdout -j --with-kompose-annotation=false" +os_cmd="kompose --provider=openshift -f $KOMPOSE_ROOT/script/test/fixtures/deploy/placement/docker-compose-placement.yaml convert --stdout -j --with-kompose-annotation=false" +k8s_output="$KOMPOSE_ROOT/script/test/fixtures/deploy/placement/output-placement-k8s.json" +os_output="$KOMPOSE_ROOT/script/test/fixtures/deploy/placement/output-placement-os.json" +convert::expect_success_and_warning "$k8s_cmd" "$k8s_output" +convert::expect_success_and_warning "$os_cmd" "$os_output" \ No newline at end of file diff --git a/script/test/fixtures/buildargs/output-os-template.json b/script/test/fixtures/buildargs/output-os-template.json index 0067fe82..95662b9a 100644 --- a/script/test/fixtures/buildargs/output-os-template.json +++ b/script/test/fixtures/buildargs/output-os-template.json @@ -145,7 +145,7 @@ }, "resources": {}, "postCommit": {}, - "nodeSelector": null + "affinity": null }, "status": { "lastVersion": 0 @@ -297,7 +297,7 @@ }, "resources": {}, "postCommit": {}, - "nodeSelector": null + "affinity": null }, "status": { "lastVersion": 0 diff --git a/script/test/fixtures/deploy/placement/docker-compose-placement.yaml b/script/test/fixtures/deploy/placement/docker-compose-placement.yaml new file mode 100644 index 00000000..4d1e6f8a --- /dev/null +++ b/script/test/fixtures/deploy/placement/docker-compose-placement.yaml @@ -0,0 +1,13 @@ +version: "3" +services: + redis: + image: redis + ports: + - "6379" + deploy: + placement: + constraints: + - node.hostname == machine + - engine.labels.operatingsystem == ubuntu 14.04 + - node.labels.foo != bar + - baz != qux \ No newline at end of file diff --git a/script/test/fixtures/deploy/placement/output-placement-k8s.json b/script/test/fixtures/deploy/placement/output-placement-k8s.json new file mode 100644 index 00000000..5cd34d5b --- /dev/null +++ b/script/test/fixtures/deploy/placement/output-placement-k8s.json @@ -0,0 +1,104 @@ +{ + "kind": "List", + "apiVersion": "v1", + "metadata": {}, + "items": [ + { + "kind": "Service", + "apiVersion": "v1", + "metadata": { + "name": "redis", + "creationTimestamp": null, + "labels": { + "io.kompose.service": "redis" + } + }, + "spec": { + "ports": [ + { + "name": "6379", + "port": 6379, + "targetPort": 6379 + } + ], + "selector": { + "io.kompose.service": "redis" + } + }, + "status": { + "loadBalancer": {} + } + }, + { + "kind": "Deployment", + "apiVersion": "apps/v1", + "metadata": { + "name": "redis", + "creationTimestamp": null, + "labels": { + "io.kompose.service": "redis" + } + }, + "spec": { + "replicas": 1, + "selector": { + "matchLabels": { + "io.kompose.service": "redis" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "io.kompose.service": "redis" + } + }, + "spec": { + "containers": [ + { + "name": "redis", + "image": "redis", + "ports": [ + { + "containerPort": 6379 + } + ], + "resources": {} + } + ], + "restartPolicy": "Always", + "affinity": { + "nodeAffinity": { + "requiredDuringSchedulingIgnoredDuringExecution": { + "nodeSelectorTerms": [ + { + "matchExpressions": [ + { + "key": "kubernetes.io/hostname", + "operator": "In", + "values": ["machine"] + }, + { + "key": "beta.kubernetes.io/os", + "operator": "In", + "values": ["ubuntu 14.04"] + }, + { + "key": "foo", + "operator": "NotIn", + "values": ["bar"] + } + ] + } + ] + } + } + } + } + }, + "strategy": {} + }, + "status": {} + } + ] +} \ No newline at end of file diff --git a/script/test/fixtures/deploy/placement/output-placement-os.json b/script/test/fixtures/deploy/placement/output-placement-os.json new file mode 100644 index 00000000..cce844bf --- /dev/null +++ b/script/test/fixtures/deploy/placement/output-placement-os.json @@ -0,0 +1,164 @@ +{ + "kind": "List", + "apiVersion": "v1", + "metadata": {}, + "items": [ + { + "kind": "Service", + "apiVersion": "v1", + "metadata": { + "name": "redis", + "creationTimestamp": null, + "labels": { + "io.kompose.service": "redis" + } + }, + "spec": { + "ports": [ + { + "name": "6379", + "port": 6379, + "targetPort": 6379 + } + ], + "selector": { + "io.kompose.service": "redis" + } + }, + "status": { + "loadBalancer": {} + } + }, + { + "kind": "DeploymentConfig", + "apiVersion": "v1", + "metadata": { + "name": "redis", + "creationTimestamp": null, + "labels": { + "io.kompose.service": "redis" + } + }, + "spec": { + "strategy": { + "resources": {} + }, + "triggers": [ + { + "type": "ConfigChange" + }, + { + "type": "ImageChange", + "imageChangeParams": { + "automatic": true, + "containerNames": [ + "redis" + ], + "from": { + "kind": "ImageStreamTag", + "name": "redis:latest" + } + } + } + ], + "replicas": 1, + "test": false, + "selector": { + "io.kompose.service": "redis" + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "io.kompose.service": "redis" + } + }, + "spec": { + "containers": [ + { + "name": "redis", + "image": " ", + "ports": [ + { + "containerPort": 6379 + } + ], + "resources": {} + } + ], + "restartPolicy": "Always", + "affinity": { + "nodeAffinity": { + "requiredDuringSchedulingIgnoredDuringExecution": { + "nodeSelectorTerms": [ + { + "matchExpressions": [ + { + "key": "kubernetes.io/hostname", + "operator": "In", + "values": ["machine"] + }, + { + "key": "beta.kubernetes.io/os", + "operator": "In", + "values": ["ubuntu 14.04"] + }, + { + "key": "foo", + "operator": "NotIn", + "values": ["bar"] + } + ] + } + ] + } + } + } + } + } + }, + "status": { + "latestVersion": 0, + "observedGeneration": 0, + "replicas": 0, + "updatedReplicas": 0, + "availableReplicas": 0, + "unavailableReplicas": 0 + } + }, + { + "kind": "ImageStream", + "apiVersion": "v1", + "metadata": { + "name": "redis", + "creationTimestamp": null, + "labels": { + "io.kompose.service": "redis" + } + }, + "spec": { + "lookupPolicy": { + "local": false + }, + "tags": [ + { + "name": "", + "annotations": null, + "from": { + "kind": "DockerImage", + "name": "redis" + }, + "generation": null, + "importPolicy": {}, + "referencePolicy": { + "type": "" + } + } + ] + }, + "status": { + "dockerImageRepository": "" + } + } + ] +} \ No newline at end of file diff --git a/script/test/fixtures/nginx-node-redis/output-os-template-v3.json b/script/test/fixtures/nginx-node-redis/output-os-template-v3.json index 6af0faeb..2ebe489d 100644 --- a/script/test/fixtures/nginx-node-redis/output-os-template-v3.json +++ b/script/test/fixtures/nginx-node-redis/output-os-template-v3.json @@ -289,7 +289,7 @@ }, "resources": {}, "postCommit": {}, - "nodeSelector": null + "affinity": null }, "status": { "lastVersion": 0 @@ -431,7 +431,7 @@ }, "resources": {}, "postCommit": {}, - "nodeSelector": null + "affinity": null }, "status": { "lastVersion": 0 @@ -573,7 +573,7 @@ }, "resources": {}, "postCommit": {}, - "nodeSelector": null + "affinity": null }, "status": { "lastVersion": 0 @@ -715,7 +715,7 @@ }, "resources": {}, "postCommit": {}, - "nodeSelector": null + "affinity": null }, "status": { "lastVersion": 0 diff --git a/script/test/fixtures/nginx-node-redis/output-os-template.json b/script/test/fixtures/nginx-node-redis/output-os-template.json index 6af0faeb..2ebe489d 100644 --- a/script/test/fixtures/nginx-node-redis/output-os-template.json +++ b/script/test/fixtures/nginx-node-redis/output-os-template.json @@ -289,7 +289,7 @@ }, "resources": {}, "postCommit": {}, - "nodeSelector": null + "affinity": null }, "status": { "lastVersion": 0 @@ -431,7 +431,7 @@ }, "resources": {}, "postCommit": {}, - "nodeSelector": null + "affinity": null }, "status": { "lastVersion": 0 @@ -573,7 +573,7 @@ }, "resources": {}, "postCommit": {}, - "nodeSelector": null + "affinity": null }, "status": { "lastVersion": 0 @@ -715,7 +715,7 @@ }, "resources": {}, "postCommit": {}, - "nodeSelector": null + "affinity": null }, "status": { "lastVersion": 0 diff --git a/script/test/fixtures/v3/docker-compose-deploy.yaml b/script/test/fixtures/v3/docker-compose-deploy.yaml index fa7ed37c..88f2864e 100644 --- a/script/test/fixtures/v3/docker-compose-deploy.yaml +++ b/script/test/fixtures/v3/docker-compose-deploy.yaml @@ -19,6 +19,7 @@ services: constraints: - node.hostname == machine - engine.labels.operatingsystem == ubuntu 14.04 + - node.labels.foo != bar foo: # test labels labels: kompose.service.type: headless diff --git a/script/test/fixtures/v3/output-deploy-k8s.json b/script/test/fixtures/v3/output-deploy-k8s.json index f9b17628..bce5c3d5 100644 --- a/script/test/fixtures/v3/output-deploy-k8s.json +++ b/script/test/fixtures/v3/output-deploy-k8s.json @@ -77,9 +77,32 @@ "resources": {} } ], - "nodeSelector": { - "beta.kubernetes.io/os": "ubuntu 14.04", - "kubernetes.io/hostname": "machine" + "affinity": { + "nodeAffinity": { + "requiredDuringSchedulingIgnoredDuringExecution": { + "nodeSelectorTerms": [ + { + "matchExpressions": [ + { + "key": "kubernetes.io/hostname", + "operator": "In", + "values": ["machine"] + }, + { + "key": "beta.kubernetes.io/os", + "operator": "In", + "values": ["ubuntu 14.04"] + }, + { + "key": "foo", + "operator": "NotIn", + "values": ["bar"] + } + ] + } + ] + } + } }, "restartPolicy": "Always", "serviceAccountName": "", @@ -182,8 +205,22 @@ "resources": {} } ], - "nodeSelector": { - "kubernetes.io/hostname": "machine" + "affinity": { + "nodeAffinity": { + "requiredDuringSchedulingIgnoredDuringExecution": { + "nodeSelectorTerms": [ + { + "matchExpressions": [ + { + "key": "kubernetes.io/hostname", + "operator": "In", + "values": ["machine"] + } + ] + } + ] + } + } }, "restartPolicy": "Always", "serviceAccountName": "", diff --git a/script/test/fixtures/v3/output-deploy-os.json b/script/test/fixtures/v3/output-deploy-os.json index 096b4d4d..e3d0efde 100644 --- a/script/test/fixtures/v3/output-deploy-os.json +++ b/script/test/fixtures/v3/output-deploy-os.json @@ -92,9 +92,32 @@ } ], "restartPolicy": "Always", - "nodeSelector": { - "beta.kubernetes.io/os": "ubuntu 14.04", - "kubernetes.io/hostname": "machine" + "affinity": { + "nodeAffinity": { + "requiredDuringSchedulingIgnoredDuringExecution": { + "nodeSelectorTerms": [ + { + "matchExpressions": [ + { + "key": "kubernetes.io/hostname", + "operator": "In", + "values": ["machine"] + }, + { + "key": "beta.kubernetes.io/os", + "operator": "In", + "values": ["ubuntu 14.04"] + }, + { + "key": "foo", + "operator": "NotIn", + "values": ["bar"] + } + ] + } + ] + } + } } } } @@ -334,8 +357,22 @@ } ], "restartPolicy": "Always", - "nodeSelector": { - "kubernetes.io/hostname": "machine" + "affinity": { + "nodeAffinity": { + "requiredDuringSchedulingIgnoredDuringExecution": { + "nodeSelectorTerms": [ + { + "matchExpressions": [ + { + "key": "kubernetes.io/hostname", + "operator": "In", + "values": ["machine"] + } + ] + } + ] + } + } } } }