Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(compute/metadata): add context aware functions #9733

Merged
merged 6 commits into from
Apr 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 4 additions & 2 deletions compute/metadata/examples_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,24 @@
package metadata_test

import (
"context"
"net/http"

"cloud.google.com/go/compute/metadata"
)

// This example demonstrates how to use your own transport when using this package.
func ExampleNewClient() {
ctx := context.Background()
c := metadata.NewClient(&http.Client{Transport: userAgentTransport{
userAgent: "my-user-agent",
base: http.DefaultTransport,
}})
p, err := c.ProjectID()
pID, err := c.GetWithContext(ctx, "project/project-id")
if err != nil {
// TODO: Handle error.
}
_ = p // TODO: Use p.
_ = pID // TODO: Use p.
}

// userAgentTransport sets the User-Agent header before calling base.
Expand Down
104 changes: 70 additions & 34 deletions compute/metadata/metadata.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"io"
"net"
"net/http"
"net/url"
Expand Down Expand Up @@ -95,9 +95,9 @@ func (c *cachedValue) get(cl *Client) (v string, err error) {
return c.v, nil
}
if c.trim {
v, err = cl.getTrimmed(c.k)
v, err = cl.getTrimmed(context.Background(), c.k)
} else {
v, err = cl.Get(c.k)
v, err = cl.GetWithContext(context.Background(), c.k)
}
if err == nil {
c.v = v
Expand Down Expand Up @@ -197,18 +197,32 @@ func systemInfoSuggestsGCE() bool {
// We don't have any non-Linux clues available, at least yet.
return false
}
slurp, _ := ioutil.ReadFile("/sys/class/dmi/id/product_name")
slurp, _ := os.ReadFile("/sys/class/dmi/id/product_name")
name := strings.TrimSpace(string(slurp))
return name == "Google" || name == "Google Compute Engine"
}

// Subscribe calls Client.Subscribe on the default client.
// Subscribe calls Client.SubscribeWithContext on the default client.
func Subscribe(suffix string, fn func(v string, ok bool) error) error {
return defaultClient.Subscribe(suffix, fn)
return defaultClient.SubscribeWithContext(context.Background(), suffix, func(ctx context.Context, v string, ok bool) error { return fn(v, ok) })
}

// Get calls Client.Get on the default client.
func Get(suffix string) (string, error) { return defaultClient.Get(suffix) }
// SubscribeWithContext calls Client.SubscribeWithContext on the default client.
func SubscribeWithContext(ctx context.Context, suffix string, fn func(ctx context.Context, v string, ok bool) error) error {
return defaultClient.SubscribeWithContext(ctx, suffix, fn)
}

// Get calls Client.GetWithContext on the default client.
//
// Deprecated: Please use the context aware variant [GetWithContext].
func Get(suffix string) (string, error) {
return defaultClient.GetWithContext(context.Background(), suffix)
}

// GetWithContext calls Client.GetWithContext on the default client.
func GetWithContext(ctx context.Context, suffix string) (string, error) {
return defaultClient.GetWithContext(ctx, suffix)
}

