Skip to content

refactor: share duplicated helpers and route output through ui #94

Closed

Description

@entelecheia

Context

Audit on 2026-11-09 found the same logic implemented several times, with drift between copies. No behaviour change is intended except where the copies already disagree (then the safer one wins and the release note says so).

Internal (internal/module, internal/tenant, internal/fleet):

  • write-managed-file-if-changed in four forms (fileEquals + writes ~15x, exporterGuardFileCurrent, tenant writeIfChanged, hand-written loops in security.go, system.go, monitoring_hub.go).
  • systemdReload lives in gpu.go:176 (failure = warning); monitoring, hub and ssh call daemon-reload directly (failure = error).
  • generatePassword twice with different alphabets (module/accounts.go:150, tenant/manager.go:421).
  • module-name list in four places (defaultOrder, NewRegistry, config/distro.go:67, IsModuleEnabled); only the first two are tested.
  • if homeBase == "" { homeBase = "/home" } 14 times.
  • shellQuote / shellQuoteCLI identical; defaultRunLog / defaultFleetInventory near-identical.
  • status output in module.go RunAll, users.go (36 prints), lifecycle.go, quota.go, ssh.go bypasses internal/ui and writes to stdout directly.

CLI (internal/cli):

  • capability-validation block repeated in apply/check/status/doctor/configcmd (configcmd hardcodes 14 names).
  • read-only RunContext construction repeated in status/doctor/check.
  • status text and JSON are built by two code paths; text skips ValidateCapabilities.
  • cloudflared binary path defined 3 times, version parsing twice; fetchLatestVersion duplicates FetchLatestCloudflaredVersion; token masking two ways; firstNonEmptyCloudflared duplicates firstNonEmpty.
  • apply/backup/tunnel/tenant print with raw fmt and literal glyphs instead of ui helpers and cmd.OutOrStdout(); fleet has its own JSON encoder and hardcoded exit 2.

Implementation starts only on the owner's go-ahead.

Decisions

  • One helper per concern, in the package that already owns it; no new interfaces.
  • Output goes through internal/ui and an io.Writer passed in (rc.Out), as tenant.Manager already does.
  • daemon-reload failure policy: warning (the unit write already succeeded; next apply retries).

Design

  • internal/module/files.go: ensureFile(rc, path, content, mode) (changed bool, err error), systemdReload, enableUnit.
  • UsersConfig.Base(); exported tenant.GeneratePassword (or internal/secret).
  • config.ModuleNames() used by distro and IsModuleEnabled, with a test against defaultOrder.
  • CLI: validateModules(cfg, names), readOnlyRunContext(cmd), status text rendered from collectStatus, githubLatestTag(owner, repo), one maskSecret.
  • RunContext.Out io.Writer; module prints become ui.WriteBullet(rc.Out, ...).

Changes

  • internal/module/*, internal/tenant/manager.go, internal/fleet/{ssh,rollout}.go, internal/config/{config,distro}.go, internal/cli/*.

Acceptance criteria

  • AC1: status -o text and -o json come from one collector (test compares module sets).
  • AC2: module-name lists agree (test fails if any of the four diverge).
  • AC3: no fmt.Print in internal/module non-test files; no literal ✓✗⚠→ outside internal/ui.
  • AC4: go test ./... -race passes; apply --dry-run output on each profile is unchanged except glyph routing (golden diff reviewed in the PR).

Tasks

  • T1: module files.go + callers
  • T2: config module names, home base, password helper (AC2)
  • T3: CLI helpers and single status collector (AC1)
  • T4: output through ui / rc.Out (AC3, AC4)

Split into several PRs along T1-T4; each PR keeps go test ./... -race green.

Out of scope

  • Behaviour changes beyond reconciling the drifted copies.

Activity

  1. entelecheia commented on Nov 9, 2026

    @entelecheia
    MemberAuthor

    Reopened: PR #120 delivered only T1 (shared file and systemd helpers). GitHub closed this issue because the PR body contained the phrase 'does not close #94', which it parsed as a closing keyword. T2-T4 continue; the final PR closes it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions