Skip to content

Commit 8c35e0a

Browse files
authored
Merge pull request #6066 from aleskandro/fix-default-label-arch
The autoscaler does not scale node groups on non-amd64 clusters when pods explicitly require non-amd64 nodes in node affinity
2 parents 4103407 + 54d3a4c commit 8c35e0a

File tree

4 files changed

+154
-1
lines changed

4 files changed

+154
-1
lines changed

cluster-autoscaler/cloudprovider/clusterapi/README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ cluster.
2020
* [Scale from zero support](#scale-from-zero-support)
2121
* [RBAC changes for scaling from zero](#rbac-changes-for-scaling-from-zero)
2222
* [Pre-defined labels and taints on nodes scaled from zero](#pre-defined-labels-and-taints-on-nodes-scaled-from-zero)
23+
* [CPU Architecture awareness for single-arch clusters](#cpu-architecture-awareness-for-single-arch-clusters)
2324
* [Specifying a Custom Resource Group](#specifying-a-custom-resource-group)
2425
* [Specifying a Custom Resource Version](#specifying-a-custom-resource-version)
2526
* [Sample manifest](#sample-manifest)
@@ -276,6 +277,16 @@ metadata:
276277
capacity.cluster-autoscaler.kubernetes.io/taints: "key1=value1:NoSchedule,key2=value2:NoExecute"
277278
```
278279

280+
#### CPU Architecture awareness for single-arch clusters
281+
282+
Users of single-arch non-amd64 clusters who are using scale from zero
283+
support should also set the `CAPI_SCALE_ZERO_DEFAULT_ARCH` environment variable
284+
to set the architecture of the nodes they want to default the node group templates to.
285+
The autoscaler will default to `amd64` if it is not set, and the node
286+
group templates may not match the nodes' architecture, specifically when
287+
the workload triggering the scale-up uses a node affinity predicate checking
288+
for the node's architecture.
289+
279290
## Specifying a Custom Resource Group
280291

281292
By default all Kubernetes resources consumed by the Cluster API provider will

cluster-autoscaler/cloudprovider/clusterapi/clusterapi_nodegroup.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -369,7 +369,7 @@ func buildGenericLabels(nodeName string) map[string]string {
369369
// TODO revisit this function and add an explanation about what these
370370
// labels are used for, or remove them if not necessary
371371
m := make(map[string]string)
372-
m[corev1.LabelArchStable] = cloudprovider.DefaultArch
372+
m[corev1.LabelArchStable] = GetDefaultScaleFromZeroArchitecture().Name()
373373

374374
m[corev1.LabelOSStable] = cloudprovider.DefaultOS
375375

cluster-autoscaler/cloudprovider/clusterapi/clusterapi_utils.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,11 @@ package clusterapi
1818

1919
import (
2020
"fmt"
21+
"k8s.io/klog/v2"
22+
"os"
2123
"strconv"
2224
"strings"
25+
"sync"
2326

2427
"github.com/pkg/errors"
2528
"k8s.io/apimachinery/pkg/api/resource"
@@ -36,6 +39,20 @@ const (
3639
maxPodsKey = "capacity.cluster-autoscaler.kubernetes.io/maxPods"
3740
taintsKey = "capacity.cluster-autoscaler.kubernetes.io/taints"
3841
labelsKey = "capacity.cluster-autoscaler.kubernetes.io/labels"
42+
// UnknownArch is used if the Architecture is Unknown
43+
UnknownArch SystemArchitecture = ""
44+
// Amd64 is used if the Architecture is x86_64
45+
Amd64 SystemArchitecture = "amd64"
46+
// Arm64 is used if the Architecture is ARM64
47+
Arm64 SystemArchitecture = "arm64"
48+
// Ppc64le is used if the Architecture is ppc64le
49+
Ppc64le SystemArchitecture = "ppc64le"
50+
// S390x is used if the Architecture is s390x
51+
S390x SystemArchitecture = "s390x"
52+
// DefaultArch should be used as a fallback if not passed by the environment via the --scale-up-from-zero-default-arch
53+
DefaultArch = Amd64
54+
// scaleUpFromZeroDefaultEnvVar is the name of the env var for the default architecture
55+
scaleUpFromZeroDefaultArchEnvVar = "CAPI_SCALE_ZERO_DEFAULT_ARCH"
3956
)
4057

4158
var (
@@ -79,10 +96,25 @@ var (
7996
nodeGroupMinSizeAnnotationKey = getNodeGroupMinSizeAnnotationKey()
8097
nodeGroupMaxSizeAnnotationKey = getNodeGroupMaxSizeAnnotationKey()
8198
zeroQuantity = resource.MustParse("0")
99+
100+
systemArchitecture *SystemArchitecture
101+
once sync.Once
82102
)
83103

84104
type normalizedProviderID string
85105

106+
// SystemArchitecture represents a CPU architecture (e.g., amd64, arm64, ppc64le, s390x).
107+
// It is used to determine the default architecture to use when building the nodes templates for scaling up from zero
108+
// by some cloud providers. This code is the same as the GCE implementation at
109+
// https://github.com/kubernetes/autoscaler/blob/3852f352d96b8763292a9122163c1152dfedec55/cluster-autoscaler/cloudprovider/gce/templates.go#L611-L657
110+
// which is kept to allow for a smooth transition to this package, once the GCE team is ready to use it.
111+
type SystemArchitecture string
112+
113+
// Name returns the string value for SystemArchitecture
114+
func (s SystemArchitecture) Name() string {
115+
return string(s)
116+
}
117+
86118
// minSize returns the minimum value encoded in the annotations keyed
87119
// by nodeGroupMinSizeAnnotationKey. Returns errMissingMinAnnotation
88120
// if the annotation doesn't exist or errInvalidMinAnnotation if the
@@ -279,3 +311,37 @@ func getClusterNameLabel() string {
279311
key := fmt.Sprintf("%s/cluster-name", getCAPIGroup())
280312
return key
281313
}
314+
315+
// SystemArchitectureFromString parses a string to SystemArchitecture. Returns UnknownArch if the string doesn't represent a
316+
// valid architecture.
317+
func SystemArchitectureFromString(arch string) SystemArchitecture {
318+
switch arch {
319+
case string(Arm64):
320+
return Arm64
321+
case string(Amd64):
322+
return Amd64
323+
case string(Ppc64le):
324+
return Ppc64le
325+
case string(S390x):
326+
return S390x
327+
default:
328+
return UnknownArch
329+
}
330+
}
331+
332+
// GetDefaultScaleFromZeroArchitecture returns the SystemArchitecture from the environment variable
333+
// CAPI_SCALE_ZERO_DEFAULT_ARCH or DefaultArch if the variable is set to an invalid value.
334+
func GetDefaultScaleFromZeroArchitecture() SystemArchitecture {
335+
once.Do(func() {
336+
archStr := os.Getenv(scaleUpFromZeroDefaultArchEnvVar)
337+
arch := SystemArchitectureFromString(archStr)
338+
klog.V(5).Infof("the default scale from zero architecture value is set to %s (%s)", scaleUpFromZeroDefaultArchEnvVar, archStr, arch.Name())
339+
if arch == UnknownArch {
340+
arch = DefaultArch
341+
klog.Errorf("Unrecognized architecture '%s', falling back to %s",
342+
scaleUpFromZeroDefaultArchEnvVar, DefaultArch.Name())
343+
}
344+
systemArchitecture = &arch
345+
})
346+
return *systemArchitecture
347+
}

cluster-autoscaler/cloudprovider/clusterapi/clusterapi_utils_test.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,10 @@ package clusterapi
1818

1919
import (
2020
"fmt"
21+
"github.com/google/go-cmp/cmp"
2122
"reflect"
2223
"strings"
24+
"sync"
2325
"testing"
2426

2527
"k8s.io/apimachinery/pkg/api/resource"
@@ -853,3 +855,77 @@ func Test_getKeyHelpers(t *testing.T) {
853855
})
854856
}
855857
}
858+
859+
func TestSystemArchitectureFromString(t *testing.T) {
860+
tcs := []struct {
861+
name string
862+
archName string
863+
wantArch SystemArchitecture
864+
}{
865+
{
866+
name: "valid architecture is converted",
867+
archName: "amd64",
868+
wantArch: Amd64,
869+
},
870+
{
871+
name: "invalid architecture results in UnknownArchitecture",
872+
archName: "some-arch",
873+
wantArch: UnknownArch,
874+
},
875+
}
876+
for _, tc := range tcs {
877+
t.Run(tc.name, func(t *testing.T) {
878+
gotArch := SystemArchitectureFromString(tc.archName)
879+
if diff := cmp.Diff(tc.wantArch, gotArch); diff != "" {
880+
t.Errorf("ToSystemArchitecture diff (-want +got):\n%s", diff)
881+
}
882+
})
883+
}
884+
}
885+
886+
func TestGetSystemArchitectureFromEnvOrDefault(t *testing.T) {
887+
amd64 := Amd64.Name()
888+
arm64 := Arm64.Name()
889+
wrongValue := "wrong"
890+
891+
tcs := []struct {
892+
name string
893+
envValue *string
894+
want SystemArchitecture
895+
}{
896+
{
897+
name: fmt.Sprintf("%s is set to arm64", scaleUpFromZeroDefaultArchEnvVar),
898+
envValue: &arm64,
899+
want: Arm64,
900+
},
901+
{
902+
name: fmt.Sprintf("%s is set to amd64", scaleUpFromZeroDefaultArchEnvVar),
903+
envValue: &amd64,
904+
want: Amd64,
905+
},
906+
{
907+
name: fmt.Sprintf("%s is not set", scaleUpFromZeroDefaultArchEnvVar),
908+
envValue: nil,
909+
want: DefaultArch,
910+
},
911+
{
912+
name: fmt.Sprintf("%s is set to a wrong value", scaleUpFromZeroDefaultArchEnvVar),
913+
envValue: &wrongValue,
914+
want: DefaultArch,
915+
},
916+
}
917+
for _, tc := range tcs {
918+
t.Run(tc.name, func(t *testing.T) {
919+
// Reset the systemArchitecture variable to nil before each test due to the lazy initialization of the variable.
920+
systemArchitecture = nil
921+
// Reset the once variable to its initial state before each test.
922+
once = sync.Once{}
923+
if tc.envValue != nil {
924+
t.Setenv(scaleUpFromZeroDefaultArchEnvVar, *tc.envValue)
925+
}
926+
if got := GetDefaultScaleFromZeroArchitecture(); got != tc.want {
927+
t.Errorf("GetDefaultScaleFromZeroArchitecture() = %v, want %v", got, tc.want)
928+
}
929+
})
930+
}
931+
}

0 commit comments

Comments
 (0)