-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Set labels required for autoscaling on machineset
- Loading branch information
Showing
3 changed files
with
225 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,150 @@ | ||
package controllers | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"regexp" | ||
"slices" | ||
"strconv" | ||
"strings" | ||
|
||
machinev1beta1 "github.com/openshift/api/machine/v1beta1" | ||
"k8s.io/apimachinery/pkg/api/equality" | ||
"k8s.io/apimachinery/pkg/runtime" | ||
ctrl "sigs.k8s.io/controller-runtime" | ||
"sigs.k8s.io/controller-runtime/pkg/client" | ||
|
||
csv1beta1 "github.com/appuio/machine-api-provider-cloudscale/api/cloudscale/provider/v1beta1" | ||
) | ||
|
||
// MachineSetReconciler reconciles a MachineSet object | ||
type MachineSetReconciler struct { | ||
client.Client | ||
Scheme *runtime.Scheme | ||
} | ||
|
||
const ( | ||
// This exposes compute information based on the providerSpec input. | ||
// This is needed by the autoscaler to foresee upcoming capacity when scaling from zero. | ||
// https://github.com/openshift/enhancements/pull/186 | ||
cpuKey = "machine.openshift.io/vCPU" | ||
memoryKey = "machine.openshift.io/memoryMb" | ||
gpuKey = "machine.openshift.io/GPU" | ||
labelsKey = "capacity.cluster-autoscaler.kubernetes.io/labels" | ||
|
||
gpuKeyValue = "0" | ||
arch = "kubernetes.io/arch=amd64" | ||
) | ||
|
||
// Reconcile reacts to MachineSet changes and updates the annotations used by the OpenShift autoscaler. | ||
// GPU is always set to 0, as cloudscale does not provide GPU instances. | ||
// The architecture label is always set to amd64. | ||
func (r *MachineSetReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { | ||
var machineSet machinev1beta1.MachineSet | ||
if err := r.Get(ctx, req.NamespacedName, &machineSet); err != nil { | ||
return ctrl.Result{}, client.IgnoreNotFound(err) | ||
} | ||
if !machineSet.DeletionTimestamp.IsZero() { | ||
return ctrl.Result{}, nil | ||
} | ||
|
||
origSet := machineSet.DeepCopy() | ||
|
||
if machineSet.Annotations == nil { | ||
machineSet.Annotations = make(map[string]string) | ||
} | ||
|
||
spec, err := csv1beta1.ProviderSpecFromRawExtension(machineSet.Spec.Template.Spec.ProviderSpec.Value) | ||
if err != nil { | ||
return ctrl.Result{}, fmt.Errorf("failed to get provider spec from machine template: %w", err) | ||
} | ||
if spec == nil { | ||
return ctrl.Result{}, nil | ||
} | ||
|
||
flavour, err := parseCloudscaleFlavour(spec.Flavor) | ||
if err != nil { | ||
return ctrl.Result{}, fmt.Errorf("failed to parse flavour %q: %w", spec.Flavor, err) | ||
} | ||
|
||
machineSet.Annotations[cpuKey] = strconv.Itoa(flavour.CPU) | ||
machineSet.Annotations[memoryKey] = strconv.Itoa(flavour.MemGB * 1024) | ||
machineSet.Annotations[gpuKey] = gpuKeyValue | ||
|
||
// We guarantee that any existing labels provided via the capacity annotations are preserved. | ||
// See https://github.com/kubernetes/autoscaler/pull/5382 and https://github.com/kubernetes/autoscaler/pull/5697 | ||
machineSet.Annotations[labelsKey] = mergeCommaSeparatedKeyValuePairs( | ||
arch, | ||
machineSet.Annotations[labelsKey]) | ||
|
||
if equality.Semantic.DeepEqual(origSet.Annotations, machineSet.Annotations) { | ||
return ctrl.Result{}, nil | ||
} | ||
|
||
if err := r.Patch(ctx, &machineSet, client.MergeFrom(origSet)); err != nil { | ||
return ctrl.Result{}, fmt.Errorf("failed to patch MachineSet %q: %w", machineSet.Name, err) | ||
} | ||
|
||
return ctrl.Result{}, nil | ||
} | ||
|
||
// SetupWithManager sets up the controller with the Manager. | ||
func (r *MachineSetReconciler) SetupWithManager(mgr ctrl.Manager) error { | ||
return ctrl.NewControllerManagedBy(mgr). | ||
For(&machinev1beta1.MachineSet{}). | ||
Complete(r) | ||
} | ||
|
||
type cloudscaleFlavour struct { | ||
Type string | ||
CPU int | ||
MemGB int | ||
} | ||
|
||
var cloudscaleFlavourRegexp = regexp.MustCompile(`^(\w+)-(\d+)-(\d+)$`) | ||
|
||
// Parse parses a cloudscale flavour string. | ||
func parseCloudscaleFlavour(flavour string) (cloudscaleFlavour, error) { | ||
parts := cloudscaleFlavourRegexp.FindStringSubmatch(flavour) | ||
|
||
if len(parts) != 4 { | ||
return cloudscaleFlavour{}, fmt.Errorf("flavour %q does not match expected format", flavour) | ||
} | ||
cpu, err := strconv.Atoi(parts[2]) | ||
if err != nil { | ||
return cloudscaleFlavour{}, fmt.Errorf("failed to parse CPU from flavour %q: %w", flavour, err) | ||
} | ||
mem, err := strconv.Atoi(parts[3]) | ||
if err != nil { | ||
return cloudscaleFlavour{}, fmt.Errorf("failed to parse memory from flavour %q: %w", flavour, err) | ||
} | ||
|
||
return cloudscaleFlavour{ | ||
Type: parts[1], | ||
CPU: cpu, | ||
MemGB: mem, | ||
}, nil | ||
} | ||
|
||
// mergeCommaSeparatedKeyValuePairs merges multiple comma separated lists of key=value pairs into a single, comma-separated, list | ||
// of key=value pairs. If a key is present in multiple lists, the value from the last list is used. | ||
func mergeCommaSeparatedKeyValuePairs(lists ...string) string { | ||
merged := make(map[string]string) | ||
for _, list := range lists { | ||
for _, kv := range strings.Split(list, ",") { | ||
kv := strings.Split(kv, "=") | ||
if len(kv) != 2 { | ||
// ignore invalid key=value pairs | ||
continue | ||
} | ||
merged[kv[0]] = kv[1] | ||
} | ||
} | ||
// convert the map back to a comma separated list | ||
var result []string | ||
for k, v := range merged { | ||
result = append(result, fmt.Sprintf("%s=%s", k, v)) | ||
} | ||
slices.Sort(result) | ||
return strings.Join(result, ",") | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
package controllers | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"testing" | ||
|
||
machinev1beta1 "github.com/openshift/api/machine/v1beta1" | ||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/require" | ||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
"k8s.io/apimachinery/pkg/runtime" | ||
clientgoscheme "k8s.io/client-go/kubernetes/scheme" | ||
ctrl "sigs.k8s.io/controller-runtime" | ||
"sigs.k8s.io/controller-runtime/pkg/client" | ||
"sigs.k8s.io/controller-runtime/pkg/client/fake" | ||
) | ||
|
||
func Test_MachineSetReconciler_Reconcile(t *testing.T) { | ||
ctx := context.Background() | ||
|
||
scheme := runtime.NewScheme() | ||
require.NoError(t, clientgoscheme.AddToScheme(scheme)) | ||
require.NoError(t, machinev1beta1.AddToScheme(scheme)) | ||
|
||
ms := &machinev1beta1.MachineSet{ | ||
ObjectMeta: metav1.ObjectMeta{ | ||
Name: "machineset1", | ||
Namespace: "default", | ||
Annotations: map[string]string{ | ||
"random": "annotation", | ||
labelsKey: "a=a,b=b", | ||
}, | ||
}, | ||
Spec: machinev1beta1.MachineSetSpec{}, | ||
} | ||
|
||
setFlavorOnMachineSet(ms, "plus-2-4") | ||
|
||
c := fake.NewClientBuilder(). | ||
WithScheme(scheme). | ||
WithRuntimeObjects(ms). | ||
Build() | ||
|
||
subject := &MachineSetReconciler{ | ||
Client: c, | ||
Scheme: scheme, | ||
} | ||
|
||
_, err := subject.Reconcile(ctx, ctrl.Request{NamespacedName: client.ObjectKeyFromObject(ms)}) | ||
require.NoError(t, err) | ||
updated := &machinev1beta1.MachineSet{} | ||
require.NoError(t, c.Get(ctx, client.ObjectKeyFromObject(ms), updated)) | ||
assert.Equal(t, "2", updated.Annotations[cpuKey]) | ||
assert.Equal(t, "4096", updated.Annotations[memoryKey]) | ||
assert.Equal(t, "0", updated.Annotations[gpuKey]) | ||
assert.Equal(t, "a=a,b=b,kubernetes.io/arch=amd64", updated.Annotations[labelsKey]) | ||
} | ||
|
||
func setFlavorOnMachineSet(machine *machinev1beta1.MachineSet, flavor string) { | ||
machine.Spec.Template.Spec.ProviderSpec.Value = &runtime.RawExtension{ | ||
Raw: []byte(fmt.Sprintf(`{"flavor": "%s"}`, flavor)), | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters