From 646c15d7f8b1c2b126f49347569d1e93b0d9771f Mon Sep 17 00:00:00 2001 From: Mauritz Uphoff <39736813+h3adex@users.noreply.github.com> Date: Mon, 24 Mar 2025 12:24:42 +0100 Subject: [PATCH] feat/implement-sa-keys (#720) * feat: implement service account key resource --- docs/resources/service_account_key.md | 82 ++++ .../services/serviceaccount/key/const.go | 26 ++ .../services/serviceaccount/key/resource.go | 379 ++++++++++++++++++ .../serviceaccount/key/resource_test.go | 124 ++++++ .../serviceaccount/serviceaccount_acc_test.go | 14 + stackit/provider.go | 2 + 6 files changed, 627 insertions(+) create mode 100644 docs/resources/service_account_key.md create mode 100644 stackit/internal/services/serviceaccount/key/const.go create mode 100644 stackit/internal/services/serviceaccount/key/resource.go create mode 100644 stackit/internal/services/serviceaccount/key/resource_test.go diff --git a/docs/resources/service_account_key.md b/docs/resources/service_account_key.md new file mode 100644 index 00000000..6a2b3655 --- /dev/null +++ b/docs/resources/service_account_key.md @@ -0,0 +1,82 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "stackit_service_account_key Resource - stackit" +subcategory: "" +description: |- + Service account key schema. + ~> 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. + Example Usage + Automatically rotate service account keys + + resource "stackit_service_account" "sa" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + name = "sa01" + } + + resource "time_rotating" "rotate" { + rotation_days = 80 + } + + resource "stackit_service_account_key" "sa_key" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + service_account_email = stackit_service_account.sa.email + ttl_days = 90 + + rotate_when_changed = { + rotation = time_rotating.rotate.id + } + } +--- + +# stackit_service_account_key (Resource) + +Service account key schema. + +~> 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. +## Example Usage + + +### Automatically rotate service account keys +```terraform +resource "stackit_service_account" "sa" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + name = "sa01" +} + +resource "time_rotating" "rotate" { + rotation_days = 80 +} + +resource "stackit_service_account_key" "sa_key" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + service_account_email = stackit_service_account.sa.email + ttl_days = 90 + + rotate_when_changed = { + rotation = time_rotating.rotate.id + } +} + +``` + + + + +## Schema + +### Required + +- `project_id` (String) The STACKIT project ID associated with the service account key. +- `service_account_email` (String) The email address associated with the service account, used for account identification and communication. + +### Optional + +- `public_key` (String) Specifies the public_key (RSA2048 key-pair). If not provided, a certificate from STACKIT will be used to generate a private_key. +- `rotate_when_changed` (Map of String) A map of arbitrary key/value pairs designed to force key recreation when they change, facilitating key rotation based on external factors such as a changing timestamp. Modifying this map triggers the creation of a new resource. +- `ttl_days` (Number) Specifies the key's validity duration in days. If left unspecified, the key is considered valid until it is deleted + +### Read-Only + +- `id` (String) Terraform's internal resource identifier. It is structured as "`project_id`,`service_account_email`,`key_id`". +- `json` (String, Sensitive) The raw JSON representation of the service account key json, available for direct use. +- `key_id` (String) The unique identifier for the key associated with the service account. diff --git a/stackit/internal/services/serviceaccount/key/const.go b/stackit/internal/services/serviceaccount/key/const.go new file mode 100644 index 00000000..5ebeea5f --- /dev/null +++ b/stackit/internal/services/serviceaccount/key/const.go @@ -0,0 +1,26 @@ +package key + +const markdownDescription = ` +## Example Usage` + "\n" + ` + +### Automatically rotate service account keys` + "\n" + + "```terraform" + ` +resource "stackit_service_account" "sa" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + name = "sa01" +} + +resource "time_rotating" "rotate" { + rotation_days = 80 +} + +resource "stackit_service_account_key" "sa_key" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + service_account_email = stackit_service_account.sa.email + ttl_days = 90 + + rotate_when_changed = { + rotation = time_rotating.rotate.id + } +} +` + "\n```" diff --git a/stackit/internal/services/serviceaccount/key/resource.go b/stackit/internal/services/serviceaccount/key/resource.go new file mode 100644 index 00000000..5c02c158 --- /dev/null +++ b/stackit/internal/services/serviceaccount/key/resource.go @@ -0,0 +1,379 @@ +package key + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "strings" + "time" + + "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/mapplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/stackitcloud/stackit-sdk-go/core/config" + "github.com/stackitcloud/stackit-sdk-go/core/oapierror" + "github.com/stackitcloud/stackit-sdk-go/services/serviceaccount" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" +) + +// resourceBetaCheckDone is used to prevent multiple checks for beta resources. +// This is a workaround for the lack of a global state in the provider and +// needs to exist because the Configure method is called twice. +var resourceBetaCheckDone bool + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ resource.Resource = &serviceAccountKeyResource{} + _ resource.ResourceWithConfigure = &serviceAccountKeyResource{} +) + +// Model represents the schema for the service account key resource in Terraform. +type Model struct { + Id types.String `tfsdk:"id"` + KeyId types.String `tfsdk:"key_id"` + ServiceAccountEmail types.String `tfsdk:"service_account_email"` + ProjectId types.String `tfsdk:"project_id"` + RotateWhenChanged types.Map `tfsdk:"rotate_when_changed"` + TtlDays types.Int64 `tfsdk:"ttl_days"` + PublicKey types.String `tfsdk:"public_key"` + Json types.String `tfsdk:"json"` +} + +// NewServiceAccountKeyResource is a helper function to create a new service account key resource instance. +func NewServiceAccountKeyResource() resource.Resource { + return &serviceAccountKeyResource{} +} + +// serviceAccountKeyResource implements the resource interface for service account key. +type serviceAccountKeyResource struct { + client *serviceaccount.APIClient +} + +// Configure sets up the API client for the service account resource. +func (r *serviceAccountKeyResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + // Prevent potential panics if the provider is not properly configured. + if req.ProviderData == nil { + return + } + + // Validate provider data type before proceeding. + providerData, ok := req.ProviderData.(core.ProviderData) + if !ok { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData)) + return + } + + if !resourceBetaCheckDone { + features.CheckBetaResourcesEnabled(ctx, &providerData, &resp.Diagnostics, "stackit_service_account_key", "resource") + if resp.Diagnostics.HasError() { + return + } + resourceBetaCheckDone = true + } + + // Initialize the API client with the appropriate authentication and endpoint settings. + var apiClient *serviceaccount.APIClient + var err error + if providerData.ServiceAccountCustomEndpoint != "" { + apiClient, err = serviceaccount.NewAPIClient( + config.WithCustomAuth(providerData.RoundTripper), + config.WithEndpoint(providerData.ServiceAccountCustomEndpoint), + ) + } else { + apiClient, err = serviceaccount.NewAPIClient( + config.WithCustomAuth(providerData.RoundTripper), + ) + } + + // Handle API client initialization errors. + if err != nil { + 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)) + return + } + + // Store the initialized client. + r.client = apiClient + tflog.Info(ctx, "Service Account client configured") +} + +// Metadata sets the resource type name for the service account key resource. +func (r *serviceAccountKeyResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_service_account_key" +} + +// Schema defines the resource schema for the service account access key. +func (r *serviceAccountKeyResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + descriptions := map[string]string{ + "id": "Terraform's internal resource identifier. It is structured as \"`project_id`,`service_account_email`,`key_id`\".", + "main": "Service account key schema.", + "project_id": "The STACKIT project ID associated with the service account key.", + "key_id": "The unique identifier for the key associated with the service account.", + "service_account_email": "The email address associated with the service account, used for account identification and communication.", + "ttl_days": "Specifies the key's validity duration in days. If left unspecified, the key is considered valid until it is deleted", + "rotate_when_changed": "A map of arbitrary key/value pairs designed to force key recreation when they change, facilitating key rotation based on external factors such as a changing timestamp. Modifying this map triggers the creation of a new resource.", + "public_key": "Specifies the public_key (RSA2048 key-pair). If not provided, a certificate from STACKIT will be used to generate a private_key.", + "json": "The raw JSON representation of the service account key json, available for direct use.", + } + resp.Schema = schema.Schema{ + MarkdownDescription: fmt.Sprintf("%s%s", features.AddBetaDescription(descriptions["main"]), markdownDescription), + Description: descriptions["main"], + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: descriptions["id"], + Computed: true, + }, + "project_id": schema.StringAttribute{ + Description: descriptions["project_id"], + Required: true, + Validators: []validator.String{ + validate.UUID(), + }, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "service_account_email": schema.StringAttribute{ + Description: descriptions["service_account_email"], + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "public_key": schema.StringAttribute{ + Description: descriptions["public_key"], + Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "ttl_days": schema.Int64Attribute{ + Description: descriptions["ttl_days"], + Optional: true, + Validators: []validator.Int64{ + int64validator.AtLeast(1), + }, + PlanModifiers: []planmodifier.Int64{ + int64planmodifier.RequiresReplace(), + }, + }, + "rotate_when_changed": schema.MapAttribute{ + Description: descriptions["rotate_when_changed"], + Optional: true, + ElementType: types.StringType, + PlanModifiers: []planmodifier.Map{ + mapplanmodifier.RequiresReplace(), + }, + }, + "key_id": schema.StringAttribute{ + Description: descriptions["key_id"], + Computed: true, + }, + "json": schema.StringAttribute{ + Description: descriptions["json"], + Computed: true, + Sensitive: true, + }, + }, + } +} + +// Create creates the resource and sets the initial Terraform state for service accounts. +func (r *serviceAccountKeyResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform + // Retrieve the planned values for the resource. + var model Model + diags := req.Plan.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Set logging context with the project ID and service account email. + projectId := model.ProjectId.ValueString() + serviceAccountEmail := model.ServiceAccountEmail.ValueString() + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "service_account_email", serviceAccountEmail) + + if utils.IsUndefined(model.TtlDays) { + model.TtlDays = types.Int64Null() + } + + // Generate the API request payload. + payload, err := toCreatePayload(&model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating service account key", fmt.Sprintf("Creating API payload: %v", err)) + return + } + + // Initialize the API request with the required parameters. + saAccountKeyResp, err := r.client.CreateServiceAccountKey(ctx, projectId, serviceAccountEmail).CreateServiceAccountKeyPayload(*payload).Execute() + + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Failed to create service account key", fmt.Sprintf("API call error: %v", err)) + return + } + + // Map the response to the resource schema. + err = mapCreateResponse(saAccountKeyResp, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating service account key", fmt.Sprintf("Processing API payload: %v", err)) + return + } + + // Set the state with fully populated data. + diags = resp.State.Set(ctx, model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "Service account key created") +} + +// Read refreshes the Terraform state with the latest service account data. +func (r *serviceAccountKeyResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform + // Retrieve the current state of the resource. + var model Model + diags := req.State.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + projectId := model.ProjectId.ValueString() + serviceAccountEmail := model.ServiceAccountEmail.ValueString() + keyId := model.KeyId.ValueString() + + _, err := r.client.GetServiceAccountKey(ctx, projectId, serviceAccountEmail, keyId).Execute() + if err != nil { + var oapiErr *oapierror.GenericOpenAPIError + ok := errors.As(err, &oapiErr) + // due to security purposes, attempting to get access key for a non-existent Service Account will return 403. + if ok && oapiErr.StatusCode == http.StatusNotFound || oapiErr.StatusCode == http.StatusForbidden || oapiErr.StatusCode == http.StatusBadRequest { + resp.State.RemoveResource(ctx) + return + } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading service account key", fmt.Sprintf("Calling API: %v", err)) + return + } + + // No mapping needed for read response, as private_key is excluded and ID remains unchanged. + diags = resp.State.Set(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "key read") +} + +// Update attempts to update the resource. In this case, service account key cannot be updated. +// Note: This method is intentionally left without update logic because changes +// to 'project_id', 'service_account_email', 'ttl_days' or 'public_key' require the resource to be entirely replaced. +// As a result, the Update function is redundant since any modifications will +// automatically trigger a resource recreation through Terraform's built-in +// lifecycle management. +func (r *serviceAccountKeyResource) Update(ctx context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform + // Service accounts cannot be updated, so we log an error. + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating service account key", "Service account key can't be updated") +} + +// Delete deletes the service account key and removes it from the Terraform state on success. +func (r *serviceAccountKeyResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform + // Retrieve current state of the resource. + var model Model + diags := req.State.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + projectId := model.ProjectId.ValueString() + serviceAccountEmail := model.ServiceAccountEmail.ValueString() + keyId := model.KeyId.ValueString() + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "service_account_email", serviceAccountEmail) + ctx = tflog.SetField(ctx, "key_id", keyId) + + // Call API to delete the existing service account key. + err := r.client.DeleteServiceAccountKey(ctx, projectId, serviceAccountEmail, keyId).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting service account key", fmt.Sprintf("Calling API: %v", err)) + return + } + + tflog.Info(ctx, "Service account key deleted") +} + +func toCreatePayload(model *Model) (*serviceaccount.CreateServiceAccountKeyPayload, error) { + if model == nil { + return nil, fmt.Errorf("model is nil") + } + + // Prepare the payload + payload := &serviceaccount.CreateServiceAccountKeyPayload{} + + // Set ValidUntil based on TtlDays if specified + if !utils.IsUndefined(model.TtlDays) { + validUntil, err := computeValidUntil(model.TtlDays.ValueInt64Pointer()) + if err != nil { + return nil, err + } + payload.ValidUntil = &validUntil + } + + // Set PublicKey if specified + if !utils.IsUndefined(model.PublicKey) && model.PublicKey.ValueString() != "" { + payload.PublicKey = conversion.StringValueToPointer(model.PublicKey) + } + + return payload, nil +} + +// computeValidUntil calculates the timestamp for when the item will no longer be valid. +func computeValidUntil(ttlDays *int64) (time.Time, error) { + if ttlDays == nil { + return time.Time{}, fmt.Errorf("ttlDays is nil") + } + return time.Now().UTC().Add(time.Duration(*ttlDays) * 24 * time.Hour), nil +} + +// mapCreateResponse maps response data from a create operation to the model. +func mapCreateResponse(resp *serviceaccount.CreateServiceAccountKeyResponse, model *Model) error { + if model == nil { + return fmt.Errorf("model input is nil") + } + + if resp == nil { + return fmt.Errorf("service account key response is nil") + } + + if resp.Id == nil { + return fmt.Errorf("service account key id not present") + } + + idParts := []string{model.ProjectId.ValueString(), model.ServiceAccountEmail.ValueString(), *resp.Id} + model.Id = types.StringValue(strings.Join(idParts, core.Separator)) + model.KeyId = types.StringPointerValue(resp.Id) + + jsonData, err := json.Marshal(resp) + if err != nil { + return fmt.Errorf("JSON encoding error: %w", err) + } + + if jsonData != nil { + model.Json = types.StringValue(string(jsonData)) + } + + return nil +} diff --git a/stackit/internal/services/serviceaccount/key/resource_test.go b/stackit/internal/services/serviceaccount/key/resource_test.go new file mode 100644 index 00000000..15f90a1b --- /dev/null +++ b/stackit/internal/services/serviceaccount/key/resource_test.go @@ -0,0 +1,124 @@ +package key + +import ( + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/stackitcloud/stackit-sdk-go/core/utils" + "github.com/stackitcloud/stackit-sdk-go/services/serviceaccount" +) + +func TestComputeValidUntil(t *testing.T) { + tests := []struct { + name string + ttlDays *int + isValid bool + expected time.Time + }{ + { + name: "ttlDays is 10", + ttlDays: utils.Ptr(10), + isValid: true, + expected: time.Now().UTC().Add(time.Duration(10) * 24 * time.Hour), + }, + { + name: "ttlDays is 0", + ttlDays: utils.Ptr(0), + isValid: true, + expected: time.Now().UTC().Add(time.Duration(0) * 24 * time.Hour), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + int64TTlDays := int64(*tt.ttlDays) + validUntil, err := computeValidUntil(&int64TTlDays) + if !tt.isValid && err == nil { + t.Fatalf("Should have failed") + } + if tt.isValid && err != nil { + t.Fatalf("Should not have failed: %v", err) + } + if tt.isValid { + tolerance := 1 * time.Second + if validUntil.Sub(tt.expected) > tolerance && tt.expected.Sub(validUntil) > tolerance { + t.Fatalf("Times do not match. got: %v expected: %v", validUntil, tt.expected) + } + } + }) + } +} + +func TestMapResponse(t *testing.T) { + tests := []struct { + description string + input *serviceaccount.CreateServiceAccountKeyResponse + expected Model + isValid bool + }{ + { + description: "default_values", + input: &serviceaccount.CreateServiceAccountKeyResponse{ + Id: utils.Ptr("id"), + }, + expected: Model{ + Id: types.StringValue("pid,email,id"), + KeyId: types.StringValue("id"), + ProjectId: types.StringValue("pid"), + ServiceAccountEmail: types.StringValue("email"), + Json: types.StringValue("{}"), + RotateWhenChanged: types.MapValueMust(types.StringType, map[string]attr.Value{}), + }, + isValid: true, + }, + { + description: "nil_response", + input: nil, + expected: Model{}, + isValid: false, + }, + { + description: "nil_response_2", + input: &serviceaccount.CreateServiceAccountKeyResponse{}, + expected: Model{}, + isValid: false, + }, + { + description: "no_id", + input: &serviceaccount.CreateServiceAccountKeyResponse{ + Active: utils.Ptr(true), + }, + expected: Model{}, + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + model := &Model{ + ProjectId: tt.expected.ProjectId, + ServiceAccountEmail: tt.expected.ServiceAccountEmail, + KeyId: types.StringNull(), + Json: types.StringValue("{}"), + RotateWhenChanged: types.MapValueMust(types.StringType, map[string]attr.Value{}), + } + err := mapCreateResponse(tt.input, model) + if !tt.isValid && err == nil { + t.Fatalf("Should have failed") + } + if tt.isValid && err != nil { + t.Fatalf("Should not have failed: %v", err) + } + if tt.isValid { + model.Json = types.StringValue("{}") + diff := cmp.Diff(*model, tt.expected) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + } + }) + } +} diff --git a/stackit/internal/services/serviceaccount/serviceaccount_acc_test.go b/stackit/internal/services/serviceaccount/serviceaccount_acc_test.go index 266f3e7d..032dae78 100644 --- a/stackit/internal/services/serviceaccount/serviceaccount_acc_test.go +++ b/stackit/internal/services/serviceaccount/serviceaccount_acc_test.go @@ -35,6 +35,12 @@ func inputServiceAccountResourceConfig(name string) string { project_id = stackit_service_account.sa.project_id service_account_email = stackit_service_account.sa.email } + + resource "stackit_service_account_key" "key" { + project_id = stackit_service_account.sa.project_id + service_account_email = stackit_service_account.sa.email + ttl_days = 90 + } `, testutil.ServiceAccountProviderConfig(), serviceAccountResource["project_id"], @@ -71,7 +77,11 @@ func TestServiceAccount(t *testing.T) { resource.TestCheckResourceAttrSet("stackit_service_account_access_token.token", "created_at"), resource.TestCheckResourceAttrSet("stackit_service_account_access_token.token", "valid_until"), resource.TestCheckResourceAttrSet("stackit_service_account_access_token.token", "service_account_email"), + resource.TestCheckResourceAttrSet("stackit_service_account_key.key", "ttl_days"), + resource.TestCheckResourceAttrSet("stackit_service_account_key.key", "json"), + resource.TestCheckResourceAttrSet("stackit_service_account_key.key", "service_account_email"), resource.TestCheckResourceAttrPair("stackit_service_account.sa", "email", "stackit_service_account_access_token.token", "service_account_email"), + resource.TestCheckResourceAttrPair("stackit_service_account.sa", "email", "stackit_service_account_key.key", "service_account_email"), ), }, // Update @@ -85,7 +95,11 @@ func TestServiceAccount(t *testing.T) { resource.TestCheckResourceAttrSet("stackit_service_account_access_token.token", "created_at"), resource.TestCheckResourceAttrSet("stackit_service_account_access_token.token", "valid_until"), resource.TestCheckResourceAttrSet("stackit_service_account_access_token.token", "service_account_email"), + resource.TestCheckResourceAttrSet("stackit_service_account_key.key", "ttl_days"), + resource.TestCheckResourceAttrSet("stackit_service_account_key.key", "json"), + resource.TestCheckResourceAttrSet("stackit_service_account_key.key", "service_account_email"), resource.TestCheckResourceAttrPair("stackit_service_account.sa", "email", "stackit_service_account_access_token.token", "service_account_email"), + resource.TestCheckResourceAttrPair("stackit_service_account.sa", "email", "stackit_service_account_key.key", "service_account_email"), ), }, // Data source diff --git a/stackit/provider.go b/stackit/provider.go index b2ea3a0a..c15ee818 100644 --- a/stackit/provider.go +++ b/stackit/provider.go @@ -66,6 +66,7 @@ import ( serverBackupSchedule "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/serverbackup/schedule" serverUpdateSchedule "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/serverupdate/schedule" serviceAccount "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/serviceaccount/account" + serviceAccountKey "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/serviceaccount/key" serviceAccountToken "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/serviceaccount/token" skeCluster "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/ske/cluster" skeKubeconfig "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/ske/kubeconfig" @@ -566,6 +567,7 @@ func (p *Provider) Resources(_ context.Context) []func() resource.Resource { serverUpdateSchedule.NewScheduleResource, serviceAccount.NewServiceAccountResource, serviceAccountToken.NewServiceAccountTokenResource, + serviceAccountKey.NewServiceAccountKeyResource, skeProject.NewProjectResource, skeCluster.NewClusterResource, skeKubeconfig.NewKubeconfigResource,