internal/config/dynconfig: move dynamic config to its own package
This lets us use our log package, and internal.Experiment.
Change-Id: I48772c2f5d254cfa57c5b791535b9dad9bd3be8d
Reviewed-on: https://go-review.googlesource.com/c/pkgsite/+/256518
Trust: Jonathan Amsterdam <jba@google.com>
Run-TryBot: Jonathan Amsterdam <jba@google.com>
TryBot-Result: kokoro <noreply+kokoro@google.com>
Reviewed-by: Julie Qiu <julie@golang.org>
diff --git a/cmd/internal/cmdconfig/cmdconfig.go b/cmd/internal/cmdconfig/cmdconfig.go
index 1315813..f6f3acf 100644
--- a/cmd/internal/cmdconfig/cmdconfig.go
+++ b/cmd/internal/cmdconfig/cmdconfig.go
@@ -13,6 +13,7 @@
"cloud.google.com/go/errorreporting"
"golang.org/x/pkgsite/internal"
"golang.org/x/pkgsite/internal/config"
+ "golang.org/x/pkgsite/internal/config/dynconfig"
"golang.org/x/pkgsite/internal/log"
"golang.org/x/pkgsite/internal/middleware"
)
@@ -52,19 +53,11 @@
// Ignore getter, use dynamic config.
log.Infof(ctx, "using dynamic config for experiments")
getter = func(ctx context.Context) ([]*internal.Experiment, error) {
- var exps []*internal.Experiment
- dc, err := cfg.ReadDynamic(ctx)
+ dc, err := dynconfig.Read(ctx, cfg.Bucket, cfg.DynamicObject)
if err != nil {
return nil, err
}
- for _, cexp := range dc.Experiments {
- exps = append(exps, &internal.Experiment{
- Name: cexp.Name,
- Description: cexp.Description,
- Rollout: cexp.Rollout,
- })
- }
- return exps, nil
+ return dc.Experiments, nil
}
}
e, err := middleware.NewExperimenter(ctx, 1*time.Minute, getter, reportingClient)
diff --git a/internal/config/config.go b/internal/config/config.go
index 687acda..6ea2452 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -169,8 +169,10 @@
// In case of invalid/empty value, all logs will be printed.
LogLevel string
- // GCS bucket and object for dynamic config.
- bucket, dynamicObject string
+ // GCS Bucket for overrides and dynamic config.
+ Bucket string
+ // GCS object name for dynamic config.
+ DynamicObject string
}
// AppVersionLabel returns the version label for the current instance. This is
@@ -398,23 +400,17 @@
SuccsToGreen: GetEnvInt("GO_DISCOVERY_TEEPROXY_SUCCS_TO_GREEN", 20),
},
LogLevel: os.Getenv("GO_DISCOVERY_LOG_LEVEL"),
- bucket: os.Getenv("GO_DISCOVERY_CONFIG_BUCKET"),
- dynamicObject: os.Getenv("GO_DISCOVERY_CONFIG_DYNAMIC"),
+ Bucket: os.Getenv("GO_DISCOVERY_CONFIG_BUCKET"),
+ DynamicObject: os.Getenv("GO_DISCOVERY_CONFIG_DYNAMIC"),
}
// Check that the dynamic config is readable, but don't do anything with it.
- if cfg.bucket == "" {
+ if cfg.Bucket == "" {
return nil, errors.New("GO_DISCOVERY_CONFIG_BUCKET must be set")
}
- if cfg.dynamicObject == "" {
+ if cfg.DynamicObject == "" {
return nil, errors.New("GO_DISCOVERY_CONFIG_DYNAMIC must be set")
}
- if _, err := cfg.ReadDynamic(ctx); err != nil {
- // This should be an error, but initially, to test permissions in the prod environment,
- // just log it.
- log.Printf("ERROR: %v", err)
- }
-
if cfg.OnGCP() {
// Zone is not available in the environment but can be queried via the metadata API.
zone, err := gceMetadata(ctx, "instance/zone")
@@ -479,11 +475,11 @@
// to re-deploy. (Otherwise, do not use it.)
overrideObj := os.Getenv("GO_DISCOVERY_CONFIG_OVERRIDE")
if overrideObj != "" {
- overrideBytes, err := readOverrideFile(ctx, cfg.bucket, overrideObj)
+ overrideBytes, err := readOverrideFile(ctx, cfg.Bucket, overrideObj)
if err != nil {
log.Print(err)
} else {
- log.Printf("processing overrides from gs://%s/%s", cfg.bucket, overrideObj)
+ log.Printf("processing overrides from gs://%s/%s", cfg.Bucket, overrideObj)
processOverrides(cfg, overrideBytes)
}
}
diff --git a/internal/config/dynamic.go b/internal/config/dynamic.go
deleted file mode 100644
index cd46296..0000000
--- a/internal/config/dynamic.go
+++ /dev/null
@@ -1,71 +0,0 @@
-// Copyright 2020 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
-
-import (
- "context"
- "io/ioutil"
- "log"
-
- "cloud.google.com/go/storage"
- "github.com/ghodss/yaml"
- "golang.org/x/pkgsite/internal/derrors"
-)
-
-// DynamicConfig holds configuration that can change over the lifetime of the
-// process. It is loaded from a GCS file or other external source.
-type DynamicConfig struct {
- // Fields can be added at any time, but removing or changing a field
- // requires careful coordination with the config file contents.
-
- Experiments []Experiment
-}
-
-// An experiment, as dynamically configured.
-// Intended to match internal.Experiment, but we can't use
-// that directly due to an import cycle.
-type Experiment struct {
- Name string
- Rollout uint
- Description string
-}
-
-// ReadDynamic reads the dynamic configuration.
-func (c *Config) ReadDynamic(ctx context.Context) (*DynamicConfig, error) {
- return ReadDynamic(ctx, c.bucket, c.dynamicObject)
-}
-
-// ReadDynamic reads dynamic configuration from the given GCS bucket and object.
-func ReadDynamic(ctx context.Context, bucket, object string) (_ *DynamicConfig, err error) {
- defer derrors.Wrap(&err, "ReadDynamic(%q, %q)", bucket, object)
-
- log.Printf("reading experiments from gs://%s/%s", bucket, object)
- client, err := storage.NewClient(ctx)
- if err != nil {
- return nil, err
- }
- defer client.Close()
- r, err := client.Bucket(bucket).Object(object).NewReader(ctx)
- if err != nil {
- return nil, err
- }
- defer r.Close()
- data, err := ioutil.ReadAll(r)
- if err != nil {
- return nil, err
- }
- return ParseDynamic(data)
-}
-
-// ParseDynamic parses yamlData as a YAML description of DynamicConfig.
-func ParseDynamic(yamlData []byte) (_ *DynamicConfig, err error) {
- defer derrors.Wrap(&err, "ParseDynamic(data)")
-
- var dc DynamicConfig
- if err := yaml.Unmarshal(yamlData, &dc); err != nil {
- return nil, err
- }
- return &dc, nil
-}
diff --git a/internal/config/dynconfig/dynconfig.go b/internal/config/dynconfig/dynconfig.go
new file mode 100644
index 0000000..7a4815a
--- /dev/null
+++ b/internal/config/dynconfig/dynconfig.go
@@ -0,0 +1,61 @@
+// Copyright 2020 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 dynconfig supports dynamic configuration for pkgsite services.
+// Dynamic configuration is read from a file and can change over the lifetime of
+// the process.
+package dynconfig
+
+import (
+ "context"
+ "io/ioutil"
+
+ "cloud.google.com/go/storage"
+ "github.com/ghodss/yaml"
+ "golang.org/x/pkgsite/internal"
+ "golang.org/x/pkgsite/internal/derrors"
+ "golang.org/x/pkgsite/internal/log"
+)
+
+// DynamicConfig holds configuration that can change over the lifetime of the
+// process. It is loaded from a GCS file or other external source.
+type DynamicConfig struct {
+ // Fields can be added at any time, but removing or changing a field
+ // requires careful coordination with the config file contents.
+
+ Experiments []*internal.Experiment
+}
+
+// Read reads dynamic configuration from the given GCS bucket and object.
+func Read(ctx context.Context, bucket, object string) (_ *DynamicConfig, err error) {
+ defer derrors.Wrap(&err, "dynconfig.Read(%q, %q)", bucket, object)
+
+ log.Infof(ctx, "reading dynamic config from gs://%s/%s", bucket, object)
+ client, err := storage.NewClient(ctx)
+ if err != nil {
+ return nil, err
+ }
+ defer client.Close()
+ r, err := client.Bucket(bucket).Object(object).NewReader(ctx)
+ if err != nil {
+ return nil, err
+ }
+ defer r.Close()
+ data, err := ioutil.ReadAll(r)
+ if err != nil {
+ return nil, err
+ }
+ return Parse(data)
+}
+
+// Parse parses yamlData as a YAML description of DynamicConfig.
+func Parse(yamlData []byte) (_ *DynamicConfig, err error) {
+ defer derrors.Wrap(&err, "dynconfig.Parse(data)")
+
+ var dc DynamicConfig
+ if err := yaml.Unmarshal(yamlData, &dc); err != nil {
+ return nil, err
+ }
+ return &dc, nil
+}
diff --git a/internal/experiment.go b/internal/experiment.go
index 99d39bc..1ef1a0d 100644
--- a/internal/experiment.go
+++ b/internal/experiment.go
@@ -24,6 +24,10 @@
// Experiment holds data associated with an experimental feature for frontend
// or worker.
type Experiment struct {
+ // This struct is used to decode dynamic config (see
+ // internal/config/dynconfig). Make sure that changes to this struct are
+ // coordinated with the deployment of config files.
+
// Name is the name of the feature.
Name string
diff --git a/internal/middleware/experiment.go b/internal/middleware/experiment.go
index 393c4f1..a977952 100644
--- a/internal/middleware/experiment.go
+++ b/internal/middleware/experiment.go
@@ -48,6 +48,7 @@
reporter: rep,
pollEvery: pollEvery,
}
+ // If we can't load the initial state, then fail.
if err := e.loadNextSnapshot(ctx); err != nil {
return nil, err
}
@@ -109,6 +110,8 @@
case <-ticker.C:
ctx2, cancel := context.WithTimeout(ctx, e.pollEvery)
if err := e.loadNextSnapshot(ctx2); err != nil {
+ // We already have a snapshot to fall back on, so log and report
+ // the error, but don't fail.
log.Error(ctx, err)
if e.reporter != nil {
e.reporter.Report(errorreporting.Entry{