mirror of https://github.com/docker/cli.git
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 <silvin.lubecki@docker.com>
This commit is contained in:
parent
2731c71c99
commit
b816bde6cc
|
@ -116,25 +116,31 @@ func (t tasksBySlot) Less(i, j int) bool {
|
||||||
return t[j].Meta.CreatedAt.Before(t[i].CreatedAt)
|
return t[j].Meta.CreatedAt.Before(t[i].CreatedAt)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
publishedServiceSuffix = "-published"
|
||||||
|
publishedOnRandomPortSuffix = "-random-ports"
|
||||||
|
)
|
||||||
|
|
||||||
// Replicas conversion
|
// Replicas conversion
|
||||||
func replicasToServices(replicas *appsv1beta2.ReplicaSetList, services *apiv1.ServiceList) ([]swarm.Service, map[string]formatter.ServiceListInfo, error) {
|
func replicasToServices(replicas *appsv1beta2.ReplicaSetList, services *apiv1.ServiceList) ([]swarm.Service, map[string]formatter.ServiceListInfo, error) {
|
||||||
result := make([]swarm.Service, len(replicas.Items))
|
result := make([]swarm.Service, len(replicas.Items))
|
||||||
infos := make(map[string]formatter.ServiceListInfo, len(replicas.Items))
|
infos := make(map[string]formatter.ServiceListInfo, len(replicas.Items))
|
||||||
for i, r := range 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 {
|
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 {
|
if ok {
|
||||||
stack += "_"
|
stack += "_"
|
||||||
}
|
}
|
||||||
uid := string(service.UID)
|
uid := string(serviceHeadless.UID)
|
||||||
s := swarm.Service{
|
s := swarm.Service{
|
||||||
ID: uid,
|
ID: uid,
|
||||||
Spec: swarm.ServiceSpec{
|
Spec: swarm.ServiceSpec{
|
||||||
Annotations: swarm.Annotations{
|
Annotations: swarm.Annotations{
|
||||||
Name: stack + service.Name,
|
Name: stack + serviceHeadless.Name,
|
||||||
},
|
},
|
||||||
TaskTemplate: swarm.TaskSpec{
|
TaskTemplate: swarm.TaskSpec{
|
||||||
ContainerSpec: &swarm.ContainerSpec{
|
ContainerSpec: &swarm.ContainerSpec{
|
||||||
|
@ -143,17 +149,11 @@ func replicasToServices(replicas *appsv1beta2.ReplicaSetList, services *apiv1.Se
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
if service.Spec.Type == apiv1.ServiceTypeLoadBalancer {
|
if serviceNodePort, ok := findService(services, serviceName+publishedOnRandomPortSuffix); ok && serviceNodePort.Spec.Type == apiv1.ServiceTypeNodePort {
|
||||||
configs := make([]swarm.PortConfig, len(service.Spec.Ports))
|
s.Endpoint = serviceEndpoint(serviceNodePort, swarm.PortConfigPublishModeHost)
|
||||||
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),
|
|
||||||
}
|
}
|
||||||
}
|
if serviceLoadBalancer, ok := findService(services, serviceName+publishedServiceSuffix); ok && serviceLoadBalancer.Spec.Type == apiv1.ServiceTypeLoadBalancer {
|
||||||
s.Endpoint = swarm.Endpoint{Ports: configs}
|
s.Endpoint = serviceEndpoint(serviceLoadBalancer, swarm.PortConfigPublishModeIngress)
|
||||||
}
|
}
|
||||||
result[i] = s
|
result[i] = s
|
||||||
infos[uid] = formatter.ServiceListInfo{
|
infos[uid] = formatter.ServiceListInfo{
|
||||||
|
@ -172,3 +172,16 @@ func findService(services *apiv1.ServiceList, name string) (apiv1.Service, bool)
|
||||||
}
|
}
|
||||||
return apiv1.Service{}, false
|
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}
|
||||||
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue