Skip to content

Commit

Permalink
Add Group Resource (#772)
Browse files Browse the repository at this point in the history
* define group resource

* group resource implementation

* group resource test coverage

* syntax, validation tweaks

* add docs - group resource

* add docs - group data source

* revise schema constraints

* cleanup docs/comments

* add pre-destroy check to test coverage/update docs

* create group before fetching in datasource test coverage
  • Loading branch information
JolisaBrownHashiCorp committed Mar 13, 2024
1 parent 1dc1c26 commit 7ad89d1
Show file tree
Hide file tree
Showing 11 changed files with 503 additions and 23 deletions.
31 changes: 31 additions & 0 deletions docs/data-sources/group.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
page_title: "hcp_group Data Source - terraform-provider-hcp"
subcategory: "Cloud Platform"
description: |-
The group data source retrieves the given group.
---

# hcp_group (Data Source)

The group data source retrieves the given group.

## Example Usage

```terraform
data "hcp_group" "example" {
resource_name = var.resource_name
}
```

<!-- schema generated by tfplugindocs -->
## Schema

### Required

- `resource_name` (String) The group's resource name in format `iam/organization/<organization_id>/group/<resource_name>` or shortened `<resource_name>`

### Read-Only

- `description` (String) The group's description
- `display_name` (String) The group's display name
- `resource_id` (String) The group's unique identifier
47 changes: 47 additions & 0 deletions docs/resources/group.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
---
page_title: "Resource hcp_group - terraform-provider-hcp"
subcategory: "Cloud Platform"
description: |-
The group resource manages a HCP Group.
The user or service account that is running Terraform when creating an hcp_group resource must have roles/admin on the parent resource; either the project or organization.
---

# hcp_group (Resource)

The group resource manages a HCP Group.

The user or service account that is running Terraform when creating an `hcp_group` resource must have `roles/admin` on the parent resource; either the project or organization.

## Example Usage

```terraform
resource "hcp_group" "example" {
display_name = "example-group"
description = "My new group!"
}
```

<!-- schema generated by tfplugindocs -->
## Schema

### Required

- `display_name` (String) The group's display_name - maximum length of 50 characters

### Optional

- `description` (String) The group's description - maximum length of 300 characters

### Read-Only

- `resource_id` (String) The group's unique identifier
- `resource_name` (String) The group's resource name in the format `iam/organization/<organization_id>/group/<name>`

## Import

Import is supported using the following syntax:

```shell
# Group can be imported by specifying the group display name
terraform import hcp_group.example my-test-group
```
3 changes: 3 additions & 0 deletions examples/data-sources/hcp_group/data-source.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
data "hcp_group" "example" {
resource_name = var.resource_name
}
2 changes: 2 additions & 0 deletions examples/resources/hcp_group/import.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Group can be imported by specifying the group display name
terraform import hcp_group.example my-test-group
4 changes: 4 additions & 0 deletions examples/resources/hcp_group/resource.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
resource "hcp_group" "example" {
display_name = "example-group"
description = "My new group!"
}
34 changes: 11 additions & 23 deletions internal/provider/iam/data_group_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,17 @@ import (
"github.com/hashicorp/terraform-provider-hcp/internal/provider/acctest"
)

// TODO : update the below tests to use a created group resource via Terraform
func TestAccGroupDataSource(t *testing.T) {
dataSourceAddress := "data.hcp_group.test"
groupName := acctest.RandString(16)
description := acctest.RandString(64)
resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories,
PreCheck: func() { acctest.PreCheck(t) },
CheckDestroy: testAccCheckGroupDestroy(t, groupName),
Steps: []resource.TestStep{
{
Config: testAccGroupConfig("int-tooling-e2e-test-group"),
Config: testAccGroupConfig(groupName, description),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttrSet(dataSourceAddress, "resource_id"),
resource.TestCheckResourceAttrSet(dataSourceAddress, "description"),
Expand All @@ -27,28 +29,14 @@ func TestAccGroupDataSource(t *testing.T) {
})
}

func TestAccGroupDataSourceFullResourceName(t *testing.T) {
dataSourceAddress := "data.hcp_group.test"
resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories,
PreCheck: func() { acctest.PreCheck(t) },
Steps: []resource.TestStep{
{
Config: testAccGroupConfig("iam/organization/d11d7309-5072-44f9-aaea-c8f37c09a8b5/group/int-tooling-e2e-test-group"),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttrSet(dataSourceAddress, "resource_id"),
resource.TestCheckResourceAttrSet(dataSourceAddress, "description"),
resource.TestCheckResourceAttrSet(dataSourceAddress, "display_name"),
),
},
},
})
}

