Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Group Resource #772

Merged
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_project" "example" {
display_name = "example-group"
description = "My new group!"
}
```

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

### Required

- `display_name` (String) The group's display_name.
squaresurf marked this conversation as resolved.
Show resolved Hide resolved

### Optional

- `description` (String) The group's description
squaresurf marked this conversation as resolved.
Show resolved Hide resolved

### 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>` or `<name>`
squaresurf marked this conversation as resolved.
Show resolved Hide resolved

## 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_project" "example" {
JolisaBrownHashiCorp marked this conversation as resolved.
Show resolved Hide resolved
display_name = "example-group"
description = "My new group!"
}
228 changes: 228 additions & 0 deletions internal/provider/iam/resource_group.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
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"
"github.com/hashicorp/terraform-provider-hcp/internal/hcpvalidator"
)

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` or `%s`",
"iam/organization/<organization_id>/group/<name>", "<name>"),
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"display_name": schema.StringAttribute{
Required: true,
Description: "The group's display_name.",
Validators: []validator.String{
hcpvalidator.ResourceNamePart(),
JolisaBrownHashiCorp marked this conversation as resolved.
Show resolved Hide resolved
stringvalidator.LengthBetween(3, 90),
},
},
"description": schema.StringAttribute{
Description: "The group's description ",
Optional: true,
Validators: []validator.String{
hcpvalidator.ResourceNamePart(),
squaresurf marked this conversation as resolved.
Show resolved Hide resolved
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()
JolisaBrownHashiCorp marked this conversation as resolved.
Show resolved Hide resolved
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()
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()
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) {
JolisaBrownHashiCorp marked this conversation as resolved.
Show resolved Hide resolved
var state Group
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}

deleteParams := groups_service.NewGroupsServiceDeleteGroupParams()
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