Skip to content

Commit 23e9a25

Browse files
authored
feat: add stackit service account creation to tf provider (#708)
* feat: implement service account resource/datasource
1 parent 6dc6f41 commit 23e9a25

File tree

14 files changed

+978
-8
lines changed

14 files changed

+978
-8
lines changed
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
---
2+
# generated by https://github.com/hashicorp/terraform-plugin-docs
3+
page_title: "stackit_service_account Data Source - stackit"
4+
subcategory: ""
5+
description: |-
6+
Service account data source schema.
7+
~> This resource is in beta and may be subject to breaking changes in the future. Use with caution. See our guide https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/guides/opting_into_beta_resources for how to opt-in to use beta resources.
8+
---
9+
10+
# stackit_service_account (Data Source)
11+
12+
Service account data source schema.
13+
14+
~> This resource is in beta and may be subject to breaking changes in the future. Use with caution. See our [guide](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/guides/opting_into_beta_resources) for how to opt-in to use beta resources.
15+
16+
## Example Usage
17+
18+
```terraform
19+
data "stackit_service_account" "sa" {
20+
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
21+
email = "sa01-8565oq1@sa.stackit.cloud"
22+
}
23+
```
24+
25+
<!-- schema generated by tfplugindocs -->
26+
## Schema
27+
28+
### Required
29+
30+
- `email` (String) Email of the service account.
31+
- `project_id` (String) STACKIT project ID to which the service account is associated.
32+
33+
### Read-Only
34+
35+
- `id` (String) Terraform's internal resource ID, structured as "`project_id`,`email`".
36+
- `name` (String) Name of the service account.

docs/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@ Note: AWS specific checks must be skipped as they do not work on STACKIT. For de
176176
- `secretsmanager_custom_endpoint` (String) Custom endpoint for the Secrets Manager service
177177
- `server_backup_custom_endpoint` (String) Custom endpoint for the Server Backup service
178178
- `server_update_custom_endpoint` (String) Custom endpoint for the Server Update service
179+
- `service_account_custom_endpoint` (String) Custom endpoint for the Service Account service
179180
- `service_account_email` (String, Deprecated) Service account email. It can also be set using the environment variable STACKIT_SERVICE_ACCOUNT_EMAIL. It is required if you want to use the resource manager project resource.
180181
- `service_account_key` (String) Service account key used for authentication. If set, the key flow will be used to authenticate all operations.
181182
- `service_account_key_path` (String) Path for the service account key used for authentication. If set, the key flow will be used to authenticate all operations.

docs/resources/service_account.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
---
2+
# generated by https://github.com/hashicorp/terraform-plugin-docs
3+
page_title: "stackit_service_account Resource - stackit"
4+
subcategory: ""
5+
description: |-
6+
Service account resource schema.
7+
~> This resource is in beta and may be subject to breaking changes in the future. Use with caution. See our guide https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/guides/opting_into_beta_resources for how to opt-in to use beta resources.
8+
---
9+
10+
# stackit_service_account (Resource)
11+
12+
Service account resource schema.
13+
14+
~> This resource is in beta and may be subject to breaking changes in the future. Use with caution. See our [guide](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/guides/opting_into_beta_resources) for how to opt-in to use beta resources.
15+
16+
## Example Usage
17+
18+
```terraform
19+
resource "stackit_service_account" "sa" {
20+
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
21+
name = "sa01"
22+
}
23+
```
24+
25+
<!-- schema generated by tfplugindocs -->
26+
## Schema
27+
28+
### Required
29+
30+
- `name` (String) Name of the service account.
31+
- `project_id` (String) STACKIT project ID to which the service account is associated.
32+
33+
### Read-Only
34+
35+
- `email` (String) Email of the service account.
36+
- `id` (String) Terraform's internal resource ID, structured as "`project_id`,`email`".
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
data "stackit_service_account" "sa" {
2+
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
3+
email = "sa01-8565oq1@sa.stackit.cloud"
4+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
resource "stackit_service_account" "sa" {
2+
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
3+
name = "sa01"
4+
}

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ require (
2929
github.com/stackitcloud/stackit-sdk-go/services/secretsmanager v0.11.0
3030
github.com/stackitcloud/stackit-sdk-go/services/serverbackup v0.6.0
3131
github.com/stackitcloud/stackit-sdk-go/services/serverupdate v0.5.0
32+
github.com/stackitcloud/stackit-sdk-go/services/serviceaccount v0.6.0
3233
github.com/stackitcloud/stackit-sdk-go/services/serviceenablement v0.5.0
3334
github.com/stackitcloud/stackit-sdk-go/services/ske v0.22.0
3435
github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex v1.0.0

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,8 @@ github.com/stackitcloud/stackit-sdk-go/services/serverbackup v0.6.0 h1:cESGAkm0f
189189
github.com/stackitcloud/stackit-sdk-go/services/serverbackup v0.6.0/go.mod h1:aYPLsiImzWaYXEfYIZ0wJnV56PwcR+buy8Xu9jjbfGA=
190190
github.com/stackitcloud/stackit-sdk-go/services/serverupdate v0.5.0 h1:TMUxDh8XGgWUpnWo7GsawVq2ICDsy/r8dMlfC26MR5g=
191191
github.com/stackitcloud/stackit-sdk-go/services/serverupdate v0.5.0/go.mod h1:giHnHz3kHeLY8Av9MZLsyJlaTXYz+BuGqdP/SKB5Vo0=
192+
github.com/stackitcloud/stackit-sdk-go/services/serviceaccount v0.6.0 h1:y+XzJcntHJ7M+IWWvAUkiVFA8op+jZxwHs3ktW2aLoA=
193+
github.com/stackitcloud/stackit-sdk-go/services/serviceaccount v0.6.0/go.mod h1:J/Wa67cbDI1wAyxib9PiEbNqGfIoFUH+DSLueVazQx8=
192194
github.com/stackitcloud/stackit-sdk-go/services/serviceenablement v0.5.0 h1:QG+rGBHsyXOlJ3ZIeOgExGqu9PoTlGY1rltW/VpG6lw=
193195
github.com/stackitcloud/stackit-sdk-go/services/serviceenablement v0.5.0/go.mod h1:16dOVT052cMuHhUJ3NIcPuY7TrpCr9QlxmvvfjLZubA=
194196
github.com/stackitcloud/stackit-sdk-go/services/ske v0.22.0 h1:3KUVls8zXsbT2tOYRSHyp3/l0Kpjl4f3INmQKYTe65Y=

stackit/internal/core/core.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ type ProviderData struct {
4040
ServerUpdateCustomEndpoint string
4141
SKECustomEndpoint string
4242
ServiceEnablementCustomEndpoint string
43+
ServiceAccountCustomEndpoint string
4344
EnableBetaResources bool
4445
Experiments []string
4546
}
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
package account
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/hashicorp/terraform-plugin-framework/datasource"
8+
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
9+
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
10+
"github.com/hashicorp/terraform-plugin-framework/types"
11+
"github.com/hashicorp/terraform-plugin-log/tflog"
12+
"github.com/stackitcloud/stackit-sdk-go/core/config"
13+
"github.com/stackitcloud/stackit-sdk-go/services/serviceaccount"
14+
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
15+
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features"
16+
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate"
17+
)
18+
19+
// dataSourceBetaCheckDone is used to prevent multiple checks for beta resources.
20+
// This is a workaround for the lack of a global state in the provider and
21+
// needs to exist because the Configure method is called twice.
22+
var dataSourceBetaCheckDone bool
23+
24+
// Ensure the implementation satisfies the expected interfaces.
25+
var (
26+
_ datasource.DataSource = &serviceAccountDataSource{}
27+
)
28+
29+
// NewServiceAccountDataSource creates a new instance of the serviceAccountDataSource.
30+
func NewServiceAccountDataSource() datasource.DataSource {
31+
return &serviceAccountDataSource{}
32+
}
33+
34+
// serviceAccountDataSource is the datasource implementation for service accounts.
35+
type serviceAccountDataSource struct {
36+
client *serviceaccount.APIClient
37+
}
38+
39+
// Configure initializes the serviceAccountDataSource with the provided provider data.
40+
func (r *serviceAccountDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
41+
// Prevent panic if the provider has not been configured correctly.
42+
if req.ProviderData == nil {
43+
return
44+
}
45+
46+
providerData, ok := req.ProviderData.(core.ProviderData)
47+
if !ok {
48+
core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData))
49+
return
50+
}
51+
52+
if !dataSourceBetaCheckDone {
53+
features.CheckBetaResourcesEnabled(ctx, &providerData, &resp.Diagnostics, "stackit_service_account", "datasource")
54+
if resp.Diagnostics.HasError() {
55+
return
56+
}
57+
dataSourceBetaCheckDone = true
58+
}
59+
60+
var apiClient *serviceaccount.APIClient
61+
var err error
62+
if providerData.ServiceAccountCustomEndpoint != "" {
63+
apiClient, err = serviceaccount.NewAPIClient(
64+
config.WithCustomAuth(providerData.RoundTripper),
65+
config.WithEndpoint(providerData.ServiceAccountCustomEndpoint),
66+
)
67+
} else {
68+
apiClient, err = serviceaccount.NewAPIClient(
69+
config.WithCustomAuth(providerData.RoundTripper),
70+
)
71+
}
72+
73+
if err != nil {
74+
core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the resource configuration", err))
75+
return
76+
}
77+
78+
r.client = apiClient
79+
tflog.Info(ctx, "Service Account client configured")
80+
}
81+
82+
// Metadata provides metadata for the service account datasource.
83+
func (r *serviceAccountDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
84+
resp.TypeName = req.ProviderTypeName + "_service_account"
85+
}
86+
87+
// Schema defines the schema for the service account data source.
88+
func (r *serviceAccountDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
89+
descriptions := map[string]string{
90+
"id": "Terraform's internal resource ID, structured as \"`project_id`,`email`\".",
91+
"project_id": "STACKIT project ID to which the service account is associated.",
92+
"name": "Name of the service account.",
93+
"email": "Email of the service account.",
94+
}
95+
96+
// Define the schema with validation rules and descriptions for each attribute.
97+
// The datasource schema differs slightly from the resource schema.
98+
// In this case, the email attribute is required to read the service account data from the API.
99+
resp.Schema = schema.Schema{
100+
MarkdownDescription: features.AddBetaDescription("Service account data source schema."),
101+
Description: "Service account data source schema.",
102+
Attributes: map[string]schema.Attribute{
103+
"id": schema.StringAttribute{
104+
Description: descriptions["id"],
105+
Computed: true,
106+
},
107+
"project_id": schema.StringAttribute{
108+
Description: descriptions["project_id"],
109+
Required: true,
110+
Validators: []validator.String{
111+
validate.UUID(),
112+
validate.NoSeparator(),
113+
},
114+
},
115+
"email": schema.StringAttribute{
116+
Description: descriptions["email"],
117+
Required: true,
118+
},
119+
"name": schema.StringAttribute{
120+
Description: descriptions["name"],
121+
Computed: true,
122+
},
123+
},
124+
}
125+
}
126+
127+
// Read reads all service accounts from the API and updates the state with the latest information.
128+
func (r *serviceAccountDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform
129+
var model Model
130+
diags := req.Config.Get(ctx, &model)
131+
resp.Diagnostics.Append(diags...)
132+
if resp.Diagnostics.HasError() {
133+
return
134+
}
135+
136+
// Extract the project ID from the model configuration
137+
projectId := model.ProjectId.ValueString()
138+
139+
// Call the API to list service accounts in the specified project
140+
listSaResp, err := r.client.ListServiceAccounts(ctx, projectId).Execute()
141+
if err != nil {
142+
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading service account", fmt.Sprintf("Error calling API: %v", err))
143+
return
144+
}
145+
146+
// Iterate over the service accounts returned by the API to find the one matching the email
147+
serviceAccounts := *listSaResp.Items
148+
for i := range serviceAccounts {
149+
// Skip if the service account email does not match
150+
if *serviceAccounts[i].Email != model.Email.ValueString() {
151+
continue
152+
}
153+
154+
// Map the API response to the model, updating its fields with the service account data
155+
err = mapFields(&serviceAccounts[i], &model)
156+
if err != nil {
157+
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading service account", fmt.Sprintf("Error processing API response: %v", err))
158+
return
159+
}
160+
161+
// Try to parse the name from the provided email address
162+
name, err := parseNameFromEmail(model.Email.ValueString())
163+
if name != "" && err == nil {
164+
model.Name = types.StringValue(name)
165+
}
166+
167+
// Update the state with the service account model
168+
diags = resp.State.Set(ctx, &model)
169+
resp.Diagnostics.Append(diags...)
170+
return
171+
}
172+
173+
// If no matching service account is found, remove the resource from the state
174+
core.LogAndAddError(ctx, &resp.Diagnostics, "Service account not found", "")
175+
resp.State.RemoveResource(ctx)
176+
}

0 commit comments

Comments
 (0)