blob: 6bfc21d6144318d2043dec5fcf141a04f91dda3a [file] [log] [blame]
// Copyright 2017 The Upspin Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// The upspin-setupstorage-gcp command is an external upspin subcommand that
// executes the second step in establishing an upspinserver for GCP.
// Run upspin setupstorage-gcp -help for more information.
package main // import "gcp.upspin.io/cmd/upspin-setupstorage-gcp"
import (
"context"
"flag"
"fmt"
"log"
"os"
"path/filepath"
"golang.org/x/oauth2/google"
"google.golang.org/api/googleapi"
iam "google.golang.org/api/iam/v1"
storage "google.golang.org/api/storage/v1"
"upspin.io/subcmd"
)
type state struct {
*subcmd.State
}
const help = `
Setupstorage-gcp is the second step in establishing an upspinserver,
It sets up GCS storage for your Upspin installation. You may skip this step
if you wish to store Upspin data on your server's local disk.
The first step is 'setupdomain' and the final step is 'setupserver'.
Setupstorage-gcp creates a Google Cloud Storage bucket and a service account for
accessing that bucket. It then writes the service account private key to
$where/$domain/serviceaccount.json and updates the server configuration files
in that directory to use the specified bucket.
Before running this command, you must create a Google Cloud Project and
associated Billing Account using the Cloud Console:
https://cloud.google.com/console
The project ID can be any available string, but for clarity it's helpful to
pick something that resembles your domain name.
You must also install the Google Cloud SDK:
https://cloud.google.com/sdk/downloads
Authenticate and enable the necessary APIs:
$ gcloud auth login
$ gcloud --project <project> beta service-management enable iam.googleapis.com storage_api
And, finally, authenticate again in a different way:
$ gcloud auth application-default login
Running this command when the service account or bucket exists is a no-op.
`
func main() {
const name = "setupstorage-gcp"
log.SetFlags(0)
log.SetPrefix("upspin setupstorage-gcp: ")
s := &state{
State: subcmd.NewState(name),
}
where := flag.String("where", filepath.Join(os.Getenv("HOME"), "upspin", "deploy"), "`directory` to store private configuration files")
domain := flag.String("domain", "", "domain `name` for this Upspin installation")
project := flag.String("project", "", "GCP `project` name")
s.ParseFlags(flag.CommandLine, os.Args[1:], help,
"setupstorage-gcp -domain=<name> -project=<gcp_project_name> <bucket_name>")
if flag.NArg() != 1 {
s.Exitf("a single bucket name must be provided")
}
if *domain == "" || *project == "" {
s.Exitf("the -domain and -project flags must be provided")
}
bucket := flag.Arg(0)
cfgPath := filepath.Join(*where, *domain)
cfg := s.ReadServerConfig(cfgPath)
email, privateKeyData := s.createServiceAccount(*project)
s.createBucket(*project, email, bucket)
cfg.StoreConfig = []string{
"backend=GCS",
"defaultACL=publicRead",
"gcpBucketName=" + bucket,
"privateKeyData=" + privateKeyData,
}
s.WriteServerConfig(cfgPath, cfg)
fmt.Fprintf(os.Stderr, "You should now deploy the upspinserver binary and run 'upspin setupserver'.\n")
s.ExitNow()
}
func (s *state) createServiceAccount(project string) (email, privateKeyData string) {
client, err := google.DefaultClient(context.Background(), iam.CloudPlatformScope)
if err != nil {
// TODO: ask the user to run 'gcloud auth application-default login'
s.Exit(err)
}
svc, err := iam.New(client)
if err != nil {
s.Exit(err)
}
name := "projects/" + project
req := &iam.CreateServiceAccountRequest{
AccountId: "upspinstorage", // TODO(adg): flag?
ServiceAccount: &iam.ServiceAccount{
DisplayName: "Upspin Storage",
},
}
created := true
acct, err := svc.Projects.ServiceAccounts.Create(name, req).Do()
if isExists(err) {
// This should be the name we need to get.
// TODO(adg): make this more robust by listing instead.
guess := name + "/serviceAccounts/upspinstorage@" + project + ".iam.gserviceaccount.com"
acct, err = svc.Projects.ServiceAccounts.Get(guess).Do()
if err != nil {
s.Exit(err)
}
created = false
} else if err != nil {
s.Exit(err)
}
name += "/serviceAccounts/" + acct.Email
req2 := &iam.CreateServiceAccountKeyRequest{}
key, err := svc.Projects.ServiceAccounts.Keys.Create(name, req2).Do()
if err != nil {
s.Exit(err)
}
if created {
fmt.Fprintf(os.Stderr, "Service account %q created.\n", acct.Email)
} else {
fmt.Fprintf(os.Stderr, "A new key for the service account %q was created.\n", acct.Email)
}
return acct.Email, key.PrivateKeyData
}
func (s *state) createBucket(project, email, bucket string) {
client, err := google.DefaultClient(context.Background(), storage.DevstorageFullControlScope)
if err != nil {
// TODO: ask the user to run 'gcloud auth application-default login'
s.Exit(err)
}
svc, err := storage.New(client)
if err != nil {
s.Exit(err)
}
_, err = svc.Buckets.Insert(project, &storage.Bucket{
Acl: []*storage.BucketAccessControl{{
Bucket: bucket,
Entity: "user-" + email,
Email: email,
Role: "OWNER",
}},
Name: bucket,
// TODO(adg): flag for location
}).Do()
if isExists(err) {
// TODO(adg): update bucket ACL to make sure the service
// account has access. (For now, we assume that the user
// created the bucket using this command and that the bucket
// has the correct permissions.)
fmt.Fprintf(os.Stderr, "Bucket %q already exists; re-using it.\n", bucket)
} else if err != nil {
s.Exit(err)
} else {
fmt.Fprintf(os.Stderr, "Bucket %q created.\n", bucket)
}
}
func isExists(err error) bool {
if e, ok := err.(*googleapi.Error); ok && len(e.Errors) > 0 {
for _, e := range e.Errors {
if e.Reason != "alreadyExists" && e.Reason != "conflict" {
return false
}
}
return true
}
return false
}