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

zalando-incubator / stackset-controller / 15821082546

25 Oct 2022 12:04PM UTC coverage: 55.545% (+0.02%) from 55.521%
15821082546

Pull #441

github

AlexanderYastrebov
autoscaler: ensure HPA min replicas is not below Stack replicas

Users may scale stack manually via `kubectl scale stack` which updates
Stack `spec.replicas` but it would not work when HPA is present and has
lower minReplicas value.

This change ensures that Stack's HPA min replicas is not below Stack
`spec.replicas` (if present).

Signed-off-by: Alexander Yastrebov <alexander.yastrebov@zalando.de>
Pull Request #441: autoscaler: ensure HPA min replicas is not below Stack replicas

5 of 5 new or added lines in 1 file covered. (100.0%)

2 existing lines in 1 file now uncovered.

3015 of 5428 relevant lines covered (55.55%)

0.62 hits per line

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

93.94
/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/v2beta2"
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

27
type ingressOrRouteGroupSpec interface {
28
        GetAnnotations() map[string]string
29
        GetHosts() []string
30
}
31

32
var (
33
        // set implementation with 0 Byte value
34
        selectorLabels = map[string]struct{}{
35
                StacksetHeritageLabelKey: {},
36
                StackVersionLabelKey:     {},
37
        }
38

39
        // PathTypeImplementationSpecific is the used implementation path type
40
        // for k8s.io/api/networking/v1.HTTPIngressPath resources.
41
        PathTypeImplementationSpecific = networking.PathTypeImplementationSpecific
42
)
43

44
func mapCopy(m map[string]string) map[string]string {
45
        newMap := map[string]string{}
46
        for k, v := range m {
47
                newMap[k] = v
1✔
48
        }
1✔
49
        return newMap
2✔
50
}
1✔
51

1✔
52
// limitLabels returns a limited set of labels based on the validKeys.
1✔
53
func limitLabels(labels map[string]string, validKeys map[string]struct{}) map[string]string {
54
        newLabels := make(map[string]string, len(labels))
55
        for k, v := range labels {
56
                if _, ok := validKeys[k]; ok {
1✔
57
                        newLabels[k] = v
1✔
58
                }
2✔
59
        }
2✔
60
        return newLabels
1✔
61
}
1✔
62

63
// templateObjectMetaInjectLabels injects labels into a pod template spec.
1✔
64
func objectMetaInjectLabels(objectMeta metav1.ObjectMeta, labels map[string]string) metav1.ObjectMeta {
65
        if objectMeta.Labels == nil {
66
                objectMeta.Labels = map[string]string{}
67
        }
1✔
68
        for key, value := range labels {
2✔
69
                if _, ok := objectMeta.Labels[key]; !ok {
1✔
70
                        objectMeta.Labels[key] = value
1✔
71
                }
2✔
72
        }
2✔
73
        return objectMeta
1✔
74
}
1✔
75

76
func (sc *StackContainer) resourceMeta() metav1.ObjectMeta {
1✔
77
        resourceLabels := mapCopy(sc.Stack.Labels)
78

79
        return metav1.ObjectMeta{
1✔
80
                Name:      sc.Name(),
1✔
81
                Namespace: sc.Namespace(),
1✔
82
                Annotations: map[string]string{
1✔
83
                        stackGenerationAnnotationKey: strconv.FormatInt(sc.Stack.Generation, 10),
2✔
84
                },
1✔
85
                Labels: resourceLabels,
1✔
86
                OwnerReferences: []metav1.OwnerReference{
87
                        {
1✔
88
                                APIVersion: APIVersion,
1✔
89
                                Kind:       KindStack,
1✔
90
                                Name:       sc.Name(),
1✔
91
                                UID:        sc.Stack.UID,
1✔
92
                        },
1✔
93
                },
1✔
94
        }
1✔
95
}
1✔
96

