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

zalando-incubator / stackset-controller / 8738031909

19 Mar 2024 06:41PM UTC coverage: 51.212% (-0.9%) from 52.101%
8738031909

Pull #593

github

linki
some labels
Pull Request #593: [2/3] Add Inline Solution for ConfigMaps

5 of 143 new or added lines in 5 files covered. (3.5%)

766 existing lines in 14 files now uncovered.

3084 of 6022 relevant lines covered (51.21%)

0.58 hits per line

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

81.58
/pkg/core/types.go
1
package core
2

3
import (
4
        "fmt"
5
        "math"
6
        "sort"
7
        "time"
8

9
        rgv1 "github.com/szuecs/routegroup-client/apis/zalando.org/v1"
10
        zv1 "github.com/zalando-incubator/stackset-controller/pkg/apis/zalando.org/v1"
11
        appsv1 "k8s.io/api/apps/v1"
12
        autoscaling "k8s.io/api/autoscaling/v2"
13
        v1 "k8s.io/api/core/v1"
14
        networking "k8s.io/api/networking/v1"
15
        "k8s.io/apimachinery/pkg/types"
16
        "k8s.io/apimachinery/pkg/util/intstr"
17
)
18

19
const (
20
        defaultVersion             = "default"
21
        defaultStackLifecycleLimit = 10
22
        defaultScaledownTTL        = 300 * time.Second
23
)
24

25
// StackSetContainer is a container for storing the full state of a StackSet
26
// including the sub-resources which are part of the StackSet. It represents a
27
// snapshot of the resources currently in the Cluster. This includes an
28
// optional Ingress resource as well as the current Traffic distribution. It
29
// also contains a set of StackContainers which represents the full state of
30
// the individual Stacks part of the StackSet.
31
type StackSetContainer struct {
32
        StackSet *zv1.StackSet
33

34
        // StackContainers is a set of stacks belonging to the StackSet
35
        // including the Stack sub resources like Deployments and Services.
36
        StackContainers map[types.UID]*StackContainer
37

38
        // Ingress defines the current Ingress resource belonging to the
39
        // StackSet. This is a reference to the actual resource while
40
        // `StackSet.Spec.Ingress` defines the ingress configuration specified
41
        // by the user on the StackSet.
42
        Ingress *networking.Ingress
43

44
        // RouteGroups defines the current RouteGroup resource belonging to the
45
        // StackSet. This is a reference to the actual resource while
46
        // `StackSet.Spec.RouteGroup` defines the route group configuration
47
        // specified by the user on the StackSet.
48
        RouteGroup *rgv1.RouteGroup
49

50
        // TrafficReconciler is the reconciler implementation used for
51
        // switching traffic between stacks. E.g. for prescaling stacks before
52
        // switching traffic.
53
        TrafficReconciler TrafficReconciler
54

55
        // ExternalIngressBackendPort defines the backendPort mapping
56
        // if an external entity creates ingress objects for us. The
57
        // Ingress of stackset should be nil in this case.
58
        externalIngressBackendPort *intstr.IntOrString
59

60
        // backendWeightsAnnotationKey to store the runtime decision
61
        // which annotation is used, defaults to
62
        // traffic.DefaultBackendWeightsAnnotationKey
63
        backendWeightsAnnotationKey string
64

65
        // clusterDomains stores the main domain names of the cluster;
66
        // per-stack ingress hostnames are not generated for names outside of them
67
        clusterDomains []string
68

69
        // segmentTraffic controls support of managing traffic weight by using
70
        // dedicated Ingress and RouteGroup resources with Skipper's TrafficSegment
71
        // predicate.
72
        segmentTraffic bool
73

74
        // ingressAnnotationsToSync is a list of ingress annotations that should be
75
        // synchronized across all existing stacks.
76
        ingressAnnotationsToSync []string
77
}
78

