From 0763a5f01f95049b6012ff26e5219eb407b3001f Mon Sep 17 00:00:00 2001 From: Maximilian Bischoff Date: Mon, 6 Oct 2025 10:52:53 +0200 Subject: [PATCH] feat(ske): add refresh_before field to ske_kubeconfig resource (#1000) --- docs/resources/ske_kubeconfig.md | 6 +++- .../stackit_ske_kubeconfig/resource.tf | 5 ++- .../services/ske/kubeconfig/resource.go | 36 +++++++++++++------ .../services/ske/kubeconfig/resource_test.go | 28 ++++++++++----- stackit/internal/services/ske/ske_acc_test.go | 3 ++ .../services/ske/testdata/resource-max.tf | 10 +++--- 6 files changed, 64 insertions(+), 24 deletions(-) diff --git a/docs/resources/ske_kubeconfig.md b/docs/resources/ske_kubeconfig.md index 10d15401..ff890a8f 100644 --- a/docs/resources/ske_kubeconfig.md +++ b/docs/resources/ske_kubeconfig.md @@ -16,7 +16,10 @@ SKE kubeconfig resource schema. Must have a `region` specified in the provider c resource "stackit_ske_kubeconfig" "example" { project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" cluster_name = "example-cluster" - refresh = true + + refresh = true + expiration = 7200 # 2 hours + refresh_before = 3600 # 1 hour } ``` @@ -32,6 +35,7 @@ resource "stackit_ske_kubeconfig" "example" { - `expiration` (Number) Expiration time of the kubeconfig, in seconds. Defaults to `3600` - `refresh` (Boolean) If set to true, the provider will check if the kubeconfig has expired and will generated a new valid one in-place +- `refresh_before` (Number) Number of seconds before expiration to trigger refresh of the kubeconfig at. Only used if refresh is set to true. - `region` (String) The resource region. If not defined, the provider region is used. ### Read-Only diff --git a/examples/resources/stackit_ske_kubeconfig/resource.tf b/examples/resources/stackit_ske_kubeconfig/resource.tf index 927f564f..a71b59aa 100644 --- a/examples/resources/stackit_ske_kubeconfig/resource.tf +++ b/examples/resources/stackit_ske_kubeconfig/resource.tf @@ -1,5 +1,8 @@ resource "stackit_ske_kubeconfig" "example" { project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" cluster_name = "example-cluster" - refresh = true + + refresh = true + expiration = 7200 # 2 hours + refresh_before = 3600 # 1 hour } diff --git a/stackit/internal/services/ske/kubeconfig/resource.go b/stackit/internal/services/ske/kubeconfig/resource.go index 1ddb6b98..c4637b76 100644 --- a/stackit/internal/services/ske/kubeconfig/resource.go +++ b/stackit/internal/services/ske/kubeconfig/resource.go @@ -10,6 +10,7 @@ import ( skeUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/ske/utils" "github.com/google/uuid" + "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-log/tflog" @@ -38,16 +39,17 @@ var ( ) type Model struct { - Id types.String `tfsdk:"id"` // needed by TF - ClusterName types.String `tfsdk:"cluster_name"` - ProjectId types.String `tfsdk:"project_id"` - KubeconfigId types.String `tfsdk:"kube_config_id"` // uuid generated internally because kubeconfig has no identifier - Kubeconfig types.String `tfsdk:"kube_config"` - Expiration types.Int64 `tfsdk:"expiration"` - Refresh types.Bool `tfsdk:"refresh"` - ExpiresAt types.String `tfsdk:"expires_at"` - CreationTime types.String `tfsdk:"creation_time"` - Region types.String `tfsdk:"region"` + Id types.String `tfsdk:"id"` // needed by TF + ClusterName types.String `tfsdk:"cluster_name"` + ProjectId types.String `tfsdk:"project_id"` + KubeconfigId types.String `tfsdk:"kube_config_id"` // uuid generated internally because kubeconfig has no identifier + Kubeconfig types.String `tfsdk:"kube_config"` + Expiration types.Int64 `tfsdk:"expiration"` + Refresh types.Bool `tfsdk:"refresh"` + RefreshBefore types.Int64 `tfsdk:"refresh_before"` + ExpiresAt types.String `tfsdk:"expires_at"` + CreationTime types.String `tfsdk:"creation_time"` + Region types.String `tfsdk:"region"` } // NewKubeconfigResource is a helper function to simplify the provider implementation. @@ -94,6 +96,7 @@ func (r *kubeconfigResource) Schema(_ context.Context, _ resource.SchemaRequest, "expiration": "Expiration time of the kubeconfig, in seconds. Defaults to `3600`", "expires_at": "Timestamp when the kubeconfig expires", "refresh": "If set to true, the provider will check if the kubeconfig has expired and will generated a new valid one in-place", + "refresh_before": "Number of seconds before expiration to trigger refresh of the kubeconfig at. Only used if refresh is set to true.", "creation_time": "Date-time when the kubeconfig was created", "region": "The resource region. If not defined, the provider region is used.", } @@ -155,6 +158,16 @@ func (r *kubeconfigResource) Schema(_ context.Context, _ resource.SchemaRequest, boolplanmodifier.RequiresReplace(), }, }, + "refresh_before": schema.Int64Attribute{ + Description: descriptions["refresh_before"], + Optional: true, + PlanModifiers: []planmodifier.Int64{ + int64planmodifier.UseStateForUnknown(), + }, + Validators: []validator.Int64{ + int64validator.AtLeast(1), + }, + }, "kube_config": schema.StringAttribute{ Description: descriptions["kube_config"], Computed: true, @@ -442,6 +455,9 @@ func checkHasExpired(model *Model, currentTime time.Time) (bool, error) { if err != nil { return false, fmt.Errorf("converting expiresAt field to timestamp: %w", err) } + if !model.RefreshBefore.IsNull() { + expiresAt = expiresAt.Add(-time.Duration(model.RefreshBefore.ValueInt64()) * time.Second) + } if expiresAt.Before(currentTime) { return true, nil } diff --git a/stackit/internal/services/ske/kubeconfig/resource_test.go b/stackit/internal/services/ske/kubeconfig/resource_test.go index 09149d57..f2a7fac8 100644 --- a/stackit/internal/services/ske/kubeconfig/resource_test.go +++ b/stackit/internal/services/ske/kubeconfig/resource_test.go @@ -26,14 +26,15 @@ func TestMapFields(t *testing.T) { Kubeconfig: utils.Ptr("kubeconfig"), }, Model{ - ClusterName: types.StringValue("name"), - ProjectId: types.StringValue("pid"), - Kubeconfig: types.StringValue("kubeconfig"), - Expiration: types.Int64Null(), - Refresh: types.BoolNull(), - ExpiresAt: types.StringValue("2024-02-07T16:42:12Z"), - CreationTime: types.StringValue("2024-02-05T14:40:12Z"), - Region: types.StringValue(testRegion), + ClusterName: types.StringValue("name"), + ProjectId: types.StringValue("pid"), + Kubeconfig: types.StringValue("kubeconfig"), + Expiration: types.Int64Null(), + Refresh: types.BoolNull(), + RefreshBefore: types.Int64Null(), + ExpiresAt: types.StringValue("2024-02-07T16:42:12Z"), + CreationTime: types.StringValue("2024-02-05T14:40:12Z"), + Region: types.StringValue(testRegion), }, true, }, @@ -169,6 +170,17 @@ func TestCheckHasExpired(t *testing.T) { expected: false, expectedError: false, }, + { + description: "not expired but refresh_before reached", + inputModel: &Model{ + Refresh: types.BoolValue(true), + RefreshBefore: types.Int64Value(int64(time.Hour.Seconds())), + ExpiresAt: types.StringValue(time.Now().Add(1 * time.Hour).Format(time.RFC3339)), // in one hour + }, + currentTime: time.Now(), + expected: true, + expectedError: false, + }, { description: "invalid time", inputModel: &Model{ diff --git a/stackit/internal/services/ske/ske_acc_test.go b/stackit/internal/services/ske/ske_acc_test.go index c340d9b0..b5a2d179 100644 --- a/stackit/internal/services/ske/ske_acc_test.go +++ b/stackit/internal/services/ske/ske_acc_test.go @@ -86,6 +86,7 @@ var testConfigVarsMax = config.Variables{ "region": config.StringVariable(testutil.Region), "expiration": config.StringVariable("3600"), "refresh": config.StringVariable("true"), + "refresh_before": config.StringVariable("600"), "dns_zone_name": config.StringVariable("acc-" + acctest.RandStringFromCharSet(6, acctest.CharSetAlpha)), "dns_name": config.StringVariable("acc-" + acctest.RandStringFromCharSet(6, acctest.CharSetAlpha) + ".runs.onstackit.cloud"), } @@ -301,6 +302,8 @@ func TestAccSKEMax(t *testing.T) { "stackit_ske_cluster.cluster", "name", ), resource.TestCheckResourceAttr("stackit_ske_kubeconfig.kubeconfig", "expiration", testutil.ConvertConfigVariable(testConfigVarsMax["expiration"])), + resource.TestCheckResourceAttr("stackit_ske_kubeconfig.kubeconfig", "refresh", testutil.ConvertConfigVariable(testConfigVarsMax["refresh"])), + resource.TestCheckResourceAttr("stackit_ske_kubeconfig.kubeconfig", "refresh_before", testutil.ConvertConfigVariable(testConfigVarsMax["refresh_before"])), resource.TestCheckResourceAttrSet("stackit_ske_kubeconfig.kubeconfig", "expires_at"), ), }, diff --git a/stackit/internal/services/ske/testdata/resource-max.tf b/stackit/internal/services/ske/testdata/resource-max.tf index 1bda783d..192c9138 100644 --- a/stackit/internal/services/ske/testdata/resource-max.tf +++ b/stackit/internal/services/ske/testdata/resource-max.tf @@ -32,6 +32,7 @@ variable "maintenance_end" {} variable "region" {} variable "expiration" {} variable "refresh" {} +variable "refresh_before" {} variable "dns_zone_name" {} variable "dns_name" {} @@ -94,10 +95,11 @@ resource "stackit_ske_cluster" "cluster" { } resource "stackit_ske_kubeconfig" "kubeconfig" { - project_id = stackit_ske_cluster.cluster.project_id - cluster_name = stackit_ske_cluster.cluster.name - expiration = var.expiration - refresh = var.refresh + project_id = stackit_ske_cluster.cluster.project_id + cluster_name = stackit_ske_cluster.cluster.name + expiration = var.expiration + refresh = var.refresh + refresh_before = var.refresh_before } data "stackit_ske_cluster" "cluster" {