Skip to content

feat(up): use Apple container DNS when domain registered, /etc/hosts fallback otherwise - #97

Merged
Cyb3rDudu merged 2 commits into
Mcrich23:mainfrom
Cyb3rDudu:feat/dns-domain-resolution
Jun 30, 2026
Merged

feat(up): use Apple container DNS when domain registered, /etc/hosts fallback otherwise#97
Cyb3rDudu merged 2 commits into
Mcrich23:mainfrom
Cyb3rDudu:feat/dns-domain-resolution

Conversation

@Cyb3rDudu

Copy link
Copy Markdown
Collaborator

Closes #81

When a project's sanitized name is already registered via container system dns create, containers are named <svc>.<domain> and get --dns-domain so the daemon's DNS server resolves peers. The /etc/hosts cross-patcher is skipped on this path. When the domain isn't registered, behavior is unchanged — legacy dashed names, /etc/hosts patching, and a one-time note with the sudo command.

ComposeDown tries all candidate name shapes (legacy dashed, dotted DNS, explicit container_name) so teardown works regardless of which mode created the containers.

Changes:

  • sanitizeDnsDomain / dnsListContainsDomain — pure helpers for deriving a DNS label from the project name and checking registration
  • DNS detection in run() — shells out to container system dns list, sets dnsAvailable
  • configService() — passes --name <svc>.<domain> + --dns-domain when DNS is available
  • crossPatchHostsForService — /etc/hosts fallback, gated on !dnsAvailable
  • stopOldStuffByName + ComposeDown multi-shape cleanup

Tested with container 0.12.3 on macOS 26. Two alpine services with name: dnstest — dotted names resolve via getent, /etc/hosts untouched, resolv.conf carries the domain. Auto-skips when no domain is pre-registered.

Static tests: 11 tests covering sanitization rules and dns list parsing.
Dynamic test: end-to-end DNS path verification (skips gracefully without registered domain).

…fallback otherwise

When the project's sanitized name is already registered via
container system dns create, name containers <svc>.<domain> and pass
--dns-domain so the daemon serves DNS for peers. The /etc/hosts
cross-patcher is skipped on this path. Otherwise keep the legacy dashed
names + patcher unchanged, with a notice pointing at the sudo command.

ComposeDown now tries every candidate name shape (legacy, dotted,
explicit) so it cleans up containers from either mode.

Static tests cover sanitizeDnsDomain and dnsListContainsDomain.
Dynamic test verifies dotted naming, peer resolution via getent,
and that /etc/hosts is left untouched when DNS is available.
@Cyb3rDudu
Cyb3rDudu force-pushed the feat/dns-domain-resolution branch from 9f2c630 to 1e42796 Compare May 29, 2026 08:40
@Mcrich23

Copy link
Copy Markdown
Owner

@Cyb3rDudu so sorry for the delay. Cleaning out the backlog right now, can you please resolve the conflicts with main?

