| // 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 |
| } |