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 }