teamcity/pool_resource.go (318 lines of code) (raw):

package teamcity import ( "context" "errors" "fmt" "strconv" "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" "github.com/hashicorp/terraform-plugin-framework/attr" "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/setdefault" "github.com/hashicorp/terraform-plugin-framework/resource/schema/setplanmodifier" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" "terraform-provider-teamcity/client" "terraform-provider-teamcity/models" ) var ( _ resource.Resource = &poolResource{} _ resource.ResourceWithConfigure = &poolResource{} ) func NewPoolResource() resource.Resource { return &poolResource{} } type poolResource struct { client *client.Client } // Resource functions implementation // returns the full name of the resource func (r *poolResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = req.ProviderTypeName + "_pool" } // returns the schema of the resource func (r *poolResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = schema.Schema{ Description: "An Agent Pool in TeamCity is a group of agents that can be associated with projects. More info [here](https://www.jetbrains.com/help/teamcity/configuring-agent-pools.html)", Attributes: map[string]schema.Attribute{ "name": schema.StringAttribute{ Required: true, }, "id": schema.Int64Attribute{ Computed: true, PlanModifiers: []planmodifier.Int64{ int64planmodifier.UseStateForUnknown(), }, }, "size": schema.Int64Attribute{ Required: false, Optional: true, MarkdownDescription: "Agents capacity for the given pool, don't add for unlimited", Validators: []validator.Int64{ int64validator.AtLeast(0), }, }, "projects": schema.SetAttribute{ Computed: true, Required: false, Optional: true, MarkdownDescription: "Projects assigned to the given pool. List of Project IDs.", ElementType: types.StringType, Default: setdefault.StaticValue(basetypes.NewSetValueMust(types.StringType, []attr.Value{})), PlanModifiers: []planmodifier.Set{ setplanmodifier.UseStateForUnknown(), }, }, }, } } // creates a resource and sets the initial terraform state func (r *poolResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // get values from plan var plan models.PoolDataModel diags := req.Plan.Get(ctx, &plan) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } // Generate API request var pool models.PoolJson proj := models.ProjectsJson{Project: make([]models.ProjectJson, 0)} var size int64 pool.Name = plan.Name.ValueString() if !plan.Size.IsNull() { size = plan.Size.ValueInt64() pool.Size = &size } // Create new agent pool result, err := r.client.NewPool(pool) if err != nil && errors.Is(err, context.DeadlineExceeded) { resp.Diagnostics.AddError( "Error creating pool: Timeout", err.Error(), ) return } if err != nil { resp.Diagnostics.AddError( "Error creating pool", "Cannot create pool, unexpected error: "+err.Error(), ) return } // Populate computed attributes plan.Id = types.Int64Value(int64(*(result.Id))) // Two way process: setup associated projects now // Assing projects from the plan elements := make([]types.String, 0, len(plan.Projects.Elements())) diags = plan.Projects.ElementsAs(ctx, &elements, false) if diags.HasError() { return } for _, project := range elements { id := project.ValueString() proj.Project = append(proj.Project, models.ProjectJson{Name: "-", Id: &id}) } response, err := r.client.SetPoolProjects(pool.Name, &proj) if err != nil { resp.Diagnostics.AddError( "Error setting agent pool projects, please check projects IDs are correct", err.Error(), ) return } else { plan.Projects, diags = types.SetValue(types.StringType, getProjectsAttrValue(response.Project)) if diags.HasError() { return } } // Set state diags = resp.State.Set(ctx, &plan) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } } // reads a resource and sets latest terraform state func (r *poolResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // get current state var state models.PoolDataModel diags := req.State.Get(ctx, &state) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } // get refreshed pool pool, err := r.client.GetPool(state.Name.ValueString()) if err != nil && errors.Is(err, context.DeadlineExceeded) { resp.Diagnostics.AddError( "Agent Pool not found: Timeout", err.Error(), ) return } if err != nil { resp.Diagnostics.AddAttributeError( path.Root("name"), "Agent Pool not found", "Cannot get an Agent Pool since there is no Agent Pool with the provided name.", ) return } // refresh state to reflect resource not found if pool == nil { resp.State.RemoveResource(ctx) return } // overwrite with refreshed state state = models.PoolDataModel{ Name: types.StringValue(string(pool.Name)), Size: pool.GetSize(), Id: types.Int64Value(int64(*(pool.Id))), Projects: types.SetNull(types.StringType), } if pool.Projects != nil { state.Projects, diags = types.SetValue(types.StringType, getProjectsAttrValue(pool.Projects.Project)) if diags.HasError() { return } } // set state diags = resp.State.Set(ctx, &state) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } } // updates a resource and sets the latest updated terraform state func (r *poolResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // get values from plan var plan models.PoolDataModel diags := req.Plan.Get(ctx, &plan) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } // get state var state models.PoolDataModel diags = req.State.Get(ctx, &state) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } var newName string var newSize string proj := models.ProjectsJson{Project: make([]models.ProjectJson, 0)} // Assing projects from the plan elements := make([]types.String, 0, len(plan.Projects.Elements())) diags = plan.Projects.ElementsAs(ctx, &elements, false) if diags.HasError() { return } for _, project := range elements { id := project.ValueString() proj.Project = append(proj.Project, models.ProjectJson{Name: "-", Id: &id}) } // verify plan values newName = plan.Name.ValueString() if plan.Size.IsNull() { newSize = "-1" // unlimited } else { size := plan.Size.ValueInt64() newSize = strconv.FormatInt(size, 10) } // verify state id if state.Id.IsNull() { resp.Diagnostics.AddAttributeError( path.Root("id"), "Agent pool state's id cannot be null", "The Resource cannot update an Agent Pool since there is an inconsistent state.", ) return } id := state.Id.String() // call update methods // Name result, err := r.client.SetField("agentPools", id, "name", &newName) if err != nil { resp.Diagnostics.AddError( "Error setting agent pool name field", err.Error(), ) return } else { state.Name = types.StringValue(result) } // Size result, err = r.client.SetField("agentPools", id, "maxAgents", &newSize) if err != nil { resp.Diagnostics.AddError( "Error setting agent pool size field", err.Error(), ) return } else { if result == "" { state.Size = basetypes.NewInt64Null() } else { i, err := strconv.ParseInt(result, 10, 64) if err != nil { resp.Diagnostics.AddError( "Could not parse field update response to int64", err.Error(), ) return } state.Size = basetypes.NewInt64Value(i) } } // Projects response, err := r.client.SetPoolProjects(newName, &proj) if err != nil { resp.Diagnostics.AddError( "Error setting agent pool projects, please check projects IDs are correct", err.Error(), ) return } else { state.Projects, diags = types.SetValue(types.StringType, getProjectsAttrValue(response.Project)) if diags.HasError() { return } } diags = resp.State.Set(ctx, state) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } } // deletes a resource and removes its terraform state func (r *poolResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // get state var state models.PoolDataModel diags := req.State.Get(ctx, &state) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } // verify state id if state.Id.IsNull() { resp.Diagnostics.AddAttributeError( path.Root("id"), "Agent pool state's id cannot be null", "The Resource cannot delete an Agent Pool since there is an inconsistent state.", ) return } id := state.Id.String() err := r.client.DeletePool(id) if err != nil && errors.Is(err, context.DeadlineExceeded) { resp.Diagnostics.AddError( "Couldn't delete agent pool: Timeout", err.Error(), ) return } if err != nil { resp.Diagnostics.AddError( fmt.Sprintf("Could not delete pool resource with name %s", state.Name), err.Error(), ) return } } func getProjectsAttrValue(data []models.ProjectJson) []attr.Value { projects := []attr.Value{} for _, p := range data { projects = append(projects, types.StringValue(*p.Id)) } return projects } // configure client func (r *poolResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { if req.ProviderData == nil { return } client, ok := req.ProviderData.(*client.Client) if !ok { resp.Diagnostics.AddError( "Unexpected Resource Configure Type", fmt.Sprintf("Expected *client.Client, got: %T.", req.ProviderData), ) return } r.client = client }