Repository: honeycombio/terraform-provider-honeycombio
Pull request #635 (2025-03-13 19:24:26)

feat(slo): add md slo support for slo data source
<!--
Thank you for contributing to the project! 💜
Please see our [OSS process document](https://github.com/honeycombio/home/blob/main/honeycomb-oss-lifecycle-and-practices.md#) to get an idea of how we operate.
-->

## Which problem is this PR solving?
https://app.asana.com/1/72322038700732/project/1205961717360815/task/1205427815943458?focus=true

## Short description of the changes
Adds Multi Dataset support for SLO data source 
## How to verify that this has the expected result
Added unit tests to ensure SLO  read with `data honeycombio_slo` is still possible with optional dataset argument for MD SLOs

## Manual Verifications:
MD SLO:
<img width="752" alt="Screenshot 2025-03-14 at 2 07 01 PM" src="https://github.com/user-attachments/assets/ae48ceb0-71ab-4b7c-88fe-6bde1ec77202" />

Single SLO w/dataset argument:
<img width="752" alt="Screenshot 2025-03-14 at 2 03 32 PM" src="https://github.com/user-attachments/assets/65fbada9-fcc5-4c12-b495-0520d685312f" />

Single SLO w/o dataset argument:
<img width="750" alt="Screenshot 2025-03-14 at 2 02 15 PM" src="https://github.com/user-attachments/assets/4a3388f7-2083-4b61-8909-9bcac77e9e6a" />


ronybzzz: @jharley in the screenshot the dataset field returns a null value. should we return something to the dataset field when its omitted from the config? spoke with Madhu about it but we're not sure if anything might be relying on a value being returned? and if so, what would we make the value for MD SLOs, would it just be "__all__"?

Patch:
diff --git a/client/client.go b/client/client.go
index c2900c8..21df29a 100644
--- a/client/client.go
+++ b/client/client.go
@@ -30,6 +30,8 @@ const (
 	// Deprecated: use DefaultAPIKeyEnv instead. To be removed in v1.0
 	LegacyAPIKeyEnv  = "HONEYCOMBIO_APIKEY"
 	defaultUserAgent = "go-honeycombio"
+
+	EnvironmentWideSlug = "__all__"
 )
 
 // Config holds all configuration options for the client.
diff --git a/client/slo.go b/client/slo.go
index cd57e4c..28fdfe7 100644
--- a/client/slo.go
+++ b/client/slo.go
@@ -45,6 +45,7 @@ type SLO struct {
 	Description      string    `json:"description,omitempty"`
 	TimePeriodDays   int       `json:"time_period_days"`
 	TargetPerMillion int       `json:"target_per_million"`
+	DatasetSlugs     []string  `json:"dataset_slugs,omitempty"`
 	SLI              SLIRef    `json:"sli"`
 	CreatedAt        time.Time `json:"created_at,omitempty"`
 	UpdatedAt        time.Time `json:"updated_at,omitempty"`
diff --git a/docs/data-sources/slo.md b/docs/data-sources/slo.md
index 7c22724..22feb59 100644
--- a/docs/data-sources/slo.md
+++ b/docs/data-sources/slo.md
@@ -1,18 +1,13 @@
 # Data Source: honeycombio_slo
 
-The `honeycombio_slo` data source retrieves the details of a single SLO for a dataset.
+The `honeycombio_slo` data source retrieves the details of a single SLO.  
 If you want to retreive multiple SLOs, use the `honeycombio_slos` data source instead.
 
 ## Example Usage
 
 ```hcl
-variable "dataset" {
-  type = string
-}
-
 # Retrieve the details of a single SLO
 data "honeycombio_slo" "myslo" {
-  dataset = var.dataset
   id      = "fS4WfA82ACt"
 }
 ```
@@ -21,8 +16,8 @@ data "honeycombio_slo" "myslo" {
 
 The following arguments are supported:
 
-* `dataset` - (Required) The dataset this SLO is associated with
 * `id` - (Required) The ID of the SLO
+* `dataset` - (Deprecated) No longer required. The dataset this SLO is associated with.
 
 ## Attribute Reference
 
@@ -31,5 +26,6 @@ In addition to all arguments above, the following attributes are exported:
 * `name` - the name of the SLO.
 * `description` - the SLO's description.
 * `sli` - the alias of the Derived Column used as the SLO's SLI.
+* `datasets` - A list of dataset slugs the SLO is evaluated on.
 * `target_percentage` - the percentage of qualified events expected to succeed during the `time_period`.
-* `time_period` - The time period, in days, over which the SLO is evaluated.
+* `time_period` - The time period, in days, over which the SLO is evaluated.
\ No newline at end of file
diff --git a/docs/data-sources/slos.md b/docs/data-sources/slos.md
index 19a0e35..b41d33a 100644
--- a/docs/data-sources/slos.md
+++ b/docs/data-sources/slos.md
@@ -1,7 +1,9 @@
 # Data Source: honeycombio_slos
-
 The SLOs data source retrieves the SLOs of a dataset, with the option of narrowing the retrieval by providing a `detail_filter`.
 
+~> **Note** Multi-Dataset SLOs are not supported yet for this data source.
+
+
 ## Example Usage
 
 ```hcl
diff --git a/internal/models/slo.go b/internal/models/slo.go
index f344605..764f4ec 100644
--- a/internal/models/slo.go
+++ b/internal/models/slo.go
@@ -12,11 +12,12 @@ type SLOsDataSourceModel struct {
 }
 
 type SLODataSourceModel struct {
-	ID               types.String  `tfsdk:"id"`
-	Dataset          types.String  `tfsdk:"dataset"`
-	Name             types.String  `tfsdk:"name"`
-	Description      types.String  `tfsdk:"description"`
-	SLI              types.String  `tfsdk:"sli"`
-	TargetPercentage types.Float64 `tfsdk:"target_percentage"`
-	TimePeriod       types.Int64   `tfsdk:"time_period"`
+	ID               types.String   `tfsdk:"id"`
+	Dataset          types.String   `tfsdk:"dataset"`
+	Datasets         []types.String `tfsdk:"datasets"`
+	Name             types.String   `tfsdk:"name"`
+	Description      types.String   `tfsdk:"description"`
+	SLI              types.String   `tfsdk:"sli"`
+	TargetPercentage types.Float64  `tfsdk:"target_percentage"`
+	TimePeriod       types.Int64    `tfsdk:"time_period"`
 }
diff --git a/internal/provider/slo_data_source.go b/internal/provider/slo_data_source.go
index 6358205..1d6598e 100644
--- a/internal/provider/slo_data_source.go
+++ b/internal/provider/slo_data_source.go
@@ -41,8 +41,18 @@ func (d *sloDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, re
 				Required:    true,
 			},
 			"dataset": schema.StringAttribute{
-				Description: "The dataset to fetch the SLO from.",
-				Required:    true,
+				Description:        "The dataset to fetch the SLO from.",
+				Computed:           true,
+				Optional:           true,
+				Required:           false,
+				DeprecationMessage: "No longer required.",
+			},
+			"datasets": schema.ListAttribute{
+				ElementType: types.StringType,
+				Description: "The datasets the SLO is evaluated on.",
+				Computed:    true,
+				Optional:    false,
+				Required:    false,
 			},
 			"name": schema.StringAttribute{
 				Description: "The name of the SLO.",
@@ -99,7 +109,13 @@ func (d *sloDataSource) Read(ctx context.Context, req datasource.ReadRequest, re
 		return
 	}
 
-	slo, err := d.client.SLOs.Get(ctx, data.Dataset.ValueString(), data.ID.ValueString())
+	dataset := data.Dataset.ValueString()
+
+	if data.Dataset.IsNull() {
+		dataset = "__all__"
+	}
+
+	slo, err := d.client.SLOs.Get(ctx, dataset, data.ID.ValueString())
 	if helper.AddDiagnosticOnError(&resp.Diagnostics,
 		fmt.Sprintf("Looking up SLO %q", data.ID.ValueString()),
 		err) {
@@ -112,6 +128,10 @@ func (d *sloDataSource) Read(ctx context.Context, req datasource.ReadRequest, re
 	data.SLI = types.StringValue(slo.SLI.Alias)
 	data.TargetPercentage = types.Float64Value(float64(slo.TargetPerMillion) / 10000)
 	data.TimePeriod = types.Int64Value(int64(slo.TimePeriodDays))
+	data.Datasets = make([]types.String, len(slo.DatasetSlugs))
+	for i, slug := range slo.DatasetSlugs {
+		data.Datasets[i] = types.StringValue(slug)
+	}
 
 	diags := resp.State.Set(ctx, &data)
 	resp.Diagnostics.Append(diags...)


Test patch:
diff --git a/client/slo_test.go b/client/slo_test.go
index 0c10a07..3f76540 100644
--- a/client/slo_test.go
+++ b/client/slo_test.go
@@ -8,6 +8,7 @@ import (
 	"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"
 )
 
@@ -51,6 +52,8 @@ func TestSLOs(t *testing.T) {
 		data.ID = slo.ID
 		data.CreatedAt = slo.CreatedAt
 		data.UpdatedAt = slo.UpdatedAt
+		data.DatasetSlugs = []string{dataset}
+
 		assert.Equal(t, data, slo)
 	})
 
@@ -97,3 +100,124 @@ func TestSLOs(t *testing.T) {
 		assert.True(t, de.IsNotFound())
 	})
 }
+
+func Test_MDSLOs(t *testing.T) {
+	t.Parallel()
+
+	ctx := context.Background()
+	var err error
+
+	c := newTestClient(t)
+
+	if c.IsClassic(ctx) {
+		t.Skip("Classic does not support multi-dataset SLOs")
+	}
+
+	// test setup for MD SLO
+	var mdSLO *client.SLO
+	var mdSLI *client.DerivedColumn
+	var dataset1, dataset2 *client.Dataset
+
+	dataset1, err = c.Datasets.Create(ctx, &client.Dataset{
+		Name:        test.RandomStringWithPrefix("test.", 10),
+		Description: "test dataset 1",
+	})
+	require.NoError(t, err)
+
+	dataset2, err = c.Datasets.Create(ctx, &client.Dataset{
+		Name:        test.RandomStringWithPrefix("test.", 10),
+		Description: "test dataset 2",
+	})
+	require.NoError(t, err)
+
+	mdSLI, err = c.DerivedColumns.Create(ctx, client.EnvironmentWideSlug, &client.DerivedColumn{
+		Alias:       test.RandomStringWithPrefix("test.", 10),
+		Description: "test SLI",
+		Expression:  "BOOL(1)",
+	})
+	require.NoError(t, err)
+
+	t.Cleanup(func() {
+		c.SLOs.Delete(ctx, client.EnvironmentWideSlug, mdSLO.ID)
+		c.DerivedColumns.Delete(ctx, client.EnvironmentWideSlug, mdSLI.ID)
+
+		c.Datasets.Update(ctx, &client.Dataset{
+			Slug: dataset1.Slug,
+			Settings: client.DatasetSettings{
+				DeleteProtected: helper.ToPtr(false),
+			},
+		})
+		err = c.Datasets.Delete(ctx, dataset1.Slug)
+
+		c.Datasets.Update(ctx, &client.Dataset{
+			Slug: dataset2.Slug,
+			Settings: client.DatasetSettings{
+				DeleteProtected: helper.ToPtr(false),
+			},
+		})
+		err = c.Datasets.Delete(ctx, dataset2.Slug)
+	})
+
+	t.Run("Create MD SLO", func(t *testing.T) {
+		if c.IsClassic(ctx) {
+			t.Skip("Classic does not support multi-dataset SLOs")
+		}
+
+		mdData := &client.SLO{
+			Name:             test.RandomStringWithPrefix("test.", 10),
+			Description:      "My Super Sweet Test",
+			TimePeriodDays:   30,
+			TargetPerMillion: 995000,
+			SLI:              client.SLIRef{Alias: mdSLI.Alias},
+			DatasetSlugs:     []string{dataset1.Slug, dataset2.Slug},
+		}
+
+		mdSLO, err = c.SLOs.Create(ctx, client.EnvironmentWideSlug, mdData)
+
+		require.NoError(t, err, "unable to create SLO")
+		assert.NotNil(t, mdSLO.ID, "SLO ID is empty")
+		assert.NotNil(t, mdSLO.CreatedAt, "created at is empty")
+		assert.NotNil(t, mdSLO.UpdatedAt, "updated at is empty")
+		// copy dynamic fields before asserting equality
+		mdData.ID = mdSLO.ID
+		mdData.CreatedAt = mdSLO.CreatedAt
+		mdData.UpdatedAt = mdSLO.UpdatedAt
+		mdData.DatasetSlugs = []string{dataset1.Slug, dataset2.Slug}
+
+		assert.Equal(t, mdData, mdSLO)
+	})
+
+	t.Run("Get MD SLO", func(t *testing.T) {
+		if c.IsClassic(ctx) {
+			t.Skip("Classic does not support multi-dataset SLOs")
+		}
+
+		getMDSLO, err := c.SLOs.Get(ctx, client.EnvironmentWideSlug, mdSLO.ID)
+		require.NoError(t, err, "failed to get MD SLO by ID")
+		assert.Equal(t, *mdSLO, *getMDSLO)
+	})
+
+	t.Run("Update MD SLO", func(t *testing.T) {
+		if c.IsClassic(ctx) {
+			t.Skip("Classic does not support multi-dataset SLOs")
+		}
+
+		mdSLO.Name = test.RandomStringWithPrefix("test.", 10)
+		mdSLO.TimePeriodDays = 14
+		mdSLO.Description = "Even sweeter"
+		mdSLO.TargetPerMillion = 990000
+
+		result, err := c.SLOs.Update(ctx, client.EnvironmentWideSlug, mdSLO)
+
+		require.NoError(t, err, "failed to update MD SLO")
+		// copy dynamic field before asserting equality
+		mdSLO.UpdatedAt = result.UpdatedAt
+		assert.Equal(t, result, mdSLO)
+	})
+
+	t.Run("Delete MD SLO", func(t *testing.T) {
+
+		err = c.SLOs.Delete(ctx, client.EnvironmentWideSlug, mdSLO.ID)
+		require.NoError(t, err, "failed to delete MD SLO")
+	})
+}
diff --git a/internal/provider/slo_data_source_test.go b/internal/provider/slo_data_source_test.go
index 55b9057..5d4b98c 100644
--- a/internal/provider/slo_data_source_test.go
+++ b/internal/provider/slo_data_source_test.go
@@ -10,6 +10,8 @@ import (
 	"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_SLODataSource(t *testing.T) {
@@ -54,6 +56,89 @@ data "honeycombio_slo" "test" {
 					resource.TestCheckResourceAttr("data.honeycombio_slo.test", "sli", slo.SLI.Alias),
 					resource.TestCheckResourceAttr("data.honeycombio_slo.test", "target_percentage", "99.5"),
 					resource.TestCheckResourceAttr("data.honeycombio_slo.test", "time_period", "30"),
+					resource.TestCheckResourceAttr("data.honeycombio_slo.test", "dataset", dataset),
+				),
+			},
+		},
+	})
+}
+
+func TestAcc_MDSLODataSource(t *testing.T) {
+	ctx := context.Background()
+	c := testAccClient(t)
+
+	if c.IsClassic(ctx) {
+		t.Skip("Classic does not support multi-dataset SLOs")
+	}
+	dataset1, err := c.Datasets.Create(ctx, &client.Dataset{
+		Name:        test.RandomStringWithPrefix("test.", 10),
+		Description: "test dataset 1",
+	})
+	require.NoError(t, err)
+
+	dataset2, err := c.Datasets.Create(ctx, &client.Dataset{
+		Name:        test.RandomStringWithPrefix("test.", 10),
+		Description: "test dataset 2",
+	})
+	require.NoError(t, err)
+
+	sli, err := c.DerivedColumns.Create(ctx, client.EnvironmentWideSlug, &client.DerivedColumn{
+		Alias:       test.RandomStringWithPrefix("test.", 10),
+		Description: "test SLI",
+		Expression:  "BOOL(1)",
+	})
+	require.NoError(t, err)
+
+	slo, err := c.SLOs.Create(ctx, client.EnvironmentWideSlug, &client.SLO{
+		Name:             test.RandomStringWithPrefix("test.", 10),
+		Description:      "test SLO",
+		TimePeriodDays:   30,
+		TargetPerMillion: 995000,
+		SLI:              client.SLIRef{Alias: sli.Alias},
+		DatasetSlugs:     []string{dataset1.Slug, dataset2.Slug},
+	})
+	require.NoError(t, err)
+
+	t.Cleanup(func() {
+		c.SLOs.Delete(ctx, client.EnvironmentWideSlug, slo.ID)
+		c.DerivedColumns.Delete(ctx, client.EnvironmentWideSlug, sli.ID)
+
+		c.Datasets.Update(ctx, &client.Dataset{
+			Slug: dataset1.Slug,
+			Settings: client.DatasetSettings{
+				DeleteProtected: helper.ToPtr(false),
+			},
+		})
+		err = c.Datasets.Delete(ctx, dataset1.Slug)
+
+		c.Datasets.Update(ctx, &client.Dataset{
+			Slug: dataset2.Slug,
+			Settings: client.DatasetSettings{
+				DeleteProtected: helper.ToPtr(false),
+			},
+		})
+		err = c.Datasets.Delete(ctx, dataset2.Slug)
+	})
+
+	resource.Test(t, resource.TestCase{
+		PreCheck:                 testAccPreCheck(t),
+		ProtoV5ProviderFactories: testAccProtoV5ProviderFactory,
+		Steps: []resource.TestStep{
+			{
+				Config: fmt.Sprintf(`
+data "honeycombio_slo" "test" {
+  id      = "%s"
+}`, slo.ID),
+				Check: resource.ComposeAggregateTestCheckFunc(
+					resource.TestCheckResourceAttr("data.honeycombio_slo.test", "name", slo.Name),
+					resource.TestCheckResourceAttr("data.honeycombio_slo.test", "description", slo.Description),
+					resource.TestCheckResourceAttr("data.honeycombio_slo.test", "sli", slo.SLI.Alias),
+					resource.TestCheckResourceAttr("data.honeycombio_slo.test", "target_percentage", "99.5"),
+					resource.TestCheckResourceAttr("data.honeycombio_slo.test", "time_period", "30"),
+					resource.TestCheckNoResourceAttr("data.honeycombio_slo.test", "dataset"),
+					resource.TestCheckResourceAttr("data.honeycombio_slo.test", "datasets.#", "2"),
+					resource.TestCheckResourceAttr("data.honeycombio_slo.test", "datasets.0", dataset1.Slug),
+					resource.TestCheckResourceAttr("data.honeycombio_slo.test", "datasets.1", dataset2.Slug),
 				),
 			},
 		},