utils/metadata.go (101 lines of code) (raw):
// Copyright 2024 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
//
// http://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 utils
import (
"context"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
)
const (
metadataURLPrefix = "http://metadata.google.internal/computeMetadata/v1/"
)
var (
// ErrMDSEntryNotFound is an error used to report 404 status code.
ErrMDSEntryNotFound = errors.New("No metadata entry found: 404 error")
)
// GetMetadata does a HTTP Get request to the metadata server, the metadata entry of
// interest is provided by elem as the elements of the entry path, the following example
// does a Get request to the entry "instance/guest-attributes":
//
// resp, err := GetAttribute(context.Background(), "instance", "guest-attributes")
// ...
func GetMetadata(ctx context.Context, elem ...string) (string, error) {
path, err := url.JoinPath(metadataURLPrefix, elem...)
if err != nil {
return "", fmt.Errorf("failed to parse metadata url: %+s", err)
}
body, _, err := doHTTPGet(ctx, path)
return body, err
}
// GetMetadataWithHeaders is similar to GetMetadata it only differs on the return where GetMetadata
// returns only the response's body as a string and an error GetMetadataWithHeaders returns the
// response's body as a string, the headers and an error.
func GetMetadataWithHeaders(ctx context.Context, elem ...string) (string, http.Header, error) {
path, err := url.JoinPath(metadataURLPrefix, elem...)
if err != nil {
return "", nil, fmt.Errorf("failed to parse metadata url: %+s", err)
}
return doHTTPGet(ctx, path)
}
// PutMetadata does a HTTP Put request to the metadata server, the metadata entry of
// interest is provided by path as the section of the path after the metadata server,
// with the data string as the post data. The following example sets the key
// "instance/guest-attributes/example" to "data":
//
// err := PutMetadata(context.Background(), path.Join("instance", "guest-attributes", "example"), "data")
// ...
func PutMetadata(ctx context.Context, path string, data string) error {
path, err := url.JoinPath(metadataURLPrefix, path)
if err != nil {
return fmt.Errorf("failed to parse metadata url: %+v", err)
}
err = doHTTPPut(ctx, path, data)
if err != nil {
return err
}
return nil
}
func doHTTPRequest(req *http.Request) (*http.Response, error) {
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to do the http request: %+v", err)
}
if resp.StatusCode == 404 {
return nil, ErrMDSEntryNotFound
}
if resp.StatusCode != 200 {
return nil, fmt.Errorf("http response code is %v", resp.StatusCode)
}
return resp, nil
}
func doHTTPGet(ctx context.Context, path string) (string, http.Header, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, path, nil)
if err != nil {
return "", nil, fmt.Errorf("failed to create a http request with context: %+v", err)
}
req.Header.Add("Metadata-Flavor", "Google")
httpGet := func() (string, http.Header, error) {
resp, err := doHTTPRequest(req)
if err != nil {
return "", nil, err
}
val, err := io.ReadAll(resp.Body)
if err != nil {
return "", nil, fmt.Errorf("failed to read http request body: %+v", err)
}
return string(val), resp.Header, nil
}
var resp string
var header http.Header
var getErr error
for i := 1; i <= 5; i++ {
if resp, header, getErr = httpGet(); getErr != nil {
time.Sleep(time.Duration(i) * time.Second)
continue
}
break
}
return resp, header, getErr
}
func doHTTPPut(ctx context.Context, path string, data string) error {
req, err := http.NewRequestWithContext(ctx, http.MethodPut, path, strings.NewReader(data))
if err != nil {
return fmt.Errorf("failed to create a http request with context: %+v", err)
}
req.Header.Add("Metadata-Flavor", "Google")
for i := 1; i <= 5; i++ {
if _, err = doHTTPRequest(req); err != nil {
time.Sleep(time.Duration(i) * time.Second)
continue
}
break
}
return err
}