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/blob"
|
||||||
"camlistore.org/pkg/blobserver"
|
"camlistore.org/pkg/blobserver"
|
||||||
"camlistore.org/pkg/blobserver/localdisk"
|
"camlistore.org/pkg/blobserver/localdisk"
|
||||||
"camlistore.org/pkg/context"
|
camliCtx "camlistore.org/pkg/context"
|
||||||
"camlistore.org/pkg/httputil"
|
"camlistore.org/pkg/httputil"
|
||||||
"camlistore.org/pkg/osutil"
|
"camlistore.org/pkg/osutil"
|
||||||
"camlistore.org/pkg/sorted"
|
"camlistore.org/pkg/sorted"
|
||||||
"camlistore.org/pkg/sorted/leveldb"
|
"camlistore.org/pkg/sorted/leveldb"
|
||||||
|
|
||||||
"camlistore.org/third_party/code.google.com/p/xsrftoken"
|
"camlistore.org/third_party/code.google.com/p/xsrftoken"
|
||||||
|
"golang.org/x/net/context"
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
"golang.org/x/oauth2/google"
|
"golang.org/x/oauth2/google"
|
||||||
compute "google.golang.org/api/compute/v1"
|
compute "google.golang.org/api/compute/v1"
|
||||||
|
"google.golang.org/cloud/compute/metadata"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -124,23 +126,18 @@ type DeployHandler struct {
|
||||||
|
|
||||||
// Config is the set of parameters to initialize the DeployHandler.
|
// Config is the set of parameters to initialize the DeployHandler.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
ClientID string `json:"clientID"` // handler's credentials for OAuth. required.
|
ClientID string `json:"clientID"` // handler's credentials for OAuth. Required.
|
||||||
ClientSecret string `json:"clientSecret"` // 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.
|
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.
|
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.
|
DataDir string `json:"dataDir"` // where to store the instances configurations and states. Optional.
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewDeployHandlerFromConfig initializes a DeployHandler from the JSON config file.
|
// NewDeployHandlerFromConfig initializes a DeployHandler from cfg.
|
||||||
// Host and prefix have the same meaning as for NewDeployHandler.
|
// Host and prefix have the same meaning as for NewDeployHandler. cfg should not be nil.
|
||||||
func NewDeployHandlerFromConfig(host, prefix, configFile string) (http.Handler, error) {
|
func NewDeployHandlerFromConfig(host, prefix string, cfg *Config) (http.Handler, error) {
|
||||||
var cfg Config
|
if cfg == nil {
|
||||||
data, err := ioutil.ReadFile(configFile)
|
panic("NewDeployHandlerFromConfig: nil config")
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
if cfg.ClientID == "" {
|
if cfg.ClientID == "" {
|
||||||
return nil, errors.New("oauth2 clientID required in config")
|
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)
|
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 {
|
func (h *DeployHandler) refreshZones() error {
|
||||||
h.zonesMu.Lock()
|
h.zonesMu.Lock()
|
||||||
defer h.zonesMu.Unlock()
|
defer h.zonesMu.Unlock()
|
||||||
|
@ -253,28 +278,16 @@ func (h *DeployHandler) refreshZones() error {
|
||||||
h.regions = append(h.regions, r)
|
h.regions = append(h.regions, r)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
// TODO(mpl): get projectID and access tokens from metadata once camweb is on GCE.
|
project, hc, err := h.authenticatedClient()
|
||||||
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)
|
|
||||||
if err != nil {
|
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
|
return err
|
||||||
}
|
}
|
||||||
conf, err := google.JWTConfigFromJSON(data, "https://www.googleapis.com/auth/compute.readonly")
|
s, err := compute.New(hc)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
s, err := compute.New(conf.Client(oauth2.NoContext))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -415,7 +428,7 @@ func (h *DeployHandler) serveCallback(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
inst, err := depl.Create(context.TODO())
|
inst, err := depl.Create(camliCtx.TODO())
|
||||||
state := &creationState{
|
state := &creationState{
|
||||||
InstConf: br,
|
InstConf: br,
|
||||||
}
|
}
|
||||||
|
@ -782,7 +795,11 @@ type creationState struct {
|
||||||
func dataStores() (blobserver.Storage, sorted.KeyValue, error) {
|
func dataStores() (blobserver.Storage, sorted.KeyValue, error) {
|
||||||
dataDir := os.Getenv("CAMLI_GCE_DATA")
|
dataDir := os.Getenv("CAMLI_GCE_DATA")
|
||||||
if dataDir == "" {
|
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)
|
log.Printf("data dir not provided as env var CAMLI_GCE_DATA, so defaulting to %v", dataDir)
|
||||||
}
|
}
|
||||||
blobsDir := filepath.Join(dataDir, "instance-conf")
|
blobsDir := filepath.Join(dataDir, "instance-conf")
|
||||||
|
|
|
@ -20,6 +20,7 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"encoding/json"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"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,
|
// gceDeployHandler conditionally returns an http.Handler for a GCE launcher,
|
||||||
// configured to run at /prefix/ (the trailing slash can be omitted).
|
// 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,
|
// The launcher is not initialized if:
|
||||||
// is used instead of environment variables to initialize the launcher. If a
|
// - in production, the launcher-config.json file is not found in the relevant bucket
|
||||||
// launcher isn't enabled, gceDeployHandler returns nil. If another error occurs,
|
// - 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.
|
// log.Fatal is called.
|
||||||
func gceDeployHandler(prefix string) http.Handler {
|
func gceDeployHandler(prefix string) http.Handler {
|
||||||
hostPort, err := netutil.HostPort("https://" + *httpsAddr)
|
hostPort, err := netutil.HostPort("https://" + *httpsAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
hostPort = "camlistore.org:443"
|
hostPort = "camlistore.org:443"
|
||||||
}
|
}
|
||||||
var gceh http.Handler
|
config, err := gceDeployHandlerConfig()
|
||||||
if e := os.Getenv("CAMLI_GCE_CLIENTID"); e != "" {
|
if config == nil {
|
||||||
gceh, err = gce.NewDeployHandler(hostPort, prefix)
|
if err != nil {
|
||||||
} else {
|
log.Printf("Starting without a GCE deploy handler because: %v", err)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
gceh, err = gce.NewDeployHandlerFromConfig(hostPort, prefix, config)
|
return nil
|
||||||
}
|
}
|
||||||
|
gceh, err := gce.NewDeployHandlerFromConfig(hostPort, prefix, config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Error initializing gce deploy handler: %v", err)
|
log.Fatalf("Error initializing gce deploy handler: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -742,6 +772,34 @@ func tlsCertFromGCS() (*tls.Certificate, error) {
|
||||||
return &cert, nil
|
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*)?$`)
|
var issueNum = regexp.MustCompile(`^/(?:issue|bug)s?(/\d*)?$`)
|
||||||
|
|
||||||
// issueRedirect returns whether the request should be redirected to the
|
// issueRedirect returns whether the request should be redirected to the
|
||||||
|
|
Loading…
Reference in New Issue