• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

zalando-incubator / stackset-controller / 7104061596

05 Dec 2023 05:02PM UTC coverage: 70.738% (-2.5%) from 73.244%
7104061596

Pull #495

github

gargravarr
Ensure create/update traffic segments when reconciling stack.
Pull Request #495: Add support for traffic segments.

330 of 582 new or added lines in 6 files covered. (56.7%)

1 existing line in 1 file now uncovered.

2548 of 3602 relevant lines covered (70.74%)

0.8 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

90.59
/pkg/core/stack_resources.go
1
package core
2

3
import (
4
        "fmt"
5
        "sort"
6
        "strconv"
7
        "strings"
8

9
        log "github.com/sirupsen/logrus"
10
        rgv1 "github.com/szuecs/routegroup-client/apis/zalando.org/v1"
11
        zv1 "github.com/zalando-incubator/stackset-controller/pkg/apis/zalando.org/v1"
12
        appsv1 "k8s.io/api/apps/v1"
13
        autoscaling "k8s.io/api/autoscaling/v2"
14
        v1 "k8s.io/api/core/v1"
15
        networking "k8s.io/api/networking/v1"
16
        metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
17
        "k8s.io/apimachinery/pkg/labels"
18
        "k8s.io/apimachinery/pkg/util/intstr"
19
        "k8s.io/apimachinery/pkg/util/sets"
20
)
21

22
const (
23
        apiVersionAppsV1 = "apps/v1"
24
        kindDeployment   = "Deployment"
25

26
        SegmentSuffix       = "-traffic-segment"
27
        InitialSegment      = "TrafficSegment(0.0, 0.0)"
28
        IngressPredicateKey = "zalando.org/skipper-predicate"
29
)
30

31
type ingressOrRouteGroupSpec interface {
32
        GetAnnotations() map[string]string
33
        GetHosts() []string
34
}
35

36
var (
37
        initialIngressSegment = map[string]string{
38
                IngressPredicateKey: InitialSegment,
39
        }
40

41
        // set implementation with 0 Byte value
42
        selectorLabels = map[string]struct{}{
43
                StacksetHeritageLabelKey: {},
44
                StackVersionLabelKey:     {},
45
        }
46

47
        // PathTypeImplementationSpecific is the used implementation path type
48
        // for k8s.io/api/networking/v1.HTTPIngressPath resources.
49
        PathTypeImplementationSpecific = networking.PathTypeImplementationSpecific
50
)
51

52
func mapCopy(m map[string]string) map[string]string {
1✔
53
        newMap := map[string]string{}
1✔
54
        for k, v := range m {
2✔
55
                newMap[k] = v
1✔
56
        }
1✔
57
        return newMap
1✔
58
}
59

60
// limitLabels returns a limited set of labels based on the validKeys.
61
func limitLabels(labels map[string]string, validKeys map[string]struct{}) map[string]string {
1✔
62
        newLabels := make(map[string]string, len(labels))
1✔
63
        for k, v := range labels {
2✔
64
                if _, ok := validKeys[k]; ok {
2✔
65
                        newLabels[k] = v
1✔
66
                }
1✔
67
        }
68
        return newLabels
1✔
69
}
70

71
// templateObjectMetaInjectLabels injects labels into a pod template spec.
72
func objectMetaInjectLabels(objectMeta metav1.ObjectMeta, labels map[string]string) metav1.ObjectMeta {
1✔
73
        if objectMeta.Labels == nil {
2✔
74
                objectMeta.Labels = map[string]string{}
1✔
75
        }
1✔
76
        for key, value := range labels {
2✔
77
                if _, ok := objectMeta.Labels[key]; !ok {
2✔
78
                        objectMeta.Labels[key] = value
1✔
79
                }
1✔
80
        }
81
        return objectMeta
1✔
82
}
83

