diff --git a/CHANGELOG.md b/CHANGELOG.md index 10ff096f..119268f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## 4.0.4 (unreleased) + +BUG FIXES: + +* resource/tls_locally_signed_cert: Ensure `terraform refresh` updates state when cert is ready for renewal ([#278](https://github.com/hashicorp/terraform-provider-tls/issues/278)). +* resource/tls_self_signed_cert: Ensure `terraform refresh` updates state when cert is ready for renewal ([#278](https://github.com/hashicorp/terraform-provider-tls/issues/278)). + ## 4.0.3 (September 20, 2022) BUG FIXES: diff --git a/internal/provider/attribute_plan_modifier/default_value.go b/internal/provider/attribute_plan_modifier/default_value.go index e48c060e..151369a4 100644 --- a/internal/provider/attribute_plan_modifier/default_value.go +++ b/internal/provider/attribute_plan_modifier/default_value.go @@ -5,7 +5,9 @@ import ( "fmt" "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" ) // defaultValueAttributePlanModifier specifies a default value (attr.Value) for an attribute. @@ -42,3 +44,58 @@ func (apm *defaultValueAttributePlanModifier) Modify(_ context.Context, req tfsd res.AttributePlan = apm.DefaultValue } + +// readyForRenewalAttributePlanModifier determines whether the certificate is ready for renewal. +type readyForRenewalAttributePlanModifier struct { +} + +// ReadyForRenewal is an helper to instantiate a defaultValueAttributePlanModifier. +func ReadyForRenewal() tfsdk.AttributePlanModifier { + return &readyForRenewalAttributePlanModifier{} +} + +var _ tfsdk.AttributePlanModifier = (*readyForRenewalAttributePlanModifier)(nil) + +func (apm *readyForRenewalAttributePlanModifier) Description(ctx context.Context) string { + return apm.MarkdownDescription(ctx) +} + +func (apm *readyForRenewalAttributePlanModifier) MarkdownDescription(ctx context.Context) string { + return "Sets the value of ready_for_renewal depending on value of validity_period_hours and early_renewal_hours" +} + +func (apm *readyForRenewalAttributePlanModifier) Modify(ctx context.Context, req tfsdk.ModifyAttributePlanRequest, res *tfsdk.ModifyAttributePlanResponse) { + var validityPeriodHours types.Int64 + + res.Diagnostics.Append(req.Plan.GetAttribute(ctx, path.Root("validity_period_hours"), &validityPeriodHours)...) + if res.Diagnostics.HasError() { + return + } + + if validityPeriodHours.Value == 0 { + res.AttributePlan = types.Bool{ + Value: true, + } + + return + } + + var earlyRenewalHours types.Int64 + + res.Diagnostics.Append(req.Plan.GetAttribute(ctx, path.Root("early_renewal_hours"), &earlyRenewalHours)...) + if res.Diagnostics.HasError() { + return + } + + if earlyRenewalHours.IsNull() || earlyRenewalHours.IsUnknown() { + return + } + + if earlyRenewalHours.Value >= validityPeriodHours.Value { + res.AttributePlan = types.Bool{ + Value: true, + } + + return + } +} diff --git a/internal/provider/common_cert.go b/internal/provider/common_cert.go index 0aa4bfd7..c3aed707 100644 --- a/internal/provider/common_cert.go +++ b/internal/provider/common_cert.go @@ -291,11 +291,56 @@ func modifyPlanIfCertificateReadyForRenewal(ctx context.Context, req *resource.M if timeToEarlyRenewal <= 0 { tflog.Info(ctx, "Certificate is ready for early renewal") readyForRenewalPath := path.Root("ready_for_renewal") - res.Diagnostics.Append(res.Plan.SetAttribute(ctx, readyForRenewalPath, true)...) + res.Diagnostics.Append(res.Plan.SetAttribute(ctx, readyForRenewalPath, types.Bool{Unknown: true})...) res.RequiresReplace = append(res.RequiresReplace, readyForRenewalPath) } } +func modifyStateIfCertificateReadyForRenewal(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + // Retrieve `validity_end_time` and confirm is a known, non-null value + validityEndTimePath := path.Root("validity_end_time") + var validityEndTimeStr types.String + resp.Diagnostics.Append(req.State.GetAttribute(ctx, validityEndTimePath, &validityEndTimeStr)...) + if resp.Diagnostics.HasError() { + return + } + if validityEndTimeStr.IsNull() || validityEndTimeStr.IsUnknown() { + return + } + + // Parse `validity_end_time` + validityEndTime, err := time.Parse(time.RFC3339, validityEndTimeStr.Value) + if err != nil { + resp.Diagnostics.AddError( + fmt.Sprintf("Failed to parse data from string: %s", validityEndTimeStr.Value), + err.Error(), + ) + return + } + + // Retrieve `early_renewal_hours` + earlyRenewalHoursPath := path.Root("early_renewal_hours") + var earlyRenewalHours int64 + resp.Diagnostics.Append(req.State.GetAttribute(ctx, earlyRenewalHoursPath, &earlyRenewalHours)...) + if resp.Diagnostics.HasError() { + return + } + + currentTime := overridableTimeFunc() + + // Determine the time from which an "early renewal" is possible + earlyRenewalPeriod := time.Duration(-earlyRenewalHours) * time.Hour + earlyRenewalTime := validityEndTime.Add(earlyRenewalPeriod) + + // If "early renewal" time has passed, mark it "ready for renewal" + timeToEarlyRenewal := earlyRenewalTime.Sub(currentTime) + if timeToEarlyRenewal <= 0 { + tflog.Info(ctx, "Certificate is ready for early renewal") + readyForRenewalPath := path.Root("ready_for_renewal") + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, readyForRenewalPath, true)...) + } +} + // createSubjectDistinguishedNames return a *pkix.Name (i.e. a "Certificate Subject") if the non-empty. // This used for creating x509.Certificate or x509.CertificateRequest. func createSubjectDistinguishedNames(ctx context.Context, subject certificateSubjectModel) pkix.Name { diff --git a/internal/provider/resource_locally_signed_cert.go b/internal/provider/resource_locally_signed_cert.go index e69f3c9f..b1e6d324 100644 --- a/internal/provider/resource_locally_signed_cert.go +++ b/internal/provider/resource_locally_signed_cert.go @@ -15,6 +15,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/hashicorp/terraform-provider-tls/internal/provider/attribute_plan_modifier" ) @@ -157,6 +158,7 @@ func (r *locallySignedCertResource) GetSchema(_ context.Context) (tfsdk.Schema, Computed: true, PlanModifiers: []tfsdk.AttributePlanModifier{ attribute_plan_modifier.DefaultValue(types.Bool{Value: false}), + attribute_plan_modifier.ReadyForRenewal(), }, Description: "Is the certificate either expired (i.e. beyond the `validity_period_hours`) " + "or ready for an early renewal (i.e. within the `early_renewal_hours`)?", @@ -275,9 +277,10 @@ func (r *locallySignedCertResource) Create(ctx context.Context, req resource.Cre res.Diagnostics.Append(res.State.Set(ctx, newState)...) } -func (r *locallySignedCertResource) Read(ctx context.Context, _ resource.ReadRequest, _ *resource.ReadResponse) { - // NO-OP: all there is to read is in the State, and response is already populated with that. +func (r *locallySignedCertResource) Read(ctx context.Context, req resource.ReadRequest, res *resource.ReadResponse) { tflog.Debug(ctx, "Reading locally signed certificate from state") + + modifyStateIfCertificateReadyForRenewal(ctx, req, res) } func (r *locallySignedCertResource) Update(ctx context.Context, req resource.UpdateRequest, res *resource.UpdateResponse) { diff --git a/internal/provider/resource_locally_signed_cert_test.go b/internal/provider/resource_locally_signed_cert_test.go index af81d7a3..7246d482 100644 --- a/internal/provider/resource_locally_signed_cert_test.go +++ b/internal/provider/resource_locally_signed_cert_test.go @@ -200,6 +200,106 @@ func TestResourceLocallySignedCert_DetectExpiringAndExpired(t *testing.T) { overridableTimeFunc = oldNow } +func TestResourceLocallySignedCert_DetectExpiring_Refresh(t *testing.T) { + oldNow := overridableTimeFunc + r.UnitTest(t, r.TestCase{ + ProtoV5ProviderFactories: protoV5ProviderFactories(), + PreCheck: setTimeForTest("2019-06-14T12:00:00Z"), + Steps: []r.TestStep{ + { + Config: locallySignedCertConfig(10, 2), + Check: r.TestCheckResourceAttr("tls_locally_signed_cert.test", "ready_for_renewal", "false"), + }, + { + PreConfig: setTimeForTest("2019-06-14T21:30:00Z"), + RefreshState: true, + ExpectNonEmptyPlan: true, + Check: r.TestCheckResourceAttr("tls_locally_signed_cert.test", "ready_for_renewal", "true"), + }, + { + PreConfig: setTimeForTest("2019-06-14T21:30:00Z"), + Config: locallySignedCertConfig(10, 2), + Check: r.TestCheckResourceAttr("tls_locally_signed_cert.test", "ready_for_renewal", "false"), + }, + }, + }) + overridableTimeFunc = oldNow +} + +func TestResourceLocallySignedCert_DetectExpired_Refresh(t *testing.T) { + oldNow := overridableTimeFunc + r.UnitTest(t, r.TestCase{ + ProtoV5ProviderFactories: protoV5ProviderFactories(), + PreCheck: setTimeForTest("2019-06-14T12:00:00Z"), + Steps: []r.TestStep{ + { + Config: locallySignedCertConfig(10, 2), + Check: r.TestCheckResourceAttr("tls_locally_signed_cert.test", "ready_for_renewal", "false"), + }, + { + PreConfig: setTimeForTest("2019-06-14T23:30:00Z"), + RefreshState: true, + ExpectNonEmptyPlan: true, + Check: r.TestCheckResourceAttr("tls_locally_signed_cert.test", "ready_for_renewal", "true"), + }, + { + PreConfig: setTimeForTest("2019-06-14T23:30:00Z"), + Config: locallySignedCertConfig(10, 2), + Check: r.TestCheckResourceAttr("tls_locally_signed_cert.test", "ready_for_renewal", "false"), + }, + }, + }) + overridableTimeFunc = oldNow +} + +func TestResourceLocallySignedCert_ReadyForRenewal_ValidityPeriodZero(t *testing.T) { + oldNow := overridableTimeFunc + r.UnitTest(t, r.TestCase{ + ProtoV5ProviderFactories: protoV5ProviderFactories(), + PreCheck: setTimeForTest("2019-06-14T12:00:00Z"), + Steps: []r.TestStep{ + { + Config: locallySignedCertConfig(0, 0), + ExpectNonEmptyPlan: true, + Check: r.TestCheckResourceAttr("tls_locally_signed_cert.test", "ready_for_renewal", "true"), + }, + }, + }) + overridableTimeFunc = oldNow +} + +func TestResourceLocallySignedCert_ReadyForRenewal_EarlyRenewalGreaterThanValidityPeriod(t *testing.T) { + oldNow := overridableTimeFunc + r.UnitTest(t, r.TestCase{ + ProtoV5ProviderFactories: protoV5ProviderFactories(), + PreCheck: setTimeForTest("2019-06-14T12:00:00Z"), + Steps: []r.TestStep{ + { + Config: locallySignedCertConfig(1, 2), + ExpectNonEmptyPlan: true, + Check: r.TestCheckResourceAttr("tls_locally_signed_cert.test", "ready_for_renewal", "true"), + }, + }, + }) + overridableTimeFunc = oldNow +} + +func TestResourceLocallySignedCert_ReadyForRenewal_EarlyRenewalEqualsValidityPeriod(t *testing.T) { + oldNow := overridableTimeFunc + r.UnitTest(t, r.TestCase{ + ProtoV5ProviderFactories: protoV5ProviderFactories(), + PreCheck: setTimeForTest("2019-06-14T12:00:00Z"), + Steps: []r.TestStep{ + { + Config: locallySignedCertConfig(1, 1), + ExpectNonEmptyPlan: true, + Check: r.TestCheckResourceAttr("tls_locally_signed_cert.test", "ready_for_renewal", "true"), + }, + }, + }) + overridableTimeFunc = oldNow +} + func TestResourceLocallySignedCert_RecreatesAfterExpired(t *testing.T) { oldNow := overridableTimeFunc var previousCert string diff --git a/internal/provider/resource_self_signed_cert.go b/internal/provider/resource_self_signed_cert.go index 7c975c0c..5d7d93ac 100644 --- a/internal/provider/resource_self_signed_cert.go +++ b/internal/provider/resource_self_signed_cert.go @@ -16,6 +16,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/hashicorp/terraform-provider-tls/internal/provider/attribute_plan_modifier" ) @@ -180,6 +181,7 @@ func (r *selfSignedCertResource) GetSchema(_ context.Context) (tfsdk.Schema, dia Computed: true, PlanModifiers: []tfsdk.AttributePlanModifier{ attribute_plan_modifier.DefaultValue(types.Bool{Value: false}), + attribute_plan_modifier.ReadyForRenewal(), }, Description: "Is the certificate either expired (i.e. beyond the `validity_period_hours`) " + "or ready for an early renewal (i.e. within the `early_renewal_hours`)?", @@ -431,9 +433,10 @@ func (r *selfSignedCertResource) Create(ctx context.Context, req resource.Create res.Diagnostics.Append(res.State.Set(ctx, newState)...) } -func (r *selfSignedCertResource) Read(ctx context.Context, _ resource.ReadRequest, _ *resource.ReadResponse) { - // NO-OP: all there is to read is in the State, and response is already populated with that. +func (r *selfSignedCertResource) Read(ctx context.Context, req resource.ReadRequest, res *resource.ReadResponse) { tflog.Debug(ctx, "Reading self signed certificate from state") + + modifyStateIfCertificateReadyForRenewal(ctx, req, res) } func (r *selfSignedCertResource) Update(ctx context.Context, req resource.UpdateRequest, res *resource.UpdateResponse) { diff --git a/internal/provider/resource_self_signed_cert_test.go b/internal/provider/resource_self_signed_cert_test.go index 721307bd..ff213ca0 100644 --- a/internal/provider/resource_self_signed_cert_test.go +++ b/internal/provider/resource_self_signed_cert_test.go @@ -218,6 +218,106 @@ func TestResourceSelfSignedCert_DetectExpiringAndExpired(t *testing.T) { overridableTimeFunc = oldNow } +func TestResourceSelfSignedCert_DetectExpiring_Refresh(t *testing.T) { + oldNow := overridableTimeFunc + r.UnitTest(t, r.TestCase{ + ProtoV5ProviderFactories: protoV5ProviderFactories(), + PreCheck: setTimeForTest("2019-06-14T12:00:00Z"), + Steps: []r.TestStep{ + { + Config: selfSignedCertConfig(10, 2), + Check: r.TestCheckResourceAttr("tls_self_signed_cert.test1", "ready_for_renewal", "false"), + }, + { + PreConfig: setTimeForTest("2019-06-14T21:30:00Z"), + RefreshState: true, + ExpectNonEmptyPlan: true, + Check: r.TestCheckResourceAttr("tls_self_signed_cert.test1", "ready_for_renewal", "true"), + }, + { + PreConfig: setTimeForTest("2019-06-14T21:30:00Z"), + Config: selfSignedCertConfig(10, 2), + Check: r.TestCheckResourceAttr("tls_self_signed_cert.test1", "ready_for_renewal", "false"), + }, + }, + }) + overridableTimeFunc = oldNow +} + +func TestResourceSelfSignedCert_DetectExpired_Refresh(t *testing.T) { + oldNow := overridableTimeFunc + r.UnitTest(t, r.TestCase{ + ProtoV5ProviderFactories: protoV5ProviderFactories(), + PreCheck: setTimeForTest("2019-06-14T12:00:00Z"), + Steps: []r.TestStep{ + { + Config: selfSignedCertConfig(10, 2), + Check: r.TestCheckResourceAttr("tls_self_signed_cert.test1", "ready_for_renewal", "false"), + }, + { + PreConfig: setTimeForTest("2019-06-14T23:30:00Z"), + RefreshState: true, + ExpectNonEmptyPlan: true, + Check: r.TestCheckResourceAttr("tls_self_signed_cert.test1", "ready_for_renewal", "true"), + }, + { + PreConfig: setTimeForTest("2019-06-14T23:30:00Z"), + Config: selfSignedCertConfig(10, 2), + Check: r.TestCheckResourceAttr("tls_self_signed_cert.test1", "ready_for_renewal", "false"), + }, + }, + }) + overridableTimeFunc = oldNow +} + +func TestResourceSelfSignedCert_ReadyForRenewal_ValidityPeriodZero(t *testing.T) { + oldNow := overridableTimeFunc + r.UnitTest(t, r.TestCase{ + ProtoV5ProviderFactories: protoV5ProviderFactories(), + PreCheck: setTimeForTest("2019-06-14T12:00:00Z"), + Steps: []r.TestStep{ + { + Config: selfSignedCertConfig(0, 0), + ExpectNonEmptyPlan: true, + Check: r.TestCheckResourceAttr("tls_self_signed_cert.test1", "ready_for_renewal", "true"), + }, + }, + }) + overridableTimeFunc = oldNow +} + +func TestResourceSelfSignedCert_ReadyForRenewal_EarlyRenewalGreaterThanValidityPeriod(t *testing.T) { + oldNow := overridableTimeFunc + r.UnitTest(t, r.TestCase{ + ProtoV5ProviderFactories: protoV5ProviderFactories(), + PreCheck: setTimeForTest("2019-06-14T12:00:00Z"), + Steps: []r.TestStep{ + { + Config: selfSignedCertConfig(1, 2), + ExpectNonEmptyPlan: true, + Check: r.TestCheckResourceAttr("tls_self_signed_cert.test1", "ready_for_renewal", "true"), + }, + }, + }) + overridableTimeFunc = oldNow +} + +func TestResourceSelfSignedCert_ReadyForRenewal_EarlyRenewalEqualsValidityPeriod(t *testing.T) { + oldNow := overridableTimeFunc + r.UnitTest(t, r.TestCase{ + ProtoV5ProviderFactories: protoV5ProviderFactories(), + PreCheck: setTimeForTest("2019-06-14T12:00:00Z"), + Steps: []r.TestStep{ + { + Config: selfSignedCertConfig(1, 1), + ExpectNonEmptyPlan: true, + Check: r.TestCheckResourceAttr("tls_self_signed_cert.test1", "ready_for_renewal", "true"), + }, + }, + }) + overridableTimeFunc = oldNow +} + func TestResourceSelfSignedCert_RecreatesAfterExpired(t *testing.T) { oldNow := overridableTimeFunc var previousCert string