From b816bde6ccfb19def376502fa56e2116401c4d01 Mon Sep 17 00:00:00 2001 From: Silvin Lubecki Date: Tue, 13 Mar 2018 18:31:44 +0100 Subject: [PATCH] Fix docker stack services command on Port output when kubernetes service is a LoadBalancer or a NodePort * added tests on Kubernetes service conversion to swarm service Signed-off-by: Silvin Lubecki --- cli/command/stack/kubernetes/conversion.go | 45 ++-- .../stack/kubernetes/conversion_test.go | 192 ++++++++++++++++++ 2 files changed, 221 insertions(+), 16 deletions(-) create mode 100644 cli/command/stack/kubernetes/conversion_test.go diff --git a/cli/command/stack/kubernetes/conversion.go b/cli/command/stack/kubernetes/conversion.go index cd2df51cabc3..e5becda85dda 100644 --- a/cli/command/stack/kubernetes/conversion.go +++ b/cli/command/stack/kubernetes/conversion.go @@ -116,25 +116,31 @@ func (t tasksBySlot) Less(i, j int) bool { return t[j].Meta.CreatedAt.Before(t[i].CreatedAt) } +const ( + publishedServiceSuffix = "-published" + publishedOnRandomPortSuffix = "-random-ports" +) + // Replicas conversion func replicasToServices(replicas *appsv1beta2.ReplicaSetList, services *apiv1.ServiceList) ([]swarm.Service, map[string]formatter.ServiceListInfo, error) { result := make([]swarm.Service, len(replicas.Items)) infos := make(map[string]formatter.ServiceListInfo, len(replicas.Items)) for i, r := range replicas.Items { - service, ok := findService(services, r.Labels[labels.ForServiceName]) + serviceName := r.Labels[labels.ForServiceName] + serviceHeadless, ok := findService(services, serviceName) if !ok { - return nil, nil, fmt.Errorf("could not find service '%s'", r.Labels[labels.ForServiceName]) + return nil, nil, fmt.Errorf("could not find service '%s'", serviceName) } - stack, ok := service.Labels[labels.ForStackName] + stack, ok := serviceHeadless.Labels[labels.ForStackName] if ok { stack += "_" } - uid := string(service.UID) + uid := string(serviceHeadless.UID) s := swarm.Service{ ID: uid, Spec: swarm.ServiceSpec{ Annotations: swarm.Annotations{ - Name: stack + service.Name, + Name: stack + serviceHeadless.Name, }, TaskTemplate: swarm.TaskSpec{ ContainerSpec: &swarm.ContainerSpec{ @@ -143,17 +149,11 @@ func replicasToServices(replicas *appsv1beta2.ReplicaSetList, services *apiv1.Se }, }, } - if service.Spec.Type == apiv1.ServiceTypeLoadBalancer { - configs := make([]swarm.PortConfig, len(service.Spec.Ports)) - for i, p := range service.Spec.Ports { - configs[i] = swarm.PortConfig{ - PublishMode: swarm.PortConfigPublishModeIngress, - PublishedPort: uint32(p.Port), - TargetPort: uint32(p.TargetPort.IntValue()), - Protocol: toSwarmProtocol(p.Protocol), - } - } - s.Endpoint = swarm.Endpoint{Ports: configs} + if serviceNodePort, ok := findService(services, serviceName+publishedOnRandomPortSuffix); ok && serviceNodePort.Spec.Type == apiv1.ServiceTypeNodePort { + s.Endpoint = serviceEndpoint(serviceNodePort, swarm.PortConfigPublishModeHost) + } + if serviceLoadBalancer, ok := findService(services, serviceName+publishedServiceSuffix); ok && serviceLoadBalancer.Spec.Type == apiv1.ServiceTypeLoadBalancer { + s.Endpoint = serviceEndpoint(serviceLoadBalancer, swarm.PortConfigPublishModeIngress) } result[i] = s infos[uid] = formatter.ServiceListInfo{ @@ -172,3 +172,16 @@ func findService(services *apiv1.ServiceList, name string) (apiv1.Service, bool) } return apiv1.Service{}, false } + +func serviceEndpoint(service apiv1.Service, publishMode swarm.PortConfigPublishMode) swarm.Endpoint { + configs := make([]swarm.PortConfig, len(service.Spec.Ports)) + for i, p := range service.Spec.Ports { + configs[i] = swarm.PortConfig{ + PublishMode: publishMode, + PublishedPort: uint32(p.Port), + TargetPort: uint32(p.TargetPort.IntValue()), + Protocol: toSwarmProtocol(p.Protocol), + } + } + return swarm.Endpoint{Ports: configs} +} diff --git a/cli/command/stack/kubernetes/conversion_test.go b/cli/command/stack/kubernetes/conversion_test.go new file mode 100644 index 000000000000..4c61ab3e4c54 --- /dev/null +++ b/cli/command/stack/kubernetes/conversion_test.go @@ -0,0 +1,192 @@ +package kubernetes + +import ( + "testing" + + "github.com/docker/cli/cli/command/formatter" + "github.com/docker/cli/kubernetes/labels" + "github.com/docker/docker/api/types/swarm" + "github.com/gotestyourself/gotestyourself/assert" + appsv1beta2 "k8s.io/api/apps/v1beta2" + apiv1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + apimachineryTypes "k8s.io/apimachinery/pkg/types" + apimachineryUtil "k8s.io/apimachinery/pkg/util/intstr" +) + +func TestReplicasConversionNeedsAService(t *testing.T) { + replicas := appsv1beta2.ReplicaSetList{ + Items: []appsv1beta2.ReplicaSet{makeReplicaSet("unknown", 0, 0)}, + } + services := apiv1.ServiceList{} + _, _, err := replicasToServices(&replicas, &services) + assert.ErrorContains(t, err, "could not find service") +} + +func TestKubernetesServiceToSwarmServiceConversion(t *testing.T) { + testCases := []struct { + replicas *appsv1beta2.ReplicaSetList + services *apiv1.ServiceList + expectedServices []swarm.Service + expectedListInfo map[string]formatter.ServiceListInfo + }{ + // Match replicas with headless stack services + { + &appsv1beta2.ReplicaSetList{ + Items: []appsv1beta2.ReplicaSet{ + makeReplicaSet("service1", 2, 5), + makeReplicaSet("service2", 3, 3), + }, + }, + &apiv1.ServiceList{ + Items: []apiv1.Service{ + makeKubeService("service1", "stack", "uid1", apiv1.ServiceTypeClusterIP, nil), + makeKubeService("service2", "stack", "uid2", apiv1.ServiceTypeClusterIP, nil), + makeKubeService("service3", "other-stack", "uid2", apiv1.ServiceTypeClusterIP, nil), + }, + }, + []swarm.Service{ + makeSwarmService("stack_service1", "uid1", nil), + makeSwarmService("stack_service2", "uid2", nil), + }, + map[string]formatter.ServiceListInfo{ + "uid1": {"replicated", "2/5"}, + "uid2": {"replicated", "3/3"}, + }, + }, + // Headless service and LoadBalancer Service are tied to the same Swarm service + { + &appsv1beta2.ReplicaSetList{ + Items: []appsv1beta2.ReplicaSet{ + makeReplicaSet("service", 1, 1), + }, + }, + &apiv1.ServiceList{ + Items: []apiv1.Service{ + makeKubeService("service", "stack", "uid1", apiv1.ServiceTypeClusterIP, nil), + makeKubeService("service-published", "stack", "uid2", apiv1.ServiceTypeLoadBalancer, []apiv1.ServicePort{ + { + Port: 80, + TargetPort: apimachineryUtil.FromInt(80), + Protocol: apiv1.ProtocolTCP, + }, + }), + }, + }, + []swarm.Service{ + makeSwarmService("stack_service", "uid1", []swarm.PortConfig{ + { + PublishMode: swarm.PortConfigPublishModeIngress, + PublishedPort: 80, + TargetPort: 80, + Protocol: swarm.PortConfigProtocolTCP, + }, + }), + }, + map[string]formatter.ServiceListInfo{ + "uid1": {"replicated", "1/1"}, + }, + }, + // Headless service and NodePort Service are tied to the same Swarm service + + { + &appsv1beta2.ReplicaSetList{ + Items: []appsv1beta2.ReplicaSet{ + makeReplicaSet("service", 1, 1), + }, + }, + &apiv1.ServiceList{ + Items: []apiv1.Service{ + makeKubeService("service", "stack", "uid1", apiv1.ServiceTypeClusterIP, nil), + makeKubeService("service-random-ports", "stack", "uid2", apiv1.ServiceTypeNodePort, []apiv1.ServicePort{ + { + Port: 35666, + TargetPort: apimachineryUtil.FromInt(80), + Protocol: apiv1.ProtocolTCP, + }, + }), + }, + }, + []swarm.Service{ + makeSwarmService("stack_service", "uid1", []swarm.PortConfig{ + { + PublishMode: swarm.PortConfigPublishModeHost, + PublishedPort: 35666, + TargetPort: 80, + Protocol: swarm.PortConfigProtocolTCP, + }, + }), + }, + map[string]formatter.ServiceListInfo{ + "uid1": {"replicated", "1/1"}, + }, + }, + } + + for _, tc := range testCases { + swarmServices, listInfo, err := replicasToServices(tc.replicas, tc.services) + assert.NilError(t, err) + assert.DeepEqual(t, tc.expectedServices, swarmServices) + assert.DeepEqual(t, tc.expectedListInfo, listInfo) + } +} + +func makeReplicaSet(service string, available, replicas int32) appsv1beta2.ReplicaSet { + return appsv1beta2.ReplicaSet{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + labels.ForServiceName: service, + }, + }, + Spec: appsv1beta2.ReplicaSetSpec{ + Template: apiv1.PodTemplateSpec{ + Spec: apiv1.PodSpec{ + Containers: []apiv1.Container{ + { + Image: "image", + }, + }, + }, + }, + }, + Status: appsv1beta2.ReplicaSetStatus{ + AvailableReplicas: available, + Replicas: replicas, + }, + } +} + +func makeKubeService(service, stack, uid string, serviceType apiv1.ServiceType, ports []apiv1.ServicePort) apiv1.Service { + return apiv1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + labels.ForStackName: stack, + }, + Name: service, + UID: apimachineryTypes.UID(uid), + }, + Spec: apiv1.ServiceSpec{ + Type: serviceType, + Ports: ports, + }, + } +} + +func makeSwarmService(service, id string, ports []swarm.PortConfig) swarm.Service { + return swarm.Service{ + ID: id, + Spec: swarm.ServiceSpec{ + Annotations: swarm.Annotations{ + Name: service, + }, + TaskTemplate: swarm.TaskSpec{ + ContainerSpec: &swarm.ContainerSpec{ + Image: "image", + }, + }, + }, + Endpoint: swarm.Endpoint{ + Ports: ports, + }, + } +}