Update kubeconfig when invalid (#627)
- kubeconfig expires - credentials rotation - cluster recreation Signed-off-by: Alexander Dahmen <alexander.dahmen@inovex.de>
This commit is contained in:
parent
1a66887c01
commit
3adff492b6
3 changed files with 332 additions and 25 deletions
|
|
@ -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.
|
||||
|
|
@ -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{
|
||||
|
|
@ -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.UseStateForUnknown(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -246,26 +257,43 @@ 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)
|
||||
|
||||
if model.Refresh.ValueBool() && !model.ExpiresAt.IsNull() {
|
||||
expiresAt, err := time.Parse("2006-01-02T15:04:05Z07:00", model.ExpiresAt.ValueString())
|
||||
cluster, err := r.client.GetClusterExecute(ctx, projectId, clusterName)
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading kubeconfig", fmt.Sprintf("Could not get cluster(%s): %v", clusterName, err))
|
||||
return
|
||||
}
|
||||
|
||||
// check if kubeconfig has expired
|
||||
hasExpired, err := checkHasExpired(&model, time.Now())
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading kubeconfig", fmt.Sprintf("%v", err))
|
||||
return
|
||||
}
|
||||
|
||||
clusterRecreation, err := checkClusterRecreation(cluster, &model)
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading kubeconfig", fmt.Sprintf("%v", err))
|
||||
return
|
||||
}
|
||||
|
||||
credentialsRotation, err := checkCredentialsRotation(cluster, &model)
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading kubeconfig", fmt.Sprintf("%v", err))
|
||||
return
|
||||
}
|
||||
|
||||
if hasExpired || clusterRecreation || credentialsRotation {
|
||||
err := r.createKubeconfig(ctx, &model)
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading kubeconfig", fmt.Sprintf("Converting expiresAt field to timestamp: %v", err))
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading kubeconfig", fmt.Sprintf("The existing kubeconfig is invalid, creating a new one: %v", err))
|
||||
return
|
||||
}
|
||||
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
|
||||
}
|
||||
// Set state to fully populated data
|
||||
diags = resp.State.Set(ctx, model)
|
||||
resp.Diagnostics.Append(diags...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -285,7 +313,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)
|
||||
}
|
||||
|
|
@ -320,7 +348,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")
|
||||
}
|
||||
|
|
@ -343,6 +371,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
|
||||
}
|
||||
|
||||
|
|
@ -361,3 +391,55 @@ func toCreatePayload(model *Model) (*ske.CreateKubeconfigPayload, error) {
|
|||
ExpirationSeconds: expirationStringPtr,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// helper function to check if kubecondig has expired
|
||||
func checkHasExpired(model *Model, currentTime time.Time) (bool, error) {
|
||||
if model.Refresh.ValueBool() && !model.ExpiresAt.IsNull() {
|
||||
expiresAt, err := time.Parse(time.RFC3339, model.ExpiresAt.ValueString())
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("converting expiresAt field to timestamp: %w", err)
|
||||
}
|
||||
if expiresAt.Before(currentTime) {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// helper function to check if a credentials rotation was done
|
||||
func checkCredentialsRotation(cluster *ske.Cluster, model *Model) (bool, error) {
|
||||
creationTime, err := time.Parse(time.RFC3339, model.CreationTime.ValueString())
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("converting creationTime field to timestamp: %w", err)
|
||||
}
|
||||
if cluster.Status.CredentialsRotation.LastCompletionTime != nil {
|
||||
lastCompletionTime, err := time.Parse(time.RFC3339, *cluster.Status.CredentialsRotation.LastCompletionTime)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("converting LastCompletionTime to timestamp: %w", err)
|
||||
}
|
||||
|
||||
if creationTime.Before(lastCompletionTime) {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// helper function to check if a cluster recreation was done
|
||||
func checkClusterRecreation(cluster *ske.Cluster, model *Model) (bool, error) {
|
||||
creationTime, err := time.Parse(time.RFC3339, model.CreationTime.ValueString())
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("converting creationTime field to timestamp: %w", err)
|
||||
}
|
||||
if cluster.Status.CreationTime != nil {
|
||||
clusterCreationTime, err := time.Parse(time.RFC3339, *cluster.Status.CreationTime)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("converting clusterCreationTime to timestamp: %w", err)
|
||||
}
|
||||
|
||||
if creationTime.Before(clusterCreationTime) {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package ske
|
|||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
|
|
@ -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,
|
||||
},
|
||||
|
|
@ -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")
|
||||
}
|
||||
|
|
@ -125,3 +128,224 @@ func TestToCreatePayload(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckHasExpired(t *testing.T) {
|
||||
tests := []struct {
|
||||
description string
|
||||
inputModel *Model
|
||||
currentTime time.Time
|
||||
expected bool
|
||||
expectedError bool
|
||||
}{
|
||||
{
|
||||
description: "has expired",
|
||||
inputModel: &Model{
|
||||
Refresh: types.BoolValue(true),
|
||||
ExpiresAt: types.StringValue(time.Now().Add(-1 * time.Hour).Format(time.RFC3339)), // one hour ago
|
||||
},
|
||||
currentTime: time.Now(),
|
||||
expected: true,
|
||||
expectedError: false,
|
||||
},
|
||||
{
|
||||
description: "not expired",
|
||||
inputModel: &Model{
|
||||
Refresh: types.BoolValue(true),
|
||||
ExpiresAt: types.StringValue(time.Now().Add(1 * time.Hour).Format(time.RFC3339)), // in one hour
|
||||
},
|
||||
currentTime: time.Now(),
|
||||
expected: false,
|
||||
expectedError: false,
|
||||
},
|
||||
{
|
||||
description: "refresh is false, expired won't be checked",
|
||||
inputModel: &Model{
|
||||
Refresh: types.BoolValue(false),
|
||||
ExpiresAt: types.StringValue(time.Now().Add(-1 * time.Hour).Format(time.RFC3339)), // one hour ago
|
||||
},
|
||||
currentTime: time.Now(),
|
||||
expected: false,
|
||||
expectedError: false,
|
||||
},
|
||||
{
|
||||
description: "invalid time",
|
||||
inputModel: &Model{
|
||||
Refresh: types.BoolValue(true),
|
||||
ExpiresAt: types.StringValue("invalid time"),
|
||||
},
|
||||
currentTime: time.Now(),
|
||||
expected: false,
|
||||
expectedError: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.description, func(t *testing.T) {
|
||||
got, err := checkHasExpired(tt.inputModel, tt.currentTime)
|
||||
if (err != nil) != tt.expectedError {
|
||||
t.Errorf("checkHasExpired() error = %v, expectedError %v", err, tt.expectedError)
|
||||
return
|
||||
}
|
||||
if got != tt.expected {
|
||||
t.Errorf("checkHasExpired() = %v, expected %v", got, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckCredentialsRotation(t *testing.T) {
|
||||
tests := []struct {
|
||||
description string
|
||||
inputCluster *ske.Cluster
|
||||
inputModel *Model
|
||||
expected bool
|
||||
expectedError bool
|
||||
}{
|
||||
{
|
||||
description: "creation time after credentials rotation",
|
||||
inputCluster: &ske.Cluster{
|
||||
Status: &ske.ClusterStatus{
|
||||
CredentialsRotation: &ske.CredentialsRotationState{
|
||||
LastCompletionTime: utils.Ptr(time.Now().Add(-1 * time.Hour).Format(time.RFC3339)), // one hour ago
|
||||
},
|
||||
},
|
||||
},
|
||||
inputModel: &Model{
|
||||
CreationTime: types.StringValue(time.Now().Format(time.RFC3339)),
|
||||
},
|
||||
expected: false,
|
||||
expectedError: false,
|
||||
},
|
||||
{
|
||||
description: "creation time before credentials rotation",
|
||||
inputCluster: &ske.Cluster{
|
||||
Status: &ske.ClusterStatus{
|
||||
CredentialsRotation: &ske.CredentialsRotationState{
|
||||
LastCompletionTime: utils.Ptr(time.Now().Add(1 * time.Hour).Format(time.RFC3339)),
|
||||
},
|
||||
},
|
||||
},
|
||||
inputModel: &Model{
|
||||
CreationTime: types.StringValue(time.Now().Format(time.RFC3339)),
|
||||
},
|
||||
expected: true,
|
||||
expectedError: false,
|
||||
},
|
||||
{
|
||||
description: "last completion time not set",
|
||||
inputCluster: &ske.Cluster{
|
||||
Status: &ske.ClusterStatus{
|
||||
CredentialsRotation: &ske.CredentialsRotationState{
|
||||
LastCompletionTime: nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
inputModel: &Model{
|
||||
CreationTime: types.StringValue(time.Now().Format(time.RFC3339)),
|
||||
},
|
||||
expected: false,
|
||||
expectedError: false,
|
||||
},
|
||||
{
|
||||
description: "invalid last completion time",
|
||||
inputCluster: &ske.Cluster{
|
||||
Status: &ske.ClusterStatus{
|
||||
CredentialsRotation: &ske.CredentialsRotationState{
|
||||
LastCompletionTime: utils.Ptr("invalid time"),
|
||||
},
|
||||
},
|
||||
},
|
||||
inputModel: &Model{
|
||||
CreationTime: types.StringValue(time.Now().Format(time.RFC3339)),
|
||||
},
|
||||
expected: false,
|
||||
expectedError: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.description, func(t *testing.T) {
|
||||
got, err := checkCredentialsRotation(tt.inputCluster, tt.inputModel)
|
||||
if (err != nil) != tt.expectedError {
|
||||
t.Errorf("checkCredentialsRotation() error = %v, expectedError %v", err, tt.expectedError)
|
||||
return
|
||||
}
|
||||
if got != tt.expected {
|
||||
t.Errorf("checkCredentialsRotation() = %v, expected %v", got, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckClusterRecreation(t *testing.T) {
|
||||
tests := []struct {
|
||||
description string
|
||||
inputCluster *ske.Cluster
|
||||
inputModel *Model
|
||||
expected bool
|
||||
expectedError bool
|
||||
}{
|
||||
{
|
||||
description: "cluster creation time after kubeconfig creation time",
|
||||
inputCluster: &ske.Cluster{
|
||||
Status: &ske.ClusterStatus{
|
||||
CreationTime: utils.Ptr(time.Now().Add(-1 * time.Hour).Format(time.RFC3339)),
|
||||
},
|
||||
},
|
||||
inputModel: &Model{
|
||||
CreationTime: types.StringValue(time.Now().Format(time.RFC3339)),
|
||||
},
|
||||
expected: false,
|
||||
expectedError: false,
|
||||
},
|
||||
{
|
||||
description: "cluster creation time before kubeconfig creation time",
|
||||
inputCluster: &ske.Cluster{
|
||||
Status: &ske.ClusterStatus{
|
||||
CreationTime: utils.Ptr(time.Now().Add(1 * time.Hour).Format(time.RFC3339)),
|
||||
},
|
||||
},
|
||||
inputModel: &Model{
|
||||
CreationTime: types.StringValue(time.Now().Format(time.RFC3339)),
|
||||
},
|
||||
expected: true,
|
||||
expectedError: false,
|
||||
},
|
||||
{
|
||||
description: "cluster creation time not set",
|
||||
inputCluster: &ske.Cluster{
|
||||
Status: &ske.ClusterStatus{
|
||||
CreationTime: nil,
|
||||
},
|
||||
},
|
||||
inputModel: &Model{
|
||||
CreationTime: types.StringValue(time.Now().Format(time.RFC3339)),
|
||||
},
|
||||
expected: false,
|
||||
expectedError: false,
|
||||
},
|
||||
{
|
||||
description: "invalid cluster creation time",
|
||||
inputCluster: &ske.Cluster{
|
||||
Status: &ske.ClusterStatus{
|
||||
CreationTime: utils.Ptr("invalid time"),
|
||||
},
|
||||
},
|
||||
inputModel: &Model{
|
||||
CreationTime: types.StringValue(time.Now().Format(time.RFC3339)),
|
||||
},
|
||||
expected: false,
|
||||
expectedError: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.description, func(t *testing.T) {
|
||||
got, err := checkClusterRecreation(tt.inputCluster, tt.inputModel)
|
||||
if (err != nil) != tt.expectedError {
|
||||
t.Errorf("checkClusterRecreation() error = %v, expectedError %v", err, tt.expectedError)
|
||||
return
|
||||
}
|
||||
if got != tt.expected {
|
||||
t.Errorf("checkClusterRecreation() = %v, expected %v", got, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue