blob: b508afde8e8b9042296b6bae61efda28f23bd191 [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 configfetcher
// Implementation of the MetadataRetriever interface for running on an actual
// GCE instance. This implementation allows us to reduce the number of calls
// to metadata server, thus reducing the cpu utilization of device_policy_manager.
// If we were using go pkg for metadata server(https://pkg.go.dev/cloud.google.com/go/compute/metadata),
// we can use two functions: InstanceAttributes() and InstanceAttributeValue().
// This would have resulted in multiple calls to metadata server.
// Current implementation allows us to call metadata server once,
// and then parse the raw metadata on our side, thus reducing the cpu
// utilization.
// API for querying the metadata server is specified here:
// https://cloud.google.com/compute/docs/metadata
import (
"fmt"
"io/ioutil"
"net/http"
"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 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
}
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
}
// 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 FetchMetadata(lastEtag string, gceMetadataURL string) (
string, string, error) {
path := instanceCustomDataDirectory + "?recursive=true"
if lastEtag != "" {
path += "&wait_for_change=true&last_etag=" + lastEtag
}
resp, etag, err := getEntry(path, gceMetadataURL)
if err != nil {
return "", "", err
}
return strings.TrimSpace(string(resp)), etag, nil
}