contexts: drop the inert ClusterContexts field entirely · Entire
contexts: drop the inert ClusterContexts field entirely
8f46c95→main·
toothbrush·1mo ago·4 files·+21 added/-50 removed
json.Unmarshal ignores unknown keys, so an existing contexts.json with a "cluster_contexts" entry still loads cleanly — the obsolete key is simply dropped on the next write. With nothing reading or writing it, removing the field (and the last references in Delete and RemoveAllContexts) is safe and leaves no dead weight.
Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com
Sessions
4f015a69dc6bView transcript
Changes
4
cmd/entire/cli/auth
Mcontext_store.go+5/-7
- Mcontexts_test.go+3/-13
internal/entireclient/contexts
Mcontexts.go+9/-16
- Mcontexts_test.go+4/-14
60 unmodified lines
61
62
63
64
65
66
67
68
64
65
66
67
68
69
70
72
71
72
73
74
4 unmodified lines
79
80
81
83
82
83
84
60 unmodified lines
return nil
}
// RemoveAllContexts deletes every stored context, its keyring token, and all
// cluster bindings — a full local logout. Returns the number of contexts
// removed. Best-effort on the keyring deletes; the contexts.json clear is
// what makes the CLI fully logged out (no surviving binding can authenticate
// a clone afterward).
// RemoveAllContexts deletes every stored context and its keyring token — a
// full local logout. Returns the number of contexts removed. Best-effort on
// the keyring deletes; the contexts.json clear is what makes the CLI fully
// logged out.
func RemoveAllContexts() (int, error) {
var removed int
if err := contexts.Modify(contexts.DefaultConfigDir(), func(f *contexts.File) (bool, error) {
if len(f.Contexts) == 0 && f.CurrentContext == "" && len(f.ClusterContexts) == 0 {
if len(f.Contexts) == 0 && f.CurrentContext == "" {
return false, nil
}
for _, c := range f.Contexts {
4 unmodified lines
}
f.Contexts = nil
f.CurrentContext = ""
f.ClusterContexts = nil
return true, nil
}); err != nil {
return 0, fmt.Errorf("remove all contexts: %w", err)
}
Mcmd/entire/cli/auth/context_store.go+5/-7
222 unmodified lines
223
224
225
226
227
226
227
228
229
230
231
233
234
235
236
237
238
239
240
241
232
233
234
5 unmodified lines
240
241
242
253
254
243
244
245
246
247
222 unmodified lines
t.Cleanup(restore)
exp := time.Now().Add(time.Hour).Unix()
a, err := RecordLoginContext(makeJWT(t, fmt.Sprintf(`{"iss":"https://a.example.com","handle":"alice","exp":%d}`, exp)), true)
if err != nil {
if _, err := RecordLoginContext(makeJWT(t, fmt.Sprintf(`{"iss":"https://a.example.com","handle":"alice","exp":%d}`, exp)), true); err != nil {
t.Fatalf("record a: %v", err)
}
if _, err := RecordLoginContext(makeJWT(t, fmt.Sprintf(`{"iss":"https://b.example.com","handle":"bob","exp":%d}`, exp)), true); err != nil {
t.Fatalf("record b: %v", err)
}
// Seed a legacy cluster_contexts entry directly to prove RemoveAllContexts
// still clears the inert field on a full logout.
if err := contexts.Modify(cfgDir, func(f *contexts.File) (bool, error) {
f.ClusterContexts = map[string]string{"cluster.example.com": a}
return true, nil
}); err != nil {
t.Fatalf("seed legacy binding: %v", err)
}
n, err := RemoveAllContexts()
if err != nil {
t.Fatalf("RemoveAllContexts: %v", err)
}
if err != nil {
t.Fatalf("load: %v", err)
}
if len(f.Contexts) != 0 || f.CurrentContext != "" || len(f.ClusterContexts) != 0 {
t.Fatalf("expected fully cleared, got contexts=%d current=%q bindings=%d", len(f.Contexts), f.CurrentContext, len(f.ClusterContexts))
}
if len(f.Contexts) != 0 || f.CurrentContext != "" {
t.Fatalf("expected fully cleared, got contexts=%d current=%q", len(f.Contexts), f.CurrentContext)
}
// Idempotent.
Mcmd/entire/cli/auth/contexts_test.go+3/-13
48 unmodified lines
49
50
51
52
53
54
55
56
57
58
52
53
54
55
56
57
58
59
60
61
62
63
108 unmodified lines
172
173
174
176
177
178
175
176
177
178
179
2 unmodified lines
182
183
184
187
188
189
190
191
185
186
187
6 unmodified lines
194
195
196
204
197
198
199
200
48 unmodified lines
// CurrentContext is the active login; the default identity for cluster
// operations and direct CLI commands. Empty until the first login.
CurrentContext string `json:"current_context,omitempty"`
// ClusterContexts is a legacy cluster host → context name binding map.
// No longer read or written: the cluster's cores are cached separately
// (discovery.ClusterCoresCache) and the account is chosen fresh per
// operation. The field is retained only so an existing contexts.json
// round-trips without losing data; Delete still prunes entries and a
// full logout clears it. Safe to drop once no deployed tool writes it.
ClusterContexts map[string]string `json:"cluster_contexts,omitempty"`
// Contexts is the list of stored credentials. Order is preserved on
// disk so list output stays stable across saves.
Contexts []*Context `json:"contexts,omitempty"`
// NOTE: a legacy "cluster_contexts" map used to live here, binding a
// cluster host to one context name. It's gone — cluster cores are cached
// in discovery.ClusterCoresCache and the account is chosen fresh per
// operation. json.Unmarshal ignores the obsolete key, so an old file
// loads cleanly; the key drops out on the next write.
}
// FilePath returns $configDir/contexts.json after ensuring the directory
108 unmodified lines
}
// Delete drops the context with the given name, plus any cluster
// bindings that pointed at it. If the deleted context was current, the
// current pointer advances to whatever remains (or empty).
// Delete drops the context with the given name. If the deleted context
// was current, the current pointer advances to whatever remains (or empty).
func (f *File) Delete(name string) {
if f == nil || name == "" {
return
}
2 unmodified lines
if idx >= 0 {
f.Contexts = slices.Delete(f.Contexts, idx, idx+1)
}
for host, target := range f.ClusterContexts {
if target == name {
delete(f.ClusterContexts, host)
}
}
if f.CurrentContext == name {
f.CurrentContext = ""
if len(f.Contexts) > 0 {
6 unmodified lines
// exclusive flock — load, mutate, write all happen with the lock held.
// Use this for any read-modify-write sequence; calling Load and Save
// separately releases the lock between them and races concurrent
// writers (e.g. a parallel git push triggering BindCluster).
// writers (e.g. a parallel login recording a context).
//
// fn returns (changed, err). When changed is false the file isn't
// rewritten — useful for idempotent operations that often have
Minternal/entireclient/contexts/contexts.go+9/-16
21 unmodified lines
22
23
24
25
25
26
27
28
2 unmodified lines
31
32
33
34
35
36
34
35
36
83 unmodified lines
120
121
122
126
123
124
125
129
130
131
132
126
127
128
3 unmodified lines
132
133
134
142
143
144
145
146
135
136
137
138
139
21 unmodified lines
if f == nil {
t.Fatal("Load returned nil File")
}
if f.CurrentContext != "" || len(f.Contexts) != 0 || len(f.ClusterContexts) != 0 {
if f.CurrentContext != "" || len(f.Contexts) != 0 {
t.Errorf("expected zero-valued File, got %+v", f)
}
}
dir := t.TempDir()
want := &contexts.File{
CurrentContext: "eu-paul",
ClusterContexts: map[string]string{
"royalcanin.partial.to": "us-superadmin",
},
Contexts: []*contexts.Context{
{Name: "eu-paul", CoreURL: "https://eu.example", Handle: "paul", KeychainService: "entire-core:eu-paul"},
{Name: "us-superadmin", CoreURL: "https://us.example", Handle: "superadmin", KeychainService: "entire-core:us-superadmin"},
83 unmodified lines
}
}
}
func TestDelete_DropsContextAndBindings(t *testing.T) {
func TestDelete_DropsContext(t *testing.T) {
f := &contexts.File{
CurrentContext: "stays",
ClusterContexts: map[string]string{
"a.example": "doomed",
"b.example": "stays",
},
Contexts: []*contexts.Context{
{Name: "stays"},
{Name: "doomed"},
3 unmodified lines
if f.Find("doomed") != nil {
t.Error("Delete left context behind")
}
if _, exists := f.ClusterContexts["a.example"]; exists {
t.Error("Delete left dangling cluster binding")
}
if f.ClusterContexts["b.example"] != "stays" {
t.Error("Delete clobbered unrelated cluster binding")
}
if f.Find("stays") == nil {
t.Error("Delete dropped an unrelated context")
}
if f.CurrentContext != "stays" {
t.Errorf("CurrentContext = %q, want %q (untouched)", f.CurrentContext, "stays")