blob: 74b93261cb45943670393f0145dd0a74d22362ca [file] [edit]
// Copyright 2018 Google LLC
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// https://www.apache.org/licenses/LICENSE-2.0
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package metadata
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"reflect"
"strings"
"testing"
"time"
"github.com/google/go-cmp/cmp"
)
func TestWatchMetadata(t *testing.T) {
etag1, etag2 := "foo", "bar"
var req int
et := time.Now().Add(10 * time.Second).Format(time.RFC3339)
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if req == 0 {
w.Header().Set("etag", etag1)
} else {
w.Header().Set("etag", etag2)
}
fmt.Fprintf(w, `{"instance":{"attributes":{"enable-oslogin":"true","ssh-keys":"name:ssh-rsa [KEY] hostname\nname:ssh-rsa [KEY] hostname","windows-keys":"{}\n{\"expireOn\":\"%[1]s\",\"exponent\":\"exponent\",\"modulus\":\"modulus\",\"username\":\"username\"}\n{\"expireOn\":\"%[1]s\",\"exponent\":\"exponent\",\"modulus\":\"modulus\",\"username\":\"username\",\"addToAdministrators\":true}","wsfc-addrs":"foo"}}}`, et)
req++
}))
defer ts.Close()
client := &Client{
metadataURL: ts.URL,
httpClient: &http.Client{
Timeout: 1 * time.Second,
},
}
truebool := new(bool)
*truebool = true
want := Attributes{
EnableOSLogin: truebool,
WSFCAddresses: "foo",
WindowsKeys: WindowsKeys{
WindowsKey{Exponent: "exponent", UserName: "username", Modulus: "modulus", ExpireOn: et, AddToAdministrators: nil},
WindowsKey{Exponent: "exponent", UserName: "username", Modulus: "modulus", ExpireOn: et, AddToAdministrators: func() *bool { ret := true; return &ret }()},
},
SSHKeys: []string{"name:ssh-rsa [KEY] hostname", "name:ssh-rsa [KEY] hostname"},
DisableTelemetry: false,
}
for _, e := range []string{etag1, etag2} {
got, err := client.Watch(context.Background())
if err != nil {
t.Fatalf("error running watchMetadata: %v", err)
}
gotA := got.Instance.Attributes
if !reflect.DeepEqual(gotA, want) {
t.Fatalf("Did not parse expected metadata.\ngot:\n'%+v'\nwant:\n'%+v'", gotA, want)
}
if client.etag != e {
t.Fatalf("etag not updated as expected (%q != %q)", client.etag, e)
}
}
}
func TestBlockProjectKeys(t *testing.T) {
tests := []struct {
json string
res bool
}{
{
`{"instance": {"attributes": {"ssh-keys": "name:ssh-rsa [KEY] hostname\nname:ssh-rsa [KEY] hostname"},"project": {"attributes": {"ssh-keys": "name:ssh-rsa [KEY] hostname\nname:ssh-rsa [KEY] hostname"}}}}`,
false,
},
{
`{"instance": {"attributes": {"sshKeys": "name:ssh-rsa [KEY] hostname\nname:ssh-rsa [KEY] hostname"},"project": {"attributes": {"ssh-keys": "name:ssh-rsa [KEY] hostname\nname:ssh-rsa [KEY] hostname"}}}}`,
true,
},
{
`{"instance": {"attributes": {"block-project-ssh-keys": "true", "ssh-keys": "name:ssh-rsa [KEY] hostname\nname:ssh-rsa [KEY] hostname"},"project": {"attributes": {"ssh-keys": "name:ssh-rsa [KEY] hostname\nname:ssh-rsa [KEY] hostname"}}}}`,
true,
},
}
for _, test := range tests {
var md Descriptor
if err := json.Unmarshal([]byte(test.json), &md); err != nil {
t.Errorf("failed to unmarshal JSON: %v", err)
}
if md.Instance.Attributes.BlockProjectKeys != test.res {
t.Errorf("instance-level sshKeys didn't set block-project-keys (got %t expected %t)", md.Instance.Attributes.BlockProjectKeys, test.res)
}
}
}
func TestGetKey(t *testing.T) {
var gotHeaders http.Header
var gotReqURI string
wantValue := "value"
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotHeaders = r.Header
gotReqURI = r.RequestURI
fmt.Fprint(w, wantValue)
})
testsrv := httptest.NewServer(handler)
defer testsrv.Close()
client := New()
client.metadataURL = testsrv.URL
key := "key"
wantURI := "/" + key
headers := map[string]string{"key": "value"}
gotValue, err := client.GetKey(context.Background(), key, headers)
if err != nil {
t.Fatal(err)
}
headers["Metadata-Flavor"] = "Google"
for k, v := range headers {
if gotHeaders.Get(k) != v {
t.Fatalf("received headers does not contain all expected headers, want: %q, got: %q", headers, gotHeaders)
}
}
if wantValue != gotValue {
t.Errorf("did not get expected return value, got :%q, want: %q", gotValue, wantValue)
}
if gotReqURI != wantURI {
t.Errorf("did not get expected request uri, got :%q, want: %q", gotReqURI, wantURI)
}
}
func TestGetKeyRecursive(t *testing.T) {
var gotReqURI string
wantValue := `{"ssh-keys":"name:ssh-rsa [KEY] instance1\nothername:ssh-rsa [KEY] instance2","block-project-ssh-keys":"false","other-metadata":"foo"}`
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotReqURI = r.RequestURI
fmt.Fprint(w, wantValue)
})
testsrv := httptest.NewServer(handler)
defer testsrv.Close()
client := New()
client.metadataURL = testsrv.URL
key := "key"
wantURI := fmt.Sprintf("/%s?alt=json&recursive=true", key)
gotValue, err := client.GetKeyRecursive(context.Background(), key)
if err != nil {
t.Errorf("client.GetKeyRecursive(ctx, %s) failed unexpectedly with error: %v", key, err)
}
if wantValue != gotValue {
t.Errorf("client.GetKeyRecursive(ctx, %s) = %q, want: %q", key, gotValue, wantValue)
}
if gotReqURI != wantURI {
t.Errorf("did not get expected request uri, got :%q, want: %q", gotReqURI, wantURI)
}
}
func TestShouldRetry(t *testing.T) {
tests := []struct {
desc string
status int
err error
want bool
}{
{
desc: "404_should_not_retry",
status: 404,
want: false,
err: nil,
},
{
desc: "429_should_retry",
status: 429,
want: true,
err: nil,
},
{
desc: "random_err_should_retry",
status: 200,
want: true,
err: fmt.Errorf("fake retriable error"),
},
}
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
err := &MDSReqError{test.status, test.err}
if got := shouldRetry(err); got != test.want {
t.Errorf("shouldRetry(%+v) = %t, want %t", err, got, test.want)
}
})
}
}
func TestRetry(t *testing.T) {
want := "some-metadata"
ctr := 0
retries := 3
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if ctr == retries {
fmt.Fprint(w, want)
} else {
ctr++
// 412 error code should be retried.
w.WriteHeader(412)
}
}))
defer ts.Close()
client := &Client{
metadataURL: ts.URL,
httpClient: &http.Client{
Timeout: 1 * time.Second,
},
}
reqURL, err := url.JoinPath(ts.URL, "key")
if err != nil {
t.Fatalf("Failed to setup mock URL: %v", err)
}
req := requestConfig{baseURL: reqURL}
got, err := client.retry(context.Background(), req)
if err != nil {
t.Errorf("retry(ctx, %+v) failed unexpectedly with error: %v", req, err)
}
if got != want {
t.Errorf("retry(ctx, %+v) = %s, want %s", req, got, want)
}
if ctr != retries {
t.Errorf("retry(ctx, %+v) retried %d times, should have returned after %d retries", req, ctr, retries)
}
}
func TestRetryError(t *testing.T) {
ctx := context.Background()
ctr := make(map[string]int)
backoffAttempts = 5
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.Contains(r.URL.Path, "retry") {
// Retriable status code.
w.WriteHeader(412)
} else {
// Non-retriable status code.
w.WriteHeader(404)
}
ctr[r.URL.Path] = ctr[r.URL.Path] + 1
}))
defer ts.Close()
client := &Client{
metadataURL: ts.URL,
httpClient: &http.Client{
Timeout: 1 * time.Second,
},
}
tests := []struct {
desc string
mdsKey string
wantCtr int
}{
{
desc: "retries_exhausted",
wantCtr: backoffAttempts,
mdsKey: "/retry",
},
{
desc: "non_retriable_failure",
wantCtr: 1,
mdsKey: "/fail_fast",
},
}
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
reqURL, err := url.JoinPath(ts.URL, test.mdsKey)
if err != nil {
t.Fatalf("Failed to setup mock URL: %v", err)
}
req := requestConfig{baseURL: reqURL}
_, err = client.retry(ctx, req)
if err == nil {
t.Errorf("retry(ctx, %+v) succeeded, want error", req)
}
if ctr[test.mdsKey] != test.wantCtr {
t.Errorf("retry(ctx, %+v) retried %d times, should have returned after %d retries", req, ctr[test.mdsKey], test.wantCtr)
}
})
}
}
func TestVlanInterfaces(t *testing.T) {
vlan := `
{
"0": {
"5": {
"ipv6": [
"::0"
],
"mac": "abcd_mac",
"parentInterface": "/computeMetadata/v1/instance/network-interfaces/0/",
"mtu": 1460,
"vlan": 5
}
}
}`
cfg := fmt.Sprintf(`{"instance": {"vlanNetworkInterfaces": %s}}`, vlan)
want := map[int]map[int]VlanInterface{
0: {
5: {
Mac: "abcd_mac",
ParentInterface: "/computeMetadata/v1/instance/network-interfaces/0/",
MTU: 1460,
Vlan: 5,
IPv6: []string{"::0"},
},
},
}
var md *Descriptor
if err := json.Unmarshal([]byte(cfg), &md); err != nil {
t.Fatalf("json.Unmarshal(%s, &md) failed unexpectedly with error: %v", cfg, err)
}
if diff := cmp.Diff(want, md.Instance.VlanNetworkInterfaces); diff != "" {
t.Errorf("json.Unmarshal(%s, &md) returned unexpected diff (-want,+got):\n %s", cfg, diff)
}
}