Skip to content

Commit

Permalink
resource_pki_secret_backend_cert: Report when renewal is pending
Browse files Browse the repository at this point in the history
This exports an additional boolean attribute renew_pending, which should
start set to false but will transition to true during refresh if the
current time is less than "min_seconds_remaining" seconds before the
certificate's expiration time.

This adds a little extra information to the plan output to explain why
the provider is proposing to replace the object, and also adds a useful
hook for postconditions that wish to detect (e.g. during a refresh-only
plan) that a renewal is pending.
  • Loading branch information
apparentlymart committed Sep 7, 2022
1 parent c3dcbc3 commit da6005d
Show file tree
Hide file tree
Showing 3 changed files with 64 additions and 13 deletions.
67 changes: 54 additions & 13 deletions vault/resource_pki_secret_backend_cert.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,12 @@ func pkiSecretBackendCertResource() *schema.Resource {
"expiration": {
Type: schema.TypeInt,
Computed: true,
Description: "The certificate expiration.",
Description: "The certificate expiration as a Unix-style timestamp.",
},
"renew_pending": {
Type: schema.TypeBool,
Computed: true,
Description: "Initially false, and then set to true during refresh once the expiration is less than min_seconds_remaining in the future.",
},
"revoke": {
Type: schema.TypeBool,
Expand Down Expand Up @@ -245,24 +250,23 @@ func pkiSecretBackendCertCreate(d *schema.ResourceData, meta interface{}) error
d.Set("serial_number", resp.Data["serial_number"])
d.Set("expiration", resp.Data["expiration"])

err = pkiSecretBackendCertSynchronizeRenewPending(d)
if err != nil {
return err
}

d.SetId(fmt.Sprintf("%s/%s/%s", backend, name, commonName))
return pkiSecretBackendCertRead(d, meta)
}

func checkPKICertExpiry(expiration int64) bool {
expiry := time.Unix(expiration, 0)
now := time.Now()

return now.After(expiry)
}

func pkiCertAutoRenewCustomizeDiff(_ context.Context, d *schema.ResourceDiff, meta interface{}) error {
// The Create and Read functions will both set renew_pending if
// the current time is after the min_seconds_remaining timestamp. During
// planning we respond to that by proposing automatic renewal, if enabled.
if d.Id() == "" || !d.Get("auto_renew").(bool) {
return nil
}

expiration := int64(d.Get("expiration").(int) - d.Get("min_seconds_remaining").(int))
if checkPKICertExpiry(expiration) {
if d.Get("renew_pending").(bool) {
log.Printf("[DEBUG] certificate %q is due for renewal", d.Id())
if err := d.SetNewComputed("certificate"); err != nil {
return err
Expand All @@ -272,6 +276,12 @@ func pkiCertAutoRenewCustomizeDiff(_ context.Context, d *schema.ResourceDiff, me
return err
}

// Renewing the certificate will reset the value of renew_pending
d.SetNewComputed("renew_pending")
if err := d.ForceNew("renew_pending"); err != nil {
return err
}

return nil
}

Expand All @@ -295,8 +305,13 @@ func pkiSecretBackendCertRead(d *schema.ResourceData, meta interface{}) error {
return nil
}

// trigger a resource re-creation whenever the engine's mount has disappeared
if !enabled {
if enabled {
err := pkiSecretBackendCertSynchronizeRenewPending(d)
if err != nil {
return err
}
} else {
// trigger a resource re-creation whenever the engine's mount has disappeared
log.Printf("[WARN] Mount %q does not exist, setting resource for re-creation", path)
d.SetId("")
}
Expand Down Expand Up @@ -344,6 +359,32 @@ func pkiSecretBackendCertPath(backend string, name string) string {
return strings.Trim(backend, "/") + "/issue/" + strings.Trim(name, "/")
}

// pkiSecretBackendCertSynchronizeRenewPending calculates whether the
// expiration time of the certificate is fewer than min_seconds_remaining
// seconds in the future (relative to the current system time), and then
// updates the renew_pending attribute accordingly.
func pkiSecretBackendCertSynchronizeRenewPending(d *schema.ResourceData) error {
if _, ok := d.Get("renew_pending").(bool); !ok {
// pkiSecretBackendCertRead is shared between vault_pki_secret_backend_cert
// and vault_pki_secret_backend_root_cert, and the latter doesn't have
// an auto-renew mechanism so doesn't have a "renew_pending" attribute
// to update.
return nil
}

expiration := d.Get("expiration").(int)
earlyRenew := d.Get("min_seconds_remaining").(int)
effectiveExpiration := int64(expiration - earlyRenew)
return d.Set("renew_pending", checkPKICertExpiry(effectiveExpiration))
}

func checkPKICertExpiry(expiration int64) bool {
expiry := time.Unix(expiration, 0)
now := time.Now()

return now.After(expiry)
}

func convertIntoSliceOfString(slice interface{}) []string {
intSlice := slice.([]interface{})
strSlice := make([]string, len(intSlice))
Expand Down
8 changes: 8 additions & 0 deletions vault/resource_pki_secret_backend_cert_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ func TestPkiSecretBackendCert_renew(t *testing.T) {
resource.TestCheckResourceAttr(resourceName, "revoke", "false"),
resource.TestCheckResourceAttrSet(resourceName, "expiration"),
resource.TestCheckResourceAttrSet(resourceName, "serial_number"),
resource.TestCheckResourceAttrSet(resourceName, "renew_pending"),
}

resource.Test(t, resource.TestCase{
Expand All @@ -200,6 +201,13 @@ func TestPkiSecretBackendCert_renew(t *testing.T) {
},
{
// test renewal based on cert expiry
// NOTE: Ideally we'd also directly test that the refreshed
// state has renew_pending set to true before creating the plan,
// but the test harness only exposes the state after applying
// the plan so we can't make assertions against the intermediate
// refresh and planning steps. Therefore we're only testing
// that renew_pending got set to true indirectly by observing
// that it then caused the certificate to get re-issued.
PreConfig: testWaitCertExpiry(store),
Config: testPkiSecretBackendCertConfig_renew(path),
Check: resource.ComposeTestCheckFunc(
Expand Down
2 changes: 2 additions & 0 deletions website/docs/r/pki_secret_backend_cert.html.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,5 @@ In addition to the fields above, the following attributes are exported:
* `serial_number` - The serial number

* `expiration` - The expiration date of the certificate in unix epoch format

* `renew_pending` - `true` if the current time (during refresh) is after the start of the early renewal window declared by `min_seconds_remaining`, and `false` otherwise; if `auto_renew` is set to `true` then the provider will plan to replace the certificate once renewal is pending.

0 comments on commit da6005d

Please sign in to comment.