79
// StackContainer is a container for storing the full state of a Stack
80
// including all the managed sub-resources. This includes the Stack resource
81
// itself and all the sub resources like Deployment, HPA and Service.
82
type StackContainer struct {
83
        // Stack represents the desired state of the stack, updated by the reconciliation logic
84
        Stack *zv1.Stack
85

86
        // PendingRemoval is set to true if the stack should be deleted
87
        PendingRemoval bool
88

89
        // Resources contains Kubernetes entities for the Stack's resources (Deployment, Ingress, etc)
90
        Resources StackResources
91

92
        // Fields from the parent stackset
93
        stacksetName   string
94
        scaledownTTL   time.Duration
95
        clusterDomains []string
96

97
        // Ingress annotations to synchronize
98
        ingressAnnotationsToSync []string
99

100
        // Ingress annotations present in the StackSet
101
        syncAnnotationsInIngress map[string]string
102

103
        // RouteGroup annotations present in the StackSet
104
        syncAnnotationsInRouteGroup map[string]string
105

106
        // Fields from the stack itself.
107
        ingressSpec    *zv1.StackSetIngressSpec
108
        routeGroupSpec *zv1.RouteGroupSpec
109
        backendPort    *intstr.IntOrString
110

111
        // Fields from the stack itself, with some defaults applied
112
        stackReplicas int32
113

114
        // Fields from the stack resources
115

116
        // Set to true only if all related resources have been updated according to the latest stack version
117
        resourcesUpdated bool
118

119
        // Current number of replicas that the deployment is expected to have, from Deployment.spec
120
        deploymentReplicas int32
121

122
        // Current number of replicas that the deployment has, from Deployment.status
123
        createdReplicas int32
124

125
        // Current number of replicas that the deployment has, from Deployment.status
126
        readyReplicas int32
127

128
        // Current number of up-to-date replicas that the deployment has, from Deployment.status
129
        updatedReplicas int32
130

131
        // When using traffic segments: the traffic segment associated to this stack
132
        segmentLowerLimit float64
133
        segmentUpperLimit float64
134

135
        // Traffic & scaling
136
        currentActualTrafficWeight     float64
137
        actualTrafficWeight            float64
138
        desiredTrafficWeight           float64
139
        noTrafficSince                 time.Time
140
        prescalingActive               bool
141
        prescalingReplicas             int32
142
        prescalingDesiredTrafficWeight float64
143
        prescalingLastTrafficIncrease  time.Time
144
        minReadyPercent                float64
145
}
146

147
// TrafficChange contains information about a traffic change event
148
type TrafficChange struct {
149
        StackName        string
150
        OldTrafficWeight float64
151
        NewTrafficWeight float64
152
}
153

UNCOV
154
func (tc TrafficChange) String() string {
×
UNCOV
155
        return fmt.Sprintf("%s: %.1f%% to %.1f%%", tc.StackName, tc.OldTrafficWeight, tc.NewTrafficWeight)
×
UNCOV
156
}
×
157

158
func (sc *StackContainer) HasBackendPort() bool {
1✔
159
        return sc.backendPort != nil
1✔
160
}
1✔
161

162
func (sc *StackContainer) HasTraffic() bool {
1✔
163
        return sc.actualTrafficWeight > 0 || sc.desiredTrafficWeight > 0
1✔
164
}
1✔
165

166
func (sc *StackContainer) IsReady() bool {
1✔
167
        // Calculate minimum required replicas for the Deployment to be considered ready
1✔
168
        minRequiredReplicas := int32(math.Ceil(float64(sc.deploymentReplicas) * sc.minReadyPercent))
1✔
169

1✔
170
        // Stacks are considered ready when all subresources have been updated
1✔
171
        // and the minimum ready percentage is hit on and replicas
1✔
172
        return (sc.resourcesUpdated && sc.deploymentReplicas > 0 &&
1✔
173
                minRequiredReplicas <= sc.updatedReplicas &&
1✔
174
                minRequiredReplicas <= sc.readyReplicas)
1✔
175
}
1✔
176

177
func (sc *StackContainer) MaxReplicas() int32 {
1✔
178
        if sc.Stack.Spec.StackSpec.Autoscaler != nil {
2✔
179
                return sc.Stack.Spec.StackSpec.Autoscaler.MaxReplicas
1✔
180
        }
1✔
181
        return math.MaxInt32
1✔
182
}
183

