Fix runtime bugs, implement service layer, and overhaul documentation

Runtime fixes:
- Fix env var mismatch (CERTCTL_DB_URL → CERTCTL_DATABASE_URL)
- Fix table name mismatches (certificates → managed_certificates, notifications → notification_events)
- Add renewal_policy_id to certificate queries
- Remove non-existent created_at from notification queries
- Add env var fallback for agent CLI flags
- Graceful degradation for missing notifiers/issuers in demo mode
- Copy web/ directory in Dockerfile for dashboard serving

Service layer:
- Implement handler-service interface pattern across all services
- Wire up certificate, agent, job, policy, team, owner, audit, notification services

Documentation:
- Add concepts.md: beginner-friendly guide to TLS, CAs, private keys
- Rewrite quickstart.md with accurate API examples matching actual handlers
- Add demo-advanced.md: interactive demo with cert issuance and automated script
- Update architecture.md with correct table names and connector interfaces
- Update connectors.md to match actual Go interface signatures
- Update demo-guide.md with cross-references to new docs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
shankar0123
2026-03-14 21:38:11 -04:00
parent 3a9fe8ba37
commit 9b4122b159
21 changed files with 1597 additions and 1591 deletions
+14 -14
View File
@@ -75,7 +75,7 @@ func (r *CertificateRepository) List(ctx context.Context, filter *repository.Cer
}
// Get total count
countQuery := fmt.Sprintf("SELECT COUNT(*) FROM certificates %s", whereClause)
countQuery := fmt.Sprintf("SELECT COUNT(*) FROM managed_certificates %s", whereClause)
var total int
if err := r.db.QueryRowContext(ctx, countQuery, args...).Scan(&total); err != nil {
return nil, 0, fmt.Errorf("failed to count certificates: %w", err)
@@ -84,9 +84,9 @@ func (r *CertificateRepository) List(ctx context.Context, filter *repository.Cer
// Get paginated results
offset := (filter.Page - 1) * filter.PerPage
query := fmt.Sprintf(`
SELECT id, name, common_name, sans, environment, owner_id, team_id, issuer_id,
SELECT id, name, common_name, sans, environment, owner_id, team_id, issuer_id, renewal_policy_id,
status, expires_at, tags, last_renewal_at, last_deployment_at, created_at, updated_at
FROM certificates
FROM managed_certificates
%s
ORDER BY created_at DESC
LIMIT $%d OFFSET $%d
@@ -119,9 +119,9 @@ func (r *CertificateRepository) List(ctx context.Context, filter *repository.Cer
// Get retrieves a certificate by ID
func (r *CertificateRepository) Get(ctx context.Context, id string) (*domain.ManagedCertificate, error) {
row := r.db.QueryRowContext(ctx, `
SELECT id, name, common_name, sans, environment, owner_id, team_id, issuer_id,
SELECT id, name, common_name, sans, environment, owner_id, team_id, issuer_id, renewal_policy_id,
status, expires_at, tags, last_renewal_at, last_deployment_at, created_at, updated_at
FROM certificates
FROM managed_certificates
WHERE id = $1
`, id)
@@ -148,13 +148,13 @@ func (r *CertificateRepository) Create(ctx context.Context, cert *domain.Managed
}
err = r.db.QueryRowContext(ctx, `
INSERT INTO certificates (
id, name, common_name, sans, environment, owner_id, team_id, issuer_id,
INSERT INTO managed_certificates (
id, name, common_name, sans, environment, owner_id, team_id, issuer_id, renewal_policy_id,
status, expires_at, tags, last_renewal_at, last_deployment_at, created_at, updated_at
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)
RETURNING id
`, cert.ID, cert.Name, cert.CommonName, pq.Array(cert.SANs), cert.Environment,
cert.OwnerID, cert.TeamID, cert.IssuerID, cert.Status, cert.ExpiresAt,
cert.OwnerID, cert.TeamID, cert.IssuerID, cert.RenewalPolicyID, cert.Status, cert.ExpiresAt,
tagsJSON, cert.LastRenewalAt, cert.LastDeploymentAt, cert.CreatedAt, cert.UpdatedAt).Scan(&cert.ID)
if err != nil {
@@ -172,7 +172,7 @@ func (r *CertificateRepository) Update(ctx context.Context, cert *domain.Managed
}
result, err := r.db.ExecContext(ctx, `
UPDATE certificates SET
UPDATE managed_certificates SET
name = $1,
common_name = $2,
sans = $3,
@@ -210,7 +210,7 @@ func (r *CertificateRepository) Update(ctx context.Context, cert *domain.Managed
// Archive marks a certificate as archived
func (r *CertificateRepository) Archive(ctx context.Context, id string) error {
result, err := r.db.ExecContext(ctx, `
UPDATE certificates SET status = $1, updated_at = $2 WHERE id = $3
UPDATE managed_certificates SET status = $1, updated_at = $2 WHERE id = $3
`, domain.CertificateStatusArchived, time.Now(), id)
if err != nil {
@@ -286,9 +286,9 @@ func (r *CertificateRepository) CreateVersion(ctx context.Context, version *doma
// GetExpiringCertificates returns certificates expiring before the given time
func (r *CertificateRepository) GetExpiringCertificates(ctx context.Context, before time.Time) ([]*domain.ManagedCertificate, error) {
rows, err := r.db.QueryContext(ctx, `
SELECT id, name, common_name, sans, environment, owner_id, team_id, issuer_id,
SELECT id, name, common_name, sans, environment, owner_id, team_id, issuer_id, renewal_policy_id,
status, expires_at, tags, last_renewal_at, last_deployment_at, created_at, updated_at
FROM certificates
FROM managed_certificates
WHERE expires_at < $1 AND status != $2
ORDER BY expires_at ASC
`, before, domain.CertificateStatusArchived)
@@ -324,7 +324,7 @@ func scanCertificate(scanner interface {
err := scanner.Scan(
&cert.ID, &cert.Name, &cert.CommonName, &sans, &cert.Environment, &cert.OwnerID,
&cert.TeamID, &cert.IssuerID, &cert.Status, &cert.ExpiresAt, &tagsJSON,
&cert.TeamID, &cert.IssuerID, &cert.RenewalPolicyID, &cert.Status, &cert.ExpiresAt, &tagsJSON,
&cert.LastRenewalAt, &cert.LastDeploymentAt, &cert.CreatedAt, &cert.UpdatedAt)
if err != nil {
+10 -10
View File
@@ -29,12 +29,12 @@ func (r *NotificationRepository) Create(ctx context.Context, notif *domain.Notif
}
err := r.db.QueryRowContext(ctx, `
INSERT INTO notifications (
id, type, certificate_id, channel, recipient, message, sent_at, status, error, created_at
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
INSERT INTO notification_events (
id, type, certificate_id, channel, recipient, message, sent_at, status, error
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
RETURNING id
`, notif.ID, notif.Type, notif.CertificateID, notif.Channel, notif.Recipient,
notif.Message, notif.SentAt, notif.Status, notif.Error, notif.CreatedAt).Scan(&notif.ID)
notif.Message, notif.SentAt, notif.Status, notif.Error).Scan(&notif.ID)
if err != nil {
return fmt.Errorf("failed to create notification: %w", err)
@@ -84,7 +84,7 @@ func (r *NotificationRepository) List(ctx context.Context, filter *repository.No
}
// Get total count
countQuery := fmt.Sprintf("SELECT COUNT(*) FROM notifications %s", whereClause)
countQuery := fmt.Sprintf("SELECT COUNT(*) FROM notification_events %s", whereClause)
var total int
if err := r.db.QueryRowContext(ctx, countQuery, args...).Scan(&total); err != nil {
return nil, fmt.Errorf("failed to count notifications: %w", err)
@@ -93,10 +93,10 @@ func (r *NotificationRepository) List(ctx context.Context, filter *repository.No
// Get paginated results
offset := (filter.Page - 1) * filter.PerPage
query := fmt.Sprintf(`
SELECT id, type, certificate_id, channel, recipient, message, sent_at, status, error, created_at
FROM notifications
SELECT id, type, certificate_id, channel, recipient, message, sent_at, status, error
FROM notification_events
%s
ORDER BY created_at DESC
ORDER BY sent_at DESC NULLS LAST
LIMIT $%d OFFSET $%d
`, whereClause, argCount, argCount+1)
@@ -127,7 +127,7 @@ func (r *NotificationRepository) List(ctx context.Context, filter *repository.No
// UpdateStatus updates a notification's delivery status
func (r *NotificationRepository) UpdateStatus(ctx context.Context, id string, status string, sentAt time.Time) error {
result, err := r.db.ExecContext(ctx, `
UPDATE notifications SET status = $1, sent_at = $2 WHERE id = $3
UPDATE notification_events SET status = $1, sent_at = $2 WHERE id = $3
`, status, sentAt, id)
if err != nil {
@@ -152,7 +152,7 @@ func scanNotification(scanner interface {
}) (*domain.NotificationEvent, error) {
var notif domain.NotificationEvent
err := scanner.Scan(&notif.ID, &notif.Type, &notif.CertificateID, &notif.Channel,
&notif.Recipient, &notif.Message, &notif.SentAt, &notif.Status, &notif.Error, &notif.CreatedAt)
&notif.Recipient, &notif.Message, &notif.SentAt, &notif.Status, &notif.Error)
if err != nil {
return nil, fmt.Errorf("failed to scan notification: %w", err)