feat: support external traffic policy

Signed-off-by: AhmedGrati <ahmedgrati1999@gmail.com>
This commit is contained in:
AhmedGrati 2023-02-09 23:22:49 +01:00
parent f2aeb70e59
commit 6be6fdd165
18 changed files with 650 additions and 6 deletions

View File

@ -200,6 +200,7 @@ The currently supported options are:
| kompose.service.healthcheck.liveness.http_get_path | kubernetes liveness httpGet path |
| kompose.service.healthcheck.liveness.http_get_port | kubernetes liveness httpGet port |
| kompose.service.healthcheck.liveness.tcp_port | kubernetes liveness tcpSocket port |
| kompose.service.external-traffic-policy | 'cluster', 'local', '' | |
**Note**: `kompose.service.type` label should be defined with `ports` only (except for headless service), otherwise `kompose` will fail.
@ -411,6 +412,25 @@ services:
- `kompose.service.healthcheck.readiness` defines Kubernetes [readiness](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/#define-readiness-probes)
- `kompose.service.external-traffic-policy` defines Kubernetes Service [external traffic policy.](https://kubernetes.io/docs/tasks/access-application-cluster/create-external-load-balancer/#preserving-the-client-source-ip).
For example:
```yaml
version: "3.3"
services:
front-end:
image: gcr.io/google-samples/gb-frontend:v4
environment:
- GET_HOSTS_FROM=dns
ports:
- 80:80
labels:
kompose.service.expose: lb
kompose.service.external-traffic-policy: local
kompose.service.type: loadbalancer
```
## Restart
If you want to create normal pods without controller you can use `restart` construct of docker-compose to define that. Follow table below to see what happens on the `restart` value.

View File

@ -126,6 +126,7 @@ type ServiceConfig struct {
User string `compose:"user"`
VolumesFrom []string `compose:"volumes_from"`
ServiceType string `compose:"kompose.service.type"`
ServiceExternalTrafficPolicy string `compose:"kompose.service.external-traffic-policy"`
NodePortPort int32 `compose:"kompose.service.nodeport.port"`
StopGracePeriod string `compose:"stop_grace_period"`
Build string `compose:"build"`

View File

@ -698,6 +698,13 @@ func parseKomposeLabels(labels map[string]string, serviceConfig *kobject.Service
}
serviceConfig.ServiceType = serviceType
case LabelServiceExternalTrafficPolicy:
serviceExternalTypeTrafficPolicy, err := handleServiceExternalTrafficPolicy(value)
if err != nil {
return errors.Wrap(err, "handleServiceExternalTrafficPolicy failed")
}
serviceConfig.ServiceExternalTrafficPolicy = serviceExternalTypeTrafficPolicy
case LabelServiceExpose:
serviceConfig.ExposeService = strings.Trim(strings.ToLower(value), " ,")
case LabelNodePortPort:

View File

@ -32,6 +32,8 @@ import (
const (
// LabelServiceType defines the type of service to be created
LabelServiceType = "kompose.service.type"
// LabelServiceExternalTrafficPolicy defines the external policy traffic of service to be created
LabelServiceExternalTrafficPolicy = "kompose.service.external-traffic-policy"
// LabelServiceGroup defines the group of services in a single pod
LabelServiceGroup = "kompose.service.group"
// LabelNodePortPort defines the port value for NodePort service
@ -151,6 +153,17 @@ func handleServiceType(ServiceType string) (string, error) {
}
}
func handleServiceExternalTrafficPolicy(ServiceExternalTrafficPolicyType string) (string, error) {
switch strings.ToLower(ServiceExternalTrafficPolicyType) {
case "", "cluster":
return string(api.ServiceExternalTrafficPolicyTypeCluster), nil
case "local":
return string(api.ServiceExternalTrafficPolicyTypeLocal), nil
default:
return "", errors.New("Unknown value " + ServiceExternalTrafficPolicyType + " , supported values are 'local, cluster'")
}
}
func normalizeContainerNames(svcName string) string {
return strings.ToLower(svcName)
}

View File

@ -1357,6 +1357,7 @@ func (k *Kubernetes) configKubeServiceAndIngressForService(service kobject.Servi
if service.ServiceType == "LoadBalancer" {
svcs := k.CreateLBService(name, service)
for _, svc := range svcs {
svc.Spec.ExternalTrafficPolicy = api.ServiceExternalTrafficPolicyType(service.ServiceExternalTrafficPolicy)
*objects = append(*objects, svc)
}
if len(svcs) > 1 {
@ -1368,11 +1369,17 @@ func (k *Kubernetes) configKubeServiceAndIngressForService(service kobject.Servi
if service.ExposeService != "" {
*objects = append(*objects, k.initIngress(name, service, svc.Spec.Ports[0].Port))
}
if service.ServiceExternalTrafficPolicy != "" && svc.Spec.Type != api.ServiceTypeNodePort {
log.Warningf("External Traffic Policy is ignored for the service %v of type %v", name, service.ServiceType)
}
}
} else {
if service.ServiceType == "Headless" {
svc := k.CreateHeadlessService(name, service)
*objects = append(*objects, svc)
if service.ServiceExternalTrafficPolicy != "" {
log.Warningf("External Traffic Policy is ignored for the service %v of type Headless", name)
}
} else {
log.Warnf("Service %q won't be created because 'ports' is not specified", service.Name)
}

View File

@ -93,6 +93,16 @@ func newKomposeObjectHostPortProtocolConfig() kobject.ServiceConfig {
}
}
func newServiceConfigWithExternalTrafficPolicy() kobject.ServiceConfig {
loadBalancerServiceType := string(api.ServiceTypeLoadBalancer)
return kobject.ServiceConfig{
Name: "app",
Port: []kobject.Ports{{HostPort: 123, ContainerPort: 456}},
ServiceType: loadBalancerServiceType,
ServiceExternalTrafficPolicy: "local",
}
}
func equalStringSlice(s1, s2 []string) bool {
if len(s1) != len(s2) {
return false
@ -985,3 +995,27 @@ func TestCreateHostPortAndProtocol(t *testing.T) {
}
}
}
func TestServiceExternalTrafficPolicy(t *testing.T) {
groupName := "pod_group"
komposeObject := kobject.KomposeObject{
ServiceConfigs: map[string]kobject.ServiceConfig{"app": newServiceConfigWithExternalTrafficPolicy()},
}
k := Kubernetes{}
objs, err := k.Transform(komposeObject, kobject.ConvertOptions{ServiceGroupMode: groupName})
if err != nil {
t.Error(errors.Wrap(err, "k.Transform failed"))
}
for _, obj := range objs {
if service, ok := obj.(*api.Service); ok {
serviceExternalTrafficPolicy := string(service.Spec.ExternalTrafficPolicy)
if serviceExternalTrafficPolicy != strings.ToLower(string(api.ServiceExternalTrafficPolicyTypeLocal)) {
t.Errorf("Expected Local as external lifecycle policy, got %v", serviceExternalTrafficPolicy)
}
serviceType := service.Spec.Type
if serviceType != api.ServiceTypeLoadBalancer {
t.Errorf("Expected LoadBalancer as service type, got %v", serviceType)
}
}
}
}

View File

@ -386,6 +386,7 @@ func (o *OpenShift) Transform(komposeObject kobject.KomposeObject, opt kobject.C
if service.ServiceType == "LoadBalancer" {
svcs := o.CreateLBService(name, service)
for _, svc := range svcs {
svc.Spec.ExternalTrafficPolicy = corev1.ServiceExternalTrafficPolicyType(service.ServiceExternalTrafficPolicy)
objects = append(objects, svc)
}
if len(svcs) > 1 {
@ -398,10 +399,16 @@ func (o *OpenShift) Transform(komposeObject kobject.KomposeObject, opt kobject.C
if service.ExposeService != "" {
objects = append(objects, o.initRoute(name, service, svc.Spec.Ports[0].Port))
}
if service.ServiceExternalTrafficPolicy != "" && svc.Spec.Type != corev1.ServiceTypeNodePort {
log.Warningf("External Traffic Policy is ignored for the service %v of type %v", name, service.ServiceType)
}
}
} else if service.ServiceType == "Headless" {
svc := o.CreateHeadlessService(name, service)
objects = append(objects, svc)
if service.ServiceExternalTrafficPolicy != "" {
log.Warningf("External Traffic Policy is ignored for the service %v of type Headless", name)
}
}
err := o.UpdateKubernetesObjects(name, service, opt, &objects)

View File

@ -20,6 +20,7 @@ import (
"os"
"path/filepath"
"reflect"
"strings"
"testing"
"github.com/kubernetes/kompose/pkg/kobject"
@ -56,6 +57,16 @@ func newServiceConfig() kobject.ServiceConfig {
}
}
func newServiceConfigWithExternalTrafficPolicy() kobject.ServiceConfig {
loadBalancerServiceType := string(corev1.ServiceTypeLoadBalancer)
return kobject.ServiceConfig{
Name: "app",
Port: []kobject.Ports{{HostPort: 123, ContainerPort: 456}},
ServiceType: loadBalancerServiceType,
ServiceExternalTrafficPolicy: "local",
}
}
func TestOpenShiftUpdateKubernetesObjects(t *testing.T) {
t.Log("Test case: Testing o.UpdateKubernetesObjects()")
var object []runtime.Object
@ -425,3 +436,27 @@ func TestRecreateStrategyWithVolumesPresent(t *testing.T) {
}
}
}
func TestServiceExternalTrafficPolicy(t *testing.T) {
groupName := "pod_group"
komposeObject := kobject.KomposeObject{
ServiceConfigs: map[string]kobject.ServiceConfig{"app": newServiceConfigWithExternalTrafficPolicy()},
}
o := OpenShift{}
objs, err := o.Transform(komposeObject, kobject.ConvertOptions{ServiceGroupMode: groupName})
if err != nil {
t.Error(errors.Wrap(err, "k.Transform failed"))
}
for _, obj := range objs {
if service, ok := obj.(*corev1.Service); ok {
serviceExternalTrafficPolicy := string(service.Spec.ExternalTrafficPolicy)
if serviceExternalTrafficPolicy != strings.ToLower(string(corev1.ServiceExternalTrafficPolicyTypeLocal)) {
t.Errorf("Expected Local as external lifecycle policy, got %v", serviceExternalTrafficPolicy)
}
serviceType := service.Spec.Type
if serviceType != corev1.ServiceTypeLoadBalancer {
t.Errorf("Expected LoadBalancer as service type, got %v", serviceType)
}
}
}
}

View File

@ -224,3 +224,18 @@ k8s_output="$KOMPOSE_ROOT/script/test/fixtures/host-port-protocol/output-k8s.yam
os_output="$KOMPOSE_ROOT/script/test/fixtures/host-port-protocol/output-os.yaml"
convert::expect_success_and_warning "$k8s_cmd" "$k8s_output"
convert::expect_success "$os_cmd" "$os_output"
# Test external traffic policy feature with valid configuration, warning is coming from the network policy.
k8s_cmd="kompose -f $KOMPOSE_ROOT/script/test/fixtures/external-traffic-policy/docker-compose-v1.yaml convert --stdout --with-kompose-annotation=false"
k8s_output="$KOMPOSE_ROOT/script/test/fixtures/external-traffic-policy/output-k8s-v1.yaml"
os_cmd="kompose --provider=openshift -f $KOMPOSE_ROOT/script/test/fixtures/external-traffic-policy/docker-compose-v1.yaml convert --stdout --with-kompose-annotation=false"
os_output="$KOMPOSE_ROOT/script/test/fixtures/external-traffic-policy/output-os-v1.yaml"
convert::expect_success_and_warning "$k8s_cmd" "$k8s_output"
convert::expect_success "$os_cmd" "$os_output"
# Test external traffic policy feature with warning, because we have nodeport with external traffic policy
k8s_cmd="kompose -f $KOMPOSE_ROOT/script/test/fixtures/external-traffic-policy/docker-compose-v2.yaml convert --stdout --with-kompose-annotation=false"
k8s_output="$KOMPOSE_ROOT/script/test/fixtures/external-traffic-policy/output-k8s-v2.yaml"
os_cmd="kompose --provider=openshift -f $KOMPOSE_ROOT/script/test/fixtures/external-traffic-policy/docker-compose-v2.yaml convert --stdout --with-kompose-annotation=false"
os_output="$KOMPOSE_ROOT/script/test/fixtures/external-traffic-policy/output-os-v2.yaml"
convert::expect_success_and_warning "$k8s_cmd" "$k8s_output"
convert::expect_success_and_warning "$os_cmd" "$os_output"

View File

@ -0,0 +1,14 @@
version: "3.3"
services:
front-end:
image: gcr.io/google-samples/gb-frontend:v4
environment:
- GET_HOSTS_FROM=dns
ports:
- 80:80
labels:
kompose.service.expose: lb
kompose.service.expose.ingress-class-name: nginx
kompose.service.external-traffic-policy: local
kompose.service.type: loadbalancer

View File

@ -0,0 +1,14 @@
version: "3.3"
services:
front-end:
image: gcr.io/google-samples/gb-frontend:v4
environment:
- GET_HOSTS_FROM=dns
ports:
- 80:80
labels:
kompose.service.expose: lb
kompose.service.expose.ingress-class-name: nginx
kompose.service.external-traffic-policy: local
kompose.service.type: headless

View File

@ -0,0 +1,85 @@
---
apiVersion: v1
kind: Service
metadata:
annotations:
kompose.service.expose: lb
kompose.service.expose.ingress-class-name: nginx
kompose.service.external-traffic-policy: local
kompose.service.type: loadbalancer
creationTimestamp: null
labels:
io.kompose.service: front-end-tcp
name: front-end-tcp
spec:
externalTrafficPolicy: Local
ports:
- name: "80"
port: 80
targetPort: 80
selector:
io.kompose.service: front-end
type: LoadBalancer
status:
loadBalancer: {}
---
apiVersion: apps/v1
kind: Deployment
metadata:
annotations:
kompose.service.expose: lb
kompose.service.expose.ingress-class-name: nginx
kompose.service.external-traffic-policy: local
kompose.service.type: loadbalancer
creationTimestamp: null
labels:
io.kompose.service: front-end
name: front-end
spec:
replicas: 1
selector:
matchLabels:
io.kompose.service: front-end
strategy: {}
template:
metadata:
annotations:
kompose.service.expose: lb
kompose.service.expose.ingress-class-name: nginx
kompose.service.external-traffic-policy: local
kompose.service.type: loadbalancer
creationTimestamp: null
labels:
io.kompose.network/external-traffic-policy-default: "true"
io.kompose.service: front-end
spec:
containers:
- env:
- name: GET_HOSTS_FROM
value: dns
image: gcr.io/google-samples/gb-frontend:v4
name: front-end
ports:
- containerPort: 80
hostPort: 80
protocol: TCP
resources: {}
restartPolicy: Always
status: {}
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
creationTimestamp: null
name: external-traffic-policy-default
spec:
ingress:
- from:
- podSelector:
matchLabels:
io.kompose.network/external-traffic-policy-default: "true"
podSelector:
matchLabels:
io.kompose.network/external-traffic-policy-default: "true"

View File

@ -0,0 +1,115 @@
---
apiVersion: v1
kind: Service
metadata:
annotations:
kompose.service.expose: lb
kompose.service.expose.ingress-class-name: nginx
kompose.service.external-traffic-policy: local
kompose.service.type: headless
creationTimestamp: null
labels:
io.kompose.service: front-end
name: front-end
spec:
clusterIP: None
ports:
- name: "80"
port: 80
targetPort: 80
selector:
io.kompose.service: front-end
type: ClusterIP
status:
loadBalancer: {}
---
apiVersion: apps/v1
kind: Deployment
metadata:
annotations:
kompose.service.expose: lb
kompose.service.expose.ingress-class-name: nginx
kompose.service.external-traffic-policy: local
kompose.service.type: headless
creationTimestamp: null
labels:
io.kompose.service: front-end
name: front-end
spec:
replicas: 1
selector:
matchLabels:
io.kompose.service: front-end
strategy: {}
template:
metadata:
annotations:
kompose.service.expose: lb
kompose.service.expose.ingress-class-name: nginx
kompose.service.external-traffic-policy: local
kompose.service.type: headless
creationTimestamp: null
labels:
io.kompose.network/external-traffic-policy-default: "true"
io.kompose.service: front-end
spec:
containers:
- env:
- name: GET_HOSTS_FROM
value: dns
image: gcr.io/google-samples/gb-frontend:v4
name: front-end
ports:
- containerPort: 80
hostPort: 80
protocol: TCP
resources: {}
restartPolicy: Always
status: {}
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
kompose.service.expose: lb
kompose.service.expose.ingress-class-name: nginx
kompose.service.external-traffic-policy: local
kompose.service.type: headless
creationTimestamp: null
labels:
io.kompose.service: front-end
name: front-end
spec:
ingressClassName: nginx
rules:
- host: lb
http:
paths:
- backend:
service:
name: front-end
port:
number: 80
path: /
pathType: Prefix
status:
loadBalancer: {}
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
creationTimestamp: null
name: external-traffic-policy-default
spec:
ingress:
- from:
- podSelector:
matchLabels:
io.kompose.network/external-traffic-policy-default: "true"
podSelector:
matchLabels:
io.kompose.network/external-traffic-policy-default: "true"

View File

@ -0,0 +1,106 @@
---
apiVersion: v1
kind: Service
metadata:
annotations:
kompose.service.expose: lb
kompose.service.expose.ingress-class-name: nginx
kompose.service.external-traffic-policy: local
kompose.service.type: loadbalancer
creationTimestamp: null
labels:
io.kompose.service: front-end-tcp
name: front-end-tcp
spec:
externalTrafficPolicy: Local
ports:
- name: "80"
port: 80
targetPort: 80
selector:
io.kompose.service: front-end
type: LoadBalancer
status:
loadBalancer: {}
---
apiVersion: v1
kind: DeploymentConfig
metadata:
annotations:
kompose.service.expose: lb
kompose.service.expose.ingress-class-name: nginx
kompose.service.external-traffic-policy: local
kompose.service.type: loadbalancer
creationTimestamp: null
labels:
io.kompose.service: front-end
name: front-end
spec:
replicas: 1
selector:
io.kompose.service: front-end
strategy:
resources: {}
template:
metadata:
creationTimestamp: null
labels:
io.kompose.network/external-traffic-policy-default: "true"
io.kompose.service: front-end
spec:
containers:
- env:
- name: GET_HOSTS_FROM
value: dns
image: ' '
name: front-end
ports:
- containerPort: 80
hostPort: 80
protocol: TCP
resources: {}
restartPolicy: Always
test: false
triggers:
- type: ConfigChange
- imageChangeParams:
automatic: true
containerNames:
- front-end
from:
kind: ImageStreamTag
name: front-end:v4
type: ImageChange
status:
availableReplicas: 0
latestVersion: 0
observedGeneration: 0
replicas: 0
unavailableReplicas: 0
updatedReplicas: 0
---
apiVersion: v1
kind: ImageStream
metadata:
creationTimestamp: null
labels:
io.kompose.service: front-end
name: front-end
spec:
lookupPolicy:
local: false
tags:
- annotations: null
from:
kind: DockerImage
name: gcr.io/google-samples/gb-frontend:v4
generation: null
importPolicy: {}
name: v4
referencePolicy:
type: ""
status:
dockerImageRepository: ""

View File

@ -0,0 +1,125 @@
---
apiVersion: v1
kind: Service
metadata:
annotations:
kompose.service.expose: lb
kompose.service.expose.ingress-class-name: nginx
kompose.service.external-traffic-policy: local
kompose.service.type: headless
creationTimestamp: null
labels:
io.kompose.service: front-end
name: front-end
spec:
clusterIP: None
ports:
- name: "80"
port: 80
targetPort: 80
selector:
io.kompose.service: front-end
type: ClusterIP
status:
loadBalancer: {}
---
apiVersion: v1
kind: DeploymentConfig
metadata:
annotations:
kompose.service.expose: lb
kompose.service.expose.ingress-class-name: nginx
kompose.service.external-traffic-policy: local
kompose.service.type: headless
creationTimestamp: null
labels:
io.kompose.service: front-end
name: front-end
spec:
replicas: 1
selector:
io.kompose.service: front-end
strategy:
resources: {}
template:
metadata:
creationTimestamp: null
labels:
io.kompose.network/external-traffic-policy-default: "true"
io.kompose.service: front-end
spec:
containers:
- env:
- name: GET_HOSTS_FROM
value: dns
image: ' '
name: front-end
ports:
- containerPort: 80
hostPort: 80
protocol: TCP
resources: {}
restartPolicy: Always
test: false
triggers:
- type: ConfigChange
- imageChangeParams:
automatic: true
containerNames:
- front-end
from:
kind: ImageStreamTag
name: front-end:v4
type: ImageChange
status:
availableReplicas: 0
latestVersion: 0
observedGeneration: 0
replicas: 0
unavailableReplicas: 0
updatedReplicas: 0
---
apiVersion: v1
kind: ImageStream
metadata:
creationTimestamp: null
labels:
io.kompose.service: front-end
name: front-end
spec:
lookupPolicy:
local: false
tags:
- annotations: null
from:
kind: DockerImage
name: gcr.io/google-samples/gb-frontend:v4
generation: null
importPolicy: {}
name: v4
referencePolicy:
type: ""
status:
dockerImageRepository: ""
---
apiVersion: v1
kind: Route
metadata:
creationTimestamp: null
labels:
io.kompose.service: front-end
name: front-end
spec:
host: lb
port:
targetPort: 80
to:
kind: Service
name: front-end
weight: null
status:
ingress: null

View File

@ -25,4 +25,4 @@ networks:
name: app-network
web:
external:
name: web-network
name: web-network

View File

@ -53,6 +53,54 @@ spec:
restartPolicy: Always
status: {}
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
creationTimestamp: null
name: normalized-network
spec:
ingress:
- from:
- podSelector:
matchLabels:
io.kompose.network/normalized-network: "true"
podSelector:
matchLabels:
io.kompose.network/normalized-network: "true"
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
creationTimestamp: null
name: v30-normalized-network
spec:
ingress:
- from:
- podSelector:
matchLabels:
io.kompose.network/v30-normalized-network: "true"
podSelector:
matchLabels:
io.kompose.network/v30-normalized-network: "true"
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
creationTimestamp: null
name: app-network
spec:
ingress:
- from:
- podSelector:
matchLabels:
io.kompose.network/normalized-network: "true"
podSelector:
matchLabels:
io.kompose.network/normalized-network: "true"
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
@ -90,16 +138,16 @@ apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
creationTimestamp: null
name: v30-normalized-network
name: normalized-network
spec:
ingress:
- from:
- podSelector:
matchLabels:
io.kompose.network/v30-normalized-network: "true"
io.kompose.network/normalized-network: "true"
podSelector:
matchLabels:
io.kompose.network/v30-normalized-network: "true"
io.kompose.network/normalized-network: "true"
---
apiVersion: apps/v1
@ -155,4 +203,3 @@ spec:
podSelector:
matchLabels:
io.kompose.network/v30-default: "true"

View File

@ -171,4 +171,3 @@ spec:
type: ""
status:
dockerImageRepository: ""