From d009e07d53e7238352fa823e06324c9a218041fa Mon Sep 17 00:00:00 2001 From: domenicsim1 <87625140+domenicsim1@users.noreply.github.com> Date: Wed, 4 Dec 2024 13:54:21 +1100 Subject: [PATCH] feat: project versioning strategy resource (#834) --- docs/resources/project.md | 2 +- docs/resources/project_versioning_strategy.md | 100 +++++++++ .../resource.tf | 60 ++++++ octopusdeploy_framework/framework_provider.go | 1 + .../resource_project_versioning_strategy.go | 190 ++++++++++++++++++ octopusdeploy_framework/schemas/project.go | 2 +- .../schemas/project_versioning_strategy.go | 71 +++++++ 7 files changed, 424 insertions(+), 2 deletions(-) create mode 100644 docs/resources/project_versioning_strategy.md create mode 100644 examples/resources/octopusdeploy_project_versioning_strategy/resource.tf create mode 100644 octopusdeploy_framework/resource_project_versioning_strategy.go create mode 100644 octopusdeploy_framework/schemas/project_versioning_strategy.go diff --git a/docs/resources/project.md b/docs/resources/project.md index 218c4259f..d64796886 100644 --- a/docs/resources/project.md +++ b/docs/resources/project.md @@ -97,7 +97,7 @@ resource "octopusdeploy_project" "example" { - `space_id` (String) The space ID associated with this project. - `template` (Block List) (see [below for nested schema](#nestedblock--template)) - `tenanted_deployment_participation` (String) The tenanted deployment mode of the resource. Valid account types are `Untenanted`, `TenantedOrUntenanted`, or `Tenanted`. -- `versioning_strategy` (Block List) (see [below for nested schema](#nestedblock--versioning_strategy)) +- `versioning_strategy` (Block List, Deprecated) (see [below for nested schema](#nestedblock--versioning_strategy)) ### Read-Only diff --git a/docs/resources/project_versioning_strategy.md b/docs/resources/project_versioning_strategy.md new file mode 100644 index 000000000..eb86b23f6 --- /dev/null +++ b/docs/resources/project_versioning_strategy.md @@ -0,0 +1,100 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "octopusdeploy_project_versioning_strategy Resource - terraform-provider-octopusdeploy" +subcategory: "" +description: |- + +--- + +# octopusdeploy_project_versioning_strategy (Resource) + + + +## Example Usage + +```terraform +resource "octopusdeploy_project_group" "tp" { + name = "DevOps Projects" + description = "My DevOps projects group" +} + +resource "octopusdeploy_project" "tp" { + name = "My DevOps Project" + description = "test project" + lifecycle_id = "Lifecycles-1" + project_group_id = octopusdeploy_project_group.tp.id + + depends_on = [octopusdeploy_project_group.tp] +} + +resource "octopusdeploy_deployment_process" "process" { + project_id = octopusdeploy_project.tp.id + + step { + name = "Hello World" + target_roles = [ "hello-world" ] + start_trigger = "StartAfterPrevious" + package_requirement = "LetOctopusDecide" + condition = "Success" + + run_script_action { + name = "Hello World" + is_disabled = false + is_required = true + script_body = "Write-Host 'hello world'" + script_syntax = "PowerShell" + can_be_used_for_project_versioning = true + sort_order = 1 + + + package { + name = "Package" + feed_id = "feeds-builtin" + package_id = "myExpressApp" + acquisition_location = "Server" + extract_during_deployment = true + } + } + } + + depends_on = [octopusdeploy_project.tp] +} + +resource "octopusdeploy_project_versioning_strategy" "tp" { + project_id = octopusdeploy_project.tp.id + space_id = octopusdeploy_project.tp.space_id + donor_package_step_id = octopusdeploy_deployment_process.process.step[0].run_script_action[0].id + donor_package = { + deployment_action = "Hello World" + package_reference = "Package" + } + depends_on = [ + octopusdeploy_project_group.tp, + octopusdeploy_deployment_process.process + ] +} +``` + + +## Schema + +### Required + +- `donor_package` (Attributes) Donor Packages. (see [below for nested schema](#nestedatt--donor_package)) +- `project_id` (String) The associated project ID. +- `space_id` (String) Space ID of the associated project. + +### Optional + +- `donor_package_step_id` (String) The associated donor package step ID. +- `template` (String) + + +### Nested Schema for `donor_package` + +Optional: + +- `deployment_action` (String) Deployment action. +- `package_reference` (String) Package reference. + + diff --git a/examples/resources/octopusdeploy_project_versioning_strategy/resource.tf b/examples/resources/octopusdeploy_project_versioning_strategy/resource.tf new file mode 100644 index 000000000..dfcc6bbec --- /dev/null +++ b/examples/resources/octopusdeploy_project_versioning_strategy/resource.tf @@ -0,0 +1,60 @@ +resource "octopusdeploy_project_group" "tp" { + name = "DevOps Projects" + description = "My DevOps projects group" +} + +resource "octopusdeploy_project" "tp" { + name = "My DevOps Project" + description = "test project" + lifecycle_id = "Lifecycles-1" + project_group_id = octopusdeploy_project_group.tp.id + + depends_on = [octopusdeploy_project_group.tp] +} + +resource "octopusdeploy_deployment_process" "process" { + project_id = octopusdeploy_project.tp.id + + step { + name = "Hello World" + target_roles = [ "hello-world" ] + start_trigger = "StartAfterPrevious" + package_requirement = "LetOctopusDecide" + condition = "Success" + + run_script_action { + name = "Hello World" + is_disabled = false + is_required = true + script_body = "Write-Host 'hello world'" + script_syntax = "PowerShell" + can_be_used_for_project_versioning = true + sort_order = 1 + + + package { + name = "Package" + feed_id = "feeds-builtin" + package_id = "myExpressApp" + acquisition_location = "Server" + extract_during_deployment = true + } + } + } + + depends_on = [octopusdeploy_project.tp] +} + +resource "octopusdeploy_project_versioning_strategy" "tp" { + project_id = octopusdeploy_project.tp.id + space_id = octopusdeploy_project.tp.space_id + donor_package_step_id = octopusdeploy_deployment_process.process.step[0].run_script_action[0].id + donor_package = { + deployment_action = "Hello World" + package_reference = "Package" + } + depends_on = [ + octopusdeploy_project_group.tp, + octopusdeploy_deployment_process.process + ] +} diff --git a/octopusdeploy_framework/framework_provider.go b/octopusdeploy_framework/framework_provider.go index a1cdb241e..161b54619 100644 --- a/octopusdeploy_framework/framework_provider.go +++ b/octopusdeploy_framework/framework_provider.go @@ -113,6 +113,7 @@ func (p *octopusDeployFrameworkProvider) Resources(ctx context.Context) []func() NewLibraryVariableSetFeedResource, NewVariableResource, NewProjectResource, + NewProjectVersioningStrategyResource, NewMachineProxyResource, NewTagResource, NewDockerContainerRegistryFeedResource, diff --git a/octopusdeploy_framework/resource_project_versioning_strategy.go b/octopusdeploy_framework/resource_project_versioning_strategy.go new file mode 100644 index 000000000..bf587d864 --- /dev/null +++ b/octopusdeploy_framework/resource_project_versioning_strategy.go @@ -0,0 +1,190 @@ +package octopusdeploy_framework + +import ( + "context" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/core" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/packages" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/projects" + "github.com/OctopusDeploy/terraform-provider-octopusdeploy/octopusdeploy_framework/schemas" + "github.com/OctopusDeploy/terraform-provider-octopusdeploy/octopusdeploy_framework/util" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/types" + "log" + "net/http" +) + +var _ resource.Resource = &projectVersioningStrategyResource{} + +type projectVersioningStrategyResource struct { + *Config +} + +func NewProjectVersioningStrategyResource() resource.Resource { + return &projectVersioningStrategyResource{} +} + +func (r *projectVersioningStrategyResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = util.GetTypeName(schemas.ProjectVersioningStrategyResourceName) +} + +func (r *projectVersioningStrategyResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schemas.ProjectVersioningStrategySchema{}.GetResourceSchema() +} + +func (r *projectVersioningStrategyResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + r.Config = ResourceConfiguration(req, resp) +} + +func (r *projectVersioningStrategyResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan schemas.ProjectVersioningStrategyModel + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + project, err := projects.GetByID(r.Client, plan.SpaceID.ValueString(), plan.ProjectID.ValueString()) + if err != nil { + if apiError, ok := err.(*core.APIError); ok { + if apiError.StatusCode == http.StatusNotFound { + log.Printf("[INFO] associated project (%s) not found; deleting version strategy from state", plan.ProjectID.ValueString()) + resp.State.RemoveResource(ctx) + } + } else { + resp.Diagnostics.AddError("Failed to read associated project", err.Error()) + } + return + } + versioningStrategy := mapStateToProjectVersioningStrategy(&plan) + project.VersioningStrategy = versioningStrategy + + _, err = projects.Update(r.Client, project) + if err != nil { + resp.Diagnostics.AddError("Error updating associated project", err.Error()) + return + } + + updatedProject, err := projects.GetByID(r.Client, plan.SpaceID.ValueString(), plan.ProjectID.ValueString()) + if err != nil { + if apiError, ok := err.(*core.APIError); ok { + if apiError.StatusCode == http.StatusNotFound { + log.Printf("[INFO] associated project (%s) not found; deleting version strategy from state", plan.ProjectID.ValueString()) + resp.State.RemoveResource(ctx) + } + } else { + resp.Diagnostics.AddError("Failed to read associated project", err.Error()) + } + return + } + + mapProjectVersioningStrategyToState(updatedProject.VersioningStrategy, &plan) + resp.Diagnostics.Append(resp.State.Set(ctx, plan)...) +} + +func (r *projectVersioningStrategyResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state schemas.ProjectVersioningStrategyModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + project, err := projects.GetByID(r.Client, state.SpaceID.ValueString(), state.ProjectID.ValueString()) + if err != nil { + if apiError, ok := err.(*core.APIError); ok { + if apiError.StatusCode == http.StatusNotFound { + log.Printf("[INFO] associated project (%s) not found; deleting version strategy from state", state.ProjectID.ValueString()) + resp.State.RemoveResource(ctx) + } + } else { + resp.Diagnostics.AddError("Failed to read associated project", err.Error()) + } + return + } + mapProjectVersioningStrategyToState(project.VersioningStrategy, &state) + + resp.Diagnostics.Append(resp.State.Set(ctx, state)...) +} + +func (r *projectVersioningStrategyResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan schemas.ProjectVersioningStrategyModel + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + existingProject, err := projects.GetByID(r.Client, plan.SpaceID.ValueString(), plan.ProjectID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Error retrieving associated project", err.Error()) + return + } + + versioningStrategy := mapStateToProjectVersioningStrategy(&plan) + existingProject.VersioningStrategy = versioningStrategy + + _, err = projects.Update(r.Client, existingProject) + if err != nil { + resp.Diagnostics.AddError("Error updating associated project", err.Error()) + return + } + + updatedProject, err := projects.GetByID(r.Client, plan.SpaceID.ValueString(), plan.ProjectID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Error retrieving associated project", err.Error()) + return + } + + mapProjectVersioningStrategyToState(updatedProject.VersioningStrategy, &plan) + resp.Diagnostics.Append(resp.State.Set(ctx, plan)...) +} + +func (r *projectVersioningStrategyResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state schemas.ProjectVersioningStrategyModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + project, err := projects.GetByID(r.Client, state.SpaceID.ValueString(), state.ProjectID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Error retrieving project", err.Error()) + return + } + + project.VersioningStrategy = &projects.VersioningStrategy{} + _, err = projects.Update(r.Client, project) + if err != nil { + resp.Diagnostics.AddError("Error updating project to remove versioning strategy", err.Error()) + return + } + + resp.State.RemoveResource(ctx) +} + +func mapStateToProjectVersioningStrategy(state *schemas.ProjectVersioningStrategyModel) *projects.VersioningStrategy { + var donorPackageStepID *string + donorPackageStepIDString := state.DonorPackageStepID.ValueString() + if donorPackageStepIDString != "" { + donorPackageStepID = &donorPackageStepIDString + } + + return &projects.VersioningStrategy{ + Template: state.Template.ValueString(), + DonorPackageStepID: donorPackageStepID, + DonorPackage: &packages.DeploymentActionPackage{ + DeploymentAction: state.DonorPackage.DeploymentAction.ValueString(), + PackageReference: state.DonorPackage.PackageReference.ValueString(), + }, + } +} + +func mapProjectVersioningStrategyToState(versioningStrategy *projects.VersioningStrategy, state *schemas.ProjectVersioningStrategyModel) { + if versioningStrategy.DonorPackageStepID != nil { + state.DonorPackageStepID = types.StringValue(*versioningStrategy.DonorPackageStepID) + } + state.Template = types.StringValue(versioningStrategy.Template) + state.DonorPackage.PackageReference = types.StringValue(versioningStrategy.DonorPackage.PackageReference) + state.DonorPackage.DeploymentAction = types.StringValue(versioningStrategy.DonorPackage.DeploymentAction) +} diff --git a/octopusdeploy_framework/schemas/project.go b/octopusdeploy_framework/schemas/project.go index ac7898ab2..93d7b32a9 100644 --- a/octopusdeploy_framework/schemas/project.go +++ b/octopusdeploy_framework/schemas/project.go @@ -147,8 +147,8 @@ func (p ProjectSchema) GetResourceSchema() resourceSchema.Schema { }, }, "versioning_strategy": resourceSchema.ListNestedBlock{ + DeprecationMessage: "versioning_strategy is deprecated in favor of resource project_versioning strategy", NestedObject: resourceSchema.NestedBlockObject{ - Attributes: map[string]resourceSchema.Attribute{ "donor_package_step_id": util.ResourceString().Optional().Build(), "template": util.ResourceString().Optional().Computed().Build(), diff --git a/octopusdeploy_framework/schemas/project_versioning_strategy.go b/octopusdeploy_framework/schemas/project_versioning_strategy.go new file mode 100644 index 000000000..c581f8037 --- /dev/null +++ b/octopusdeploy_framework/schemas/project_versioning_strategy.go @@ -0,0 +1,71 @@ +package schemas + +import ( + "github.com/OctopusDeploy/terraform-provider-octopusdeploy/octopusdeploy_framework/util" + datasourceSchema "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + resourceSchema "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +type ProjectVersioningStrategySchema struct{} + +var _ EntitySchema = ProjectVersioningStrategySchema{} + +const ProjectVersioningStrategyResourceName = "project_versioning_strategy" + +func (p ProjectVersioningStrategySchema) GetResourceSchema() resourceSchema.Schema { + return resourceSchema.Schema{ + Attributes: map[string]resourceSchema.Attribute{ + "project_id": util.ResourceString(). + Description("The associated project ID."). + PlanModifiers(stringplanmodifier.RequiresReplace()). + Required(). + Build(), + "space_id": util.ResourceString(). + Description("Space ID of the associated project."). + Required(). + Build(), + "donor_package_step_id": util.ResourceString(). + Description("The associated donor package step ID."). + Optional(). + Build(), + "template": util.ResourceString(). + Optional(). + Computed(). + Build(), + "donor_package": resourceSchema.SingleNestedAttribute{ + Required: true, + Description: "Donor Packages.", + Attributes: map[string]resourceSchema.Attribute{ + "deployment_action": util.ResourceString(). + Description("Deployment action."). + Optional(). + Build(), + "package_reference": util.ResourceString(). + Description("Package reference."). + Optional(). + Build(), + }, + }, + }, + } +} + +func (p ProjectVersioningStrategySchema) GetDatasourceSchema() datasourceSchema.Schema { + // no datasource required, returned as part of project datasource + return datasourceSchema.Schema{} +} + +type ProjectVersioningStrategyModel struct { + ProjectID types.String `tfsdk:"project_id"` + SpaceID types.String `tfsdk:"space_id"` + DonorPackageStepID types.String `tfsdk:"donor_package_step_id"` + Template types.String `tfsdk:"template"` + DonorPackage DonorPackageModel `tfsdk:"donor_package"` +} + +type DonorPackageModel struct { + DeploymentAction types.String `tfsdk:"deployment_action"` + PackageReference types.String `tfsdk:"package_reference"` +}