diff --git a/builder/googlecompute/builder.go b/builder/googlecompute/builder.go index d23481a50..6b8dfd331 100644 --- a/builder/googlecompute/builder.go +++ b/builder/googlecompute/builder.go @@ -36,7 +36,7 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) { // representing a GCE machine image. func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (packer.Artifact, error) { driver, err := NewDriverGCE( - ui, b.config.ProjectId, b.config.Account) + ui, b.config.ProjectId, b.config.Account, b.config.VaultGCPOauthEngine) if err != nil { return nil, err } diff --git a/builder/googlecompute/config.go b/builder/googlecompute/config.go index 73a8bfe2e..2fb9dbfc4 100644 --- a/builder/googlecompute/config.go +++ b/builder/googlecompute/config.go @@ -165,6 +165,18 @@ type Config struct { // If true, use the instance's internal IP instead of its external IP // during building. UseInternalIP bool `mapstructure:"use_internal_ip" required:"false"` + // Can be set instead of account_file. If set, this builder will use + // HashiCorp Vault to generate an Oauth token for authenticating against + // Google's cloud. The value should be the path of the token generator + // within vault. + // For information on how to configure your Vault + GCP engine to produce + // Oauth tokens, see https://www.vaultproject.io/docs/auth/gcp.html + // You must have the environment variables VAULT_ADDR and VAULT_TOKEN set, + // along with any other relevant variables for accessing your vault + // instance. For more information, see the Vault docs: + // https://www.vaultproject.io/docs/commands/#environment-variables + // Example:`"vault_gcp_oauth_engine": "gcp/token/my-project-editor",` + VaultGCPOauthEngine string `mapstructure:"vault_gcp_oauth_engine"` // The zone in which to launch the instance used to create the image. // Example: "us-central1-a" Zone string `mapstructure:"zone" required:"true"` @@ -322,7 +334,12 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) { errs = packer.MultiErrorAppend(errs, err) } + // Authenticating via an account file if c.AccountFile != "" { + if c.VaultGCPOauthEngine != "" { + errs = packer.MultiErrorAppend(errs, fmt.Errorf("You cannot "+ + "specify both account_file and vault_gcp_oauth_engine.")) + } cfg, err := ProcessAccountFile(c.AccountFile) if err != nil { errs = packer.MultiErrorAppend(errs, err) diff --git a/builder/googlecompute/driver_gce.go b/builder/googlecompute/driver_gce.go index c7e1fecb0..6409c09c6 100644 --- a/builder/googlecompute/driver_gce.go +++ b/builder/googlecompute/driver_gce.go @@ -19,6 +19,7 @@ import ( "github.com/hashicorp/packer/common/retry" "github.com/hashicorp/packer/helper/useragent" "github.com/hashicorp/packer/packer" + vaultapi "github.com/hashicorp/vault/api" "golang.org/x/oauth2" "golang.org/x/oauth2/google" @@ -35,13 +36,52 @@ type driverGCE struct { var DriverScopes = []string{"https://www.googleapis.com/auth/compute", "https://www.googleapis.com/auth/devstorage.full_control"} -func NewClientGCE(conf *jwt.Config) (*http.Client, error) { +// Define a TokenSource that gets tokens from Vault +type OauthTokenSource struct { + Path string +} + +func (ots OauthTokenSource) Token() (*oauth2.Token, error) { + log.Printf("Retrieving Oauth token from Vault...") + vaultConfig := vaultapi.DefaultConfig() + cli, err := vaultapi.NewClient(vaultConfig) + if err != nil { + return nil, fmt.Errorf("%s\n", err) + } + resp, err := cli.Logical().Read(ots.Path) + if err != nil { + return nil, fmt.Errorf("Error reading vault resp: %s", err) + } + if resp == nil { + return nil, fmt.Errorf("Vault Oauth Engine does not exist at the given path.") + } + token, ok := resp.Data["token"] + if !ok { + return nil, fmt.Errorf("ERROR, token was not present in response body") + } + at := token.(string) + + log.Printf("Retrieved Oauth token from Vault") + return &oauth2.Token{ + AccessToken: at, + Expiry: time.Now().Add(time.Minute * time.Duration(60)), + }, nil + +} + +func NewClientGCE(conf *jwt.Config, vaultOauth string) (*http.Client, error) { var err error var client *http.Client - // Auth with AccountFile first if provided - if conf != nil && len(conf.PrivateKey) > 0 { + if vaultOauth != "" { + // Auth with Vault Oauth + log.Printf("Using Vault to generate Oauth token.") + ts := OauthTokenSource{vaultOauth} + return oauth2.NewClient(oauth2.NoContext, ts), nil + + } else if conf != nil && len(conf.PrivateKey) > 0 { + // Auth with AccountFile if provided log.Printf("[INFO] Requesting Google token via account_file...") log.Printf("[INFO] -- Email: %s", conf.Email) log.Printf("[INFO] -- Scopes: %s", DriverScopes) @@ -75,8 +115,8 @@ func NewClientGCE(conf *jwt.Config) (*http.Client, error) { return client, nil } -func NewDriverGCE(ui packer.Ui, p string, conf *jwt.Config) (Driver, error) { - client, err := NewClientGCE(conf) +func NewDriverGCE(ui packer.Ui, p string, conf *jwt.Config, vaultOauth string) (Driver, error) { + client, err := NewClientGCE(conf, vaultOauth) if err != nil { return nil, err } diff --git a/post-processor/googlecompute-export/post-processor.go b/post-processor/googlecompute-export/post-processor.go index b5ddc8906..b3adebff3 100644 --- a/post-processor/googlecompute-export/post-processor.go +++ b/post-processor/googlecompute-export/post-processor.go @@ -19,13 +19,14 @@ type Config struct { AccountFile string `mapstructure:"account_file"` - DiskSizeGb int64 `mapstructure:"disk_size"` - DiskType string `mapstructure:"disk_type"` - MachineType string `mapstructure:"machine_type"` - Network string `mapstructure:"network"` - Paths []string `mapstructure:"paths"` - Subnetwork string `mapstructure:"subnetwork"` - Zone string `mapstructure:"zone"` + DiskSizeGb int64 `mapstructure:"disk_size"` + DiskType string `mapstructure:"disk_type"` + MachineType string `mapstructure:"machine_type"` + Network string `mapstructure:"network"` + Paths []string `mapstructure:"paths"` + Subnetwork string `mapstructure:"subnetwork"` + VaultGCPOauthEngine string `mapstructure:"vault_gcp_oauth_engine"` + Zone string `mapstructure:"zone"` Account *jwt.Config ctx interpolate.Context @@ -69,6 +70,12 @@ func (p *PostProcessor) Configure(raws ...interface{}) error { p.config.Network = "default" } + if p.config.AccountFile != "" && p.config.VaultGCPOauthEngine != "" { + errs = packer.MultiErrorAppend( + errs, fmt.Errorf("May set either account_file or "+ + "vault_gcp_oauth_engine, but not both.")) + } + if len(errs.Errors) > 0 { return errs } @@ -142,7 +149,8 @@ func (p *PostProcessor) PostProcess(ctx context.Context, ui packer.Ui, artifact } exporterConfig.CalcTimeout() - driver, err := googlecompute.NewDriverGCE(ui, builderProjectId, p.config.Account) + driver, err := googlecompute.NewDriverGCE(ui, builderProjectId, + p.config.Account, p.config.VaultGCPOauthEngine) if err != nil { return nil, false, false, err } diff --git a/post-processor/googlecompute-import/post-processor.go b/post-processor/googlecompute-import/post-processor.go index 32073f993..105969af2 100644 --- a/post-processor/googlecompute-import/post-processor.go +++ b/post-processor/googlecompute-import/post-processor.go @@ -34,6 +34,7 @@ type Config struct { ImageLabels map[string]string `mapstructure:"image_labels"` ImageName string `mapstructure:"image_name"` SkipClean bool `mapstructure:"skip_clean"` + VaultGCPOauthEngine string `mapstructure:"vault_gcp_oauth_engine"` Account *jwt.Config ctx interpolate.Context @@ -78,6 +79,12 @@ func (p *PostProcessor) Configure(raws ...interface{}) error { p.config.Account = cfg } + if p.config.AccountFile != "" && p.config.VaultGCPOauthEngine != "" { + errs = packer.MultiErrorAppend( + errs, fmt.Errorf("May set either account_file or "+ + "vault_gcp_oauth_engine, but not both.")) + } + templates := map[string]*string{ "bucket": &p.config.Bucket, "image_name": &p.config.ImageName, @@ -98,7 +105,7 @@ func (p *PostProcessor) Configure(raws ...interface{}) error { } func (p *PostProcessor) PostProcess(ctx context.Context, ui packer.Ui, artifact packer.Artifact) (packer.Artifact, bool, bool, error) { - client, err := googlecompute.NewClientGCE(p.config.Account) + client, err := googlecompute.NewClientGCE(p.config.Account, p.config.VaultGCPOauthEngine) if err != nil { return nil, false, false, err } diff --git a/website/source/partials/builder/googlecompute/_Config-not-required.html.md b/website/source/partials/builder/googlecompute/_Config-not-required.html.md index 308cedd32..4f0942747 100644 --- a/website/source/partials/builder/googlecompute/_Config-not-required.html.md +++ b/website/source/partials/builder/googlecompute/_Config-not-required.html.md @@ -124,4 +124,16 @@ - `use_internal_ip` (bool) - If true, use the instance's internal IP instead of its external IP during building. + +- `vault_gcp_oauth_engine` (string) - Can be set instead of account_file. If set, this builder will use + HashiCorp Vault to generate an Oauth token for authenticating against + Google's cloud. The value should be the path of the token generator + within vault. + For information on how to configure your Vault + GCP engine to produce + Oauth tokens, see https://www.vaultproject.io/docs/auth/gcp.html + You must have the environment variables VAULT_ADDR and VAULT_TOKEN set, + along with any other relevant variables for accessing your vault + instance. For more information, see the Vault docs: + https://www.vaultproject.io/docs/commands/#environment-variables + Example:`"vault_gcp_oauth_engine": "gcp/token/my-project-editor",` \ No newline at end of file