suthep 0.1.0 ā 0.2.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +172 -71
- package/dist/commands/deploy.js +251 -37
- package/dist/commands/deploy.js.map +1 -1
- package/dist/commands/down.js +179 -0
- package/dist/commands/down.js.map +1 -0
- package/dist/commands/redeploy.js +59 -0
- package/dist/commands/redeploy.js.map +1 -0
- package/dist/commands/up.js +213 -0
- package/dist/commands/up.js.map +1 -0
- package/dist/index.js +36 -1
- package/dist/index.js.map +1 -1
- package/dist/utils/certbot.js +40 -3
- package/dist/utils/certbot.js.map +1 -1
- package/dist/utils/config-loader.js +30 -0
- package/dist/utils/config-loader.js.map +1 -1
- package/dist/utils/deployment.js +49 -16
- package/dist/utils/deployment.js.map +1 -1
- package/dist/utils/docker.js +396 -25
- package/dist/utils/docker.js.map +1 -1
- package/dist/utils/nginx.js +167 -8
- package/dist/utils/nginx.js.map +1 -1
- package/docs/README.md +25 -49
- package/docs/english/01-introduction.md +84 -0
- package/docs/english/02-installation.md +200 -0
- package/docs/english/03-quick-start.md +256 -0
- package/docs/english/04-configuration.md +358 -0
- package/docs/english/05-commands.md +363 -0
- package/docs/english/06-examples.md +456 -0
- package/docs/english/07-troubleshooting.md +417 -0
- package/docs/english/08-advanced.md +411 -0
- package/docs/english/README.md +48 -0
- package/docs/thai/01-introduction.md +84 -0
- package/docs/thai/02-installation.md +200 -0
- package/docs/thai/03-quick-start.md +256 -0
- package/docs/thai/04-configuration.md +358 -0
- package/docs/thai/05-commands.md +363 -0
- package/docs/thai/06-examples.md +456 -0
- package/docs/thai/07-troubleshooting.md +417 -0
- package/docs/thai/08-advanced.md +411 -0
- package/docs/thai/README.md +48 -0
- package/example/README.md +286 -53
- package/example/suthep-complete.yml +103 -0
- package/example/suthep-docker-only.yml +71 -0
- package/example/suthep-no-docker.yml +51 -0
- package/example/suthep-path-routing.yml +62 -0
- package/example/suthep.example.yml +89 -0
- package/package.json +1 -1
- package/src/commands/deploy.ts +322 -50
- package/src/commands/down.ts +240 -0
- package/src/commands/redeploy.ts +78 -0
- package/src/commands/up.ts +271 -0
- package/src/index.ts +62 -1
- package/src/types/config.ts +25 -24
- package/src/utils/certbot.ts +68 -6
- package/src/utils/config-loader.ts +40 -0
- package/src/utils/deployment.ts +61 -36
- package/src/utils/docker.ts +634 -30
- package/src/utils/nginx.ts +187 -4
- package/suthep-0.1.0-beta.1.tgz +0 -0
- package/suthep-0.1.1.tgz +0 -0
- package/suthep.example.yml +34 -0
- package/suthep.yml +39 -0
- package/test +0 -0
- package/docs/api-reference.md +0 -545
- package/docs/architecture.md +0 -367
- package/docs/commands.md +0 -273
- package/docs/configuration.md +0 -347
- package/docs/examples.md +0 -537
- package/docs/getting-started.md +0 -197
- package/docs/troubleshooting.md +0 -441
- package/example/docker-compose.yml +0 -72
- package/example/suthep.yml +0 -31
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
import fs from 'fs-extra'
|
|
3
|
+
import type { ServiceConfig } from '../types/config'
|
|
4
|
+
import { loadConfig } from '../utils/config-loader'
|
|
5
|
+
import { isContainerRunning, removeDockerContainer, stopDockerContainer } from '../utils/docker'
|
|
6
|
+
import {
|
|
7
|
+
disableSite,
|
|
8
|
+
enableSite,
|
|
9
|
+
generateMultiServiceNginxConfig,
|
|
10
|
+
generateNginxConfig,
|
|
11
|
+
reloadNginx,
|
|
12
|
+
writeNginxConfig,
|
|
13
|
+
} from '../utils/nginx'
|
|
14
|
+
|
|
15
|
+
interface DownOptions {
|
|
16
|
+
file: string
|
|
17
|
+
all: boolean
|
|
18
|
+
serviceName?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function downCommand(options: DownOptions): Promise<void> {
|
|
22
|
+
console.log(chalk.blue.bold('\nš Bringing Down Services\n'))
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
// Load configuration
|
|
26
|
+
if (!(await fs.pathExists(options.file))) {
|
|
27
|
+
throw new Error(`Configuration file not found: ${options.file}`)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
console.log(chalk.cyan(`š Loading configuration from ${options.file}...`))
|
|
31
|
+
const config = await loadConfig(options.file)
|
|
32
|
+
|
|
33
|
+
console.log(chalk.green(`ā
Configuration loaded for project: ${config.project.name}`))
|
|
34
|
+
|
|
35
|
+
// Determine which services to bring down
|
|
36
|
+
let servicesToDown: ServiceConfig[] = []
|
|
37
|
+
|
|
38
|
+
if (options.all) {
|
|
39
|
+
servicesToDown = config.services
|
|
40
|
+
console.log(
|
|
41
|
+
chalk.cyan(
|
|
42
|
+
`š Bringing down all services: ${servicesToDown.map((s) => s.name).join(', ')}\n`
|
|
43
|
+
)
|
|
44
|
+
)
|
|
45
|
+
} else if (options.serviceName) {
|
|
46
|
+
const service = config.services.find((s) => s.name === options.serviceName)
|
|
47
|
+
if (!service) {
|
|
48
|
+
throw new Error(
|
|
49
|
+
`Service "${
|
|
50
|
+
options.serviceName
|
|
51
|
+
}" not found in configuration. Available services: ${config.services
|
|
52
|
+
.map((s) => s.name)
|
|
53
|
+
.join(', ')}`
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
servicesToDown = [service]
|
|
57
|
+
console.log(chalk.cyan(`š Bringing down service: ${options.serviceName}\n`))
|
|
58
|
+
} else {
|
|
59
|
+
throw new Error('Either specify a service name or use --all flag')
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Group services by domain for nginx config management
|
|
63
|
+
const domainToServices = new Map<string, ServiceConfig[]>()
|
|
64
|
+
const allDomains = new Set<string>()
|
|
65
|
+
|
|
66
|
+
for (const service of servicesToDown) {
|
|
67
|
+
for (const domain of service.domains) {
|
|
68
|
+
allDomains.add(domain)
|
|
69
|
+
if (!domainToServices.has(domain)) {
|
|
70
|
+
domainToServices.set(domain, [])
|
|
71
|
+
}
|
|
72
|
+
domainToServices.get(domain)!.push(service)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Stop and remove Docker containers
|
|
77
|
+
for (const service of servicesToDown) {
|
|
78
|
+
if (service.docker) {
|
|
79
|
+
console.log(chalk.cyan(`\nš³ Stopping Docker container for service: ${service.name}`))
|
|
80
|
+
try {
|
|
81
|
+
const containerName = service.docker.container
|
|
82
|
+
|
|
83
|
+
// Stop container
|
|
84
|
+
try {
|
|
85
|
+
await stopDockerContainer(containerName)
|
|
86
|
+
console.log(chalk.green(` ā
Stopped container: ${containerName}`))
|
|
87
|
+
} catch (error: any) {
|
|
88
|
+
const errorMessage = error?.message || String(error) || 'Unknown error'
|
|
89
|
+
if (
|
|
90
|
+
errorMessage.toLowerCase().includes('no such container') ||
|
|
91
|
+
errorMessage.toLowerCase().includes('container not found')
|
|
92
|
+
) {
|
|
93
|
+
console.log(
|
|
94
|
+
chalk.yellow(` ā ļø Container ${containerName} not found (already stopped)`)
|
|
95
|
+
)
|
|
96
|
+
} else {
|
|
97
|
+
throw error
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Remove container
|
|
102
|
+
try {
|
|
103
|
+
await removeDockerContainer(containerName)
|
|
104
|
+
console.log(chalk.green(` ā
Removed container: ${containerName}`))
|
|
105
|
+
} catch (error: any) {
|
|
106
|
+
const errorMessage = error?.message || String(error) || 'Unknown error'
|
|
107
|
+
if (
|
|
108
|
+
errorMessage.toLowerCase().includes('no such container') ||
|
|
109
|
+
errorMessage.toLowerCase().includes('container not found')
|
|
110
|
+
) {
|
|
111
|
+
console.log(
|
|
112
|
+
chalk.yellow(` ā ļø Container ${containerName} not found (already removed)`)
|
|
113
|
+
)
|
|
114
|
+
} else {
|
|
115
|
+
throw error
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
} catch (error) {
|
|
119
|
+
console.error(
|
|
120
|
+
chalk.red(` ā Failed to stop/remove container for service ${service.name}:`),
|
|
121
|
+
error instanceof Error ? error.message : error
|
|
122
|
+
)
|
|
123
|
+
throw error
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Handle Nginx configs by domain
|
|
129
|
+
// Check which services remain active for each domain
|
|
130
|
+
if (allDomains.size > 0) {
|
|
131
|
+
console.log(chalk.cyan(`\nāļø Updating Nginx configurations...`))
|
|
132
|
+
|
|
133
|
+
// Create a set of service names being brought down for quick lookup
|
|
134
|
+
const servicesBeingDowned = new Set(servicesToDown.map((s) => s.name))
|
|
135
|
+
|
|
136
|
+
// For each domain, find all services that use it (from all config services)
|
|
137
|
+
// and determine which ones remain active
|
|
138
|
+
for (const domain of allDomains) {
|
|
139
|
+
const configName = domain.replace(/\./g, '_')
|
|
140
|
+
|
|
141
|
+
// Find all services that use this domain (from entire config)
|
|
142
|
+
const allServicesForDomain = config.services.filter((s) => s.domains.includes(domain))
|
|
143
|
+
|
|
144
|
+
// Filter out services being brought down
|
|
145
|
+
const remainingServices = allServicesForDomain.filter(
|
|
146
|
+
(s) => !servicesBeingDowned.has(s.name)
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
// Check if remaining services are actually running (have active containers)
|
|
150
|
+
const activeServices: ServiceConfig[] = []
|
|
151
|
+
for (const service of remainingServices) {
|
|
152
|
+
if (service.docker) {
|
|
153
|
+
const isRunning = await isContainerRunning(service.docker.container)
|
|
154
|
+
if (isRunning) {
|
|
155
|
+
activeServices.push(service)
|
|
156
|
+
}
|
|
157
|
+
} else {
|
|
158
|
+
// Service without docker - assume it's running if not being brought down
|
|
159
|
+
activeServices.push(service)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
if (activeServices.length === 0) {
|
|
165
|
+
// No active services on this domain, disable the config
|
|
166
|
+
console.log(
|
|
167
|
+
chalk.cyan(` š No active services on ${domain}, disabling Nginx config...`)
|
|
168
|
+
)
|
|
169
|
+
await disableSite(configName, config.nginx.configPath)
|
|
170
|
+
console.log(chalk.green(` ā
Disabled Nginx config for ${domain}`))
|
|
171
|
+
} else {
|
|
172
|
+
// Regenerate config with remaining active services
|
|
173
|
+
console.log(
|
|
174
|
+
chalk.cyan(
|
|
175
|
+
` š Regenerating Nginx config for ${domain} with ${
|
|
176
|
+
activeServices.length
|
|
177
|
+
} active service(s): ${activeServices.map((s) => s.name).join(', ')}`
|
|
178
|
+
)
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
// Generate config for remaining services
|
|
182
|
+
let nginxConfigContent: string
|
|
183
|
+
if (activeServices.length === 1) {
|
|
184
|
+
nginxConfigContent = generateNginxConfig(activeServices[0], false)
|
|
185
|
+
} else {
|
|
186
|
+
nginxConfigContent = generateMultiServiceNginxConfig(activeServices, domain, false)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Check if HTTPS is enabled (check if certificate exists)
|
|
190
|
+
const { certificateExists } = await import('../utils/certbot')
|
|
191
|
+
const hasHttps = await certificateExists(domain)
|
|
192
|
+
if (hasHttps) {
|
|
193
|
+
// Regenerate with HTTPS
|
|
194
|
+
if (activeServices.length === 1) {
|
|
195
|
+
nginxConfigContent = generateNginxConfig(activeServices[0], true)
|
|
196
|
+
} else {
|
|
197
|
+
nginxConfigContent = generateMultiServiceNginxConfig(activeServices, domain, true)
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
await writeNginxConfig(configName, config.nginx.configPath, nginxConfigContent)
|
|
202
|
+
await enableSite(configName, config.nginx.configPath)
|
|
203
|
+
console.log(
|
|
204
|
+
chalk.green(
|
|
205
|
+
` ā
Updated Nginx config for ${domain} (${activeServices.length} service(s) active)`
|
|
206
|
+
)
|
|
207
|
+
)
|
|
208
|
+
}
|
|
209
|
+
} catch (error) {
|
|
210
|
+
console.error(
|
|
211
|
+
chalk.red(` ā Failed to update Nginx config for ${domain}:`),
|
|
212
|
+
error instanceof Error ? error.message : error
|
|
213
|
+
)
|
|
214
|
+
throw error
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Reload Nginx to apply changes
|
|
219
|
+
console.log(chalk.cyan(`\nš Reloading Nginx...`))
|
|
220
|
+
try {
|
|
221
|
+
await reloadNginx(config.nginx.reloadCommand)
|
|
222
|
+
console.log(chalk.green(` ā
Nginx reloaded`))
|
|
223
|
+
} catch (error) {
|
|
224
|
+
console.error(
|
|
225
|
+
chalk.red(` ā Failed to reload Nginx:`),
|
|
226
|
+
error instanceof Error ? error.message : error
|
|
227
|
+
)
|
|
228
|
+
throw error
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
console.log(chalk.green.bold('\nā
Services brought down successfully!\n'))
|
|
233
|
+
} catch (error) {
|
|
234
|
+
console.error(
|
|
235
|
+
chalk.red('\nā Failed to bring down services:'),
|
|
236
|
+
error instanceof Error ? error.message : error
|
|
237
|
+
)
|
|
238
|
+
process.exit(1)
|
|
239
|
+
}
|
|
240
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
import fs from 'fs-extra'
|
|
3
|
+
import type { ServiceConfig } from '../types/config'
|
|
4
|
+
import { loadConfig } from '../utils/config-loader'
|
|
5
|
+
import { deployCommand } from './deploy'
|
|
6
|
+
import { downCommand } from './down'
|
|
7
|
+
|
|
8
|
+
interface RedeployOptions {
|
|
9
|
+
file: string
|
|
10
|
+
all: boolean
|
|
11
|
+
serviceName?: string
|
|
12
|
+
https: boolean
|
|
13
|
+
nginx: boolean
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function redeployCommand(options: RedeployOptions): Promise<void> {
|
|
17
|
+
console.log(chalk.blue.bold('\nš Redeploying Services\n'))
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
// Load configuration to validate service names
|
|
21
|
+
if (!(await fs.pathExists(options.file))) {
|
|
22
|
+
throw new Error(`Configuration file not found: ${options.file}`)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const config = await loadConfig(options.file)
|
|
26
|
+
|
|
27
|
+
// Determine which services to redeploy
|
|
28
|
+
let servicesToRedeploy: ServiceConfig[] = []
|
|
29
|
+
|
|
30
|
+
if (options.all) {
|
|
31
|
+
servicesToRedeploy = config.services
|
|
32
|
+
console.log(
|
|
33
|
+
chalk.cyan(
|
|
34
|
+
`š Redeploying all services: ${servicesToRedeploy.map((s) => s.name).join(', ')}\n`
|
|
35
|
+
)
|
|
36
|
+
)
|
|
37
|
+
} else if (options.serviceName) {
|
|
38
|
+
const service = config.services.find((s) => s.name === options.serviceName)
|
|
39
|
+
if (!service) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
`Service "${
|
|
42
|
+
options.serviceName
|
|
43
|
+
}" not found in configuration. Available services: ${config.services
|
|
44
|
+
.map((s) => s.name)
|
|
45
|
+
.join(', ')}`
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
servicesToRedeploy = [service]
|
|
49
|
+
console.log(chalk.cyan(`š Redeploying service: ${options.serviceName}\n`))
|
|
50
|
+
} else {
|
|
51
|
+
throw new Error('Either specify a service name or use --all flag')
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Step 1: Bring down the services
|
|
55
|
+
console.log(chalk.yellow('Step 1: Bringing down services...\n'))
|
|
56
|
+
await downCommand({
|
|
57
|
+
file: options.file,
|
|
58
|
+
all: options.all,
|
|
59
|
+
serviceName: options.serviceName,
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
// Step 2: Deploy the services (which will bring them back up)
|
|
63
|
+
console.log(chalk.yellow('\nStep 2: Deploying services...\n'))
|
|
64
|
+
await deployCommand({
|
|
65
|
+
file: options.file,
|
|
66
|
+
https: options.https,
|
|
67
|
+
nginx: options.nginx,
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
console.log(chalk.green.bold('\nā
Services redeployed successfully!\n'))
|
|
71
|
+
} catch (error) {
|
|
72
|
+
console.error(
|
|
73
|
+
chalk.red('\nā Failed to redeploy services:'),
|
|
74
|
+
error instanceof Error ? error.message : error
|
|
75
|
+
)
|
|
76
|
+
process.exit(1)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
import fs from 'fs-extra'
|
|
3
|
+
import type { ServiceConfig } from '../types/config'
|
|
4
|
+
import { loadConfig } from '../utils/config-loader'
|
|
5
|
+
import { waitForService } from '../utils/deployment'
|
|
6
|
+
import { isContainerRunning, startDockerContainer } from '../utils/docker'
|
|
7
|
+
import {
|
|
8
|
+
enableSite,
|
|
9
|
+
generateMultiServiceNginxConfig,
|
|
10
|
+
generateNginxConfig,
|
|
11
|
+
reloadNginx,
|
|
12
|
+
writeNginxConfig,
|
|
13
|
+
} from '../utils/nginx'
|
|
14
|
+
|
|
15
|
+
interface UpOptions {
|
|
16
|
+
file: string
|
|
17
|
+
all: boolean
|
|
18
|
+
serviceName?: string
|
|
19
|
+
https: boolean
|
|
20
|
+
nginx: boolean
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function upCommand(options: UpOptions): Promise<void> {
|
|
24
|
+
console.log(chalk.blue.bold('\nš Bringing Up Services\n'))
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
// Load configuration
|
|
28
|
+
if (!(await fs.pathExists(options.file))) {
|
|
29
|
+
throw new Error(`Configuration file not found: ${options.file}`)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
console.log(chalk.cyan(`š Loading configuration from ${options.file}...`))
|
|
33
|
+
const config = await loadConfig(options.file)
|
|
34
|
+
|
|
35
|
+
console.log(chalk.green(`ā
Configuration loaded for project: ${config.project.name}`))
|
|
36
|
+
|
|
37
|
+
// Determine which services to bring up
|
|
38
|
+
let servicesToUp: ServiceConfig[] = []
|
|
39
|
+
|
|
40
|
+
if (options.all) {
|
|
41
|
+
servicesToUp = config.services
|
|
42
|
+
console.log(
|
|
43
|
+
chalk.cyan(`š Bringing up all services: ${servicesToUp.map((s) => s.name).join(', ')}\n`)
|
|
44
|
+
)
|
|
45
|
+
} else if (options.serviceName) {
|
|
46
|
+
const service = config.services.find((s) => s.name === options.serviceName)
|
|
47
|
+
if (!service) {
|
|
48
|
+
throw new Error(
|
|
49
|
+
`Service "${
|
|
50
|
+
options.serviceName
|
|
51
|
+
}" not found in configuration. Available services: ${config.services
|
|
52
|
+
.map((s) => s.name)
|
|
53
|
+
.join(', ')}`
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
servicesToUp = [service]
|
|
57
|
+
console.log(chalk.cyan(`š Bringing up service: ${options.serviceName}\n`))
|
|
58
|
+
} else {
|
|
59
|
+
throw new Error('Either specify a service name or use --all flag')
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Group services by domain for nginx config management
|
|
63
|
+
const domainToServices = new Map<string, ServiceConfig[]>()
|
|
64
|
+
const allDomains = new Set<string>()
|
|
65
|
+
|
|
66
|
+
for (const service of servicesToUp) {
|
|
67
|
+
for (const domain of service.domains) {
|
|
68
|
+
allDomains.add(domain)
|
|
69
|
+
if (!domainToServices.has(domain)) {
|
|
70
|
+
domainToServices.set(domain, [])
|
|
71
|
+
}
|
|
72
|
+
domainToServices.get(domain)!.push(service)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Start Docker containers
|
|
77
|
+
for (const service of servicesToUp) {
|
|
78
|
+
if (service.docker) {
|
|
79
|
+
console.log(chalk.cyan(`\nš³ Starting Docker container for service: ${service.name}`))
|
|
80
|
+
try {
|
|
81
|
+
await startDockerContainer(service)
|
|
82
|
+
console.log(chalk.green(` ā
Container started for service: ${service.name}`))
|
|
83
|
+
} catch (error) {
|
|
84
|
+
console.error(
|
|
85
|
+
chalk.red(` ā Failed to start container for service ${service.name}:`),
|
|
86
|
+
error instanceof Error ? error.message : error
|
|
87
|
+
)
|
|
88
|
+
throw error
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Wait for services to be healthy
|
|
94
|
+
for (const service of servicesToUp) {
|
|
95
|
+
if (service.healthCheck) {
|
|
96
|
+
console.log(chalk.cyan(`\nš„ Waiting for service ${service.name} to be healthy...`))
|
|
97
|
+
const isHealthy = await waitForService(service, config.deployment.healthCheckTimeout)
|
|
98
|
+
if (isHealthy) {
|
|
99
|
+
console.log(chalk.green(` ā
Service ${service.name} is healthy`))
|
|
100
|
+
} else {
|
|
101
|
+
console.log(
|
|
102
|
+
chalk.yellow(` ā ļø Service ${service.name} health check timeout, continuing anyway...`)
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Configure Nginx
|
|
109
|
+
if (options.nginx && allDomains.size > 0) {
|
|
110
|
+
console.log(chalk.cyan(`\nāļø Configuring Nginx reverse proxy...`))
|
|
111
|
+
|
|
112
|
+
// Create a set of service names being brought up for quick lookup
|
|
113
|
+
const servicesBeingUpped = new Set(servicesToUp.map((s) => s.name))
|
|
114
|
+
|
|
115
|
+
// For each domain, find all services that use it (from all config services)
|
|
116
|
+
// and include all active services (both newly brought up and already running)
|
|
117
|
+
for (const domain of allDomains) {
|
|
118
|
+
const configName = domain.replace(/\./g, '_')
|
|
119
|
+
|
|
120
|
+
// Find all services that use this domain (from entire config)
|
|
121
|
+
const allServicesForDomain = config.services.filter((s) => s.domains.includes(domain))
|
|
122
|
+
|
|
123
|
+
// Check which services are actually running (have active containers)
|
|
124
|
+
const activeServices: ServiceConfig[] = []
|
|
125
|
+
for (const service of allServicesForDomain) {
|
|
126
|
+
if (service.docker) {
|
|
127
|
+
const isRunning = await isContainerRunning(service.docker.container)
|
|
128
|
+
if (isRunning) {
|
|
129
|
+
activeServices.push(service)
|
|
130
|
+
}
|
|
131
|
+
} else {
|
|
132
|
+
// Service without docker - include if it's being brought up or assume it's running
|
|
133
|
+
if (servicesBeingUpped.has(service.name)) {
|
|
134
|
+
activeServices.push(service)
|
|
135
|
+
} else {
|
|
136
|
+
// For non-docker services, assume they're running if not explicitly being brought up
|
|
137
|
+
// This is a best-effort approach
|
|
138
|
+
activeServices.push(service)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (activeServices.length === 0) {
|
|
144
|
+
console.log(
|
|
145
|
+
chalk.yellow(` ā ļø No active services found for ${domain}, skipping Nginx config`)
|
|
146
|
+
)
|
|
147
|
+
continue
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
// Log domain and services configuration
|
|
152
|
+
if (activeServices.length > 1) {
|
|
153
|
+
console.log(
|
|
154
|
+
chalk.cyan(
|
|
155
|
+
` š Configuring ${domain} with ${
|
|
156
|
+
activeServices.length
|
|
157
|
+
} active service(s): ${activeServices.map((s) => s.name).join(', ')}`
|
|
158
|
+
)
|
|
159
|
+
)
|
|
160
|
+
} else {
|
|
161
|
+
console.log(
|
|
162
|
+
chalk.cyan(` š Configuring ${domain} for service: ${activeServices[0].name}`)
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Generate Nginx config for all active services on this domain
|
|
167
|
+
let nginxConfigContent: string
|
|
168
|
+
if (activeServices.length === 1) {
|
|
169
|
+
nginxConfigContent = generateNginxConfig(activeServices[0], false)
|
|
170
|
+
} else {
|
|
171
|
+
nginxConfigContent = generateMultiServiceNginxConfig(activeServices, domain, false)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Write config file
|
|
175
|
+
await writeNginxConfig(configName, config.nginx.configPath, nginxConfigContent)
|
|
176
|
+
await enableSite(configName, config.nginx.configPath)
|
|
177
|
+
|
|
178
|
+
console.log(chalk.green(` ā
Nginx configured for ${domain}`))
|
|
179
|
+
} catch (error) {
|
|
180
|
+
console.error(
|
|
181
|
+
chalk.red(` ā Failed to configure Nginx for ${domain}:`),
|
|
182
|
+
error instanceof Error ? error.message : error
|
|
183
|
+
)
|
|
184
|
+
throw error
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Update with HTTPS if enabled
|
|
189
|
+
if (options.https) {
|
|
190
|
+
console.log(chalk.cyan(`\nš Updating Nginx configs with HTTPS...`))
|
|
191
|
+
for (const domain of allDomains) {
|
|
192
|
+
const configName = domain.replace(/\./g, '_')
|
|
193
|
+
|
|
194
|
+
// Find all active services for this domain again
|
|
195
|
+
const allServicesForDomain = config.services.filter((s) => s.domains.includes(domain))
|
|
196
|
+
const activeServices: ServiceConfig[] = []
|
|
197
|
+
for (const service of allServicesForDomain) {
|
|
198
|
+
if (service.docker) {
|
|
199
|
+
const isRunning = await isContainerRunning(service.docker.container)
|
|
200
|
+
if (isRunning) {
|
|
201
|
+
activeServices.push(service)
|
|
202
|
+
}
|
|
203
|
+
} else {
|
|
204
|
+
if (servicesBeingUpped.has(service.name)) {
|
|
205
|
+
activeServices.push(service)
|
|
206
|
+
} else {
|
|
207
|
+
activeServices.push(service)
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (activeServices.length === 0) {
|
|
213
|
+
continue
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
try {
|
|
217
|
+
let nginxConfigContent: string
|
|
218
|
+
if (activeServices.length === 1) {
|
|
219
|
+
nginxConfigContent = generateNginxConfig(activeServices[0], true)
|
|
220
|
+
} else {
|
|
221
|
+
nginxConfigContent = generateMultiServiceNginxConfig(activeServices, domain, true)
|
|
222
|
+
}
|
|
223
|
+
await writeNginxConfig(configName, config.nginx.configPath, nginxConfigContent)
|
|
224
|
+
console.log(chalk.green(` ā
HTTPS config updated for ${domain}`))
|
|
225
|
+
} catch (error) {
|
|
226
|
+
console.error(
|
|
227
|
+
chalk.red(` ā Failed to update HTTPS config for ${domain}:`),
|
|
228
|
+
error instanceof Error ? error.message : error
|
|
229
|
+
)
|
|
230
|
+
throw error
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Reload Nginx
|
|
236
|
+
console.log(chalk.cyan(`\nš Reloading Nginx...`))
|
|
237
|
+
try {
|
|
238
|
+
await reloadNginx(config.nginx.reloadCommand)
|
|
239
|
+
console.log(chalk.green(` ā
Nginx reloaded`))
|
|
240
|
+
} catch (error) {
|
|
241
|
+
console.error(
|
|
242
|
+
chalk.red(` ā Failed to reload Nginx:`),
|
|
243
|
+
error instanceof Error ? error.message : error
|
|
244
|
+
)
|
|
245
|
+
throw error
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
console.log(chalk.green.bold('\nā
Services brought up successfully!\n'))
|
|
250
|
+
|
|
251
|
+
// Print service URLs
|
|
252
|
+
if (allDomains.size > 0) {
|
|
253
|
+
console.log(chalk.cyan('š Service URLs:'))
|
|
254
|
+
for (const service of servicesToUp) {
|
|
255
|
+
for (const domain of service.domains) {
|
|
256
|
+
const protocol = options.https ? 'https' : 'http'
|
|
257
|
+
const servicePath = service.path || '/'
|
|
258
|
+
const fullPath = servicePath === '/' ? '' : servicePath
|
|
259
|
+
console.log(chalk.dim(` ${service.name}: ${protocol}://${domain}${fullPath}`))
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
console.log()
|
|
263
|
+
}
|
|
264
|
+
} catch (error) {
|
|
265
|
+
console.error(
|
|
266
|
+
chalk.red('\nā Failed to bring up services:'),
|
|
267
|
+
error instanceof Error ? error.message : error
|
|
268
|
+
)
|
|
269
|
+
process.exit(1)
|
|
270
|
+
}
|
|
271
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,14 +1,25 @@
|
|
|
1
1
|
import { Command } from 'commander'
|
|
2
|
+
import { readFileSync } from 'fs'
|
|
3
|
+
import { dirname, join } from 'path'
|
|
4
|
+
import { fileURLToPath } from 'url'
|
|
2
5
|
import { deployCommand } from './commands/deploy'
|
|
6
|
+
import { downCommand } from './commands/down'
|
|
3
7
|
import { initCommand } from './commands/init'
|
|
8
|
+
import { redeployCommand } from './commands/redeploy'
|
|
4
9
|
import { setupCommand } from './commands/setup'
|
|
10
|
+
import { upCommand } from './commands/up'
|
|
11
|
+
|
|
12
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
13
|
+
const __dirname = dirname(__filename)
|
|
14
|
+
const packageJsonPath = join(__dirname, '..', 'package.json')
|
|
15
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'))
|
|
5
16
|
|
|
6
17
|
const program = new Command()
|
|
7
18
|
|
|
8
19
|
program
|
|
9
20
|
.name('suthep')
|
|
10
21
|
.description('CLI tool for deploying projects with automatic Nginx reverse proxy and HTTPS setup')
|
|
11
|
-
.version(
|
|
22
|
+
.version(packageJson.version)
|
|
12
23
|
|
|
13
24
|
program
|
|
14
25
|
.command('init')
|
|
@@ -31,4 +42,54 @@ program
|
|
|
31
42
|
.option('--no-nginx', 'Skip Nginx configuration')
|
|
32
43
|
.action(deployCommand)
|
|
33
44
|
|
|
45
|
+
program
|
|
46
|
+
.command('down')
|
|
47
|
+
.description('Bring down services (stop containers and disable Nginx configs)')
|
|
48
|
+
.option('-f, --file <path>', 'Configuration file path', 'suthep.yml')
|
|
49
|
+
.option('--all', 'Bring down all services', false)
|
|
50
|
+
.argument('[service-name]', 'Name of the service to bring down')
|
|
51
|
+
.action((serviceName, options) => {
|
|
52
|
+
downCommand({
|
|
53
|
+
file: options.file || 'suthep.yml',
|
|
54
|
+
all: options.all || false,
|
|
55
|
+
serviceName: serviceName,
|
|
56
|
+
})
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
program
|
|
60
|
+
.command('up')
|
|
61
|
+
.description('Bring up services (start containers and enable Nginx configs)')
|
|
62
|
+
.option('-f, --file <path>', 'Configuration file path', 'suthep.yml')
|
|
63
|
+
.option('--all', 'Bring up all services', false)
|
|
64
|
+
.option('--no-https', 'Skip HTTPS setup')
|
|
65
|
+
.option('--no-nginx', 'Skip Nginx configuration')
|
|
66
|
+
.argument('[service-name]', 'Name of the service to bring up')
|
|
67
|
+
.action((serviceName, options) => {
|
|
68
|
+
upCommand({
|
|
69
|
+
file: options.file || 'suthep.yml',
|
|
70
|
+
all: options.all || false,
|
|
71
|
+
serviceName: serviceName,
|
|
72
|
+
https: options.https !== false,
|
|
73
|
+
nginx: options.nginx !== false,
|
|
74
|
+
})
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
program
|
|
78
|
+
.command('redeploy')
|
|
79
|
+
.description('Redeploy services (bring down and deploy again)')
|
|
80
|
+
.option('-f, --file <path>', 'Configuration file path', 'suthep.yml')
|
|
81
|
+
.option('--all', 'Redeploy all services', false)
|
|
82
|
+
.option('--no-https', 'Skip HTTPS setup')
|
|
83
|
+
.option('--no-nginx', 'Skip Nginx configuration')
|
|
84
|
+
.argument('[service-name]', 'Name of the service to redeploy')
|
|
85
|
+
.action((serviceName, options) => {
|
|
86
|
+
redeployCommand({
|
|
87
|
+
file: options.file || 'suthep.yml',
|
|
88
|
+
all: options.all || false,
|
|
89
|
+
serviceName: serviceName,
|
|
90
|
+
https: options.https !== false,
|
|
91
|
+
nginx: options.nginx !== false,
|
|
92
|
+
})
|
|
93
|
+
})
|
|
94
|
+
|
|
34
95
|
program.parse()
|