diff --git a/consul/resource_consul_namespace_policy_attachment.go b/consul/resource_consul_namespace_policy_attachment.go index 384c35e9..abf26c22 100644 --- a/consul/resource_consul_namespace_policy_attachment.go +++ b/consul/resource_consul_namespace_policy_attachment.go @@ -37,16 +37,13 @@ func resourceConsulNamespacePolicyAttachmentCreate(d *schema.ResourceData, meta client, qOpts, wOpts := getClient(d, meta) name := d.Get("namespace").(string) - namespace, _, err := client.Namespaces().Read(name, qOpts) - if err != nil { - return fmt.Errorf("Failed to read namespace %q: %s", name, err) - } + policy := d.Get("policy").(string) - if namespace == nil { - return fmt.Errorf("Namespace %q not found", name) + namespace, err := findNamespace(client, qOpts, name) + if err != nil { + return err } - policy := d.Get("policy").(string) for _, p := range namespace.ACLs.PolicyDefaults { if p.Name == policy { return fmt.Errorf("Policy %q already attached to the namespace", policy) @@ -59,7 +56,7 @@ func resourceConsulNamespacePolicyAttachmentCreate(d *schema.ResourceData, meta _, _, err = client.Namespaces().Update(namespace, wOpts) if err != nil { - return fmt.Errorf("Failed to update namespace %q to attach policy %q", name, policy) + return fmt.Errorf("Failed to update namespace %q to attach policy %q: %s", name, policy, err) } d.SetId(fmt.Sprintf("%s:%s", name, policy)) @@ -76,6 +73,9 @@ func resourceConsulNamespacePolicyAttachmentRead(d *schema.ResourceData, meta in } namespace, _, err := client.Namespaces().Read(name, qOpts) + if err != nil { + return fmt.Errorf("Failed to read namespace %q: %s", name, err) + } if namespace == nil { d.SetId("") return nil @@ -111,12 +111,9 @@ func resourceConsulNamespacePolicyAttachmentDelete(d *schema.ResourceData, meta return err } - namespace, _, err := client.Namespaces().Read(name, qOpts) + namespace, err := findNamespace(client, qOpts, name) if err != nil { - return fmt.Errorf("Failed to get namespace %q: %s", name, err) - } - if namespace == nil { - return fmt.Errorf("Namespace %q not found", name) + return err } for i, p := range namespace.ACLs.PolicyDefaults { diff --git a/consul/resource_consul_namespace_role_attachment.go b/consul/resource_consul_namespace_role_attachment.go new file mode 100644 index 00000000..1ff4dee0 --- /dev/null +++ b/consul/resource_consul_namespace_role_attachment.go @@ -0,0 +1,148 @@ +package consul + +import ( + "fmt" + + consulapi "github.com/hashicorp/consul/api" + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" +) + +func resourceConsulNamespaceRoleAttachment() *schema.Resource { + return &schema.Resource{ + Create: resourceConsulNamespaceRoleAttachmentCreate, + Read: resourceConsulNamespaceRoleAttachmentRead, + Delete: resourceConsulNamespaceRoleAttachmentDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "namespace": { + Type: schema.TypeString, + ForceNew: true, + Required: true, + Description: "The namespace to attach the role to.", + }, + "role": { + Type: schema.TypeString, + ForceNew: true, + Required: true, + Description: "The role name.", + }, + }, + } +} + +func resourceConsulNamespaceRoleAttachmentCreate(d *schema.ResourceData, meta interface{}) error { + client, qOpts, wOpts := getClient(d, meta) + + name := d.Get("namespace").(string) + role := d.Get("role").(string) + + namespace, err := findNamespace(client, qOpts, name) + if err != nil { + return err + } + + for _, r := range namespace.ACLs.RoleDefaults { + if r.Name == role { + return fmt.Errorf("Role %q already attached to the namespace", role) + } + } + + namespace.ACLs.RoleDefaults = append(namespace.ACLs.RoleDefaults, consulapi.ACLLink{ + Name: role, + }) + + _, _, err = client.Namespaces().Update(namespace, wOpts) + if err != nil { + return fmt.Errorf("Failed to update namespace %q to attach role %q: %s", name, role, err) + } + + d.SetId(fmt.Sprintf("%s:%s", name, role)) + + return resourceConsulNamespaceRoleAttachmentRead(d, meta) +} + +func resourceConsulNamespaceRoleAttachmentRead(d *schema.ResourceData, meta interface{}) error { + client, qOpts, _ := getClient(d, meta) + + name, role, err := parseTwoPartID(d.Id(), "namespace", "role") + if err != nil { + return err + } + + namespace, _, err := client.Namespaces().Read(name, qOpts) + if err != nil { + return fmt.Errorf("Failed to read namespace %q: %s", name, err) + } + if namespace == nil { + d.SetId("") + return nil + } + + var found bool + for _, l := range namespace.ACLs.RoleDefaults { + if l.Name == role { + found = true + break + } + } + + if !found { + d.SetId("") + return nil + } + + if err = d.Set("namespace", name); err != nil { + return fmt.Errorf("Failed to set 'namespace': %s", err) + } + if err = d.Set("role", role); err != nil { + return fmt.Errorf("Failed to set 'role': %s", err) + } + + return nil +} + +func resourceConsulNamespaceRoleAttachmentDelete(d *schema.ResourceData, meta interface{}) error { + client, qOpts, wOpts := getClient(d, meta) + name, role, err := parseTwoPartID(d.Id(), "namespace", "role") + if err != nil { + return err + } + + namespace, err := findNamespace(client, qOpts, name) + if err != nil { + return err + } + + for i, p := range namespace.ACLs.RoleDefaults { + if p.Name == role { + namespace.ACLs.RoleDefaults = append( + namespace.ACLs.RoleDefaults[:i], + namespace.ACLs.RoleDefaults[i+1:]..., + ) + break + } + } + + _, _, err = client.Namespaces().Update(namespace, wOpts) + if err != nil { + return fmt.Errorf("Failed to remove role %q from namespace %q", role, name) + } + + return nil +} + +func findNamespace(client *consulapi.Client, qOpts *consulapi.QueryOptions, name string) (*consulapi.Namespace, error) { + namespace, _, err := client.Namespaces().Read(name, qOpts) + if err != nil { + return nil, fmt.Errorf("Failed to read namespace %q: %s", name, err) + } + + if namespace == nil { + return nil, fmt.Errorf("Namespace %q not found", name) + } + + return namespace, nil +} diff --git a/consul/resource_consul_namespace_role_attachment_test.go b/consul/resource_consul_namespace_role_attachment_test.go new file mode 100644 index 00000000..06e0ffa0 --- /dev/null +++ b/consul/resource_consul_namespace_role_attachment_test.go @@ -0,0 +1,108 @@ +package consul + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/terraform" +) + +func TestAccConsulNamespaceRoleAttachment(t *testing.T) { + testRole := func(name string) func(*terraform.State) error { + return func(s *terraform.State) error { + client := getTestClient(testAccProvider.Meta()) + namespace, _, err := client.Namespaces().Read("testroleattachment", nil) + if err != nil { + return fmt.Errorf("failed to read namespace testroleattachment: %s", err) + } + if namespace == nil { + return fmt.Errorf("namespace testroleattachment not found") + } + if len(namespace.ACLs.RoleDefaults) != 1 { + return fmt.Errorf("wrong number of roles: %d", len(namespace.ACLs.RoleDefaults)) + } + if namespace.ACLs.RoleDefaults[0].Name != name { + return fmt.Errorf("wrong role, expected %q, found %q", name, namespace.ACLs.RoleDefaults[0].Name) + } + return nil + } + } + + resource.Test(t, resource.TestCase{ + Providers: testAccProviders, + PreCheck: func() { skipTestOnConsulCommunityEdition(t) }, + Steps: []resource.TestStep{ + { + Config: testResourceNamespaceRoleConfig, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("consul_namespace_role_attachment.test", "namespace", "testroleattachment"), + resource.TestCheckResourceAttr("consul_namespace_role_attachment.test", "role", "role"), + testRole("role"), + ), + }, + { + Config: testResourceNamespaceRoleConfigUpdate, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("consul_namespace_role_attachment.test", "namespace", "testroleattachment"), + resource.TestCheckResourceAttr("consul_namespace_role_attachment.test", "role", "role2"), + testRole("role2"), + ), + }, + { + Config: testResourceNamespaceRoleConfigUpdate, + }, + { + ImportState: true, + ImportStateVerify: true, + ResourceName: "consul_namespace_role_attachment.test", + }, + }, + }) +} + +const testResourceNamespaceRoleConfig = ` +resource "consul_namespace" "test" { + name = "testroleattachment" + + lifecycle { + ignore_changes = [role_defaults] + } +} + +resource "consul_acl_role" "test" { + name = "role" + + service_identities { + service_name = "foo" + } +} + +resource "consul_namespace_role_attachment" "test" { + namespace = consul_namespace.test.name + role = consul_acl_role.test.name +} +` + +const testResourceNamespaceRoleConfigUpdate = ` +resource "consul_namespace" "test" { + name = "testroleattachment" + + lifecycle { + ignore_changes = [role_defaults] + } +} + +resource "consul_acl_role" "test2" { + name = "role2" + + service_identities { + service_name = "foo" + } +} + +resource "consul_namespace_role_attachment" "test" { + namespace = consul_namespace.test.name + role = consul_acl_role.test2.name +} +` diff --git a/consul/resource_provider.go b/consul/resource_provider.go index 31008d89..b0de1016 100644 --- a/consul/resource_provider.go +++ b/consul/resource_provider.go @@ -175,6 +175,7 @@ func Provider() terraform.ResourceProvider { "consul_license": resourceConsulLicense(), "consul_namespace": resourceConsulNamespace(), "consul_namespace_policy_attachment": resourceConsulNamespacePolicyAttachment(), + "consul_namespace_role_attachment": resourceConsulNamespaceRoleAttachment(), "consul_node": resourceConsulNode(), "consul_prepared_query": resourceConsulPreparedQuery(), "consul_autopilot_config": resourceConsulAutopilotConfig(), diff --git a/website/consul.erb b/website/consul.erb index 6747fe38..64dd3532 100644 --- a/website/consul.erb +++ b/website/consul.erb @@ -155,6 +155,10 @@ namespace_policy_attachment + > + namespace_role_attachment + + > consul_network_area diff --git a/website/docs/r/namespace_role_attachment.html.markdown b/website/docs/r/namespace_role_attachment.html.markdown new file mode 100644 index 00000000..dfdb22f1 --- /dev/null +++ b/website/docs/r/namespace_role_attachment.html.markdown @@ -0,0 +1,91 @@ +--- +layout: "consul" +page_title: "Consul: consul_namespace_role_attachment" +sidebar_current: "docs-consul-resource-namespace-role-attachment" +description: |- + Allows Terraform to add a role as a default for a namespace +--- + +# consul_namespace_role_attachment + +~> **NOTE:** This feature requires Consul Enterprise. + +The `consul_namespace_role_attachment` resource links a Consul Namespace and an ACL +role. The link is implemented through an update to the Consul Namespace. + +~> **NOTE:** This resource is only useful to attach roles to a namespace +that has been created outside the current Terraform configuration, like the +`default` namespace. If the namespace you need to attach a role to has +been created in the current Terraform configuration and will only be used in it, +you should use the `role_defaults` attribute of [`consul_namespace`](/docs/providers/consul/r/namespace.html). + +## Example Usage + +### Attach a role to the default namespace + +```hcl +resource "consul_acl_role" "agent" { + name = "agent" +} + +resource "consul_namespace_role_attachment" "attachment" { + namespace = "default" + role = consul_acl_role.agent.name +} +``` + +### Attach a role to a namespace created in another Terraform configuration + +#### In `first_configuration/main.tf` + +```hcl +resource "consul_namespace" "qa" { + name = "qa" + + lifecycle { + ignore_changes = [role_defaults] + } +} +``` + +#### In `second_configuration/main.tf` + +```hcl +resource "consul_acl_role" "agent" { + name = "agent" +} + +resource "consul_namespace_role_attachment" "attachment" { + namespace = "qa" + role = consul_acl_role.agent.name +} +``` +**NOTE**: consul_acl_namespace would attempt to enforce an empty set of default +roles, because its `role_defaults` attribute is empty. For this reason it +is necessary to add the lifecycle clause to prevent Terraform from attempting to +empty the set of policies associated to the namespace. + +## Argument Reference + +The following arguments are supported: + +* `namespace` - (Required) The namespace to attach the role to. +* `role` - (Required) The name of the role attached to the namespace. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The attachment ID. +* `namespace` - The name of the namespace. +* `role` - The name of the role attached to the namespace. + + +## Import + +`consul_namespace_role_attachment` can be imported. This is especially useful +to manage the policies attached to the `default` namespace: + +``` +$ terraform import consul_namespace_role_attachment.default default:role_name +```