Skip to content

Commit 8666b3e

Browse files
committed
fix(tunnel-client): keep module_domains in sync after discovery
Two related gaps in the consumer-to-domain resolution surfaced once we started exercising NS8 with more modules: * The provider modules themselves (openldap, samba, …) never appeared in cluster/module_domains because they ARE the domain, so the UI hid the domain user inside their accordion. fetchProviderDomains() walks cluster/list-user-domains and maps each provider id back to its domain as a final fallback. * users.ModuleDomains was populated only during the initial RunSetup, so modules installed after the support session started (NethVoice in our case) never made it into the credentials modal. The discovery ticker now calls users.RefreshModuleDomains and resends the users report when the map actually grew, with no flood of updates for steady-state sessions.
1 parent 8e0b557 commit 8666b3e

2 files changed

Lines changed: 89 additions & 3 deletions

File tree

services/support/cmd/tunnel-client/internal/connection/connection.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,18 @@ func connect(ctx context.Context, cfg *config.ClientConfig, sessionUsers **users
405405
} else {
406406
log.Printf("Manifest updated with %d services", len(newServices))
407407
}
408+
// A module installed after session start (e.g. NethVoice
409+
// configured later) brings a new user-domain binding that
410+
// the frontend needs to show domain credentials. Refresh
411+
// users.ModuleDomains and resend the report when changed.
412+
if *sessionUsers != nil &&
413+
users.RefreshModuleDomains(*sessionUsers, newServices, cfg.RedisAddr) {
414+
if err := sendUsersReport(session, *sessionUsers); err != nil {
415+
log.Printf("Failed to send updated users report: %v", err)
416+
} else {
417+
log.Printf("Users report updated (module_domains: %d)", len(((*sessionUsers).ModuleDomains)))
418+
}
419+
}
408420
}
409421
}
410422
}

services/support/cmd/tunnel-client/internal/users/configurator.go

Lines changed: 77 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -206,12 +206,22 @@ func buildModuleContexts(services map[string]models.ServiceInfo, redisAddr strin
206206
return nil
207207
}
208208

209-
// Fetch USER_DOMAIN for each module instance (NS8 only)
209+
// Fetch the user domain for each module instance (NS8 only). Resolution
210+
// covers three cases:
211+
// 1. Consumer binding via cluster/module_domains (set by bind-user-domains)
212+
// 2. Provider's own environment USER_DOMAIN (some modules expose it)
213+
// 3. Provider listed in cluster/list-user-domains[].providers[].id —
214+
// this catches the LDAP/AD providers themselves (openldap, samba…),
215+
// which are not in cluster/module_domains because they ARE the domain
210216
domains := make(map[string]string)
211217
if redisAddr != "" {
218+
providerDomains := fetchProviderDomains()
212219
for moduleID := range moduleMap {
213-
domain := fetchModuleDomain(moduleID)
214-
if domain != "" {
220+
if domain := fetchModuleDomain(moduleID); domain != "" {
221+
domains[moduleID] = domain
222+
continue
223+
}
224+
if domain, ok := providerDomains[moduleID]; ok {
215225
domains[moduleID] = domain
216226
}
217227
}
@@ -288,6 +298,38 @@ func discoverLocalModulesFromRedis() map[string]*moduleInfo {
288298
return result
289299
}
290300

301+
// RefreshModuleDomains recomputes the moduleID → domain map from the given
302+
// services manifest and merges it into users.ModuleDomains. Returns true when
303+
// any entry was added or changed so the caller can decide to resend the
304+
// users report. Newly removed modules are NOT pruned — keeping a stale entry
305+
// is harmless and avoids racing the next discovery if a module is briefly
306+
// missing from the manifest.
307+
func RefreshModuleDomains(users *SessionUsers, services map[string]models.ServiceInfo, redisAddr string) bool {
308+
if users == nil {
309+
return false
310+
}
311+
contexts := buildModuleContexts(services, redisAddr)
312+
if len(contexts) == 0 {
313+
return false
314+
}
315+
if users.ModuleDomains == nil {
316+
users.ModuleDomains = make(map[string]string)
317+
}
318+
changed := false
319+
for _, mc := range contexts {
320+
for _, inst := range mc.Instances {
321+
if inst.Domain == "" {
322+
continue
323+
}
324+
if users.ModuleDomains[inst.ID] != inst.Domain {
325+
users.ModuleDomains[inst.ID] = inst.Domain
326+
changed = true
327+
}
328+
}
329+
}
330+
return changed
331+
}
332+
291333
// fetchModuleDomain returns the user domain bound to a module instance.
292334
// NS8 stores consumer-to-domain bindings in the cluster/module_domains hash
293335
// (set by the cluster/bind-user-domains action); some providers also expose
@@ -304,6 +346,38 @@ func fetchModuleDomain(moduleID string) string {
304346
return redisOutput("HGET", fmt.Sprintf("module/%s/environment", moduleID), "USER_DOMAIN")
305347
}
306348

349+
// fetchProviderDomains returns a moduleID → domain map for the modules that
350+
// provide a user domain (LDAP/AD providers such as openldap, samba). These
351+
// modules are not in cluster/module_domains because they ARE the domain, so
352+
// fetchModuleDomain alone would miss them.
353+
func fetchProviderDomains() map[string]string {
354+
out := make(map[string]string)
355+
cmd := exec.Command("api-cli", "run", "cluster/list-user-domains", "--data", "{}") //nolint:gosec // api-cli is trusted
356+
output, err := cmd.Output()
357+
if err != nil {
358+
return out
359+
}
360+
var resp struct {
361+
Domains []struct {
362+
Name string `json:"name"`
363+
Providers []struct {
364+
ID string `json:"id"`
365+
} `json:"providers"`
366+
} `json:"domains"`
367+
}
368+
if err := json.Unmarshal(output, &resp); err != nil {
369+
return out
370+
}
371+
for _, d := range resp.Domains {
372+
for _, p := range d.Providers {
373+
if p.ID != "" && d.Name != "" {
374+
out[p.ID] = d.Name
375+
}
376+
}
377+
}
378+
return out
379+
}
380+
307381
// redisOutput runs `redis-cli <args...>` and returns the trimmed stdout,
308382
// or the empty string when the call fails or the value is nil.
309383
func redisOutput(args ...string) string {

0 commit comments

Comments
 (0)