Skip to content

Commit faff3aa

Browse files
committed
Allow multiple ports to be defined explicitly
Add a `ports` field in the canary service specs that allows full control over port mapping and protocol, as an alternative to port discovery. If the new `ports` field is set, it takes precedence over both the single port fields and port discovery. Fixes #927 Signed-off-by: Yap Sok Ann <sokann@gmail.com>
1 parent 27daa2c commit faff3aa

File tree

5 files changed

+186
-19
lines changed

5 files changed

+186
-19
lines changed

charts/flagger/crds/crd.yaml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,25 @@ spec:
219219
delegation:
220220
description: enable behaving as a delegate VirtualService
221221
type: boolean
222+
ports:
223+
description: Service ports
224+
type: array
225+
items:
226+
type: object
227+
required: ["port"]
228+
properties:
229+
port:
230+
description: Container port number
231+
type: number
232+
portName:
233+
description: Container port name
234+
type: string
235+
appProtocol:
236+
description: Application protocol of the port
237+
type: string
238+
targetPort:
239+
description: Container target port name
240+
x-kubernetes-int-or-string: true
222241
match:
223242
description: URI match conditions
224243
type: array

docs/gitbook/usage/how-it-works.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,26 @@ If port discovery is enabled, Flagger scans the target workload and extracts the
161161
excluding the port specified in the canary service and service mesh sidecar ports.
162162
These ports will be used when generating the ClusterIP services.
163163

164+
Alternatively, you can explicitly define multiple ports using the `ports` field, which provides more control
165+
over port configuration and takes precedence over the single `port` fields and port discovery:
166+
167+
```yaml
168+
spec:
169+
service:
170+
name: podinfo
171+
ports:
172+
- port: 9999
173+
portName: grpc
174+
appProtocol: grpc
175+
targetPort: 9090
176+
- port: 8888
177+
portName: http
178+
appProtocol: http
179+
targetPort: web
180+
```
181+
182+
When `ports` is specified, it overrides the `port`, `portName`, `targetPort`, `appProtocol`, and `portDiscovery` fields.
183+
164184
Based on the canary spec service, Flagger creates the following Kubernetes ClusterIP service:
165185

166186
* `<service.name>.<namespace>.svc.cluster.local`

pkg/apis/flagger/v1beta1/canary.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,32 @@ type CanaryService struct {
223223
// Canary is the metadata to add to the canary service
224224
// +optional
225225
Canary *CustomMetadata `json:"canary,omitempty"`
226+
227+
// For service with more than 1 port->targetPort mappings, which can't be done
228+
// with `PortDiscovery`. Should work with providers that support multiple
229+
// ports, e.g. Istio and Linkerd.
230+
// When this field is set, it will override the fields `Port` / `PortName` /
231+
// `TargetPort` / `AppProtocol` / `PortDiscovery`.
232+
// +optional
233+
Ports []ServicePort `json:"ports,omitempty"`
234+
}
235+
236+
type ServicePort struct {
237+
// Port of the generated Kubernetes service
238+
Port int32 `json:"port"`
239+
240+
// Port name of the generated Kubernetes service
241+
PortName string `json:"portName"`
242+
243+
// Target port number or name of the generated Kubernetes service
244+
// Defaults to CanaryService.Port
245+
// +optional
246+
TargetPort intstr.IntOrString `json:"targetPort,omitempty"`
247+
248+
// AppProtocol of the service
249+
// https://kubernetes.io/docs/concepts/services-networking/service/#application-protocol
250+
// +optional
251+
AppProtocol string `json:"appProtocol,omitempty"`
226252
}
227253

228254
// CanaryAnalysis is used to describe how the analysis should be done

pkg/controller/scheduler_deployment_test.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -533,6 +533,52 @@ func TestScheduler_DeploymentPortDiscovery(t *testing.T) {
533533
}
534534
}
535535

