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

zalando-incubator / stackset-controller / 20348603794

18 Dec 2025 07:21PM UTC coverage: 49.888% (-0.2%) from 50.085%
20348603794

Pull #721

github

mikkeloscar
Improve forward feature

Signed-off-by: Mikkel Oscar Lyderik Larsen <mikkel.larsen@zalando.de>
Pull Request #721: Improve forward feature

14 of 37 new or added lines in 4 files covered. (37.84%)

138 existing lines in 3 files now uncovered.

2662 of 5336 relevant lines covered (49.89%)

0.56 hits per line

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

92.03
/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✔
UNCOV
70
                        parentSpec.Service = sanitizeServicePorts(parentSpec.Service)
×
UNCOV
71
                }
×
72
                spec.StackSpec = *parentSpec
1✔
73

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

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

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

86
                if forwardMigration {
1✔
87
                        if ssc.StackSet.Spec.StackTemplate.Annotations == nil {
×
UNCOV
88
                                ssc.StackSet.Spec.StackTemplate.Annotations = make(map[string]string)
×
UNCOV
89
                        }
×
90
                        ssc.StackSet.Spec.StackTemplate.Annotations[forwardBackendAnnotation] = forwardBackendName
×
91
                }
92

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

120
        return nil, ""
1✔
121
}
122

123
// MarkExpiredStacks marks stacks that should be deleted
124
func (ssc *StackSetContainer) MarkExpiredStacks() {
1✔
125
        historyLimit := defaultStackLifecycleLimit
1✔
126
        if ssc.StackSet.Spec.StackLifecycle.Limit != nil {
2✔
127
                historyLimit = int(*ssc.StackSet.Spec.StackLifecycle.Limit)
1✔
128
        }
1✔
129

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

1✔
132
        for _, sc := range ssc.StackContainers {
2✔
133
                // 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✔
134
                hasIngress := sc.routeGroupSpec != nil || sc.ingressSpec != nil || ssc.StackSet.Spec.ExternalIngress != nil
1✔
135
                if !hasIngress || sc.ScaledDown() {
2✔
136
                        gcCandidates = append(gcCandidates, sc)
1✔
137
                }
1✔
138
        }
139

140
        // only garbage collect if history limit is reached
141
        if len(gcCandidates) <= historyLimit {
2✔
142
                return
1✔
143
        }
1✔
144

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

153
                jTime := gcCandidates[j].noTrafficSince
1✔
154
                if jTime.IsZero() {
2✔
155
                        jTime = gcCandidates[j].Stack.CreationTimestamp.Time
1✔
156
                }
1✔
157
                return iTime.Before(jTime)
1✔
158
        })
159

160
        excessStacks := len(gcCandidates) - historyLimit
1✔
161
        for _, sc := range gcCandidates[:excessStacks] {
2✔
162
                sc.PendingRemoval = true
1✔
163
        }
1✔
164
}
165

166
func (ssc *StackSetContainer) GenerateRouteGroup() (*rgv1.RouteGroup, error) {
1✔
167
        stackset := ssc.StackSet
1✔
168
        if stackset.Spec.RouteGroup == nil {
1✔
UNCOV
169
                return nil, nil
×
UNCOV
170
        }
×
171

172
        labels := mergeLabels(
1✔
173
                map[string]string{StacksetHeritageLabelKey: stackset.Name},
1✔
174
                stackset.Labels,
1✔
175
        )
1✔
176

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

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

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

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

238
        return result, nil
1✔
239
}
240

241
func (ssc *StackSetContainer) GenerateIngress() (*networking.Ingress, error) {
1✔
242
        stackset := ssc.StackSet
1✔
243
        if stackset.Spec.Ingress == nil {
2✔
244
                return nil, nil
1✔
245
        }
1✔
246

247
        labels := mergeLabels(
1✔
248
                map[string]string{StacksetHeritageLabelKey: stackset.Name},
1✔
249
                stackset.Labels,
1✔
250
        )
1✔
251

1✔
252
        trafficAuthoritative := map[string]string{
1✔
253
                ingressTrafficAuthoritativeAnnotation: "false",
1✔
254
        }
1✔
255

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

1✔
276
        rule := networking.IngressRule{
1✔
277
                IngressRuleValue: networking.IngressRuleValue{
1✔
278
                        HTTP: &networking.HTTPIngressRuleValue{
1✔
279
                                Paths: make([]networking.HTTPIngressPath, 0),
1✔
280
                        },
1✔
281
                },
1✔
282
        }
1✔
283

1✔
284
        actualWeights := make(map[string]float64)
1✔
285

1✔
286
        for _, sc := range ssc.StackContainers {
2✔
287
                if sc.actualTrafficWeight > 0 {
2✔
288
                        actualWeights[sc.Name()] = sc.actualTrafficWeight
1✔
289

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

306
        if len(rule.IngressRuleValue.HTTP.Paths) == 0 {
1✔
UNCOV
307
                return nil, errNoPaths
×
UNCOV
308
        }
×
309

310
        // sort backends by name to have a consistent generated ingress resource.
311
        sort.Slice(rule.IngressRuleValue.HTTP.Paths, func(i, j int) bool {
2✔
312
                return rule.IngressRuleValue.HTTP.Paths[i].Backend.Service.Name < rule.IngressRuleValue.HTTP.Paths[j].Backend.Service.Name
1✔
313
        })
1✔
314

315
        // create rule per hostname
316
        for _, host := range stackset.Spec.Ingress.Hosts {
2✔
317
                r := rule
1✔
318
                r.Host = host
1✔
319
                result.Spec.Rules = append(result.Spec.Rules, r)
1✔
320
        }
1✔
321

322
        // sort rules by host to have a consistent generated ingress resource.
323
        sort.Slice(result.Spec.Rules, func(i, j int) bool {
2✔
324
                return result.Spec.Rules[i].Host < result.Spec.Rules[j].Host
1✔
325
        })
1✔
326

327
        actualWeightsData, err := json.Marshal(&actualWeights)
1✔
328
        if err != nil {
1✔
UNCOV
329
                return nil, err
×
UNCOV
330
        }
×
331

332
        result.Annotations[ssc.backendWeightsAnnotationKey] = string(actualWeightsData)
1✔
333
        return result, nil
1✔
334
}
335

336
func (ssc *StackSetContainer) GenerateStackSetStatus() *zv1.StackSetStatus {
1✔
337
        result := &zv1.StackSetStatus{
1✔
338
                Stacks:               0,
1✔
339
                ReadyStacks:          0,
1✔
340
                StacksWithTraffic:    0,
1✔
341
                ObservedStackVersion: ssc.StackSet.Status.ObservedStackVersion,
1✔
342
        }
1✔
343
        var traffic []*zv1.ActualTraffic
1✔
344

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

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

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

© 2025 Coveralls, Inc