-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
```- Loading branch information
James Nugent
committed
Jul 12, 2020
1 parent
6593631
commit 2a30cb1
Showing
5 changed files
with
265 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| ``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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) | ||
| } | ||
| } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 "" | ||
| } |