84
func (sc *StackContainer) objectMeta(segment bool) metav1.ObjectMeta {
1✔
85
        resourceLabels := mapCopy(sc.Stack.Labels)
1✔
86

1✔
87
        name := sc.Name()
1✔
88
        if segment {
2✔
89
                name += SegmentSuffix
1✔
90
        }
1✔
91

92
        return metav1.ObjectMeta{
1✔
93
                Name:      name,
1✔
94
                Namespace: sc.Namespace(),
1✔
95
                Annotations: map[string]string{
1✔
96
                        stackGenerationAnnotationKey: strconv.FormatInt(sc.Stack.Generation, 10),
1✔
97
                },
1✔
98
                Labels: resourceLabels,
1✔
99
                OwnerReferences: []metav1.OwnerReference{
1✔
100
                        {
1✔
101
                                APIVersion: APIVersion,
1✔
102
                                Kind:       KindStack,
1✔
103
                                Name:       sc.Name(),
1✔
104
                                UID:        sc.Stack.UID,
1✔
105
                        },
1✔
106
                },
1✔
107
        }
1✔
108
}
109

110
func (sc *StackContainer) resourceMeta() metav1.ObjectMeta {
1✔
111
        return sc.objectMeta(false)
1✔
112
}
1✔
113

114
// getServicePorts gets the service ports to be used for the stack service.
115
func getServicePorts(stackSpec zv1.StackSpecInternal, backendPort *intstr.IntOrString) ([]v1.ServicePort, error) {
1✔
116
        var servicePorts []v1.ServicePort
1✔
117
        if stackSpec.StackSpec.Service == nil ||
1✔
118
                len(stackSpec.StackSpec.Service.Ports) == 0 {
2✔
119

1✔
120
                servicePorts = servicePortsFromContainers(
1✔
121
                        stackSpec.StackSpec.PodTemplate.Spec.Containers,
1✔
122
                )
1✔
123
        } else {
2✔
124
                servicePorts = stackSpec.StackSpec.Service.Ports
1✔
125
        }
1✔
126

127
        // validate that one port in the list maps to the backendPort.
128
        if backendPort != nil {
2✔
129
                for _, port := range servicePorts {
2✔
130
                        switch backendPort.Type {
1✔
131
                        case intstr.Int:
1✔
132
                                if port.Port == backendPort.IntVal {
2✔
133
                                        return servicePorts, nil
1✔
134
                                }
1✔
135
                        case intstr.String:
1✔
136
                                if port.Name == backendPort.StrVal {
2✔
137
                                        return servicePorts, nil
1✔
138
                                }
1✔
139
                        }
140
                }
141

142
                return nil, fmt.Errorf("no service ports matching backendPort '%s'", backendPort.String())
1✔
143
        }
144

145
        return servicePorts, nil
1✔
146
}
147

148
// servicePortsFromTemplate gets service port from pod template.
149
func servicePortsFromContainers(containers []v1.Container) []v1.ServicePort {
1✔
150
        ports := make([]v1.ServicePort, 0)
1✔
151
        for i, container := range containers {
2✔
152
                for j, port := range container.Ports {
2✔
153
                        name := fmt.Sprintf("port-%d-%d", i, j)
1✔
154
                        if port.Name != "" {
2✔
155
                                name = port.Name
1✔
156
                        }
1✔
157
                        servicePort := v1.ServicePort{
1✔
158
                                Name:       name,
1✔
159
                                Protocol:   port.Protocol,
1✔
160
                                Port:       port.ContainerPort,
1✔
161
                                TargetPort: intstr.FromInt(int(port.ContainerPort)),
1✔
162
                        }
1✔
163
                        // set default protocol if not specified
1✔
164
                        if servicePort.Protocol == "" {
2✔
165
                                servicePort.Protocol = v1.ProtocolTCP
1✔
166
                        }
1✔
167
                        ports = append(ports, servicePort)
1✔
168
                }
169
        }
170
        return ports
1✔
171
}
172

173
func (sc *StackContainer) selector() map[string]string {
1✔
174
        return limitLabels(sc.Stack.Labels, selectorLabels)
1✔
175
}
1✔
176

