diff --git a/database/certificate.sql.go b/database/certificate.sql.go new file mode 100644 index 0000000..a217602 --- /dev/null +++ b/database/certificate.sql.go @@ -0,0 +1,192 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.25.0 +// source: certificate.sql + +package database + +import ( + "context" + "database/sql" + "time" +) + +const addCertificate = `-- name: AddCertificate :exec +INSERT INTO certificates (owner, dns, not_after, updated_at) +VALUES (?, ?, ?, ?) +` + +type AddCertificateParams struct { + Owner string `json:"owner"` + Dns sql.NullInt64 `json:"dns"` + NotAfter time.Time `json:"not_after"` + UpdatedAt time.Time `json:"updated_at"` +} + +func (q *Queries) AddCertificate(ctx context.Context, arg AddCertificateParams) error { + _, err := q.db.ExecContext(ctx, addCertificate, + arg.Owner, + arg.Dns, + arg.NotAfter, + arg.UpdatedAt, + ) + return err +} + +const checkCertOwner = `-- name: CheckCertOwner :one +SELECT id, owner +FROM certificates +WHERE active = 1 + and id = ? +` + +type CheckCertOwnerRow struct { + ID int64 `json:"id"` + Owner string `json:"owner"` +} + +func (q *Queries) CheckCertOwner(ctx context.Context, id int64) (CheckCertOwnerRow, error) { + row := q.db.QueryRowContext(ctx, checkCertOwner, id) + var i CheckCertOwnerRow + err := row.Scan(&i.ID, &i.Owner) + return i, err +} + +const findNextCert = `-- name: FindNextCert :one +SELECT cert.id, cert.not_after, dns_acme.type, dns_acme.token, cert.temp_parent +FROM certificates AS cert + LEFT OUTER JOIN dns_acme ON cert.dns = dns_acme.id +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 (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 +` + +type FindNextCertRow struct { + ID int64 `json:"id"` + NotAfter time.Time `json:"not_after"` + Type sql.NullString `json:"type"` + Token sql.NullString `json:"token"` + TempParent sql.NullInt64 `json:"temp_parent"` +} + +func (q *Queries) FindNextCert(ctx context.Context) (FindNextCertRow, error) { + row := q.db.QueryRowContext(ctx, findNextCert) + var i FindNextCertRow + err := row.Scan( + &i.ID, + &i.NotAfter, + &i.Type, + &i.Token, + &i.TempParent, + ) + return i, err +} + +const findOwnedCerts = `-- name: FindOwnedCerts :many +SELECT cert.id, + cert.auto_renew, + cert.active, + cert.renewing, + cert.renew_failed, + cert.not_after, + cert.updated_at, + certificate_domains.domain +FROM certificates AS cert + INNER JOIN certificate_domains ON cert.id = certificate_domains.cert_id +` + +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"` +} + +func (q *Queries) FindOwnedCerts(ctx context.Context) ([]FindOwnedCertsRow, error) { + rows, err := q.db.QueryContext(ctx, findOwnedCerts) + if err != nil { + return nil, err + } + defer rows.Close() + var items []FindOwnedCertsRow + for rows.Next() { + var i FindOwnedCertsRow + if err := rows.Scan( + &i.ID, + &i.AutoRenew, + &i.Active, + &i.Renewing, + &i.RenewFailed, + &i.NotAfter, + &i.UpdatedAt, + &i.Domain, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const removeCertificate = `-- name: RemoveCertificate :exec +UPDATE certificates +SET active = 0 +WHERE id = ? +` + +func (q *Queries) RemoveCertificate(ctx context.Context, id int64) error { + _, err := q.db.ExecContext(ctx, removeCertificate, id) + return err +} + +const updateCertAfterRenewal = `-- name: UpdateCertAfterRenewal :exec +UPDATE certificates +SET renewing = 0, + renew_failed=0, + not_after=?, + updated_at=? +WHERE id = ? +` + +type UpdateCertAfterRenewalParams struct { + NotAfter time.Time `json:"not_after"` + UpdatedAt time.Time `json:"updated_at"` + ID int64 `json:"id"` +} + +func (q *Queries) UpdateCertAfterRenewal(ctx context.Context, arg UpdateCertAfterRenewalParams) error { + _, err := q.db.ExecContext(ctx, updateCertAfterRenewal, arg.NotAfter, arg.UpdatedAt, arg.ID) + return err +} + +const updateRenewingState = `-- name: UpdateRenewingState :exec +UPDATE certificates +SET renewing = ?, + renew_failed = ? +WHERE id = ? +` + +type UpdateRenewingStateParams struct { + Renewing bool `json:"renewing"` + RenewFailed bool `json:"renew_failed"` + 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) + return err +} diff --git a/database/certificate_domains.sql.go b/database/certificate_domains.sql.go new file mode 100644 index 0000000..678ba0f --- /dev/null +++ b/database/certificate_domains.sql.go @@ -0,0 +1,121 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.25.0 +// source: certificate_domains.sql + +package database + +import ( + "context" +) + +const addDomains = `-- name: AddDomains :exec +INSERT INTO certificate_domains (cert_id, domain, state) +VALUES (?, ?, ?) +` + +type AddDomainsParams struct { + CertID int64 `json:"cert_id"` + Domain string `json:"domain"` + State int64 `json:"state"` +} + +func (q *Queries) AddDomains(ctx context.Context, arg AddDomainsParams) error { + _, err := q.db.ExecContext(ctx, addDomains, arg.CertID, arg.Domain, arg.State) + return err +} + +const getDomainStatesForCert = `-- name: GetDomainStatesForCert :many +SELECT domain, state +FROM certificate_domains +WHERE cert_id = ? +` + +type GetDomainStatesForCertRow struct { + Domain string `json:"domain"` + State int64 `json:"state"` +} + +func (q *Queries) GetDomainStatesForCert(ctx context.Context, certID int64) ([]GetDomainStatesForCertRow, error) { + rows, err := q.db.QueryContext(ctx, getDomainStatesForCert, certID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetDomainStatesForCertRow + for rows.Next() { + var i GetDomainStatesForCertRow + if err := rows.Scan(&i.Domain, &i.State); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getDomainsForCertificate = `-- name: GetDomainsForCertificate :many +SELECT domain +FROM certificate_domains +WHERE cert_id = ? +` + +func (q *Queries) GetDomainsForCertificate(ctx context.Context, certID int64) ([]string, error) { + rows, err := q.db.QueryContext(ctx, getDomainsForCertificate, certID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []string + for rows.Next() { + var domain string + if err := rows.Scan(&domain); err != nil { + return nil, err + } + items = append(items, domain) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const setDomainStateForCert = `-- name: SetDomainStateForCert :exec +UPDATE certificate_domains +SET state = ? +WHERE cert_id = ? +` + +type SetDomainStateForCertParams struct { + State int64 `json:"state"` + CertID int64 `json:"cert_id"` +} + +func (q *Queries) SetDomainStateForCert(ctx context.Context, arg SetDomainStateForCertParams) error { + _, err := q.db.ExecContext(ctx, setDomainStateForCert, arg.State, arg.CertID) + return err +} + +const updateDomains = `-- name: UpdateDomains :exec +UPDATE certificate_domains +SET state = ? +WHERE domain IN ? +` + +type UpdateDomainsParams struct { + State int64 `json:"state"` + Domain string `json:"domain"` +} + +func (q *Queries) UpdateDomains(ctx context.Context, arg UpdateDomainsParams) error { + _, err := q.db.ExecContext(ctx, updateDomains, arg.State, arg.Domain) + return err +} diff --git a/database/db.go b/database/db.go new file mode 100644 index 0000000..61f5bf4 --- /dev/null +++ b/database/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.25.0 + +package database + +import ( + "context" + "database/sql" +) + +type DBTX interface { + ExecContext(context.Context, string, ...interface{}) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...interface{}) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/database/migrations/20240308160822_init.down.sql b/database/migrations/20240308160822_init.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/database/migrations/20240308160822_init.up.sql b/database/migrations/20240308160822_init.up.sql new file mode 100644 index 0000000..2f6bd97 --- /dev/null +++ b/database/migrations/20240308160822_init.up.sql @@ -0,0 +1,33 @@ +CREATE TABLE IF NOT EXISTS certificates +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + owner VARCHAR NOT NULL, + dns INTEGER, + auto_renew BOOLEAN NOT NULL DEFAULT 0, + active BOOLEAN NOT NULL DEFAULT 0, + renewing BOOLEAN NOT NULL DEFAULT 0, + renew_failed BOOLEAN NOT NULL DEFAULT 0, + not_after DATETIME NOT NULL, + updated_at DATETIME NOT NULL, + temp_parent INTEGER, + FOREIGN KEY (dns) REFERENCES dns_acme (id), + FOREIGN KEY (temp_parent) REFERENCES certificates (id) +); + +CREATE TABLE IF NOT EXISTS certificate_domains +( + domain_id INTEGER PRIMARY KEY AUTOINCREMENT, + cert_id INTEGER NOT NULL, + domain VARCHAR NOT NULL, + state INTEGER NOT NULL DEFAULT 1, + UNIQUE (cert_id, domain), + FOREIGN KEY (cert_id) REFERENCES certificates (id) +); + +CREATE TABLE IF NOT EXISTS dns_acme +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + type VARCHAR NOT NULL, + email VARCHAR NOT NULL, + token VARCHAR NOT NULL +); diff --git a/database/models.go b/database/models.go new file mode 100644 index 0000000..4140fad --- /dev/null +++ b/database/models.go @@ -0,0 +1,37 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.25.0 + +package database + +import ( + "database/sql" + "time" +) + +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"` +} + +type CertificateDomain struct { + DomainID int64 `json:"domain_id"` + CertID int64 `json:"cert_id"` + Domain string `json:"domain"` + State int64 `json:"state"` +} + +type DnsAcme struct { + ID int64 `json:"id"` + Type string `json:"type"` + Email string `json:"email"` + Token string `json:"token"` +} diff --git a/database/queries/certificate.sql b/database/queries/certificate.sql new file mode 100644 index 0000000..15eb34b --- /dev/null +++ b/database/queries/certificate.sql @@ -0,0 +1,52 @@ +-- name: FindNextCert :one +SELECT cert.id, cert.not_after, dns_acme.type, dns_acme.token, cert.temp_parent +FROM certificates AS cert + LEFT OUTER JOIN dns_acme ON cert.dns = dns_acme.id +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 (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; + +-- name: FindOwnedCerts :many +SELECT cert.id, + cert.auto_renew, + cert.active, + cert.renewing, + cert.renew_failed, + cert.not_after, + cert.updated_at, + certificate_domains.domain +FROM certificates AS cert + INNER JOIN certificate_domains ON cert.id = certificate_domains.cert_id; + +-- name: UpdateRenewingState :exec +UPDATE certificates +SET renewing = ?, + renew_failed = ? +WHERE id = ?; + +-- name: UpdateCertAfterRenewal :exec +UPDATE certificates +SET renewing = 0, + renew_failed=0, + not_after=?, + updated_at=? +WHERE id = ?; + +-- name: AddCertificate :exec +INSERT INTO certificates (owner, dns, not_after, updated_at) +VALUES (?, ?, ?, ?); + +-- name: RemoveCertificate :exec +UPDATE certificates +SET active = 0 +WHERE id = ?; + +-- name: CheckCertOwner :one +SELECT id, owner +FROM certificates +WHERE active = 1 + and id = ?; diff --git a/database/queries/certificate_domains.sql b/database/queries/certificate_domains.sql new file mode 100644 index 0000000..08f35f5 --- /dev/null +++ b/database/queries/certificate_domains.sql @@ -0,0 +1,23 @@ +-- name: GetDomainsForCertificate :many +SELECT domain +FROM certificate_domains +WHERE cert_id = ?; + +-- name: GetDomainStatesForCert :many +SELECT domain, state +FROM certificate_domains +WHERE cert_id = ?; + +-- name: SetDomainStateForCert :exec +UPDATE certificate_domains +SET state = ? +WHERE cert_id = ?; + +-- name: AddDomains :exec +INSERT INTO certificate_domains (cert_id, domain, state) +VALUES (?, ?, ?); + +-- name: UpdateDomains :exec +UPDATE certificate_domains +SET state = ? +WHERE domain IN (sqlc.slice("domains")); diff --git a/database/tx.go b/database/tx.go new file mode 100644 index 0000000..2b1c0c7 --- /dev/null +++ b/database/tx.go @@ -0,0 +1,23 @@ +package database + +import ( + "context" + "database/sql" +) + +func (q *Queries) UseTx(ctx context.Context, cb func(tx *Queries) error) error { + sqlDB, ok := q.db.(*sql.DB) + if !ok { + panic("cannot open transaction without sql.DB") + } + tx, err := sqlDB.BeginTx(ctx, nil) + if err != nil { + return err + } + defer tx.Rollback() + err = cb(q.WithTx(tx)) + if err != nil { + return err + } + return tx.Commit() +} diff --git a/go.mod b/go.mod index ad1d4da..f24abfe 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/MrMelon54/certgen v0.0.1 github.com/MrMelon54/exit-reload v0.0.1 github.com/go-acme/lego/v4 v4.14.2 + github.com/golang-migrate/migrate/v4 v4.17.1 github.com/google/subcommands v1.2.0 github.com/google/uuid v1.4.0 github.com/julienschmidt/httprouter v1.3.0 @@ -25,6 +26,8 @@ require ( github.com/go-jose/go-jose/v3 v3.0.3 // indirect github.com/golang-jwt/jwt/v4 v4.5.0 // indirect github.com/google/go-querystring v1.1.0 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect @@ -32,9 +35,10 @@ require ( github.com/nrdcg/namesilo v0.2.1 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/crypto v0.19.0 // indirect + go.uber.org/atomic v1.7.0 // indirect + golang.org/x/crypto v0.20.0 // indirect golang.org/x/mod v0.14.0 // indirect - golang.org/x/net v0.19.0 // indirect + golang.org/x/net v0.21.0 // indirect golang.org/x/sys v0.17.0 // indirect golang.org/x/term v0.17.0 // indirect golang.org/x/text v0.14.0 // indirect diff --git a/go.sum b/go.sum index 9c8eec4..2736dbf 100644 --- a/go.sum +++ b/go.sum @@ -25,6 +25,8 @@ github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7 github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-migrate/migrate/v4 v4.17.1 h1:4zQ6iqL6t6AiItphxJctQb3cFqWiSpMnX7wLTPnnYO4= +github.com/golang-migrate/migrate/v4 v4.17.1/go.mod h1:m8hinFyWBn0SA4QKHuKh175Pm9wjmxj3S2Mia7dbXzM= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= @@ -35,6 +37,11 @@ github.com/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog= github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= @@ -45,6 +52,8 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= @@ -74,10 +83,13 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.20.0 h1:jmAMJJZXr5KiCw05dfYK9QnqaqKLYXijU23lsEdcQqg= +golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= @@ -87,8 +99,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= -golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= diff --git a/initdb.go b/initdb.go new file mode 100644 index 0000000..308a5aa --- /dev/null +++ b/initdb.go @@ -0,0 +1,38 @@ +package orchid + +import ( + "database/sql" + "embed" + "errors" + "github.com/1f349/orchid/database" + "github.com/golang-migrate/migrate/v4" + "github.com/golang-migrate/migrate/v4/database/sqlite3" + "github.com/golang-migrate/migrate/v4/source/iofs" +) + +//go:embed database/migrations/*.sql +var migrations embed.FS + +func InitDB(p string) (*database.Queries, error) { + migDrv, err := iofs.New(migrations, "database/migrations") + if err != nil { + return nil, err + } + dbOpen, err := sql.Open("sqlite3", p) + if err != nil { + return nil, err + } + dbDrv, err := sqlite3.WithInstance(dbOpen, &sqlite3.Config{}) + if err != nil { + return nil, err + } + mig, err := migrate.NewWithInstance("iofs", migDrv, "sqlite3", dbDrv) + if err != nil { + return nil, err + } + err = mig.Up() + if err != nil && !errors.Is(err, migrate.ErrNoChange) { + return nil, err + } + return database.New(dbOpen), nil +} diff --git a/renewal/create-tables.sql b/renewal/create-tables.sql deleted file mode 100644 index c594638..0000000 --- a/renewal/create-tables.sql +++ /dev/null @@ -1,33 +0,0 @@ -CREATE TABLE IF NOT EXISTS certificates -( - id INTEGER PRIMARY KEY AUTOINCREMENT, - owner VARCHAR, - dns INTEGER, - auto_renew INTEGER DEFAULT 0, - active INTEGER DEFAULT 0, - renewing INTEGER DEFAULT 0, - renew_failed INTEGER DEFAULT 0, - not_after DATETIME, - updated_at DATETIME, - temp_parent INTEGER DEFAULT 0, - FOREIGN KEY (dns) REFERENCES dns_acme (id), - FOREIGN KEY (temp_parent) REFERENCES certificates (id) -); - -CREATE TABLE IF NOT EXISTS certificate_domains -( - domain_id INTEGER PRIMARY KEY AUTOINCREMENT, - cert_id INTEGER, - domain VARCHAR, - state INTEGER DEFAULT 1, - UNIQUE (cert_id, domain), - FOREIGN KEY (cert_id) REFERENCES certificates (id) -); - -CREATE TABLE IF NOT EXISTS dns_acme -( - id INTEGER PRIMARY KEY AUTOINCREMENT, - type VARCHAR, - email VARCHAR, - token VARCHAR -); diff --git a/renewal/find-next-cert.sql b/renewal/find-next-cert.sql deleted file mode 100644 index 6f719b8..0000000 --- a/renewal/find-next-cert.sql +++ /dev/null @@ -1,9 +0,0 @@ -select cert.id, cert.not_after, dns_acme.type, dns_acme.token, cert.temp_parent -from certificates as cert - left outer join dns_acme on cert.dns = dns_acme.id -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 (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 diff --git a/renewal/local.go b/renewal/local.go index 10957b1..d83b800 100644 --- a/renewal/local.go +++ b/renewal/local.go @@ -2,16 +2,17 @@ package renewal import ( "database/sql" + "time" ) // Contains local types for the renewal service type localCertData struct { - id uint64 + id int64 dns struct { name sql.NullString token sql.NullString } - notAfter sql.NullTime + notAfter time.Time domains []string - tempParent uint64 + tempParent sql.NullInt64 } diff --git a/renewal/service.go b/renewal/service.go index bac3e0b..2ff2d96 100644 --- a/renewal/service.go +++ b/renewal/service.go @@ -2,6 +2,7 @@ package renewal import ( "bytes" + "context" "crypto/rsa" "crypto/tls" "crypto/x509" @@ -10,6 +11,7 @@ import ( "encoding/pem" "errors" "fmt" + "github.com/1f349/orchid/database" "github.com/1f349/orchid/pebble" "github.com/go-acme/lego/v4/certificate" "github.com/go-acme/lego/v4/challenge" @@ -18,7 +20,6 @@ import ( "github.com/go-acme/lego/v4/providers/dns/duckdns" "github.com/go-acme/lego/v4/providers/dns/namesilo" "github.com/go-acme/lego/v4/registration" - "io" "log" "math/rand" "net/http" @@ -28,13 +29,7 @@ import ( "time" ) -var ( - ErrUnsupportedDNSProvider = errors.New("unsupported DNS provider") - //go:embed find-next-cert.sql - findNextCertSql string - //go:embed create-tables.sql - createTableCertificates string -) +var ErrUnsupportedDNSProvider = errors.New("unsupported DNS provider") const ( DomainStateNormal = 0 @@ -57,7 +52,7 @@ var testDnsOptions interface { // `_acme-challenges` TXT records are updated to validate the ownership of the // specified domains. type Service struct { - db *sql.DB + db *database.Queries httpAcme challenge.Provider certTicker *time.Ticker certDone chan struct{} @@ -73,7 +68,7 @@ type Service struct { } // NewService creates a new certificate renewal service. -func NewService(wg *sync.WaitGroup, db *sql.DB, httpAcme challenge.Provider, leConfig LetsEncryptConfig, certDir, keyDir string) (*Service, error) { +func NewService(wg *sync.WaitGroup, db *database.Queries, httpAcme challenge.Provider, leConfig LetsEncryptConfig, certDir, keyDir string) (*Service, error) { s := &Service{ db: db, httpAcme: httpAcme, @@ -104,12 +99,6 @@ func NewService(wg *sync.WaitGroup, db *sql.DB, httpAcme challenge.Provider, leC return nil, fmt.Errorf("failed to resolve LetsEncrypt account private key: %w", err) } - // init domains table - _, err = s.db.Exec(createTableCertificates) - if err != nil { - return nil, fmt.Errorf("failed to create certificates table: %w", err) - } - // resolve CA information s.resolveCADirectory(leConfig.Directory) err = s.resolveCACertificate(leConfig.Certificate) @@ -286,50 +275,30 @@ func (s *Service) findNextCertificateToRenew() (*localCertData, error) { d := &localCertData{} // sql or something, the query is in `find-next-cert.sql` - row, err := s.db.Query(findNextCertSql) + row, err := s.db.FindNextCert(context.Background()) + if errors.Is(err, sql.ErrNoRows) { + return nil, nil + } if err != nil { return nil, fmt.Errorf("failed to run query: %w", err) } - defer row.Close() - // if next fails no rows were found - if !row.Next() { - return nil, nil - } - - // scan the first row - err = row.Scan(&d.id, &d.notAfter, &d.dns.name, &d.dns.token, &d.tempParent) - switch err { - case nil: - // no nothing - break - case io.EOF: - // no certificate to update - return nil, nil - default: - return nil, fmt.Errorf("failed to scan table row: %w", err) - } + d.id = row.ID + d.dns.name = row.Type + d.dns.token = row.Token + d.notAfter = row.NotAfter + d.tempParent = row.TempParent return d, nil } func (s *Service) fetchDomains(localData *localCertData) ([]string, error) { // more sql: this one just grabs all the domains for a certificate - query, err := s.db.Query(`SELECT domain FROM certificate_domains WHERE cert_id = ?`, resolveTempParent(localData)) + domains, err := s.db.GetDomainsForCertificate(context.Background(), resolveTempParent(localData)) if err != nil { return nil, fmt.Errorf("failed to fetch domains for certificate: %d: %w", localData.id, err) } - // convert query responses to a string slice - domains := make([]string, 0) - for query.Next() { - var domain string - err := query.Scan(&domain) - if err != nil { - return nil, fmt.Errorf("failed to scan row from domains table: %d: %w", localData.id, err) - } - domains = append(domains, domain) - } // if no domains were found then the renewal will fail if len(domains) == 0 { return nil, fmt.Errorf("no domains registered for certificate: %d", localData.id) @@ -391,7 +360,7 @@ func (s *Service) getDnsProvider(name, token string) (challenge.Provider, error) // getPrivateKey reads the private key for the specified certificate id, or // generates one is the file doesn't exist -func (s *Service) getPrivateKey(id uint64) (*rsa.PrivateKey, error) { +func (s *Service) getPrivateKey(id int64) (*rsa.PrivateKey, error) { fPath := filepath.Join(s.keyDir, fmt.Sprintf("%d.key.pem", id)) pemBytes, err := os.ReadFile(fPath) if err != nil { @@ -433,13 +402,20 @@ func (s *Service) renewCert(localData *localCertData) error { } // set the NotAfter/NotBefore in the database - _, err = s.db.Exec(`UPDATE certificates SET renewing = 0, renew_failed = 0, not_after = ?, updated_at = ? WHERE id = ?`, cert.NotAfter, cert.NotBefore, localData.id) + err = s.db.UpdateCertAfterRenewal(context.Background(), database.UpdateCertAfterRenewalParams{ + NotAfter: cert.NotAfter, + UpdatedAt: cert.NotBefore, + ID: localData.id, + }) if err != nil { return fmt.Errorf("failed to update cert %d in database: %w", localData.id, err) } // set domains to normal state - _, err = s.db.Exec(`UPDATE certificate_domains SET state = ? WHERE cert_id = ?`, DomainStateNormal, localData.id) + err = s.db.SetDomainStateForCert(context.Background(), database.SetDomainStateForCertParams{ + State: DomainStateNormal, + CertID: localData.id, + }) if err != nil { return fmt.Errorf("failed to update domains for %d in database: %w", localData.id, err) } @@ -517,8 +493,12 @@ 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 uint64, renewing, failed bool) { - _, err := s.db.Exec("UPDATE certificates SET renewing = ?, renew_failed = ? WHERE id = ?", renewing, failed, id) +func (s *Service) setRenewing(id int64, renewing, failed bool) { + err := s.db.UpdateRenewingState(context.Background(), database.UpdateRenewingStateParams{ + Renewing: renewing, + RenewFailed: failed, + ID: id, + }) if err != nil { log.Printf("[Renewal] Failed to set renewing/failed mode in database %d: %s\n", id, err) } @@ -526,7 +506,7 @@ func (s *Service) setRenewing(id uint64, renewing, failed bool) { // writeCertFile writes the output certificate file and renames the current one // to include `-old` in the name. -func (s *Service) writeCertFile(id uint64, certBytes []byte) error { +func (s *Service) writeCertFile(id int64, certBytes []byte) error { oldPath := filepath.Join(s.certDir, fmt.Sprintf("%d-old.cert.pem", id)) newPath := filepath.Join(s.certDir, fmt.Sprintf("%d.cert.pem", id)) @@ -552,9 +532,9 @@ func (s *Service) writeCertFile(id uint64, certBytes []byte) error { return nil } -func resolveTempParent(local *localCertData) uint64 { - if local.tempParent > 0 { - return local.tempParent +func resolveTempParent(local *localCertData) int64 { + if local.tempParent.Valid { + return local.tempParent.Int64 } return local.id } diff --git a/servers/api.go b/servers/api.go index 4ca3d63..042532e 100644 --- a/servers/api.go +++ b/servers/api.go @@ -1,12 +1,14 @@ package servers import ( + "context" "database/sql" _ "embed" "encoding/json" "fmt" "github.com/1f349/mjwt" "github.com/1f349/mjwt/claims" + "github.com/1f349/orchid/database" oUtils "github.com/1f349/orchid/utils" vUtils "github.com/1f349/violet/utils" "github.com/julienschmidt/httprouter" @@ -22,7 +24,7 @@ type DomainStateValue struct { } type Certificate struct { - Id int `json:"id"` + Id int64 `json:"id"` AutoRenew bool `json:"auto_renew"` Active bool `json:"active"` Renewing bool `json:"renewing"` @@ -32,14 +34,11 @@ type Certificate struct { Domains []string `json:"domains"` } -//go:embed find-owned-certs.sql -var findOwnedCerts string - // NewApiServer creates and runs a http server containing all the API // endpoints for the software // // `/cert` - edit certificate -func NewApiServer(listen string, db *sql.DB, signer mjwt.Verifier, domains oUtils.DomainChecker) *http.Server { +func NewApiServer(listen string, db *database.Queries, signer mjwt.Verifier, domains oUtils.DomainChecker) *http.Server { r := httprouter.New() r.GET("/", func(rw http.ResponseWriter, req *http.Request, params httprouter.Params) { @@ -55,25 +54,28 @@ func NewApiServer(listen string, db *sql.DB, signer mjwt.Verifier, domains oUtil } // query database - query, err := db.Query(findOwnedCerts) + rows, err := db.FindOwnedCerts(context.Background()) if err != nil { + log.Println("Failed after reading certificates from database: ", err) http.Error(rw, "Database Error", http.StatusInternalServerError) return } - mOther := make(map[int]*Certificate) // other certificates - m := make(map[int]*Certificate) // certificates owned by this user + mOther := make(map[int64]*Certificate) // other certificates + m := make(map[int64]*Certificate) // certificates owned by this user // loop over query rows - for query.Next() { - var c Certificate - var d string - err := query.Scan(&c.Id, &c.AutoRenew, &c.Active, &c.Renewing, &c.RenewFailed, &c.NotAfter, &c.UpdatedAt, &d) - if err != nil { - log.Println("Failed to read certificate from database: ", err) - http.Error(rw, "Database Error", http.StatusInternalServerError) - return + 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, } + d := row.Domain // check in owned map if cert, ok := m[c.Id]; ok { @@ -105,11 +107,6 @@ func NewApiServer(listen string, db *sql.DB, signer mjwt.Verifier, domains oUtil m[c.Id] = &c } } - if err := query.Err(); err != nil { - log.Println("Failed after reading certificates from database: ", err) - http.Error(rw, "Database Error", http.StatusInternalServerError) - return - } rw.WriteHeader(http.StatusOK) _ = json.NewEncoder(rw).Encode(m) })) @@ -124,15 +121,20 @@ func NewApiServer(listen string, db *sql.DB, signer mjwt.Verifier, domains oUtil })) r.POST("/cert", checkAuthWithPerm(signer, "orchid:cert", func(rw http.ResponseWriter, req *http.Request, params httprouter.Params, b AuthClaims) { - _, err := db.Exec(`INSERT INTO certificates (owner, dns, updated_at) VALUES (?, ?, ?)`, b.Subject, 0, time.Now()) + err := db.AddCertificate(req.Context(), database.AddCertificateParams{ + Owner: b.Subject, + Dns: sql.NullInt64{}, + NotAfter: time.Now(), + UpdatedAt: time.Now(), + }) if err != nil { apiError(rw, http.StatusInternalServerError, "Failed to delete certificate") return } rw.WriteHeader(http.StatusAccepted) })) - r.DELETE("/cert/:id", checkAuthForCertificate(signer, "orchid:cert", db, func(rw http.ResponseWriter, req *http.Request, params httprouter.Params, b AuthClaims, certId uint64) { - _, err := db.Exec(`UPDATE certificates SET active = 0 WHERE id = ?`, certId) + r.DELETE("/cert/:id", checkAuthForCertificate(signer, "orchid:cert", db, func(rw http.ResponseWriter, req *http.Request, params httprouter.Params, b AuthClaims, certId int64) { + err := db.RemoveCertificate(req.Context(), certId) if err != nil { apiError(rw, http.StatusInternalServerError, "Failed to delete certificate") return @@ -194,7 +196,7 @@ func apiError(rw http.ResponseWriter, code int, m string) { // lookupCertOwner finds the certificate matching the id string and returns the // numeric id, owner and possible error, only works for active certificates. -func checkCertOwner(db *sql.DB, idStr string, b AuthClaims) (uint64, error) { +func checkCertOwner(db *database.Queries, idStr string, b AuthClaims) (int64, error) { // parse the id rawId, err := strconv.ParseUint(idStr, 10, 64) if err != nil { @@ -202,54 +204,18 @@ func checkCertOwner(db *sql.DB, idStr string, b AuthClaims) (uint64, error) { } // run database query - row := db.QueryRow(`SELECT id, owner FROM certificates WHERE active = 1 and id = ?`, rawId) - - // scan in result values - var id uint64 - var owner string - err = row.Scan(&id, &owner) + row, err := db.CheckCertOwner(context.Background(), int64(rawId)) if err != nil { - return 0, fmt.Errorf("scan error: %w", err) + return 0, err } // check the owner is the mjwt token subject - if b.Subject != owner { - return id, fmt.Errorf("not the certificate owner") + if b.Subject != row.Owner { + return row.ID, fmt.Errorf("not the certificate owner") } // it's all valid, return the values - return id, nil -} - -// safeTransaction completes a database transaction safely allowing for rollbacks -// if the callback errors -func safeTransaction(rw http.ResponseWriter, db *sql.DB, cb func(rw http.ResponseWriter, tx *sql.Tx) error) error { - // start a transaction - begin, err := db.Begin() - if err != nil { - return fmt.Errorf("failed to begin a transaction") - } - - // init defer rollback - needsRollback := true - defer func() { - if needsRollback { - _ = begin.Rollback() - } - }() - - // run main code within the transaction session - err = cb(rw, begin) - if err != nil { - return err - } - - // clear the rollback flag and commit the transaction - needsRollback = false - if begin.Commit() != nil { - return fmt.Errorf("failed to commit a transaction") - } - return nil + return row.ID, nil } // getDomainOwnershipClaims returns the domains marked as owned from PermStorage, diff --git a/servers/auth.go b/servers/auth.go index 866e73b..ece2eae 100644 --- a/servers/auth.go +++ b/servers/auth.go @@ -1,9 +1,9 @@ package servers import ( - "database/sql" "github.com/1f349/mjwt" "github.com/1f349/mjwt/auth" + "github.com/1f349/orchid/database" vUtils "github.com/1f349/violet/utils" "github.com/julienschmidt/httprouter" "log" @@ -14,7 +14,7 @@ type AuthClaims mjwt.BaseTypeClaims[auth.AccessTokenClaims] type AuthCallback func(rw http.ResponseWriter, req *http.Request, params httprouter.Params, b AuthClaims) -type CertAuthCallback func(rw http.ResponseWriter, req *http.Request, params httprouter.Params, b AuthClaims, certId uint64) +type CertAuthCallback func(rw http.ResponseWriter, req *http.Request, params httprouter.Params, b AuthClaims, certId int64) // checkAuth validates the bearer token against a mjwt.Verifier and returns an // error message or continues to the next handler @@ -53,7 +53,7 @@ func checkAuthWithPerm(verify mjwt.Verifier, perm string, cb AuthCallback) httpr } // checkAuthForCertificate -func checkAuthForCertificate(verify mjwt.Verifier, perm string, db *sql.DB, cb CertAuthCallback) httprouter.Handle { +func checkAuthForCertificate(verify mjwt.Verifier, perm string, db *database.Queries, cb CertAuthCallback) httprouter.Handle { return checkAuthWithPerm(verify, perm, func(rw http.ResponseWriter, req *http.Request, params httprouter.Params, b AuthClaims) { // lookup certificate owner id, err := checkCertOwner(db, params.ByName("id"), b) diff --git a/servers/certDomainManage.go b/servers/certDomainManage.go index eb4941c..c6b6e9a 100644 --- a/servers/certDomainManage.go +++ b/servers/certDomainManage.go @@ -1,48 +1,37 @@ package servers import ( - "database/sql" + "context" "encoding/json" "fmt" "github.com/1f349/mjwt" + "github.com/1f349/orchid/database" "github.com/1f349/orchid/renewal" "github.com/1f349/orchid/utils" "github.com/julienschmidt/httprouter" "net/http" ) -func certDomainManageGET(db *sql.DB, signer mjwt.Verifier) httprouter.Handle { +func certDomainManageGET(db *database.Queries, signer mjwt.Verifier) httprouter.Handle { return checkAuthForCertificate(signer, "orchid:cert:edit", db, func(rw http.ResponseWriter, req *http.Request, params httprouter.Params, b AuthClaims, certId uint64) { - query, err := db.Query(`SELECT domain, state FROM certificate_domains WHERE cert_id = ?`, certId) + rows, err := db.GetDomainStatesForCert(context.Background(), int64(certId)) if err != nil { apiError(rw, http.StatusInsufficientStorage, "Database error") return } - // collect all the domains and state values - var domainStates []DomainStateValue - for query.Next() { - var a DomainStateValue - err := query.Scan(&a.Domain, &a.State) - if err != nil { - apiError(rw, http.StatusInsufficientStorage, "Database error") - return - } - domainStates = append(domainStates, a) - } - // write output rw.WriteHeader(http.StatusAccepted) m := map[string]any{ "id": fmt.Sprintf("%d", certId), - "domains": domainStates, + "domains": rows, } _ = json.NewEncoder(rw).Encode(m) }) } -func certDomainManagePUTandDELETE(db *sql.DB, signer mjwt.Verifier, domains utils.DomainChecker) httprouter.Handle { - return checkAuthForCertificate(signer, "orchid:cert:edit", db, func(rw http.ResponseWriter, req *http.Request, params httprouter.Params, b AuthClaims, certId uint64) { +func certDomainManagePUTandDELETE(db *database.Queries, signer mjwt.Verifier, domains utils.DomainChecker) httprouter.Handle { + return checkAuthForCertificate(signer, "orchid:cert:edit", db, func(rw http.ResponseWriter, req *http.Request, params httprouter.Params, b AuthClaims, certId int64) { // check request type isAdd := req.Method == http.MethodPut @@ -66,18 +55,25 @@ func certDomainManagePUTandDELETE(db *sql.DB, signer mjwt.Verifier, domains util } // run a safe transaction to insert or update the certificate domains - if safeTransaction(rw, db, func(rw http.ResponseWriter, tx *sql.Tx) error { + if db.UseTx(req.Context(), func(tx *database.Queries) error { if isAdd { // insert domains to add for _, i := range d { - _, err := tx.Exec(`INSERT INTO certificate_domains (cert_id, domain, state) VALUES (?, ?, ?)`, certId, i, renewal.DomainStateAdded) + err := tx.AddDomains(req.Context(), database.AddDomainsParams{ + CertID: certId, + Domain: i, + State: renewal.DomainStateAdded, + }) if err != nil { return fmt.Errorf("failed to add domains to the database") } } } else { // update domains to removed state - _, err := tx.Exec(`UPDATE certificate_domains SET state = ? WHERE domain IN ?`, renewal.DomainStateRemoved, d) + err := tx.UpdateDomains(req.Context(), database.UpdateDomainsParams{ + State: renewal.DomainStateRemoved, + Domain: d, + }) if err != nil { return fmt.Errorf("failed to remove domains from the database") } diff --git a/servers/find-owned-certs.sql b/servers/find-owned-certs.sql deleted file mode 100644 index b254131..0000000 --- a/servers/find-owned-certs.sql +++ /dev/null @@ -1,3 +0,0 @@ -select cert.id, cert.auto_renew, cert.active, cert.renewing, cert.renew_failed, cert.not_after, cert.updated_at, certificate_domains.domain -from certificates as cert - inner join certificate_domains on cert.id = certificate_domains.cert_id diff --git a/sqlc.yaml b/sqlc.yaml new file mode 100644 index 0000000..7e08599 --- /dev/null +++ b/sqlc.yaml @@ -0,0 +1,10 @@ +version: "2" +sql: + - engine: sqlite + queries: database/queries + schema: database/migrations + gen: + go: + package: "database" + out: "database" + emit_json_tags: true