184
func (sc *StackContainer) IsAutoscaled() bool {
1✔
185
        return sc.Stack.Spec.StackSpec.Autoscaler != nil
1✔
186
}
1✔
187

188
func (sc *StackContainer) ScaledDown() bool {
1✔
189
        if sc.HasTraffic() {
2✔
190
                return false
1✔
191
        }
1✔
192
        return !sc.noTrafficSince.IsZero() && time.Since(sc.noTrafficSince) > sc.scaledownTTL
1✔
193
}
194

195
func (sc *StackContainer) Name() string {
1✔
196
        return sc.Stack.Name
1✔
197
}
1✔
198

199
func (sc *StackContainer) Namespace() string {
1✔
200
        return sc.Stack.Namespace
1✔
201
}
1✔
202

203
// StackResources describes the resources of a stack.
204
type StackResources struct {
205
        Deployment        *appsv1.Deployment
206
        HPA               *autoscaling.HorizontalPodAutoscaler
207
        Service           *v1.Service
208
        Ingress           *networking.Ingress
209
        IngressSegment    *networking.Ingress
210
        RouteGroup        *rgv1.RouteGroup
211
        RouteGroupSegment *rgv1.RouteGroup
212
        ConfigMaps        []*v1.ConfigMap
213
        Secrets           []*v1.Secret
214
}
215

UNCOV
216
func NewContainer(stackset *zv1.StackSet, reconciler TrafficReconciler, backendWeightsAnnotationKey string, clusterDomains []string) *StackSetContainer {
×
UNCOV
217
        return &StackSetContainer{
×
218
                StackSet:                    stackset,
×
219
                StackContainers:             map[types.UID]*StackContainer{},
×
220
                TrafficReconciler:           reconciler,
×
221
                backendWeightsAnnotationKey: backendWeightsAnnotationKey,
×
222
                clusterDomains:              clusterDomains,
×
223
        }
×
224
}
×
225

226
// SynchronizeIngressAnnotations ensures that the container propagates the
227
// specified annotations to all segment Ingress or Routegroups, when they are
228
// present in the StackSet's Ingress or RouteGroup definition.
229
//
230
// This synchronization is only relevant when  the container supports traffic
231
// segments.
232
func (ssc *StackSetContainer) SynchronizeIngressAnnotations(
233
        annotations []string,
234
) {
1✔
235
        ssc.ingressAnnotationsToSync = annotations
1✔
236
}
1✔
237

238
func (ssc *StackSetContainer) stackByName(name string) *StackContainer {
1✔
239
        for _, container := range ssc.StackContainers {
2✔
240
                if container.Name() == name {
2✔
241
                        return container
1✔
242
                }
1✔
243
        }
244
        return nil
1✔
245
}
246

247
// updateDesiredTraffic gets desired from stackset spec
248
// and populates it to stack containers
249
func (ssc *StackSetContainer) updateDesiredTraffic() error {
1✔
250
        weights := make(map[string]float64)
1✔
251

1✔
252
        for _, desiredTraffic := range ssc.StackSet.Spec.Traffic {
2✔
253
                weights[desiredTraffic.StackName] = desiredTraffic.Weight
1✔
254
        }
1✔
255

256
        // filter stacks and normalize weights
257
        stacksetNames := make(map[string]struct{})
1✔
258
        for _, sc := range ssc.StackContainers {
2✔
259
                stacksetNames[sc.Name()] = struct{}{}
1✔
260
        }
1✔
261
        for name := range weights {
2✔
262
                if _, ok := stacksetNames[name]; !ok {
2✔
263
                        delete(weights, name)
1✔
264
                }
1✔
265
        }
266

267
        if !allZero(weights) {
2✔
268
                normalizeWeights(weights)
1✔
269
        }
1✔
270

271
        // save values in stack containers
272
        for _, container := range ssc.StackContainers {
2✔
273
                container.desiredTrafficWeight = weights[container.Name()]
1✔
274
        }
1✔
275

276
        return nil
1✔
277
}
278

