Skip to content

Commit

Permalink
cloud_stack_service_account: Use new GET and DELETE endpoints
Browse files Browse the repository at this point in the history
Rather than having to POST a temporary service account and using that to do full CRUD management, the gcom now exposes full CRUD capability that we can use.

This means that doing a `terraform plan` will no longer POST a temporary SA for every `cloud_stack_service_account` and `cloud_stack_service_account_token` resource in the state
This should help users that are being throttled by the gcom API
  • Loading branch information
julienduchesne committed Mar 22, 2024
1 parent aaa20a2 commit 278dffd
Show file tree
Hide file tree
Showing 5 changed files with 52 additions and 138 deletions.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ require (
github.com/go-openapi/runtime v0.28.0
github.com/go-openapi/strfmt v0.23.0
github.com/grafana/amixr-api-go-client v0.0.11
github.com/grafana/grafana-com-public-clients/go/gcom v0.0.0-20240305213348-af081a82f063
github.com/grafana/grafana-com-public-clients/go/gcom v0.0.0-20240322153219-42c6a1d2bcab
github.com/grafana/grafana-openapi-client-go v0.0.0-20240306142804-861284d1ba83
github.com/grafana/machine-learning-go-client v0.5.0
github.com/grafana/slo-openapi-client/go v0.0.0-20240112175006-de02e75b9d73
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grafana/amixr-api-go-client v0.0.11 h1:jlE+5t0tRuCtjbpM81j70Dr2J4eCySuWyNGdfLMGdhE=
github.com/grafana/amixr-api-go-client v0.0.11/go.mod h1:N6x26XUrM5zGtK5zL5vNJnAn2JFMxLFPPLTw/6pDkFE=
github.com/grafana/grafana-com-public-clients/go/gcom v0.0.0-20240305213348-af081a82f063 h1:DI3xC2krNY3Su40eQn50rfkodv92Ce1i5Updr0TMBH4=
github.com/grafana/grafana-com-public-clients/go/gcom v0.0.0-20240305213348-af081a82f063/go.mod h1:6sYY1qgwYfSDNQhKQA0tar8Oc38cIGfyqwejhxoOsPs=
github.com/grafana/grafana-com-public-clients/go/gcom v0.0.0-20240322153219-42c6a1d2bcab h1:/5R8NO996/keDkZqKXEkU3/QgFs1wzChKYkakjsBpRk=
github.com/grafana/grafana-com-public-clients/go/gcom v0.0.0-20240322153219-42c6a1d2bcab/go.mod h1:6sYY1qgwYfSDNQhKQA0tar8Oc38cIGfyqwejhxoOsPs=
github.com/grafana/grafana-openapi-client-go v0.0.0-20240306142804-861284d1ba83 h1:n5vecnZOTpRAZu7rIyNmg54k860JPvAuMvnz2AYQD0E=
github.com/grafana/grafana-openapi-client-go v0.0.0-20240306142804-861284d1ba83/go.mod h1:FiVsMOTtVuo/Ajmt1efuk3/KmDpFQ3qMurQf7e6HwQg=
github.com/grafana/machine-learning-go-client v0.5.0 h1:Q1K+MPSy8vfMm2jsk3WQ7O77cGr2fM5hxwtPSoPc5NU=
Expand Down
103 changes: 23 additions & 80 deletions internal/resources/cloud/resource_cloud_stack_service_account.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ import (

"github.com/grafana/grafana-com-public-clients/go/gcom"
goapi "github.com/grafana/grafana-openapi-client-go/client"
"github.com/grafana/grafana-openapi-client-go/client/service_accounts"
"github.com/grafana/grafana-openapi-client-go/models"
"github.com/grafana/terraform-provider-grafana/v2/internal/common"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
Expand Down Expand Up @@ -42,7 +40,6 @@ Required access policy scopes:

CreateContext: withClient[schema.CreateContextFunc](createStackServiceAccount),
ReadContext: withClient[schema.ReadContextFunc](readStackServiceAccount),
UpdateContext: withClient[schema.UpdateContextFunc](updateStackServiceAccount),
DeleteContext: withClient[schema.DeleteContextFunc](deleteStackServiceAccount),
Importer: &schema.ResourceImporter{
StateContext: schema.ImportStatePassthroughContext,
Expand All @@ -56,21 +53,22 @@ Required access policy scopes:
"name": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
Description: "The name of the service account.",
ForceNew: true,
},
"role": {
Type: schema.TypeString,
Optional: true,
ValidateFunc: validation.StringInSlice([]string{"Viewer", "Editor", "Admin"}, false),
Description: "The basic role of the service account in the organization.",
ForceNew: true, // The grafana API does not support updating the service account
},
"is_disabled": {
Type: schema.TypeBool,
Optional: true,
Default: false,
ForceNew: true,
Description: "The disabled status for the service account.",
ForceNew: true, // The grafana API does not support updating the service account
},
},
}
Expand All @@ -84,25 +82,21 @@ Required access policy scopes:

