Skip to content

Commit b7feb18

Browse files
committed
Create external managed kubernetes cluster on CloudStack
1 parent 53105d4 commit b7feb18

16 files changed

+442
-9
lines changed

api/v1beta3/cloudstackcluster_types.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ type CloudStackClusterStatus struct {
4343
// +optional
4444
FailureDomains clusterv1.FailureDomains `json:"failureDomains,omitempty"`
4545

46+
// Id of CAPC managed kubernetes cluster created in CloudStack
47+
// +optional
48+
CloudStackClusterID string `json:"cloudStackClusterId"`
49+
4650
// Reflects the readiness of the CS cluster.
4751
Ready bool `json:"ready"`
4852
}

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,9 @@ spec:
422422
status:
423423
description: The actual cluster state reported by CloudStack.
424424
properties:
425+
cloudStackClusterId:
426+
description: Id of CAPC managed kubernetes cluster created in CloudStack
427+
type: string
425428
failureDomains:
426429
additionalProperties:
427430
description: FailureDomainSpec is the Schema for Cluster API failure

controllers/cloudstackcluster_controller.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"context"
2121
"fmt"
2222
"reflect"
23+
"strings"
2324

2425
ctrl "sigs.k8s.io/controller-runtime"
2526
"sigs.k8s.io/controller-runtime/pkg/controller"
@@ -94,9 +95,28 @@ func (r *CloudStackClusterReconciliationRunner) Reconcile() (res ctrl.Result, re
9495
r.GetFailureDomains(r.FailureDomains),
9596
r.RemoveExtraneousFailureDomains(r.FailureDomains),
9697
r.VerifyFailureDomainCRDs,
98+
r.GetOrCreateUnmanagedCluster,
9799
r.SetReady)
98100
}
99101

102+
// GetOrCreateUnmanagedCluster checks if an unmanaged cluster is present in Cloudstack else creates one.
103+
func (r *CloudStackClusterReconciliationRunner) GetOrCreateUnmanagedCluster() (ctrl.Result, error) {
104+
res, err := r.AsFailureDomainUser(&r.CSCluster.Spec.FailureDomains[0])()
105+
if r.ShouldReturn(res, err) {
106+
return res, err
107+
}
108+
err = r.CSUser.GetOrCreateUnmanagedCluster(r.CAPICluster, r.ReconciliationSubject, &r.FailureDomains.Items[0].Spec)
109+
if err != nil {
110+
if strings.Contains(err.Error(), "Kubernetes Service plugin is disabled") {
111+
r.Log.Info("Kubernetes Service plugin is disabled on CloudStack. Skipping ExternalManaged kubernetes cluster creation")
112+
return ctrl.Result{}, nil
113+
}
114+
// Not requeueing the failure to support CloudStack v4.18 and before
115+
r.Log.Info(fmt.Sprintf("Failed creating ExternalManaged kubernetes cluster on CloudStack. Error: %s", err.Error()))
116+
}
117+
return ctrl.Result{}, nil
118+
}
119+
100120
// SetReady adds a finalizer and sets the cluster status to ready.
101121
func (r *CloudStackClusterReconciliationRunner) SetReady() (ctrl.Result, error) {
102122
controllerutil.AddFinalizer(r.ReconciliationSubject, infrav1.ClusterFinalizer)
@@ -151,10 +171,30 @@ func (r *CloudStackClusterReconciliationRunner) ReconcileDelete() (ctrl.Result,
151171
}
152172
return r.RequeueWithMessage("Child FailureDomains still present, requeueing.")
153173
}
174+
if res, err := r.DeleteUnmanagedCluster(); r.ShouldReturn(res, err) {
175+
return res, err
176+
}
154177
controllerutil.RemoveFinalizer(r.ReconciliationSubject, infrav1.ClusterFinalizer)
155178
return ctrl.Result{}, nil
156179
}
157180

