github.com/x-helm/helm@v3.0.0-beta.3+incompatible/pkg/plugin/plugin.go (about) 1 /* 2 Copyright The Helm Authors. 3 Licensed under the Apache License, Version 2.0 (the "License"); 4 you may not use this file except in compliance with the License. 5 You may obtain a copy of the License at 6 7 http://www.apache.org/licenses/LICENSE-2.0 8 9 Unless required by applicable law or agreed to in writing, software 10 distributed under the License is distributed on an "AS IS" BASIS, 11 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 See the License for the specific language governing permissions and 13 limitations under the License. 14 */ 15 16 package plugin // import "helm.sh/helm/pkg/plugin" 17 18 import ( 19 "fmt" 20 "io/ioutil" 21 "os" 22 "path/filepath" 23 "runtime" 24 "strings" 25 26 "sigs.k8s.io/yaml" 27 28 "helm.sh/helm/pkg/cli" 29 "helm.sh/helm/pkg/helmpath" 30 ) 31 32 const pluginFileName = "plugin.yaml" 33 34 // Downloaders represents the plugins capability if it can retrieve 35 // charts from special sources 36 type Downloaders struct { 37 // Protocols are the list of schemes from the charts URL. 38 Protocols []string `json:"protocols"` 39 // Command is the executable path with which the plugin performs 40 // the actual download for the corresponding Protocols 41 Command string `json:"command"` 42 } 43 44 // PlatformCommand represents a command for a particular operating system and architecture 45 type PlatformCommand struct { 46 OperatingSystem string `json:"os"` 47 Architecture string `json:"arch"` 48 Command string `json:"command"` 49 } 50 51 // Metadata describes a plugin. 52 // 53 // This is the plugin equivalent of a chart.Metadata. 54 type Metadata struct { 55 // Name is the name of the plugin 56 Name string `json:"name"` 57 58 // Version is a SemVer 2 version of the plugin. 59 Version string `json:"version"` 60 61 // Usage is the single-line usage text shown in help 62 Usage string `json:"usage"` 63 64 // Description is a long description shown in places like `helm help` 65 Description string `json:"description"` 66 67 // Command is the command, as a single string. 68 // 69 // The command will be passed through environment expansion, so env vars can 70 // be present in this command. Unless IgnoreFlags is set, this will 71 // also merge the flags passed from Helm. 72 // 73 // Note that command is not executed in a shell. To do so, we suggest 74 // pointing the command to a shell script. 75 // 76 // The following rules will apply to processing commands: 77 // - If platformCommand is present, it will be searched first 78 // - If both OS and Arch match the current platform, search will stop and the command will be executed 79 // - If OS matches and there is no more specific match, the command will be executed 80 // - If no OS/Arch match is found, the default command will be executed 81 // - If no command is present and no matches are found in platformCommand, Helm will exit with an error 82 PlatformCommand []PlatformCommand `json:"platformCommand"` 83 Command string `json:"command"` 84 85 // IgnoreFlags ignores any flags passed in from Helm 86 // 87 // For example, if the plugin is invoked as `helm --debug myplugin`, if this 88 // is false, `--debug` will be appended to `--command`. If this is true, 89 // the `--debug` flag will be discarded. 90 IgnoreFlags bool `json:"ignoreFlags"` 91 92 // Hooks are commands that will run on events. 93 Hooks Hooks 94 95 // Downloaders field is used if the plugin supply downloader mechanism 96 // for special protocols. 97 Downloaders []Downloaders `json:"downloaders"` 98 } 99 100 // Plugin represents a plugin. 101 type Plugin struct { 102 // Metadata is a parsed representation of a plugin.yaml 103 Metadata *Metadata 104 // Dir is the string path to the directory that holds the plugin. 105 Dir string 106 } 107 108 // The following rules will apply to processing the Plugin.PlatformCommand.Command: 109 // - If both OS and Arch match the current platform, search will stop and the command will be prepared for execution 110 // - If OS matches and there is no more specific match, the command will be prepared for execution 111 // - If no OS/Arch match is found, return nil 112 func getPlatformCommand(cmds []PlatformCommand) []string { 113 var command []string 114 eq := strings.EqualFold 115 for _, c := range cmds { 116 if eq(c.OperatingSystem, runtime.GOOS) { 117 command = strings.Split(os.ExpandEnv(c.Command), " ") 118 } 119 if eq(c.OperatingSystem, runtime.GOOS) && eq(c.Architecture, runtime.GOARCH) { 120 return strings.Split(os.ExpandEnv(c.Command), " ") 121 } 122 } 123 return command 124 } 125 126 // PrepareCommand takes a Plugin.PlatformCommand.Command, a Plugin.Command and will applying the following processing: 127 // - If platformCommand is present, it will be searched first 128 // - If both OS and Arch match the current platform, search will stop and the command will be prepared for execution 129 // - If OS matches and there is no more specific match, the command will be prepared for execution 130 // - If no OS/Arch match is found, the default command will be prepared for execution 131 // - If no command is present and no matches are found in platformCommand, will exit with an error 132 // 133 // It merges extraArgs into any arguments supplied in the plugin. It 134 // returns the name of the command and an args array. 135 // 136 // The result is suitable to pass to exec.Command. 137 func (p *Plugin) PrepareCommand(extraArgs []string) (string, []string, error) { 138 var parts []string 139 platCmdLen := len(p.Metadata.PlatformCommand) 140 if platCmdLen > 0 { 141 parts = getPlatformCommand(p.Metadata.PlatformCommand) 142 } 143 if platCmdLen == 0 || parts == nil { 144 parts = strings.Split(os.ExpandEnv(p.Metadata.Command), " ") 145 } 146 if len(parts) == 0 || parts[0] == "" { 147 return "", nil, fmt.Errorf("No plugin command is applicable") 148 } 149 150 main := parts[0] 151 baseArgs := []string{} 152 if len(parts) > 1 { 153 baseArgs = parts[1:] 154 } 155 if !p.Metadata.IgnoreFlags { 156 baseArgs = append(baseArgs, extraArgs...) 157 } 158 return main, baseArgs, nil 159 } 160 161 // LoadDir loads a plugin from the given directory. 162 func LoadDir(dirname string) (*Plugin, error) { 163 data, err := ioutil.ReadFile(filepath.Join(dirname, pluginFileName)) 164 if err != nil { 165 return nil, err 166 } 167 168 plug := &Plugin{Dir: dirname} 169 if err := yaml.Unmarshal(data, &plug.Metadata); err != nil { 170 return nil, err 171 } 172 return plug, nil 173 } 174 175 // LoadAll loads all plugins found beneath the base directory. 176 // 177 // This scans only one directory level. 178 func LoadAll(basedir string) ([]*Plugin, error) { 179 plugins := []*Plugin{} 180 // We want basedir/*/plugin.yaml 181 scanpath := filepath.Join(basedir, "*", pluginFileName) 182 matches, err := filepath.Glob(scanpath) 183 if err != nil { 184 return plugins, err 185 } 186 187 if matches == nil { 188 return plugins, nil 189 } 190 191 for _, yaml := range matches { 192 dir := filepath.Dir(yaml) 193 p, err := LoadDir(dir) 194 if err != nil { 195 return plugins, err 196 } 197 plugins = append(plugins, p) 198 } 199 return plugins, nil 200 } 201 202 // FindPlugins returns a list of YAML files that describe plugins. 203 func FindPlugins(plugdirs string) ([]*Plugin, error) { 204 found := []*Plugin{} 205 // Let's get all UNIXy and allow path separators 206 for _, p := range filepath.SplitList(plugdirs) { 207 matches, err := LoadAll(p) 208 if err != nil { 209 return matches, err 210 } 211 found = append(found, matches...) 212 } 213 return found, nil 214 } 215 216 // SetupPluginEnv prepares os.Env for plugins. It operates on os.Env because 217 // the plugin subsystem itself needs access to the environment variables 218 // created here. 219 func SetupPluginEnv(settings *cli.EnvSettings, name, base string) { 220 for key, val := range map[string]string{ 221 "HELM_PLUGIN_NAME": name, 222 "HELM_PLUGIN_DIR": base, 223 "HELM_BIN": os.Args[0], 224 "HELM_PLUGIN": settings.PluginsDirectory, 225 226 // Set vars that convey common information. 227 "HELM_REGISTRY_CONFIG": settings.RegistryConfig, 228 "HELM_REPOSITORY_CONFIG": settings.RepositoryConfig, 229 "HELM_REPOSITORY_CACHE": settings.RepositoryCache, 230 "HELM_PATH_STARTER": helmpath.DataPath("starters"), 231 "HELM_HOME": helmpath.DataPath(), // for backwards compatibility with Helm 2 plugins 232 "HELM_DEBUG": fmt.Sprint(settings.Debug), 233 } { 234 os.Setenv(key, val) 235 } 236 }