Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 34 additions & 1 deletion cmd/certificatee/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import (

var (
version = "dev" // GoReleaser will inject the Git tag here

errNoUsableVaultCertificate = errors.New("no usable Vault certificate")
)

type certificateStore interface {
Expand All @@ -32,6 +34,20 @@ type certificateSyncResult struct {
storageSynced bool
runtimeUpdated bool
reason string
skipReason string
}

type noUsableVaultCertificateError struct {
domains []string
cause error
}

func (e *noUsableVaultCertificateError) Error() string {
return fmt.Sprintf("no usable Vault certificate found for candidates %s: %v", strings.Join(e.domains, ", "), e.cause)
}

func (e *noUsableVaultCertificateError) Unwrap() []error {
return []error{errNoUsableVaultCertificate, e.cause}
}

func main() {
Expand Down Expand Up @@ -137,6 +153,7 @@ func processHAProxyEndpoint(logger *logrus.Logger, cfg config.Config, vaultClien

var errs []error
var expiringCount int
var skippedVaultCount int

for _, ref := range certRefs {
displayName := ref.DisplayName
Expand Down Expand Up @@ -180,6 +197,12 @@ func processHAProxyEndpoint(logger *logrus.Logger, cfg config.Config, vaultClien
continue
}

if syncResult.skipReason != "" {
skippedVaultCount++
logger.Debugf("[%s] Skipping certificate %s: %s", endpoint, displayName, syncResult.skipReason)
continue
}

if syncResult.storageSynced {
logger.Infof("[%s] Certificate %s persisted to storage", endpoint, displayName)
}
Expand All @@ -195,6 +218,9 @@ func processHAProxyEndpoint(logger *logrus.Logger, cfg config.Config, vaultClien

// Record expiring certificates count
certmetrics.CertificatesExpiring.WithLabelValues(endpoint).Set(float64(expiringCount))
if skippedVaultCount > 0 {
logger.Infof("[%s] Skipped %d certificate(s) without usable Vault replacement material", endpoint, skippedVaultCount)
}

return errors.Join(errs...)
}
Expand Down Expand Up @@ -403,7 +429,10 @@ func readValidVaultCertificateBundle(domains []string, vaultClient certificateSt
return "", nil, nil, fmt.Errorf("no Vault certificate candidates found")
}

return "", nil, nil, fmt.Errorf("no usable Vault certificate found for candidates %s: %w", strings.Join(domains, ", "), errors.Join(errs...))
return "", nil, nil, &noUsableVaultCertificateError{
domains: domains,
cause: errors.Join(errs...),
}
}

func firstDomain(domains []string) string {
Expand All @@ -425,6 +454,10 @@ func syncCertificate(certPath string, domains []string, vaultClient certificateS

domain, certificateSecrets, vaultCert, err := readValidVaultCertificateBundle(domains, vaultClient, haproxyCert)
if err != nil {
if !isExpiring && errors.Is(err, errNoUsableVaultCertificate) {
result.skipReason = err.Error()
return result, nil
}
return result, err
}
result.domain = domain
Expand Down
67 changes: 67 additions & 0 deletions cmd/certificatee/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"errors"
"fmt"
"io"
"math/big"
Expand Down Expand Up @@ -317,6 +318,72 @@ func TestProcessHAProxyEndpointMarksV3ReadyEndpoint(t *testing.T) {
}
}

func TestSyncCertificateSkipsNonExpiringUnusableVaultCandidate(t *testing.T) {
logger := logrus.New()
logger.SetLevel(logrus.PanicLevel)

server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Fatalf("unexpected HAProxy request for skipped certificate: %s %s", r.Method, r.URL.String())
}))
defer server.Close()

haproxyClient, err := haproxy.NewClient(haproxy.ClientConfig{BaseURL: server.URL}, logger)
if err != nil {
t.Fatalf("NewClient() error = %v", err)
}

result, err := syncCertificate(
"certs/_.mainnet.rpcpool.com.pem",
[]string{"*.mainnet.rpcpool.com"},
fakeCertificateStore{},
haproxyClient,
&haproxy.CertificateDetail{NotAfter: time.Now().AddDate(0, 0, 60)},
30,
)
if err != nil {
t.Fatalf("syncCertificate() error = %v, want nil", err)
}
if result.skipReason == "" {
t.Fatal("skipReason is empty, want non-empty")
}
if result.storageSynced || result.runtimeUpdated {
t.Fatalf("storageSynced=%v runtimeUpdated=%v, want both false", result.storageSynced, result.runtimeUpdated)
}
}

func TestSyncCertificateErrorsWhenExpiringWithoutUsableVaultCandidate(t *testing.T) {
logger := logrus.New()
logger.SetLevel(logrus.PanicLevel)

server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Fatalf("unexpected HAProxy request for failed certificate: %s %s", r.Method, r.URL.String())
}))
defer server.Close()

haproxyClient, err := haproxy.NewClient(haproxy.ClientConfig{BaseURL: server.URL}, logger)
if err != nil {
t.Fatalf("NewClient() error = %v", err)
}

result, err := syncCertificate(
"certs/_.mainnet.rpcpool.com.pem",
[]string{"*.mainnet.rpcpool.com"},
fakeCertificateStore{},
haproxyClient,
&haproxy.CertificateDetail{NotAfter: time.Now().AddDate(0, 0, 10)},
30,
)
if err == nil {
t.Fatal("syncCertificate() error = nil, want error")
}
if !errors.Is(err, errNoUsableVaultCertificate) {
t.Fatalf("syncCertificate() error = %v, want no usable Vault certificate", err)
}
if !result.isExpiring {
t.Fatal("isExpiring = false, want true")
}
}

func TestSyncCertificatePersistsStorageWhenRuntimeCurrent(t *testing.T) {
logger := logrus.New()
logger.SetLevel(logrus.PanicLevel)
Expand Down
Loading