177
func (sc *StackContainer) GenerateDeployment() *appsv1.Deployment {
1✔
178
        stack := sc.Stack
1✔
179

1✔
180
        desiredReplicas := sc.stackReplicas
1✔
181
        if sc.prescalingActive {
2✔
182
                desiredReplicas = sc.prescalingReplicas
1✔
183
        }
1✔
184

185
        var updatedReplicas *int32
1✔
186

1✔
187
        if desiredReplicas != 0 && !sc.ScaledDown() {
2✔
188
                // Stack scaled up, rescale the deployment if it's at 0 replicas, or if HPA is unused and we don't run autoscaling
1✔
189
                if sc.deploymentReplicas == 0 || (!sc.IsAutoscaled() && desiredReplicas != sc.deploymentReplicas) {
2✔
190
                        updatedReplicas = wrapReplicas(desiredReplicas)
1✔
191
                }
1✔
192
        } else {
1✔
193
                // Stack scaled down (manually or because it doesn't receive traffic), check if we need to scale down the deployment
1✔
194
                if sc.deploymentReplicas != 0 {
2✔
195
                        updatedReplicas = wrapReplicas(0)
1✔
196
                }
1✔
197
        }
198

199
        if updatedReplicas == nil {
2✔
200
                updatedReplicas = wrapReplicas(sc.deploymentReplicas)
1✔
201
        }
1✔
202

203
        var strategy *appsv1.DeploymentStrategy
1✔
204
        if stack.Spec.StackSpec.Strategy != nil {
2✔
205
                strategy = stack.Spec.StackSpec.Strategy.DeepCopy()
1✔
206
        }
1✔
207

208
        embeddedCopy := stack.Spec.StackSpec.PodTemplate.EmbeddedObjectMeta.DeepCopy()
1✔
209

1✔
210
        templateObjectMeta := metav1.ObjectMeta{
1✔
211
                Annotations: embeddedCopy.Annotations,
1✔
212
                Labels:      embeddedCopy.Labels,
1✔
213
        }
1✔
214

1✔
215
        deployment := &appsv1.Deployment{
1✔
216
                ObjectMeta: sc.resourceMeta(),
1✔
217
                Spec: appsv1.DeploymentSpec{
1✔
218
                        Replicas:        updatedReplicas,
1✔
219
                        MinReadySeconds: sc.Stack.Spec.StackSpec.MinReadySeconds,
1✔
220
                        Selector: &metav1.LabelSelector{
1✔
221
                                MatchLabels: sc.selector(),
1✔
222
                        },
1✔
223
                        Template: v1.PodTemplateSpec{
1✔
224
                                ObjectMeta: objectMetaInjectLabels(templateObjectMeta, stack.Labels),
1✔
225
                                Spec:       *stack.Spec.StackSpec.PodTemplate.Spec.DeepCopy(),
1✔
226
                        },
1✔
227
                },
1✔
228
        }
1✔
229
        if strategy != nil {
2✔
230
                deployment.Spec.Strategy = *strategy
1✔
231
        }
1✔
232
        return deployment
1✔
233
}
234

235
func (sc *StackContainer) GenerateHPA() (*autoscaling.HorizontalPodAutoscaler, error) {
1✔
236
        autoscalerSpec := sc.Stack.Spec.StackSpec.Autoscaler
1✔
237
        trafficWeight := sc.actualTrafficWeight
1✔
238

1✔
239
        if autoscalerSpec == nil {
1✔
240
                return nil, nil
×
241
        }
×
242

243
        if sc.ScaledDown() {
2✔
244
                return nil, nil
1✔
245
        }
1✔
246

247
        result := &autoscaling.HorizontalPodAutoscaler{
1✔
248
                ObjectMeta: sc.resourceMeta(),
1✔
249
                TypeMeta: metav1.TypeMeta{
1✔
250
                        Kind:       "HorizontalPodAutoscaler",
1✔
251
                        APIVersion: "autoscaling/v2",
1✔
252
                },
1✔
253
                Spec: autoscaling.HorizontalPodAutoscalerSpec{
1✔
254
                        ScaleTargetRef: autoscaling.CrossVersionObjectReference{
1✔
255
                                APIVersion: apiVersionAppsV1,
1✔
256
                                Kind:       kindDeployment,
1✔
257
                                Name:       sc.Name(),
1✔
258
                        },
1✔
259
                },
1✔
260
        }
1✔
261

1✔
262
        result.Spec.MinReplicas = autoscalerSpec.MinReplicas
1✔
263
        result.Spec.MaxReplicas = autoscalerSpec.MaxReplicas
1✔
264

1✔
265
        metrics, annotations, err := convertCustomMetrics(sc.stacksetName, sc.Name(), sc.Namespace(), autoscalerMetricsList(autoscalerSpec.Metrics), trafficWeight)
1✔
266

1✔
267
        if err != nil {
1✔
268
                return nil, err
×
269
        }
×
270
        result.Spec.Metrics = metrics
1✔
271
        result.Annotations = mergeLabels(result.Annotations, annotations)
1✔
272
        result.Spec.Behavior = autoscalerSpec.Behavior
1✔
273

1✔
274
        // If prescaling is enabled, ensure we have at least `precalingReplicas` pods
1✔
275
        if sc.prescalingActive && (result.Spec.MinReplicas == nil || *result.Spec.MinReplicas < sc.prescalingReplicas) {
1✔
276
                pr := sc.prescalingReplicas
×
277
                result.Spec.MinReplicas = &pr
×
278
        }
×
279

280
        return result, nil
1✔
281
}
282

