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

zalando-incubator / stackset-controller / 8633798479

10 Apr 2024 03:22PM UTC coverage: 50.624% (-1.2%) from 51.844%
8633798479

Pull #620

github

gargravarr
Remove central ingress/routegroup logic; remove ingress source switch ttl logic.

Signed-off-by: Rodrigo Reis <rodrigo.gargravarr@gmail.com>
Pull Request #620: Remove central ingress/routegroup logic

16 of 55 new or added lines in 4 files covered. (29.09%)

134 existing lines in 3 files now uncovered.

2839 of 5608 relevant lines covered (50.62%)

0.57 hits per line

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

88.31
/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
        // ingressAnnotationsToSync is a list of ingress annotations that should be
70
        // synchronized across all existing stacks.
71
        ingressAnnotationsToSync []string
72
}
73

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

81
        // PendingRemoval is set to true if the stack should be deleted
82
        PendingRemoval bool
83

84
        // Resources contains Kubernetes entities for the Stack's resources (Deployment, Ingress, etc)
85
        Resources StackResources
86

87
        // Fields from the parent stackset
88
        stacksetName   string
89
        scaledownTTL   time.Duration
90
        clusterDomains []string
91

92
        // Ingress annotations to synchronize
93
        ingressAnnotationsToSync []string
94

95
        // Ingress annotations present in the StackSet
96
        syncAnnotationsInIngress map[string]string
97

98
        // RouteGroup annotations present in the StackSet
99
        syncAnnotationsInRouteGroup map[string]string
100

101
        // Fields from the stack itself.
102
        ingressSpec    *zv1.StackSetIngressSpec
103
        routeGroupSpec *zv1.RouteGroupSpec
104
        backendPort    *intstr.IntOrString
105

106
        // Fields from the stack itself, with some defaults applied
107
        stackReplicas int32
108

109
        // Fields from the stack resources
110

111
        // Set to true only if all related resources have been updated according to the latest stack version
112
        resourcesUpdated bool
113

114
        // Current number of replicas that the deployment is expected to have, from Deployment.spec
115
        deploymentReplicas int32
116

117
        // Current number of replicas that the deployment has, from Deployment.status
118
        createdReplicas int32
119

120
        // Current number of replicas that the deployment has, from Deployment.status
121
        readyReplicas int32
122

123
        // Current number of up-to-date replicas that the deployment has, from Deployment.status
124
        updatedReplicas int32
125

126
        // When using traffic segments: the traffic segment associated to this stack
127
        segmentLowerLimit float64
128
        segmentUpperLimit float64
129

130
        // Traffic & scaling
131
        currentActualTrafficWeight     float64
132
        actualTrafficWeight            float64
133
        desiredTrafficWeight           float64
134
        noTrafficSince                 time.Time
135
        prescalingActive               bool
136
        prescalingReplicas             int32
137
        prescalingDesiredTrafficWeight float64
138
        prescalingLastTrafficIncrease  time.Time
139
        minReadyPercent                float64
140
}
141

142
// TrafficChange contains information about a traffic change event
143
type TrafficChange struct {
144
        StackName        string
145
        OldTrafficWeight float64
146
        NewTrafficWeight float64
147
}
148

149
func (tc TrafficChange) String() string {
×
150
        return fmt.Sprintf("%s: %.1f%% to %.1f%%", tc.StackName, tc.OldTrafficWeight, tc.NewTrafficWeight)
×
151
}
×
152

153
func (sc *StackContainer) HasBackendPort() bool {
1✔
154
        return sc.backendPort != nil
1✔
155
}
1✔
156

157
func (sc *StackContainer) HasTraffic() bool {
1✔
158
        return sc.actualTrafficWeight > 0 || sc.desiredTrafficWeight > 0
1✔
159
}
1✔
160

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

1✔
165
        // Stacks are considered ready when all subresources have been updated
1✔
166
        // and the minimum ready percentage is hit on and replicas
1✔
167
        return (sc.resourcesUpdated && sc.deploymentReplicas > 0 &&
1✔
168
                minRequiredReplicas <= sc.updatedReplicas &&
1✔
169
                minRequiredReplicas <= sc.readyReplicas)
1✔
170
}
1✔
171

172
func (sc *StackContainer) MaxReplicas() int32 {
1✔
173
        if sc.Stack.Spec.StackSpec.Autoscaler != nil {
2✔
174
                return sc.Stack.Spec.StackSpec.Autoscaler.MaxReplicas
1✔
175
        }
1✔
176
        return math.MaxInt32
1✔
177
}
178

179
func (sc *StackContainer) IsAutoscaled() bool {
1✔
180
        return sc.Stack.Spec.StackSpec.Autoscaler != nil
1✔
181
}
1✔
182

