Skip to content

Commit 87de6a4

Browse files
committed
feat(service-accounts): Add rotating token resource
1 parent a217859 commit 87de6a4

File tree

8 files changed

+565
-58
lines changed

8 files changed

+565
-58
lines changed
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
---
2+
# generated by https://github.com/hashicorp/terraform-plugin-docs
3+
page_title: "grafana_service_account_rotating_token Resource - terraform-provider-grafana"
4+
subcategory: "Grafana OSS"
5+
description: |-
6+
Note: This resource is available only with Grafana 9.1+.
7+
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
8+
---
9+
10+
# grafana_service_account_rotating_token (Resource)
11+
12+
**Note:** This resource is available only with Grafana 9.1+.
13+
14+
* [Official documentation](https://grafana.com/docs/grafana/latest/administration/service-accounts/)
15+
* [HTTP API](https://grafana.com/docs/grafana/latest/developers/http_api/serviceaccount/#service-account-api)
16+
17+
18+
19+
<!-- schema generated by tfplugindocs -->
20+
## Schema
21+
22+
### Required
23+
24+
- `early_rotation_window_seconds` (Number) Duration of the time window before expiring where the token can be rotated, in seconds.
25+
- `name_prefix` (String) Prefix for the name of the service account tokens created by this resource. The actual name will be stored in the computed field `name`, which will be in the format `<name_prefix>-<expiration timestamp>`
26+
- `seconds_to_live` (Number) The token expiration in seconds.
27+
- `service_account_id` (String) The ID of the service account to which the token belongs.
28+
29+
### Optional
30+
31+
- `delete_on_destroy` (Boolean) Deletes the service account token in Grafana when the resource is destroyed in Terraform, instead of leaving it to expire at its `expiration` time. Use it with `lifecycle { create_before_destroy = true }` to make sure that the new token is created before the old one is deleted. Defaults to `false`.
32+
33+
### Read-Only
34+
35+
- `expiration` (String) The expiration date of the service account token.
36+
- `has_expired` (Boolean) The status of the service account token.
37+
- `id` (String) The ID of this resource.
38+
- `key` (String, Sensitive) The key of the service account token.
39+
- `name` (String) The name of the service account token.
40+
- `ready_for_rotation` (Boolean) Signals that the service account token is expired or within the period to be early rotated.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
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_rotating_token" "foo" {
10+
name_prefix = "key_foo"
11+
service_account_id = grafana_cloud_stack_service_account.cloud_sa.id
12+
seconds_to_live = 7776000 # 3 months
13+
early_rotation_window_seconds = 604800 # 1 week
14+
}
15+
16+
output "service_account_token_foo_key" {
17+
value = grafana_cloud_stack_service_account_rotating_token.foo.key
18+
sensitive = true
19+
}
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
package grafana
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"time"
7+
8+
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
9+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
10+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
11+
12+
"github.com/grafana/terraform-provider-grafana/v4/internal/common"
13+
)
14+
15+
func resourceServiceAccountRotatingToken() *common.Resource {
16+
schema := &schema.Resource{
17+
Description: `
18+
**Note:** This resource is available only with Grafana 9.1+.
19+
20+
* [Official documentation](https://grafana.com/docs/grafana/latest/administration/service-accounts/)
21+
* [HTTP API](https://grafana.com/docs/grafana/latest/developers/http_api/serviceaccount/#service-account-api)`,
22+
23+
// We use the same function for Read and Update because fields are only updated in the Terraform state,
24+
// not in Grafana, for this resource.
25+
CreateContext: serviceAccountRotatingTokenCreate,
26+
ReadContext: serviceAccountRotatingTokenRead,
27+
UpdateContext: serviceAccountRotatingTokenRead,
28+
DeleteContext: serviceAccountRotatingTokenDelete,
29+
30+
CustomizeDiff: func(ctx context.Context, d *schema.ResourceDiff, meta any) error {
31+
secondsToLive := d.Get("seconds_to_live").(int)
32+
earlyRotationWindowSec := d.Get("early_rotation_window_seconds").(int)
33+
34+
if earlyRotationWindowSec > secondsToLive {
35+
return fmt.Errorf("`early_rotation_window_seconds` cannot be bigger than `seconds_to_live`")
36+
}
37+
38+
// We need to use GetChange() to get the value from the state because Get() omits computed values that are
39+
// not being changed.
40+
hasExpired, _ := d.GetChange("has_expired")
41+
if hasExpired != nil && hasExpired.(bool) {
42+
return d.SetNew("ready_for_rotation", true)
43+
}
44+
45+
expirationState, _ := d.GetChange("expiration")
46+
if expirationState != nil && expirationState.(string) != "" {
47+
expiration, err := time.Parse(time.RFC3339, expirationState.(string))
48+
if err != nil {
49+
return fmt.Errorf("could not parse 'expiration' while calculating custom diff: %w", err)
50+
}
51+
if ServiceAccountRotatingTokenNow().After(expiration.Add(-1 * time.Duration(earlyRotationWindowSec) * time.Second)) {
52+
return d.SetNew("ready_for_rotation", true)
53+
}
54+
}
55+
56+
return nil
57+
},
58+
59+
Schema: serviceAccountTokenResourceWithCustomSchema(map[string]*schema.Schema{
60+
"name_prefix": {
61+
Type: schema.TypeString,
62+
Required: true,
63+
ForceNew: true,
64+
Description: "Prefix for the name of the service account tokens created by this resource. " +
65+
"The actual name will be stored in the computed field `name`, which will be in the format " +
66+
"`<name_prefix>-<expiration timestamp>`",
67+
},
68+
"seconds_to_live": {
69+
Type: schema.TypeInt,
70+
Required: true,
71+
ForceNew: true,
72+
ValidateFunc: validation.IntAtLeast(0),
73+
Description: "The token expiration in seconds.",
74+
},
75+
"early_rotation_window_seconds": {
76+
Type: schema.TypeInt,
77+
Required: true,
78+
ValidateFunc: validation.IntAtLeast(0),
79+
Description: "Duration of the time window before expiring where the token can be rotated, in seconds.",
80+
},
81+
"delete_on_destroy": {
82+
Type: schema.TypeBool,
83+
Optional: true,
84+
Default: false,
85+
Description: "Deletes the service account token in Grafana when the resource " +
86+
"is destroyed in Terraform, instead of leaving it to expire at its `expiration` " +
87+
"time. Use it with `lifecycle { create_before_destroy = true }` to make sure " +
88+
"that the new token is created before the old one is deleted.",
89+
},
90+
// Computed
91+
"name": {
92+
Type: schema.TypeString,
93+
Computed: true,
94+
Description: "The name of the service account token.",
95+
},
96+
"ready_for_rotation": {
97+
Type: schema.TypeBool,
98+
Computed: true,
99+
ForceNew: true,
100+
Description: "Signals that the service account token is expired or " +
101+
"within the period to be early rotated.",
102+
},
103+
}),
104+
}
105+
106+
return common.NewLegacySDKResource(
107+
common.CategoryGrafanaOSS,
108+
"grafana_service_account_rotating_token",
109+
nil,
110+
schema,
111+
)
112+
}
113+
114+
func serviceAccountRotatingTokenCreate(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics {
115+
namePrefix := d.Get("name_prefix").(string)
116+
ttl := d.Get("seconds_to_live").(int)
117+
118+
expiration := ServiceAccountRotatingTokenNow().Add(time.Duration(ttl) * time.Second)
119+
name := fmt.Sprintf("%s-%d", namePrefix, expiration.Unix())
120+
121+
err := serviceAccountTokenCreateHelper(ctx, d, m, name)
122+
if err != nil {
123+
return diag.FromErr(err)
124+
}
125+
126+
// Fill the true resource's state by performing a read
127+
return serviceAccountRotatingTokenRead(ctx, d, m)
128+
}
129+
130+
func serviceAccountRotatingTokenRead(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics {
131+
return serviceAccountTokenRead(ctx, d, m)
132+
}
133+
134+
func serviceAccountRotatingTokenDelete(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics {
135+
if !d.Get("delete_on_destroy").(bool) {
136+
return diag.Diagnostics{
137+
diag.Diagnostic{
138+
Severity: diag.Warning,
139+
Summary: "Rotating tokens do not get deleted by default.",
140+
Detail: "The Service Account token will not be deleted and will expire automatically at its expiration time. " +
141+
"If it does not have an expiration, it will need to be deleted manually. To change this behaviour " +
142+
"enable `delete_on_destroy`.",
143+
},
144+
}
145+
}
146+
return serviceAccountTokenDelete(ctx, d, m)
147+
}
148+
149+
// ServiceAccountRotatingTokenNow returns the current time.
150+
// It can be overridden in tests to provide a different time.
151+
var ServiceAccountRotatingTokenNow = time.Now

0 commit comments

Comments
 (0)