Skip to content

Commit 5668d84

Browse files
Service accounts: Create Grafana service accounts through cloud provider (#858)
* add resource for creating Grafana service accounts through the cloud provider * documentation for the new resources * deprecated Grafana API key resources * small change to example and import ordering * more import ordering * remove deprecation notices * fix the example
1 parent a11b175 commit 5668d84

File tree

12 files changed

+612
-9
lines changed

12 files changed

+612
-9
lines changed

docs/resources/api_key.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ output "api_key_bar" {
4848

4949
### Optional
5050

51-
- `cloud_stack_slug` (String, Deprecated) Deprecated: Use the `grafana_cloud_stack_api_key` resource instead
51+
- `cloud_stack_slug` (String, Deprecated) Deprecated: Use `grafana_cloud_stack_service_account` and `grafana_cloud_stack_service_account_token` resources instead
5252
- `org_id` (String) The Organization ID. If not set, the Org ID defined in the provider block will be used.
5353
- `seconds_to_live` (Number)
5454

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
---
2+
# generated by https://github.com/hashicorp/terraform-plugin-docs
3+
page_title: "grafana_cloud_stack_service_account Resource - terraform-provider-grafana"
4+
subcategory: "Cloud"
5+
description: |-
6+
Note: This resource is available only with Grafana 9.1+.
7+
Manages service accounts of a Grafana Cloud stack using the Cloud API
8+
This can be used to bootstrap a management service account for a new stack
9+
Official documentation https://grafana.com/docs/grafana/latest/administration/service-accounts/HTTP API https://grafana.com/docs/grafana/latest/developers/http_api/serviceaccount/#service-account-api
10+
---
11+
12+
# grafana_cloud_stack_service_account (Resource)
13+
14+
**Note:** This resource is available only with Grafana 9.1+.
15+
16+
Manages service accounts of a Grafana Cloud stack using the Cloud API
17+
This can be used to bootstrap a management service account for a new stack
18+
19+
* [Official documentation](https://grafana.com/docs/grafana/latest/administration/service-accounts/)
20+
* [HTTP API](https://grafana.com/docs/grafana/latest/developers/http_api/serviceaccount/#service-account-api)
21+
22+
## Example Usage
23+
24+
```terraform
25+
resource "grafana_cloud_stack_service_account" "cloud_sa" {
26+
stack_slug = "<your stack slug>"
27+
28+
name = "cloud service account"
29+
role = "Admin"
30+
is_disabled = false
31+
}
32+
```
33+
34+
<!-- schema generated by tfplugindocs -->
35+
## Schema
36+
37+
### Required
38+
39+
- `name` (String) The name of the service account.
40+
- `stack_slug` (String)
41+
42+
### Optional
43+
44+
- `is_disabled` (Boolean) The disabled status for the service account. Defaults to `false`.
45+
- `role` (String) The basic role of the service account in the organization.
46+
47+
### Read-Only
48+
49+
- `id` (String) The ID of this resource.
50+
51+
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
---
2+
# generated by https://github.com/hashicorp/terraform-plugin-docs
3+
page_title: "grafana_cloud_stack_service_account_token Resource - terraform-provider-grafana"
4+
subcategory: "Cloud"
5+
description: |-
6+
Note: This resource is available only with Grafana 9.1+.
7+
Manages service account tokens of a Grafana Cloud stack using the Cloud API
8+
This can be used to bootstrap a management service account token for a new stack
9+
Official documentation https://grafana.com/docs/grafana/latest/administration/service-accounts/HTTP API https://grafana.com/docs/grafana/latest/developers/http_api/serviceaccount/#service-account-api
10+
---
11+
12+
# grafana_cloud_stack_service_account_token (Resource)
13+
14+
**Note:** This resource is available only with Grafana 9.1+.
15+
16+
Manages service account tokens of a Grafana Cloud stack using the Cloud API
17+
This can be used to bootstrap a management service account token for a new stack
18+
19+
* [Official documentation](https://grafana.com/docs/grafana/latest/administration/service-accounts/)
20+
* [HTTP API](https://grafana.com/docs/grafana/latest/developers/http_api/serviceaccount/#service-account-api)
21+
22+
## Example Usage
23+
24+
```terraform
25+
resource "grafana_cloud_stack_service_account" "cloud_sa" {
26+
stack_slug = "<your stack slug>"
27+
28+
name = "cloud service account"
29+
role = "Admin"
30+
is_disabled = false
31+
}
32+
33+
resource "grafana_cloud_stack_service_account_token" "foo" {
34+
name = "key_foo"
35+
service_account_id = grafana_cloud_stack_service_account.cloud_sa.id
36+
}
37+
38+
output "service_account_token_foo_key" {
39+
value = grafana_cloud_stack_service_account_token.foo.key
40+
sensitive = true
41+
}
42+
```
43+
44+
<!-- schema generated by tfplugindocs -->
45+
## Schema
46+
47+
### Required
48+
49+
- `name` (String)
50+
- `service_account_id` (String)
51+
- `stack_slug` (String)
52+
53+
### Optional
54+
55+
- `seconds_to_live` (Number)
56+
57+
### Read-Only
58+
59+
- `expiration` (String)
60+
- `has_expired` (Boolean)
61+
- `id` (String) The ID of this resource.
62+
- `key` (String, Sensitive)
63+
64+
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
resource "grafana_cloud_stack_service_account" "cloud_sa" {
2+
stack_slug = "<your stack slug>"
3+
4+
name = "cloud service account"
5+
role = "Admin"
6+
is_disabled = false
7+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
resource "grafana_cloud_stack_service_account" "cloud_sa" {
2+
stack_slug = "<your stack slug>"
3+
4+
name = "cloud service account"
5+
role = "Admin"
6+
is_disabled = false
7+
}
8+
9+
resource "grafana_cloud_stack_service_account_token" "foo" {
10+
name = "key_foo"
11+
service_account_id = grafana_cloud_stack_service_account.cloud_sa.id
12+
}
13+
14+
output "service_account_token_foo_key" {
15+
value = grafana_cloud_stack_service_account_token.foo.key
16+
sensitive = true
17+
}

internal/provider/provider.go

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -87,12 +87,14 @@ func Provider(version string) func() *schema.Provider {
8787

8888
// Resources that require the Cloud client to exist.
8989
cloudClientResources = addResourcesMetadataValidation(cloudClientPresent, map[string]*schema.Resource{
90-
"grafana_cloud_access_policy": cloud.ResourceAccessPolicy(),
91-
"grafana_cloud_access_policy_token": cloud.ResourceAccessPolicyToken(),
92-
"grafana_cloud_api_key": cloud.ResourceAPIKey(),
93-
"grafana_cloud_plugin_installation": cloud.ResourcePluginInstallation(),
94-
"grafana_cloud_stack": cloud.ResourceStack(),
95-
"grafana_cloud_stack_api_key": cloud.ResourceStackAPIKey(),
90+
"grafana_cloud_access_policy": cloud.ResourceAccessPolicy(),
91+
"grafana_cloud_access_policy_token": cloud.ResourceAccessPolicyToken(),
92+
"grafana_cloud_api_key": cloud.ResourceAPIKey(),
93+
"grafana_cloud_plugin_installation": cloud.ResourcePluginInstallation(),
94+
"grafana_cloud_stack": cloud.ResourceStack(),
95+
"grafana_cloud_stack_api_key": cloud.ResourceStackAPIKey(),
96+
"grafana_cloud_stack_service_account": cloud.ResourceStackServiceAccount(),
97+
"grafana_cloud_stack_service_account_token": cloud.ResourceStackServiceAccountToken(),
9698
})
9799

98100
// Resources that require the OnCall client to exist.
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
package cloud
2+
3+
import (
4+
"context"
5+
"log"
6+
"strconv"
7+
"time"
8+
9+
gapi "github.com/grafana/grafana-api-golang-client"
10+
"github.com/grafana/terraform-provider-grafana/internal/common"
11+
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
12+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
13+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
14+
)
15+
16+
func ResourceStackServiceAccount() *schema.Resource {
17+
return &schema.Resource{
18+
19+
Description: `
20+
**Note:** This resource is available only with Grafana 9.1+.
21+
22+
Manages service accounts of a Grafana Cloud stack using the Cloud API
23+
This can be used to bootstrap a management service account for a new stack
24+
25+
* [Official documentation](https://grafana.com/docs/grafana/latest/administration/service-accounts/)
26+
* [HTTP API](https://grafana.com/docs/grafana/latest/developers/http_api/serviceaccount/#service-account-api)`,
27+
28+
CreateContext: createStackServiceAccount,
29+
ReadContext: readStackServiceAccount,
30+
UpdateContext: updateStackServiceAccount,
31+
DeleteContext: deleteStackServiceAccount,
32+
Importer: &schema.ResourceImporter{
33+
StateContext: schema.ImportStatePassthroughContext,
34+
},
35+
Schema: map[string]*schema.Schema{
36+
"stack_slug": {
37+
Type: schema.TypeString,
38+
Required: true,
39+
ForceNew: true,
40+
},
41+
"name": {
42+
Type: schema.TypeString,
43+
Required: true,
44+
ForceNew: true,
45+
Description: "The name of the service account.",
46+
},
47+
"role": {
48+
Type: schema.TypeString,
49+
Optional: true,
50+
ValidateFunc: validation.StringInSlice([]string{"Viewer", "Editor", "Admin"}, false),
51+
Description: "The basic role of the service account in the organization.",
52+
},
53+
"is_disabled": {
54+
Type: schema.TypeBool,
55+
Optional: true,
56+
Default: false,
57+
Description: "The disabled status for the service account.",
58+
},
59+
},
60+
}
61+
}
62+
63+
func createStackServiceAccount(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
64+
client, cleanup, err := getClientForSAManagement(d, meta)
65+
if err != nil {
66+
return diag.FromErr(err)
67+
}
68+
defer cleanup()
69+
70+
isDisabled := d.Get("is_disabled").(bool)
71+
req := gapi.CreateServiceAccountRequest{
72+
Name: d.Get("name").(string),
73+
Role: d.Get("role").(string),
74+
IsDisabled: &isDisabled,
75+
}
76+
sa, err := client.CreateServiceAccount(req)
77+
if err != nil {
78+
return diag.FromErr(err)
79+
}
80+
81+
d.SetId(strconv.FormatInt(sa.ID, 10))
82+
return readStackServiceAccount(ctx, d, meta)
83+
}
84+
85+
func readStackServiceAccount(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
86+
client, cleanup, err := getClientForSAManagement(d, meta)
87+
if err != nil {
88+
return diag.FromErr(err)
89+
}
90+
defer cleanup()
91+
92+
id, err := strconv.ParseInt(d.Id(), 10, 64)
93+
if err != nil {
94+
return diag.FromErr(err)
95+
}
96+
97+
sas, err := client.GetServiceAccounts()
98+
if err != nil {
99+
return diag.FromErr(err)
100+
}
101+
102+
for _, sa := range sas {
103+
if sa.ID == id {
104+
err = d.Set("name", sa.Name)
105+
if err != nil {
106+
return diag.FromErr(err)
107+
}
108+
err = d.Set("role", sa.Role)
109+
if err != nil {
110+
return diag.FromErr(err)
111+
}
112+
err = d.Set("is_disabled", sa.IsDisabled)
113+
if err != nil {
114+
return diag.FromErr(err)
115+
}
116+
117+
return nil
118+
}
119+
}
120+
log.Printf("[WARN] removing service account %d from state because it no longer exists in grafana", id)
121+
d.SetId("")
122+
123+
return nil
124+
}
125+
126+
func updateStackServiceAccount(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
127+
client, cleanup, err := getClientForSAManagement(d, meta)
128+
if err != nil {
129+
return diag.FromErr(err)
130+
}
131+
defer cleanup()
132+
133+
id, err := strconv.ParseInt(d.Id(), 10, 64)
134+
if err != nil {
135+
return diag.FromErr(err)
136+
}
137+
138+
updateRequest := gapi.UpdateServiceAccountRequest{}
139+
if d.HasChange("name") {
140+
updateRequest.Name = d.Get("name").(string)
141+
}
142+
if d.HasChange("role") {
143+
updateRequest.Role = d.Get("role").(string)
144+
}
145+
if d.HasChange("is_disabled") {
146+
isDisabled := d.Get("is_disabled").(bool)
147+
updateRequest.IsDisabled = &isDisabled
148+
}
149+
150+
if _, err := client.UpdateServiceAccount(id, updateRequest); err != nil {
151+
return diag.FromErr(err)
152+
}
153+
154+
return readStackServiceAccount(ctx, d, meta)
155+
}
156+
157+
func deleteStackServiceAccount(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
158+
client, cleanup, err := getClientForSAManagement(d, meta)
159+
if err != nil {
160+
return diag.FromErr(err)
161+
}
162+
defer cleanup()
163+
164+
id, err := strconv.ParseInt(d.Id(), 10, 64)
165+
if err != nil {
166+
return diag.FromErr(err)
167+
}
168+
169+
_, err = client.DeleteServiceAccount(id)
170+
return diag.FromErr(err)
171+
}
172+
173+
func getClientForSAManagement(d *schema.ResourceData, m interface{}) (c *gapi.Client, cleanup func() error, err error) {
174+
cloudClient := m.(*common.Client).GrafanaCloudAPI
175+
return cloudClient.CreateTemporaryStackGrafanaClient(d.Get("stack_slug").(string), "terraform-temp-sa-", 60*time.Second)
176+
}

0 commit comments

Comments
 (0)