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 }