Skip to content

Commit 88b84d7

Browse files
committed
✨ Add ControlPlane upgrade featureGate
1 parent 975a898 commit 88b84d7

File tree

7 files changed

+140
-46
lines changed

7 files changed

+140
-46
lines changed

virtualcluster/pkg/controller/controllers/provisioner/provisioner.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,6 @@ type Provisioner interface {
2626
CreateVirtualCluster(ctx context.Context, vc *tenancyv1alpha1.VirtualCluster) error
2727
DeleteVirtualCluster(ctx context.Context, vc *tenancyv1alpha1.VirtualCluster) error
2828
GetProvisioner() string
29+
// UpgradeVirtualCluster is used to apply current clusterversion if featuregate.VirtualClusterApplyUpdate enabled
30+
UpgradeVirtualCluster(ctx context.Context, vc *tenancyv1alpha1.VirtualCluster) error
2931
}

virtualcluster/pkg/controller/controllers/provisioner/provisioner_aliyun.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,3 +288,7 @@ OuterLoop:
288288
func (mpa *Aliyun) GetProvisioner() string {
289289
return "aliyun"
290290
}
291+
292+
func (mpa *Aliyun) UpgradeVirtualCluster(ctx context.Context, vc *tenancyv1alpha1.VirtualCluster) error {
293+
return fmt.Errorf("not implemented")
294+
}

virtualcluster/pkg/controller/controllers/provisioner/provisioner_native.go

Lines changed: 72 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ import (
2525

2626
"github.com/go-logr/logr"
2727
corev1 "k8s.io/api/core/v1"
28-
apierrors "k8s.io/apimachinery/pkg/api/errors"
2928
"k8s.io/apimachinery/pkg/runtime"
3029
"k8s.io/client-go/util/cert"
3130
"sigs.k8s.io/controller-runtime/pkg/client"
@@ -36,7 +35,9 @@ import (
3635
vcpki "sigs.k8s.io/cluster-api-provider-nested/virtualcluster/pkg/controller/pki"
3736
"sigs.k8s.io/cluster-api-provider-nested/virtualcluster/pkg/controller/secret"
3837
kubeutil "sigs.k8s.io/cluster-api-provider-nested/virtualcluster/pkg/controller/util/kube"
38+
"sigs.k8s.io/cluster-api-provider-nested/virtualcluster/pkg/syncer/constants"
3939
"sigs.k8s.io/cluster-api-provider-nested/virtualcluster/pkg/syncer/conversion"
40+
"sigs.k8s.io/cluster-api-provider-nested/virtualcluster/pkg/syncer/util/featuregate"
4041
pkiutil "sigs.k8s.io/cluster-api-provider-nested/virtualcluster/pkg/util/pki"
4142
)
4243

@@ -45,6 +46,11 @@ const (
4546
ComponentPollPeriodSec = 2
4647
)
4748

49+
var (
50+
definitelyTrue = true
51+
patchOptions = &client.PatchOptions{Force: &definitelyTrue, FieldManager: "virtualcluster/provisioner/native"}
52+
)
53+
4854
type Native struct {
4955
client.Client
5056
scheme *runtime.Scheme
@@ -61,60 +67,93 @@ func NewProvisionerNative(mgr manager.Manager, log logr.Logger, provisionerTimeo
6167
}, nil
6268
}
6369

70+
func updateLabelClusterVersionApplied(vc *tenancyv1alpha1.VirtualCluster, cv *tenancyv1alpha1.ClusterVersion) {
71+
if featuregate.DefaultFeatureGate.Enabled(featuregate.VirtualClusterApplyUpdate) {
72+
if vc.Labels == nil {
73+
vc.Labels = map[string]string{}
74+
}
75+
vc.Labels[constants.LabelClusterVersionApplied] = cv.ObjectMeta.ResourceVersion
76+
}
77+
}
78+
6479
// CreateVirtualCluster sets up the control plane for vc on meta k8s
6580
func (mpn *Native) CreateVirtualCluster(ctx context.Context, vc *tenancyv1alpha1.VirtualCluster) error {
81+
cv, err := mpn.fetchClusterVersion(vc)
82+
if err != nil {
83+
return err
84+
}
85+
86+
updateLabelClusterVersionApplied(vc, cv)
87+
88+
// 1. create the root ns
89+
_, err = kubeutil.CreateRootNS(mpn, vc)
90+
if err != nil {
91+
return err
92+
}
93+
return mpn.applyVirtualCluster(ctx, cv, vc)
94+
}
95+
96+
func (mpn *Native) fetchClusterVersion(vc *tenancyv1alpha1.VirtualCluster) (*tenancyv1alpha1.ClusterVersion, error) {
6697
cvObjectKey := client.ObjectKey{Name: vc.Spec.ClusterVersionName}
6798
cv := &tenancyv1alpha1.ClusterVersion{}
6899
if err := mpn.Get(context.Background(), cvObjectKey, cv); err != nil {
69100
err = fmt.Errorf("desired ClusterVersion %s not found",
70101
vc.Spec.ClusterVersionName)
71-
return err
102+
return nil, err
72103
}
104+
return cv, nil
105+
}
73106

74-
// 1. create the root ns
75-
_, err := kubeutil.CreateRootNS(mpn, vc)
107+
func (mpn *Native) UpgradeVirtualCluster(ctx context.Context, vc *tenancyv1alpha1.VirtualCluster) error {
108+
cv, err := mpn.fetchClusterVersion(vc)
76109
if err != nil {
77110
return err
78111
}
112+
if cvVersion, ok := vc.Labels[constants.LabelClusterVersionApplied]; ok && cvVersion == cv.ObjectMeta.ResourceVersion {
113+
mpn.Log.Info("cluster is already in desired version")
114+
return nil
115+
}
116+
updateLabelClusterVersionApplied(vc, cv)
117+
return mpn.applyVirtualCluster(ctx, cv, vc)
118+
}
119+
120+
func (mpn *Native) applyVirtualCluster(ctx context.Context, cv *tenancyv1alpha1.ClusterVersion, vc *tenancyv1alpha1.VirtualCluster) error {
121+
var err error
79122
isClusterIP := cv.Spec.APIServer.Service != nil && cv.Spec.APIServer.Service.Spec.Type == corev1.ServiceTypeClusterIP
80-
// if ClusterIP, have to create API Server ahead of time to lay it down in the PKI
123+
// if ClusterIP, have to update API Server ahead of time to lay it down in the PKI
81124
if isClusterIP {
82-
mpn.Log.Info("deploying ClusterIP Service for API component", "component", cv.Spec.APIServer.Name)
125+
mpn.Log.Info("applying ClusterIP Service for API component", "component", cv.Spec.APIServer.Name)
83126
complementAPIServerTemplate(conversion.ToClusterKey(vc), cv.Spec.APIServer)
84-
err = mpn.Create(context.TODO(), cv.Spec.APIServer.Service)
127+
err := mpn.Patch(ctx, cv.Spec.APIServer.Service, client.Apply, patchOptions)
85128
if err != nil {
86-
if !apierrors.IsAlreadyExists(err) {
87-
return err
88-
}
89-
mpn.Log.Info("service already exist",
90-
"service", cv.Spec.APIServer.Service.GetName())
129+
mpn.Log.Error(err, "failed to update service", "service", cv.Spec.APIServer.Service.GetName())
130+
return err
91131
}
92132
}
93133

94-
// 2. create PKI
95-
err = mpn.createPKI(vc, cv, isClusterIP)
134+
// 2. apply PKI
135+
err = mpn.createAndApplyPKI(ctx, vc, cv, isClusterIP)
96136
if err != nil {
97137
return err
98138
}
99139

100140
// 3. deploy etcd
101-
err = mpn.deployComponent(vc, cv.Spec.ETCD)
141+
err = mpn.deployComponent(ctx, vc, cv.Spec.ETCD)
102142
if err != nil {
103143
return err
104144
}
105145

106146
// 4. deploy apiserver
107-
err = mpn.deployComponent(vc, cv.Spec.APIServer)
147+
err = mpn.deployComponent(ctx, vc, cv.Spec.APIServer)
108148
if err != nil {
109149
return err
110150
}
111151

112152
// 5. deploy controller-manager
113-
err = mpn.deployComponent(vc, cv.Spec.ControllerManager)
153+
err = mpn.deployComponent(ctx, vc, cv.Spec.ControllerManager)
114154
if err != nil {
115155
return err
116156
}
117-
118157
return nil
119158
}
120159

@@ -162,7 +201,7 @@ func complementCtrlMgrTemplate(vcns string, ctrlMgrBdl *tenancyv1alpha1.Stateful
162201

163202
// deployComponent deploys control plane component in namespace vcName based on the given StatefulSet
164203
// and Service Bundle ssBdl
165-
func (mpn *Native) deployComponent(vc *tenancyv1alpha1.VirtualCluster, ssBdl *tenancyv1alpha1.StatefulSetSvcBundle) error {
204+
func (mpn *Native) deployComponent(ctx context.Context, vc *tenancyv1alpha1.VirtualCluster, ssBdl *tenancyv1alpha1.StatefulSetSvcBundle) error {
166205
mpn.Log.Info("deploying StatefulSet for control plane component", "component", ssBdl.Name)
167206

168207
ns := conversion.ToClusterKey(vc)
@@ -178,26 +217,17 @@ func (mpn *Native) deployComponent(vc *tenancyv1alpha1.VirtualCluster, ssBdl *te
178217
return fmt.Errorf("try to deploy unknown component: %s", ssBdl.Name)
179218
}
180219

181-
err := mpn.Create(context.TODO(), ssBdl.StatefulSet)
220+
err := mpn.Patch(ctx, ssBdl.StatefulSet, client.Apply, patchOptions)
182221
if err != nil {
183-
if !apierrors.IsAlreadyExists(err) {
184-
return err
185-
}
186-
mpn.Log.Info("statefuleset already exist",
187-
"statefuleset", ssBdl.StatefulSet.GetName(),
188-
"namespace", ssBdl.StatefulSet.GetNamespace())
222+
return err
189223
}
190224

191225
// skip apiserver clusterIP service creation as it is already created in CreateVirtualCluster()
192226
if ssBdl.Service != nil && !(ssBdl.Name == "apiserver" && ssBdl.Service.Spec.Type == corev1.ServiceTypeClusterIP) {
193227
mpn.Log.Info("deploying Service for control plane component", "component", ssBdl.Name)
194-
err = mpn.Create(context.TODO(), ssBdl.Service)
228+
err := mpn.Patch(ctx, ssBdl.Service, client.Apply, patchOptions)
195229
if err != nil {
196-
if !apierrors.IsAlreadyExists(err) {
197-
return err
198-
}
199-
mpn.Log.Info("service already exist",
200-
"service", ssBdl.Service.GetName())
230+
return err
201231
}
202232
}
203233

@@ -209,9 +239,9 @@ func (mpn *Native) deployComponent(vc *tenancyv1alpha1.VirtualCluster, ssBdl *te
209239
return nil
210240
}
211241

212-
// createPKISecrets creates secrets to store crt/key pairs and kubeconfigs
242+
// createOrUpdatePKISecrets creates secrets to store crt/key pairs and kubeconfigs
213243
// for control plane components of the virtual cluster
214-
func (mpn *Native) createPKISecrets(caGroup *vcpki.ClusterCAGroup, namespace string) error {
244+
func (mpn *Native) createOrUpdatePKISecrets(ctx context.Context, caGroup *vcpki.ClusterCAGroup, namespace string) error {
215245
// create secret for root crt/key pair
216246
rootSrt := secret.CrtKeyPairToSecret(secret.RootCASecretName, namespace, caGroup.RootCA)
217247
// create secret for apiserver crt/key pair
@@ -240,25 +270,21 @@ func (mpn *Native) createPKISecrets(caGroup *vcpki.ClusterCAGroup, namespace str
240270

241271
// create all secrets on metacluster
242272
for _, srt := range secrets {
243-
mpn.Log.Info("creating secret", "name",
273+
mpn.Log.Info("applying secret", "name",
244274
srt.Name, "namespace", srt.Namespace)
245-
err := mpn.Create(context.TODO(), srt)
275+
276+
err := mpn.Patch(ctx, srt, client.Apply, patchOptions)
246277
if err != nil {
247-
if !apierrors.IsAlreadyExists(err) {
248-
return err
249-
}
250-
mpn.Log.Info("Secret already exists",
251-
"secret", srt.Name,
252-
"namespace", srt.Namespace)
278+
return err
253279
}
254280
}
255281

256282
return nil
257283
}
258284

259-
// createPKI constructs the PKI (all crt/key pair and kubeconfig) for the
285+
// createAndApplyPKI constructs the PKI (all crt/key pair and kubeconfig) for the
260286
// virtual clusters, and store them as secrets in the meta cluster
261-
func (mpn *Native) createPKI(vc *tenancyv1alpha1.VirtualCluster, cv *tenancyv1alpha1.ClusterVersion, isClusterIP bool) error {
287+
func (mpn *Native) createAndApplyPKI(ctx context.Context, vc *tenancyv1alpha1.VirtualCluster, cv *tenancyv1alpha1.ClusterVersion, isClusterIP bool) error {
262288
ns := conversion.ToClusterKey(vc)
263289
caGroup := &vcpki.ClusterCAGroup{}
264290
// create root ca, all components will share a single root ca
@@ -346,15 +372,15 @@ func (mpn *Native) createPKI(vc *tenancyv1alpha1.VirtualCluster, cv *tenancyv1al
346372
caGroup.ServiceAccountPrivateKey = svcAcctCAPair
347373

348374
// store ca and kubeconfig into secrets
349-
genSrtsErr := mpn.createPKISecrets(caGroup, ns)
375+
genSrtsErr := mpn.createOrUpdatePKISecrets(ctx, caGroup, ns)
350376
if genSrtsErr != nil {
351377
return genSrtsErr
352378
}
353379

354380
return nil
355381
}
356382

357-
func (mpn *Native) DeleteVirtualCluster(ctx context.Context, vc *tenancyv1alpha1.VirtualCluster) error {
383+
func (mpn *Native) DeleteVirtualCluster(_ context.Context, _ *tenancyv1alpha1.VirtualCluster) error {
358384
return nil
359385
}
360386

virtualcluster/pkg/controller/controllers/virtualcluster_controller.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ import (
2525

2626
"github.com/go-logr/logr"
2727
apierrors "k8s.io/apimachinery/pkg/api/errors"
28+
"k8s.io/apimachinery/pkg/types"
29+
"k8s.io/client-go/util/retry"
2830

2931
ctrl "sigs.k8s.io/controller-runtime"
3032
"sigs.k8s.io/controller-runtime/pkg/client"
@@ -35,6 +37,8 @@ import (
3537
"sigs.k8s.io/cluster-api-provider-nested/virtualcluster/pkg/controller/controllers/provisioner"
3638
kubeutil "sigs.k8s.io/cluster-api-provider-nested/virtualcluster/pkg/controller/util/kube"
3739
strutil "sigs.k8s.io/cluster-api-provider-nested/virtualcluster/pkg/controller/util/strings"
40+
"sigs.k8s.io/cluster-api-provider-nested/virtualcluster/pkg/syncer/constants"
41+
"sigs.k8s.io/cluster-api-provider-nested/virtualcluster/pkg/syncer/util/featuregate"
3842
)
3943

4044
// GetProvisioner returns a new provisioner.Provisioner by ProvisionerName
@@ -163,6 +167,39 @@ func (r *ReconcileVirtualCluster) Reconcile(ctx context.Context, request reconci
163167
return
164168
case tenancyv1alpha1.ClusterRunning:
165169
r.Log.Info("VirtualCluster is running", "vc", vc.GetName())
170+
if !featuregate.DefaultFeatureGate.Enabled(featuregate.VirtualClusterApplyUpdate) {
171+
return
172+
}
173+
if isReady, ok := vc.Labels[constants.LabelVCReadyForUpgrade]; !ok || isReady != "true" {
174+
return
175+
}
176+
r.Log.Info("VirtualCluster is ready for upgrade", "vc", vc.GetName())
177+
err = r.Provisioner.UpgradeVirtualCluster(ctx, vc)
178+
if err != nil {
179+
r.Log.Error(err, "fail to upgrade virtualcluster", "vc", vc.GetName())
180+
kubeutil.SetVCStatus(vc, tenancyv1alpha1.ClusterRunning, fmt.Sprintf("fail to upgrade: %s", err), "TenantControlPlaneUpgradeFailed")
181+
} else {
182+
r.Log.Info("upgrade finished", "vc", vc.GetName())
183+
kubeutil.SetVCStatus(vc, tenancyv1alpha1.ClusterRunning, "tenant control plane is upgraded", "TenantControlPlaneUpgradeCompleted")
184+
}
185+
186+
err = retry.RetryOnConflict(retry.DefaultRetry, func() error {
187+
vcStatus := vc.Status
188+
delete(vc.Labels, constants.LabelVCReadyForUpgrade)
189+
vcLabels := vc.Labels
190+
updateErr := r.Update(ctx, vc)
191+
if updateErr != nil {
192+
if err := r.Get(ctx, types.NamespacedName{
193+
Namespace: vc.GetNamespace(),
194+
Name: vc.GetName(),
195+
}, vc); err != nil {
196+
r.Log.Info("fail to get obj on update failure", "object", vc.GetName(), "error", err.Error())
197+
}
198+
vc.Status = vcStatus
199+
vc.Labels = vcLabels
200+
}
201+
return updateErr
202+
})
166203
return
167204
case tenancyv1alpha1.ClusterError:
168205
r.Log.Info("fail to create virtualcluster", "vc", vc.GetName())

virtualcluster/pkg/controller/secret/secret.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@ func RsaKeyToSecret(name, namespace string, rsaKey *rsa.PrivateKey) (*corev1.Sec
5050
return nil, err
5151
}
5252
return &corev1.Secret{
53+
TypeMeta: metav1.TypeMeta{
54+
Kind: "Secret",
55+
APIVersion: corev1.SchemeGroupVersion.String(),
56+
},
5357
ObjectMeta: metav1.ObjectMeta{
5458
Name: name,
5559
Namespace: namespace,
@@ -65,6 +69,10 @@ func RsaKeyToSecret(name, namespace string, rsaKey *rsa.PrivateKey) (*corev1.Sec
6569
// CrtKeyPairToSecret encapsulates ca/key pair ckp into a secret object
6670
func CrtKeyPairToSecret(name, namespace string, ckp *vcpki.CrtKeyPair) *corev1.Secret {
6771
return &corev1.Secret{
72+
TypeMeta: metav1.TypeMeta{
73+
Kind: "Secret",
74+
APIVersion: corev1.SchemeGroupVersion.String(),
75+
},
6876
ObjectMeta: metav1.ObjectMeta{
6977
Name: name,
7078
Namespace: namespace,
@@ -80,6 +88,10 @@ func CrtKeyPairToSecret(name, namespace string, ckp *vcpki.CrtKeyPair) *corev1.S
8088
// KubeconfigToSecret encapsulates kubeconfig cfgContent into a secret object
8189
func KubeconfigToSecret(name, namespace string, cfgContent string) *corev1.Secret {
8290
return &corev1.Secret{
91+
TypeMeta: metav1.TypeMeta{
92+
Kind: "Secret",
93+
APIVersion: corev1.SchemeGroupVersion.String(),
94+
},
8395
ObjectMeta: metav1.ObjectMeta{
8496
Name: name,
8597
Namespace: namespace,

virtualcluster/pkg/syncer/constants/constants.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,14 @@ const (
5050
// LabelVCRootNS means the namespace is the rootns created by vc-manager.
5151
LabelVCRootNS = "tenancy.x-k8s.io/vcrootns"
5252

53+
// LabelVCReadyForUpgrade is set to "true" when the cluster is ready for the upgrade being applied
54+
// (use featuregate.VirtualClusterApplyUpdate to enable it in the provisioner)
55+
LabelVCReadyForUpgrade = "tenancy.x-k8s.io/ready-for-upgrade"
56+
57+
// LabelClusterVersionApplied should be set equal to the ClusterVersion.metadata.resourceVersion value
58+
// This label is used in featuregate.VirtualClusterApplyUpdate to compare if the update must be applied.
59+
LabelClusterVersionApplied = "tenancy.x-k8s.io/cluster-version-applied"
60+
5361
// LabelExternalApiserverDomain is the domain name for apiserver url from outside the cluster
5462
LabelExternalApiserverDomain = "tenancy.x-k8s.io/external-apiserver-domain"
5563

virtualcluster/pkg/syncer/util/featuregate/gate.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,10 @@ const (
6464
// vn-agent to run as a daemonset but run without hostNetworking and
6565
// accessed by the PodIP on each pod on the node
6666
VNodeProviderPodIP = "VNodeProviderPodIP"
67+
68+
// VirtualClusterApplyUpdate is an experimental feature that allows the cluster provisioner
69+
// to apply ClusterVersion updates if VirtualCluster object is requested it
70+
VirtualClusterApplyUpdate = "VirtualClusterApplyUpdate"
6771
)
6872

6973
var defaultFeatures = FeatureList{
@@ -74,6 +78,7 @@ var defaultFeatures = FeatureList{
7478
VNodeProviderService: {Default: false},
7579
TenantAllowDNSPolicy: {Default: false},
7680
VNodeProviderPodIP: {Default: false},
81+
VirtualClusterApplyUpdate: {Default: false},
7782
}
7883

7984
type Feature string

0 commit comments

Comments
 (0)