teamcity/group_role_assignment.go (178 lines of code) (raw):
package teamcity
import (
"context"
"fmt"
_ "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/types"
"strings"
"terraform-provider-teamcity/client"
)
var (
_ resource.Resource = &groupRoleAssignmentResource{}
_ resource.ResourceWithConfigure = &groupRoleAssignmentResource{}
_ resource.ResourceWithImportState = &groupRoleAssignmentResource{}
)
type groupRoleAssignmentResource struct {
client *client.Client
}
func NewGroupRoleAssignmentResource() resource.Resource {
return &groupRoleAssignmentResource{}
}
func (r *groupRoleAssignmentResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_group_role_assignment"
}
type groupRoleAssignmentResourceModel struct {
Id types.String `tfsdk:"id"`
GroupId types.String `tfsdk:"group_id"`
RoleId types.String `tfsdk:"role_id"`
Scope types.String `tfsdk:"scope"`
}
func (r *groupRoleAssignmentResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
Description: "Manages role assignments for TeamCity groups. This resource allows you to assign roles to groups either globally or for specific projects.",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
Description: "The ID of the role assignment (computed).",
},
"group_id": schema.StringAttribute{
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
Description: "The ID or key of the group to assign the role to.",
},
"role_id": schema.StringAttribute{
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
Description: "The ID of the role to assign.",
},
"scope": schema.StringAttribute{
Optional: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
Description: "The scope of the role assignment. Use 'g' for global scope or 'p:PROJECT_ID' for project-specific scope. Defaults to global if not specified.",
},
},
}
}
func (r *groupRoleAssignmentResource) Configure(_ context.Context, req resource.ConfigureRequest, _ *resource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
r.client = req.ProviderData.(*client.Client)
}
func (r *groupRoleAssignmentResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var plan groupRoleAssignmentResourceModel
diags := req.Plan.Get(ctx, &plan)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
// Determine scope
scope := "g" // default to global
if !plan.Scope.IsNull() {
scope = plan.Scope.ValueString()
}
// Add role to group
err := r.client.AddGroupRole(plan.GroupId.ValueString(), plan.RoleId.ValueString(), scope)
if err != nil {
resp.Diagnostics.AddError(
"Error assigning role to group",
err.Error(),
)
return
}
// Set state
state := groupRoleAssignmentResourceModel{
Id: types.StringValue(fmt.Sprintf("%s_%s_%s", plan.GroupId.ValueString(), plan.RoleId.ValueString(), scope)),
GroupId: plan.GroupId,
RoleId: plan.RoleId,
Scope: types.StringValue(scope),
}
diags = resp.State.Set(ctx, state)
resp.Diagnostics.Append(diags...)
}
func (r *groupRoleAssignmentResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var state groupRoleAssignmentResourceModel
diags := req.State.Get(ctx, &state)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
// Get group to verify role assignment still exists
group, err := r.client.GetGroup(state.GroupId.ValueString())
if err != nil {
resp.Diagnostics.AddError(
"Error reading group",
err.Error(),
)
return
}
if group == nil {
resp.State.RemoveResource(ctx)
return
}
// Check if role assignment still exists
found := false
if group.Roles != nil {
for _, role := range group.Roles.RoleAssignment {
if role.Id == state.RoleId.ValueString() && role.Scope == state.Scope.ValueString() {
found = true
break
}
}
}
if !found {
resp.State.RemoveResource(ctx)
return
}
// Role assignment exists, keep current state
diags = resp.State.Set(ctx, state)
resp.Diagnostics.Append(diags...)
}
func (r *groupRoleAssignmentResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
// This resource doesn't support updates - all attributes have RequiresReplace
// The framework will handle this by destroying and recreating the resource
resp.Diagnostics.AddError(
"Update not supported",
"This resource does not support updates. All changes require resource replacement.",
)
}
func (r *groupRoleAssignmentResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var state groupRoleAssignmentResourceModel
diags := req.State.Get(ctx, &state)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
// Remove role from group
err := r.client.RemoveGroupRole(state.GroupId.ValueString(), state.RoleId.ValueString(), state.Scope.ValueString())
if err != nil {
// Check if it's a 404 error (already deleted)
if !strings.Contains(err.Error(), "404") {
resp.Diagnostics.AddError(
"Error removing role from group",
err.Error(),
)
}
}
}
func (r *groupRoleAssignmentResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
// Import ID format: group_id/role_id/scope
parts := strings.Split(req.ID, "/")
if len(parts) != 3 {
resp.Diagnostics.AddError(
"Invalid import ID",
"Import ID must be in the format: group_id/role_id/scope",
)
return
}
state := groupRoleAssignmentResourceModel{
Id: types.StringValue(fmt.Sprintf("%s_%s_%s", parts[0], parts[1], parts[2])),
GroupId: types.StringValue(parts[0]),
RoleId: types.StringValue(parts[1]),
Scope: types.StringValue(parts[2]),
}
diags := resp.State.Set(ctx, &state)
resp.Diagnostics.Append(diags...)
}