blob: 6c8fc71825e9954d9784eefa84d5400c53006ba6 [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 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
}