Skip to content

Commit 38bd29b

Browse files
committed
Create external managed kubernetes cluster on CloudStack
1 parent b860d9f commit 38bd29b

11 files changed

+212
-5
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
@@ -417,6 +417,9 @@ spec:
417417
status:
418418
description: The actual cluster state reported by CloudStack.
419419
properties:
420+
cloudStackClusterId:
421+
description: Id of CAPC managed kubernetes cluster created in CloudStack
422+
type: string
420423
failureDomains:
421424
additionalProperties:
422425
description: FailureDomainSpec is the Schema for Cluster API failure

controllers/cloudstackcluster_controller.go

Lines changed: 34 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"
@@ -93,9 +94,25 @@ func (r *CloudStackClusterReconciliationRunner) Reconcile() (res ctrl.Result, re
9394
r.GetFailureDomains(r.FailureDomains),
9495
r.RemoveExtraneousFailureDomains(r.FailureDomains),
9596
r.VerifyFailureDomainCRDs,
97+
r.GetOrCreateCluster,
9698
r.SetReady)
9799
}
98100

101+
// GetOrCreateCluster checks if an unmanaged cluster is present in Cloudstack else creates one.
102+
func (r *CloudStackClusterReconciliationRunner) GetOrCreateCluster() (ctrl.Result, error) {
103+
r.AsFailureDomainUser(&r.FailureDomains.Items[0].Spec)()
104+
err := r.CSUser.GetOrCreateCluster(r.CAPICluster, r.ReconciliationSubject, &r.FailureDomains.Items[0].Spec)
105+
if err != nil {
106+
if strings.Contains(err.Error(), "Kubernetes Service plugin is disabled") {
107+
r.Log.Info("Kubernetes Service plugin is disabled on CloudStack. Skipping creating unmanaged kubernets cluster")
108+
return ctrl.Result{}, nil
109+
} else {
110+
return r.RequeueWithMessage(fmt.Sprintf("Creating unmanaged kubernetes cluster failed. Error: %s", err.Error()))
111+
}
112+
}
113+
return ctrl.Result{}, nil
114+
}
115+
99116
// SetReady adds a finalizer and sets the cluster status to ready.
100117
func (r *CloudStackClusterReconciliationRunner) SetReady() (ctrl.Result, error) {
101118
controllerutil.AddFinalizer(r.ReconciliationSubject, infrav1.ClusterFinalizer)
@@ -150,10 +167,27 @@ func (r *CloudStackClusterReconciliationRunner) ReconcileDelete() (ctrl.Result,
150167
}
151168
return r.RequeueWithMessage("Child FailureDomains still present, requeueing.")
152169
}
170+
if res, err := r.DeleteCluster(); r.ShouldReturn(res, err) {
171+
return res, err
172+
}
153173
controllerutil.RemoveFinalizer(r.ReconciliationSubject, infrav1.ClusterFinalizer)
154174
return ctrl.Result{}, nil
155175
}
156176

177+
// DeleteCluster checks if an unmanaged cluster is present in Cloudstack and then deletes it.
178+
func (r *CloudStackClusterReconciliationRunner) DeleteCluster() (ctrl.Result, error) {
179+
// If field is present and delete fails, then requeue
180+
r.AsFailureDomainUser(&r.CSCluster.Spec.FailureDomains[0])()
181+
err := r.CSUser.DeleteCluster(r.ReconciliationSubject)
182+
if err != nil {
183+
if strings.Contains(err.Error(), " not found") {
184+
return ctrl.Result{}, nil
185+
}
186+
return r.RequeueWithMessage(fmt.Sprintf("Deleting unmanaged kubernetes cluster on CloudStack failed. error: %s", err.Error()))
187+
}
188+
return ctrl.Result{}, nil
189+
}
190+
157191
// Called in main, this registers the cluster reconciler to the CAPI controller manager.
158192
func (reconciler *CloudStackClusterReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager, opts controller.Options) error {
159193
controller, err := ctrl.NewControllerManagedBy(mgr).

controllers/cloudstackfailuredomain_controller.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,15 @@ 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"
2426
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
2527
ctrl "sigs.k8s.io/controller-runtime"
2628
"sigs.k8s.io/controller-runtime/pkg/client"
2729
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
28-
"sort"
2930

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

controllers/cloudstackmachine_controller.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ func (r *CloudStackMachineReconciliationRunner) Reconcile() (retRes ctrl.Result,
127127
r.RequeueIfInstanceNotRunning,
128128
r.AddToLBIfNeeded,
129129
r.GetOrCreateMachineStateChecker,
130+
r.AttachVM,
130131
)
131132
}
132133

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

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

