Skip to content

Commit

Permalink
Merge pull request #278 from hashicorp/bendbennett/issues-268
Browse files Browse the repository at this point in the history
Update certificate ready_for_renewal during refresh?
  • Loading branch information
bendbennett committed Oct 14, 2022
2 parents f995ab4 + 3a89efb commit 180df62
Show file tree
Hide file tree
Showing 7 changed files with 320 additions and 5 deletions.
7 changes: 7 additions & 0 deletions 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:
Expand Down
57 changes: 57 additions & 0 deletions internal/provider/attribute_plan_modifier/default_value.go
Expand Up @@ -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.
Expand Down Expand Up @@ -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
}
}
47 changes: 46 additions & 1 deletion internal/provider/common_cert.go
Expand Up @@ -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 {
Expand Down
7 changes: 5 additions & 2 deletions internal/provider/resource_locally_signed_cert.go
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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`)?",
Expand Down Expand Up @@ -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) {
Expand Down
100 changes: 100 additions & 0 deletions internal/provider/resource_locally_signed_cert_test.go
Expand Up @@ -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
Expand Down
7 changes: 5 additions & 2 deletions internal/provider/resource_self_signed_cert.go
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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`)?",
Expand Down Expand Up @@ -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) {
Expand Down

0 comments on commit 180df62

Please sign in to comment.