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  }