// ProjectID returns the current instance's project ID string.
func ProjectID() (string, error) { return defaultClient.ProjectID() }
Expand Down Expand Up @@ -288,8 +302,7 @@ func NewClient(c *http.Client) *Client {

// getETag returns a value from the metadata service as well as the associated ETag.
// This func is otherwise equivalent to Get.
func (c *Client) getETag(suffix string) (value, etag string, err error) {
ctx := context.TODO()
func (c *Client) getETag(ctx context.Context, suffix string) (value, etag string, err error) {
// Using a fixed IP makes it very difficult to spoof the metadata service in
// a container, which is an important use-case for local testing of cloud
// deployments. To enable spoofing of the metadata service, the environment
Expand All @@ -306,7 +319,7 @@ func (c *Client) getETag(suffix string) (value, etag string, err error) {
}
suffix = strings.TrimLeft(suffix, "/")
u := "http://" + host + "/computeMetadata/v1/" + suffix
req, err := http.NewRequest("GET", u, nil)
req, err := http.NewRequestWithContext(ctx, "GET", u, nil)
if err != nil {
return "", "", err
}
Expand Down Expand Up @@ -336,7 +349,7 @@ func (c *Client) getETag(suffix string) (value, etag string, err error) {
if res.StatusCode == http.StatusNotFound {
return "", "", NotDefinedError(suffix)
}
all, err := ioutil.ReadAll(res.Body)
all, err := io.ReadAll(res.Body)
if err != nil {
return "", "", err
}
Expand All @@ -354,19 +367,33 @@ func (c *Client) getETag(suffix string) (value, etag string, err error) {
//
// If the requested metadata is not defined, the returned error will
// be of type NotDefinedError.
//
// Deprecated: Please use the context aware variant [Client.GetWithContext].
func (c *Client) Get(suffix string) (string, error) {
val, _, err := c.getETag(suffix)
return c.GetWithContext(context.Background(), suffix)
}

// GetWithContext returns a value from the metadata service.
// The suffix is appended to "http://${GCE_METADATA_HOST}/computeMetadata/v1/".
//
// If the GCE_METADATA_HOST environment variable is not defined, a default of
// 169.254.169.254 will be used instead.
//
// If the requested metadata is not defined, the returned error will
// be of type NotDefinedError.
func (c *Client) GetWithContext(ctx context.Context, suffix string) (string, error) {
val, _, err := c.getETag(ctx, suffix)
return val, err
}

func (c *Client) getTrimmed(suffix string) (s string, err error) {
s, err = c.Get(suffix)
func (c *Client) getTrimmed(ctx context.Context, suffix string) (s string, err error) {
s, err = c.GetWithContext(ctx, suffix)
s = strings.TrimSpace(s)
return
}

func (c *Client) lines(suffix string) ([]string, error) {
j, err := c.Get(suffix)
j, err := c.GetWithContext(context.Background(), suffix)
if err != nil {
return nil, err
}
Expand All @@ -388,7 +415,7 @@ func (c *Client) InstanceID() (string, error) { return instID.get(c) }

// InternalIP returns the instance's primary internal IP address.
func (c *Client) InternalIP() (string, error) {
return c.getTrimmed("instance/network-interfaces/0/ip")
return c.getTrimmed(context.Background(), "instance/network-interfaces/0/ip")
}

// Email returns the email address associated with the service account.
Expand All @@ -398,25 +425,25 @@ func (c *Client) Email(serviceAccount string) (string, error) {
if serviceAccount == "" {
serviceAccount = "default"
}
return c.getTrimmed("instance/service-accounts/" + serviceAccount + "/email")
return c.getTrimmed(context.Background(), "instance/service-accounts/"+serviceAccount+"/email")
}

// ExternalIP returns the instance's primary external (public) IP address.
func (c *Client) ExternalIP() (string, error) {
return c.getTrimmed("instance/network-interfaces/0/access-configs/0/external-ip")
return c.getTrimmed(context.Background(), "instance/network-interfaces/0/access-configs/0/external-ip")
}

// Hostname returns the instance's hostname. This will be of the form
// "<instanceID>.c.<projID>.internal".
func (c *Client) Hostname() (string, error) {
return c.getTrimmed("instance/hostname")
return c.getTrimmed(context.Background(), "instance/hostname")
}

// InstanceTags returns the list of user-defined instance tags,
// assigned when initially creating a GCE instance.
func (c *Client) InstanceTags() ([]string, error) {
var s []string
j, err := c.Get("instance/tags")
j, err := c.GetWithContext(context.Background(), "instance/tags")
if err != nil {
return nil, err
}
Expand All @@ -428,12 +455,12 @@ func (c *Client) InstanceTags() ([]string, error) {

// InstanceName returns the current VM's instance ID string.
func (c *Client) InstanceName() (string, error) {
return c.getTrimmed("instance/name")
return c.getTrimmed(context.Background(), "instance/name")
}

// Zone returns the current VM's zone, such as "us-central1-b".
func (c *Client) Zone() (string, error) {
zone, err := c.getTrimmed("instance/zone")
zone, err := c.getTrimmed(context.Background(), "instance/zone")
// zone is of the form "projects/<projNum>/zones/<zoneName>".
if err != nil {
return "", err
Expand All @@ -460,7 +487,7 @@ func (c *Client) ProjectAttributes() ([]string, error) { return c.lines("project
// InstanceAttributeValue may return ("", nil) if the attribute was
// defined to be the empty string.
func (c *Client) InstanceAttributeValue(attr string) (string, error) {
return c.Get("instance/attributes/" + attr)
return c.GetWithContext(context.Background(), "instance/attributes/"+attr)
}

// ProjectAttributeValue returns the value of the provided
Expand All @@ -472,7 +499,7 @@ func (c *Client) InstanceAttributeValue(attr string) (string, error) {
// ProjectAttributeValue may return ("", nil) if the attribute was
// defined to be the empty string.
func (c *Client) ProjectAttributeValue(attr string) (string, error) {
return c.Get("project/attributes/" + attr)
return c.GetWithContext(context.Background(), "project/attributes/"+attr)
}

// Scopes returns the service account scopes for the given account.
Expand All @@ -489,21 +516,30 @@ func (c *Client) Scopes(serviceAccount string) ([]string, error) {
// The suffix is appended to "http://${GCE_METADATA_HOST}/computeMetadata/v1/".
// The suffix may contain query parameters.
//
// Subscribe calls fn with the latest metadata value indicated by the provided
// suffix. If the metadata value is deleted, fn is called with the empty string
// and ok false. Subscribe blocks until fn returns a non-nil error or the value
// is deleted. Subscribe returns the error value returned from the last call to
// fn, which may be nil when ok == false.
// Deprecated: Please use the context aware variant [Client.SubscribeWithContext].
func (c *Client) Subscribe(suffix string, fn func(v string, ok bool) error) error {
return c.SubscribeWithContext(context.Background(), suffix, func(ctx context.Context, v string, ok bool) error { return fn(v, ok) })
}

// SubscribeWithContext subscribes to a value from the metadata service.
// The suffix is appended to "http://${GCE_METADATA_HOST}/computeMetadata/v1/".
// The suffix may contain query parameters.
//
// SubscribeWithContext calls fn with the latest metadata value indicated by the
// provided suffix. If the metadata value is deleted, fn is called with the
// empty string and ok false. Subscribe blocks until fn returns a non-nil error
// or the value is deleted. Subscribe returns the error value returned from the
// last call to fn, which may be nil when ok == false.
func (c *Client) SubscribeWithContext(ctx context.Context, suffix string, fn func(ctx context.Context, v string, ok bool) error) error {
const failedSubscribeSleep = time.Second * 5

// First check to see if the metadata value exists at all.
val, lastETag, err := c.getETag(suffix)
val, lastETag, err := c.getETag(ctx, suffix)
if err != nil {
return err
}

if err := fn(val, true); err != nil {
if err := fn(ctx, val, true); err != nil {
return err
}

Expand All @@ -514,7 +550,7 @@ func (c *Client) Subscribe(suffix string, fn func(v string, ok bool) error) erro
suffix += "?wait_for_change=true&last_etag="
}
for {
val, etag, err := c.getETag(suffix + url.QueryEscape(lastETag))
val, etag, err := c.getETag(ctx, suffix+url.QueryEscape(lastETag))
if err != nil {
if _, deleted := err.(NotDefinedError); !deleted {
time.Sleep(failedSubscribeSleep)
Expand All @@ -524,7 +560,7 @@ func (c *Client) Subscribe(suffix string, fn func(v string, ok bool) error) erro
}
lastETag = etag

if err := fn(val, ok); err != nil || !ok {
if err := fn(ctx, val, ok); err != nil || !ok {
return err
}
}
Expand Down
111 changes: 0 additions & 111 deletions compute/metadata/metadata_go113_test.go

This file was deleted.