358+
r.RemoveVM()
343359
controllerutil.RemoveFinalizer(r.ReconciliationSubject, infrav1.MachineFinalizer)
344360
r.Log.Info("VM Deleted", "instanceID", r.ReconciliationSubject.Spec.InstanceID)
345361
return ctrl.Result{}, nil

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: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,3 +105,5 @@ replace github.com/dgrijalva/jwt-go => github.com/golang-jwt/jwt/v4 v4.0.0 // In
105105
replace sigs.k8s.io/cluster-api/test => sigs.k8s.io/cluster-api/test v1.2.12
106106

107107
replace sigs.k8s.io/cluster-api => sigs.k8s.io/cluster-api v1.2.12
108+
109+
replace github.com/apache/cloudstack-go/v2 => github.com/shapeblue/cloudstack-go/v2 support-for-unmanaged-k8s

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: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
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+
GetOrCreateCluster(*clusterv1.Cluster, *infrav1.CloudStackCluster, *infrav1.CloudStackFailureDomainSpec) error
31+
DeleteCluster(*infrav1.CloudStackCluster) error
32+
AddVMToCluster(*infrav1.CloudStackCluster, *infrav1.CloudStackMachine) error
33+
RemoveVMFromCluster(*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) GetOrCreateCluster(cluster *clusterv1.Cluster, csCluster *infrav1.CloudStackCluster, fd *infrav1.CloudStackFailureDomainSpec) error {
52+
// Get cluster
53+
if csCluster.Status.CloudStackClusterID != "" {
54+
_, count, err := c.cs.Kubernetes.GetKubernetesClusterByID(csCluster.Status.CloudStackClusterID, withExternalManaged())
55+
if err != nil {
56+
return err
57+
}
58+
if count == 1 {
59+
return nil
60+
}
61+
}
62+
63+
// Check if a cluster exists with the same name
64+
clusterName := fmt.Sprintf("%s - %s", cluster.GetName(), csCluster.GetName())
65+
csUnmanagedCluster, 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+
// Create cluster
71+
domain := Domain{Path: rootDomain}
72+
if csCluster.Spec.FailureDomains[0].Domain != "" {
73+
domain.Path = fd.Domain
74+
}
75+
_ = c.ResolveDomain(&domain)
76+
77+
accountName := csCluster.Spec.FailureDomains[0].Account
78+
if accountName == "" {
79+
userParams := c.cs.User.NewGetUserParams(c.config.APIKey)
80+
user, err := c.cs.User.GetUser(userParams)
81+
if err != nil {
82+
return err
83+
}
84+
accountName = user.Account
85+
}
86+
params := c.cs.Kubernetes.NewCreateKubernetesClusterParams(fmt.Sprintf("%s managed by CAPC", clusterName), clusterName, fd.Zone.ID)
87+
88+
setIfNotEmpty(accountName, params.SetAccount)
89+
setIfNotEmpty(domain.ID, params.SetDomainid)
90+
setIfNotEmpty(fd.Zone.Network.ID, params.SetNetworkid)
91+
setIfNotEmpty(csCluster.Spec.ControlPlaneEndpoint.Host, params.SetExternalloadbalanceripaddress)
92+
params.SetClustertype("ExternalManaged")
93+
94+
r, err := c.cs.Kubernetes.CreateKubernetesCluster(params)
95+
if err != nil {
96+
return err
97+
}
98+
csUnmanagedCluster, count, err = c.cs.Kubernetes.GetKubernetesClusterByID(r.Id)
99+
if err != nil {
100+
return err
101+
}
102+
if count == 0 {
103+
return errors.New("cluster not found")
104+
}
105+
}
106+
csCluster.Status.CloudStackClusterID = csUnmanagedCluster.Id
107+
return nil
108+
}
109+
110+
func (c *client) DeleteCluster(csCluster *infrav1.CloudStackCluster) error {
111+
if csCluster.Status.CloudStackClusterID != "" {
112+
csUnmanagedCluster, count, err := c.cs.Kubernetes.GetKubernetesClusterByID(csCluster.Status.CloudStackClusterID, withExternalManaged())
113+
if err != nil {
114+
return err
115+
}
116+
if count != 0 {
117+
params := c.cs.Kubernetes.NewDeleteKubernetesClusterParams(csUnmanagedCluster.Id)
118+
_, err = c.cs.Kubernetes.DeleteKubernetesCluster(params)
119+
if err != nil {
120+
return err
121+
}
122+
}
123+
csCluster.Status.CloudStackClusterID = ""
124+
return nil
125+
}
126+
return nil
127+
}
128+
129+
func (c *client) AddVMToCluster(csCluster *infrav1.CloudStackCluster, csMachine *infrav1.CloudStackMachine) error {
130+
if csCluster.Status.CloudStackClusterID != "" {
131+
params := c.cs.Kubernetes.NewAddVirtualMachinesToKubernetesClusterParams(csCluster.Status.CloudStackClusterID, []string{*csMachine.Spec.InstanceID})
132+
_, err := c.cs.Kubernetes.AddVirtualMachinesToKubernetesCluster(params)
133+
return err
134+
}
135+
return nil
136+
}
137+
138+
func (c *client) RemoveVMFromCluster(csCluster *infrav1.CloudStackCluster, csMachine *infrav1.CloudStackMachine) error {
139+
if csCluster.Status.CloudStackClusterID != "" {
140+
params := c.cs.Kubernetes.NewRemoveVirtualMachinesFromKubernetesClusterParams(csCluster.Status.CloudStackClusterID, []string{*csMachine.Spec.InstanceID})
141+
_, err := c.cs.Kubernetes.RemoveVirtualMachinesFromKubernetesCluster(params)
142+
return err
143+
}
144+
return nil
145+
}

