summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDave Henderson <dhenderson@gmail.com>2020-07-13 08:22:32 -0400
committerGitHub <noreply@github.com>2020-07-13 08:22:32 -0400
commitfa832c355922ff3e84f4caef855bd45a152c2ae2 (patch)
tree4b87947a11c6b490605a86446cc21e5c0db2aa9e
parent597e72403db00bcef09e6e595efd4a213af73a1a (diff)
parent2a30cb153fc6a1edecf571ba125b2daac12e404b (diff)
Merge pull request #891 from jen20/jen20/gcp-meta
Add gcp.Meta function, equivalent to aws.EC2Meta but for GCP
-rw-r--r--docs-src/content/functions/gcp.yml36
-rw-r--r--docs/content/functions/gcp.md50
-rw-r--r--funcs.go1
-rw-r--r--funcs/gcp.go46
-rw-r--r--gcp/meta.go132
5 files changed, 265 insertions, 0 deletions
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 ""
+}