Copyright 2019 The Go Authors. All rights reserved. Use of this source code is governed by a BSD-style license that can be found in the LICENSE file.
Package config resolves shared configuration for Go Discovery services, and provides functions to access this configuration. The Init function should be called before using any of the configuration accessors.
package config

import (
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	

	
	
	
	
	
	mrpb 
)
GetEnv looks up the given key from the environment, returning its value if it exists, and otherwise returning the given fallback value.
func (,  string) string {
	if ,  := os.LookupEnv();  {
		return 
	}
	return 
}
GetEnvInt looks up the given key from the environment and expects an integer, returning the integer value if it exists, and otherwise returning the given fallback value. If the environment variable has a value but it can't be parsed as an integer, GetEnvInt terminates the program.
func ( string,  int) int {
	if ,  := os.LookupEnv();  {
		,  := strconv.Atoi()
		if  != nil {
			log.Fatalf("bad value %q for %s: %v", , , )
		}
		return 
	}
	return 
}
GetEnvFloat64 looks up the given key from the environment and expects a float64, returning the float64 value if it exists, and otherwise returning the given fallback value.
func ( string,  float64) float64 {
	if ,  := os.LookupEnv();  {
		if ,  := strconv.ParseFloat(, 64);  == nil {
			return 
		}
	}
	return 
}
AppVersionFormat is the expected format of the app version timestamp.
const AppVersionFormat = "20060102t150405"
ValidateAppVersion validates that appVersion follows the expected format defined by AppVersionFormat.
Accept GKE versions, which start with the docker image name.
	if strings.HasPrefix(, "gcr.io/") {
		return nil
	}
Accept alternative version, used by our AppEngine deployment script.
		const  = "2006-01-02t15-04"
		if len() > len() {
			 = [:len()]
		}
		if ,  := time.Parse(, );  != nil {
			return fmt.Errorf("app version %q does not match time formats %q or %q: %v",
				, AppVersionFormat, , )
		}
	}
	return nil
}

BypassQuotaAuthHeader is the header key used by the frontend server to know that a request can bypass the quota server.
	BypassQuotaAuthHeader = "X-Go-Discovery-Auth-Bypass-Quota"
BypassCacheAuthHeader is the header key used by the frontend server to know that a request can bypass cache.
	BypassCacheAuthHeader = "X-Go-Discovery-Auth-Bypass-Cache"
