gddo-server: collect GCE metadata configuration in Viper

Remove the gaAccount global variable in favor of reading from
configuration.

Minor behavior changes:
- The GA_ACCOUNT environment variable will override the GCE metadata
  ga-account, where before it was not being read on GCE.
- The project ID across the program defaults to the one from GCE
  metadata, overridden by the GCLOUD_PROJECT environment variable or the
  command-line flag.  The GCE logger now uses this project ID instead of
  just considering the GCE metadata.
- The GCE log name can now be set using the gddo_gce_log_name
  environment variable.

Change-Id: I0ed2ae7a5130d63146e9bf2746f22980c8a782b9
Reviewed-on: https://go-review.googlesource.com/67050
Reviewed-by: Tuo Shan <shantuo@google.com>
diff --git a/gddo-server/config.go b/gddo-server/config.go
index 5f07548..a246bd3 100644
--- a/gddo-server/config.go
+++ b/gddo-server/config.go
@@ -8,14 +8,16 @@
 	"strings"
 	"time"
 
-	"github.com/golang/gddo/log"
-
+	"cloud.google.com/go/compute/metadata"
 	"github.com/spf13/pflag"
 	"github.com/spf13/viper"
+
+	"github.com/golang/gddo/log"
 )
 
 const (
 	gaeProjectEnvVar = "GCLOUD_PROJECT"
+	gaAccountEnvVar  = "GA_ACCOUNT"
 )
 
 const (
@@ -25,6 +27,7 @@
 	ConfigBindAddress       = "http"
 	ConfigAssetsDir         = "assets"
 	ConfigRobotThreshold    = "robot"
+	ConfigGCELogName        = "gce_log_name"
 
 	// Database Config
 	ConfigDBServer      = "db-server"
@@ -36,6 +39,7 @@
 	ConfigSidebar        = "sidebar"
 	ConfigSourcegraphURL = "sourcegraph_url"
 	ConfigDefaultGOOS    = "default_goos"
+	ConfigGAAccount      = "ga_account"
 
 	// Crawl Config
 	ConfigMaxAge          = "max_age"
@@ -52,12 +56,21 @@
 func init() {
 	ctx := context.Background()
 
-	// Automatically detect if we are on App Engine.
+	// Gather information from execution environment.
 	if os.Getenv(gaeProjectEnvVar) != "" {
 		viper.Set("on_appengine", true)
 	} else {
 		viper.Set("on_appengine", false)
 	}
+	if metadata.OnGCE() {
+		gceProjectAttributeDefault(ctx, viper.GetViper(), ConfigGAAccount, "ga-account")
+		gceProjectAttributeDefault(ctx, viper.GetViper(), ConfigGCELogName, "gce-log-name")
+		if id, err := metadata.ProjectID(); err != nil {
+			log.Warn(ctx, "failed to retrieve project ID", "error", err)
+		} else {
+			viper.SetDefault(ConfigProject, id)
+		}
+	}
 
 	// Setup command line flags
 	flags := buildFlags()
@@ -70,9 +83,8 @@
 	viper.SetEnvPrefix("gddo")
 	viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
 	viper.AutomaticEnv()
-
-	// Automatically get project ID from env on Google App Engine
 	viper.BindEnv(ConfigProject, gaeProjectEnvVar)
+	viper.BindEnv(ConfigGAAccount, gaAccountEnvVar)
 
 	// Read from config.
 	readViperConfig(ctx)
@@ -83,6 +95,17 @@
 	log.Info(ctx, "config values loaded", "values", viper.AllSettings())
 }
 
+func gceProjectAttributeDefault(ctx context.Context, v *viper.Viper, cfg, attr string) {
+	val, err := metadata.ProjectAttributeValue(attr)
+	if err != nil {
+		if _, undef := err.(metadata.NotDefinedError); !undef {
+			log.Warn(ctx, "failed to query metadata", "key", attr, "error", err)
+		}
+		return
+	}
+	v.SetDefault(cfg, val)
+}
+
 // setDefaults sets defaults for configuration options that depend on other
 // configuration options. This allows for smart defaults but allows for
 // overrides.
@@ -99,7 +122,7 @@
 	flags := pflag.NewFlagSet("default", pflag.ExitOnError)
 
 	flags.StringP("config", "c", "", "path to motd config file")
-	flags.String("project", "", "Google Cloud Platform project used for Google services")
+	flags.String(ConfigProject, "", "Google Cloud Platform project used for Google services")
 	// TODO(stephenmw): flags.Bool("enable-admin-pages", false, "When true, enables /admin pages")
 	flags.Float64(ConfigRobotThreshold, 100, "Request counter threshold for robots.")
 	flags.String(ConfigAssetsDir, filepath.Join(defaultBase("github.com/golang/gddo/gddo-server"), "assets"), "Base directory for templates and static files.")
diff --git a/gddo-server/main.go b/gddo-server/main.go
index 8ac09ad..9e94ef5 100644
--- a/gddo-server/main.go
+++ b/gddo-server/main.go
@@ -19,7 +19,6 @@
 	"io"
 	"log"
 	"net/http"
-	"os"
 	"path"
 	"regexp"
 	"runtime/debug"
@@ -28,7 +27,6 @@
 	"strings"
 	"time"
 
-	"cloud.google.com/go/compute/metadata"
 	"cloud.google.com/go/logging"
 	"github.com/spf13/viper"
 
@@ -848,35 +846,6 @@
 	doc.SetDefaultGOOS(viper.GetString(ConfigDefaultGOOS))
 	httpClient = newHTTPClient(viper.GetViper())
 
-	var (
-		gceLogName string
-		projID     string
-	)
-
-	// TODO(stephenmw): merge into viper config infrastructure.
-	if metadata.OnGCE() {
-		acct, err := metadata.ProjectAttributeValue("ga-account")
-		if err != nil {
-			log.Printf("querying metadata for ga-account: %v", err)
-		} else {
-			gaAccount = acct
-		}
-
-		// Get the log name on GCE and setup context for creating a GCE log client.
-		if name, err := metadata.ProjectAttributeValue("gce-log-name"); err != nil {
-			log.Printf("querying metadata for gce-log-name: %v", err)
-		} else {
-			gceLogName = name
-			if id, err := metadata.ProjectID(); err != nil {
-				log.Printf("querying metadata for project ID: %v", err)
-			} else {
-				projID = id
-			}
-		}
-	} else {
-		gaAccount = os.Getenv("GA_ACCOUNT")
-	}
-
 	if err := parseHTMLTemplates([][]string{
 		{"about.html", "common.html", "layout.html"},
 		{"bot.html", "common.html", "layout.html"},
@@ -1011,10 +980,10 @@
 		{"talks.godoc.org", otherDomainHandler{"https", "go-talks.appspot.com"}},
 		{"", httpsRedirectHandler{mux}},
 	}
-	if gceLogName != "" {
+	if gceLogName := viper.GetString(ConfigGCELogName); gceLogName != "" {
 		ctx := context.Background()
 
-		logc, err := logging.NewClient(ctx, projID)
+		logc, err := logging.NewClient(ctx, viper.GetString(ConfigProject))
 		if err != nil {
 			log.Fatalf("Failed to create cloud logging client: %v", err)
 		}
diff --git a/gddo-server/template.go b/gddo-server/template.go
index 819e58c..3aefd5b 100644
--- a/gddo-server/template.go
+++ b/gddo-server/template.go
@@ -472,12 +472,6 @@
 	return isInterfacePat.MatchString(t.Decl.Text)
 }
 
-var gaAccount string
-
-func gaAccountFn() string {
-	return gaAccount
-}
-
 func noteTitleFn(s string) string {
 	return strings.Title(strings.ToLower(s))
 }
@@ -531,7 +525,7 @@
 			"code":              codeFn,
 			"comment":           commentFn,
 			"equal":             reflect.DeepEqual,
-			"gaAccount":         gaAccountFn,
+			"gaAccount":         func() string { return viper.GetString(ConfigGAAccount) },
 			"host":              hostFn,
 			"htmlComment":       htmlCommentFn,
 			"importPath":        importPathFn,