mirror of https://github.com/perkeep/perkeep.git
website: update (camlistored) deployer to run on GCE
Change-Id: I205c6df51e8d8c4bcf2710fc9da1db68419fc2df
This commit is contained in:
parent
afd28ae279
commit
a2aee25cfb
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue