blob: 649b1f4280285780ff96c1d48f4301755291a0d1 [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
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// 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(,
// 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:
import (
// Non-exported constants for querying the metadata server.
const (
// Header to include in every request to the metadata server.
requestHeaderKey = "Metadata-Flavor"
requestHeaderValue = "Google"
// 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(ctx context.Context, 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.NewRequestWithContext(ctx, "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(ctx context.Context, lastEtag string, dataDirectory string, gceMetadataURL string) (
string, string, error) {
path := dataDirectory + "?recursive=true"
if lastEtag != "" {
path += "&wait_for_change=true&last_etag=" + lastEtag
resp, etag, err := getEntry(ctx, path, gceMetadataURL)
if err != nil {
return "", "", err
return strings.TrimSpace(string(resp)), etag, nil