Cyb3rDudu added a commit to Cyb3rDudu/Container-Compose that referenced this pull request Jun 30, 2026
Resolve ComposeUp.swift conflicts:
- getIPForRunningService: keep containerName(for:) helper, retain main's
  container-address comment (Mcrich23#100).
- waitUntilServiceIsRunning: adopt main's ActivityClock idle/maxWait
  signature (Mcrich23#112); keep containerName(for:) helper in place of the
  silent guard-let-projectName return.
- configService: keep BOTH Mcrich23#97's --dns-domain args and main's compose
  project/service labels (Mcrich23#110) — independent features at the same site.
- Failed-wait teardown: call stopExistingContainers([containerName]) (Mcrich23#97
  rename) instead of main's stopOldStuff([serviceName]); pass the actual
  launched name so dotted/explicit names are torn down correctly.

Builds clean; 63 static tests pass (DNS helpers, parsing, volumes, helpers).

Co-Authored-By: Claude <noreply@anthropic.com>
Resolve ComposeUp.swift conflicts:
- getIPForRunningService: keep containerName(for:) helper, retain main's
  container-address comment (Mcrich23#100).
- waitUntilServiceIsRunning: adopt main's ActivityClock idle/maxWait
  signature (Mcrich23#112); keep containerName(for:) helper in place of the
  silent guard-let-projectName return.
- configService: keep BOTH Mcrich23#97's --dns-domain args and main's compose
  project/service labels (Mcrich23#110) — independent features at the same site.
- Failed-wait teardown: call stopExistingContainers([containerName]) (Mcrich23#97
  rename) instead of main's stopOldStuff([serviceName]); pass the actual
  launched name so dotted/explicit names are torn down correctly.

Builds clean; 63 static tests pass (DNS helpers, parsing, volumes, helpers).
@Cyb3rDudu
Cyb3rDudu force-pushed the feat/dns-domain-resolution branch from 5e3c395 to 9261ed3 Compare June 30, 2026 15:50
@Cyb3rDudu
Cyb3rDudu merged commit 1f0641a into Mcrich23:main Jun 30, 2026
1 check passed
@Cyb3rDudu
Cyb3rDudu deleted the feat/dns-domain-resolution branch June 30, 2026 16:03
Cyb3rDudu added a commit to thromel/Container-Compose that referenced this pull request Jun 30, 2026
Resolve ComposeUp.swift conflicts after Mcrich23#97 (DNS + compose labels +
ActivityClock idle wait + stopOldStuff->stopExistingContainers rename)
landed on main:

- Wait function: keep Mcrich23#119's waitUntilServiceStarted name, ServiceStartState
  return, and .stopped exit-code validation; adopt Mcrich23#97's ActivityClock
  idle/maxWait signature and containerName(for:) lookup.
- Run path: keep Mcrich23#119's detach (exit-code guard) vs attached (fire-and-forget
  Task) structure, threading an ActivityClock through both branches so the
  idle wait sees pull progress; keep Mcrich23#119's startState switch (healthcheck +
  dependency gating).
- Stop: keep Mcrich23#97's stopExistingContainers (container-name strings); drop
  Mcrich23#119's stopOldStuff (no remaining callers after the merge took Mcrich23#97's call
  sites).
- Instance state: keep both Mcrich23#119's serviceStartStates/serviceHealth and Mcrich23#97's
  serviceContainerNames.
- Helpers: keep both Mcrich23#119's run-arg/service-selection helpers and Mcrich23#97's
  containerName(for:).

Builds clean; 104 static tests pass (dependencies, healthcheck, networks,
parsing, volumes, entrypoint, helpers).

Follow-up: Mcrich23#97's runTask teardown-on-wait-failure is intentionally omitted —
it assumes a single wrapping runTask, which Mcrich23#119's detach/attached split
doesn't have. Worth re-adding once the run path is unified.
adrum added a commit to adrum/Container-Compose that referenced this pull request Jul 4, 2026
…ice>

runForegroundUntilStopped rebuilt names as <project>-<service>, so
containers named via explicit container_name or the dotted
<service>.<dnsDomain> DNS convention (Mcrich23#97) were never found by the
stop/kill/monitor paths — ctrl-c exited container-compose but left
those containers running. Pass names resolved through
containerName(for:) instead of rebuilding them from service keys.

Also port the Mcrich23#126 idle-baseline technique into ForegroundWaitCpuTests
(successor of the deleted WaitForeverCpuTests) so the process-wide
getrusage flake fixed there isn't reintroduced: assert on incremental
CPU over an immediately-preceding idle window rather than an absolute
threshold.
Cyb3rDudu pushed a commit that referenced this pull request Jul 8, 2026
* fix(up): match docker compose foreground behavior

Foreground `up` (without --detach) parked in waitForever() with no signal
handling and never noticed when its containers stopped. Combined with the
ContainerCommands signal machinery leaving SIGINT/SIGTERM neutered (SIG_IGN)
after image-pull/network/build calls return, Ctrl-C did nothing and the
process hung even after `container compose down` from another shell.

Replace waitForever() with runForegroundUntilStopped(), matching docker compose:

- SIGINT/SIGTERM (observed via a DispatchSource stream, so they fire despite
  the SIG_IGN disposition) gracefully stop the project's containers, then exit
- a second signal force-kills the containers with SIGKILL, then exits
  ("press Ctrl+C again to force")
- a monitor task exits `up` once all services have run and then stopped —
  whether they exit on their own or are stopped via `container compose down`
  from another shell (it waits until each container is seen running first, so
  it never returns before startup completes)

Also retarget the #27 busy-loop regression test at the new wait function and
make it reliable in the full parallel suite: getrusage(RUSAGE_SELF) is
process-wide, so a 1s window (vs 200ms) lets a real busy-loop's full-core
~1,000,000 µs dominate the transient CPU noise from other parallel suites.

* fix(up): stop resolved container names on ctrl-c, not <project>-<service>

runForegroundUntilStopped rebuilt names as <project>-<service>, so
containers named via explicit container_name or the dotted
<service>.<dnsDomain> DNS convention (#97) were never found by the
stop/kill/monitor paths — ctrl-c exited container-compose but left
those containers running. Pass names resolved through
containerName(for:) instead of rebuilding them from service keys.

Also port the #126 idle-baseline technique into ForegroundWaitCpuTests
(successor of the deleted WaitForeverCpuTests) so the process-wide
getrusage flake fixed there isn't reintroduced: assert on incremental
CPU over an immediately-preceding idle window rather than an absolute
threshold.
Cyb3rDudu pushed a commit that referenced this pull request Jul 22, 2026
* Add shared ComposeProjectOptions for compose-file/service resolution

Introduce a reusable ComposeProjectOptions (compose-file location + decode,
project-name derivation, dependency-ordered service resolution, container-name
mapping) and a resolved ComposeProject value. New subcommands adopt this
instead of re-duplicating the boilerplate that up/down/build/logs each grew.

* test(project): cover ComposeProjectOptions file discovery, naming, and ordering

Covers composePath (--file precedence, discovery order, default),
projectName fallback to the derived cwd name, and orderedServices
topological sort/filtering.

* feat(project): resolve services to candidate container names, not one guess

ComposeProject mapped each service to a single container name
(container_name ?? <project>-<service>), which misses the dotted
<service>.<dnsDomain> names 'up' uses when the project's DNS domain is
registered (#97) — the same bug class fixed for ctrl-c in #127. Replace
ServiceTarget.containerName with candidateContainerNames (explicit name
first, then both conventions, mirroring ComposeDown.stopOldStuff) and
add existingContainer(using:) to resolve which candidate actually
exists, so every subcommand built on ComposeProject shares one correct
lookup instead of re-deriving names.

* feat(project): delegate service selection to Service.selectServices

orderedServices hand-rolled its own name filter, which ignored Compose
profiles gating and didn't expand depends_on — diverging from the
shared selection algorithm up/build/down adopted in #126. Route it
through Service.selectServices with the ComposeFileOptions active
profiles so commands built on ComposeProject select identically to the
existing ones: profile-eligible services plus dependencies by default,
requested services plus transitive dependencies (bypassing the profile
gate) when named explicitly.

* refactor(down): resolve project through shared ComposeProjectOptions

First consumer of ComposeProject: drop ComposeDown's own copies of
compose-file discovery, project-name derivation, topo-sort/selection,
and candidate-container-name construction in favor of the shared API.
Behavior is preserved (profile skip note, stop-all-candidates loop for
mixed naming-mode leftovers, extra_hosts cleanup) with one refinement:
explicit 'down <service>' now expands dependencies via
Service.selectServices instead of the topo-sort dependedBy side effect,
which only recorded each service's first visitor and so stopped an
explicit request's dependencies only when visit order happened to
align.

* refactor(build): resolve project through shared ComposeProjectOptions

Drop ComposeBuild's copies of compose-file discovery, project-name
derivation, and service selection in favor of ComposeProjectOptions.
Selection semantics are unchanged (Service.selectServices with active
profiles, then keep services with a build config); ordering improves
from dictionary order to topological, so dependencies build before
dependents.

* refactor(up): resolve project through shared ComposeProjectOptions

Drop ComposeUp's copies of compose-file discovery/decoding,
project-name derivation, topo-sort/selection, and the hand-built
stop-candidates list in favor of ComposeProjectOptions and
ComposeProject.candidateContainerNames. Behavior is unchanged: 'up'
keeps deciding the single creation-time name itself (explicit
container_name, dotted DNS name when the domain is registered, else
<project>-<service>) since that depends on runtime DNS availability,
and the pre-start cleanup still stops every name shape a previous run
might have used.

* refactor(project): simplify shared resolution layer and its consumers

Cleanups from a reuse/simplification/efficiency/altitude review of the
branch:

- down: resolve once instead of loadCompose()+resolve() (the compose
  file was read and YAML-decoded twice per run); drop the stop loop's
  remove parameter, which was always false
- up: consume resolve() instead of re-composing loadCompose +
  projectName + orderedServices + candidate names piecewise; this also
  gives up the same 'No such service' warning down/build already had
- move the 'No such service' warning from resolve() into
  orderedServices() so every selection path warns, not just resolve()
- hoist the envFilePath derivation duplicated verbatim in up and build
  onto ComposeProjectOptions
- move sanitizeDnsDomain from ComposeUp to ComposeProject: it is
  project naming policy, and the shared layer should not reach into a
  command for it
- delete dead code: ServiceTarget.existingContainer (no callers, and
  its first-match semantics is the exact behavior down's stop-every-
  candidate loop exists to avoid), ComposeProject.cwd (never read),
  ComposeError.invalidProjectName (no throwers left), and ComposeUp's
  vacuous projectName optionality (guards that could never fire once
  resolution guarantees a name)
- narrow supportedComposeFilenames to private

* fix(up): resolve healthcheck container name instead of assuming <project>-<service>

waitUntilServiceIsHealthy rebuilt the container name by hand, so
healthchecks for services created under an explicit container_name or
the dotted <service>.<dnsDomain> DNS-mode name (#97) exec'd against a
nonexistent container and burned every retry before failing. Use
containerName(for:), which reads the resolved name configService
records before any healthcheck runs — the same fix #127 applied to the
foreground stop paths.

* test: cover loadCompose missing-file throw and healthcheck name resolution

Closes the two meaningful coverage gaps over this branch's diff: the
composeFileNotFound path in ComposeProjectOptions.loadCompose, and a
dynamic regression test proving healthchecks exec against the resolved
container name — a service created under an explicit container_name
previously health-checked the nonexistent <project>-<service> and
failed up after exhausting retries.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Use Apple container's built-in DNS for inter-container resolution when domain is registered

2 participants