mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-02 23:06:36 +08:00
feat: add TLS certificate management functionality
- Implemented TLS certificate model and service for managing certificates. - Added API endpoints for creating, importing, listing, and deleting TLS certificates. - Enhanced proxy route configuration to support HTTPS with certificate selection. - Updated frontend to include TLS certificate management UI with manual and file import options. - Added validation for HTTPS routes to ensure certificates are selected. - Implemented tests for TLS certificate creation and proxy route validation.
This commit is contained in:
@@ -33,12 +33,16 @@ func main() {
|
||||
HeartbeatService: heartbeat.New(client),
|
||||
SyncService: syncservice.New(client, &nginx.Manager{
|
||||
RouteConfigPath: cfg.RouteConfigPath,
|
||||
CertDir: cfg.CertDir,
|
||||
NginxCertDir: cfg.NginxCertDir,
|
||||
Executor: nginx.NewExecutor(nginx.ExecutorOptions{
|
||||
NginxPath: cfg.NginxPath,
|
||||
DockerBinary: cfg.DockerBinary,
|
||||
ContainerName: cfg.NginxContainerName,
|
||||
Image: cfg.NginxDockerImage,
|
||||
RouteConfigPath: cfg.RouteConfigPath,
|
||||
CertDir: cfg.CertDir,
|
||||
NginxCertDir: cfg.NginxCertDir,
|
||||
}),
|
||||
}, stateStore),
|
||||
}
|
||||
|
||||
@@ -10,7 +10,9 @@ import (
|
||||
|
||||
const (
|
||||
defaultDockerRouteConfigRelativePath = "etc/nginx/conf.d/atsflare_routes.conf"
|
||||
defaultCertDirRelativePath = "etc/nginx/certs"
|
||||
defaultDockerStateRelativePath = "var/lib/atsflare/agent-state.json"
|
||||
defaultDockerNginxCertDir = "/etc/nginx/atsflare-certs"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
@@ -26,6 +28,8 @@ type Config struct {
|
||||
DockerBinary string `json:"docker_binary"`
|
||||
DataDir string `json:"data_dir"`
|
||||
RouteConfigPath string `json:"route_config_path"`
|
||||
CertDir string `json:"cert_dir"`
|
||||
NginxCertDir string `json:"nginx_cert_dir"`
|
||||
StatePath string `json:"state_path"`
|
||||
HeartbeatInterval time.Duration `json:"heartbeat_interval"`
|
||||
SyncInterval time.Duration `json:"sync_interval"`
|
||||
@@ -76,6 +80,16 @@ func applyDefaults(cfg *Config, baseDir string) {
|
||||
cfg.StatePath = filepath.Join(cfg.DataDir, defaultDockerStateRelativePath)
|
||||
}
|
||||
}
|
||||
if cfg.CertDir == "" {
|
||||
cfg.CertDir = filepath.Join(cfg.DataDir, defaultCertDirRelativePath)
|
||||
}
|
||||
if cfg.NginxCertDir == "" {
|
||||
if cfg.NginxPath != "" {
|
||||
cfg.NginxCertDir = cfg.CertDir
|
||||
} else {
|
||||
cfg.NginxCertDir = defaultDockerNginxCertDir
|
||||
}
|
||||
}
|
||||
if cfg.HeartbeatInterval <= 0 {
|
||||
cfg.HeartbeatInterval = 30 * time.Second
|
||||
}
|
||||
|
||||
@@ -36,6 +36,12 @@ func TestLoadDockerModeUsesManagedPaths(t *testing.T) {
|
||||
if cfg.RouteConfigPath != filepath.Join(dir, "data", defaultDockerRouteConfigRelativePath) {
|
||||
t.Fatalf("unexpected route config path: %s", cfg.RouteConfigPath)
|
||||
}
|
||||
if cfg.CertDir != filepath.Join(dir, "data", defaultCertDirRelativePath) {
|
||||
t.Fatalf("unexpected cert dir: %s", cfg.CertDir)
|
||||
}
|
||||
if cfg.NginxCertDir != defaultDockerNginxCertDir {
|
||||
t.Fatalf("unexpected nginx cert dir: %s", cfg.NginxCertDir)
|
||||
}
|
||||
if cfg.StatePath != filepath.Join(dir, "data", defaultDockerStateRelativePath) {
|
||||
t.Fatalf("unexpected state path: %s", cfg.StatePath)
|
||||
}
|
||||
@@ -72,6 +78,9 @@ func TestLoadPathModeKeepsExplicitPaths(t *testing.T) {
|
||||
if cfg.StatePath != "/tmp/agent-state.json" {
|
||||
t.Fatalf("unexpected state path: %s", cfg.StatePath)
|
||||
}
|
||||
if cfg.NginxCertDir != cfg.CertDir {
|
||||
t.Fatalf("expected path mode nginx cert dir to equal cert dir, got %s / %s", cfg.NginxCertDir, cfg.CertDir)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadUsesCustomDataDirForGeneratedFiles(t *testing.T) {
|
||||
@@ -104,4 +113,7 @@ func TestLoadUsesCustomDataDirForGeneratedFiles(t *testing.T) {
|
||||
if cfg.StatePath != "/srv/atsflare/"+defaultDockerStateRelativePath {
|
||||
t.Fatalf("unexpected state path: %s", cfg.StatePath)
|
||||
}
|
||||
if cfg.CertDir != "/srv/atsflare/"+defaultCertDirRelativePath {
|
||||
t.Fatalf("unexpected cert dir: %s", cfg.CertDir)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,9 +9,14 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"atsflare-agent/internal/protocol"
|
||||
)
|
||||
|
||||
const CertDirPlaceholder = "__ATSF_CERT_DIR__"
|
||||
|
||||
type Executor interface {
|
||||
Test(ctx context.Context) error
|
||||
Reload(ctx context.Context) error
|
||||
@@ -60,6 +65,8 @@ type DockerExecutor struct {
|
||||
ContainerName string
|
||||
Image string
|
||||
RouteConfigDir string
|
||||
CertDir string
|
||||
NginxCertDir string
|
||||
Runner CommandRunner
|
||||
}
|
||||
|
||||
@@ -71,6 +78,8 @@ func (e *DockerExecutor) Test(ctx context.Context) error {
|
||||
"--rm",
|
||||
"-v",
|
||||
fmt.Sprintf("%s:/etc/nginx/conf.d", e.RouteConfigDir),
|
||||
"-v",
|
||||
fmt.Sprintf("%s:%s", e.CertDir, e.NginxCertDir),
|
||||
e.Image,
|
||||
"nginx",
|
||||
"-t",
|
||||
@@ -124,6 +133,7 @@ func (e *DockerExecutor) runContainer(ctx context.Context) error {
|
||||
"-p", "80:80",
|
||||
"-p", "443:443",
|
||||
"-v", fmt.Sprintf("%s:/etc/nginx/conf.d", e.RouteConfigDir),
|
||||
"-v", fmt.Sprintf("%s:%s", e.CertDir, e.NginxCertDir),
|
||||
e.Image,
|
||||
}
|
||||
runOutput, runErr := e.Runner.Run(ctx, e.DockerBinary, runArgs...)
|
||||
@@ -135,28 +145,33 @@ func (e *DockerExecutor) runContainer(ctx context.Context) error {
|
||||
|
||||
type Manager struct {
|
||||
RouteConfigPath string
|
||||
CertDir string
|
||||
NginxCertDir string
|
||||
Executor Executor
|
||||
}
|
||||
|
||||
func (m *Manager) Apply(ctx context.Context, content string) error {
|
||||
backupPath, hadExisting, err := m.backup()
|
||||
func (m *Manager) Apply(ctx context.Context, content string, supportFiles []protocol.SupportFile) error {
|
||||
backup, err := m.backup()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = os.WriteFile(m.RouteConfigPath, []byte(content), 0o644); err != nil {
|
||||
if err = m.writeSupportFiles(supportFiles); err != nil {
|
||||
_ = m.restore(backup)
|
||||
return err
|
||||
}
|
||||
renderedContent := m.renderConfig(content)
|
||||
if err = os.WriteFile(m.RouteConfigPath, []byte(renderedContent), 0o644); err != nil {
|
||||
_ = m.restore(backup)
|
||||
return err
|
||||
}
|
||||
if err = m.Executor.Test(ctx); err != nil {
|
||||
_ = m.restore(backupPath, hadExisting)
|
||||
_ = m.restore(backup)
|
||||
return err
|
||||
}
|
||||
if err = m.Executor.Reload(ctx); err != nil {
|
||||
_ = m.restore(backupPath, hadExisting)
|
||||
_ = m.restore(backup)
|
||||
return err
|
||||
}
|
||||
if backupPath != "" {
|
||||
_ = os.Remove(backupPath)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -178,8 +193,15 @@ func (m *Manager) CurrentChecksum() (string, error) {
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
sum := sha256.Sum256(data)
|
||||
return hex.EncodeToString(sum[:]), nil
|
||||
normalized := string(data)
|
||||
if m.NginxCertDir != "" {
|
||||
normalized = strings.ReplaceAll(normalized, m.NginxCertDir, CertDirPlaceholder)
|
||||
}
|
||||
files, err := m.readSupportFiles()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return bundleChecksum(normalized, files), nil
|
||||
}
|
||||
|
||||
type ExecutorOptions struct {
|
||||
@@ -188,6 +210,8 @@ type ExecutorOptions struct {
|
||||
ContainerName string
|
||||
Image string
|
||||
RouteConfigPath string
|
||||
CertDir string
|
||||
NginxCertDir string
|
||||
}
|
||||
|
||||
func NewExecutor(options ExecutorOptions) Executor {
|
||||
@@ -202,46 +226,167 @@ func NewExecutor(options ExecutorOptions) Executor {
|
||||
if absDir, err := filepath.Abs(routeConfigDir); err == nil {
|
||||
routeConfigDir = absDir
|
||||
}
|
||||
certDir := options.CertDir
|
||||
if absDir, err := filepath.Abs(certDir); err == nil {
|
||||
certDir = absDir
|
||||
}
|
||||
return &DockerExecutor{
|
||||
DockerBinary: options.DockerBinary,
|
||||
ContainerName: options.ContainerName,
|
||||
Image: options.Image,
|
||||
RouteConfigDir: routeConfigDir,
|
||||
CertDir: certDir,
|
||||
NginxCertDir: options.NginxCertDir,
|
||||
Runner: runner,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) backup() (string, bool, error) {
|
||||
if m.RouteConfigPath == "" {
|
||||
return "", false, errors.New("route config path 不能为空")
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(m.RouteConfigPath), 0o755); err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
data, err := os.ReadFile(m.RouteConfigPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return "", false, nil
|
||||
}
|
||||
return "", false, err
|
||||
}
|
||||
backupPath := m.RouteConfigPath + ".bak"
|
||||
if err = os.WriteFile(backupPath, data, 0o644); err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
return backupPath, true, nil
|
||||
type backupState struct {
|
||||
RouteExisted bool
|
||||
RouteData []byte
|
||||
Files []protocol.SupportFile
|
||||
}
|
||||
|
||||
func (m *Manager) restore(backupPath string, hadExisting bool) error {
|
||||
if hadExisting {
|
||||
data, err := os.ReadFile(backupPath)
|
||||
if err != nil {
|
||||
func (m *Manager) backup() (*backupState, error) {
|
||||
if m.RouteConfigPath == "" {
|
||||
return nil, errors.New("route config path 不能为空")
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(m.RouteConfigPath), 0o755); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := os.MkdirAll(m.CertDir, 0o755); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
state := &backupState{}
|
||||
data, err := os.ReadFile(m.RouteConfigPath)
|
||||
if err == nil {
|
||||
state.RouteExisted = true
|
||||
state.RouteData = data
|
||||
} else if !os.IsNotExist(err) {
|
||||
return nil, err
|
||||
}
|
||||
files, err := m.readSupportFiles()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
state.Files = files
|
||||
return state, nil
|
||||
}
|
||||
|
||||
func (m *Manager) restore(state *backupState) error {
|
||||
if state == nil {
|
||||
return nil
|
||||
}
|
||||
if state.RouteExisted {
|
||||
if err := os.WriteFile(m.RouteConfigPath, state.RouteData, 0o644); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(m.RouteConfigPath, data, 0o644)
|
||||
}
|
||||
if err := os.Remove(m.RouteConfigPath); err != nil && !os.IsNotExist(err) {
|
||||
} else if err := os.Remove(m.RouteConfigPath); err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
if err := os.RemoveAll(m.CertDir); err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
if err := os.MkdirAll(m.CertDir, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, file := range state.Files {
|
||||
targetPath := filepath.Join(m.CertDir, filepath.Clean(file.Path))
|
||||
if err := os.MkdirAll(filepath.Dir(targetPath), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.WriteFile(targetPath, []byte(file.Content), 0o600); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) writeSupportFiles(supportFiles []protocol.SupportFile) error {
|
||||
if err := os.RemoveAll(m.CertDir); err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
if err := os.MkdirAll(m.CertDir, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, file := range supportFiles {
|
||||
targetPath := filepath.Join(m.CertDir, filepath.Clean(file.Path))
|
||||
if err := os.MkdirAll(filepath.Dir(targetPath), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.WriteFile(targetPath, []byte(file.Content), 0o600); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) readSupportFiles() ([]protocol.SupportFile, error) {
|
||||
if m.CertDir == "" {
|
||||
return nil, nil
|
||||
}
|
||||
if _, err := os.Stat(m.CertDir); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
files := make([]protocol.SupportFile, 0)
|
||||
err := filepath.Walk(m.CertDir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
relativePath, err := filepath.Rel(m.CertDir, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
files = append(files, protocol.SupportFile{
|
||||
Path: filepath.ToSlash(relativePath),
|
||||
Content: string(data),
|
||||
})
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sort.Slice(files, func(i int, j int) bool {
|
||||
return files[i].Path < files[j].Path
|
||||
})
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func (m *Manager) renderConfig(content string) string {
|
||||
if m.NginxCertDir == "" {
|
||||
return content
|
||||
}
|
||||
return strings.ReplaceAll(content, CertDirPlaceholder, m.NginxCertDir)
|
||||
}
|
||||
|
||||
func checksum(content string) string {
|
||||
sum := sha256.Sum256([]byte(content))
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
func bundleChecksum(renderedConfig string, supportFiles []protocol.SupportFile) string {
|
||||
files := append([]protocol.SupportFile(nil), supportFiles...)
|
||||
sort.Slice(files, func(i int, j int) bool {
|
||||
return files[i].Path < files[j].Path
|
||||
})
|
||||
var builder strings.Builder
|
||||
builder.WriteString(renderedConfig)
|
||||
builder.WriteString("\n--support-files--\n")
|
||||
for _, file := range files {
|
||||
builder.WriteString(file.Path)
|
||||
builder.WriteString("\n")
|
||||
builder.WriteString(file.Content)
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
return checksum(builder.String())
|
||||
}
|
||||
|
||||
@@ -3,10 +3,13 @@ package nginx
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"atsflare-agent/internal/protocol"
|
||||
)
|
||||
|
||||
type runCall struct {
|
||||
@@ -19,6 +22,11 @@ type fakeRunner struct {
|
||||
runFn func(name string, args ...string) ([]byte, error)
|
||||
}
|
||||
|
||||
type fakeExecutor struct {
|
||||
testErr error
|
||||
reloadErr error
|
||||
}
|
||||
|
||||
func (r *fakeRunner) Run(ctx context.Context, name string, args ...string) ([]byte, error) {
|
||||
r.calls = append(r.calls, runCall{name: name, args: append([]string{}, args...)})
|
||||
if r.runFn != nil {
|
||||
@@ -27,6 +35,18 @@ func (r *fakeRunner) Run(ctx context.Context, name string, args ...string) ([]by
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (e *fakeExecutor) Test(ctx context.Context) error {
|
||||
return e.testErr
|
||||
}
|
||||
|
||||
func (e *fakeExecutor) Reload(ctx context.Context) error {
|
||||
return e.reloadErr
|
||||
}
|
||||
|
||||
func (e *fakeExecutor) EnsureRuntime(ctx context.Context, recreate bool) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestPathExecutorCommands(t *testing.T) {
|
||||
runner := &fakeRunner{}
|
||||
executor := &PathExecutor{
|
||||
@@ -74,6 +94,8 @@ func TestDockerExecutorStartsContainerWhenMissing(t *testing.T) {
|
||||
ContainerName: "atsflare-nginx",
|
||||
Image: "nginx:stable-alpine",
|
||||
RouteConfigDir: filepath.Clean("/tmp/routes"),
|
||||
CertDir: filepath.Clean("/tmp/certs"),
|
||||
NginxCertDir: "/etc/nginx/atsflare-certs",
|
||||
Runner: runner,
|
||||
}
|
||||
|
||||
@@ -103,6 +125,8 @@ func TestDockerExecutorStartsStoppedContainer(t *testing.T) {
|
||||
ContainerName: "atsflare-nginx",
|
||||
Image: "nginx:stable-alpine",
|
||||
RouteConfigDir: filepath.Clean("/tmp/routes"),
|
||||
CertDir: filepath.Clean("/tmp/certs"),
|
||||
NginxCertDir: "/etc/nginx/atsflare-certs",
|
||||
Runner: runner,
|
||||
}
|
||||
|
||||
@@ -138,6 +162,8 @@ func TestDockerExecutorRecreatesContainerOnStartup(t *testing.T) {
|
||||
ContainerName: "atsflare-nginx",
|
||||
Image: "nginx:stable-alpine",
|
||||
RouteConfigDir: filepath.Clean("/tmp/routes"),
|
||||
CertDir: filepath.Clean("/tmp/certs"),
|
||||
NginxCertDir: "/etc/nginx/atsflare-certs",
|
||||
Runner: runner,
|
||||
}
|
||||
|
||||
@@ -161,6 +187,8 @@ func TestNewExecutorUsesAbsoluteDockerMountPath(t *testing.T) {
|
||||
ContainerName: "atsflare-nginx",
|
||||
Image: "nginx:stable-alpine",
|
||||
RouteConfigPath: "./data/etc/nginx/conf.d/atsflare_routes.conf",
|
||||
CertDir: "./data/etc/nginx/certs",
|
||||
NginxCertDir: "/etc/nginx/atsflare-certs",
|
||||
})
|
||||
|
||||
dockerExecutor, ok := executor.(*DockerExecutor)
|
||||
@@ -174,3 +202,81 @@ func TestNewExecutorUsesAbsoluteDockerMountPath(t *testing.T) {
|
||||
t.Fatalf("unexpected route config dir: %s", dockerExecutor.RouteConfigDir)
|
||||
}
|
||||
}
|
||||
|
||||
func TestManagerApplyWritesSupportFilesAndReplacesPlaceholder(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
manager := &Manager{
|
||||
RouteConfigPath: filepath.Join(tempDir, "routes.conf"),
|
||||
CertDir: filepath.Join(tempDir, "certs"),
|
||||
NginxCertDir: "/etc/nginx/atsflare-certs",
|
||||
Executor: &fakeExecutor{},
|
||||
}
|
||||
|
||||
err := manager.Apply(context.Background(), "ssl_certificate __ATSF_CERT_DIR__/1.crt;", []protocol.SupportFile{
|
||||
{Path: "1.crt", Content: "cert-data"},
|
||||
{Path: "1.key", Content: "key-data"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Apply failed: %v", err)
|
||||
}
|
||||
|
||||
routeData, err := os.ReadFile(manager.RouteConfigPath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read route config: %v", err)
|
||||
}
|
||||
if !strings.Contains(string(routeData), "/etc/nginx/atsflare-certs/1.crt") {
|
||||
t.Fatalf("expected placeholder replacement in route config, got %s", string(routeData))
|
||||
}
|
||||
certData, err := os.ReadFile(filepath.Join(manager.CertDir, "1.crt"))
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read cert file: %v", err)
|
||||
}
|
||||
if string(certData) != "cert-data" {
|
||||
t.Fatalf("unexpected cert file content: %s", string(certData))
|
||||
}
|
||||
}
|
||||
|
||||
func TestManagerRollbackRestoresSupportFiles(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
routePath := filepath.Join(tempDir, "routes.conf")
|
||||
certDir := filepath.Join(tempDir, "certs")
|
||||
if err := os.MkdirAll(certDir, 0o755); err != nil {
|
||||
t.Fatalf("MkdirAll failed: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(routePath, []byte("old-route"), 0o644); err != nil {
|
||||
t.Fatalf("WriteFile failed: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(certDir, "1.crt"), []byte("old-cert"), 0o600); err != nil {
|
||||
t.Fatalf("WriteFile failed: %v", err)
|
||||
}
|
||||
manager := &Manager{
|
||||
RouteConfigPath: routePath,
|
||||
CertDir: certDir,
|
||||
NginxCertDir: "/etc/nginx/atsflare-certs",
|
||||
Executor: &fakeExecutor{
|
||||
testErr: errors.New("nginx test failed"),
|
||||
},
|
||||
}
|
||||
|
||||
err := manager.Apply(context.Background(), "new-route", []protocol.SupportFile{
|
||||
{Path: "1.crt", Content: "new-cert"},
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("expected Apply to fail")
|
||||
}
|
||||
|
||||
routeData, err := os.ReadFile(routePath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read route config: %v", err)
|
||||
}
|
||||
if string(routeData) != "old-route" {
|
||||
t.Fatalf("expected route rollback, got %s", string(routeData))
|
||||
}
|
||||
certData, err := os.ReadFile(filepath.Join(certDir, "1.crt"))
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read cert file: %v", err)
|
||||
}
|
||||
if string(certData) != "old-cert" {
|
||||
t.Fatalf("expected cert rollback, got %s", string(certData))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,8 +24,14 @@ type ApplyLogPayload struct {
|
||||
}
|
||||
|
||||
type ActiveConfigResponse struct {
|
||||
Version string `json:"version"`
|
||||
Checksum string `json:"checksum"`
|
||||
RenderedConfig string `json:"rendered_config"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
Version string `json:"version"`
|
||||
Checksum string `json:"checksum"`
|
||||
RenderedConfig string `json:"rendered_config"`
|
||||
SupportFiles []SupportFile `json:"support_files"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
type SupportFile struct {
|
||||
Path string `json:"path"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ type ConfigClient interface {
|
||||
}
|
||||
|
||||
type NginxManager interface {
|
||||
Apply(ctx context.Context, content string) error
|
||||
Apply(ctx context.Context, content string, supportFiles []protocol.SupportFile) error
|
||||
EnsureRuntime(ctx context.Context, recreate bool) error
|
||||
CurrentChecksum() (string, error)
|
||||
}
|
||||
@@ -72,7 +72,7 @@ func (s *Service) sync(ctx context.Context, startup bool) error {
|
||||
if snapshot.CurrentVersion == config.Version && snapshot.CurrentChecksum == config.Checksum && !startup {
|
||||
return nil
|
||||
}
|
||||
if err = s.nginxManager.Apply(ctx, config.RenderedConfig); err != nil {
|
||||
if err = s.nginxManager.Apply(ctx, config.RenderedConfig, config.SupportFiles); err != nil {
|
||||
snapshot.LastError = err.Error()
|
||||
_ = s.stateStore.Save(snapshot)
|
||||
reportErr := s.client.ReportApplyLog(ctx, protocol.ApplyLogPayload{
|
||||
|
||||
@@ -28,6 +28,7 @@ type fakeManager struct {
|
||||
currentChecksumErr error
|
||||
ensureCalls []bool
|
||||
applyContents []string
|
||||
applyFiles [][]protocol.SupportFile
|
||||
}
|
||||
|
||||
func (f *fakeExecutor) Test(ctx context.Context) error {
|
||||
@@ -51,8 +52,9 @@ func (f *fakeClient) ReportApplyLog(ctx context.Context, payload protocol.ApplyL
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *fakeManager) Apply(ctx context.Context, content string) error {
|
||||
func (m *fakeManager) Apply(ctx context.Context, content string, supportFiles []protocol.SupportFile) error {
|
||||
m.applyContents = append(m.applyContents, content)
|
||||
m.applyFiles = append(m.applyFiles, append([]protocol.SupportFile(nil), supportFiles...))
|
||||
return m.applyErr
|
||||
}
|
||||
|
||||
@@ -71,6 +73,7 @@ func TestSyncOnceSuccess(t *testing.T) {
|
||||
Version: "20260309-001",
|
||||
Checksum: "checksum-1",
|
||||
RenderedConfig: "server { listen 80; }",
|
||||
SupportFiles: []protocol.SupportFile{{Path: "1.crt", Content: "cert"}},
|
||||
CreatedAt: time.Now().Format(time.RFC3339),
|
||||
},
|
||||
}
|
||||
@@ -121,6 +124,7 @@ func TestSyncOnceRollbackOnNginxFailure(t *testing.T) {
|
||||
Version: "20260309-002",
|
||||
Checksum: "checksum-2",
|
||||
RenderedConfig: "server { listen 81; }",
|
||||
SupportFiles: []protocol.SupportFile{{Path: "1.crt", Content: "cert"}},
|
||||
CreatedAt: time.Now().Format(time.RFC3339),
|
||||
},
|
||||
}
|
||||
@@ -181,6 +185,7 @@ func TestSyncOnStartupRecreatesRuntimeWhenChecksumMatches(t *testing.T) {
|
||||
Version: "20260309-003",
|
||||
Checksum: "checksum-3",
|
||||
RenderedConfig: "server { listen 82; }",
|
||||
SupportFiles: []protocol.SupportFile{{Path: "1.crt", Content: "cert"}},
|
||||
CreatedAt: time.Now().Format(time.RFC3339),
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user