diff --git a/pkg/cmd/kubeblocks/upgrade.go b/pkg/cmd/kubeblocks/upgrade.go index d49d52b89..8acd74355 100644 --- a/pkg/cmd/kubeblocks/upgrade.go +++ b/pkg/cmd/kubeblocks/upgrade.go @@ -31,6 +31,7 @@ import ( appsv1 "k8s.io/api/apps/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" apitypes "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/wait" "k8s.io/cli-runtime/pkg/genericiooptions" @@ -44,6 +45,7 @@ import ( "github.com/apecloud/kbcli/pkg/types" "github.com/apecloud/kbcli/pkg/util" "github.com/apecloud/kbcli/pkg/util/breakingchange" + "github.com/apecloud/kbcli/pkg/util/conversion" "github.com/apecloud/kbcli/pkg/util/helm" "github.com/apecloud/kbcli/pkg/util/prompt" ) @@ -205,6 +207,16 @@ func (o *InstallOptions) Upgrade() error { return err } + // save old version crs + conversionMeta := conversion.NewVersionConversion(o.Dynamic, o.OldVersion, o.Version) + s = spinner.New(o.Out, spinnerMsg("Conversion old version[%s] CRs to new version[%s]", o.OldVersion, o.Version)) + defer s.Fail() + var unstructuredObjects []unstructured.Unstructured + if unstructuredObjects, err = conversion.FetchAndConversionResources(conversionMeta); err != nil { + return fmt.Errorf("conversion crs failed: %s", err.Error()) + } + s.Success() + // create or update crds s = spinner.New(o.Out, spinnerMsg("Upgrade CRDs")) defer s.Fail() @@ -213,6 +225,14 @@ func (o *InstallOptions) Upgrade() error { } s.Success() + // conversion new version crs + s = spinner.New(o.Out, spinnerMsg("update new version CRs")) + defer s.Fail() + if err = conversion.UpdateNewVersionResources(conversionMeta, unstructuredObjects); err != nil { + return fmt.Errorf("update new crs failed: %s", err.Error()) + } + s.Success() + s = spinner.New(o.Out, spinnerMsg("Upgrading KubeBlocks "+msg)) defer s.Fail() // upgrade KubeBlocks chart diff --git a/pkg/types/types.go b/pkg/types/types.go index 02ed5c6d5..0940c00c4 100644 --- a/pkg/types/types.go +++ b/pkg/types/types.go @@ -409,6 +409,10 @@ func ConfigConstraintGVR() schema.GroupVersionResource { return schema.GroupVersionResource{Group: AppsAPIGroup, Version: AppsAPIBetaVersion, Resource: ResourceConfigConstraintVersions} } +func ConfigConstraintOldGVR() schema.GroupVersionResource { + return schema.GroupVersionResource{Group: AppsAPIGroup, Version: AppsAPIVersion, Resource: ResourceConfigConstraintVersions} +} + func StorageClassGVR() schema.GroupVersionResource { return schema.GroupVersionResource{ Group: "storage.k8s.io", diff --git a/pkg/util/breakingchange/upgrader.go b/pkg/util/breakingchange/upgrader.go index 9a51161fa..46f8b8046 100644 --- a/pkg/util/breakingchange/upgrader.go +++ b/pkg/util/breakingchange/upgrader.go @@ -61,14 +61,14 @@ func registerUpgradeHandler(fromVersions []string, toVersion string, upgradeHand var majorMinorFromVersions []string for _, v := range fromVersions { - majorMinorFromVersion := getMajorMinorVersion(v) + majorMinorFromVersion := GetMajorMinorVersion(v) if majorMinorFromVersion == "" { panic(formatErr(v)) } majorMinorFromVersions = append(majorMinorFromVersions, majorMinorFromVersion) } - majorMinorToVersion := getMajorMinorVersion(toVersion) + majorMinorToVersion := GetMajorMinorVersion(toVersion) if majorMinorToVersion == "" { panic(formatErr(toVersion)) } @@ -80,8 +80,8 @@ func registerUpgradeHandler(fromVersions []string, toVersion string, upgradeHand // getUpgradeHandler gets the upgrade handler according to fromVersion and toVersion from upgradeHandlerMapper. func getUpgradeHandler(fromVersion, toVersion string) upgradeHandler { - majorMinorFromVersion := getMajorMinorVersion(fromVersion) - majorMinorToVersion := getMajorMinorVersion(toVersion) + majorMinorFromVersion := GetMajorMinorVersion(fromVersion) + majorMinorToVersion := GetMajorMinorVersion(toVersion) handlerRecorder, ok := upgradeHandlerMapper[majorMinorToVersion] if !ok { return nil @@ -123,7 +123,7 @@ func ValidateUpgradeVersion(fromVersion, toVersion string) error { return nil } -func getMajorMinorVersion(version string) string { +func GetMajorMinorVersion(version string) string { vs := strings.Split(version, ".") if len(vs) < 2 { return "" diff --git a/pkg/util/conversion/conversion.go b/pkg/util/conversion/conversion.go new file mode 100644 index 000000000..69dbef29e --- /dev/null +++ b/pkg/util/conversion/conversion.go @@ -0,0 +1,133 @@ +/* +Copyright (C) 2022-2024 ApeCloud Co., Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package conversion + +import ( + "fmt" + + appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" + appsv1beta1 "github.com/apecloud/kubeblocks/apis/apps/v1beta1" + "github.com/apecloud/kubeblocks/pkg/constant" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + apiruntime "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/dynamic" + "k8s.io/klog/v2" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/apecloud/kbcli/pkg/types" + "github.com/apecloud/kbcli/pkg/util" +) + +const ( + OldVersion = "08" + NewVersion = "09" +) + +func FetchAndConversionResources(versionMeta *VersionConversionMeta) ([]unstructured.Unstructured, error) { + var resources []unstructured.Unstructured + + if versionMeta.FromVersion == versionMeta.ToVersion { + return nil, nil + } + + if versionMeta.FromVersion != OldVersion || versionMeta.ToVersion != NewVersion { + klog.V(1).Infof("not to convert configconstraint multiversion") + return nil, nil + } + + oldResources, err := ResourcesWithGVR(versionMeta, types.ConfigConstraintOldGVR(), metav1.ListOptions{}) + if err != nil { + return nil, err + } + for _, oldObj := range oldResources { + newObj := appsv1beta1.ConfigConstraint{ + TypeMeta: metav1.TypeMeta{ + Kind: types.KindConfigConstraint, + APIVersion: types.ConfigConstraintGVR().GroupVersion().String(), + }, + } + klog.V(1).Infof("convert configconstraint[%s] cr from v1alpha1 to v1beta1", oldObj.GetName()) + // If v1alpha1 is converted from v1beta1 version + if hasConversionVersion(&oldObj) { + klog.V(1).Infof("configconstraint[%s] v1alpha1 is converted from v1beta1 version and ignore.", + client.ObjectKeyFromObject(&oldObj).String()) + continue + } + // If the converted version v1beta1 already exists + if hasValidBetaVersion(&oldObj, versionMeta) { + klog.V(1).Infof("configconstraint[%s] v1beta1 already exist and ignore.", + client.ObjectKeyFromObject(&oldObj).String()) + continue + } + if err := oldObj.ConvertTo(&newObj); err != nil { + return nil, err + } + item, err := apiruntime.DefaultUnstructuredConverter.ToUnstructured(&newObj) + if err != nil { + return nil, err + } + resources = append(resources, unstructured.Unstructured{Object: item}) + } + return resources, nil +} + +func hasValidBetaVersion(obj *appsv1alpha1.ConfigConstraint, dynamic dynamic.Interface) bool { + newObj := appsv1beta1.ConfigConstraint{} + if err := util.GetResourceObjectFromGVR(types.ConfigConstraintGVR(), client.ObjectKeyFromObject(obj), dynamic, &newObj); err != nil { + return false + } + + return hasConversionVersion(&newObj) +} + +func hasConversionVersion(obj client.Object) bool { + annotations := obj.GetAnnotations() + if len(annotations) == 0 { + return false + } + return annotations[constant.KubeblocksAPIConversionTypeAnnotationName] == constant.MigratedAPIVersion +} + +func UpdateNewVersionResources(versionMeta *VersionConversionMeta, targetObjects []unstructured.Unstructured) error { + if len(targetObjects) == 0 { + return nil + } + if versionMeta.FromVersion == versionMeta.ToVersion { + return nil + } + + for _, unstructuredObj := range targetObjects { + klog.V(1).Infof("update CR %s", unstructuredObj.GetName()) + _, err := versionMeta.Resource(types.ConfigConstraintGVR()). + Update(versionMeta.Ctx, &unstructuredObj, metav1.UpdateOptions{}) + if err != nil { + if apierrors.IsNotFound(err) { + if _, err := versionMeta.Resource(types.ConfigConstraintGVR()). + Create(versionMeta.Ctx, &unstructuredObj, metav1.CreateOptions{}); err != nil { + klog.V(1).ErrorS(err, "failed to create configConstraint") + return err + } + continue + } + klog.V(1).ErrorS(err, fmt.Sprintf("failed to update configconstraint cr[%v]", unstructuredObj.GetName())) + return err + } + } + return nil +} diff --git a/pkg/util/conversion/types.go b/pkg/util/conversion/types.go new file mode 100644 index 000000000..9e332721c --- /dev/null +++ b/pkg/util/conversion/types.go @@ -0,0 +1,42 @@ +/* +Copyright (C) 2022-2024 ApeCloud Co., Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package conversion + +import ( + "context" + + "k8s.io/client-go/dynamic" + + "github.com/apecloud/kbcli/pkg/util/breakingchange" +) + +type VersionConversionMeta struct { + dynamic.Interface + Ctx context.Context + + FromVersion string + ToVersion string +} + +func NewVersionConversion(dynamic dynamic.Interface, fromVersion, toVersion string) *VersionConversionMeta { + return &VersionConversionMeta{ + Ctx: context.Background(), + Interface: dynamic, + FromVersion: breakingchange.GetMajorMinorVersion(fromVersion), + ToVersion: breakingchange.GetMajorMinorVersion(toVersion), + } +} diff --git a/pkg/util/conversion/utils.go b/pkg/util/conversion/utils.go new file mode 100644 index 000000000..3fbee5f92 --- /dev/null +++ b/pkg/util/conversion/utils.go @@ -0,0 +1,41 @@ +/* +Copyright (C) 2022-2024 ApeCloud Co., Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package conversion + +import ( + appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + apiruntime "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +func ResourcesWithGVR(versionMeta *VersionConversionMeta, gvr schema.GroupVersionResource, listOptions metav1.ListOptions) ([]appsv1alpha1.ConfigConstraint, error) { + var resourcesList []appsv1alpha1.ConfigConstraint + + objList, err := versionMeta.Resource(gvr).List(versionMeta.Ctx, listOptions) + if err != nil { + return nil, err + } + for _, v := range objList.Items { + obj := appsv1alpha1.ConfigConstraint{} + if err := apiruntime.DefaultUnstructuredConverter.FromUnstructured(v.Object, &obj); err != nil { + return nil, err + } + resourcesList = append(resourcesList, obj) + } + return resourcesList, nil +}