Skip to content

Commit

Permalink
fix(variable): change 'value' type to Dynamic (#277)
Browse files Browse the repository at this point in the history
* fix(variable): change 'value' type to JSON

Changes the Variable 'value' type from string to JSON.

In PrefectHQ/prefect#13543 and associated
changes around May 2024, Variables were updated from simple strings to
JSON objects. The Terraform provider has still been treating them as
strings, so when folks tried to put JSON-compatible values in them,
Terraform would fail to work with them as found in
#254

Related to https://linear.app/prefect/issue/PLA-247/changing-variable-to-a-json-value-in-the-ui-makes-next-terraform-run

Related to #254

* Datasource: use Dynamic attribute

* Change variable attribute type to dynamic

The JSON custom type won't work for variables, because they can actually
be almost any type - including standalone numbers and strings.

* Update test to change value types

Goes from a string to a bool in the test to confirm variables can store
multiple types.

* Correctly test the name change

Before, when trying to test a variable resource name change, we were
actually creating an entirely new resource because we use the same
(randomized) value for the resource name and the attribute 'name'.

To ensure we're updating the same resource, this hard-codes the resource
name to 'test'. This shouldn't cause any conflicts with other tests
because the resource is created in an ephemeral workspace.

* Support and test bool, number, and object

* Tidy up

* Support tuple, correctly test tuple and object

* Generate Terraform Docs

* Document supported value types

* Datasource: break value type checking into func

It's big enough that it helps to have it separated out.

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
  • Loading branch information
mitchnielsen and github-actions[bot] authored Oct 25, 2024
1 parent 956d9c6 commit e98e2d7
Show file tree
Hide file tree
Showing 6 changed files with 226 additions and 56 deletions.
2 changes: 1 addition & 1 deletion docs/data-sources/variable.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,4 @@ data "prefect_variable" "existing_by_name" {
- `created` (String) Timestamp of when the resource was created (RFC3339)
- `tags` (List of String) Tags associated with the variable
- `updated` (String) Timestamp of when the resource was updated (RFC3339)
- `value` (String) Value of the variable
- `value` (Dynamic) Value of the variable, supported Terraform value types: string, number, bool, tuple, object
2 changes: 1 addition & 1 deletion docs/resources/variable.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ resource "prefect_variable" "example" {
### Required

- `name` (String) Name of the variable
- `value` (String) Value of the variable
- `value` (Dynamic) Value of the variable, supported Terraform value types: string, number, bool, tuple, object

### Optional

Expand Down
18 changes: 9 additions & 9 deletions internal/api/variables.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,23 +19,23 @@ type VariablesClient interface {
// Variable is a representation of a variable.
type Variable struct {
BaseModel
Name string `json:"name"`
Value string `json:"value"`
Tags []string `json:"tags"`
Name string `json:"name"`
Value interface{} `json:"value"`
Tags []string `json:"tags"`
}

// VariableCreate is a subset of Variable used when creating variables.
type VariableCreate struct {
Name string `json:"name"`
Value string `json:"value"`
Tags []string `json:"tags"`
Name string `json:"name"`
Value interface{} `json:"value"`
Tags []string `json:"tags"`
}

// VariableUpdate is a subset of Variable used when updating variables.
type VariableUpdate struct {
Name string `json:"name"`
Value string `json:"value"`
Tags []string `json:"tags"`
Name string `json:"name"`
Value interface{} `json:"value"`
Tags []string `json:"tags"`
}

// VariableFilterSettings defines settings when searching for variables.
Expand Down
81 changes: 75 additions & 6 deletions internal/provider/datasources/variable.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,16 @@ package datasources

import (
"context"
"encoding/json"
"fmt"

"github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"

"github.com/prefecthq/terraform-provider-prefect/internal/api"
"github.com/prefecthq/terraform-provider-prefect/internal/provider/customtypes"
Expand All @@ -27,9 +33,9 @@ type VariableDataSourceModel struct {
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"`
Name types.String `tfsdk:"name"`
Value types.Dynamic `tfsdk:"value"`
Tags types.List `tfsdk:"tags"`
}

// NewVariableDataSource returns a new VariableDataSource.
Expand Down Expand Up @@ -92,9 +98,9 @@ var variableAttributes = map[string]schema.Attribute{
Description: "Name of the variable",
Optional: true,
},
"value": schema.StringAttribute{
"value": schema.DynamicAttribute{
Computed: true,
Description: "Value of the variable",
Description: "Value of the variable, supported Terraform value types: string, number, bool, tuple, object",
},
"tags": schema.ListAttribute{
Computed: true,
Expand Down Expand Up @@ -168,7 +174,13 @@ func (d *VariableDataSource) Read(ctx context.Context, req datasource.ReadReques
model.Updated = customtypes.NewTimestampPointerValue(variable.Updated)

model.Name = types.StringValue(variable.Name)
model.Value = types.StringValue(variable.Value)

value, diags := getDynamicValue(model, variable)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
model.Value = value

list, diags := types.ListValueFrom(ctx, types.StringType, variable.Tags)
resp.Diagnostics.Append(diags...)
Expand All @@ -182,3 +194,60 @@ func (d *VariableDataSource) Read(ctx context.Context, req datasource.ReadReques
return
}
}

// getDynamicValue converts the 'value' attribute from a native Go type to a DynamicValue.
func getDynamicValue(model VariableDataSourceModel, variable *api.Variable) (basetypes.DynamicValue, diag.Diagnostics) {
var result basetypes.DynamicValue
var diags diag.Diagnostics

switch value := variable.Value.(type) {
case string:
result = types.DynamicValue(types.StringValue(value))

case float64:
result = types.DynamicValue(types.Float64Value(value))

case bool:
result = types.DynamicValue(types.BoolValue(value))

case map[string]interface{}:
byteSlice, err := json.Marshal(value)
if err != nil {
diags = append(diags, helpers.SerializeDataErrorDiagnostic("data", "Variable Value", err))
}

model.Value = types.DynamicValue(jsontypes.NewNormalizedValue(string(byteSlice)))

case []interface{}:
tupleTypes := make([]attr.Type, len(value))
tupleValues := make([]attr.Value, len(value))

for i, v := range value {
// For now, we only support string values in tuples.
// This can be expanded in the future when we're ready to type check
// inside of a type check :).
tupleTypes[i] = types.StringType

val, ok := v.(string)
if !ok {
diags.Append(helpers.SerializeDataErrorDiagnostic("data", "Variable Value", fmt.Errorf("unable to convert variable value to string")))
}

tupleValues[i] = types.StringValue(val)
}

tupleValue, diags := types.TupleValue(tupleTypes, tupleValues)
if diags.HasError() {
diags.Append(diags...)

return result, diags
}

result = types.DynamicValue(tupleValue)

default:
diags.Append(helpers.ResourceClientErrorDiagnostic("Variable", "type", fmt.Errorf("unsupported type: %T", value)))
}

return result, diags
}
81 changes: 73 additions & 8 deletions internal/provider/resources/variable.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ package resources

import (
"context"
"encoding/json"
"fmt"
"strconv"
"strings"

"github.com/google/uuid"
Expand Down Expand Up @@ -39,9 +42,9 @@ type VariableResourceModel struct {
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"`
Name types.String `tfsdk:"name"`
Value types.Dynamic `tfsdk:"value"`
Tags types.List `tfsdk:"tags"`
}

// NewVariableResource returns a new VariableResource.
Expand Down Expand Up @@ -118,8 +121,8 @@ func (r *VariableResource) Schema(_ context.Context, _ resource.SchemaRequest, r
Description: "Name of the variable",
Required: true,
},
"value": schema.StringAttribute{
Description: "Value of the variable",
"value": schema.DynamicAttribute{
Description: "Value of the variable, supported Terraform value types: string, number, bool, tuple, object",
Required: true,
},
"tags": schema.ListAttribute{
Expand All @@ -141,7 +144,6 @@ func copyVariableToModel(ctx context.Context, variable *api.Variable, tfModel *V
tfModel.Updated = customtypes.NewTimestampPointerValue(variable.Updated)

tfModel.Name = types.StringValue(variable.Name)
tfModel.Value = types.StringValue(variable.Value)

tags, diags := types.ListValueFrom(ctx, types.StringType, variable.Tags)
if diags.HasError() {
Expand All @@ -152,6 +154,58 @@ func copyVariableToModel(ctx context.Context, variable *api.Variable, tfModel *V
return nil
}

// 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) {
var diags diag.Diagnostics
var value interface{}

switch underlyingValue := plan.Value.UnderlyingValue().(type) {
case types.String:
value = underlyingValue.ValueString()

case types.Number:
var err error
value, err = strconv.ParseFloat(underlyingValue.String(), 64)
if err != nil {
diags.Append(diag.NewErrorDiagnostic(
"unable to convert number to float64",
fmt.Sprintf("number: %v, error: %v", value, err),
))
}

case types.Bool:
value = underlyingValue.ValueBool()

case types.Tuple:
result := make([]string, len(underlyingValue.Elements()))
for i, e := range underlyingValue.Elements() {
result[i] = e.String()
}

value = result

case types.Object:
result := map[string]interface{}{}
if err := json.Unmarshal([]byte(underlyingValue.String()), &result); err != nil {
diags.Append(diag.NewErrorDiagnostic(
"unable to convert object to map[string]interface",
fmt.Sprintf("object: %v, error: %v", value, err),
))
}

value = result

default:
diags.Append(diag.NewErrorDiagnostic(
"unexpected value type",
fmt.Sprintf("type: %T", underlyingValue),
))
}

return value, diags
}

// 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
Expand All @@ -162,6 +216,11 @@ func (r *VariableResource) Create(ctx context.Context, req resource.CreateReques
return
}

value, diags := getUnderlyingValue(plan)
if diags.HasError() {
return
}

var tags []string
resp.Diagnostics.Append(plan.Tags.ElementsAs(ctx, &tags, false)...)
if resp.Diagnostics.HasError() {
Expand All @@ -177,9 +236,10 @@ func (r *VariableResource) Create(ctx context.Context, req resource.CreateReques

variable, err := client.Create(ctx, api.VariableCreate{
Name: plan.Name.ValueString(),
Value: plan.Value.ValueString(),
Value: value,
Tags: tags,
})

if err != nil {
resp.Diagnostics.Append(helpers.ResourceClientErrorDiagnostic("Variable", "create", err))

Expand Down Expand Up @@ -273,6 +333,11 @@ func (r *VariableResource) Update(ctx context.Context, req resource.UpdateReques
return
}

value, diags := getUnderlyingValue(plan)
if diags.HasError() {
return
}

var tags []string
resp.Diagnostics.Append(plan.Tags.ElementsAs(ctx, &tags, false)...)
if resp.Diagnostics.HasError() {
Expand All @@ -288,7 +353,7 @@ func (r *VariableResource) Update(ctx context.Context, req resource.UpdateReques

err = client.Update(ctx, variableID, api.VariableUpdate{
Name: plan.Name.ValueString(),
Value: plan.Value.ValueString(),
Value: value,
Tags: tags,
})
if err != nil {
Expand Down
Loading

0 comments on commit e98e2d7

Please sign in to comment.