BypassErrorReportingHeader is the header key used by the ErrorReporting middleware to avoid calling the errorreporting service.
	BypassErrorReportingHeader = "X-Go-Discovery-Bypass-Error-Reporting"
)
Config holds shared configuration values used in instantiating our server components.
AuthValues is the set of values that could be set on the AuthHeader, in order to bypass checks by the cache.
Discovery environment variables
Ports used for hosting. 'DebugPort' is used for serving HTTP debug pages.
ServiceAccount is the email of the service account that this process is running as when on GCP.
QueueURL is the URL that the Cloud Tasks queue should send requests to. It should be used when the worker is not on AppEngine.
QueueAudience is used to allow the Cloud Tasks queue to authorize itself to the worker. It should be the OAuth 2.0 client ID associated with the IAP that is gating access to the worker.
GoogleTagManagerID is the ID used for GoogleTagManager. It has the structure GTM-XXXX.
MonitoredResource represents the resource that is running the current binary. It might be a Google AppEngine app or a Kubernetes pod. See https://cloud.google.com/monitoring/api/resources for more details: "An object representing a resource that can be used for monitoring, logging, billing, or other purposes. Examples include virtual machine instances, databases, and storage devices such as disks.""
FallbackVersionLabel is used as the VersionLabel when not hosting on AppEngine.
Configuration for redis page cache.
Configuration for redis autocompletion. This is different from the page cache instance as it has different availability requirements.
UseProfiler specifies whether to enable Stackdriver Profiler.
Minimum log level below which no logs will be printed. Possible values are [debug, info, error, fatal]. In case of invalid/empty value, all logs will be printed.
DynamicConfigLocation is the location (either a file or gs://bucket/object) for dynamic configuration.
ServeStats determines whether the server has an endpoint that serves statistics for benchmarking or other purposes.
DisableErrorReporting disables sending errors to the GCP ErrorReporting system.
AppVersionLabel returns the version label for the current instance. This is the AppVersionID available, otherwise a string constructed using the timestamp of process start.
func ( *Config) () string {
	if .VersionID != "" {
		return .VersionID
	}
	return .FallbackVersionLabel
}
OnAppEngine reports if the current process is running in an AppEngine environment.
func ( *Config) () bool {
	return os.Getenv("GAE_ENV") == "standard"
}
OnGKE reports whether the current process is running on GKE.
func ( *Config) () bool {
	return os.Getenv("GO_DISCOVERY_ON_GKE") == "true"
}
OnGCP reports whether the current process is running on Google Cloud Platform.
func ( *Config) () bool {
	return .OnAppEngine() || .OnGKE()
}
StatementTimeout is the value of the Postgres statement_timeout parameter. Statements that run longer than this are terminated. 10 minutes is the App Engine standard request timeout, but we set this longer for the worker.
SourceTimeout is the value of the timeout for source.Client, which is used to fetch source code from third party URLs.
TaskIDChangeIntervalFrontend is the time period during which a given module version can be re-enqueued to frontend tasks.
DBConnInfo returns a PostgreSQL connection string constructed from environment variables, using the primary database host.
func ( *Config) () string {
	return .dbConnInfo(.DBHost)
}
DBSecondaryConnInfo returns a PostgreSQL connection string constructed from environment variables, using the backup database host. It returns the empty string if no backup is configured.
func ( *Config) () string {
	if .DBSecondaryHost == "" {
		return ""
	}
	return .dbConnInfo(.DBSecondaryHost)
}
dbConnInfo returns a PostgresSQL connection string for the given host.
For the connection string syntax, see https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING. Set the statement_timeout config parameter for this session. See https://www.postgresql.org/docs/current/runtime-config-client.html.
	 := fmt.Sprintf("-c statement_timeout=%d", StatementTimeout/time.Millisecond)
	return fmt.Sprintf("user='%s' password='%s' host='%s' port=%s dbname='%s' sslmode=disable options='%s'",
		.DBUser, .DBPassword, , .DBPort, .DBName, )
}
HostAddr returns the network on which to serve the primary HTTP service.
func ( *Config) ( string) string {
	if .Port != "" {
		return fmt.Sprintf(":%s", .Port)
	}
	return 
}
DebugAddr returns the network address on which to serve debugging information.
func ( *Config) ( string) string {
	if .DebugPort != "" {
		return fmt.Sprintf(":%s", .DebugPort)
	}
	return 
}
DeploymentEnvironment returns the deployment environment this process is in: usually one of "local", "exp", "dev", "staging" or "prod".
func ( *Config) () string {
	if .ServiceID == "" {
		return "local"
	}
	 := strings.SplitN(.ServiceID, "-", 2)
	if len() == 1 {
		return "prod"
	}
	if [0] == "" {
		return "unknownEnv"
	}
	return [0]
}
Application returns the name of the running application: "worker", "frontend", etc.
func ( *Config) () string {
	if .ServiceID == "" {
		return "unknownApp"
	}
	 := strings.SplitN(.ServiceID, "-", 2)
	var  string
	if len() == 1 {
		 = [0]
	} else {
		 = [1]
	}
	switch  {
	case "default":
		return "frontend"
	default:
		return 
	}
}
configOverride holds selected config settings that can be dynamically overridden.
QuotaSettings is config for internal/middleware/quota.go
type QuotaSettings struct {
	Enable     bool
	QPS        int // allowed queries per second, per IP block
	Burst      int // maximum requests per second, per block; the size of the token bucket
Record data about blocking, but do not actually block. This is a *bool, so we can distinguish "not present" from "false" in an override
AuthValues is the set of values that could be set on the AuthHeader, in order to bypass checks by the quota server.
	AuthValues []string
	HMACKey    []byte `json:"-"` // key for obfuscating IPs
}
Init resolves all configuration values provided by the config package. It must be called before any configuration values are used.
func ( context.Context) ( *Config,  error) {
Build a Config from the execution environment, loading some values from envvars and others from remote services.
	 := &Config{
		AuthValues: parseCommaList(os.Getenv("GO_DISCOVERY_AUTH_VALUES")),
		IndexURL:   GetEnv("GO_MODULE_INDEX_URL", "https://index.golang.org/index"),
		ProxyURL:   GetEnv("GO_MODULE_PROXY_URL", "https://proxy.golang.org"),
		Port:       os.Getenv("PORT"),
Resolve AppEngine identifiers
		ProjectID:          os.Getenv("GOOGLE_CLOUD_PROJECT"),
		ServiceID:          GetEnv("GAE_SERVICE", os.Getenv("GO_DISCOVERY_SERVICE")),
		VersionID:          GetEnv("GAE_VERSION", os.Getenv("DOCKER_IMAGE")),
		InstanceID:         GetEnv("GAE_INSTANCE", os.Getenv("GO_DISCOVERY_INSTANCE")),
		GoogleTagManagerID: os.Getenv("GO_DISCOVERY_GOOGLE_TAG_MANAGER_ID"),
		QueueURL:           os.Getenv("GO_DISCOVERY_QUEUE_URL"),
		QueueAudience:      os.Getenv("GO_DISCOVERY_QUEUE_AUDIENCE"),
LocationID is essentially hard-coded until we figure out a good way to determine it programmatically, but we check an environment variable in case it needs to be overridden.
This fallback should only be used when developing locally.
		FallbackVersionLabel: time.Now().Format(AppVersionFormat),
		DBHost:               chooseOne(GetEnv("GO_DISCOVERY_DATABASE_HOST", "localhost")),
		DBUser:               GetEnv("GO_DISCOVERY_DATABASE_USER", "postgres"),
		DBPassword:           os.Getenv("GO_DISCOVERY_DATABASE_PASSWORD"),
		DBSecondaryHost:      chooseOne(os.Getenv("GO_DISCOVERY_DATABASE_SECONDARY_HOST")),
		DBPort:               GetEnv("GO_DISCOVERY_DATABASE_PORT", "5432"),
		DBName:               GetEnv("GO_DISCOVERY_DATABASE_NAME", "discovery-db"),
		DBSecret:             os.Getenv("GO_DISCOVERY_DATABASE_SECRET"),
		DBDriver:             GetEnv("GO_DISCOVERY_DATABASE_DRIVER", "pgx"),
		RedisCacheHost:       os.Getenv("GO_DISCOVERY_REDIS_HOST"),
		RedisCachePort:       GetEnv("GO_DISCOVERY_REDIS_PORT", "6379"),
		RedisHAHost:          os.Getenv("GO_DISCOVERY_REDIS_HA_HOST"),
		RedisHAPort:          GetEnv("GO_DISCOVERY_REDIS_HA_PORT", "6379"),
		Quota: QuotaSettings{
			Enable:     os.Getenv("GO_DISCOVERY_ENABLE_QUOTA") == "true",
			QPS:        GetEnvInt("GO_DISCOVERY_QUOTA_QPS", 10),
			Burst:      20,   // ignored in redis-based quota implementation
			MaxEntries: 1000, // ignored in redis-based quota implementation
			RecordOnly: func() *bool {
				 := (os.Getenv("GO_DISCOVERY_QUOTA_RECORD_ONLY") != "false")
				return &
			}(),
			AuthValues: parseCommaList(os.Getenv("GO_DISCOVERY_AUTH_VALUES")),
		},
		UseProfiler:           os.Getenv("GO_DISCOVERY_USE_PROFILER") == "true",
		LogLevel:              os.Getenv("GO_DISCOVERY_LOG_LEVEL"),
		ServeStats:            os.Getenv("GO_DISCOVERY_SERVE_STATS") == "true",
		DisableErrorReporting: os.Getenv("GO_DISCOVERY_DISABLE_ERROR_REPORTING") == "true",
	}
	 := os.Getenv("GO_DISCOVERY_CONFIG_BUCKET")
	 := os.Getenv("GO_DISCOVERY_CONFIG_DYNAMIC")
	if  != "" {
		if  == "" {
			return nil, errors.New("GO_DISCOVERY_CONFIG_DYNAMIC must be set if GO_DISCOVERY_CONFIG_BUCKET is")
		}
		.DynamicConfigLocation = fmt.Sprintf("gs://%s/%s", , )
	} else {
		.DynamicConfigLocation = 
	}
Zone is not available in the environment but can be queried via the metadata API.
		,  := gceMetadata(, "instance/zone")
		if  != nil {
			return nil, 
		}
		.ZoneID = 
		,  := gceMetadata(, "instance/service-accounts/default/email")
		if  != nil {
			return nil, 
		}
		.ServiceAccount = 
		switch {
Use the gae_app monitored resource. It would be better to use the gae_instance monitored resource, but that's not currently supported: https://cloud.google.com/logging/docs/api/v2/resource-list#resource-types
			.MonitoredResource = &mrpb.MonitoredResource{
				Type: "gae_app",
				Labels: map[string]string{
					"project_id": .ProjectID,
					"module_id":  .ServiceID,
					"version_id": .VersionID,
					"zone":       .ZoneID,
				},
			}
		case .OnGKE():
			.MonitoredResource = &mrpb.MonitoredResource{
				Type: "k8s_container",
				Labels: map[string]string{
					"project_id":     .ProjectID,
					"location":       path.Base(.ZoneID),
					"cluster_name":   .DeploymentEnvironment() + "-pkgsite",
					"namespace_name": "default",
					"pod_name":       os.Getenv("HOSTNAME"),
					"container_name": .Application(),
				},
			}
		default:
			return nil, errors.New("on GCP but using an unknown product")
		}
	} else { // running locally, perhaps
		.MonitoredResource = &mrpb.MonitoredResource{
			Type:   "global",
			Labels: map[string]string{"project_id": .ProjectID},
		}
	}
	if .DBHost == "" {
		panic("DBHost is empty; impossible")
	}
	if .DBSecret != "" {
		var  error
		.DBPassword,  = secrets.Get(, .DBSecret)
		if  != nil {
			return nil, fmt.Errorf("could not get database password secret: %v", )
		}
	}
	if .Quota.Enable {
		,  := secrets.Get(, "quota-hmac-key")
		if  != nil {
			return nil, 
		}
		,  := hex.DecodeString()
		if  != nil {
			return nil, 
		}
		if len() < 16 {
			return nil, errors.New("HMAC secret must be at least 16 bytes")
		}
		.Quota.HMACKey = 
	} else {
		log.Print("quota enforcement disabled")
	}
