blob: 0502faa5d505aea1af277b0455e026b1e0ea2221 [file] [log] [blame]
// Copyright 2021 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 policymanagerutil
// Implementation of the MetadataRetriever interface for running on an actual
// GCE instance.
// API for querying the metadata server is specified here:
// https://cloud.google.com/compute/docs/metadata
import (
"fmt"
"io/ioutil"
"net/http"
"strconv"
"strings"
)
// Non-exported constants for querying the metadata server.
const (
// Header to include in every request to the metadata server.
requestHeaderKey = "Metadata-Flavor"
requestHeaderValue = "Google"
// Note that there are 2 types of metadata entries: endpoint and
// directory. Endpoint maps to a single value, while directory contains
// a list of key value pairs.
// URL path for individual metadata endpoint.
// The leading slash is required since these entries are relative to the
// metadata server URL of the form http://.../...
instanceIDEndpoint = "/instance/id"
// URL path for metadata directory. Note that the trailing slash
// indicates that the entry is a directory instead of an endpoint.
// Without it the request will just be forwared by the metadata
// server with a 301 status code.
// The leading slash is required since these entries are relative to the
// metadata server URL of the form http://.../...
instanceCustomDataDirectory = "/instance/attributes/"
projectCustomDataDirectory = "/project/attributes/"
)
// getEntry performs a HTTP GET request for the metadata entry specified by
// its url path, which is relative to the metadata server's URL.
// It returns the body of the response if and only if the status code of the
// response is 200.
//
// Args:
// entry: the path of the metadata entry, e.g. /instance/attributes/cos-update-strategy
// metadataURL: the standard url to fetch metadata from.
// Returns:
// resp: the response body of the HTTP GET request.
// etag: "ETag" field of the response head.
// err: error.
func getEntry(entry, metadataURL string) ([]byte, string, error) {
url := metadataURL + entry
// Construct HTTP client to make the request with the necessary headers.
client := &http.Client{
// Use default policy to stop after 10 consecutive requests.
CheckRedirect: nil,
}
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, "", err
}
req.Header.Add(requestHeaderKey, requestHeaderValue)
// Make the request and read the entire response.
resp, err := client.Do(req)
if resp != nil && resp.Body != nil {
defer resp.Body.Close()
}
if err != nil {
return nil, "", err
} else if resp.StatusCode != http.StatusOK {
return nil, "", fmt.Errorf("request for %s returned status %d",
entry, resp.StatusCode)
}
body, err := ioutil.ReadAll(resp.Body)
return body, resp.Header.Get("etag"), err
}
// metadataRetrieverImpl implements the MetadataRetriever interface. It makes
// HTTP requests to a user provided metadata server URL.
type metadataRetrieverImpl struct {
metadataURL string
}
// NewMetadataRetriever initializes and returns a new MetadataRetriever.
func NewMetadataRetriever(metadataURL string) MetadataRetriever {
return &metadataRetrieverImpl{metadataURL}
}
// GetInstanceID returns the id of the instance.
func (r *metadataRetrieverImpl) GetInstanceID() (uint64, error) {
resp, _, err := getEntry(instanceIDEndpoint, r.metadataURL)
if err != nil {
return 0, err
}
return strconv.ParseUint(string(resp), 10, 64)
}
// FetchMetadata fetches the content and the ETag of the instance custom
// metadata directory. If the parameter 'lastEtag' is not empty, FetchMetadata
// won't return until the directory is updated (compared to 'lastEtag').
// Otherwise, it returns immediately without waiting.
func (r *metadataRetrieverImpl) FetchMetadata(lastEtag string) (
string, string, error) {
path := instanceCustomDataDirectory + "?recursive=true"
if lastEtag != "" {
path += "&wait_for_change=true&last_etag=" + lastEtag
}
resp, etag, err := getEntry(path, r.metadataURL)
if err != nil {
return "", "", err
}
return strings.TrimSpace(string(resp)), etag, nil
}