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 (
3032type talosClusterKubeConfigResource struct {}
3133
3234var (
33- _ resource.Resource = & talosClusterKubeConfigResource {}
34- _ resource.ResourceWithModifyPlan = & talosClusterKubeConfigResource {}
35+ _ resource.Resource = & talosClusterKubeConfigResource {}
36+ _ resource.ResourceWithModifyPlan = & talosClusterKubeConfigResource {}
37+ _ resource.ResourceWithUpgradeState = & talosClusterKubeConfigResource {}
3538)
3639
3740type 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+
4761type 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
6377func (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
146165func (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
248292func (r * talosClusterKubeConfigResource ) Read (_ context.Context , _ resource.ReadRequest , _ * resource.ReadResponse ) {
249293}
250294
295+ // ModifyPlan implements the resource.ResourceWithModifyPlan interface.
296+ //
297+ //nolint:gocyclo,cyclop
251298func (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