teamcity/group.go (309 lines of code) (raw):
package teamcity
import (
"context"
"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"
"terraform-provider-teamcity/client"
)
var (
_ resource.Resource = &groupResource{}
_ resource.ResourceWithConfigure = &groupResource{}
_ resource.ResourceWithImportState = &groupResource{}
)
type groupResource struct {
client *client.Client
}
func NewGroupResource() resource.Resource {
return &groupResource{}
}
func (r *groupResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_group"
}
type groupResourceModel struct {
Id types.String `tfsdk:"id"`
Key types.String `tfsdk:"key"`
Name types.String `tfsdk:"name"`
Roles []roleAssignment `tfsdk:"roles"`
ParentGroups types.Set `tfsdk:"parent_groups"`
}
func (r *groupResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
Description: "User groups help manage user accounts more efficiently via roles and notification rules. More details [here](https://www.jetbrains.com/help/teamcity/creating-and-managing-user-groups.html).",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
Description: "Deprecated. Use key instead, id is generated on creation and is read only.",
},
"key": schema.StringAttribute{
Optional: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
Description: "Custom key (id) for the group. If not provided, TeamCity will generate one based on the name.",
},
"name": schema.StringAttribute{
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"roles": schema.SetNestedAttribute{
Optional: true,
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Required: true,
},
"global": schema.BoolAttribute{
Optional: true,
},
"project": schema.StringAttribute{
Optional: true,
},
},
},
},
"parent_groups": schema.SetAttribute{
ElementType: types.StringType,
Optional: true,
},
},
}
}
func (r *groupResource) Configure(_ context.Context, req resource.ConfigureRequest, _ *resource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
r.client = req.ProviderData.(*client.Client)
}
func (r *groupResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var plan groupResourceModel
diags := req.Plan.Get(ctx, &plan)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
group := client.Group{
Name: plan.Name.ValueString(),
}
if !plan.Key.IsNull() {
group.Key = plan.Key.ValueString()
}
group.Roles = &client.RoleAssignments{
RoleAssignment: []client.RoleAssignment{},
}
if plan.Roles != nil {
for _, role := range plan.Roles {
assignment := client.RoleAssignment{
Id: role.Id.ValueString(),
}
assignment.Scope = scope(role)
group.Roles.RoleAssignment = append(group.Roles.RoleAssignment, assignment)
}
}
if !plan.ParentGroups.IsNull() {
var parents []types.String
diags = plan.ParentGroups.ElementsAs(ctx, &parents, false)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
group.Parents = &client.ParentGroups{}
for _, i := range parents {
val := i.ValueString()
group.Parents.Group = append(
group.Parents.Group,
client.Group{Key: val},
)
}
}
actual, err := r.client.NewGroup(group)
if err != nil {
resp.Diagnostics.AddError(
"Error adding group",
err.Error(),
)
return
}
newState := r.readState(actual)
diags = resp.State.Set(ctx, newState)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
}
func (r *groupResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var oldState groupResourceModel
diags := req.State.Get(ctx, &oldState)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
actual, err := r.client.GetGroup(oldState.Id.ValueString())
if err != nil {
resp.Diagnostics.AddError(
"Error Reading group",
err.Error(),
)
return
}
if actual == nil {
resp.State.RemoveResource(ctx)
return
}
newState := r.readState(actual)
// Only set the key if it was explicitly set before
if oldState.Key.IsNull() {
newState.Key = types.StringNull()
}
diags = resp.State.Set(ctx, newState)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
}
func (r *groupResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var plan groupResourceModel
diags := req.Plan.Get(ctx, &plan)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
var oldState groupResourceModel
diags = req.State.Get(ctx, &oldState)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
// items present in old state but missing in a plan -> remove
for _, i := range oldState.Roles {
if !contains3(plan.Roles, i) {
err := r.client.RemoveGroupRole(plan.Id.ValueString(), i.Id.ValueString(), scope(i))
if err != nil {
resp.Diagnostics.AddError(
"Error removing group role",
err.Error(),
)
return
}
}
}
// items missing in old state but present in a plan -> add
for _, i := range plan.Roles {
if !contains3(oldState.Roles, i) {
err := r.client.AddGroupRole(plan.Id.ValueString(), i.Id.ValueString(), scope(i))
if err != nil {
resp.Diagnostics.AddError(
"Error adding group role",
err.Error(),
)
return
}
}
}
if !plan.ParentGroups.Equal(oldState.ParentGroups) {
var parents []string
diags = plan.ParentGroups.ElementsAs(ctx, &parents, false)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
err := r.client.SetGroupParents(plan.Id.ValueString(), parents)
if err != nil {
resp.Diagnostics.AddError(
"Error Reading group",
err.Error(),
)
return
}
}
actual, err := r.client.GetGroup(plan.Id.ValueString())
if err != nil {
resp.Diagnostics.AddError(
"Error Reading group",
err.Error(),
)
return
}
if actual == nil {
resp.State.RemoveResource(ctx)
return
}
newState := r.readState(actual)
// Only set the key if it was explicitly set before
if oldState.Key.IsNull() {
newState.Key = types.StringNull()
}
diags = resp.State.Set(ctx, newState)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
}
func (r *groupResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var state groupResourceModel
diags := req.State.Get(ctx, &state)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
err := r.client.DeleteGroup(state.Id.ValueString())
if err != nil {
resp.Diagnostics.AddError(
"Error Deleting group",
err.Error(),
)
return
}
}
func (r *groupResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
}
func (r *groupResource) readState(actual *client.Group) groupResourceModel {
var newState groupResourceModel
newState.Id = types.StringValue(actual.Key)
newState.Key = types.StringValue(actual.Key)
newState.Name = types.StringValue(actual.Name)
if actual.Roles != nil && len(actual.Roles.RoleAssignment) > 0 {
newState.Roles = []roleAssignment{}
for _, role := range actual.Roles.RoleAssignment {
assignment := roleAssignment{
Id: types.StringValue(role.Id),
}
if role.Scope == "g" {
assignment.Global = types.BoolValue(role.Scope == "g")
} else {
assignment.Project = types.StringValue(role.Scope[2:])
}
newState.Roles = append(newState.Roles, assignment)
}
}
newState.ParentGroups = types.SetNull(types.StringType)
for _, parent := range actual.Parents.Group {
newState.ParentGroups, _ = types.SetValue(
types.StringType,
append(newState.ParentGroups.Elements(), types.StringValue(parent.Key)),
)
}
return newState
}
func scope(i roleAssignment) string {
var scope string
if i.Global.ValueBool() {
scope = "g"
} else {
scope = "p:" + i.Project.ValueString()
}
return scope
}
func contains3(items []roleAssignment, item roleAssignment) bool {
for _, i := range items {
if i == item {
return true
}
}
return false
}