279
// updateActualTraffic gets actual from stackset status
280
// and populates it to stack containers
281
func (ssc *StackSetContainer) updateActualTraffic() error {
1✔
282
        weights := make(map[string]float64)
1✔
283
        for _, actualTraffic := range ssc.StackSet.Status.Traffic {
2✔
284
                weights[actualTraffic.ServiceName] = actualTraffic.Weight
1✔
285
        }
1✔
286

287
        // filter stacks and normalize weights
288
        stacksetNames := make(map[string]struct{})
1✔
289
        for _, sc := range ssc.StackContainers {
2✔
290
                stacksetNames[sc.Name()] = struct{}{}
1✔
291
        }
1✔
292
        for name := range weights {
2✔
293
                if _, ok := stacksetNames[name]; !ok {
2✔
294
                        delete(weights, name)
1✔
295
                }
1✔
296
        }
297
        if !allZero(weights) {
2✔
298
                normalizeWeights(weights)
1✔
299
        }
1✔
300

301
        // save values in stack containers
302
        for _, container := range ssc.StackContainers {
2✔
303
                container.actualTrafficWeight = weights[container.Name()]
1✔
304
                container.currentActualTrafficWeight = weights[container.Name()]
1✔
305
        }
1✔
306

307
        return nil
1✔
308
}
309

310
// UpdateFromResources populates stack state information (e.g. replica counts or
311
// traffic) from related resources
312
func (ssc *StackSetContainer) UpdateFromResources() error {
1✔
313
        if len(ssc.StackContainers) == 0 {
1✔
314
                return nil
×
315
        }
×
316

317
        backendPort, err := findBackendPort(
1✔
318
                ssc.StackSet.Spec.Ingress,
1✔
319
                ssc.StackSet.Spec.RouteGroup,
1✔
320
                ssc.StackSet.Spec.ExternalIngress,
1✔
321
        )
1✔
322
        if err != nil {
1✔
UNCOV
323
                return err
×
UNCOV
324
        }
×
325

326
        syncAnnotationsInIngress := map[string]string{}
1✔
327
        if ssc.StackSet.Spec.Ingress != nil {
2✔
328
                syncAnnotationsInIngress = getKeyValues(
1✔
329
                        ssc.ingressAnnotationsToSync,
1✔
330
                        ssc.StackSet.Spec.Ingress.Annotations,
1✔
331
                )
1✔
332
        }
1✔
333

334
        syncAnnotationsInRouteGroup := map[string]string{}
1✔
335
        if ssc.StackSet.Spec.RouteGroup != nil {
2✔
336
                syncAnnotationsInRouteGroup = getKeyValues(
1✔
337
                        ssc.ingressAnnotationsToSync,
1✔
338
                        ssc.StackSet.Spec.RouteGroup.Annotations,
1✔
339
                )
1✔
340
        }
1✔
341

342
        // if backendPort is not defined from Ingress or Routegroup fallback
343
        // to externalIngress if defined
344
        if ssc.StackSet.Spec.ExternalIngress != nil {
2✔
345
                ssc.externalIngressBackendPort = backendPort
1✔
346
        }
1✔
347

348
        var scaledownTTL time.Duration
1✔
349
        if ssc.StackSet.Spec.StackLifecycle.ScaledownTTLSeconds == nil {
2✔
350
                scaledownTTL = defaultScaledownTTL
1✔
351
        } else {
2✔
352
                scaledownTTL = time.Duration(*ssc.StackSet.Spec.StackLifecycle.ScaledownTTLSeconds) * time.Second
1✔
353
        }
1✔
354

355
        for _, sc := range ssc.StackContainers {
2✔
356
                sc.stacksetName = ssc.StackSet.Name
1✔
357
                sc.ingressAnnotationsToSync = ssc.ingressAnnotationsToSync
1✔
358
                sc.syncAnnotationsInIngress = syncAnnotationsInIngress
1✔
359
                sc.syncAnnotationsInRouteGroup = syncAnnotationsInRouteGroup
1✔
360
                sc.backendPort = backendPort
1✔
361
                sc.scaledownTTL = scaledownTTL
1✔
362
                sc.clusterDomains = ssc.clusterDomains
1✔
363
                err := sc.updateStackResources()
1✔
364
                if err != nil {
1✔
UNCOV
365
                        return err
×
UNCOV
366
                }
×
367

368
                sc.updateFromResources()
1✔
369
                // This is to support both central and segment-based traffic.
1✔
370
                if ssc.SupportsSegmentTraffic() {
1✔
UNCOV
371
                        sc.updateFromSegmentResources()
×
UNCOV
372
                }
×
373
        }
374

375
        // only populate traffic if traffic management is enabled
376
        if ssc.StackSet.Spec.Ingress != nil ||
1✔
377
                ssc.StackSet.Spec.RouteGroup != nil ||
1✔
378
                ssc.StackSet.Spec.ExternalIngress != nil {
2✔
379

1✔
380
                err := ssc.updateDesiredTraffic()
1✔
381
                if err != nil {
1✔
UNCOV
382
                        return err
×
UNCOV
383
                }
×
384
                return ssc.updateActualTraffic()
1✔
385
        }
386

387
        return nil
1✔
388
}
389