func createStackServiceAccount(ctx context.Context, d *schema.ResourceData, cloudClient *gcom.APIClient) diag.Diagnostics {
stackSlug := d.Get("stack_slug").(string)
client, cleanup, err := CreateTemporaryStackGrafanaClient(ctx, cloudClient, stackSlug, "terraform-temp-")
if err != nil {
return diag.FromErr(err)
}
defer cleanup()

req := service_accounts.NewCreateServiceAccountParams().WithBody(&models.CreateServiceAccountForm{
req := gcom.PostInstanceServiceAccountsRequest{
Name: d.Get("name").(string),
Role: d.Get("role").(string),
IsDisabled: d.Get("is_disabled").(bool),
})
resp, err := client.ServiceAccounts.CreateServiceAccount(req)
IsDisabled: common.Ref(d.Get("is_disabled").(bool)),
}
resp, _, err := cloudClient.InstancesAPI.PostInstanceServiceAccounts(ctx, stackSlug).
PostInstanceServiceAccountsRequest(req).
XRequestId(ClientRequestID()).
Execute()
if err != nil {
return diag.FromErr(err)
}
sa := resp.Payload

d.SetId(resourceStackServiceAccountID.Make(stackSlug, sa.ID))
return readStackServiceAccountWithClient(client, d, sa.ID)
d.SetId(resourceStackServiceAccountID.Make(stackSlug, resp.Id))
return readStackServiceAccount(ctx, d, cloudClient)
}

func readStackServiceAccount(ctx context.Context, d *schema.ResourceData, cloudClient *gcom.APIClient) diag.Diagnostics {
Expand All @@ -122,69 +116,22 @@ func readStackServiceAccount(ctx context.Context, d *schema.ResourceData, cloudC
stackSlug, serviceAccountID = split[0].(string), split[1].(int64)
}

client, cleanup, err := CreateTemporaryStackGrafanaClient(ctx, cloudClient, stackSlug, "terraform-temp-")
if err != nil {
return diag.FromErr(err)
}
defer cleanup()

d.Set("stack_slug", stackSlug)
d.SetId(resourceStackServiceAccountID.Make(stackSlug, serviceAccountID))

return readStackServiceAccountWithClient(client, d, serviceAccountID)
}

func readStackServiceAccountWithClient(client *goapi.GrafanaHTTPAPI, d *schema.ResourceData, serviceAccountID int64) diag.Diagnostics {
resp, err := client.ServiceAccounts.RetrieveServiceAccount(serviceAccountID)
if err != nil {
return diag.FromErr(err)
}
sa := resp.Payload

err = d.Set("name", sa.Name)
if err != nil {
return diag.FromErr(err)
}
err = d.Set("role", sa.Role)
if err != nil {
return diag.FromErr(err)
}
err = d.Set("is_disabled", sa.IsDisabled)
if err != nil {
return diag.FromErr(err)
}

return nil
}