1✔
97
// getServicePorts gets the service ports to be used for the stack service.
1✔
98
func getServicePorts(stackSpec zv1.StackSpec, backendPort *intstr.IntOrString) ([]v1.ServicePort, error) {
1✔
99
        var servicePorts []v1.ServicePort
1✔
100
        if stackSpec.Service == nil || len(stackSpec.Service.Ports) == 0 {
1✔
101
                servicePorts = servicePortsFromContainers(stackSpec.PodTemplate.Spec.Containers)
1✔
102
        } else {
1✔
103
                servicePorts = stackSpec.Service.Ports
104
        }
105

1✔
106
        // validate that one port in the list maps to the backendPort.
1✔
107
        if backendPort != nil {
1✔
108
                for _, port := range servicePorts {
109
                        switch backendPort.Type {
110
                        case intstr.Int:
1✔
111
                                if port.Port == backendPort.IntVal {
1✔
112
                                        return servicePorts, nil
1✔
113
                                }
2✔
114
                        case intstr.String:
1✔
115
                                if port.Name == backendPort.StrVal {
1✔
116
                                        return servicePorts, nil
1✔
117
                                }
1✔
118
                        }
2✔
119
                }
1✔
120

1✔
121
                return nil, fmt.Errorf("no service ports matching backendPort '%s'", backendPort.String())
122
        }
123

2✔
124
        return servicePorts, nil
2✔
125
}
1✔
126

1✔
127
// servicePortsFromTemplate gets service port from pod template.
2✔
128
func servicePortsFromContainers(containers []v1.Container) []v1.ServicePort {
1✔
129
        ports := make([]v1.ServicePort, 0)
1✔
130
        for i, container := range containers {
1✔
131
                for j, port := range container.Ports {
2✔
132
                        name := fmt.Sprintf("port-%d-%d", i, j)
1✔
133
                        if port.Name != "" {
1✔
134
                                name = port.Name
135
                        }
136
                        servicePort := v1.ServicePort{
137
                                Name:       name,
1✔
138
                                Protocol:   port.Protocol,
139
                                Port:       port.ContainerPort,
140
                                TargetPort: intstr.FromInt(int(port.ContainerPort)),
1✔
141
                        }
142
                        // set default protocol if not specified
143
                        if servicePort.Protocol == "" {
144
                                servicePort.Protocol = v1.ProtocolTCP
1✔
145
                        }
1✔
146
                        ports = append(ports, servicePort)
2✔
147
                }
2✔
148
        }
1✔
149
        return ports
2✔
150
}
1✔
151

1✔
152
func (sc *StackContainer) selector() map[string]string {
1✔
153
        return limitLabels(sc.Stack.Labels, selectorLabels)
1✔
154
}
1✔
155

1✔
156
func (sc *StackContainer) GenerateDeployment() *appsv1.Deployment {
1✔
157
        stack := sc.Stack
1✔
158

1✔
159
        desiredReplicas := sc.stackReplicas
2✔
160
        if sc.prescalingActive {
1✔
161
                desiredReplicas = sc.prescalingReplicas
1✔
162
        }
1✔
163

164
        var updatedReplicas *int32
165

1✔
166
        if desiredReplicas != 0 && !sc.ScaledDown() {
167
                // Stack scaled up, rescale the deployment if it's at 0 replicas, or if HPA is unused and we don't run autoscaling
168
                if sc.deploymentReplicas == 0 || (!sc.IsAutoscaled() && desiredReplicas != sc.deploymentReplicas) {
1✔
169
                        updatedReplicas = wrapReplicas(desiredReplicas)
1✔
170
                }
1✔
171
        } else {
172
                // Stack scaled down (manually or because it doesn't receive traffic), check if we need to scale down the deployment
1✔
173
                if sc.deploymentReplicas != 0 {
1✔
174
                        updatedReplicas = wrapReplicas(0)
1✔
175
                }
1✔
176
        }
2✔
177

1✔
178
        if updatedReplicas == nil {
1✔
179
                updatedReplicas = wrapReplicas(sc.deploymentReplicas)
180
        }
1✔
181

1✔
182
        var strategy *appsv1.DeploymentStrategy
2✔
183
        if stack.Spec.Strategy != nil {
1✔
184
                strategy = stack.Spec.Strategy.DeepCopy()
2✔
185
        }
1✔
186

1✔
187
        embeddedCopy := stack.Spec.PodTemplate.EmbeddedObjectMeta.DeepCopy()
1✔
188

1✔
189
        templateObjectMeta := metav1.ObjectMeta{
2✔
190
                Annotations: embeddedCopy.Annotations,
1✔
191
                Labels:      embeddedCopy.Labels,
1✔
192
        }
193

194
        deployment := &appsv1.Deployment{
2✔
195
                ObjectMeta: sc.resourceMeta(),
1✔
196
                Spec: appsv1.DeploymentSpec{
1✔
197
                        Replicas:        updatedReplicas,
198
                        MinReadySeconds: sc.Stack.Spec.MinReadySeconds,
1✔
199
                        Selector: &metav1.LabelSelector{
2✔
200
                                MatchLabels: sc.selector(),
1✔
201
                        },
1✔
202
                        Template: v1.PodTemplateSpec{
203
                                ObjectMeta: objectMetaInjectLabels(templateObjectMeta, stack.Labels),
1✔
204
                                Spec:       *stack.Spec.PodTemplate.Spec.DeepCopy(),
1✔
205
                        },
1✔
206
                },
1✔
207
        }
1✔
208
        if strategy != nil {
1✔
209
                deployment.Spec.Strategy = *strategy
1✔
210
        }
1✔
211
        return deployment
1✔
212
}
1✔
213

