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 WHERE cert.active = 1
AND (cert.auto_renew = 1 OR cert.not_after IS NULL) AND (cert.auto_renew = 1 OR cert.not_after IS NULL)
AND cert.renewing = 0 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()) 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 ORDER BY cert.temp_parent, cert.not_after DESC NULLS FIRST
LIMIT 1 LIMIT 1
@ -107,7 +107,7 @@ SELECT cert.id,
cert.auto_renew, cert.auto_renew,
cert.active, cert.active,
cert.renewing, cert.renewing,
cert.renew_failed, cert.renew_retry,
cert.not_after, cert.not_after,
cert.updated_at, cert.updated_at,
certificate_domains.domain certificate_domains.domain
@ -116,14 +116,14 @@ FROM certificates AS cert
` `
type FindOwnedCertsRow struct { type FindOwnedCertsRow struct {
ID int64 `json:"id"` ID int64 `json:"id"`
AutoRenew bool `json:"auto_renew"` AutoRenew bool `json:"auto_renew"`
Active bool `json:"active"` Active bool `json:"active"`
Renewing bool `json:"renewing"` Renewing bool `json:"renewing"`
RenewFailed bool `json:"renew_failed"` RenewRetry time.Time `json:"renew_retry"`
NotAfter time.Time `json:"not_after"` NotAfter time.Time `json:"not_after"`
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
Domain string `json:"domain"` Domain string `json:"domain"`
} }
func (q *Queries) FindOwnedCerts(ctx context.Context) ([]FindOwnedCertsRow, error) { 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.AutoRenew,
&i.Active, &i.Active,
&i.Renewing, &i.Renewing,
&i.RenewFailed, &i.RenewRetry,
&i.NotAfter, &i.NotAfter,
&i.UpdatedAt, &i.UpdatedAt,
&i.Domain, &i.Domain,
@ -169,10 +169,21 @@ func (q *Queries) RemoveCertificate(ctx context.Context, id int64) error {
return err 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 const updateCertAfterRenewal = `-- name: UpdateCertAfterRenewal :exec
UPDATE certificates UPDATE certificates
SET renewing = 0, SET renewing = 0,
renew_failed=0, renew_retry=0,
not_after=?, not_after=?,
updated_at=? updated_at=?
WHERE id = ? WHERE id = ?
@ -191,18 +202,18 @@ func (q *Queries) UpdateCertAfterRenewal(ctx context.Context, arg UpdateCertAfte
const updateRenewingState = `-- name: UpdateRenewingState :exec const updateRenewingState = `-- name: UpdateRenewingState :exec
UPDATE certificates UPDATE certificates
SET renewing = ?, SET renewing = ?,
renew_failed = ? renew_retry = ?
WHERE id = ? WHERE id = ?
` `
type UpdateRenewingStateParams struct { type UpdateRenewingStateParams struct {
Renewing bool `json:"renewing"` Renewing bool `json:"renewing"`
RenewFailed bool `json:"renew_failed"` RenewRetry time.Time `json:"renew_retry"`
ID int64 `json:"id"` ID int64 `json:"id"`
} }
func (q *Queries) UpdateRenewingState(ctx context.Context, arg UpdateRenewingStateParams) error { 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 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 { type Certificate struct {
ID int64 `json:"id"` ID int64 `json:"id"`
Owner string `json:"owner"` Owner string `json:"owner"`
Dns sql.NullInt64 `json:"dns"` Dns sql.NullInt64 `json:"dns"`
AutoRenew bool `json:"auto_renew"` AutoRenew bool `json:"auto_renew"`
Active bool `json:"active"` Active bool `json:"active"`
Renewing bool `json:"renewing"` Renewing bool `json:"renewing"`
RenewFailed bool `json:"renew_failed"` NotAfter time.Time `json:"not_after"`
NotAfter time.Time `json:"not_after"` UpdatedAt time.Time `json:"updated_at"`
UpdatedAt time.Time `json:"updated_at"` TempParent sql.NullInt64 `json:"temp_parent"`
TempParent sql.NullInt64 `json:"temp_parent"` RenewRetry time.Time `json:"renew_retry"`
} }
type CertificateDomain struct { type CertificateDomain struct {

View File

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

View File

@ -399,13 +399,14 @@ func (s *Service) getPrivateKey(id int64) (*rsa.PrivateKey, error) {
// certificate to the certDir directory. // certificate to the certDir directory.
func (s *Service) renewCert(localData *localCertData) error { func (s *Service) renewCert(localData *localCertData) error {
// database synchronous state // database synchronous state
s.setRenewing(localData.id, true, false) s.setRenewing(localData.id, true)
Logger.Debug("No certificates to update") Logger.Debug("No certificates to update")
// run internal renewal code and log errors // run internal renewal code and log errors
cert, certBytes, err := s.renewCertInternal(localData) cert, certBytes, err := s.renewCertInternal(localData)
if err != nil { 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) 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 // setRenewing sets the renewing and failed states in the database for a
// specified certificate id. // 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{ err := s.db.UpdateRenewingState(context.Background(), database.UpdateRenewingStateParams{
Renewing: renewing, Renewing: renewing,
RenewFailed: failed, ID: id,
ID: id,
}) })
if err != nil { 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") _, err := db2.Exec("DELETE FROM certificate_domains")
assert.NoError(t, err) 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) assert.NoError(t, err)
for _, j := range i.domains { for _, j := range i.domains {
_, err = db2.Exec(`INSERT INTO certificate_domains (cert_id, domain) VALUES (1, ?)`, j) _, 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 { type Certificate struct {
Id int64 `json:"id"` Id int64 `json:"id"`
AutoRenew bool `json:"auto_renew"` AutoRenew bool `json:"auto_renew"`
Active bool `json:"active"` Active bool `json:"active"`
Renewing bool `json:"renewing"` Renewing bool `json:"renewing"`
RenewFailed bool `json:"renew_failed"` RenewRetry time.Time `json:"renew_retry"`
NotAfter time.Time `json:"not_after"` NotAfter time.Time `json:"not_after"`
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
Domains []string `json:"domains"` Domains []string `json:"domains"`
} }
// NewApiServer creates and runs a http server containing all the API // 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 // loop over query rows
for _, row := range rows { for _, row := range rows {
c := Certificate{ c := Certificate{
Id: row.ID, Id: row.ID,
AutoRenew: row.AutoRenew, AutoRenew: row.AutoRenew,
Active: row.Active, Active: row.Active,
Renewing: row.Renewing, Renewing: row.Renewing,
RenewFailed: row.RenewFailed, RenewRetry: row.RenewRetry,
NotAfter: row.NotAfter, NotAfter: row.NotAfter,
UpdatedAt: row.UpdatedAt, UpdatedAt: row.UpdatedAt,
} }
d := row.Domain d := row.Domain