func testAccGroupConfig(resourceName string) string {
func testAccGroupConfig(name, description string) string {
return fmt.Sprintf(`
resource "hcp_group" "test" {
display_name = %q
description = %q
}
data "hcp_group" "test" {
resource_name = %q
resource_name = hcp_group.test.resource_name
}
`, resourceName)
`, name, description)
}
225 changes: 225 additions & 0 deletions internal/provider/iam/resource_group.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
package iam

import (
"context"
"errors"
"fmt"
"net/http"
"strings"

"github.com/hashicorp/hcp-sdk-go/clients/cloud-iam/stable/2019-12-10/client/groups_service"
"github.com/hashicorp/hcp-sdk-go/clients/cloud-iam/stable/2019-12-10/models"
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types"
clients "github.com/hashicorp/terraform-provider-hcp/internal/clients"
)

func NewGroupResource() resource.Resource {
return &resourceGroup{}
}

type resourceGroup struct {
client *clients.Client
}

func (r *resourceGroup) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_group"
}

func (r *resourceGroup) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: fmt.Sprintf(`The group resource manages a HCP Group.
The user or service account that is running Terraform when creating an %s resource must have %s on the parent resource; either the project or organization.`,
"`hcp_group`", "`roles/admin`"),
Attributes: map[string]schema.Attribute{
"resource_id": schema.StringAttribute{
Computed: true,
Description: "The group's unique identifier",
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"resource_name": schema.StringAttribute{
Computed: true,
Description: fmt.Sprintf("The group's resource name in the format `%s`",
"iam/organization/<organization_id>/group/<name>"),
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"display_name": schema.StringAttribute{
Required: true,
Description: "The group's display_name - maximum length of 50 characters",
Validators: []validator.String{
stringvalidator.LengthBetween(1, 50),
},
},
"description": schema.StringAttribute{
Description: "The group's description - maximum length of 300 characters",
Optional: true,
Validators: []validator.String{
stringvalidator.LengthBetween(0, 300),
},
},
},
}
}

func (r *resourceGroup) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
client, ok := req.ProviderData.(*clients.Client)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Data Source Configure Type",
fmt.Sprintf("Expected *clients.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
)
return
}
r.client = client
}

type Group struct {
ResourceID types.String `tfsdk:"resource_id"`
ResourceName types.String `tfsdk:"resource_name"`
DisplayName types.String `tfsdk:"display_name"`
Description types.String `tfsdk:"description"`
}

func (r *resourceGroup) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var plan Group
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
if resp.Diagnostics.HasError() {
return
}

orgID := r.client.Config.OrganizationID
parent := fmt.Sprintf("organization/%s", orgID)

createParams := groups_service.NewGroupsServiceCreateGroupParams().WithContext(ctx)
createParams.ParentResourceName = parent
createParams.Body = groups_service.GroupsServiceCreateGroupBody{
Name: plan.DisplayName.ValueString(),
Description: plan.Description.ValueString(),
}

res, err := r.client.Groups.GroupsServiceCreateGroup(createParams, nil)

if err != nil {
resp.Diagnostics.AddError("Error creating group", err.Error())
return
}

group := res.GetPayload().Group

plan.ResourceID = types.StringValue(group.ResourceID)
plan.ResourceName = types.StringValue(group.ResourceName)
plan.DisplayName = types.StringValue(group.DisplayName)
plan.Description = types.StringValue(group.Description)
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)

}

func (r *resourceGroup) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var state Group
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}
getParams := groups_service.NewGroupsServiceGetGroupParams().WithContext(ctx)
getParams.ResourceName = state.ResourceName.ValueString()
res, err := r.client.Groups.GroupsServiceGetGroup(getParams, nil)

if err != nil {
var getErr *groups_service.GroupsServiceGetGroupDefault
if errors.As(err, &getErr) && getErr.IsCode(http.StatusNotFound) {
resp.State.RemoveResource(ctx)
return
}

resp.Diagnostics.AddError("Error retrieving group", err.Error())
return
}

group := res.GetPayload().Group

state.ResourceID = types.StringValue(group.ResourceID)
state.ResourceName = types.StringValue(group.ResourceName)
state.DisplayName = types.StringValue(group.DisplayName)
state.Description = types.StringValue(group.Description)

resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
}

func (r *resourceGroup) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var plan, state Group
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}

updateParams := groups_service.NewGroupsServiceUpdateGroupParams().WithContext(ctx)
updateParams.ResourceName = state.ResourceName.ValueString()
updateParams.Body = groups_service.GroupsServiceUpdateGroupBody{Group: &models.HashicorpCloudIamGroup{ResourceName: state.ResourceName.ValueString(), ResourceID: state.ResourceID.ValueString()}}
paths := []string{}

// Check if the display name was updated
if !plan.DisplayName.Equal(state.DisplayName) {
updateParams.Body.Group.DisplayName = plan.DisplayName.ValueString()
paths = append(paths, "displayName")
}

// Check if the description was updated
if !plan.Description.Equal(state.Description) {
updateParams.Body.Group.Description = plan.Description.ValueString()
paths = append(paths, "description")
}

mask := strings.Join(paths, `,`)
updateParams.Body.UpdateMask = mask
_, err := r.client.Groups.GroupsServiceUpdateGroup(updateParams, nil)

if err != nil {
resp.Diagnostics.AddError("Error updating group", err.Error())
return
}

// Store the updated values
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
}

func (r *resourceGroup) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var state Group
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}

deleteParams := groups_service.NewGroupsServiceDeleteGroupParams().WithContext(ctx)
deleteParams.ResourceName = state.ResourceName.ValueString()
_, err := r.client.Groups.GroupsServiceDeleteGroup(deleteParams, nil)

if err != nil {
var getErr *groups_service.GroupsServiceDeleteGroupDefault
if errors.As(err, &getErr) && getErr.IsCode(http.StatusNotFound) {
resp.State.RemoveResource(ctx)
return
}

resp.Diagnostics.AddError("Error deleting group", err.Error())
return
}
}

func (r *resourceGroup) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
resource.ImportStatePassthroughID(ctx, path.Root("resource_name"), req, resp)
}
Loading

0 comments on commit 7ad89d1

Please sign in to comment.