fix: harden operations and configuration recovery
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user