Skip to content

Commit fe72a4f

Browse files
committed
refactor(setup): move provisioning commands to infrastructure/provisioning (sprint 3)
1 parent 4ec0ea3 commit fe72a4f

3 files changed

Lines changed: 143 additions & 139 deletions

File tree

src/cli/commands/setup.ts

Lines changed: 9 additions & 138 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,23 @@ import { Listr } from 'listr2';
22
import { runRemoteCommand } from '../runner.js';
33
import { ui } from '../ui.js';
44
import type { RemoteExecutor } from '../../domain/remote/executor.js';
5-
import type { ShipnodeConfig, DatabaseConfig, NetworkDatabaseConfig, RedisConfig } from '../../shared/types.js';
5+
import type { ShipnodeConfig, NetworkDatabaseConfig } from '../../shared/types.js';
6+
import {
7+
buildDbInstallCommand,
8+
buildDbCreateCommand,
9+
buildDbProbeCommand,
10+
buildRedisInstallCommand,
11+
buildRedisConfigureCommand,
12+
buildRedisProbeCommand,
13+
} from '../../infrastructure/provisioning/commands.js';
614

715
export async function cmdSetup(cwd: string, options: { config?: string }): Promise<void> {
816
await runRemoteCommand(
917
cwd,
1018
async ({ config, executor }) => {
1119
ui.banner();
1220
ui.step(`Setting up ${config.ssh.user}@${config.ssh.host}`);
13-
1421
await buildTasks(executor, config).run();
15-
1622
ui.outro('Server ready — run: shipnode deploy');
1723
},
1824
{ configPath: options.config },
@@ -163,138 +169,3 @@ function buildTasks(executor: RemoteExecutor, config: ShipnodeConfig) {
163169
{ rendererOptions: { collapseErrors: false } },
164170
);
165171
}
166-
167-
168-
function sh(s: string): string {
169-
return s.replace(/'/g, "'\"'\"'");
170-
}
171-
172-
function shDq(s: string): string {
173-
return s.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\$/g, '\\$').replace(/`/g, '\\`');
174-
}
175-
176-
export function buildDbInstallCommand(db: NetworkDatabaseConfig): string {
177-
const preamble = 'SUDO=""; [ "$EUID" -ne 0 ] && SUDO="sudo"';
178-
179-
if (db.type === 'postgres') {
180-
return `${preamble}; $SUDO apt-get install -y postgresql postgresql-contrib && $SUDO systemctl enable postgresql && $SUDO systemctl start postgresql`;
181-
}
182-
183-
if (db.type === 'mysql') {
184-
return `${preamble}; $SUDO apt-get install -y mysql-server && $SUDO systemctl enable mysql && $SUDO systemctl start mysql`;
185-
}
186-
187-
if (db.type === 'mongodb') {
188-
const installCmd =
189-
'if ! command -v mongod &>/dev/null; then ' +
190-
'curl -fsSL https://www.mongodb.org/static/pgp/server-7.0.asc | $SUDO gpg --dearmor -o /usr/share/keyrings/mongodb-server-7.0.gpg; ' +
191-
'echo "deb [ arch=amd64,arm64 signed-by=/usr/share/keyrings/mongodb-server-7.0.gpg ] https://repo.mongodb.org/apt/ubuntu jammy/mongodb-org/7.0 multiverse" | $SUDO tee /etc/apt/sources.list.d/mongodb-org-7.0.list; ' +
192-
'$SUDO apt-get update -qq && $SUDO apt-get install -y mongodb-org; ' +
193-
'fi';
194-
return `${preamble}; ${installCmd} && $SUDO systemctl enable mongod && $SUDO systemctl start mongod`;
195-
}
196-
197-
throw new Error(`Unsupported database type: ${(db as DatabaseConfig).type}`);
198-
}
199-
200-
export function buildDbCreateCommand(db: NetworkDatabaseConfig): string {
201-
// preamble uses ';' so the conditional SUDO/PG_RUN assignments don't break
202-
// the '&&' chain when running as root (EUID=0 makes [ -ne 0 ] exit 1)
203-
const preamble = 'SUDO=""; [ "$EUID" -ne 0 ] && SUDO="sudo"; PG_RUN="runuser -u postgres --"; [ "$EUID" -ne 0 ] && PG_RUN="sudo -u postgres"';
204-
205-
if (db.type === 'postgres') {
206-
const userDq = shDq(db.user);
207-
const nameDq = shDq(db.name);
208-
const createUser = db.password
209-
? `$PG_RUN psql -c "CREATE USER \\"${userDq}\\" WITH PASSWORD '${shDq(db.password)}';" 2>/dev/null || true`
210-
: `$PG_RUN psql -c "CREATE USER \\"${userDq}\\";" 2>/dev/null || true`;
211-
const commands = [
212-
createUser,
213-
`$PG_RUN psql -tc "SELECT 1 FROM pg_database WHERE datname='${nameDq}'" | grep -q 1 || $PG_RUN createdb -O "${userDq}" "${nameDq}"`,
214-
];
215-
return `${preamble}; ${commands.join(' && ')}`;
216-
}
217-
218-
if (db.type === 'mysql') {
219-
const userDq = shDq(db.user);
220-
const nameDq = shDq(db.name);
221-
const pwClause = db.password ? `IDENTIFIED BY '${shDq(db.password)}'` : '';
222-
const commands = [
223-
`$SUDO mysql -e "CREATE USER IF NOT EXISTS '${userDq}'@'localhost' ${pwClause};"`,
224-
`$SUDO mysql -e "CREATE DATABASE IF NOT EXISTS \\\`${nameDq}\\\`;"`,
225-
`$SUDO mysql -e "GRANT ALL PRIVILEGES ON \\\`${nameDq}\\\`.* TO '${userDq}'@'localhost';"`,
226-
`$SUDO mysql -e "FLUSH PRIVILEGES;"`,
227-
];
228-
return `${preamble}; ${commands.join(' && ')}`;
229-
}
230-
231-
if (db.type === 'mongodb') {
232-
if (!db.password) return 'true';
233-
const nameDq = shDq(db.name);
234-
const userDq = shDq(db.user);
235-
const pwdDq = shDq(db.password);
236-
const commands = [
237-
`if ! grep -q "authorization: enabled" /etc/mongod.conf 2>/dev/null; then echo -e "\\nsecurity:\\n authorization: enabled" | $SUDO tee -a /etc/mongod.conf > /dev/null && $SUDO systemctl restart mongod; fi`,
238-
`mongosh "${nameDq}" --eval "db.createUser({user:'${userDq}',pwd:'${pwdDq}',roles:[{role:'readWrite',db:'${nameDq}'}]})" 2>/dev/null || true`,
239-
];
240-
return `${preamble}; ${commands.join(' && ')}`;
241-
}
242-
243-
throw new Error(`Unsupported database type: ${(db as DatabaseConfig).type}`);
244-
}
245-
246-
export function buildDbProbeCommand(db: NetworkDatabaseConfig): string | null {
247-
if (!db.password) return null;
248-
249-
if (db.type === 'postgres') {
250-
return `PGPASSWORD='${sh(db.password)}' psql -h localhost -U '${sh(db.user)}' -d '${sh(db.name)}' -c "SELECT 1" > /dev/null`;
251-
}
252-
253-
if (db.type === 'mysql') {
254-
return `MYSQL_PWD='${sh(db.password)}' mysql -h 127.0.0.1 -u '${sh(db.user)}' '${sh(db.name)}' -e "SELECT 1" > /dev/null`;
255-
}
256-
257-
if (db.type === 'mongodb') {
258-
return `mongosh --host localhost '${sh(db.name)}' -u '${sh(db.user)}' -p '${sh(db.password)}' --authenticationDatabase '${sh(db.name)}' --eval "db.runCommand({ping:1})" --quiet > /dev/null`;
259-
}
260-
261-
throw new Error(`Unsupported database type: ${(db as DatabaseConfig).type}`);
262-
}
263-
264-
export function buildDbSetupCommand(db: DatabaseConfig): string {
265-
if (db.type === 'sqlite') {
266-
throw new Error(`buildDbSetupCommand called for sqlite — caller should guard against this`);
267-
}
268-
const install = buildDbInstallCommand(db);
269-
const create = buildDbCreateCommand(db);
270-
const probe = buildDbProbeCommand(db);
271-
return [install, create, ...(probe ? [probe] : [])].join(' && ');
272-
}
273-
274-
export function buildRedisInstallCommand(_redis: RedisConfig): string {
275-
const preamble = 'SUDO=""; [ "$EUID" -ne 0 ] && SUDO="sudo"';
276-
return `${preamble}; $SUDO apt-get install -y redis-server && $SUDO systemctl enable redis-server && $SUDO systemctl start redis-server`;
277-
}
278-
279-
export function buildRedisConfigureCommand(redis: RedisConfig): string {
280-
const preamble = 'SUDO=""; [ "$EUID" -ne 0 ] && SUDO="sudo"';
281-
if (!redis.password) return 'true';
282-
const parts = [
283-
`$SUDO sed -i '/^#* *requirepass/d' /etc/redis/redis.conf`,
284-
`echo "requirepass ${shDq(redis.password)}" | $SUDO tee -a /etc/redis/redis.conf > /dev/null`,
285-
'$SUDO systemctl restart redis-server',
286-
];
287-
return `${preamble}; ${parts.join(' && ')}`;
288-
}
289-
290-
export function buildRedisProbeCommand(redis: RedisConfig): string | null {
291-
if (!redis.password) return null;
292-
return `redis-cli -h localhost -p ${redis.port} -a '${sh(redis.password)}' PING > /dev/null 2>&1`;
293-
}
294-
295-
export function buildRedisSetupCommand(redis: RedisConfig): string {
296-
const install = buildRedisInstallCommand(redis);
297-
const configure = buildRedisConfigureCommand(redis);
298-
const probe = buildRedisProbeCommand(redis);
299-
return [install, configure, ...(probe ? [probe] : [])].join(' && ');
300-
}
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import type { NetworkDatabaseConfig, RedisConfig, DatabaseConfig } from '../../shared/types.js';
2+
3+
function sh(s: string): string {
4+
return s.replace(/'/g, "'\"'\"'");
5+
}
6+
7+
function shDq(s: string): string {
8+
return s.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\$/g, '\\$').replace(/`/g, '\\`');
9+
}
10+
11+
export function buildDbInstallCommand(db: NetworkDatabaseConfig): string {
12+
const preamble = 'SUDO=""; [ "$EUID" -ne 0 ] && SUDO="sudo"';
13+
14+
if (db.type === 'postgres') {
15+
return `${preamble}; $SUDO apt-get install -y postgresql postgresql-contrib && $SUDO systemctl enable postgresql && $SUDO systemctl start postgresql`;
16+
}
17+
18+
if (db.type === 'mysql') {
19+
return `${preamble}; $SUDO apt-get install -y mysql-server && $SUDO systemctl enable mysql && $SUDO systemctl start mysql`;
20+
}
21+
22+
if (db.type === 'mongodb') {
23+
const installCmd =
24+
'if ! command -v mongod &>/dev/null; then ' +
25+
'curl -fsSL https://www.mongodb.org/static/pgp/server-7.0.asc | $SUDO gpg --dearmor -o /usr/share/keyrings/mongodb-server-7.0.gpg; ' +
26+
'echo "deb [ arch=amd64,arm64 signed-by=/usr/share/keyrings/mongodb-server-7.0.gpg ] https://repo.mongodb.org/apt/ubuntu jammy/mongodb-org/7.0 multiverse" | $SUDO tee /etc/apt/sources.list.d/mongodb-org-7.0.list; ' +
27+
'$SUDO apt-get update -qq && $SUDO apt-get install -y mongodb-org; ' +
28+
'fi';
29+
return `${preamble}; ${installCmd} && $SUDO systemctl enable mongod && $SUDO systemctl start mongod`;
30+
}
31+
32+
throw new Error(`Unsupported database type: ${(db as DatabaseConfig).type}`);
33+
}
34+
35+
export function buildDbCreateCommand(db: NetworkDatabaseConfig): string {
36+
const preamble = 'SUDO=""; [ "$EUID" -ne 0 ] && SUDO="sudo"; PG_RUN="runuser -u postgres --"; [ "$EUID" -ne 0 ] && PG_RUN="sudo -u postgres"';
37+
38+
if (db.type === 'postgres') {
39+
const userDq = shDq(db.user);
40+
const nameDq = shDq(db.name);
41+
const createUser = db.password
42+
? `$PG_RUN psql -c "CREATE USER \\"${userDq}\\" WITH PASSWORD '${shDq(db.password)}';" 2>/dev/null || true`
43+
: `$PG_RUN psql -c "CREATE USER \\"${userDq}\\";" 2>/dev/null || true`;
44+
const commands = [
45+
createUser,
46+
`$PG_RUN psql -tc "SELECT 1 FROM pg_database WHERE datname='${nameDq}'" | grep -q 1 || $PG_RUN createdb -O "${userDq}" "${nameDq}"`,
47+
];
48+
return `${preamble}; ${commands.join(' && ')}`;
49+
}
50+
51+
if (db.type === 'mysql') {
52+
const userDq = shDq(db.user);
53+
const nameDq = shDq(db.name);
54+
const pwClause = db.password ? `IDENTIFIED BY '${shDq(db.password)}'` : '';
55+
const commands = [
56+
`$SUDO mysql -e "CREATE USER IF NOT EXISTS '${userDq}'@'localhost' ${pwClause};"`,
57+
`$SUDO mysql -e "CREATE DATABASE IF NOT EXISTS \\\`${nameDq}\\\`;"`,
58+
`$SUDO mysql -e "GRANT ALL PRIVILEGES ON \\\`${nameDq}\\\`.* TO '${userDq}'@'localhost';"`,
59+
`$SUDO mysql -e "FLUSH PRIVILEGES;"`,
60+
];
61+
return `${preamble}; ${commands.join(' && ')}`;
62+
}
63+
64+
if (db.type === 'mongodb') {
65+
if (!db.password) return 'true';
66+
const nameDq = shDq(db.name);
67+
const userDq = shDq(db.user);
68+
const pwdDq = shDq(db.password);
69+
const commands = [
70+
`if ! grep -q "authorization: enabled" /etc/mongod.conf 2>/dev/null; then echo -e "\\nsecurity:\\n authorization: enabled" | $SUDO tee -a /etc/mongod.conf > /dev/null && $SUDO systemctl restart mongod; fi`,
71+
`mongosh "${nameDq}" --eval "db.createUser({user:'${userDq}',pwd:'${pwdDq}',roles:[{role:'readWrite',db:'${nameDq}'}]})" 2>/dev/null || true`,
72+
];
73+
return `${preamble}; ${commands.join(' && ')}`;
74+
}
75+
76+
throw new Error(`Unsupported database type: ${(db as DatabaseConfig).type}`);
77+
}
78+
79+
export function buildDbProbeCommand(db: NetworkDatabaseConfig): string | null {
80+
if (!db.password) return null;
81+
82+
if (db.type === 'postgres') {
83+
return `PGPASSWORD='${sh(db.password)}' psql -h localhost -U '${sh(db.user)}' -d '${sh(db.name)}' -c "SELECT 1" > /dev/null`;
84+
}
85+
86+
if (db.type === 'mysql') {
87+
return `MYSQL_PWD='${sh(db.password)}' mysql -h 127.0.0.1 -u '${sh(db.user)}' '${sh(db.name)}' -e "SELECT 1" > /dev/null`;
88+
}
89+
90+
if (db.type === 'mongodb') {
91+
return `mongosh --host localhost '${sh(db.name)}' -u '${sh(db.user)}' -p '${sh(db.password)}' --authenticationDatabase '${sh(db.name)}' --eval "db.runCommand({ping:1})" --quiet > /dev/null`;
92+
}
93+
94+
throw new Error(`Unsupported database type: ${(db as DatabaseConfig).type}`);
95+
}
96+
97+
export function buildDbSetupCommand(db: DatabaseConfig): string {
98+
if (db.type === 'sqlite') {
99+
throw new Error(`buildDbSetupCommand called for sqlite — caller should guard against this`);
100+
}
101+
const install = buildDbInstallCommand(db);
102+
const create = buildDbCreateCommand(db);
103+
const probe = buildDbProbeCommand(db);
104+
return [install, create, ...(probe ? [probe] : [])].join(' && ');
105+
}
106+
107+
export function buildRedisInstallCommand(_redis: RedisConfig): string {
108+
const preamble = 'SUDO=""; [ "$EUID" -ne 0 ] && SUDO="sudo"';
109+
return `${preamble}; $SUDO apt-get install -y redis-server && $SUDO systemctl enable redis-server && $SUDO systemctl start redis-server`;
110+
}
111+
112+
export function buildRedisConfigureCommand(redis: RedisConfig): string {
113+
const preamble = 'SUDO=""; [ "$EUID" -ne 0 ] && SUDO="sudo"';
114+
if (!redis.password) return 'true';
115+
const parts = [
116+
`$SUDO sed -i '/^#* *requirepass/d' /etc/redis/redis.conf`,
117+
`echo "requirepass ${shDq(redis.password)}" | $SUDO tee -a /etc/redis/redis.conf > /dev/null`,
118+
'$SUDO systemctl restart redis-server',
119+
];
120+
return `${preamble}; ${parts.join(' && ')}`;
121+
}
122+
123+
export function buildRedisProbeCommand(redis: RedisConfig): string | null {
124+
if (!redis.password) return null;
125+
return `redis-cli -h localhost -p ${redis.port} -a '${sh(redis.password)}' PING > /dev/null 2>&1`;
126+
}
127+
128+
export function buildRedisSetupCommand(redis: RedisConfig): string {
129+
const install = buildRedisInstallCommand(redis);
130+
const configure = buildRedisConfigureCommand(redis);
131+
const probe = buildRedisProbeCommand(redis);
132+
return [install, configure, ...(probe ? [probe] : [])].join(' && ');
133+
}

tests/unit/setup.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
buildRedisConfigureCommand,
99
buildRedisProbeCommand,
1010
buildRedisSetupCommand,
11-
} from '../../src/cli/commands/setup.js';
11+
} from '../../src/infrastructure/provisioning/commands.js';
1212

1313
describe('buildDbInstallCommand', () => {
1414
it('postgres: installs package and starts service', () => {

0 commit comments

Comments
 (0)