283
func (sc *StackContainer) GenerateService() (*v1.Service, error) {
1✔
284
        // get service ports to be used for the service
1✔
285
        var backendPort *intstr.IntOrString
1✔
286
        // Ingress or external managed Ingress
1✔
287
        if sc.HasBackendPort() {
1✔
288
                backendPort = sc.backendPort
×
289
        }
×
290

291
        servicePorts, err := getServicePorts(sc.Stack.Spec, backendPort)
1✔
292
        if err != nil {
1✔
293
                return nil, err
×
294
        }
×
295

296
        metaObj := sc.resourceMeta()
1✔
297
        stackSpec := sc.Stack.Spec
1✔
298
        if stackSpec.StackSpec.Service != nil {
2✔
299
                metaObj.Annotations = mergeLabels(
1✔
300
                        metaObj.Annotations,
1✔
301
                        stackSpec.StackSpec.Service.Annotations,
1✔
302
                )
1✔
303
        }
1✔
304
        return &v1.Service{
1✔
305
                ObjectMeta: metaObj,
1✔
306
                Spec: v1.ServiceSpec{
1✔
307
                        Selector: sc.selector(),
1✔
308
                        Type:     v1.ServiceTypeClusterIP,
1✔
309
                        Ports:    servicePorts,
1✔
310
                },
1✔
311
        }, nil
1✔
312
}
313

314
func (sc *StackContainer) stackHostnames(
315
        spec ingressOrRouteGroupSpec,
316
        segment bool,
317
) ([]string, error) {
1✔
318

1✔
319
        // The Ingress segment uses the original hostnames
1✔
320
        if segment {
2✔
321
                return spec.GetHosts(), nil
1✔
322
        }
1✔
323

324
        result := sets.NewString()
1✔
325

1✔
326
        // Old-style autogenerated hostnames
1✔
327
        for _, host := range spec.GetHosts() {
2✔
328
                for _, domain := range sc.clusterDomains {
2✔
329
                        if strings.HasSuffix(host, domain) {
2✔
330
                                result.Insert(fmt.Sprintf("%s.%s", sc.Name(), domain))
1✔
331
                        } else {
2✔
332
                                log.Debugf(
1✔
333
                                        "Ingress host: %s suffix did not match cluster-domain %s",
1✔
334
                                        host,
1✔
335
                                        domain,
1✔
336
                                )
1✔
337
                        }
1✔
338
                }
339
        }
340

341
        return result.List(), nil
1✔
342
}
343

344
func (sc *StackContainer) GenerateIngress() (*networking.Ingress, error) {
1✔
345
        return sc.generateIngress(false)
1✔
346
}
1✔
347

348
func (sc *StackContainer) GenerateIngressSegment() (
349
        *networking.Ingress,
350
        error,
351
) {
1✔
352
        if sc.IngressSegmentToUpdate != nil {
1✔
NEW
353
                return sc.IngressSegmentToUpdate, nil
×
NEW
354
        }
×
355
        return sc.generateIngress(true)
1✔
356
}
357

