mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-10 10:58:52 +00:00
Implement M3: expiration threshold alerting with dedup and status transitions
- Add alert_thresholds_days JSONB column to renewal_policies (default [30,14,7,0]) - Add RenewalPolicy.AlertThresholdsDays field + EffectiveAlertThresholds() helper - Add RenewalPolicyRepository interface + postgres implementation - Rewrite CheckExpiringCertificates with per-policy threshold alerting - Add SendThresholdAlert + HasThresholdNotification for deduplication via [threshold:N] tags - Add Type and MessageLike filters to NotificationFilter + postgres query support - Auto-transition certs to Expiring (>0 days) or Expired (<=0 days) status - Record expiration_alert_sent audit events per threshold crossing - Fix .gitignore: allow SQL migration files, scope server/agent build artifact rules - Track previously untracked cmd/ and migrations/ directories - Update docs (README, architecture, demo-advanced) for threshold alerting Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -67,11 +67,21 @@ func (r *NotificationRepository) List(ctx context.Context, filter *repository.No
|
||||
args = append(args, filter.CertificateID)
|
||||
argCount++
|
||||
}
|
||||
if filter.Type != "" {
|
||||
whereConditions = append(whereConditions, fmt.Sprintf("type = $%d", argCount))
|
||||
args = append(args, filter.Type)
|
||||
argCount++
|
||||
}
|
||||
if filter.Status != "" {
|
||||
whereConditions = append(whereConditions, fmt.Sprintf("status = $%d", argCount))
|
||||
args = append(args, filter.Status)
|
||||
argCount++
|
||||
}
|
||||
if filter.MessageLike != "" {
|
||||
whereConditions = append(whereConditions, fmt.Sprintf("message LIKE $%d", argCount))
|
||||
args = append(args, filter.MessageLike)
|
||||
argCount++
|
||||
}
|
||||
if filter.Channel != "" {
|
||||
whereConditions = append(whereConditions, fmt.Sprintf("channel = $%d", argCount))
|
||||
args = append(args, filter.Channel)
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/shankar0123/certctl/internal/domain"
|
||||
)
|
||||
|
||||
// RenewalPolicyRepository implements repository.RenewalPolicyRepository
|
||||
type RenewalPolicyRepository struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
// NewRenewalPolicyRepository creates a new RenewalPolicyRepository
|
||||
func NewRenewalPolicyRepository(db *sql.DB) *RenewalPolicyRepository {
|
||||
return &RenewalPolicyRepository{db: db}
|
||||
}
|
||||
|
||||
// Get retrieves a renewal policy by ID
|
||||
func (r *RenewalPolicyRepository) Get(ctx context.Context, id string) (*domain.RenewalPolicy, error) {
|
||||
var policy domain.RenewalPolicy
|
||||
var thresholdsJSON []byte
|
||||
|
||||
err := r.db.QueryRowContext(ctx, `
|
||||
SELECT id, name, renewal_window_days, auto_renew, max_retries,
|
||||
retry_interval_minutes, alert_thresholds_days, created_at, updated_at
|
||||
FROM renewal_policies
|
||||
WHERE id = $1
|
||||
`, id).Scan(&policy.ID, &policy.Name, &policy.RenewalWindowDays, &policy.AutoRenew,
|
||||
&policy.MaxRetries, &policy.RetryInterval, &thresholdsJSON,
|
||||
&policy.CreatedAt, &policy.UpdatedAt)
|
||||
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, fmt.Errorf("renewal policy not found: %s", id)
|
||||
}
|
||||
return nil, fmt.Errorf("failed to query renewal policy: %w", err)
|
||||
}
|
||||
|
||||
// Parse alert thresholds from JSONB
|
||||
if len(thresholdsJSON) > 0 {
|
||||
if err := json.Unmarshal(thresholdsJSON, &policy.AlertThresholdsDays); err != nil {
|
||||
// Fall back to defaults if JSON is malformed
|
||||
policy.AlertThresholdsDays = domain.DefaultAlertThresholds()
|
||||
}
|
||||
}
|
||||
|
||||
return &policy, nil
|
||||
}
|
||||
|
||||
// List returns all renewal policies
|
||||
func (r *RenewalPolicyRepository) List(ctx context.Context) ([]*domain.RenewalPolicy, error) {
|
||||
rows, err := r.db.QueryContext(ctx, `
|
||||
SELECT id, name, renewal_window_days, auto_renew, max_retries,
|
||||
retry_interval_minutes, alert_thresholds_days, created_at, updated_at
|
||||
FROM renewal_policies
|
||||
ORDER BY name
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to query renewal policies: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var policies []*domain.RenewalPolicy
|
||||
for rows.Next() {
|
||||
var policy domain.RenewalPolicy
|
||||
var thresholdsJSON []byte
|
||||
|
||||
if err := rows.Scan(&policy.ID, &policy.Name, &policy.RenewalWindowDays, &policy.AutoRenew,
|
||||
&policy.MaxRetries, &policy.RetryInterval, &thresholdsJSON,
|
||||
&policy.CreatedAt, &policy.UpdatedAt); err != nil {
|
||||
return nil, fmt.Errorf("failed to scan renewal policy: %w", err)
|
||||
}
|
||||
|
||||
if len(thresholdsJSON) > 0 {
|
||||
if err := json.Unmarshal(thresholdsJSON, &policy.AlertThresholdsDays); err != nil {
|
||||
policy.AlertThresholdsDays = domain.DefaultAlertThresholds()
|
||||
}
|
||||
}
|
||||
|
||||
policies = append(policies, &policy)
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("error iterating renewal policy rows: %w", err)
|
||||
}
|
||||
|
||||
return policies, nil
|
||||
}
|
||||
Reference in New Issue
Block a user