github.com/stevenmatthewt/agent@v3.5.4+incompatible/agent/plugin/plugin.go (about)

     1  package plugin
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"net/url"
     7  	"regexp"
     8  	"sort"
     9  	"strings"
    10  
    11  	"github.com/buildkite/agent/env"
    12  )
    13  
    14  type Plugin struct {
    15  	// Where the plugin can be found (can either be a file system path, or
    16  	// a git repository)
    17  	Location string
    18  
    19  	// The version of the plugin that should be running
    20  	Version string
    21  
    22  	// The clone method
    23  	Scheme string
    24  
    25  	// Any authentication attached to the repostiory
    26  	Authentication string
    27  
    28  	// Configuration for the plugin
    29  	Configuration map[string]interface{}
    30  }
    31  
    32  var locationSchemeRegex = regexp.MustCompile(`^[a-z\+]+://`)
    33  
    34  func CreatePlugin(location string, config map[string]interface{}) (*Plugin, error) {
    35  	plugin := &Plugin{Configuration: config}
    36  
    37  	u, err := url.Parse(location)
    38  	if err != nil {
    39  		return nil, err
    40  	}
    41  
    42  	plugin.Scheme = u.Scheme
    43  	plugin.Location = u.Host + u.Path
    44  	plugin.Version = u.Fragment
    45  
    46  	if plugin.Version != "" && strings.Count(plugin.Version, "#") > 0 {
    47  		return nil, fmt.Errorf("Too many #'s in \"%s\"", location)
    48  	}
    49  
    50  	if u.User != nil {
    51  		plugin.Authentication = u.User.String()
    52  	}
    53  
    54  	return plugin, nil
    55  }
    56  
    57  // Given a JSON structure, convert it to an array of plugins
    58  func CreateFromJSON(j string) ([]*Plugin, error) {
    59  	// Use more versatile number decoding
    60  	decoder := json.NewDecoder(strings.NewReader(j))
    61  	decoder.UseNumber()
    62  
    63  	// Parse the JSON
    64  	var f interface{}
    65  	err := decoder.Decode(&f)
    66  	if err != nil {
    67  		return nil, err
    68  	}
    69  
    70  	// Try and convert the structure to an array
    71  	m, ok := f.([]interface{})
    72  	if !ok {
    73  		return nil, fmt.Errorf("JSON structure was not an array")
    74  	}
    75  
    76  	// Convert the JSON elements to plugins
    77  	plugins := []*Plugin{}
    78  	for _, v := range m {
    79  		switch vv := v.(type) {
    80  		case string:
    81  			// Add the plugin with no config to the array
    82  			plugin, err := CreatePlugin(string(vv), map[string]interface{}{})
    83  			if err != nil {
    84  				return nil, err
    85  			}
    86  			plugins = append(plugins, plugin)
    87  		case map[string]interface{}:
    88  			for location, config := range vv {
    89  				// Plugins without configs are easy!
    90  				if config == nil {
    91  					plugin, err := CreatePlugin(string(location), map[string]interface{}{})
    92  					if err != nil {
    93  						return nil, err
    94  					}
    95  
    96  					plugins = append(plugins, plugin)
    97  					continue
    98  				}
    99  
   100  				// Since there is a config, it's gotta be a hash
   101  				config, ok := config.(map[string]interface{})
   102  				if !ok {
   103  					return nil, fmt.Errorf("Configuration for \"%s\" is not a hash", location)
   104  				}
   105  
   106  				// Add the plugin with config to the array
   107  				plugin, err := CreatePlugin(string(location), config)
   108  				if err != nil {
   109  					return nil, err
   110  				}
   111  
   112  				plugins = append(plugins, plugin)
   113  			}
   114  		default:
   115  			return nil, fmt.Errorf("Unknown type in plugin definition (%s)", vv)
   116  		}
   117  	}
   118  
   119  	return plugins, nil
   120  }
   121  
   122  // Returns the name of the plugin
   123  func (p *Plugin) Name() string {
   124  	if p.Location != "" {
   125  		// Grab the last part of the location
   126  		parts := strings.Split(p.Location, "/")
   127  		name := parts[len(parts)-1]
   128  
   129  		// Clean up the name
   130  		name = strings.ToLower(name)
   131  		name = regexp.MustCompile(`\s+`).ReplaceAllString(name, " ")
   132  		name = regexp.MustCompile(`[^a-zA-Z0-9]`).ReplaceAllString(name, "-")
   133  		name = strings.Replace(name, "-buildkite-plugin-git", "", -1)
   134  		name = strings.Replace(name, "-buildkite-plugin", "", -1)
   135  
   136  		return name
   137  	} else {
   138  		return ""
   139  	}
   140  }
   141  
   142  // Returns and ID for the plugin that can be used as a folder name
   143  func (p *Plugin) Identifier() (string, error) {
   144  	nonIdCharacterRegex := regexp.MustCompile(`[^a-zA-Z0-9]`)
   145  	removeDoubleUnderscore := regexp.MustCompile(`-+`)
   146  
   147  	id := p.Label()
   148  	id = nonIdCharacterRegex.ReplaceAllString(id, "-")
   149  	id = removeDoubleUnderscore.ReplaceAllString(id, "-")
   150  	id = strings.Trim(id, "-")
   151  
   152  	return id, nil
   153  }
   154  
   155  // Returns the repository host where the code is stored
   156  func (p *Plugin) Repository() (string, error) {
   157  	s, err := p.constructRepositoryHost()
   158  	if err != nil {
   159  		return "", err
   160  	}
   161  
   162  	// Add the authentication if there is one
   163  	if p.Authentication != "" {
   164  		s = p.Authentication + "@" + s
   165  	}
   166  
   167  	// If it's not a file system plugin, add the scheme
   168  	if !strings.HasPrefix(s, "/") {
   169  		if p.Scheme != "" {
   170  			s = p.Scheme + "://" + s
   171  		} else {
   172  			s = "https://" + s
   173  		}
   174  	}
   175  
   176  	return s, nil
   177  }
   178  
   179  // Returns the subdirectory path that the plugin is in
   180  func (p *Plugin) RepositorySubdirectory() (string, error) {
   181  	repository, err := p.constructRepositoryHost()
   182  	if err != nil {
   183  		return "", err
   184  	}
   185  
   186  	dir := strings.TrimPrefix(p.Location, repository)
   187  
   188  	return strings.TrimPrefix(dir, "/"), nil
   189  }
   190  
   191  var (
   192  	toDashRegex            = regexp.MustCompile(`-|\s+`)
   193  	removeWhitespaceRegex  = regexp.MustCompile(`\s+`)
   194  	removeDoubleUnderscore = regexp.MustCompile(`_+`)
   195  )
   196  
   197  // formatEnvKey converts strings into an ENV key friendly format
   198  func formatEnvKey(key string) string {
   199  	key = strings.ToUpper(key)
   200  	key = removeWhitespaceRegex.ReplaceAllString(key, " ")
   201  	key = toDashRegex.ReplaceAllString(key, "_")
   202  	key = removeDoubleUnderscore.ReplaceAllString(key, "_")
   203  	return key
   204  }
   205  
   206  func walkConfigValues(prefix string, v interface{}, into *[]string) error {
   207  	switch vv := v.(type) {
   208  
   209  	// handles all of our primitive types, golang provides a good string representation
   210  	case string, bool, json.Number:
   211  		*into = append(*into, fmt.Sprintf("%s=%v", prefix, vv))
   212  		return nil
   213  
   214  	// handle lists of things, which get a KEY_N prefix depending on the index
   215  	case []interface{}:
   216  		for i := range vv {
   217  			if err := walkConfigValues(fmt.Sprintf("%s_%d", prefix, i), vv[i], into); err != nil {
   218  				return err
   219  			}
   220  		}
   221  		return nil
   222  
   223  	// handle maps of things, which get a KEY_SUBKEY prefix depending on the map keys
   224  	case map[string]interface{}:
   225  		for k, vvv := range vv {
   226  			if err := walkConfigValues(fmt.Sprintf("%s_%s", prefix, formatEnvKey(k)), vvv, into); err != nil {
   227  				return err
   228  			}
   229  		}
   230  		return nil
   231  	}
   232  
   233  	return fmt.Errorf("Unknown type %T %v", v, v)
   234  }
   235  
   236  // Converts the plugin configuration values to environment variables
   237  func (p *Plugin) ConfigurationToEnvironment() (*env.Environment, error) {
   238  	envSlice := []string{}
   239  	envPrefix := fmt.Sprintf("BUILDKITE_PLUGIN_%s", formatEnvKey(p.Name()))
   240  
   241  	for k, v := range p.Configuration {
   242  		configPrefix := fmt.Sprintf("%s_%s", envPrefix, formatEnvKey(k))
   243  		if err := walkConfigValues(configPrefix, v, &envSlice); err != nil {
   244  			return nil, err
   245  		}
   246  	}
   247  
   248  	// Sort them into a consistent order
   249  	sort.Strings(envSlice)
   250  
   251  	return env.FromSlice(envSlice), nil
   252  }
   253  
   254  // Pretty name for the plugin
   255  func (p *Plugin) Label() string {
   256  	if p.Version != "" {
   257  		return p.Location + "#" + p.Version
   258  	} else {
   259  		return p.Location
   260  	}
   261  }
   262  
   263  func (p *Plugin) constructRepositoryHost() (string, error) {
   264  	if p.Location == "" {
   265  		return "", fmt.Errorf("Missing plugin location")
   266  	}
   267  
   268  	parts := strings.Split(p.Location, "/")
   269  	if len(parts) < 2 {
   270  		return "", fmt.Errorf("Incomplete plugin path \"%s\"", p.Location)
   271  	}
   272  
   273  	var s string
   274  
   275  	if parts[0] == "github.com" || parts[0] == "bitbucket.org" || parts[0] == "gitlab.com" {
   276  		if len(parts) < 3 {
   277  			return "", fmt.Errorf("Incomplete %s path \"%s\"", parts[0], p.Location)
   278  		}
   279  
   280  		s = strings.Join(parts[:3], "/")
   281  	} else {
   282  		repo := []string{}
   283  
   284  		for _, p := range parts {
   285  			repo = append(repo, p)
   286  
   287  			if strings.HasSuffix(p, ".git") {
   288  				break
   289  			}
   290  		}
   291  
   292  		s = strings.Join(repo, "/")
   293  	}
   294  
   295  	return s, nil
   296  }