Skip to content

Commit

Permalink
Convert Cloud resources to new "resource" framework
Browse files Browse the repository at this point in the history
- Puts the cloud client validation into the "cloud" package (private)
- Makes sure all resources have an ID helper (to generate imports)
- Paves the way for Terraform code gen
  • Loading branch information
julienduchesne committed Feb 29, 2024
1 parent e9aeecc commit f414600
Show file tree
Hide file tree
Showing 23 changed files with 460 additions and 320 deletions.
3 changes: 1 addition & 2 deletions docs/resources/cloud_stack.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,5 @@ resource "grafana_cloud_stack" "test" {
Import is supported using the following syntax:

```shell
terraform import grafana_cloud_stack.stack_name {{stack_id}} // import by numerical ID
terraform import grafana_cloud_stack.stack_name {{stack_slug}} // or import by slug
terraform import grafana_cloud_stack.name "{{ stackSlugOrID }}"
```
8 changes: 8 additions & 0 deletions docs/resources/cloud_stack_service_account.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,11 @@ resource "grafana_cloud_stack_service_account" "cloud_sa" {
### Read-Only

- `id` (String) The ID of this resource.

## Import

Import is supported using the following syntax:

```shell
terraform import grafana_cloud_stack_service_account.name "{{ stackSlug }}:{{ serviceAccountID }}"
```
3 changes: 1 addition & 2 deletions examples/resources/grafana_cloud_stack/import.sh
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
terraform import grafana_cloud_stack.stack_name {{stack_id}} // import by numerical ID
terraform import grafana_cloud_stack.stack_name {{stack_slug}} // or import by slug
terraform import grafana_cloud_stack.name "{{ stackSlugOrID }}"
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
terraform import grafana_cloud_stack_service_account.name "{{ stackSlug }}:{{ serviceAccountID }}"
84 changes: 0 additions & 84 deletions internal/common/id.go

This file was deleted.

60 changes: 60 additions & 0 deletions internal/common/resource.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package common

import (
"fmt"
"log"
"os"
"path/filepath"
"strings"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)

var allResources = []*Resource{}

type Resource struct {
Name string
IDType *TFID
Schema *schema.Resource
}

func NewResource(name string, idType *TFID, schema *schema.Resource) *Resource {
r := &Resource{
Name: name,
IDType: idType,
Schema: schema,
}
allResources = append(allResources, r)
return r
}

func (r *Resource) ImportExample() string {
id := r.IDType
fields := make([]string, len(id.expectedFields))
for i := range fields {
fields[i] = fmt.Sprintf("{{ %s }}", id.expectedFields[i])
}
return fmt.Sprintf(`terraform import %s.name %q
`, r.Name, strings.Join(fields, defaultSeparator))
}

// GenerateImportFiles generates import files for all resources that use a helper defined in this package
func GenerateImportFiles(path string) error {
for _, r := range allResources {
resourcePath := filepath.Join(path, "resources", r.Name, "import.sh")
if err := os.RemoveAll(resourcePath); err != nil { // Remove the file if it exists
return err
}

if r.IDType == nil {
log.Printf("Skipping import file generation for %s because it does not have an ID type\n", r.Name)
continue
}

log.Printf("Generating import file for %s (writing to %s)\n", r.Name, resourcePath)
if err := os.WriteFile(resourcePath, []byte(r.ImportExample()), 0600); err != nil {
return err
}
}
return nil
}
73 changes: 73 additions & 0 deletions internal/common/resource_id.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package common

import (
"fmt"
"strconv"
"strings"
)

var (
defaultSeparator = ":"
)

type TFID struct {
separators []string
expectedFields []string
}

func NewTFID(expectedFields ...string) *TFID {
return newTFIDWithSeparators([]string{defaultSeparator}, expectedFields...)
}

// Deprecated: Use NewTFID instead
// We should standardize on a single separator, so that function should only be used for old resources
// On major versions, switch to NewTFID and remove uses of this function
func NewTFIDWithLegacySeparator(legacySeparator string, expectedFields ...string) *TFID {
return newTFIDWithSeparators([]string{defaultSeparator, legacySeparator}, expectedFields...)
}

func newTFIDWithSeparators(separators []string, expectedFields ...string) *TFID {
tfID := &TFID{
separators: separators,
expectedFields: expectedFields,
}
return tfID
}

func (id *TFID) Make(parts ...any) string {
if len(parts) != len(id.expectedFields) {
panic(fmt.Sprintf("expected %d fields, got %d", len(id.expectedFields), len(parts))) // This is a coding error, so panic is appropriate
}
stringParts := make([]string, len(parts))
for i, part := range parts {
stringParts[i] = fmt.Sprintf("%v", part)
}
return strings.Join(stringParts, defaultSeparator)
}

func (id *TFID) AsInt64(resourceID string) (int64, error) {
parts, err := id.Split(resourceID)
if err != nil {
return 0, err
}
return strconv.ParseInt(parts[0], 10, 64)
}

func (id *TFID) AsString(resourceID string) (string, error) {
parts, err := id.Split(resourceID)
if err != nil {
return "", err
}

return parts[0], nil
}

func (id *TFID) Split(resourceID string) ([]string, error) {
for _, sep := range id.separators {
parts := strings.Split(resourceID, sep)
if len(parts) == len(id.expectedFields) {
return parts, nil
}
}
return nil, fmt.Errorf("id %q does not match expected format. Should be in the format: %s", resourceID, strings.Join(id.expectedFields, defaultSeparator))
}
24 changes: 2 additions & 22 deletions internal/provider/legacy_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,19 +85,6 @@ func Provider(version string) *schema.Provider {
"grafana_synthetic_monitoring_probe": syntheticmonitoring.ResourceProbe(),
})

// Resources that require the Cloud client to exist.
cloudClientResources = addResourcesMetadataValidation(cloudClientPresent, map[string]*schema.Resource{
"grafana_cloud_access_policy": cloud.ResourceAccessPolicy(),
"grafana_cloud_access_policy_token": cloud.ResourceAccessPolicyToken(),
"grafana_cloud_api_key": cloud.ResourceAPIKey(),
"grafana_cloud_plugin_installation": cloud.ResourcePluginInstallation(),
"grafana_cloud_stack": cloud.ResourceStack(),
"grafana_cloud_stack_api_key": cloud.ResourceStackAPIKey(),
"grafana_cloud_stack_service_account": cloud.ResourceStackServiceAccount(),
"grafana_cloud_stack_service_account_token": cloud.ResourceStackServiceAccountToken(),
"grafana_synthetic_monitoring_installation": cloud.ResourceInstallation(),
})

// Resources that require the OnCall client to exist.
onCallClientResources = addResourcesMetadataValidation(onCallClientPresent, map[string]*schema.Resource{
"grafana_oncall_integration": oncall.ResourceIntegration(),
Expand Down Expand Up @@ -138,13 +125,6 @@ func Provider(version string) *schema.Provider {
"grafana_synthetic_monitoring_probes": syntheticmonitoring.DataSourceProbes(),
})

// Datasources that require the Cloud client to exist.
cloudClientDatasources = addResourcesMetadataValidation(cloudClientPresent, map[string]*schema.Resource{
"grafana_cloud_ips": cloud.DataSourceIPs(),
"grafana_cloud_organization": cloud.DataSourceOrganization(),
"grafana_cloud_stack": cloud.DataSourceStack(),
})

// Datasources that require the OnCall client to exist.
onCallClientDatasources = addResourcesMetadataValidation(onCallClientPresent, map[string]*schema.Resource{
"grafana_oncall_user": oncall.DataSourceUser(),
Expand Down Expand Up @@ -279,14 +259,14 @@ func Provider(version string) *schema.Provider {
grafanaClientResources,
smClientResources,
onCallClientResources,
cloudClientResources,
cloud.ResourcesMap(),
),

DataSourcesMap: mergeResourceMaps(
grafanaClientDatasources,
smClientDatasources,
onCallClientDatasources,
cloudClientDatasources,
cloud.DatasourcesMap,
),
}

Expand Down
7 changes: 0 additions & 7 deletions internal/provider/legacy_provider_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,6 @@ func smClientPresent(resourceName string, d *schema.ResourceData, m interface{})
return nil
}

func cloudClientPresent(resourceName string, d *schema.ResourceData, m interface{}) error {
if m.(*common.Client).GrafanaCloudAPI == nil {
return fmt.Errorf("the Cloud API client is required for `%s`. Set the cloud_api_key provider attribute", resourceName)
}
return nil
}

func onCallClientPresent(resourceName string, d *schema.ResourceData, m interface{}) error {
if m.(*common.Client).OnCallClient == nil {
return fmt.Errorf("the Oncall client is required for `%s`. Set the oncall_access_token provider attribute", resourceName)
Expand Down
16 changes: 16 additions & 0 deletions internal/resources/cloud/common.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
package cloud

import (
"context"

"github.com/grafana/grafana-com-public-clients/go/gcom"
"github.com/grafana/terraform-provider-grafana/internal/common"
"github.com/hashicorp/go-uuid"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)

func ClientRequestID() string {
Expand All @@ -14,6 +18,18 @@ func ClientRequestID() string {
return "tf-" + uuid
}

type crudWithClientFunc func(ctx context.Context, d *schema.ResourceData, client *gcom.APIClient) 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).GrafanaCloudAPI
if client == nil {
return diag.Errorf("the Cloud API client is required for this resource. Set the cloud_access_policy_token provider attribute")
}
return f(ctx, d, client)
}
}

func apiError(err error) diag.Diagnostics {
if err == nil {
return nil
Expand Down
6 changes: 3 additions & 3 deletions internal/resources/cloud/data_source_cloud_ips.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ import (
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)

func DataSourceIPs() *schema.Resource {
func datasourceIPs() *schema.Resource {
return &schema.Resource{
Description: "Data source for retrieving sets of cloud IPs. See https://grafana.com/docs/grafana-cloud/reference/allow-list/ for more info",
ReadContext: DataSourceIPsRead,
ReadContext: datasourceIPsRead,
Schema: map[string]*schema.Schema{
"hosted_alerts": {
Description: "Set of IP addresses that are used for hosted alerts.",
Expand Down Expand Up @@ -59,7 +59,7 @@ func DataSourceIPs() *schema.Resource {
}
}

func DataSourceIPsRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
func datasourceIPsRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
d.SetId("cloud_ips")
for attr, dataURL := range map[string]string{
"hosted_alerts": "https://grafana.com/api/hosted-alerts/source-ips.txt",
Expand Down
10 changes: 4 additions & 6 deletions internal/resources/cloud/data_source_cloud_organization.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ import (
"context"
"strconv"

"github.com/grafana/terraform-provider-grafana/internal/common"
"github.com/grafana/grafana-com-public-clients/go/gcom"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)

func DataSourceOrganization() *schema.Resource {
func datasourceOrganization() *schema.Resource {
return &schema.Resource{
ReadContext: DataSourceOrganizationRead,
ReadContext: withClient[schema.ReadContextFunc](datasourceOrganizationRead),
Schema: map[string]*schema.Schema{
"id": {
Type: schema.TypeString,
Expand Down Expand Up @@ -43,9 +43,7 @@ func DataSourceOrganization() *schema.Resource {
}
}

func DataSourceOrganizationRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*common.Client).GrafanaCloudAPI

func datasourceOrganizationRead(ctx context.Context, d *schema.ResourceData, client *gcom.APIClient) diag.Diagnostics {
id := d.Get("id").(string)
if id == "" {
id = d.Get("slug").(string)
Expand Down
Loading

0 comments on commit f414600

Please sign in to comment.