Skip to content

Commit

Permalink
fix(variables): add UpgradeState method to migrate existing resources (
Browse files Browse the repository at this point in the history
…#297)

* copy to clipboard

* fix(variables): add state upgrade to migrate existing resources

* dry it up

* fix deprecated golangci-lint plugin
  • Loading branch information
parkedwards authored Nov 1, 2024
1 parent 47f66ff commit 0beb8cd
Show file tree
Hide file tree
Showing 4 changed files with 145 additions and 59 deletions.
2 changes: 1 addition & 1 deletion .golangci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ linters:
- errname
- errorlint
- exhaustive
- exportloopref
- copyloopvar
- forbidigo
- forcetypeassert
- gocheckcompilerdirectives
Expand Down
2 changes: 1 addition & 1 deletion .mise.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ golang = '1.21.10'
golangci-lint = '1.61.0'
goreleaser = '1.26.2'
pre-commit = '3.7.1'
terraform = '1.8.4'
terraform = '1.9.8'

# tomlv does not currently have an entry in the registry.
# For now, install with `go`:
Expand Down
5 changes: 4 additions & 1 deletion create-dev-testfile.sh
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,12 @@ provider "prefect" {}
resource "${resource}" "${name}" {}
EOF

cmd="cd ${dev_file_target} && terraform plan"
echo ""
echo "run:"
echo "cd ${dev_file_target} && terraform plan"
echo "${cmd}"
echo "(copied to clipboard)"
printf "${cmd}" | pbcopy
}

main $@
195 changes: 139 additions & 56 deletions internal/provider/resources/variable.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
var (
_ = resource.ResourceWithConfigure(&VariableResource{})
_ = resource.ResourceWithImportState(&VariableResource{})
_ = resource.ResourceWithUpgradeState(&VariableResource{})
)

// VariableResource contains state for the resource.
Expand All @@ -35,7 +36,26 @@ type VariableResource struct {
}

