mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 15:32:02 +00:00
07275bf92f
Agents now report OS, architecture, IP address, hostname, and version via heartbeat using runtime.GOOS, runtime.GOARCH, and net.Dial. New migration adds columns to agents table. Heartbeat handler, service, and repository updated to accept and persist metadata. GUI shows OS/Arch in agent list and full system info in agent detail page. Apache httpd connector: separate cert/chain/key files, apachectl configtest validation, graceful reload. HAProxy connector: combined PEM file (cert+chain+key), optional config validation, reload. Both wired into agent binary's target connector switch. 14 tests for new connectors. All existing tests updated for new Heartbeat/UpdateHeartbeat signatures. Docs updated across README, architecture, concepts, and connectors guides. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
204 lines
5.1 KiB
Go
204 lines
5.1 KiB
Go
package haproxy_test
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"log/slog"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/shankar0123/certctl/internal/connector/target"
|
|
"github.com/shankar0123/certctl/internal/connector/target/haproxy"
|
|
)
|
|
|
|
func TestHAProxyConnector_ValidateConfig(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
|
ctx := context.Background()
|
|
|
|
t.Run("valid config", func(t *testing.T) {
|
|
cfg := haproxy.Config{
|
|
PEMPath: "/tmp/haproxy/cert.pem",
|
|
ReloadCommand: "echo reload",
|
|
}
|
|
|
|
connector := haproxy.New(&cfg, logger)
|
|
rawConfig, _ := json.Marshal(cfg)
|
|
err := connector.ValidateConfig(ctx, rawConfig)
|
|
if err != nil {
|
|
t.Fatalf("ValidateConfig failed: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("missing pem_path", func(t *testing.T) {
|
|
cfg := haproxy.Config{
|
|
ReloadCommand: "echo reload",
|
|
}
|
|
|
|
connector := haproxy.New(&cfg, logger)
|
|
rawConfig, _ := json.Marshal(cfg)
|
|
err := connector.ValidateConfig(ctx, rawConfig)
|
|
if err == nil {
|
|
t.Fatal("expected error for missing pem_path")
|
|
}
|
|
})
|
|
|
|
t.Run("missing reload_command", func(t *testing.T) {
|
|
cfg := haproxy.Config{
|
|
PEMPath: "/tmp/cert.pem",
|
|
}
|
|
|
|
connector := haproxy.New(&cfg, logger)
|
|
rawConfig, _ := json.Marshal(cfg)
|
|
err := connector.ValidateConfig(ctx, rawConfig)
|
|
if err == nil {
|
|
t.Fatal("expected error for missing reload_command")
|
|
}
|
|
})
|
|
|
|
t.Run("invalid JSON", func(t *testing.T) {
|
|
connector := haproxy.New(&haproxy.Config{}, logger)
|
|
err := connector.ValidateConfig(ctx, json.RawMessage(`{invalid}`))
|
|
if err == nil {
|
|
t.Fatal("expected error for invalid JSON")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestHAProxyConnector_DeployCertificate(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
|
ctx := context.Background()
|
|
|
|
t.Run("successful deployment with combined PEM", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
pemPath := filepath.Join(tmpDir, "combined.pem")
|
|
|
|
cfg := &haproxy.Config{
|
|
PEMPath: pemPath,
|
|
ReloadCommand: "echo reload",
|
|
}
|
|
|
|
connector := haproxy.New(cfg, logger)
|
|
|
|
certPEM := "-----BEGIN CERTIFICATE-----\ncert\n-----END CERTIFICATE-----"
|
|
chainPEM := "-----BEGIN CERTIFICATE-----\nchain\n-----END CERTIFICATE-----"
|
|
keyPEM := "-----BEGIN EC PRIVATE KEY-----\nkey\n-----END EC PRIVATE KEY-----"
|
|
|
|
req := target.DeploymentRequest{
|
|
CertPEM: certPEM,
|
|
KeyPEM: keyPEM,
|
|
ChainPEM: chainPEM,
|
|
}
|
|
|
|
result, err := connector.DeployCertificate(ctx, req)
|
|
if err != nil {
|
|
t.Fatalf("DeployCertificate failed: %v", err)
|
|
}
|
|
|
|
if !result.Success {
|
|
t.Fatalf("expected success, got: %s", result.Message)
|
|
}
|
|
|
|
// Verify combined PEM was written
|
|
data, err := os.ReadFile(pemPath)
|
|
if err != nil {
|
|
t.Fatalf("failed to read PEM file: %v", err)
|
|
}
|
|
|
|
content := string(data)
|
|
if !strings.Contains(content, "cert") {
|
|
t.Error("combined PEM missing certificate")
|
|
}
|
|
if !strings.Contains(content, "chain") {
|
|
t.Error("combined PEM missing chain")
|
|
}
|
|
if !strings.Contains(content, "key") {
|
|
t.Error("combined PEM missing key")
|
|
}
|
|
|
|
// Verify secure permissions (contains private key)
|
|
info, err := os.Stat(pemPath)
|
|
if err != nil {
|
|
t.Fatalf("failed to stat PEM file: %v", err)
|
|
}
|
|
if info.Mode().Perm() != 0600 {
|
|
t.Errorf("expected PEM permissions 0600, got %v", info.Mode().Perm())
|
|
}
|
|
})
|
|
|
|
t.Run("reload command fails", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
pemPath := filepath.Join(tmpDir, "combined.pem")
|
|
|
|
cfg := &haproxy.Config{
|
|
PEMPath: pemPath,
|
|
ReloadCommand: "false", // always fails
|
|
}
|
|
|
|
connector := haproxy.New(cfg, logger)
|
|
|
|
req := target.DeploymentRequest{
|
|
CertPEM: "cert",
|
|
}
|
|
|
|
result, err := connector.DeployCertificate(ctx, req)
|
|
if err == nil {
|
|
t.Fatal("expected error when reload command fails")
|
|
}
|
|
if result.Success {
|
|
t.Fatal("expected failure result")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestHAProxyConnector_ValidateDeployment(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
|
ctx := context.Background()
|
|
|
|
t.Run("valid deployment", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
pemPath := filepath.Join(tmpDir, "combined.pem")
|
|
os.WriteFile(pemPath, []byte("combined-pem-content"), 0600)
|
|
|
|
cfg := &haproxy.Config{
|
|
PEMPath: pemPath,
|
|
ReloadCommand: "echo reload",
|
|
ValidateCommand: "echo ok",
|
|
}
|
|
|
|
connector := haproxy.New(cfg, logger)
|
|
|
|
result, err := connector.ValidateDeployment(ctx, target.ValidationRequest{
|
|
CertificateID: "mc-test",
|
|
Serial: "123",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("ValidateDeployment failed: %v", err)
|
|
}
|
|
if !result.Valid {
|
|
t.Fatal("expected valid deployment")
|
|
}
|
|
})
|
|
|
|
t.Run("missing PEM file", func(t *testing.T) {
|
|
cfg := &haproxy.Config{
|
|
PEMPath: "/nonexistent/combined.pem",
|
|
ReloadCommand: "echo reload",
|
|
}
|
|
|
|
connector := haproxy.New(cfg, logger)
|
|
|
|
result, err := connector.ValidateDeployment(ctx, target.ValidationRequest{
|
|
CertificateID: "mc-test",
|
|
Serial: "123",
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected error for missing PEM file")
|
|
}
|
|
if result.Valid {
|
|
t.Fatal("expected invalid result")
|
|
}
|
|
})
|
|
}
|