358
func (sc *StackContainer) generateIngress(segment bool) (
359
        *networking.Ingress,
360
        error,
361
) {
1✔
362

1✔
363
        if !sc.HasBackendPort() || sc.ingressSpec == nil {
2✔
364
                return nil, nil
1✔
365
        }
1✔
366

367
        hostnames, err := sc.stackHostnames(sc.ingressSpec, segment)
1✔
368
        if err != nil {
1✔
369
                return nil, err
×
370
        }
×
371
        if len(hostnames) == 0 {
1✔
372
                return nil, nil
×
373
        }
×
374

375
        rules := make([]networking.IngressRule, 0, len(hostnames))
1✔
376
        for _, hostname := range hostnames {
2✔
377
                rules = append(rules, networking.IngressRule{
1✔
378
                        IngressRuleValue: networking.IngressRuleValue{
1✔
379
                                HTTP: &networking.HTTPIngressRuleValue{
1✔
380
                                        Paths: []networking.HTTPIngressPath{
1✔
381
                                                {
1✔
382
                                                        PathType: &PathTypeImplementationSpecific,
1✔
383
                                                        Path:     sc.ingressSpec.Path,
1✔
384
                                                        Backend: networking.IngressBackend{
1✔
385
                                                                Service: &networking.IngressServiceBackend{
1✔
386
                                                                        Name: sc.Name(),
1✔
387
                                                                        Port: networking.ServiceBackendPort{
1✔
388
                                                                                Name:   sc.backendPort.StrVal,
1✔
389
                                                                                Number: sc.backendPort.IntVal,
1✔
390
                                                                        },
1✔
391
                                                                },
1✔
392
                                                        },
1✔
393
                                                },
1✔
394
                                        },
1✔
395
                                },
1✔
396
                        },
1✔
397
                        Host: hostname,
1✔
398
                })
1✔
399
        }
1✔
400

401
        // sort rules by hostname for a stable order
402
        sort.Slice(rules, func(i, j int) bool {
1✔
403
                return rules[i].Host < rules[j].Host
×
404
        })
×
405

406
        result := &networking.Ingress{
1✔
407
                ObjectMeta: sc.objectMeta(segment),
1✔
408
                Spec: networking.IngressSpec{
1✔
409
                        Rules: rules,
1✔
410
                },
1✔
411
        }
1✔
412

1✔
413
        // insert annotations
1✔
414
        result.Annotations = mergeLabels(
1✔
415
                result.Annotations,
1✔
416
                sc.ingressSpec.GetAnnotations(),
1✔
417
        )
1✔
418

1✔
419
        if segment {
2✔
420
                result.Annotations = mergeLabels(
1✔
421
                        result.Annotations,
1✔
422
                        initialIngressSegment,
1✔
423
                )
1✔
424
        }
1✔
425

426
        return result, nil
1✔
427
}
428

429
func (sc *StackContainer) GenerateRouteGroup() (*rgv1.RouteGroup, error) {
1✔
430
        return sc.generateRouteGroup(false)
1✔
431
}
1✔
432

433
func (sc *StackContainer) GenerateRouteGroupSegment() (
434
        *rgv1.RouteGroup,
435
        error,
436
) {
1✔
437
        if sc.RouteGroupSegmentToUpdate != nil {
1✔
NEW
438
                return sc.RouteGroupSegmentToUpdate, nil
×
NEW
439
        }
×
440
        return sc.generateRouteGroup(true)
1✔
441
}
442

