Skip to content

Commit 0cfc05b

Browse files
feat(iam): datasource to query service-accounts (#1256)
* feat(iam): datasource to query service-accounts Signed-off-by: Mauritz Uphoff <mauritz.uphoff@stackit.cloud> * feat(iam): added service account id attribute and removed service account token resource Signed-off-by: Mauritz Uphoff <mauritz.uphoff@stackit.cloud> --------- Signed-off-by: Mauritz Uphoff <mauritz.uphoff@stackit.cloud> Co-authored-by: cgoetz-inovex <carlo.goetz@inovex.de>
1 parent fadc859 commit 0cfc05b

23 files changed

Lines changed: 799 additions & 896 deletions

docs/data-sources/service_account.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,4 @@ data "stackit_service_account" "sa" {
3131

3232
- `id` (String) Terraform's internal resource ID, structured as "`project_id`,`email`".
3333
- `name` (String) Name of the service account.
34+
- `service_account_id` (String) The internal UUID of the service account.
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
---
2+
# generated by https://github.com/hashicorp/terraform-plugin-docs
3+
page_title: "stackit_service_accounts Data Source - stackit"
4+
subcategory: ""
5+
description: |-
6+
Service accounts plural data source schema. Returns a list of all service accounts in a project, optionally filtered.
7+
---
8+
9+
# stackit_service_accounts (Data Source)
10+
11+
Service accounts plural data source schema. Returns a list of all service accounts in a project, optionally filtered.
12+
13+
## Example Usage
14+
15+
```terraform
16+
data "stackit_service_accounts" "all_sas" {
17+
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
18+
}
19+
20+
data "stackit_service_accounts" "sas_default_suffix" {
21+
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
22+
email_suffix = "@sa.stackit.cloud"
23+
}
24+
25+
data "stackit_service_accounts" "sas_default_suffix_sort_asc" {
26+
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
27+
email_suffix = "@sa.stackit.cloud"
28+
sort_ascending = true
29+
}
30+
31+
data "stackit_service_accounts" "sas_ske_regex" {
32+
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
33+
email_regex = ".*@ske\\.sa\\.stackit\\.cloud$"
34+
}
35+
36+
data "stackit_service_accounts" "sas_ske_suffix" {
37+
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
38+
email_suffix = "@ske.sa.stackit.cloud"
39+
}
40+
```
41+
42+
<!-- schema generated by tfplugindocs -->
43+
## Schema
44+
45+
### Required
46+
47+
- `project_id` (String) STACKIT project ID.
48+
49+
### Optional
50+
51+
- `email_regex` (String) Optional regular expression to filter service accounts by email.
52+
- `email_suffix` (String) Optional suffix to filter service accounts by email (e.g.,`@sa.stackit.cloud`, `@ske.sa.stackit.cloud`).
53+
- `sort_ascending` (Boolean) If set to `true`, service accounts are sorted in ascending lexicographical order by email. Defaults to `false` (descending).
54+
55+
### Read-Only
56+
57+
- `id` (String) Terraform's internal resource ID, structured as "`project_id`".
58+
- `items` (Attributes List) The list of service accounts matching the provided filters. (see [below for nested schema](#nestedatt--items))
59+
60+
<a id="nestedatt--items"></a>
61+
### Nested Schema for `items`
62+
63+
Read-Only:
64+
65+
- `email` (String) Email of the service account.
66+
- `name` (String) Name of the service account.
67+
- `service_account_id` (String) The internal UUID of the service account.

docs/resources/service_account.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,4 @@ import {
3737

3838
- `email` (String) Email of the service account.
3939
- `id` (String) Terraform's internal resource ID, structured as "`project_id`,`email`".
40+
- `service_account_id` (String) The internal UUID of the service account.

docs/resources/service_account_access_token.md

Lines changed: 0 additions & 85 deletions
This file was deleted.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
data "stackit_service_accounts" "all_sas" {
2+
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
3+
}
4+
5+
data "stackit_service_accounts" "sas_default_suffix" {
6+
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
7+
email_suffix = "@sa.stackit.cloud"
8+
}
9+
10+
data "stackit_service_accounts" "sas_default_suffix_sort_asc" {
11+
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
12+
email_suffix = "@sa.stackit.cloud"
13+
sort_ascending = true
14+
}
15+
16+
data "stackit_service_accounts" "sas_ske_regex" {
17+
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
18+
email_regex = ".*@ske\\.sa\\.stackit\\.cloud$"
19+
}
20+
21+
data "stackit_service_accounts" "sas_ske_suffix" {
22+
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
23+
email_suffix = "@ske.sa.stackit.cloud"
24+
}

stackit/internal/services/serviceaccount/account/datasource.go

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -56,15 +56,14 @@ func (r *serviceAccountDataSource) Metadata(_ context.Context, req datasource.Me
5656
// Schema defines the schema for the service account data source.
5757
func (r *serviceAccountDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
5858
descriptions := map[string]string{
59-
"id": "Terraform's internal resource ID, structured as \"`project_id`,`email`\".",
60-
"project_id": "STACKIT project ID to which the service account is associated.",
61-
"name": "Name of the service account.",
62-
"email": "Email of the service account.",
59+
"id": "Terraform's internal resource ID, structured as \"`project_id`,`email`\".",
60+
"project_id": "STACKIT project ID to which the service account is associated.",
61+
"service_account_id": "The internal UUID of the service account.",
62+
"name": "Name of the service account.",
63+
"email": "Email of the service account.",
6364
}
6465

6566
// Define the schema with validation rules and descriptions for each attribute.
66-
// The datasource schema differs slightly from the resource schema.
67-
// In this case, the email attribute is required to read the service account data from the API.
6867
resp.Schema = schema.Schema{
6968
MarkdownDescription: "Service account data source schema.",
7069
Description: "Service account data source schema.",
@@ -81,6 +80,13 @@ func (r *serviceAccountDataSource) Schema(_ context.Context, _ datasource.Schema
8180
validate.NoSeparator(),
8281
},
8382
},
83+
"service_account_id": schema.StringAttribute{
84+
Description: descriptions["service_account_id"],
85+
Computed: true,
86+
Validators: []validator.String{
87+
validate.UUID(),
88+
},
89+
},
8490
"email": schema.StringAttribute{
8591
Description: descriptions["email"],
8692
Required: true,
@@ -140,7 +146,7 @@ func (r *serviceAccountDataSource) Read(ctx context.Context, req datasource.Read
140146
}
141147

142148
// Try to parse the name from the provided email address
143-
name, err := parseNameFromEmail(model.Email.ValueString())
149+
name, err := serviceaccountUtils.ParseNameFromEmail(model.Email.ValueString())
144150
if name != "" && err == nil {
145151
model.Name = types.StringValue(name)
146152
}

stackit/internal/services/serviceaccount/account/resource.go

Lines changed: 23 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,11 @@ var (
3535

3636
// Model represents the schema for the service account resource.
3737
type Model struct {
38-
Id types.String `tfsdk:"id"` // Required by Terraform
39-
ProjectId types.String `tfsdk:"project_id"` // ProjectId associated with the service account
40-
Name types.String `tfsdk:"name"` // Name of the service account
41-
Email types.String `tfsdk:"email"` // Email linked to the service account
38+
Id types.String `tfsdk:"id"` // Required by Terraform
39+
ProjectId types.String `tfsdk:"project_id"` // ProjectId associated with the service account
40+
ServiceAccountId types.String `tfsdk:"service_account_id"` // Internal ID of the service account
41+
Name types.String `tfsdk:"name"` // Name of the service account
42+
Email types.String `tfsdk:"email"` // Email linked to the service account
4243
}
4344

4445
// NewServiceAccountResource is a helper function to create a new service account resource instance.
@@ -74,10 +75,11 @@ func (r *serviceAccountResource) Metadata(_ context.Context, req resource.Metada
7475
// Schema defines the schema for the resource.
7576
func (r *serviceAccountResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
7677
descriptions := map[string]string{
77-
"id": "Terraform's internal resource ID, structured as \"`project_id`,`email`\".",
78-
"project_id": "STACKIT project ID to which the service account is associated.",
79-
"name": "Name of the service account.",
80-
"email": "Email of the service account.",
78+
"id": "Terraform's internal resource ID, structured as \"`project_id`,`email`\".",
79+
"project_id": "STACKIT project ID to which the service account is associated.",
80+
"service_account_id": "The internal UUID of the service account.",
81+
"name": "Name of the service account.",
82+
"email": "Email of the service account.",
8183
}
8284

8385
resp.Schema = schema.Schema{
@@ -99,6 +101,13 @@ func (r *serviceAccountResource) Schema(_ context.Context, _ resource.SchemaRequ
99101
stringplanmodifier.RequiresReplace(),
100102
},
101103
},
104+
"service_account_id": schema.StringAttribute{
105+
Description: descriptions["service_account_id"],
106+
Computed: true,
107+
Validators: []validator.String{
108+
validate.UUID(),
109+
},
110+
},
102111
"name": schema.StringAttribute{
103112
Description: descriptions["name"],
104113
Required: true,
@@ -221,11 +230,6 @@ func (r *serviceAccountResource) Read(ctx context.Context, req resource.ReadRequ
221230
}
222231

223232
// Update attempts to update the resource. In this case, service accounts cannot be updated.
224-
// Note: This method is intentionally left without update logic because changes
225-
// to 'project_id' or 'name' require the resource to be entirely replaced.
226-
// As a result, the Update function is redundant since any modifications will
227-
// automatically trigger a resource recreation through Terraform's built-in
228-
// lifecycle management.
229233
func (r *serviceAccountResource) Update(ctx context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform
230234
// Service accounts cannot be updated, so we log an error.
231235
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating service account", "Service accounts can't be updated")
@@ -280,7 +284,7 @@ func (r *serviceAccountResource) ImportState(ctx context.Context, req resource.I
280284
email := idParts[1]
281285

282286
// Attempt to parse the name from the email if valid.
283-
name, err := parseNameFromEmail(email)
287+
name, err := serviceaccountUtils.ParseNameFromEmail(email)
284288
if name != "" && err == nil {
285289
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("name"), name)...)
286290
}
@@ -315,26 +319,15 @@ func mapFields(resp *serviceaccount.ServiceAccount, model *Model) error {
315319
return fmt.Errorf("service account email not present")
316320
}
317321

322+
if resp.Id == nil {
323+
return fmt.Errorf("service account id not present")
324+
}
325+
318326
// Build the ID by combining the project ID and email and assign the model's fields.
319327
model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), *resp.Email)
328+
model.ServiceAccountId = types.StringPointerValue(resp.Id)
320329
model.Email = types.StringPointerValue(resp.Email)
321330
model.ProjectId = types.StringPointerValue(resp.ProjectId)
322331

323332
return nil
324333
}
325-
326-
// parseNameFromEmail extracts the name component from an email address.
327-
// The email format must be `name-<random7characters>@sa.stackit.cloud`.
328-
func parseNameFromEmail(email string) (string, error) {
329-
namePattern := `^([a-z][a-z0-9]*(?:-[a-z0-9]+)*)-\w{7}@sa\.stackit\.cloud$`
330-
re := regexp.MustCompile(namePattern)
331-
match := re.FindStringSubmatch(email)
332-
333-
// If a match is found, return the name component
334-
if len(match) > 1 {
335-
return match[1], nil
336-
}
337-
338-
// If no match is found, return an error
339-
return "", fmt.Errorf("unable to parse name from email")
340-
}

0 commit comments

Comments
 (0)