Repository: honeycombio/terraform-provider-honeycombio
Pull request #535 (2024-08-22 12:51:40)

feat: full lifecycle support for datasets
# Summary

This brings full lifecycle management to Datasets: which is a fancy way of saying "datasets will be deleted on destroy now" with a side of "we will now fail on 'create' if the dataset already exists instead of silently importing the dataset" -- behaviour that previously existed with columns and derived columns and was removed because it was surprising and arguably dangerous.

This is a breaking change in behaviour, but one done in the name of being more Terraform-y and that feels "correct" as we move toward declaring the provider stable.

Datasets have deletion protection -- enabled by default -- which must be disabled to successfully destroy them, and matches the pattern used with Environments (`r/environment`) in v0.25.0.

# Breaking changes

* client: if the Datasets API returns an HTTP '200 OK' -- as opposed to '201 Created' -- on create the `Datasets.Create` method will now return `ErrDatasetExists`.
* r/dataset: will now do the more standard thing and error if a dataset with that name already exists in the Environment. The end-user can choose to adjust the name or import the existing dataset.

# Also included:

* a fly-by bug fix for v2 client calls not closing the HTTP response body
* a new data source for a single dataset (`d/dataset`) now that folks will potentially want to look them up without relying on the previous "import on exists" behaviour in `r/dataset`
* a re-write of the existing `d/datasets` in the Plugin Framework
  * this deprecates `starts_with` in favour of the standard `detail_filter` block
* a dataset 'sweeper' for cleaning up datasets having names beginning with `test.` in CI

