diff --git a/go.mod b/go.mod index 5528e132..3f308fea 100644 --- a/go.mod +++ b/go.mod @@ -19,9 +19,11 @@ replace golang.org/x/sys => golang.org/x/sys v0.0.0-20201029080932-201ba4db2418 require ( github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect github.com/docker/cli v0.0.0-20190711175710-5b38d82aa076 + github.com/docker/go-connections v0.4.0 github.com/docker/libcompose v0.4.0 github.com/fatih/structs v1.1.0 github.com/fsouza/go-dockerclient v1.6.5 + github.com/google/go-cmp v0.4.0 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/gotestyourself/gotestyourself v2.2.0+incompatible // indirect github.com/imdario/mergo v0.3.10 // indirect diff --git a/pkg/loader/compose/compose_test.go b/pkg/loader/compose/compose_test.go index 3fb878e5..7b827eca 100644 --- a/pkg/loader/compose/compose_test.go +++ b/pkg/loader/compose/compose_test.go @@ -17,20 +17,21 @@ limitations under the License. package compose import ( + "fmt" "os" "reflect" "strings" "testing" - - "github.com/kubernetes/kompose/pkg/kobject" - api "k8s.io/api/core/v1" "time" "github.com/docker/cli/cli/compose/types" "github.com/docker/libcompose/config" "github.com/docker/libcompose/project" "github.com/docker/libcompose/yaml" + "github.com/google/go-cmp/cmp" + "github.com/kubernetes/kompose/pkg/kobject" "github.com/pkg/errors" + api "k8s.io/api/core/v1" ) func durationPtr(value time.Duration) *time.Duration { @@ -147,50 +148,95 @@ 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 + expose []string + want []kobject.Ports }{ - {port1, result1}, - {port2, result2}, - {port3, result3}, - {port4, result4}, + { + ports: []string{"127.0.0.1:80:80/tcp"}, + want: []kobject.Ports{ + {HostIP: "127.0.0.1", HostPort: 80, ContainerPort: 80, Protocol: api.ProtocolTCP}, + }, + }, + { + ports: []string{"80:80/tcp"}, + want: []kobject.Ports{ + {HostPort: 80, ContainerPort: 80, Protocol: api.ProtocolTCP}, + }, + }, + { + ports: []string{"80:80"}, + want: []kobject.Ports{ + {HostPort: 80, ContainerPort: 80, Protocol: api.ProtocolTCP}, + }, + }, + { + ports: []string{"80"}, + want: []kobject.Ports{ + {ContainerPort: 80, Protocol: api.ProtocolTCP}, + }, + }, + { + ports: []string{"3000-3005"}, + want: []kobject.Ports{ + {ContainerPort: 3000, Protocol: api.ProtocolTCP}, + {ContainerPort: 3001, Protocol: api.ProtocolTCP}, + {ContainerPort: 3002, Protocol: api.ProtocolTCP}, + {ContainerPort: 3003, Protocol: api.ProtocolTCP}, + {ContainerPort: 3004, Protocol: api.ProtocolTCP}, + {ContainerPort: 3005, Protocol: api.ProtocolTCP}, + }, + }, + { + ports: []string{"3000-3005:5000-5005"}, + want: []kobject.Ports{ + {HostPort: 3000, ContainerPort: 5000, Protocol: api.ProtocolTCP}, + {HostPort: 3001, ContainerPort: 5001, Protocol: api.ProtocolTCP}, + {HostPort: 3002, ContainerPort: 5002, Protocol: api.ProtocolTCP}, + {HostPort: 3003, ContainerPort: 5003, Protocol: api.ProtocolTCP}, + {HostPort: 3004, ContainerPort: 5004, Protocol: api.ProtocolTCP}, + {HostPort: 3005, ContainerPort: 5005, Protocol: api.ProtocolTCP}, + }, + }, + { + ports: []string{"127.0.0.1:3000-3005:5000-5005"}, + want: []kobject.Ports{ + {HostIP: "127.0.0.1", HostPort: 3000, ContainerPort: 5000, Protocol: api.ProtocolTCP}, + {HostIP: "127.0.0.1", HostPort: 3001, ContainerPort: 5001, Protocol: api.ProtocolTCP}, + {HostIP: "127.0.0.1", HostPort: 3002, ContainerPort: 5002, Protocol: api.ProtocolTCP}, + {HostIP: "127.0.0.1", HostPort: 3003, ContainerPort: 5003, Protocol: api.ProtocolTCP}, + {HostIP: "127.0.0.1", HostPort: 3004, ContainerPort: 5004, Protocol: api.ProtocolTCP}, + {HostIP: "127.0.0.1", HostPort: 3005, ContainerPort: 5005, Protocol: api.ProtocolTCP}, + }, + }, + { + ports: []string{"80", "3000"}, + want: []kobject.Ports{ + {HostPort: 0, ContainerPort: 80, Protocol: api.ProtocolTCP}, + {HostPort: 0, ContainerPort: 3000, Protocol: api.ProtocolTCP}, + }, + }, + { + ports: []string{"80", "3000"}, + expose: []string{"80"}, + want: []kobject.Ports{ + {HostPort: 0, ContainerPort: 80, Protocol: api.ProtocolTCP}, + {HostPort: 0, ContainerPort: 3000, Protocol: api.ProtocolTCP}, + }, + }, } for _, tt := range tests { - result, err := loadPorts(tt.ports, nil) - 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]) - } + t.Run(fmt.Sprintf("port=%q,expose=%q", tt.ports, tt.expose), func(t *testing.T) { + got, err := loadPorts(tt.ports, nil) + if err != nil { + t.Fatalf("Unexpected error with loading ports %v", err) + } + if diff := cmp.Diff(tt.want, got); diff != "" { + t.Errorf("loadPorts() mismatch (-want +got):\n%s", diff) + } + }) } } diff --git a/pkg/loader/compose/v1v2.go b/pkg/loader/compose/v1v2.go index a88232ba..a035a4ec 100644 --- a/pkg/loader/compose/v1v2.go +++ b/pkg/loader/compose/v1v2.go @@ -18,13 +18,15 @@ package compose import ( "fmt" - "github.com/spf13/cast" "net" "os" "path/filepath" + "sort" "strconv" "strings" + "github.com/docker/cli/opts" + "github.com/docker/go-connections/nat" "github.com/docker/libcompose/config" "github.com/docker/libcompose/lookup" "github.com/docker/libcompose/project" @@ -32,7 +34,7 @@ import ( "github.com/kubernetes/kompose/pkg/transformer" "github.com/pkg/errors" log "github.com/sirupsen/logrus" - + "github.com/spf13/cast" api "k8s.io/api/core/v1" ) @@ -87,102 +89,60 @@ func parseV1V2(files []string) (kobject.KomposeObject, error) { // Load ports from compose file // also load `expose` here func loadPorts(composePorts []string, expose []string) ([]kobject.Ports, error) { - ports := []kobject.Ports{} - character := ":" + kp := []kobject.Ports{} exist := map[string]bool{} - // For each port listed - for _, port := range composePorts { + for _, cp := range composePorts { + var hostIP string - // 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 - protocolCheck := strings.Split(port, "/") - if len(protocolCheck) == 2 { - if strings.EqualFold("tcp", protocolCheck[1]) { - proto = api.ProtocolTCP - } else if strings.EqualFold("udp", protocolCheck[1]) { - proto = api.ProtocolUDP - } else { - return nil, fmt.Errorf("invalid protocol %q", protocolCheck[1]) + if parts := strings.Split(cp, ":"); len(parts) == 3 { + if ip := net.ParseIP(parts[0]); ip.To4() == nil && ip.To16() == nil { + return nil, fmt.Errorf("%q contains an invalid IPv4 or IPv6 IP address", parts[0]) + } + hostIP = parts[0] + } + + np, pbs, err := nat.ParsePortSpecs([]string{cp}) + if err != nil { + return nil, fmt.Errorf("invalid port, error = %v", err) + } + // Force HostIP value to avoid warning raised by github.com/docker/cli/opts + // The opts package will warn if the bindings contains host IP except + // 0.0.0.0. However, the message is not useful in this case since the value + // should be handled by kompose properly. + for _, pb := range pbs { + for i, p := range pb { + p.HostIP = "" + pb[i] = p } } - // 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) - } - - // Get the host port - hostPortInt, err := strconv.Atoi(justPorts[1]) - if err != nil { - 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 { - // ex. 80 - - containerPortInt, err := strconv.Atoi(justPorts[0]) - if err != nil { - return nil, fmt.Errorf("invalid container port %q valid example: 80", port) - } - ports = append(ports, kobject.Ports{ - ContainerPort: int32(containerPortInt), - Protocol: proto, - }) + var ports []string + for p := range np { + ports = append(ports, string(p)) } + sort.Strings(ports) + for _, p := range ports { + pc, err := opts.ConvertPortToPortConfig(nat.Port(p), pbs) + if err != nil { + return nil, fmt.Errorf("invalid port, error = %v", err) + } + for _, cfg := range pc { + kp = append(kp, kobject.Ports{ + HostPort: int32(cfg.PublishedPort), + ContainerPort: int32(cfg.TargetPort), + HostIP: hostIP, + Protocol: api.Protocol(strings.ToUpper(string(cfg.Protocol))), + }) + } + } } // load remain expose ports - for _, port := range ports { + for _, p := range kp { // must use cast... - exist[cast.ToString(port.ContainerPort)+string(port.Protocol)] = true + exist[cast.ToString(p.ContainerPort)+string(p.Protocol)] = true } if expose != nil { @@ -196,7 +156,7 @@ func loadPorts(composePorts []string, expose []string) ([]kobject.Ports, error) } if !exist[portValue+string(protocol)] { - ports = append(ports, kobject.Ports{ + kp = append(kp, kobject.Ports{ ContainerPort: cast.ToInt32(portValue), Protocol: protocol, }) @@ -204,7 +164,7 @@ func loadPorts(composePorts []string, expose []string) ([]kobject.Ports, error) } } - return ports, nil + return kp, nil } // Uses libcompose's APIProject type and converts it to a Kompose object for us to understand diff --git a/pkg/loader/compose/v3.go b/pkg/loader/compose/v3.go index 86b3159b..cc47a3f6 100755 --- a/pkg/loader/compose/v3.go +++ b/pkg/loader/compose/v3.go @@ -189,7 +189,6 @@ func loadV3Ports(ports []types.ServicePortConfig, expose []string) []kobject.Por exist := map[string]bool{} for _, port := range ports { - // Convert to a kobject struct with ports // NOTE: V3 doesn't use IP (they utilize Swarm instead for host-networking). // Thus, IP is blank. diff --git a/script/test/fixtures/v2/docker-compose.yaml b/script/test/fixtures/v2/docker-compose.yaml index 99abfb9c..1c129e26 100644 --- a/script/test/fixtures/v2/docker-compose.yaml +++ b/script/test/fixtures/v2/docker-compose.yaml @@ -8,9 +8,15 @@ services: environment: GITHUB: surajssd ports: - - "2345" - "6379/tcp" - "6379/udp" + - "3000" + - "3000-3005" + - "8000:8000" + - "9090-9091:8080-8081" + - "49100:22" + - "127.0.0.1:8001:8001" + - "127.0.0.1:5000-5010:5000-5010" mem_limit: 10000 group_add: diff --git a/script/test/fixtures/v2/output-k8s.json b/script/test/fixtures/v2/output-k8s.json index c7234d4c..7bac4a29 100644 --- a/script/test/fixtures/v2/output-k8s.json +++ b/script/test/fixtures/v2/output-k8s.json @@ -15,11 +15,6 @@ }, "spec": { "ports": [ - { - "name": "2345", - "port": 2345, - "targetPort": 2345 - }, { "name": "6379", "port": 6379, @@ -30,6 +25,121 @@ "protocol": "UDP", "port": 6379, "targetPort": 6379 + }, + { + "name": "3000", + "port": 3000, + "targetPort": 3000 + }, + { + "name": "3000-tcp", + "port": 3000, + "targetPort": 3000 + }, + { + "name": "3001", + "port": 3001, + "targetPort": 3001 + }, + { + "name": "3002", + "port": 3002, + "targetPort": 3002 + }, + { + "name": "3003", + "port": 3003, + "targetPort": 3003 + }, + { + "name": "3004", + "port": 3004, + "targetPort": 3004 + }, + { + "name": "3005", + "port": 3005, + "targetPort": 3005 + }, + { + "name": "8000", + "port": 8000, + "targetPort": 8000 + }, + { + "name": "9090", + "port": 9090, + "targetPort": 8080 + }, + { + "name": "9091", + "port": 9091, + "targetPort": 8081 + }, + { + "name": "49100", + "port": 49100, + "targetPort": 22 + }, + { + "name": "8001", + "port": 8001, + "targetPort": 8001 + }, + { + "name": "5000", + "port": 5000, + "targetPort": 5000 + }, + { + "name": "5001", + "port": 5001, + "targetPort": 5001 + }, + { + "name": "5002", + "port": 5002, + "targetPort": 5002 + }, + { + "name": "5003", + "port": 5003, + "targetPort": 5003 + }, + { + "name": "5004", + "port": 5004, + "targetPort": 5004 + }, + { + "name": "5005", + "port": 5005, + "targetPort": 5005 + }, + { + "name": "5006", + "port": 5006, + "targetPort": 5006 + }, + { + "name": "5007", + "port": 5007, + "targetPort": 5007 + }, + { + "name": "5008", + "port": 5008, + "targetPort": 5008 + }, + { + "name": "5009", + "port": 5009, + "targetPort": 5009 + }, + { + "name": "5010", + "port": 5010, + "targetPort": 5010 } ], "selector": { @@ -117,15 +227,90 @@ "name": "foo", "image": "foobar", "ports": [ - { - "containerPort": 2345 - }, { "containerPort": 6379 }, { "containerPort": 6379, "protocol": "UDP" + }, + { + "containerPort": 3000 + }, + { + "containerPort": 3001 + }, + { + "containerPort": 3002 + }, + { + "containerPort": 3003 + }, + { + "containerPort": 3004 + }, + { + "containerPort": 3005 + }, + { + "containerPort": 8000 + }, + { + "containerPort": 8080 + }, + { + "containerPort": 8081 + }, + { + "containerPort": 22 + }, + { + "containerPort": 8001, + "hostIP": "127.0.0.1" + }, + { + "containerPort": 5000, + "hostIP": "127.0.0.1" + }, + { + "containerPort": 5001, + "hostIP": "127.0.0.1" + }, + { + "containerPort": 5002, + "hostIP": "127.0.0.1" + }, + { + "containerPort": 5003, + "hostIP": "127.0.0.1" + }, + { + "containerPort": 5004, + "hostIP": "127.0.0.1" + }, + { + "containerPort": 5005, + "hostIP": "127.0.0.1" + }, + { + "containerPort": 5006, + "hostIP": "127.0.0.1" + }, + { + "containerPort": 5007, + "hostIP": "127.0.0.1" + }, + { + "containerPort": 5008, + "hostIP": "127.0.0.1" + }, + { + "containerPort": 5009, + "hostIP": "127.0.0.1" + }, + { + "containerPort": 5010, + "hostIP": "127.0.0.1" } ], "env": [ @@ -158,6 +343,9 @@ "creationTimestamp": null, "labels": { "io.kompose.service": "redis" + }, + "annotations": { + "kompose.service.type": "loadbalancer" } }, "spec": { @@ -172,6 +360,9 @@ "creationTimestamp": null, "labels": { "io.kompose.service": "redis" + }, + "annotations": { + "kompose.service.type": "loadbalancer" } }, "spec": { diff --git a/script/test/fixtures/v2/output-os.json b/script/test/fixtures/v2/output-os.json index a3b15c4c..cb63ed2e 100644 --- a/script/test/fixtures/v2/output-os.json +++ b/script/test/fixtures/v2/output-os.json @@ -15,11 +15,6 @@ }, "spec": { "ports": [ - { - "name": "2345", - "port": 2345, - "targetPort": 2345 - }, { "name": "6379", "port": 6379, @@ -30,6 +25,121 @@ "protocol": "UDP", "port": 6379, "targetPort": 6379 + }, + { + "name": "3000", + "port": 3000, + "targetPort": 3000 + }, + { + "name": "3000-tcp", + "port": 3000, + "targetPort": 3000 + }, + { + "name": "3001", + "port": 3001, + "targetPort": 3001 + }, + { + "name": "3002", + "port": 3002, + "targetPort": 3002 + }, + { + "name": "3003", + "port": 3003, + "targetPort": 3003 + }, + { + "name": "3004", + "port": 3004, + "targetPort": 3004 + }, + { + "name": "3005", + "port": 3005, + "targetPort": 3005 + }, + { + "name": "8000", + "port": 8000, + "targetPort": 8000 + }, + { + "name": "9090", + "port": 9090, + "targetPort": 8080 + }, + { + "name": "9091", + "port": 9091, + "targetPort": 8081 + }, + { + "name": "49100", + "port": 49100, + "targetPort": 22 + }, + { + "name": "8001", + "port": 8001, + "targetPort": 8001 + }, + { + "name": "5000", + "port": 5000, + "targetPort": 5000 + }, + { + "name": "5001", + "port": 5001, + "targetPort": 5001 + }, + { + "name": "5002", + "port": 5002, + "targetPort": 5002 + }, + { + "name": "5003", + "port": 5003, + "targetPort": 5003 + }, + { + "name": "5004", + "port": 5004, + "targetPort": 5004 + }, + { + "name": "5005", + "port": 5005, + "targetPort": 5005 + }, + { + "name": "5006", + "port": 5006, + "targetPort": 5006 + }, + { + "name": "5007", + "port": 5007, + "targetPort": 5007 + }, + { + "name": "5008", + "port": 5008, + "targetPort": 5008 + }, + { + "name": "5009", + "port": 5009, + "targetPort": 5009 + }, + { + "name": "5010", + "port": 5010, + "targetPort": 5010 } ], "selector": { @@ -117,15 +227,90 @@ "name": "foo", "image": "foobar", "ports": [ - { - "containerPort": 2345 - }, { "containerPort": 6379 }, { "containerPort": 6379, "protocol": "UDP" + }, + { + "containerPort": 3000 + }, + { + "containerPort": 3001 + }, + { + "containerPort": 3002 + }, + { + "containerPort": 3003 + }, + { + "containerPort": 3004 + }, + { + "containerPort": 3005 + }, + { + "containerPort": 8000 + }, + { + "containerPort": 8080 + }, + { + "containerPort": 8081 + }, + { + "containerPort": 22 + }, + { + "containerPort": 8001, + "hostIP": "127.0.0.1" + }, + { + "containerPort": 5000, + "hostIP": "127.0.0.1" + }, + { + "containerPort": 5001, + "hostIP": "127.0.0.1" + }, + { + "containerPort": 5002, + "hostIP": "127.0.0.1" + }, + { + "containerPort": 5003, + "hostIP": "127.0.0.1" + }, + { + "containerPort": 5004, + "hostIP": "127.0.0.1" + }, + { + "containerPort": 5005, + "hostIP": "127.0.0.1" + }, + { + "containerPort": 5006, + "hostIP": "127.0.0.1" + }, + { + "containerPort": 5007, + "hostIP": "127.0.0.1" + }, + { + "containerPort": 5008, + "hostIP": "127.0.0.1" + }, + { + "containerPort": 5009, + "hostIP": "127.0.0.1" + }, + { + "containerPort": 5010, + "hostIP": "127.0.0.1" } ], "env": [ @@ -158,6 +343,9 @@ "creationTimestamp": null, "labels": { "io.kompose.service": "redis" + }, + "annotations": { + "kompose.service.type": "loadbalancer" } }, "spec": {