From 438088f37da21b3d6d3dcdc73f1d0d8d47324c8f Mon Sep 17 00:00:00 2001 From: Charlie Drage Date: Thu, 9 Feb 2017 12:21:17 -0500 Subject: [PATCH] Add support for host:port:port This adds support for supplying for example: "127.0.0.1:80:80/tcp" to docker-compose.yaml and converting it to it's corresponding Kubernetes / OpenShift hostIP. This commit also refactors the loadPorts function of compose.go Closes https://github.com/kubernetes-incubator/kompose/issues/335 --- pkg/kobject/kobject.go | 1 + pkg/loader/compose/compose.go | 86 ++++++++--- pkg/loader/compose/compose_test.go | 50 ++++++ pkg/transformer/kubernetes/kubernetes.go | 4 + script/test/cmd/tests.sh | 5 + .../fixtures/ports-with-ip/docker-compose.yml | 19 +++ .../fixtures/ports-with-ip/output-k8s.json | 142 ++++++++++++++++++ 7 files changed, 287 insertions(+), 20 deletions(-) create mode 100644 script/test/fixtures/ports-with-ip/docker-compose.yml create mode 100644 script/test/fixtures/ports-with-ip/output-k8s.json diff --git a/pkg/kobject/kobject.go b/pkg/kobject/kobject.go index 26bebeee..4f5bed01 100644 --- a/pkg/kobject/kobject.go +++ b/pkg/kobject/kobject.go @@ -91,5 +91,6 @@ type EnvVar struct { type Ports struct { HostPort int32 ContainerPort int32 + HostIP string Protocol api.Protocol } diff --git a/pkg/loader/compose/compose.go b/pkg/loader/compose/compose.go index 39cb2c4e..29a4ed67 100644 --- a/pkg/loader/compose/compose.go +++ b/pkg/loader/compose/compose.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 +http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -18,6 +18,7 @@ package compose import ( "fmt" + "net" "os" "path/filepath" "reflect" @@ -180,41 +181,86 @@ func loadEnvVars(envars []string) []kobject.EnvVar { func loadPorts(composePorts []string) ([]kobject.Ports, error) { ports := []kobject.Ports{} character := ":" + + // For each port listed for _, port := range composePorts { + + // Get the TCP / UDP protocol. Checks to see if it splits in 2 with '/' character. + // ex. 15000:15000/tcp + // else, set a default protocol of using TCP proto := api.ProtocolTCP - // get protocol - p := strings.Split(port, "/") - if len(p) == 2 { - if strings.EqualFold("tcp", p[1]) { + protocolCheck := strings.Split(port, "/") + if len(protocolCheck) == 2 { + if strings.EqualFold("tcp", protocolCheck[1]) { proto = api.ProtocolTCP - } else if strings.EqualFold("udp", p[1]) { + } else if strings.EqualFold("udp", protocolCheck[1]) { proto = api.ProtocolUDP + } else { + return nil, fmt.Errorf("invalid protocol %q", protocolCheck[1]) } } - // port mappings without protocol part - portNoProto := p[0] - if strings.Contains(portNoProto, character) { - hostPort := portNoProto[0:strings.Index(portNoProto, character)] - hostPort = strings.TrimSpace(hostPort) - hostPortInt, err := strconv.Atoi(hostPort) - if err != nil { - return nil, fmt.Errorf("invalid host port %q", port) + + // Split up the ports / IP without the "/tcp" or "/udp" appended to it + justPorts := strings.Split(protocolCheck[0], character) + + if len(justPorts) == 3 { + // ex. 127.0.0.1:80:80 + + // Get the IP address + hostIP := justPorts[0] + ip := net.ParseIP(hostIP) + if ip.To4() == nil && ip.To16() == nil { + return nil, fmt.Errorf("%q contains an invalid IPv4 or IPv6 IP address", port) } - containerPort := portNoProto[strings.Index(portNoProto, character)+1:] - containerPort = strings.TrimSpace(containerPort) - containerPortInt, err := strconv.Atoi(containerPort) + + // Get the host port + hostPortInt, err := strconv.Atoi(justPorts[1]) if err != nil { - return nil, fmt.Errorf("invalid container port %q", port) + return nil, fmt.Errorf("invalid host port %q valid example: 127.0.0.1:80:80", port) } + + // Get the container port + containerPortInt, err := strconv.Atoi(justPorts[2]) + if err != nil { + return nil, fmt.Errorf("invalid container port %q valid example: 127.0.0.1:80:80", port) + } + + // Convert to a kobject struct with ports as well as IP + ports = append(ports, kobject.Ports{ + HostPort: int32(hostPortInt), + ContainerPort: int32(containerPortInt), + HostIP: hostIP, + Protocol: proto, + }) + + } else if len(justPorts) == 2 { + // ex. 80:80 + + // Get the host port + hostPortInt, err := strconv.Atoi(justPorts[0]) + if err != nil { + return nil, fmt.Errorf("invalid host port %q valid example: 80:80", port) + } + + // Get the container port + containerPortInt, err := strconv.Atoi(justPorts[1]) + if err != nil { + return nil, fmt.Errorf("invalid container port %q valid example: 80:80", port) + } + + // Convert to a kobject struct and add to the list of ports ports = append(ports, kobject.Ports{ HostPort: int32(hostPortInt), ContainerPort: int32(containerPortInt), Protocol: proto, }) + } else { - containerPortInt, err := strconv.Atoi(portNoProto) + // ex. 80 + + containerPortInt, err := strconv.Atoi(justPorts[0]) if err != nil { - return nil, fmt.Errorf("invalid container port %q", port) + return nil, fmt.Errorf("invalid container port %q valid example: 80", port) } ports = append(ports, kobject.Ports{ ContainerPort: int32(containerPortInt), diff --git a/pkg/loader/compose/compose_test.go b/pkg/loader/compose/compose_test.go index 3525bf9e..068001d2 100644 --- a/pkg/loader/compose/compose_test.go +++ b/pkg/loader/compose/compose_test.go @@ -23,6 +23,7 @@ import ( "testing" "github.com/kubernetes-incubator/kompose/pkg/kobject" + "k8s.io/kubernetes/pkg/api" "github.com/docker/libcompose/config" "github.com/docker/libcompose/project" @@ -53,6 +54,55 @@ func TestHandleServiceType(t *testing.T) { } } +// Test loading of ports +func TestLoadPorts(t *testing.T) { + port1 := []string{"127.0.0.1:80:80/tcp"} + result1 := kobject.Ports{ + HostIP: "127.0.0.1", + HostPort: 80, + ContainerPort: 80, + Protocol: api.ProtocolTCP, + } + port2 := []string{"80:80/tcp"} + result2 := kobject.Ports{ + HostPort: 80, + ContainerPort: 80, + Protocol: api.ProtocolTCP, + } + port3 := []string{"80:80"} + result3 := kobject.Ports{ + HostPort: 80, + ContainerPort: 80, + Protocol: api.ProtocolTCP, + } + port4 := []string{"80"} + result4 := kobject.Ports{ + HostPort: 0, + ContainerPort: 80, + Protocol: api.ProtocolTCP, + } + + tests := []struct { + ports []string + result kobject.Ports + }{ + {port1, result1}, + {port2, result2}, + {port3, result3}, + {port4, result4}, + } + + for _, tt := range tests { + result, err := loadPorts(tt.ports) + if err != nil { + t.Errorf("Unexpected error with loading ports %v", err) + } + if result[0] != tt.result { + t.Errorf("Expected %q, got %q", tt.result, result[0]) + } + } +} + func TestLoadEnvVar(t *testing.T) { ev1 := []string{"foo=bar"} rs1 := kobject.EnvVar{ diff --git a/pkg/transformer/kubernetes/kubernetes.go b/pkg/transformer/kubernetes/kubernetes.go index 78dac33c..04d63d22 100644 --- a/pkg/transformer/kubernetes/kubernetes.go +++ b/pkg/transformer/kubernetes/kubernetes.go @@ -285,13 +285,16 @@ func (k *Kubernetes) ConfigPorts(name string, service kobject.ServiceConfig) []a if port.Protocol == api.ProtocolTCP { ports = append(ports, api.ContainerPort{ ContainerPort: port.ContainerPort, + HostIP: port.HostIP, }) } else { ports = append(ports, api.ContainerPort{ ContainerPort: port.ContainerPort, Protocol: port.Protocol, + HostIP: port.HostIP, }) } + } return ports @@ -304,6 +307,7 @@ func (k *Kubernetes) ConfigServicePorts(name string, service kobject.ServiceConf if port.HostPort == 0 { port.HostPort = port.ContainerPort } + var targetPort intstr.IntOrString targetPort.IntVal = port.ContainerPort targetPort.StrVal = strconv.Itoa(int(port.ContainerPort)) diff --git a/script/test/cmd/tests.sh b/script/test/cmd/tests.sh index 714075f8..1ff7dcfe 100755 --- a/script/test/cmd/tests.sh +++ b/script/test/cmd/tests.sh @@ -123,6 +123,11 @@ export $(cat $KOMPOSE_ROOT/script/test/fixtures/keyonly-envs/envs) convert::expect_success "kompose --file $KOMPOSE_ROOT/script/test/fixtures/keyonly-envs/env.yml convert --stdout -j" "$KOMPOSE_ROOT/script/test/fixtures/keyonly-envs/output-k8s.json" unset $(cat $KOMPOSE_ROOT/script/test/fixtures/keyonly-envs/envs | cut -d'=' -f1) +##### +# Test related to host:port:container in docker-compose +# kubernetes test +convert::expect_success "kompose -f $KOMPOSE_ROOT/script/test/fixtures/ports-with-ip/docker-compose.yml convert --stdout -j" "$KOMPOSE_ROOT/script/test/fixtures/ports-with-ip/output-k8s.json" + ###### # Test related to "stdin_open: true" in docker-compose diff --git a/script/test/fixtures/ports-with-ip/docker-compose.yml b/script/test/fixtures/ports-with-ip/docker-compose.yml new file mode 100644 index 00000000..e73e55bf --- /dev/null +++ b/script/test/fixtures/ports-with-ip/docker-compose.yml @@ -0,0 +1,19 @@ +version: "2" + +services: + web: + image: tuna/docker-counter23 + ports: + - "127.0.0.1:5000:5000/tcp" + links: + - redis + networks: + - default + + redis: + image: redis:3.0 + networks: + - default + ports: + - "6379/tcp" + - "1234:1235/udp" diff --git a/script/test/fixtures/ports-with-ip/output-k8s.json b/script/test/fixtures/ports-with-ip/output-k8s.json new file mode 100644 index 00000000..5fe006fb --- /dev/null +++ b/script/test/fixtures/ports-with-ip/output-k8s.json @@ -0,0 +1,142 @@ +{ + "kind": "List", + "apiVersion": "v1", + "metadata": {}, + "items": [ + { + "kind": "Service", + "apiVersion": "v1", + "metadata": { + "name": "redis", + "creationTimestamp": null, + "labels": { + "service": "redis" + } + }, + "spec": { + "ports": [ + { + "name": "6379", + "port": 6379, + "targetPort": 6379 + }, + { + "name": "1234", + "protocol": "UDP", + "port": 1234, + "targetPort": 1235 + } + ], + "selector": { + "service": "redis" + } + }, + "status": { + "loadBalancer": {} + } + }, + { + "kind": "Service", + "apiVersion": "v1", + "metadata": { + "name": "web", + "creationTimestamp": null, + "labels": { + "service": "web" + } + }, + "spec": { + "ports": [ + { + "name": "5000", + "port": 5000, + "targetPort": 5000 + } + ], + "selector": { + "service": "web" + } + }, + "status": { + "loadBalancer": {} + } + }, + { + "kind": "Deployment", + "apiVersion": "extensions/v1beta1", + "metadata": { + "name": "redis", + "creationTimestamp": null + }, + "spec": { + "replicas": 1, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "service": "redis" + } + }, + "spec": { + "containers": [ + { + "name": "redis", + "image": "redis:3.0", + "ports": [ + { + "containerPort": 6379 + }, + { + "containerPort": 1235, + "protocol": "UDP" + } + ], + "resources": {} + } + ], + "restartPolicy": "Always" + } + }, + "strategy": {} + }, + "status": {} + }, + { + "kind": "Deployment", + "apiVersion": "extensions/v1beta1", + "metadata": { + "name": "web", + "creationTimestamp": null + }, + "spec": { + "replicas": 1, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "service": "web" + } + }, + "spec": { + "containers": [ + { + "name": "web", + "image": "tuna/docker-counter23", + "ports": [ + { + "containerPort": 5000, + "hostIP": "127.0.0.1" + } + ], + "resources": {} + } + ], + "restartPolicy": "Always" + } + }, + "strategy": {} + }, + "status": {} + } + ] +}