diff --git a/pkg/deploy/gce/handler.go b/pkg/deploy/gce/handler.go index 71167c200..35fa4fa16 100644 --- a/pkg/deploy/gce/handler.go +++ b/pkg/deploy/gce/handler.go @@ -40,16 +40,18 @@ import ( "camlistore.org/pkg/blob" "camlistore.org/pkg/blobserver" "camlistore.org/pkg/blobserver/localdisk" - "camlistore.org/pkg/context" + camliCtx "camlistore.org/pkg/context" "camlistore.org/pkg/httputil" "camlistore.org/pkg/osutil" "camlistore.org/pkg/sorted" "camlistore.org/pkg/sorted/leveldb" "camlistore.org/third_party/code.google.com/p/xsrftoken" + "golang.org/x/net/context" "golang.org/x/oauth2" "golang.org/x/oauth2/google" compute "google.golang.org/api/compute/v1" + "google.golang.org/cloud/compute/metadata" ) const ( @@ -124,23 +126,18 @@ type DeployHandler struct { // Config is the set of parameters to initialize the DeployHandler. type Config struct { - ClientID string `json:"clientID"` // handler's credentials for OAuth. required. - ClientSecret string `json:"clientSecret"` // handler's credentials for OAuth. required. - Project string `json:"project"` // any Google Cloud project we can query to get the valid Google Cloud zones. optional. - ServiceAccount string `json:"serviceAccount"` // JSON file with credentials to Project. optional. - DataDir string `json:"dataDir"` // where to store the instances configurations and states. optional. + ClientID string `json:"clientID"` // handler's credentials for OAuth. Required. + ClientSecret string `json:"clientSecret"` // handler's credentials for OAuth. Required. + Project string `json:"project"` // any Google Cloud project we can query to get the valid Google Cloud zones. Optional. Set from metadata on GCE. + ServiceAccount string `json:"serviceAccount"` // JSON file with credentials to Project. Optional. Unused on GCE. + DataDir string `json:"dataDir"` // where to store the instances configurations and states. Optional. } -// NewDeployHandlerFromConfig initializes a DeployHandler from the JSON config file. -// Host and prefix have the same meaning as for NewDeployHandler. -func NewDeployHandlerFromConfig(host, prefix, configFile string) (http.Handler, error) { - var cfg Config - data, err := ioutil.ReadFile(configFile) - if err != nil { - return nil, fmt.Errorf("Could not read handler's config at %v: %v", configFile, err) - } - if err := json.Unmarshal(data, &cfg); err != nil { - return nil, fmt.Errorf("Could not JSON decode config at %v: %v", configFile, err) +// NewDeployHandlerFromConfig initializes a DeployHandler from cfg. +// Host and prefix have the same meaning as for NewDeployHandler. cfg should not be nil. +func NewDeployHandlerFromConfig(host, prefix string, cfg *Config) (http.Handler, error) { + if cfg == nil { + panic("NewDeployHandlerFromConfig: nil config") } if cfg.ClientID == "" { return nil, errors.New("oauth2 clientID required in config") @@ -244,6 +241,34 @@ func (h *DeployHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { h.mux.ServeHTTP(w, r) } +func (h *DeployHandler) authenticatedClient() (project string, hc *http.Client, err error) { + project = os.Getenv("CAMLI_GCE_PROJECT") + accountFile := os.Getenv("CAMLI_GCE_SERVICE_ACCOUNT") + if project != "" && accountFile != "" { + data, errr := ioutil.ReadFile(accountFile) + err = errr + if err != nil { + return + } + jwtConf, errr := google.JWTConfigFromJSON(data, "https://www.googleapis.com/auth/compute.readonly") + err = errr + if err != nil { + return + } + hc = jwtConf.Client(context.Background()) + return + } + if !metadata.OnGCE() { + err = errNoRefresh + return + } + project, _ = metadata.ProjectID() + hc, err = google.DefaultClient(oauth2.NoContext) + return project, hc, err +} + +var errNoRefresh error = errors.New("not on GCE, and at least one of CAMLI_GCE_PROJECT or CAMLI_GCE_SERVICE_ACCOUNT not defined.") + func (h *DeployHandler) refreshZones() error { h.zonesMu.Lock() defer h.zonesMu.Unlock() @@ -253,28 +278,16 @@ func (h *DeployHandler) refreshZones() error { h.regions = append(h.regions, r) } }() - // TODO(mpl): get projectID and access tokens from metadata once camweb is on GCE. - accountFile := os.Getenv("CAMLI_GCE_SERVICE_ACCOUNT") - if accountFile == "" { - h.Printf("No service account to query for the zones, using hard-coded ones instead.") - h.zones = backupZones - return nil - } - project := os.Getenv("CAMLI_GCE_PROJECT") - if project == "" { - h.Printf("No project we can query on to get the zones, using hard-coded ones instead.") - h.zones = backupZones - return nil - } - data, err := ioutil.ReadFile(accountFile) + project, hc, err := h.authenticatedClient() if err != nil { + if err == errNoRefresh { + h.zones = backupZones + h.Printf("Cannot refresh zones because %v. Using hard-coded ones instead.") + return nil + } return err } - conf, err := google.JWTConfigFromJSON(data, "https://www.googleapis.com/auth/compute.readonly") - if err != nil { - return err - } - s, err := compute.New(conf.Client(oauth2.NoContext)) + s, err := compute.New(hc) if err != nil { return err } @@ -415,7 +428,7 @@ func (h *DeployHandler) serveCallback(w http.ResponseWriter, r *http.Request) { } go func() { - inst, err := depl.Create(context.TODO()) + inst, err := depl.Create(camliCtx.TODO()) state := &creationState{ InstConf: br, } @@ -782,7 +795,11 @@ type creationState struct { func dataStores() (blobserver.Storage, sorted.KeyValue, error) { dataDir := os.Getenv("CAMLI_GCE_DATA") if dataDir == "" { - dataDir = "camli-gce-data" + var err error + dataDir, err = ioutil.TempDir("", "camli-gcedeployer-data") + if err != nil { + return nil, nil, err + } log.Printf("data dir not provided as env var CAMLI_GCE_DATA, so defaulting to %v", dataDir) } blobsDir := filepath.Join(dataDir, "instance-conf") diff --git a/website/camweb.go b/website/camweb.go index 0d730ccec..2a7991d16 100644 --- a/website/camweb.go +++ b/website/camweb.go @@ -20,6 +20,7 @@ import ( "bytes" "crypto/rand" "crypto/tls" + "encoding/json" "flag" "fmt" "html/template" @@ -355,30 +356,59 @@ func runAsChild(res string) { }() } +func gceDeployHandlerConfig() (*gce.Config, error) { + if inProd { + return deployerCredsFromGCS() + } + clientId := os.Getenv("CAMLI_GCE_CLIENTID") + if clientId != "" { + return &gce.Config{ + ClientID: clientId, + ClientSecret: os.Getenv("CAMLI_GCE_CLIENTSECRET"), + Project: os.Getenv("CAMLI_GCE_PROJECT"), + ServiceAccount: os.Getenv("CAMLI_GCE_SERVICE_ACCOUNT"), + DataDir: os.Getenv("CAMLI_GCE_DATA"), + }, nil + } + configFile := filepath.Join(osutil.CamliConfigDir(), "launcher-config.json") + if _, err := os.Stat(configFile); err != nil { + if os.IsNotExist(err) { + return nil, nil + } + return nil, fmt.Errorf("Could not stat %v: %v", configFile, err) + } + data, err := ioutil.ReadFile(configFile) + if err != nil { + return nil, err + } + var config gce.Config + if err := json.Unmarshal(data, &config); err != nil { + return nil, err + } + return &config, nil +} + // gceDeployHandler conditionally returns an http.Handler for a GCE launcher, // configured to run at /prefix/ (the trailing slash can be omitted). -// If CAMLI_GCE_CLIENTID is not set, the launcher-config.json file, if present, -// is used instead of environment variables to initialize the launcher. If a -// launcher isn't enabled, gceDeployHandler returns nil. If another error occurs, +// The launcher is not initialized if: +// - in production, the launcher-config.json file is not found in the relevant bucket +// - neither CAMLI_GCE_CLIENTID is set, nor launcher-config.json is found in the +// camlistore server config dir. +// If a launcher isn't enabled, gceDeployHandler returns nil. If another error occurs, // log.Fatal is called. func gceDeployHandler(prefix string) http.Handler { hostPort, err := netutil.HostPort("https://" + *httpsAddr) if err != nil { hostPort = "camlistore.org:443" } - var gceh http.Handler - if e := os.Getenv("CAMLI_GCE_CLIENTID"); e != "" { - gceh, err = gce.NewDeployHandler(hostPort, prefix) - } else { - config := filepath.Join(*root, "launcher-config.json") - if _, err := os.Stat(config); err != nil { - if os.IsNotExist(err) { - return nil - } - log.Fatalf("Could not stat launcher-config.json: %v", err) + config, err := gceDeployHandlerConfig() + if config == nil { + if err != nil { + log.Printf("Starting without a GCE deploy handler because: %v", err) } - gceh, err = gce.NewDeployHandlerFromConfig(hostPort, prefix, config) + return nil } + gceh, err := gce.NewDeployHandlerFromConfig(hostPort, prefix, config) if err != nil { log.Fatalf("Error initializing gce deploy handler: %v", err) } @@ -742,6 +772,34 @@ func tlsCertFromGCS() (*tls.Certificate, error) { return &cert, nil } +func deployerCredsFromGCS() (*gce.Config, error) { + c, err := googlestorage.NewServiceClient() + if err != nil { + return nil, err + } + slurp := func(key string) ([]byte, error) { + const bucket = "camlistore-website-resource" + rc, _, err := c.GetObject(&googlestorage.Object{ + Bucket: bucket, + Key: key, + }) + if err != nil { + return nil, fmt.Errorf("Error fetching GCS object %q in bucket %q: %v", key, bucket, err) + } + defer rc.Close() + return ioutil.ReadAll(rc) + } + var cfg gce.Config + data, err := slurp("launcher-config.json") + if err != nil { + return nil, err + } + if err := json.Unmarshal(data, &cfg); err != nil { + return nil, fmt.Errorf("Could not JSON decode camli GCE launcher config: %v", err) + } + return &cfg, nil +} + var issueNum = regexp.MustCompile(`^/(?:issue|bug)s?(/\d*)?$`) // issueRedirect returns whether the request should be redirected to the