Skip to content

Commit fb346d8

Browse files
authored
Merge pull request #350 from shapeblue/reconcile-capc-cluster-to-cks
Reconcile CAPC Cluster as CloudStack CKS Cluster (Default: Opt Out)
2 parents c43cec6 + 5bbb717 commit fb346d8

33 files changed

+931
-42
lines changed

api/v1beta1/conversion.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ func Convert_v1beta1_CloudStackCluster_To_v1beta3_CloudStackCluster(in *CloudSta
5151
//nolint:golint,revive,stylecheck
5252
func Convert_v1beta3_CloudStackCluster_To_v1beta1_CloudStackCluster(in *v1beta3.CloudStackCluster, out *CloudStackCluster, scope conv.Scope) error {
5353
if len(in.Spec.FailureDomains) < 1 {
54-
return fmt.Errorf("v1beta3 to v1beta1 conversion not supported when < 1 failure domain is provided. Input CloudStackCluster spec %s", in.Spec)
54+
return fmt.Errorf("v1beta3 to v1beta1 conversion not supported when < 1 failure domain is provided. Input CloudStackCluster spec %v", in.Spec)
5555
}
5656
out.ObjectMeta = in.ObjectMeta
5757
out.Spec = CloudStackClusterSpec{

api/v1beta2/cloudstackcluster_conversion.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ limitations under the License.
1717
package v1beta2
1818

1919
import (
20+
machineryconversion "k8s.io/apimachinery/pkg/conversion"
2021
"sigs.k8s.io/cluster-api-provider-cloudstack/api/v1beta3"
2122
"sigs.k8s.io/controller-runtime/pkg/conversion"
2223
)
@@ -30,3 +31,11 @@ func (dst *CloudStackCluster) ConvertFrom(srcRaw conversion.Hub) error { // noli
3031
src := srcRaw.(*v1beta3.CloudStackCluster)
3132
return Convert_v1beta3_CloudStackCluster_To_v1beta2_CloudStackCluster(src, dst, nil)
3233
}
34+
35+
func Convert_v1beta3_CloudStackClusterSpec_To_v1beta2_CloudStackClusterSpec(in *v1beta3.CloudStackClusterSpec, out *CloudStackClusterSpec, s machineryconversion.Scope) error { // nolint
36+
return autoConvert_v1beta3_CloudStackClusterSpec_To_v1beta2_CloudStackClusterSpec(in, out, s)
37+
}
38+
39+
func Convert_v1beta3_CloudStackClusterStatus_To_v1beta2_CloudStackClusterStatus(in *v1beta3.CloudStackClusterStatus, out *CloudStackClusterStatus, s machineryconversion.Scope) error { // nolint
40+
return autoConvert_v1beta3_CloudStackClusterStatus_To_v1beta2_CloudStackClusterStatus(in, out, s)
41+
}

api/v1beta2/zz_generated.conversion.go

Lines changed: 12 additions & 20 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api/v1beta3/cloudstackcluster_types.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ type CloudStackClusterSpec struct {
3434

3535
// The kubernetes control plane endpoint.
3636
ControlPlaneEndpoint clusterv1.APIEndpoint `json:"controlPlaneEndpoint"`
37+
38+
// SyncWithACS determines if an externalManaged CKS cluster should be created on ACS.
39+
// +optional
40+
SyncWithACS *bool `json:"syncWithACS,omitempty"`
3741
}
3842

3943
// The status of the CloudStackCluster object.
@@ -43,6 +47,10 @@ type CloudStackClusterStatus struct {
4347
// +optional
4448
FailureDomains clusterv1.FailureDomains `json:"failureDomains,omitempty"`
4549

50+
// Id of CAPC managed kubernetes cluster created in CloudStack
51+
// +optional
52+
CloudStackClusterID string `json:"cloudStackClusterId"`
53+
4654
// Reflects the readiness of the CS cluster.
4755
Ready bool `json:"ready"`
4856
}

api/v1beta3/zz_generated.deepcopy.go

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

config/crd/bases/infrastructure.cluster.x-k8s.io_cloudstackclusters.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,13 +415,20 @@ spec:
415415
- zone
416416
type: object
417417
type: array
418+
syncWithACS:
419+
description: SyncWithACS determines if an externalManaged CKS cluster
420+
should be created on ACS.
421+
type: boolean
418422
required:
419423
- controlPlaneEndpoint
420424
- failureDomains
421425
type: object
422426
status:
423427
description: The actual cluster state reported by CloudStack.
424428
properties:
429+
cloudStackClusterId:
430+
description: Id of CAPC managed kubernetes cluster created in CloudStack
431+
type: string
425432
failureDomains:
426433
additionalProperties:
427434
description: FailureDomainSpec is the Schema for Cluster API failure

config/manager/manager.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ spec:
2727
- "--metrics-bind-addr=localhost:8080"
2828
- "--cloudstackcluster-concurrency=${CAPC_CLOUDSTACKCLUSTER_CONCURRENCY:=10}"
2929
- "--cloudstackmachine-concurrency=${CAPC_CLOUDSTACKMACHINE_CONCURRENCY:=10}"
30+
- "--enable-cloudstack-cks-sync=${CAPC_CLOUDSTACKMACHINE_CKS_SYNC:=false}"
3031
image: controller:latest
3132
name: manager
3233
securityContext:

controllers/cks_cluster_controller.go

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/*
2+
Copyright 2022 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package controllers
18+
19+
import (
20+
"context"
21+
"fmt"
22+
"strings"
23+
24+
ctrl "sigs.k8s.io/controller-runtime"
25+
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
26+
27+
infrav1 "sigs.k8s.io/cluster-api-provider-cloudstack/api/v1beta3"
28+
csCtrlrUtils "sigs.k8s.io/cluster-api-provider-cloudstack/controllers/utils"
29+
)
30+
31+
const CksClusterFinalizer = "ckscluster.infrastructure.cluster.x-k8s.io"
32+
33+
// RBAC permissions for CloudStackCluster.
34+
// +kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=cloudstackclusters,verbs=get;list;watch;update;patch
35+
// +kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=cloudstackclusters/status,verbs=create;get;update;patch
36+
// +kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=cloudstackclusters/finalizers,verbs=update
37+
38+
// CksClusterReconciliationRunner is a ReconciliationRunner with extensions specific to CloudStackClusters.
39+
// The runner does the actual reconciliation.
40+
type CksClusterReconciliationRunner struct {
41+
*csCtrlrUtils.ReconciliationRunner
42+
FailureDomains *infrav1.CloudStackFailureDomainList
43+
ReconciliationSubject *infrav1.CloudStackCluster
44+
}
45+
46+
// CksClusterReconciler is the k8s controller manager's interface to reconcile a CloudStackCluster.
47+
type CksClusterReconciler struct {
48+
csCtrlrUtils.ReconcilerBase
49+
}
50+
51+
// Initialize a new CloudStackCluster reconciliation runner with concrete types and initialized member fields.
52+
func NewCksClusterReconciliationRunner() *CksClusterReconciliationRunner {
53+
// Set concrete type and init pointers.
54+
runner := &CksClusterReconciliationRunner{ReconciliationSubject: &infrav1.CloudStackCluster{}}
55+
runner.FailureDomains = &infrav1.CloudStackFailureDomainList{}
56+
// Setup the base runner. Initializes pointers and links reconciliation methods.
57+
runner.ReconciliationRunner = csCtrlrUtils.NewRunner(runner, runner.ReconciliationSubject, "CKSClusterController")
58+
runner.CSCluster = runner.ReconciliationSubject
59+
return runner
60+
}
61+
62+
// Reconcile is the method k8s will call upon a reconciliation request.
63+
func (reconciler *CksClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request) (retRes ctrl.Result, retErr error) {
64+
r := NewCksClusterReconciliationRunner()
65+
r.UsingBaseReconciler(reconciler.ReconcilerBase).ForRequest(req).WithRequestCtx(ctx)
66+
r.WithAdditionalCommonStages(r.GetFailureDomains(r.FailureDomains))
67+
return r.RunBaseReconciliationStages()
68+
}
69+
70+
// Reconcile actually reconciles the CloudStackCluster.
71+
func (r *CksClusterReconciliationRunner) Reconcile() (res ctrl.Result, reterr error) {
72+
if r.CSCluster.Spec.SyncWithACS == nil || !*r.CSCluster.Spec.SyncWithACS || len(r.FailureDomains.Items) == 0 {
73+
return ctrl.Result{}, nil
74+
}
75+
// Prevent premature deletion.
76+
controllerutil.AddFinalizer(r.ReconciliationSubject, CksClusterFinalizer)
77+
78+
res, err := r.AsFailureDomainUser(&r.FailureDomains.Items[0].Spec)()
79+
if r.ShouldReturn(res, err) {
80+
return res, err
81+
}
82+
83+
r.Log.Info("Creating entry with CKS")
84+
err = r.CSUser.GetOrCreateCksCluster(r.CAPICluster, r.ReconciliationSubject, &r.FailureDomains.Items[0].Spec)
85+
if err != nil {
86+
return r.RequeueWithMessage(fmt.Sprintf("Failed creating ExternalManaged CKS cluster on CloudStack. error: %s", err.Error()))
87+
}
88+
return ctrl.Result{}, nil
89+
}
90+
91+
// ReconcileDelete cleans up resources used by the cluster and finally removes the CloudStackCluster's finalizers.
92+
func (r *CksClusterReconciliationRunner) ReconcileDelete() (ctrl.Result, error) {
93+
if r.ReconciliationSubject.Status.CloudStackClusterID != "" {
94+
if len(r.FailureDomains.Items) == 0 {
95+
return ctrl.Result{}, fmt.Errorf("no failure domains found")
96+
}
97+
res, err := r.AsFailureDomainUser(&r.FailureDomains.Items[0].Spec)()
98+
if r.ShouldReturn(res, err) {
99+
return res, err
100+
}
101+
err = r.CSUser.DeleteCksCluster(r.ReconciliationSubject)
102+
if err != nil && !strings.Contains(err.Error(), " not found") {
103+
return r.RequeueWithMessage(fmt.Sprintf("Deleting cks cluster on CloudStack failed. error: %s", err.Error()))
104+
}
105+
}
106+
controllerutil.RemoveFinalizer(r.ReconciliationSubject, CksClusterFinalizer)
107+
return ctrl.Result{}, nil
108+
}
109+
110+
// SetupWithManager sets up the controller with the Manager.
111+
func (reconciler *CksClusterReconciler) SetupWithManager(mgr ctrl.Manager) error {
112+
return ctrl.NewControllerManagedBy(mgr).
113+
For(&infrav1.CloudStackCluster{}).
114+
Complete(reconciler)
115+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/*
2+
Copyright 2022 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package controllers_test
18+
19+
import (
20+
"github.com/golang/mock/gomock"
21+
. "github.com/onsi/ginkgo/v2"
22+
. "github.com/onsi/gomega"
23+
"k8s.io/apimachinery/pkg/api/errors"
24+
infrav1 "sigs.k8s.io/cluster-api-provider-cloudstack/api/v1beta3"
25+
"sigs.k8s.io/cluster-api-provider-cloudstack/controllers"
26+
"sigs.k8s.io/cluster-api-provider-cloudstack/pkg/cloud"
27+
dummies "sigs.k8s.io/cluster-api-provider-cloudstack/test/dummies/v1beta3"
28+
"sigs.k8s.io/controller-runtime/pkg/client"
29+
"sigs.k8s.io/controller-runtime/pkg/controller"
30+
)
31+
32+
var _ = Describe("CksCloudStackClusterReconciler", func() {
33+
Context("With k8s like test environment.", func() {
34+
BeforeEach(func() {
35+
dummies.SetDummyVars()
36+
SetupTestEnvironment()
37+
Ω(ClusterReconciler.SetupWithManager(ctx, k8sManager, controller.Options{})).Should(Succeed()) // Register CloudStack ClusterReconciler.
38+
Ω(FailureDomainReconciler.SetupWithManager(k8sManager, controller.Options{})).Should(Succeed()) // Register CloudStack FailureDomainReconciler.
39+
Ω(CksClusterReconciler.SetupWithManager(k8sManager)).Should(Succeed()) // Register CloudStack Cks ClusterReconciler.
40+
})
41+
42+
It("Should create a cluster in CKS.", func() {
43+
mockCloudClient.EXPECT().GetOrCreateCksCluster(gomock.Any(), gomock.Any(), gomock.Any()).Do(func(_, arg1, _ interface{}) {
44+
arg1.(*infrav1.CloudStackCluster).Status.CloudStackClusterID = "cluster-id-123"
45+
}).MinTimes(1).Return(nil)
46+
mockCloudClient.EXPECT().ResolveZone(gomock.Any()).AnyTimes()
47+
mockCloudClient.EXPECT().ResolveNetworkForZone(gomock.Any()).AnyTimes().Do(
48+
func(arg1 interface{}) {
49+
arg1.(*infrav1.CloudStackZoneSpec).Network.ID = "SomeID"
50+
arg1.(*infrav1.CloudStackZoneSpec).Network.Type = cloud.NetworkTypeShared
51+
}).MinTimes(1)
52+
53+
Eventually(func() string {
54+
key := client.ObjectKeyFromObject(dummies.CSCluster)
55+
if err := k8sClient.Get(ctx, key, dummies.CSCluster); err != nil {
56+
return ""
57+
}
58+
return dummies.CSCluster.Status.CloudStackClusterID
59+
}, timeout).WithPolling(pollInterval).Should(Equal("cluster-id-123"))
60+
61+
})
62+
})
63+
64+
Context("With k8s like test environment.", func() {
65+
BeforeEach(func() {
66+
dummies.SetDummyVars()
67+
dummies.CSCluster.Status.CloudStackClusterID = "cluster-id-123"
68+
SetupTestEnvironment()
69+
Ω(ClusterReconciler.SetupWithManager(ctx, k8sManager, controller.Options{})).Should(Succeed()) // Register CloudStack ClusterReconciler.
70+
Ω(FailureDomainReconciler.SetupWithManager(k8sManager, controller.Options{})).Should(Succeed()) // Register CloudStack FailureDomainReconciler.
71+
Ω(CksClusterReconciler.SetupWithManager(k8sManager)).Should(Succeed()) // Register CloudStack Cks ClusterReconciler.
72+
})
73+
74+
It("Should delete the cluster in CKS.", func() {
75+
76+
Ω(k8sClient.Delete(ctx, dummies.CSCluster)).Should(Succeed())
77+
78+
Eventually(func() bool {
79+
csCluster := &infrav1.CloudStackCluster{}
80+
csClusterKey := client.ObjectKey{Namespace: dummies.ClusterNameSpace, Name: dummies.CSCluster.Name}
81+
if err := k8sClient.Get(ctx, csClusterKey, csCluster); err != nil {
82+
return errors.IsNotFound(err)
83+
}
84+
return false
85+
}, timeout).WithPolling(pollInterval).Should(BeTrue())
86+
})
87+
88+
})
89+
90+
Context("Without a k8s test environment.", func() {
91+
It("Should create a reconciliation runner with a Cloudstack Cluster as the reconciliation subject.", func() {
92+
reconRunner := controllers.NewCksClusterReconciliationRunner()
93+
Ω(reconRunner.ReconciliationSubject).ShouldNot(BeNil())
94+
})
95+
})
96+
})

0 commit comments

Comments
 (0)