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  }