Skip to content

Commit daddfb7

Browse files
committed
feat: configurable cert refresh time for kubeconfig
Support configurable cert refresh time for `talos_cluster_kubeconfig`. Fixes: #203 Signed-off-by: Noel Georgi <[email protected]>
1 parent 96c9a85 commit daddfb7

File tree

3 files changed

+207
-16
lines changed

3 files changed

+207
-16
lines changed

docs/resources/cluster_kubeconfig.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ resource "talos_cluster_kubeconfig" "this" {
6969

7070
### Optional
7171

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

pkg/talos/talos_cluster_kubeconfig_data_source.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,8 +121,6 @@ func (d *talosClusterKubeConfigDataSource) Schema(ctx context.Context, _ datasou
121121
}
122122

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

pkg/talos/talos_cluster_kubeconfig_resource.go

Lines changed: 206 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"context"
99
"crypto/x509"
1010
"encoding/pem"
11+
"fmt"
1112
"time"
1213

1314
"github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts"
@@ -16,6 +17,7 @@ import (
1617
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
1718
"github.com/hashicorp/terraform-plugin-framework/resource/schema/objectplanmodifier"
1819
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
20+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
1921
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
2022
"github.com/hashicorp/terraform-plugin-framework/types"
2123
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
@@ -30,8 +32,9 @@ import (
3032
type talosClusterKubeConfigResource struct{}
3133

3234
var (
33-
_ resource.Resource = &talosClusterKubeConfigResource{}
34-
_ resource.ResourceWithModifyPlan = &talosClusterKubeConfigResource{}
35+
_ resource.Resource = &talosClusterKubeConfigResource{}
36+
_ resource.ResourceWithModifyPlan = &talosClusterKubeConfigResource{}
37+
_ resource.ResourceWithUpgradeState = &talosClusterKubeConfigResource{}
3538
)
3639

3740
type talosClusterKubeConfigResourceModelV0 struct {
@@ -44,6 +47,17 @@ type talosClusterKubeConfigResourceModelV0 struct {
4447
Timeouts timeouts.Value `tfsdk:"timeouts"`
4548
}
4649

50+
type talosClusterKubeConfigResourceModelV1 struct {
51+
ID types.String `tfsdk:"id"`
52+
Node types.String `tfsdk:"node"`
53+
Endpoint types.String `tfsdk:"endpoint"`
54+
ClientConfiguration clientConfiguration `tfsdk:"client_configuration"`
55+
KubeConfigRaw types.String `tfsdk:"kubeconfig_raw"`
56+
KubernetesClientConfiguration kubernetesClientConfiguration `tfsdk:"kubernetes_client_configuration"`
57+
CertificateRenewalDuration types.String `tfsdk:"certificate_renewal_duration"`
58+
Timeouts timeouts.Value `tfsdk:"timeouts"`
59+
}
60+
4761
type kubernetesClientConfiguration struct {
4862
Host types.String `tfsdk:"host"`
4963
CACertificate types.String `tfsdk:"ca_certificate"`
@@ -62,6 +76,7 @@ func (r *talosClusterKubeConfigResource) Metadata(_ context.Context, req resourc
6276

6377
func (r *talosClusterKubeConfigResource) Schema(ctx context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
6478
resp.Schema = schema.Schema{
79+
Version: 1,
6580
Description: "Retrieves the kubeconfig for a Talos cluster",
6681
Attributes: map[string]schema.Attribute{
6782
"id": schema.StringAttribute{
@@ -132,6 +147,12 @@ func (r *talosClusterKubeConfigResource) Schema(ctx context.Context, _ resource.
132147
objectplanmodifier.UseStateForUnknown(),
133148
},
134149
},
150+
"certificate_renewal_duration": schema.StringAttribute{
151+
Optional: true,
152+
Computed: true,
153+
Description: "The duration in hours before the certificate is renewed, defaults to 720h. Must be a valid duration string",
154+
Default: stringdefault.StaticString("720h"),
155+
},
135156
"timeouts": timeouts.Attributes(ctx, timeouts.Opts{
136157
Create: true,
137158
Update: true,
@@ -141,8 +162,6 @@ func (r *talosClusterKubeConfigResource) Schema(ctx context.Context, _ resource.
141162
}
142163

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

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

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

235254
state.ID = basetypes.NewStringValue(clusterName)
236255

256+
var planObj types.Object
257+
258+
diags = req.Plan.Get(ctx, &planObj)
259+
resp.Diagnostics.Append(diags...)
260+
261+
if resp.Diagnostics.HasError() {
262+
return
263+
}
264+
265+
var planState talosClusterKubeConfigResourceModelV1
266+
267+
diags = planObj.As(ctx, &planState, basetypes.ObjectAsOptions{
268+
UnhandledNullAsEmpty: true,
269+
UnhandledUnknownAsEmpty: true,
270+
})
271+
resp.Diagnostics.Append(diags...)
272+
273+
if resp.Diagnostics.HasError() {
274+
return
275+
}
276+
277+
if state.CertificateRenewalDuration.IsNull() || state.CertificateRenewalDuration.IsUnknown() {
278+
state.CertificateRenewalDuration = planState.CertificateRenewalDuration
279+
}
280+
237281
diags = resp.State.Set(ctx, &state)
238282
resp.Diagnostics.Append(diags...)
239283

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

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

266-
var config talosClusterKubeConfigResourceModelV0
313+
var config talosClusterKubeConfigResourceModelV1
267314

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

292-
var planState talosClusterKubeConfigResourceModelV0
339+
var planState talosClusterKubeConfigResourceModelV1
293340

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

361-
// check if NotAfter expires in a month
362-
if x509Cert.NotAfter.Before(OverridableTimeFunc().AddDate(0, 1, 0)) {
363-
tflog.Info(ctx, "kubernetes client certificate expires in a month, needs regeneration")
408+
var exisitingStateObj types.Object
409+
410+
diags = req.State.Get(ctx, &exisitingStateObj)
411+
resp.Diagnostics.Append(diags...)
412+
413+
if resp.Diagnostics.HasError() {
414+
return
415+
}
416+
417+
var existingState talosClusterKubeConfigResourceModelV1
418+
419+
diags = exisitingStateObj.As(ctx, &existingState, basetypes.ObjectAsOptions{
420+
UnhandledNullAsEmpty: true,
421+
UnhandledUnknownAsEmpty: true,
422+
})
423+
resp.Diagnostics.Append(diags...)
424+
425+
if resp.Diagnostics.HasError() {
426+
return
427+
}
428+
429+
if planState.CertificateRenewalDuration.IsNull() || planState.CertificateRenewalDuration.IsUnknown() {
430+
planState.CertificateRenewalDuration = existingState.CertificateRenewalDuration
431+
}
432+
433+
renewalDuration, err := time.ParseDuration(planState.CertificateRenewalDuration.ValueString())
434+
if err != nil {
435+
resp.Diagnostics.AddError("failed to parse certificate renewal duration in plan", err.Error())
436+
437+
return
438+
}
439+
440+
// check if NotAfter expires in the given duration
441+
if x509Cert.NotAfter.Before(OverridableTimeFunc().Add(renewalDuration)) {
442+
tflog.Info(ctx, fmt.Sprintf("kubernetes client certificate expires in %s, needs regeneration", existingState.CertificateRenewalDuration.ValueString()))
364443

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

456+
func (r *talosClusterKubeConfigResource) UpgradeState(ctx context.Context) map[int64]resource.StateUpgrader {
457+
return map[int64]resource.StateUpgrader{
458+
0: {
459+
PriorSchema: &schema.Schema{
460+
Attributes: map[string]schema.Attribute{
461+
"id": schema.StringAttribute{
462+
Computed: true,
463+
},
464+
"node": schema.StringAttribute{
465+
Required: true,
466+
},
467+
"endpoint": schema.StringAttribute{
468+
Optional: true,
469+
Computed: true,
470+
},
471+
"client_configuration": schema.SingleNestedAttribute{
472+
Attributes: map[string]schema.Attribute{
473+
"ca_certificate": schema.StringAttribute{
474+
Required: true,
475+
},
476+
"client_certificate": schema.StringAttribute{
477+
Required: true,
478+
},
479+
"client_key": schema.StringAttribute{
480+
Required: true,
481+
Sensitive: true,
482+
},
483+
},
484+
Required: true,
485+
},
486+
"kubeconfig_raw": schema.StringAttribute{
487+
Computed: true,
488+
489+
Sensitive: true,
490+
},
491+
"kubernetes_client_configuration": schema.SingleNestedAttribute{
492+
Attributes: map[string]schema.Attribute{
493+
"host": schema.StringAttribute{
494+
Computed: true,
495+
},
496+
"ca_certificate": schema.StringAttribute{
497+
Computed: true,
498+
},
499+
"client_certificate": schema.StringAttribute{
500+
Computed: true,
501+
},
502+
"client_key": schema.StringAttribute{
503+
Computed: true,
504+
Sensitive: true,
505+
},
506+
},
507+
Computed: true,
508+
},
509+
"timeouts": timeouts.Attributes(ctx, timeouts.Opts{
510+
Create: true,
511+
Update: true,
512+
}),
513+
},
514+
},
515+
StateUpgrader: func(ctx context.Context, req resource.UpgradeStateRequest, resp *resource.UpgradeStateResponse) {
516+
var obj types.Object
517+
518+
diags := req.State.Get(ctx, &obj)
519+
resp.Diagnostics.Append(diags...)
520+
if diags.HasError() {
521+
return
522+
}
523+
524+
var priorStateData talosClusterKubeConfigResourceModelV0
525+
526+
diags = obj.As(ctx, &priorStateData, basetypes.ObjectAsOptions{
527+
UnhandledNullAsEmpty: true,
528+
UnhandledUnknownAsEmpty: true,
529+
})
530+
resp.Diagnostics.Append(diags...)
531+
if diags.HasError() {
532+
return
533+
}
534+
535+
state := talosClusterKubeConfigResourceModelV1{
536+
ID: priorStateData.ID,
537+
Node: priorStateData.Node,
538+
Endpoint: priorStateData.Endpoint,
539+
ClientConfiguration: priorStateData.ClientConfiguration,
540+
KubeConfigRaw: priorStateData.KubeConfigRaw,
541+
KubernetesClientConfiguration: kubernetesClientConfiguration{
542+
Host: priorStateData.KubernetesClientConfiguration.Host,
543+
CACertificate: priorStateData.KubernetesClientConfiguration.CACertificate,
544+
ClientCertificate: priorStateData.KubernetesClientConfiguration.ClientCertificate,
545+
ClientKey: priorStateData.KubernetesClientConfiguration.ClientKey,
546+
},
547+
CertificateRenewalDuration: basetypes.NewStringValue("720h"),
548+
Timeouts: priorStateData.Timeouts,
549+
}
550+
551+
// Set state to fully populated data
552+
diags = resp.State.Set(ctx, &state)
553+
resp.Diagnostics.Append(diags...)
554+
if resp.Diagnostics.HasError() {
555+
return
556+
}
557+
},
558+
},
559+
}
560+
}
561+
377562
// Update implements the resource.ResourceWithModifyPlan interface.
378563
//
379564
//nolint:gocognit,gocyclo,cyclop
@@ -386,7 +571,7 @@ func (r *talosClusterKubeConfigResource) Update(ctx context.Context, req resourc
386571
return
387572
}
388573

389-
var state talosClusterKubeConfigResourceModelV0
574+
var state talosClusterKubeConfigResourceModelV1
390575

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

454-
// check if NotAfter expires in a month
455-
if x509Cert.NotAfter.Before(OverridableTimeFunc().AddDate(0, 1, 0)) {
456-
tflog.Info(ctx, "kubernetes client certificate expires in a month, regenerating")
639+
renewalDuration, err := time.ParseDuration(state.CertificateRenewalDuration.ValueString())
640+
if err != nil {
641+
resp.Diagnostics.AddError("failed to parse certificate renewal duration", err.Error())
642+
643+
return
644+
}
645+
646+
// check if NotAfter expires in the given duration
647+
if x509Cert.NotAfter.Before(OverridableTimeFunc().Add(renewalDuration)) {
648+
tflog.Info(ctx, fmt.Sprintf("kubernetes client certificate expires in %s, regenerating", state.CertificateRenewalDuration.ValueString()))
457649

458650
talosConfig, err := talosClientTFConfigToTalosClientConfig(
459651
"dynamic",

0 commit comments

Comments
 (0)