diff --git a/vault/provider.go b/vault/provider.go index 22148df94..13eec003b 100644 --- a/vault/provider.go +++ b/vault/provider.go @@ -276,6 +276,10 @@ var ( Resource: consulSecretBackendResource(), PathInventory: []string{"/consul/config/access"}, }, + "vault_consul_secret_backend_role": { + Resource: consulSecretBackendRoleResource(), + PathInventory: []string{"/consul/roles/{name}"}, + }, "vault_database_secret_backend_connection": { Resource: databaseSecretBackendConnectionResource(), PathInventory: []string{"/database/config/{name}"}, diff --git a/vault/resource_consul_secret_backend_role.go b/vault/resource_consul_secret_backend_role.go new file mode 100644 index 000000000..15924a90a --- /dev/null +++ b/vault/resource_consul_secret_backend_role.go @@ -0,0 +1,170 @@ +package vault + +import ( + "fmt" + "log" + "strings" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/vault/api" +) + +func consulSecretBackendRoleResource() *schema.Resource { + return &schema.Resource{ + Create: consulSecretBackendRoleCreate, + Read: consulSecretBackendRoleRead, + Update: consulSecretBackendRoleUpdate, + Delete: consulSecretBackendRoleDelete, + Exists: consulSecretBackendRoleExists, + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "The name of an existing role against which to create this Consul credential", + }, + "path": { + Type: schema.TypeString, + ForceNew: true, + Required: true, + Description: "Unique name of the Vault Consul mount to configure", + }, + "policies": { + Type: schema.TypeList, + Required: true, + Description: "List of Consul policies to associate with this role", + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + }, + } +} + +func consulSecretBackendRoleCreate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*api.Client) + + name := d.Get("name").(string) + path := d.Get("path").(string) + policies := d.Get("policies").([]interface{}) + + reqPath := consulSecretBackendRolePath(path, name) + + payload := map[string]interface{}{ + "policies": policies, + } + + d.Partial(true) + log.Printf("[DEBUG] Configuring Consul secrets backend role at %q", reqPath) + + d.SetId(path + "," + name) + + if _, err := client.Logical().Write(reqPath, payload); err != nil { + return fmt.Errorf("error writing role configuration for %q: %s", reqPath, err) + } + d.SetPartial("name") + d.SetPartial("path") + d.SetPartial("policies") + d.Partial(false) + + return nil +} + +func consulSecretBackendRoleRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*api.Client) + + s := strings.Split(d.Id(), ",") + path := s[0] + name := s[1] + + reqPath := consulSecretBackendRolePath(path, name) + + log.Printf("[DEBUG] Reading Consul secrets backend role at %q", reqPath) + + secret, err := client.Logical().Read(reqPath) + if err != nil { + return fmt.Errorf("error reading role configuration for %q: %s", reqPath, err) + } + + if secret == nil { + return fmt.Errorf("resource not found") + } + + data := secret.Data + d.Set("name", name) + d.Set("path", path) + d.Set("policies", data["policies"]) + + return nil +} + +func consulSecretBackendRoleUpdate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*api.Client) + + s := strings.Split(d.Id(), ",") + path := s[0] + name := s[1] + + reqPath := consulSecretBackendRolePath(path, name) + + d.Partial(true) + + if d.HasChange("policies") { + log.Printf("[DEBUG] Updating role configuration at %q", reqPath) + policies := d.Get("policies").([]interface{}) + + payload := map[string]interface{}{ + "policies": policies, + } + if _, err := client.Logical().Write(reqPath, payload); err != nil { + return fmt.Errorf("error writing role configuration for %q: %s", reqPath, err) + } + log.Printf("[DEBUG] Updated role configuration at %q", reqPath) + d.SetPartial("policies") + } + + d.Partial(false) + return consulSecretBackendRoleRead(d, meta) +} + +func consulSecretBackendRoleDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*api.Client) + + s := strings.Split(d.Id(), ",") + path := s[0] + name := s[1] + + reqPath := consulSecretBackendRolePath(path, name) + + log.Printf("[DEBUG] Deleting Consul backend role at %q", reqPath) + + if _, err := client.Logical().Delete(reqPath); err != nil { + return fmt.Errorf("error deleting Consul backend role at %q: %s", reqPath, err) + } + log.Printf("[DEBUG] Deleted Consul backend role at %q", reqPath) + return nil +} + +func consulSecretBackendRoleExists(d *schema.ResourceData, meta interface{}) (bool, error) { + client := meta.(*api.Client) + + s := strings.Split(d.Id(), ",") + path := s[0] + name := s[1] + + reqPath := consulSecretBackendRolePath(path, name) + + log.Printf("[DEBUG] Checking Consul secrets backend role at %q", reqPath) + + secret, err := client.Logical().Read(reqPath) + if err != nil { + return false, fmt.Errorf("error reading role configuration for %q: %s", reqPath, err) + } + + return secret != nil, nil +} + +func consulSecretBackendRolePath(path, name string) string { + return strings.Trim(path, "/") + "/roles/" + name +} diff --git a/vault/resource_consul_secret_backend_role_test.go b/vault/resource_consul_secret_backend_role_test.go new file mode 100644 index 000000000..13cb4c675 --- /dev/null +++ b/vault/resource_consul_secret_backend_role_test.go @@ -0,0 +1,109 @@ +package vault + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/hashicorp/vault/api" +) + +func TestConsulSecretBackendRole(t *testing.T) { + path := acctest.RandomWithPrefix("tf-test-path") + name := acctest.RandomWithPrefix("tf-test-name") + token := "026a0c16-87cd-4c2d-b3f3-fb539f592b7e" + resource.Test(t, resource.TestCase{ + Providers: testProviders, + PreCheck: func() { testAccPreCheck(t) }, + CheckDestroy: testAccConsulSecretBackendRoleCheckDestroy(path, name), + Steps: []resource.TestStep{ + { + Config: testConsulSecretBackendRole_initialConfig(path, name, token), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("vault_consul_secret_backend_role.test", "path", path), + resource.TestCheckResourceAttr("vault_consul_secret_backend_role.test", "name", name), + resource.TestCheckResourceAttr("vault_consul_secret_backend_role.test", "policies.0", "foo"), + ), + }, + { + Config: testConsulSecretBackendRole_updateConfig(path, name, token), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("vault_consul_secret_backend_role.test", "path", path), + resource.TestCheckResourceAttr("vault_consul_secret_backend_role.test", "name", name), + resource.TestCheckResourceAttr("vault_consul_secret_backend_role.test", "policies.0", "foo"), + resource.TestCheckResourceAttr("vault_consul_secret_backend_role.test", "policies.1", "bar"), + ), + }, + }, + }) +} + +func testAccConsulSecretBackendRoleCheckDestroy(path, name string) func(*terraform.State) error { + return func(s *terraform.State) error { + client := testProvider.Meta().(*api.Client) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "vault_consul_secret_backend_role" { + continue + } + + reqPath := consulSecretBackendRolePath(path, name) + + secret, err := client.Logical().Read(reqPath) + if err != nil { + return err + } + + if secret != nil { + return fmt.Errorf("Role %q still exists", reqPath) + } + } + + return nil + } +} + +func testConsulSecretBackendRole_initialConfig(path, name, token string) string { + return fmt.Sprintf(` +resource "vault_consul_secret_backend" "test" { + path = "%s" + description = "test description" + default_lease_ttl_seconds = 3600 + max_lease_ttl_seconds = 86400 + address = "127.0.0.1:8500" + token = "%s" +} + +resource "vault_consul_secret_backend_role" "test" { + path = "${vault_consul_secret_backend.test.path}" + name = "%s" + + policies = [ + "foo" + ] +}`, path, token, name) +} + +func testConsulSecretBackendRole_updateConfig(path, name, token string) string { + return fmt.Sprintf(` +resource "vault_consul_secret_backend" "test" { + path = "%s" + description = "test description" + default_lease_ttl_seconds = 3600 + max_lease_ttl_seconds = 86400 + address = "127.0.0.1:8500" + token = "%s" +} + +resource "vault_consul_secret_backend_role" "test" { + path = "${vault_consul_secret_backend.test.path}" + name = "%s" + + policies = [ + "foo", + "bar", + ] +}`, path, token, name) +} diff --git a/website/docs/r/consul_secret_backend_role.html.md b/website/docs/r/consul_secret_backend_role.html.md new file mode 100644 index 000000000..39b96fd26 --- /dev/null +++ b/website/docs/r/consul_secret_backend_role.html.md @@ -0,0 +1,46 @@ +--- +layout: "vault" +page_title: "Vault: vault_consul_secret_backend_role resource" +sidebar_current: "docs-vault-resource-consul-secret-backend-role" +description: |- + Manages a Consul secrets role for a Consul secrets engine in Vault. +--- + +# vault\_consul\_secret\_backend\_role + +Manages a Consul secrets role for a Consul secrets engine in Vault. Consul secret backends can then issue Consul tokens. + +## Example Usage + +```hcl +resource "vault_consul_secret_backend" "test" { + path = "consul" + description = "Manages the Consul backend" + + address = "127.0.0.1:8500" + token = "4240861b-ce3d-8530-115a-521ff070dd29" +} + +resource vault_consul_secret_backend_role" "example" { + name = "test-role" + path = "${vault_consul_secret_backend.test.path}" + + policies = [ + "example-policy", + ] +} +``` + +## Argument Reference + +The following arguments are supported: + +* `path` - (Required) The unique name of an existing Consul secrets backend mount. Must not begin or end with a `/`. + +* `name` - (Required) The name of the Consul secrets engine role to create. + +* `policies` - (Required) The list of Consul ACL policies to associate with these roles. + +## Attributes Reference + +No additional attributes are exported by this resource.