diff --git a/builtin/providers/vault/provider.go b/builtin/providers/vault/provider.go index d9c7719e9..a9cd35492 100644 --- a/builtin/providers/vault/provider.go +++ b/builtin/providers/vault/provider.go @@ -90,6 +90,7 @@ func Provider() terraform.ResourceProvider { "vault_auth_backend": authBackendResource(), "vault_generic_secret": genericSecretResource(), "vault_policy": policyResource(), + "vault_mount": mountResource(), }, } } diff --git a/builtin/providers/vault/resource_mount.go b/builtin/providers/vault/resource_mount.go new file mode 100644 index 000000000..72c32fa6e --- /dev/null +++ b/builtin/providers/vault/resource_mount.go @@ -0,0 +1,133 @@ +package vault + +import ( + "fmt" + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/vault/api" + "log" +) + +func mountResource() *schema.Resource { + return &schema.Resource{ + Create: mountWrite, + Update: mountUpdate, + Delete: mountDelete, + Read: mountRead, + + Schema: map[string]*schema.Schema{ + "path": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "Where the secret backend will be mounted", + }, + + "type": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "Type of the backend, such as 'aws'", + }, + + "description": { + Type: schema.TypeString, + Optional: true, + Required: false, + ForceNew: true, + Description: "Human-friendly description of the mount", + }, + + "default_lease_ttl_seconds": { + Type: schema.TypeInt, + Required: false, + Optional: true, + ForceNew: false, + Description: "Default lease duration for tokens and secrets in seconds", + }, + + "max_lease_ttl_seconds": { + Type: schema.TypeInt, + Required: false, + Optional: true, + ForceNew: false, + Description: "Maximum possible lease duration for tokens and secrets in seconds", + }, + }, + } +} + +func mountWrite(d *schema.ResourceData, meta interface{}) error { + client := meta.(*api.Client) + + info := &api.MountInput{ + Type: d.Get("type").(string), + Description: d.Get("description").(string), + Config: api.MountConfigInput{ + DefaultLeaseTTL: fmt.Sprintf("%ds", d.Get("default_lease_ttl_seconds")), + MaxLeaseTTL: fmt.Sprintf("%ds", d.Get("max_lease_ttl_seconds")), + }, + } + + path := d.Get("path").(string) + + log.Printf("[DEBUG] Writing mount %s to Vault", path) + + if err := client.Sys().Mount(path, info); err != nil { + return fmt.Errorf("error writing to Vault: %s", err) + } + + d.SetId(path) + + return nil +} + +func mountUpdate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*api.Client) + + config := api.MountConfigInput{ + DefaultLeaseTTL: fmt.Sprintf("%ds", d.Get("default_lease_ttl_seconds")), + MaxLeaseTTL: fmt.Sprintf("%ds", d.Get("max_lease_ttl_seconds")), + } + + path := d.Id() + + log.Printf("[DEBUG] Updating mount %s in Vault", path) + + if err := client.Sys().TuneMount(path, config); err != nil { + return fmt.Errorf("error updating Vault: %s", err) + } + + return nil +} + +func mountDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*api.Client) + + path := d.Id() + + log.Printf("[DEBUG] Unmounting %s from Vault", path) + + if err := client.Sys().Unmount(path); err != nil { + return fmt.Errorf("error deleting from Vault: %s", err) + } + + return nil +} + +func mountRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*api.Client) + + path := d.Id() + + log.Printf("[DEBUG] Reading mount %s from Vault", path) + + mount, err := client.Sys().MountConfig(path) + if err != nil { + return fmt.Errorf("error reading from Vault: %s", err) + } + + d.Set("default_lease_ttl_seconds", mount.DefaultLeaseTTL) + d.Set("max_lease_ttl_seconds", mount.MaxLeaseTTL) + + return nil +} diff --git a/builtin/providers/vault/resource_mount_test.go b/builtin/providers/vault/resource_mount_test.go new file mode 100644 index 000000000..425d88dc5 --- /dev/null +++ b/builtin/providers/vault/resource_mount_test.go @@ -0,0 +1,151 @@ +package vault + +import ( + "fmt" + "testing" + + r "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/hashicorp/vault/api" +) + +func TestResourceMount(t *testing.T) { + r.Test(t, r.TestCase{ + Providers: testProviders, + PreCheck: func() { testAccPreCheck(t) }, + Steps: []r.TestStep{ + { + Config: testResourceMount_initialConfig, + Check: testResourceMount_initialCheck, + }, + { + Config: testResourceMount_updateConfig, + Check: testResourceMount_updateCheck, + }, + }, + }) +} + +var testResourceMount_initialConfig = ` + +resource "vault_mount" "test" { + path = "example" + type = "generic" + description = "Example mount for testing" + default_lease_ttl_seconds = 3600 + max_lease_ttl_seconds = 36000 +} + +` + +func testResourceMount_initialCheck(s *terraform.State) error { + resourceState := s.Modules[0].Resources["vault_mount.test"] + if resourceState == nil { + return fmt.Errorf("resource not found in state") + } + + instanceState := resourceState.Primary + if instanceState == nil { + return fmt.Errorf("resource has no primary instance") + } + + path := instanceState.ID + + if path != instanceState.Attributes["path"] { + return fmt.Errorf("id doesn't match path") + } + + if path != "example" { + return fmt.Errorf("unexpected path value") + } + + mount, err := findMount(path) + if err != nil { + return fmt.Errorf("error reading back mount: %s", err) + } + + if wanted := "Example mount for testing"; mount.Description != wanted { + return fmt.Errorf("description is %v; wanted %v", mount.Description, wanted) + } + + if wanted := "generic"; mount.Type != wanted { + return fmt.Errorf("type is %v; wanted %v", mount.Description, wanted) + } + + if wanted := 3600; mount.Config.DefaultLeaseTTL != wanted { + return fmt.Errorf("default lease ttl is %v; wanted %v", mount.Description, wanted) + } + + if wanted := 36000; mount.Config.MaxLeaseTTL != wanted { + return fmt.Errorf("max lease ttl is %v; wanted %v", mount.Description, wanted) + } + + return nil +} + +var testResourceMount_updateConfig = ` + +resource "vault_mount" "test" { + path = "example" + type = "generic" + description = "Example mount for testing" + default_lease_ttl_seconds = 7200 + max_lease_ttl_seconds = 72000 +} + +` + +func testResourceMount_updateCheck(s *terraform.State) error { + resourceState := s.Modules[0].Resources["vault_mount.test"] + instanceState := resourceState.Primary + + path := instanceState.ID + + if path != instanceState.Attributes["path"] { + return fmt.Errorf("id doesn't match path") + } + + if path != "example" { + return fmt.Errorf("unexpected path value") + } + + mount, err := findMount(path) + if err != nil { + return fmt.Errorf("error reading back mount: %s", err) + } + + if wanted := "Example mount for testing"; mount.Description != wanted { + return fmt.Errorf("description is %v; wanted %v", mount.Description, wanted) + } + + if wanted := "generic"; mount.Type != wanted { + return fmt.Errorf("type is %v; wanted %v", mount.Description, wanted) + } + + if wanted := 7200; mount.Config.DefaultLeaseTTL != wanted { + return fmt.Errorf("default lease ttl is %v; wanted %v", mount.Description, wanted) + } + + if wanted := 72000; mount.Config.MaxLeaseTTL != wanted { + return fmt.Errorf("max lease ttl is %v; wanted %v", mount.Description, wanted) + } + + return nil +} + +func findMount(path string) (*api.MountOutput, error) { + client := testProvider.Meta().(*api.Client) + + path = path + "/" + + mounts, err := client.Sys().ListMounts() + if err != nil { + return nil, err + } + + if mounts[path] != nil { + return mounts[path], nil + } + + return nil, fmt.Errorf("Unable to find mount %s in Vault; current list: %v", path, mounts) +} diff --git a/website/source/docs/providers/vault/r/mount.html.md b/website/source/docs/providers/vault/r/mount.html.md new file mode 100644 index 000000000..72114961a --- /dev/null +++ b/website/source/docs/providers/vault/r/mount.html.md @@ -0,0 +1,38 @@ +--- +layout: "vault" +page_title: "Vault: vault_mount resource" +sidebar_current: "docs-vault-resource-mount" +description: |- + Managing the mounting of secret backends in Vault +--- + +# vault\_mount + + +## Example Usage + +```hcl +resource "vault_mount" "example" { + path = "dummy" + type = "generic" + description = "This is an example mount" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `path` - (Required) Where the secret backend will be mounted + +* `type` - (Required) Type of the backend, such as "aws" + +* `description` - (Optional) Human-friendly description of the mount + +* `default_lease_ttl_seconds` - (Optional) Default lease duration for tokens and secrets in seconds + +* `max_lease_ttl_seconds` - (Optional) Maximum possible lease duration for tokens and secrets in seconds + +## Attributes Reference + +No additional attributes are exported by this resource. diff --git a/website/source/layouts/vault.erb b/website/source/layouts/vault.erb index e00dd71a6..35eaf2f43 100644 --- a/website/source/layouts/vault.erb +++ b/website/source/layouts/vault.erb @@ -32,6 +32,10 @@ vault_generic_secret + > + vault_mount + + > vault_policy