390
func (ssc *StackSetContainer) TrafficChanges() []TrafficChange {
1✔
391
        var result []TrafficChange
1✔
392

1✔
393
        for _, sc := range ssc.StackContainers {
2✔
394
                oldWeight := sc.currentActualTrafficWeight
1✔
395
                newWeight := sc.actualTrafficWeight
1✔
396

1✔
397
                if oldWeight != newWeight {
2✔
398
                        result = append(result, TrafficChange{
1✔
399
                                StackName:        sc.Name(),
1✔
400
                                OldTrafficWeight: oldWeight,
1✔
401
                                NewTrafficWeight: newWeight,
1✔
402
                        })
1✔
403
                }
1✔
404
        }
405

406
        sort.Slice(result, func(i, j int) bool {
2✔
407
                return result[i].StackName < result[j].StackName
1✔
408
        })
1✔
409
        return result
1✔
410
}
411

412
// updateStackResources writes sets the Ingress and RouteGroup
413
// resources, based on this container's Spec.
414
func (sc *StackContainer) updateStackResources() error {
1✔
415
        sc.ingressSpec = sc.Stack.Spec.Ingress
1✔
416
        sc.routeGroupSpec = sc.Stack.Spec.RouteGroup
1✔
417

1✔
418
        backendPort, err := findBackendPort(
1✔
419
                sc.ingressSpec,
1✔
420
                sc.routeGroupSpec,
1✔
421
                sc.Stack.Spec.ExternalIngress,
1✔
422
        )
1✔
423
        if err != nil {
1✔
UNCOV
424
                return err
×
UNCOV
425
        }
×
426

427
        if backendPort != nil {
2✔
428
                sc.backendPort = backendPort
1✔
429
        }
1✔
430

431
        return nil
1✔
432
}
433

