diff --git a/builder/googlecompute/account.go b/builder/googlecompute/account.go new file mode 100644 index 000000000..438181965 --- /dev/null +++ b/builder/googlecompute/account.go @@ -0,0 +1,35 @@ +package googlecompute + +import ( + "encoding/json" + "os" +) + +// accountFile represents the structure of the account file JSON file. +type accountFile struct { + PrivateKeyId string `json:"private_key_id"` + PrivateKey string `json:"private_key"` + ClientEmail string `json:"client_email"` + ClientId string `json:"client_id"` +} + +// clientSecretsFile represents the structure of the client secrets JSON file. +type clientSecretsFile struct { + Web struct { + AuthURI string `json:"auth_uri"` + ClientEmail string `json:"client_email"` + ClientId string `json:"client_id"` + TokenURI string `json:"token_uri"` + } +} + +func loadJSON(result interface{}, path string) error { + f, err := os.Open(path) + if err != nil { + return err + } + defer f.Close() + + dec := json.NewDecoder(f) + return dec.Decode(result) +} diff --git a/builder/googlecompute/builder.go b/builder/googlecompute/builder.go index 84c60d9a3..e5b3d171a 100644 --- a/builder/googlecompute/builder.go +++ b/builder/googlecompute/builder.go @@ -35,7 +35,7 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) { // representing a GCE machine image. func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) { driver, err := NewDriverGCE( - ui, b.config.ProjectId, b.config.clientSecrets, b.config.privateKeyBytes) + ui, b.config.ProjectId, &b.config.account, &b.config.clientSecrets) if err != nil { return nil, err } diff --git a/builder/googlecompute/client_secrets.go b/builder/googlecompute/client_secrets.go deleted file mode 100644 index cbe071514..000000000 --- a/builder/googlecompute/client_secrets.go +++ /dev/null @@ -1,32 +0,0 @@ -package googlecompute - -import ( - "encoding/json" - "io/ioutil" -) - -// clientSecrets represents the client secrets of a GCE service account. -type clientSecrets struct { - Web struct { - AuthURI string `json:"auth_uri"` - ClientEmail string `json:"client_email"` - ClientId string `json:"client_id"` - TokenURI string `json:"token_uri"` - } -} - -// loadClientSecrets loads the GCE client secrets file identified by path. -func loadClientSecrets(path string) (*clientSecrets, error) { - var cs *clientSecrets - secretBytes, err := ioutil.ReadFile(path) - if err != nil { - return nil, err - } - - err = json.Unmarshal(secretBytes, &cs) - if err != nil { - return nil, err - } - - return cs, nil -} diff --git a/builder/googlecompute/client_secrets_test.go b/builder/googlecompute/client_secrets_test.go deleted file mode 100644 index 584aa0ce1..000000000 --- a/builder/googlecompute/client_secrets_test.go +++ /dev/null @@ -1,31 +0,0 @@ -package googlecompute - -import ( - "io/ioutil" - "testing" -) - -func testClientSecretsFile(t *testing.T) string { - tf, err := ioutil.TempFile("", "packer") - if err != nil { - t.Fatalf("err: %s", err) - } - defer tf.Close() - - if _, err := tf.Write([]byte(testClientSecretsContent)); err != nil { - t.Fatalf("err: %s", err) - } - - return tf.Name() -} - -func TestLoadClientSecrets(t *testing.T) { - _, err := loadClientSecrets(testClientSecretsFile(t)) - if err != nil { - t.Fatalf("err: %s", err) - } -} - -// This is just some dummy data that doesn't actually work (it was revoked -// a long time ago). -const testClientSecretsContent = `{"web":{"auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://accounts.google.com/o/oauth2/token","client_email":"774313886706-eorlsj0r4eqkh5e7nvea5fuf59ifr873@developer.gserviceaccount.com","client_x509_cert_url":"https://www.googleapis.com/robot/v1/metadata/x509/774313886706-eorlsj0r4eqkh5e7nvea5fuf59ifr873@developer.gserviceaccount.com","client_id":"774313886706-eorlsj0r4eqkh5e7nvea5fuf59ifr873.apps.googleusercontent.com","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs"}}` diff --git a/builder/googlecompute/config.go b/builder/googlecompute/config.go index 52df02f3a..5bed83b24 100644 --- a/builder/googlecompute/config.go +++ b/builder/googlecompute/config.go @@ -16,8 +16,11 @@ import ( type Config struct { common.PackerConfig `mapstructure:",squash"` + AccountFile string `mapstructure:"account_file"` + ClientSecretsFile string `mapstructure:"client_secrets_file"` + ProjectId string `mapstructure:"project_id"` + BucketName string `mapstructure:"bucket_name"` - ClientSecretsFile string `mapstructure:"client_secrets_file"` DiskSizeGb int64 `mapstructure:"disk_size"` ImageName string `mapstructure:"image_name"` ImageDescription string `mapstructure:"image_description"` @@ -25,9 +28,6 @@ type Config struct { MachineType string `mapstructure:"machine_type"` Metadata map[string]string `mapstructure:"metadata"` Network string `mapstructure:"network"` - Passphrase string `mapstructure:"passphrase"` - PrivateKeyFile string `mapstructure:"private_key_file"` - ProjectId string `mapstructure:"project_id"` SourceImage string `mapstructure:"source_image"` SourceImageProjectId string `mapstructure:"source_image_project_id"` SSHUsername string `mapstructure:"ssh_username"` @@ -37,7 +37,8 @@ type Config struct { Tags []string `mapstructure:"tags"` Zone string `mapstructure:"zone"` - clientSecrets *clientSecrets + account accountFile + clientSecrets clientSecretsFile instanceName string privateKeyBytes []byte sshTimeout time.Duration @@ -104,15 +105,15 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) { // Process Templates templates := map[string]*string{ + "account_file": &c.AccountFile, + "client_secrets_file": &c.ClientSecretsFile, + "bucket_name": &c.BucketName, - "client_secrets_file": &c.ClientSecretsFile, "image_name": &c.ImageName, "image_description": &c.ImageDescription, "instance_name": &c.InstanceName, "machine_type": &c.MachineType, "network": &c.Network, - "passphrase": &c.Passphrase, - "private_key_file": &c.PrivateKeyFile, "project_id": &c.ProjectId, "source_image": &c.SourceImage, "source_image_project_id": &c.SourceImageProjectId, @@ -137,14 +138,14 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) { errs, errors.New("a bucket_name must be specified")) } - if c.ClientSecretsFile == "" { + if c.AccountFile == "" { errs = packer.MultiErrorAppend( - errs, errors.New("a client_secrets_file must be specified")) + errs, errors.New("an account_file must be specified")) } - if c.PrivateKeyFile == "" { + if c.ClientSecretsFile == "" { errs = packer.MultiErrorAppend( - errs, errors.New("a private_key_file must be specified")) + errs, errors.New("a client_secrets_file must be specified")) } if c.ProjectId == "" { @@ -177,22 +178,17 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) { } c.stateTimeout = stateTimeout - if c.ClientSecretsFile != "" { - // Load the client secrets file. - cs, err := loadClientSecrets(c.ClientSecretsFile) - if err != nil { + if c.AccountFile != "" { + if err := loadJSON(&c.account, c.AccountFile); err != nil { errs = packer.MultiErrorAppend( - errs, fmt.Errorf("Failed parsing client secrets file: %s", err)) + errs, fmt.Errorf("Failed parsing account file: %s", err)) } - c.clientSecrets = cs } - if c.PrivateKeyFile != "" { - // Load the private key. - c.privateKeyBytes, err = processPrivateKeyFile(c.PrivateKeyFile, c.Passphrase) - if err != nil { + if c.ClientSecretsFile != "" { + if err := loadJSON(&c.clientSecrets, c.ClientSecretsFile); err != nil { errs = packer.MultiErrorAppend( - errs, fmt.Errorf("Failed loading private key file: %s", err)) + errs, fmt.Errorf("Failed parsing client secrets file: %s", err)) } } diff --git a/builder/googlecompute/config_test.go b/builder/googlecompute/config_test.go index 6fa1425db..f99b9fece 100644 --- a/builder/googlecompute/config_test.go +++ b/builder/googlecompute/config_test.go @@ -1,14 +1,15 @@ package googlecompute import ( + "io/ioutil" "testing" ) func testConfig(t *testing.T) map[string]interface{} { return map[string]interface{}{ + "account_file": testAccountFile(t), "bucket_name": "foo", "client_secrets_file": testClientSecretsFile(t), - "private_key_file": testPrivateKeyFile(t), "project_id": "hashicorp", "source_image": "foo", "zone": "us-east-1a", @@ -84,16 +85,6 @@ func TestConfigPrepare(t *testing.T) { true, }, - { - "private_key_file", - nil, - true, - }, - { - "private_key_file", - testPrivateKeyFile(t), - false, - }, { "private_key_file", "/tmp/i/should/not/exist", @@ -174,3 +165,37 @@ func TestConfigPrepare(t *testing.T) { } } } + +func testAccountFile(t *testing.T) string { + tf, err := ioutil.TempFile("", "packer") + if err != nil { + t.Fatalf("err: %s", err) + } + defer tf.Close() + + if _, err := tf.Write([]byte(testAccountContent)); err != nil { + t.Fatalf("err: %s", err) + } + + return tf.Name() +} + +func testClientSecretsFile(t *testing.T) string { + tf, err := ioutil.TempFile("", "packer") + if err != nil { + t.Fatalf("err: %s", err) + } + defer tf.Close() + + if _, err := tf.Write([]byte(testClientSecretsContent)); err != nil { + t.Fatalf("err: %s", err) + } + + return tf.Name() +} + +// This is just some dummy data that doesn't actually work (it was revoked +// a long time ago). +const testAccountContent = `{}` + +const testClientSecretsContent = `{"web":{"auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://accounts.google.com/o/oauth2/token","client_email":"774313886706-eorlsj0r4eqkh5e7nvea5fuf59ifr873@developer.gserviceaccount.com","client_x509_cert_url":"https://www.googleapis.com/robot/v1/metadata/x509/774313886706-eorlsj0r4eqkh5e7nvea5fuf59ifr873@developer.gserviceaccount.com","client_id":"774313886706-eorlsj0r4eqkh5e7nvea5fuf59ifr873.apps.googleusercontent.com","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs"}}` diff --git a/builder/googlecompute/driver_gce.go b/builder/googlecompute/driver_gce.go index 1439b7e3a..0936d36a0 100644 --- a/builder/googlecompute/driver_gce.go +++ b/builder/googlecompute/driver_gce.go @@ -23,22 +23,27 @@ type driverGCE struct { const DriverScopes string = "https://www.googleapis.com/auth/compute " + "https://www.googleapis.com/auth/devstorage.full_control" -func NewDriverGCE(ui packer.Ui, projectId string, c *clientSecrets, key []byte) (Driver, error) { - log.Printf("[INFO] Requesting token...") - log.Printf("[INFO] -- Email: %s", c.Web.ClientEmail) +func NewDriverGCE(ui packer.Ui, p string, a *accountFile, c *clientSecretsFile) (Driver, error) { + // Get the token for use in our requests + log.Printf("[INFO] Requesting Google token...") + log.Printf("[INFO] -- Email: %s", a.ClientEmail) log.Printf("[INFO] -- Scopes: %s", DriverScopes) - log.Printf("[INFO] -- Private Key Length: %d", len(key)) + log.Printf("[INFO] -- Private Key Length: %d", len(a.PrivateKey)) log.Printf("[INFO] -- Token URL: %s", c.Web.TokenURI) - jwtTok := jwt.NewToken(c.Web.ClientEmail, DriverScopes, key) + jwtTok := jwt.NewToken( + a.ClientEmail, + DriverScopes, + []byte(a.PrivateKey)) jwtTok.ClaimSet.Aud = c.Web.TokenURI token, err := jwtTok.Assert(new(http.Client)) if err != nil { - return nil, err + return nil, fmt.Errorf("Error retrieving auth token: %s", err) } + // Instantiate the transport to communicate to Google transport := &oauth.Transport{ Config: &oauth.Config{ - ClientId: c.Web.ClientId, + ClientId: a.ClientId, Scope: DriverScopes, TokenURL: c.Web.TokenURI, AuthURL: c.Web.AuthURI, @@ -46,14 +51,14 @@ func NewDriverGCE(ui packer.Ui, projectId string, c *clientSecrets, key []byte) Token: token, } - log.Printf("[INFO] Instantiating client...") + log.Printf("[INFO] Instantiating GCE client...") service, err := compute.New(transport.Client()) if err != nil { return nil, err } return &driverGCE{ - projectId: projectId, + projectId: p, service: service, ui: ui, }, nil