func updateStackServiceAccount(ctx context.Context, d *schema.ResourceData, cloudClient *gcom.APIClient) diag.Diagnostics {
split, err := resourceStackServiceAccountID.Split(d.Id())
if err != nil {
return diag.FromErr(err)
resp, httpResp, err := cloudClient.InstancesAPI.GetInstanceServiceAccount(ctx, stackSlug, strconv.FormatInt(serviceAccountID, 10)).Execute()
if httpResp != nil && httpResp.StatusCode == 404 {
d.SetId("")
return nil
}
stackSlug, serviceAccountID := split[0].(string), split[1].(int64)

client, cleanup, err := CreateTemporaryStackGrafanaClient(ctx, cloudClient, stackSlug, "terraform-temp-")
if err != nil {
return diag.FromErr(err)
}
defer cleanup()

updateRequest := service_accounts.NewUpdateServiceAccountParams().
WithBody(&models.UpdateServiceAccountForm{
Name: d.Get("name").(string),
Role: d.Get("role").(string),
IsDisabled: d.Get("is_disabled").(bool),
}).
WithServiceAccountID(serviceAccountID)

if _, err := client.ServiceAccounts.UpdateServiceAccount(updateRequest); err != nil {
return diag.FromErr(err)
}
d.Set("stack_slug", stackSlug)
d.Set("name", resp.Name)
d.Set("role", resp.Role)
d.Set("is_disabled", resp.IsDisabled)
d.SetId(resourceStackServiceAccountID.Make(stackSlug, serviceAccountID))

return readStackServiceAccountWithClient(client, d, serviceAccountID)
return nil
}

func deleteStackServiceAccount(ctx context.Context, d *schema.ResourceData, cloudClient *gcom.APIClient) diag.Diagnostics {
Expand All @@ -194,13 +141,9 @@ func deleteStackServiceAccount(ctx context.Context, d *schema.ResourceData, clou
}
stackSlug, serviceAccountID := split[0].(string), split[1].(int64)

client, cleanup, err := CreateTemporaryStackGrafanaClient(ctx, cloudClient, stackSlug, "terraform-temp-")
if err != nil {
return diag.FromErr(err)
}
defer cleanup()

_, err = client.ServiceAccounts.DeleteServiceAccount(serviceAccountID)
_, err = cloudClient.InstancesAPI.DeleteInstanceServiceAccount(ctx, stackSlug, strconv.FormatInt(serviceAccountID, 10)).
XRequestId(ClientRequestID()).
Execute()
return diag.FromErr(err)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,6 @@ import (
"strconv"

"github.com/grafana/grafana-com-public-clients/go/gcom"
goapi "github.com/grafana/grafana-openapi-client-go/client"
"github.com/grafana/grafana-openapi-client-go/client/service_accounts"
"github.com/grafana/grafana-openapi-client-go/models"
"github.com/grafana/terraform-provider-grafana/v2/internal/common"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
Expand Down Expand Up @@ -84,57 +81,43 @@ Required access policy scopes:
}

func stackServiceAccountTokenCreate(ctx context.Context, d *schema.ResourceData, cloudClient *gcom.APIClient) diag.Diagnostics {
c, cleanup, err := CreateTemporaryStackGrafanaClient(ctx, cloudClient, d.Get("stack_slug").(string), "terraform-temp-")
if err != nil {
return diag.FromErr(err)
}
defer cleanup()

stackSlug := d.Get("stack_slug").(string)
serviceAccountID, err := getStackServiceAccountID(d.Get("service_account_id").(string))
if err != nil {
return diag.FromErr(err)
}

name := d.Get("name").(string)
ttl := d.Get("seconds_to_live").(int)
req := gcom.PostInstanceServiceAccountTokensRequest{
Name: d.Get("name").(string),
SecondsToLive: common.Ref(int32(d.Get("seconds_to_live").(int))),
}

request := service_accounts.NewCreateTokenParams().WithBody(&models.AddServiceAccountTokenCommand{
Name: name,
SecondsToLive: int64(ttl),
}).WithServiceAccountID(serviceAccountID)
response, err := c.ServiceAccounts.CreateToken(request)
resp, _, err := cloudClient.InstancesAPI.PostInstanceServiceAccountTokens(ctx, stackSlug, strconv.FormatInt(serviceAccountID, 10)).
PostInstanceServiceAccountTokensRequest(req).
XRequestId(ClientRequestID()).
Execute()
if err != nil {
return diag.FromErr(err)
}
t := response.Payload

d.SetId(strconv.FormatInt(t.ID, 10))
err = d.Set("key", t.Key)
d.SetId(strconv.FormatInt(*resp.Id, 10))
err = d.Set("key", resp.Key)
if err != nil {
return diag.FromErr(err)
}

// Fill the true resource's state by performing a read
return stackServiceAccountTokenReadWithClient(c, d)
return stackServiceAccountTokenRead(ctx, d, cloudClient)
}

func stackServiceAccountTokenRead(ctx context.Context, d *schema.ResourceData, cloudClient *gcom.APIClient) diag.Diagnostics {
c, cleanup, err := CreateTemporaryStackGrafanaClient(ctx, cloudClient, d.Get("stack_slug").(string), "terraform-temp-")
if err != nil {
return diag.FromErr(err)
}
defer cleanup()

return stackServiceAccountTokenReadWithClient(c, d)
}

func stackServiceAccountTokenReadWithClient(c *goapi.GrafanaHTTPAPI, d *schema.ResourceData) diag.Diagnostics {
stackSlug := d.Get("stack_slug").(string)
serviceAccountID, err := getStackServiceAccountID(d.Get("service_account_id").(string))
if err != nil {
return diag.FromErr(err)
}

response, err := c.ServiceAccounts.ListTokens(serviceAccountID)
response, _, err := cloudClient.InstancesAPI.GetInstanceServiceAccountTokens(ctx, stackSlug, strconv.FormatInt(serviceAccountID, 10)).Execute()
if err != nil {
return diag.FromErr(err)
}
Expand All @@ -143,14 +126,14 @@ func stackServiceAccountTokenReadWithClient(c *goapi.GrafanaHTTPAPI, d *schema.R
if err != nil {
return diag.FromErr(err)
}
for _, key := range response.Payload {
if id == key.ID {
d.SetId(strconv.FormatInt(key.ID, 10))
for _, key := range response {
if id == *key.Id {
d.SetId(strconv.FormatInt(*key.Id, 10))
err = d.Set("name", key.Name)
if err != nil {
return diag.FromErr(err)
}
if !key.Expiration.IsZero() {
if key.Expiration != nil && !key.Expiration.IsZero() {
err = d.Set("expiration", key.Expiration.String())
if err != nil {
return diag.FromErr(err)
Expand All @@ -162,35 +145,23 @@ func stackServiceAccountTokenReadWithClient(c *goapi.GrafanaHTTPAPI, d *schema.R
}
}

log.Printf("[WARN] removing service account token%d from state because it no longer exists in grafana", id)
log.Printf("[WARN] removing service account token %d from state because it no longer exists in grafana", id)
d.SetId("")

return nil
}

func stackServiceAccountTokenDelete(ctx context.Context, d *schema.ResourceData, cloudClient *gcom.APIClient) diag.Diagnostics {
c, cleanup, err := CreateTemporaryStackGrafanaClient(ctx, cloudClient, d.Get("stack_slug").(string), "terraform-temp-")
if err != nil {
return diag.FromErr(err)
}
defer cleanup()

stackSlug := d.Get("stack_slug").(string)
serviceAccountID, err := getStackServiceAccountID(d.Get("service_account_id").(string))
if err != nil {
return diag.FromErr(err)
}

id, err := strconv.ParseInt(d.Id(), 10, 32)
if err != nil {
return diag.FromErr(err)
}

_, err = c.ServiceAccounts.DeleteToken(id, serviceAccountID)
if err != nil {
return diag.FromErr(err)
}

return nil
_, err = cloudClient.InstancesAPI.DeleteInstanceServiceAccountToken(ctx, stackSlug, strconv.FormatInt(serviceAccountID, 10), d.Id()).
XRequestId(ClientRequestID()).
Execute()
return diag.FromErr(err)
}

func getStackServiceAccountID(id string) (int64, error) {
Expand Down
4 changes: 2 additions & 2 deletions internal/resources/cloud/resource_cloud_stack_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -231,8 +231,8 @@ func testAccStackCheckDestroy(a *gcom.FormattedApiInstance) resource.TestCheckFu
return func(s *terraform.State) error {
client := testutils.Provider.Meta().(*common.Client).GrafanaCloudAPI
stack, _, err := client.InstancesAPI.GetInstance(context.Background(), a.Slug).Execute()
if err == nil && stack.Name != "" {
return fmt.Errorf("stack `%s` with ID `%d` still exists after destroy", stack.Name, int(stack.Id))
if err == nil && stack.Name != "" && stack.Status != "deleting" {
return fmt.Errorf("stack `%s` with ID `%d` still exists after destroy. Status: %s", stack.Name, int(stack.Id), stack.Status)
}

return nil
Expand Down

0 comments on commit 278dffd

Please sign in to comment.