1✔
214
func (sc *StackContainer) GenerateHPA() (*autoscaling.HorizontalPodAutoscaler, error) {
1✔
215
        autoscalerSpec := sc.Stack.Spec.Autoscaler
1✔
216
        hpaSpec := sc.Stack.Spec.HorizontalPodAutoscaler
1✔
217

1✔
218
        if autoscalerSpec == nil && hpaSpec == nil {
1✔
219
                return nil, nil
1✔
220
        }
1✔
221

1✔
222
        result := &autoscaling.HorizontalPodAutoscaler{
1✔
223
                ObjectMeta: sc.resourceMeta(),
1✔
224
                TypeMeta: metav1.TypeMeta{
2✔
225
                        Kind:       "HorizontalPodAutoscaler",
1✔
226
                        APIVersion: "autoscaling/v2beta2",
1✔
227
                },
1✔
228
                Spec: autoscaling.HorizontalPodAutoscalerSpec{
229
                        ScaleTargetRef: autoscaling.CrossVersionObjectReference{
230
                                APIVersion: apiVersionAppsV1,
231
                                Kind:       kindDeployment,
232
                                Name:       sc.Name(),
233
                        },
1✔
234
                },
1✔
235
        }
1✔
236

1✔
237
        if autoscalerSpec != nil {
1✔
238
                result.Spec.MinReplicas = autoscalerSpec.MinReplicas
×
239
                result.Spec.MaxReplicas = autoscalerSpec.MaxReplicas
×
240

241
                metrics, annotations, err := convertCustomMetrics(sc.stacksetName, sc.Name(), sc.Namespace(), autoscalerSpec.Metrics)
2✔
242
                if err != nil {
1✔
243
                        return nil, err
1✔
244
                }
245
                result.Spec.Metrics = metrics
1✔
246
                result.Annotations = mergeLabels(result.Annotations, annotations)
1✔
247
                result.Spec.Behavior = autoscalerSpec.Behavior
1✔
248
        } else {
1✔
249
                result.Spec.MinReplicas = hpaSpec.MinReplicas
1✔
250
                result.Spec.MaxReplicas = hpaSpec.MaxReplicas
1✔
251
                metrics := make([]autoscaling.MetricSpec, 0, len(hpaSpec.Metrics))
1✔
252
                for _, m := range hpaSpec.Metrics {
1✔
253
                        m := m
1✔
254
                        metric := autoscaling.MetricSpec{}
1✔
255
                        err := Convert_v2beta1_MetricSpec_To_autoscaling_MetricSpec(&m, &metric, nil)
1✔
256
                        if err != nil {
1✔
257
                                return nil, err
1✔
258
                        }
1✔
259
                        metrics = append(metrics, metric)
1✔
260
                }
1✔
261
                result.Spec.Metrics = metrics
1✔
262
                result.Spec.Behavior = hpaSpec.Behavior
1✔
263
        }
1✔
264

1✔
265
        // Ensure HPA min replicas is not below Stack replicas in case Stack was scaled
1✔
266
        if sc.Stack.Spec.Replicas != nil && (result.Spec.MinReplicas == nil || *result.Spec.MinReplicas < *sc.Stack.Spec.Replicas) {
1✔
267
                result.Spec.MinReplicas = sc.Stack.Spec.Replicas
1✔
268
        }
1✔
269

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

1✔
276
        return result, nil
1✔
277
}
1✔
278

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

×
287
        servicePorts, err := getServicePorts(sc.Stack.Spec, backendPort)
×
288
        if err != nil {
289
                return nil, err
1✔
290
        }
291

292
        metaObj := sc.resourceMeta()
1✔
293
        stackSpec := sc.Stack.Spec
1✔
294
        if stackSpec.Service != nil {
1✔
295
                metaObj.Annotations = mergeLabels(metaObj.Annotations, stackSpec.Service.Annotations)
1✔
296
        }
1✔
297
        return &v1.Service{
×
298
                ObjectMeta: metaObj,
×
299
                Spec: v1.ServiceSpec{
300
                        Selector: sc.selector(),
1✔
301
                        Type:     v1.ServiceTypeClusterIP,
1✔
302
                        Ports:    servicePorts,
×
303
                },
×
304
        }, nil