pkg/cloud/isolated_network.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ func (c *client) CreateIsolatedNetwork(fd *infrav1.CloudStackFailureDomain, isoN
100100
}
101101

102102
// Do isolated network creation.
103-
p := c.cs.Network.NewCreateNetworkParams(isoNet.Spec.Name, isoNet.Spec.Name, offeringID, fd.Spec.Zone.ID)
103+
p := c.cs.Network.NewCreateNetworkParams(isoNet.Spec.Name, offeringID, fd.Spec.Zone.ID)
104104
resp, err := c.cs.Network.CreateNetwork(p)
105105
if err != nil {
106106
c.customMetrics.EvaluateErrorAndIncrementAcsReconciliationErrorCounter(err)

pkg/cloud/isolated_network_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ var _ = Describe("Network", func() {
7272
dummies.Zone1.Network.ID = ""
7373

7474
nos.EXPECT().GetNetworkOfferingID(gomock.Any()).Return("someOfferingID", 1, nil)
75-
ns.EXPECT().NewCreateNetworkParams(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
75+
ns.EXPECT().NewCreateNetworkParams(gomock.Any(), gomock.Any(), gomock.Any()).
7676
Return(&csapi.CreateNetworkParams{})
7777
ns.EXPECT().GetNetworkByName(dummies.ISONet1.Name).Return(nil, 0, nil)
7878
ns.EXPECT().GetNetworkByID(dummies.ISONet1.ID).Return(nil, 0, nil)

0 commit comments

Comments
 (0)