github.com/wynshop-open-source/gomplate@v3.5.0+incompatible/data/datasource.go (about) 1 package data 2 3 import ( 4 "fmt" 5 "io" 6 "io/ioutil" 7 "mime" 8 "net/http" 9 "net/url" 10 "os" 11 "path" 12 "path/filepath" 13 "strings" 14 15 "github.com/spf13/afero" 16 17 "github.com/pkg/errors" 18 19 "github.com/hairyhenderson/gomplate/libkv" 20 "github.com/hairyhenderson/gomplate/vault" 21 ) 22 23 // stdin - for overriding in tests 24 var stdin io.Reader 25 26 func regExtension(ext, typ string) { 27 err := mime.AddExtensionType(ext, typ) 28 if err != nil { 29 panic(err) 30 } 31 } 32 33 func init() { 34 // Add some types we want to be able to handle which can be missing by default 35 regExtension(".json", jsonMimetype) 36 regExtension(".yml", yamlMimetype) 37 regExtension(".yaml", yamlMimetype) 38 regExtension(".csv", csvMimetype) 39 regExtension(".toml", tomlMimetype) 40 regExtension(".env", envMimetype) 41 } 42 43 // registerReaders registers the source-reader functions 44 func (d *Data) registerReaders() { 45 d.sourceReaders = make(map[string]func(*Source, ...string) ([]byte, error)) 46 47 d.sourceReaders["aws+smp"] = readAWSSMP 48 d.sourceReaders["aws+sm"] = readAWSSecretsManager 49 d.sourceReaders["boltdb"] = readBoltDB 50 d.sourceReaders["consul"] = readConsul 51 d.sourceReaders["consul+http"] = readConsul 52 d.sourceReaders["consul+https"] = readConsul 53 d.sourceReaders["env"] = readEnv 54 d.sourceReaders["file"] = readFile 55 d.sourceReaders["http"] = readHTTP 56 d.sourceReaders["https"] = readHTTP 57 d.sourceReaders["merge"] = d.readMerge 58 d.sourceReaders["stdin"] = readStdin 59 d.sourceReaders["vault"] = readVault 60 d.sourceReaders["vault+http"] = readVault 61 d.sourceReaders["vault+https"] = readVault 62 } 63 64 // lookupReader - return the reader function for the given scheme 65 func (d *Data) lookupReader(scheme string) (func(*Source, ...string) ([]byte, error), error) { 66 if d.sourceReaders == nil { 67 d.registerReaders() 68 } 69 r, ok := d.sourceReaders[scheme] 70 if !ok { 71 return nil, errors.Errorf("scheme %s not registered", scheme) 72 } 73 return r, nil 74 } 75 76 // Data - 77 type Data struct { 78 Sources map[string]*Source 79 80 sourceReaders map[string]func(*Source, ...string) ([]byte, error) 81 cache map[string][]byte 82 83 // headers from the --datasource-header/-H option that don't reference datasources from the commandline 84 extraHeaders map[string]http.Header 85 } 86 87 // Cleanup - clean up datasources before shutting the process down - things 88 // like Logging out happen here 89 func (d *Data) Cleanup() { 90 for _, s := range d.Sources { 91 s.cleanup() 92 } 93 } 94 95 // NewData - constructor for Data 96 func NewData(datasourceArgs, headerArgs []string) (*Data, error) { 97 headers, err := parseHeaderArgs(headerArgs) 98 if err != nil { 99 return nil, err 100 } 101 102 data := &Data{ 103 Sources: make(map[string]*Source), 104 extraHeaders: headers, 105 } 106 107 for _, v := range datasourceArgs { 108 s, err := parseSource(v) 109 if err != nil { 110 return nil, errors.Wrapf(err, "error parsing datasource") 111 } 112 s.header = headers[s.Alias] 113 // pop the header out of the map, so we end up with only the unreferenced ones 114 delete(headers, s.Alias) 115 116 data.Sources[s.Alias] = s 117 } 118 return data, nil 119 } 120 121 // Source - a data source 122 type Source struct { 123 Alias string 124 URL *url.URL 125 mediaType string 126 fs afero.Fs // used for file: URLs, nil otherwise 127 hc *http.Client // used for http[s]: URLs, nil otherwise 128 vc *vault.Vault // used for vault: URLs, nil otherwise 129 kv *libkv.LibKV // used for consul:, etcd:, zookeeper: & boltdb: URLs, nil otherwise 130 asmpg awssmpGetter // used for aws+smp:, nil otherwise 131 awsSecretsManager awsSecretsManagerGetter // used for aws+sm, nil otherwise 132 header http.Header // used for http[s]: URLs, nil otherwise 133 } 134 135 func (s *Source) inherit(parent *Source) { 136 s.fs = parent.fs 137 s.hc = parent.hc 138 s.vc = parent.vc 139 s.kv = parent.kv 140 s.asmpg = parent.asmpg 141 } 142 143 func (s *Source) cleanup() { 144 if s.vc != nil { 145 s.vc.Logout() 146 } 147 if s.kv != nil { 148 s.kv.Logout() 149 } 150 } 151 152 // mimeType returns the MIME type to use as a hint for parsing the datasource. 153 // It's expected that the datasource will have already been read before 154 // this function is called, and so the Source's Type property may be already set. 155 // 156 // The MIME type is determined by these rules: 157 // 1. the 'type' URL query parameter is used if present 158 // 2. otherwise, the Type property on the Source is used, if present 159 // 3. otherwise, a MIME type is calculated from the file extension, if the extension is registered 160 // 4. otherwise, the default type of 'text/plain' is used 161 func (s *Source) mimeType() (mimeType string, err error) { 162 mediatype := s.URL.Query().Get("type") 163 if mediatype == "" { 164 mediatype = s.mediaType 165 } 166 if mediatype == "" { 167 ext := filepath.Ext(s.URL.Path) 168 mediatype = mime.TypeByExtension(ext) 169 } 170 171 if mediatype != "" { 172 t, _, err := mime.ParseMediaType(mediatype) 173 if err != nil { 174 return "", err 175 } 176 mediatype = t 177 return mediatype, nil 178 } 179 180 return textMimetype, nil 181 } 182 183 // String is the method to format the flag's value, part of the flag.Value interface. 184 // The String method's output will be used in diagnostics. 185 func (s *Source) String() string { 186 return fmt.Sprintf("%s=%s (%s)", s.Alias, s.URL.String(), s.mediaType) 187 } 188 189 // parseSource creates a *Source by parsing the value provided to the 190 // --datasource/-d commandline flag 191 func parseSource(value string) (source *Source, err error) { 192 source = &Source{} 193 parts := strings.SplitN(value, "=", 2) 194 if len(parts) == 1 { 195 f := parts[0] 196 source.Alias = strings.SplitN(value, ".", 2)[0] 197 if path.Base(f) != f { 198 err = errors.Errorf("Invalid datasource (%s). Must provide an alias with files not in working directory", value) 199 return nil, err 200 } 201 source.URL, err = absURL(f) 202 if err != nil { 203 return nil, err 204 } 205 } else if len(parts) == 2 { 206 source.Alias = parts[0] 207 source.URL, err = parseSourceURL(parts[1]) 208 if err != nil { 209 return nil, err 210 } 211 } 212 213 return source, nil 214 } 215 216 func parseSourceURL(value string) (*url.URL, error) { 217 if value == "-" { 218 value = "stdin://" 219 } 220 value = filepath.ToSlash(value) 221 // handle absolute Windows paths 222 volName := "" 223 if volName = filepath.VolumeName(value); volName != "" { 224 // handle UNCs 225 if len(volName) > 2 { 226 value = "file:" + value 227 } else { 228 value = "file:///" + value 229 } 230 } 231 srcURL, err := url.Parse(value) 232 if err != nil { 233 return nil, err 234 } 235 236 if volName != "" { 237 if strings.HasPrefix(srcURL.Path, "/") && srcURL.Path[2] == ':' { 238 srcURL.Path = srcURL.Path[1:] 239 } 240 } 241 242 if !srcURL.IsAbs() { 243 srcURL, err = absURL(value) 244 if err != nil { 245 return nil, err 246 } 247 } 248 return srcURL, nil 249 } 250 251 func absURL(value string) (*url.URL, error) { 252 cwd, err := os.Getwd() 253 if err != nil { 254 return nil, errors.Wrapf(err, "can't get working directory") 255 } 256 urlCwd := filepath.ToSlash(cwd) 257 baseURL := &url.URL{ 258 Scheme: "file", 259 Path: urlCwd + "/", 260 } 261 relURL := &url.URL{ 262 Path: value, 263 } 264 resolved := baseURL.ResolveReference(relURL) 265 // deal with Windows drive letters 266 if !strings.HasPrefix(urlCwd, "/") && resolved.Path[2] == ':' { 267 resolved.Path = resolved.Path[1:] 268 } 269 return resolved, nil 270 } 271 272 // DefineDatasource - 273 func (d *Data) DefineDatasource(alias, value string) (string, error) { 274 if alias == "" { 275 return "", errors.New("datasource alias must be provided") 276 } 277 if d.DatasourceExists(alias) { 278 return "", nil 279 } 280 srcURL, err := parseSourceURL(value) 281 if err != nil { 282 return "", err 283 } 284 s := &Source{ 285 Alias: alias, 286 URL: srcURL, 287 header: d.extraHeaders[alias], 288 } 289 if d.Sources == nil { 290 d.Sources = make(map[string]*Source) 291 } 292 d.Sources[alias] = s 293 return "", nil 294 } 295 296 // DatasourceExists - 297 func (d *Data) DatasourceExists(alias string) bool { 298 _, ok := d.Sources[alias] 299 return ok 300 } 301 302 func (d *Data) lookupSource(alias string) (*Source, error) { 303 source, ok := d.Sources[alias] 304 if !ok { 305 srcURL, err := url.Parse(alias) 306 if err != nil || !srcURL.IsAbs() { 307 return nil, errors.Errorf("Undefined datasource '%s'", alias) 308 } 309 source = &Source{ 310 Alias: alias, 311 URL: srcURL, 312 header: d.extraHeaders[alias], 313 } 314 d.Sources[alias] = source 315 } 316 if source.Alias == "" { 317 source.Alias = alias 318 } 319 return source, nil 320 } 321 322 func (d *Data) readDataSource(alias string, args ...string) (data, mimeType string, err error) { 323 source, err := d.lookupSource(alias) 324 if err != nil { 325 return "", "", err 326 } 327 b, err := d.readSource(source, args...) 328 if err != nil { 329 return "", "", errors.Wrapf(err, "Couldn't read datasource '%s'", alias) 330 } 331 332 mimeType, err = source.mimeType() 333 if err != nil { 334 return "", "", err 335 } 336 return string(b), mimeType, nil 337 } 338 339 // Include - 340 func (d *Data) Include(alias string, args ...string) (string, error) { 341 data, _, err := d.readDataSource(alias, args...) 342 return data, err 343 } 344 345 // Datasource - 346 func (d *Data) Datasource(alias string, args ...string) (interface{}, error) { 347 data, mimeType, err := d.readDataSource(alias, args...) 348 if err != nil { 349 return nil, err 350 } 351 352 return parseData(mimeType, data) 353 } 354 355 func parseData(mimeType, s string) (out interface{}, err error) { 356 switch mimeType { 357 case jsonMimetype: 358 out, err = JSON(s) 359 case jsonArrayMimetype: 360 out, err = JSONArray(s) 361 case yamlMimetype: 362 out, err = YAML(s) 363 case csvMimetype: 364 out, err = CSV(s) 365 case tomlMimetype: 366 out, err = TOML(s) 367 case envMimetype: 368 out, err = dotEnv(s) 369 case textMimetype: 370 out = s 371 default: 372 return nil, errors.Errorf("Datasources of type %s not yet supported", mimeType) 373 } 374 return out, err 375 } 376 377 // DatasourceReachable - Determines if the named datasource is reachable with 378 // the given arguments. Reads from the datasource, and discards the returned data. 379 func (d *Data) DatasourceReachable(alias string, args ...string) bool { 380 source, ok := d.Sources[alias] 381 if !ok { 382 return false 383 } 384 _, err := d.readSource(source, args...) 385 return err == nil 386 } 387 388 // readSource returns the (possibly cached) data from the given source, 389 // as referenced by the given args 390 func (d *Data) readSource(source *Source, args ...string) ([]byte, error) { 391 if d.cache == nil { 392 d.cache = make(map[string][]byte) 393 } 394 cacheKey := source.Alias 395 for _, v := range args { 396 cacheKey += v 397 } 398 cached, ok := d.cache[cacheKey] 399 if ok { 400 return cached, nil 401 } 402 r, err := d.lookupReader(source.URL.Scheme) 403 if err != nil { 404 return nil, errors.Wrap(err, "Datasource not yet supported") 405 } 406 data, err := r(source, args...) 407 if err != nil { 408 return nil, err 409 } 410 d.cache[cacheKey] = data 411 return data, nil 412 } 413 414 func readStdin(source *Source, args ...string) ([]byte, error) { 415 if stdin == nil { 416 stdin = os.Stdin 417 } 418 b, err := ioutil.ReadAll(stdin) 419 if err != nil { 420 return nil, errors.Wrapf(err, "Can't read %s", stdin) 421 } 422 return b, nil 423 } 424 425 func readBoltDB(source *Source, args ...string) (data []byte, err error) { 426 if source.kv == nil { 427 source.kv, err = libkv.NewBoltDB(source.URL) 428 if err != nil { 429 return nil, err 430 } 431 } 432 433 if len(args) != 1 { 434 return nil, errors.New("missing key") 435 } 436 p := args[0] 437 438 data, err = source.kv.Read(p) 439 if err != nil { 440 return nil, err 441 } 442 443 return data, nil 444 }