305
}
1✔
306

1✔
307
func (sc *StackContainer) stackHostnames(spec ingressOrRouteGroupSpec, overrides *zv1.StackIngressRouteGroupOverrides) ([]string, error) {
2✔
308
        result := sets.NewString()
1✔
309

1✔
310
        if overrides != nil && len(overrides.Hosts) > 0 {
1✔
311
                for _, host := range overrides.Hosts {
1✔
312
                        interpolated := strings.ReplaceAll(host, "$(STACK_NAME)", sc.Name())
1✔
313
                        if interpolated == host {
1✔
314
                                return nil, fmt.Errorf("override hostname must contain $(STACK_NAME)")
1✔
315
                        }
1✔
316
                        result.Insert(interpolated)
1✔
317
                }
1✔
318
        } else {
1✔
319
                // Old-style autogenerated hostnames
1✔
320
                for _, host := range spec.GetHosts() {
1✔
321
                        for _, domain := range sc.clusterDomains {
322
                                if strings.HasSuffix(host, domain) {
323
                                        result.Insert(fmt.Sprintf("%s.%s", sc.Name(), domain))
324
                                } else {
325
                                        log.Debugf("Ingress host: %s suffix did not match cluster-domain %s", host, domain)
326
                                }
1✔
327
                        }
1✔
328
                }
2✔
329
        }
1✔
330

1✔
331
        return result.List(), nil
1✔
332
}
1✔
333

1✔
334
func effectiveAnnotations(spec ingressOrRouteGroupSpec, overrides *zv1.StackIngressRouteGroupOverrides) map[string]string {
2✔
335
        if overrides != nil && len(overrides.Annotations) > 0 {
2✔
336
                return overrides.Annotations
2✔
337
        }
1✔
338
        return spec.GetAnnotations()
2✔
339
}
1✔
340

1✔
341
func (sc *StackContainer) GenerateIngress() (*networking.Ingress, error) {
1✔
342
        if !sc.HasBackendPort() || sc.ingressSpec == nil || !sc.Stack.Spec.IngressOverrides.IsEnabled() {
1✔
343
                return nil, nil
1✔
344
        }
1✔
345

346
        hostnames, err := sc.stackHostnames(sc.ingressSpec, sc.Stack.Spec.IngressOverrides)
347
        if err != nil {
1✔
348
                return nil, err
349
        }
350
        if len(hostnames) == 0 {
1✔
351
                return nil, nil
1✔
352
        }
1✔
353

354
        rules := make([]networking.IngressRule, 0, len(hostnames))
355
        for _, hostname := range hostnames {
356
                rules = append(rules, networking.IngressRule{
357
                        IngressRuleValue: networking.IngressRuleValue{
1✔
358
                                HTTP: &networking.HTTPIngressRuleValue{
1✔
359
                                        Paths: []networking.HTTPIngressPath{
2✔
360
                                                {
1✔
361
                                                        PathType: &PathTypeImplementationSpecific,
1✔
362
                                                        Path:     sc.ingressSpec.Path,
363
                                                        Backend: networking.IngressBackend{
364
                                                                Service: &networking.IngressServiceBackend{
1✔
365
                                                                        Name: sc.Name(),
1✔
366
                                                                        Port: networking.ServiceBackendPort{
1✔
367
                                                                                Name:   sc.backendPort.StrVal,
1✔
368
                                                                                Number: sc.backendPort.IntVal,
1✔
369
                                                                        },
1✔
370
                                                                },
2✔
371
                                                        },
1✔
372
                                                },
1✔
373
                                        },
1✔
374
                                },
1✔
375
                        },
2✔
376
                        Host: hostname,
1✔
377
                })
1✔
378
        }
1✔
379

1✔
380
        // sort rules by hostname for a stable order
1✔
381
        sort.Slice(rules, func(i, j int) bool {
1✔
382
                return rules[i].Host < rules[j].Host
1✔
383
        })
384

1✔
385
        result := &networking.Ingress{
386
                ObjectMeta: sc.resourceMeta(),
387
                Spec: networking.IngressSpec{
388
                        Rules: rules,
389
                },
390
        }
1✔
391

1✔
392
        // insert annotations
2✔
393
        result.Annotations = mergeLabels(result.Annotations, effectiveAnnotations(sc.ingressSpec, sc.Stack.Spec.IngressOverrides))
1✔
394
        return result, nil
1✔
395
}
396

