# fix: case-insensitive cluster-host matching in auth-context resolution

`cfc653d`·

toothbrush·3w ago·4 files·+66 added/-6 removed

DNS hosts are case-insensitive. Fold case at the auth-context determination chokepoint (clusterdiscovery.ResolveContextForCluster / ResolveClusterCores) so every cluster-addressed flow — control-plane cluster commands and the git/repo-token path — resolves the cache key, /.well-known discovery, and cores→context match regardless of case. Also fold case in clone's selectCloneTarget --cluster match, the spot the reviewer flagged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

## Sessions

4158c01c7c46View transcript

## Changes

4

- cmd/entire/cli

- Mrepo_clone.go+9/-5
  
  - Mrepo_clone_test.go+9

- internal/entireclient/clusterdiscovery

- Mresolve.go+14/-1
  
  - Mresolve_test.go+34

```
198 unmodified lines

199
200
201
202
202
203
204
205
206
207
208
206
209
210
211
212
209
210
213
214
215
216
217
218
215
219
220
221
222

198 unmodified lines

// interactively, failing fast with a --cluster pointer when there's no terminal.
func selectCloneTarget(cmd *cobra.Command, mirrors []coreapi.Mirror, clusterFlag string) (coreapi.Mirror, error) {
    // Dedupe by cluster host: one placement per cluster is what a clone targets,
    // and the same host appearing twice would only confuse the picker.
    // and the same host appearing twice would only confuse the picker. Key on the
    // case-folded host — DNS is case-insensitive, so a --cluster value differing
    // only in case from the API's ClusterHost must still match (the alternative is
    // a misleading "not mirrored on ..." after a successful lookup + dial).
    byHost := make(map[string]coreapi.Mirror, len(mirrors))
    hosts := make([]string, 0, len(mirrors))
    for _, m := range mirrors {
        if _, seen := byHost[m.ClusterHost]; seen {
            continue
        }
        key := strings.ToLower(m.ClusterHost)
        if _, seen := byHost[key]; seen {
            continue
        }
        byHost[m.ClusterHost] = m
        hosts = append(hosts, m.ClusterHost)
        byHost[key] = m
        hosts = append(hosts, key)
    }
    sort.Strings(hosts)

if clusterFlag != "" {
        m, ok := byHost[clusterFlag]
        m, ok := byHost[strings.ToLower(strings.TrimSpace(clusterFlag))]
        if !ok {
            return coreapi.Mirror{}, fmt.Errorf("repo is not mirrored on %q; available: %s", clusterFlag, strings.Join(hosts, ", "))
        }
    }
}
```

Mcmd/entire/cli/repo_clone.go+9/-5

```
167 unmodified lines

168
169
170
171
172
173
174
175
176
177
178
179
180
181
182

167 unmodified lines

require.Equal(t, "aws-eu-west-1.entire.io", got.ClusterHost)
})

t.Run("--cluster matches case-insensitively", func(t *testing.T) {
    t.Parallel()
    // DNS hosts are case-insensitive: a mixed-case --cluster must still match
    // the API's lowercase ClusterHost rather than falsely \"not mirrored\".
    got, err := selectCloneTarget(newCloneTestCmd(), []coreapi.Mirror{usEast, euWest}, "AWS-EU-West-1.Entire.IO")
    require.NoError(t, err)
    require.Equal(t, "aws-eu-west-1.entire.io", got.ClusterHost)
})

t.Run("--cluster with no match errors and lists hosts", func(t *testing.T) {
    t.Parallel()
    _, err := selectCloneTarget(newCloneTestCmd(), []coreapi.Mirror{usEast, euWest}, "aws-ap-south-1.entire.io")
```

Mcmd/entire/cli/repo_clone_test.go+9

```
52 unmodified lines

53
54
55
56
57
58
59
60
61
62
63
18 unmodified lines

82
83
84
80
85
86
87
88
89
90
91
92
93
94
95
96

52 unmodified lines

if debugf == nil {
        debugf = func(string, ...any) {}
    }
    // DNS hostnames are case-insensitive, so fold case before the host drives any
    // lookup: the cache key, the /.well-known fetch, and the cores→context match.
    // Without this, `aws-US-east-2.entire.io` and `aws-us-east-2.entire.io`
    // resolve as different hosts and a context determination can fail spuriously.
    clusterHost = normalizeClusterHost(clusterHost)
    f, err := contexts.Load(configDir)
    if err != nil {
        return nil, fmt.Errorf("load contexts: %w", err)
    }
}

18 unmodified lines

if debugf == nil {
        debugf = func(string, ...any) {}
    }
    return resolveClusterCores(ctx, cacheDir, clusterHost, httpClient, debugf)
    return resolveClusterCores(ctx, cacheDir, normalizeClusterHost(clusterHost), httpClient, debugf)
}

// normalizeClusterHost folds a cluster host to its canonical form for use as a
// lookup key. DNS is case-insensitive, so two hosts differing only in case (or
// surrounding whitespace) name the same cluster and must resolve identically —
// for the host→cores cache, /.well-known discovery, and context determination.
func normalizeClusterHost(clusterHost string) string {
    return strings.ToLower(strings.TrimSpace(clusterHost))
}

// resolveCachedCores is the shared cache-then-/.well-known resolution behind
```

Minternal/entireclient/clusterdiscovery/resolve.go+14/-1

```
162 unmodified lines

163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202

162 unmodified lines

assert.Equal(t, []string{"https://eu.auth.entire.io"}, urls)
}

// TestResolve_ClusterHostCaseInsensitive: a mixed-case cluster host resolves
// the same context as its lowercase form and caches under the canonical
// (lowercased) key, since DNS hosts are case-insensitive.
func TestResolve_ClusterHostCaseInsensitive(t *testing.T) {
    t.Parallel()
    var calls int32
    srv := httptest.NewServer(coresHandler(t, &calls, "https://eu.auth.entire.io"))
    defer srv.Close()

configDir := t.TempDir()
    cacheDir := t.TempDir()
    require.NoError(t, contexts.Save(configDir, &contexts.File{
        CurrentContext: "prod-eu",
        Contexts: []*contexts.Context{
            {Name: "prod-eu", CoreURL: "https://eu.auth.entire.io", Handle: "paul", KeychainService: "kc:prod"},
        },
    }))

c, err := ResolveContextForCluster(t.Context(), configDir, cacheDir, "AWS-EU-Central-1.Entire.IO", hostPinningClient(t, srv), t.Logf)
    require.NoError(t, err)
    assert.Equal(t, "prod-eu", c.Name)

// Cached under the canonical lowercase host, so the lowercase form is a hit.
    cache, err := discovery.LoadClusterCores(cacheDir)
    require.NoError(t, err)
    _, _, ok := cache.Get("aws-eu-central-1.entire.io")
    assert.True(t, ok, "cores cached under the lowercased host key")

c2, err := ResolveContextForCluster(t.Context(), configDir, cacheDir, "aws-eu-central-1.entire.io", hostPinningClient(t, srv), t.Logf)
    require.NoError(t, err)
    assert.Equal(t, "prod-eu", c2.Name)
    assert.Equal(t, int32(1), atomic.LoadInt32(&calls), "lowercase form hits the cache the mixed-case call populated")
}

// TestResolve_StaleCacheFallbackOnDiscoveryFailure: an expired cache entry
// is used when the live re-fetch fails, so a brief cluster outage doesn't
// break an operation whose cores we already knew.
```

Minternal/entireclient/clusterdiscovery/resolve_test.go+34