434
func (sc *StackContainer) updateFromResources() {
1✔
435
        sc.stackReplicas = effectiveReplicas(sc.Stack.Spec.StackSpec.Replicas)
1✔
436

1✔
437
        var deploymentUpdated, serviceUpdated, ingressUpdated, routeGroupUpdated, hpaUpdated bool
1✔
438

1✔
439
        // deployment
1✔
440
        if sc.Resources.Deployment != nil {
2✔
441
                deployment := sc.Resources.Deployment
1✔
442
                sc.deploymentReplicas = effectiveReplicas(deployment.Spec.Replicas)
1✔
443
                sc.createdReplicas = deployment.Status.Replicas
1✔
444
                sc.readyReplicas = deployment.Status.ReadyReplicas
1✔
445
                sc.updatedReplicas = deployment.Status.UpdatedReplicas
1✔
446
                deploymentUpdated = IsResourceUpToDate(sc.Stack, sc.Resources.Deployment.ObjectMeta) && deployment.Status.ObservedGeneration == deployment.Generation
1✔
447
        }
1✔
448

449
        // service
450
        serviceUpdated = sc.Resources.Service != nil && IsResourceUpToDate(sc.Stack, sc.Resources.Service.ObjectMeta)
1✔
451

1✔
452
        // ingress: ignore if ingress is not set or check if we are up to date
1✔
453
        if sc.ingressSpec != nil {
2✔
454
                ingressUpdated = sc.Resources.Ingress != nil && IsResourceUpToDate(sc.Stack, sc.Resources.Ingress.ObjectMeta)
1✔
455
        } else {
2✔
456
                // ignore if ingress is not set
1✔
457
                ingressUpdated = sc.Resources.Ingress == nil
1✔
458
        }
1✔
459

460
        // routegroup: ignore if routegroup is not set or check if we are up to date
461
        if sc.routeGroupSpec != nil {
1✔
UNCOV
462
                routeGroupUpdated = sc.Resources.RouteGroup != nil && IsResourceUpToDate(sc.Stack, sc.Resources.RouteGroup.ObjectMeta)
×
463
        } else {
1✔
464
                // ignore if route group is not set
1✔
465
                routeGroupUpdated = sc.Resources.RouteGroup == nil
1✔
466
        }
1✔
467

468
        // hpa
469
        if sc.IsAutoscaled() {
2✔
470
                hpaUpdated = sc.Resources.HPA != nil && IsResourceUpToDate(sc.Stack, sc.Resources.HPA.ObjectMeta)
1✔
471
        } else {
2✔
472
                hpaUpdated = sc.Resources.HPA == nil
1✔
473
        }
1✔
474

475
        // aggregated 'resources updated' for the readiness
476
        sc.resourcesUpdated = deploymentUpdated &&
1✔
477
                serviceUpdated &&
1✔
478
                ingressUpdated &&
1✔
479
                routeGroupUpdated &&
1✔
480
                hpaUpdated
1✔
481

1✔
482
        status := sc.Stack.Status
1✔
483
        sc.noTrafficSince = unwrapTime(status.NoTrafficSince)
1✔
484
        if status.Prescaling.Active {
2✔
485
                sc.prescalingActive = true
1✔
486
                sc.prescalingReplicas = status.Prescaling.Replicas
1✔
487
                sc.prescalingDesiredTrafficWeight = status.Prescaling.DesiredTrafficWeight
1✔
488
                sc.prescalingLastTrafficIncrease = unwrapTime(status.Prescaling.LastTrafficIncrease)
1✔
489
        }
1✔
490
}
491

UNCOV
492
func (sc *StackContainer) updateFromSegmentResources() {
×
UNCOV
493
        var ingressSegmentUpdated, routeGroupSegmentUpdated bool
×
UNCOV
494

×
UNCOV
495
        // ingress: ignore if ingress is not set or check if we are up to date
×
UNCOV
496
        if sc.ingressSpec != nil {
×
UNCOV
497
                ingressSegmentUpdated = sc.Resources.IngressSegment != nil &&
×
UNCOV
498
                        IsResourceUpToDate(sc.Stack, sc.Resources.IngressSegment.ObjectMeta)
×
UNCOV
499
        } else {
×
UNCOV
500
                // ignore if ingress is not set
×
UNCOV
501
                ingressSegmentUpdated = sc.Resources.Ingress == nil
×
UNCOV
502
        }
×
503

504
        // routegroup: ignore if routegroup is not set or check if we are up to date
UNCOV
505
        if sc.routeGroupSpec != nil {
×
UNCOV
506
                routeGroupSegmentUpdated = sc.Resources.RouteGroupSegment != nil &&
×
UNCOV
507
                        IsResourceUpToDate(
×
UNCOV
508
                                sc.Stack,
×
UNCOV
509
                                sc.Resources.RouteGroupSegment.ObjectMeta,
×
UNCOV
510
                        )
×
UNCOV
511
        } else {
×
UNCOV
512
                // ignore if route group is not set
×
UNCOV
513
                routeGroupSegmentUpdated = sc.Resources.RouteGroup == nil
×
UNCOV
514
        }
×
515

UNCOV
516
        sc.resourcesUpdated = sc.resourcesUpdated &&
×
UNCOV
517
                ingressSegmentUpdated &&
×
UNCOV
518
                routeGroupSegmentUpdated
×
519
}
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