1✔
397
func (sc *StackContainer) GenerateRouteGroup() (*rgv1.RouteGroup, error) {
1✔
398
        if !sc.HasBackendPort() || sc.routeGroupSpec == nil || !sc.Stack.Spec.RouteGroupOverrides.IsEnabled() {
×
399
                return nil, nil
×
400
        }
2✔
401

1✔
402
        hostnames, err := sc.stackHostnames(sc.routeGroupSpec, sc.Stack.Spec.RouteGroupOverrides)
1✔
403
        if err != nil {
404
                return nil, err
1✔
405
        }
2✔
406
        if len(hostnames) == 0 {
1✔
407
                return nil, nil
1✔
408
        }
1✔
409

1✔
410
        result := &rgv1.RouteGroup{
1✔
411
                ObjectMeta: sc.resourceMeta(),
1✔
412
                Spec: rgv1.RouteGroupSpec{
1✔
413
                        Hosts: hostnames,
1✔
414
                        Backends: []rgv1.RouteGroupBackend{
1✔
415
                                {
1✔
416
                                        Name:        sc.Name(),
1✔
417
                                        Type:        rgv1.ServiceRouteGroupBackend,
1✔
418
                                        ServiceName: sc.Name(),
1✔
419
                                        ServicePort: sc.backendPort.IntValue(),
1✔
420
                                        Algorithm:   sc.routeGroupSpec.LBAlgorithm,
1✔
421
                                },
1✔
422
                        },
1✔
423
                        DefaultBackends: []rgv1.RouteGroupBackendReference{
1✔
424
                                {
1✔
425
                                        BackendName: sc.Name(),
1✔
426
                                        Weight:      100,
1✔
427
                                },
1✔
428
                        },
1✔
429
                        Routes: sc.routeGroupSpec.Routes,
430
                },
431
        }
2✔
432

1✔
433
        // validate not overlapping with main backend
1✔
434
        for _, backend := range sc.routeGroupSpec.AdditionalBackends {
435
                if backend.Name == sc.Name() {
1✔
436
                        return nil, fmt.Errorf("invalid additionalBackend '%s', overlaps with Stack name", backend.Name)
1✔
437
                }
1✔
438
                if backend.ServiceName == sc.Name() {
1✔
439
                        return nil, fmt.Errorf("invalid additionalBackend '%s', serviceName '%s' overlaps with Stack name", backend.Name, backend.ServiceName)
1✔
440
                }
1✔
441
                result.Spec.Backends = append(result.Spec.Backends, backend)
1✔
442
        }
1✔
443

1✔
444
        // sort backends to ensure have a consistent generated RoutGroup resource
1✔
445
        sort.Slice(result.Spec.Backends, func(i, j int) bool {
1✔
446
                return result.Spec.Backends[i].Name < result.Spec.Backends[j].Name
1✔
447
        })
1✔
448

1✔
449
        // insert annotations
450
        result.Annotations = mergeLabels(result.Annotations, effectiveAnnotations(sc.routeGroupSpec, sc.Stack.Spec.RouteGroupOverrides))
451

1✔
452
        return result, nil
1✔
453
}
1✔
454

455
func (sc *StackContainer) GenerateStackStatus() *zv1.StackStatus {
456
        prescaling := zv1.PrescalingStatus{}
457
        if sc.prescalingActive {
458
                prescaling = zv1.PrescalingStatus{
1✔
459
                        Active:               sc.prescalingActive,
1✔
460
                        Replicas:             sc.prescalingReplicas,
2✔
461
                        DesiredTrafficWeight: sc.prescalingDesiredTrafficWeight,
1✔
462
                        LastTrafficIncrease:  wrapTime(sc.prescalingLastTrafficIncrease),
1✔
463
                }
464
        }
465
        return &zv1.StackStatus{
1✔
466
                ActualTrafficWeight:  sc.actualTrafficWeight,
1✔
467
                DesiredTrafficWeight: sc.desiredTrafficWeight,
1✔
468
                Replicas:             sc.createdReplicas,
1✔
469
                ReadyReplicas:        sc.readyReplicas,
1✔
470
                UpdatedReplicas:      sc.updatedReplicas,
1✔
471
                DesiredReplicas:      sc.deploymentReplicas,
1✔
472
                Prescaling:           prescaling,
2✔
473
                NoTrafficSince:       wrapTime(sc.noTrafficSince),
1✔
474
                LabelSelector:        labels.Set(sc.selector()).String(),
1✔
475
        }
1✔
476
}
1✔
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