diff --git a/cmd/certificatee/main.go b/cmd/certificatee/main.go index 5691282..1083ac5 100644 --- a/cmd/certificatee/main.go +++ b/cmd/certificatee/main.go @@ -1,8 +1,6 @@ package main import ( - "crypto/x509" - "encoding/hex" "errors" "fmt" "time" @@ -111,35 +109,28 @@ func processHAProxyEndpoint(logger *logrus.Logger, cfg config.Config, vaultClien certPath := ref.DisplayName logger.Infof("[%s] Checking certificate: %s", endpoint, certPath) - // Get certificate info from HAProxy using the file path - haproxyCertInfo, err := haproxyClient.GetCertificateInfoByRef(ref) - if err != nil { - errs = append(errs, fmt.Errorf("failed to get certificate info for %s: %w", certPath, err)) - logger.Errorf("[%s] %v", endpoint, err) - continue - } - // Extract domain name from certificate path domain := haproxy.ExtractDomainFromPath(certPath) logger.Debugf("[%s] Extracted domain '%s' from path '%s'", endpoint, domain, certPath) - // Track expiring certificates - if haproxy.IsExpiring(haproxyCertInfo, cfg.Certificatee.RenewBeforeDays) { - expiringCount++ - } - - // Check if certificate needs update - shouldUpdate, reason, err := shouldUpdateCertificate(logger, haproxyCertInfo, domain, vaultClient, cfg.Certificatee.RenewBeforeDays) + // Check if certificate needs update (uses Vault as source of truth for cert details, + // since HAProxy Data Plane API doesn't provide certificate metadata) + shouldUpdate, reason, isExpiring, err := shouldUpdateCertificate(domain, vaultClient, cfg.Certificatee.RenewBeforeDays) if err != nil { errs = append(errs, err) logger.Errorf("[%s] %v", endpoint, err) continue } + // Track expiring certificates + if isExpiring { + expiringCount++ + } + if shouldUpdate { logger.Infof("[%s] Certificate %s needs update: %s", endpoint, certPath, reason) - if err := updateCertificate(logger, certPath, domain, vaultClient, haproxyClient); err != nil { + if err := updateCertificate(certPath, domain, vaultClient, haproxyClient); err != nil { errs = append(errs, err) logger.Errorf("[%s] %v", endpoint, err) certmetrics.CertificatesUpdateFailures.WithLabelValues(endpoint, domain).Inc() @@ -158,49 +149,31 @@ func processHAProxyEndpoint(logger *logrus.Logger, cfg config.Config, vaultClien return errors.Join(errs...) } -func shouldUpdateCertificate(logger *logrus.Logger, haproxyCertInfo *haproxy.CertInfo, domain string, vaultClient *vault.VaultClient, renewBeforeDays int) (bool, string, error) { - // Get certificate from Vault +func shouldUpdateCertificate(domain string, vaultClient *vault.VaultClient, renewBeforeDays int) (shouldUpdate bool, reason string, isExpiring bool, err error) { + // Get certificate from Vault - this is the source of truth for certificate details + // (HAProxy Data Plane API doesn't provide certificate metadata like expiry or serial) vaultCert, err := certificate.GetCertificate(domain, vaultClient) if err != nil { - return false, "", fmt.Errorf("failed to get certificate %s from vault: %w", domain, err) + return false, "", false, fmt.Errorf("failed to get certificate %s from vault: %w", domain, err) } if vaultCert == nil { - return false, "", fmt.Errorf("certificate for %s does not exist in vault", domain) - } - - // Check if HAProxy certificate is expiring - if haproxy.IsExpiring(haproxyCertInfo, renewBeforeDays) { - return true, fmt.Sprintf("certificate expires on %s (within %d days)", haproxyCertInfo.NotAfter.Format(time.RFC3339), renewBeforeDays), nil - } - - // Compare serial numbers - if serialsDiffer(haproxyCertInfo, vaultCert) { - return true, fmt.Sprintf("serial mismatch: HAProxy=%s, Vault=%s", - haproxyCertInfo.Serial, formatSerial(vaultCert.SerialNumber.Bytes())), nil + return false, "", false, fmt.Errorf("certificate for %s does not exist in vault", domain) } - return false, "", nil -} + // Check if Vault certificate is expiring + threshold := time.Now().AddDate(0, 0, renewBeforeDays) + isExpiring = vaultCert.NotAfter.Before(threshold) -// serialsDiffer compares the serial numbers of HAProxy and Vault certificates -func serialsDiffer(haproxyCertInfo *haproxy.CertInfo, vaultCert *x509.Certificate) bool { - if haproxyCertInfo == nil || vaultCert == nil { - return true + if isExpiring { + // Certificate is expiring, sync to HAProxy (likely was recently renewed) + return true, fmt.Sprintf("certificate expires on %s (within %d days)", vaultCert.NotAfter.Format(time.RFC3339), renewBeforeDays), true, nil } - haproxySerial := haproxy.NormalizeSerial(haproxyCertInfo.Serial) - vaultSerial := haproxy.NormalizeSerial(formatSerial(vaultCert.SerialNumber.Bytes())) - - return haproxySerial != vaultSerial -} - -// formatSerial converts a certificate serial number to hex string -func formatSerial(serial []byte) string { - return hex.EncodeToString(serial) + return false, "", false, nil } -func updateCertificate(logger *logrus.Logger, certPath, domain string, vaultClient *vault.VaultClient, haproxyClient *haproxy.Client) error { +func updateCertificate(certPath, domain string, vaultClient *vault.VaultClient, haproxyClient *haproxy.Client) error { // Read certificate data from Vault certificateSecrets, err := vaultClient.KVRead(certificate.VaultCertLocation(domain)) if err != nil { diff --git a/cmd/certificatee/main_test.go b/cmd/certificatee/main_test.go deleted file mode 100644 index a1dda35..0000000 --- a/cmd/certificatee/main_test.go +++ /dev/null @@ -1,392 +0,0 @@ -package main - -import ( - "crypto/rand" - "crypto/x509" - "math/big" - "testing" - "time" - - "github.com/vinted/certificator/pkg/haproxy" -) - -func TestEndsWith(t *testing.T) { - tests := []struct { - name string - s string - suffix string - expected bool - }{ - {"empty strings", "", "", true}, - {"empty suffix", "hello", "", true}, - {"empty string with suffix", "", "x", false}, - {"exact match", "hello", "hello", true}, - {"suffix match", "hello", "lo", true}, - {"no match", "hello", "la", false}, - {"suffix longer than string", "lo", "hello", false}, - {"newline suffix", "hello\n", "\n", true}, - {"no newline suffix", "hello", "\n", false}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := endsWith(tt.s, tt.suffix) - if result != tt.expected { - t.Errorf("endsWith(%q, %q) = %v, want %v", tt.s, tt.suffix, result, tt.expected) - } - }) - } -} - -func TestFormatSerial(t *testing.T) { - tests := []struct { - name string - serial []byte - expected string - }{ - {"empty", []byte{}, ""}, - {"single byte", []byte{0x1f}, "1f"}, - {"multiple bytes", []byte{0x1f, 0x52, 0x02}, "1f5202"}, - {"leading zero byte", []byte{0x00, 0x1f}, "001f"}, - {"all zeros", []byte{0x00, 0x00}, "0000"}, - {"max bytes", []byte{0xff, 0xff}, "ffff"}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := formatSerial(tt.serial) - if result != tt.expected { - t.Errorf("formatSerial(%v) = %q, want %q", tt.serial, result, tt.expected) - } - }) - } -} - -func TestSerialsDiffer(t *testing.T) { - // Create test certificates with specific serial numbers - serial1 := big.NewInt(0x1f5202) - serial2 := big.NewInt(0x1f5203) - - cert1 := &x509.Certificate{SerialNumber: serial1} - cert2 := &x509.Certificate{SerialNumber: serial2} - - tests := []struct { - name string - haproxyCert *haproxy.CertInfo - vaultCert *x509.Certificate - expectDiffer bool - }{ - { - name: "nil haproxy cert", - haproxyCert: nil, - vaultCert: cert1, - expectDiffer: true, - }, - { - name: "nil vault cert", - haproxyCert: &haproxy.CertInfo{Serial: "1F5202"}, - vaultCert: nil, - expectDiffer: true, - }, - { - name: "both nil", - haproxyCert: nil, - vaultCert: nil, - expectDiffer: true, - }, - { - name: "matching serials uppercase", - haproxyCert: &haproxy.CertInfo{Serial: "1F5202"}, - vaultCert: cert1, - expectDiffer: false, - }, - { - name: "matching serials lowercase", - haproxyCert: &haproxy.CertInfo{Serial: "1f5202"}, - vaultCert: cert1, - expectDiffer: false, - }, - { - name: "matching serials with colons", - haproxyCert: &haproxy.CertInfo{Serial: "1F:52:02"}, - vaultCert: cert1, - expectDiffer: false, - }, - { - name: "different serials", - haproxyCert: &haproxy.CertInfo{Serial: "1F5202"}, - vaultCert: cert2, - expectDiffer: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := serialsDiffer(tt.haproxyCert, tt.vaultCert) - if result != tt.expectDiffer { - t.Errorf("serialsDiffer() = %v, want %v", result, tt.expectDiffer) - } - }) - } -} - -func TestBuildPEMBundle(t *testing.T) { - tests := []struct { - name string - secrets map[string]interface{} - expectError bool - expectPEM string - }{ - { - name: "valid cert and key with newlines", - secrets: map[string]interface{}{ - "certificate": "-----BEGIN CERTIFICATE-----\nMIIB...\n-----END CERTIFICATE-----\n", - "private_key": "-----BEGIN PRIVATE KEY-----\nMIIE...\n-----END PRIVATE KEY-----\n", - }, - expectError: false, - expectPEM: "-----BEGIN CERTIFICATE-----\nMIIB...\n-----END CERTIFICATE-----\n-----BEGIN PRIVATE KEY-----\nMIIE...\n-----END PRIVATE KEY-----\n", - }, - { - name: "valid cert and key without trailing newline", - secrets: map[string]interface{}{ - "certificate": "-----BEGIN CERTIFICATE-----\nMIIB...\n-----END CERTIFICATE-----", - "private_key": "-----BEGIN PRIVATE KEY-----\nMIIE...\n-----END PRIVATE KEY-----", - }, - expectError: false, - expectPEM: "-----BEGIN CERTIFICATE-----\nMIIB...\n-----END CERTIFICATE-----\n-----BEGIN PRIVATE KEY-----\nMIIE...\n-----END PRIVATE KEY-----", - }, - { - name: "missing certificate", - secrets: map[string]interface{}{ - "private_key": "-----BEGIN PRIVATE KEY-----\nMIIE...\n-----END PRIVATE KEY-----", - }, - expectError: true, - }, - { - name: "missing private_key", - secrets: map[string]interface{}{ - "certificate": "-----BEGIN CERTIFICATE-----\nMIIB...\n-----END CERTIFICATE-----", - }, - expectError: true, - }, - { - name: "empty secrets", - secrets: map[string]interface{}{}, - expectError: true, - }, - { - name: "empty certificate value", - secrets: map[string]interface{}{ - "certificate": "", - "private_key": "-----BEGIN PRIVATE KEY-----\nMIIE...\n-----END PRIVATE KEY-----", - }, - expectError: true, - }, - { - name: "empty private_key value", - secrets: map[string]interface{}{ - "certificate": "-----BEGIN CERTIFICATE-----\nMIIB...\n-----END CERTIFICATE-----", - "private_key": "", - }, - expectError: true, - }, - { - name: "wrong type for certificate", - secrets: map[string]interface{}{ - "certificate": 12345, - "private_key": "-----BEGIN PRIVATE KEY-----\nMIIE...\n-----END PRIVATE KEY-----", - }, - expectError: true, - }, - { - name: "wrong type for private_key", - secrets: map[string]interface{}{ - "certificate": "-----BEGIN CERTIFICATE-----\nMIIB...\n-----END CERTIFICATE-----", - "private_key": 12345, - }, - expectError: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result, err := buildPEMBundle(tt.secrets) - if tt.expectError { - if err == nil { - t.Errorf("buildPEMBundle() expected error, got nil") - } - } else { - if err != nil { - t.Errorf("buildPEMBundle() unexpected error: %v", err) - } - if result != tt.expectPEM { - t.Errorf("buildPEMBundle() = %q, want %q", result, tt.expectPEM) - } - } - }) - } -} - -func TestBuildPEMBundleWithRealCertFormat(t *testing.T) { - // Test with realistic PEM format - cert := `-----BEGIN CERTIFICATE----- -MIIDXTCCAkWgAwIBAgIJAJC1HiIAZAiUMA0GCSqGSIb3Qw0BBQUAMEUxCzAJBgNV -BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX -aWRnaXRzIFB0eSBMdGQwHhcNMTExMjMxMDg1OTQ0WhcNMTIxMjMwMDg1OTQ0WjBF ------END CERTIFICATE-----` - - //nolint:gosec // This is a test key, not a real credential - key := `-----BEGIN RSA PRIVATE KEY----- -MIIEowIBAAKCAQEA0m59l2u9iDnMbrXHfqkOrn2dVQ3vfBJqcDuFUK03d+1PZGbV ------END RSA PRIVATE KEY-----` - - secrets := map[string]interface{}{ - "certificate": cert, - "private_key": key, - } - - result, err := buildPEMBundle(secrets) - if err != nil { - t.Fatalf("buildPEMBundle() unexpected error: %v", err) - } - - // Verify structure - if !containsSubstring(result, "-----BEGIN CERTIFICATE-----") { - t.Error("result should contain certificate header") - } - if !containsSubstring(result, "-----END CERTIFICATE-----") { - t.Error("result should contain certificate footer") - } - if !containsSubstring(result, "-----BEGIN RSA PRIVATE KEY-----") { - t.Error("result should contain private key header") - } - if !containsSubstring(result, "-----END RSA PRIVATE KEY-----") { - t.Error("result should contain private key footer") - } -} - -// Helper function to check if string contains substring -func containsSubstring(s, substr string) bool { - return len(s) >= len(substr) && (s == substr || len(substr) == 0 || findSubstring(s, substr)) -} - -func findSubstring(s, substr string) bool { - for i := 0; i <= len(s)-len(substr); i++ { - if s[i:i+len(substr)] == substr { - return true - } - } - return false -} - -// Test that formatSerial works with actual x509 certificate serial numbers -func TestFormatSerialWithRandomSerial(t *testing.T) { - // Generate a random serial number like a real CA would - serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128)) - if err != nil { - t.Fatalf("failed to generate serial number: %v", err) - } - - serialBytes := serialNumber.Bytes() - result := formatSerial(serialBytes) - - // Verify the result is a valid hex string - if len(result) == 0 && len(serialBytes) > 0 { - t.Error("formatSerial should return non-empty string for non-empty bytes") - } - - // Verify all characters are valid hex - for _, c := range result { - isDigit := c >= '0' && c <= '9' - isHexLetter := c >= 'a' && c <= 'f' - if !isDigit && !isHexLetter { - t.Errorf("formatSerial returned invalid hex character: %c", c) - } - } -} - -// Benchmark tests -func BenchmarkEndsWith(b *testing.B) { - s := "-----BEGIN CERTIFICATE-----\nMIIB...\n-----END CERTIFICATE-----\n" - suffix := "\n" - - for i := 0; i < b.N; i++ { - endsWith(s, suffix) - } -} - -func BenchmarkFormatSerial(b *testing.B) { - serial := []byte{0x1f, 0x52, 0x02, 0xe0, 0x20, 0x83, 0x86, 0x1b, 0x30, 0x2f, 0xfa, 0x09, 0x04, 0x57, 0x21, 0xf0, 0x7c, 0x86, 0x5e, 0xfd} - - for i := 0; i < b.N; i++ { - formatSerial(serial) - } -} - -func BenchmarkBuildPEMBundle(b *testing.B) { - secrets := map[string]interface{}{ - "certificate": "-----BEGIN CERTIFICATE-----\nMIIB...\n-----END CERTIFICATE-----\n", - "private_key": "-----BEGIN PRIVATE KEY-----\nMIIE...\n-----END PRIVATE KEY-----\n", - } - - for i := 0; i < b.N; i++ { - _, _ = buildPEMBundle(secrets) - } -} - -// Test edge cases for time-based logic (used in shouldUpdateCertificate) -func TestCertificateExpiryLogic(t *testing.T) { - // Test IsExpiring function from haproxy package - now := time.Now() - - tests := []struct { - name string - notAfter time.Time - renewBeforeDays int - expectExpiring bool - }{ - { - name: "expired certificate", - notAfter: now.AddDate(0, 0, -1), - renewBeforeDays: 30, - expectExpiring: true, - }, - { - name: "expiring within threshold", - notAfter: now.AddDate(0, 0, 15), - renewBeforeDays: 30, - expectExpiring: true, - }, - { - name: "expiring at threshold", - notAfter: now.AddDate(0, 0, 30), - renewBeforeDays: 30, - expectExpiring: true, - }, - { - name: "not expiring", - notAfter: now.AddDate(0, 0, 60), - renewBeforeDays: 30, - expectExpiring: false, - }, - { - name: "zero threshold", - notAfter: now.AddDate(0, 0, 1), - renewBeforeDays: 0, - expectExpiring: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - certInfo := &haproxy.CertInfo{ - NotAfter: tt.notAfter, - } - result := haproxy.IsExpiring(certInfo, tt.renewBeforeDays) - if result != tt.expectExpiring { - t.Errorf("IsExpiring() = %v, want %v (notAfter: %v, threshold: %d days)", - result, tt.expectExpiring, tt.notAfter, tt.renewBeforeDays) - } - }) - } -} diff --git a/pkg/config/config.go b/pkg/config/config.go index b5ad930..e54b186 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -63,7 +63,7 @@ type Certificatee struct { // HAProxyDataPlaneAPIUser is the username for HAProxy Data Plane API basic auth HAProxyDataPlaneAPIUser string `envconfig:"HAPROXY_DATAPLANE_API_USER"` // HAProxyDataPlaneAPIPassword is the password for HAProxy Data Plane API basic auth - HAProxyDataPlaneAPIPassword string `envconfig:"HAPROXY_DATAPLANE_API_PASSWORD""` + HAProxyDataPlaneAPIPassword string `envconfig:"HAPROXY_DATAPLANE_API_PASSWORD"` // HAProxyDataPlaneAPIInsecure skips TLS certificate verification (not recommended for production) HAProxyDataPlaneAPIInsecure bool `envconfig:"HAPROXY_DATAPLANE_API_INSECURE" default:"false"` } diff --git a/pkg/haproxy/client.go b/pkg/haproxy/client.go index 2e05ee0..ef6c035 100644 --- a/pkg/haproxy/client.go +++ b/pkg/haproxy/client.go @@ -3,14 +3,11 @@ package haproxy import ( "bytes" "crypto/tls" - "crypto/x509" "encoding/json" - "encoding/pem" "fmt" "io" "mime/multipart" "net/http" - "net/url" "regexp" "strings" "time" @@ -20,21 +17,14 @@ import ( "github.com/sirupsen/logrus" ) -// CertInfo holds certificate information from HAProxy Data Plane API +// CertInfo holds certificate information. +// Note: HAProxy Data Plane API only returns Filename and StorageName. +// Other fields (NotAfter, Serial) are only populated when parsing PEM data directly. type CertInfo struct { - Filename string `json:"file"` - StorageName string `json:"storage_name"` - Status string `json:"status"` - Serial string `json:"serial"` - NotBefore time.Time `json:"-"` - NotBeforeStr string `json:"not_before"` - NotAfter time.Time `json:"-"` - NotAfterStr string `json:"not_after"` - Subject string `json:"subject"` - Issuer string `json:"issuer"` - Algorithm string `json:"algorithm"` - SHA1 string `json:"sha1_fingerprint"` - SANs []string `json:"subject_alternative_names"` + Filename string `json:"file"` + StorageName string `json:"storage_name"` + NotAfter time.Time `json:"-"` + Serial string `json:"serial"` } // Client is a HAProxy Data Plane API client @@ -207,103 +197,6 @@ func (c *Client) ListCertificateRefs() ([]CertificateRef, error) { return refs, nil } -// GetCertificateInfo retrieves detailed information about a specific certificate by name -func (c *Client) GetCertificateInfo(certName string) (*CertInfo, error) { - return c.GetCertificateInfoByPath(certName, certName) -} - -// GetCertificateInfoByPath retrieves detailed information about a certificate using its file path -func (c *Client) GetCertificateInfoByPath(filePath, displayName string) (*CertInfo, error) { - // URL-encode the file path for the API request - encodedPath := url.PathEscape(filePath) - path := fmt.Sprintf("/v2/services/haproxy/storage/ssl_certificates/%s", encodedPath) - resp, err := c.doRequest("GET", path, nil, "") - if err != nil { - return nil, err - } - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode == http.StatusNotFound { - return nil, errors.Errorf("certificate %s not found", displayName) - } - - if resp.StatusCode != http.StatusOK { - body, _ := io.ReadAll(resp.Body) - return nil, errors.Errorf("failed to get certificate info: status %d, body: %s", resp.StatusCode, string(body)) - } - - // The storage API returns the PEM content directly - pemData, err := io.ReadAll(resp.Body) - if err != nil { - return nil, errors.Wrap(err, "failed to read certificate data") - } - - // Parse the PEM certificate to extract info - info, err := parsePEMCertificate(pemData, displayName) - if err != nil { - return nil, errors.Wrap(err, "failed to parse certificate") - } - - return info, nil -} - -// GetCertificateInfoByRef retrieves detailed information using a CertificateRef -func (c *Client) GetCertificateInfoByRef(ref CertificateRef) (*CertInfo, error) { - // Use FilePath for API lookup, as the HAProxy Data Plane API v2 storage endpoint - // expects the full file path, not the storage name - lookupPath := ref.FilePath - if lookupPath == "" { - lookupPath = ref.DisplayName - } - return c.GetCertificateInfoByPath(lookupPath, ref.DisplayName) -} - -// parsePEMCertificate parses a PEM certificate and extracts certificate info -func parsePEMCertificate(pemData []byte, certName string) (*CertInfo, error) { - block, _ := pem.Decode(pemData) - if block == nil { - return nil, errors.New("failed to decode PEM block") - } - - cert, err := x509.ParseCertificate(block.Bytes) - if err != nil { - return nil, errors.Wrap(err, "failed to parse X.509 certificate") - } - - info := &CertInfo{ - StorageName: certName, - Subject: cert.Subject.String(), - Issuer: cert.Issuer.String(), - Serial: cert.SerialNumber.Text(16), - NotBefore: cert.NotBefore, - NotAfter: cert.NotAfter, - SANs: cert.DNSNames, - } - - return info, nil -} - -// parseDataPlaneAPITime parses time strings from Data Plane API -func parseDataPlaneAPITime(s string) (time.Time, error) { - // Data Plane API may return time in various formats - formats := []string{ - time.RFC3339, - "2006-01-02T15:04:05Z", - "Jan 2 15:04:05 2006 MST", - "Jan 02 15:04:05 2006 MST", - "Jan _2 15:04:05 2006 MST", - } - - for _, format := range formats { - t, err := time.Parse(format, s) - if err == nil { - return t, nil - } - } - - return time.Time{}, fmt.Errorf("failed to parse time: %s", s) -} - // UpdateCertificate uploads and commits a certificate update via Data Plane API func (c *Client) UpdateCertificate(certName, pemData string) error { // Create multipart form data @@ -434,19 +327,19 @@ type logrusLeveledLogger struct { logger *logrus.Logger } -func (l *logrusLeveledLogger) Error(msg string, keysAndValues ...interface{}) { +func (l *logrusLeveledLogger) Error(msg string, keysAndValues ...any) { l.logger.WithFields(toLogrusFields(keysAndValues)).Error(msg) } -func (l *logrusLeveledLogger) Info(msg string, keysAndValues ...interface{}) { +func (l *logrusLeveledLogger) Info(msg string, keysAndValues ...any) { l.logger.WithFields(toLogrusFields(keysAndValues)).Info(msg) } -func (l *logrusLeveledLogger) Debug(msg string, keysAndValues ...interface{}) { +func (l *logrusLeveledLogger) Debug(msg string, keysAndValues ...any) { l.logger.WithFields(toLogrusFields(keysAndValues)).Debug(msg) } -func (l *logrusLeveledLogger) Warn(msg string, keysAndValues ...interface{}) { +func (l *logrusLeveledLogger) Warn(msg string, keysAndValues ...any) { l.logger.WithFields(toLogrusFields(keysAndValues)).Warn(msg) } diff --git a/pkg/haproxy/client_test.go b/pkg/haproxy/client_test.go index d1e9a5b..d952f7f 100644 --- a/pkg/haproxy/client_test.go +++ b/pkg/haproxy/client_test.go @@ -129,79 +129,6 @@ func TestNormalizeSerial(t *testing.T) { } } -func TestParseDataPlaneAPITime(t *testing.T) { - tests := []struct { - name string - input string - wantErr bool - check func(t *testing.T, tm time.Time) - }{ - { - name: "RFC3339 format", - input: "2024-08-12T17:05:34Z", - wantErr: false, - check: func(t *testing.T, tm time.Time) { - if tm.Day() != 12 || tm.Month() != time.August || tm.Year() != 2024 { - t.Errorf("got %v, want Aug 12 2024", tm) - } - }, - }, - { - name: "RFC3339 without T", - input: "2025-01-15T00:00:00Z", - wantErr: false, - check: func(t *testing.T, tm time.Time) { - if tm.Month() != time.January || tm.Year() != 2025 { - t.Errorf("got %v, want Jan 2025", tm) - } - }, - }, - { - name: "HAProxy format double digit day", - input: "Aug 12 17:05:34 2020 GMT", - wantErr: false, - check: func(t *testing.T, tm time.Time) { - if tm.Day() != 12 || tm.Month() != time.August || tm.Year() != 2020 { - t.Errorf("got %v, want Aug 12 2020", tm) - } - }, - }, - { - name: "HAProxy format single digit day padded", - input: "Aug 02 17:05:34 2020 GMT", - wantErr: false, - check: func(t *testing.T, tm time.Time) { - if tm.Day() != 2 { - t.Errorf("Day = %d, want 2", tm.Day()) - } - }, - }, - { - name: "invalid format", - input: "not a date", - wantErr: true, - }, - { - name: "empty string", - input: "", - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tm, err := parseDataPlaneAPITime(tt.input) - if (err != nil) != tt.wantErr { - t.Errorf("parseDataPlaneAPITime(%q) error = %v, wantErr %v", tt.input, err, tt.wantErr) - return - } - if !tt.wantErr && tt.check != nil { - tt.check(t, tm) - } - }) - } -} - func TestNewClient(t *testing.T) { logger := logrus.New() logger.SetLevel(logrus.PanicLevel) // Suppress logs in tests @@ -523,109 +450,6 @@ func TestListCertificates(t *testing.T) { } } -func TestGetCertificateInfo(t *testing.T) { - logger := logrus.New() - logger.SetLevel(logrus.PanicLevel) - - // Sample PEM certificate for testing (self-signed, CN=example.com) - validPEM := `-----BEGIN CERTIFICATE----- -MIIDDTCCAfWgAwIBAgIUe9mCIn9FkwgXLlsXK6cwCMbavacwDQYJKoZIhvcNAQEL -BQAwFjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wHhcNMjYwMTIwMTQwMTM0WhcNMjcw -MTIwMTQwMTM0WjAWMRQwEgYDVQQDDAtleGFtcGxlLmNvbTCCASIwDQYJKoZIhvcN -AQEBBQADggEPADCCAQoCggEBAK9vQbb4mhM0EKzKF40tM4UtZNquBfAR4RwaJWme -WowIe/zBK8qZxSO8W+1LmJguR1CLlytfQ3iv5y4LdQ1tsn350EmmHKfD31NOHxr9 -F3GmsmSkHJBbukcpAl28ezTajtCImn6wciuui5ivUbKfuZXn4AEBNlaerGywQE2Y -0CNMKZ1/HnyrJymWyPb4tyJzfyYOdsPLwPt7GTAt4yqsRHnjIaIO2KD2OkmgFWMC -K5w64M8Zs7cg5Jk1zE0hFDKAE/3T78SYDGh+kHmDe68P75VACJBDgWYLWRAFWsOA -o8IrAUNYvCKHHXshEnR2HJSgoPT6nkNOgVzWTG52hnNuhLsCAwEAAaNTMFEwHQYD -VR0OBBYEFMljzE+9nN38vJhdB1ovTbiyuuZAMB8GA1UdIwQYMBaAFMljzE+9nN38 -vJhdB1ovTbiyuuZAMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEB -ABI48y8xh9jQSYPmt9dIkMUmI8WyjkdVzBIs4vAqZ1DeOsxUJ3dLwmr1ImTTY7Sw -m6yDoTNInWsdjo5rjA9mgrkq5OTSVJNVe2bcNfZyFsTJ7B1OwGffCzBnFNwW/Zzf -OzZ53OaXmtWHeMP2cHhH7yEX7NVuB0HB/8CTu1F/jLuUTaaiCGbF+VCCHtLL5RAL -N4Vg0dt1Ls7qBpX/22o3cMNI15ixOOhW6Qug2at304/K0SJsXQifJ7SQiMRU84ov -FouJ5aRz+i5UvgFqDEMHY1PaEDXPAwHH+Kl3iC6L59McPRD3yRNlOMqquAOS2b8Y -kF7B68QUswmVK4Icz6zBgmo= ------END CERTIFICATE-----` - - tests := []struct { - name string - certPath string - pemData string - statusCode int - wantErr bool - checkFunc func(t *testing.T, info *CertInfo) - }{ - { - name: "valid certificate info", - certPath: "example.com.pem", - pemData: validPEM, - statusCode: http.StatusOK, - wantErr: false, - checkFunc: func(t *testing.T, info *CertInfo) { - if info.Subject == "" { - t.Error("Subject should not be empty") - } - if info.NotAfter.IsZero() { - t.Error("NotAfter should be set") - } - }, - }, - { - name: "certificate not found", - certPath: "notfound.pem", - pemData: "", - statusCode: http.StatusNotFound, - wantErr: true, - }, - { - name: "server error", - certPath: "error.pem", - pemData: "", - statusCode: http.StatusInternalServerError, - wantErr: true, - }, - { - name: "invalid PEM data", - certPath: "invalid.pem", - pemData: "not a valid PEM", - statusCode: http.StatusOK, - wantErr: true, // Should fail to parse - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - mock := newMockDataPlaneAPI(t) - defer mock.Close() - - mock.SetHandler("GET", "/v2/services/haproxy/storage/ssl_certificates/"+tt.certPath, func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(tt.statusCode) - if tt.statusCode == http.StatusOK { - _, _ = w.Write([]byte(tt.pemData)) - } else { - _, _ = w.Write([]byte(`{"message": "error"}`)) - } - }) - - client, err := NewClient(ClientConfig{BaseURL: mock.URL()}, logger) - if err != nil { - t.Fatalf("NewClient() error = %v", err) - } - - info, err := client.GetCertificateInfo(tt.certPath) - if (err != nil) != tt.wantErr { - t.Errorf("GetCertificateInfo() error = %v, wantErr %v", err, tt.wantErr) - return - } - - if !tt.wantErr && tt.checkFunc != nil { - tt.checkFunc(t, info) - } - }) - } -} - func TestUpdateCertificate(t *testing.T) { logger := logrus.New() logger.SetLevel(logrus.PanicLevel)