Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update kubeconfig when invalid #627

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/resources/ske_kubeconfig.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ resource "stackit_ske_kubeconfig" "example" {

### Read-Only

- `creation_time` (String) Date-time when the kubeconfig was created
- `expires_at` (String) Timestamp when the kubeconfig expires
- `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`cluster_name`,`kube_config_id`".
- `kube_config` (String, Sensitive) Raw short-lived admin kubeconfig.
Expand Down
86 changes: 72 additions & 14 deletions stackit/internal/services/ske/kubeconfig/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ type Model struct {
Expiration types.Int64 `tfsdk:"expiration"`
Refresh types.Bool `tfsdk:"refresh"`
ExpiresAt types.String `tfsdk:"expires_at"`
CreationTime types.String `tfsdk:"creation_time"`
}

// NewKubeconfigResource is a helper function to simplify the provider implementation.
Expand Down Expand Up @@ -108,6 +109,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",
"creation_time": "Date-time when the kubeconfig was created",
}

resp.Schema = schema.Schema{
Expand Down Expand Up @@ -182,6 +184,13 @@ func (r *kubeconfigResource) Schema(_ context.Context, _ resource.SchemaRequest,
stringplanmodifier.UseStateForUnknown(),
},
},
"creation_time": schema.StringAttribute{
Description: descriptions["creation_time"],
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not to sure here. Wouldn't RequiresReplace force a recreation of the cluster, when the creation time has been updated? I have'nt tested it though

},
},
},
}
}
Expand Down Expand Up @@ -230,6 +239,8 @@ func (r *kubeconfigResource) Create(ctx context.Context, req resource.CreateRequ
// Read refreshes the Terraform state with the latest data.
// There is no GET kubeconfig endpoint.
// If the refresh field is set, Read will check the expiration date and will get a new valid kubeconfig if it has expired
// If kubeconfig creation time is before lastCompletionTime of the credentials rotation or
// before cluster creation time a new kubeconfig is created.
func (r *kubeconfigResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform
// Retrieve values from plan
var model Model
Expand All @@ -246,6 +257,21 @@ func (r *kubeconfigResource) Read(ctx context.Context, req resource.ReadRequest,
ctx = tflog.SetField(ctx, "cluster_name", clusterName)
ctx = tflog.SetField(ctx, "kube_config_id", kubeconfigUUID)

cluster, err := r.client.GetClusterExecute(ctx, projectId, clusterName)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would recommend to extract helper functions for this and the following block to keep this function a little bit shorter (and facilitate testing).

if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading kubeconfig", fmt.Sprintf("Could not get cluster(%s): %v", clusterName, err))
return
}

creationTime, err := time.Parse("2006-01-02T15:04:05Z07:00", model.CreationTime.ValueString())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the pattern is the same as time.RFC3339, we should use that one

if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading kubeconfig", fmt.Sprintf("Converting creationTime field to timestamp: %v", err))
return
}

recreateKubeconfig := false

// check if kubeconfig has expired
if model.Refresh.ValueBool() && !model.ExpiresAt.IsNull() {
expiresAt, err := time.Parse("2006-01-02T15:04:05Z07:00", model.ExpiresAt.ValueString())
if err != nil {
Expand All @@ -254,18 +280,48 @@ func (r *kubeconfigResource) Read(ctx context.Context, req resource.ReadRequest,
}
currentTime := time.Now()
if expiresAt.Before(currentTime) {
err := r.createKubeconfig(ctx, &model)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading kubeconfig", fmt.Sprintf("The existing kubeconfig is expired and the refresh field is enabled, creating a new one: %v", err))
return
}

// Set state to fully populated data
diags = resp.State.Set(ctx, model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
recreateKubeconfig = true
}
}

// check credentials rotation
if cluster.Status.CredentialsRotation.LastCompletionTime != nil {
lastCompletionTime, err := time.Parse("2006-01-02T15:04:05Z07:00", *cluster.Status.CredentialsRotation.LastCompletionTime)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see above, time.RFC3389

if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading kubeconfig", fmt.Sprintf("Converting LastCompletionTime to timestamp: %v", err))
return
}

if creationTime.Before(lastCompletionTime) {
recreateKubeconfig = true
}
}

// check cluster recreation
if cluster.Status.CreationTime != nil {
clusterCreationTime, err := time.Parse("2006-01-02T15:04:05Z07:00", *cluster.Status.CreationTime)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

time.RFC3389

if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading kubeconfig", fmt.Sprintf("Converting clusterCreationTime to timestamp: %v", err))
return
}

if creationTime.Before(clusterCreationTime) {
recreateKubeconfig = true
}
}

if recreateKubeconfig {
err := r.createKubeconfig(ctx, &model)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading kubeconfig", fmt.Sprintf("The existing kubeconfig is invalid, creating a new one: %v", err))
return
}

// Set state to fully populated data
diags = resp.State.Set(ctx, model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
}

Expand All @@ -285,7 +341,7 @@ func (r *kubeconfigResource) createKubeconfig(ctx context.Context, model *Model)
}

// Map response body to schema
err = mapFields(kubeconfigResp, model)
err = mapFields(kubeconfigResp, model, time.Now())
if err != nil {
return fmt.Errorf("processing API payload: %w", err)
}
Expand Down Expand Up @@ -320,7 +376,7 @@ func (r *kubeconfigResource) Delete(ctx context.Context, req resource.DeleteRequ
tflog.Info(ctx, "SKE kubeconfig deleted")
}

func mapFields(kubeconfigResp *ske.Kubeconfig, model *Model) error {
func mapFields(kubeconfigResp *ske.Kubeconfig, model *Model, creationTime time.Time) error {
if kubeconfigResp == nil {
return fmt.Errorf("response is nil")
}
Expand All @@ -343,6 +399,8 @@ func mapFields(kubeconfigResp *ske.Kubeconfig, model *Model) error {

model.Kubeconfig = types.StringPointerValue(kubeconfigResp.Kubeconfig)
model.ExpiresAt = types.StringPointerValue(kubeconfigResp.ExpirationTimestamp)
// set creation time
model.CreationTime = types.StringValue(creationTime.Format(time.RFC3339))
return nil
}

Expand Down
17 changes: 10 additions & 7 deletions stackit/internal/services/ske/kubeconfig/resource_test.go
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would propose to add testcases for the added functionality (which would have to extracted to dedicated functions first)

Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package ske

import (
"testing"
"time"

"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
Expand All @@ -24,12 +25,13 @@ 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"),
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"),
},
true,
},
Expand Down Expand Up @@ -60,7 +62,8 @@ func TestMapFields(t *testing.T) {
ProjectId: tt.expected.ProjectId,
ClusterName: tt.expected.ClusterName,
}
err := mapFields(tt.input, state)
creationTime, _ := time.Parse("2006-01-02T15:04:05Z07:00", tt.expected.CreationTime.ValueString())
err := mapFields(tt.input, state, creationTime)
if !tt.isValid && err == nil {
t.Fatalf("Should have failed")
}
Expand Down
Loading