Skip to content

Commit

Permalink
Merge pull request #891 from jen20/jen20/gcp-meta
Browse files Browse the repository at this point in the history
Add gcp.Meta function, equivalent to aws.EC2Meta but for GCP
  • Loading branch information
Dave Henderson authored and GitHub committed Jul 13, 2020
2 parents 597e724 + 2a30cb1 commit fa832c3
Show file tree
Hide file tree
Showing 5 changed files with 265 additions and 0 deletions.
36 changes: 36 additions & 0 deletions docs-src/content/functions/gcp.yml
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
50 changes: 50 additions & 0 deletions docs/content/functions/gcp.md
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
```
1 change: 1 addition & 0 deletions funcs.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
46 changes: 46 additions & 0 deletions funcs/gcp.go
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)
}
}
132 changes: 132 additions & 0 deletions gcp/meta.go
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 ""
}

0 comments on commit fa832c3

Please sign in to comment.