codecov-commenter: ## [Codecov](https://app.codecov.io/gh/honeycombio/terraform-provider-honeycombio/pull/535?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=honeycombio) Report
Attention: Patch coverage is `79.95227%` with `84 lines` in your changes missing coverage. Please review.
> Project coverage is 79.24%. Comparing base [(`c4f3020`)](https://app.codecov.io/gh/honeycombio/terraform-provider-honeycombio/commit/c4f3020b12dd01a66a879db54f7027cfb843568d?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=honeycombio) to head [(`8ff8456`)](https://app.codecov.io/gh/honeycombio/terraform-provider-honeycombio/commit/8ff8456de4b5fc0c4f96e4bce367aaefd36a2bf2?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=honeycombio).

| [Files](https://app.codecov.io/gh/honeycombio/terraform-provider-honeycombio/pull/535?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=honeycombio) | Patch % | Lines |
|---|---|---|
| [internal/provider/dataset\_resource.go](https://app.codecov.io/gh/honeycombio/terraform-provider-honeycombio/pull/535?src=pr&el=tree&filepath=internal%2Fprovider%2Fdataset_resource.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=honeycombio#diff-aW50ZXJuYWwvcHJvdmlkZXIvZGF0YXNldF9yZXNvdXJjZS5nbw==) | 74.25% | [38 Missing and 14 partials :warning: ](https://app.codecov.io/gh/honeycombio/terraform-provider-honeycombio/pull/535?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=honeycombio) |
| [internal/provider/datasets\_data\_source.go](https://app.codecov.io/gh/honeycombio/terraform-provider-honeycombio/pull/535?src=pr&el=tree&filepath=internal%2Fprovider%2Fdatasets_data_source.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=honeycombio#diff-aW50ZXJuYWwvcHJvdmlkZXIvZGF0YXNldHNfZGF0YV9zb3VyY2UuZ28=) | 87.17% | [6 Missing and 4 partials :warning: ](https://app.codecov.io/gh/honeycombio/terraform-provider-honeycombio/pull/535?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=honeycombio) |
| [internal/provider/dataset\_data\_source.go](https://app.codecov.io/gh/honeycombio/terraform-provider-honeycombio/pull/535?src=pr&el=tree&filepath=internal%2Fprovider%2Fdataset_data_source.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=honeycombio#diff-aW50ZXJuYWwvcHJvdmlkZXIvZGF0YXNldF9kYXRhX3NvdXJjZS5nbw==) | 91.56% | [4 Missing and 3 partials :warning: ](https://app.codecov.io/gh/honeycombio/terraform-provider-honeycombio/pull/535?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=honeycombio) |
| [client/client.go](https://app.codecov.io/gh/honeycombio/terraform-provider-honeycombio/pull/535?src=pr&el=tree&filepath=client%2Fclient.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=honeycombio#diff-Y2xpZW50L2NsaWVudC5nbw==) | 64.70% | [3 Missing and 3 partials :warning: ](https://app.codecov.io/gh/honeycombio/terraform-provider-honeycombio/pull/535?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=honeycombio) |
| [client/dataset.go](https://app.codecov.io/gh/honeycombio/terraform-provider-honeycombio/pull/535?src=pr&el=tree&filepath=client%2Fdataset.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=honeycombio#diff-Y2xpZW50L2RhdGFzZXQuZ28=) | 68.42% | [3 Missing and 3 partials :warning: ](https://app.codecov.io/gh/honeycombio/terraform-provider-honeycombio/pull/535?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=honeycombio) |
| [client/v2/environments.go](https://app.codecov.io/gh/honeycombio/terraform-provider-honeycombio/pull/535?src=pr&el=tree&filepath=client%2Fv2%2Fenvironments.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=honeycombio#diff-Y2xpZW50L3YyL2Vudmlyb25tZW50cy5nbw==) | 76.92% | [0 Missing and 3 partials :warning: ](https://app.codecov.io/gh/honeycombio/terraform-provider-honeycombio/pull/535?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=honeycombio) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main     #535      +/-   ##
==========================================
+ Coverage   79.18%   79.24%   +0.05%     
==========================================
  Files          74       75       +1     
  Lines        5669     5925     +256     
==========================================
+ Hits         4489     4695     +206     
- Misses        880      912      +32     
- Partials      300      318      +18     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/honeycombio/terraform-provider-honeycombio/pull/535?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=honeycombio).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=honeycombio).


Patch:
diff --git a/client/client.go b/client/client.go
index 34d1505..0d477d6 100644
--- a/client/client.go
+++ b/client/client.go
@@ -191,29 +191,11 @@ func (c *Client) IsClassic(ctx context.Context) bool {
 //
 // Attempts to return a DetailedError if the response status code is not 2xx,
 // but can return a generic error.
-func (c *Client) Do(ctx context.Context, method, path string, requestBody, responseBody interface{}) error {
-	var body io.Reader
-
-	if requestBody != nil {
-		buf := new(bytes.Buffer)
-		err := json.NewEncoder(buf).Encode(requestBody)
-		if err != nil {
-			return err
-		}
-		body = buf
-	}
-
-	requestURL, err := c.apiURL.Parse(path)
+func (c *Client) Do(ctx context.Context, method, path string, requestBody, responseBody any) error {
+	req, err := c.newRequest(ctx, method, path, requestBody)
 	if err != nil {
 		return err
 	}
-
-	req, err := retryablehttp.NewRequestWithContext(ctx, method, requestURL.String(), body)
-	if err != nil {
-		return err
-	}
-	req.Header = c.headers
-
 	resp, err := c.httpClient.Do(req)
 	if err != nil {
 		return err
@@ -230,6 +212,30 @@ func (c *Client) Do(ctx context.Context, method, path string, requestBody, respo
 	return err
 }
 
+func (c *Client) newRequest(ctx context.Context, method, path string, body any) (*retryablehttp.Request, error) {
+	var bodyReader io.Reader
+	if body != nil {
+		buf := bytes.NewBuffer(nil)
+		if err := json.NewEncoder(buf).Encode(body); err != nil {
+			return nil, err
+		}
+		bodyReader = buf
+	}
+
+	url, err := c.apiURL.Parse(path)
+	if err != nil {
+		return nil, err
+	}
+
+	req, err := retryablehttp.NewRequestWithContext(ctx, method, url.String(), bodyReader)
+	if err != nil {
+		return nil, err
+	}
+	req.Header = c.headers
+
+	return req, nil
+}
+
 // retryHTTPCheck provides a callback for Client.CheckRetry which
 // will retry both rate limit (429) and server gateway (502, 504) errors.
 func (c *Client) retryHTTPCheck(ctx context.Context, resp *http.Response, err error) (bool, error) {
diff --git a/client/dataset.go b/client/dataset.go
index eaf6242..abe2152 100644
--- a/client/dataset.go
+++ b/client/dataset.go
@@ -2,7 +2,9 @@ package client
 
 import (
 	"context"
+	"encoding/json"
 	"fmt"
+	"net/http"
 	"time"
 )
 
@@ -17,13 +19,18 @@ type Datasets interface {
 	// Get a dataset by its slug.
 	Get(ctx context.Context, slug string) (*Dataset, error)
 
-	// Create a new dataset. Only name should be set when creating a dataset,
-	// all other fields are ignored.
+	// Creates a new dataset.
+	//
+	// Will return ErrDatasetExists if a dataset with that name already exists
+	// in the Environment.
 	Create(ctx context.Context, dataset *Dataset) (*Dataset, error)
 
 	// Update an existing dataset. Missing (optional) fields will set to their
 	// respective defaults and not the currently existing values.
 	Update(ctx context.Context, dataset *Dataset) (*Dataset, error)
+
+	// Delete a dataset by its slug.
+	Delete(ctx context.Context, slug string) error
 }
 
 // datasets implements Datasets.
@@ -31,22 +38,31 @@ type datasets struct {
 	client *Client
 }
 
+// ErrDatasetExists is returned by Create when the dataset already exists.
+var ErrDatasetExists = fmt.Errorf("dataset already exists")
+
 // Compile-time proof of interface implementation by type datasets.
 var _ Datasets = (*datasets)(nil)
 
 // Dataset represents a Honeycomb dataset.
 //
-// API docs: https://docs.honeycomb.io/api/dataset
+// API docs: https://docs.honeycomb.io/api/datasets/
 type Dataset struct {
-	Name            string `json:"name"`
-	Description     string `json:"description,omitempty"`
-	Slug            string `json:"slug,omitempty"`
-	ExpandJSONDepth int    `json:"expand_json_depth,omitempty"`
+	Name            string          `json:"name"`
+	Description     string          `json:"description,omitempty"`
+	Slug            string          `json:"slug,omitempty"`
+	ExpandJSONDepth int             `json:"expand_json_depth,omitempty"`
+	Settings        DatasetSettings `json:"settings,omitempty"`
 	// Read only
 	LastWrittenAt time.Time `json:"last_written_at,omitempty"`
 	CreatedAt     time.Time `json:"created_at,omitempty"`
 }
 
+type DatasetSettings struct {
+	// Optional, defaults to true. Cannot be set on creation.
+	DeleteProtected *bool `json:"delete_protected,omitempty"`
+}
+
 func (s datasets) List(ctx context.Context) ([]Dataset, error) {
 	var datasets []Dataset
 	err := s.client.Do(ctx, "GET", "/1/datasets", nil, &datasets)
@@ -59,14 +75,38 @@ func (s datasets) Get(ctx context.Context, slug string) (*Dataset, error) {
 	return &dataset, err
 }
 
-func (s datasets) Create(ctx context.Context, data *Dataset) (*Dataset, error) {
-	var dataset Dataset
-	err := s.client.Do(ctx, "POST", "/1/datasets", data, &dataset)
-	return &dataset, err
+func (s datasets) Create(ctx context.Context, d *Dataset) (*Dataset, error) {
+	req, err := s.client.newRequest(ctx, "POST", "/1/datasets", d)
+	if err != nil {
+		return nil, err
+	}
+	resp, err := s.client.httpClient.Do(req)
+	if err != nil {
+		return nil, err
+	}
+	defer resp.Body.Close()
+
+	switch resp.StatusCode {
+	case http.StatusOK: // the API doesn't consider this an error, but we do
+		return nil, ErrDatasetExists
+	case http.StatusCreated:
+		var dataset Dataset
+		err = json.NewDecoder(resp.Body).Decode(&dataset)
+		if err != nil {
+			return nil, err
+		}
+		return &dataset, err
+	default:
+		return nil, ErrorFromResponse(resp)
+	}
 }
 
-func (s datasets) Update(ctx context.Context, data *Dataset) (*Dataset, error) {
+func (s datasets) Update(ctx context.Context, d *Dataset) (*Dataset, error) {
 	var dataset Dataset
-	err := s.client.Do(ctx, "PUT", fmt.Sprintf("/1/datasets/%s", urlEncodeDataset(data.Name)), data, &dataset)
+	err := s.client.Do(ctx, "PUT", fmt.Sprintf("/1/datasets/%s", urlEncodeDataset(d.Slug)), d, &dataset)
 	return &dataset, err
 }
+
+func (s datasets) Delete(ctx context.Context, slug string) error {
+	return s.client.Do(ctx, "DELETE", fmt.Sprintf("/1/datasets/%s", urlEncodeDataset(slug)), nil, nil)
+}
diff --git a/client/v2/api_keys.go b/client/v2/api_keys.go
index 58d4712..3ae5c8c 100644
--- a/client/v2/api_keys.go
+++ b/client/v2/api_keys.go
@@ -40,6 +40,8 @@ func (a *apiKeys) Create(ctx context.Context, k *APIKey) (*APIKey, error) {
 	if err != nil {
 		return nil, err
 	}
+	defer r.Body.Close()
+
 	if r.StatusCode != http.StatusCreated {
 		return nil, hnyclient.ErrorFromResponse(r)
 	}
@@ -60,6 +62,8 @@ func (a *apiKeys) Get(ctx context.Context, id string) (*APIKey, error) {
 	if err != nil {
 		return nil, err
 	}
+	defer r.Body.Close()
+
 	if r.StatusCode != http.StatusOK {
 		return nil, hnyclient.ErrorFromResponse(r)
 	}
@@ -80,6 +84,8 @@ func (a *apiKeys) Update(ctx context.Context, k *APIKey) (*APIKey, error) {
 	if err != nil {
 		return nil, err
 	}
+	defer r.Body.Close()
+
 	if r.StatusCode != http.StatusOK {
 		return nil, hnyclient.ErrorFromResponse(r)
 	}
@@ -100,6 +106,8 @@ func (a *apiKeys) Delete(ctx context.Context, id string) error {
 	if err != nil {
 		return err
 	}
+	defer r.Body.Close()
+
 	if r.StatusCode != http.StatusNoContent {
 		return hnyclient.ErrorFromResponse(r)
 	}
diff --git a/client/v2/environments.go b/client/v2/environments.go
index 1201fb8..4396c35 100644
--- a/client/v2/environments.go
+++ b/client/v2/environments.go
@@ -68,15 +68,17 @@ func (e *environments) Create(ctx context.Context, env *Environment) (*Environme
 	if err != nil {
 		return nil, err
 	}
+	defer r.Body.Close()
+
 	if r.StatusCode != http.StatusCreated {
 		return nil, hnyclient.ErrorFromResponse(r)
 	}
 
-	envrion := new(Environment)
-	if err := jsonapi.UnmarshalPayload(r.Body, envrion); err != nil {
+	environ := new(Environment)
+	if err := jsonapi.UnmarshalPayload(r.Body, environ); err != nil {
 		return nil, err
 	}
-	return envrion, nil
+	return environ, nil
 }
 
 func (e *environments) Get(ctx context.Context, id string) (*Environment, error) {
@@ -88,15 +90,17 @@ func (e *environments) Get(ctx context.Context, id string) (*Environment, error)
 	if err != nil {
 		return nil, err
 	}
+	defer r.Body.Close()
+
 	if r.StatusCode != http.StatusOK {
 		return nil, hnyclient.ErrorFromResponse(r)
 	}
 
-	envrion := new(Environment)
-	if err := jsonapi.UnmarshalPayload(r.Body, envrion); err != nil {
+	environ := new(Environment)
+	if err := jsonapi.UnmarshalPayload(r.Body, environ); err != nil {
 		return nil, err
 	}
-	return envrion, nil
+	return environ, nil
 }
 
 func (e *environments) Update(ctx context.Context, env *Environment) (*Environment, error) {
@@ -108,15 +112,17 @@ func (e *environments) Update(ctx context.Context, env *Environment) (*Environme
 	if err != nil {
 		return nil, err
 	}
+	defer r.Body.Close()
+
 	if r.StatusCode != http.StatusOK {
 		return nil, hnyclient.ErrorFromResponse(r)
 	}
 
-	envrion := new(Environment)
-	if err := jsonapi.UnmarshalPayload(r.Body, envrion); err != nil {
+	environ := new(Environment)
+	if err := jsonapi.UnmarshalPayload(r.Body, environ); err != nil {
 		return nil, err
 	}
-	return envrion, nil
+	return environ, nil
 }
 
 func (e *environments) Delete(ctx context.Context, id string) error {
@@ -128,6 +134,8 @@ func (e *environments) Delete(ctx context.Context, id string) error {
 	if err != nil {
 		return err
 	}
+	defer r.Body.Close()
+
 	if r.StatusCode != http.StatusNoContent {
 		return hnyclient.ErrorFromResponse(r)
 	}
diff --git a/docs/data-sources/dataset.md b/docs/data-sources/dataset.md
new file mode 100644
index 0000000..2f205d8
--- /dev/null
+++ b/docs/data-sources/dataset.md
@@ -0,0 +1,31 @@
+# Data Source: honeycombio_dataset
+
+The `honeycombio_dataset` data source retrieves the details of a single Dataset.
+If you want to retrieve multiple Datasets, use the `honeycombio_datasets` data source instead.
+
+## Example Usage
+
+```hcl
+# Retrieve the details of a Dataset
+data "honeycombio_dataset" "my-service" {
+  slug = "my-service"
+}
+```
+
+## Argument Reference
+
+The following arguments are supported:
+
+* `slug` - (Required) The Slug of the Dataset
+
+## Attribute Reference
+
+In addition to all arguments above, the following attributes are exported:
+
+* `name` - the Dataset's name.
+* `description` - the Dataset's description.
+* `expand_json_depth` - The Dataset's maximum unpacking depth of nested JSON fields.
+* `delete_protected` - the current state of the Dataset's deletion protection status.
+* `created_at` - ISO8601-formatted time the dataset was created.
+* `last_written_at` - ISO8601-formatted time the dataset was last written to (received event data).
+
diff --git a/docs/data-sources/datasets.md b/docs/data-sources/datasets.md
index 2d0b71c..b634de9 100644
--- a/docs/data-sources/datasets.md
+++ b/docs/data-sources/datasets.md
@@ -1,28 +1,19 @@
 # Data Source: honeycombio_datasets
 
-The datasets data source allows the datasets of an account to be retrieved.
+The Datasets data source retrieves the Environment's Datasets.
 
 ## Example Usage
 
 ```hcl
 # returns all datasets
-data "honeycombio_datasets" "all" {
-}
+data "honeycombio_datasets" "all" {}
 
-# only returns the datasets starting with 'foo_'
+# only returns the datasets with names starting with 'foo_'
 data "honeycombio_datasets" "foo" {
-  starts_with = "foo_"
-}
-
-# create a resource for every dataset
-resource "honeycombio_derived_column" "duration_ms_log10" {
-    for_each = toset(data.honeycombio_datasets.all.names)
-
-    alias       = "duration_ms_log10"
-    expression  = "LOG10($duration_ms)"
-    description = "LOG10 of duration_ms"
-
-    dataset = each.key
+  detail_filter {
+    name        = "name"
+    value_regex = "foo_*"
+  }
 }
 ```
 
@@ -30,7 +21,16 @@ resource "honeycombio_derived_column" "duration_ms_log10" {
 
 The following arguments are supported:
 
-* `starts_with` - (Optional) Only return datasets starting with the given value.
+* `detail_filter` - (Optional) a block to further filter results as described below. `name` must be set when providing a filter. Conflicts with `starts_with`.
+* `starts_with` - (Optional) Deprecated: use `detail_filter` instead. Only return datasets whose name starts with the given value.
+
+To filter the results, a `detail_filter` block can be provided which accepts the following arguments:
+
+* `name` - (Required) The name of the detail field to filter by. Currently only `name` is supported.
+* `value` - (Optional) The value of the detail field to match on.
+* `value_regex` - (Optional) A regular expression string to apply to the value of the detail field to match on.
+
+~> **Note** one of `value` or `value_regex` is required.
 
 ## Attribute Reference
 
diff --git a/docs/resources/dataset.md b/docs/resources/dataset.md
index c23638a..09d949f 100644
--- a/docs/resources/dataset.md
+++ b/docs/resources/dataset.md
@@ -1,10 +1,8 @@
 # Resource: honeycombio_dataset
 
-Creates a dataset.
+Creates a Dataset in an Environment.
 
--> **Note** If this dataset already exists, creating this resource is a no-op.
-
--> **Note** Destroying or replacing this resource will not delete the created dataset. It's not possible to delete a dataset using the API.
+-> **Note**: prior to version 0.27.0 of the provider, datasets were *not* deleted on destroy but left in place and only removed from state.
 
 ## Example Usage
 
@@ -22,14 +20,15 @@ The following arguments are supported:
 * `name` - (Required) The name of the dataset.
 * `description` - (Optional) A longer description for dataset.
 * `expand_json_depth` - (Optional) The maximum unpacking depth of nested JSON fields.
+* `delete_protected` - (Optional) the current state of the Dataset's deletion protection status. Defaults to true. Cannot be set to false on create.
 
 ## Attribute Reference
 
 In addition to all arguments above, the following attributes are exported:
 
 * `slug` - The slug of the dataset.
-* `created_at` - ISO8601 formatted time the column was created
-* `last_written_at` - ISO8601 formatted time the column was last written to (received event data)
+* `created_at` - ISO8601-formatted time the dataset was created
+* `last_written_at` - ISO8601-formatted time the dataset was last written to (received event data)
 
 ## Import
 
@@ -38,5 +37,3 @@ Datasets can be imported by their slug, e.g.
 ```shell
 $ terraform import honeycombio_dataset.my_dataset my-dataset
 ```
-
-You can find the slug in the URL bar when visiting the Dataset from the UI.
diff --git a/honeycombio/data_source_datasets.go b/honeycombio/data_source_datasets.go
deleted file mode 100644
index 5668859..0000000
--- a/honeycombio/data_source_datasets.go
+++ /dev/null
@@ -1,71 +0,0 @@
-package honeycombio
-
-import (
-	"context"
-	"strconv"
-	"strings"
-
-	"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
-	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
-
-	"github.com/honeycombio/terraform-provider-honeycombio/internal/helper/hashcode"
-)
-
-func dataSourceHoneycombioDatasets() *schema.Resource {
-	return &schema.Resource{
-		ReadContext: dataSourceHoneycombioDatasetsRead,
-
-		Schema: map[string]*schema.Schema{
-			"starts_with": {
-				Type:     schema.TypeString,
-				Optional: true,
-			},
-			"names": {
-				Type:     schema.TypeList,
-				Computed: true,
-				Elem: &schema.Schema{
-					Type: schema.TypeString,
-				},
-			},
-			"slugs": {
-				Type:     schema.TypeList,
-				Computed: true,
-				Elem: &schema.Schema{
-					Type: schema.TypeString,
-				},
-			},
-		},
-	}
-}
-
-func dataSourceHoneycombioDatasetsRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
-	client, err := getConfiguredClient(meta)
-	if err != nil {
-		return diagFromErr(err)
-	}
-
-	datasets, err := client.Datasets.List(ctx)
-	if err != nil {
-		return diagFromErr(err)
-	}
-
-	var startsWith string
-	if value, ok := d.GetOk("starts_with"); ok {
-		startsWith = value.(string)
-	}
-
-	names := make([]string, 0, len(datasets))
-	slugs := make([]string, 0, len(datasets))
-	for _, d := range datasets {
-		if startsWith != "" && !strings.HasPrefix(d.Name, startsWith) {
-			continue
-		}
-		names = append(names, d.Name)
-		slugs = append(slugs, d.Slug)
-	}
-	d.Set("names", names)
-	d.Set("slugs", slugs)
-
-	d.SetId(strconv.Itoa(hashcode.String(strings.Join(names, ","))))
-	return nil
-}
diff --git a/honeycombio/provider.go b/honeycombio/provider.go
index e3087db..c5bc0be 100644
--- a/honeycombio/provider.go
+++ b/honeycombio/provider.go
@@ -51,7 +51,6 @@ func Provider(version string) *schema.Provider {
 			},
 		},
 		DataSourcesMap: map[string]*schema.Resource{
-			"honeycombio_datasets":          dataSourceHoneycombioDatasets(),
 			"honeycombio_column":            dataSourceHoneycombioColumn(),
 			"honeycombio_columns":           dataSourceHoneycombioColumns(),
 			"honeycombio_query_result":      dataSourceHoneycombioQueryResult(),
@@ -62,7 +61,6 @@ func Provider(version string) *schema.Provider {
 		ResourcesMap: map[string]*schema.Resource{
 			"honeycombio_board":                      newBoard(),
 			"honeycombio_column":                     newColumn(),
-			"honeycombio_dataset":                    newDataset(),
 			"honeycombio_dataset_definition":         newDatasetDefinition(),
 			"honeycombio_derived_column":             newDerivedColumn(),
 			"honeycombio_marker":                     newMarker(),
diff --git a/honeycombio/resource_dataset.go b/honeycombio/resource_dataset.go
deleted file mode 100644
index c3d85f8..0000000
--- a/honeycombio/resource_dataset.go
+++ /dev/null
@@ -1,129 +0,0 @@
-package honeycombio
-
-import (
-	"context"
-	"errors"
-	"time"
-
-	"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
-	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
-	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
-
-	honeycombio "github.com/honeycombio/terraform-provider-honeycombio/client"
-)
-
-func newDataset() *schema.Resource {
-	return &schema.Resource{
-		CreateContext: resourceDatasetCreate,
-		ReadContext:   resourceDatasetRead,
-		UpdateContext: resourceDatasetUpdate,
-		DeleteContext: schema.NoopContext,
-		Importer: &schema.ResourceImporter{
-			StateContext: schema.ImportStatePassthroughContext,
-		},
-
-		Schema: map[string]*schema.Schema{
-			"name": {
-				Type:         schema.TypeString,
-				Required:     true,
-				ForceNew:     true,
-				ValidateFunc: validation.StringLenBetween(1, 255),
-			},
-			"description": {
-				Type:         schema.TypeString,
-				Optional:     true,
-				ValidateFunc: validation.StringLenBetween(0, 1024),
-			},
-			"slug": {
-				Type:     schema.TypeString,
-				Computed: true,
-			},
-			"expand_json_depth": {
-				Type:         schema.TypeInt,
-				Optional:     true,
-				ValidateFunc: validation.IntBetween(0, 10),
-			},
-			"created_at": {
-				Type:     schema.TypeString,
-				Computed: true,
-				Required: false,
-				Optional: false,
-			},
-			"last_written_at": {
-				Type:     schema.TypeString,
-				Computed: true,
-				Required: false,
-				Optional: false,
-			},
-		},
-	}
-}
-
-func resourceDatasetCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
-	client, err := getConfiguredClient(meta)
-	if err != nil {
-		return diagFromErr(err)
-	}
-
-	data := &honeycombio.Dataset{
-		Name:            d.Get("name").(string),
-		Description:     d.Get("description").(string),
-		ExpandJSONDepth: d.Get("expand_json_depth").(int),
-	}
-	dataset, err := client.Datasets.Create(ctx, data)
-	if err != nil {
-		return diag.FromErr(err)
-	}
-
-	d.SetId(dataset.Slug)
-	return resourceDatasetRead(ctx, d, meta)
-}
-
-func resourceDatasetRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
-	client, err := getConfiguredClient(meta)
-	if err != nil {
-		return diagFromErr(err)
-	}
-
-	var detailedErr honeycombio.DetailedError
-	dataset, err := client.Datasets.Get(ctx, d.Id())
-	if errors.As(err, &detailedErr) {
-		if detailedErr.IsNotFound() {
-			d.SetId("")
-			return nil
-		} else {
-			return diagFromDetailedErr(detailedErr)
-		}
-	} else if err != nil {
-		return diag.FromErr(err)
-	}
-
-	d.SetId(dataset.Slug)
-	d.Set("name", dataset.Name)
-	d.Set("description", dataset.Description)
-	d.Set("expand_json_depth", dataset.ExpandJSONDepth)
-	d.Set("slug", dataset.Slug)
-	d.Set("created_at", dataset.CreatedAt.UTC().Format(time.RFC3339))
-	d.Set("last_written_at", dataset.CreatedAt.UTC().Format(time.RFC3339))
-	return nil
-}
-
-func resourceDatasetUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
-	client, err := getConfiguredClient(meta)
-	if err != nil {
-		return diagFromErr(err)
-	}
-
-	data := &honeycombio.Dataset{
-		Name:            d.Get("name").(string),
-		Description:     d.Get("description").(string),
-		ExpandJSONDepth: d.Get("expand_json_depth").(int),
-	}
-	dataset, err := client.Datasets.Update(ctx, data)
-	if err != nil {
-		return diagFromErr(err)
-	}
-
-	d.SetId(dataset.Slug)
-	return resourceDatasetRead(ctx, d, meta)
-}
diff --git a/internal/helper/modifiers/deletion_protection.go b/internal/helper/modifiers/deletion_protection.go
index 77ed598..13caebd 100644
--- a/internal/helper/modifiers/deletion_protection.go
+++ b/internal/helper/modifiers/deletion_protection.go
@@ -24,7 +24,7 @@ func (m deletionProtectionModifier) PlanModifyBool(ctx context.Context, req plan
 	if req.State.Raw.IsNull() && !req.PlanValue.ValueBool() {
 		resp.Diagnostics.AddAttributeError(
 			path.Root("").AtName("delete_protected"),
-			"Environment validation error",
+			"Validation error",
 			"Delete protection cannot be disabled at creation.",
 		)
 	}
diff --git a/internal/models/datasets.go b/internal/models/datasets.go
new file mode 100644
index 0000000..86f12dc
--- /dev/null
+++ b/internal/models/datasets.go
@@ -0,0 +1,22 @@
+package models
+
+import "github.com/hashicorp/terraform-plugin-framework/types"
+
+type DatasetResourceModel struct {
+	ID              types.String `tfsdk:"id"`
+	Name            types.String `tfsdk:"name"`
+	Slug            types.String `tfsdk:"slug"`
+	Description     types.String `tfsdk:"description"`
+	ExpandJSONDepth types.Int32  `tfsdk:"expand_json_depth"`
+	DeleteProtected types.Bool   `tfsdk:"delete_protected"`
+	CreatedAt       types.String `tfsdk:"created_at"`
+	LastWrittenAt   types.String `tfsdk:"last_written_at"`
+}
+
+type DatasetsDataSourceModel struct {
+	ID           types.String        `tfsdk:"id"`
+	StartsWith   types.String        `tfsdk:"starts_with"`
+	DetailFilter []DetailFilterModel `tfsdk:"detail_filter"`
+	Names        []types.String      `tfsdk:"names"`
+	Slugs        []types.String      `tfsdk:"slugs"`
+}
diff --git a/internal/provider/dataset_data_source.go b/internal/provider/dataset_data_source.go
new file mode 100644
index 0000000..22799f7
--- /dev/null
+++ b/internal/provider/dataset_data_source.go
@@ -0,0 +1,127 @@
+package provider
+
+import (
+	"context"
+	"fmt"
+	"time"
+
+	"github.com/hashicorp/terraform-plugin-framework/datasource"
+	"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
+	"github.com/hashicorp/terraform-plugin-framework/types"
+
+	"github.com/honeycombio/terraform-provider-honeycombio/client"
+	"github.com/honeycombio/terraform-provider-honeycombio/internal/helper"
+	"github.com/honeycombio/terraform-provider-honeycombio/internal/models"
+)
+
+// Ensure the implementation satisfies the expected interfaces.
+var (
+	_ datasource.DataSource              = &datasetDataSource{}
+	_ datasource.DataSourceWithConfigure = &datasetDataSource{}
+)
+
+func NewDatasetDataSource() datasource.DataSource {
+	return &datasetDataSource{}
+}
+
+type datasetDataSource struct {
+	client *client.Client
+}
+
+func (d *datasetDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
+	resp.TypeName = req.ProviderTypeName + "_dataset"
+}
+
+func (d *datasetDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
+	resp.Schema = schema.Schema{
+		Description: "Fetches the details of a single Dataset.",
+		Attributes: map[string]schema.Attribute{
+			"slug": schema.StringAttribute{
+				Description: "The Slug of the Dataset to fetch.",
+				Required:    true,
+			},
+			"id": schema.StringAttribute{
+				Description: "The ID of the Dataset.",
+				Computed:    true,
+				Optional:    false,
+				Required:    false,
+			},
+			"name": schema.StringAttribute{
+				Description: "The name of the Dataset.",
+				Computed:    true,
+				Optional:    false,
+				Required:    false,
+			},
+			"description": schema.StringAttribute{
+				Description: "The Dataset's description.",
+				Computed:    true,
+				Optional:    false,
+				Required:    false,
+			},
+			"expand_json_depth": schema.Int32Attribute{
+				Description: "The Dataset's maximum unpacking depth of nested JSON fields.",
+				Computed:    true,
+				Optional:    false,
+				Required:    false,
+			},
+			"delete_protected": schema.BoolAttribute{
+				Description: "The current delete protection status of the Environment.",
+				Computed:    true,
+				Optional:    false,
+				Required:    false,
+			},
+			"created_at": schema.StringAttribute{
+				Description: "ISO8601-formatted time the dataset was created.",
+				Computed:    true,
+				Required:    false,
+				Optional:    false,
+			},
+			"last_written_at": schema.StringAttribute{
+				Description: "ISO8601-formatted time the dataset was last written to (received event data).",
+				Computed:    true,
+				Required:    false,
+				Optional:    false,
+			},
+		},
+	}
+}
+
+func (d *datasetDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
+	w := getClientFromDatasourceRequest(&req)
+	if w == nil {
+		return
+	}
+
+	c, err := w.V1Client()
+	if err != nil || c == nil {
+		resp.Diagnostics.AddError("Failed to configure client", err.Error())
+		return
+	}
+	d.client = c
+}
+
+func (d *datasetDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
+	var data models.DatasetResourceModel
+	resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+
+	ds, err := d.client.Datasets.Get(ctx, data.Slug.ValueString())
+	if helper.AddDiagnosticOnError(&resp.Diagnostics,
+		fmt.Sprintf("Looking up Dataset %q", data.Slug.ValueString()), err) {
+		return
+	}
+
+	data.ID = types.StringValue(ds.Slug)
+	data.Slug = types.StringValue(ds.Slug)
+	data.Name = types.StringValue(ds.Name)
+	data.Description = types.StringValue(ds.Description)
+	data.ExpandJSONDepth = types.Int32Value(int32(ds.ExpandJSONDepth))
+	data.DeleteProtected = types.BoolPointerValue(ds.Settings.DeleteProtected)
+	data.CreatedAt = types.StringValue(ds.CreatedAt.Format(time.RFC3339))
+	data.LastWrittenAt = types.StringValue(ds.LastWrittenAt.Format(time.RFC3339))
+
+	diags := resp.State.Set(ctx, &data)
+	resp.Diagnostics.Append(diags...)
+}
diff --git a/internal/provider/dataset_resource.go b/internal/provider/dataset_resource.go
new file mode 100644
index 0000000..5fc6a13
--- /dev/null
+++ b/internal/provider/dataset_resource.go
@@ -0,0 +1,292 @@
+package provider
+
+import (
+	"context"
+	"errors"
+	"net/http"
+	"time"
+
+	"github.com/hashicorp/terraform-plugin-framework-validators/int32validator"
+	"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
+	"github.com/hashicorp/terraform-plugin-framework/resource"
+	"github.com/hashicorp/terraform-plugin-framework/resource/schema"
+	"github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+	"github.com/hashicorp/terraform-plugin-framework/resource/schema/int32default"
+	"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+	"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+	"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+	"github.com/hashicorp/terraform-plugin-framework/schema/validator"
+	"github.com/hashicorp/terraform-plugin-framework/types"
+
+	"github.com/honeycombio/terraform-provider-honeycombio/client"
+	"github.com/honeycombio/terraform-provider-honeycombio/internal/helper"
+	"github.com/honeycombio/terraform-provider-honeycombio/internal/helper/modifiers"
+	"github.com/honeycombio/terraform-provider-honeycombio/internal/models"
+)
+
+// Ensure the implementation satisfies the expected interfaces.
+var (
+	_ resource.Resource                = &datasetResource{}
+	_ resource.ResourceWithConfigure   = &datasetResource{}
+	_ resource.ResourceWithModifyPlan  = &datasetResource{}
+	_ resource.ResourceWithImportState = &datasetResource{}
+)
+
+type datasetResource struct {
+	client *client.Client
+}
+
+func NewDatasetResource() resource.Resource {
+	return &datasetResource{}
+}
+
+func (*datasetResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+	resp.TypeName = req.ProviderTypeName + "_dataset"
+}
+
+func (r *datasetResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
+	w := getClientFromResourceRequest(&req)
+	if w == nil {
+		return
+	}
+
+	c, err := w.V1Client()
+	if err != nil || c == nil {
+		resp.Diagnostics.AddError("Failed to configure client", err.Error())
+		return
+	}
+	r.client = c
+}
+
+func (*datasetResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
+	resp.Schema = schema.Schema{
+		Description: "Manages a Dataset in a Honeycomb Environment.",
+		Attributes: map[string]schema.Attribute{
+			"id": schema.StringAttribute{
+				Computed:    true,
+				Required:    false,
+				Optional:    false,
+				Description: "The ID of the Dataset.",
+				PlanModifiers: []planmodifier.String{
+					stringplanmodifier.UseStateForUnknown(),
+				},
+			},
+			"name": schema.StringAttribute{
+				Required:    true,
+				Description: "The name of the Dataset.",
+				Validators: []validator.String{
+					stringvalidator.LengthBetween(1, 255),
+				},
+			},
+			"slug": schema.StringAttribute{
+				Description: "The slug of the Dataset.",
+				Computed:    true,
+				Optional:    false,
+				Required:    false,
+			},
+			"description": schema.StringAttribute{
+				Description: "The Dataset's description.",
+				Optional:    true,
+				Computed:    true,
+				Default:     stringdefault.StaticString(""),
+				Validators: []validator.String{
+					stringvalidator.LengthAtMost(1024),
+				},
+			},
+			"expand_json_depth": schema.Int32Attribute{
+				Description: "The maximum unpacking depth of nested JSON fields.",
+				Computed:    true,
+				Optional:    true,
+				Default:     int32default.StaticInt32(0),
+				Validators: []validator.Int32{
+					int32validator.Between(0, 10),
+				},
+			},
+			"delete_protected": schema.BoolAttribute{
+				Description: "The current delete protection status of the Dataset. Cannot be set to false on creation.",
+				Default:     booldefault.StaticBool(true),
+				Computed:    true,
+				Optional:    true,
+				PlanModifiers: []planmodifier.Bool{
+					modifiers.EnforceDeletionProtection(),
+				},
+			},
+			"created_at": schema.StringAttribute{
+				Description: "ISO8601-formatted time the dataset was created.",
+				Computed:    true,
+				Required:    false,
+				Optional:    false,
+			},
+			"last_written_at": schema.StringAttribute{
+				Description: "ISO8601-formatted time the dataset was last written to (received event data).",
+				Computed:    true,
+				Required:    false,
+				Optional:    false,
+			},
+		},
+	}
+}
+
+func (r *datasetResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+	if req.ID == "" {
+		resp.Diagnostics.AddError("Invalid Import ID", "The Dataset Slug must be provided")
+		return
+	}
+
+	resp.Diagnostics.Append(resp.State.Set(ctx, &models.DatasetResourceModel{
+		ID: types.StringValue(req.ID),
+	})...)
+}
+
+func (r *datasetResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+	if req.Plan.Raw.IsNull() {
+		// If the entire plan is null, the resource is planned for destruction -- let's add a warning
+		resp.Diagnostics.AddWarning(
+			"Resource Destruction Warning",
+			"Applying this plan will delete the Dataset and all of its contents. "+
+				"This is an irreversible operation.",
+		)
+	}
+}
+
+func (r *datasetResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+	var plan models.DatasetResourceModel
+	resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+
+	ds, err := r.client.Datasets.Create(ctx, &client.Dataset{
+		Name:            plan.Name.ValueString(),
+		Description:     plan.Description.ValueString(),
+		ExpandJSONDepth: int(plan.ExpandJSONDepth.ValueInt32()),
+	})
+	if errors.Is(err, client.ErrDatasetExists) {
+		resp.Diagnostics.AddError("Error Creating Honeycomb Dataset", "Dataset \""+plan.Name.ValueString()+"\" already exists")
+		return
+	} else if helper.AddDiagnosticOnError(&resp.Diagnostics, "Creating Dataset", err) {
+		return
+	}
+
+	var state models.DatasetResourceModel
+	state.ID = types.StringValue(ds.Slug)
+	state.Slug = types.StringValue(ds.Slug)
+	state.Name = types.StringValue(ds.Name)
+	state.Description = types.StringValue(ds.Description)
+	state.ExpandJSONDepth = types.Int32Value(int32(ds.ExpandJSONDepth))
+	state.DeleteProtected = types.BoolPointerValue(ds.Settings.DeleteProtected)
+	state.CreatedAt = types.StringValue(ds.CreatedAt.Format(time.RFC3339))
+	state.LastWrittenAt = types.StringValue(ds.LastWrittenAt.Format(time.RFC3339))
+
+	resp.Diagnostics.Append(resp.State.Set(ctx, state)...)
+}
+
+func (r *datasetResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+	var state models.DatasetResourceModel
+	resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+
+	var detailedErr client.DetailedError
+	ds, err := r.client.Datasets.Get(ctx, state.ID.ValueString())
+	if errors.As(err, &detailedErr) {
+		if detailedErr.IsNotFound() {
+			// if not found consider it deleted -- so just remove it from state
+			resp.State.RemoveResource(ctx)
+			return
+		} else {
+			resp.Diagnostics.Append(helper.NewDetailedErrorDiagnostic(
+				"Error Reading Honeycomb Dataset",
+				&detailedErr,
+			))
+		}
+	} else if err != nil {
+		resp.Diagnostics.AddError(
+			"Error Reading Honeycomb Dataset",
+			"Unexpected error reading Dataset "+state.ID.ValueString()+": "+err.Error(),
+		)
+	}
+	if resp.Diagnostics.HasError() {
+		return
+	}
+
+	state.ID = types.StringValue(ds.Slug)
+	state.Name = types.StringValue(ds.Name)
+	state.Slug = types.StringValue(ds.Slug)
+	state.Description = types.StringValue(ds.Description)
+	state.ExpandJSONDepth = types.Int32Value(int32(ds.ExpandJSONDepth))
+	state.DeleteProtected = types.BoolPointerValue(ds.Settings.DeleteProtected)
+	state.CreatedAt = types.StringValue(ds.CreatedAt.Format(time.RFC3339))
+	state.LastWrittenAt = types.StringValue(ds.LastWrittenAt.Format(time.RFC3339))
+
+	resp.Diagnostics.Append(resp.State.Set(ctx, state)...)
+}
+
+func (r *datasetResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+	var plan, state models.DatasetResourceModel
+	resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+	resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+
+	_, err := r.client.Datasets.Update(ctx, &client.Dataset{
+		Slug:            plan.ID.ValueString(),
+		Description:     plan.Description.ValueString(),
+		ExpandJSONDepth: int(plan.ExpandJSONDepth.ValueInt32()),
+		Settings: client.DatasetSettings{
+			DeleteProtected: plan.DeleteProtected.ValueBoolPointer(),
+		},
+	})
+	if helper.AddDiagnosticOnError(&resp.Diagnostics, "Updating Honeycomb Dataset", err) {
+		return
+	}
+
+	ds, err := r.client.Datasets.Get(ctx, plan.ID.ValueString())
+	if helper.AddDiagnosticOnError(&resp.Diagnostics, "Updating Honeycomb Dataset", err) {
+		return
+	}
+
+	state.Name = types.StringValue(ds.Name)
+	state.Slug = types.StringValue(ds.Slug)
+	state.Description = types.StringValue(ds.Description)
+	state.ExpandJSONDepth = types.Int32Value(int32(ds.ExpandJSONDepth))
+	state.DeleteProtected = types.BoolPointerValue(ds.Settings.DeleteProtected)
+	state.CreatedAt = types.StringValue(ds.CreatedAt.Format(time.RFC3339))
+	state.LastWrittenAt = types.StringValue(ds.LastWrittenAt.Format(time.RFC3339))
+
+	resp.Diagnostics.Append(resp.State.Set(ctx, state)...)
+}
+
+func (r *datasetResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+	var state models.DatasetResourceModel
+	resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+
+	err := r.client.Datasets.Delete(ctx, state.ID.ValueString())
+	var detailedErr client.DetailedError
+	if err != nil {
+		if errors.As(err, &detailedErr) {
+			if detailedErr.Status == http.StatusConflict {
+				resp.Diagnostics.AddError(
+					"Unable to Delete Dataset",
+					"Delete Protection is enabled. "+
+						"You must disable delete protection before it can be deleted.",
+				)
+			} else {
+				resp.Diagnostics.Append(helper.NewDetailedErrorDiagnostic(
+					"Error Deleting Honeycomb Dataset",
+					&detailedErr,
+				))
+			}
+		} else {
+			resp.Diagnostics.AddError(
+				"Error Deleting Honeycomb Dataset",
+				"Could not delete Dataset "+state.ID.ValueString()+": "+err.Error(),
+			)
+		}
+	}
+}
diff --git a/internal/provider/datasets_data_source.go b/internal/provider/datasets_data_source.go
new file mode 100644
index 0000000..b5b62e6
--- /dev/null
+++ b/internal/provider/datasets_data_source.go
@@ -0,0 +1,133 @@
+package provider
+
+import (
+	"context"
+	"regexp"
+
+	"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
+	"github.com/hashicorp/terraform-plugin-framework/datasource"
+	"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
+	"github.com/hashicorp/terraform-plugin-framework/path"
+	"github.com/hashicorp/terraform-plugin-framework/schema/validator"
+	"github.com/hashicorp/terraform-plugin-framework/types"
+
+	"github.com/honeycombio/terraform-provider-honeycombio/client"
+	"github.com/honeycombio/terraform-provider-honeycombio/internal/helper"
+	"github.com/honeycombio/terraform-provider-honeycombio/internal/helper/filter"
+	"github.com/honeycombio/terraform-provider-honeycombio/internal/helper/hashcode"
+	"github.com/honeycombio/terraform-provider-honeycombio/internal/models"
+)
+
+// Ensure the implementation satisfies the expected interfaces.
+var (
+	_ datasource.DataSource              = &datasetsDataSource{}
+	_ datasource.DataSourceWithConfigure = &datasetsDataSource{}
+)
+
+func NewDatasetsDataSource() datasource.DataSource {
+	return &datasetsDataSource{}
+}
+
+type datasetsDataSource struct {
+	client *client.Client
+}
+
+func (d *datasetsDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
+	resp.TypeName = req.ProviderTypeName + "_datasets"
+}
+
+func (d *datasetsDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
+	w := getClientFromDatasourceRequest(&req)
+	if w == nil {
+		return
+	}
+
+	c, err := w.V1Client()
+	if err != nil || c == nil {
+		resp.Diagnostics.AddError("Failed to configure client", err.Error())
+		return
+	}
+	d.client = c
+}
+
+func (d *datasetsDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
+	resp.Schema = schema.Schema{
+		Description: "Fetches the Datasets in an Environment.",
+		Attributes: map[string]schema.Attribute{
+			"id": schema.StringAttribute{
+				Computed: true,
+				Optional: false,
+				Required: false,
+			},
+			"starts_with": schema.StringAttribute{
+				DeprecationMessage: "Use the `detail_filter` block instead.",
+				Description:        "The prefix to filter the Dataset Names by.",
+				Optional:           true,
+				Validators: []validator.String{
+					stringvalidator.ConflictsWith(path.MatchRelative().AtParent().AtName("detail_filter")),
+				},
+			},
+			"names": schema.ListAttribute{
+				Description: "The list returned of Dataset Names.",
+				Computed:    true,
+				Optional:    false,
+				Required:    false,
+				ElementType: types.StringType,
+			},
+			"slugs": schema.ListAttribute{
+				Description: "The list returned of Dataset Slugs.",
+				Computed:    true,
+				Optional:    false,
+				Required:    false,
+				ElementType: types.StringType,
+			},
+		},
+		Blocks: map[string]schema.Block{
+			"detail_filter": detailFilterSchema(),
+		},
+	}
+}
+
+func (d *datasetsDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
+	var data models.DatasetsDataSourceModel
+	resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+
+	datasets, err := d.client.Datasets.List(ctx)
+	if helper.AddDiagnosticOnError(&resp.Diagnostics, "Listing Datasets", err) {
+		return
+	}
+
+	var nameFilter *filter.DetailFilter
+	if len(data.DetailFilter) > 0 {
+		nameFilter, err = data.DetailFilter[0].NewFilter()
+		if err != nil {
+			resp.Diagnostics.AddError("Unable to create Dataset filter", err.Error())
+			return
+		}
+	} else if !data.StartsWith.IsNull() {
+		exp, err := regexp.Compile(data.StartsWith.ValueString() + "*")
+		if err != nil {
+			resp.Diagnostics.AddError("Unable to create Dataset filter", err.Error())
+			return
+		}
+		nameFilter = &filter.DetailFilter{
+			Type:       "name",
+			ValueRegex: exp,
+		}
+	}
+
+	for _, e := range datasets {
+		if !nameFilter.MatchName(e.Name) {
+			continue
+		}
+		data.Names = append(data.Names, types.StringValue(e.Name))
+		data.Slugs = append(data.Slugs, types.StringValue(e.Slug))
+	}
+	data.ID = types.StringValue(hashcode.StringValues(data.Slugs))
+
+	diags := resp.State.Set(ctx, &data)
+	resp.Diagnostics.Append(diags...)
+}
diff --git a/internal/provider/environment_data_source.go b/internal/provider/environment_data_source.go
index 0db4894..090cfaa 100644
--- a/internal/provider/environment_data_source.go
+++ b/internal/provider/environment_data_source.go
@@ -15,8 +15,8 @@ import (
 
 // Ensure the implementation satisfies the expected interfaces.
 var (
-	_ datasource.DataSource              = &sloDataSource{}
-	_ datasource.DataSourceWithConfigure = &sloDataSource{}
+	_ datasource.DataSource              = &environmentDataSource{}
+	_ datasource.DataSourceWithConfigure = &environmentDataSource{}
 )
 
 func NewEnvironmentDataSource() datasource.DataSource {
diff --git a/internal/provider/provider.go b/internal/provider/provider.go
index daa87e1..c24e330 100644
--- a/internal/provider/provider.go
+++ b/internal/provider/provider.go
@@ -77,6 +77,7 @@ func (p *HoneycombioProvider) Schema(_ context.Context, _ provider.SchemaRequest
 func (p *HoneycombioProvider) Resources(ctx context.Context) []func() resource.Resource {
 	return []func() resource.Resource{
 		NewBurnAlertResource,
+		NewDatasetResource,
 		NewTriggerResource,
 		NewQueryResource,
 		NewAPIKeyResource,
@@ -87,6 +88,8 @@ func (p *HoneycombioProvider) Resources(ctx context.Context) []func() resource.R
 func (p *HoneycombioProvider) DataSources(ctx context.Context) []func() datasource.DataSource {
 	return []func() datasource.DataSource{
 		NewAuthMetadataDataSource,
+		NewDatasetDataSource,
+		NewDatasetsDataSource,
 		NewDerivedColumnDataSource,
 		NewDerivedColumnsDataSource,
 		NewEnvironmentDataSource,


Test patch:
diff --git a/client/dataset_test.go b/client/dataset_test.go
index abcabd9..67d5bcb 100644
--- a/client/dataset_test.go
+++ b/client/dataset_test.go
@@ -2,98 +2,113 @@ package client_test
 
 import (
 	"context"
+	"net/http"
 	"testing"
+	"time"
 
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 
 	"github.com/honeycombio/terraform-provider-honeycombio/client"
+	"github.com/honeycombio/terraform-provider-honeycombio/internal/helper"
+	"github.com/honeycombio/terraform-provider-honeycombio/internal/helper/test"
 )
 
 func TestDatasets(t *testing.T) {
 	t.Parallel()
 
 	ctx := context.Background()
-
 	c := newTestClient(t)
-	datasetName := testDataset(t)
-
-	currentDataset := &client.Dataset{
-		Name: datasetName,
-	}
 
-	t.Run("List", func(t *testing.T) {
-		d, err := c.Datasets.List(ctx)
+	t.Run("happy path", func(t *testing.T) {
+		// create a new dataset
+		name := test.RandomStringWithPrefix("test.", 10)
+		description := test.RandomString(70)
+		ds, err := c.Datasets.Create(ctx, &client.Dataset{
+			Name:            name,
+			Description:     description,
+			ExpandJSONDepth: 2,
+		})
 		require.NoError(t, err)
-
-		for _, dataset := range d {
-			assert.NotNil(t, dataset.LastWrittenAt, "last written at is empty")
-			assert.NotNil(t, dataset.CreatedAt, "created at is empty")
-			// copy dynamic fields before asserting - will be skipped if expected dataset not found
-			if dataset.Name == currentDataset.Name {
-				currentDataset.Slug = dataset.Slug
-				currentDataset.LastWrittenAt = dataset.LastWrittenAt
-				currentDataset.CreatedAt = dataset.CreatedAt
-			}
+		assert.Equal(t, name, ds.Name)
+		assert.NotEmpty(t, ds.Slug)
+		assert.Equal(t, description, ds.Description)
+		assert.Equal(t, 2, ds.ExpandJSONDepth)
+		assert.True(t, *ds.Settings.DeleteProtected)
+		assert.WithinDuration(t, time.Now(), ds.CreatedAt, 5*time.Second)
+
+		// read back the dataset by Slug and compare
+		dataset, err := c.Datasets.Get(ctx, ds.Slug)
+		require.NoError(t, err)
+		assert.Equal(t, ds.Name, dataset.Name)
+		assert.Equal(t, ds.Slug, dataset.Slug)
+		assert.Equal(t, ds.Description, dataset.Description)
+		assert.Equal(t, ds.ExpandJSONDepth, dataset.ExpandJSONDepth)
+		if assert.NotNil(t, dataset.Settings) {
+			assert.True(t, *dataset.Settings.DeleteProtected)
 		}
-		assert.Contains(t, d, *currentDataset, "could not find current dataset with List")
-	})
 
-	t.Run("Get", func(t *testing.T) {
-		d, err := c.Datasets.Get(ctx, currentDataset.Slug)
+		// list all datasets and verify the dataset is in the list
+		dds, err := c.Datasets.List(ctx)
 		require.NoError(t, err)
+		assert.Contains(t, dds, *dataset)
+
+		// update the dataset's description and expand_json_depth
+		newDescription := test.RandomString(70)
+		dataset, err = c.Datasets.Update(ctx, &client.Dataset{
+			Slug:            dataset.Slug,
+			Description:     newDescription,
+			ExpandJSONDepth: 5,
+			Settings:        dataset.Settings,
+		})
+		require.NoError(t, err)
+		assert.Equal(t, ds.Slug, dataset.Slug)
+		assert.Equal(t, newDescription, dataset.Description)
+		assert.Equal(t, 5, dataset.ExpandJSONDepth)
+		assert.True(t, *dataset.Settings.DeleteProtected)
 
-		assert.NotNil(t, d.LastWrittenAt, "last written at is empty")
-		assert.NotNil(t, d.CreatedAt, "created at is empty")
-		// copy dynamic fields before asserting equality
-		d.LastWrittenAt = currentDataset.LastWrittenAt
-		d.CreatedAt = currentDataset.CreatedAt
-
-		assert.Equal(t, *currentDataset, *d)
-	})
-
-	t.Run("Fail to Get bogus Dataset", func(t *testing.T) {
-		_, err := c.Datasets.Get(ctx, "does-not-exist")
-
+		// try to delete the dataset with deletion protection enabled
 		var de client.DetailedError
-		require.Error(t, err)
+		err = c.Datasets.Delete(ctx, dataset.Slug)
 		require.ErrorAs(t, err, &de)
-		assert.True(t, de.IsNotFound())
-	})
-
-	t.Run("Create", func(t *testing.T) {
-		createDataset := &client.Dataset{
-			Name: datasetName,
-		}
-		d, err := c.Datasets.Create(ctx, createDataset)
+		assert.Equal(t, http.StatusConflict, de.Status)
+
+		// disable deletion protection and delete the dataset
+		_, err = c.Datasets.Update(ctx, &client.Dataset{
+			Slug: dataset.Slug,
+			Settings: client.DatasetSettings{
+				DeleteProtected: helper.ToPtr(false),
+			},
+		})
+		require.NoError(t, err)
+		err = c.Datasets.Delete(ctx, dataset.Slug)
 		require.NoError(t, err)
 
-		assert.NotNil(t, d.LastWrittenAt, "last written at is empty")
-		assert.NotNil(t, d.CreatedAt, "created at is empty")
-		// copy dynamic fields before asserting equality
-		d.LastWrittenAt = currentDataset.LastWrittenAt
-		d.CreatedAt = currentDataset.CreatedAt
-
-		assert.Equal(t, currentDataset, d)
+		// verify the dataset was deleted
+		_, err = c.Datasets.Get(ctx, dataset.Slug)
+		require.ErrorAs(t, err, &de)
+		assert.True(t, de.IsNotFound())
 	})
 
-	t.Run("Update", func(t *testing.T) {
-		updatedDescription := "buzzing with data"
-		updatedExpandJSONDepth := 3
-
-		updateDataset := &client.Dataset{
-			Name:            datasetName,
-			Description:     updatedDescription,
-			ExpandJSONDepth: updatedExpandJSONDepth,
-		}
-		t.Cleanup(func() {
-			// revert updated fields to defaults after the test run
-			//nolint:errcheck
-			c.Datasets.Update(ctx, &client.Dataset{Name: datasetName})
+	t.Run("returns DatasetExistsErr when creating dataset with same name", func(t *testing.T) {
+		name := test.RandomStringWithPrefix("test.", 10)
+		ds, err := c.Datasets.Create(ctx, &client.Dataset{
+			Name: name,
 		})
-		d, err := c.Datasets.Update(ctx, updateDataset)
 		require.NoError(t, err)
-		assert.Equal(t, d.Description, updatedDescription)
-		assert.Equal(t, d.ExpandJSONDepth, updatedExpandJSONDepth)
+		t.Cleanup(func() {
+			c.Datasets.Update(ctx, &client.Dataset{
+				Slug: ds.Slug,
+				Settings: client.DatasetSettings{
+					DeleteProtected: helper.ToPtr(false),
+				},
+			})
+			c.Datasets.Delete(ctx, ds.Slug)
+		})
+
+		_, err = c.Datasets.Create(ctx, &client.Dataset{
+			Name: name,
+		})
+		require.ErrorIs(t, err, client.ErrDatasetExists)
 	})
 }
diff --git a/honeycombio/data_source_datasets_test.go b/honeycombio/data_source_datasets_test.go
deleted file mode 100644
index 3fcca3d..0000000
--- a/honeycombio/data_source_datasets_test.go
+++ /dev/null
@@ -1,49 +0,0 @@
-package honeycombio
-
-import (
-	"fmt"
-	"strings"
-	"testing"
-
-	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
-)
-
-func TestAccDataSourceHoneycombioDataset_basic(t *testing.T) {
-	dataset := testAccDataset()
-
-	resource.Test(t, resource.TestCase{
-		PreCheck:                 testAccPreCheck(t),
-		ProtoV5ProviderFactories: testAccProtoV5ProviderFactory,
-		Steps: []resource.TestStep{
-			{
-				Config: testAccDataSourceDatasetConfig(nil),
-				Check: resource.ComposeTestCheckFunc(
-					testCheckOutputContains("names", dataset),
-				),
-			},
-			{
-				Config: testAccDataSourceDatasetConfig([]string{"starts_with = \"" + dataset[0:2] + "\""}),
-				Check: resource.ComposeTestCheckFunc(
-					testCheckOutputContains("names", dataset),
-				),
-			},
-			{
-				Config: testAccDataSourceDatasetConfig([]string{"starts_with = \"foo\""}),
-				Check: resource.ComposeTestCheckFunc(
-					testCheckOutputDoesNotContain("names", dataset),
-				),
-			},
-		},
-	})
-}
-
-func testAccDataSourceDatasetConfig(filters []string) string {
-	return fmt.Sprintf(`
-data "honeycombio_datasets" "test" {
-	%s
-}
-
-output "names" {
-  value = data.honeycombio_datasets.test.names
-}`, strings.Join(filters, "\n"))
-}
diff --git a/honeycombio/resource_dataset_test.go b/honeycombio/resource_dataset_test.go
deleted file mode 100644
index ef70757..0000000
--- a/honeycombio/resource_dataset_test.go
+++ /dev/null
@@ -1,63 +0,0 @@
-package honeycombio
-
-import (
-	"context"
-	"fmt"
-	"testing"
-
-	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
-	"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
-)
-
-func TestAccHoneycombioDataset_basic(t *testing.T) {
-	testDatasetName := "testacc-test"
-
-	resource.Test(t, resource.TestCase{
-		PreCheck:                 testAccPreCheck(t),
-		ProtoV5ProviderFactories: testAccProtoV5ProviderFactory,
-		Steps: []resource.TestStep{
-			{
-				Config: testAccDatasetConfig(testDatasetName, "a nice description", 3),
-				Check: resource.ComposeTestCheckFunc(
-					testAccCheckDatasetExists(t, "honeycombio_dataset.test"),
-					resource.TestCheckResourceAttr("honeycombio_dataset.test", "name", testDatasetName),
-					resource.TestCheckResourceAttr("honeycombio_dataset.test", "description", "a nice description"),
-					resource.TestCheckResourceAttr("honeycombio_dataset.test", "expand_json_depth", "3"),
-				),
-			},
-			{
-				ResourceName:      "honeycombio_dataset.test",
-				ImportStateId:     testDatasetName,
-				ImportState:       true,
-				ImportStateVerify: true,
-			},
-		},
-	})
-}
-
-func testAccDatasetConfig(name, description string, depth int) string {
-	return fmt.Sprintf(`
-resource "honeycombio_dataset" "test" {
-  name              = "%s"
-  description       = "%s"
-  expand_json_depth = %d
-}`, name, description, depth)
-}
-
-// testAccCheckDatasetExists queries the API to verify the Dataset exists
-func testAccCheckDatasetExists(t *testing.T, name string) resource.TestCheckFunc {
-	return func(s *terraform.State) error {
-		resourceState, ok := s.RootModule().Resources[name]
-		if !ok {
-			return fmt.Errorf("not found: %s", name)
-		}
-
-		c := testAccClient(t)
-		_, err := c.Datasets.Get(context.Background(), resourceState.Primary.ID)
-		if err != nil {
-			return fmt.Errorf("could not retrieve dataset: %w", err)
-		}
-
-		return nil
-	}
-}
diff --git a/internal/provider/dataset_data_source_test.go b/internal/provider/dataset_data_source_test.go
new file mode 100644
index 0000000..6af208e
--- /dev/null
+++ b/internal/provider/dataset_data_source_test.go
@@ -0,0 +1,58 @@
+package provider
+
+import (
+	"context"
+	"fmt"
+	"testing"
+
+	"github.com/hashicorp/terraform-plugin-testing/helper/resource"
+	"github.com/stretchr/testify/require"
+
+	"github.com/honeycombio/terraform-provider-honeycombio/client"
+	"github.com/honeycombio/terraform-provider-honeycombio/internal/helper"
+	"github.com/honeycombio/terraform-provider-honeycombio/internal/helper/test"
+)
+
+func TestAcc_DatsetDataSource(t *testing.T) {
+	ctx := context.Background()
+	c := testAccClient(t)
+
+	ds, err := c.Datasets.Create(ctx, &client.Dataset{
+		Name:            test.RandomStringWithPrefix("test.", 20),
+		Description:     test.RandomString(70),
+		ExpandJSONDepth: 3,
+	})
+	require.NoError(t, err)
+
+	t.Cleanup(func() {
+		// disable deletion protection and delete the Dataset
+		c.Datasets.Update(ctx, &client.Dataset{
+			Slug: ds.Slug,
+			Settings: client.DatasetSettings{
+				DeleteProtected: helper.ToPtr(false),
+			},
+		})
+		c.Datasets.Delete(ctx, ds.Slug)
+	})
+
+	resource.Test(t, resource.TestCase{
+		PreCheck:                 testAccPreCheckV2API(t),
+		ProtoV5ProviderFactories: testAccProtoV5ProviderFactory,
+		Steps: []resource.TestStep{
+			{
+				Config: fmt.Sprintf(`
+data "honeycombio_dataset" "test" {
+  slug = "%s"
+}`, ds.Slug),
+				Check: resource.ComposeAggregateTestCheckFunc(
+					resource.TestCheckResourceAttr("data.honeycombio_dataset.test", "id", ds.Slug),
+					resource.TestCheckResourceAttr("data.honeycombio_dataset.test", "name", ds.Name),
+					resource.TestCheckResourceAttr("data.honeycombio_dataset.test", "slug", ds.Slug),
+					resource.TestCheckResourceAttr("data.honeycombio_dataset.test", "expand_json_depth", "3"),
+					resource.TestCheckResourceAttr("data.honeycombio_dataset.test", "description", ds.Description),
+					resource.TestCheckResourceAttr("data.honeycombio_dataset.test", "delete_protected", "true"),
+				),
+			},
+		},
+	})
+}
diff --git a/internal/provider/dataset_resource_test.go b/internal/provider/dataset_resource_test.go
new file mode 100644
index 0000000..e368fed
--- /dev/null
+++ b/internal/provider/dataset_resource_test.go
@@ -0,0 +1,228 @@
+package provider
+
+import (
+	"context"
+	"fmt"
+	"regexp"
+	"testing"
+
+	"github.com/hashicorp/terraform-plugin-testing/helper/resource"
+	"github.com/hashicorp/terraform-plugin-testing/terraform"
+	"github.com/stretchr/testify/require"
+
+	"github.com/honeycombio/terraform-provider-honeycombio/client"
+	"github.com/honeycombio/terraform-provider-honeycombio/internal/helper"
+	"github.com/honeycombio/terraform-provider-honeycombio/internal/helper/test"
+)
+
+func TestAcc_DatasetResource(t *testing.T) {
+
+	t.Run("happy path", func(t *testing.T) {
+		name := test.RandomStringWithPrefix("test.", 20)
+		resource.Test(t, resource.TestCase{
+			PreCheck:                 testAccPreCheck(t),
+			ProtoV5ProviderFactories: testAccProtoV5ProviderFactory,
+			Steps: []resource.TestStep{
+				{
+					Config: testAccConfigBasicDatasetTest(name, "test description", 0, true),
+					Check: resource.ComposeAggregateTestCheckFunc(
+						testAccEnsureDatasetExists(t, "honeycombio_dataset.test"),
+						resource.TestCheckResourceAttrSet("honeycombio_dataset.test", "id"),
+						resource.TestCheckResourceAttrSet("honeycombio_dataset.test", "slug"),
+						resource.TestCheckResourceAttr("honeycombio_dataset.test", "name", name),
+						resource.TestCheckResourceAttr("honeycombio_dataset.test", "description", "test description"),
+						resource.TestCheckResourceAttr("honeycombio_dataset.test", "expand_json_depth", "0"),
+						resource.TestCheckResourceAttr("honeycombio_dataset.test", "delete_protected", "true"),
+						resource.TestCheckResourceAttrSet("honeycombio_dataset.test", "created_at"),
+						resource.TestCheckResourceAttrSet("honeycombio_dataset.test", "last_written_at"),
+					),
+				},
+				{ // fail to delete protected dataset
+					Config:      testAccConfigBasicDatasetTest(name, "test description", 0, true),
+					Destroy:     true,
+					ExpectError: regexp.MustCompile(`must disable delete protection`),
+				},
+				{ // now update the description and JSON expansion, and disable delete protection
+					Config: testAccConfigBasicDatasetTest(name, "new description", 3, false),
+					Check: resource.ComposeAggregateTestCheckFunc(
+						testAccEnsureDatasetExists(t, "honeycombio_dataset.test"),
+						resource.TestCheckResourceAttrSet("honeycombio_dataset.test", "id"),
+						resource.TestCheckResourceAttrSet("honeycombio_dataset.test", "slug"),
+						resource.TestCheckResourceAttr("honeycombio_dataset.test", "name", name),
+						resource.TestCheckResourceAttr("honeycombio_dataset.test", "description", "new description"),
+						resource.TestCheckResourceAttr("honeycombio_dataset.test", "expand_json_depth", "3"),
+						resource.TestCheckResourceAttr("honeycombio_dataset.test", "delete_protected", "false"),
+						resource.TestCheckResourceAttrSet("honeycombio_dataset.test", "created_at"),
+						resource.TestCheckResourceAttrSet("honeycombio_dataset.test", "last_written_at"),
+					),
+				},
+				{
+					ResourceName: "honeycombio_dataset.test",
+					ImportState:  true,
+				},
+			},
+		})
+	})
+
+	t.Run("default values", func(t *testing.T) {
+		name := test.RandomStringWithPrefix("test.", 20)
+		resource.Test(t, resource.TestCase{
+			PreCheck:                 testAccPreCheck(t),
+			ProtoV5ProviderFactories: testAccProtoV5ProviderFactory,
+			Steps: []resource.TestStep{
+				{
+					Config: fmt.Sprintf(`
+resource "honeycombio_dataset" "test" {
+  name = "%s"
+}`, name),
+					Check: resource.ComposeAggregateTestCheckFunc(
+						testAccEnsureDatasetExists(t, "honeycombio_dataset.test"),
+						resource.TestCheckResourceAttrSet("honeycombio_dataset.test", "id"),
+						resource.TestCheckResourceAttrSet("honeycombio_dataset.test", "slug"),
+						resource.TestCheckResourceAttr("honeycombio_dataset.test", "name", name),
+						resource.TestCheckResourceAttr("honeycombio_dataset.test", "description", ""),
+						resource.TestCheckResourceAttr("honeycombio_dataset.test", "expand_json_depth", "0"),
+						resource.TestCheckResourceAttr("honeycombio_dataset.test", "delete_protected", "true"),
+						resource.TestCheckResourceAttrSet("honeycombio_dataset.test", "created_at"),
+						resource.TestCheckResourceAttrSet("honeycombio_dataset.test", "last_written_at"),
+					),
+				},
+				{ // disable delete protection to allow cleanup
+					Config: fmt.Sprintf(`
+resource "honeycombio_dataset" "test" {
+  name             = "%s"
+  delete_protected = false
+}`, name),
+				},
+			},
+		})
+	})
+
+	t.Run("fails to create dataset with delete protection disabled", func(t *testing.T) {
+		resource.Test(t, resource.TestCase{
+			PreCheck:                 testAccPreCheck(t),
+			ProtoV5ProviderFactories: testAccProtoV5ProviderFactory,
+			Steps: []resource.TestStep{
+				{
+					Config:      testAccConfigBasicDatasetTest("nope", "", 0, false),
+					PlanOnly:    true,
+					ExpectError: regexp.MustCompile(`protection cannot be disabled at creation`),
+				},
+			},
+		})
+	})
+
+	t.Run("fails to create a dataset with duplicate name", func(t *testing.T) {
+		ctx := context.Background()
+		c := testAccClient(t)
+
+		ds, err := c.Datasets.Create(ctx, &client.Dataset{
+			Name: test.RandomStringWithPrefix("test.", 20),
+		})
+		require.NoError(t, err)
+		t.Cleanup(func() {
+			// disable deletion protection and delete the Dataset
+			c.Datasets.Update(ctx, &client.Dataset{
+				Slug: ds.Slug,
+				Settings: client.DatasetSettings{
+					DeleteProtected: helper.ToPtr(false),
+				},
+			})
+			c.Datasets.Delete(ctx, ds.Slug)
+		})
+
+		resource.Test(t, resource.TestCase{
+			PreCheck:                 testAccPreCheckV2API(t),
+			ProtoV5ProviderFactories: testAccProtoV5ProviderFactory,
+			Steps: []resource.TestStep{
+				{
+					Config:      testAccConfigBasicDatasetTest(ds.Name, "", 0, true),
+					ExpectError: regexp.MustCompile(`Dataset "` + ds.Name + `" already exists`),
+				},
+			},
+		})
+	})
+}
+
+// TestAcc_DatasetResource_UpgradeFromVersion026 tests the migration case from the
+// last SDK-based version of the Dataset resource to the current Framework-based version.
+//
+// See: https://developer.hashicorp.com/terraform/plugin/framework/migrating/testing#testing-migration
+func TestAcc_DatasetResource_UpgradeFromVersion026(t *testing.T) {
+	name := test.RandomStringWithPrefix("test.", 20)
+
+	resource.Test(t, resource.TestCase{
+		PreCheck: testAccPreCheck(t),
+		Steps: []resource.TestStep{
+			{
+				ExternalProviders: map[string]resource.ExternalProvider{
+					"honeycombio": {
+						VersionConstraint: "0.26.0",
+						Source:            "honeycombio/honeycombio",
+					},
+				},
+				Config: fmt.Sprintf(`
+resource "honeycombio_dataset" "test" {
+  name = "%s"
+}`, name),
+				Check: resource.ComposeAggregateTestCheckFunc(
+					testAccEnsureDatasetExists(t, "honeycombio_dataset.test"),
+				),
+			},
+			{
+				ProtoV5ProviderFactories: testAccProtoV5ProviderFactory,
+				Config: fmt.Sprintf(`
+resource "honeycombio_dataset" "test" {
+  name = "%s"
+}`, name),
+				Check: resource.ComposeAggregateTestCheckFunc(
+					resource.TestCheckResourceAttrSet("honeycombio_dataset.test", "id"),
+					resource.TestCheckResourceAttrSet("honeycombio_dataset.test", "slug"),
+					resource.TestCheckResourceAttr("honeycombio_dataset.test", "name", name),
+					resource.TestCheckResourceAttr("honeycombio_dataset.test", "description", ""),
+					resource.TestCheckResourceAttr("honeycombio_dataset.test", "expand_json_depth", "0"),
+					resource.TestCheckResourceAttr("honeycombio_dataset.test", "delete_protected", "true"),
+					resource.TestCheckResourceAttrSet("honeycombio_dataset.test", "created_at"),
+					resource.TestCheckResourceAttrSet("honeycombio_dataset.test", "last_written_at"),
+				),
+			},
+			{ // disable delete protection to allow cleanup
+				ProtoV5ProviderFactories: testAccProtoV5ProviderFactory,
+				Config: fmt.Sprintf(`
+resource "honeycombio_dataset" "test" {
+  name             = "%s"
+  delete_protected = false
+}`, name),
+			},
+		},
+	},
+	)
+}
+
+func testAccConfigBasicDatasetTest(name, description string, jsonDepth int, protected bool) string {
+	return fmt.Sprintf(`
+resource "honeycombio_dataset" "test" {
+  name        = "%s"
+  description = "%s"
+
+  expand_json_depth = %d
+  delete_protected  = %t
+}`, name, description, jsonDepth, protected)
+}
+
+func testAccEnsureDatasetExists(t *testing.T, name string) resource.TestCheckFunc { //nolint:unparam
+	return func(s *terraform.State) error {
+		rs, ok := s.RootModule().Resources[name]
+		if !ok {
+			return fmt.Errorf("resource not found: %s", name)
+		}
+
+		client := testAccClient(t)
+		_, err := client.Datasets.Get(context.Background(), rs.Primary.ID)
+		if err != nil {
+			return fmt.Errorf("failed to fetch created Dataset: %s", err)
+		}
+
+		return nil
+	}
+}
diff --git a/internal/provider/datasets_data_source_test.go b/internal/provider/datasets_data_source_test.go
new file mode 100644
index 0000000..fd60b0e
--- /dev/null
+++ b/internal/provider/datasets_data_source_test.go
@@ -0,0 +1,78 @@
+package provider
+
+import (
+	"context"
+	"fmt"
+	"testing"
+
+	"github.com/hashicorp/terraform-plugin-testing/helper/resource"
+	"github.com/stretchr/testify/require"
+
+	"github.com/honeycombio/terraform-provider-honeycombio/client"
+	"github.com/honeycombio/terraform-provider-honeycombio/internal/helper"
+	"github.com/honeycombio/terraform-provider-honeycombio/internal/helper/test"
+)
+
+func TestAcc_DatasetsDatasource(t *testing.T) {
+	ctx := context.Background()
+	c := testAccClient(t)
+	const numDatasets = 15
+
+	// create a bunch of datasets
+	testDatasets := make([]*client.Dataset, numDatasets)
+	for i := 0; i < numDatasets; i++ {
+		d, err := c.Datasets.Create(ctx, &client.Dataset{
+			Name:        test.RandomStringWithPrefix("test.ds.", 20),
+			Description: test.RandomString(70),
+		})
+		require.NoError(t, err)
+		testDatasets[i] = d
+	}
+
+	t.Cleanup(func() {
+		for _, d := range testDatasets {
+			// disable deletion protection and delete the Dataset
+			c.Datasets.Update(ctx, &client.Dataset{
+				Slug: d.Slug,
+				Settings: client.DatasetSettings{
+					DeleteProtected: helper.ToPtr(false),
+				},
+			})
+			c.Datasets.Delete(ctx, d.Slug)
+		}
+	})
+
+	resource.Test(t, resource.TestCase{
+		PreCheck:                 testAccPreCheckV2API(t),
+		ProtoV5ProviderFactories: testAccProtoV5ProviderFactory,
+		Steps: []resource.TestStep{
+			{
+				Config: fmt.Sprintf(`
+data "honeycombio_datasets" "all" {}
+
+data "honeycombio_datasets" "regex" {
+  detail_filter {
+    name        = "name"
+    value_regex = "test.ds.*"
+  }
+}
+
+data "honeycombio_datasets" "starts_with" {
+  starts_with = "test.ds."
+}
+
+data "honeycombio_datasets" "exact" {
+  detail_filter {
+    name  = "name"
+    value = "%s"
+  }
+}`, testDatasets[0].Name),
+				Check: resource.ComposeAggregateTestCheckFunc(
+					resource.TestCheckResourceAttr("data.honeycombio_datasets.regex", "slugs.#", fmt.Sprintf("%d", numDatasets)),
+					resource.TestCheckResourceAttr("data.honeycombio_datasets.starts_with", "slugs.#", fmt.Sprintf("%d", numDatasets)),
+					resource.TestCheckResourceAttr("data.honeycombio_datasets.exact", "slugs.#", "1"),
+				),
+			},
+		},
+	})
+}
diff --git a/internal/provider/environments_data_source_test.go b/internal/provider/environments_data_source_test.go
index f63f16c..85b1754 100644
--- a/internal/provider/environments_data_source_test.go
+++ b/internal/provider/environments_data_source_test.go
@@ -16,14 +16,11 @@ func TestAcc_EnvironmentsDatasource(t *testing.T) {
 	const numEnvs = 15
 
 	// create a bunch of environments
-	testEnvs := make([]*v2client.Environment, numEnvs+1)
+	testEnvs := make([]*v2client.Environment, numEnvs)
 	for i := 0; i < numEnvs; i++ {
 		e := testAccEnvironment(ctx, t, c)
 		testEnvs[i] = e
 	}
-	// one additional with a different prefix for filter testing
-	e := testAccEnvironment(ctx, t, c)
-	testEnvs[numEnvs] = e
 
 	resource.Test(t, resource.TestCase{
 		PreCheck:                 testAccPreCheckV2API(t),
@@ -45,12 +42,12 @@ data "honeycombio_environments" "exact" {
     name  = "name"
     value = "%s"
   }
-}`, e.Name),
+}`, testEnvs[0].Name),
 				Check: resource.ComposeAggregateTestCheckFunc(
 					resource.TestCheckResourceAttr(
 						"data.honeycombio_environments.regex",
 						"ids.#",
-						fmt.Sprintf("%d", numEnvs+1), // +1 because of the additional environment created
+						fmt.Sprintf("%d", numEnvs),
 					),
 					resource.TestCheckResourceAttr("data.honeycombio_environments.exact", "ids.#", "1"),
 				),
diff --git a/internal/provider/honeycombio_sweeper_test.go b/internal/provider/honeycombio_sweeper_test.go
index 164532b..afb5464 100644
--- a/internal/provider/honeycombio_sweeper_test.go
+++ b/internal/provider/honeycombio_sweeper_test.go
@@ -27,10 +27,46 @@ func TestMain(m *testing.M) {
 }
 
 func init() {
+	resource.AddTestSweepers("datasets", getDatasetSweeper("datasets"))
 	resource.AddTestSweepers("environments", getEnvironmentSweeper("environments"))
 	resource.AddTestSweepers("recipients", getRecipientSweeper("recipients"))
 }
 
+func getDatasetSweeper(name string) *resource.Sweeper {
+	return &resource.Sweeper{
+		Name: name,
+		F: func(_ string) error {
+			ctx := context.Background()
+			c, err := client.NewClient()
+			if err != nil {
+				return fmt.Errorf("could not initialize client: %w", err)
+			}
+			datasets, err := c.Datasets.List(ctx)
+			if err != nil {
+				return fmt.Errorf("could not list datasets: %w", err)
+			}
+
+			for _, ds := range datasets {
+				if strings.HasPrefix(ds.Name, SweeperTargetPrefix) {
+					log.Printf("[DEBUG] deleting dataset %s (%s)", ds.Name, ds.Slug)
+					c.Datasets.Update(ctx, &client.Dataset{
+						Slug: ds.Slug,
+						Settings: client.DatasetSettings{
+							DeleteProtected: client.ToPtr(false),
+						},
+					})
+					err = c.Datasets.Delete(ctx, ds.Slug)
+					if err != nil {
+						log.Printf("[ERROR] could not delete dataset %s: %s", ds.Slug, err)
+					}
+				}
+			}
+
+			return nil
+		},
+	}
+}
+
 func getEnvironmentSweeper(name string) *resource.Sweeper {
 	return &resource.Sweeper{
 		Name: name,