// VariableResourceModel defines the Terraform resource model.
type VariableResourceModel struct {
// NOTE: we version the VersionResourceModel here due to a schema
// update, and we want to be able to properly migrate existing
// prefect_variable resources in state from one schema version to the next.
// See UpgradeState for more details.
//
// V0: Value is types.String.
type VariableResourceModelV0 struct {
ID types.String `tfsdk:"id"`
Created customtypes.TimestampValue `tfsdk:"created"`
Updated customtypes.TimestampValue `tfsdk:"updated"`
AccountID customtypes.UUIDValue `tfsdk:"account_id"`
WorkspaceID customtypes.UUIDValue `tfsdk:"workspace_id"`

Name types.String `tfsdk:"name"`
Value types.String `tfsdk:"value"`
Tags types.List `tfsdk:"tags"`
}

// V1: Value is types.Dynamic.
type VariableResourceModelV1 struct {
ID types.String `tfsdk:"id"`
Created customtypes.TimestampValue `tfsdk:"created"`
Updated customtypes.TimestampValue `tfsdk:"updated"`
Expand All @@ -47,6 +67,59 @@ type VariableResourceModel struct {
Tags types.List `tfsdk:"tags"`
}

var defaultEmptyTagList, _ = basetypes.NewListValue(types.StringType, []attr.Value{})

var VariableResourceSchemaAttributes = map[string]schema.Attribute{
"id": schema.StringAttribute{
Computed: true,
// We cannot use a CustomType due to a conflict with PlanModifiers; see
// https://github.com/hashicorp/terraform-plugin-framework/issues/763
// https://github.com/hashicorp/terraform-plugin-framework/issues/754
Description: "Variable ID (UUID)",
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"created": schema.StringAttribute{
Computed: true,
CustomType: customtypes.TimestampType{},
Description: "Timestamp of when the resource was created (RFC3339)",
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"updated": schema.StringAttribute{
Computed: true,
CustomType: customtypes.TimestampType{},
Description: "Timestamp of when the resource was updated (RFC3339)",
},
"account_id": schema.StringAttribute{
CustomType: customtypes.UUIDType{},
Description: "Account ID (UUID), defaults to the account set in the provider",
Optional: true,
},
"workspace_id": schema.StringAttribute{
CustomType: customtypes.UUIDType{},
Description: "Workspace ID (UUID), defaults to the workspace set in the provider",
Optional: true,
},
"name": schema.StringAttribute{
Description: "Name of the variable",
Required: true,
},
"value": schema.DynamicAttribute{
Description: "Value of the variable, supported Terraform value types: string, number, bool, tuple, object",
Required: true,
},
"tags": schema.ListAttribute{
Description: "Tags associated with the variable",
ElementType: types.StringType,
Optional: true,
Computed: true,
Default: listdefault.StaticValue(defaultEmptyTagList),
},
}

// NewVariableResource returns a new VariableResource.
//
//nolint:ireturn // required by Terraform API
Expand Down Expand Up @@ -77,68 +150,78 @@ func (r *VariableResource) Configure(_ context.Context, req resource.ConfigureRe

// Schema defines the schema for the resource.
func (r *VariableResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
defaultEmptyTagList, _ := basetypes.NewListValue(types.StringType, []attr.Value{})

resp.Schema = schema.Schema{
Description: "The resource `variable` represents a Prefect Cloud Variable. " +
"Variables enable you to store and reuse non-sensitive information in your flows. ",
Version: 0,
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Computed: true,
// We cannot use a CustomType due to a conflict with PlanModifiers; see
// https://github.com/hashicorp/terraform-plugin-framework/issues/763
// https://github.com/hashicorp/terraform-plugin-framework/issues/754
Description: "Variable ID (UUID)",
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"created": schema.StringAttribute{
Computed: true,
CustomType: customtypes.TimestampType{},
Description: "Timestamp of when the resource was created (RFC3339)",
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
Version: 1,
Attributes: VariableResourceSchemaAttributes,
}
}

// UpgradeState adds upgraders to the VariableResource.
// This is needed when a resource schema change is made (eg. an attribute type).
// The key/index in the return object is the source version (eg. 0 -> current).
// The target version is the one defined in Schema.Version above
// https://developer.hashicorp.com/terraform/plugin/framework/resources/state-upgrade
func (r *VariableResource) UpgradeState(_ context.Context) map[int64]resource.StateUpgrader {
return map[int64]resource.StateUpgrader{
// State upgrade implementation from prior (0) => current (Schema.Version)
0: {
// PriorSchema allows the framework to populate the req.State argument
// for easier data handling when migrating existing state resources
// to a new schema.
PriorSchema: &schema.Schema{
Attributes: map[string]schema.Attribute{
"id": VariableResourceSchemaAttributes["id"],
"created": VariableResourceSchemaAttributes["created"],
"updated": VariableResourceSchemaAttributes["updated"],
"account_id": VariableResourceSchemaAttributes["account_id"],
"workspace_id": VariableResourceSchemaAttributes["workspace_id"],
"name": VariableResourceSchemaAttributes["name"],
"value": schema.StringAttribute{
Required: true,
},
"tags": VariableResourceSchemaAttributes["tags"],
},
},
"updated": schema.StringAttribute{
Computed: true,
CustomType: customtypes.TimestampType{},
Description: "Timestamp of when the resource was updated (RFC3339)",
},
"account_id": schema.StringAttribute{
CustomType: customtypes.UUIDType{},
Description: "Account ID (UUID), defaults to the account set in the provider",
Optional: true,
},
"workspace_id": schema.StringAttribute{
CustomType: customtypes.UUIDType{},
Description: "Workspace ID (UUID), defaults to the workspace set in the provider",
Optional: true,
},
"name": schema.StringAttribute{
Description: "Name of the variable",
Required: true,
},
"value": schema.DynamicAttribute{
Description: "Value of the variable, supported Terraform value types: string, number, bool, tuple, object",
Required: true,
},
"tags": schema.ListAttribute{
Description: "Tags associated with the variable",
ElementType: types.StringType,
Optional: true,
Computed: true,
Default: listdefault.StaticValue(defaultEmptyTagList),
StateUpgrader: func(ctx context.Context, req resource.UpgradeStateRequest, resp *resource.UpgradeStateResponse) {
var priorStateData VariableResourceModelV0

resp.Diagnostics.Append(req.State.Get(ctx, &priorStateData)...)

if resp.Diagnostics.HasError() {
return
}

// In order to update a prefect_variable resource in state
// that is tied to the old schema version, we need to copy
// the existing state into the new schema version.
upgradedStateData := VariableResourceModelV1{
ID: priorStateData.ID,
Created: priorStateData.Created,
Updated: priorStateData.Updated,
AccountID: priorStateData.AccountID,
WorkspaceID: priorStateData.WorkspaceID,
Name: priorStateData.Name,
Tags: priorStateData.Tags,
}

// This is the main upgrade operation between v0 => v1.
// Convert the "value" attribute's type from
// StringAttribute (v0) to a DynamicValue (v1)
// to prevent a Terraform error when deserializing the state
// from the old schema to the new one.
upgradedStateData.Value = types.DynamicValue(basetypes.NewStringValue(priorStateData.Value.ValueString()))

resp.Diagnostics.Append(resp.State.Set(ctx, upgradedStateData)...)
},
},
}
}

// copyVariableToModel maps an API response to a model that is saved in Terraform state.
// A model can be a Terraform Plan, State, or Config object.
func copyVariableToModel(ctx context.Context, variable *api.Variable, tfModel *VariableResourceModel) diag.Diagnostics {
func copyVariableToModel(ctx context.Context, variable *api.Variable, tfModel *VariableResourceModelV1) diag.Diagnostics {
tfModel.ID = types.StringValue(variable.ID.String())
tfModel.Created = customtypes.NewTimestampPointerValue(variable.Created)
tfModel.Updated = customtypes.NewTimestampPointerValue(variable.Updated)
Expand All @@ -156,7 +239,7 @@ func copyVariableToModel(ctx context.Context, variable *api.Variable, tfModel *V

// getUnderlyingValue converts the 'value' attribute from a DynamicValue to
// a native Go type that can be sent to the Prefect API.
func getUnderlyingValue(plan VariableResourceModel) (interface{}, diag.Diagnostics) {
func getUnderlyingValue(plan VariableResourceModelV1) (interface{}, diag.Diagnostics) {
var diags diag.Diagnostics
var value interface{}

Expand Down Expand Up @@ -208,7 +291,7 @@ func getUnderlyingValue(plan VariableResourceModel) (interface{}, diag.Diagnosti

// Create creates the resource and sets the initial Terraform state.
func (r *VariableResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var plan VariableResourceModel
var plan VariableResourceModelV1

// Populate the model from resource configuration and emit diagnostics on error
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
Expand Down Expand Up @@ -259,7 +342,7 @@ func (r *VariableResource) Create(ctx context.Context, req resource.CreateReques

// Read refreshes the Terraform state with the latest data.
func (r *VariableResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var state VariableResourceModel
var state VariableResourceModelV1

// Populate the model from state and emit diagnostics on error
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
Expand Down Expand Up @@ -319,7 +402,7 @@ func (r *VariableResource) Read(ctx context.Context, req resource.ReadRequest, r

// Update updates the resource and sets the updated Terraform state on success.
func (r *VariableResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var plan VariableResourceModel
var plan VariableResourceModelV1

resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
if resp.Diagnostics.HasError() {
Expand Down Expand Up @@ -382,7 +465,7 @@ func (r *VariableResource) Update(ctx context.Context, req resource.UpdateReques

// Delete deletes the resource and removes the Terraform state on success.
func (r *VariableResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var state VariableResourceModel
var state VariableResourceModelV1

resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
Expand Down

0 comments on commit 0beb8cd

Please sign in to comment.