diff --git a/cmd/ocm-agent/main.go b/cmd/ocm-agent/main.go index 936f119f04108652933e4ac8fa745f32d1bd7d54..ff951574a196575a8e5bccd7cf7aa377de78b674 100644 --- a/cmd/ocm-agent/main.go +++ b/cmd/ocm-agent/main.go @@ -280,7 +280,7 @@ func connectionSupervisor( // 控制面 CMD_* handler reloadFn := func(b []byte) error { - _, err := config.Load(configPath) + _, err := config.Parse(b) return err } dispatcher.Register(protocol.TypeCmdRestart, diff --git a/internal/command/config_push.go b/internal/command/config_push.go index 1dc89c842c7423bf100824b01c5b7b5755cea304..537f559471266e1618ed281843ca543376e67b3b 100644 --- a/internal/command/config_push.go +++ b/internal/command/config_push.go @@ -67,7 +67,7 @@ func (h *ConfigPushHandler) Handle(frame *protocol.Frame) { } } -// handleInline 实现 INLINE 模式的"size check → atomic write → reload 校验 → ack"流程。 +// handleInline 实现 INLINE 模式的"size check → reload 校验 → atomic write → ack"流程。 func (h *ConfigPushHandler) handleInline(p *commandpb.CmdConfigPushPayload) { if len(p.ConfigBytes) == 0 { _ = WriteAck(h.fw, p.TaskId, commandpb.AckStatus_ACK_STATUS_FAILED, @@ -80,6 +80,18 @@ func (h *ConfigPushHandler) handleInline(p *commandpb.CmdConfigPushPayload) { return } + // reload 在本 change 中只做"语法/字段校验",不替换运行时状态。 + // 必须先校验字节,再替换磁盘文件,确保失败 ACK 不改变本地配置。 + if h.reload != nil { + if err := h.reload(p.ConfigBytes); err != nil { + h.log.Warn("CMD_CONFIG_PUSH: reload validation failed", + zap.String("task_id", p.TaskId), zap.Error(err)) + _ = WriteAck(h.fw, p.TaskId, commandpb.AckStatus_ACK_STATUS_FAILED, + ErrCodeInvalidArg, "reload: "+err.Error(), nil) + return + } + } + // atomic-write:先写到 .tmp,再 rename,避免崩溃半写。 tmp := h.configPath + ".tmp" if err := os.WriteFile(tmp, p.ConfigBytes, 0o600); err != nil { @@ -96,17 +108,6 @@ func (h *ConfigPushHandler) handleInline(p *commandpb.CmdConfigPushPayload) { return } - // reload 在本 change 中只做"语法/字段校验",不替换运行时状态。 - if h.reload != nil { - if err := h.reload(p.ConfigBytes); err != nil { - h.log.Warn("CMD_CONFIG_PUSH: reload validation failed", - zap.String("task_id", p.TaskId), zap.Error(err)) - _ = WriteAck(h.fw, p.TaskId, commandpb.AckStatus_ACK_STATUS_FAILED, - ErrCodeInvalidArg, "reload: "+err.Error(), nil) - return - } - } - h.log.Info("CMD_CONFIG_PUSH success", zap.String("task_id", p.TaskId), zap.Int("bytes", len(p.ConfigBytes))) _ = WriteAck(h.fw, p.TaskId, commandpb.AckStatus_ACK_STATUS_SUCCESS, 0, "", nil) diff --git a/internal/command/config_push_test.go b/internal/command/config_push_test.go index b0e789d2d868be97a1964fda4ee6451ce8ea2030..302974a58a62fb8b1dc6212431efcac71a0cdccd 100644 --- a/internal/command/config_push_test.go +++ b/internal/command/config_push_test.go @@ -79,6 +79,10 @@ func TestConfigPushHandler_InlineOversized(t *testing.T) { func TestConfigPushHandler_InlineReloadError(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "config.yaml") + oldConfig := []byte("log:\n level: info\n") + if err := os.WriteFile(path, oldConfig, 0o600); err != nil { + t.Fatalf("seed config: %v", err) + } fw := &fakeFW{} reload := func(_ []byte) error { return errors.New("syntax error: bad yaml") } h := NewConfigPushHandler(fw, path, reload, zap.NewNop()) @@ -94,6 +98,16 @@ func TestConfigPushHandler_InlineReloadError(t *testing.T) { if ack.Status != commandpb.AckStatus_ACK_STATUS_FAILED || ack.ErrorCode != ErrCodeInvalidArg { t.Fatalf("got status=%v code=%d, want FAILED+InvalidArg", ack.Status, ack.ErrorCode) } + got, err := os.ReadFile(path) + if err != nil { + t.Fatalf("read config after failed validation: %v", err) + } + if string(got) != string(oldConfig) { + t.Fatalf("config changed after failed validation: got %q want %q", got, oldConfig) + } + if _, err := os.Stat(path + ".tmp"); !os.IsNotExist(err) { + t.Fatalf("validation failure left a temporary file: %v", err) + } } func TestConfigPushHandler_ReferenceModeNotSupported(t *testing.T) { diff --git a/internal/config/config.go b/internal/config/config.go index 0f567f84d7071017768d4861a1c1f0d3d7fa6968..6d8ce7d46f8ef4f193a8beb6aeaebc9a0580ecd4 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -111,13 +111,22 @@ func DefaultConfig() *Config { } func Load(path string) (*Config, error) { - cfg := DefaultConfig() data, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("read config: %w", err) } - if err := yaml.Unmarshal(data, cfg); err != nil { + cfg, err := Parse(data) + if err != nil { return nil, fmt.Errorf("parse config: %w", err) } return cfg, nil } + +// Parse 校验配置字节,不读取或替换当前生效的配置文件。 +func Parse(data []byte) (*Config, error) { + cfg := DefaultConfig() + if err := yaml.Unmarshal(data, cfg); err != nil { + return nil, err + } + return cfg, nil +}