Repository: honeycombio/terraform-provider-honeycombio
Pull request #694 (2025-06-05 15:31:25)

feat(r/flexible_board): add support for tags on Boards
<!--
Thank you for contributing to the project! 💜
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.
-->

## Which problem is this PR solving?

Adds support for tagging (flexible type) Board resources

## Short description of the changes

update create, read and update methods to support tags on boards.

## How to verify that this has the expected result

1. Create a flexible board with tags: 
```
resource "honeycombio_flexible_board" "overview" {
  name        = "Now With Tags"
  description = "tag all the things"
  tags = {
    team = "blue"
    env = "staging"
  }
}
```

2. Update the tags and the state should show the latest updated values




Patch:
diff --git a/client/board.go b/client/board.go
index 44f0b04..4e92dfe 100644
--- a/client/board.go
+++ b/client/board.go
@@ -67,6 +67,8 @@ type Board struct {
 	Queries []BoardQuery `json:"queries"`
 	// A list of SLO IDs to be added to the board
 	SLOs []string `json:"slos"`
+	// A list of tags to organize the Board, for flexible boards only
+	Tags []Tag `json:"tags"`
 }
 
 // BoardPanel represents a single panel on a board.
diff --git a/client/trigger.go b/client/trigger.go
index b50c06e..9dd5b0f 100644
--- a/client/trigger.go
+++ b/client/trigger.go
@@ -159,6 +159,7 @@ func (t *Trigger) MarshalJSON() ([]byte, error) {
 			Recipients:             t.Recipients,
 			EvaluationScheduleType: t.EvaluationScheduleType,
 			EvaluationSchedule:     t.EvaluationSchedule,
+			Tags:                   t.Tags,
 		}
 		return json.Marshal(&struct{ *ATrigger }{ATrigger: a})
 	}
