diff --git a/docs/conversion.md b/docs/conversion.md index 6cf9d489..eb18bfe2 100644 --- a/docs/conversion.md +++ b/docs/conversion.md @@ -49,7 +49,7 @@ __Glossary:__ | external_links | X | X | X | | Kubernetes uses a flat-structure for all containers and thus external_links does not have a 1-1 conversion | | extra_hosts | N | N | N | | | | group_add | ✓ | ✓ | ✓ | | | -| healthcheck | - | N | N | | | +| healthcheck | - | N | ✓ | | | | image | ✓ | ✓ | ✓ | Deployment.Spec.Containers.Image | | | isolation | X | X | X | | Not applicable as this applies to Windows with HyperV support | | labels | ✓ | ✓ | ✓ | Metadata.Annotations | | diff --git a/pkg/kobject/kobject.go b/pkg/kobject/kobject.go index cd83684e..5a6ecdde 100644 --- a/pkg/kobject/kobject.go +++ b/pkg/kobject/kobject.go @@ -99,8 +99,20 @@ type ServiceConfig struct { Dockerfile string `compose:"dockerfile"` Replicas int `compose:"replicas"` GroupAdd []int64 `compose:"group_add"` - // Volumes is a struct which contains all information about each volume - Volumes []Volumes `compose:""` + Volumes []Volumes `compose:""` + HealthChecks HealthCheck `compose:""` +} + +// HealthCheck the healthcheck configuration for a service +// "StartPeriod" is not yet added to compose, see: +// https://github.com/docker/cli/issues/116 +type HealthCheck struct { + Test []string + Timeout int32 + Interval int32 + Retries int32 + StartPeriod int32 + Disable bool } // EnvVar holds the environment variable struct of a container diff --git a/pkg/loader/compose/compose_test.go b/pkg/loader/compose/compose_test.go index 0666a6db..e7f1c666 100644 --- a/pkg/loader/compose/compose_test.go +++ b/pkg/loader/compose/compose_test.go @@ -32,6 +32,34 @@ import ( "github.com/pkg/errors" ) +func TestParseHealthCheck(t *testing.T) { + helperValue := uint64(2) + check := types.HealthCheckConfig{ + Test: []string{"CMD-SHELL", "echo", "foobar"}, + Timeout: "1s", + Interval: "2s", + Retries: &helperValue, + StartPeriod: "3s", + } + + // CMD-SHELL or SHELL is included Test within docker/cli, thus we remove the first value in Test + expected := kobject.HealthCheck{ + Test: []string{"echo", "foobar"}, + Timeout: 1, + Interval: 2, + Retries: 2, + StartPeriod: 3, + } + output, err := parseHealthCheck(check) + if err != nil { + t.Errorf("Unable to convert HealthCheckConfig: %s", err) + } + + if !reflect.DeepEqual(output, expected) { + t.Errorf("Structs are not equal, expected: %s, output: %s", expected, output) + } +} + func TestLoadV3Volumes(t *testing.T) { vol := types.ServiceVolumeConfig{ Type: "volume", @@ -66,6 +94,7 @@ func TestLoadV3Ports(t *testing.T) { if output[0] != expected { t.Errorf("Expected %v, got %v", expected, output[0]) } + } // Test if service types are parsed properly on user input diff --git a/pkg/loader/compose/v3.go b/pkg/loader/compose/v3.go index 624aebc7..05fd1baa 100644 --- a/pkg/loader/compose/v3.go +++ b/pkg/loader/compose/v3.go @@ -20,6 +20,7 @@ import ( "io/ioutil" "strconv" "strings" + "time" libcomposeyaml "github.com/docker/libcompose/yaml" @@ -162,6 +163,52 @@ func loadV3Ports(ports []types.ServicePortConfig) []kobject.Ports { return komposePorts } +/* Convert the HealthCheckConfig as designed by Docker to +a Kubernetes-compatible format. +*/ +func parseHealthCheck(composeHealthCheck types.HealthCheckConfig) (kobject.HealthCheck, error) { + + var timeout, interval, retries, startPeriod int32 + + // Here we convert the timeout from 1h30s (example) to 36030 seconds. + if composeHealthCheck.Timeout != "" { + parse, err := time.ParseDuration(composeHealthCheck.Timeout) + if err != nil { + return kobject.HealthCheck{}, errors.Wrap(err, "unable to parse health check timeout variable") + } + timeout = int32(parse.Seconds()) + } + + if composeHealthCheck.Interval != "" { + parse, err := time.ParseDuration(composeHealthCheck.Interval) + if err != nil { + return kobject.HealthCheck{}, errors.Wrap(err, "unable to parse health check interval variable") + } + interval = int32(parse.Seconds()) + } + + if *composeHealthCheck.Retries != 0 { + retries = int32(*composeHealthCheck.Retries) + } + + if composeHealthCheck.StartPeriod != "" { + parse, err := time.ParseDuration(composeHealthCheck.StartPeriod) + if err != nil { + return kobject.HealthCheck{}, errors.Wrap(err, "unable to parse health check startPeriod variable") + } + startPeriod = int32(parse.Seconds()) + } + + // Due to docker/cli adding "CMD-SHELL" to the struct, we remove the first element of composeHealthCheck.Test + return kobject.HealthCheck{ + Test: composeHealthCheck.Test[1:], + Timeout: timeout, + Interval: interval, + Retries: retries, + StartPeriod: startPeriod, + }, nil +} + func dockerComposeToKomposeMapping(composeObject *types.Config) (kobject.KomposeObject, error) { // Step 1. Initialize what's going to be returned @@ -198,8 +245,18 @@ func dockerComposeToKomposeMapping(composeObject *types.Config) (kobject.Kompose // Deploy keys // + // mode: serviceConfig.DeployMode = composeServiceConfig.Deploy.Mode + // HealthCheck + if composeServiceConfig.HealthCheck != nil && !composeServiceConfig.HealthCheck.Disable { + var err error + serviceConfig.HealthChecks, err = parseHealthCheck(*composeServiceConfig.HealthCheck) + if err != nil { + return kobject.KomposeObject{}, errors.Wrap(err, "Unable to parse health check") + } + } + if (composeServiceConfig.Deploy.Resources != types.Resources{}) { // memory: diff --git a/pkg/transformer/kubernetes/k8sutils.go b/pkg/transformer/kubernetes/k8sutils.go index f9a1648c..00a31bb2 100644 --- a/pkg/transformer/kubernetes/k8sutils.go +++ b/pkg/transformer/kubernetes/k8sutils.go @@ -381,6 +381,33 @@ func (k *Kubernetes) UpdateKubernetesObjects(name string, service kobject.Servic template.Spec.Containers[0].TTY = service.Tty template.Spec.Volumes = volumes + // Configure the HealthCheck + // We check to see if it's blank + if !reflect.DeepEqual(service.HealthChecks, kobject.HealthCheck{}) { + probe := api.Probe{} + + if len(service.HealthChecks.Test) > 0 { + probe.Handler = api.Handler{ + Exec: &api.ExecAction{ + Command: service.HealthChecks.Test, + }, + } + } else { + return errors.New("Health check must contain a command") + } + + probe.TimeoutSeconds = service.HealthChecks.Timeout + probe.PeriodSeconds = service.HealthChecks.Interval + probe.FailureThreshold = service.HealthChecks.Retries + + // See issue: https://github.com/docker/cli/issues/116 + // StartPeriod has been added to docker/cli however, it is not yet added + // to compose. Once the feature has been implemented, this will automatically work + probe.InitialDelaySeconds = service.HealthChecks.StartPeriod + + template.Spec.Containers[0].LivenessProbe = &probe + } + if service.StopGracePeriod != "" { template.Spec.TerminationGracePeriodSeconds, err = DurationStrToSecondsInt(service.StopGracePeriod) if err != nil { diff --git a/script/test/cmd/tests.sh b/script/test/cmd/tests.sh index f9286856..16d5bf34 100755 --- a/script/test/cmd/tests.sh +++ b/script/test/cmd/tests.sh @@ -392,6 +392,15 @@ convert::expect_success "kompose --provider=openshift convert --stdout -j" "/tmp # Return back to the original path cd $CURRENT_DIR +# Test HealthCheck +cmd="kompose convert --stdout -j -f $KOMPOSE_ROOT/script/test/fixtures/healthcheck/docker-compose.yaml" +sed -e "s;%VERSION%;$version;g" -e "s;%CMD%;$cmd;g" "$KOMPOSE_ROOT/script/test/fixtures/healthcheck/output-k8s-template.json" > /tmp/output-k8s.json +convert::expect_success "kompose convert --stdout -j -f $KOMPOSE_ROOT/script/test/fixtures/healthcheck/docker-compose.yaml" "/tmp/output-k8s.json" + +cmd="kompose convert --stdout -j --provider=openshift -f $KOMPOSE_ROOT/script/test/fixtures/healthcheck/docker-compose.yaml" +sed -e "s;%VERSION%;$version;g" -e "s;%CMD%;$cmd;g" "$KOMPOSE_ROOT/script/test/fixtures/healthcheck/output-os-template.json" > /tmp/output-os.json +convert::expect_success "kompose convert --stdout -j --provider=openshift -f $KOMPOSE_ROOT/script/test/fixtures/healthcheck/docker-compose.yaml" "/tmp/output-os.json" + # Test V3 Support of Docker Compose # Test deploy mode: global diff --git a/script/test/fixtures/healthcheck/docker-compose.yaml b/script/test/fixtures/healthcheck/docker-compose.yaml new file mode 100644 index 00000000..6ce92c4a --- /dev/null +++ b/script/test/fixtures/healthcheck/docker-compose.yaml @@ -0,0 +1,10 @@ +version: "3" + +services: + redis: + image: redis + healthcheck: + test: echo "hello world" + interval: 10s + timeout: 1s + retries: 5 diff --git a/script/test/fixtures/healthcheck/output-k8s-template.json b/script/test/fixtures/healthcheck/output-k8s-template.json new file mode 100644 index 00000000..ee4965b9 --- /dev/null +++ b/script/test/fixtures/healthcheck/output-k8s-template.json @@ -0,0 +1,86 @@ +{ + "kind": "List", + "apiVersion": "v1", + "metadata": {}, + "items": [ + { + "kind": "Service", + "apiVersion": "v1", + "metadata": { + "name": "redis", + "creationTimestamp": null, + "labels": { + "io.kompose.service": "redis" + }, + "annotations": { + "kompose.cmd": "%CMD%", + "kompose.version": "%VERSION%" + } + }, + "spec": { + "ports": [ + { + "name": "headless", + "port": 55555, + "targetPort": 0 + } + ], + "selector": { + "io.kompose.service": "redis" + }, + "clusterIP": "None" + }, + "status": { + "loadBalancer": {} + } + }, + { + "kind": "Deployment", + "apiVersion": "extensions/v1beta1", + "metadata": { + "name": "redis", + "creationTimestamp": null, + "labels": { + "io.kompose.service": "redis" + }, + "annotations": { + "kompose.cmd": "%CMD%", + "kompose.version": "%VERSION%" + } + }, + "spec": { + "replicas": 1, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "io.kompose.service": "redis" + } + }, + "spec": { + "containers": [ + { + "name": "redis", + "image": "redis", + "resources": {}, + "livenessProbe": { + "exec": { + "command": [ + "echo \"hello world\"" + ] + }, + "timeoutSeconds": 1, + "periodSeconds": 10, + "failureThreshold": 5 + } + } + ], + "restartPolicy": "Always" + } + }, + "strategy": {} + }, + "status": {} + } + ] +} diff --git a/script/test/fixtures/healthcheck/output-os-template.json b/script/test/fixtures/healthcheck/output-os-template.json new file mode 100644 index 00000000..bfe21eb1 --- /dev/null +++ b/script/test/fixtures/healthcheck/output-os-template.json @@ -0,0 +1,138 @@ +{ + "kind": "List", + "apiVersion": "v1", + "metadata": {}, + "items": [ + { + "kind": "Service", + "apiVersion": "v1", + "metadata": { + "name": "redis", + "creationTimestamp": null, + "labels": { + "io.kompose.service": "redis" + }, + "annotations": { + "kompose.cmd": "%CMD%", + "kompose.version": "%VERSION%" + } + }, + "spec": { + "ports": [ + { + "name": "headless", + "port": 55555, + "targetPort": 0 + } + ], + "selector": { + "io.kompose.service": "redis" + }, + "clusterIP": "None" + }, + "status": { + "loadBalancer": {} + } + }, + { + "kind": "DeploymentConfig", + "apiVersion": "v1", + "metadata": { + "name": "redis", + "creationTimestamp": null, + "labels": { + "io.kompose.service": "redis" + }, + "annotations": { + "kompose.cmd": "%CMD%", + "kompose.version": "%VERSION%" + } + }, + "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": " ", + "resources": {}, + "livenessProbe": { + "exec": { + "command": [ + "echo \"hello world\"" + ] + }, + "timeoutSeconds": 1, + "periodSeconds": 10, + "failureThreshold": 5 + } + } + ], + "restartPolicy": "Always" + } + } + }, + "status": {} + }, + { + "kind": "ImageStream", + "apiVersion": "v1", + "metadata": { + "name": "redis", + "creationTimestamp": null, + "labels": { + "io.kompose.service": "redis" + } + }, + "spec": { + "tags": [ + { + "name": "latest", + "annotations": null, + "from": { + "kind": "DockerImage", + "name": "redis" + }, + "generation": null, + "importPolicy": {} + } + ] + }, + "status": { + "dockerImageRepository": "" + } + } + ] +} diff --git a/script/test/fixtures/v3/output-k8s-full-example.json b/script/test/fixtures/v3/output-k8s-full-example.json index 9e1f6f0e..a7dc8208 100644 --- a/script/test/fixtures/v3/output-k8s-full-example.json +++ b/script/test/fixtures/v3/output-k8s-full-example.json @@ -197,6 +197,16 @@ "mountPath": "/tmp" } ], + "livenessProbe": { + "exec": { + "command": [ + "echo \"hello world\"" + ] + }, + "timeoutSeconds": 1, + "periodSeconds": 10, + "failureThreshold": 5 + }, "securityContext": { "capabilities": { "add": [ diff --git a/script/test/fixtures/v3/output-os-full-example.json b/script/test/fixtures/v3/output-os-full-example.json index 9e1f6f0e..a7dc8208 100644 --- a/script/test/fixtures/v3/output-os-full-example.json +++ b/script/test/fixtures/v3/output-os-full-example.json @@ -197,6 +197,16 @@ "mountPath": "/tmp" } ], + "livenessProbe": { + "exec": { + "command": [ + "echo \"hello world\"" + ] + }, + "timeoutSeconds": 1, + "periodSeconds": 10, + "failureThreshold": 5 + }, "securityContext": { "capabilities": { "add": [