Skip to content

Commit 216997a

Browse files
Validate that clients exist in all resources (#615)
* Validate that clients exist in all resources This is a long standing issue, since we started using the provider for multiple things (SM, Grafana, Cloud, Oncall) You can now configure your provider in a way that is valid but not for the resources you are using Example, configuring a cloud provider but using a Grafana resource The OnCall team added a validation on all of their resources, which works but is a bit tedious This PR adds a validation on every resource, the corresponding client has to exist at read and create time Here's what an error looks like: ``` │ Error: the Grafana client is required for `grafana_data_source`. Set the auth and url provider attributes │ │ with grafana_data_source.elasticsearch-arbitrary, │ on test-2022-08-24-datasources.tf line 14, in resource "grafana_data_source" "elasticsearch-arbitrary": │ 14: resource "grafana_data_source" "elasticsearch-arbitrary" { ``` Instead of `Error: Post "/api/v1/provisioning/contact-points": unsupported protocol scheme ""` Fixes #610 * PR comments! * Move SM installation resource * SM installation requires no clients at all * Set smURL outside `if` * arg, linting
1 parent d275546 commit 216997a

10 files changed

+220
-182
lines changed

grafana/provider.go

Lines changed: 141 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,101 @@ func init() {
4444
}
4545

4646
func Provider(version string) func() *schema.Provider {
47+
var (
48+
// Resources that require the Grafana client to exist.
49+
grafanaClientResources = addResourcesMetadataValidation(grafanaClientPresent, map[string]*schema.Resource{
50+
// Grafana
51+
"grafana_annotation": ResourceAnnotation(),
52+
"grafana_alert_notification": ResourceAlertNotification(),
53+
"grafana_builtin_role_assignment": ResourceBuiltInRoleAssignment(),
54+
"grafana_contact_point": ResourceContactPoint(),
55+
"grafana_dashboard": ResourceDashboard(),
56+
"grafana_dashboard_permission": ResourceDashboardPermission(),
57+
"grafana_data_source": ResourceDataSource(),
58+
"grafana_data_source_permission": ResourceDatasourcePermission(),
59+
"grafana_folder": ResourceFolder(),
60+
"grafana_folder_permission": ResourceFolderPermission(),
61+
"grafana_library_panel": ResourceLibraryPanel(),
62+
"grafana_message_template": ResourceMessageTemplate(),
63+
"grafana_mute_timing": ResourceMuteTiming(),
64+
"grafana_notification_policy": ResourceNotificationPolicy(),
65+
"grafana_organization": ResourceOrganization(),
66+
"grafana_playlist": ResourcePlaylist(),
67+
"grafana_report": ResourceReport(),
68+
"grafana_role": ResourceRole(),
69+
"grafana_rule_group": ResourceRuleGroup(),
70+
"grafana_team": ResourceTeam(),
71+
"grafana_team_preferences": ResourceTeamPreferences(),
72+
"grafana_team_external_group": ResourceTeamExternalGroup(),
73+
"grafana_service_account_token": ResourceServiceAccountToken(),
74+
"grafana_service_account": ResourceServiceAccount(),
75+
"grafana_user": ResourceUser(),
76+
77+
// Machine Learning
78+
"grafana_machine_learning_job": ResourceMachineLearningJob(),
79+
})
80+
81+
// Resources that require the Synthetic Monitoring client to exist.
82+
smClientResources = addResourcesMetadataValidation(smClientPresent, map[string]*schema.Resource{
83+
"grafana_synthetic_monitoring_check": ResourceSyntheticMonitoringCheck(),
84+
"grafana_synthetic_monitoring_probe": ResourceSyntheticMonitoringProbe(),
85+
})
86+
87+
// Resources that require the Cloud client to exist.
88+
cloudClientResources = addResourcesMetadataValidation(cloudClientPresent, map[string]*schema.Resource{
89+
"grafana_cloud_api_key": ResourceCloudAPIKey(),
90+
"grafana_cloud_plugin_installation": ResourceCloudPluginInstallation(),
91+
"grafana_cloud_stack": ResourceCloudStack(),
92+
})
93+
94+
// Resources that require the OnCall client to exist.
95+
onCallClientResources = addResourcesMetadataValidation(onCallClientPresent, map[string]*schema.Resource{
96+
"grafana_oncall_integration": ResourceOnCallIntegration(),
97+
"grafana_oncall_route": ResourceOnCallRoute(),
98+
"grafana_oncall_escalation_chain": ResourceOnCallEscalationChain(),
99+
"grafana_oncall_escalation": ResourceOnCallEscalation(),
100+
"grafana_oncall_on_call_shift": ResourceOnCallOnCallShift(),
101+
"grafana_oncall_schedule": ResourceOnCallSchedule(),
102+
"grafana_oncall_outgoing_webhook": ResourceOnCallOutgoingWebhook(),
103+
})
104+
105+
// Datasources that require the Grafana client to exist.
106+
grafanaClientDatasources = addResourcesMetadataValidation(grafanaClientPresent, map[string]*schema.Resource{
107+
"grafana_dashboard": DatasourceDashboard(),
108+
"grafana_dashboards": DatasourceDashboards(),
109+
"grafana_folder": DatasourceFolder(),
110+
"grafana_folders": DatasourceFolders(),
111+
"grafana_library_panel": DatasourceLibraryPanel(),
112+
"grafana_user": DatasourceUser(),
113+
"grafana_team": DatasourceTeam(),
114+
"grafana_organization": DatasourceOrganization(),
115+
})
116+
117+
// Datasources that require the Synthetic Monitoring client to exist.
118+
smClientDatasources = addResourcesMetadataValidation(smClientPresent, map[string]*schema.Resource{
119+
"grafana_synthetic_monitoring_probe": DatasourceSyntheticMonitoringProbe(),
120+
"grafana_synthetic_monitoring_probes": DatasourceSyntheticMonitoringProbes(),
121+
})
122+
123+
// Datasources that require the Cloud client to exist.
124+
cloudClientDatasources = addResourcesMetadataValidation(cloudClientPresent, map[string]*schema.Resource{
125+
"grafana_cloud_ips": DatasourceCloudIPs(),
126+
"grafana_cloud_stack": DatasourceCloudStack(),
127+
})
128+
129+
// Datasources that require the OnCall client to exist.
130+
onCallClientDatasources = addResourcesMetadataValidation(onCallClientPresent, map[string]*schema.Resource{
131+
"grafana_oncall_user": DataSourceOnCallUser(),
132+
"grafana_oncall_escalation_chain": DataSourceOnCallEscalationChain(),
133+
"grafana_oncall_schedule": DataSourceOnCallSchedule(),
134+
"grafana_oncall_slack_channel": DataSourceOnCallSlackChannel(),
135+
"grafana_oncall_action": DataSourceOnCallAction(), // deprecated
136+
"grafana_oncall_outgoing_webhook": DataSourceOnCallOutgoingWebhook(),
137+
"grafana_oncall_user_group": DataSourceOnCallUserGroup(),
138+
"grafana_oncall_team": DataSourceOnCallTeam(),
139+
})
140+
)
141+
47142
return func() *schema.Provider {
48143
p := &schema.Provider{
49144
Schema: map[string]*schema.Schema{
@@ -159,87 +254,25 @@ func Provider(version string) func() *schema.Provider {
159254
},
160255
},
161256

162-
ResourcesMap: map[string]*schema.Resource{
163-
// Grafana
164-
"grafana_annotation": ResourceAnnotation(),
165-
"grafana_api_key": ResourceAPIKey(),
166-
"grafana_alert_notification": ResourceAlertNotification(),
167-
"grafana_builtin_role_assignment": ResourceBuiltInRoleAssignment(),
168-
"grafana_contact_point": ResourceContactPoint(),
169-
"grafana_dashboard": ResourceDashboard(),
170-
"grafana_dashboard_permission": ResourceDashboardPermission(),
171-
"grafana_data_source": ResourceDataSource(),
172-
"grafana_data_source_permission": ResourceDatasourcePermission(),
173-
"grafana_folder": ResourceFolder(),
174-
"grafana_folder_permission": ResourceFolderPermission(),
175-
"grafana_library_panel": ResourceLibraryPanel(),
176-
"grafana_message_template": ResourceMessageTemplate(),
177-
"grafana_mute_timing": ResourceMuteTiming(),
178-
"grafana_notification_policy": ResourceNotificationPolicy(),
179-
"grafana_organization": ResourceOrganization(),
180-
"grafana_playlist": ResourcePlaylist(),
181-
"grafana_report": ResourceReport(),
182-
"grafana_role": ResourceRole(),
183-
"grafana_rule_group": ResourceRuleGroup(),
184-
"grafana_team": ResourceTeam(),
185-
"grafana_team_preferences": ResourceTeamPreferences(),
186-
"grafana_team_external_group": ResourceTeamExternalGroup(),
187-
"grafana_service_account_token": ResourceServiceAccountToken(),
188-
"grafana_service_account": ResourceServiceAccount(),
189-
"grafana_user": ResourceUser(),
190-
191-
// Cloud
192-
"grafana_cloud_api_key": ResourceCloudAPIKey(),
193-
"grafana_cloud_plugin_installation": ResourceCloudPluginInstallation(),
194-
"grafana_cloud_stack": ResourceCloudStack(),
195-
196-
// Synthetic Monitoring
197-
"grafana_synthetic_monitoring_check": ResourceSyntheticMonitoringCheck(),
198-
"grafana_synthetic_monitoring_probe": ResourceSyntheticMonitoringProbe(),
199-
"grafana_synthetic_monitoring_installation": ResourceSyntheticMonitoringInstallation(),
200-
201-
// Machine Learning
202-
"grafana_machine_learning_job": ResourceMachineLearningJob(),
203-
204-
// OnCall
205-
"grafana_oncall_integration": ResourceOnCallIntegration(),
206-
"grafana_oncall_route": ResourceOnCallRoute(),
207-
"grafana_oncall_escalation_chain": ResourceOnCallEscalationChain(),
208-
"grafana_oncall_escalation": ResourceOnCallEscalation(),
209-
"grafana_oncall_on_call_shift": ResourceOnCallOnCallShift(),
210-
"grafana_oncall_schedule": ResourceOnCallSchedule(),
211-
"grafana_oncall_outgoing_webhook": ResourceOnCallOutgoingWebhook(),
212-
},
213-
214-
DataSourcesMap: map[string]*schema.Resource{
215-
// Grafana
216-
"grafana_dashboard": DatasourceDashboard(),
217-
"grafana_dashboards": DatasourceDashboards(),
218-
"grafana_folder": DatasourceFolder(),
219-
"grafana_folders": DatasourceFolders(),
220-
"grafana_library_panel": DatasourceLibraryPanel(),
221-
"grafana_user": DatasourceUser(),
222-
"grafana_team": DatasourceTeam(),
223-
"grafana_organization": DatasourceOrganization(),
224-
225-
// Cloud
226-
"grafana_cloud_ips": DatasourceCloudIPs(),
227-
"grafana_cloud_stack": DatasourceCloudStack(),
228-
229-
// Synthetic Monitoring
230-
"grafana_synthetic_monitoring_probe": DatasourceSyntheticMonitoringProbe(),
231-
"grafana_synthetic_monitoring_probes": DatasourceSyntheticMonitoringProbes(),
232-
233-
// OnCall
234-
"grafana_oncall_user": DataSourceOnCallUser(),
235-
"grafana_oncall_escalation_chain": DataSourceOnCallEscalationChain(),
236-
"grafana_oncall_schedule": DataSourceOnCallSchedule(),
237-
"grafana_oncall_slack_channel": DataSourceOnCallSlackChannel(),
238-
"grafana_oncall_action": DataSourceOnCallAction(), // deprecated
239-
"grafana_oncall_outgoing_webhook": DataSourceOnCallOutgoingWebhook(),
240-
"grafana_oncall_user_group": DataSourceOnCallUserGroup(),
241-
"grafana_oncall_team": DataSourceOnCallTeam(),
242-
},
257+
ResourcesMap: mergeResourceMaps(
258+
map[string]*schema.Resource{
259+
// Special case, this resource supports both Grafana and Cloud (depending on context)
260+
"grafana_api_key": ResourceAPIKey(),
261+
// This one installs SM on a cloud instance, everything it needs is in its attributes
262+
"grafana_synthetic_monitoring_installation": ResourceSyntheticMonitoringInstallation(),
263+
},
264+
grafanaClientResources,
265+
smClientResources,
266+
onCallClientResources,
267+
cloudClientResources,
268+
),
269+
270+
DataSourcesMap: mergeResourceMaps(
271+
grafanaClientDatasources,
272+
smClientDatasources,
273+
onCallClientDatasources,
274+
cloudClientDatasources,
275+
),
243276
}
244277

245278
p.ConfigureContextFunc = configure(version, p)
@@ -274,19 +307,26 @@ func configure(version string, p *schema.Provider) func(context.Context, *schema
274307

275308
c := &client{}
276309

277-
c.gapiURL, c.gapiConfig, c.gapi, err = createGrafanaClient(d)
278-
if err != nil {
279-
return nil, diag.FromErr(err)
310+
if d.Get("auth").(string) != "" && d.Get("url").(string) != "" {
311+
c.gapiURL, c.gapiConfig, c.gapi, err = createGrafanaClient(d)
312+
if err != nil {
313+
return nil, diag.FromErr(err)
314+
}
315+
c.mlapi, err = createMLClient(c.gapiURL, c.gapiConfig)
316+
if err != nil {
317+
return nil, diag.FromErr(err)
318+
}
280319
}
281-
c.gcloudapi, err = createCloudClient(d)
282-
if err != nil {
283-
return nil, diag.FromErr(err)
320+
if d.Get("cloud_api_key").(string) != "" {
321+
c.gcloudapi, err = createCloudClient(d)
322+
if err != nil {
323+
return nil, diag.FromErr(err)
324+
}
284325
}
285-
c.mlapi, err = createMLClient(c.gapiURL, c.gapiConfig)
286-
if err != nil {
287-
return nil, diag.FromErr(err)
326+
c.smURL = d.Get("sm_url").(string)
327+
if smToken := d.Get("sm_access_token").(string); smToken != "" {
328+
c.smapi = smapi.NewClient(c.smURL, smToken, nil)
288329
}
289-
c.smURL, c.smapi = createSMClient(d)
290330
if d.Get("oncall_access_token").(string) != "" {
291331
c.onCallAPI, err = createOnCallClient(d)
292332
if err != nil {
@@ -397,12 +437,6 @@ func createCloudClient(d *schema.ResourceData) (*gapi.Client, error) {
397437
return gapi.New(d.Get("cloud_api_url").(string), cfg)
398438
}
399439

400-
func createSMClient(d *schema.ResourceData) (string, *smapi.Client) {
401-
smToken := d.Get("sm_access_token").(string)
402-
smURL := d.Get("sm_url").(string)
403-
return smURL, smapi.NewClient(smURL, smToken, nil)
404-
}
405-
406440
func createOnCallClient(d *schema.ResourceData) (*onCallAPI.Client, error) {
407441
aToken := d.Get("oncall_access_token").(string)
408442
baseURL := d.Get("oncall_url").(string)
@@ -421,3 +455,13 @@ func getJSONMap(k string) (map[string]interface{}, error) {
421455
}
422456
return nil, nil
423457
}
458+
459+
func mergeResourceMaps(maps ...map[string]*schema.Resource) map[string]*schema.Resource {
460+
result := make(map[string]*schema.Resource)
461+
for _, m := range maps {
462+
for k, v := range m {
463+
result[k] = v
464+
}
465+
}
466+
return result
467+
}

grafana/provider_validation.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package grafana
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"log"
7+
8+
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
9+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
10+
)
11+
12+
// This file contains validations functions that can be added to a map of resources.
13+
// These validations are added to the Create and Read functions of all resources,
14+
// because they are entrypoints (code that will be run in all cases).
15+
16+
type metadataValidation func(resourceName string, m interface{}) error
17+
18+
func grafanaClientPresent(resourceName string, m interface{}) error {
19+
if m.(*client).gapi == nil {
20+
return fmt.Errorf("the Grafana client is required for `%s`. Set the auth and url provider attributes", resourceName)
21+
}
22+
return nil
23+
}
24+
25+
func smClientPresent(resourceName string, m interface{}) error {
26+
if m.(*client).smapi == nil {
27+
return fmt.Errorf("the Synthetic Monitoring client is required for `%s`. Set the sm_access_token provider attribute", resourceName)
28+
}
29+
return nil
30+
}
31+
32+
func cloudClientPresent(resourceName string, m interface{}) error {
33+
if m.(*client).gcloudapi == nil {
34+
return fmt.Errorf("the Cloud API client is required for `%s`. Set the cloud_api_key provider attribute", resourceName)
35+
}
36+
return nil
37+
}
38+
39+
func onCallClientPresent(resourceName string, m interface{}) error {
40+
if m.(*client).onCallAPI == nil {
41+
return fmt.Errorf("the Oncall client is required for `%s`. Set the oncall_access_token provider attribute", resourceName)
42+
}
43+
return nil
44+
}
45+
46+
func addResourcesMetadataValidation(validateFunc metadataValidation, resources map[string]*schema.Resource) map[string]*schema.Resource {
47+
for name, r := range resources {
48+
name := name
49+
//nolint:staticcheck
50+
if r.Read != nil {
51+
log.Fatalf("%s: Read function is not supported", name)
52+
}
53+
if r.ReadContext != nil {
54+
prev := r.ReadContext
55+
r.ReadContext = func(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
56+
if err := validateFunc(name, m); err != nil {
57+
return diag.FromErr(err)
58+
}
59+
return prev(ctx, d, m)
60+
}
61+
}
62+
//nolint:staticcheck
63+
if r.Create != nil {
64+
log.Fatalf("%s: Create function is not supported", name)
65+
}
66+
if r.CreateContext != nil {
67+
prev := r.CreateContext
68+
r.CreateContext = func(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
69+
if err := validateFunc(name, m); err != nil {
70+
return diag.FromErr(err)
71+
}
72+
return prev(ctx, d, m)
73+
}
74+
}
75+
resources[name] = r
76+
}
77+
return resources
78+
}

grafana/resource_oncall_escalation.go

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -200,9 +200,6 @@ func ResourceOnCallEscalation() *schema.Resource {
200200

201201
func resourceEscalationCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
202202
client := m.(*client).onCallAPI
203-
if client == nil {
204-
return diag.Errorf("grafana OnCall api client is not configured")
205-
}
206203

207204
escalationChainIDData := d.Get("escalation_chain_id").(string)
208205

@@ -309,9 +306,6 @@ func resourceEscalationCreate(ctx context.Context, d *schema.ResourceData, m int
309306

310307
func resourceEscalationRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
311308
client := m.(*client).onCallAPI
312-
if client == nil {
313-
return diag.Errorf("grafana OnCall api client is not configured")
314-
}
315309

316310
escalation, r, err := client.Escalations.GetEscalation(d.Id(), &onCallAPI.GetEscalationOptions{})
317311
if err != nil {
@@ -341,9 +335,6 @@ func resourceEscalationRead(ctx context.Context, d *schema.ResourceData, m inter
341335

342336
func resourceEscalationUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
343337
client := m.(*client).onCallAPI
344-
if client == nil {
345-
return diag.Errorf("grafana OnCall api client is not configured")
346-
}
347338

348339
updateOptions := &onCallAPI.UpdateEscalationOptions{
349340
ManualOrder: true,
@@ -430,9 +421,6 @@ func resourceEscalationUpdate(ctx context.Context, d *schema.ResourceData, m int
430421

431422
func resourceEscalationDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
432423
client := m.(*client).onCallAPI
433-
if client == nil {
434-
return diag.Errorf("grafana OnCall api client is not configured")
435-
}
436424

437425
_, err := client.Escalations.DeleteEscalation(d.Id(), &onCallAPI.DeleteEscalationOptions{})
438426
if err != nil {

0 commit comments

Comments
 (0)