diff --git a/.drone/drone.jsonnet b/.drone/drone.jsonnet index 03cb065eb..aaeb28b67 100644 --- a/.drone/drone.jsonnet +++ b/.drone/drone.jsonnet @@ -70,6 +70,19 @@ local pipeline(name, steps, services=[]) = { ] ), + pipeline( + 'unit tests', + steps=[ + { + name: 'tests', + image: images.go, + commands: [ + 'go test ./...', + ], + }, + ] + ), + pipeline( 'cloud tests', steps=[ diff --git a/.drone/drone.yml b/.drone/drone.yml index 152729b88..6fbf67c9c 100644 --- a/.drone/drone.yml +++ b/.drone/drone.yml @@ -49,6 +49,27 @@ workspace: path: /drone/terraform-provider-grafana --- kind: pipeline +name: unit tests +platform: + arch: amd64 + os: linux +services: [] +steps: +- commands: + - go test ./... + image: golang:1.16 + name: tests +trigger: + branch: + - master + event: + - pull_request + - push +type: docker +workspace: + path: /drone/terraform-provider-grafana +--- +kind: pipeline name: cloud tests platform: arch: amd64 @@ -252,6 +273,6 @@ workspace: path: /drone/terraform-provider-grafana --- kind: signature -hmac: 21722dfbb237f702dd2e062347c7adfeb8a15a7cf103670928142a975d743d33 +hmac: da6ac4bfbcd28b5ba91a57b10128a0bae335bf46b1bc0ca08ea59326eae3ed16 ... diff --git a/grafana/provider.go b/grafana/provider.go index 3d7db73d4..2d7325609 100644 --- a/grafana/provider.go +++ b/grafana/provider.go @@ -66,7 +66,7 @@ func Provider(version string) func() *schema.Provider { Type: schema.TypeMap, Optional: true, Sensitive: true, - DefaultFunc: JSONEnvDefaultFunc("GRAFANA_HTTP_HEADERS", nil), + Elem: &schema.Schema{Type: schema.TypeString}, Description: "Optional. HTTP headers mapping keys to values used for accessing the Grafana API. May alternatively be set via the `GRAFANA_HTTP_HEADERS` environment variable in JSON format.", }, "retries": { @@ -195,17 +195,17 @@ func Provider(version string) func() *schema.Provider { } type client struct { - gapiURL string - gapi *gapi.Client - gcloudapi *gapi.Client - smapi *smapi.Client - mlapi *mlapi.Client + gapiURL string + gapi *gapi.Client + gapiConfig *gapi.Config + gcloudapi *gapi.Client + smapi *smapi.Client + mlapi *mlapi.Client } func configure(version string, p *schema.Provider) func(context.Context, *schema.ResourceData) (interface{}, diag.Diagnostics) { return func(ctx context.Context, d *schema.ResourceData) (interface{}, diag.Diagnostics) { var ( - cfg *gapi.Config diags diag.Diagnostics err error ) @@ -213,7 +213,7 @@ func configure(version string, p *schema.Provider) func(context.Context, *schema c := &client{} - c.gapiURL, cfg, c.gapi, err = createGrafanaClient(d) + c.gapiURL, c.gapiConfig, c.gapi, err = createGrafanaClient(d) if err != nil { return nil, diag.FromErr(err) } @@ -221,7 +221,7 @@ func configure(version string, p *schema.Provider) func(context.Context, *schema if err != nil { return nil, diag.FromErr(err) } - c.mlapi, err = createMLClient(c.gapiURL, cfg) + c.mlapi, err = createMLClient(c.gapiURL, c.gapiConfig) if err != nil { return nil, diag.FromErr(err) } @@ -279,17 +279,14 @@ func createGrafanaClient(d *schema.ResourceData) (string, *gapi.Config, *gapi.Cl headersMap := d.Get("http_headers").(map[string]interface{}) if headersMap != nil && len(headersMap) == 0 { - // Workaround for a bug when DefaultFunc returns a schema.TypeMap - headersMapAbs, err := JSONEnvDefaultFunc("GRAFANA_HTTP_HEADERS", nil)() + // We cannot use a DefaultFunc because they do not work on maps + var err error + headersMap, err = getJSONMap("GRAFANA_HTTP_HEADERS") if err != nil { - return "", nil, nil, err - } - if headersMapAbs != nil { - headersMap = headersMapAbs.(map[string]interface{}) + return "", nil, nil, fmt.Errorf("invalid http_headers config: %w", err) } } - if headersMap != nil { - // Convert headers from map[string]interface{} to map[string]string + if len(headersMap) > 0 { headers := make(map[string]string) for k, v := range headersMap { if v, ok := v.(string); ok { @@ -338,19 +335,15 @@ func createSMClient(d *schema.ResourceData) *smapi.Client { return smapi.NewClient(smURL, smToken, nil) } -// JSONEnvDefaultFunc is a helper function that parses the given environment -// variable as a JSON object, or returns the default value otherwise. -func JSONEnvDefaultFunc(k string, dv interface{}) schema.SchemaDefaultFunc { - return func() (interface{}, error) { - if valStr := os.Getenv(k); valStr != "" { - var valObj map[string]interface{} - err := json.Unmarshal([]byte(valStr), &valObj) - if err != nil { - return nil, err - } - return valObj, nil +// getJSONMap is a helper function that parses the given environment variable as a JSON object +func getJSONMap(k string) (map[string]interface{}, error) { + if valStr := os.Getenv(k); valStr != "" { + var valObj map[string]interface{} + err := json.Unmarshal([]byte(valStr), &valObj) + if err != nil { + return nil, err } - - return dv, nil + return valObj, nil } + return nil, nil } diff --git a/grafana/provider_test.go b/grafana/provider_test.go index 8f0bac902..d8bb5b562 100644 --- a/grafana/provider_test.go +++ b/grafana/provider_test.go @@ -4,11 +4,14 @@ import ( "context" "io/ioutil" "os" + "regexp" "strconv" + "strings" "sync" "testing" "github.com/Masterminds/semver/v3" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" ) @@ -46,11 +49,142 @@ func init() { } func TestProvider(t *testing.T) { + IsUnitTest(t) + if err := Provider("dev")().InternalValidate(); err != nil { t.Fatalf("err: %s", err) } } +func TestProviderConfigure(t *testing.T) { + IsUnitTest(t) + + // Helper for header tests + checkHeaders := func(t *testing.T, provider *schema.Provider) { + gotHeaders := provider.Meta().(*client).gapiConfig.HTTPHeaders + if len(gotHeaders) != 2 { + t.Errorf("expected 2 HTTP header, got %d", len(gotHeaders)) + } + if gotHeaders["Authorization"] != "Bearer test" { + t.Errorf("expected HTTP header Authorization to be \"Bearer test\", got %q", gotHeaders["Authorization"]) + } + if gotHeaders["X-Custom-Header"] != "custom-value" { + t.Errorf("expected HTTP header X-Custom-Header to be \"custom-value\", got %q", gotHeaders["X-Custom-Header"]) + } + } + + envBackup := os.Environ() + defer func() { + os.Clearenv() + for _, v := range envBackup { + kv := strings.SplitN(v, "=", 2) + os.Setenv(kv[0], kv[1]) + } + }() + + cases := []struct { + name string + config map[string]interface{} + env map[string]string + expectedErr string + check func(t *testing.T, provider *schema.Provider) + }{ + { + name: "no config", + env: map[string]string{}, + expectedErr: "\"auth\": one of `auth,cloud_api_key,sm_access_token` must be specified", + }, + { + name: "grafana config from env", + env: map[string]string{ + "GRAFANA_AUTH": "admin:admin", + "GRAFANA_URL": "https://test.com", + }, + }, + { + name: "header config", + env: map[string]string{ + "GRAFANA_AUTH": "admin:admin", + "GRAFANA_URL": "https://test.com", + }, + config: map[string]interface{}{ + "http_headers": map[string]interface{}{ + "Authorization": "Bearer test", + "X-Custom-Header": "custom-value", + }, + }, + check: checkHeaders, + }, + { + name: "header config from env", + env: map[string]string{ + "GRAFANA_AUTH": "admin:admin", + "GRAFANA_URL": "https://test.com", + "GRAFANA_HTTP_HEADERS": `{"X-Custom-Header": "custom-value", "Authorization": "Bearer test"}`, + }, + check: checkHeaders, + }, + { + name: "invalid header", + env: map[string]string{ + "GRAFANA_AUTH": "admin:admin", + "GRAFANA_URL": "https://test.com", + "GRAFANA_HTTP_HEADERS": `blabla`, + }, + expectedErr: "invalid http_headers config: invalid character 'b' looking for beginning of value", + }, + { + name: "grafana cloud config from env", + env: map[string]string{ + "GRAFANA_CLOUD_API_KEY": "testtest", + }, + }, + { + name: "grafana sm config from env", + env: map[string]string{ + "GRAFANA_SM_ACCESS_TOKEN": "testtest", + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + os.Clearenv() + for k, v := range tc.env { + os.Setenv(k, v) + } + + test := resource.TestStep{ + // Resource is irrelevant, it's just there to test the provider being configured + // Terraform will "validate" the provider, but not actually use it when planning + PlanOnly: true, + ExpectNonEmptyPlan: true, + Config: `resource "grafana_folder" "test" { + title = "test" + }`, + } + + if tc.expectedErr != "" { + test.ExpectError = regexp.MustCompile(tc.expectedErr) + } + + // Configure the provider and check it + provider := Provider("dev")() + provider.Configure(context.Background(), terraform.NewResourceConfigRaw(tc.config)) + if tc.check != nil { + tc.check(t, provider) + } + // Run the plan to check for validation errors + resource.UnitTest(t, resource.TestCase{ + Providers: map[string]*schema.Provider{ + "grafana": provider, + }, + Steps: []resource.TestStep{test}, + }) + }) + } +} + // testAccPreCheckEnv contains all environment variables that must be present // for acceptance tests to run. These are checked in testAccPreCheck. var testAccPreCheckEnv = []string{ @@ -114,6 +248,14 @@ func accTestsEnabled(t *testing.T, envVarName string) bool { return enabled } +func IsUnitTest(t *testing.T) { + t.Helper() + + if accTestsEnabled(t, "TF_ACC") { + t.Skip("Skipping acceptance tests") + } +} + func CheckOSSTestsEnabled(t *testing.T) { t.Helper() if !accTestsEnabled(t, "TF_ACC_OSS") { diff --git a/grafana/resource_dashboard_test.go b/grafana/resource_dashboard_test.go index 30815ae4b..073e1e3c9 100644 --- a/grafana/resource_dashboard_test.go +++ b/grafana/resource_dashboard_test.go @@ -236,6 +236,8 @@ func testAccDashboardFolderCheckDestroy(dashboard *gapi.Dashboard, folder *gapi. } func Test_normalizeDashboardConfigJSON(t *testing.T) { + IsUnitTest(t) + type args struct { config interface{} } diff --git a/grafana/resource_data_source_test.go b/grafana/resource_data_source_test.go index 4bb9661a9..6cc74fb18 100644 --- a/grafana/resource_data_source_test.go +++ b/grafana/resource_data_source_test.go @@ -568,6 +568,8 @@ func TestAccDataSource_basic(t *testing.T) { } func TestDatasourceMigrationV0(t *testing.T) { + IsUnitTest(t) + cases := []struct { name string state map[string]interface{}