From 2a30cb153fc6a1edecf571ba125b2daac12e404b Mon Sep 17 00:00:00 2001 From: James Nugent Date: Fri, 10 Jul 2020 12:19:04 -0500 Subject: Add gcp.Meta function, equivalent to aws.EC2Meta This commit adds a new namespace and function: `gcp.Meta`, which can be used to look up values from the GCP Instance Metadata service. An example usage: ``` echo '{{ gcp.Meta "id" }}' | gomplate ``` This also supports paths, so usage like this works: ``` echo '{{ gcp.Meta "network-interfaces/0/ip" }}' | gomplate ``` --- docs-src/content/functions/gcp.yml | 36 ++++++++++ docs/content/functions/gcp.md | 50 ++++++++++++++ funcs.go | 1 + funcs/gcp.go | 46 +++++++++++++ gcp/meta.go | 132 +++++++++++++++++++++++++++++++++++++ 5 files changed, 265 insertions(+) create mode 100644 docs-src/content/functions/gcp.yml create mode 100644 docs/content/functions/gcp.md create mode 100644 funcs/gcp.go create mode 100644 gcp/meta.go diff --git a/docs-src/content/functions/gcp.yml b/docs-src/content/functions/gcp.yml new file mode 100644 index 00000000..036b1ecb --- /dev/null +++ b/docs-src/content/functions/gcp.yml @@ -0,0 +1,36 @@ +ns: gcp +preamble: | + The functions in the `gcp` namespace interface with various Google Cloud Platform + APIs to make it possible for a template to render differently based on the GCP + environment and metadata. + + ### Configuring GCP + + A number of environment variables can be used to control how gomplate communicates + with GCP APIs. + + | Environment Variable | Description | + | -------------------- | ----------- | + | `GCP_META_ENDPOINT` | _(Default `http://metadata.google.internal`)_ Sets the base address of the instance metadata service. | + | `GCP_TIMEOUT` | _(Default `500`)_ Adjusts timeout for API requests, in milliseconds. | +funcs: + - name: gcp.Meta + description: | + Queries GCP [Instance Metadata](https://cloud.google.com/compute/docs/storing-retrieving-metadata) for information. + + For times when running outside GCP, or when the metadata API can't be reached, a `default` value can be provided. + pipeline: false + arguments: + - name: key + required: true + description: the metadata key to query + - name: default + required: false + description: the default value + examples: + - | + $ echo '{{gcp.Meta "id"}}' | gomplate + 1334999446930701104 + - | + $ echo '{{gcp.Meta "network-interfaces/0/ip"}}' | gomplate + 10.128.0.23 diff --git a/docs/content/functions/gcp.md b/docs/content/functions/gcp.md new file mode 100644 index 00000000..be5acb9d --- /dev/null +++ b/docs/content/functions/gcp.md @@ -0,0 +1,50 @@ +--- +title: gcp functions +menu: + main: + parent: functions +--- + +The functions in the `gcp` namespace interface with various Google Cloud Platform +APIs to make it possible for a template to render differently based on the GCP +environment and metadata. + +### Configuring GCP + +A number of environment variables can be used to control how gomplate communicates +with GCP APIs. + +| Environment Variable | Description | +| -------------------- | ----------- | +| `GCP_META_ENDPOINT` | _(Default `http://metadata.google.internal`)_ Sets the base address of the instance metadata service. | +| `GCP_TIMEOUT` | _(Default `500`)_ Adjusts timeout for API requests, in milliseconds. | + +## `gcp.Meta` + +Queries GCP [Instance Metadata](https://cloud.google.com/compute/docs/storing-retrieving-metadata) for information. + +For times when running outside GCP, or when the metadata API can't be reached, a `default` value can be provided. + +### Usage + +```go +gcp.Meta key [default] +``` + +### Arguments + +| name | description | +|------|-------------| +| `key` | _(required)_ the metadata key to query | +| `default` | _(optional)_ the default value | + +### Examples + +```console +$ echo '{{gcp.Meta "id"}}' | gomplate +1334999446930701104 +``` +```console +$ echo '{{gcp.Meta "network-interfaces/0/ip"}}' | gomplate +10.128.0.23 +``` diff --git a/funcs.go b/funcs.go index e63c9872..2c7e70f1 100644 --- a/funcs.go +++ b/funcs.go @@ -12,6 +12,7 @@ func Funcs(d *data.Data) template.FuncMap { f := template.FuncMap{} funcs.AddDataFuncs(f, d) funcs.AWSFuncs(f) + funcs.AddGCPFuncs(f) funcs.AddBase64Funcs(f) funcs.AddNetFuncs(f) funcs.AddReFuncs(f) diff --git a/funcs/gcp.go b/funcs/gcp.go new file mode 100644 index 00000000..5227909d --- /dev/null +++ b/funcs/gcp.go @@ -0,0 +1,46 @@ +package funcs + +import ( + "sync" + + "github.com/hairyhenderson/gomplate/v3/gcp" +) + +var ( + gcpf *GcpFuncs + gcpfInit sync.Once +) + +// GCPNS - the gcp namespace +func GCPNS() *GcpFuncs { + gcpfInit.Do(func() { + gcpf = &GcpFuncs{ + gcpopts: gcp.GetClientOptions(), + } + }) + return gcpf +} + +// AddGCPFuncs - +func AddGCPFuncs(f map[string]interface{}) { + f["gcp"] = GCPNS +} + +// Funcs - +type GcpFuncs struct { + meta *gcp.MetaClient + metaInit sync.Once + gcpopts gcp.ClientOptions +} + +// Meta - +func (a *GcpFuncs) Meta(key string, def ...string) (string, error) { + a.metaInit.Do(a.initGcpMeta) + return a.meta.Meta(key, def...) +} + +func (a *GcpFuncs) initGcpMeta() { + if a.meta == nil { + a.meta = gcp.NewMetaClient(a.gcpopts) + } +} diff --git a/gcp/meta.go b/gcp/meta.go new file mode 100644 index 00000000..2ba8cf92 --- /dev/null +++ b/gcp/meta.go @@ -0,0 +1,132 @@ +package gcp + +import ( + "io/ioutil" + "net/http" + "os" + "strconv" + "strings" + "sync" + "time" + + "github.com/pkg/errors" + + "github.com/hairyhenderson/gomplate/v3/env" +) + +// DefaultEndpoint is the DNS name for the default GCP compute instance metadata service. +var DefaultEndpoint = "http://metadata.google.internal" + +var ( + // co is a ClientOptions populated from the environment. + co ClientOptions + // coInit ensures that `co` is only set once. + coInit sync.Once +) + +// ClientOptions contains various user-specifiable options for a MetaClient. +type ClientOptions struct { + Timeout time.Duration +} + +// GetClientOptions - Centralised reading of GCP_TIMEOUT +// ... but cannot use in vault/auth.go as different strconv.Atoi error handling +func GetClientOptions() ClientOptions { + coInit.Do(func() { + timeout := os.Getenv("GCP_TIMEOUT") + if timeout == "" { + timeout = "500" + } + + t, err := strconv.Atoi(timeout) + if err != nil { + panic(errors.Wrapf(err, "Invalid GCP_TIMEOUT value '%s' - must be an integer\n", timeout)) + } + + co.Timeout = time.Duration(t) * time.Millisecond + }) + return co +} + +// MetaClient is used to access metadata accessible via the GCP compute instance +// metadata service version 1. +type MetaClient struct { + client *http.Client + endpoint string + options ClientOptions + cache map[string]string +} + +// NewMetaClient constructs a new MetaClient with the given ClientOptions. If the environment +// contains a variable named `GCP_META_ENDPOINT`, the client will address that, if not the +// value of `DefaultEndpoint` is used. +func NewMetaClient(options ClientOptions) *MetaClient { + endpoint := env.Getenv("GCP_META_ENDPOINT") + if endpoint == "" { + endpoint = DefaultEndpoint + } + + return &MetaClient{ + cache: make(map[string]string), + endpoint: endpoint, + options: options, + } +} + +// Meta retrieves a value from the GCP Instance Metadata Service, returning the given default +// if the service is unavailable or the requested URL does not exist. +func (c *MetaClient) Meta(key string, def ...string) (string, error) { + url := c.endpoint + "/computeMetadata/v1/instance/" + key + return c.retrieveMetadata(url, def...) +} + +// retrieveMetadata executes an HTTP request to the GCP Instance Metadata Service with the +// correct headers set, and extracts the returned value. +func (c *MetaClient) retrieveMetadata(url string, def ...string) (string, error) { + if value, ok := c.cache[url]; ok { + return value, nil + } + + if c.client == nil { + timeout := c.options.Timeout + if timeout == 0 { + timeout = 500 * time.Millisecond + } + c.client = &http.Client{Timeout: timeout} + } + + request, err := http.NewRequest(http.MethodGet, url, nil) + if err != nil { + return returnDefault(def), nil + } + request.Header.Add("Metadata-Flavor", "Google") + + resp, err := c.client.Do(request) + if err != nil { + return returnDefault(def), nil + } + + // nolint: errcheck + defer resp.Body.Close() + if resp.StatusCode > 399 { + return returnDefault(def), nil + } + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return "", errors.Wrapf(err, "Failed to read response body from %s", url) + } + value := strings.TrimSpace(string(body)) + c.cache[url] = value + + return value, nil +} + +// returnDefault returns the first element of the given slice (often taken from varargs) +// if there is one, or returns an empty string if the slice has no elements. +func returnDefault(def []string) string { + if len(def) > 0 { + return def[0] + } + return "" +} -- cgit v1.2.3