Skip to content

Add role_assignment resource and admin_role_id to space resource #105

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

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .changes/unreleased/Added-20250807-074304.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
kind: Added
body: Add role_assignment resource for assigning roles to teams in spaces
time: 2025-08-07T07:43:04.398571665Z
3 changes: 3 additions & 0 deletions .changes/unreleased/Added-20250807-074320.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
kind: Added
body: Add admin_role_id output to space resource and data source
time: 2025-08-07T07:43:20.435902554Z
2 changes: 2 additions & 0 deletions internal/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"github.com/labd/terraform-provider-contentful/internal/resources/locale"
"github.com/labd/terraform-provider-contentful/internal/resources/preview_environment"
"github.com/labd/terraform-provider-contentful/internal/resources/role"
"github.com/labd/terraform-provider-contentful/internal/resources/role_assignment"
"github.com/labd/terraform-provider-contentful/internal/resources/space"
"github.com/labd/terraform-provider-contentful/internal/resources/webhook"
"github.com/labd/terraform-provider-contentful/internal/utils"
Expand Down Expand Up @@ -160,6 +161,7 @@ func (c contentfulProvider) Resources(_ context.Context) []func() resource.Resou
locale.NewLocaleResource,
preview_environment.NewPreviewEnvironmentResource,
role.NewRoleResource,
role_assignment.NewRoleAssignmentResource,
space.NewSpaceResource,
webhook.NewWebhookResource,
}
Expand Down
43 changes: 43 additions & 0 deletions internal/resources/role_assignment/models.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package role_assignment

import (
"github.com/hashicorp/terraform-plugin-framework/types"
)

// RoleAssignment is the main resource schema data
type RoleAssignment struct {
ID types.String `tfsdk:"id"`
Version types.Int64 `tfsdk:"version"`
SpaceID types.String `tfsdk:"space_id"`
TeamID types.String `tfsdk:"team_id"`
RoleID types.String `tfsdk:"role_id"`
IsAdmin types.Bool `tfsdk:"is_admin"`
}

// Import populates the RoleAssignment struct from an SDK object
// Note: This is a placeholder - actual API endpoints are needed
func (r *RoleAssignment) Import(assignment interface{}) {
// TODO: Implement when API endpoints become available
// This would populate from actual role assignment API response
}

// DraftForCreate creates a request object for creating a new role assignment
// Note: This is a placeholder - actual API endpoints are needed
func (r *RoleAssignment) DraftForCreate() interface{} {
// TODO: Implement when API endpoints become available
// This would create the appropriate request body for role assignment creation
return map[string]interface{}{
"spaceId": r.SpaceID.ValueString(),
"teamId": r.TeamID.ValueString(),
"roleId": r.RoleID.ValueString(),
"isAdmin": r.IsAdmin.ValueBool(),
}
}

// DraftForUpdate creates a request object for updating an existing role assignment
// Note: This is a placeholder - actual API endpoints are needed
func (r *RoleAssignment) DraftForUpdate() interface{} {
// TODO: Implement when API endpoints become available
// This would create the appropriate request body for role assignment updates
return r.DraftForCreate()
}
206 changes: 206 additions & 0 deletions internal/resources/role_assignment/resource.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
package role_assignment

import (
"context"
"fmt"
"strings"

"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"

"github.com/labd/terraform-provider-contentful/internal/sdk"
"github.com/labd/terraform-provider-contentful/internal/utils"
)

// Ensure the implementation satisfies the expected interfaces.
var (
_ resource.Resource = &roleAssignmentResource{}
_ resource.ResourceWithConfigure = &roleAssignmentResource{}
_ resource.ResourceWithImportState = &roleAssignmentResource{}
)

func NewRoleAssignmentResource() resource.Resource {
return &roleAssignmentResource{}
}

// roleAssignmentResource is the resource implementation.
type roleAssignmentResource struct {
client *sdk.ClientWithResponses
}

func (r *roleAssignmentResource) Metadata(_ context.Context, request resource.MetadataRequest, response *resource.MetadataResponse) {
response.TypeName = request.ProviderTypeName + "_role_assignment"
}