183
func (sc *StackContainer) ScaledDown() bool {
1✔
184
        if sc.HasTraffic() {
2✔
185
                return false
1✔
186
        }
1✔
187
        return !sc.noTrafficSince.IsZero() && time.Since(sc.noTrafficSince) > sc.scaledownTTL
1✔
188
}
189

190
func (sc *StackContainer) Name() string {
1✔
191
        return sc.Stack.Name
1✔
192
}
1✔
193

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

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

211
func NewContainer(
212
        stackset *zv1.StackSet,
213
        reconciler TrafficReconciler,
214
        backendWeightsAnnotationKey string,
215
        clusterDomains []string,
216
        syncIngressAnnotations []string,
NEW
217
) *StackSetContainer {
×
218
        return &StackSetContainer{
×
219
                StackSet:                    stackset,
×
220
                StackContainers:             map[types.UID]*StackContainer{},
×
221
                TrafficReconciler:           reconciler,
×
222
                backendWeightsAnnotationKey: backendWeightsAnnotationKey,
×
223
                clusterDomains:              clusterDomains,
×
NEW
224
                ingressAnnotationsToSync:    syncIngressAnnotations,
×
225
        }
×
226
}
×
227

228
func (ssc *StackSetContainer) stackByName(name string) *StackContainer {
1✔
229
        for _, container := range ssc.StackContainers {
2✔
230
                if container.Name() == name {
2✔
231
                        return container
1✔
232
                }
1✔
233
        }
234
        return nil
1✔
235
}
236

237
// updateDesiredTraffic gets desired from stackset spec
238
// and populates it to stack containers
239
func (ssc *StackSetContainer) updateDesiredTraffic() error {
1✔
240
        weights := make(map[string]float64)
1✔
241

1✔
242
        for _, desiredTraffic := range ssc.StackSet.Spec.Traffic {
2✔
243
                weights[desiredTraffic.StackName] = desiredTraffic.Weight
1✔
244
        }
1✔
245

246
        // filter stacks and normalize weights
247
        stacksetNames := make(map[string]struct{})
1✔
248
        for _, sc := range ssc.StackContainers {
2✔
249
                stacksetNames[sc.Name()] = struct{}{}
1✔
250
        }
1✔
251
        for name := range weights {
2✔
252
                if _, ok := stacksetNames[name]; !ok {
2✔
253
                        delete(weights, name)
1✔
254
                }
1✔
255
        }
256

257
        if !allZero(weights) {
2✔
258
                normalizeWeights(weights)
1✔
259
        }
1✔
260

261
        // save values in stack containers
262
        for _, container := range ssc.StackContainers {
2✔
263
                container.desiredTrafficWeight = weights[container.Name()]
1✔
264
        }
1✔
265

266
        return nil
1✔
267
}
268

269
// updateActualTraffic gets actual from stackset status
270
// and populates it to stack containers
271
func (ssc *StackSetContainer) updateActualTraffic() error {
1✔
272
        weights := make(map[string]float64)
1✔
273
        for _, actualTraffic := range ssc.StackSet.Status.Traffic {
2✔
274
                weights[actualTraffic.ServiceName] = actualTraffic.Weight
1✔
275
        }
1✔
276

277
        // filter stacks and normalize weights
278
        stacksetNames := make(map[string]struct{})
1✔
279
        for _, sc := range ssc.StackContainers {
2✔
280
                stacksetNames[sc.Name()] = struct{}{}
1✔
281
        }
1✔
282
        for name := range weights {
2✔
283
                if _, ok := stacksetNames[name]; !ok {
2✔
284
                        delete(weights, name)
1✔
285
                }
1✔
286
        }
287
        if !allZero(weights) {
2✔
288
                normalizeWeights(weights)
1✔
289
        }
1✔
290

291
        // save values in stack containers
292
        for _, container := range ssc.StackContainers {
2✔
293
                container.actualTrafficWeight = weights[container.Name()]
1✔
294
                container.currentActualTrafficWeight = weights[container.Name()]
1✔
295
        }
1✔
296

297
        return nil
1✔
298
}
299

