github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/cli/cmd/plugin/utils.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 plugin 21 22 import ( 23 "io" 24 "os" 25 "path/filepath" 26 "regexp" 27 "strings" 28 "unicode" 29 30 "github.com/pkg/errors" 31 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 32 "k8s.io/client-go/util/homedir" 33 "k8s.io/klog/v2" 34 "sigs.k8s.io/yaml" 35 ) 36 37 var ( 38 ErrIsAlreadyInstalled = errors.New("can't install, the newest version is already installed") 39 ErrIsNotInstalled = errors.New("plugin is not installed") 40 ErrIsAlreadyUpgraded = errors.New("can't upgrade, the newest version is already installed") 41 ) 42 43 func GetKbcliPluginPath() *Paths { 44 base := filepath.Join(homedir.HomeDir(), ".kbcli", "plugins") 45 return NewPaths(base) 46 } 47 48 func EnsureDirs(paths ...string) error { 49 for _, p := range paths { 50 if err := os.MkdirAll(p, os.ModePerm); err != nil { 51 return err 52 } 53 } 54 return nil 55 } 56 57 func NewPaths(base string) *Paths { 58 return &Paths{base: base, tmp: os.TempDir()} 59 } 60 61 // LoadPluginByName loads plugin from index repository 62 func LoadPluginByName(pluginsDirs []string, pluginName string) (Plugin, error) { 63 var plugin Plugin 64 var err error 65 for _, p := range pluginsDirs { 66 plugin, err = ReadPluginFromFile(filepath.Join(p, pluginName+ManifestExtension)) 67 if errors.Is(err, os.ErrNotExist) { 68 continue 69 } 70 break 71 } 72 return plugin, err 73 } 74 75 func ReadPluginFromFile(path string) (Plugin, error) { 76 var plugin Plugin 77 err := readFromFile(path, &plugin) 78 if err != nil { 79 return plugin, err 80 } 81 return plugin, errors.Wrap(ValidatePlugin(plugin.Name, plugin), "plugin manifest validation error") 82 } 83 84 func ReadReceiptFromFile(path string) (Receipt, error) { 85 var receipt Receipt 86 err := readFromFile(path, &receipt) 87 if err != nil { 88 return receipt, err 89 } 90 return receipt, nil 91 } 92 93 func readFromFile(path string, as interface{}) error { 94 f, err := os.Open(path) 95 if err != nil { 96 return err 97 } 98 err = decodeFile(f, &as) 99 return errors.Wrapf(err, "failed to parse yaml file %q", path) 100 } 101 102 func decodeFile(r io.ReadCloser, as interface{}) error { 103 defer r.Close() 104 b, err := io.ReadAll(r) 105 if err != nil { 106 return err 107 } 108 return yaml.Unmarshal(b, &as) 109 } 110 111 func indent(s string) string { 112 out := "\\\n" 113 s = strings.TrimRightFunc(s, unicode.IsSpace) 114 out += regexp.MustCompile("(?m)^").ReplaceAllString(s, " | ") 115 out += "\n/" 116 return out 117 } 118 119 func applyDefaults(platform *Platform) { 120 if platform.Files == nil { 121 platform.Files = []FileOperation{{From: "*", To: "."}} 122 klog.V(4).Infof("file operation not specified, assuming %v", platform.Files) 123 } 124 } 125 126 // GetInstalledPluginReceipts returns a list of receipts. 127 func GetInstalledPluginReceipts(receiptsDir string) ([]Receipt, error) { 128 files, err := filepath.Glob(filepath.Join(receiptsDir, "*"+ManifestExtension)) 129 if err != nil { 130 return nil, errors.Wrapf(err, "failed to glob receipts directory (%s) for manifests", receiptsDir) 131 } 132 out := make([]Receipt, 0, len(files)) 133 for _, f := range files { 134 r, err := ReadReceiptFromFile(f) 135 if err != nil { 136 return nil, errors.Wrapf(err, "failed to parse plugin install receipt %s", f) 137 } 138 out = append(out, r) 139 klog.V(4).Infof("parsed receipt for %s: version=%s", r.GetObjectMeta().GetName(), r.Spec.Version) 140 141 } 142 return out, nil 143 } 144 145 func isSupportAPIVersion(apiVersion string) bool { 146 for _, v := range SupportAPIVersion { 147 if apiVersion == v { 148 return true 149 } 150 } 151 return false 152 } 153 154 func ValidatePlugin(name string, p Plugin) error { 155 if !isSupportAPIVersion(p.APIVersion) { 156 return errors.Errorf("plugin manifest has apiVersion=%q, not supported in this version of krew (try updating plugin index or install a newer version of krew)", p.APIVersion) 157 } 158 if p.Kind != PluginKind { 159 return errors.Errorf("plugin manifest has kind=%q, but only %q is supported", p.Kind, PluginKind) 160 } 161 if p.Name != name { 162 return errors.Errorf("plugin manifest has name=%q, but expected %q", p.Name, name) 163 } 164 if p.Spec.ShortDescription == "" { 165 return errors.New("should have a short description") 166 } 167 if len(p.Spec.Platforms) == 0 { 168 return errors.New("should have a platform") 169 } 170 if p.Spec.Version == "" { 171 return errors.New("should have a version") 172 } 173 if _, err := parseVersion(p.Spec.Version); err != nil { 174 return errors.Wrap(err, "failed to parse version") 175 } 176 for _, pl := range p.Spec.Platforms { 177 if err := validatePlatform(pl); err != nil { 178 return errors.Wrapf(err, "platform (%+v) is badly constructed", pl) 179 } 180 } 181 return nil 182 } 183 184 func validatePlatform(p Platform) error { 185 if p.URI == "" { 186 return errors.New("`uri` is unset") 187 } 188 if p.Sha256 == "" { 189 return errors.New("`sha256` sum is unset") 190 } 191 if p.Bin == "" { 192 return errors.New("`bin` is unset") 193 } 194 if err := validateFiles(p.Files); err != nil { 195 return errors.Wrap(err, "`files` is invalid") 196 } 197 if err := validateSelector(p.Selector); err != nil { 198 return errors.Wrap(err, "invalid platform selector") 199 } 200 return nil 201 } 202 203 func validateFiles(fops []FileOperation) error { 204 if fops == nil { 205 return nil 206 } 207 if len(fops) == 0 { 208 return errors.New("`files` is empty, set it") 209 } 210 for _, op := range fops { 211 if op.From == "" { 212 return errors.New("`from` field is unset") 213 } else if op.To == "" { 214 return errors.New("`to` field is unset") 215 } 216 } 217 return nil 218 } 219 220 // validateSelector checks if the platform selector uses supported keys and is not empty or nil. 221 func validateSelector(sel *metav1.LabelSelector) error { 222 if sel == nil { 223 return errors.New("nil selector is not supported") 224 } 225 if sel.MatchLabels == nil && len(sel.MatchExpressions) == 0 { 226 return errors.New("empty selector is not supported") 227 } 228 229 // check for unsupported keys 230 keys := []string{} 231 for k := range sel.MatchLabels { 232 keys = append(keys, k) 233 } 234 for _, expr := range sel.MatchExpressions { 235 keys = append(keys, expr.Key) 236 } 237 for _, key := range keys { 238 if key != "os" && key != "arch" { 239 return errors.Errorf("key %q not supported", key) 240 } 241 } 242 243 if sel.MatchLabels != nil && len(sel.MatchLabels) == 0 { 244 return errors.New("`matchLabels` specified but empty") 245 } 246 if sel.MatchExpressions != nil && len(sel.MatchExpressions) == 0 { 247 return errors.New("`matchExpressions` specified but empty") 248 } 249 250 return nil 251 } 252 253 func findPluginManifestFiles(indexDir string) ([]string, error) { 254 var out []string 255 files, err := os.ReadDir(indexDir) 256 if err != nil { 257 return nil, errors.Wrap(err, "failed to open index dir") 258 } 259 for _, file := range files { 260 if file.Type().IsRegular() && filepath.Ext(file.Name()) == ManifestExtension { 261 out = append(out, file.Name()) 262 } 263 } 264 return out, nil 265 } 266 267 // LoadPluginListFromFS will parse and retrieve all plugin files. 268 func LoadPluginListFromFS(pluginDirs []string) ([]Plugin, error) { 269 list := make([]Plugin, 0) 270 for _, pluginDir := range pluginDirs { 271 pluginDir, err := filepath.EvalSymlinks(pluginDir) 272 if err != nil { 273 return nil, err 274 } 275 276 files, err := findPluginManifestFiles(pluginDir) 277 if err != nil { 278 return nil, errors.Wrap(err, "failed to scan plugins in index directory") 279 } 280 klog.V(4).Infof("found %d plugins in dir %s", len(files), pluginDir) 281 282 for _, file := range files { 283 pluginName := strings.TrimSuffix(file, filepath.Ext(file)) 284 p, err := LoadPluginByName([]string{pluginDir}, pluginName) 285 if err != nil { 286 // loading the index repository shouldn't fail because of one plugin 287 // if loading the plugin fails, log the error and continue 288 klog.Errorf("failed to read or parse plugin manifest %q: %v", pluginName, err) 289 continue 290 } 291 list = append(list, p) 292 } 293 } 294 return list, nil 295 }