Merge remote-tracking branch 'upstream/main' into feat/base-service-delivery

# Conflicts:
#	ansible/mysql-deploy.yml
#	frontend/src/api/delivery.ts
#	frontend/src/views/service/Catalog.vue
#	server/.env.example
#	server/internal/config/config.go
#	server/internal/handler/task_log.go
#	server/internal/service/delivery.go
This commit is contained in:
mac
2026-07-28 15:08:37 +08:00
15 changed files with 1145 additions and 345 deletions
+59 -14
View File
@@ -1,6 +1,11 @@
package service
import "testing"
import (
"testing"
"time"
"github.com/1024XEngineer/xinfra/server/internal/model"
)
func TestValidateDeliveryInput(t *testing.T) {
dataDisks := []string{"/data", "/disk1"}
@@ -18,15 +23,18 @@ func TestValidateDeliveryInput(t *testing.T) {
full.CPUMilli = 2000
full.MemoryMi = 8192
full.StorageGi = 2000
full.TimeZone = "+08:00"
full.LowerCaseTableNames = 0
full.Timezone = "+08:00"
lowerCaseZero := 0
full.LowerCaseTableNames = &lowerCaseZero
full.CharacterSet = "utf8mb4"
full.Collation = "utf8mb4_general_ci"
full.MaxConnections = "auto"
full.InnoDBRedoLogCapacity = "256M"
full.InnoDBFlushLogAtTrxCommit = 2
full.SyncBinlog = 0
full.InnoDBIOCapacity = 2000
full.InnodbRedoLogCapacity = "256M"
flushLog := 2
full.InnodbFlushLogAtTrxCommit = &flushLog
syncBinlog := 0
full.SyncBinlog = &syncBinlog
full.InnodbIOCapacity = 2000
full.LongQueryTime = 0.5
full.BinlogExpireLogsSeconds = 604800
full.MaxBinlogSize = "512M"
@@ -34,7 +42,7 @@ func TestValidateDeliveryInput(t *testing.T) {
t.Fatalf("valid full input rejected: %v", err)
}
namedZone := valid
namedZone.TimeZone = "Asia/Shanghai"
namedZone.Timezone = "Asia/Shanghai"
if err := validateDeliveryInput(namedZone, dataDisks); err != nil {
t.Fatalf("named timezone rejected: %v", err)
}
@@ -58,15 +66,15 @@ func TestValidateDeliveryInput(t *testing.T) {
"port below pool": func(in *MySQLDeliveryInput) { in.MySQLPort = 3307 },
"port above pool": func(in *MySQLDeliveryInput) { in.MySQLPort = 14000 },
"bad target host": func(in *MySQLDeliveryInput) { in.TargetHost = "-bad-host" },
"bad timezone": func(in *MySQLDeliveryInput) { in.TimeZone = "UTC+8" },
"bad lower case": func(in *MySQLDeliveryInput) { in.LowerCaseTableNames = 2 },
"bad timezone": func(in *MySQLDeliveryInput) { in.Timezone = "UTC+8" },
"bad lower case": func(in *MySQLDeliveryInput) { v := 2; in.LowerCaseTableNames = &v },
"bad charset": func(in *MySQLDeliveryInput) { in.CharacterSet = "big5" },
"collation mismatch": func(in *MySQLDeliveryInput) { in.CharacterSet = "gbk"; in.Collation = "utf8mb4_general_ci" },
"bad max connections": func(in *MySQLDeliveryInput) { in.MaxConnections = "300" },
"bad redo capacity": func(in *MySQLDeliveryInput) { in.InnoDBRedoLogCapacity = "2G" },
"bad flush log": func(in *MySQLDeliveryInput) { in.InnoDBFlushLogAtTrxCommit = 3 },
"bad sync binlog": func(in *MySQLDeliveryInput) { in.SyncBinlog = 2 },
"bad io capacity": func(in *MySQLDeliveryInput) { in.InnoDBIOCapacity = 500 },
"bad redo capacity": func(in *MySQLDeliveryInput) { in.InnodbRedoLogCapacity = "2G" },
"bad flush log": func(in *MySQLDeliveryInput) { v := 3; in.InnodbFlushLogAtTrxCommit = &v },
"bad sync binlog": func(in *MySQLDeliveryInput) { v := 2; in.SyncBinlog = &v },
"bad io capacity": func(in *MySQLDeliveryInput) { in.InnodbIOCapacity = 500 },
"bad long query time": func(in *MySQLDeliveryInput) { in.LongQueryTime = 3 },
"bad binlog expire": func(in *MySQLDeliveryInput) { in.BinlogExpireLogsSeconds = 3600 },
"bad max binlog size": func(in *MySQLDeliveryInput) { in.MaxBinlogSize = "64M" },
@@ -122,3 +130,40 @@ func TestAllocatePort(t *testing.T) {
t.Fatal("exhausted pool still allocated a port")
}
}
func TestRollbackExtraVarsTargetsOnlyTheAllocatedInstance(t *testing.T) {
task := &model.DeliveryTask{ID: "task-1", TargetHost: "db-01"}
payload := deliveryPayload{MySQLDeliveryInput: MySQLDeliveryInput{InstanceName: "mysql-a", DataDisk: "/disk1"}}
vars := rollbackExtraVars(task, payload)
if vars["target_hosts"] != "db-01" || vars["instance_name"] != "mysql-a" || vars["data_disk"] != "/disk1" {
t.Fatalf("rollback vars target the wrong instance: %#v", vars)
}
if vars["rollback"] != true {
t.Fatalf("rollback marker missing: %#v", vars)
}
}
func TestRegisterFailedIsProtectedFromRollback(t *testing.T) {
if !rollbackProtectedStatus(model.TaskRegisterFailed) {
t.Fatal("register_failed must preserve the healthy instance and resource usage")
}
if rollbackProtectedStatus(model.TaskValidationFailed) {
t.Fatal("validation_failed must still be eligible for cleanup rollback")
}
}
func TestRollbackLaunchExpired(t *testing.T) {
now := time.Date(2026, 7, 28, 12, 0, 0, 0, time.UTC)
started := now.Add(-rollbackLaunchTimeout - time.Second)
if !rollbackLaunchExpired(model.RollbackJob{Status: "launching", StartedAt: &started}, now) {
t.Fatal("stale launching rollback job must be recoverable")
}
if rollbackLaunchExpired(model.RollbackJob{Status: "launching", StartedAt: ptrTime(now.Add(-rollbackLaunchTimeout + time.Second))}, now) {
t.Fatal("recent launching rollback job must remain pending")
}
if rollbackLaunchExpired(model.RollbackJob{Status: "running", StartedAt: &started}, now) {
t.Fatal("running rollback job is not a launch timeout")
}
}
func ptrTime(v time.Time) *time.Time { return &v }