300
// UpdateFromResources populates stack state information (e.g. replica counts or
301
// traffic) from related resources
302
func (ssc *StackSetContainer) UpdateFromResources() error {
1✔
303
        if len(ssc.StackContainers) == 0 {
1✔
304
                return nil
×
305
        }
×
306

307
        backendPort, err := findBackendPort(
1✔
308
                ssc.StackSet.Spec.Ingress,
1✔
309
                ssc.StackSet.Spec.RouteGroup,
1✔
310
                ssc.StackSet.Spec.ExternalIngress,
1✔
311
        )
1✔
312
        if err != nil {
1✔
313
                return err
×
314
        }
×
315

316
        syncAnnotationsInIngress := map[string]string{}
1✔
317
        if ssc.StackSet.Spec.Ingress != nil {
2✔
318
                syncAnnotationsInIngress = getKeyValues(
1✔
319
                        ssc.ingressAnnotationsToSync,
1✔
320
                        ssc.StackSet.Spec.Ingress.Annotations,
1✔
321
                )
1✔
322
        }
1✔
323

324
        syncAnnotationsInRouteGroup := map[string]string{}
1✔
325
        if ssc.StackSet.Spec.RouteGroup != nil {
2✔
326
                syncAnnotationsInRouteGroup = getKeyValues(
1✔
327
                        ssc.ingressAnnotationsToSync,
1✔
328
                        ssc.StackSet.Spec.RouteGroup.Annotations,
1✔
329
                )
1✔
330
        }
1✔
331

332
        // if backendPort is not defined from Ingress or Routegroup fallback
333
        // to externalIngress if defined
334
        if ssc.StackSet.Spec.ExternalIngress != nil {
2✔
335
                ssc.externalIngressBackendPort = backendPort
1✔
336
        }
1✔
337

338
        var scaledownTTL time.Duration
1✔
339
        if ssc.StackSet.Spec.StackLifecycle.ScaledownTTLSeconds == nil {
2✔
340
                scaledownTTL = defaultScaledownTTL
1✔
341
        } else {
2✔
342
                scaledownTTL = time.Duration(*ssc.StackSet.Spec.StackLifecycle.ScaledownTTLSeconds) * time.Second
1✔
343
        }
1✔
344

345
        for _, sc := range ssc.StackContainers {
2✔
346
                sc.stacksetName = ssc.StackSet.Name
1✔
347
                sc.ingressAnnotationsToSync = ssc.ingressAnnotationsToSync
1✔
348
                sc.syncAnnotationsInIngress = syncAnnotationsInIngress
1✔
349
                sc.syncAnnotationsInRouteGroup = syncAnnotationsInRouteGroup
1✔
350
                sc.backendPort = backendPort
1✔
351
                sc.scaledownTTL = scaledownTTL
1✔
352
                sc.clusterDomains = ssc.clusterDomains
1✔
353
                err := sc.updateStackResources()
1✔
354
                if err != nil {
1✔
355
                        return err
×
356
                }
×
357

358
                sc.updateFromResources()
1✔
359
        }
360

361
        // only populate traffic if traffic management is enabled
362
        if ssc.StackSet.Spec.Ingress != nil ||
1✔
363
                ssc.StackSet.Spec.RouteGroup != nil ||
1✔
364
                ssc.StackSet.Spec.ExternalIngress != nil {
2✔
365

1✔
366
                err := ssc.updateDesiredTraffic()
1✔
367
                if err != nil {
1✔
368
                        return err
×
369
                }
×
370
                return ssc.updateActualTraffic()
1✔
371
        }
372

373
        return nil
1✔
374
}
375

376
func (ssc *StackSetContainer) TrafficChanges() []TrafficChange {
1✔
377
        var result []TrafficChange
1✔
378

1✔
379
        for _, sc := range ssc.StackContainers {
2✔
380
                oldWeight := sc.currentActualTrafficWeight
1✔
381
                newWeight := sc.actualTrafficWeight
1✔
382

1✔
383
                if oldWeight != newWeight {
2✔
384
                        result = append(result, TrafficChange{
1✔
385
                                StackName:        sc.Name(),
1✔
386
                                OldTrafficWeight: oldWeight,
1✔
387
                                NewTrafficWeight: newWeight,
1✔
388
                        })
1✔
389
                }
1✔
390
        }
391

392
        sort.Slice(result, func(i, j int) bool {
2✔
393
                return result[i].StackName < result[j].StackName
1✔
394
        })
1✔
395
        return result
1✔
396
}
397

398
// updateStackResources writes sets the Ingress and RouteGroup
399
// resources, based on this container's Spec.
400
func (sc *StackContainer) updateStackResources() error {
1✔
401
        sc.ingressSpec = sc.Stack.Spec.Ingress
1✔
402
        sc.routeGroupSpec = sc.Stack.Spec.RouteGroup
1✔
403

1✔
404
        backendPort, err := findBackendPort(
1✔
405
                sc.ingressSpec,
1✔
406
                sc.routeGroupSpec,
1✔
407
                sc.Stack.Spec.ExternalIngress,
1✔
408
        )
1✔
409
        if err != nil {
1✔
410
                return err
×
411
        }
×
412

413
        if backendPort != nil {
2✔
414
                sc.backendPort = backendPort
1✔
415
        }
1✔
416

417
        return nil
1✔
418
}
419

