gddo-server/dynconfig: add package

Copied from https://go.googlesource.com/pkgsite/+/cb9361e611fe25baabab637bc260f48b345bd1da/internal/config/dynconfig
with minor modification to remove reference to derrors package.

Change-Id: Ib006861369613183ac29eb1456582e34dbd83b22
Reviewed-on: https://go-review.googlesource.com/c/gddo/+/285834
Trust: Julie Qiu <julie@golang.org>
Run-TryBot: Julie Qiu <julie@golang.org>
Reviewed-by: Jonathan Amsterdam <jba@google.com>
diff --git a/gddo-server/dynconfig/dynconfig.go b/gddo-server/dynconfig/dynconfig.go
new file mode 100644
index 0000000..d75f8bc
--- /dev/null
+++ b/gddo-server/dynconfig/dynconfig.go
@@ -0,0 +1,87 @@
+// 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"
+	"errors"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"log"
+	"os"
+	"strings"
+
+	"cloud.google.com/go/storage"
+	"github.com/ghodss/yaml"
+)
+
+// 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.
+}
+
+// Read reads dynamic configuration from the given location.
+// Location may be of the form gs://bucket/object, denoting a GCS bucket.
+// Otherwise it is interpreted as a filename.
+func Read(ctx context.Context, location string) (_ *DynamicConfig, err error) {
+	defer wrap(&err, "dynconfig.Read(%q)", location)
+
+	log.Printf("reading dynamic config from %s", location)
+	var r io.ReadCloser
+	if strings.HasPrefix(location, "gs://") {
+		parts := strings.SplitN(location[5:], "/", 2)
+		if len(parts) != 2 {
+			return nil, errors.New("bad GCS URL")
+		}
+		bucket := parts[0]
+		object := parts[1]
+		if err != nil {
+			return nil, err
+		}
+		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
+		}
+	} else {
+		r, err = os.Open(location)
+		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 wrap(&err, "dynconfig.Parse(data)")
+
+	var dc DynamicConfig
+	if err := yaml.Unmarshal(yamlData, &dc); err != nil {
+		return nil, err
+	}
+	return &dc, nil
+}
+
+func wrap(errp *error, format string, args ...interface{}) {
+	if *errp != nil {
+		*errp = fmt.Errorf("%s: %w", fmt.Sprintf(format, args...), *errp)
+	}
+}
diff --git a/go.mod b/go.mod
index 8b23dfb..d21b016 100644
--- a/go.mod
+++ b/go.mod
@@ -9,6 +9,7 @@
 	github.com/davecgh/go-spew v1.1.1 // indirect
 	github.com/fsnotify/fsnotify v1.4.3-0.20170329110642-4da3e2cfbabc // indirect
 	github.com/garyburd/redigo v1.1.1-0.20170914051019-70e1b1943d4f
+	github.com/ghodss/yaml v1.0.0
 	github.com/go-stack/stack v1.6.0 // indirect
 	github.com/golang/lint v0.0.0-20170918230701-e5d664eb928e
 	github.com/golang/snappy v0.0.0-20170215233205-553a64147049
diff --git a/go.sum b/go.sum
index 84fd4e3..bf30d80 100644
--- a/go.sum
+++ b/go.sum
@@ -11,6 +11,8 @@
 github.com/fsnotify/fsnotify v1.4.3-0.20170329110642-4da3e2cfbabc/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
 github.com/garyburd/redigo v1.1.1-0.20170914051019-70e1b1943d4f h1:Sk0u0gIncQaQD23zAoAZs2DNi2u2l5UTLi4CmCBL5v8=
 github.com/garyburd/redigo v1.1.1-0.20170914051019-70e1b1943d4f/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY=
+github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
+github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
 github.com/go-stack/stack v1.6.0 h1:MmJCxYVKTJ0SplGKqFVX3SBnmaUhODHZrrFF6jMbpZk=
 github.com/go-stack/stack v1.6.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
 github.com/golang/lint v0.0.0-20170918230701-e5d664eb928e h1:ior8LN6127GsA53E9mD9nH/oP/LVbJplmLH5V8o+/Uk=