| // 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 |
| |
| import ( |
| "context" |
| "encoding/json" |
| "fmt" |
| "strconv" |
| "time" |
| |
| "policy-manager/protos" |
| |
| metadataServer "cloud.google.com/go/compute/metadata" |
| "github.com/golang/glog" |
| "github.com/golang/protobuf/jsonpb" |
| "github.com/golang/protobuf/proto" |
| ) |
| |
| const ( |
| // gciLegacyConfigKey is the GCE metadata Key used to specify user's preference of |
| // the InstanceConfig. The value of this key should be JSON representation |
| // of the InstanceConfig proto. |
| // DEPRECATED. Use the specific keys below. |
| gciLegacyConfigKey = "gci-instance-config" |
| |
| // Metadata keys for each InstanceConfig attribute that users can specify. |
| // Although we continue supporting metadata key with 'gci-' prefix, users |
| // should use the keys with 'cos-' prefix if possible. |
| gciKeyUpdateStrategy = "gci-update-strategy" |
| gciKeyMetricsEnabled = "gci-metrics-enabled" |
| |
| // Metadata keys with 'cos-' prefix |
| cosKeyUpdateStrategy = "cos-update-strategy" |
| cosKeyMetricsEnabled = "cos-metrics-enabled" |
| |
| // Metadata keys for GCP-related features. |
| keyGoogleLoggingEnabled = "google-logging-enabled" |
| keyGoogleMonitoringEnabled = "google-monitoring-enabled" |
| |
| // 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/" |
| |
| // The sleep time when watching user config failed. |
| retryInterval = 5 * time.Second |
| ) |
| |
| type mtd struct { |
| metadata map[string]string |
| etag string |
| err error |
| } |
| |
| // PollUserConfig watches the update of the InstanceConfig specified by user in GCE |
| // metadata. It repeatedly sends request to wait for update of userConfig. When an update |
| // is returned, PollUserConfig compares it to the cached userConfig and outputs to the |
| // channel if it is really updated. |
| func PollUserConfig(out chan *protos.InstanceConfig, gceMetadataURL string) { |
| var userConfig, lastUserConfig *protos.InstanceConfig |
| var err error |
| var instanceEtag, projectEtag string |
| lastUserConfig = nil |
| for { |
| userConfig, instanceEtag, projectEtag, err = getUserConfig(instanceEtag, projectEtag, gceMetadataURL) |
| if err != nil { |
| time.Sleep(retryInterval) |
| instanceEtag = "" |
| projectEtag = "" |
| continue // Retry forever. |
| } |
| // Check whether userConfig is updated. |
| if proto.Equal(lastUserConfig, userConfig) { |
| glog.V(1).Infof( |
| "Instance custom metadata are updated but instanceConfig stays the same: {%+v}", |
| userConfig) |
| continue |
| } |
| // To cache the original userConfig, a deep copy is necessary. |
| lastUserConfig = proto.Clone(userConfig).(*protos.InstanceConfig) |
| out <- userConfig |
| } |
| } |
| |
| // fetchMetadataTag gets the content of the metadata from the metadata |
| // server and parses the raw content. It then returns metadata as a |
| // map and etag value. |
| func fetchMetadataTag(ctx context.Context, lastEtag string, dataDirectory string, gceMetadataURL string) (map[string]string, string, error) { |
| rawMetadata, etag, err := FetchMetadata(ctx, lastEtag, dataDirectory, gceMetadataURL) |
| if err != nil { |
| return nil, "", err |
| } |
| |
| mtd, err := parseRawMetadata(rawMetadata) |
| if err != nil { |
| return nil, "", err |
| } |
| return mtd, etag, nil |
| } |
| |
| // fetchMetadataWithChannel gets the content of the metadata from |
| // either instance or project and then writes the result to a channel. |
| func fetchMetadataWithChannel(ctx context.Context, lastEtag string, dataDirectory string, gceMetadataURL string, c chan<- mtd) { |
| metadata, etag, err := fetchMetadataTag(ctx, lastEtag, dataDirectory, gceMetadataURL) |
| if err != nil { |
| c <- mtd{nil, "", err} |
| return |
| } |
| c <- mtd{metadata, etag, nil} |
| } |
| |
| // getUserConfig gets the content of the instance and project custom |
| // metadata keys. Then getUserConfig parses the metadata keys and |
| // returns userConfig. |
| func getUserConfig(lastInstanceEtag string, lastProjectEtag string, gceMetadataURL string) (*protos.InstanceConfig, string, string, error) { |
| var instanceMetadata, projectMetadata map[string]string |
| var instanceEtag, projectEtag string |
| var err error |
| instanceChannel := make(chan mtd, 1) |
| projectChannel := make(chan mtd, 1) |
| |
| ctx1, cancelCtx1 := context.WithCancel(context.Background()) |
| ctx2, cancelCtx2 := context.WithCancel(context.Background()) |
| |
| go fetchMetadataWithChannel(ctx1, lastInstanceEtag, instanceCustomDataDirectory, gceMetadataURL, instanceChannel) |
| go fetchMetadataWithChannel(ctx2, lastProjectEtag, projectCustomDataDirectory, gceMetadataURL, projectChannel) |
| |
| select { |
| case instance := <-instanceChannel: |
| if instance.err != nil { |
| err = instance.err |
| break |
| } |
| cancelCtx2() |
| instanceMetadata = instance.metadata |
| instanceEtag = instance.etag |
| projectMetadata, projectEtag, err = fetchMetadataTag(ctx1, "", projectCustomDataDirectory, gceMetadataURL) |
| if err != nil { |
| break |
| } |
| case project := <-projectChannel: |
| if project.err != nil { |
| err = project.err |
| break |
| } |
| cancelCtx1() |
| projectMetadata = project.metadata |
| projectEtag = project.etag |
| instanceMetadata, instanceEtag, err = fetchMetadataTag(ctx2, "", instanceCustomDataDirectory, gceMetadataURL) |
| if err != nil { |
| break |
| } |
| } |
| |
| if err != nil { |
| glog.Errorf("Failed to fetch/parse metadata from metadata server: %s", err) |
| return nil, "", "", err |
| } |
| |
| userConfig, err := getUserConfigFromMetadata(instanceMetadata, projectMetadata) |
| if err != nil { |
| glog.Errorf("Failed to resolve userconfig: %s", err) |
| return nil, "", "", err |
| } |
| return userConfig, instanceEtag, projectEtag, nil |
| } |
| |
| // parseRawMetadata converts instance custom metadata from string to key:value pairs. |
| func parseRawMetadata(rawMetadata string) (map[string]string, error) { |
| output := make(map[string]string) |
| |
| if err := json.Unmarshal([]byte(rawMetadata), &output); err != nil { |
| return nil, err |
| } |
| return output, nil |
| } |
| |
| // GetInstanceID returns the id of the instance. |
| func GetInstanceID() (uint64, error) { |
| var resp, err = metadataServer.InstanceID() |
| if err != nil { |
| return 0, err |
| } |
| |
| return strconv.ParseUint(string(resp), 10, 64) |
| } |
| |
| // Helper function to get metadata value of a key with 'cos-' prefix and the |
| // corresponding key with 'gci-' prefix. For example, 'cos-update-strategy' and |
| // 'gci-update-strategy'. The value of 'gci-' key is returned only if the function |
| // fail to get the value of 'cos-' key. |
| func getCOSGCIConfigSetting(cosKey string, gciKey string, instanceMetadata map[string]string, projectMetadata map[string]string) (string, bool) { |
| value, ok := instanceMetadata[cosKey] |
| if ok { |
| return value, ok |
| } |
| value, ok = instanceMetadata[gciKey] |
| if ok { |
| return value, ok |
| } |
| value, ok = projectMetadata[cosKey] |
| if ok { |
| return value, ok |
| } |
| value, ok = projectMetadata[gciKey] |
| if ok { |
| return value, ok |
| } |
| return "", false |
| } |
| |
| // Gets user config based on the metadata keys. An empty InstanceConfig is returned |
| // if there is no InstanceConfig-related metadata in the pairs. |
| func getUserConfigFromMetadata(instanceMetadata map[string]string, projectMetadata map[string]string) (*protos.InstanceConfig, error) { |
| var configStr string |
| var err error |
| var ok, boolean bool |
| userConfig := new(protos.InstanceConfig) |
| userConfig.HealthMonitorConfig = new(protos.HealthMonitorConfig) |
| |
| // Get legacy instance config from metadata. This is deprecated, but if this is still |
| // being specified, use it and ignore others. |
| if configStr, ok = getCOSGCIConfigSetting("", gciLegacyConfigKey, instanceMetadata, projectMetadata); ok { |
| if configStr != "" { |
| if err := jsonpb.UnmarshalString(configStr, userConfig); err != nil { |
| return nil, fmt.Errorf("failed to unmarshal InstanceConfig %s: %s", configStr, err) |
| } |
| } |
| return userConfig, nil |
| } |
| |
| // Get individual config settings. Keys with 'cos-' prefix have the priority. |
| if configStr, ok = getCOSGCIConfigSetting(cosKeyUpdateStrategy, gciKeyUpdateStrategy, instanceMetadata, projectMetadata); ok { |
| if configStr == "update_disabled" { |
| userConfig.UpdateStrategy = proto.String(configStr) |
| } else { |
| userConfig.UpdateStrategy = proto.String("") |
| } |
| } |
| |
| if configStr, ok = getCOSGCIConfigSetting(cosKeyMetricsEnabled, gciKeyMetricsEnabled, instanceMetadata, projectMetadata); ok { |
| if boolean, err = strconv.ParseBool(configStr); err == nil { |
| userConfig.MetricsEnabled = proto.Bool(boolean) |
| } |
| } |
| |
| if configStr, ok := getCOSGCIConfigSetting(keyGoogleLoggingEnabled, "", instanceMetadata, projectMetadata); ok { |
| if boolean, err = strconv.ParseBool(configStr); err == nil { |
| userConfig.HealthMonitorConfig.LoggingEnabled = proto.Bool(boolean) |
| } |
| } |
| |
| if configStr, ok := getCOSGCIConfigSetting(keyGoogleMonitoringEnabled, "", instanceMetadata, projectMetadata); ok { |
| if boolean, err = strconv.ParseBool(configStr); err == nil { |
| userConfig.HealthMonitorConfig.MonitoringEnabled = proto.Bool(boolean) |
| } |
| } |
| |
| // Set Enforced to true if user is enabling logging or monitoring. |
| if userConfig.HealthMonitorConfig.GetLoggingEnabled() || userConfig.HealthMonitorConfig.GetMonitoringEnabled() { |
| userConfig.HealthMonitorConfig.Enforced = proto.Bool(true) |
| } |
| |
| return userConfig, nil |
| } |