Repository: honeycombio/terraform-provider-honeycombio (language: python)

Issue:
Value Conversion Error using a dynamic block on evaluation_schedule in honeycombio_trigger resource
<!---
Thank you for taking the time to report bugs!

We love code snippets and links to repositories that reproduce the issue, but understand if you don't have the time to add them. We'll do our best with the info you provide, and might ask follow-up questions.

Please see our [OSS process document](https://github.com/honeycombio/home/blob/main/honeycomb-oss-lifecycle-and-practices.md#) to get an idea of how we operate.
--->
Terraform Validate/Plan receives the following Value Conversion Error whenever I tried to use dynamic block on evaluation_schedule in `honeycombio_trigger`

```
terraform init && terraform plan
Initializing the backend...
Initializing provider plugins...
- Reusing previous version of honeycombio/honeycombio from the dependency lock file
- Reusing previous version of hashicorp/aws from the dependency lock file
- Using previously-installed honeycombio/honeycombio v0.27.0
- Using previously-installed hashicorp/aws v5.64.0

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.
╷
│ Error: Value Conversion Error
│ 
│   with honeycombio_trigger.this,
│   on main.tf line 44, in resource "honeycombio_trigger" "this":
│   44: resource "honeycombio_trigger" "this" {
│ 
│ An unexpected error was encountered trying to build a value. This is always an error in the provider.
│ Please report the following to the provider developer:
│ 
│ Received unknown value, however the target type cannot handle unknown values. Use the corresponding
│ `types` package type or a custom type that handles unknown values.
│ 
│ Path: evaluation_schedule
│ Target Type: []models.TriggerEvaluationScheduleModel
│ Suggested Type: basetypes.ListValue
╵
```

```
terraform validate
╷
│ Error: Value Conversion Error
│ 
│   with honeycombio_trigger.this,
│   on main.tf line 44, in resource "honeycombio_trigger" "this":
│   44: resource "honeycombio_trigger" "this" {
│ 
│ An unexpected error was encountered trying to build a value. This is always an error in the provider.
│ Please report the following to the provider developer:
│ 
│ Received unknown value, however the target type cannot handle unknown values. Use the corresponding
│ `types` package type or a custom type that handles unknown values.
│ 
│ Path: evaluation_schedule
│ Target Type: []models.TriggerEvaluationScheduleModel
│ Suggested Type: basetypes.ListValue
```
**Versions**
This affects honeycombio provider version `0.24.0` or higher. We were on `0.21.0` and was only able to update to version 0.23.0 as we keep running into the `Value Conversion Error`.

```
terraform --version
Terraform v1.9.3
on darwin_arm64
+ provider registry.terraform.io/hashicorp/aws v5.64.0
+ provider registry.terraform.io/honeycombio/honeycombio v0.27.0
```

**Steps to reproduce**
``` hcl
locals {
  dataset = "dataset"
  business_hour = {
    start_time = "14:00" # 9:00 EST
    end_time   = "22:00" # 17:00 EST
    days_of_week = [
      "monday",
      "tuesday",
      "wednesday",
      "thursday",
      "friday",
    ]
  }
}

variable "enable_evaluation_schedule_business_hour" {
  type    = bool
  default = true
}

resource "honeycombio_query" "this" {
  dataset    = local.dataset
  query_json = data.honeycombio_query_specification.this.json
}

resource "honeycombio_trigger" "this" {
  name = "test-trigger"

  query_id = honeycombio_query.this.id
  dataset  = local.dataset

  threshold {
    exceeded_limit = 1
    op             = ">"
    value          = "0"
  }

  dynamic "evaluation_schedule" {
    for_each = var.enable_evaluation_schedule_business_hour ? [true] : []

    content {
      start_time   = local.business_hour.start_time
      end_time     = local.business_hour.end_time
      days_of_week = local.business_hour.days_of_week
    }
  }
}

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "5.64"
    }
    honeycombio = {
      source  = "honeycombio/honeycombio"
      version = "~> 0.27.0"
    }
  }
}
```

**Additional context**

When I didn't use dynamic block for `evaluation_schedule`, it worked fine 

```
  evaluation_schedule {
    start_time   = local.business_hour.start_time
    end_time     = local.business_hour.end_time
    days_of_week = local.business_hour.days_of_week
  }
```


Discussion / hints:

Fix patch:
diff --git a/internal/models/notification_recipients.go b/internal/models/notification_recipients.go
index 7ce83fa7..e6ef1ff0 100644
--- a/internal/models/notification_recipients.go
+++ b/internal/models/notification_recipients.go
@@ -12,10 +12,6 @@ type NotificationRecipientModel struct {
 	Details types.List   `tfsdk:"notification_details"` // NotificationRecipientDetailsModel
 }
 
-type NotificationRecipientDetailsModel struct {
-	PDSeverity types.String `tfsdk:"pagerduty_severity"`
-}
-
 var NotificationRecipientAttrType = map[string]attr.Type{
 	"id":     types.StringType,
 	"type":   types.StringType,
@@ -25,6 +21,10 @@ var NotificationRecipientAttrType = map[string]attr.Type{
 	}},
 }
 
+type NotificationRecipientDetailsModel struct {
+	PDSeverity types.String `tfsdk:"pagerduty_severity"`
+}
+
 var NotificationRecipientDetailsAttrType = map[string]attr.Type{
 	"pagerduty_severity": types.StringType,
 }
diff --git a/internal/models/triggers.go b/internal/models/triggers.go
index 5fb1874c..dcd5b955 100644
--- a/internal/models/triggers.go
+++ b/internal/models/triggers.go
@@ -1,20 +1,23 @@
 package models
 
-import "github.com/hashicorp/terraform-plugin-framework/types"
+import (
+	"github.com/hashicorp/terraform-plugin-framework/attr"
+	"github.com/hashicorp/terraform-plugin-framework/types"
+)
 
 type TriggerResourceModel struct {
-	ID                 types.String                     `tfsdk:"id"`
-	Name               types.String                     `tfsdk:"name"`
-	Dataset            types.String                     `tfsdk:"dataset"`
-	Description        types.String                     `tfsdk:"description"`
-	Disabled           types.Bool                       `tfsdk:"disabled"`
-	QueryID            types.String                     `tfsdk:"query_id"`
-	QueryJson          types.String                     `tfsdk:"query_json"`
-	AlertType          types.String                     `tfsdk:"alert_type"`
-	Frequency          types.Int64                      `tfsdk:"frequency"`
-	Threshold          []TriggerThresholdModel          `tfsdk:"threshold"`
-	Recipients         types.Set                        `tfsdk:"recipient"` // NotificationRecipientModel
-	EvaluationSchedule []TriggerEvaluationScheduleModel `tfsdk:"evaluation_schedule"`
+	ID                 types.String `tfsdk:"id"`
+	Name               types.String `tfsdk:"name"`
+	Dataset            types.String `tfsdk:"dataset"`
+	Description        types.String `tfsdk:"description"`
+	Disabled           types.Bool   `tfsdk:"disabled"`
+	QueryID            types.String `tfsdk:"query_id"`
+	QueryJson          types.String `tfsdk:"query_json"`
+	AlertType          types.String `tfsdk:"alert_type"`
+	Frequency          types.Int64  `tfsdk:"frequency"`
+	Threshold          types.List   `tfsdk:"threshold"`           // TriggerThresholdModel
+	Recipients         types.Set    `tfsdk:"recipient"`           // NotificationRecipientModel
+	EvaluationSchedule types.List   `tfsdk:"evaluation_schedule"` // TriggerEvaluationScheduleModel
 }
 
 type TriggerThresholdModel struct {
@@ -23,8 +26,20 @@ type TriggerThresholdModel struct {
 	ExceededLimit types.Int64   `tfsdk:"exceeded_limit"`
 }
 
+var TriggerThresholdAttrType = map[string]attr.Type{
+	"op":             types.StringType,
+	"value":          types.Float64Type,
+	"exceeded_limit": types.Int64Type,
+}
+
 type TriggerEvaluationScheduleModel struct {
 	DaysOfWeek []types.String `tfsdk:"days_of_week"`
 	StartTime  types.String   `tfsdk:"start_time"`
 	EndTime    types.String   `tfsdk:"end_time"`
 }
+
+var TriggerEvaluationScheduleAttrType = map[string]attr.Type{
+	"days_of_week": types.ListType{ElemType: types.StringType},
+	"start_time":   types.StringType,
+	"end_time":     types.StringType,
+}
diff --git a/internal/provider/notification_recipients.go b/internal/provider/notification_recipients.go
index 90ad7ff4..e48f69a4 100644
--- a/internal/provider/notification_recipients.go
+++ b/internal/provider/notification_recipients.go
@@ -241,7 +241,6 @@ func notificationRecipientDetailsToList(ctx context.Context, details *client.Not
 		diags.Append(d...)
 		result, d = types.ListValueFrom(ctx, types.ObjectType{AttrTypes: models.NotificationRecipientDetailsAttrType}, []attr.Value{objVal})
 		diags.Append(d...)
-
 	} else {
 		result = types.ListNull(types.ObjectType{AttrTypes: models.NotificationRecipientDetailsAttrType})
 	}
diff --git a/internal/provider/trigger_resource.go b/internal/provider/trigger_resource.go
index 124ec645..f7e76aab 100644
--- a/internal/provider/trigger_resource.go
+++ b/internal/provider/trigger_resource.go
@@ -10,6 +10,8 @@ import (
 	"github.com/hashicorp/terraform-plugin-framework-validators/int64validator"
 	"github.com/hashicorp/terraform-plugin-framework-validators/listvalidator"
 	"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
+	"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/resource/schema"
@@ -241,10 +243,13 @@ func (r *triggerResource) Create(ctx context.Context, req resource.CreateRequest
 		Description:        plan.Description.ValueString(),
 		Disabled:           plan.Disabled.ValueBool(),
 		AlertType:          client.TriggerAlertType(plan.AlertType.ValueString()),
-		Threshold:          expandTriggerThreshold(plan.Threshold),
+		Threshold:          expandTriggerThreshold(ctx, plan.Threshold, &resp.Diagnostics),
 		Frequency:          int(plan.Frequency.ValueInt64()),
 		Recipients:         expandNotificationRecipients(ctx, plan.Recipients, &resp.Diagnostics),
-		EvaluationSchedule: expandTriggerEvaluationSchedule(plan.EvaluationSchedule),
+		EvaluationSchedule: expandTriggerEvaluationSchedule(ctx, plan.EvaluationSchedule, &resp.Diagnostics),
+	}
+	if resp.Diagnostics.HasError() {
+		return
 	}
 
 	specifiedByID := !plan.QueryID.IsNull()
@@ -265,7 +270,7 @@ func (r *triggerResource) Create(ctx context.Context, req resource.CreateRequest
 		newTrigger.Query = &q
 	}
 
-	if plan.EvaluationSchedule != nil {
+	if !plan.EvaluationSchedule.IsNull() {
 		newTrigger.EvaluationScheduleType = client.TriggerEvaluationScheduleWindow
 	}
 
@@ -281,9 +286,9 @@ func (r *triggerResource) Create(ctx context.Context, req resource.CreateRequest
 	state.Description = types.StringValue(trigger.Description)
 	state.Disabled = types.BoolValue(trigger.Disabled)
 	state.AlertType = types.StringValue(string(trigger.AlertType))
-	state.Threshold = flattenTriggerThreshold(trigger.Threshold)
+	state.Threshold = flattenTriggerThreshold(ctx, trigger.Threshold, &resp.Diagnostics)
 	state.Frequency = types.Int64Value(int64(trigger.Frequency))
-	state.EvaluationSchedule = flattenTriggerEvaluationSchedule(trigger)
+	state.EvaluationSchedule = flattenTriggerEvaluationSchedule(ctx, trigger.EvaluationSchedule, &resp.Diagnostics)
 	// we created them as authored so to avoid matching type-target or ID we can just use the same value
 	state.Recipients = config.Recipients
 
@@ -343,9 +348,9 @@ func (r *triggerResource) Read(ctx context.Context, req resource.ReadRequest, re
 	state.Description = types.StringValue(trigger.Description)
 	state.Disabled = types.BoolValue(trigger.Disabled)
 	state.AlertType = types.StringValue(string(trigger.AlertType))
-	state.Threshold = flattenTriggerThreshold(trigger.Threshold)
+	state.Threshold = flattenTriggerThreshold(ctx, trigger.Threshold, &resp.Diagnostics)
 	state.Frequency = types.Int64Value(int64(trigger.Frequency))
-	state.EvaluationSchedule = flattenTriggerEvaluationSchedule(trigger)
+	state.EvaluationSchedule = flattenTriggerEvaluationSchedule(ctx, trigger.EvaluationSchedule, &resp.Diagnostics)
 	state.Recipients = reconcileReadNotificationRecipientState(ctx, trigger.Recipients, state.Recipients, &resp.Diagnostics)
 
 	specifiedByID := !state.QueryID.IsNull()
@@ -385,9 +390,12 @@ func (r *triggerResource) Update(ctx context.Context, req resource.UpdateRequest
 		Disabled:           plan.Disabled.ValueBool(),
 		AlertType:          client.TriggerAlertType(plan.AlertType.ValueString()),
 		Frequency:          int(plan.Frequency.ValueInt64()),
-		Threshold:          expandTriggerThreshold(plan.Threshold),
+		Threshold:          expandTriggerThreshold(ctx, plan.Threshold, &resp.Diagnostics),
 		Recipients:         expandNotificationRecipients(ctx, plan.Recipients, &resp.Diagnostics),
-		EvaluationSchedule: expandTriggerEvaluationSchedule(plan.EvaluationSchedule),
+		EvaluationSchedule: expandTriggerEvaluationSchedule(ctx, plan.EvaluationSchedule, &resp.Diagnostics),
+	}
+	if resp.Diagnostics.HasError() {
+		return
 	}
 
 	specifiedByID := !plan.QueryID.IsNull()
@@ -432,8 +440,8 @@ func (r *triggerResource) Update(ctx context.Context, req resource.UpdateRequest
 	state.Disabled = types.BoolValue(trigger.Disabled)
 	state.AlertType = types.StringValue(string(trigger.AlertType))
 	state.Frequency = types.Int64Value(int64(trigger.Frequency))
-	state.Threshold = flattenTriggerThreshold(trigger.Threshold)
-	state.EvaluationSchedule = flattenTriggerEvaluationSchedule(trigger)
+	state.Threshold = flattenTriggerThreshold(ctx, trigger.Threshold, &resp.Diagnostics)
+	state.EvaluationSchedule = flattenTriggerEvaluationSchedule(ctx, trigger.EvaluationSchedule, &resp.Diagnostics)
 	// we created them as authored so to avoid matching type-target or ID we can just use the same value
 	state.Recipients = config.Recipients
 
@@ -497,11 +505,13 @@ func (r *triggerResource) ImportState(ctx context.Context, req resource.ImportSt
 	}
 
 	resp.Diagnostics.Append(resp.State.Set(ctx, &models.TriggerResourceModel{
-		ID:         types.StringValue(id),
-		Dataset:    types.StringValue(dataset),
-		QueryID:    types.StringNull(),
-		QueryJson:  types.StringUnknown(), // favor QueryJSON on import
-		Recipients: types.SetUnknown(types.ObjectType{AttrTypes: models.NotificationRecipientAttrType}),
+		ID:                 types.StringValue(id),
+		Dataset:            types.StringValue(dataset),
+		QueryID:            types.StringNull(),
+		QueryJson:          types.StringUnknown(), // favor QueryJSON on import
+		Recipients:         types.SetUnknown(types.ObjectType{AttrTypes: models.NotificationRecipientAttrType}),
+		Threshold:          types.ListUnknown(types.ObjectType{AttrTypes: models.TriggerThresholdAttrType}),
+		EvaluationSchedule: types.ListUnknown(types.ObjectType{AttrTypes: models.TriggerEvaluationScheduleAttrType}),
 	})...)
 }
 
@@ -600,8 +610,18 @@ func (r *triggerResource) ValidateConfig(ctx context.Context, req resource.Valid
 	}
 }
 
-func expandTriggerThreshold(t []models.TriggerThresholdModel) *client.TriggerThreshold {
-	if len(t) != 1 {
+func expandTriggerThreshold(
+	ctx context.Context,
+	l types.List,
+	diags *diag.Diagnostics,
+) *client.TriggerThreshold {
+	if l.IsNull() || l.IsUnknown() {
+		return nil
+	}
+
+	var t []models.TriggerThresholdModel
+	diags.Append(l.ElementsAs(ctx, &t, false)...)
+	if diags.HasError() {
 		return nil
 	}
 
@@ -612,48 +632,90 @@ func expandTriggerThreshold(t []models.TriggerThresholdModel) *client.TriggerThr
 	}
 }
 
-func flattenTriggerThreshold(t *client.TriggerThreshold) []models.TriggerThresholdModel {
-	return []models.TriggerThresholdModel{{
-		Op:            types.StringValue(string(t.Op)),
-		Value:         types.Float64Value(t.Value),
-		ExceededLimit: types.Int64Value(int64(t.ExceededLimit)),
-	}}
+func flattenTriggerThreshold(
+	ctx context.Context,
+	t *client.TriggerThreshold,
+	diags *diag.Diagnostics,
+) types.List {
+	if t == nil {
+		return types.ListNull(types.ObjectType{AttrTypes: models.TriggerThresholdAttrType})
+	}
+
+	thresholdObj, d := types.ObjectValue(models.TriggerThresholdAttrType, map[string]attr.Value{
+		"op":             types.StringValue(string(t.Op)),
+		"value":          types.Float64Value(t.Value),
+		"exceeded_limit": types.Int64Value(int64(t.ExceededLimit)),
+	})
+	diags.Append(d...)
+
+	result, d := types.ListValueFrom(
+		ctx,
+		types.ObjectType{AttrTypes: models.TriggerThresholdAttrType},
+		[]attr.Value{thresholdObj},
+	)
+	diags.Append(d...)
+
+	return result
 }
 
-func expandTriggerEvaluationSchedule(s []models.TriggerEvaluationScheduleModel) *client.TriggerEvaluationSchedule {
-	if s != nil {
-		days := make([]string, len(s[0].DaysOfWeek))
-		for i, d := range s[0].DaysOfWeek {
-			days[i] = d.ValueString()
-		}
+func expandTriggerEvaluationSchedule(
+	ctx context.Context,
+	l types.List,
+	diags *diag.Diagnostics,
+) *client.TriggerEvaluationSchedule {
+	if l.IsNull() || l.IsUnknown() {
+		return nil
+	}
 
-		return &client.TriggerEvaluationSchedule{
-			Window: client.TriggerEvaluationWindow{
-				StartTime:  s[0].StartTime.ValueString(),
-				EndTime:    s[0].EndTime.ValueString(),
-				DaysOfWeek: days,
-			},
-		}
+	var s []models.TriggerEvaluationScheduleModel
+	diags.Append(l.ElementsAs(ctx, &s, false)...)
+	if diags.HasError() {
+		return nil
+	}
+
+	days := make([]string, len(s[0].DaysOfWeek))
+	for i, d := range s[0].DaysOfWeek {
+		days[i] = d.ValueString()
 	}
 
-	return nil
+	return &client.TriggerEvaluationSchedule{
+		Window: client.TriggerEvaluationWindow{
+			StartTime:  s[0].StartTime.ValueString(),
+			EndTime:    s[0].EndTime.ValueString(),
+			DaysOfWeek: days,
+		},
+	}
 }
 
-func flattenTriggerEvaluationSchedule(t *client.Trigger) []models.TriggerEvaluationScheduleModel {
-	if t.EvaluationScheduleType == client.TriggerEvaluationScheduleWindow {
-		days := make([]basetypes.StringValue, len(t.EvaluationSchedule.Window.DaysOfWeek))
-		for i, d := range t.EvaluationSchedule.Window.DaysOfWeek {
-			days[i] = types.StringValue(d)
-		}
+func flattenTriggerEvaluationSchedule(
+	ctx context.Context,
+	w *client.TriggerEvaluationSchedule,
+	diags *diag.Diagnostics,
+) types.List {
+	if w == nil {
+		return types.ListNull(types.ObjectType{AttrTypes: models.TriggerEvaluationScheduleAttrType})
+	}
 
-		return []models.TriggerEvaluationScheduleModel{
-			{
-				StartTime:  types.StringValue(t.EvaluationSchedule.Window.StartTime),
-				EndTime:    types.StringValue(t.EvaluationSchedule.Window.EndTime),
-				DaysOfWeek: days,
-			},
-		}
+	days := make([]basetypes.StringValue, len(w.Window.DaysOfWeek))
+	for i, d := range w.Window.DaysOfWeek {
+		days[i] = types.StringValue(d)
 	}
+	daysObj, d := types.ListValueFrom(ctx, types.StringType, days)
+	diags.Append(d...)
+
+	scheduleObj, d := types.ObjectValue(models.TriggerEvaluationScheduleAttrType, map[string]attr.Value{
+		"days_of_week": daysObj,
+		"start_time":   types.StringValue(w.Window.StartTime),
+		"end_time":     types.StringValue(w.Window.EndTime),
+	})
+	diags.Append(d...)
+
+	result, d := types.ListValueFrom(
+		ctx,
+		types.ObjectType{AttrTypes: models.TriggerEvaluationScheduleAttrType},
+		[]attr.Value{scheduleObj},
+	)
+	diags.Append(d...)
 
-	return nil
+	return result
 }


Test patch:
diff --git a/internal/provider/trigger_resource_test.go b/internal/provider/trigger_resource_test.go
index 97dcb915..d1822750 100644
--- a/internal/provider/trigger_resource_test.go
+++ b/internal/provider/trigger_resource_test.go
@@ -406,6 +406,63 @@ resource "honeycombio_trigger" "test" {
 			},
 		},
 	})
+
+	t.Run("handles dynamic evaluation schedule block", func(t *testing.T) {
+		resource.Test(t, resource.TestCase{
+			PreCheck:                 testAccPreCheck(t),
+			ProtoV5ProviderFactories: testAccProtoV5MuxServerFactory,
+			Steps: []resource.TestStep{
+				{
+					Config: fmt.Sprintf(`
+locals {
+  business_hour = {
+    start_time = "14:00"
+    end_time   = "22:00"
+    days_of_week = [
+      "monday",
+      "tuesday",
+      "wednesday",
+      "thursday",
+      "friday",
+    ]
+  }
+}
+
+variable "enable_evaluation_schedule_business_hour" {
+  type    = bool
+  default = true
+}
+
+data "honeycombio_query_specification" "test" {}
+
+resource "honeycombio_trigger" "test" {
+  name     = "test"
+  dataset  = "%s"
+
+  query_json = data.honeycombio_query_specification.test.json
+
+  frequency = 1800
+
+  threshold {
+    exceeded_limit = 1
+    op             = ">"
+    value          = "0"
+  }
+
+  dynamic "evaluation_schedule" {
+    for_each = var.enable_evaluation_schedule_business_hour ? [true] : []
+
+    content {
+      start_time   = local.business_hour.start_time
+      end_time     = local.business_hour.end_time
+      days_of_week = local.business_hour.days_of_week
+    }
+  }
+}`, testAccDataset()),
+				},
+			},
+		})
+	})
 }
 
 func TestAcc_TriggerResourcePagerDutyUnsetSeverity(t *testing.T) {