| // 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 ( |
| "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" |
| |
| // The sleep time when watching user config failed. |
| retryInterval = 5 * time.Second |
| ) |
| |
| // 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 |
| etag := "" |
| lastUserConfig = nil |
| for { |
| userConfig, etag, err = getUserConfig(etag, gceMetadataURL) |
| if err != nil { |
| time.Sleep(retryInterval) |
| etag = "" |
| 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 |
| } |
| } |
| |
| // getUserConfig gets the content of the instance custom metadata keys'. |
| // Then getUserConfig parses the metadata keys and returns userConfig. |
| func getUserConfig(lastEtag string, gceMetadataURL string) (*protos.InstanceConfig, string, error) { |
| rawMetadata, etag, err := FetchMetadata(lastEtag, gceMetadataURL) |
| if err != nil { |
| glog.Errorf("Failed to fetch metadata from metadata server: %s", err) |
| return nil, "", err |
| } |
| |
| metadata, err := parseRawMetadata(rawMetadata) |
| if err != nil { |
| glog.Errorf("Failed to parse raw metadata: %s", err) |
| return nil, "", err |
| } |
| glog.Infof("Current value of instance custom metadata keys: %v", metadata) |
| userConfig, err := getUserConfigFromMetadata(metadata) |
| if err != nil { |
| glog.Errorf("Failed to resolve userconfig: %s", err) |
| return nil, "", err |
| } |
| return userConfig, etag, nil |
| } |
| |
| // 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, metadata map[string]string) (string, bool) { |
| value, ok := metadata[cosKey] |
| if !ok { |
| value, ok = metadata[gciKey] |
| } |
| return value, ok |
| } |
| |
| // 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(metadata 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, metadata); 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, metadata); ok { |
| if configStr == "update_disabled" { |
| userConfig.UpdateStrategy = proto.String(configStr) |
| } else { |
| userConfig.UpdateStrategy = proto.String("") |
| } |
| } |
| |
| if configStr, ok = getCOSGCIConfigSetting(cosKeyMetricsEnabled, gciKeyMetricsEnabled, metadata); ok { |
| if boolean, err = strconv.ParseBool(configStr); err == nil { |
| userConfig.MetricsEnabled = proto.Bool(boolean) |
| } |
| } |
| |
| if configStr, ok := getCOSGCIConfigSetting(keyGoogleLoggingEnabled, "", metadata); ok { |
| if boolean, err = strconv.ParseBool(configStr); err == nil { |
| userConfig.HealthMonitorConfig.LoggingEnabled = proto.Bool(boolean) |
| } |
| } |
| |
| if configStr, ok := getCOSGCIConfigSetting(keyGoogleMonitoringEnabled, "", metadata); 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 |
| } |