From 272030be8887f9e519136d69c33fc8423811040b Mon Sep 17 00:00:00 2001 From: Julien Duchesne Date: Thu, 14 Mar 2024 08:33:27 -0400 Subject: [PATCH] Self-contained SM package (#1419) * Self-contained SM package Just like what was done for cloud, slo and ml packages This moves the mapping of resources and client validation to the SM package. This will make it easier to support TF code generation + upgrade to the new TF plugin framework * Remove unused function --- internal/provider/legacy_provider.go | 16 +--- .../provider/legacy_provider_validation.go | 11 --- .../syntheticmonitoring/data_source_probe.go | 10 +-- .../syntheticmonitoring/data_source_probes.go | 9 +-- .../syntheticmonitoring/resource_check.go | 31 ++++---- .../syntheticmonitoring/resource_probe.go | 27 +++---- .../resource_probe_helper_test.go | 76 ------------------- .../resource_probe_test.go | 66 ++++++++++++++++ .../syntheticmonitoring/resources.go | 32 ++++++++ 9 files changed, 135 insertions(+), 143 deletions(-) delete mode 100644 internal/resources/syntheticmonitoring/resource_probe_helper_test.go create mode 100644 internal/resources/syntheticmonitoring/resources.go diff --git a/internal/provider/legacy_provider.go b/internal/provider/legacy_provider.go index cd1ee694b..dd4c963e8 100644 --- a/internal/provider/legacy_provider.go +++ b/internal/provider/legacy_provider.go @@ -71,12 +71,6 @@ func Provider(version string) *schema.Provider { "grafana_user": grafana.ResourceUser(), }) - // Resources that require the Synthetic Monitoring client to exist. - smClientResources = addResourcesMetadataValidation(smClientPresent, map[string]*schema.Resource{ - "grafana_synthetic_monitoring_check": syntheticmonitoring.ResourceCheck(), - "grafana_synthetic_monitoring_probe": syntheticmonitoring.ResourceProbe(), - }) - // Datasources that require the Grafana client to exist. grafanaClientDatasources = addCreateReadResourcesMetadataValidation( readGrafanaClientValidation, @@ -96,12 +90,6 @@ func Provider(version string) *schema.Provider { "grafana_organization": grafana.DatasourceOrganization(), "grafana_organization_preferences": grafana.DatasourceOrganizationPreferences(), }) - - // Datasources that require the Synthetic Monitoring client to exist. - smClientDatasources = addResourcesMetadataValidation(smClientPresent, map[string]*schema.Resource{ - "grafana_synthetic_monitoring_probe": syntheticmonitoring.DataSourceProbe(), - "grafana_synthetic_monitoring_probes": syntheticmonitoring.DataSourceProbes(), - }) ) p := &schema.Provider{ @@ -225,7 +213,7 @@ func Provider(version string) *schema.Provider { grafanaClientResources, machinelearning.ResourcesMap, slo.ResourcesMap, - smClientResources, + syntheticmonitoring.ResourcesMap, oncall.ResourcesMap, cloud.ResourcesMap, ), @@ -234,7 +222,7 @@ func Provider(version string) *schema.Provider { grafanaClientDatasources, machinelearning.DatasourcesMap, slo.DatasourcesMap, - smClientDatasources, + syntheticmonitoring.DatasourcesMap, oncall.DatasourcesMap, cloud.DatasourcesMap, ), diff --git a/internal/provider/legacy_provider_validation.go b/internal/provider/legacy_provider_validation.go index e14a7d338..25436df4a 100644 --- a/internal/provider/legacy_provider_validation.go +++ b/internal/provider/legacy_provider_validation.go @@ -35,17 +35,6 @@ func createGrafanaClientValidation(resourceName string, d *schema.ResourceData, return nil } -func smClientPresent(resourceName string, d *schema.ResourceData, m interface{}) error { - if m.(*common.Client).SMAPI == nil { - return fmt.Errorf("the Synthetic Monitoring client is required for `%s`. Set the sm_access_token provider attribute", resourceName) - } - return nil -} - -func addResourcesMetadataValidation(validateFunc metadataValidation, resources map[string]*schema.Resource) map[string]*schema.Resource { - return addCreateReadResourcesMetadataValidation(validateFunc, validateFunc, resources) -} - func addCreateReadResourcesMetadataValidation(readValidateFunc, createValidateFunc metadataValidation, resources map[string]*schema.Resource) map[string]*schema.Resource { for name, r := range resources { name := name diff --git a/internal/resources/syntheticmonitoring/data_source_probe.go b/internal/resources/syntheticmonitoring/data_source_probe.go index 43f870b32..709d574c8 100644 --- a/internal/resources/syntheticmonitoring/data_source_probe.go +++ b/internal/resources/syntheticmonitoring/data_source_probe.go @@ -8,14 +8,15 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" sm "github.com/grafana/synthetic-monitoring-agent/pkg/pb/synthetic_monitoring" + smapi "github.com/grafana/synthetic-monitoring-api-go-client" "github.com/grafana/terraform-provider-grafana/internal/common" ) -func DataSourceProbe() *schema.Resource { +func dataSourceProbe() *schema.Resource { return &schema.Resource{ Description: "Data source for retrieving a single probe by name.", - ReadContext: DataSourceProbeRead, - Schema: common.CloneResourceSchemaForDatasource(ResourceProbe(), map[string]*schema.Schema{ + ReadContext: withClient[schema.ReadContextFunc](dataSourceProbeRead), + Schema: common.CloneResourceSchemaForDatasource(resourceProbe(), map[string]*schema.Schema{ "name": { Description: "Name of the probe.", Type: schema.TypeString, @@ -26,8 +27,7 @@ func DataSourceProbe() *schema.Resource { } } -func DataSourceProbeRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - c := meta.(*common.Client).SMAPI +func dataSourceProbeRead(ctx context.Context, d *schema.ResourceData, c *smapi.Client) diag.Diagnostics { var diags diag.Diagnostics prbs, err := c.ListProbes(ctx) if err != nil { diff --git a/internal/resources/syntheticmonitoring/data_source_probes.go b/internal/resources/syntheticmonitoring/data_source_probes.go index 885469c52..afb2f84df 100644 --- a/internal/resources/syntheticmonitoring/data_source_probes.go +++ b/internal/resources/syntheticmonitoring/data_source_probes.go @@ -3,15 +3,15 @@ package syntheticmonitoring import ( "context" - "github.com/grafana/terraform-provider-grafana/internal/common" + smapi "github.com/grafana/synthetic-monitoring-api-go-client" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) -func DataSourceProbes() *schema.Resource { +func dataSourceProbes() *schema.Resource { return &schema.Resource{ Description: "Data source for retrieving all probes.", - ReadContext: DataSourceProbesRead, + ReadContext: withClient[schema.ReadContextFunc](dataSourceProbesRead), Schema: map[string]*schema.Schema{ "filter_deprecated": { Type: schema.TypeBool, @@ -31,8 +31,7 @@ func DataSourceProbes() *schema.Resource { } } -func DataSourceProbesRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - c := meta.(*common.Client).SMAPI +func dataSourceProbesRead(ctx context.Context, d *schema.ResourceData, c *smapi.Client) diag.Diagnostics { var diags diag.Diagnostics prbs, err := c.ListProbes(ctx) if err != nil { diff --git a/internal/resources/syntheticmonitoring/resource_check.go b/internal/resources/syntheticmonitoring/resource_check.go index c3a2b7ec0..85c86c108 100644 --- a/internal/resources/syntheticmonitoring/resource_check.go +++ b/internal/resources/syntheticmonitoring/resource_check.go @@ -13,6 +13,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" sm "github.com/grafana/synthetic-monitoring-agent/pkg/pb/synthetic_monitoring" + smapi "github.com/grafana/synthetic-monitoring-api-go-client" "github.com/grafana/terraform-provider-grafana/internal/common" ) @@ -624,7 +625,7 @@ var ( } ) -func ResourceCheck() *schema.Resource { +func resourceCheck() *schema.Resource { return &schema.Resource{ Description: ` @@ -637,14 +638,14 @@ multiple checks for a single endpoint to check different capabilities. * [Official documentation](https://grafana.com/docs/grafana-cloud/monitor-public-endpoints/checks/) `, - CreateContext: ResourceCheckCreate, - ReadContext: ResourceCheckRead, - UpdateContext: ResourceCheckUpdate, - DeleteContext: ResourceCheckDelete, + CreateContext: withClient[schema.CreateContextFunc](resourceCheckCreate), + ReadContext: withClient[schema.ReadContextFunc](resourceCheckRead), + UpdateContext: withClient[schema.UpdateContextFunc](resourceCheckUpdate), + DeleteContext: withClient[schema.DeleteContextFunc](resourceCheckDelete), Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, - CustomizeDiff: ResourceCheckCustomizeDiff, + CustomizeDiff: resourceCheckCustomizeDiff, Schema: map[string]*schema.Schema{ "id": { @@ -742,8 +743,7 @@ multiple checks for a single endpoint to check different capabilities. } } -func ResourceCheckCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - c := meta.(*common.Client).SMAPI +func resourceCheckCreate(ctx context.Context, d *schema.ResourceData, c *smapi.Client) diag.Diagnostics { chk, err := makeCheck(d) if err != nil { return diag.FromErr(err) @@ -754,11 +754,10 @@ func ResourceCheckCreate(ctx context.Context, d *schema.ResourceData, meta inter } d.SetId(strconv.FormatInt(res.Id, 10)) d.Set("tenant_id", res.TenantId) - return ResourceCheckRead(ctx, d, meta) + return resourceCheckRead(ctx, d, c) } -func ResourceCheckRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - c := meta.(*common.Client).SMAPI +func resourceCheckRead(ctx context.Context, d *schema.ResourceData, c *smapi.Client) diag.Diagnostics { id, err := strconv.ParseInt(d.Id(), 10, 64) if err != nil { return diag.FromErr(err) @@ -1058,8 +1057,7 @@ func ResourceCheckRead(ctx context.Context, d *schema.ResourceData, meta interfa return nil } -func ResourceCheckUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - c := meta.(*common.Client).SMAPI +func resourceCheckUpdate(ctx context.Context, d *schema.ResourceData, c *smapi.Client) diag.Diagnostics { chk, err := makeCheck(d) if err != nil { return diag.FromErr(err) @@ -1068,11 +1066,10 @@ func ResourceCheckUpdate(ctx context.Context, d *schema.ResourceData, meta inter if err != nil { return diag.FromErr(err) } - return ResourceCheckRead(ctx, d, meta) + return resourceCheckRead(ctx, d, c) } -func ResourceCheckDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - c := meta.(*common.Client).SMAPI +func resourceCheckDelete(ctx context.Context, d *schema.ResourceData, c *smapi.Client) diag.Diagnostics { var diags diag.Diagnostics id, _ := strconv.ParseInt(d.Id(), 10, 64) err := c.DeleteCheck(ctx, id) @@ -1477,7 +1474,7 @@ func makeCheckSettings(settings map[string]interface{}) (sm.CheckSettings, error // Ideally, we'd use `ExactlyOneOf` here but it doesn't support TypeSet. // Also, TypeSet doesn't support ValidateFunc. // To maintain backwards compatibility, we do a custom validation in the CustomizeDiff function. -func ResourceCheckCustomizeDiff(ctx context.Context, diff *schema.ResourceDiff, meta interface{}) error { +func resourceCheckCustomizeDiff(ctx context.Context, diff *schema.ResourceDiff, meta interface{}) error { settingsList := diff.Get("settings").(*schema.Set).List() if len(settingsList) == 0 { return fmt.Errorf("at least one check setting must be defined") diff --git a/internal/resources/syntheticmonitoring/resource_probe.go b/internal/resources/syntheticmonitoring/resource_probe.go index e2f50e3b9..bc585fffb 100644 --- a/internal/resources/syntheticmonitoring/resource_probe.go +++ b/internal/resources/syntheticmonitoring/resource_probe.go @@ -13,10 +13,11 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" sm "github.com/grafana/synthetic-monitoring-agent/pkg/pb/synthetic_monitoring" + smapi "github.com/grafana/synthetic-monitoring-api-go-client" "github.com/grafana/terraform-provider-grafana/internal/common" ) -func ResourceProbe() *schema.Resource { +func resourceProbe() *schema.Resource { return &schema.Resource{ Description: ` @@ -28,10 +29,10 @@ Grafana Synthetic Monitoring Agent. * [Official documentation](https://grafana.com/docs/grafana-cloud/monitor-public-endpoints/private-probes/) `, - CreateContext: ResourceProbeCreate, - ReadContext: ResourceProbeRead, - UpdateContext: ResourceProbeUpdate, - DeleteContext: ResourceProbeDelete, + CreateContext: withClient[schema.CreateContextFunc](resourceProbeCreate), + ReadContext: withClient[schema.ReadContextFunc](resourceProbeRead), + UpdateContext: withClient[schema.UpdateContextFunc](resourceProbeUpdate), + DeleteContext: withClient[schema.DeleteContextFunc](resourceProbeDelete), Importer: &schema.ResourceImporter{ StateContext: ImportProbeStateWithToken, }, @@ -103,8 +104,7 @@ Grafana Synthetic Monitoring Agent. } } -func ResourceProbeCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - c := meta.(*common.Client).SMAPI +func resourceProbeCreate(ctx context.Context, d *schema.ResourceData, c *smapi.Client) diag.Diagnostics { p := makeProbe(d) res, token, err := c.AddProbe(ctx, *p) if err != nil { @@ -113,11 +113,10 @@ func ResourceProbeCreate(ctx context.Context, d *schema.ResourceData, meta inter d.SetId(strconv.FormatInt(res.Id, 10)) d.Set("tenant_id", res.TenantId) d.Set("auth_token", base64.StdEncoding.EncodeToString(token)) - return ResourceProbeRead(ctx, d, meta) + return resourceProbeRead(ctx, d, c) } -func ResourceProbeRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - c := meta.(*common.Client).SMAPI +func resourceProbeRead(ctx context.Context, d *schema.ResourceData, c *smapi.Client) diag.Diagnostics { id, err := strconv.ParseInt(d.Id(), 10, 64) if err != nil { return diag.FromErr(err) @@ -151,18 +150,16 @@ func ResourceProbeRead(ctx context.Context, d *schema.ResourceData, meta interfa return nil } -func ResourceProbeUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - c := meta.(*common.Client).SMAPI +func resourceProbeUpdate(ctx context.Context, d *schema.ResourceData, c *smapi.Client) diag.Diagnostics { p := makeProbe(d) _, err := c.UpdateProbe(ctx, *p) if err != nil { return diag.FromErr(err) } - return ResourceProbeRead(ctx, d, meta) + return resourceProbeRead(ctx, d, c) } -func ResourceProbeDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - c := meta.(*common.Client).SMAPI +func resourceProbeDelete(ctx context.Context, d *schema.ResourceData, c *smapi.Client) diag.Diagnostics { id, _ := strconv.ParseInt(d.Id(), 10, 64) // Remove the probe from any checks that use it. diff --git a/internal/resources/syntheticmonitoring/resource_probe_helper_test.go b/internal/resources/syntheticmonitoring/resource_probe_helper_test.go deleted file mode 100644 index 8a2fe5a38..000000000 --- a/internal/resources/syntheticmonitoring/resource_probe_helper_test.go +++ /dev/null @@ -1,76 +0,0 @@ -package syntheticmonitoring_test - -import ( - "context" - "testing" - - "github.com/grafana/terraform-provider-grafana/internal/resources/syntheticmonitoring" - "github.com/grafana/terraform-provider-grafana/internal/testutils" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" -) - -func TestImportProbeStateWithToken(t *testing.T) { - testutils.CheckCloudInstanceTestsEnabled(t) - - testcases := map[string]struct { - input string - expectError bool - expectedID string - expectedAuthToken string - }{ - "valid id, no auth_token": { - input: "1", - expectError: false, - expectedID: "1", - expectedAuthToken: "", - }, - "valid id, valid auth_token": { - input: "1:aGVsbG8=", - expectError: false, - expectedID: "1", - expectedAuthToken: "aGVsbG8=", - }, - "valid id, invalid auth_token": { - input: "1:xxx", - expectError: true, - }, - "invalid id, valid auth_token": { - input: ":aGVsbG8=", - expectError: true, - }, - } - - for name, tc := range testcases { - t.Run(name, func(t *testing.T) { - d := schema.TestResourceDataRaw(t, syntheticmonitoring.ResourceProbe().Schema, nil) - d.SetId(tc.input) - - res, err := syntheticmonitoring.ImportProbeStateWithToken(context.Background(), d, nil) - switch { - case tc.expectError && err == nil: - t.Fatalf("calling ImportProbeStateWithToken with id %q, expecting error, got nil", tc.input) - - case !tc.expectError && err != nil: - t.Fatalf("calling ImportProbeStateWithToken with id %q, expecting no error, got %s", tc.input, err) - - case !tc.expectError: - if len(res) != 1 { - t.Fatalf("expecting 1 ResourceData, got %d", len(res)) - } - - if tc.expectedID != res[0].Id() { - t.Fatalf("expecting id %q, got %q", tc.expectedID, res[0].Id()) - } - - if tc.expectedAuthToken != "" { - output, ok := res[0].GetOk("auth_token") - if !ok { - t.Fatalf("expecting auth_token to be set") - } else if str, ok := output.(string); !ok || str != tc.expectedAuthToken { - t.Fatalf("expecting auth_token to match string %q, got %#v", tc.expectedAuthToken, output) - } - } - } - }) - } -} diff --git a/internal/resources/syntheticmonitoring/resource_probe_test.go b/internal/resources/syntheticmonitoring/resource_probe_test.go index 28464ff63..e0c7f7dda 100644 --- a/internal/resources/syntheticmonitoring/resource_probe_test.go +++ b/internal/resources/syntheticmonitoring/resource_probe_test.go @@ -152,6 +152,72 @@ func TestAccResourceProbe_recreateProbeUsedInCheck(t *testing.T) { }) } +func TestAccResourceProbe_Import(t *testing.T) { + testutils.CheckCloudInstanceTestsEnabled(t) + + randomName := acctest.RandomWithPrefix("My Probe") + + resource.ParallelTest(t, resource.TestCase{ + ProviderFactories: testutils.ProviderFactories, + Steps: []resource.TestStep{ + { + Config: testutils.TestAccExampleWithReplace(t, "resources/grafana_synthetic_monitoring_probe/resource.tf", map[string]string{ + "Mount Everest": randomName, + }), + }, + // Test import with invalid token + { + ResourceName: "grafana_synthetic_monitoring_probe.main", + ImportState: true, + ImportStateIdFunc: func(s *terraform.State) (string, error) { + id := s.RootModule().Resources["grafana_synthetic_monitoring_probe.main"].Primary.ID + return fmt.Sprintf("%s:xxx", id), nil + }, + ExpectError: regexp.MustCompile(`invalid auth_token "xxx", expecting a base64-encoded string`), + }, + // Test import with invalid id + { + ResourceName: "grafana_synthetic_monitoring_probe.main", + ImportState: true, + ImportStateId: ":aGVsbG8=", + ExpectError: regexp.MustCompile(`invalid id ":aGVsbG8=", expected format 'probe_id:auth_token'`), + }, + // Test import without token + { + ResourceName: "grafana_synthetic_monitoring_probe.main", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"auth_token"}, + ImportStateIdFunc: func(s *terraform.State) (string, error) { + return s.RootModule().Resources["grafana_synthetic_monitoring_probe.main"].Primary.ID, nil + }, + ImportStateCheck: func(is []*terraform.InstanceState) error { + if is[0].Attributes["auth_token"] != "" { + return fmt.Errorf("expected auth_token to be empty, got %s", is[0].Attributes["auth_token"]) + } + return nil + }, + }, + // Test import with token + { + ResourceName: "grafana_synthetic_monitoring_probe.main", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"auth_token"}, + ImportStateIdFunc: func(s *terraform.State) (string, error) { + return fmt.Sprintf("%s:aGVsbG8=", s.RootModule().Resources["grafana_synthetic_monitoring_probe.main"].Primary.ID), nil + }, + ImportStateCheck: func(is []*terraform.InstanceState) error { + if is[0].Attributes["auth_token"] != "aGVsbG8=" { + return fmt.Errorf("expected auth_token to be 'aGVsbG8=', got %s", is[0].Attributes["auth_token"]) + } + return nil + }, + }, + }, + }) +} + func TestAccResourceProbe_InvalidLabels(t *testing.T) { testutils.CheckCloudInstanceTestsEnabled(t) diff --git a/internal/resources/syntheticmonitoring/resources.go b/internal/resources/syntheticmonitoring/resources.go new file mode 100644 index 000000000..8fa3416cd --- /dev/null +++ b/internal/resources/syntheticmonitoring/resources.go @@ -0,0 +1,32 @@ +package syntheticmonitoring + +import ( + "context" + + smapi "github.com/grafana/synthetic-monitoring-api-go-client" + "github.com/grafana/terraform-provider-grafana/internal/common" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +type crudWithClientFunc func(ctx context.Context, d *schema.ResourceData, client *smapi.Client) diag.Diagnostics + +func withClient[T schema.CreateContextFunc | schema.UpdateContextFunc | schema.ReadContextFunc | schema.DeleteContextFunc](f crudWithClientFunc) T { + return func(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*common.Client).SMAPI + if client == nil { + return diag.Errorf("the SM client is required for this resource. Set the sm_access_token provider attribute") + } + return f(ctx, d, client) + } +} + +var DatasourcesMap = map[string]*schema.Resource{ + "grafana_synthetic_monitoring_probe": dataSourceProbe(), + "grafana_synthetic_monitoring_probes": dataSourceProbes(), +} + +var ResourcesMap = map[string]*schema.Resource{ + "grafana_synthetic_monitoring_check": resourceCheck(), + "grafana_synthetic_monitoring_probe": resourceProbe(), +}