420
func (sc *StackContainer) updateFromResources() {
1✔
421
        sc.stackReplicas = effectiveReplicas(sc.Stack.Spec.StackSpec.Replicas)
1✔
422

1✔
423
        var deploymentUpdated, serviceUpdated, ingressUpdated, routeGroupUpdated, hpaUpdated bool
1✔
424
        var ingressSegmentUpdated, routeGroupSegmentUpdated bool
1✔
425

1✔
426
        // deployment
1✔
427
        if sc.Resources.Deployment != nil {
2✔
428
                deployment := sc.Resources.Deployment
1✔
429
                sc.deploymentReplicas = effectiveReplicas(deployment.Spec.Replicas)
1✔
430
                sc.createdReplicas = deployment.Status.Replicas
1✔
431
                sc.readyReplicas = deployment.Status.ReadyReplicas
1✔
432
                sc.updatedReplicas = deployment.Status.UpdatedReplicas
1✔
433
                deploymentUpdated = IsResourceUpToDate(sc.Stack, sc.Resources.Deployment.ObjectMeta) && deployment.Status.ObservedGeneration == deployment.Generation
1✔
434
        }
1✔
435

436
        // service
437
        serviceUpdated = sc.Resources.Service != nil && IsResourceUpToDate(sc.Stack, sc.Resources.Service.ObjectMeta)
1✔
438

1✔
439
        // ingress: ignore if ingress is not set or check if we are up to date
1✔
440
        if sc.ingressSpec != nil {
2✔
441
                ingressUpdated = sc.Resources.Ingress != nil && IsResourceUpToDate(sc.Stack, sc.Resources.Ingress.ObjectMeta)
1✔
442
                ingressSegmentUpdated = sc.Resources.IngressSegment != nil &&
1✔
443
                        IsResourceUpToDate(sc.Stack, sc.Resources.IngressSegment.ObjectMeta)
1✔
444
        } else {
2✔
445
                // ignore if ingress is not set
1✔
446
                ingressUpdated = sc.Resources.Ingress == nil
1✔
447
                ingressSegmentUpdated = sc.Resources.Ingress == nil
1✔
448
        }
1✔
449

450
        // routegroup: ignore if routegroup is not set or check if we are up to date
451
        if sc.routeGroupSpec != nil {
1✔
452
                routeGroupUpdated = sc.Resources.RouteGroup != nil && IsResourceUpToDate(sc.Stack, sc.Resources.RouteGroup.ObjectMeta)
×
NEW
453
                routeGroupSegmentUpdated = sc.Resources.RouteGroupSegment != nil &&
×
NEW
454
                        IsResourceUpToDate(
×
NEW
455
                                sc.Stack,
×
NEW
456
                                sc.Resources.RouteGroupSegment.ObjectMeta,
×
NEW
457
                        )
×
458
        } else {
1✔
459
                // ignore if route group is not set
1✔
460
                routeGroupUpdated = sc.Resources.RouteGroup == nil
1✔
461
                routeGroupSegmentUpdated = sc.Resources.RouteGroup == nil
1✔
462
        }
1✔
463

464
        // hpa
465
        if sc.IsAutoscaled() {
2✔
466
                hpaUpdated = sc.Resources.HPA != nil && IsResourceUpToDate(sc.Stack, sc.Resources.HPA.ObjectMeta)
1✔
467
        } else {
2✔
468
                hpaUpdated = sc.Resources.HPA == nil
1✔
469
        }
1✔
470

471
        // aggregated 'resources updated' for the readiness
472
        sc.resourcesUpdated = deploymentUpdated &&
1✔
473
                serviceUpdated &&
1✔
474
                ingressUpdated &&
1✔
475
                routeGroupUpdated &&
1✔
476
                hpaUpdated &&
1✔
477
                ingressSegmentUpdated &&
1✔
478
                routeGroupSegmentUpdated
1✔
479

1✔
480
        status := sc.Stack.Status
1✔
481
        sc.noTrafficSince = unwrapTime(status.NoTrafficSince)
1✔
482
        if status.Prescaling.Active {
2✔
483
                sc.prescalingActive = true
1✔
484
                sc.prescalingReplicas = status.Prescaling.Replicas
1✔
485
                sc.prescalingDesiredTrafficWeight = status.Prescaling.DesiredTrafficWeight
1✔
486
                sc.prescalingLastTrafficIncrease = unwrapTime(status.Prescaling.LastTrafficIncrease)
1✔
487
        }
1✔
488
}
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