Retry certificate renewal after failure

This commit is contained in:
Melon 2024-09-21 13:11:51 +01:00
parent 7e65015b89
commit d0fc76cd73
Signed by: melon
GPG Key ID: 6C9D970C50D26A25
8 changed files with 86 additions and 58 deletions

View File

@ -75,7 +75,7 @@ FROM certificates AS cert
WHERE cert.active = 1
AND (cert.auto_renew = 1 OR cert.not_after IS NULL)
AND cert.renewing = 0
AND cert.renew_failed = 0
AND DATETIME() > DATETIME(cert.renew_retry)
AND (cert.not_after IS NULL OR DATETIME(cert.not_after, 'utc', '-30 days') < DATETIME())
ORDER BY cert.temp_parent, cert.not_after DESC NULLS FIRST
LIMIT 1
@ -107,7 +107,7 @@ SELECT cert.id,
cert.auto_renew,
cert.active,
cert.renewing,
cert.renew_failed,
cert.renew_retry,
cert.not_after,
cert.updated_at,
certificate_domains.domain
@ -116,14 +116,14 @@ FROM certificates AS cert
`
type FindOwnedCertsRow struct {
ID int64 `json:"id"`
AutoRenew bool `json:"auto_renew"`
Active bool `json:"active"`
Renewing bool `json:"renewing"`
RenewFailed bool `json:"renew_failed"`
NotAfter time.Time `json:"not_after"`
UpdatedAt time.Time `json:"updated_at"`
Domain string `json:"domain"`
ID int64 `json:"id"`
AutoRenew bool `json:"auto_renew"`
Active bool `json:"active"`
Renewing bool `json:"renewing"`
RenewRetry time.Time `json:"renew_retry"`
NotAfter time.Time `json:"not_after"`
UpdatedAt time.Time `json:"updated_at"`
Domain string `json:"domain"`
}
func (q *Queries) FindOwnedCerts(ctx context.Context) ([]FindOwnedCertsRow, error) {
@ -140,7 +140,7 @@ func (q *Queries) FindOwnedCerts(ctx context.Context) ([]FindOwnedCertsRow, erro
&i.AutoRenew,
&i.Active,
&i.Renewing,
&i.RenewFailed,
&i.RenewRetry,
&i.NotAfter,
&i.UpdatedAt,
&i.Domain,
@ -169,10 +169,21 @@ func (q *Queries) RemoveCertificate(ctx context.Context, id int64) error {
return err
}
const setRetryFlag = `-- name: SetRetryFlag :exec
UPDATE certificates
SET renew_retry = DATETIME('now', '+1 day')
WHERE id = ?
`
func (q *Queries) SetRetryFlag(ctx context.Context, id int64) error {
_, err := q.db.ExecContext(ctx, setRetryFlag, id)
return err
}
const updateCertAfterRenewal = `-- name: UpdateCertAfterRenewal :exec
UPDATE certificates
SET renewing = 0,
renew_failed=0,
SET renewing = 0,
renew_retry=0,
not_after=?,
updated_at=?
WHERE id = ?
@ -191,18 +202,18 @@ func (q *Queries) UpdateCertAfterRenewal(ctx context.Context, arg UpdateCertAfte
const updateRenewingState = `-- name: UpdateRenewingState :exec
UPDATE certificates
SET renewing = ?,
renew_failed = ?
SET renewing = ?,
renew_retry = ?
WHERE id = ?
`
type UpdateRenewingStateParams struct {
Renewing bool `json:"renewing"`
RenewFailed bool `json:"renew_failed"`
ID int64 `json:"id"`
Renewing bool `json:"renewing"`
RenewRetry time.Time `json:"renew_retry"`
ID int64 `json:"id"`
}
func (q *Queries) UpdateRenewingState(ctx context.Context, arg UpdateRenewingStateParams) error {
_, err := q.db.ExecContext(ctx, updateRenewingState, arg.Renewing, arg.RenewFailed, arg.ID)
_, err := q.db.ExecContext(ctx, updateRenewingState, arg.Renewing, arg.RenewRetry, arg.ID)
return err
}

View File

@ -0,0 +1,5 @@
ALTER TABLE certificates
DROP COLUMN renew_failed;
ALTER TABLE certificates
ADD COLUMN renew_retry DATETIME NOT NULL DEFAULT 0;

View File

@ -10,16 +10,16 @@ import (
)
type Certificate struct {
ID int64 `json:"id"`
Owner string `json:"owner"`
Dns sql.NullInt64 `json:"dns"`
AutoRenew bool `json:"auto_renew"`
Active bool `json:"active"`
Renewing bool `json:"renewing"`
RenewFailed bool `json:"renew_failed"`
NotAfter time.Time `json:"not_after"`
UpdatedAt time.Time `json:"updated_at"`
TempParent sql.NullInt64 `json:"temp_parent"`
ID int64 `json:"id"`
Owner string `json:"owner"`
Dns sql.NullInt64 `json:"dns"`
AutoRenew bool `json:"auto_renew"`
Active bool `json:"active"`
Renewing bool `json:"renewing"`
NotAfter time.Time `json:"not_after"`
UpdatedAt time.Time `json:"updated_at"`
TempParent sql.NullInt64 `json:"temp_parent"`
RenewRetry time.Time `json:"renew_retry"`
}
type CertificateDomain struct {

View File

@ -5,7 +5,7 @@ FROM certificates AS cert
WHERE cert.active = 1
AND (cert.auto_renew = 1 OR cert.not_after IS NULL)
AND cert.renewing = 0
AND cert.renew_failed = 0
AND DATETIME() > DATETIME(cert.renew_retry)
AND (cert.not_after IS NULL OR DATETIME(cert.not_after, 'utc', '-30 days') < DATETIME())
ORDER BY cert.temp_parent, cert.not_after DESC NULLS FIRST
LIMIT 1;
@ -15,7 +15,7 @@ SELECT cert.id,
cert.auto_renew,
cert.active,
cert.renewing,
cert.renew_failed,
cert.renew_retry,
cert.not_after,
cert.updated_at,
certificate_domains.domain
@ -24,14 +24,19 @@ FROM certificates AS cert
-- name: UpdateRenewingState :exec
UPDATE certificates
SET renewing = ?,
renew_failed = ?
SET renewing = ?,
renew_retry = ?
WHERE id = ?;
-- name: SetRetryFlag :exec
UPDATE certificates
SET renew_retry = DATETIME('now', '+1 day')
WHERE id = ?;
-- name: UpdateCertAfterRenewal :exec
UPDATE certificates
SET renewing = 0,
renew_failed=0,
SET renewing = 0,
renew_retry=0,
not_after=?,
updated_at=?
WHERE id = ?;

View File

@ -399,13 +399,14 @@ func (s *Service) getPrivateKey(id int64) (*rsa.PrivateKey, error) {
// certificate to the certDir directory.
func (s *Service) renewCert(localData *localCertData) error {
// database synchronous state
s.setRenewing(localData.id, true, false)
s.setRenewing(localData.id, true)
Logger.Debug("No certificates to update")
// run internal renewal code and log errors
cert, certBytes, err := s.renewCertInternal(localData)
if err != nil {
s.setRenewing(localData.id, false, true)
s.setRenewing(localData.id, false)
s.setRetry(localData.id)
return fmt.Errorf("failed to renew cert %d: %w", localData.id, err)
}
@ -501,14 +502,20 @@ func (s *Service) renewCertInternal(localData *localCertData) (*x509.Certificate
// setRenewing sets the renewing and failed states in the database for a
// specified certificate id.
func (s *Service) setRenewing(id int64, renewing, failed bool) {
func (s *Service) setRenewing(id int64, renewing bool) {
err := s.db.UpdateRenewingState(context.Background(), database.UpdateRenewingStateParams{
Renewing: renewing,
RenewFailed: failed,
ID: id,
Renewing: renewing,
ID: id,
})
if err != nil {
Logger.Warn("Failed to set renewing/failed mode in database", "id", id, "err", err)
Logger.Warn("Failed to set renewing mode in database", "id", id, "err", err)
}
}
func (s *Service) setRetry(id int64) {
err := s.db.SetRetryFlag(context.Background(), id)
if err != nil {
Logger.Warn("Failed to set retry time in database", "id", id, "err", err)
}
}

View File

@ -170,7 +170,7 @@ func TestPebbleRenewal(t *testing.T) {
_, err := db2.Exec("DELETE FROM certificate_domains")
assert.NoError(t, err)
_, err = db2.Exec(`INSERT INTO certificates (owner, dns, auto_renew, active, renewing, renew_failed, not_after, updated_at) VALUES (1, 1, 1, 1, 0, 0, "2000-01-01 00:00:00+00:00", "2000-01-01 00:00:00+00:00")`)
_, err = db2.Exec(`INSERT INTO certificates (owner, dns, auto_renew, active, renewing, renew_retry, not_after, updated_at) VALUES (1, 1, 1, 1, 0, '2000-01-01 00:00:00+00:00', '2000-01-01 00:00:00+00:00', '2000-01-01 00:00:00+00:00');`)
assert.NoError(t, err)
for _, j := range i.domains {
_, err = db2.Exec(`INSERT INTO certificate_domains (cert_id, domain) VALUES (1, ?)`, j)

View File

@ -25,14 +25,14 @@ type DomainStateValue struct {
}
type Certificate struct {
Id int64 `json:"id"`
AutoRenew bool `json:"auto_renew"`
Active bool `json:"active"`
Renewing bool `json:"renewing"`
RenewFailed bool `json:"renew_failed"`
NotAfter time.Time `json:"not_after"`
UpdatedAt time.Time `json:"updated_at"`
Domains []string `json:"domains"`
Id int64 `json:"id"`
AutoRenew bool `json:"auto_renew"`
Active bool `json:"active"`
Renewing bool `json:"renewing"`
RenewRetry time.Time `json:"renew_retry"`
NotAfter time.Time `json:"not_after"`
UpdatedAt time.Time `json:"updated_at"`
Domains []string `json:"domains"`
}
// NewApiServer creates and runs a http server containing all the API
@ -69,13 +69,13 @@ func NewApiServer(listen string, db *database.Queries, signer *mjwt.KeyStore, do
// loop over query rows
for _, row := range rows {
c := Certificate{
Id: row.ID,
AutoRenew: row.AutoRenew,
Active: row.Active,
Renewing: row.Renewing,
RenewFailed: row.RenewFailed,
NotAfter: row.NotAfter,
UpdatedAt: row.UpdatedAt,
Id: row.ID,
AutoRenew: row.AutoRenew,
Active: row.Active,
Renewing: row.Renewing,
RenewRetry: row.RenewRetry,
NotAfter: row.NotAfter,
UpdatedAt: row.UpdatedAt,
}
d := row.Domain