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

Issue:
Data source: column
Add a pair of column data sources: `honeycombio_column` and `honeycombio_columns`


Discussion / hints:

Fix patch:
diff --git a/client/column.go b/client/column.go
index 24de62d2..c9ebdf2a 100644
--- a/client/column.go
+++ b/client/column.go
@@ -3,6 +3,7 @@ package client
 import (
 	"context"
 	"fmt"
+	"net/url"
 	"time"
 )
 
@@ -94,7 +95,7 @@ func (s *columns) Get(ctx context.Context, dataset string, id string) (*Column,
 
 func (s *columns) GetByKeyName(ctx context.Context, dataset string, keyName string) (*Column, error) {
 	var c Column
-	err := s.client.performRequest(ctx, "GET", fmt.Sprintf("/1/columns/%s?key_name=%s", urlEncodeDataset(dataset), keyName), nil, &c)
+	err := s.client.performRequest(ctx, "GET", fmt.Sprintf("/1/columns/%s?key_name=%s", urlEncodeDataset(dataset), url.QueryEscape(keyName)), nil, &c)
 	return &c, err
 }
 
diff --git a/docs/data-sources/column.md b/docs/data-sources/column.md
new file mode 100644
index 00000000..f85e252f
--- /dev/null
+++ b/docs/data-sources/column.md
@@ -0,0 +1,38 @@
+# Data Source: honeycombio_column
+
+The `honeycombio_column` data source retrieves the details of a single column in a dataset.
+
+-> **Note** Terraform will fail unless a column is returned by the search. Ensure that your search is specific enough to return a column.
+If you want to match multiple columns, use the `honeycombio_columns` data source instead.
+
+## Example Usage
+
+```hcl
+variable "dataset" {
+  type = string
+}
+
+# Retrieve the details of a single column
+data "honeycombio_column" "mycol" {
+  dataset = var.dataset
+  name    = "mycol"
+}
+```
+
+## Argument Reference
+
+The following arguments are supported:
+
+* `dataset` - (Required) The dataset this column is associated with
+* `name` - (Required) The name of the column
+
+## Attribute Reference
+
+In addition to all arguments above, the following attributes are exported:
+
+* `type` - the type of the column (string, integer, float, or boolean)
+* `description` - the description of the column
+* `hidden` - whether or not the column is hidden from the query builder and results
+* `last_written_at` - the ISO8601 formatted time that the column last received data
+* `created_at` - the ISO8601 formatted time when the column was created
+* `updated_at` - the  ISO8601 formatted time when the column's metadata (type, description, etc) was last changed
diff --git a/docs/data-sources/columns.md b/docs/data-sources/columns.md
new file mode 100644
index 00000000..819cebd7
--- /dev/null
+++ b/docs/data-sources/columns.md
@@ -0,0 +1,35 @@
+# Data Source: honeycombio_columns
+
+The columns data source allows the columns of a dataset to be retrieved.
+
+## Example Usage
+
+```hcl
+variable "dataset" {
+  type = string
+}
+
+# returns all columns
+data "honeycombio_columns" "all" {
+  dataset = var.dataset
+}
+
+# only returns the columns starting with 'foo_'
+data "honeycombio_columns" "foo" {
+  dataset     = var.dataset
+  starts_with = "foo_"
+}
+```
+
+## Argument Reference
+
+The following arguments are supported:
+
+* `dataset` - (Required) The dataset to retrieve the columns list from
+* `starts_with` - (Optional) Only return columns starting with the given value.
+
+## Attribute Reference
+
+In addition to all arguments above, the following attributes are exported:
+
+* `names` - a list of all the column names found in the dataset
diff --git a/example/column_sync/main.tf b/example/column_sync/main.tf
new file mode 100644
index 00000000..152dfaf5
--- /dev/null
+++ b/example/column_sync/main.tf
@@ -0,0 +1,25 @@
+variable "dataset" {
+  type = string
+}
+
+variable "otherdataset" {
+  type = string
+}
+
+# Retrieve the details of a single column
+data "honeycombio_column" "mycol" {
+  dataset = var.dataset
+  name    = "mycol"
+}
+
+# Ensure a column in another dataset remains in sync
+resource "honeycombio_column" "othercol" {
+  dataset     = var.otherdataset
+  name        = data.honeycombio_column.mycol.name
+  type        = data.honeycombio_column.mycol.type
+  description = data.honeycombio_column.mycol.description
+  hidden      = data.honeycombio_column.mycol.hidden
+
+  # but only if its updated_at is greater than an arbitrary date
+  count = timecmp(data.honeycombio_column.mycol.updated_at, "2023-04-20T00:00:00Z") == 1 ? 1 : 0
+}
diff --git a/example/columns_conditional_create/main.tf b/example/columns_conditional_create/main.tf
new file mode 100644
index 00000000..9d08094b
--- /dev/null
+++ b/example/columns_conditional_create/main.tf
@@ -0,0 +1,37 @@
+####################################################
+# Example: Create only the missing columns
+####################################################
+
+variable "dataset" {
+  type = string
+}
+
+# returns all columns in a dataset
+data "honeycombio_columns" "all" {
+  dataset = var.dataset
+}
+
+# A list of columns and their types that are necessary
+locals {
+  required_columns = {
+    "db.system"              = "string",
+    "db.type"                = "string",
+    "duration_ms"            = "float",
+    "error"                  = "boolean",
+    "http.flavor"            = "string",
+  }
+
+  cols_to_create = setsubtract(keys(local.required_columns), data.honeycombio_columns.all.names)
+}
+
+resource "honeycombio_column" "required_columns" {
+  for_each = toset(local.cols_to_create)
+
+  name    = each.key
+  type    = lookup(local.required_columns, each.key, "string")
+  dataset = var.dataset
+}
+
+output "columns_created" {
+  value = local.cols_to_create
+}
diff --git a/honeycombio/data_source_column.go b/honeycombio/data_source_column.go
new file mode 100644
index 00000000..b516d773
--- /dev/null
+++ b/honeycombio/data_source_column.go
@@ -0,0 +1,102 @@
+package honeycombio
+
+import (
+	"context"
+	"fmt"
+	"time"
+
+	"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
+	honeycombio "github.com/honeycombio/terraform-provider-honeycombio/client"
+)
+
+func dataSourceHoneycombioColumn() *schema.Resource {
+	return &schema.Resource{
+		ReadContext: dataSourceHoneycombioColumnRead,
+
+		Description: `
+'honeycombio_column' data source provides details about a specific column in a dataset, matched by name.
+`,
+
+		Schema: map[string]*schema.Schema{
+			"dataset": {
+				Type:        schema.TypeString,
+				Optional:    false,
+				Required:    true,
+				Description: "The dataset this column is associated with",
+			},
+			"name": {
+				Type:        schema.TypeString,
+				Optional:    false,
+				Required:    true,
+				Description: "Name of the column",
+			},
+			// Generated attributes
+			"hidden": {
+				Type:        schema.TypeBool,
+				Required:    false,
+				Optional:    false,
+				Computed:    true,
+				Description: "Whether the column is hidden",
+			},
+			"type": {
+				Type:        schema.TypeString,
+				Required:    false,
+				Optional:    false,
+				Computed:    true,
+				Description: "The type of column, allowed types are `string`, `integer`, `float`, and `boolean`.",
+			},
+			"description": {
+				Type:        schema.TypeString,
+				Required:    false,
+				Optional:    false,
+				Computed:    true,
+				Description: "A description of the column",
+			},
+			"last_written_at": {
+				Type:        schema.TypeString,
+				Required:    false,
+				Optional:    false,
+				Computed:    true,
+				Description: "The last time the column was written to",
+			},
+			"created_at": {
+				Type:        schema.TypeString,
+				Required:    false,
+				Optional:    false,
+				Computed:    true,
+				Description: "The time the column was created",
+			},
+			"updated_at": {
+				Type:        schema.TypeString,
+				Required:    false,
+				Optional:    false,
+				Computed:    true,
+				Description: "The time the column was last updated",
+			},
+		},
+	}
+}
+
+func dataSourceHoneycombioColumnRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
+	client := meta.(*honeycombio.Client)
+
+	dataset := d.Get("dataset").(string)
+	matchName := d.Get("name").(string)
+
+	column, err := client.Columns.GetByKeyName(ctx, dataset, matchName)
+	if err != nil {
+		return diag.FromErr(fmt.Errorf("column not found, API returned: %s", err.Error()))
+	}
+
+	d.SetId(column.ID)
+	d.Set("name", column.KeyName)
+	d.Set("hidden", column.Hidden)
+	d.Set("type", column.Type)
+	d.Set("description", column.Description)
+	d.Set("last_written_at", column.LastWrittenAt.UTC().Format(time.RFC3339))
+	d.Set("created_at", column.CreatedAt.UTC().Format(time.RFC3339))
+	d.Set("updated_at", column.UpdatedAt.UTC().Format(time.RFC3339))
+
+	return nil
+}
diff --git a/honeycombio/data_source_columns.go b/honeycombio/data_source_columns.go
new file mode 100644
index 00000000..a5a918ec
--- /dev/null
+++ b/honeycombio/data_source_columns.go
@@ -0,0 +1,70 @@
+package honeycombio
+
+import (
+	"context"
+	"strconv"
+	"strings"
+
+	"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
+	honeycombio "github.com/honeycombio/terraform-provider-honeycombio/client"
+	"github.com/honeycombio/terraform-provider-honeycombio/honeycombio/internal/hashcode"
+)
+
+func dataSourceHoneycombioColumns() *schema.Resource {
+	return &schema.Resource{
+		ReadContext: dataSourceHoneycombioColumnsRead,
+
+		Schema: map[string]*schema.Schema{
+			"dataset": {
+				Type:     schema.TypeString,
+				Optional: false,
+				Required: true,
+			},
+			"starts_with": {
+				Type:     schema.TypeString,
+				Optional: true,
+			},
+			"names": {
+				Type:     schema.TypeList,
+				Computed: true,
+				Optional: false,
+				Required: false,
+				Elem: &schema.Schema{
+					Type: schema.TypeString,
+				},
+			},
+		},
+	}
+}
+
+func dataSourceHoneycombioColumnsRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
+	client := meta.(*honeycombio.Client)
+
+	dataset := d.Get("dataset").(string)
+
+	columns, err := client.Columns.List(ctx, dataset)
+	if err != nil {
+		return diag.FromErr(err)
+	}
+
+	value, ok := d.GetOk("starts_with")
+	if ok {
+		startsWith := value.(string)
+
+		for i := len(columns) - 1; i >= 0; i-- {
+			if !strings.HasPrefix(columns[i].KeyName, startsWith) {
+				columns = append(columns[:i], columns[i+1:]...)
+			}
+		}
+	}
+
+	names := make([]string, len(columns))
+	for i, d := range columns {
+		names[i] = d.KeyName
+	}
+	d.Set("names", names)
+
+	d.SetId(strconv.Itoa(hashcode.String(strings.Join(names, ","))))
+	return nil
+}
diff --git a/honeycombio/provider.go b/honeycombio/provider.go
index daf9e0ea..2df4e19d 100644
--- a/honeycombio/provider.go
+++ b/honeycombio/provider.go
@@ -44,6 +44,8 @@ func Provider() *schema.Provider {
 		},
 		DataSourcesMap: map[string]*schema.Resource{
 			"honeycombio_datasets":            dataSourceHoneycombioDatasets(),
+			"honeycombio_column":              dataSourceHoneycombioColumn(),
+			"honeycombio_columns":             dataSourceHoneycombioColumns(),
 			"honeycombio_query_result":        dataSourceHoneycombioQueryResult(),
 			"honeycombio_query_specification": dataSourceHoneycombioQuerySpec(),
 			"honeycombio_trigger_recipient":   dataSourceHoneycombioSlackRecipient(),


Test patch:
diff --git a/honeycombio/data_source_column_test.go b/honeycombio/data_source_column_test.go
new file mode 100644
index 00000000..0d1ee78b
--- /dev/null
+++ b/honeycombio/data_source_column_test.go
@@ -0,0 +1,74 @@
+package honeycombio
+
+import (
+	"context"
+	"fmt"
+	"regexp"
+	"strings"
+	"testing"
+
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
+	honeycombio "github.com/honeycombio/terraform-provider-honeycombio/client"
+)
+
+func TestAccDataSourceHoneycombioColumn_basic(t *testing.T) {
+	ctx := context.Background()
+	c := testAccClient(t)
+	dataset := testAccDataset()
+
+	testColumns := []honeycombio.Column{
+		{
+			KeyName:     "test_column3",
+			Description: "test column3",
+			Type:        honeycombio.ToPtr(honeycombio.ColumnType("float")),
+		},
+	}
+
+	for i, column := range testColumns {
+		col, err := c.Columns.Create(ctx, dataset, &column)
+		// update ID for removal later
+		testColumns[i].ID = col.ID
+		if err != nil {
+			t.Error(err)
+		}
+	}
+	t.Cleanup(func() {
+		// remove Columns at the of the test run
+		for _, col := range testColumns {
+			c.Columns.Delete(ctx, dataset, col.ID)
+		}
+	})
+
+	resource.Test(t, resource.TestCase{
+		PreCheck:          testAccPreCheck(t),
+		ProviderFactories: testAccProviderFactories,
+		Steps: []resource.TestStep{
+			// match by name and return a single column with the right type
+			{
+				Config: testAccDataSourceColumnConfig([]string{"dataset = \"" + testAccDataset() + "\"", "name = \"test_column3\""}),
+				Check:  resource.TestCheckResourceAttr("data.honeycombio_column.test", "type", "float"),
+			},
+			// test a failed match
+			{
+				Config:      testAccDataSourceColumnConfig([]string{"dataset = \"" + testAccDataset() + "\"", "name = \"test_column5\""}),
+				ExpectError: regexp.MustCompile("Error: column not found, API returned: 404 Not Found"),
+			},
+		},
+	})
+}
+
+func testAccDataSourceColumnConfig(filters []string) string {
+	return fmt.Sprintf(`
+data "honeycombio_column" "test" {
+	%s
+}
+
+output "type" {
+  value = data.honeycombio_column.test.type
+}
+
+output "description" {
+  value = data.honeycombio_column.test.description
+}
+`, strings.Join(filters, "\n"))
+}
diff --git a/honeycombio/data_source_columns_test.go b/honeycombio/data_source_columns_test.go
new file mode 100644
index 00000000..9e3f4fb7
--- /dev/null
+++ b/honeycombio/data_source_columns_test.go
@@ -0,0 +1,77 @@
+package honeycombio
+
+import (
+	"context"
+	"fmt"
+	"strings"
+	"testing"
+
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
+	honeycombio "github.com/honeycombio/terraform-provider-honeycombio/client"
+)
+
+func TestAccDataSourceHoneycombioColumns_basic(t *testing.T) {
+	ctx := context.Background()
+	c := testAccClient(t)
+	dataset := testAccDataset()
+
+	testColumns := []honeycombio.Column{
+		{
+			KeyName:     "test_column1",
+			Description: "test column1",
+		},
+		{
+			KeyName:     "test_column2",
+			Description: "test column2",
+		},
+	}
+
+	for i, column := range testColumns {
+		col, err := c.Columns.Create(ctx, dataset, &column)
+		// update ID for removal later
+		testColumns[i].ID = col.ID
+		if err != nil {
+			t.Error(err)
+		}
+	}
+	t.Cleanup(func() {
+		// remove Columns at the of the test run
+		for _, col := range testColumns {
+			c.Columns.Delete(ctx, dataset, col.ID)
+		}
+	})
+
+	resource.Test(t, resource.TestCase{
+		PreCheck:          testAccPreCheck(t),
+		ProviderFactories: testAccProviderFactories,
+		Steps: []resource.TestStep{
+			{
+				Config: testAccDataSourceColumnsConfig([]string{"dataset = \"" + testAccDataset() + "\""}),
+				Check: resource.ComposeTestCheckFunc(
+					testCheckOutputContains("names", testColumns[0].KeyName),
+				),
+			},
+			{
+				Config: testAccDataSourceColumnsConfig([]string{"dataset = \"" + testAccDataset() + "\"", "starts_with = \"test_column\""}),
+				Check:  resource.TestCheckResourceAttr("data.honeycombio_columns.test", "names.#", "2"),
+			},
+			{
+				Config: testAccDataSourceColumnsConfig([]string{"dataset = \"" + testAccDataset() + "\"", "starts_with = \"foo\""}),
+				Check: resource.ComposeTestCheckFunc(
+					testCheckOutputDoesNotContain("names", testColumns[0].KeyName),
+				),
+			},
+		},
+	})
+}
+
+func testAccDataSourceColumnsConfig(filters []string) string {
+	return fmt.Sprintf(`
+data "honeycombio_columns" "test" {
+	%s
+}
+
+output "names" {
+  value = data.honeycombio_columns.test.names
+}`, strings.Join(filters, "\n"))
+}