diff --git a/docs/resources/resource_group.md b/docs/resources/resource_group.md
new file mode 100644
index 00000000..e1b84a14
--- /dev/null
+++ b/docs/resources/resource_group.md
@@ -0,0 +1,62 @@
+---
+# generated by https://github.com/hashicorp/terraform-plugin-docs
+page_title: "trocco_resource_group Resource - trocco"
+subcategory: ""
+description: |-
+ Provides a TROCCO resource_group resource.
+---
+
+# trocco_resource_group (Resource)
+
+Provides a TROCCO resource_group resource.
+
+## Example Usage
+
+```terraform
+resource "trocco_resource_group" "example" {
+ name = "resource group name"
+ description = "description"
+ teams = [
+ {
+ team_id = 1
+ role = "administrator"
+ },
+ {
+ team_id = 2
+ role = "operator"
+ }
+ ]
+}
+```
+
+
+## Schema
+
+### Required
+
+- `name` (String) The name of the resource group.
+- `teams` (Attributes Set) The team roles of the resource group. (see [below for nested schema](#nestedatt--teams))
+
+### Optional
+
+- `description` (String) The description of the resource group.
+
+### Read-Only
+
+- `id` (Number) The ID of the resource group.
+
+
+### Nested Schema for `teams`
+
+Required:
+
+- `role` (String) The role of the team. Valid values are `administrator`, `editor`, `operator`, `viewer`.
+- `team_id` (Number) The team ID of the role.
+
+## Import
+
+Import is supported using the following syntax:
+
+```shell
+terraform import trocco_resource.import_resource_group
+```
diff --git a/examples/resources/trocco_resource_group/import.sh b/examples/resources/trocco_resource_group/import.sh
new file mode 100644
index 00000000..683e944f
--- /dev/null
+++ b/examples/resources/trocco_resource_group/import.sh
@@ -0,0 +1 @@
+terraform import trocco_resource.import_resource_group
diff --git a/examples/resources/trocco_resource_group/resource.tf b/examples/resources/trocco_resource_group/resource.tf
new file mode 100644
index 00000000..1c92789d
--- /dev/null
+++ b/examples/resources/trocco_resource_group/resource.tf
@@ -0,0 +1,15 @@
+resource "trocco_resource_group" "example" {
+ name = "resource group name"
+ description = "description"
+ teams = [
+ {
+ team_id = 1
+ role = "administrator"
+ },
+ {
+ team_id = 2
+ role = "operator"
+ }
+ ]
+}
+
diff --git a/internal/client/resource_group.go b/internal/client/resource_group.go
new file mode 100644
index 00000000..a61827b7
--- /dev/null
+++ b/internal/client/resource_group.go
@@ -0,0 +1,124 @@
+package client
+
+import (
+ "fmt"
+ "net/http"
+ "net/url"
+)
+
+const resourceGroupBasePath = "/api/resource_groups"
+
+type ResourceGroupWithTeams struct {
+ ID int64 `json:"id"`
+ Name string `json:"name"`
+ Description *string `json:"description"`
+ Teams []ResourceGroupPermission `json:"teams"`
+ CreatedAt string `json:"created_at"`
+ UpdatedAt string `json:"updated_at"`
+}
+
+type ResourceGroupPermission struct {
+ TeamID int64 `json:"team_id"`
+ Role string `json:"role"`
+}
+
+// List of ResourceGroups
+
+type ListResourceGroupInput struct {
+ limit *int
+ cursor *string
+}
+
+func (input *ListResourceGroupInput) SetLimit(limit int) {
+ input.limit = &limit
+}
+
+func (input *ListResourceGroupInput) SetCursor(cursor string) {
+ input.cursor = &cursor
+}
+
+type ListResourceGroupOutput struct {
+ Items []ResourceGroupWithTeams `json:"items"`
+ NextCursor *string `json:"next_cursor"`
+}
+
+const MaxListResourceGroupsLimit = 100
+
+func (client *TroccoClient) ListResourceGroups(input *ListResourceGroupInput) (*ListResourceGroupOutput, error) {
+ params := url.Values{}
+ if input != nil && input.limit != nil {
+ if *input.limit < 1 || *input.limit > MaxListResourceGroupsLimit {
+ return nil, fmt.Errorf("limit must be between 1 and %d", MaxListResourceGroupsLimit)
+ }
+ params.Add("limit", fmt.Sprintf("%d", *input.limit))
+ }
+ if input != nil && input.cursor != nil {
+ params.Add("cursor", *input.cursor)
+ }
+ path := fmt.Sprintf(resourceGroupBasePath+"?%s", params.Encode())
+ output := new(ListResourceGroupOutput)
+ err := client.do(http.MethodGet, path, nil, output)
+ if err != nil {
+ return nil, err
+ }
+ return output, nil
+}
+
+// Get a Team
+
+func (client *TroccoClient) GetResourceGroup(id int64) (*ResourceGroupWithTeams, error) {
+ path := fmt.Sprintf("%s/%d", resourceGroupBasePath, id)
+ output := new(ResourceGroupWithTeams)
+ err := client.do(http.MethodGet, path, nil, output)
+ if err != nil {
+ return nil, err
+ }
+ return output, nil
+}
+
+// Create
+
+type CreateResourceGroupInput struct {
+ Name string `json:"name"`
+ Description *string `json:"description,omitempty"`
+ Teams []TeamRoleInput `json:"teams"`
+}
+
+type TeamRoleInput struct {
+ TeamID int64 `json:"team_id"`
+ Role string `json:"role"`
+}
+
+func (client *TroccoClient) CreateResourceGroup(input *CreateResourceGroupInput) (*ResourceGroupWithTeams, error) {
+ output := new(ResourceGroupWithTeams)
+ err := client.do(http.MethodPost, resourceGroupBasePath, input, output)
+ if err != nil {
+ return nil, err
+ }
+ return output, nil
+}
+
+// Update
+
+type UpdateResourceGroupInput struct {
+ Name *string `json:"name"`
+ Description *string `json:"description,omitempty"`
+ Teams []TeamRoleInput `json:"teams"`
+}
+
+func (client *TroccoClient) UpdateResourceGroup(id int64, input *UpdateResourceGroupInput) (*ResourceGroupWithTeams, error) {
+ path := fmt.Sprintf("%s/%d", resourceGroupBasePath, id)
+ output := new(ResourceGroupWithTeams)
+ err := client.do(http.MethodPatch, path, input, output)
+ if err != nil {
+ return nil, err
+ }
+ return output, nil
+}
+
+// Delete
+
+func (client *TroccoClient) DeleteResourceGroup(id int64) error {
+ path := fmt.Sprintf("%s/%d", resourceGroupBasePath, id)
+ return client.do(http.MethodDelete, path, nil, nil)
+}
diff --git a/internal/client/resource_group_test.go b/internal/client/resource_group_test.go
new file mode 100644
index 00000000..85ffca0f
--- /dev/null
+++ b/internal/client/resource_group_test.go
@@ -0,0 +1,278 @@
+package client
+
+import (
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/samber/lo"
+ "github.com/stretchr/testify/assert"
+)
+
+// List Teams
+
+func TestListResourceGroup(t *testing.T) {
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ assert.Equal(t, "/api/resource_groups", r.URL.Path)
+ assert.Equal(t, http.MethodGet, r.Method)
+
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ resp := `
+ {
+ "items": [
+ {
+ "id": 1,
+ "name": "ResourceGroup 1",
+ "description": "ResourceGroup 1 description",
+ "created_at": "2023-10-16T18:24:51.806+09:00",
+ "updated_at": "2023-10-16T18:24:51.806+09:00"
+ },
+ {
+ "id": 2,
+ "name": "ResourceGroup 2",
+ "description": "ResourceGroup 2 description",
+ "created_at": "2023-10-16T18:24:51.806+09:00",
+ "updated_at": "2023-10-16T18:24:51.806+09:00"
+ }
+ ]
+ }
+ `
+ _, err := w.Write([]byte(resp))
+ assert.NoError(t, err)
+ }))
+ defer server.Close()
+
+ output, err := NewDevTroccoClient("1234567890", server.URL).ListResourceGroups(nil)
+
+ assert.NoError(t, err)
+ assert.Len(t, output.Items, 2)
+ assert.Equal(t, int64(1), output.Items[0].ID)
+ assert.Equal(t, "ResourceGroup 1", output.Items[0].Name)
+ assert.Equal(t, "ResourceGroup 1 description", *output.Items[0].Description)
+ assert.Equal(t, "2023-10-16T18:24:51.806+09:00", output.Items[0].CreatedAt)
+ assert.Equal(t, "2023-10-16T18:24:51.806+09:00", output.Items[0].UpdatedAt)
+ assert.Equal(t, int64(2), output.Items[1].ID)
+ assert.Equal(t, "ResourceGroup 2", output.Items[1].Name)
+ assert.Equal(t, "ResourceGroup 2 description", *output.Items[1].Description)
+ assert.Equal(t, "2023-10-16T18:24:51.806+09:00", output.Items[1].CreatedAt)
+ assert.Equal(t, "2023-10-16T18:24:51.806+09:00", output.Items[1].UpdatedAt)
+}
+
+func TestListResourceGroupsLimitAndCursor(t *testing.T) {
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ assert.Equal(t, "1", r.URL.Query().Get("limit"))
+ assert.Equal(t, "test_prev_cursor", r.URL.Query().Get("cursor"))
+
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ resp := `
+ {
+ "items": [
+ {
+ "id": 1,
+ "name": "ResourceGroup 1",
+ "description": "ResourceGroup 1 description",
+ "created_at": "2023-10-16T18:24:51.806+09:00",
+ "updated_at": "2023-10-16T18:24:51.806+09:00"
+ }
+ ],
+ "next_cursor": "test_next_cursor"
+ }
+ `
+ _, err := w.Write([]byte(resp))
+ assert.NoError(t, err)
+ }))
+ defer server.Close()
+
+ input := &ListResourceGroupInput{}
+ input.SetLimit(1)
+ input.SetCursor("test_prev_cursor")
+
+ output, err := NewDevTroccoClient("1234567890", server.URL).ListResourceGroups(input)
+
+ assert.NoError(t, err)
+ assert.Len(t, output.Items, 1)
+ assert.Equal(t, int64(1), output.Items[0].ID)
+ assert.Equal(t, "ResourceGroup 1", output.Items[0].Name)
+ assert.Equal(t, "ResourceGroup 1 description", *output.Items[0].Description)
+ assert.Equal(t, "2023-10-16T18:24:51.806+09:00", output.Items[0].CreatedAt)
+ assert.Equal(t, "2023-10-16T18:24:51.806+09:00", output.Items[0].UpdatedAt)
+ assert.Equal(t, "test_next_cursor", *output.NextCursor)
+}
+
+// Get Team
+
+func TestGetResourceGroup(t *testing.T) {
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ assert.Equal(t, "/api/resource_groups/1", r.URL.Path)
+ assert.Equal(t, http.MethodGet, r.Method)
+
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ resp := `
+ {
+ "id": 1,
+ "name": "ResourceGroup 1",
+ "description": "ResourceGroup 1 description",
+ "created_at": "2023-10-16T18:24:51.806+09:00",
+ "updated_at": "2023-10-16T18:24:51.806+09:00",
+ "teams": [
+ {
+ "team_id": 1,
+ "role": "administrator"
+ },
+ {
+ "team_id": 2,
+ "role": "operator"
+ }
+ ]
+ }
+ `
+ _, err := w.Write([]byte(resp))
+ assert.NoError(t, err)
+ }))
+ defer server.Close()
+
+ output, err := NewDevTroccoClient("1234567890", server.URL).GetResourceGroup(1)
+
+ assert.NoError(t, err)
+ assert.Equal(t, int64(1), output.ID)
+ assert.Equal(t, "ResourceGroup 1", output.Name)
+ assert.Equal(t, "ResourceGroup 1 description", *output.Description)
+ assert.Equal(t, "2023-10-16T18:24:51.806+09:00", output.CreatedAt)
+ assert.Equal(t, "2023-10-16T18:24:51.806+09:00", output.UpdatedAt)
+ assert.Len(t, output.Teams, 2)
+ assert.Equal(t, int64(1), output.Teams[0].TeamID)
+ assert.Equal(t, "administrator", output.Teams[0].Role)
+ assert.Equal(t, int64(2), output.Teams[1].TeamID)
+ assert.Equal(t, "operator", output.Teams[1].Role)
+}
+
+// Create Team
+
+func TestCreateResourceGroup(t *testing.T) {
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ assert.Equal(t, "/api/resource_groups", r.URL.Path)
+ assert.Equal(t, http.MethodPost, r.Method)
+
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusCreated)
+
+ resp := `
+ {
+ "id": 1,
+ "name": "ResourceGroup",
+ "description": "description",
+ "created_at": "2023-10-16T18:24:51.806+09:00",
+ "updated_at": "2023-10-16T18:24:51.806+09:00",
+ "teams": [
+ {
+ "team_id": 1,
+ "role": "administrator"
+ }
+ ]
+ }
+ `
+ _, err := w.Write([]byte(resp))
+ assert.NoError(t, err)
+ }))
+ defer server.Close()
+
+ input := &CreateResourceGroupInput{
+ Name: "ResourceGroup",
+ Description: lo.ToPtr("description"),
+ Teams: []TeamRoleInput{
+ {TeamID: 1, Role: "administrator"},
+ },
+ }
+
+ output, err := NewDevTroccoClient("1234567890", server.URL).CreateResourceGroup(input)
+
+ assert.NoError(t, err)
+ assert.Equal(t, int64(1), output.ID)
+ assert.Equal(t, "ResourceGroup", output.Name)
+ assert.Equal(t, "description", *output.Description)
+ assert.Equal(t, "2023-10-16T18:24:51.806+09:00", output.CreatedAt)
+ assert.Equal(t, "2023-10-16T18:24:51.806+09:00", output.UpdatedAt)
+ assert.Len(t, output.Teams, 1)
+ assert.Equal(t, int64(1), output.Teams[0].TeamID)
+ assert.Equal(t, "administrator", output.Teams[0].Role)
+}
+
+// Update Team
+
+func TestUpdateResourceGroup(t *testing.T) {
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ assert.Equal(t, "/api/resource_groups/1", r.URL.Path)
+ assert.Equal(t, http.MethodPatch, r.Method)
+
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ resp := `
+ {
+ "id": 1,
+ "name": "ResourceGroup",
+ "description": "description",
+ "created_at": "2023-10-16T18:24:51.806+09:00",
+ "updated_at": "2023-10-16T18:24:51.806+09:00",
+ "teams": [
+ {
+ "team_id": 1,
+ "role": "administrator"
+ },
+ {
+ "team_id": 2,
+ "role": "operator"
+ }
+ ]
+ }
+ `
+ _, err := w.Write([]byte(resp))
+ assert.NoError(t, err)
+ }))
+ defer server.Close()
+
+ input := &UpdateResourceGroupInput{
+ Name: lo.ToPtr("ResourceGroup"),
+ Description: lo.ToPtr("description"),
+ Teams: []TeamRoleInput{
+ {TeamID: 1, Role: "administrator"},
+ {TeamID: 2, Role: "operator"},
+ },
+ }
+
+ output, err := NewDevTroccoClient("1234567890", server.URL).UpdateResourceGroup(1, input)
+
+ assert.NoError(t, err)
+ assert.Equal(t, int64(1), output.ID)
+ assert.Equal(t, "ResourceGroup", output.Name)
+ assert.Equal(t, "description", *output.Description)
+ assert.Equal(t, "2023-10-16T18:24:51.806+09:00", output.CreatedAt)
+ assert.Equal(t, "2023-10-16T18:24:51.806+09:00", output.UpdatedAt)
+ assert.Len(t, output.Teams, 2)
+ assert.Equal(t, int64(1), output.Teams[0].TeamID)
+ assert.Equal(t, "administrator", output.Teams[0].Role)
+ assert.Equal(t, int64(2), output.Teams[1].TeamID)
+ assert.Equal(t, "operator", output.Teams[1].Role)
+}
+
+// Delete Team
+
+func TestDeleteResourceGroup(t *testing.T) {
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ assert.Equal(t, "/api/resource_groups/1", r.URL.Path)
+ assert.Equal(t, http.MethodDelete, r.Method)
+
+ w.WriteHeader(http.StatusNoContent)
+ }))
+ defer server.Close()
+
+ err := NewDevTroccoClient("1234567890", server.URL).DeleteResourceGroup(1)
+
+ assert.NoError(t, err)
+}
diff --git a/internal/provider/model/resource_group/resource_group.go b/internal/provider/model/resource_group/resource_group.go
new file mode 100644
index 00000000..d0ea6a6b
--- /dev/null
+++ b/internal/provider/model/resource_group/resource_group.go
@@ -0,0 +1,17 @@
+package resource_group
+
+import (
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+type ResourceGroupResourceModel struct {
+ ID types.Int64 `tfsdk:"id"`
+ Name types.String `tfsdk:"name"`
+ Description types.String `tfsdk:"description"`
+ Teams []TeamRoleResourceModel `tfsdk:"teams"`
+}
+
+type TeamRoleResourceModel struct {
+ TeamID types.Int64 `tfsdk:"team_id"`
+ Role types.String `tfsdk:"role"`
+}
diff --git a/internal/provider/provider.go b/internal/provider/provider.go
index a8d274ae..b8b44753 100644
--- a/internal/provider/provider.go
+++ b/internal/provider/provider.go
@@ -135,6 +135,7 @@ func (p *TroccoProvider) Resources(ctx context.Context) []func() resource.Resour
NewPipelineDefinitionResource,
NewJobDefinitionResource,
NewTeamResource,
+ NewResourceGroupResource,
}
}
diff --git a/internal/provider/resource_group_resource.go b/internal/provider/resource_group_resource.go
new file mode 100644
index 00000000..da58c889
--- /dev/null
+++ b/internal/provider/resource_group_resource.go
@@ -0,0 +1,256 @@
+package provider
+
+import (
+ "context"
+ "fmt"
+ "strconv"
+ "terraform-provider-trocco/internal/client"
+ model "terraform-provider-trocco/internal/provider/model/resource_group"
+ troccoValidator "terraform-provider-trocco/internal/provider/validator"
+
+ "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/int64planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/schema/validator"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+var (
+ _ resource.Resource = &resourceGroupResource{}
+ _ resource.ResourceWithConfigure = &resourceGroupResource{}
+ _ resource.ResourceWithImportState = &resourceGroupResource{}
+)
+
+func NewResourceGroupResource() resource.Resource {
+ return &resourceGroupResource{}
+}
+
+type resourceGroupResource struct {
+ client *client.TroccoClient
+}
+
+func (r *resourceGroupResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_resource_group"
+}
+
+func (r *resourceGroupResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
+ if req.ProviderData == nil {
+ return
+ }
+
+ client, ok := req.ProviderData.(*client.TroccoClient)
+ if !ok {
+ resp.Diagnostics.AddError(
+ "Unexpected Resource Configure Type",
+ fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
+ )
+ return
+ }
+
+ r.client = client
+}
+
+func (r *resourceGroupResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
+ resp.Schema = schema.Schema{
+ MarkdownDescription: "Provides a TROCCO resource_group resource.",
+ Attributes: map[string]schema.Attribute{
+ "id": schema.Int64Attribute{
+ Computed: true,
+ PlanModifiers: []planmodifier.Int64{
+ int64planmodifier.UseStateForUnknown(),
+ },
+ MarkdownDescription: "The ID of the resource group.",
+ },
+ "name": schema.StringAttribute{
+ Required: true,
+ MarkdownDescription: "The name of the resource group.",
+ Validators: []validator.String{
+ stringvalidator.UTF8LengthAtLeast(1),
+ stringvalidator.UTF8LengthAtMost(255),
+ },
+ },
+ "description": schema.StringAttribute{
+ Optional: true,
+ Computed: true,
+ MarkdownDescription: "The description of the resource group.",
+ Default: stringdefault.StaticString(""),
+ },
+ "teams": schema.SetNestedAttribute{
+ Required: true,
+ MarkdownDescription: "The team roles of the resource group.",
+ Validators: []validator.Set{
+ troccoValidator.UniqueTeamValidator{},
+ },
+ NestedObject: schema.NestedAttributeObject{
+ Attributes: map[string]schema.Attribute{
+ "team_id": schema.Int64Attribute{
+ Required: true,
+ MarkdownDescription: "The team ID of the role.",
+ },
+ "role": schema.StringAttribute{
+ Required: true,
+ Validators: []validator.String{
+ stringvalidator.OneOf("administrator", "editor", "operator", "viewer"),
+ },
+ MarkdownDescription: "The role of the team. Valid values are `administrator`, `editor`, `operator`, `viewer`.",
+ },
+ },
+ },
+ },
+ },
+ }
+}
+
+func (r *resourceGroupResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var plan model.ResourceGroupResourceModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ input := client.CreateResourceGroupInput{
+ Name: plan.Name.ValueString(),
+ Description: plan.Description.ValueStringPointer(),
+ Teams: []client.TeamRoleInput{},
+ }
+ for _, m := range plan.Teams {
+ input.Teams = append(input.Teams, client.TeamRoleInput{
+ TeamID: m.TeamID.ValueInt64(),
+ Role: m.Role.ValueString(),
+ })
+
+ }
+
+ resourceGroup, err := r.client.CreateResourceGroup(&input)
+ if err != nil {
+ resp.Diagnostics.AddError(
+ "Creating resource group",
+ fmt.Sprintf("Unable to create resource group, got error: %s", err),
+ )
+ return
+ }
+
+ newState := model.ResourceGroupResourceModel{
+ ID: types.Int64Value(resourceGroup.ID),
+ Name: types.StringValue(resourceGroup.Name),
+ Description: types.StringPointerValue(resourceGroup.Description),
+ Teams: []model.TeamRoleResourceModel{},
+ }
+ for _, m := range resourceGroup.Teams {
+ newState.Teams = append(newState.Teams, model.TeamRoleResourceModel{
+ TeamID: types.Int64Value(m.TeamID),
+ Role: types.StringValue(m.Role),
+ })
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, newState)...)
+}
+
+func (r *resourceGroupResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state model.ResourceGroupResourceModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resourceGroup, err := r.client.GetResourceGroup(state.ID.ValueInt64())
+ if err != nil {
+ resp.Diagnostics.AddError(
+ "Reading resource group",
+ fmt.Sprintf("Unable to read resource group, got error: %s", err),
+ )
+ return
+ }
+
+ newState := model.ResourceGroupResourceModel{
+ ID: types.Int64Value(resourceGroup.ID),
+ Name: types.StringValue(resourceGroup.Name),
+ Description: types.StringPointerValue(resourceGroup.Description),
+ Teams: []model.TeamRoleResourceModel{},
+ }
+ for _, m := range resourceGroup.Teams {
+ newState.Teams = append(newState.Teams, model.TeamRoleResourceModel{
+ TeamID: types.Int64Value(m.TeamID),
+ Role: types.StringValue(m.Role),
+ })
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, newState)...)
+}
+func (r *resourceGroupResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan, state model.ResourceGroupResourceModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ input := client.UpdateResourceGroupInput{
+ Name: plan.Name.ValueStringPointer(),
+ Description: plan.Description.ValueStringPointer(),
+ Teams: []client.TeamRoleInput{},
+ }
+ for _, m := range plan.Teams {
+ input.Teams = append(input.Teams, client.TeamRoleInput{
+ TeamID: m.TeamID.ValueInt64(),
+ Role: m.Role.ValueString(),
+ })
+ }
+
+ resourceGroup, err := r.client.UpdateResourceGroup(state.ID.ValueInt64(), &input)
+ if err != nil {
+ resp.Diagnostics.AddError(
+ "Updating team",
+ fmt.Sprintf("Unable to update team, got error: %s", err),
+ )
+ return
+ }
+
+ newState := model.ResourceGroupResourceModel{
+ ID: types.Int64Value(resourceGroup.ID),
+ Name: types.StringValue(resourceGroup.Name),
+ Description: types.StringPointerValue(resourceGroup.Description),
+ Teams: []model.TeamRoleResourceModel{},
+ }
+ for _, m := range resourceGroup.Teams {
+ newState.Teams = append(newState.Teams, model.TeamRoleResourceModel{
+ TeamID: types.Int64Value(m.TeamID),
+ Role: types.StringValue(m.Role),
+ })
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, newState)...)
+}
+func (r *resourceGroupResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state model.ResourceGroupResourceModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ err := r.client.DeleteResourceGroup(state.ID.ValueInt64())
+ if err != nil {
+ resp.Diagnostics.AddError(
+ "Deleting resource group",
+ fmt.Sprintf("Unable to delete resource group, got error: %s", err),
+ )
+ return
+ }
+}
+
+func (r *resourceGroupResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ id, err := strconv.ParseInt(req.ID, 10, 64)
+ if err != nil {
+ resp.Diagnostics.AddError(
+ "Importing resource group",
+ fmt.Sprintf("Unable to parse id, got error: %s", err),
+ )
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), id)...)
+}
diff --git a/internal/provider/resource_group_resource_test.go b/internal/provider/resource_group_resource_test.go
new file mode 100644
index 00000000..3f9dfce8
--- /dev/null
+++ b/internal/provider/resource_group_resource_test.go
@@ -0,0 +1,117 @@
+package provider
+
+import (
+ "regexp"
+ "testing"
+
+ "github.com/hashicorp/terraform-plugin-testing/helper/resource"
+)
+
+func TestAccResourceGroupResource(t *testing.T) {
+ resource.Test(t, resource.TestCase{
+ ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
+ Steps: []resource.TestStep{
+ // Create and Read testing
+ {
+ Config: providerConfig + `
+ resource "trocco_resource_group" "test" {
+ name = "test"
+ description = "test"
+ teams = [
+ {
+ team_id = 1
+ role = "operator"
+ }
+ ]
+ }
+ `,
+ Check: resource.ComposeAggregateTestCheckFunc(
+ resource.TestCheckResourceAttr("trocco_resource_group.test", "name", "test"),
+ resource.TestCheckResourceAttr("trocco_resource_group.test", "description", "test"),
+ resource.TestCheckResourceAttr("trocco_resource_group.test", "teams.#", "1"),
+ resource.TestCheckResourceAttr("trocco_resource_group.test", "teams.0.team_id", "1"),
+ resource.TestCheckResourceAttr("trocco_resource_group.test", "teams.0.role", "operator"),
+ ),
+ },
+ // ImportState testing
+ {
+ ResourceName: "trocco_resource_group.test",
+ ImportState: true,
+ ImportStateVerify: true,
+ ImportStateVerifyIgnore: []string{"id"},
+ },
+ // Update testing
+ {
+ Config: providerConfig + `
+ resource "trocco_resource_group" "test" {
+ name = "updated"
+ description = "updated"
+ teams = [
+ {
+ team_id = 1
+ role = "administrator"
+ },
+ {
+ team_id = 2
+ role = "operator"
+ }
+ ]
+ }
+ `,
+ Check: resource.ComposeAggregateTestCheckFunc(
+ resource.TestCheckResourceAttr("trocco_resource_group.test", "name", "updated"),
+ resource.TestCheckResourceAttr("trocco_resource_group.test", "description", "updated"),
+ resource.TestCheckResourceAttr("trocco_resource_group.test", "teams.#", "2"),
+ resource.TestCheckResourceAttr("trocco_resource_group.test", "teams.0.team_id", "1"),
+ resource.TestCheckResourceAttr("trocco_resource_group.test", "teams.0.role", "administrator"),
+ resource.TestCheckResourceAttr("trocco_resource_group.test", "teams.1.team_id", "2"),
+ resource.TestCheckResourceAttr("trocco_resource_group.test", "teams.1.role", "operator"),
+ ),
+ },
+ },
+ })
+}
+
+func TestAccResourceGroupNoTeams(t *testing.T) {
+ resource.Test(t, resource.TestCase{
+ ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
+ Steps: []resource.TestStep{
+ {
+ Config: providerConfig + `
+ resource "trocco_resource_group" "test" {
+ name = "test"
+ description = "test"
+ teams = []
+ }
+ `,
+ Check: resource.ComposeAggregateTestCheckFunc(
+ resource.TestCheckResourceAttr("trocco_resource_group.test", "name", "test"),
+ resource.TestCheckResourceAttr("trocco_resource_group.test", "description", "test"),
+ resource.TestCheckResourceAttr("trocco_resource_group.test", "teams.#", "0"),
+ ),
+ },
+ },
+ })
+}
+
+func TestAccResourceGroupDuplicateRoles(t *testing.T) {
+ resource.Test(t, resource.TestCase{
+ ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
+ Steps: []resource.TestStep{
+ {
+ Config: providerConfig + `
+ resource "trocco_resource_group" "test" {
+ name = "test"
+ description = "test"
+ teams = [
+ { team_id = 1, role = "administrator" },
+ { team_id = 1, role = "operator" },
+ ]
+ }
+ `,
+ ExpectError: regexp.MustCompile(`Team ID "1" is duplicated in the list.`),
+ },
+ },
+ })
+
+}
diff --git a/internal/provider/validator/unique_team_validator.go b/internal/provider/validator/unique_team_validator.go
new file mode 100644
index 00000000..8e328f38
--- /dev/null
+++ b/internal/provider/validator/unique_team_validator.go
@@ -0,0 +1,46 @@
+package validator
+
+import (
+ "context"
+ "fmt"
+
+ model "terraform-provider-trocco/internal/provider/model/resource_group"
+
+ "github.com/hashicorp/terraform-plugin-framework/schema/validator"
+)
+
+type UniqueTeamValidator struct{}
+
+func (v UniqueTeamValidator) Description(ctx context.Context) string {
+ return "Ensures that team IDs are unique within the set."
+}
+
+func (v UniqueTeamValidator) MarkdownDescription(ctx context.Context) string {
+ return v.Description(ctx)
+}
+
+func (v UniqueTeamValidator) ValidateSet(ctx context.Context, request validator.SetRequest, response *validator.SetResponse) {
+ var teamIDs []model.TeamRoleResourceModel
+ diags := request.ConfigValue.ElementsAs(ctx, &teamIDs, false)
+ if diags.HasError() {
+ response.Diagnostics.Append(diags...)
+ return
+ }
+
+ for i, teamI := range teamIDs {
+ for j, teamJ := range teamIDs {
+ if i == j {
+ continue
+ }
+
+ if teamI.TeamID == teamJ.TeamID {
+ response.Diagnostics.AddAttributeError(
+ request.Path,
+ "Duplicate Team ID",
+ fmt.Sprintf("Team ID %q is duplicated in the list.", teamI.TeamID),
+ )
+ return
+ }
+ }
+ }
+}