github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/cli/cmd/plugin/install.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 "fmt" 24 "os" 25 "path/filepath" 26 27 "github.com/pkg/errors" 28 "github.com/spf13/cobra" 29 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 30 "k8s.io/cli-runtime/pkg/genericiooptions" 31 "k8s.io/klog/v2" 32 cmdutil "k8s.io/kubectl/pkg/cmd/util" 33 "k8s.io/kubectl/pkg/util/templates" 34 35 "github.com/1aal/kubeblocks/pkg/cli/cmd/plugin/download" 36 ) 37 38 var ( 39 pluginInstallExample = templates.Examples(` 40 # install a kbcli or kubectl plugin by name 41 kbcli plugin install [PLUGIN] 42 43 # install a kbcli or kubectl plugin by name and index 44 kbcli plugin install [INDEX/PLUGIN] 45 `) 46 ) 47 48 type PluginInstallOption struct { 49 plugins []pluginEntry 50 51 genericiooptions.IOStreams 52 } 53 54 type pluginEntry struct { 55 index string 56 plugin Plugin 57 } 58 59 func NewPluginInstallCmd(streams genericiooptions.IOStreams) *cobra.Command { 60 o := &PluginInstallOption{ 61 IOStreams: streams, 62 } 63 cmd := &cobra.Command{ 64 Use: "install", 65 Short: "Install kbcli or kubectl plugins", 66 Example: pluginInstallExample, 67 Run: func(cmd *cobra.Command, args []string) { 68 cmdutil.CheckErr(o.Complete(args)) 69 cmdutil.CheckErr(o.Install()) 70 }, 71 } 72 return cmd 73 } 74 75 func (o *PluginInstallOption) Complete(names []string) error { 76 for _, name := range names { 77 indexName, pluginName := CanonicalPluginName(name) 78 79 // check whether the plugin exists 80 if _, err := os.Stat(paths.PluginInstallReceiptPath(pluginName)); err == nil { 81 fmt.Fprintf(o.Out, "plugin %q is already installed\n", name) 82 continue 83 } 84 85 plugin, err := LoadPluginByName(paths.IndexPluginsPath(indexName), pluginName) 86 if err != nil { 87 if os.IsNotExist(err) { 88 return errors.Errorf("plugin %q does not exist in the %s plugin index", name, indexName) 89 } 90 return errors.Wrapf(err, "failed to load plugin %q from the %s plugin index", name, indexName) 91 } 92 o.plugins = append(o.plugins, pluginEntry{ 93 index: indexName, 94 plugin: plugin, 95 }) 96 } 97 return nil 98 } 99 100 func (o *PluginInstallOption) Install() error { 101 var failed []string 102 var returnErr error 103 for _, entry := range o.plugins { 104 plugin := entry.plugin 105 fmt.Fprintf(o.Out, "Installing plugin: %s\n", plugin.Name) 106 err := Install(paths, plugin, entry.index, InstallOpts{}) 107 if err == ErrIsAlreadyInstalled { 108 continue 109 } 110 if err != nil { 111 klog.Warningf("failed to install plugin %q: %v", plugin.Name, err) 112 if returnErr == nil { 113 returnErr = err 114 } 115 failed = append(failed, plugin.Name) 116 continue 117 } 118 fmt.Fprintf(o.Out, "Installed plugin: %s\n", plugin.Name) 119 output := fmt.Sprintf("Use this plugin:\n\tkubectl %s\n", plugin.Name) 120 if plugin.Spec.Homepage != "" { 121 output += fmt.Sprintf("Documentation:\n\t%s\n", plugin.Spec.Homepage) 122 } 123 if plugin.Spec.Caveats != "" { 124 output += fmt.Sprintf("Caveats:\n%s\n", indent(plugin.Spec.Caveats)) 125 } 126 fmt.Fprintln(o.Out, indent(output)) 127 } 128 if len(failed) > 0 { 129 return errors.Wrapf(returnErr, "failed to install some plugins: %+v", failed) 130 } 131 return nil 132 } 133 134 // Install downloads and installs a plugin. The operation tries 135 // to keep the plugin dir in a healthy state if it fails during the process. 136 func Install(p *Paths, plugin Plugin, indexName string, opts InstallOpts) error { 137 klog.V(2).Infof("Looking for installed versions") 138 _, err := ReadReceiptFromFile(p.PluginInstallReceiptPath(plugin.Name)) 139 if err == nil { 140 return ErrIsAlreadyInstalled 141 } else if !os.IsNotExist(err) { 142 return errors.Wrap(err, "failed to look up plugin receipt") 143 } 144 145 // Find available installation candidate 146 candidate, ok, err := GetMatchingPlatform(plugin.Spec.Platforms) 147 if err != nil { 148 return errors.Wrap(err, "failed trying to find a matching platform in plugin spec") 149 } 150 if !ok { 151 return errors.Errorf("plugin %q does not offer installation for this platform", plugin.Name) 152 } 153 154 // The actual install should be the last action so that a failure during receipt 155 // saving does not result in an installed plugin without receipt. 156 klog.V(3).Infof("Install plugin %s at version=%s", plugin.Name, plugin.Spec.Version) 157 if err := install(installOperation{ 158 pluginName: plugin.Name, 159 platform: candidate, 160 161 binDir: p.BinPath(), 162 installDir: p.PluginVersionInstallPath(plugin.Name, plugin.Spec.Version), 163 }, opts); err != nil { 164 return errors.Wrap(err, "install failed") 165 } 166 167 klog.V(3).Infof("Storing install receipt for plugin %s", plugin.Name) 168 err = StoreReceipt(NewReceipt(plugin, indexName, metav1.Now()), p.PluginInstallReceiptPath(plugin.Name)) 169 return errors.Wrap(err, "installation receipt could not be stored, uninstall may fail") 170 } 171 172 func install(op installOperation, opts InstallOpts) error { 173 // Download and extract 174 klog.V(3).Infof("Creating download staging directory") 175 downloadStagingDir, err := os.MkdirTemp("", "kbcli-downloads") 176 if err != nil { 177 return errors.Wrapf(err, "could not create staging dir %q", downloadStagingDir) 178 } 179 klog.V(3).Infof("Successfully created download staging directory %q", downloadStagingDir) 180 defer func() { 181 klog.V(3).Infof("Deleting the download staging directory %s", downloadStagingDir) 182 if err := os.RemoveAll(downloadStagingDir); err != nil { 183 klog.Warningf("failed to clean up download staging directory: %s", err) 184 } 185 }() 186 if err := download.DownloadAndExtract(downloadStagingDir, op.platform.URI, op.platform.Sha256, opts.ArchiveFileOverride); err != nil { 187 return errors.Wrap(err, "failed to unpack into staging dir") 188 } 189 190 applyDefaults(&op.platform) 191 if err := moveToInstallDir(downloadStagingDir, op.installDir, op.platform.Files); err != nil { 192 return errors.Wrap(err, "failed while moving files to the installation directory") 193 } 194 195 subPathAbs, err := filepath.Abs(op.installDir) 196 if err != nil { 197 return errors.Wrapf(err, "failed to get the absolute fullPath of %q", op.installDir) 198 } 199 fullPath := filepath.Join(op.installDir, filepath.FromSlash(op.platform.Bin)) 200 pathAbs, err := filepath.Abs(fullPath) 201 if err != nil { 202 return errors.Wrapf(err, "failed to get the absolute fullPath of %q", fullPath) 203 } 204 if _, ok := IsSubPath(subPathAbs, pathAbs); !ok { 205 return errors.Wrapf(err, "the fullPath %q does not extend the sub-fullPath %q", fullPath, op.installDir) 206 } 207 err = createOrUpdateLink(op.binDir, fullPath, op.pluginName) 208 return errors.Wrap(err, "failed to link installed plugin") 209 }