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