fix: make daemon reset conflict safe
This commit is contained in:
@@ -336,12 +336,30 @@ func (a *Adapter) disable(ctx context.Context) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("read Docker daemon recovery metadata: %w", err)
|
||||
}
|
||||
|
||||
// daemon.json is a shared, whole-file configuration surface. Restore it only
|
||||
// while the complete current document is exactly the document proxyctl last
|
||||
// applied. This protects both managed proxy keys and unrelated administrator
|
||||
// edits from being overwritten by the original snapshot.
|
||||
current, currentErr := a.files.ReadFile(a.configuration.DaemonConfigPath)
|
||||
if currentErr != nil && !errors.Is(currentErr, fs.ErrNotExist) {
|
||||
return fmt.Errorf("read Docker daemon configuration before restore: %w", currentErr)
|
||||
}
|
||||
alreadyOriginal := metadata.OriginalExists && currentErr == nil && fingerprint(current) == fingerprint(metadata.Original)
|
||||
alreadyAbsent := !metadata.OriginalExists && errors.Is(currentErr, fs.ErrNotExist)
|
||||
if alreadyOriginal || alreadyAbsent {
|
||||
if err := a.files.Remove(a.configuration.RecoveryPath); err != nil && !errors.Is(err, fs.ErrNotExist) {
|
||||
return fmt.Errorf("clean Docker daemon recovery metadata for already-restored configuration: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if currentErr != nil || fingerprint(current) != metadata.ConfigFingerprint {
|
||||
return errors.New("Docker daemon configuration changed after proxyctl applied it; refusing to overwrite current configuration or discard recovery metadata")
|
||||
}
|
||||
|
||||
if metadata.OriginalExists {
|
||||
current, _ := a.files.ReadFile(a.configuration.DaemonConfigPath)
|
||||
if fingerprint(current) != fingerprint(metadata.Original) {
|
||||
if err := a.files.AtomicWrite(a.configuration.DaemonConfigPath, metadata.Original, os.FileMode(metadata.OriginalMode)); err != nil {
|
||||
return fmt.Errorf("restore Docker daemon configuration: %w", err)
|
||||
}
|
||||
if err := a.files.AtomicWrite(a.configuration.DaemonConfigPath, metadata.Original, os.FileMode(metadata.OriginalMode)); err != nil {
|
||||
return fmt.Errorf("restore Docker daemon configuration: %w", err)
|
||||
}
|
||||
} else if err := a.files.Remove(a.configuration.DaemonConfigPath); err != nil && !errors.Is(err, fs.ErrNotExist) {
|
||||
return fmt.Errorf("remove managed Docker daemon configuration: %w", err)
|
||||
|
||||
@@ -355,6 +355,126 @@ func TestDockerDaemonEnableOrdersManagedWriteReloadRestartAndPostInspect(t *test
|
||||
}
|
||||
}
|
||||
|
||||
func TestDockerDaemonDisableAndResetRejectDriftWithoutMutatingDaemonOrRecovery(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
mutate func(t *testing.T, managed []byte) []byte
|
||||
}{
|
||||
{
|
||||
name: "unrelated administrator edit",
|
||||
mutate: func(t *testing.T, managed []byte) []byte {
|
||||
t.Helper()
|
||||
var document map[string]any
|
||||
if err := json.Unmarshal(managed, &document); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
document["log-level"] = "debug"
|
||||
changed, err := json.MarshalIndent(document, "", " ")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return append(changed, '\n')
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "managed proxy edit",
|
||||
mutate: func(t *testing.T, managed []byte) []byte {
|
||||
t.Helper()
|
||||
var document map[string]any
|
||||
if err := json.Unmarshal(managed, &document); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
document["proxies"].(map[string]any)["http-proxy"] = "http://administrator.example:3128"
|
||||
changed, err := json.MarshalIndent(document, "", " ")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return append(changed, '\n')
|
||||
},
|
||||
},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
a, record, daemonPath, recoveryPath, _ := fixture(t)
|
||||
if result := a.SetDesired(context.Background(), true, activeProfile()); result.Err != nil {
|
||||
t.Fatal(result.Err)
|
||||
}
|
||||
managed, err := os.ReadFile(daemonPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
changed := test.mutate(t, managed)
|
||||
if err := os.WriteFile(daemonPath, changed, 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
recoveryBefore, err := os.ReadFile(recoveryPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
record.events = nil
|
||||
|
||||
reset := app.NewMaintenance(profile.State{Profiles: []profile.Profile{*activeProfile()}, ActiveProfile: activeProfile().Name}, []target.Target{a}, immediateLock{}, nil).Reset(context.Background(), true)
|
||||
if reset.Err == nil || len(reset.Targets) != 1 || reset.Targets[0].Err == nil || reset.Targets[0].Inspection.State != target.Drifted {
|
||||
t.Fatalf("conflicting reset = %#v", reset)
|
||||
}
|
||||
if events := mutationEvents(record.events); len(events) != 0 {
|
||||
t.Fatalf("conflicting reset mutated daemon state: %#v", events)
|
||||
}
|
||||
if got, err := os.ReadFile(daemonPath); err != nil || !reflect.DeepEqual(got, changed) {
|
||||
t.Fatalf("current daemon configuration changed to %q: %v", got, err)
|
||||
}
|
||||
if got, err := os.ReadFile(recoveryPath); err != nil || !reflect.DeepEqual(got, recoveryBefore) {
|
||||
t.Fatalf("recovery metadata changed to %q: %v", got, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDockerDaemonResetCleansAlreadyRestoredRecoveryWithoutDaemonWriteOrSystemctl(t *testing.T) {
|
||||
a, record, daemonPath, recoveryPath, _ := fixture(t)
|
||||
original := []byte("{\n \"log-level\": \"debug\",\n \"proxies\": {\"http-proxy\": \"http://prior.example:3128\"}\n}\n")
|
||||
if err := os.MkdirAll(filepath.Dir(daemonPath), 0o700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(daemonPath, original, 0o640); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if result := a.SetDesired(context.Background(), true, activeProfile()); result.Err != nil {
|
||||
t.Fatal(result.Err)
|
||||
}
|
||||
if err := os.WriteFile(daemonPath, original, 0o640); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
record.events = nil
|
||||
|
||||
reset := app.NewMaintenance(profile.State{Profiles: []profile.Profile{*activeProfile()}, ActiveProfile: activeProfile().Name}, []target.Target{a}, immediateLock{}, nil).Reset(context.Background(), true)
|
||||
if reset.Err != nil || len(reset.Targets) != 1 || reset.Targets[0].Err != nil || reset.Targets[0].Inspection.State != target.External {
|
||||
t.Fatalf("already-restored reset = %#v", reset)
|
||||
}
|
||||
for _, event := range record.events {
|
||||
if event == "write daemon.json" || strings.Contains(event, "daemon-reload") || strings.Contains(event, " restart ") {
|
||||
t.Fatalf("already-restored reset changed daemon or systemd state: %#v", record.events)
|
||||
}
|
||||
}
|
||||
if !contains(record.events, "remove docker-daemon.json") {
|
||||
t.Fatalf("already-restored reset did not clean recovery metadata: %#v", record.events)
|
||||
}
|
||||
if got, err := os.ReadFile(daemonPath); err != nil || !reflect.DeepEqual(got, original) {
|
||||
t.Fatalf("already-restored daemon configuration changed to %q: %v", got, err)
|
||||
}
|
||||
if info, err := os.Stat(daemonPath); err != nil || info.Mode().Perm() != 0o640 {
|
||||
t.Fatalf("already-restored daemon metadata changed: info=%v err=%v", info, err)
|
||||
}
|
||||
if _, err := os.Stat(recoveryPath); !errors.Is(err, os.ErrNotExist) {
|
||||
t.Fatalf("recovery metadata remains: %v", err)
|
||||
}
|
||||
|
||||
record.events = nil
|
||||
retry := a.SetDesired(context.Background(), false, nil)
|
||||
if retry.Err != nil || retry.Inspection.State != target.External || len(mutationEvents(record.events)) != 0 {
|
||||
t.Fatalf("already-restored cleanup retry = %#v events=%#v", retry, record.events)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDockerDaemonPreservesAndRestoresExistingConfiguration(t *testing.T) {
|
||||
a, _, daemonPath, recoveryPath, _ := fixture(t)
|
||||
original := []byte("{\n \"log-level\": \"debug\",\n \"proxies\": {\"http-proxy\": \"http://prior.example:3128\"}\n}\n")
|
||||
|
||||
Reference in New Issue
Block a user