feat: complete daemon operations and recovery tooling
This commit is contained in:
@@ -0,0 +1,203 @@
|
||||
package backendstatus
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"yms-daemon/internal/containerengine"
|
||||
"yms-daemon/internal/daemonapi"
|
||||
"yms-daemon/internal/deploymentconfig"
|
||||
"yms-daemon/internal/transaction"
|
||||
)
|
||||
|
||||
// 诊断项稳定标识,供测试与 reconcile 识别,不参与展示。
|
||||
const (
|
||||
codeUndeployed = "container_backend_undeployed"
|
||||
codeUndeployedWithHistory = "container_backend_undeployed_with_history"
|
||||
codeContainerWithoutRecord = "container_present_without_deployment_record"
|
||||
codeCommittedPortOK = "nginx_active_port"
|
||||
codeNginxWrongPort = "nginx_wrong_port"
|
||||
codeActiveContainerOK = "active_container_healthy"
|
||||
codeActiveContainerMissing = "active_container_missing"
|
||||
codeActiveContainerIdentity = "active_container_identity_mismatch"
|
||||
codeActiveContainerNotRunning = "active_container_not_running"
|
||||
codeInactiveContainerOK = "inactive_container_absent"
|
||||
codeInactiveContainerResidual = "inactive_container_residual"
|
||||
codeInactiveContainerRunning = "inactive_container_running"
|
||||
codeDeploymentConfigMismatch = "deployment_record_config_mismatch"
|
||||
)
|
||||
|
||||
// diagnoseContainer 对 container 后端做只读诊断。
|
||||
// 事实来源是 backend_container_deployment 单例记录;现场状态由容器引擎与宿主 Nginx 提供。
|
||||
func (d *Diagnoser) diagnoseContainer(ctx context.Context) (daemonapi.Diagnosis, error) {
|
||||
findings, err := d.containerGather(ctx)
|
||||
if err != nil {
|
||||
return daemonapi.Diagnosis{}, err
|
||||
}
|
||||
return toDiagnosis("backend", deploymentconfig.BackendTypeContainer, findings), nil
|
||||
}
|
||||
|
||||
// containerGather 读取 container 后端的部署记录、历史事务与宿主 Nginx 状态,生成全部诊断项。
|
||||
func (d *Diagnoser) containerGather(ctx context.Context) ([]finding, error) {
|
||||
deployment, depErr := d.store.BackendContainerDeployment(ctx)
|
||||
hasDeployment := depErr == nil
|
||||
if depErr != nil && !errors.Is(depErr, transaction.ErrNotFound) {
|
||||
return nil, fmt.Errorf("read backend container deployment: %w", depErr)
|
||||
}
|
||||
hasHistory, err := d.store.HasCommittedBackendContainerTransactionHistory(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
before, err := d.gateway.Read()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read host Nginx configuration: %w", err)
|
||||
}
|
||||
return d.containerFindings(ctx, deployment, hasDeployment, hasHistory, before.ActivePort)
|
||||
}
|
||||
|
||||
// containerFindings 生成 container 后端的全部诊断项。
|
||||
// deployment 部署记录,hasDeployment 表示是否存在该记录,hasHistory 表示是否存在历史
|
||||
// 已提交事务,activePort 是宿主 Nginx 当前指向的后端端口。
|
||||
func (d *Diagnoser) containerFindings(ctx context.Context, deployment transaction.BackendContainerDeployment, hasDeployment bool, hasHistory bool, activePort int) ([]finding, error) {
|
||||
if !hasDeployment {
|
||||
return d.containerUndeployedFindings(ctx, hasHistory)
|
||||
}
|
||||
|
||||
findings := make([]finding, 0, 6)
|
||||
|
||||
committedSlot, err := d.config.Backend.SlotForPort(deployment.ActivePort)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if deployment.ContainerName != committedSlot.ContainerName {
|
||||
findings = append(findings, finding{
|
||||
level: daemonapi.DiagnosisLevelDrift,
|
||||
code: codeDeploymentConfigMismatch,
|
||||
message: fmt.Sprintf("部署记录容器 %s 与活动端口 %d 的槽位容器 %s 不一致", deployment.ContainerName, deployment.ActivePort, committedSlot.ContainerName),
|
||||
})
|
||||
return findings, nil
|
||||
}
|
||||
|
||||
committedContainer, committedErr := d.engine.InspectContainer(ctx, deployment.ContainerName)
|
||||
committedHealthy := false
|
||||
switch {
|
||||
case errors.Is(committedErr, containerengine.ErrNotFound):
|
||||
findings = append(findings, finding{
|
||||
level: daemonapi.DiagnosisLevelDrift,
|
||||
code: codeActiveContainerMissing,
|
||||
message: "部署记录的活动容器 " + deployment.ContainerName + " 不存在",
|
||||
})
|
||||
case committedErr != nil:
|
||||
return nil, fmt.Errorf("inspect committed backend container %s: %w", deployment.ContainerName, committedErr)
|
||||
case committedContainer.ID != deployment.ContainerID:
|
||||
findings = append(findings, finding{
|
||||
level: daemonapi.DiagnosisLevelDrift,
|
||||
code: codeActiveContainerIdentity,
|
||||
message: "活动容器 " + deployment.ContainerName + " 身份与部署记录不一致",
|
||||
})
|
||||
case !committedContainer.Running || committedContainer.Dead:
|
||||
findings = append(findings, finding{
|
||||
level: daemonapi.DiagnosisLevelDrift,
|
||||
code: codeActiveContainerNotRunning,
|
||||
message: "活动容器 " + deployment.ContainerName + " 未运行",
|
||||
})
|
||||
default:
|
||||
committedHealthy = true
|
||||
findings = append(findings, finding{
|
||||
level: daemonapi.DiagnosisLevelOK,
|
||||
code: codeActiveContainerOK,
|
||||
message: "活动容器 " + deployment.ContainerName + " 健康",
|
||||
})
|
||||
}
|
||||
|
||||
if activePort == deployment.ActivePort {
|
||||
findings = append(findings, finding{
|
||||
level: daemonapi.DiagnosisLevelOK,
|
||||
code: codeCommittedPortOK,
|
||||
message: fmt.Sprintf("宿主 Nginx 指向部署记录的活动端口 %d", deployment.ActivePort),
|
||||
})
|
||||
} else if committedHealthy {
|
||||
findings = append(findings, finding{
|
||||
level: daemonapi.DiagnosisLevelFixable,
|
||||
code: codeNginxWrongPort,
|
||||
message: fmt.Sprintf("宿主 Nginx 指向 %d,但部署记录的活动端口是 %d", activePort, deployment.ActivePort),
|
||||
action: fmt.Sprintf("将宿主 Nginx 切流到端口 %d", deployment.ActivePort),
|
||||
fix: &fixAction{kind: fixSwitchPort, port: deployment.ActivePort},
|
||||
})
|
||||
} else {
|
||||
findings = append(findings, finding{
|
||||
level: daemonapi.DiagnosisLevelDrift,
|
||||
code: codeNginxWrongPort,
|
||||
message: fmt.Sprintf("宿主 Nginx 指向 %d,与部署记录活动端口 %d 不一致,且活动容器不健康", activePort, deployment.ActivePort),
|
||||
})
|
||||
}
|
||||
|
||||
inactiveSlot, err := d.config.Backend.SlotForPort(otherPort(deployment.ActivePort))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
inactiveContainer, inactiveErr := d.engine.InspectContainer(ctx, inactiveSlot.ContainerName)
|
||||
switch {
|
||||
case errors.Is(inactiveErr, containerengine.ErrNotFound):
|
||||
findings = append(findings, finding{
|
||||
level: daemonapi.DiagnosisLevelOK,
|
||||
code: codeInactiveContainerOK,
|
||||
message: "非活动槽位容器 " + inactiveSlot.ContainerName + " 不存在",
|
||||
})
|
||||
case inactiveErr != nil:
|
||||
return nil, fmt.Errorf("inspect inactive backend container %s: %w", inactiveSlot.ContainerName, inactiveErr)
|
||||
case inactiveContainer.Running && !inactiveContainer.Dead:
|
||||
findings = append(findings, finding{
|
||||
level: daemonapi.DiagnosisLevelDrift,
|
||||
code: codeInactiveContainerRunning,
|
||||
message: "非活动槽位容器 " + inactiveSlot.ContainerName + " 意外处于运行状态",
|
||||
})
|
||||
default:
|
||||
findings = append(findings, finding{
|
||||
level: daemonapi.DiagnosisLevelFixable,
|
||||
code: codeInactiveContainerResidual,
|
||||
message: "非活动槽位容器 " + inactiveSlot.ContainerName + " 已停止,属于残留",
|
||||
action: "移除残留的已停止容器 " + inactiveSlot.ContainerName,
|
||||
fix: &fixAction{kind: fixRemoveContainer, container: inactiveSlot.ContainerName},
|
||||
})
|
||||
}
|
||||
|
||||
return findings, nil
|
||||
}
|
||||
|
||||
// containerUndeployedFindings 处理不存在部署记录的情况,依据容器现场与历史记录判断是否为全新机器。
|
||||
func (d *Diagnoser) containerUndeployedFindings(ctx context.Context, hasHistory bool) ([]finding, error) {
|
||||
var present []string
|
||||
for _, port := range []int{deploymentconfig.BackendPort8080, deploymentconfig.BackendPort8081} {
|
||||
slot, err := d.config.Backend.SlotForPort(port)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := d.engine.InspectContainer(ctx, slot.ContainerName); err == nil {
|
||||
present = append(present, slot.ContainerName)
|
||||
} else if !errors.Is(err, containerengine.ErrNotFound) {
|
||||
return nil, fmt.Errorf("inspect backend container %s: %w", slot.ContainerName, err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(present) > 0 {
|
||||
return []finding{{
|
||||
level: daemonapi.DiagnosisLevelDrift,
|
||||
code: codeContainerWithoutRecord,
|
||||
message: fmt.Sprintf("SQLite 没有部署记录,但现场存在容器 %v", present),
|
||||
}}, nil
|
||||
}
|
||||
if hasHistory {
|
||||
return []finding{{
|
||||
level: daemonapi.DiagnosisLevelDrift,
|
||||
code: codeUndeployedWithHistory,
|
||||
message: "存在历史已提交事务,但缺少部署记录且两个槽位容器均不存在",
|
||||
}}, nil
|
||||
}
|
||||
return []finding{{
|
||||
level: daemonapi.DiagnosisLevelOK,
|
||||
code: codeUndeployed,
|
||||
message: "全新机器,尚未部署 backend 容器",
|
||||
}}, nil
|
||||
}
|
||||
@@ -0,0 +1,291 @@
|
||||
package backendstatus
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"io"
|
||||
"log/slog"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"yms-daemon/internal/containerengine"
|
||||
"yms-daemon/internal/daemonapi"
|
||||
"yms-daemon/internal/deploymentconfig"
|
||||
"yms-daemon/internal/hostnginx"
|
||||
"yms-daemon/internal/transaction"
|
||||
)
|
||||
|
||||
// TestContainerDiagnoseUndeployed 验证全新机器(无部署记录、无容器、无历史事务)被诊断为健康且未部署。
|
||||
func TestContainerDiagnoseUndeployed(t *testing.T) {
|
||||
d, _ := newContainerDiagnoser(t, &fakeEngine{}, &fakeGateway{port: 8080})
|
||||
diagnosis, err := d.Diagnose(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("diagnose undeployed container backend: %v", err)
|
||||
}
|
||||
if !diagnosis.Healthy {
|
||||
t.Fatalf("undeployed machine should be healthy: %+v", diagnosis)
|
||||
}
|
||||
if diagnosis.Items[0].Level != daemonapi.DiagnosisLevelOK || diagnosis.Items[0].Code != codeUndeployed {
|
||||
t.Fatalf("unexpected undeployed finding: %+v", diagnosis.Items)
|
||||
}
|
||||
}
|
||||
|
||||
// TestContainerDiagnoseHealthy 验证部署记录、活动容器与 Nginx 三者一致时被诊断为健康。
|
||||
func TestContainerDiagnoseHealthy(t *testing.T) {
|
||||
engine := &fakeEngine{containers: map[string]containerengine.Container{
|
||||
"backend-8080": {ID: "container-8080", Name: "backend-8080", Running: true},
|
||||
}}
|
||||
d, store := newContainerDiagnoser(t, engine, &fakeGateway{port: 8080})
|
||||
commitDeployment(t, store, transaction.BackendContainerDeployment{
|
||||
ActivePort: 8080, ContainerName: "backend-8080", ImageDigest: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", ContainerID: "container-8080",
|
||||
})
|
||||
|
||||
diagnosis, err := d.Diagnose(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("diagnose healthy container backend: %v", err)
|
||||
}
|
||||
if !diagnosis.Healthy {
|
||||
t.Fatalf("healthy container backend reported drift: %+v", diagnosis)
|
||||
}
|
||||
for _, item := range diagnosis.Items {
|
||||
if item.Level == daemonapi.DiagnosisLevelDrift || item.Level == daemonapi.DiagnosisLevelFixable {
|
||||
t.Fatalf("unexpected non-ok finding: %+v", item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestContainerDiagnoseNginxWrongPort 验证 Nginx 指向错误端口而活动容器健康时判定为可自动修复。
|
||||
func TestContainerDiagnoseNginxWrongPort(t *testing.T) {
|
||||
engine := &fakeEngine{containers: map[string]containerengine.Container{
|
||||
"backend-8080": {ID: "container-8080", Name: "backend-8080", Running: true},
|
||||
}}
|
||||
d, store := newContainerDiagnoser(t, engine, &fakeGateway{port: 8081})
|
||||
commitDeployment(t, store, transaction.BackendContainerDeployment{
|
||||
ActivePort: 8080, ContainerName: "backend-8080", ImageDigest: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", ContainerID: "container-8080",
|
||||
})
|
||||
|
||||
diagnosis, err := d.Diagnose(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("diagnose nginx wrong port: %v", err)
|
||||
}
|
||||
if !diagnosis.Healthy {
|
||||
t.Fatalf("nginx wrong port with healthy container should be fixable: %+v", diagnosis)
|
||||
}
|
||||
if !hasFinding(diagnosis, daemonapi.DiagnosisLevelFixable, codeNginxWrongPort) {
|
||||
t.Fatalf("missing nginx wrong port finding: %+v", diagnosis.Items)
|
||||
}
|
||||
}
|
||||
|
||||
// TestContainerDiagnoseActiveContainerMissing 验证部署记录的活动容器缺失时判定为漂移。
|
||||
func TestContainerDiagnoseActiveContainerMissing(t *testing.T) {
|
||||
d, store := newContainerDiagnoser(t, &fakeEngine{}, &fakeGateway{port: 8080})
|
||||
commitDeployment(t, store, transaction.BackendContainerDeployment{
|
||||
ActivePort: 8080, ContainerName: "backend-8080", ImageDigest: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", ContainerID: "container-8080",
|
||||
})
|
||||
|
||||
diagnosis, err := d.Diagnose(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("diagnose missing active container: %v", err)
|
||||
}
|
||||
if diagnosis.Healthy {
|
||||
t.Fatalf("missing active container should be drift: %+v", diagnosis)
|
||||
}
|
||||
if !hasFinding(diagnosis, daemonapi.DiagnosisLevelDrift, codeActiveContainerMissing) {
|
||||
t.Fatalf("missing active container finding: %+v", diagnosis.Items)
|
||||
}
|
||||
}
|
||||
|
||||
// TestContainerDiagnoseInactiveResidual 验证非活动槽位残留已停止容器时判定为可自动修复。
|
||||
func TestContainerDiagnoseInactiveResidual(t *testing.T) {
|
||||
engine := &fakeEngine{containers: map[string]containerengine.Container{
|
||||
"backend-8080": {ID: "container-8080", Name: "backend-8080", Running: true},
|
||||
"backend-8081": {ID: "container-8081", Name: "backend-8081", Running: false},
|
||||
}}
|
||||
d, store := newContainerDiagnoser(t, engine, &fakeGateway{port: 8080})
|
||||
commitDeployment(t, store, transaction.BackendContainerDeployment{
|
||||
ActivePort: 8080, ContainerName: "backend-8080", ImageDigest: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", ContainerID: "container-8080",
|
||||
})
|
||||
|
||||
diagnosis, err := d.Diagnose(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("diagnose inactive residual: %v", err)
|
||||
}
|
||||
if !hasFinding(diagnosis, daemonapi.DiagnosisLevelFixable, codeInactiveContainerResidual) {
|
||||
t.Fatalf("missing inactive residual finding: %+v", diagnosis.Items)
|
||||
}
|
||||
}
|
||||
|
||||
// TestContainerDiagnoseContainerWithoutRecord 验证无部署记录但现场存在容器时判定为漂移。
|
||||
func TestContainerDiagnoseContainerWithoutRecord(t *testing.T) {
|
||||
engine := &fakeEngine{containers: map[string]containerengine.Container{
|
||||
"backend-8080": {ID: "container-8080", Name: "backend-8080", Running: true},
|
||||
}}
|
||||
d, _ := newContainerDiagnoser(t, engine, &fakeGateway{port: 8080})
|
||||
|
||||
diagnosis, err := d.Diagnose(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("diagnose container without record: %v", err)
|
||||
}
|
||||
if diagnosis.Healthy {
|
||||
t.Fatalf("container without record should be drift: %+v", diagnosis)
|
||||
}
|
||||
if !hasFinding(diagnosis, daemonapi.DiagnosisLevelDrift, codeContainerWithoutRecord) {
|
||||
t.Fatalf("missing container-without-record finding: %+v", diagnosis.Items)
|
||||
}
|
||||
}
|
||||
|
||||
// TestContainerReconcileApplyFixesNginx 验证 reconcile --apply 能把指向错误的 Nginx 切回记录端口。
|
||||
func TestContainerReconcileApplyFixesNginx(t *testing.T) {
|
||||
engine := &fakeEngine{containers: map[string]containerengine.Container{
|
||||
"backend-8080": {ID: "container-8080", Name: "backend-8080", Running: true},
|
||||
}}
|
||||
gateway := &fakeGateway{port: 8081}
|
||||
d, store := newContainerDiagnoser(t, engine, gateway)
|
||||
commitDeployment(t, store, transaction.BackendContainerDeployment{
|
||||
ActivePort: 8080, ContainerName: "backend-8080", ImageDigest: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", ContainerID: "container-8080",
|
||||
})
|
||||
|
||||
diagnosis, err := d.Reconcile(context.Background(), true)
|
||||
if err != nil {
|
||||
t.Fatalf("reconcile apply nginx: %v", err)
|
||||
}
|
||||
if gateway.port != 8080 {
|
||||
t.Fatalf("reconcile did not switch Nginx to port 8080: port=%d", gateway.port)
|
||||
}
|
||||
if !diagnosis.Healthy {
|
||||
t.Fatalf("reconcile result should be healthy: %+v", diagnosis)
|
||||
}
|
||||
if hasFinding(diagnosis, daemonapi.DiagnosisLevelFixable, codeNginxWrongPort) {
|
||||
t.Fatalf("nginx wrong port should be fixed after reconcile: %+v", diagnosis.Items)
|
||||
}
|
||||
}
|
||||
|
||||
// hasFinding 判断诊断结果中是否存在指定级别与稳定标识的诊断项。
|
||||
func hasFinding(diagnosis daemonapi.Diagnosis, level string, code string) bool {
|
||||
for _, item := range diagnosis.Items {
|
||||
if item.Level == level && item.Code == code {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// newContainerDiagnoser 构造 container 后端的诊断器及其底层存储,返回诊断器与可写的存储。
|
||||
func newContainerDiagnoser(t *testing.T, engine *fakeEngine, gateway *fakeGateway) (*Diagnoser, *transaction.Store) {
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
store, err := transaction.OpenStore(ctx, filepath.Join(t.TempDir(), "transactions.db"))
|
||||
if err != nil {
|
||||
t.Fatalf("open transaction store: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = store.Close() })
|
||||
coordinator, err := transaction.NewCoordinator(store, slog.New(slog.NewTextHandler(io.Discard, nil)))
|
||||
if err != nil {
|
||||
t.Fatalf("create transaction coordinator: %v", err)
|
||||
}
|
||||
config := deploymentconfig.Config{
|
||||
Daemon: deploymentconfig.Daemon{Environment: deploymentconfig.EnvironmentDev},
|
||||
Backend: deploymentconfig.Backend{
|
||||
Type: deploymentconfig.BackendTypeContainer,
|
||||
SystemctlPath: "/bin/systemctl",
|
||||
Slot: deploymentconfig.BackendSlots{
|
||||
Port8080: deploymentconfig.BackendSlot{ContainerName: "backend-8080", HealthEndpoint: "http://127.0.0.1:8080/yms/actuator/health"},
|
||||
Port8081: deploymentconfig.BackendSlot{ContainerName: "backend-8081", HealthEndpoint: "http://127.0.0.1:8081/yms/actuator/health"},
|
||||
},
|
||||
},
|
||||
}
|
||||
diagnoser, err := New(config, store, coordinator, engine, nil, gateway)
|
||||
if err != nil {
|
||||
t.Fatalf("create diagnoser: %v", err)
|
||||
}
|
||||
return diagnoser, store
|
||||
}
|
||||
|
||||
// commitDeployment 通过合法的状态机推进写入一条 container 部署记录。
|
||||
func commitDeployment(t *testing.T, store *transaction.Store, deployment transaction.BackendContainerDeployment) {
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
transactionID := "deploy-" + rand.Text()
|
||||
if _, _, err := store.CreateTransaction(ctx, transaction.CreateRequest{
|
||||
ID: transactionID,
|
||||
IdempotencyKey: "backend:container:test:" + rand.Text(),
|
||||
Source: "test",
|
||||
Service: "backend",
|
||||
}); err != nil {
|
||||
t.Fatalf("create deployment transaction: %v", err)
|
||||
}
|
||||
for _, state := range []transaction.State{
|
||||
transaction.StateValidating,
|
||||
transaction.StatePrepared,
|
||||
transaction.StateStarting,
|
||||
transaction.StateSwitching,
|
||||
transaction.StateVerifying,
|
||||
transaction.StateDraining,
|
||||
} {
|
||||
if _, err := store.Transition(ctx, transactionID, state, "test"); err != nil {
|
||||
t.Fatalf("advance deployment transaction: %v", err)
|
||||
}
|
||||
}
|
||||
if _, err := store.CommitBackendContainerDeployment(ctx, transactionID, deployment, "test committed"); err != nil {
|
||||
t.Fatalf("commit deployment: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// fakeGateway gateway 接口的内存实现,记录活动端口并在 Switch 时直接更新。
|
||||
type fakeGateway struct {
|
||||
port int
|
||||
content []byte
|
||||
}
|
||||
|
||||
// Read 返回当前快照与活动端口。
|
||||
func (g *fakeGateway) Read() (hostnginx.Snapshot, error) {
|
||||
content := g.content
|
||||
if content == nil {
|
||||
content = []byte("http {\n upstream yms-server {\n # yms-update managed upstream begin\n server 127.0.0.1:8080 max_fails=1 fail_timeout=2s;\n server 127.0.0.1:8081 max_fails=1 fail_timeout=2s;\n # yms-update managed upstream end\n }\n}\n")
|
||||
}
|
||||
return hostnginx.Snapshot{Content: append([]byte(nil), content...), ActivePort: g.port}, nil
|
||||
}
|
||||
|
||||
// Switch 直接更新活动端口并返回切换前快照。
|
||||
func (g *fakeGateway) Switch(_ context.Context, port int) (hostnginx.Snapshot, error) {
|
||||
previous, _ := g.Read()
|
||||
g.port = port
|
||||
return previous, nil
|
||||
}
|
||||
|
||||
// fakeEngine containerengine.Engine 的内存实现,维护容器表。
|
||||
type fakeEngine struct {
|
||||
containers map[string]containerengine.Container
|
||||
removed []string
|
||||
}
|
||||
|
||||
func (e *fakeEngine) Ping(context.Context) error { return nil }
|
||||
func (e *fakeEngine) PullImage(context.Context, string) error { return nil }
|
||||
func (e *fakeEngine) LoadImage(context.Context, io.Reader) error { return nil }
|
||||
func (e *fakeEngine) InspectImage(context.Context, string) (containerengine.Image, error) {
|
||||
return containerengine.Image{}, nil
|
||||
}
|
||||
func (e *fakeEngine) CreateContainer(context.Context, containerengine.ContainerSpec) (containerengine.Container, error) {
|
||||
return containerengine.Container{}, nil
|
||||
}
|
||||
func (e *fakeEngine) StartContainer(context.Context, string) error { return nil }
|
||||
func (e *fakeEngine) ContainerLogs(context.Context, string) (io.ReadCloser, error) {
|
||||
return io.NopCloser(strings.NewReader("")), nil
|
||||
}
|
||||
func (e *fakeEngine) StopContainer(context.Context, string, int) error { return nil }
|
||||
func (e *fakeEngine) InspectContainer(_ context.Context, name string) (containerengine.Container, error) {
|
||||
record, found := e.containers[name]
|
||||
if !found {
|
||||
return containerengine.Container{}, containerengine.ErrNotFound
|
||||
}
|
||||
return record, nil
|
||||
}
|
||||
func (e *fakeEngine) RemoveContainer(_ context.Context, name string, _ bool) error {
|
||||
delete(e.containers, name)
|
||||
e.removed = append(e.removed, name)
|
||||
return nil
|
||||
}
|
||||
func (e *fakeEngine) Close() error { return nil }
|
||||
|
||||
var _ containerengine.Engine = (*fakeEngine)(nil)
|
||||
@@ -0,0 +1,157 @@
|
||||
// Package backendstatus 对 backend 组件的现场状态与 SQLite 事实来源做只读诊断,并按需执行自动修复。
|
||||
//
|
||||
// 诊断遵循“SQLite 是事实来源、外部系统是待校验状态”的原则:status 与 doctor 只读,
|
||||
// 不修改任何现场;reconcile 默认只生成修复计划,仅在显式要求 apply 时执行经过明确授权的
|
||||
// 自动修复动作,且每个修复动作都先写入事务再执行外部变更。
|
||||
package backendstatus
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"yms-daemon/internal/containerengine"
|
||||
"yms-daemon/internal/daemonapi"
|
||||
"yms-daemon/internal/deploymentconfig"
|
||||
"yms-daemon/internal/hostnginx"
|
||||
"yms-daemon/internal/systemd"
|
||||
"yms-daemon/internal/transaction"
|
||||
)
|
||||
|
||||
// gateway 抽象宿主 Nginx 配置的读取与切流,便于在测试中替换。
|
||||
type gateway interface {
|
||||
// Read 返回当前完整配置快照及其活动后端端口。
|
||||
Read() (hostnginx.Snapshot, error)
|
||||
// Switch 把活动后端端口切到指定端口并重载 Nginx。
|
||||
Switch(context.Context, int) (hostnginx.Snapshot, error)
|
||||
}
|
||||
|
||||
// Diagnoser 持有执行诊断与自动修复所需的全部依赖。
|
||||
// 除 store、coordinator、gateway 外,其余依赖按组件运行类型选择性使用:container 使用
|
||||
// engine,native 使用 units,未使用的依赖可以为 nil。
|
||||
type Diagnoser struct {
|
||||
// config 已校验的本地部署配置,决定组件运行类型与槽位信息。
|
||||
config deploymentconfig.Config
|
||||
// store 服务端 SQLite 事务存储,是诊断的事实来源。
|
||||
store *transaction.Store
|
||||
// coordinator 用于在 reconcile --apply 时以可恢复方式执行单个修复步骤。
|
||||
coordinator *transaction.Coordinator
|
||||
// engine 容器引擎,container 后端诊断时使用。
|
||||
engine containerengine.Engine
|
||||
// units systemd 管理器,native 后端诊断时使用。
|
||||
units systemd.Manager
|
||||
// gateway 宿主 Nginx 配置控制器,用于读取活动端口与执行切流修复。
|
||||
gateway gateway
|
||||
}
|
||||
|
||||
// New 构造一个 Diagnoser 并校验依赖。
|
||||
// config 必须已经通过 deploymentconfig.Validate;store、coordinator、gateway 不能为空;
|
||||
// 当 backend.type 为 container 时 engine 不能为空,为 native 时 units 不能为空。
|
||||
func New(config deploymentconfig.Config, store *transaction.Store, coordinator *transaction.Coordinator, engine containerengine.Engine, units systemd.Manager, gateway gateway) (*Diagnoser, error) {
|
||||
if err := config.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if store == nil || coordinator == nil || gateway == nil {
|
||||
return nil, errors.New("backend status dependencies are required")
|
||||
}
|
||||
switch config.Backend.Type {
|
||||
case deploymentconfig.BackendTypeContainer:
|
||||
if engine == nil {
|
||||
return nil, errors.New("container backend diagnosis requires a container engine")
|
||||
}
|
||||
case deploymentconfig.BackendTypeNative:
|
||||
if units == nil {
|
||||
return nil, errors.New("native backend diagnosis requires a systemd manager")
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported backend.type %q", config.Backend.Type)
|
||||
}
|
||||
return &Diagnoser{config: config, store: store, coordinator: coordinator, engine: engine, units: units, gateway: gateway}, nil
|
||||
}
|
||||
|
||||
// Diagnose 执行只读诊断并返回结构化结果,不修改任何现场状态。
|
||||
// 返回的 daemonapi.Diagnosis 中,Healthy 为 true 表示不存在需要人工处理的漂移项。
|
||||
func (d *Diagnoser) Diagnose(ctx context.Context) (daemonapi.Diagnosis, error) {
|
||||
switch d.config.Backend.Type {
|
||||
case deploymentconfig.BackendTypeContainer:
|
||||
return d.diagnoseContainer(ctx)
|
||||
case deploymentconfig.BackendTypeNative:
|
||||
return d.diagnoseNative(ctx)
|
||||
default:
|
||||
return daemonapi.Diagnosis{}, fmt.Errorf("unsupported backend.type %q", d.config.Backend.Type)
|
||||
}
|
||||
}
|
||||
|
||||
// Reconcile 生成修复计划(apply 为 false)或执行自动修复(apply 为 true)。
|
||||
// apply 为 false 时行为与 Diagnose 一致,仅返回只读诊断;apply 为 true 时先诊断,
|
||||
// 对每个可自动修复项执行修复动作,完成后重新诊断并返回修复后的结果。
|
||||
func (d *Diagnoser) Reconcile(ctx context.Context, apply bool) (daemonapi.Diagnosis, error) {
|
||||
if !apply {
|
||||
return d.Diagnose(ctx)
|
||||
}
|
||||
switch d.config.Backend.Type {
|
||||
case deploymentconfig.BackendTypeContainer:
|
||||
return d.reconcileContainer(ctx)
|
||||
case deploymentconfig.BackendTypeNative:
|
||||
return d.reconcileNative(ctx)
|
||||
default:
|
||||
return daemonapi.Diagnosis{}, fmt.Errorf("unsupported backend.type %q", d.config.Backend.Type)
|
||||
}
|
||||
}
|
||||
|
||||
// otherPort 返回给定端口的另一个蓝绿槽位端口。
|
||||
func otherPort(port int) int {
|
||||
if port == deploymentconfig.BackendPort8080 {
|
||||
return deploymentconfig.BackendPort8081
|
||||
}
|
||||
return deploymentconfig.BackendPort8080
|
||||
}
|
||||
|
||||
// finding 内部诊断结论,比协议层的 DiagnosisItem 多携带一个可选的修复参数。
|
||||
type finding struct {
|
||||
// level 诊断级别,取值 daemonapi.DiagnosisLevelOK / DiagnosisLevelFixable / DiagnosisLevelDrift。
|
||||
level string
|
||||
// code 稳定标识,供测试与 reconcile 识别。
|
||||
code string
|
||||
// message 人类可读的诊断描述。
|
||||
message string
|
||||
// action 仅在 level 为 fixable 时给出建议动作描述。
|
||||
action string
|
||||
// fix 描述可自动修复动作,仅 fixable 项非 nil。
|
||||
fix *fixAction
|
||||
}
|
||||
|
||||
// fixAction 描述一个可自动修复动作所需的精确参数。
|
||||
type fixAction struct {
|
||||
// kind 修复动作种类,取值 fixSwitchPort 或 fixRemoveContainer。
|
||||
kind string
|
||||
// port 切流修复的目标端口。
|
||||
port int
|
||||
// container 容器移除修复的目标容器名。
|
||||
container string
|
||||
}
|
||||
|
||||
const (
|
||||
// fixSwitchPort 表示把宿主 Nginx 切流到指定端口。
|
||||
fixSwitchPort = "nginx-switch"
|
||||
// fixRemoveContainer 表示移除残留的已停止容器。
|
||||
fixRemoveContainer = "container-remove"
|
||||
)
|
||||
|
||||
// toDiagnosis 把内部 finding 列表转换为协议层诊断结果,并据此计算 Healthy 标志。
|
||||
func toDiagnosis(service string, runtimeType string, findings []finding) daemonapi.Diagnosis {
|
||||
items := make([]daemonapi.DiagnosisItem, 0, len(findings))
|
||||
healthy := true
|
||||
for _, f := range findings {
|
||||
if f.level == daemonapi.DiagnosisLevelDrift {
|
||||
healthy = false
|
||||
}
|
||||
items = append(items, daemonapi.DiagnosisItem{
|
||||
Level: f.level,
|
||||
Code: f.code,
|
||||
Message: f.message,
|
||||
Action: f.action,
|
||||
})
|
||||
}
|
||||
return daemonapi.Diagnosis{Service: service, Type: runtimeType, Healthy: healthy, Items: items}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
package backendstatus
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"yms-daemon/internal/daemonapi"
|
||||
"yms-daemon/internal/deploymentconfig"
|
||||
"yms-daemon/internal/systemd"
|
||||
)
|
||||
|
||||
// 诊断项稳定标识,供测试与 reconcile 识别,不参与展示。
|
||||
const (
|
||||
codeNativeActivePortOK = "native_nginx_active_port"
|
||||
codeNativeActiveUnitOK = "native_active_unit_running"
|
||||
codeNativeActiveUnitDown = "native_active_unit_not_running"
|
||||
codeNativeInactiveUnitOK = "native_inactive_unit_stopped"
|
||||
codeNativeInactiveRunning = "native_inactive_unit_running"
|
||||
codeNativeSlotJAROK = "native_slot_jar_link"
|
||||
codeNativeSlotJARDrift = "native_slot_jar_link_drift"
|
||||
)
|
||||
|
||||
// diagnoseNative 对 native 后端做只读诊断。
|
||||
// native 没有部署单例记录,事实来源是宿主 Nginx 活动端口与 systemd 单元现场状态。
|
||||
func (d *Diagnoser) diagnoseNative(ctx context.Context) (daemonapi.Diagnosis, error) {
|
||||
findings, err := d.nativeGather(ctx)
|
||||
if err != nil {
|
||||
return daemonapi.Diagnosis{}, err
|
||||
}
|
||||
return toDiagnosis("backend", deploymentconfig.BackendTypeNative, findings), nil
|
||||
}
|
||||
|
||||
// nativeGather 生成 native 后端的全部诊断项。
|
||||
func (d *Diagnoser) nativeGather(ctx context.Context) ([]finding, error) {
|
||||
before, err := d.gateway.Read()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read host Nginx configuration: %w", err)
|
||||
}
|
||||
activePort := before.ActivePort
|
||||
findings := make([]finding, 0, 6)
|
||||
findings = append(findings, finding{
|
||||
level: daemonapi.DiagnosisLevelOK,
|
||||
code: codeNativeActivePortOK,
|
||||
message: fmt.Sprintf("宿主 Nginx 指向后端端口 %d", activePort),
|
||||
})
|
||||
|
||||
activeSlot, err := d.config.Backend.SlotForPort(activePort)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
inactiveSlot, err := d.config.Backend.SlotForPort(otherPort(activePort))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
activeUnit, err := d.units.Inspect(ctx, activeSlot.Unit)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("inspect native active unit %s: %w", activeSlot.Unit, err)
|
||||
}
|
||||
if unitRunning(activeUnit) {
|
||||
findings = append(findings, finding{
|
||||
level: daemonapi.DiagnosisLevelOK,
|
||||
code: codeNativeActiveUnitOK,
|
||||
message: "活动单元 " + activeSlot.Unit + " 正在运行",
|
||||
})
|
||||
} else {
|
||||
findings = append(findings, finding{
|
||||
level: daemonapi.DiagnosisLevelDrift,
|
||||
code: codeNativeActiveUnitDown,
|
||||
message: "活动单元 " + activeSlot.Unit + " 未运行(ActiveState=" + activeUnit.ActiveState + ")",
|
||||
})
|
||||
}
|
||||
|
||||
inactiveUnit, err := d.units.Inspect(ctx, inactiveSlot.Unit)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("inspect native inactive unit %s: %w", inactiveSlot.Unit, err)
|
||||
}
|
||||
if unitRunning(inactiveUnit) {
|
||||
findings = append(findings, finding{
|
||||
level: daemonapi.DiagnosisLevelDrift,
|
||||
code: codeNativeInactiveRunning,
|
||||
message: "非活动单元 " + inactiveSlot.Unit + " 意外运行,两个槽位同时在线",
|
||||
})
|
||||
} else {
|
||||
findings = append(findings, finding{
|
||||
level: daemonapi.DiagnosisLevelOK,
|
||||
code: codeNativeInactiveUnitOK,
|
||||
message: "非活动单元 " + inactiveSlot.Unit + " 已停止",
|
||||
})
|
||||
}
|
||||
|
||||
if ok, detail := jarLinkStatus(activeSlot.JAR); ok {
|
||||
findings = append(findings, finding{
|
||||
level: daemonapi.DiagnosisLevelOK,
|
||||
code: codeNativeSlotJAROK,
|
||||
message: "活动槽位 JAR 链接有效:" + detail,
|
||||
})
|
||||
} else {
|
||||
findings = append(findings, finding{
|
||||
level: daemonapi.DiagnosisLevelDrift,
|
||||
code: codeNativeSlotJARDrift,
|
||||
message: "活动槽位 JAR 链接异常:" + detail,
|
||||
})
|
||||
}
|
||||
|
||||
return findings, nil
|
||||
}
|
||||
|
||||
// unitRunning 判断 systemd 单元是否处于运行状态。
|
||||
func unitRunning(unit systemd.Unit) bool {
|
||||
return unit.ActiveState != "inactive" && unit.ActiveState != "failed"
|
||||
}
|
||||
|
||||
// jarLinkStatus 检查槽位 JAR 路径是否为指向现存文件的符号链接。
|
||||
// 返回 ok 与人类可读的详情;JAR 缺失、不是符号链接或目标不存在时返回 ok=false。
|
||||
func jarLinkStatus(path string) (bool, string) {
|
||||
info, err := os.Lstat(path)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return false, "路径不存在"
|
||||
}
|
||||
if err != nil {
|
||||
return false, "检查失败:" + err.Error()
|
||||
}
|
||||
if info.Mode()&os.ModeSymlink == 0 {
|
||||
return false, "不是符号链接"
|
||||
}
|
||||
target, err := os.Readlink(path)
|
||||
if err != nil {
|
||||
return false, "读取链接目标失败:" + err.Error()
|
||||
}
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
return false, "链接目标不存在:" + target
|
||||
}
|
||||
return true, "指向 " + target
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
package backendstatus
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"yms-daemon/internal/containerengine"
|
||||
"yms-daemon/internal/daemonapi"
|
||||
"yms-daemon/internal/deploymentconfig"
|
||||
"yms-daemon/internal/transaction"
|
||||
)
|
||||
|
||||
// reconcileSource 表示对账修复事务的来源标识。
|
||||
const reconcileSource = "local-cli"
|
||||
|
||||
// reconcileContainer 执行 container 后端的对账:只读时返回修复计划,apply 时执行自动修复。
|
||||
func (d *Diagnoser) reconcileContainer(ctx context.Context) (daemonapi.Diagnosis, error) {
|
||||
findings, err := d.containerGather(ctx)
|
||||
if err != nil {
|
||||
return daemonapi.Diagnosis{}, err
|
||||
}
|
||||
return d.applyFixes(ctx, deploymentconfig.BackendTypeContainer, findings)
|
||||
}
|
||||
|
||||
// reconcileNative 执行 native 后端的对账。当前 native 没有可自动修复项,仅返回只读诊断。
|
||||
func (d *Diagnoser) reconcileNative(ctx context.Context) (daemonapi.Diagnosis, error) {
|
||||
findings, err := d.nativeGather(ctx)
|
||||
if err != nil {
|
||||
return daemonapi.Diagnosis{}, err
|
||||
}
|
||||
return d.applyFixes(ctx, deploymentconfig.BackendTypeNative, findings)
|
||||
}
|
||||
|
||||
// applyFixes 对诊断结果中的可自动修复项执行修复动作。
|
||||
// 若没有可修复项则直接返回只读诊断;否则创建对账事务,通过 coordinator 对每个修复动作
|
||||
// 先记录意图、再执行、再核对,全部成功后把事务推进到 COMMITTED,最后重新诊断返回修复后状态。
|
||||
func (d *Diagnoser) applyFixes(ctx context.Context, runtimeType string, findings []finding) (daemonapi.Diagnosis, error) {
|
||||
var fixable []finding
|
||||
for _, f := range findings {
|
||||
if f.level == daemonapi.DiagnosisLevelFixable && f.fix != nil {
|
||||
fixable = append(fixable, f)
|
||||
}
|
||||
}
|
||||
if len(fixable) == 0 {
|
||||
return toDiagnosis("backend", runtimeType, findings), nil
|
||||
}
|
||||
|
||||
transactionID := rand.Text()
|
||||
codes := make([]string, 0, len(fixable))
|
||||
for _, f := range fixable {
|
||||
codes = append(codes, f.code)
|
||||
}
|
||||
requestJSON, err := json.Marshal(struct {
|
||||
Service string `json:"service"`
|
||||
Type string `json:"type"`
|
||||
Fixes []string `json:"fixes"`
|
||||
}{"backend", runtimeType, codes})
|
||||
if err != nil {
|
||||
return daemonapi.Diagnosis{}, fmt.Errorf("encode reconcile request: %w", err)
|
||||
}
|
||||
if _, _, err := d.store.CreateTransaction(ctx, transaction.CreateRequest{
|
||||
ID: transactionID,
|
||||
IdempotencyKey: "backend:reconcile:" + rand.Text(),
|
||||
Source: reconcileSource,
|
||||
Service: "backend",
|
||||
Request: requestJSON,
|
||||
}); err != nil {
|
||||
return daemonapi.Diagnosis{}, fmt.Errorf("create reconcile transaction: %w", err)
|
||||
}
|
||||
|
||||
for _, f := range fixable {
|
||||
intent, operation, err := d.fixOperation(f)
|
||||
if err != nil {
|
||||
_, _ = d.store.Transition(ctx, transactionID, transaction.StateFailed, err.Error())
|
||||
return daemonapi.Diagnosis{}, err
|
||||
}
|
||||
if _, err := d.coordinator.ExecuteStep(ctx, transactionID, intent, operation); err != nil {
|
||||
_, _ = d.store.Transition(ctx, transactionID, transaction.StateFailed, err.Error())
|
||||
return daemonapi.Diagnosis{}, fmt.Errorf("apply reconcile fix %s: %w", f.code, err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, next := range []transaction.State{
|
||||
transaction.StateValidating,
|
||||
transaction.StatePrepared,
|
||||
transaction.StateStarting,
|
||||
transaction.StateSwitching,
|
||||
transaction.StateVerifying,
|
||||
transaction.StateDraining,
|
||||
transaction.StateCommitted,
|
||||
} {
|
||||
if _, err := d.store.Transition(ctx, transactionID, next, "reconcile"); err != nil {
|
||||
return daemonapi.Diagnosis{}, fmt.Errorf("commit reconcile transaction: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
diagnosis, err := d.Diagnose(ctx)
|
||||
if err != nil {
|
||||
return daemonapi.Diagnosis{}, err
|
||||
}
|
||||
diagnosis.RepairApplied = true
|
||||
diagnosis.RepairTransactionID = transactionID
|
||||
return diagnosis, nil
|
||||
}
|
||||
|
||||
// fixOperation 把一条可修复诊断项转换为对账事务中可执行的步骤意图与操作。
|
||||
func (d *Diagnoser) fixOperation(f finding) (transaction.StepIntent, transaction.Operation, error) {
|
||||
switch f.fix.kind {
|
||||
case fixSwitchPort:
|
||||
intent := reconcileIntent("reconcile.nginx.switch."+strconv.Itoa(f.fix.port), "switch host Nginx to committed backend port", struct {
|
||||
Port int `json:"port"`
|
||||
}{f.fix.port})
|
||||
return intent, &switchNginxOperation{gateway: d.gateway, port: f.fix.port}, nil
|
||||
case fixRemoveContainer:
|
||||
intent := reconcileIntent("reconcile.container.remove."+f.fix.container, "remove residual stopped backend container", struct {
|
||||
ContainerName string `json:"containerName"`
|
||||
}{f.fix.container})
|
||||
return intent, &removeContainerOperation{engine: d.engine, name: f.fix.container}, nil
|
||||
default:
|
||||
return transaction.StepIntent{}, nil, fmt.Errorf("unsupported reconcile fix kind %q", f.fix.kind)
|
||||
}
|
||||
}
|
||||
|
||||
// reconcileIntent 构造对账修复步骤的持久化意图。
|
||||
func reconcileIntent(key, name string, value any) transaction.StepIntent {
|
||||
payload, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("marshal reconcile intent: %v", err))
|
||||
}
|
||||
return transaction.StepIntent{Key: key, Name: name, Intent: payload}
|
||||
}
|
||||
|
||||
// switchNginxOperation 把宿主 Nginx 切流到指定端口,实现 transaction.Operation。
|
||||
type switchNginxOperation struct {
|
||||
gateway gateway
|
||||
port int
|
||||
}
|
||||
|
||||
// Apply 执行宿主 Nginx 切流,切到当前已指向的端口时为空操作。
|
||||
func (o *switchNginxOperation) Apply(ctx context.Context) error {
|
||||
_, err := o.gateway.Switch(ctx, o.port)
|
||||
return err
|
||||
}
|
||||
|
||||
// Inspect 核对宿主 Nginx 当前活动端口是否等于目标端口。
|
||||
func (o *switchNginxOperation) Inspect(ctx context.Context) (transaction.Inspection, error) {
|
||||
snapshot, err := o.gateway.Read()
|
||||
if err != nil {
|
||||
return transaction.Inspection{}, err
|
||||
}
|
||||
if snapshot.ActivePort == o.port {
|
||||
return transaction.Inspection{Status: transaction.InspectionApplied}, nil
|
||||
}
|
||||
return transaction.Inspection{Status: transaction.InspectionNotApplied}, nil
|
||||
}
|
||||
|
||||
// removeContainerOperation 移除一个残留容器,实现 transaction.Operation。
|
||||
type removeContainerOperation struct {
|
||||
engine containerengine.Engine
|
||||
name string
|
||||
}
|
||||
|
||||
// Apply 强制移除目标容器,容器不存在时视为成功。
|
||||
func (o *removeContainerOperation) Apply(ctx context.Context) error {
|
||||
err := o.engine.RemoveContainer(ctx, o.name, true)
|
||||
if errors.Is(err, containerengine.ErrNotFound) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Inspect 核对目标容器是否已经不存在。
|
||||
func (o *removeContainerOperation) Inspect(ctx context.Context) (transaction.Inspection, error) {
|
||||
if _, err := o.engine.InspectContainer(ctx, o.name); errors.Is(err, containerengine.ErrNotFound) {
|
||||
return transaction.Inspection{Status: transaction.InspectionApplied}, nil
|
||||
} else if err != nil {
|
||||
return transaction.Inspection{}, err
|
||||
}
|
||||
return transaction.Inspection{Status: transaction.InspectionNotApplied}, nil
|
||||
}
|
||||
|
||||
var _ transaction.Operation = (*switchNginxOperation)(nil)
|
||||
var _ transaction.Operation = (*removeContainerOperation)(nil)
|
||||
Reference in New Issue
Block a user