mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-09 19:28:58 +00:00
0e29c416b1
Closes one 2026-04-24 audit finding (P2):
- cat-s6-efc7f6f6bd50: 30 strings.Contains(err.Error(), ...) sites
in internal/api/handler/ — brittle to repository-layer message
changes, untyped against the actual failure mode.
Approach (Option B from prompt design notes):
- New typed sentinels in internal/repository/errors.go:
ErrNotFound, ErrForeignKeyConstraint
IsForeignKeyError(err) helper (the only place substring
matching at the lib/pq boundary is allowed; isolates the
DB-driver string knowledge to one function).
- New typed sentinel in internal/domain/errors.go:
ErrValidation (reserved for future per-entity validation
wrappers; not yet used by all handlers).
- 49 sites in internal/repository/postgres/*.go updated to wrap
sql.ErrNoRows-derived errors via fmt.Errorf("...: %w",
repository.ErrNotFound).
- 18 not-found handler sites + 2 FK-constraint handler sites
refactored to errors.Is(err, repository.ErrNotFound) /
repository.IsForeignKeyError(err).
- 23 inline `fmt.Errorf("X not found")` test fixtures across
handler tests rewrapped to wrap repository.ErrNotFound.
- test_utils.go::ErrMockNotFound rewrapped to wrap
repository.ErrNotFound; renewal_policy.go closure docblock
updated to reflect the new convention.
- integration test mockJobRepository.Get wraps repository.ErrNotFound.
CI regression guardrail:
- .github/workflows/ci.yml::"Forbidden strings.Contains(err.Error())
regression guard (S-2)" greps for the three patterns ("not found",
"violates foreign key", "RESTRICT") under internal/api/handler/
and fails the build on regression.
Verification:
- go build ./... — clean
- go vet ./... — clean
- go test ./... -short -count=1 — all packages pass (handler +
repository + service + integration)
- golangci-lint v2.11.4 run ./... — 0 issues
- S-2 guardrail dry-run on post-fix tree → empty (good)
- All sibling guardrails (S-1, G-3, D-1+D-2, B-1, L-1, H-1, C-1, F-1, P-1) pass
Audit findings closed:
- cat-s6-efc7f6f6bd50 (P2)
Deferred follow-ups:
- 6 domain-specific substring patterns still inline in handlers
("cannot approve", "cannot reject", "cannot be parsed",
"no certificates found", "challenge password", "invalid"/
"required" validation chains in profiles + agent_groups). Each
needs its own typed sentinel, scoped per service. Documented
by the S-2 CI guardrail's allowlist for closure-comments only.
- Per-entity not-found sentinels (Option A — ErrCertificateNotFound,
ErrAgentNotFound, etc.) deferred. Generic ErrNotFound covers the
current dispatch needs; per-entity precision would let handlers
return entity-aware error bodies without a domain.Type field,
but not blocking.
454 lines
16 KiB
Go
454 lines
16 KiB
Go
package postgres
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/shankar0123/certctl/internal/domain"
|
|
"github.com/shankar0123/certctl/internal/repository"
|
|
)
|
|
|
|
// HealthCheckRepository implements repository.HealthCheckRepository using PostgreSQL.
|
|
type HealthCheckRepository struct {
|
|
db *sql.DB
|
|
}
|
|
|
|
// NewHealthCheckRepository creates a new PostgreSQL-backed health check repository.
|
|
func NewHealthCheckRepository(db *sql.DB) *HealthCheckRepository {
|
|
return &HealthCheckRepository{db: db}
|
|
}
|
|
|
|
// Create stores a new health check.
|
|
func (r *HealthCheckRepository) Create(ctx context.Context, check *domain.EndpointHealthCheck) error {
|
|
_, err := r.db.ExecContext(ctx, `
|
|
INSERT INTO endpoint_health_checks (
|
|
id, endpoint, certificate_id, network_scan_target_id,
|
|
expected_fingerprint, observed_fingerprint, status,
|
|
consecutive_failures, response_time_ms, tls_version, cipher_suite,
|
|
cert_subject, cert_issuer, cert_expiry,
|
|
last_checked_at, last_success_at, last_failure_at, last_transition_at,
|
|
failure_reason, degraded_threshold, down_threshold, check_interval_seconds,
|
|
enabled, acknowledged, acknowledged_by, acknowledged_at,
|
|
created_at, updated_at
|
|
) VALUES (
|
|
$1, $2, $3, $4,
|
|
$5, $6, $7,
|
|
$8, $9, $10, $11,
|
|
$12, $13, $14,
|
|
$15, $16, $17, $18,
|
|
$19, $20, $21, $22,
|
|
$23, $24, $25, $26,
|
|
$27, $28
|
|
)`,
|
|
check.ID, check.Endpoint, check.CertificateID, check.NetworkScanTargetID,
|
|
check.ExpectedFingerprint, check.ObservedFingerprint, string(check.Status),
|
|
check.ConsecutiveFailures, check.ResponseTimeMs, check.TLSVersion, check.CipherSuite,
|
|
check.CertSubject, check.CertIssuer, check.CertExpiry,
|
|
check.LastCheckedAt, check.LastSuccessAt, check.LastFailureAt, check.LastTransitionAt,
|
|
check.FailureReason, check.DegradedThreshold, check.DownThreshold, check.CheckIntervalSecs,
|
|
check.Enabled, check.Acknowledged, check.AcknowledgedBy, check.AcknowledgedAt,
|
|
check.CreatedAt, check.UpdatedAt,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("create health check: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Update modifies an existing health check.
|
|
func (r *HealthCheckRepository) Update(ctx context.Context, check *domain.EndpointHealthCheck) error {
|
|
check.UpdatedAt = time.Now()
|
|
_, err := r.db.ExecContext(ctx, `
|
|
UPDATE endpoint_health_checks SET
|
|
endpoint = $2, certificate_id = $3, network_scan_target_id = $4,
|
|
expected_fingerprint = $5, observed_fingerprint = $6, status = $7,
|
|
consecutive_failures = $8, response_time_ms = $9, tls_version = $10, cipher_suite = $11,
|
|
cert_subject = $12, cert_issuer = $13, cert_expiry = $14,
|
|
last_checked_at = $15, last_success_at = $16, last_failure_at = $17, last_transition_at = $18,
|
|
failure_reason = $19, degraded_threshold = $20, down_threshold = $21, check_interval_seconds = $22,
|
|
enabled = $23, acknowledged = $24, acknowledged_by = $25, acknowledged_at = $26,
|
|
updated_at = $27
|
|
WHERE id = $1`,
|
|
check.ID,
|
|
check.Endpoint, check.CertificateID, check.NetworkScanTargetID,
|
|
check.ExpectedFingerprint, check.ObservedFingerprint, string(check.Status),
|
|
check.ConsecutiveFailures, check.ResponseTimeMs, check.TLSVersion, check.CipherSuite,
|
|
check.CertSubject, check.CertIssuer, check.CertExpiry,
|
|
check.LastCheckedAt, check.LastSuccessAt, check.LastFailureAt, check.LastTransitionAt,
|
|
check.FailureReason, check.DegradedThreshold, check.DownThreshold, check.CheckIntervalSecs,
|
|
check.Enabled, check.Acknowledged, check.AcknowledgedBy, check.AcknowledgedAt,
|
|
check.UpdatedAt,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("update health check: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Get retrieves a health check by ID.
|
|
func (r *HealthCheckRepository) Get(ctx context.Context, id string) (*domain.EndpointHealthCheck, error) {
|
|
check := &domain.EndpointHealthCheck{}
|
|
var status string
|
|
var certExpiry, lastCheckedAt, lastSuccessAt, lastFailureAt, lastTransitionAt, acknowledgedAt sql.NullTime
|
|
err := r.db.QueryRowContext(ctx, `
|
|
SELECT id, endpoint, certificate_id, network_scan_target_id,
|
|
expected_fingerprint, observed_fingerprint, status,
|
|
consecutive_failures, response_time_ms, tls_version, cipher_suite,
|
|
cert_subject, cert_issuer, cert_expiry,
|
|
last_checked_at, last_success_at, last_failure_at, last_transition_at,
|
|
failure_reason, degraded_threshold, down_threshold, check_interval_seconds,
|
|
enabled, acknowledged, acknowledged_by, acknowledged_at,
|
|
created_at, updated_at
|
|
FROM endpoint_health_checks
|
|
WHERE id = $1`, id).Scan(
|
|
&check.ID, &check.Endpoint, &check.CertificateID, &check.NetworkScanTargetID,
|
|
&check.ExpectedFingerprint, &check.ObservedFingerprint, &status,
|
|
&check.ConsecutiveFailures, &check.ResponseTimeMs, &check.TLSVersion, &check.CipherSuite,
|
|
&check.CertSubject, &check.CertIssuer, &certExpiry,
|
|
&lastCheckedAt, &lastSuccessAt, &lastFailureAt, &lastTransitionAt,
|
|
&check.FailureReason, &check.DegradedThreshold, &check.DownThreshold, &check.CheckIntervalSecs,
|
|
&check.Enabled, &check.Acknowledged, &check.AcknowledgedBy, &acknowledgedAt,
|
|
&check.CreatedAt, &check.UpdatedAt,
|
|
)
|
|
if err == sql.ErrNoRows {
|
|
return nil, fmt.Errorf("health check not found: %w", repository.ErrNotFound)
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get health check: %w", err)
|
|
}
|
|
check.Status = domain.HealthStatus(status)
|
|
if certExpiry.Valid {
|
|
check.CertExpiry = &certExpiry.Time
|
|
}
|
|
if lastCheckedAt.Valid {
|
|
check.LastCheckedAt = &lastCheckedAt.Time
|
|
}
|
|
if lastSuccessAt.Valid {
|
|
check.LastSuccessAt = &lastSuccessAt.Time
|
|
}
|
|
if lastFailureAt.Valid {
|
|
check.LastFailureAt = &lastFailureAt.Time
|
|
}
|
|
if lastTransitionAt.Valid {
|
|
check.LastTransitionAt = &lastTransitionAt.Time
|
|
}
|
|
if acknowledgedAt.Valid {
|
|
check.AcknowledgedAt = &acknowledgedAt.Time
|
|
}
|
|
return check, nil
|
|
}
|
|
|
|
// Delete removes a health check.
|
|
func (r *HealthCheckRepository) Delete(ctx context.Context, id string) error {
|
|
_, err := r.db.ExecContext(ctx, `DELETE FROM endpoint_health_checks WHERE id = $1`, id)
|
|
if err != nil {
|
|
return fmt.Errorf("delete health check: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// List returns health checks matching the filter with pagination.
|
|
func (r *HealthCheckRepository) List(ctx context.Context, filter *repository.HealthCheckFilter) ([]*domain.EndpointHealthCheck, int, error) {
|
|
query := `SELECT id, endpoint, certificate_id, network_scan_target_id,
|
|
expected_fingerprint, observed_fingerprint, status,
|
|
consecutive_failures, response_time_ms, tls_version, cipher_suite,
|
|
cert_subject, cert_issuer, cert_expiry,
|
|
last_checked_at, last_success_at, last_failure_at, last_transition_at,
|
|
failure_reason, degraded_threshold, down_threshold, check_interval_seconds,
|
|
enabled, acknowledged, acknowledged_by, acknowledged_at,
|
|
created_at, updated_at
|
|
FROM endpoint_health_checks`
|
|
countQuery := `SELECT COUNT(*) FROM endpoint_health_checks`
|
|
|
|
var conditions []string
|
|
var args []interface{}
|
|
argIdx := 1
|
|
|
|
if filter != nil {
|
|
if filter.Status != "" {
|
|
conditions = append(conditions, fmt.Sprintf("status = $%d", argIdx))
|
|
args = append(args, filter.Status)
|
|
argIdx++
|
|
}
|
|
if filter.CertificateID != "" {
|
|
conditions = append(conditions, fmt.Sprintf("certificate_id = $%d", argIdx))
|
|
args = append(args, filter.CertificateID)
|
|
argIdx++
|
|
}
|
|
if filter.NetworkScanTargetID != "" {
|
|
conditions = append(conditions, fmt.Sprintf("network_scan_target_id = $%d", argIdx))
|
|
args = append(args, filter.NetworkScanTargetID)
|
|
argIdx++
|
|
}
|
|
if filter.Enabled != nil {
|
|
conditions = append(conditions, fmt.Sprintf("enabled = $%d", argIdx))
|
|
args = append(args, *filter.Enabled)
|
|
argIdx++
|
|
}
|
|
}
|
|
|
|
if len(conditions) > 0 {
|
|
where := " WHERE " + conditions[0]
|
|
for i := 1; i < len(conditions); i++ {
|
|
where += " AND " + conditions[i]
|
|
}
|
|
query += where
|
|
countQuery += where
|
|
}
|
|
|
|
// Get total count
|
|
var total int
|
|
err := r.db.QueryRowContext(ctx, countQuery, args...).Scan(&total)
|
|
if err != nil {
|
|
return nil, 0, fmt.Errorf("count health checks: %w", err)
|
|
}
|
|
|
|
// Apply pagination
|
|
query += " ORDER BY created_at DESC"
|
|
page := 1
|
|
perPage := 50
|
|
if filter != nil {
|
|
if filter.Page > 0 {
|
|
page = filter.Page
|
|
}
|
|
if filter.PerPage > 0 {
|
|
perPage = filter.PerPage
|
|
}
|
|
}
|
|
offset := (page - 1) * perPage
|
|
query += fmt.Sprintf(" LIMIT $%d OFFSET $%d", argIdx, argIdx+1)
|
|
args = append(args, perPage, offset)
|
|
|
|
rows, err := r.db.QueryContext(ctx, query, args...)
|
|
if err != nil {
|
|
return nil, 0, fmt.Errorf("list health checks: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var checks []*domain.EndpointHealthCheck
|
|
for rows.Next() {
|
|
check, err := scanHealthCheck(rows)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
checks = append(checks, check)
|
|
}
|
|
return checks, total, rows.Err()
|
|
}
|
|
|
|
// ListDueForCheck returns health checks where the check interval has been exceeded.
|
|
func (r *HealthCheckRepository) ListDueForCheck(ctx context.Context) ([]*domain.EndpointHealthCheck, error) {
|
|
rows, err := r.db.QueryContext(ctx, `
|
|
SELECT id, endpoint, certificate_id, network_scan_target_id,
|
|
expected_fingerprint, observed_fingerprint, status,
|
|
consecutive_failures, response_time_ms, tls_version, cipher_suite,
|
|
cert_subject, cert_issuer, cert_expiry,
|
|
last_checked_at, last_success_at, last_failure_at, last_transition_at,
|
|
failure_reason, degraded_threshold, down_threshold, check_interval_seconds,
|
|
enabled, acknowledged, acknowledged_by, acknowledged_at,
|
|
created_at, updated_at
|
|
FROM endpoint_health_checks
|
|
WHERE enabled = TRUE
|
|
AND (
|
|
last_checked_at IS NULL
|
|
OR last_checked_at + (check_interval_seconds * INTERVAL '1 second') < NOW()
|
|
)
|
|
ORDER BY last_checked_at ASC NULLS FIRST`)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list due health checks: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var checks []*domain.EndpointHealthCheck
|
|
for rows.Next() {
|
|
check, err := scanHealthCheck(rows)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
checks = append(checks, check)
|
|
}
|
|
return checks, rows.Err()
|
|
}
|
|
|
|
// GetByEndpoint retrieves a health check by endpoint address.
|
|
func (r *HealthCheckRepository) GetByEndpoint(ctx context.Context, endpoint string) (*domain.EndpointHealthCheck, error) {
|
|
row := r.db.QueryRowContext(ctx, `
|
|
SELECT id, endpoint, certificate_id, network_scan_target_id,
|
|
expected_fingerprint, observed_fingerprint, status,
|
|
consecutive_failures, response_time_ms, tls_version, cipher_suite,
|
|
cert_subject, cert_issuer, cert_expiry,
|
|
last_checked_at, last_success_at, last_failure_at, last_transition_at,
|
|
failure_reason, degraded_threshold, down_threshold, check_interval_seconds,
|
|
enabled, acknowledged, acknowledged_by, acknowledged_at,
|
|
created_at, updated_at
|
|
FROM endpoint_health_checks
|
|
WHERE endpoint = $1`, endpoint)
|
|
check := &domain.EndpointHealthCheck{}
|
|
var status string
|
|
var certExpiry, lastCheckedAt, lastSuccessAt, lastFailureAt, lastTransitionAt, acknowledgedAt sql.NullTime
|
|
err := row.Scan(
|
|
&check.ID, &check.Endpoint, &check.CertificateID, &check.NetworkScanTargetID,
|
|
&check.ExpectedFingerprint, &check.ObservedFingerprint, &status,
|
|
&check.ConsecutiveFailures, &check.ResponseTimeMs, &check.TLSVersion, &check.CipherSuite,
|
|
&check.CertSubject, &check.CertIssuer, &certExpiry,
|
|
&lastCheckedAt, &lastSuccessAt, &lastFailureAt, &lastTransitionAt,
|
|
&check.FailureReason, &check.DegradedThreshold, &check.DownThreshold, &check.CheckIntervalSecs,
|
|
&check.Enabled, &check.Acknowledged, &check.AcknowledgedBy, &acknowledgedAt,
|
|
&check.CreatedAt, &check.UpdatedAt,
|
|
)
|
|
if err == sql.ErrNoRows {
|
|
return nil, fmt.Errorf("health check not found for endpoint: %w", repository.ErrNotFound)
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get health check by endpoint: %w", err)
|
|
}
|
|
check.Status = domain.HealthStatus(status)
|
|
if certExpiry.Valid {
|
|
check.CertExpiry = &certExpiry.Time
|
|
}
|
|
if lastCheckedAt.Valid {
|
|
check.LastCheckedAt = &lastCheckedAt.Time
|
|
}
|
|
if lastSuccessAt.Valid {
|
|
check.LastSuccessAt = &lastSuccessAt.Time
|
|
}
|
|
if lastFailureAt.Valid {
|
|
check.LastFailureAt = &lastFailureAt.Time
|
|
}
|
|
if lastTransitionAt.Valid {
|
|
check.LastTransitionAt = &lastTransitionAt.Time
|
|
}
|
|
if acknowledgedAt.Valid {
|
|
check.AcknowledgedAt = &acknowledgedAt.Time
|
|
}
|
|
return check, nil
|
|
}
|
|
|
|
// RecordHistory records a single probe result in history.
|
|
func (r *HealthCheckRepository) RecordHistory(ctx context.Context, entry *domain.HealthHistoryEntry) error {
|
|
_, err := r.db.ExecContext(ctx, `
|
|
INSERT INTO endpoint_health_history (id, health_check_id, status, response_time_ms, fingerprint, failure_reason, checked_at)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7)`,
|
|
entry.ID, entry.HealthCheckID, entry.Status, entry.ResponseTimeMs, entry.Fingerprint, entry.FailureReason, entry.CheckedAt,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("record health check history: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetHistory retrieves recent probe history for a health check.
|
|
func (r *HealthCheckRepository) GetHistory(ctx context.Context, healthCheckID string, limit int) ([]*domain.HealthHistoryEntry, error) {
|
|
if limit <= 0 {
|
|
limit = 100
|
|
}
|
|
rows, err := r.db.QueryContext(ctx, `
|
|
SELECT id, health_check_id, status, response_time_ms, fingerprint, failure_reason, checked_at
|
|
FROM endpoint_health_history
|
|
WHERE health_check_id = $1
|
|
ORDER BY checked_at DESC
|
|
LIMIT $2`, healthCheckID, limit)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get health check history: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var entries []*domain.HealthHistoryEntry
|
|
for rows.Next() {
|
|
entry := &domain.HealthHistoryEntry{}
|
|
if err := rows.Scan(&entry.ID, &entry.HealthCheckID, &entry.Status, &entry.ResponseTimeMs, &entry.Fingerprint, &entry.FailureReason, &entry.CheckedAt); err != nil {
|
|
return nil, fmt.Errorf("scan health history entry: %w", err)
|
|
}
|
|
entries = append(entries, entry)
|
|
}
|
|
return entries, rows.Err()
|
|
}
|
|
|
|
// PurgeHistory deletes history entries older than the specified time.
|
|
func (r *HealthCheckRepository) PurgeHistory(ctx context.Context, olderThan time.Time) (int64, error) {
|
|
result, err := r.db.ExecContext(ctx, `DELETE FROM endpoint_health_history WHERE checked_at < $1`, olderThan)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("purge health check history: %w", err)
|
|
}
|
|
return result.RowsAffected()
|
|
}
|
|
|
|
// GetSummary returns aggregate counts by health status.
|
|
func (r *HealthCheckRepository) GetSummary(ctx context.Context) (*domain.HealthCheckSummary, error) {
|
|
rows, err := r.db.QueryContext(ctx, `SELECT status, COUNT(*) FROM endpoint_health_checks GROUP BY status`)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get health check summary: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
summary := &domain.HealthCheckSummary{}
|
|
for rows.Next() {
|
|
var status string
|
|
var count int
|
|
if err := rows.Scan(&status, &count); err != nil {
|
|
return nil, fmt.Errorf("scan health check summary: %w", err)
|
|
}
|
|
switch domain.HealthStatus(status) {
|
|
case domain.HealthStatusHealthy:
|
|
summary.Healthy = count
|
|
case domain.HealthStatusDegraded:
|
|
summary.Degraded = count
|
|
case domain.HealthStatusDown:
|
|
summary.Down = count
|
|
case domain.HealthStatusCertMismatch:
|
|
summary.CertMismatch = count
|
|
case domain.HealthStatusUnknown:
|
|
summary.Unknown = count
|
|
}
|
|
summary.Total += count
|
|
}
|
|
return summary, rows.Err()
|
|
}
|
|
|
|
// scannable is an interface satisfied by both *sql.Row and *sql.Rows.
|
|
type scannable interface {
|
|
Scan(dest ...interface{}) error
|
|
}
|
|
|
|
// scanHealthCheck scans a health check from a row.
|
|
func scanHealthCheck(row scannable) (*domain.EndpointHealthCheck, error) {
|
|
check := &domain.EndpointHealthCheck{}
|
|
var status string
|
|
var certExpiry, lastCheckedAt, lastSuccessAt, lastFailureAt, lastTransitionAt, acknowledgedAt sql.NullTime
|
|
err := row.Scan(
|
|
&check.ID, &check.Endpoint, &check.CertificateID, &check.NetworkScanTargetID,
|
|
&check.ExpectedFingerprint, &check.ObservedFingerprint, &status,
|
|
&check.ConsecutiveFailures, &check.ResponseTimeMs, &check.TLSVersion, &check.CipherSuite,
|
|
&check.CertSubject, &check.CertIssuer, &certExpiry,
|
|
&lastCheckedAt, &lastSuccessAt, &lastFailureAt, &lastTransitionAt,
|
|
&check.FailureReason, &check.DegradedThreshold, &check.DownThreshold, &check.CheckIntervalSecs,
|
|
&check.Enabled, &check.Acknowledged, &check.AcknowledgedBy, &acknowledgedAt,
|
|
&check.CreatedAt, &check.UpdatedAt,
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("scan health check: %w", err)
|
|
}
|
|
check.Status = domain.HealthStatus(status)
|
|
if certExpiry.Valid {
|
|
check.CertExpiry = &certExpiry.Time
|
|
}
|
|
if lastCheckedAt.Valid {
|
|
check.LastCheckedAt = &lastCheckedAt.Time
|
|
}
|
|
if lastSuccessAt.Valid {
|
|
check.LastSuccessAt = &lastSuccessAt.Time
|
|
}
|
|
if lastFailureAt.Valid {
|
|
check.LastFailureAt = &lastFailureAt.Time
|
|
}
|
|
if lastTransitionAt.Valid {
|
|
check.LastTransitionAt = &lastTransitionAt.Time
|
|
}
|
|
if acknowledgedAt.Valid {
|
|
check.AcknowledgedAt = &acknowledgedAt.Time
|
|
}
|
|
return check, nil
|
|
}
|