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  }