Skip to content
Merged
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/cluster_kubeconfig.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ resource "talos_cluster_kubeconfig" "this" {

### Optional

- `certificate_renewal_duration` (String) The duration in hours before the certificate is renewed, defaults to 720h. Must be a valid duration string
- `endpoint` (String) endpoint to use for the talosclient. If not set, the node value will be used
- `timeouts` (Attributes) (see [below for nested schema](#nestedatt--timeouts))

Expand Down
2 changes: 0 additions & 2 deletions pkg/talos/talos_cluster_kubeconfig_data_source.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,6 @@ func (d *talosClusterKubeConfigDataSource) Schema(ctx context.Context, _ datasou
}

// Read implements the datasource.DataSource interface.
//
//nolint:dupl
func (d *talosClusterKubeConfigDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var obj types.Object

Expand Down
220 changes: 206 additions & 14 deletions pkg/talos/talos_cluster_kubeconfig_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"context"
"crypto/x509"
"encoding/pem"
"fmt"
"time"

"github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts"
Expand All @@ -16,6 +17,7 @@ import (
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/objectplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
Expand All @@ -30,8 +32,9 @@ import (
type talosClusterKubeConfigResource struct{}

var (
_ resource.Resource = &talosClusterKubeConfigResource{}
_ resource.ResourceWithModifyPlan = &talosClusterKubeConfigResource{}
_ resource.Resource = &talosClusterKubeConfigResource{}
_ resource.ResourceWithModifyPlan = &talosClusterKubeConfigResource{}
_ resource.ResourceWithUpgradeState = &talosClusterKubeConfigResource{}
)

type talosClusterKubeConfigResourceModelV0 struct {
Expand All @@ -44,6 +47,17 @@ type talosClusterKubeConfigResourceModelV0 struct {
Timeouts timeouts.Value `tfsdk:"timeouts"`
}

type talosClusterKubeConfigResourceModelV1 struct {
ID types.String `tfsdk:"id"`
Node types.String `tfsdk:"node"`
Endpoint types.String `tfsdk:"endpoint"`
ClientConfiguration clientConfiguration `tfsdk:"client_configuration"`
KubeConfigRaw types.String `tfsdk:"kubeconfig_raw"`
KubernetesClientConfiguration kubernetesClientConfiguration `tfsdk:"kubernetes_client_configuration"`
CertificateRenewalDuration types.String `tfsdk:"certificate_renewal_duration"`
Timeouts timeouts.Value `tfsdk:"timeouts"`
}

type kubernetesClientConfiguration struct {
Host types.String `tfsdk:"host"`
CACertificate types.String `tfsdk:"ca_certificate"`
Expand All @@ -62,6 +76,7 @@ func (r *talosClusterKubeConfigResource) Metadata(_ context.Context, req resourc

func (r *talosClusterKubeConfigResource) Schema(ctx context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
Version: 1,
Description: "Retrieves the kubeconfig for a Talos cluster",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Expand Down Expand Up @@ -132,6 +147,12 @@ func (r *talosClusterKubeConfigResource) Schema(ctx context.Context, _ resource.
objectplanmodifier.UseStateForUnknown(),
},
},
"certificate_renewal_duration": schema.StringAttribute{
Optional: true,
Computed: true,
Description: "The duration in hours before the certificate is renewed, defaults to 720h. Must be a valid duration string",
Default: stringdefault.StaticString("720h"),
},
"timeouts": timeouts.Attributes(ctx, timeouts.Opts{
Create: true,
Update: true,
Expand All @@ -141,8 +162,6 @@ func (r *talosClusterKubeConfigResource) Schema(ctx context.Context, _ resource.
}

// Create implements the resource.Resource interface.
//
//nolint:dupl
func (r *talosClusterKubeConfigResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var obj types.Object

Expand All @@ -153,7 +172,7 @@ func (r *talosClusterKubeConfigResource) Create(ctx context.Context, req resourc
return
}

var state talosClusterKubeConfigResourceModelV0
var state talosClusterKubeConfigResourceModelV1
diags = obj.As(ctx, &state, basetypes.ObjectAsOptions{
UnhandledNullAsEmpty: true,
UnhandledUnknownAsEmpty: true,
Expand Down Expand Up @@ -234,6 +253,31 @@ func (r *talosClusterKubeConfigResource) Create(ctx context.Context, req resourc

state.ID = basetypes.NewStringValue(clusterName)

var planObj types.Object

diags = req.Plan.Get(ctx, &planObj)
resp.Diagnostics.Append(diags...)

if resp.Diagnostics.HasError() {
return
}

var planState talosClusterKubeConfigResourceModelV1

diags = planObj.As(ctx, &planState, basetypes.ObjectAsOptions{
UnhandledNullAsEmpty: true,
UnhandledUnknownAsEmpty: true,
})
resp.Diagnostics.Append(diags...)

if resp.Diagnostics.HasError() {
return
}

if state.CertificateRenewalDuration.IsNull() || state.CertificateRenewalDuration.IsUnknown() {
state.CertificateRenewalDuration = planState.CertificateRenewalDuration
}

diags = resp.State.Set(ctx, &state)
resp.Diagnostics.Append(diags...)

Expand All @@ -248,6 +292,9 @@ func (r *talosClusterKubeConfigResource) Delete(_ context.Context, _ resource.De
func (r *talosClusterKubeConfigResource) Read(_ context.Context, _ resource.ReadRequest, _ *resource.ReadResponse) {
}

// ModifyPlan implements the resource.ResourceWithModifyPlan interface.
//
//nolint:gocyclo,cyclop
func (r *talosClusterKubeConfigResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
// delete is a no-op
if req.Plan.Raw.IsNull() {
Expand All @@ -263,7 +310,7 @@ func (r *talosClusterKubeConfigResource) ModifyPlan(ctx context.Context, req res
return
}

var config talosClusterKubeConfigResourceModelV0
var config talosClusterKubeConfigResourceModelV1

diags = configObj.As(ctx, &config, basetypes.ObjectAsOptions{
UnhandledNullAsEmpty: true,
Expand All @@ -289,7 +336,7 @@ func (r *talosClusterKubeConfigResource) ModifyPlan(ctx context.Context, req res
return
}

var planState talosClusterKubeConfigResourceModelV0
var planState talosClusterKubeConfigResourceModelV1

diags = configObj.As(ctx, &planState, basetypes.ObjectAsOptions{
UnhandledNullAsEmpty: true,
Expand Down Expand Up @@ -358,9 +405,41 @@ func (r *talosClusterKubeConfigResource) ModifyPlan(ctx context.Context, req res
return
}

// check if NotAfter expires in a month
if x509Cert.NotAfter.Before(OverridableTimeFunc().AddDate(0, 1, 0)) {
tflog.Info(ctx, "kubernetes client certificate expires in a month, needs regeneration")
var exisitingStateObj types.Object

diags = req.State.Get(ctx, &exisitingStateObj)
resp.Diagnostics.Append(diags...)

if resp.Diagnostics.HasError() {
return
}

var existingState talosClusterKubeConfigResourceModelV1

diags = exisitingStateObj.As(ctx, &existingState, basetypes.ObjectAsOptions{
UnhandledNullAsEmpty: true,
UnhandledUnknownAsEmpty: true,
})
resp.Diagnostics.Append(diags...)

if resp.Diagnostics.HasError() {
return
}

if planState.CertificateRenewalDuration.IsNull() || planState.CertificateRenewalDuration.IsUnknown() {
planState.CertificateRenewalDuration = existingState.CertificateRenewalDuration
}

renewalDuration, err := time.ParseDuration(planState.CertificateRenewalDuration.ValueString())
if err != nil {
resp.Diagnostics.AddError("failed to parse certificate renewal duration in plan", err.Error())

return
}

// check if NotAfter expires in the given duration
if x509Cert.NotAfter.Before(OverridableTimeFunc().Add(renewalDuration)) {
tflog.Info(ctx, fmt.Sprintf("kubernetes client certificate expires in %s, needs regeneration", existingState.CertificateRenewalDuration.ValueString()))

resp.Diagnostics.Append(resp.Plan.SetAttribute(ctx, path.Root("kubernetes_client_configuration").AtName("host"), types.StringUnknown())...)
resp.Diagnostics.Append(resp.Plan.SetAttribute(ctx, path.Root("kubernetes_client_configuration").AtName("client_certificate"), types.StringUnknown())...)
Expand All @@ -374,6 +453,112 @@ func (r *talosClusterKubeConfigResource) ModifyPlan(ctx context.Context, req res
}
}

func (r *talosClusterKubeConfigResource) UpgradeState(ctx context.Context) map[int64]resource.StateUpgrader {
return map[int64]resource.StateUpgrader{
0: {
PriorSchema: &schema.Schema{
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Computed: true,
},
"node": schema.StringAttribute{
Required: true,
},
"endpoint": schema.StringAttribute{
Optional: true,
Computed: true,
},
"client_configuration": schema.SingleNestedAttribute{
Attributes: map[string]schema.Attribute{
"ca_certificate": schema.StringAttribute{
Required: true,
},
"client_certificate": schema.StringAttribute{
Required: true,
},
"client_key": schema.StringAttribute{
Required: true,
Sensitive: true,
},
},
Required: true,
},
"kubeconfig_raw": schema.StringAttribute{
Computed: true,

Sensitive: true,
},
"kubernetes_client_configuration": schema.SingleNestedAttribute{
Attributes: map[string]schema.Attribute{
"host": schema.StringAttribute{
Computed: true,
},
"ca_certificate": schema.StringAttribute{
Computed: true,
},
"client_certificate": schema.StringAttribute{
Computed: true,
},
"client_key": schema.StringAttribute{
Computed: true,
Sensitive: true,
},
},
Computed: true,
},
"timeouts": timeouts.Attributes(ctx, timeouts.Opts{
Create: true,
Update: true,
}),
},
},
StateUpgrader: func(ctx context.Context, req resource.UpgradeStateRequest, resp *resource.UpgradeStateResponse) {
var obj types.Object

diags := req.State.Get(ctx, &obj)
resp.Diagnostics.Append(diags...)
if diags.HasError() {
return
}

var priorStateData talosClusterKubeConfigResourceModelV0

diags = obj.As(ctx, &priorStateData, basetypes.ObjectAsOptions{
UnhandledNullAsEmpty: true,
UnhandledUnknownAsEmpty: true,
})
resp.Diagnostics.Append(diags...)
if diags.HasError() {
return
}

state := talosClusterKubeConfigResourceModelV1{
ID: priorStateData.ID,
Node: priorStateData.Node,
Endpoint: priorStateData.Endpoint,
ClientConfiguration: priorStateData.ClientConfiguration,
KubeConfigRaw: priorStateData.KubeConfigRaw,
KubernetesClientConfiguration: kubernetesClientConfiguration{
Host: priorStateData.KubernetesClientConfiguration.Host,
CACertificate: priorStateData.KubernetesClientConfiguration.CACertificate,
ClientCertificate: priorStateData.KubernetesClientConfiguration.ClientCertificate,
ClientKey: priorStateData.KubernetesClientConfiguration.ClientKey,
},
CertificateRenewalDuration: basetypes.NewStringValue("720h"),
Timeouts: priorStateData.Timeouts,
}

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

// Update implements the resource.ResourceWithModifyPlan interface.
//
//nolint:gocognit,gocyclo,cyclop
Expand All @@ -386,7 +571,7 @@ func (r *talosClusterKubeConfigResource) Update(ctx context.Context, req resourc
return
}

var state talosClusterKubeConfigResourceModelV0
var state talosClusterKubeConfigResourceModelV1

resp.Diagnostics.Append(planObj.As(ctx, &state, basetypes.ObjectAsOptions{
UnhandledNullAsEmpty: true,
Expand Down Expand Up @@ -451,9 +636,16 @@ func (r *talosClusterKubeConfigResource) Update(ctx context.Context, req resourc
return
}

// check if NotAfter expires in a month
if x509Cert.NotAfter.Before(OverridableTimeFunc().AddDate(0, 1, 0)) {
tflog.Info(ctx, "kubernetes client certificate expires in a month, regenerating")
renewalDuration, err := time.ParseDuration(state.CertificateRenewalDuration.ValueString())
if err != nil {
resp.Diagnostics.AddError("failed to parse certificate renewal duration", err.Error())

return
}

// check if NotAfter expires in the given duration
if x509Cert.NotAfter.Before(OverridableTimeFunc().Add(renewalDuration)) {
tflog.Info(ctx, fmt.Sprintf("kubernetes client certificate expires in %s, regenerating", state.CertificateRenewalDuration.ValueString()))

talosConfig, err := talosClientTFConfigToTalosClientConfig(
"dynamic",
Expand Down