diff --git a/docs/resources/flexible_board.md b/docs/resources/flexible_board.md
index 5b550ad..4282ba8 100644
--- a/docs/resources/flexible_board.md
+++ b/docs/resources/flexible_board.md
@@ -82,6 +82,11 @@ resource "honeycombio_flexible_board" "overview" {
   name        = "Service Overview"
   description = "My flexible baord description"
 
+  tags = {
+    team    = "web"
+    project = "secret"
+  }
+
   panel {
     type = "query"
 
@@ -124,6 +129,7 @@ The following arguments are supported for flexible boards:
 - `name` - (Required) Name of the board.
 - `description` - (Optional) Description of the board. Supports Markdown.
 - `panel` - (Optional) zero or more configurations blocks
+- `tags` - (Optional) Map of up to ten (10) tags to assign to the resource.
 
 Each board configuration may have zero or more `panel` blocks which accept the following arguments:
 
diff --git a/docs/resources/trigger.md b/docs/resources/trigger.md
index ca76dfb..feec44c 100644
--- a/docs/resources/trigger.md
+++ b/docs/resources/trigger.md
@@ -279,6 +279,7 @@ Outside of the window, the trigger will not be run.
 If no schedule is specified, the trigger will be run at the specified frequency at all times.
 * `baseline_details` - (Optional) A configuration block (described below) allows you to receive notifications when the delta between values in your data, compared to a previous time period, cross thresholds you configure.
 * `recipient` - (Optional) Zero or more configuration blocks (described below) with the recipients to notify when the trigger fires.
+* `tags` - (Optional) Map of up to ten (10) tags to assign to the resource.
 
 One of `query_id` or `query_json` are required.
 
diff --git a/internal/models/flexible_board.go b/internal/models/flexible_board.go
index 8b0ab75..bd28c10 100644
--- a/internal/models/flexible_board.go
+++ b/internal/models/flexible_board.go
@@ -11,6 +11,7 @@ type FlexibleBoardResourceModel struct {
 	Description types.String `tfsdk:"description"`
 	URL         types.String `tfsdk:"board_url"`
 	Panels      types.List   `tfsdk:"panel"`
+	Tags        types.Map    `tfsdk:"tags"`
 }
 
 type BoardPanelModel struct {
diff --git a/internal/provider/flexible_board_resource.go b/internal/provider/flexible_board_resource.go
index 2803cce..c5fccd1 100644
--- a/internal/provider/flexible_board_resource.go
+++ b/internal/provider/flexible_board_resource.go
@@ -95,6 +95,7 @@ func (*flexibleBoardResource) Schema(_ context.Context, _ resource.SchemaRequest
 					stringplanmodifier.UseStateForUnknown(),
 				},
 			},
+			"tags": tagsSchema(),
 		},
 		Blocks: map[string]schema.Block{
 			"panel": schema.ListNestedBlock{
@@ -296,12 +297,18 @@ func (r *flexibleBoardResource) Create(ctx context.Context, req resource.CreateR
 		return
 	}
 
+	planTags, diags := helper.MapToTags(ctx, plan.Tags)
+	if diags.HasError() {
+		return
+	}
+
 	panelFromConfig := expandBoardPanels(ctx, plan.Panels, &resp.Diagnostics)
 	createRequest := &client.Board{
 		Name:        plan.Name.ValueString(),
 		Description: plan.Description.ValueString(),
 		BoardType:   client.BoardTypeFlexible,
 		Panels:      removeDefaultNegativeNumbers(panelFromConfig),
+		Tags:        planTags,
 	}
 
 	if resp.Diagnostics.HasError() {
@@ -343,6 +350,13 @@ func (r *flexibleBoardResource) Create(ctx context.Context, req resource.CreateR
 		state.Panels = panels
 	}
 
+	tags, diags := helper.TagsToMap(ctx, board.Tags)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+	state.Tags = tags
+
 	resp.Diagnostics.Append(resp.State.Set(ctx, state)...)
 }
 
@@ -418,8 +432,15 @@ func (r *flexibleBoardResource) Read(ctx context.Context, req resource.ReadReque
 		resp.Diagnostics.Append(diag...)
 		state.Panels = panels
 	}
+	tags, diags := helper.TagsToMap(ctx, board.Tags)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+	state.Tags = tags
 
 	resp.Diagnostics.Append(resp.State.Set(ctx, state)...)
+
 }
 
 func (r *flexibleBoardResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
@@ -430,6 +451,11 @@ func (r *flexibleBoardResource) Update(ctx context.Context, req resource.UpdateR
 		return
 	}
 
+	planTags, diags := helper.MapToTags(ctx, plan.Tags)
+	if diags.HasError() {
+		return
+	}
+
 	panelConfig := expandBoardPanels(ctx, plan.Panels, &resp.Diagnostics)
 	updateRequest := &client.Board{
 		ID:          plan.ID.ValueString(),
@@ -437,6 +463,7 @@ func (r *flexibleBoardResource) Update(ctx context.Context, req resource.UpdateR
 		Description: plan.Description.ValueString(),
 		BoardType:   client.BoardTypeFlexible,
 		Panels:      removeDefaultNegativeNumbers(panelConfig),
+		Tags:        planTags,
 	}
 	if resp.Diagnostics.HasError() {
 		return
@@ -483,7 +510,15 @@ func (r *flexibleBoardResource) Update(ctx context.Context, req resource.UpdateR
 		state.Panels = panels
 	}
 
+	tags, diags := helper.TagsToMap(ctx, board.Tags)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+	state.Tags = tags
+
 	resp.Diagnostics.Append(resp.State.Set(ctx, state)...)
+
 }
 
 func (r *flexibleBoardResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {


Test patch:
diff --git a/client/board_test.go b/client/board_test.go
index 9d91cdb..3c5ca4c 100644
--- a/client/board_test.go
+++ b/client/board_test.go
@@ -248,6 +248,61 @@ func TestFlexibleBoards(t *testing.T) {
 		assert.Equal(t, data, flexibleBoard)
 	})
 
+	t.Run("Create flexible board with tags", func(t *testing.T) {
+		data := &client.Board{
+			Name:        test.RandomStringWithPrefix("test.", 8),
+			BoardType:   "flexible",
+			Description: "A board with some tags",
+			Panels: []client.BoardPanel{
+				{
+					PanelType: client.BoardPanelTypeQuery,
+					PanelPosition: client.BoardPanelPosition{
+						X:      0,
+						Y:      0,
+						Height: 3,
+						Width:  4,
+					},
+					QueryPanel: &client.BoardQueryPanel{
+						QueryID:           *query.ID,
+						QueryAnnotationID: queryAnnotation.ID,
+						Style:             client.BoardQueryStyleGraph,
+					},
+				},
+				{
+					PanelType: client.BoardPanelTypeSLO,
+					PanelPosition: client.BoardPanelPosition{
+						X:      6,
+						Y:      0,
+						Height: 3,
+						Width:  4,
+					},
+					SLOPanel: &client.BoardSLOPanel{
+						SLOID: slo.ID,
+					},
+				},
+			},
+			Tags: []client.Tag{
+				{Key: "color", Value: "blue"},
+			},
+		}
+		flexibleBoard, err = c.Boards.Create(ctx, data)
+		require.NoError(t, err)
+		assert.NotNil(t, flexibleBoard.ID)
+
+		// copy ID before asserting equality
+		data.ID = flexibleBoard.ID
+
+		// ensure the board URL got populated
+		assert.NotEmpty(t, flexibleBoard.Links.BoardURL)
+		data.Links.BoardURL = flexibleBoard.Links.BoardURL
+
+		// ensure the tags were added
+		assert.NotEmpty(t, flexibleBoard.Tags)
+		assert.ElementsMatch(t, flexibleBoard.Tags, data.Tags, "tags do not match")
+
+		assert.Equal(t, data, flexibleBoard)
+	})
+
 	t.Run("List", func(t *testing.T) {
 		result, err := c.Boards.List(ctx)
 		require.NoError(t, err)
@@ -259,6 +314,8 @@ func TestFlexibleBoards(t *testing.T) {
 		board, err := c.Boards.Get(ctx, flexibleBoard.ID)
 		require.NoError(t, err)
 
+		assert.ElementsMatch(t, board.Tags, flexibleBoard.Tags, "tags do not match")
+
 		assert.Equal(t, *board, *flexibleBoard)
 	})
 }
diff --git a/client/trigger_test.go b/client/trigger_test.go
index 5620320..7b4d8c2 100644
--- a/client/trigger_test.go
+++ b/client/trigger_test.go
@@ -61,6 +61,9 @@ func TestTriggers(t *testing.T) {
 					Target: "This marker is created by a trigger",
 				},
 			},
+			Tags: []client.Tag{
+				{Key: "color", Value: "blue"},
+			},
 		}
 		trigger, err = c.Triggers.Create(ctx, dataset, data)
 		require.NoError(t, err)
@@ -81,6 +84,10 @@ func TestTriggers(t *testing.T) {
 		// set the default threshold exceeded limit
 		data.Threshold.ExceededLimit = 1
 
+		assert.NotEmpty(t, trigger.Tags)
+		assert.ElementsMatch(t, trigger.Tags, data.Tags, "tags do not match")
+		// trigger.Tags = data.Tags
+
 		assert.Equal(t, data, trigger)
 	})
 
@@ -112,6 +119,9 @@ func TestTriggers(t *testing.T) {
 				EndTime:    "21:00",
 			},
 		}
+		trigger.Tags = []client.Tag{
+			{Key: "team", Value: "t-team"},
+		}
 		// update the threshold exceeded limit to 3
 		trigger.Threshold.ExceededLimit = 3
 
@@ -120,7 +130,8 @@ func TestTriggers(t *testing.T) {
 		// copy IDs before asserting equality
 		trigger.QueryID = result.QueryID
 		require.NoError(t, err)
-		assert.Equal(t, trigger, result)
+		require.ElementsMatch(t, trigger.Tags, result.Tags, "tags do not match")
+		assert.Equal(t, trigger, result, "full trigger does not match")
 	})
 
 	t.Run("Delete", func(t *testing.T) {
diff --git a/internal/provider/flexible_board_resource_test.go b/internal/provider/flexible_board_resource_test.go
index 63bd957..10c66f0 100644
--- a/internal/provider/flexible_board_resource_test.go
+++ b/internal/provider/flexible_board_resource_test.go
@@ -68,11 +68,16 @@ func TestAccHoneycombioFlexibleBoard(t *testing.T) {
 					resource.TestCheckResourceAttr("honeycombio_flexible_board.test", "panel.1.query_panel.0.visualization_settings.0.chart.0.use_log_scale", "false"),
 				),
 			},
-			// remove board's panels
+			// remove board's panels, add tags
 			{
 				Config: `
 resource "honeycombio_flexible_board" "test" {
 	name          = "simple flexible board"
+
+	tags = {
+	  team = "blue"
+	  env  = "dev"
+	}
 }
 			  `,
 				Check: resource.ComposeTestCheckFunc(
@@ -80,9 +85,12 @@ resource "honeycombio_flexible_board" "test" {
 					resource.TestCheckResourceAttr("honeycombio_flexible_board.test", "name", "simple flexible board"),
 					resource.TestCheckResourceAttr("honeycombio_flexible_board.test", "description", ""),
 					resource.TestCheckResourceAttr("honeycombio_flexible_board.test", "panel.#", "0"),
+					resource.TestCheckResourceAttr("honeycombio_flexible_board.test", "tags.%", "2"),
+					resource.TestCheckResourceAttr("honeycombio_flexible_board.test", "tags.team", "blue"),
+					resource.TestCheckResourceAttr("honeycombio_flexible_board.test", "tags.env", "dev"),
 				),
 			},
-			// now add a query panel and ensure the board is updated
+			// now add a query panel, update tags, and ensure the board is updated
 			{
 				Config: fmt.Sprintf(`
 data "honeycombio_query_specification" "test" {
@@ -106,6 +114,9 @@ resource "honeycombio_query_annotation" "test" {
 resource "honeycombio_flexible_board" "test" {
   name        = "simple flexible board updated"
   description = "simple flexible board description"
+  tags = {
+	team = "green"
+  } 
   panel {
     type = "query"
     query_panel {
@@ -131,9 +142,12 @@ resource "honeycombio_flexible_board" "test" {
 					resource.TestCheckResourceAttr("honeycombio_flexible_board.test", "panel.#", "1"),
 					resource.TestCheckResourceAttrPair("honeycombio_flexible_board.test", "panel.0.query_panel.0.query_id", "honeycombio_query.test", "id"),
 					resource.TestCheckResourceAttrPair("honeycombio_flexible_board.test", "panel.0.query_panel.0.query_annotation_id", "honeycombio_query_annotation.test", "id"),
+					resource.TestCheckResourceAttr("honeycombio_flexible_board.test", "tags.%", "1"),
+					resource.TestCheckResourceAttr("honeycombio_flexible_board.test", "tags.team", "green"),
+					resource.TestCheckNoResourceAttr("honeycombio_flexible_board.test", "tags.env"),
 				),
 			},
-			// now add an SLO panel and remove chart settings from the query panel
+			// now add an SLO panel, remove chart settings from the query panel, remove tags
 			{
 				Config: fmt.Sprintf(`
 data "honeycombio_query_specification" "test" {
@@ -205,6 +219,7 @@ resource "honeycombio_flexible_board" "test" {
 					resource.TestCheckResourceAttr("honeycombio_flexible_board.test", "panel.1.position.0.y_coordinate", "0"),
 					resource.TestCheckResourceAttr("honeycombio_flexible_board.test", "panel.1.position.0.height", "4"),
 					resource.TestCheckResourceAttr("honeycombio_flexible_board.test", "panel.1.position.0.width", "3"),
+					resource.TestCheckResourceAttr("honeycombio_flexible_board.test", "tags.%", "0"),
 				),
 			},
 			// re-order the panels and remove viz settings for the query panel