181+
// DeleteUnmanagedCluster checks if an unmanaged cluster is present in Cloudstack and then deletes it.
182+
func (r *CloudStackClusterReconciliationRunner) DeleteUnmanagedCluster() (ctrl.Result, error) {
183+
// If field is present and delete fails, then requeue
184+
res, err := r.AsFailureDomainUser(&r.CSCluster.Spec.FailureDomains[0])()
185+
if r.ShouldReturn(res, err) {
186+
return res, err
187+
}
188+
err = r.CSUser.DeleteUnmanagedCluster(r.ReconciliationSubject)
189+
if err != nil {
190+
if strings.Contains(err.Error(), " not found") {
191+
return ctrl.Result{}, nil
192+
}
193+
return r.RequeueWithMessage(fmt.Sprintf("Deleting unmanaged kubernetes cluster on CloudStack failed. error: %s", err.Error()))
194+
}
195+
return ctrl.Result{}, nil
196+
}
197+
158198
// Called in main, this registers the cluster reconciler to the CAPI controller manager.
159199
func (reconciler *CloudStackClusterReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager, opts controller.Options) error {
160200
log := ctrl.LoggerFrom(ctx)

controllers/cloudstackfailuredomain_controller.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ package controllers
1818

1919
import (
2020
"context"
21+
"sort"
22+
2123
"github.com/pkg/errors"
2224
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
2325
"k8s.io/apimachinery/pkg/runtime/schema"
@@ -26,7 +28,6 @@ import (
2628
"sigs.k8s.io/controller-runtime/pkg/client"
2729
"sigs.k8s.io/controller-runtime/pkg/controller"
2830
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
29-
"sort"
3031

3132
infrav1 "sigs.k8s.io/cluster-api-provider-cloudstack/api/v1beta3"
3233
csCtrlrUtils "sigs.k8s.io/cluster-api-provider-cloudstack/controllers/utils"

controllers/cloudstackmachine_controller.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ func (r *CloudStackMachineReconciliationRunner) Reconcile() (retRes ctrl.Result,
128128
r.RequeueIfInstanceNotRunning,
129129
r.AddToLBIfNeeded,
130130
r.GetOrCreateMachineStateChecker,
131+
r.AttachVM,
131132
)
132133
}
133134

@@ -214,6 +215,20 @@ func (r *CloudStackMachineReconciliationRunner) DeleteMachineIfFailuredomainNotE
214215
return ctrl.Result{}, nil
215216
}
216217

218+
// AttachVM adds the VM to CloudStack Unmanaged kubernetes.
219+
// No action taken if it fails
220+
func (r *CloudStackMachineReconciliationRunner) AttachVM() (retRes ctrl.Result, reterr error) {
221+
_ = r.CSUser.AddVMToUnmanagedCluster(r.CSCluster, r.ReconciliationSubject)
222+
return ctrl.Result{}, nil
223+
}
224+
225+
// RemoveVM removes the VM from CloudStack Unmanaged kubernetes.
226+
// No action taken if it fails
227+
func (r *CloudStackMachineReconciliationRunner) RemoveVM() (retRes ctrl.Result, reterr error) {
228+
_ = r.CSUser.RemoveVMFromUnmanagedCluster(r.CSCluster, r.ReconciliationSubject)
229+
return ctrl.Result{}, nil
230+
}
231+
217232
// GetOrCreateVMInstance gets or creates a VM instance.
218233
// Implicitly it also fetches its bootstrap secret in order to create said instance.
219234
func (r *CloudStackMachineReconciliationRunner) GetOrCreateVMInstance() (retRes ctrl.Result, reterr error) {
@@ -341,6 +356,10 @@ func (r *CloudStackMachineReconciliationRunner) ReconcileDelete() (retRes ctrl.R
341356
return ctrl.Result{}, err
342357
}
343358

359+
res, err := r.RemoveVM()
360+
if r.ShouldReturn(res, err) {
361+
return res, err
362+
}
344363
controllerutil.RemoveFinalizer(r.ReconciliationSubject, infrav1.MachineFinalizer)
345364
r.Log.Info("VM Deleted", "instanceID", r.ReconciliationSubject.Spec.InstanceID)
346365
return ctrl.Result{}, nil

controllers/cloudstackmachine_controller_test.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,17 @@ var _ = Describe("CloudStackMachineReconciler", func() {
5454
// Setup a failure domain for the machine reconciler to find.
5555
Ω(k8sClient.Create(ctx, dummies.CSFailureDomain1)).Should(Succeed())
5656
setClusterReady(k8sClient)
57+
58+
mockCloudClient.EXPECT().GetOrCreateUnmanagedCluster(gomock.Any(), gomock.Any(), gomock.Any()).Do(
59+
func(arg1, _, _ interface{}) {
60+
arg1.(*infrav1.CloudStackCluster).Status.CloudStackClusterID = "cluster-id-123"
61+
}).AnyTimes().Return(nil)
62+
63+
mockCloudClient.EXPECT().AddVMToUnmanagedCluster(
64+
gomock.Any(), gomock.Any()).AnyTimes().Return(nil)
65+
66+
mockCloudClient.EXPECT().RemoveVMFromUnmanagedCluster(
67+
gomock.Any(), gomock.Any()).AnyTimes().Return(nil)
5768
})
5869

5970
It("Should call GetOrCreateVMInstance and set Status.Ready to true", func() {
@@ -240,6 +251,8 @@ var _ = Describe("CloudStackMachineReconciler", func() {
240251
func(arg1, _, _, _, _, _ interface{}) {
241252
arg1.(*infrav1.CloudStackMachine).Status.InstanceState = "Running"
242253
}).AnyTimes()
254+
mockCloudClient.EXPECT().AddVMToUnmanagedCluster(
255+
gomock.Any(), gomock.Any()).AnyTimes().Return(nil)
243256
Ω(fakeCtrlClient.Get(ctx, key, dummies.CSCluster)).Should(Succeed())
244257
Ω(fakeCtrlClient.Create(ctx, dummies.CAPIMachine)).Should(Succeed())
245258
Ω(fakeCtrlClient.Create(ctx, dummies.CSMachine1)).Should(Succeed())

controllers/controllers_suite_test.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,17 @@ import (
2121
"flag"
2222
"fmt"
2323
"go/build"
24-
"k8s.io/client-go/tools/record"
2524
"os"
2625
"os/exec"
2726
"path/filepath"
2827
"regexp"
29-
"sigs.k8s.io/cluster-api-provider-cloudstack/test/fakes"
3028
"strings"
3129
"testing"
3230
"time"
3331

32+
"k8s.io/client-go/tools/record"
33+
"sigs.k8s.io/cluster-api-provider-cloudstack/test/fakes"
34+
3435
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3536
"k8s.io/klog/v2"
3637
"k8s.io/klog/v2/klogr"

go.mod

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ module sigs.k8s.io/cluster-api-provider-cloudstack
33
go 1.19
44

55
require (
6-
github.com/apache/cloudstack-go/v2 v2.15.0
6+
github.com/apache/cloudstack-go/v2 v2.16.0-rc.2
77
github.com/go-logr/logr v1.2.4
88
github.com/golang/mock v1.6.0
99
github.com/hashicorp/go-multierror v1.1.1
@@ -91,3 +91,5 @@ require (
9191
)
9292

9393
replace github.com/dgrijalva/jwt-go => github.com/golang-jwt/jwt/v4 v4.0.0 // Indirect upgrade to address https://github.com/advisories/GHSA-w73w-5m7g-f7qc
94+
95+
replace github.com/apache/cloudstack-go/v2 => github.com/shapeblue/cloudstack-go/v2 v2.9.1-0.20230717062313-73e4efc8a510

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,6 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy
2222
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
2323
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
2424
github.com/antlr/antlr4/runtime/Go/antlr v1.4.10 h1:yL7+Jz0jTC6yykIK/Wh74gnTJnrGr5AyrNMXuA0gves=
25-
github.com/apache/cloudstack-go/v2 v2.15.0 h1:oojn1qx0+wBwrFSSmA2rL8XjWd4BXqwYo0RVCrAXoHk=
26-
github.com/apache/cloudstack-go/v2 v2.15.0/go.mod h1:Mc+tXpujtslBuZFk5atoGT2LanVxOrXS2GGgidAoz1A=
2725
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
2826
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
2927
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
@@ -304,6 +302,8 @@ github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD
304302
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
305303
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
306304
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
305+
github.com/shapeblue/cloudstack-go/v2 v2.9.1-0.20230717062313-73e4efc8a510 h1:FPRBv784robz6sZSqDGfZDZMse31lj96i+enH02Xzds=
306+
github.com/shapeblue/cloudstack-go/v2 v2.9.1-0.20230717062313-73e4efc8a510/go.mod h1:Mc+tXpujtslBuZFk5atoGT2LanVxOrXS2GGgidAoz1A=
307307
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
308308
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
309309
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=

pkg/cloud/client.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import (
3737
//go:generate ../../hack/tools/bin/mockgen -destination=../mocks/mock_client.go -package=mocks sigs.k8s.io/cluster-api-provider-cloudstack/pkg/cloud Client
3838

3939
type Client interface {
40+
ClusterIface
4041
VMIface
4142
NetworkIface
4243
AffinityGroupIface

pkg/cloud/cluster.go

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
/*
2+
Copyright 2023 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 cloud
18+
19+
import (
20+
"fmt"
21+
"strings"
22+
23+
"github.com/apache/cloudstack-go/v2/cloudstack"
24+
"github.com/pkg/errors"
25+
infrav1 "sigs.k8s.io/cluster-api-provider-cloudstack/api/v1beta3"
26+
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
27+
)
28+
29+
type ClusterIface interface {
30+
GetOrCreateUnmanagedCluster(*clusterv1.Cluster, *infrav1.CloudStackCluster, *infrav1.CloudStackFailureDomainSpec) error
31+
DeleteUnmanagedCluster(*infrav1.CloudStackCluster) error
32+
AddVMToUnmanagedCluster(*infrav1.CloudStackCluster, *infrav1.CloudStackMachine) error
33+
RemoveVMFromUnmanagedCluster(*infrav1.CloudStackCluster, *infrav1.CloudStackMachine) error
34+
}
35+
36+
type ClustertypeSetter interface {
37+
SetClustertype(string)
38+
}
39+
40+
func withExternalManaged() cloudstack.OptionFunc {
41+
return func(cs *cloudstack.CloudStackClient, p interface{}) error {
42+
ps, ok := p.(ClustertypeSetter)
43+
if !ok {
44+
return errors.New("invalid params type")
45+
}
46+
ps.SetClustertype("ExternalManaged")
47+
return nil
48+
}
49+
}
50+
51+
func (c *client) GetOrCreateUnmanagedCluster(cluster *clusterv1.Cluster, csCluster *infrav1.CloudStackCluster, fd *infrav1.CloudStackFailureDomainSpec) error {
52+
// Get cluster
53+
if csCluster.Status.CloudStackClusterID != "" {
54+
externalManagedCluster, count, err := c.cs.Kubernetes.GetKubernetesClusterByID(csCluster.Status.CloudStackClusterID, withExternalManaged())
55+
if err != nil {
56+
return err
57+
} else if count > 0 {
58+
csCluster.Status.CloudStackClusterID = externalManagedCluster.Id
59+
return nil
60+
}
61+
}
62+
63+
// Check if a cluster exists with the same name
64+
clusterName := fmt.Sprintf("%s - %s - %s", cluster.GetName(), csCluster.GetName(), csCluster.GetUID())
65+
externalManagedCluster, count, err := c.cs.Kubernetes.GetKubernetesClusterByName(clusterName, withExternalManaged())
66+
if err != nil && !strings.Contains(err.Error(), "No match found for ") {
67+
return err
68+
}
69+
if count > 0 {
70+
csCluster.Status.CloudStackClusterID = externalManagedCluster.Id
71+
} else if err == nil || (err != nil && strings.Contains(err.Error(), "No match found for ")) {
72+
// Create cluster
73+
domain := Domain{Path: rootDomain}
74+
if csCluster.Spec.FailureDomains[0].Domain != "" {
75+
domain.Path = fd.Domain
76+
}
77+
_ = c.ResolveDomain(&domain)
78+
79+
accountName := csCluster.Spec.FailureDomains[0].Account
80+
if accountName == "" {
81+
userParams := c.cs.User.NewGetUserParams(c.config.APIKey)
82+
user, err := c.cs.User.GetUser(userParams)
83+
if err != nil {
84+
return err
85+
}
86+
accountName = user.Account
87+
}
88+
params := c.cs.Kubernetes.NewCreateKubernetesClusterParams(fmt.Sprintf("%s managed by CAPC", clusterName), clusterName, fd.Zone.ID)
89+
90+
setIfNotEmpty(accountName, params.SetAccount)
91+
setIfNotEmpty(domain.ID, params.SetDomainid)
92+
setIfNotEmpty(fd.Zone.Network.ID, params.SetNetworkid)
93+
setIfNotEmpty(csCluster.Spec.ControlPlaneEndpoint.Host, params.SetExternalloadbalanceripaddress)
94+
params.SetClustertype("ExternalManaged")
95+
96+
cloudStackCKSCluster, err := c.cs.Kubernetes.CreateKubernetesCluster(params)
97+
if err != nil {
98+
return err
99+
}
100+
csCluster.Status.CloudStackClusterID = cloudStackCKSCluster.Id
101+
}
102+
return nil
103+
}
104+
105+
func (c *client) DeleteUnmanagedCluster(csCluster *infrav1.CloudStackCluster) error {
106+
if csCluster.Status.CloudStackClusterID != "" {
107+
csUnmanagedCluster, count, err := c.cs.Kubernetes.GetKubernetesClusterByID(csCluster.Status.CloudStackClusterID, withExternalManaged())
108+
if err != nil && strings.Contains(err.Error(), " not found") {
109+
return nil
110+
}
111+
if count != 0 {
112+
params := c.cs.Kubernetes.NewDeleteKubernetesClusterParams(csUnmanagedCluster.Id)
113+
_, err = c.cs.Kubernetes.DeleteKubernetesCluster(params)
114+
if err != nil {
115+
return err
116+
}
117+
}
118+
csCluster.Status.CloudStackClusterID = ""
119+
}
120+
return nil
121+
}
122+
123+
func (c *client) AddVMToUnmanagedCluster(csCluster *infrav1.CloudStackCluster, csMachine *infrav1.CloudStackMachine) error {
124+
if csCluster.Status.CloudStackClusterID != "" {
125+
params := c.cs.Kubernetes.NewAddVirtualMachinesToKubernetesClusterParams(csCluster.Status.CloudStackClusterID, []string{*csMachine.Spec.InstanceID})
126+
_, err := c.cs.Kubernetes.AddVirtualMachinesToKubernetesCluster(params)
127+
return err
128+
}
129+
return nil
130+
}
131+
132+
func (c *client) RemoveVMFromUnmanagedCluster(csCluster *infrav1.CloudStackCluster, csMachine *infrav1.CloudStackMachine) error {
133+
if csCluster.Status.CloudStackClusterID != "" {
134+
params := c.cs.Kubernetes.NewRemoveVirtualMachinesFromKubernetesClusterParams(csCluster.Status.CloudStackClusterID, []string{*csMachine.Spec.InstanceID})
135+
_, err := c.cs.Kubernetes.RemoveVirtualMachinesFromKubernetesCluster(params)
136+
return err
137+
}
138+
return nil
139+
}

test/e2e/common.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,17 @@ func DownloadMetricsFromCAPCManager(ctx context.Context, bootstrapKubeconfigPath
251251
return result, nil
252252
}
253253

254+
func GetACSVersion(client *cloudstack.CloudStackClient) (string, error) {
255+
msServersResp, err := client.InfrastructureUsage.ListManagementServersMetrics(client.InfrastructureUsage.NewListManagementServersMetricsParams())
256+
if err != nil {
257+
return "", err
258+
}
259+
if msServersResp.Count == 0 {
260+
return "", errors.New("no management servers found")
261+
}
262+
return msServersResp.ManagementServersMetrics[0].Version, nil
263+
}
264+
254265
func DestroyOneMachine(client *cloudstack.CloudStackClient, clusterName string, machineType string) {
255266
matcher := clusterName + "-" + machineType
256267

0 commit comments

Comments
 (0)