diff --git a/consul/resource_consul_acl_role_policy_attachment.go b/consul/resource_consul_acl_role_policy_attachment.go new file mode 100644 index 00000000..cfd756d2 --- /dev/null +++ b/consul/resource_consul_acl_role_policy_attachment.go @@ -0,0 +1,141 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package consul + +import ( + "fmt" + "strings" + + consulapi "github.com/hashicorp/consul/api" + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" +) + +func resourceConsulACLRolePolicyAttachment() *schema.Resource { + return &schema.Resource{ + Create: resourceConsulACLRolePolicyAttachmentCreate, + Read: resourceConsulACLRolePolicyAttachmentRead, + Delete: resourceConsulACLRolePolicyAttachmentDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Description: "The `consul_acl_role_policy_attachment` resource links a Consul ACL role and an ACL policy. The link is implemented through an update to the Consul ACL role.\n\n~> **NOTE:** This resource is only useful to attach policies to an ACL role that has been created outside the current Terraform configuration. If the ACL role you need to attach a policy to has been created in the current Terraform configuration and will only be used in it, you should use the `policies` attribute of [`consul_acl_role`](/docs/providers/consul/r/acl_role.html).", + + Schema: map[string]*schema.Schema{ + "role_id": { + Type: schema.TypeString, + ForceNew: true, + Required: true, + Description: "The id of the role.", + }, + "policy": { + Type: schema.TypeString, + ForceNew: true, + Required: true, + Description: "The policy name.", + }, + }, + } +} + +func resourceConsulACLRolePolicyAttachmentCreate(d *schema.ResourceData, meta interface{}) error { + client, qOpts, wOpts := getClient(d, meta) + + roleID := d.Get("role_id").(string) + + role, _, err := client.ACL().RoleRead(roleID, qOpts) + if err != nil { + return fmt.Errorf("role '%s' not found", roleID) + } + + newPolicyName := d.Get("policy").(string) + for _, iPolicy := range role.Policies { + if iPolicy.Name == newPolicyName { + return fmt.Errorf("policy '%s' already attached to role", newPolicyName) + } + } + + role.Policies = append(role.Policies, &consulapi.ACLRolePolicyLink{ + Name: newPolicyName, + }) + + _, _, err = client.ACL().RoleUpdate(role, wOpts) + if err != nil { + return fmt.Errorf("error updating role '%q' to set new policy attachment: '%s'", roleID, err) + } + + id := fmt.Sprintf("%s:%s", roleID, newPolicyName) + + d.SetId(id) + + return resourceConsulACLRolePolicyAttachmentRead(d, meta) +} + +func resourceConsulACLRolePolicyAttachmentRead(d *schema.ResourceData, meta interface{}) error { + client, qOpts, _ := getClient(d, meta) + + id := d.Id() + + roleID, policyName, err := parseTwoPartID(id, "role", "policy") + if err != nil { + return fmt.Errorf("invalid role policy attachment id '%q'", id) + } + + role, _, err := client.ACL().RoleRead(roleID, qOpts) + if err != nil { + if strings.Contains(err.Error(), "role not found") { + d.SetId("") + return nil + } + return fmt.Errorf("failed to read token '%s': %v", id, err) + } + + policyFound := false + for _, iPolicy := range role.Policies { + if iPolicy.Name == policyName { + policyFound = true + break + } + } + if !policyFound { + d.SetId("") + return nil + } + + sw := newStateWriter(d) + sw.set("role_id", roleID) + sw.set("policy", policyName) + + return sw.error() +} + +func resourceConsulACLRolePolicyAttachmentDelete(d *schema.ResourceData, meta interface{}) error { + client, qOpts, wOpts := getClient(d, meta) + + id := d.Id() + + roleID, policyName, err := parseTwoPartID(id, "role", "policy") + if err != nil { + return fmt.Errorf("invalid role policy attachment id '%q'", id) + } + + role, _, err := client.ACL().RoleRead(roleID, qOpts) + if err != nil { + return fmt.Errorf("role '%s' not found", roleID) + } + + for i, iPolicy := range role.Policies { + if iPolicy.Name == policyName { + role.Policies = append(role.Policies[:i], role.Policies[i+1:]...) + break + } + } + + _, _, err = client.ACL().RoleUpdate(role, wOpts) + if err != nil { + return fmt.Errorf("error updating role '%q' to remove policy attachment: '%s'", roleID, err) + } + + return nil +} diff --git a/consul/resource_consul_acl_role_policy_attachment_test.go b/consul/resource_consul_acl_role_policy_attachment_test.go new file mode 100644 index 00000000..9735ec86 --- /dev/null +++ b/consul/resource_consul_acl_role_policy_attachment_test.go @@ -0,0 +1,149 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package consul + +import ( + "fmt" + "testing" + + consulapi "github.com/hashicorp/consul/api" + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/terraform" +) + +func testAccCheckConsulACLRolePolicyAttachmentDestroy(client *consulapi.Client) func(s *terraform.State) error { + return func(s *terraform.State) error { + for _, rs := range s.RootModule().Resources { + if rs.Type != "consul_acl_role_policy_attachment" { + continue + } + roleID, policyName, err := parseTwoPartID(rs.Primary.ID, "role", "policy") + if err != nil { + return fmt.Errorf("Invalid role policy attachment id '%q'", rs.Primary.ID) + } + role, _, _ := client.ACL().RoleRead(roleID, nil) + if role != nil { + for _, iPolicy := range role.Policies { + if iPolicy.Name == policyName { + return fmt.Errorf("role policy attachment %q still exists", rs.Primary.ID) + } + } + } + } + return nil + } + +} + +func testAccCheckRolePolicyID(client *consulapi.Client) func(s *terraform.State) error { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources["consul_acl_role.test_role"] + if !ok { + return fmt.Errorf("Not Found: consul_acl_role.test_role") + } + + roleID := rs.Primary.Attributes["id"] + if roleID == "" { + return fmt.Errorf("No token ID is set") + } + + _, _, err := client.ACL().RoleRead(roleID, nil) + if err != nil { + return fmt.Errorf("Unable to retrieve role %q", roleID) + } + + // Make sure the policy has then same role_id + rs, ok = s.RootModule().Resources["consul_acl_role_policy_attachment.test"] + if !ok { + return fmt.Errorf("Not Found: consul_acl_role_policy_attachment.test") + } + + policyTokenID := rs.Primary.Attributes["role_id"] + if policyTokenID == "" { + return fmt.Errorf("No policy role_id is set") + } + + if policyTokenID != roleID { + return fmt.Errorf("%s != %s", policyTokenID, roleID) + } + + return nil + } +} + +func TestAccConsulACLRolePolicyAttachment_basic(t *testing.T) { + providers, client := startTestServer(t) + + resource.Test(t, resource.TestCase{ + Providers: providers, + CheckDestroy: testAccCheckConsulACLRolePolicyAttachmentDestroy(client), + Steps: []resource.TestStep{ + { + Config: testResourceACLRolePolicyAttachmentConfigBasic, + Check: resource.ComposeTestCheckFunc( + testAccCheckRolePolicyID(client), + resource.TestCheckResourceAttr("consul_acl_role_policy_attachment.test", "policy", "test-attachment"), + ), + }, + { + Config: testResourceACLRolePolicyAttachmentConfigUpdate, + Check: resource.ComposeTestCheckFunc( + testAccCheckRolePolicyID(client), + resource.TestCheckResourceAttr("consul_acl_role_policy_attachment.test", "policy", "test2"), + ), + }, + { + Config: testResourceACLRolePolicyAttachmentConfigUpdate, + }, + { + Config: testResourceACLRolePolicyAttachmentConfigUpdate, + ResourceName: "consul_acl_role_policy_attachment.test", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +const testResourceACLRolePolicyAttachmentConfigBasic = ` +resource "consul_acl_policy" "test_policy" { + name = "test-attachment" + rules = "node \"\" { policy = \"read\" }" + datacenters = [ "dc1" ] +} + +resource "consul_acl_role" "test_role" { + name = "test" + + lifecycle { + ignore_changes = ["policies"] + } +} + +resource "consul_acl_role_policy_attachment" "test" { + role_id = consul_acl_role.test_role.id + policy = consul_acl_policy.test_policy.name +} +` + +const testResourceACLRolePolicyAttachmentConfigUpdate = ` +// Using another resource to force the update of consul_acl_role +resource "consul_acl_policy" "test2" { + name = "test2" + rules = "node \"\" { policy = \"read\" }" + datacenters = [ "dc1" ] +} + +resource "consul_acl_role" "test_role" { + name = "test" + + lifecycle { + ignore_changes = ["policies"] + } +} + +resource "consul_acl_role_policy_attachment" "test" { + role_id = consul_acl_role.test_role.id + policy = consul_acl_policy.test2.name +}` diff --git a/consul/resource_provider.go b/consul/resource_provider.go index 3fc9b62b..3f3f1720 100644 --- a/consul/resource_provider.go +++ b/consul/resource_provider.go @@ -228,29 +228,30 @@ func Provider() terraform.ResourceProvider { "consul_acl_auth_method": resourceConsulACLAuthMethod(), "consul_acl_binding_rule": resourceConsulACLBindingRule(), "consul_acl_policy": resourceConsulACLPolicy(), + "consul_acl_role_policy_attachment": resourceConsulACLRolePolicyAttachment(), "consul_acl_role": resourceConsulACLRole(), - "consul_acl_token": resourceConsulACLToken(), "consul_acl_token_policy_attachment": resourceConsulACLTokenPolicyAttachment(), "consul_acl_token_role_attachment": resourceConsulACLTokenRoleAttachment(), + "consul_acl_token": resourceConsulACLToken(), "consul_admin_partition": resourceConsulAdminPartition(), "consul_agent_service": resourceConsulAgentService(), + "consul_autopilot_config": resourceConsulAutopilotConfig(), "consul_catalog_entry": resourceConsulCatalogEntry(), "consul_certificate_authority": resourceConsulCertificateAuthority(), "consul_config_entry": resourceConsulConfigEntry(), - "consul_keys": resourceConsulKeys(), + "consul_intention": resourceConsulIntention(), "consul_key_prefix": resourceConsulKeyPrefix(), + "consul_keys": resourceConsulKeys(), "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(), - "consul_service": resourceConsulService(), - "consul_intention": resourceConsulIntention(), + "consul_namespace": resourceConsulNamespace(), "consul_network_area": resourceConsulNetworkArea(), + "consul_node": resourceConsulNode(), "consul_peering_token": resourceSourceConsulPeeringToken(), "consul_peering": resourceSourceConsulPeering(), + "consul_prepared_query": resourceConsulPreparedQuery(), + "consul_service": resourceConsulService(), }, ConfigureFunc: providerConfigure, diff --git a/docs/resources/acl_role_policy_attachment.md b/docs/resources/acl_role_policy_attachment.md new file mode 100644 index 00000000..52611eac --- /dev/null +++ b/docs/resources/acl_role_policy_attachment.md @@ -0,0 +1,53 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "consul_acl_role_policy_attachment Resource - terraform-provider-consul" +subcategory: "" +description: |- + The consul_acl_role_policy_attachment resource links a Consul ACL role and an ACL policy. The link is implemented through an update to the Consul ACL role. + ~> NOTE: This resource is only useful to attach policies to an ACL role that has been created outside the current Terraform configuration. If the ACL role you need to attach a policy to has been created in the current Terraform configuration and will only be used in it, you should use the policies attribute of consul_acl_role. +--- + +# consul_acl_role_policy_attachment (Resource) + +The `consul_acl_role_policy_attachment` resource links a Consul ACL role and an ACL policy. The link is implemented through an update to the Consul ACL role. + +~> **NOTE:** This resource is only useful to attach policies to an ACL role that has been created outside the current Terraform configuration. If the ACL role you need to attach a policy to has been created in the current Terraform configuration and will only be used in it, you should use the `policies` attribute of [`consul_acl_role`](/docs/providers/consul/r/acl_role.html). + +## Example Usage + +```terraform +data "consul_acl_role" "my_role" { + name = "my_role" +} + +resource "consul_acl_policy" "read_policy" { + name = "read-policy" + rules = "node \"\" { policy = \"read\" }" + datacenters = ["dc1"] +} + +resource "consul_acl_role_policy_attachment" "my_role_read_policy" { + role_id = data.consul_acl_role.test.id + policy = consul_acl_policy.read_policy.name +} +``` + + +## Schema + +### Required + +- `policy` (String) The policy name. +- `role_id` (String) The id of the role. + +### Read-Only + +- `id` (String) The ID of this resource. + +## Import + +Import is supported using the following syntax: + +```shell +terraform import consul_acl_role_policy_attachment.my_role_read_policy 624d94ca-bc5c-f960-4e83-0a609cf588be:policy_name +``` diff --git a/examples/resources/consul_acl_role_policy_attachment/import.sh b/examples/resources/consul_acl_role_policy_attachment/import.sh new file mode 100644 index 00000000..2d8bb620 --- /dev/null +++ b/examples/resources/consul_acl_role_policy_attachment/import.sh @@ -0,0 +1 @@ +terraform import consul_acl_role_policy_attachment.my_role_read_policy 624d94ca-bc5c-f960-4e83-0a609cf588be:policy_name diff --git a/examples/resources/consul_acl_role_policy_attachment/resource.tf b/examples/resources/consul_acl_role_policy_attachment/resource.tf new file mode 100644 index 00000000..57922ebc --- /dev/null +++ b/examples/resources/consul_acl_role_policy_attachment/resource.tf @@ -0,0 +1,14 @@ +data "consul_acl_role" "my_role" { + name = "my_role" +} + +resource "consul_acl_policy" "read_policy" { + name = "read-policy" + rules = "node \"\" { policy = \"read\" }" + datacenters = ["dc1"] +} + +resource "consul_acl_role_policy_attachment" "my_role_read_policy" { + role_id = data.consul_acl_role.test.id + policy = consul_acl_policy.read_policy.name +}