mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-09 09:06:36 +08:00
[新增] 实现安全兜底配置功能,允许在无历史配置时启动 OpenResty 并返回 503 状态
This commit is contained in:
@@ -155,6 +155,25 @@ const (
|
||||
ApplyStatusFatal ApplyStatus = "fatal"
|
||||
)
|
||||
|
||||
const safeDefaultFallbackMainConfig = `# This file is generated by OpenFlare safe default fallback.
|
||||
worker_processes auto;
|
||||
pid logs/nginx.pid;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
default_type text/plain;
|
||||
|
||||
server {
|
||||
listen 80 default_server;
|
||||
server_name _;
|
||||
return 503 "OpenFlare: No Valid Configuration\n";
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
type ApplyOutcome struct {
|
||||
Status ApplyStatus
|
||||
Message string
|
||||
@@ -219,7 +238,18 @@ func (m *Manager) rollbackAfterFailedApply(ctx context.Context, backup *backupSt
|
||||
return fatalApplyOutcome(fmt.Errorf("restore openresty backup failed after apply error %v: %w", applyErr, err))
|
||||
}
|
||||
if err := m.activateConfig(ctx); err != nil {
|
||||
return fatalApplyOutcome(fmt.Errorf("apply failed: %v; rollback recovery failed: %w", applyErr, err))
|
||||
if backup != nil && backup.MainExisted {
|
||||
return fatalApplyOutcome(fmt.Errorf("apply failed: %v; rollback recovery failed: %w", applyErr, err))
|
||||
}
|
||||
if fallbackErr := m.EnsureSafeFallbackRuntime(ctx, fmt.Sprintf("apply failed: %v; rollback recovery failed: %v", applyErr, err)); fallbackErr != nil {
|
||||
return fatalApplyOutcome(fmt.Errorf("apply failed: %v; rollback recovery failed: %w; fallback recovery failed: %v", applyErr, err, fallbackErr))
|
||||
}
|
||||
message := fmt.Sprintf("apply failed, but fallback runtime started: %v; rollback recovery failed: %v", applyErr, err)
|
||||
slog.Warn("openresty apply recovered with safe default fallback", "message", message)
|
||||
return ApplyOutcome{
|
||||
Status: ApplyStatusWarning,
|
||||
Message: message,
|
||||
}
|
||||
}
|
||||
message := fmt.Sprintf("apply failed, rolled back to previous config: %v", applyErr)
|
||||
slog.Warn("openresty apply rolled back successfully", "message", message)
|
||||
@@ -276,6 +306,25 @@ func (m *Manager) EnsureRuntime(ctx context.Context, recreate bool) error {
|
||||
return m.Executor.EnsureRuntime(ctx, recreate)
|
||||
}
|
||||
|
||||
func (m *Manager) EnsureSafeFallbackRuntime(ctx context.Context, reason string) error {
|
||||
if m.Executor == nil {
|
||||
return errors.New("executor 未配置")
|
||||
}
|
||||
trimmedReason := strings.TrimSpace(reason)
|
||||
if trimmedReason == "" {
|
||||
trimmedReason = "no valid local openresty config is available"
|
||||
}
|
||||
slog.Warn("starting openresty safe default fallback runtime", "reason", trimmedReason)
|
||||
if err := m.writeSafeDefaultFallbackFiles(); err != nil {
|
||||
return fmt.Errorf("write safe default fallback config failed: %w", err)
|
||||
}
|
||||
if err := m.activateConfig(ctx); err != nil {
|
||||
return fmt.Errorf("activate safe default fallback runtime failed: %w", err)
|
||||
}
|
||||
slog.Warn("openresty safe default fallback runtime started", "main_config", m.MainConfigPath, "route_config", m.RouteConfigPath)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) CheckHealth(ctx context.Context) error {
|
||||
if m.Executor == nil {
|
||||
return errors.New("executor 未配置")
|
||||
@@ -675,6 +724,28 @@ func (m *Manager) restorePowConfig(state *backupState) error {
|
||||
return os.WriteFile(configPath, []byte(state.PowConfig.Content), 0o644)
|
||||
}
|
||||
|
||||
func (m *Manager) writeSafeDefaultFallbackFiles() error {
|
||||
if strings.TrimSpace(m.MainConfigPath) == "" {
|
||||
return errors.New("main config path 不能为空")
|
||||
}
|
||||
if strings.TrimSpace(m.RouteConfigPath) == "" {
|
||||
return errors.New("route config path 不能为空")
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(m.MainConfigPath), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(m.RouteConfigPath), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.WriteFile(m.RouteConfigPath, nil, 0o644); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.WriteFile(m.MainConfigPath, []byte(safeDefaultFallbackMainConfig), 0o644); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func removeLegacyPowConfig(path string) error {
|
||||
if strings.TrimSpace(path) == "" {
|
||||
return nil
|
||||
|
||||
@@ -29,6 +29,8 @@ type fakeExecutor struct {
|
||||
}
|
||||
|
||||
type scriptedExecutor struct {
|
||||
testErrors []error
|
||||
testCalls int
|
||||
reloadErrors []error
|
||||
reloadCalls int
|
||||
}
|
||||
@@ -62,7 +64,12 @@ func (e *fakeExecutor) Restart(ctx context.Context) error {
|
||||
}
|
||||
|
||||
func (e *scriptedExecutor) Test(ctx context.Context) error {
|
||||
return nil
|
||||
index := e.testCalls
|
||||
e.testCalls++
|
||||
if index >= len(e.testErrors) {
|
||||
return nil
|
||||
}
|
||||
return e.testErrors[index]
|
||||
}
|
||||
|
||||
func (e *scriptedExecutor) Reload(ctx context.Context) error {
|
||||
@@ -733,6 +740,48 @@ func TestManagerApplyReturnsWarningWhenRollbackRecoversRuntime(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestManagerApplyStartsSafeFallbackWhenNoRollbackConfigExists(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
routePath := filepath.Join(tempDir, "routes.conf")
|
||||
mainPath := filepath.Join(tempDir, "nginx.conf")
|
||||
executor := &scriptedExecutor{
|
||||
testErrors: []error{errors.New("target config failed"), errors.New("rollback config missing"), nil},
|
||||
}
|
||||
manager := &Manager{
|
||||
MainConfigPath: mainPath,
|
||||
RouteConfigPath: routePath,
|
||||
Executor: executor,
|
||||
}
|
||||
|
||||
outcome := manager.Apply(context.Background(), "bad-main", "bad-route", nil)
|
||||
if outcome.Status != ApplyStatusWarning {
|
||||
t.Fatalf("expected warning apply outcome, got %#v", outcome)
|
||||
}
|
||||
if !strings.Contains(outcome.Message, "fallback runtime started") {
|
||||
t.Fatalf("expected fallback message, got %q", outcome.Message)
|
||||
}
|
||||
if executor.testCalls != 3 {
|
||||
t.Fatalf("expected target, rollback, and fallback tests, got %d", executor.testCalls)
|
||||
}
|
||||
mainData, err := os.ReadFile(mainPath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read main config: %v", err)
|
||||
}
|
||||
if !strings.Contains(string(mainData), "OpenFlare: No Valid Configuration") {
|
||||
t.Fatalf("expected safe fallback main config, got %s", string(mainData))
|
||||
}
|
||||
if !strings.Contains(string(mainData), "listen 80 default_server") {
|
||||
t.Fatalf("expected fallback to listen on port 80, got %s", string(mainData))
|
||||
}
|
||||
routeData, err := os.ReadFile(routePath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read route config: %v", err)
|
||||
}
|
||||
if len(routeData) != 0 {
|
||||
t.Fatalf("expected fallback route config to be empty, got %q", string(routeData))
|
||||
}
|
||||
}
|
||||
|
||||
func TestManagerCertFileTargetPathRejectsEscapes(t *testing.T) {
|
||||
manager := &Manager{CertDir: filepath.Join(t.TempDir(), "certs")}
|
||||
if err := os.MkdirAll(manager.CertDir, 0o755); err != nil {
|
||||
|
||||
Reference in New Issue
Block a user