If GO_DISCOVERY_CONFIG_OVERRIDE is set, it should point to a file in a configured bucket which provides overrides for selected configuration. Use this when you want to fix something in prod quickly, without waiting to re-deploy. (Otherwise, do not use it.)
	 := os.Getenv("GO_DISCOVERY_CONFIG_OVERRIDE")
	if  != "" {
		,  := readOverrideFile(, , )
		if  != nil {
			log.Print()
		} else {
			log.Printf("processing overrides from gs://%s/%s", , )
			processOverrides(, )
		}
	}
	return , nil
}

func ( context.Context, ,  string) ( []byte,  error) {
	defer derrors.Wrap(&, "readOverrideFile(ctx, %q)", )

	,  := storage.NewClient()
	if  != nil {
		return nil, 
	}
	defer .Close()
	,  := .Bucket().Object().NewReader()
	if  != nil {
		return nil, 
	}
	defer .Close()
	return ioutil.ReadAll()
}

func ( *Config,  []byte) {
	var  configOverride
	if  := yaml.Unmarshal(, &);  != nil {
		log.Printf("processOverrides: %v", )
		return
	}
	overrideString("DBHost", &.DBHost, .DBHost)
	overrideString("DBSecondaryHost", &.DBSecondaryHost, .DBSecondaryHost)
	overrideString("DBName", &.DBName, .DBName)
	overrideInt("Quota.QPS", &.Quota.QPS, .Quota.QPS)
	overrideInt("Quota.Burst", &.Quota.Burst, .Quota.Burst)
	overrideInt("Quota.MaxEntries", &.Quota.MaxEntries, .Quota.MaxEntries)
	overrideBool("Quota.RecordOnly", &.Quota.RecordOnly, .Quota.RecordOnly)
}