443
func (sc *StackContainer) generateRouteGroup(segment bool) (
444
        *rgv1.RouteGroup,
445
        error,
446
) {
1✔
447
        if !sc.HasBackendPort() || sc.routeGroupSpec == nil {
2✔
448
                return nil, nil
1✔
449
        }
1✔
450

451
        hostnames, err := sc.stackHostnames(sc.routeGroupSpec, segment)
1✔
452
        if err != nil {
1✔
453
                return nil, err
×
454
        }
×
455
        if len(hostnames) == 0 {
1✔
456
                return nil, nil
×
457
        }
×
458

459
        result := &rgv1.RouteGroup{
1✔
460
                ObjectMeta: sc.objectMeta(segment),
1✔
461
                Spec: rgv1.RouteGroupSpec{
1✔
462
                        Hosts: hostnames,
1✔
463
                        Backends: []rgv1.RouteGroupBackend{
1✔
464
                                {
1✔
465
                                        Name:        sc.Name(),
1✔
466
                                        Type:        rgv1.ServiceRouteGroupBackend,
1✔
467
                                        ServiceName: sc.Name(),
1✔
468
                                        ServicePort: sc.backendPort.IntValue(),
1✔
469
                                        Algorithm:   sc.routeGroupSpec.LBAlgorithm,
1✔
470
                                },
1✔
471
                        },
1✔
472
                        DefaultBackends: []rgv1.RouteGroupBackendReference{
1✔
473
                                {
1✔
474
                                        BackendName: sc.Name(),
1✔
475
                                        Weight:      100,
1✔
476
                                },
1✔
477
                        },
1✔
478
                },
1✔
479
        }
1✔
480

1✔
481
        if !segment {
2✔
482
                result.Spec.Routes = sc.routeGroupSpec.Routes
1✔
483
        } else {
2✔
484
                routesWithSegment := []rgv1.RouteGroupRouteSpec{}
1✔
485
                for _, r := range sc.routeGroupSpec.Routes {
1✔
NEW
486
                        r.Predicates = append(r.Predicates, InitialSegment)
×
NEW
487
                        routesWithSegment = append(routesWithSegment, r)
×
NEW
488
                }
×
489
                result.Spec.Routes = routesWithSegment
1✔
490
        }
491

492
        // validate not overlapping with main backend
493
        for _, backend := range sc.routeGroupSpec.AdditionalBackends {
1✔
494
                if backend.Name == sc.Name() {
×
495
                        return nil, fmt.Errorf("invalid additionalBackend '%s', overlaps with Stack name", backend.Name)
×
496
                }
×
497
                if backend.ServiceName == sc.Name() {
×
498
                        return nil, fmt.Errorf("invalid additionalBackend '%s', serviceName '%s' overlaps with Stack name", backend.Name, backend.ServiceName)
×
499
                }
×
500
                result.Spec.Backends = append(result.Spec.Backends, backend)
×
501
        }
502

503
        // sort backends to ensure have a consistent generated RoutGroup resource
504
        sort.Slice(result.Spec.Backends, func(i, j int) bool {
1✔
505
                return result.Spec.Backends[i].Name < result.Spec.Backends[j].Name
×
506
        })
×
507

508
        // insert annotations
509
        result.Annotations = mergeLabels(
1✔
510
                result.Annotations,
1✔
511
                sc.routeGroupSpec.GetAnnotations(),
1✔
512
        )
1✔
513

1✔
514
        return result, nil
1✔
515
}
516

517
func (sc *StackContainer) GenerateStackStatus() *zv1.StackStatus {
1✔
518
        prescaling := zv1.PrescalingStatus{}
1✔
519
        if sc.prescalingActive {
2✔
520
                prescaling = zv1.PrescalingStatus{
1✔
521
                        Active:               sc.prescalingActive,
1✔
522
                        Replicas:             sc.prescalingReplicas,
1✔
523
                        DesiredTrafficWeight: sc.prescalingDesiredTrafficWeight,
1✔
524
                        LastTrafficIncrease:  wrapTime(sc.prescalingLastTrafficIncrease),
1✔
525
                }
1✔
526
        }
1✔
527
        return &zv1.StackStatus{
1✔
528
                ActualTrafficWeight:  sc.actualTrafficWeight,
1✔
529
                DesiredTrafficWeight: sc.desiredTrafficWeight,
1✔
530
                Replicas:             sc.createdReplicas,
1✔
531
                ReadyReplicas:        sc.readyReplicas,
1✔
532
                UpdatedReplicas:      sc.updatedReplicas,
1✔
533
                DesiredReplicas:      sc.deploymentReplicas,
1✔
534
                Prescaling:           prescaling,
1✔
535
                NoTrafficSince:       wrapTime(sc.noTrafficSince),
1✔
536
                LabelSelector:        labels.Set(sc.selector()).String(),
1✔
537
        }
1✔
538
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc