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

zalando-incubator / stackset-controller / 19535583719

20 Nov 2025 11:38AM UTC coverage: 50.085% (+0.3%) from 49.791%
19535583719

Pull #716

github

szuecs
cleanup routegroup generation test

Signed-off-by: Sandor Szücs <sandor.szuecs@zalando.de>
Pull Request #716: feature: zalando.org/forward-backend annotation support to enable migration to eks

43 of 50 new or added lines in 3 files covered. (86.0%)

2 existing lines in 1 file now uncovered.

2663 of 5317 relevant lines covered (50.08%)

0.56 hits per line

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

92.16
/pkg/core/stackset.go
1
package core
2

3
import (
4
        "encoding/json"
5
        "errors"
6
        "sort"
7

8
        rgv1 "github.com/szuecs/routegroup-client/apis/zalando.org/v1"
9
        zv1 "github.com/zalando-incubator/stackset-controller/pkg/apis/zalando.org/v1"
10
        corev1 "k8s.io/api/core/v1"
11
        networking "k8s.io/api/networking/v1"
12
        metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
13
)
14

15
const (
16
        StacksetHeritageLabelKey = "stackset"
17
        StackVersionLabelKey     = "stack-version"
18

19
        ingressTrafficAuthoritativeAnnotation = "zalando.org/traffic-authoritative"
20
        forwardBackendAnnotation              = "zalando.org/forward-backend"
21
        forwardBackendName                    = "fwd"
22
)
23

24
var (
25
        errNoPaths             = errors.New("invalid ingress, no paths defined")
26
        errNoStacks            = errors.New("no stacks to assign traffic to")
27
        errStackServiceBackend = errors.New("additionalBackends must not reference a Stack Service")
28
)
29

30
func currentStackVersion(stackset *zv1.StackSet) string {
1✔
31
        version := stackset.Spec.StackTemplate.Spec.Version
1✔
32
        if version == "" {
1✔
33
                version = defaultVersion
×
34
        }
×
35
        return version
1✔
36
}
37

38
func generateStackName(stackset *zv1.StackSet, version string) string {
1✔
39
        return stackset.Name + "-" + version
1✔
40
}
1✔
41

42
// sanitizeServicePorts makes sure the ports has the default fields set if not
43
// specified.
44
func sanitizeServicePorts(service *zv1.StackServiceSpec) *zv1.StackServiceSpec {
1✔
45
        for i, port := range service.Ports {
2✔
46
                // set default protocol if not specified
1✔
47
                if port.Protocol == "" {
2✔
48
                        port.Protocol = corev1.ProtocolTCP
1✔
49
                }
1✔
50
                service.Ports[i] = port
1✔
51
        }
52
        return service
1✔
53
}
54

55
// NewStack returns an (optional) stack that should be created
56
func (ssc *StackSetContainer) NewStack() (*StackContainer, string) {
1✔
57
        _, forwardMigration := ssc.StackSet.ObjectMeta.Annotations[forwardBackendAnnotation]
1✔
58
        observedStackVersion := ssc.StackSet.Status.ObservedStackVersion
1✔
59
        stackVersion := currentStackVersion(ssc.StackSet)
1✔
60
        stackName := generateStackName(ssc.StackSet, stackVersion)
1✔
61
        stack := ssc.stackByName(stackName)
1✔
62

1✔
63
        // If the current stack doesn't exist, check that we haven't created it
1✔
64
        // before. We shouldn't recreate it if it was removed for any reason.
1✔
65
        if stack == nil && observedStackVersion != stackVersion {
2✔
66
                spec := &zv1.StackSpecInternal{}
1✔
67

1✔
68
                parentSpec := ssc.StackSet.Spec.StackTemplate.Spec.StackSpec.DeepCopy()
1✔
69
                if parentSpec.Service != nil {
1✔
70
                        parentSpec.Service = sanitizeServicePorts(parentSpec.Service)
×
71
                }
×
72
                spec.StackSpec = *parentSpec
1✔
73

1✔
74
                if ssc.StackSet.Spec.Ingress != nil {
1✔
75
                        spec.Ingress = ssc.StackSet.Spec.Ingress.DeepCopy()
×
76
                }
×
77

78
                if ssc.StackSet.Spec.ExternalIngress != nil {
1✔
79
                        spec.ExternalIngress = ssc.StackSet.Spec.ExternalIngress.DeepCopy()
×
80
                }
×
81

82
                if ssc.StackSet.Spec.RouteGroup != nil {
1✔
83
                        spec.RouteGroup = ssc.StackSet.Spec.RouteGroup.DeepCopy()
×
84
                }
×
85

86
                stackAnnotations := make(map[string]string)
1✔
87
                if a := ssc.StackSet.Spec.StackTemplate.Annotations; a != nil {
1✔
NEW
88
                        stackAnnotations = a
×
NEW
89
                }
×
90

91
                if forwardMigration {
1✔
NEW
92
                        stackAnnotations[forwardBackendAnnotation] = forwardBackendName
×
NEW
93
                }
×
94
                if len(stackAnnotations) == 0 {
2✔
95
                        stackAnnotations = nil
1✔
96
                }
1✔
97

98
                return &StackContainer{
1✔
99
                        Stack: &zv1.Stack{
1✔
100
                                ObjectMeta: metav1.ObjectMeta{
1✔
101
                                        Name:      stackName,
1✔
102
                                        Namespace: ssc.StackSet.Namespace,
1✔
103
                                        OwnerReferences: []metav1.OwnerReference{
1✔
104
                                                {
1✔
105
                                                        APIVersion: ssc.StackSet.APIVersion,
1✔
106
                                                        Kind:       ssc.StackSet.Kind,
1✔
107
                                                        Name:       ssc.StackSet.Name,
1✔
108
                                                        UID:        ssc.StackSet.UID,
1✔
109
                                                },
1✔
110
                                        },
1✔
111
                                        Labels: mergeLabels(
1✔
112
                                                map[string]string{
1✔
113
                                                        StacksetHeritageLabelKey: ssc.StackSet.Name,
1✔
114
                                                },
1✔
115
                                                ssc.StackSet.Labels,
1✔
116
                                                map[string]string{StackVersionLabelKey: stackVersion},
1✔
117
                                        ),
1✔
118
                                        Annotations: stackAnnotations,
1✔
119
                                },
1✔
120
                                Spec: *spec,
1✔
121
                        },
1✔
122
                }, stackVersion
1✔
123
        }
124

125
        return nil, ""
1✔
126
}
127

128
// MarkExpiredStacks marks stacks that should be deleted
129
func (ssc *StackSetContainer) MarkExpiredStacks() {
1✔
130
        historyLimit := defaultStackLifecycleLimit
1✔
131
        if ssc.StackSet.Spec.StackLifecycle.Limit != nil {
2✔
132
                historyLimit = int(*ssc.StackSet.Spec.StackLifecycle.Limit)
1✔
133
        }
1✔
134

135
        gcCandidates := make([]*StackContainer, 0, len(ssc.StackContainers))
1✔
136

1✔
137
        for _, sc := range ssc.StackContainers {
2✔
138
                // Stacks are considered for cleanup if we don't have RouteGroup nor an ingress or if the stack is scaled down because of inactivity
1✔
139
                hasIngress := sc.routeGroupSpec != nil || sc.ingressSpec != nil || ssc.StackSet.Spec.ExternalIngress != nil
1✔
140
                if !hasIngress || sc.ScaledDown() {
2✔
141
                        gcCandidates = append(gcCandidates, sc)
1✔
142
                }
1✔
143
        }
144

145
        // only garbage collect if history limit is reached
146
        if len(gcCandidates) <= historyLimit {
2✔
147
                return
1✔
148
        }
1✔
149

150
        // sort candidates by when they last had traffic.
151
        sort.Slice(gcCandidates, func(i, j int) bool {
2✔
152
                // First check if NoTrafficSince is set. If not, fall back to the creation timestamp
1✔
153
                iTime := gcCandidates[i].noTrafficSince
1✔
154
                if iTime.IsZero() {
2✔
155
                        iTime = gcCandidates[i].Stack.CreationTimestamp.Time
1✔
156
                }
1✔
157

158
                jTime := gcCandidates[j].noTrafficSince
1✔
159
                if jTime.IsZero() {
2✔
160
                        jTime = gcCandidates[j].Stack.CreationTimestamp.Time
1✔
161
                }
1✔
162
                return iTime.Before(jTime)
1✔
163
        })
164

165
        excessStacks := len(gcCandidates) - historyLimit
1✔
166
        for _, sc := range gcCandidates[:excessStacks] {
2✔
167
                sc.PendingRemoval = true
1✔
168
        }
1✔
169
}
170

171
func (ssc *StackSetContainer) GenerateRouteGroup() (*rgv1.RouteGroup, error) {
1✔
172
        stackset := ssc.StackSet
1✔
173
        if stackset.Spec.RouteGroup == nil {
1✔
174
                return nil, nil
×
175
        }
×
176

177
        labels := mergeLabels(
1✔
178
                map[string]string{StacksetHeritageLabelKey: stackset.Name},
1✔
179
                stackset.Labels,
1✔
180
        )
1✔
181

1✔
182
        result := &rgv1.RouteGroup{
1✔
183
                ObjectMeta: metav1.ObjectMeta{
1✔
184
                        Name:        stackset.Name,
1✔
185
                        Namespace:   stackset.Namespace,
1✔
186
                        Labels:      labels,
1✔
187
                        Annotations: stackset.Spec.RouteGroup.Annotations,
1✔
188
                        OwnerReferences: []metav1.OwnerReference{
1✔
189
                                {
1✔
190
                                        APIVersion: stackset.APIVersion,
1✔
191
                                        Kind:       stackset.Kind,
1✔
192
                                        Name:       stackset.Name,
1✔
193
                                        UID:        stackset.UID,
1✔
194
                                },
1✔
195
                        },
1✔
196
                },
1✔
197
                Spec: rgv1.RouteGroupSpec{
1✔
198
                        Hosts:    stackset.Spec.RouteGroup.Hosts,
1✔
199
                        Backends: make([]rgv1.RouteGroupBackend, 0, len(ssc.StackContainers)),
1✔
200
                        Routes:   stackset.Spec.RouteGroup.Routes,
1✔
201
                },
1✔
202
        }
1✔
203

1✔
204
        // Generate backends
1✔
205
        stacks := make(map[string]struct{}, len(ssc.StackContainers))
1✔
206
        for _, sc := range ssc.StackContainers {
2✔
207
                stacks[sc.Name()] = struct{}{}
1✔
208
                result.Spec.Backends = append(result.Spec.Backends, rgv1.RouteGroupBackend{
1✔
209
                        Name:        sc.Name(),
1✔
210
                        Type:        rgv1.ServiceRouteGroupBackend,
1✔
211
                        ServiceName: sc.Name(),
1✔
212
                        ServicePort: stackset.Spec.RouteGroup.BackendPort,
1✔
213
                        Algorithm:   stackset.Spec.RouteGroup.LBAlgorithm,
1✔
214
                })
1✔
215
                if sc.actualTrafficWeight > 0 {
2✔
216
                        result.Spec.DefaultBackends = append(result.Spec.DefaultBackends, rgv1.RouteGroupBackendReference{
1✔
217
                                BackendName: sc.Name(),
1✔
218
                                Weight:      int(sc.actualTrafficWeight),
1✔
219
                        })
1✔
220
                }
1✔
221
        }
222

223
        // validate that additional backends don't overlap with the generated
224
        // backends.
225
        for _, additionalBackend := range stackset.Spec.RouteGroup.AdditionalBackends {
2✔
226
                if _, ok := stacks[additionalBackend.Name]; ok {
1✔
227
                        return nil, errStackServiceBackend
×
228
                }
×
229
                if _, ok := stacks[additionalBackend.ServiceName]; ok {
1✔
230
                        return nil, errStackServiceBackend
×
231
                }
×
232
                result.Spec.Backends = append(result.Spec.Backends, additionalBackend)
1✔
233
        }
234

235
        // sort backends/defaultBackends to ensure have a consistent generated RoutGroup resource
236
        sort.Slice(result.Spec.Backends, func(i, j int) bool {
2✔
237
                return result.Spec.Backends[i].Name < result.Spec.Backends[j].Name
1✔
238
        })
1✔
239
        sort.Slice(result.Spec.DefaultBackends, func(i, j int) bool {
2✔
240
                return result.Spec.DefaultBackends[i].BackendName < result.Spec.DefaultBackends[j].BackendName
1✔
241
        })
1✔
242

243
        return result, nil
1✔
244
}
245

246
func (ssc *StackSetContainer) GenerateIngress() (*networking.Ingress, error) {
1✔
247
        stackset := ssc.StackSet
1✔
248
        if stackset.Spec.Ingress == nil {
2✔
249
                return nil, nil
1✔
250
        }
1✔
251

252
        labels := mergeLabels(
1✔
253
                map[string]string{StacksetHeritageLabelKey: stackset.Name},
1✔
254
                stackset.Labels,
1✔
255
        )
1✔
256

1✔
257
        trafficAuthoritative := map[string]string{
1✔
258
                ingressTrafficAuthoritativeAnnotation: "false",
1✔
259
        }
1✔
260

1✔
261
        result := &networking.Ingress{
1✔
262
                ObjectMeta: metav1.ObjectMeta{
1✔
263
                        Name:        stackset.Name,
1✔
264
                        Namespace:   stackset.Namespace,
1✔
265
                        Labels:      labels,
1✔
266
                        Annotations: mergeLabels(stackset.Spec.Ingress.Annotations, trafficAuthoritative),
1✔
267
                        OwnerReferences: []metav1.OwnerReference{
1✔
268
                                {
1✔
269
                                        APIVersion: stackset.APIVersion,
1✔
270
                                        Kind:       stackset.Kind,
1✔
271
                                        Name:       stackset.Name,
1✔
272
                                        UID:        stackset.UID,
1✔
273
                                },
1✔
274
                        },
1✔
275
                },
1✔
276
                Spec: networking.IngressSpec{
1✔
277
                        Rules: make([]networking.IngressRule, 0),
1✔
278
                },
1✔
279
        }
1✔
280

1✔
281
        rule := networking.IngressRule{
1✔
282
                IngressRuleValue: networking.IngressRuleValue{
1✔
283
                        HTTP: &networking.HTTPIngressRuleValue{
1✔
284
                                Paths: make([]networking.HTTPIngressPath, 0),
1✔
285
                        },
1✔
286
                },
1✔
287
        }
1✔
288

1✔
289
        actualWeights := make(map[string]float64)
1✔
290

1✔
291
        for _, sc := range ssc.StackContainers {
2✔
292
                if sc.actualTrafficWeight > 0 {
2✔
293
                        actualWeights[sc.Name()] = sc.actualTrafficWeight
1✔
294

1✔
295
                        rule.IngressRuleValue.HTTP.Paths = append(rule.IngressRuleValue.HTTP.Paths, networking.HTTPIngressPath{
1✔
296
                                Path:     stackset.Spec.Ingress.Path,
1✔
297
                                PathType: &PathTypeImplementationSpecific,
1✔
298
                                Backend: networking.IngressBackend{
1✔
299
                                        Service: &networking.IngressServiceBackend{
1✔
300
                                                Name: sc.Name(),
1✔
301
                                                Port: networking.ServiceBackendPort{
1✔
302
                                                        Number: stackset.Spec.Ingress.BackendPort.IntVal,
1✔
303
                                                        Name:   stackset.Spec.Ingress.BackendPort.StrVal,
1✔
304
                                                },
1✔
305
                                        },
1✔
306
                                },
1✔
307
                        })
1✔
308
                }
1✔
309
        }
310

311
        if len(rule.IngressRuleValue.HTTP.Paths) == 0 {
1✔
312
                return nil, errNoPaths
×
313
        }
×
314

315
        // sort backends by name to have a consistent generated ingress resource.
316
        sort.Slice(rule.IngressRuleValue.HTTP.Paths, func(i, j int) bool {
2✔
317
                return rule.IngressRuleValue.HTTP.Paths[i].Backend.Service.Name < rule.IngressRuleValue.HTTP.Paths[j].Backend.Service.Name
1✔
318
        })
1✔
319

320
        // create rule per hostname
321
        for _, host := range stackset.Spec.Ingress.Hosts {
2✔
322
                r := rule
1✔
323
                r.Host = host
1✔
324
                result.Spec.Rules = append(result.Spec.Rules, r)
1✔
325
        }
1✔
326

327
        // sort rules by host to have a consistent generated ingress resource.
328
        sort.Slice(result.Spec.Rules, func(i, j int) bool {
2✔
329
                return result.Spec.Rules[i].Host < result.Spec.Rules[j].Host
1✔
330
        })
1✔
331

332
        actualWeightsData, err := json.Marshal(&actualWeights)
1✔
333
        if err != nil {
1✔
334
                return nil, err
×
335
        }
×
336

337
        result.Annotations[ssc.backendWeightsAnnotationKey] = string(actualWeightsData)
1✔
338
        return result, nil
1✔
339
}
340

341
func (ssc *StackSetContainer) GenerateStackSetStatus() *zv1.StackSetStatus {
1✔
342
        result := &zv1.StackSetStatus{
1✔
343
                Stacks:               0,
1✔
344
                ReadyStacks:          0,
1✔
345
                StacksWithTraffic:    0,
1✔
346
                ObservedStackVersion: ssc.StackSet.Status.ObservedStackVersion,
1✔
347
        }
1✔
348
        var traffic []*zv1.ActualTraffic
1✔
349

1✔
350
        for _, sc := range ssc.StackContainers {
2✔
351
                if sc.PendingRemoval {
2✔
352
                        continue
1✔
353
                }
354
                if sc.HasBackendPort() {
2✔
355
                        t := &zv1.ActualTraffic{
1✔
356
                                StackName:   sc.Name(),
1✔
357
                                ServiceName: sc.Name(),
1✔
358
                                ServicePort: *sc.backendPort,
1✔
359
                                Weight:      sc.actualTrafficWeight,
1✔
360
                        }
1✔
361
                        traffic = append(traffic, t)
1✔
362
                }
1✔
363

364
                result.Stacks += 1
1✔
365
                if sc.HasTraffic() {
2✔
366
                        result.StacksWithTraffic += 1
1✔
367
                }
1✔
368
                if sc.IsReady() {
2✔
369
                        result.ReadyStacks += 1
1✔
370
                }
1✔
371
        }
372
        sort.Slice(traffic, func(i, j int) bool {
2✔
373
                return traffic[i].StackName < traffic[j].StackName
1✔
374
        })
1✔
375
        result.Traffic = traffic
1✔
376
        return result
1✔
377
}
378

379
func (ssc *StackSetContainer) GenerateStackSetTraffic() []*zv1.DesiredTraffic {
1✔
380
        var traffic []*zv1.DesiredTraffic
1✔
381
        for _, sc := range ssc.StackContainers {
2✔
382
                if sc.PendingRemoval {
2✔
383
                        continue
1✔
384
                }
385
                if sc.HasBackendPort() && sc.desiredTrafficWeight > 0 {
2✔
386
                        t := &zv1.DesiredTraffic{
1✔
387
                                StackName: sc.Name(),
1✔
388
                                Weight:    sc.desiredTrafficWeight,
1✔
389
                        }
1✔
390
                        traffic = append(traffic, t)
1✔
391
                }
1✔
392
        }
393
        sort.Slice(traffic, func(i, j int) bool {
2✔
394
                return traffic[i].StackName < traffic[j].StackName
1✔
395
        })
1✔
396
        return traffic
1✔
397
}
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