diff --git a/docs/data-sources/step_template.md b/docs/data-sources/step_template.md new file mode 100644 index 000000000..369f3c886 --- /dev/null +++ b/docs/data-sources/step_template.md @@ -0,0 +1,83 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "octopusdeploy_step_template Data Source - terraform-provider-octopusdeploy" +subcategory: "" +description: |- + Provides information about existing step_template. +--- + +# octopusdeploy_step_template (Data Source) + +Provides information about existing step_template. + + + + +## Schema + +### Required + +- `id` (String) Unique identifier of the step template + +### Optional + +- `space_id` (String) SpaceID of the Step Template + +### Read-Only + +- `step_template` (Object) (see [below for nested schema](#nestedatt--step_template)) + + +### Nested Schema for `step_template` + +Read-Only: + +- `action_type` (String) +- `community_action_template_id` (String) +- `description` (String) +- `id` (String) +- `name` (String) +- `packages` (List of Object) (see [below for nested schema](#nestedobjatt--step_template--packages)) +- `parameters` (List of Object) (see [below for nested schema](#nestedobjatt--step_template--parameters)) +- `properties` (Map of String) +- `space_id` (String) +- `step_package_id` (String) +- `version` (Number) + + +### Nested Schema for `step_template.packages` + +Read-Only: + +- `acquisition_location` (String) +- `feed_id` (String) +- `id` (String) +- `name` (String) +- `package_id` (String) +- `properties` (Object) (see [below for nested schema](#nestedobjatt--step_template--packages--properties)) + + +### Nested Schema for `step_template.packages.properties` + +Read-Only: + +- `extract` (String) +- `package_parameter_name` (String) +- `purpose` (String) +- `selection_mode` (String) + + + + +### Nested Schema for `step_template.parameters` + +Read-Only: + +- `default_value` (String) +- `display_settings` (Map of String) +- `help_text` (String) +- `id` (String) +- `label` (String) +- `name` (String) + + diff --git a/docs/resources/step_template.md b/docs/resources/step_template.md new file mode 100644 index 000000000..afec18d12 --- /dev/null +++ b/docs/resources/step_template.md @@ -0,0 +1,86 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "octopusdeploy_step_template Resource - terraform-provider-octopusdeploy" +subcategory: "" +description: |- + This resource manages step_templates in Octopus Deploy. +--- + +# octopusdeploy_step_template (Resource) + +This resource manages step_templates in Octopus Deploy. + + + + +## Schema + +### Required + +- `action_type` (String) The action type of the step template +- `name` (String) The name of this resource. +- `packages` (Attributes List) Package information for the step template (see [below for nested schema](#nestedatt--packages)) +- `parameters` (Attributes List) List of parameters that can be used in Step Template. (see [below for nested schema](#nestedatt--parameters)) +- `properties` (Map of String) Properties for the step template +- `step_package_id` (String) The ID of the step package + +### Optional + +- `community_action_template_id` (String) The ID of the community action template +- `description` (String) The description of this step_template. +- `space_id` (String) The space ID associated with this step_template. + +### Read-Only + +- `id` (String) The unique ID for this resource. +- `version` (Number) The version of the step template + + +### Nested Schema for `packages` + +Required: + +- `feed_id` (String) ID of the feed. +- `name` (String) The name of this resource. +- `properties` (Attributes) Properties for the package. (see [below for nested schema](#nestedatt--packages--properties)) + +Optional: + +- `acquisition_location` (String) Acquisition location for the package. +- `package_id` (String) The ID of the package to use. + +Read-Only: + +- `id` (String) The unique ID for this resource. + + +### Nested Schema for `packages.properties` + +Required: + +- `selection_mode` (String) The selection mode. + +Optional: + +- `extract` (String) If the package should extract. +- `package_parameter_name` (String) The name of the package parameter +- `purpose` (String) The purpose of this property. + + + + +### Nested Schema for `parameters` + +Required: + +- `id` (String) The id for the property. +- `name` (String) The name of the variable set by the parameter. The name can contain letters, digits, dashes and periods. Example: `ServerName` + +Optional: + +- `default_value` (String) A default value for the parameter, if applicable. This can be a hard-coded value or a variable reference. +- `display_settings` (Map of String) The display settings for the parameter. +- `help_text` (String) The help presented alongside the parameter input. +- `label` (String) The label shown beside the parameter when presented in the deployment process. Example: `Server name`. + + diff --git a/go.mod b/go.mod index 1d574617d..1a16e81ba 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/OctopusDeploy/terraform-provider-octopusdeploy go 1.21 require ( - github.com/OctopusDeploy/go-octopusdeploy/v2 v2.50.0 + github.com/OctopusDeploy/go-octopusdeploy/v2 v2.52.0 github.com/OctopusSolutionsEngineering/OctopusTerraformTestFramework v0.0.0-20240729041805-46db6fb717b4 github.com/google/uuid v1.6.0 github.com/hashicorp/go-cty v1.4.1-0.20200723130312-85980079f637 @@ -17,7 +17,6 @@ require ( github.com/hashicorp/terraform-plugin-testing v1.8.0 github.com/stretchr/testify v1.9.0 github.com/testcontainers/testcontainers-go v0.32.0 - golang.org/x/exp v0.0.0-20240707233637-46b078467d37 golang.org/x/text v0.16.0 k8s.io/utils v0.0.0-20230505201702-9f6742963106 software.sslmate.com/src/go-pkcs12 v0.4.0 @@ -130,6 +129,7 @@ require ( go.opentelemetry.io/otel/sdk v1.21.0 // indirect go.opentelemetry.io/otel/trace v1.24.0 // indirect golang.org/x/crypto v0.25.0 // indirect + golang.org/x/exp v0.0.0-20240707233637-46b078467d37 // indirect golang.org/x/mod v0.19.0 // indirect golang.org/x/net v0.27.0 // indirect golang.org/x/sync v0.7.0 // indirect diff --git a/go.sum b/go.sum index 066bca243..bfd0603f8 100644 --- a/go.sum +++ b/go.sum @@ -20,8 +20,8 @@ github.com/Microsoft/hcsshim v0.12.4 h1:Ev7YUMHAHoWNm+aDSPzc5W9s6E2jyL1szpVDJeZ/ github.com/Microsoft/hcsshim v0.12.4/go.mod h1:Iyl1WVpZzr+UkzjekHZbV8o5Z9ZkxNGx6CtY2Qg/JVQ= github.com/OctopusDeploy/go-octodiff v1.0.0 h1:U+ORg6azniwwYo+O44giOw6TiD5USk8S4VDhOQ0Ven0= github.com/OctopusDeploy/go-octodiff v1.0.0/go.mod h1:Mze0+EkOWTgTmi8++fyUc6r0aLZT7qD9gX+31t8MmIU= -github.com/OctopusDeploy/go-octopusdeploy/v2 v2.50.0 h1:rQiLEbqt/D3lPQw3pq9sXAW1C0WhVLrfN/h0cqUzaFY= -github.com/OctopusDeploy/go-octopusdeploy/v2 v2.50.0/go.mod h1:ggvOXzMnq+w0pLg6C9zdjz6YBaHfO3B3tqmmB7JQdaw= +github.com/OctopusDeploy/go-octopusdeploy/v2 v2.52.0 h1:X3Tdij/cGqmEtmZ0HqJFeHzTJVxFmYEAog4R4w6KFIw= +github.com/OctopusDeploy/go-octopusdeploy/v2 v2.52.0/go.mod h1:ggvOXzMnq+w0pLg6C9zdjz6YBaHfO3B3tqmmB7JQdaw= github.com/OctopusSolutionsEngineering/OctopusTerraformTestFramework v0.0.0-20240729041805-46db6fb717b4 h1:QfbVf0bOIRMp/WHAWsuVDB7KHoWnRsGbvDuOf2ua7k4= github.com/OctopusSolutionsEngineering/OctopusTerraformTestFramework v0.0.0-20240729041805-46db6fb717b4/go.mod h1:Oq9KbiRNDBB5jFmrwnrgLX0urIqR/1ptY18TzkqXm7M= github.com/ProtonMail/go-crypto v1.1.0-alpha.2 h1:bkyFVUP+ROOARdgCiJzNQo2V2kiB97LyUpzH9P6Hrlg= diff --git a/octopusdeploy_framework/datasource_step_template.go b/octopusdeploy_framework/datasource_step_template.go new file mode 100644 index 000000000..7343a2d3d --- /dev/null +++ b/octopusdeploy_framework/datasource_step_template.go @@ -0,0 +1,116 @@ +package octopusdeploy_framework + +import ( + "context" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/actiontemplates" + "github.com/OctopusDeploy/terraform-provider-octopusdeploy/octopusdeploy_framework/schemas" + "github.com/OctopusDeploy/terraform-provider-octopusdeploy/octopusdeploy_framework/util" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +type stepTemplateDataSource struct { + *Config +} + +func NewStepTemplateDataSource() datasource.DataSource { + return &stepTemplateDataSource{} +} +func (*stepTemplateDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = util.GetTypeName("step_template") +} + +func (*stepTemplateDataSource) Schema(_ context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schemas.StepTemplateSchema{}.GetDatasourceSchema() +} + +func (d *stepTemplateDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + d.Config = DataSourceConfiguration(req, resp) +} + +func (d *stepTemplateDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var err error + var data schemas.StepTemplateTypeDataSourceModel + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + query := struct { + ID string + SpaceID string + }{data.ID.ValueString(), data.SpaceID.ValueString()} + + util.DatasourceReading(ctx, "step_template", query) + + actionTemplate, err := actiontemplates.GetByID(d.Config.Client, query.SpaceID, query.ID) + if err != nil { + resp.Diagnostics.AddError("Unable to load step template", err.Error()) + return + } + + resp.Diagnostics.Append(mapStepTemplateToDatasourceModel(&data, actionTemplate)...) + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func mapStepTemplateToDatasourceModel(data *schemas.StepTemplateTypeDataSourceModel, at *actiontemplates.ActionTemplate) diag.Diagnostics { + resp := diag.Diagnostics{} + + data.ID = types.StringValue(at.ID) + data.SpaceID = types.StringValue(at.SpaceID) + stepTemplate, dg := convertStepTemplateAttributes(at) + resp.Append(dg...) + data.StepTemplate = stepTemplate + return resp +} + +func convertStepTemplateAttributes(at *actiontemplates.ActionTemplate) (types.Object, diag.Diagnostics) { + diags := diag.Diagnostics{} + + params := make([]attr.Value, len(at.Parameters)) + for i, param := range at.Parameters { + p, dg := convertStepTemplateParameterAttribute(param) + diags.Append(dg...) + params[i] = p + } + paramsListValue, dg := types.ListValue(types.ObjectType{AttrTypes: schemas.GetStepTemplateParameterTypeAttributes()}, params) + diags.Append(dg...) + + pkgs := make([]attr.Value, len(at.Packages)) + for i, pkg := range at.Packages { + p, dg := convertStepTemplatePackageAttribute(pkg) + diags.Append(dg...) + pkgs[i] = p + } + packageListValue, dg := types.ListValue(types.ObjectType{AttrTypes: schemas.GetStepTemplatePackageTypeAttributes()}, pkgs) + diags.Append(dg...) + + props := make(map[string]attr.Value, len(at.Properties)) + for key, val := range at.Properties { + props[key] = types.StringValue(val.Value) + } + propertiesMap, dg := types.MapValue(types.StringType, props) + diags.Append(dg...) + + if diags.HasError() { + return types.ObjectNull(schemas.GetStepTemplateParameterTypeAttributes()), diags + } + + stepTemplate, dg := types.ObjectValue(schemas.GetStepTemplateAttributes(), map[string]attr.Value{ + "id": types.StringValue(at.ID), + "name": types.StringValue(at.Name), + "description": types.StringValue(at.Description), + "space_id": types.StringValue(at.SpaceID), + "version": types.Int32Value(at.Version), + "step_package_id": types.StringValue(at.ActionType), + "action_type": types.StringValue(at.ActionType), + "community_action_template_id": types.StringValue(at.CommunityActionTemplateID), + "packages": packageListValue, + "parameters": paramsListValue, + "properties": propertiesMap, + }) + diags.Append(dg...) + return stepTemplate, diags +} diff --git a/octopusdeploy_framework/framework_provider.go b/octopusdeploy_framework/framework_provider.go index a4a6ba9d6..4ec3df754 100644 --- a/octopusdeploy_framework/framework_provider.go +++ b/octopusdeploy_framework/framework_provider.go @@ -65,6 +65,7 @@ func (p *octopusDeployFrameworkProvider) DataSources(ctx context.Context) []func NewSpacesDataSource, NewLifecyclesDataSource, NewEnvironmentsDataSource, + NewStepTemplateDataSource, NewGitCredentialsDataSource, NewFeedsDataSource, NewLibraryVariableSetDataSource, @@ -85,6 +86,7 @@ func (p *octopusDeployFrameworkProvider) Resources(ctx context.Context) []func() NewMavenFeedResource, NewLifecycleResource, NewEnvironmentResource, + NewStepTemplateResource, NewGitCredentialResource, NewHelmFeedResource, NewArtifactoryGenericFeedResource, diff --git a/octopusdeploy_framework/resource_step_template.go b/octopusdeploy_framework/resource_step_template.go new file mode 100644 index 000000000..01bf45428 --- /dev/null +++ b/octopusdeploy_framework/resource_step_template.go @@ -0,0 +1,372 @@ +package octopusdeploy_framework + +import ( + "context" + "fmt" + + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/actiontemplates" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/core" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/packages" + "github.com/OctopusDeploy/terraform-provider-octopusdeploy/internal/errors" + "github.com/OctopusDeploy/terraform-provider-octopusdeploy/octopusdeploy_framework/schemas" + "github.com/OctopusDeploy/terraform-provider-octopusdeploy/octopusdeploy_framework/util" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +type stepTemplateTypeResource struct { + *Config +} + +var _ resource.ResourceWithImportState = &stepTemplateTypeResource{} + +func NewStepTemplateResource() resource.Resource { + return &stepTemplateTypeResource{} +} + +func (r *stepTemplateTypeResource) Metadata(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = util.GetTypeName("step_template") +} + +func (r *stepTemplateTypeResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schemas.StepTemplateSchema{}.GetResourceSchema() +} + +func (r *stepTemplateTypeResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + r.Config = ResourceConfiguration(req, resp) +} + +func (*stepTemplateTypeResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *stepTemplateTypeResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data schemas.StepTemplateTypeResourceModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + newActionTemplate, dg := mapStepTemplateResourceModelToActionTemplate(ctx, data) + resp.Diagnostics.Append(dg...) + if resp.Diagnostics.HasError() { + return + } + + actionTemplate, err := actiontemplates.Add(r.Config.Client, newActionTemplate) + if err != nil { + resp.Diagnostics.AddError("unable to create step template", err.Error()) + return + } + + resp.Diagnostics.Append(mapStepTemplateToResourceModel(&data, actionTemplate)...) + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *stepTemplateTypeResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data schemas.StepTemplateTypeResourceModel + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + actionTemplate, err := actiontemplates.GetByID(r.Config.Client, data.SpaceID.ValueString(), data.ID.ValueString()) + if err != nil { + if err := errors.ProcessApiErrorV2(ctx, resp, data, err, "action template"); err != nil { + resp.Diagnostics.AddError("unable to load environment", err.Error()) + } + return + } + + resp.Diagnostics.Append(mapStepTemplateToResourceModel(&data, actionTemplate)...) + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *stepTemplateTypeResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var data, state schemas.StepTemplateTypeResourceModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + at, err := actiontemplates.GetByID(r.Config.Client, state.SpaceID.ValueString(), state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("unable to load step template", err.Error()) + return + } + + actionTemplateUpdate, dg := mapStepTemplateResourceModelToActionTemplate(ctx, data) + resp.Diagnostics.Append(dg...) + if resp.Diagnostics.HasError() { + return + } + actionTemplateUpdate.ID = at.ID + actionTemplateUpdate.SpaceID = at.SpaceID + actionTemplateUpdate.Version = at.Version + + updatedActionTemplate, err := actiontemplates.Update(r.Config.Client, actionTemplateUpdate) + if err != nil { + resp.Diagnostics.AddError("unable to update step template", err.Error()) + return + } + + resp.Diagnostics.Append(mapStepTemplateToResourceModel(&data, updatedActionTemplate)...) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *stepTemplateTypeResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var data schemas.StepTemplateTypeResourceModel + + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + if err := actiontemplates.DeleteByID(r.Config.Client, data.SpaceID.ValueString(), data.ID.ValueString()); err != nil { + resp.Diagnostics.AddError("unable to delete step template", err.Error()) + return + } +} + +func mapStepTemplateToResourceModel(data *schemas.StepTemplateTypeResourceModel, at *actiontemplates.ActionTemplate) diag.Diagnostics { + resp := diag.Diagnostics{} + + data.ID = types.StringValue(at.ID) + data.SpaceID = types.StringValue(at.SpaceID) + data.Name = types.StringValue(at.Name) + data.Version = types.Int32Value(at.Version) + data.Description = types.StringValue(at.Description) + data.CommunityActionTemplateId = types.StringValue(at.CommunityActionTemplateID) + data.ActionType = types.StringValue(at.ActionType) + + // Parameters + sParams, dg := convertStepTemplateToParameterAttributes(at.Parameters) + resp.Append(dg...) + data.Parameters = sParams + + // Properties + stringProps := make(map[string]attr.Value, len(at.Properties)) + for keys, value := range at.Properties { + stringProps[keys] = types.StringValue(value.Value) + } + props, dg := types.MapValue(types.StringType, stringProps) + resp.Append(dg...) + data.Properties = props + + // Packages + pkgs, dg := convertStepTemplateToPackageAttributes(at.Packages) + resp.Append(dg...) + data.Packages = pkgs + + return resp +} + +func mapStepTemplateResourceModelToActionTemplate(ctx context.Context, data schemas.StepTemplateTypeResourceModel) (*actiontemplates.ActionTemplate, diag.Diagnostics) { + resp := diag.Diagnostics{} + at := actiontemplates.NewActionTemplate(data.Name.ValueString(), data.ActionType.ValueString()) + + at.SpaceID = data.SpaceID.ValueString() + at.Description = data.Description.ValueString() + if !data.CommunityActionTemplateId.IsNull() { + at.CommunityActionTemplateID = data.CommunityActionTemplateId.ValueString() + } + + pkgs := make([]schemas.StepTemplatePackageType, 0, len(data.Packages.Elements())) + resp.Append(data.Packages.ElementsAs(ctx, &pkgs, false)...) + if resp.HasError() { + return at, resp + } + + props := make(map[string]types.String, len(data.Properties.Elements())) + resp.Append(data.Properties.ElementsAs(ctx, &props, false)...) + if resp.HasError() { + return at, resp + } + + params := make([]schemas.StepTemplateParameterType, 0, len(data.Parameters.Elements())) + resp.Append(data.Parameters.ElementsAs(ctx, ¶ms, false)...) + if resp.HasError() { + return at, resp + } + + if len(props) > 0 { + templateProps := make(map[string]core.PropertyValue, len(props)) + for key, val := range props { + templateProps[key] = core.NewPropertyValue(val.ValueString(), false) + } + at.Properties = templateProps + } else { + at.Properties = make(map[string]core.PropertyValue) + } + + at.Packages = make([]packages.PackageReference, len(pkgs)) + if len(pkgs) > 0 { + for i, val := range pkgs { + pkgProps := make(map[string]string, len(val.Properties.Attributes())) + for key, prop := range val.Properties.Attributes() { + if prop.Type(ctx) == types.StringType { + pkgProps[key] = prop.(types.String).ValueString() + } else { + // We should not get this error unless we add a field to package properties in the schema that is not a string + resp.AddError("Unexpected value type in package properties.", + fmt.Sprintf("Expected [%s] to have value of string but got [%s].", key, prop.String())) + } + } + if resp.HasError() { + return at, resp + } + pkgRef := packages.PackageReference{ + AcquisitionLocation: val.AcquisitionLocation.ValueString(), + FeedID: val.FeedID.ValueString(), + Properties: pkgProps, + Name: val.Name.ValueString(), + PackageID: val.PackageID.ValueString(), + } + pkgRef.ID = val.ID.ValueString() + at.Packages[i] = pkgRef + } + } + + at.Parameters = make([]actiontemplates.ActionTemplateParameter, len(params)) + if len(params) > 0 { + paramIDMap := make(map[string]bool, len(params)) + for i, val := range params { + defaultValue := core.NewPropertyValue(val.DefaultValue.ValueString(), false) + at.Parameters[i] = actiontemplates.ActionTemplateParameter{ + DefaultValue: &defaultValue, + Name: val.Name.ValueString(), + Label: val.Label.ValueString(), + HelpText: val.HelpText.ValueString(), + DisplaySettings: util.ConvertAttrStringMapToStringMap(val.DisplaySettings.Elements()), + } + id := val.ID.ValueString() + if _, ok := paramIDMap[id]; ok { + resp.AddError("ID conflict", fmt.Sprintf("conflicting UUID's within parameters list: %s", id)) + } + paramIDMap[val.ID.ValueString()] = true + at.Parameters[i].ID = id + at.Parameters[i].ID = val.ID.ValueString() + } + } + if resp.HasError() { + return at, resp + } + return at, resp +} + +func convertStepTemplateToPackageAttributes(atPackage []packages.PackageReference) (types.List, diag.Diagnostics) { + resp := diag.Diagnostics{} + pkgs := make([]attr.Value, len(atPackage)) + for key, val := range atPackage { + mapVal, dg := convertStepTemplatePackageAttribute(val) + resp.Append(dg...) + if resp.HasError() { + return types.ListNull(types.ObjectType{AttrTypes: schemas.GetStepTemplatePackageTypeAttributes()}), resp + } + pkgs[key] = mapVal + } + pkgSet, dg := types.ListValue(types.ObjectType{AttrTypes: schemas.GetStepTemplatePackageTypeAttributes()}, pkgs) + resp.Append(dg...) + if resp.HasError() { + return types.ListNull(types.ObjectType{AttrTypes: schemas.GetStepTemplatePackageTypeAttributes()}), resp + } + return pkgSet, dg +} + +func convertStepTemplateToParameterAttributes(atParams []actiontemplates.ActionTemplateParameter) (types.List, diag.Diagnostics) { + resp := diag.Diagnostics{} + params := make([]attr.Value, len(atParams)) + for i, val := range atParams { + objVal, dg := convertStepTemplateParameterAttribute(val) + resp.Append(dg...) + if resp.HasError() { + return types.ListNull(types.ObjectType{AttrTypes: schemas.GetStepTemplateParameterTypeAttributes()}), resp + } + params[i] = objVal + } + sParams, dg := types.ListValue(types.ObjectType{AttrTypes: schemas.GetStepTemplateParameterTypeAttributes()}, params) + resp.Append(dg...) + if resp.HasError() { + return types.ListNull(types.ObjectType{AttrTypes: schemas.GetStepTemplateParameterTypeAttributes()}), resp + } + return sParams, resp +} + +func convertStepTemplateParameterAttribute(atp actiontemplates.ActionTemplateParameter) (types.Object, diag.Diagnostics) { + displaySettings, dg := types.MapValue(types.StringType, util.ConvertStringMapToAttrStringMap(atp.DisplaySettings)) + if dg.HasError() { + return types.ObjectNull(schemas.GetStepTemplateParameterTypeAttributes()), dg + } + return types.ObjectValue(schemas.GetStepTemplateParameterTypeAttributes(), map[string]attr.Value{ + "id": types.StringValue(atp.ID), + "name": types.StringValue(atp.Name), + "label": types.StringValue(atp.Label), + "help_text": types.StringValue(atp.HelpText), + "default_value": types.StringValue(atp.DefaultValue.Value), + "display_settings": displaySettings, + }) +} + +func convertStepTemplatePackageAttribute(atp packages.PackageReference) (types.Object, diag.Diagnostics) { + props, dg := convertStepTemplatePackagePropertyAttribute(atp.Properties) + if dg.HasError() { + return types.ObjectNull(schemas.GetStepTemplatePackageTypeAttributes()), dg + } + return types.ObjectValue(schemas.GetStepTemplatePackageTypeAttributes(), map[string]attr.Value{ + "id": types.StringValue(atp.ID), + "acquisition_location": types.StringValue(atp.AcquisitionLocation), + "name": types.StringValue(atp.Name), + "feed_id": types.StringValue(atp.FeedID), + "package_id": types.StringValue(atp.PackageID), + "properties": props, + }) +} + +func convertStepTemplatePackagePropertyAttribute(atpp map[string]string) (types.Object, diag.Diagnostics) { + prop := make(map[string]attr.Value) + diags := diag.Diagnostics{} + + // We need to manually convert the string map to ensure all fields are set. + if extract, ok := atpp["extract"]; ok { + prop["extract"] = types.StringValue(extract) + } else { + diags.AddWarning("Package property missing value.", "extract value missing from package property") + prop["extract"] = types.StringNull() + } + + if purpose, ok := atpp["purpose"]; ok { + prop["purpose"] = types.StringValue(purpose) + } else { + diags.AddWarning("Package property missing value.", "purpose value missing from package property") + prop["purpose"] = types.StringNull() + } + + if purpose, ok := atpp["package_parameter_name"]; ok { + prop["package_parameter_name"] = types.StringValue(purpose) + } else { + diags.AddWarning("Package property missing value.", "package_parameter_name value missing from package property") + prop["package_parameter_name"] = types.StringNull() + } + + if selectionMode, ok := atpp["selection_mode"]; ok { + prop["selection_mode"] = types.StringValue(selectionMode) + } else { + diags.AddWarning("Package property missing value.", "selection_mode value missing from package property") + prop["selection_mode"] = types.StringNull() + } + + propMap, dg := types.ObjectValue(schemas.GetStepTemplatePackagePropertiesTypeAttributes(), prop) + if dg.HasError() { + diags.Append(dg...) + return types.ObjectNull(schemas.GetStepTemplatePackagePropertiesTypeAttributes()), diags + } + return propMap, diags +} diff --git a/octopusdeploy_framework/resource_step_template_test.go b/octopusdeploy_framework/resource_step_template_test.go new file mode 100644 index 000000000..9b2365da1 --- /dev/null +++ b/octopusdeploy_framework/resource_step_template_test.go @@ -0,0 +1,228 @@ +package octopusdeploy_framework + +import ( + "fmt" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/actiontemplates" + "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "testing" +) + +type stepTemplatePackagePropsTestData struct { + extract string + purpose string + selectionMode string +} + +type stepTemplatePackageTestData struct { + packageID string + acquisitonLocation string + feedID string + name string + properties stepTemplatePackagePropsTestData +} + +type stepTemplateParamTestData struct { + defaultValue string + displaySettings map[string]string + helpText string + label string + name string + id string +} + +type stepTemplateTestData struct { + localName string + prefix string + actionType string + name string + description string + stepPackageID string + packages []stepTemplatePackageTestData + parameters []stepTemplateParamTestData + properties map[string]string +} + +func TestAccOctopusStepTemplateBasic(t *testing.T) { + localName := acctest.RandStringFromCharSet(20, acctest.CharSetAlpha) + prefix := "octopusdeploy_step_template." + localName + data := stepTemplateTestData{ + localName: localName, + prefix: prefix, + actionType: "Octopus.Script", + name: acctest.RandStringFromCharSet(10, acctest.CharSetAlpha), + description: acctest.RandStringFromCharSet(20, acctest.CharSetAlpha), + stepPackageID: "Octopus.Script", + packages: []stepTemplatePackageTestData{ + { + packageID: "force", + acquisitonLocation: "Server", + feedID: "feeds-builtin", + name: "mypackage", + properties: stepTemplatePackagePropsTestData{ + extract: "True", + purpose: "", + selectionMode: "immediate", + }, + }, + }, + parameters: []stepTemplateParamTestData{ + { + defaultValue: "Hello World", + displaySettings: map[string]string{ + "Octopus.ControlType": "SingleLineText", + }, + helpText: acctest.RandStringFromCharSet(10, acctest.CharSetAlpha), + label: acctest.RandStringFromCharSet(10, acctest.CharSetAlpha), + name: acctest.RandStringFromCharSet(10, acctest.CharSetAlpha), + id: "621e1584-cdf3-4b67-9204-fc82430c908c", + }, + { + defaultValue: "Hello Earth", + displaySettings: map[string]string{ + "Octopus.ControlType": "SingleLineText", + }, + helpText: acctest.RandStringFromCharSet(10, acctest.CharSetAlpha), + label: acctest.RandStringFromCharSet(10, acctest.CharSetAlpha), + name: acctest.RandStringFromCharSet(10, acctest.CharSetAlpha), + id: "cd731d21-669a-42e1-81af-048681fd5c69", + }, + }, + properties: map[string]string{ + "Octopus.Action.Script.ScriptBody": "echo 'Hello World'", + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "Bash", + }, + } + + resource.Test(t, resource.TestCase{ + CheckDestroy: func(s *terraform.State) error { return testStepTemplateDestroy(s, localName) }, + PreCheck: func() { TestAccPreCheck(t) }, + ProtoV6ProviderFactories: ProtoV6ProviderFactories(), + Steps: []resource.TestStep{ + { + Config: testStepTemplateRunScriptBasic(data), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(prefix, "name", data.name), + ), + }, + { + Config: testStepTemplateRunScriptUpdate(data), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(prefix, "name", data.name+"-updated"), + ), + }, + }, + }) +} + +func testStepTemplateRunScriptBasic(data stepTemplateTestData) string { + return fmt.Sprintf(` + resource "octopusdeploy_step_template" "%s" { + action_type = "%s" + name = "%s" + description = "%s" + step_package_id = "%s" + packages = [ + { + package_id = "%s" + acquisition_location = "%s" + feed_id = "%s" + name = "%s" + properties = { + extract = "%s" + purpose = "%s" + selection_mode = "%s" + } + } + ] + parameters = [ + { + default_value = "%s" + display_settings = { + "Octopus.ControlType" : "%s" + } + help_text = "%s" + label = "%s" + name = "%s" + id = "%s" + }, + { + default_value = "%s" + display_settings = { + "Octopus.ControlType" : "%s" + } + help_text = "%s" + label = "%s" + name = "%s" + id = "%s" + }, + ] + properties = { + "Octopus.Action.Script.ScriptBody" : "%s" + "Octopus.Action.Script.ScriptSource" : "%s" + "Octopus.Action.Script.Syntax" : "%s" + } + } +`, + data.localName, + data.actionType, + data.name, + data.description, + data.stepPackageID, + data.packages[0].packageID, + data.packages[0].acquisitonLocation, + data.packages[0].feedID, + data.packages[0].name, + data.packages[0].properties.extract, + data.packages[0].properties.purpose, + data.packages[0].properties.selectionMode, + data.parameters[0].defaultValue, + data.parameters[0].displaySettings["Octopus.ControlType"], + data.parameters[0].helpText, + data.parameters[0].label, + data.parameters[0].name, + data.parameters[0].id, + data.parameters[1].defaultValue, + data.parameters[1].displaySettings["Octopus.ControlType"], + data.parameters[1].helpText, + data.parameters[1].label, + data.parameters[1].name, + data.parameters[1].id, + data.properties["Octopus.Action.Script.ScriptBody"], + data.properties["Octopus.Action.Script.ScriptSource"], + data.properties["Octopus.Action.Script.Syntax"], + ) +} + +func testStepTemplateRunScriptUpdate(data stepTemplateTestData) string { + data.name = data.name + "-updated" + + return testStepTemplateRunScriptBasic(data) +} + +func testStepTemplateDestroy(s *terraform.State, localName string) error { + var actionTemplateID string + + for _, rs := range s.RootModule().Resources { + if rs.Type != "octopusdeploy_step_template" { + continue + } + + actionTemplateID = rs.Primary.ID + break + } + if actionTemplateID == "" { + return fmt.Errorf("no octopusdeploy_step_template resource found") + } + + actionTemplate, err := actiontemplates.GetByID(octoClient, octoClient.GetSpaceID(), actionTemplateID) + if err == nil { + if actionTemplate != nil { + return fmt.Errorf("step template (%s) still exists", actionTemplate.Name) + } + } + + return nil +} diff --git a/octopusdeploy_framework/schemas/step_template.go b/octopusdeploy_framework/schemas/step_template.go new file mode 100644 index 000000000..7daa17bb3 --- /dev/null +++ b/octopusdeploy_framework/schemas/step_template.go @@ -0,0 +1,288 @@ +package schemas + +import ( + "fmt" + "github.com/OctopusDeploy/terraform-provider-octopusdeploy/octopusdeploy_framework/util" + "github.com/google/uuid" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/attr" + ds "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + rs "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/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "regexp" +) + +const ( + StepTemplateResourceDescription = "step_template" + StepTemplateDatasourceDescription = "step_template" +) + +type StepTemplateTypeDataSourceModel struct { + ID types.String `tfsdk:"id"` + SpaceID types.String `tfsdk:"space_id"` + StepTemplate types.Object `tfsdk:"step_template"` +} + +type StepTemplateTypeResourceModel struct { + ActionType types.String `tfsdk:"action_type"` + SpaceID types.String `tfsdk:"space_id"` + CommunityActionTemplateId types.String `tfsdk:"community_action_template_id"` + Name types.String `tfsdk:"name"` + Description types.String `tfsdk:"description"` + Packages types.List `tfsdk:"packages"` + Parameters types.List `tfsdk:"parameters"` + Properties types.Map `tfsdk:"properties"` + StepPackageId types.String `tfsdk:"step_package_id"` + Version types.Int32 `tfsdk:"version"` + + ResourceModel +} + +type StepTemplatePackageType struct { + ID types.String `tfsdk:"id"` + AcquisitionLocation types.String `tfsdk:"acquisition_location"` + Name types.String `tfsdk:"name"` + FeedID types.String `tfsdk:"feed_id"` + PackageID types.String `tfsdk:"package_id"` + Properties types.Object `tfsdk:"properties"` +} + +type StepTemplateParameterType struct { + ID types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + Label types.String `tfsdk:"label"` + HelpText types.String `tfsdk:"help_text"` + DisplaySettings types.Map `tfsdk:"display_settings"` + DefaultValue types.String `tfsdk:"default_value"` +} + +type StepTemplateSchema struct{} + +var _ EntitySchema = StepTemplateSchema{} + +func (s StepTemplateSchema) GetDatasourceSchema() ds.Schema { + return ds.Schema{ + Description: util.GetDataSourceDescription(StepTemplateDatasourceDescription), + Attributes: map[string]ds.Attribute{ + "id": ds.StringAttribute{ + Description: "Unique identifier of the step template", + Required: true, + }, + "space_id": ds.StringAttribute{ + Description: "SpaceID of the Step Template", + Optional: true, + Computed: true, + }, + "step_template": ds.ObjectAttribute{ + Computed: true, + Optional: false, + AttributeTypes: GetStepTemplateAttributes(), + }, + }, + } +} + +func (s StepTemplateSchema) GetResourceSchema() rs.Schema { + return rs.Schema{ + Description: util.GetResourceSchemaDescription(StepTemplateResourceDescription), + Attributes: map[string]rs.Attribute{ + "id": GetIdResourceSchema(), + "name": GetNameResourceSchema(true), + "description": GetDescriptionResourceSchema(StepTemplateResourceDescription), + "space_id": GetSpaceIdResourceSchema(StepTemplateResourceDescription), + "version": rs.Int32Attribute{ + Description: "The version of the step template", + Optional: false, + Computed: true, + }, + "step_package_id": rs.StringAttribute{ + Description: "The ID of the step package", + Required: true, + }, + "action_type": rs.StringAttribute{ + Description: "The action type of the step template", + Required: true, + }, + "community_action_template_id": rs.StringAttribute{ + Description: "The ID of the community action template", + Optional: true, + Computed: true, + Default: stringdefault.StaticString(""), + }, + "packages": GetStepTemplatePackageResourceSchema(), + "parameters": GetStepTemplateParameterResourceSchema(), + "properties": rs.MapAttribute{ + Description: "Properties for the step template", + Required: true, + ElementType: types.StringType, + }, + }, + } +} + +func GetStepTemplateParameterResourceSchema() rs.ListNestedAttribute { + return rs.ListNestedAttribute{ + Description: "List of parameters that can be used in Step Template.", + Required: true, + NestedObject: rs.NestedAttributeObject{ + Attributes: map[string]rs.Attribute{ + "default_value": rs.StringAttribute{ + Description: "A default value for the parameter, if applicable. This can be a hard-coded value or a variable reference.", + Optional: true, + Computed: true, + Default: stringdefault.StaticString(""), + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "display_settings": rs.MapAttribute{ + Description: "The display settings for the parameter.", + Optional: true, + ElementType: types.StringType, + }, + "help_text": rs.StringAttribute{ + Description: "The help presented alongside the parameter input.", + Optional: true, + Computed: true, + Default: stringdefault.StaticString(""), + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "id": rs.StringAttribute{ + Description: "The id for the property.", + Required: true, + Validators: []validator.String{ + stringvalidator.RegexMatches(regexp.MustCompile("^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"), fmt.Sprintf("must be a valid UUID, unique within this list. Here is one you could use: %s.\nExpect uuid", uuid.New())), + }, + }, + "label": rs.StringAttribute{ + Description: "The label shown beside the parameter when presented in the deployment process. Example: `Server name`.", + Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "name": rs.StringAttribute{ + Description: "The name of the variable set by the parameter. The name can contain letters, digits, dashes and periods. Example: `ServerName`", + Required: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + }, + }, + } +} + +func GetStepTemplatePackageResourceSchema() rs.ListNestedAttribute { + return rs.ListNestedAttribute{ + Description: "Package information for the step template", + Required: true, + NestedObject: rs.NestedAttributeObject{ + Attributes: map[string]rs.Attribute{ + "acquisition_location": rs.StringAttribute{ + Description: "Acquisition location for the package.", + Default: stringdefault.StaticString("Server"), + Optional: true, + Computed: true, + }, + "feed_id": rs.StringAttribute{ + Description: "ID of the feed.", + Required: true, + }, + "id": GetIdResourceSchema(), + "name": GetNameResourceSchema(true), + "package_id": rs.StringAttribute{ + Description: "The ID of the package to use.", + Optional: true, + Required: false, + Computed: true, + }, + "properties": rs.SingleNestedAttribute{ + Description: "Properties for the package.", + Required: true, + Attributes: map[string]rs.Attribute{ + "extract": rs.StringAttribute{ + Description: "If the package should extract.", + Default: stringdefault.StaticString("True"), + Optional: true, + Computed: true, + Validators: []validator.String{ + stringvalidator.RegexMatches(regexp.MustCompile("^(True|Fasle)$"), "Extract must be True or False"), + }, + }, + "package_parameter_name": rs.StringAttribute{ + Description: "The name of the package parameter", + Default: stringdefault.StaticString(""), + Optional: true, + Computed: true, + }, + "purpose": rs.StringAttribute{ + Description: "The purpose of this property.", + Default: stringdefault.StaticString(""), + Optional: true, + Required: false, + Computed: true, + }, + "selection_mode": rs.StringAttribute{ + Description: "The selection mode.", + Required: true, + }, + }, + }, + }, + }, + } +} + +func GetStepTemplateAttributes() map[string]attr.Type { + return map[string]attr.Type{ + "id": types.StringType, + "name": types.StringType, + "description": types.StringType, + "space_id": types.StringType, + "version": types.Int32Type, + "step_package_id": types.StringType, + "action_type": types.StringType, + "community_action_template_id": types.StringType, + "packages": types.ListType{ElemType: types.ObjectType{AttrTypes: GetStepTemplatePackageTypeAttributes()}}, + "parameters": types.ListType{ElemType: types.ObjectType{AttrTypes: GetStepTemplateParameterTypeAttributes()}}, + "properties": types.MapType{ElemType: types.StringType}, + } +} + +func GetStepTemplatePackageTypeAttributes() map[string]attr.Type { + return map[string]attr.Type{ + "id": types.StringType, + "acquisition_location": types.StringType, + "name": types.StringType, + "feed_id": types.StringType, + "package_id": types.StringType, + "properties": types.ObjectType{AttrTypes: GetStepTemplatePackagePropertiesTypeAttributes()}, + } +} + +func GetStepTemplatePackagePropertiesTypeAttributes() map[string]attr.Type { + return map[string]attr.Type{ + "extract": types.StringType, + "package_parameter_name": types.StringType, + "purpose": types.StringType, + "selection_mode": types.StringType, + } +} + +func GetStepTemplateParameterTypeAttributes() map[string]attr.Type { + return map[string]attr.Type{ + "id": types.StringType, + "name": types.StringType, + "label": types.StringType, + "help_text": types.StringType, + "display_settings": types.MapType{ElemType: types.StringType}, + "default_value": types.StringType, + } +} diff --git a/octopusdeploy_framework/util/util.go b/octopusdeploy_framework/util/util.go index 42ae11dd1..b67394f71 100644 --- a/octopusdeploy_framework/util/util.go +++ b/octopusdeploy_framework/util/util.go @@ -3,7 +3,6 @@ package util import ( "context" "fmt" - "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/types" @@ -59,6 +58,26 @@ func SetToStringArray(ctx context.Context, set types.Set) ([]string, diag.Diagno return convertedSet, diags } +func ConvertStringMapToAttrStringMap(strMap map[string]string) map[string]attr.Value { + attrMap := make(map[string]attr.Value, len(strMap)) + for key, val := range strMap { + attrMap[key] = types.StringValue(val) + } + return attrMap +} + +func ConvertAttrStringMapToStringMap(attrMap map[string]attr.Value) map[string]string { + nativeMap := make(map[string]string, len(attrMap)) + for key, val := range attrMap { + if val.IsNull() { + nativeMap[key] = "" + } else { + nativeMap[key] = val.(types.String).ValueString() + } + } + return nativeMap +} + func FlattenStringList(list []string) types.List { if list == nil { return types.ListValueMust(types.StringType, make([]attr.Value, 0))