func ( string,  *string,  string) {
	if  != "" {
		* = 
		log.Printf("overriding %s with %q", , )
	}
}

func ( string,  *int,  int) {
	if  != 0 {
		* = 
		log.Printf("overriding %s with %d", , )
	}
}

func ( string,  **bool,  *bool) {
	if  != nil {
		* = 
		log.Printf("overriding %s with %t", , *)
	}
}
Dump outputs the current config information to the given Writer.
func ( *Config) ( io.Writer) error {
	fmt.Fprint(, "config: ")
	 := json.NewEncoder()
	.SetIndent("", "    ")
	return .Encode()
}
chooseOne selects one entry at random from a whitespace-separated string. It returns the empty string if there are no elements.
func ( string) string {
	 := strings.Fields()
	if len() == 0 {
		return ""
	}
	 := rand.NewSource(time.Now().UnixNano())
	 := rand.New()
	return [.Intn(len())]
}
gceMetadata reads a metadata value from GCE. For the possible values of name, see https://cloud.google.com/appengine/docs/standard/java/accessing-instance-metadata.
See https://cloud.google.com/appengine/docs/standard/java/accessing-instance-metadata. (This documentation doesn't exist for Golang, but it seems to work).
	defer derrors.Wrap(&, "gceMetadata(ctx, %q)", )

	const  = "http://metadata.google.internal/computeMetadata/v1/"
	,  := http.NewRequest("GET", +, nil)
	if  != nil {
		return "", fmt.Errorf("http.NewRequest: %v", )
	}
	.Header.Set("Metadata-Flavor", "Google")
	,  := ctxhttp.Do(, nil, )
	if  != nil {
		return "", fmt.Errorf("ctxhttp.Do: %v", )
	}
	defer .Body.Close()
	if .StatusCode != http.StatusOK {
		return "", fmt.Errorf("bad status: %s", .Status)
	}
	,  := ioutil.ReadAll(.Body)
	if  != nil {
		return "", fmt.Errorf("ioutil.ReadAll: %v", )
	}
	return string(), nil
}

func ( string) []string {
	var  []string
	for ,  := range strings.Split(, ",") {
		 = strings.TrimSpace()
		if  != "" {
			 = append(, )
		}
	}
	return