From 008f1560b8ba2680c4ee04603493855487eaf78e Mon Sep 17 00:00:00 2001 From: David Bloss Date: Mon, 15 Jul 2024 13:10:10 -0700 Subject: [PATCH] Db/add remaining state upgraders (#404) * WIP: add StateUpgraders for manual check and infra resources * fix StateUpgraders for infra and manual check resources * drop unused code --- .../unreleased/Bugfix-20240715-143237.yaml | 4 + ...datasource_opslevel_webhook_actions_all.go | 13 +-- opslevel/resource_opslevel_check_manual.go | 81 +++++++++++++++ opslevel/resource_opslevel_infra.go | 99 +++++++++++++++++++ 4 files changed, 187 insertions(+), 10 deletions(-) create mode 100644 .changes/unreleased/Bugfix-20240715-143237.yaml diff --git a/.changes/unreleased/Bugfix-20240715-143237.yaml b/.changes/unreleased/Bugfix-20240715-143237.yaml new file mode 100644 index 00000000..952d3849 --- /dev/null +++ b/.changes/unreleased/Bugfix-20240715-143237.yaml @@ -0,0 +1,4 @@ +kind: Bugfix +body: fix Terraform state upgrades for previous provider versions on infrastructure + and manual check resources +time: 2024-07-15T14:32:37.017279-05:00 diff --git a/opslevel/datasource_opslevel_webhook_actions_all.go b/opslevel/datasource_opslevel_webhook_actions_all.go index 1461478b..b53fb016 100644 --- a/opslevel/datasource_opslevel_webhook_actions_all.go +++ b/opslevel/datasource_opslevel_webhook_actions_all.go @@ -4,8 +4,6 @@ import ( "context" "fmt" - "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-log/tflog" @@ -26,13 +24,12 @@ type webhookActionDataSourcesAllModel struct { WebhookActions []webhookActionDataSourceModel `tfsdk:"webhook_actions"` } -func newWebhookActionDataSourcesAllModel(ctx context.Context, webhookActions []opslevel.CustomActionsExternalAction) (webhookActionDataSourcesAllModel, diag.Diagnostics) { - var diags diag.Diagnostics +func newWebhookActionDataSourcesAllModel(webhookActions []opslevel.CustomActionsExternalAction) webhookActionDataSourcesAllModel { webhookActionModels := make([]webhookActionDataSourceModel, 0) for _, webhookAction := range webhookActions { webhookActionModels = append(webhookActionModels, newWebhookActionDataSourceModel(webhookAction)) } - return webhookActionDataSourcesAllModel{WebhookActions: webhookActionModels}, diags + return webhookActionDataSourcesAllModel{WebhookActions: webhookActionModels} } func (d *WebhookActionDataSourcesAll) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { @@ -69,11 +66,7 @@ func (d *WebhookActionDataSourcesAll) Read(ctx context.Context, req datasource.R resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to list webhookActions, got error: %s", err)) return } - stateModel, diags := newWebhookActionDataSourcesAllModel(ctx, webhookActions.Nodes) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } + stateModel = newWebhookActionDataSourcesAllModel(webhookActions.Nodes) tflog.Trace(ctx, "listed all OpsLevel WebhookAction data sources") resp.Diagnostics.Append(resp.State.Set(ctx, &stateModel)...) diff --git a/opslevel/resource_opslevel_check_manual.go b/opslevel/resource_opslevel_check_manual.go index f13b4c95..e6dae3de 100644 --- a/opslevel/resource_opslevel_check_manual.go +++ b/opslevel/resource_opslevel_check_manual.go @@ -6,11 +6,13 @@ import ( "strings" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "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/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/opslevel/opslevel-go/v2024" "github.com/relvacode/iso8601" @@ -36,6 +38,12 @@ type CheckUpdateFrequency struct { Value types.Int64 `tfsdk:"value"` } +var updateFrequencyTypeV0 = map[string]attr.Type{ + "starting_data": types.StringType, + "time_value": types.StringType, + "value": types.StringType, +} + type CheckManualResourceModel struct { Category types.String `tfsdk:"category"` Description types.String `tfsdk:"description"` @@ -92,6 +100,7 @@ func (r *CheckManualResource) Metadata(ctx context.Context, req resource.Metadat func (r *CheckManualResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = schema.Schema{ + Version: 1, // This description is used by the documentation generator and the language server. MarkdownDescription: "Check Manual Resource", @@ -128,6 +137,78 @@ func (r *CheckManualResource) Schema(ctx context.Context, req resource.SchemaReq } } +func (r *CheckManualResource) UpgradeState(ctx context.Context) map[int64]resource.StateUpgrader { + return map[int64]resource.StateUpgrader{ + // State upgrade implementation from 0 (prior state version) to 1 (Schema.Version) + 0: { + PriorSchema: &schema.Schema{ + Description: "Check Repository File Resource", + Attributes: getCheckBaseSchemaV0(map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "The ID of this resource.", + Computed: true, + }, + "update_requires_comment": schema.BoolAttribute{ + Description: "Whether the check requires a comment or not.", + Optional: true, + }, + }), + Blocks: map[string]schema.Block{ + "update_frequency": schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "starting_data": schema.StringAttribute{ + Description: "The date that the check will start to evaluate.", + Required: true, + }, + "time_scale": schema.StringAttribute{ + Description: "The time scale type for the frequency.", + Required: true, + }, + "value": schema.Int64Attribute{ + Description: "The value to be used together with the frequency time_scale.", + Required: true, + }, + }, + }, + }, + }, + }, + StateUpgrader: func(ctx context.Context, req resource.UpgradeStateRequest, resp *resource.UpgradeStateResponse) { + // var diags diag.Diagnostics + upgradedStateModel := CheckManualResourceModel{} + updateFrequencyList := types.ListNull(types.ObjectType{AttrTypes: updateFrequencyTypeV0}) + + // base check attributes + resp.Diagnostics.Append(req.State.GetAttribute(ctx, path.Root("category"), &upgradedStateModel.Category)...) + resp.Diagnostics.Append(req.State.GetAttribute(ctx, path.Root("enable_on"), &upgradedStateModel.EnableOn)...) + resp.Diagnostics.Append(req.State.GetAttribute(ctx, path.Root("enabled"), &upgradedStateModel.Enabled)...) + resp.Diagnostics.Append(req.State.GetAttribute(ctx, path.Root("filter"), &upgradedStateModel.Filter)...) + resp.Diagnostics.Append(req.State.GetAttribute(ctx, path.Root("id"), &upgradedStateModel.Id)...) + resp.Diagnostics.Append(req.State.GetAttribute(ctx, path.Root("level"), &upgradedStateModel.Level)...) + resp.Diagnostics.Append(req.State.GetAttribute(ctx, path.Root("name"), &upgradedStateModel.Name)...) + resp.Diagnostics.Append(req.State.GetAttribute(ctx, path.Root("notes"), &upgradedStateModel.Notes)...) + resp.Diagnostics.Append(req.State.GetAttribute(ctx, path.Root("owner"), &upgradedStateModel.Owner)...) + + // repository file specific attributes + resp.Diagnostics.Append(req.State.GetAttribute(ctx, path.Root("update_requires_comment"), &upgradedStateModel.UpdateRequiresComment)...) + resp.Diagnostics.Append(req.State.GetAttribute(ctx, path.Root("update_frequency"), &updateFrequencyList)...) + if len(updateFrequencyList.Elements()) == 1 { + updateFrequency := updateFrequencyList.Elements()[0].(basetypes.ObjectValue) + updateFrequencyAttrs := updateFrequency.Attributes() + upgradedStateModel.UpdateFrequency = &CheckUpdateFrequency{ + StartingDate: updateFrequencyAttrs["starting_data"].(basetypes.StringValue), + TimeScale: updateFrequencyAttrs["time_scale"].(basetypes.StringValue), + Value: updateFrequencyAttrs["value"].(basetypes.Int64Value), + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, upgradedStateModel)...) + }, + }, + } +} + func (r *CheckManualResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { var planModel CheckManualResourceModel diff --git a/opslevel/resource_opslevel_infra.go b/opslevel/resource_opslevel_infra.go index aff1d8eb..31fb91b8 100644 --- a/opslevel/resource_opslevel_infra.go +++ b/opslevel/resource_opslevel_infra.go @@ -4,9 +4,11 @@ import ( "context" "fmt" + "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/types/basetypes" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" @@ -37,6 +39,13 @@ type InfraProviderData struct { Url types.String `tfsdk:"url"` } +var infraProviderDataType = map[string]attr.Type{ + "account": types.StringType, + "name": types.StringType, + "type": types.StringType, + "url": types.StringType, +} + func newInfraProviderData(infrastructure opslevel.InfrastructureResource) *InfraProviderData { return &InfraProviderData{ Account: RequiredStringValue(infrastructure.ProviderData.AccountName), @@ -85,6 +94,7 @@ func (r *InfrastructureResource) Metadata(ctx context.Context, req resource.Meta func (r *InfrastructureResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = schema.Schema{ + Version: 1, // This description is used by the documentation generator and the language server. MarkdownDescription: "Infrastructure Resource", @@ -144,6 +154,95 @@ func (r *InfrastructureResource) Schema(ctx context.Context, req resource.Schema } } +func (r *InfrastructureResource) UpgradeState(ctx context.Context) map[int64]resource.StateUpgrader { + return map[int64]resource.StateUpgrader{ + // State upgrade implementation from 0 (prior state version) to 1 (Schema.Version) + 0: { + PriorSchema: &schema.Schema{ + Description: "Infrastructure Resource", + Attributes: map[string]schema.Attribute{ + "aliases": schema.SetAttribute{ + ElementType: types.StringType, + Description: "The aliases for the infrastructure resource.", + Optional: true, + }, + "data": schema.StringAttribute{ + Description: "The data of the infrastructure resource in JSON format.", + Required: true, + Validators: []validator.String{ + JsonStringValidator(), + JsonHasNameKeyValidator(), + }, + }, + "id": schema.StringAttribute{ + Description: "The ID of the infrastructure.", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "owner": schema.StringAttribute{ + Description: "The id of the team that owns the infrastructure resource. Does not support aliases!", + Required: true, + Validators: []validator.String{IdStringValidator()}, + }, + "schema": schema.StringAttribute{ + Description: "The schema of the infrastructure resource that determines its data specification.", + Required: true, + }, + }, + Blocks: map[string]schema.Block{ + "provider_data": schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "account": schema.StringAttribute{ + Description: "The canonical account name for the provider of the infrastructure resource.", + Required: true, + }, + "name": schema.StringAttribute{ + Description: "The name of the provider of the infrastructure resource. (eg. AWS, GCP, Azure)", + Optional: true, + }, + "type": schema.StringAttribute{ + Description: "The type of the infrastructure resource as defined by its provider.", + Optional: true, + }, + "url": schema.StringAttribute{ + Description: "The url for the provider of the infrastructure resource.", + Optional: true, + }, + }, + }, + }, + }, + }, + StateUpgrader: func(ctx context.Context, req resource.UpgradeStateRequest, resp *resource.UpgradeStateResponse) { + upgradedStateModel := InfrastructureResourceModel{} + infraProviderDataList := types.ListNull(types.ObjectType{AttrTypes: infraProviderDataType}) + + resp.Diagnostics.Append(req.State.GetAttribute(ctx, path.Root("aliases"), &upgradedStateModel.Aliases)...) + resp.Diagnostics.Append(req.State.GetAttribute(ctx, path.Root("data"), &upgradedStateModel.Data)...) + resp.Diagnostics.Append(req.State.GetAttribute(ctx, path.Root("id"), &upgradedStateModel.Id)...) + resp.Diagnostics.Append(req.State.GetAttribute(ctx, path.Root("owner"), &upgradedStateModel.Owner)...) + resp.Diagnostics.Append(req.State.GetAttribute(ctx, path.Root("schema"), &upgradedStateModel.Schema)...) + resp.Diagnostics.Append(req.State.GetAttribute(ctx, path.Root("provider_data"), &infraProviderDataList)...) + if len(infraProviderDataList.Elements()) == 1 { + infraProviderData := infraProviderDataList.Elements()[0].(basetypes.ObjectValue) + infraProviderDataAttrs := infraProviderData.Attributes() + upgradedStateModel.ProviderData = &InfraProviderData{ + Account: infraProviderDataAttrs["account"].(basetypes.StringValue), + Name: infraProviderDataAttrs["name"].(basetypes.StringValue), + Type: infraProviderDataAttrs["type"].(basetypes.StringValue), + Url: infraProviderDataAttrs["url"].(basetypes.StringValue), + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, upgradedStateModel)...) + }, + }, + } +} + func (r *InfrastructureResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { var planModel InfrastructureResourceModel