func (r *roleAssignmentResource) Schema(_ context.Context, _ resource.SchemaRequest, response *resource.SchemaResponse) {
response.Schema = schema.Schema{
Description: "A Contentful Role Assignment assigns roles to teams in a space.",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Computed: true,
Description: "Role Assignment ID",
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"version": schema.Int64Attribute{
Computed: true,
Description: "The current version of the role assignment",
},
"space_id": schema.StringAttribute{
Required: true,
Description: "Space ID where the role assignment is made",
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"team_id": schema.StringAttribute{
Required: true,
Description: "Team ID to assign the role to (from contentful_team resource)",
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"role_id": schema.StringAttribute{
Optional: true,
Description: "Role ID to assign (from contentful_role resource). Mutually exclusive with is_admin.",
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"is_admin": schema.BoolAttribute{
Optional: true,
Computed: true,
Description: "Whether to assign the admin role. Mutually exclusive with role_id.",
Default: booldefault.StaticBool(false),
},
},
}
}

func (r *roleAssignmentResource) Configure(_ context.Context, request resource.ConfigureRequest, _ *resource.ConfigureResponse) {
if request.ProviderData == nil {
return
}

data := request.ProviderData.(utils.ProviderData)
r.client = data.Client
}

func (r *roleAssignmentResource) Create(ctx context.Context, request resource.CreateRequest, response *resource.CreateResponse) {
var plan RoleAssignment

response.Diagnostics.Append(request.Plan.Get(ctx, &plan)...)
if response.Diagnostics.HasError() {
return
}

// Validate mutually exclusive fields
if err := r.validateMutuallyExclusive(&plan); err != nil {
response.Diagnostics.AddError(
"Invalid Configuration",
err.Error(),
)
return
}

// TODO: Implement actual API call when endpoints become available
response.Diagnostics.AddError(
"Feature Not Available",
"Role assignment functionality is not yet available. This resource is a placeholder for when Contentful adds role assignment APIs to their Management API.",
)
}

func (r *roleAssignmentResource) Read(ctx context.Context, request resource.ReadRequest, response *resource.ReadResponse) {
var state RoleAssignment

diags := request.State.Get(ctx, &state)
response.Diagnostics.Append(diags...)
if response.Diagnostics.HasError() {
return
}

// TODO: Implement actual API call when endpoints become available
response.Diagnostics.AddError(
"Feature Not Available",
"Role assignment functionality is not yet available. This resource is a placeholder for when Contentful adds role assignment APIs to their Management API.",
)
}

func (r *roleAssignmentResource) Update(ctx context.Context, request resource.UpdateRequest, response *resource.UpdateResponse) {
var plan RoleAssignment

response.Diagnostics.Append(request.Plan.Get(ctx, &plan)...)
if response.Diagnostics.HasError() {
return
}

// Validate mutually exclusive fields
if err := r.validateMutuallyExclusive(&plan); err != nil {
response.Diagnostics.AddError(
"Invalid Configuration",
err.Error(),
)
return
}

// TODO: Implement actual API call when endpoints become available
response.Diagnostics.AddError(
"Feature Not Available",
"Role assignment functionality is not yet available. This resource is a placeholder for when Contentful adds role assignment APIs to their Management API.",
)
}

func (r *roleAssignmentResource) Delete(ctx context.Context, request resource.DeleteRequest, response *resource.DeleteResponse) {
var state RoleAssignment
response.Diagnostics.Append(request.State.Get(ctx, &state)...)
if response.Diagnostics.HasError() {
return
}

// TODO: Implement actual API call when endpoints become available
// For now, just log that this would delete the role assignment
fmt.Printf("Would delete role assignment: space=%s, team=%s, role=%s, admin=%t\n",
state.SpaceID.ValueString(),
state.TeamID.ValueString(),
state.RoleID.ValueString(),
state.IsAdmin.ValueBool(),
)
}

func (r *roleAssignmentResource) ImportState(ctx context.Context, request resource.ImportStateRequest, response *resource.ImportStateResponse) {
// Expected import format: space_id:team_id:role_id or space_id:team_id:admin
idParts := strings.Split(request.ID, ":")
if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" {
response.Diagnostics.AddError(
"Error importing role assignment",
fmt.Sprintf("Expected import format: space_id:team_id:role_id or space_id:team_id:admin, got: %s", request.ID),
)
return
}

// TODO: Implement actual import when API endpoints become available
response.Diagnostics.AddError(
"Feature Not Available",
"Role assignment functionality is not yet available. This resource is a placeholder for when Contentful adds role assignment APIs to their Management API.",
)
}

// validateMutuallyExclusive ensures that role_id and is_admin are mutually exclusive
func (r *roleAssignmentResource) validateMutuallyExclusive(assignment *RoleAssignment) error {
hasRoleID := !assignment.RoleID.IsNull() && !assignment.RoleID.IsUnknown() && assignment.RoleID.ValueString() != ""
isAdmin := assignment.IsAdmin.ValueBool()

if hasRoleID && isAdmin {
return fmt.Errorf("role_id and is_admin are mutually exclusive - specify either a specific role_id or set is_admin to true")
}

if !hasRoleID && !isAdmin {
return fmt.Errorf("either role_id must be specified or is_admin must be set to true")
}

return nil
}
36 changes: 36 additions & 0 deletions internal/resources/role_assignment/resource_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package role_assignment_test

import (
"testing"

"github.com/hashicorp/terraform-plugin-framework/providerserver"
"github.com/hashicorp/terraform-plugin-go/tfprotov6"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"

"github.com/labd/terraform-provider-contentful/internal/acctest"
"github.com/labd/terraform-provider-contentful/internal/provider"
)

func TestRoleAssignmentResource_Basic(t *testing.T) {
// Role assignment resource requires team functionality which isn't implemented yet
t.Skip("Role assignment resource depends on team resource (issue #84) and role assignment APIs that are not yet available")
}

// This test validates that the resource schema can be created without errors
func TestRoleAssignmentResource_Schema(t *testing.T) {
t.Parallel()

resource.Test(t, resource.TestCase{
PreCheck: func() { acctest.TestAccPreCheck(t) },
ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){
"contentful": providerserver.NewProtocol6WithError(provider.New("test", true)()),
},
Steps: []resource.TestStep{
{
Config: `
provider "contentful" {}
`,
},
},
})
}
Loading