536+
func TestScheduler_DeploymentExplicitPorts(t *testing.T) {
537+
mocks := newDeploymentFixture(nil)
538+
539+
cd, err := mocks.flaggerClient.FlaggerV1beta1().Canaries("default").Get(context.TODO(), "podinfo", metav1.GetOptions{})
540+
require.NoError(t, err)
541+
// explicitly define multiple ports
542+
cd.Spec.Service.Ports = []flaggerv1.ServicePort{
543+
{
544+
PortName: "grpc",
545+
Port: 9999,
546+
TargetPort: intstr.FromInt(9090),
547+
},
548+
{
549+
PortName: "http",
550+
Port: 8888,
551+
TargetPort: intstr.FromString("web"),
552+
},
553+
}
554+
// single port config will be ignored
555+
cd.Spec.Service.Port = 80
556+
// any port found via discovery will also be ignored
557+
cd.Spec.Service.PortDiscovery = true
558+
_, err = mocks.flaggerClient.FlaggerV1beta1().Canaries("default").Update(context.TODO(), cd, metav1.UpdateOptions{})
559+
require.NoError(t, err)
560+
561+
mocks.ctrl.advanceCanary("podinfo", "default")
562+
563+
canarySvc, err := mocks.kubeClient.CoreV1().Services("default").Get(context.TODO(), "podinfo-canary", metav1.GetOptions{})
564+
require.NoError(t, err)
565+
require.Len(t, canarySvc.Spec.Ports, 2)
566+
567+
matchPorts := func(lookup string) bool {
568+
switch lookup {
569+
case
570+
"grpc 9999->9090",
571+
"http 8888->web":
572+
return true
573+
}
574+
return false
575+
}
576+
577+
for _, port := range canarySvc.Spec.Ports {
578+
require.True(t, matchPorts(fmt.Sprintf("%s %v->%v", port.Name, port.Port, port.TargetPort.String())))
579+
}
580+
}
581+
536582
func TestScheduler_DeploymentTargetPortNumber(t *testing.T) {
537583
mocks := newDeploymentFixture(nil)
538584

pkg/router/kubernetes_default.go

Lines changed: 75 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ func (c *KubernetesDefaultRouter) GetRoutes(_ *flaggerv1.Canary) (primaryRoute i
8585
return 0, 0, nil
8686
}
8787

88-
func (c *KubernetesDefaultRouter) reconcileService(canary *flaggerv1.Canary, name string, podSelector string, metadata *flaggerv1.CustomMetadata) error {
88+
func (c *KubernetesDefaultRouter) getApexServicePort(canary *flaggerv1.Canary) corev1.ServicePort {
8989
portName := canary.Spec.Service.PortName
9090
if portName == "" {
9191
portName = "http"
@@ -100,28 +100,23 @@ func (c *KubernetesDefaultRouter) reconcileService(canary *flaggerv1.Canary, nam
100100
targetPort = canary.Spec.Service.TargetPort
101101
}
102102

103-
// set pod selector and apex port
104-
svcSpec := corev1.ServiceSpec{
105-
Type: corev1.ServiceTypeClusterIP,
106-
Selector: map[string]string{c.labelSelector: podSelector},
107-
Ports: []corev1.ServicePort{
108-
{
109-
Name: portName,
110-
Protocol: corev1.ProtocolTCP,
111-
Port: canary.Spec.Service.Port,
112-
TargetPort: targetPort,
113-
},
114-
},
115-
}
116-
if canary.Spec.Service.Headless {
117-
svcSpec.ClusterIP = "None"
103+
cp := corev1.ServicePort{
104+
Name: portName,
105+
Protocol: corev1.ProtocolTCP,
106+
Port: canary.Spec.Service.Port,
107+
TargetPort: targetPort,
118108
}
119109

120110
if v := canary.Spec.Service.AppProtocol; v != "" {
121-
svcSpec.Ports[0].AppProtocol = &v
111+
cp.AppProtocol = &v
122112
}
123113

124-
// set additional ports
114+
return cp
115+
}
116+
117+
func (c *KubernetesDefaultRouter) getDiscoveryServicePorts(canary *flaggerv1.Canary) []corev1.ServicePort {
118+
var ports []corev1.ServicePort
119+
125120
for n, p := range c.ports {
126121
cp := corev1.ServicePort{
127122
Name: n,
@@ -133,9 +128,70 @@ func (c *KubernetesDefaultRouter) reconcileService(canary *flaggerv1.Canary, nam
133128
},
134129
}
135130

136-
svcSpec.Ports = append(svcSpec.Ports, cp)
131+
ports = append(ports, cp)
137132
}
138133

134+
return ports
135+
}
136+
137+
func (c *KubernetesDefaultRouter) getExplicitServicePorts(canary *flaggerv1.Canary) []corev1.ServicePort {
138+
var ports []corev1.ServicePort
139+
140+
for _, p := range canary.Spec.Service.Ports {
141+
portName := p.PortName
142+
143+
targetPort := intstr.IntOrString{
144+
Type: intstr.Int,
145+
IntVal: p.Port,
146+
}
147+
148+
if p.TargetPort.String() != "0" {
149+
targetPort = p.TargetPort
150+
}
151+
152+
cp := corev1.ServicePort{
153+
Name: portName,
154+
Protocol: corev1.ProtocolTCP,
155+
Port: p.Port,
156+
TargetPort: targetPort,
157+
}
158+
159+
if v := p.AppProtocol; v != "" {
160+
cp.AppProtocol = &v
161+
}
162+
163+
ports = append(ports, cp)
164+
}
165+
166+
return ports
167+
}
168+
169+
func (c *KubernetesDefaultRouter) getServicePorts(canary *flaggerv1.Canary) []corev1.ServicePort {
170+
if len(canary.Spec.Service.Ports) == 0 {
171+
return append(
172+
[]corev1.ServicePort{
173+
c.getApexServicePort(canary),
174+
},
175+
c.getDiscoveryServicePorts(canary)...,
176+
)
177+
} else {
178+
return c.getExplicitServicePorts(canary)
179+
}
180+
}
181+
182+
func (c *KubernetesDefaultRouter) reconcileService(canary *flaggerv1.Canary, name string, podSelector string, metadata *flaggerv1.CustomMetadata) error {
183+
// set pod selector
184+
svcSpec := corev1.ServiceSpec{
185+
Type: corev1.ServiceTypeClusterIP,
186+
Selector: map[string]string{c.labelSelector: podSelector},
187+
}
188+
189+
if canary.Spec.Service.Headless {
190+
svcSpec.ClusterIP = "None"
191+
}
192+
193+
svcSpec.Ports = c.getServicePorts(canary)
194+
139195
if metadata == nil {
140196
metadata = &flaggerv1.CustomMetadata{}
141197
}

0 commit comments

Comments
 (0)