Skip to content

Commit 548e4c7

Browse files
committed
Feat: Support Externally Managed Autoscaler for MachinePools (#515)
Adding the check for annotation "cluster.x-k8s.io/replicas-managed-by"
1 parent 4a92ce8 commit 548e4c7

File tree

5 files changed

+249
-3
lines changed

5 files changed

+249
-3
lines changed

cloud/scope/machine_pool.go

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import (
4444
"k8s.io/utils/pointer"
4545
clusterv1beta1 "sigs.k8s.io/cluster-api/api/core/v1beta1"
4646
clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2"
47+
"sigs.k8s.io/cluster-api/util/annotations"
4748
v1beta1conditions "sigs.k8s.io/cluster-api/util/deprecated/v1beta1/conditions"
4849
v1beta1patch "sigs.k8s.io/cluster-api/util/deprecated/v1beta1/patch"
4950
"sigs.k8s.io/controller-runtime/pkg/client"
@@ -182,6 +183,36 @@ func (m *MachinePoolScope) SetReplicaCount(count int32) {
182183
m.OCIMachinePool.Status.Replicas = count
183184
}
184185

186+
// SyncReplicasFromInstancePool updates the owner MachinePool spec to match the
187+
// observed OCI instance pool size when replicas are externally managed.
188+
func (m *MachinePoolScope) SyncReplicasFromInstancePool(ctx context.Context, instancePool *core.InstancePool) error {
189+
if !annotations.ReplicasManagedByExternalAutoscaler(m.MachinePool) {
190+
return nil
191+
}
192+
if instancePool == nil || instancePool.Size == nil {
193+
m.Info("Synced MachinePool instancePool or size is nil.")
194+
return nil
195+
}
196+
197+
observedReplicas := int32(*instancePool.Size)
198+
if m.MachinePool.Spec.Replicas != nil && *m.MachinePool.Spec.Replicas == observedReplicas {
199+
return nil
200+
}
201+
202+
helper, err := v1beta1patch.NewHelper(m.MachinePool, m.Client)
203+
if err != nil {
204+
return errors.Wrap(err, "failed to init machinepool patch helper")
205+
}
206+
207+
m.MachinePool.Spec.Replicas = pointer.Int32(observedReplicas)
208+
if err := helper.Patch(ctx, m.MachinePool); err != nil {
209+
return errors.Wrap(err, "failed to patch machinepool replicas from observed instance pool size")
210+
}
211+
212+
m.Info("Synced MachinePool replicas from observed instance pool size", "replicas", observedReplicas)
213+
return nil
214+
}
215+
185216
// GetWorkerMachineSubnet returns the WorkerRole core.Subnet id for the cluster
186217
func (m *MachinePoolScope) GetWorkerMachineSubnet() *string {
187218
for _, subnet := range ptr.ToSubnetSlice(m.OCIClusterAccesor.GetNetworkSpec().Vcn.Subnets) {
@@ -826,7 +857,7 @@ func instancePoolNeedsUpdates(machinePoolScope *MachinePoolScope, instancePool *
826857
if instancePool.Size != nil {
827858
instanePoolSize = *instancePool.Size
828859
}
829-
if machinePoolReplicas != instanePoolSize {
860+
if !annotations.ReplicasManagedByExternalAutoscaler(machinePoolScope.MachinePool) && machinePoolReplicas != instanePoolSize {
830861
return true
831862
} else if !(reflect.DeepEqual(machinePoolScope.OCIMachinePool.Spec.InstanceConfiguration.InstanceConfigurationId, instancePool.InstanceConfigurationId)) {
832863
return true

cloud/scope/machine_pool_test.go

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import (
3333
"github.com/oracle/oci-go-sdk/v65/core"
3434
corev1 "k8s.io/api/core/v1"
3535
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
36+
"k8s.io/apimachinery/pkg/runtime"
3637
"k8s.io/apimachinery/pkg/types"
3738
clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2"
3839
"sigs.k8s.io/controller-runtime/pkg/client"
@@ -1876,6 +1877,22 @@ func TestInstancePoolUpdate(t *testing.T) {
18761877
}, nil)
18771878
},
18781879
},
1880+
{
1881+
name: "no update due to change in replica size as annotation is set",
1882+
errorExpected: false,
1883+
instancepool: &core.InstancePool{
1884+
Size: common.Int(3),
1885+
InstanceConfigurationId: common.String("config_id"),
1886+
},
1887+
testSpecificSetup: func(ms *MachinePoolScope) {
1888+
ms.MachinePool.Annotations = map[string]string{
1889+
clusterv1.ReplicasManagedByAnnotation: "", // empty value counts as true (= externally managed)
1890+
}
1891+
newReplicas := int32(4)
1892+
ms.MachinePool.Spec.Replicas = &newReplicas
1893+
ms.OCIMachinePool.Spec.InstanceConfiguration.InstanceConfigurationId = common.String("config_id")
1894+
},
1895+
},
18791896
}
18801897

18811898
for _, tc := range tests {
@@ -1893,3 +1910,120 @@ func TestInstancePoolUpdate(t *testing.T) {
18931910
})
18941911
}
18951912
}
1913+
1914+
func TestSyncReplicasFromInstancePool(t *testing.T) {
1915+
var (
1916+
ms *MachinePoolScope
1917+
mockCtrl *gomock.Controller
1918+
)
1919+
1920+
setup := func(t *testing.T, g *WithT) {
1921+
var err error
1922+
mockCtrl = gomock.NewController(t)
1923+
secret := &corev1.Secret{
1924+
ObjectMeta: metav1.ObjectMeta{
1925+
Name: "bootstrap",
1926+
Namespace: "default",
1927+
},
1928+
Data: map[string][]byte{
1929+
"value": []byte("test"),
1930+
},
1931+
}
1932+
ociCluster := &infrastructurev1beta2.OCICluster{
1933+
ObjectMeta: metav1.ObjectMeta{
1934+
UID: "cluster_uid",
1935+
},
1936+
Spec: infrastructurev1beta2.OCIClusterSpec{
1937+
CompartmentId: "test-compartment",
1938+
OCIResourceIdentifier: "resource_uid",
1939+
},
1940+
}
1941+
replicas := int32(3)
1942+
infraMachinePool := &infrav2exp.OCIMachinePool{
1943+
ObjectMeta: metav1.ObjectMeta{
1944+
Name: "test",
1945+
Namespace: "default",
1946+
},
1947+
}
1948+
machinePool := &clusterv1.MachinePool{
1949+
ObjectMeta: metav1.ObjectMeta{
1950+
Name: "test",
1951+
Namespace: "default",
1952+
},
1953+
Spec: clusterv1.MachinePoolSpec{
1954+
Replicas: &replicas,
1955+
Template: clusterv1.MachineTemplateSpec{
1956+
Spec: clusterv1.MachineSpec{
1957+
Bootstrap: clusterv1.Bootstrap{
1958+
DataSecretName: common.String("bootstrap"),
1959+
},
1960+
},
1961+
},
1962+
},
1963+
}
1964+
scheme := runtime.NewScheme()
1965+
g.Expect(corev1.AddToScheme(scheme)).To(Succeed())
1966+
g.Expect(clusterv1.AddToScheme(scheme)).To(Succeed())
1967+
g.Expect(infrav2exp.AddToScheme(scheme)).To(Succeed())
1968+
1969+
client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(secret, infraMachinePool, machinePool).Build()
1970+
ms, err = NewMachinePoolScope(MachinePoolScopeParams{
1971+
ComputeManagementClient: mock_computemanagement.NewMockClient(mockCtrl),
1972+
OCIMachinePool: infraMachinePool,
1973+
OCIClusterAccessor: OCISelfManagedCluster{
1974+
OCICluster: ociCluster,
1975+
},
1976+
Cluster: &clusterv1.Cluster{},
1977+
MachinePool: machinePool,
1978+
Client: client,
1979+
})
1980+
g.Expect(err).To(BeNil())
1981+
}
1982+
teardown := func(t *testing.T, g *WithT) {
1983+
mockCtrl.Finish()
1984+
}
1985+
1986+
tests := []struct {
1987+
name string
1988+
setup func(ms *MachinePoolScope)
1989+
instancePool *core.InstancePool
1990+
expectedReplicas int32
1991+
}{
1992+
{
1993+
name: "does not patch replicas when annotation is not set",
1994+
setup: func(ms *MachinePoolScope) {
1995+
ms.MachinePool.Annotations = nil
1996+
},
1997+
instancePool: &core.InstancePool{Size: common.Int(4)},
1998+
expectedReplicas: 3,
1999+
},
2000+
{
2001+
name: "patches replicas from observed size when annotation is set",
2002+
setup: func(ms *MachinePoolScope) {
2003+
ms.MachinePool.Annotations = map[string]string{
2004+
clusterv1.ReplicasManagedByAnnotation: "", // empty value counts as true (= externally managed)
2005+
}
2006+
},
2007+
instancePool: &core.InstancePool{Size: common.Int(4)},
2008+
expectedReplicas: 4,
2009+
},
2010+
}
2011+
2012+
for _, tc := range tests {
2013+
t.Run(tc.name, func(t *testing.T) {
2014+
g := NewWithT(t)
2015+
defer teardown(t, g)
2016+
setup(t, g)
2017+
tc.setup(ms)
2018+
2019+
err := ms.SyncReplicasFromInstancePool(context.Background(), tc.instancePool)
2020+
g.Expect(err).To(BeNil())
2021+
2022+
updatedMachinePool := &clusterv1.MachinePool{}
2023+
err = ms.Client.Get(context.Background(), client.ObjectKey{Name: ms.MachinePool.Name, Namespace: ms.MachinePool.Namespace}, updatedMachinePool)
2024+
g.Expect(err).To(BeNil())
2025+
g.Expect(updatedMachinePool.Spec.Replicas).ToNot(BeNil())
2026+
g.Expect(*updatedMachinePool.Spec.Replicas).To(Equal(tc.expectedReplicas))
2027+
})
2028+
}
2029+
}

config/rbac/role.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
---
32
apiVersion: rbac.authorization.k8s.io/v1
43
kind: ClusterRole
@@ -134,6 +133,8 @@ rules:
134133
- get
135134
- list
136135
- watch
136+
- update
137+
- patch
137138
- apiGroups:
138139
- ""
139140
resources:

exp/controllers/ocimachinepool_controller.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,10 @@ func (r *OCIMachinePoolReconciler) reconcileNormal(ctx context.Context, logger l
387387
return reconcile.Result{}, err
388388
}
389389

390+
if err := machinePoolScope.SyncReplicasFromInstancePool(ctx, instancePool); err != nil {
391+
return reconcile.Result{}, err
392+
}
393+
390394
instancePool, err = machinePoolScope.UpdatePool(ctx, instancePool)
391395
if err != nil {
392396
r.Recorder.Eventf(machinePoolScope.OCIMachinePool, corev1.EventTypeWarning, "FailedUpdate", "Failed to update instance pool: %v", err)

exp/controllers/ocimachinepool_controller_test.go

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ func TestReconciliationFunction(t *testing.T) {
223223
computeManagementClient = mock_computemanagement.NewMockClient(mockCtrl)
224224
machinePool := getMachinePool()
225225
ociMachinePool = getOCIMachinePool()
226-
client := fake.NewClientBuilder().WithScheme(testScheme()).WithStatusSubresource(ociMachinePool).WithObjects(getSecret(), ociMachinePool).Build()
226+
client := fake.NewClientBuilder().WithScheme(testScheme()).WithStatusSubresource(ociMachinePool).WithObjects(getSecret(), ociMachinePool, machinePool).Build()
227227
ociCluster := getOCIClusterWithOwner()
228228
ms, err = scope.NewMachinePoolScope(scope.MachinePoolScopeParams{
229229
ComputeManagementClient: computeManagementClient,
@@ -383,6 +383,82 @@ func TestReconciliationFunction(t *testing.T) {
383383
g.Expect(*machine.Spec.OCID).To(Equal("id-1"))
384384
},
385385
},
386+
{
387+
name: "instance pool running with externally managed replicas",
388+
errorExpected: false,
389+
conditionAssertion: []conditionAssertion{{infrav2exp.LaunchTemplateReadyCondition, corev1.ConditionTrue, "", ""}, {infrav2exp.InstancePoolReadyCondition, corev1.ConditionTrue, "", ""}},
390+
testSpecificSetup: func(t *test, machinePoolScope *scope.MachinePoolScope, computeManagementClient *mock_computemanagement.MockClient) {
391+
ms.OCIMachinePool.Spec.InstanceConfiguration = infrav2exp.InstanceConfiguration{
392+
Shape: common.String("test-shape"),
393+
InstanceConfigurationId: common.String("test"),
394+
}
395+
ms.MachinePool.Annotations = map[string]string{
396+
clusterv1beta1.ReplicasManagedByAnnotation: "",
397+
}
398+
r.Client = interceptor.NewClient(fake.NewClientBuilder().WithScheme(testScheme()).WithObjects(getSecret(), ociMachinePool, ms.MachinePool).Build(), interceptor.Funcs{
399+
Create: func(ctx context.Context, client client.WithWatch, obj client.Object, opts ...client.CreateOption) error {
400+
m := obj.(*infrav2exp.OCIMachinePoolMachine)
401+
t.createPoolMachines = append(t.createPoolMachines, *m)
402+
return nil
403+
},
404+
})
405+
ms.Client = r.Client
406+
ms.MachinePool.Spec.Template.Spec.Bootstrap.DataSecretName = common.String("bootstrap")
407+
ms.OCIMachinePool.Spec.OCID = common.String("pool-id")
408+
computeManagementClient.EXPECT().GetInstanceConfiguration(gomock.Any(), gomock.Eq(core.GetInstanceConfigurationRequest{
409+
InstanceConfigurationId: common.String("test"),
410+
})).
411+
Return(core.GetInstanceConfigurationResponse{
412+
InstanceConfiguration: core.InstanceConfiguration{
413+
Id: common.String("test"),
414+
InstanceDetails: core.ComputeInstanceDetails{
415+
LaunchDetails: &core.InstanceConfigurationLaunchInstanceDetails{
416+
DefinedTags: definedTagsInterface,
417+
FreeformTags: tags,
418+
CompartmentId: common.String("test-compartment"),
419+
Shape: common.String("test-shape"),
420+
CreateVnicDetails: &core.InstanceConfigurationCreateVnicDetails{
421+
FreeformTags: tags,
422+
NsgIds: []string{"worker-nsg-id"},
423+
SubnetId: common.String("worker-subnet-id"),
424+
},
425+
SourceDetails: core.InstanceConfigurationInstanceSourceViaImageDetails{},
426+
Metadata: map[string]string{"user_data": "dGVzdA=="},
427+
},
428+
},
429+
},
430+
}, nil)
431+
432+
computeManagementClient.EXPECT().GetInstancePool(gomock.Any(), gomock.Any()).
433+
Return(core.GetInstancePoolResponse{
434+
InstancePool: core.InstancePool{
435+
LifecycleState: core.InstancePoolLifecycleStateRunning,
436+
Id: common.String("pool-id"),
437+
InstanceConfigurationId: common.String("test"),
438+
Size: common.Int(4),
439+
},
440+
}, nil)
441+
computeManagementClient.EXPECT().ListInstancePoolInstances(gomock.Any(), gomock.Any()).
442+
Return(core.ListInstancePoolInstancesResponse{
443+
Items: []core.InstanceSummary{{
444+
Id: common.String("id-1"),
445+
State: common.String("Running"),
446+
DisplayName: common.String("name-1"),
447+
}},
448+
}, nil)
449+
computeManagementClient.EXPECT().ListInstanceConfigurations(gomock.Any(), gomock.Any()).
450+
Return(core.ListInstanceConfigurationsResponse{}, nil)
451+
},
452+
validate: func(g *WithT, t *test) {
453+
g.Expect(len(t.createPoolMachines)).To(Equal(1))
454+
455+
updatedMachinePool := &clusterv1.MachinePool{}
456+
err := ms.Client.Get(context.Background(), client.ObjectKey{Name: ms.MachinePool.Name, Namespace: ms.MachinePool.Namespace}, updatedMachinePool)
457+
g.Expect(err).To(BeNil())
458+
g.Expect(updatedMachinePool.Spec.Replicas).ToNot(BeNil())
459+
g.Expect(*updatedMachinePool.Spec.Replicas).To(Equal(int32(4)))
460+
},
461+
},
386462
{
387463
name: "delete unwanted machinepool machine",
388464
errorExpected: false,

0 commit comments

Comments
 (0)