fix: harden operations and configuration recovery

This commit is contained in:
proxyctl coordinator
2026-08-30 11:13:45 +08:00
parent e34bf7d989
commit 8928986830
27 changed files with 542 additions and 125 deletions
+29 -17
View File
@@ -329,6 +329,7 @@ func (a *Adapter) enable(active *profile.Profile) error {
if err != nil {
return fmt.Errorf("read Docker client configuration for safe JSON merge: %w", err)
}
observedConfig := fingerprintDocument(config, configPresent)
metadata, managed, recoveryErr := a.readRecovery()
if recoveryErr != nil && !errors.Is(recoveryErr, fs.ErrNotExist) {
return fmt.Errorf("read Docker client recovery metadata: %w", recoveryErr)
@@ -362,6 +363,9 @@ func (a *Adapter) enable(active *profile.Profile) error {
applyDesired(defaults, desired)
setChildObject(proxies, "default", defaults)
setChildObject(config, "proxies", proxies)
if err := a.requireConfigUnchanged(observedConfig); err != nil {
return err
}
if err := a.writeConfig(config); err != nil {
return fmt.Errorf("write Docker client proxy defaults after recovery metadata persisted: %w", err)
}
@@ -380,6 +384,7 @@ func (a *Adapter) disable() error {
if err != nil && !errors.Is(err, fs.ErrNotExist) {
return fmt.Errorf("read Docker client configuration for restoration: %w", err)
}
observedConfig := fingerprintDocument(config, configPresent)
if !configPresent {
config = make(map[string]json.RawMessage)
}
@@ -422,6 +427,9 @@ func (a *Adapter) disable() error {
} else {
setChildObject(config, "proxies", proxies)
}
if err := a.requireConfigUnchanged(observedConfig); err != nil {
return err
}
if len(config) == 0 && !metadata.ConfigPresent {
if err := a.files.Remove(a.configuration.ConfigFile); err != nil && !errors.Is(err, fs.ErrNotExist) {
return fmt.Errorf("remove newly created empty Docker client configuration: %w", err)
@@ -543,23 +551,6 @@ func applyDesired(defaults map[string]json.RawMessage, desired map[string]string
}
}
}
func recognizesProxyDefaults(config map[string]json.RawMessage) bool {
proxies, _, err := childObject(config, "proxies")
if err != nil {
return false
}
defaults, _, err := childObject(proxies, "default")
if err != nil {
return false
}
for _, key := range append(append([]string(nil), managedKeys...), "ftpProxy") {
var value string
if raw, ok := defaults[key]; ok && json.Unmarshal(raw, &value) == nil && value != "" {
return true
}
}
return false
}
type managedValue struct {
Present bool `json:"present"`
@@ -612,6 +603,27 @@ func fingerprintSnapshot(snapshot map[string]managedValue) string {
sum := sha256.Sum256(data)
return hex.EncodeToString(sum[:])
}
func fingerprintDocument(config map[string]json.RawMessage, present bool) string {
data, _ := json.Marshal(struct {
Present bool `json:"present"`
Config map[string]json.RawMessage `json:"config"`
}{Present: present, Config: config})
sum := sha256.Sum256(data)
return hex.EncodeToString(sum[:])
}
func (a *Adapter) requireConfigUnchanged(expected string) error {
current, present, err := a.readConfig()
if err != nil {
return fmt.Errorf("re-read Docker client configuration before atomic replacement: %w", err)
}
if fingerprintDocument(current, present) != expected {
return errors.New("Docker client configuration changed concurrently; refusing to overwrite external updates")
}
return nil
}
func cloneRaw(value json.RawMessage) json.RawMessage { return append(json.RawMessage(nil), value...) }
func (a *Adapter) ownership() *Ownership {
if !a.configuration.OwnFiles {
@@ -656,6 +656,39 @@ func (f *failingFS) AtomicWrite(path string, data []byte, mode os.FileMode, owne
return f.FileSystem.AtomicWrite(path, data, mode, owner)
}
type concurrentChangeFS struct {
dockerclient.FileSystem
configPath string
reads int
}
func (f *concurrentChangeFS) ReadFile(path string) ([]byte, error) {
if path == f.configPath {
f.reads++
if f.reads == 3 {
if err := os.WriteFile(path, []byte(`{"auths":{"registry.example":{"auth":"external-update"}}}`), 0o600); err != nil {
return nil, err
}
}
}
return f.FileSystem.ReadFile(path)
}
func TestConcurrentDockerConfigChangeIsNotOverwritten(t *testing.T) {
files := &concurrentChangeFS{FileSystem: dockerclient.OSFileSystem{}}
a := newAdapter(t, files)
files.configPath = a.ConfigPath()
writeConfig(t, a.ConfigPath(), `{"auths":{"registry.example":{"auth":"original"}}}`)
result := a.SetDesired(context.Background(), true, activeProfile())
if result.Err == nil || !strings.Contains(result.Err.Error(), "changed concurrently") {
t.Fatalf("concurrent update result = %#v", result)
}
if got := readFile(t, a.ConfigPath()); !strings.Contains(got, "external-update") || strings.Contains(got, "httpProxy") {
t.Fatalf("external Docker config update was overwritten: %s", got)
}
}
type countingFS struct {
dockerclient.FileSystem
writes int