Skip to content

Commit

Permalink
Implement the consul_acl_role_policy_attachment resource (#362)
Browse files Browse the repository at this point in the history
Closes #354
  • Loading branch information
remilapeyre authored Oct 24, 2023
1 parent f55cfd9 commit 4154012
Show file tree
Hide file tree
Showing 6 changed files with 367 additions and 8 deletions.
141 changes: 141 additions & 0 deletions consul/resource_consul_acl_role_policy_attachment.go
Original file line number Diff line number Diff line change
@@ -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
}
149 changes: 149 additions & 0 deletions consul/resource_consul_acl_role_policy_attachment_test.go
Original file line number Diff line number Diff line change
@@ -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
}`
17 changes: 9 additions & 8 deletions consul/resource_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
53 changes: 53 additions & 0 deletions docs/resources/acl_role_policy_attachment.md
Original file line number Diff line number Diff line change
@@ -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 generated by tfplugindocs -->
## 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
```
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
terraform import consul_acl_role_policy_attachment.my_role_read_policy 624d94ca-bc5c-f960-4e83-0a609cf588be:policy_name
Loading

0 comments on commit 4154012

Please sign in to comment.