blob: 33422a713dfda19bea71f7916af563b68827aecf [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 policymanagerutil
import (
"encoding/json"
"strconv"
"time"
"policy_manager/policymanagerproto"
"github.com/golang/glog"
"github.com/golang/protobuf/jsonpb"
"github.com/golang/protobuf/proto"
)
const (
// GCEMetadataURL is the standard url to fetch metadata from.
GCEMetadataURL = "http://metadata.google.internal/computeMetadata/v1"
// 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"
gciKeyTargetVersionPrefix = "gci-target-version-prefix"
gciKeyMetricsEnabled = "gci-metrics-enabled"
gciKeyRebootAfterUpdate = "gci-reboot-after-update"
// Metadata keys with 'cos-' prefix
cosKeyUpdateStrategy = "cos-update-strategy"
cosKeyTargetVersionPrefix = "cos-target-version-prefix"
cosKeyMetricsEnabled = "cos-metrics-enabled"
cosKeyRebootAfterUpdate = "cos-reboot-after-update"
// 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
)
// FetchUserConfig returns the InstanceConfig specified by user in GCE metadata. A nil is
// returned if there is any error.
func FetchUserConfig(retriever MetadataRetriever) *policymanagerproto.InstanceConfig {
if userConfig, _, err := getUserConfig(retriever, ""); err == nil {
return userConfig
} else {
return nil
}
}
// SubscribeUserConfig watches the update of the InstanceConfig specified by user in GCE
// metadata. It repeatedly sends request to wait for update of user config. When an update
// is returned, SubscribeUserConfig compares it to the cached user config and outputs to the
// channel if it is really updated.
func SubscribeUserConfig(retriever MetadataRetriever) <-chan *policymanagerproto.InstanceConfig {
out := make(chan *policymanagerproto.InstanceConfig)
go func() {
var userConfig, lastUserConfig *policymanagerproto.InstanceConfig
var etag string = ""
var err error
lastUserConfig = nil
for {
userConfig, etag, err = getUserConfig(retriever, etag)
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
}
// userConfig might be changed later by main.go (e.g. in resolveLocalUpdateStrategy).
// To cache the original userConfig, a deep copy is necessary.
lastUserConfig = proto.Clone(userConfig).(*policymanagerproto.InstanceConfig)
out <- userConfig
}
}()
return out
}
// getUserConfig gets the content of the instance custom metadata directory
// '.../instance/attributes/'. If the parameter 'lastEtag' is not emptry,
// getUserConfig will wait for an update of the directory (compared to the
// 'lastEtag'). Then getUserConfig parses the metadata and returns userConfig.
// It also returns the latest ETag value of the instance custom metadata directory.
func getUserConfig(retriever MetadataRetriever, lastEtag string) (
*policymanagerproto.InstanceConfig, string, error) {
var rawMetadata, etag string
var metadata map[string]string
var err error
if rawMetadata, etag, err = retriever.FetchMetadata(lastEtag); err != nil {
glog.Errorf("failed to fetch metadata from metadata server: %s", err)
return nil, "", err
}
if metadata, err = parseRawMetadata(rawMetadata); err != nil {
glog.Errorf("failed to parse raw metadata: %s", err)
return nil, "", err
}
glog.V(4).Infof("current value of instance custom metadata directory: %v", metadata)
userConfig := getUserConfigFromMetadata(metadata)
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
}
// 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 key:value metadata pairs. An empty InstanceConfig is returned
// if there is no InstanceConfig-related metadata in the pairs.
func getUserConfigFromMetadata(metadata map[string]string) *policymanagerproto.InstanceConfig {
var configStr string
var err error
var ok, boolean bool
userConfig := new(policymanagerproto.InstanceConfig)
userConfig.HealthMonitorConfig = new(policymanagerproto.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 = metadata[gciLegacyConfigKey]; ok {
if configStr != "" {
if err = jsonpb.UnmarshalString(configStr, userConfig); err != nil {
glog.Errorf("failed to unmarshal InstanceConfig %s: %s", configStr, err)
}
}
return userConfig
}
// Get individual config settings. Keys with 'cos-' prefix have the priority.
if configStr, ok = getCosGciConfigSetting(cosKeyUpdateStrategy, gciKeyUpdateStrategy, metadata); ok {
userConfig.UpdateStrategy = proto.String(configStr)
}
if configStr, ok = getCosGciConfigSetting(cosKeyTargetVersionPrefix, gciKeyTargetVersionPrefix, metadata); ok {
userConfig.TargetVersionPrefix = proto.String(configStr)
}
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(cosKeyRebootAfterUpdate, gciKeyRebootAfterUpdate, metadata); ok {
if boolean, err = strconv.ParseBool(configStr); err == nil {
userConfig.RebootAfterUpdate = proto.Bool(boolean)
}
}
// Get config settings for HealthMonitor logging/monitoring.
if configStr, ok := metadata[keyGoogleLoggingEnabled]; ok {
if boolean, err = strconv.ParseBool(configStr); err == nil {
userConfig.HealthMonitorConfig.LoggingEnabled = proto.Bool(boolean)
}
}
if configStr, ok := metadata[keyGoogleMonitoringEnabled]; 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
}