// // Keeps the "Hostname Domain" preference and Apple Container's own // configuration in step. // // A container's hostname is `.`, where the domain lives in // Apple Container's config.toml. There is no CLI setter for it, so the // preference is the front end for that file: reading it on launch, writing it // back when it changes, and following up with the two things a new domain also // needs -- a macOS resolver entry, and a restart of the services. // const { ContainerCLI } = require("./Interfaces/ContainerCLI"); const { Terminal } = require("./Interfaces/Terminal"); const systemConfig = require("./Utilities/SystemConfig"); const notify = require("./Utilities/Notify"); const prompt = require("./Utilities/Prompt"); const KEY = "apple_container.dns.domain"; // Nova reports config changes as they are typed, so settle before writing. const SETTLE_MS = 750; class Hostnames { constructor(context) { this.context = context; this.timer = null; // Set while the preference is being seeded from disk, so the resulting // change notification is not mistaken for the user editing it. this.seeding = false; this.disposables = new CompositeDisposable(); this.seed(); this.disposables.add(nova.config.onDidChange(KEY, (value) => this.onChange(value))); } dispose() { if (this.timer) { clearTimeout(this.timer); this.timer = null; } this.disposables.dispose(); } // The file is the source of truth: whatever it says is what the preference // shows when Nova starts. seed() { let domain = systemConfig.defaultDomain() || ""; if (nova.config.get(KEY, "string") === domain) { return; } this.seeding = true; nova.config.set(KEY, domain); this.seeding = false; } onChange(value) { if (this.seeding) { return; } if (this.timer) { clearTimeout(this.timer); } this.timer = setTimeout(() => { this.timer = null; this.apply(typeof value === "string" ? value.trim() : ""); }, SETTLE_MS); } async apply(value) { let domain = value.length > 0 ? value : null; if (domain === systemConfig.defaultDomain()) { return; } if (domain !== null && !/^[a-zA-Z0-9][a-zA-Z0-9.-]*$/.test(domain)) { notify.failure( "Invalid hostname domain", `“${domain}” is not a valid domain name.`, "apple-container-dns" ); return; } try { await systemConfig.setDefaultDomain(domain); } catch (error) { notify.failure("Unable to update the container configuration", error, "apple-container-dns"); return; } this.context.reload(); if (!domain) { notify.info( "Hostname domain cleared", "Containers created from now on will have unqualified hostnames.", "apple-container-dns" ); return; } let registered = await ContainerCLI.dnsDomains(); if (!registered.includes(domain)) { await Hostnames.register(domain); } await this.restart(); } async restart() { if ( !(await prompt.confirm( "Restart container services so the hostname domain takes effect?", "Restart" )) ) { return; } try { await ContainerCLI.systemStop(); await ContainerCLI.systemStart(); } catch (error) { notify.failure("Unable to restart container services", error, "apple-container-system"); } this.context.reload(); } // Registering a domain writes to /etc/resolver, so it needs an // administrator password: it runs in the terminal, where one can be given. static async register(domain) { if ( !(await prompt.confirm( `Let macOS resolve *.${domain} through Apple Container? This runs sudo in your terminal.`, "Register" )) ) { return; } await Terminal.run( `sudo ${Terminal.command([ContainerCLI.path(), "system", "dns", "create", domain])}` ); } } module.exports = { Hostnames, HOSTNAME_KEY: KEY };