github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/cli/util/breakingchange/upgrader.go (about)

     1  /*
     2  Copyright (C) 2022-2023 ApeCloud Co., Ltd
     3  
     4  This file is part of KubeBlocks project
     5  
     6  This program is free software: you can redistribute it and/or modify
     7  it under the terms of the GNU Affero General Public License as published by
     8  the Free Software Foundation, either version 3 of the License, or
     9  (at your option) any later version.
    10  
    11  This program is distributed in the hope that it will be useful
    12  but WITHOUT ANY WARRANTY; without even the implied warranty of
    13  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    14  GNU Affero General Public License for more details.
    15  
    16  You should have received a copy of the GNU Affero General Public License
    17  along with this program.  If not, see <http://www.gnu.org/licenses/>.
    18  */
    19  
    20  package breakingchange
    21  
    22  import (
    23  	"context"
    24  	"fmt"
    25  	"os"
    26  	"os/user"
    27  	"strconv"
    28  	"strings"
    29  
    30  	"golang.org/x/exp/slices"
    31  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    32  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    33  	"k8s.io/apimachinery/pkg/runtime/schema"
    34  	"k8s.io/client-go/dynamic"
    35  	"sigs.k8s.io/yaml"
    36  
    37  	"github.com/1aal/kubeblocks/pkg/constant"
    38  )
    39  
    40  var upgradeHandlerMapper = map[string]upgradeHandlerRecorder{}
    41  
    42  type upgradeHandlerRecorder struct {
    43  	formVersions []string
    44  	handler      upgradeHandler
    45  }
    46  
    47  type upgradeHandler interface {
    48  	// snapshot the resources of old version and return a map which key is namespace and value is the resources in the namespace.
    49  	snapshot(dynamic dynamic.Interface) (map[string][]unstructured.Unstructured, error)
    50  
    51  	// transform the resources of old version to new resources of the new version.
    52  	transform(dynamic dynamic.Interface, resourcesMap map[string][]unstructured.Unstructured) error
    53  }
    54  
    55  // registerUpgradeHandler registers the breakingChange handlers to upgradeHandlerMapper.
    56  // the version format should contain "MAJOR.MINOR", such as "0.6".
    57  func registerUpgradeHandler(fromVersions []string, toVersion string, upgradeHandler upgradeHandler) {
    58  	formatErr := func(version string) error {
    59  		return fmt.Errorf("the version %s is incorrect format, at least contains MAJOR and MINOR, such as MAJOR.MINOR", version)
    60  	}
    61  
    62  	var majorMinorFromVersions []string
    63  	for _, v := range fromVersions {
    64  		majorMinorFromVersion := getMajorMinorVersion(v)
    65  		if majorMinorFromVersion == "" {
    66  			panic(formatErr(v))
    67  		}
    68  		majorMinorFromVersions = append(majorMinorFromVersions, majorMinorFromVersion)
    69  	}
    70  
    71  	majorMinorToVersion := getMajorMinorVersion(toVersion)
    72  	if majorMinorToVersion == "" {
    73  		panic(formatErr(toVersion))
    74  	}
    75  	upgradeHandlerMapper[majorMinorToVersion] = upgradeHandlerRecorder{
    76  		formVersions: majorMinorFromVersions,
    77  		handler:      upgradeHandler,
    78  	}
    79  }
    80  
    81  // getUpgradeHandler gets the upgrade handler according to fromVersion and toVersion from upgradeHandlerMapper.
    82  func getUpgradeHandler(fromVersion, toVersion string) upgradeHandler {
    83  	majorMinorFromVersion := getMajorMinorVersion(fromVersion)
    84  	majorMinorToVersion := getMajorMinorVersion(toVersion)
    85  	handlerRecorder, ok := upgradeHandlerMapper[majorMinorToVersion]
    86  	if !ok {
    87  		return nil
    88  	}
    89  	// if no upgrade handler found, ignore
    90  	if !slices.Contains(handlerRecorder.formVersions, majorMinorFromVersion) {
    91  		return nil
    92  	}
    93  	return handlerRecorder.handler
    94  }
    95  
    96  // ValidateUpgradeVersion verifies the legality of the upgraded version.
    97  func ValidateUpgradeVersion(fromVersion, toVersion string) error {
    98  	handler := getUpgradeHandler(fromVersion, toVersion)
    99  	if handler != nil {
   100  		// if exists upgrade handler, validation pass.
   101  		return nil
   102  	}
   103  	fromVersionSlice := strings.Split(fromVersion, ".")
   104  	toVersionSlice := strings.Split(toVersion, ".")
   105  	if len(fromVersionSlice) < 2 || len(toVersionSlice) < 2 {
   106  		panic("unreachable, incorrect version format")
   107  	}
   108  	// can not upgrade across major versions by default.
   109  	if fromVersionSlice[0] != toVersionSlice[0] {
   110  		return fmt.Errorf("cannot upgrade across major versions")
   111  	}
   112  	fromMinorVersion, err := strconv.Atoi(fromVersionSlice[1])
   113  	if err != nil {
   114  		return err
   115  	}
   116  	toMinorVersion, err := strconv.Atoi(toVersionSlice[1])
   117  	if err != nil {
   118  		return err
   119  	}
   120  	if (toMinorVersion - fromMinorVersion) > 1 {
   121  		return fmt.Errorf("cannot upgrade across 1 minor version, you can upgrade to %s.%d.0 first", fromVersionSlice[0], toMinorVersion-1)
   122  	}
   123  	return nil
   124  }
   125  
   126  func getMajorMinorVersion(version string) string {
   127  	vs := strings.Split(version, ".")
   128  	if len(vs) < 2 {
   129  		return ""
   130  	}
   131  	return vs[0] + vs[1]
   132  }
   133  
   134  func fillResourcesMap(dynamic dynamic.Interface, resourcesMap map[string][]unstructured.Unstructured, gvr schema.GroupVersionResource, listOptions metav1.ListOptions) error {
   135  	// get custom resources
   136  	objList, err := dynamic.Resource(gvr).List(context.TODO(), listOptions)
   137  	if err != nil {
   138  		return err
   139  	}
   140  	for _, v := range objList.Items {
   141  		namespace := v.GetNamespace()
   142  		objArr := resourcesMap[namespace]
   143  		objArr = append(objArr, v)
   144  		resourcesMap[namespace] = objArr
   145  	}
   146  	return nil
   147  }
   148  
   149  // Breaking Change Upgrader.
   150  // will handle the breaking change before upgrading to target version.
   151  
   152  type Upgrader struct {
   153  	Dynamic      dynamic.Interface
   154  	FromVersion  string
   155  	ToVersion    string
   156  	ResourcesMap map[string][]unstructured.Unstructured
   157  }
   158  
   159  func (u *Upgrader) getWorkdir() (string, error) {
   160  	currentUser, err := user.Current()
   161  	if err != nil {
   162  		fmt.Printf("can not get the current user: %v\n", err)
   163  		return "", err
   164  	}
   165  	return fmt.Sprintf("%s/%s-%s-%s", currentUser.HomeDir, constant.AppName, u.FromVersion, u.ToVersion), nil
   166  }
   167  
   168  func (u *Upgrader) fileExists(filePath string) bool {
   169  	_, err := os.Stat(filePath)
   170  	return !os.IsNotExist(err)
   171  }
   172  func (u *Upgrader) getUnstructuredFromFile(filePath string) (*unstructured.Unstructured, error) {
   173  	content, err := os.ReadFile(filePath)
   174  	if err != nil {
   175  		return nil, err
   176  	}
   177  	obj := map[string]interface{}{}
   178  	if err = yaml.Unmarshal(content, &obj); err != nil {
   179  		return nil, fmt.Errorf("unmarshal content of %s failed: %s", filePath, err.Error())
   180  	}
   181  	return &unstructured.Unstructured{Object: obj}, nil
   182  }
   183  
   184  func (u *Upgrader) SaveOldResources() error {
   185  	handler := getUpgradeHandler(u.FromVersion, u.ToVersion)
   186  	// if no upgrade handler found, ignore
   187  	if handler == nil {
   188  		return nil
   189  	}
   190  	objsMap, err := handler.snapshot(u.Dynamic)
   191  	if err != nil {
   192  		return err
   193  	}
   194  
   195  	workDir, err := u.getWorkdir()
   196  	if err != nil {
   197  		return err
   198  	}
   199  	fmt.Printf("\nTransform breaking changes in %s\n", workDir)
   200  	u.ResourcesMap = objsMap
   201  	// save to tmp work dir
   202  	for namespace, objs := range objsMap {
   203  		workDir = fmt.Sprintf("%s/%s", workDir, namespace)
   204  		if err = os.MkdirAll(workDir, os.ModePerm); err != nil {
   205  			return err
   206  		}
   207  		for i, v := range objs {
   208  			filePath := fmt.Sprintf("%s/%s.yaml", workDir, v.GetName())
   209  			if u.fileExists(filePath) {
   210  				obj, err := u.getUnstructuredFromFile(filePath)
   211  				if err != nil {
   212  					return err
   213  				}
   214  				objs[i] = *obj
   215  				continue
   216  			}
   217  			// clear managedFields
   218  			v.SetManagedFields(nil)
   219  			yamlBytes, err := yaml.Marshal(v.Object)
   220  			if err != nil {
   221  				return err
   222  			}
   223  			err = os.WriteFile(filePath, yamlBytes, 0644)
   224  			if err != nil {
   225  				return err
   226  			}
   227  		}
   228  		objsMap[namespace] = objs
   229  	}
   230  	return nil
   231  }
   232  
   233  func (u *Upgrader) TransformResourcesAndClear() error {
   234  	handler := getUpgradeHandler(u.FromVersion, u.ToVersion)
   235  	// if no upgrade handler found, ignore
   236  	if handler == nil {
   237  		return nil
   238  	}
   239  	if err := handler.transform(u.Dynamic, u.ResourcesMap); err != nil {
   240  		return err
   241  	}
   242  	workDir, err := u.getWorkdir()
   243  	if err != nil {
   244  		return err
   245  	}
   246  	fmt.Printf("\nTransform breaking changes successfully, remove %s\n", workDir)
   247  	// clear the tmp work dir
   248  	return os.RemoveAll(workDir)
   249  }