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,89 @@
|
|
|
1
|
+
# Suthep Configuration Example
|
|
2
|
+
# Copy this file to suthep.yml and customize for your project
|
|
3
|
+
|
|
4
|
+
project:
|
|
5
|
+
name: my-app
|
|
6
|
+
version: 1.0.0
|
|
7
|
+
|
|
8
|
+
services:
|
|
9
|
+
# Example 1: Service without Docker (runs directly on host)
|
|
10
|
+
# Perfect for Node.js with PM2, Python with Gunicorn, etc.
|
|
11
|
+
- name: api
|
|
12
|
+
port: 3000
|
|
13
|
+
domains:
|
|
14
|
+
- api.example.com
|
|
15
|
+
- www.api.example.com
|
|
16
|
+
healthCheck:
|
|
17
|
+
path: /health
|
|
18
|
+
interval: 30
|
|
19
|
+
environment:
|
|
20
|
+
NODE_ENV: production
|
|
21
|
+
PORT: 3000
|
|
22
|
+
|
|
23
|
+
# Example 2: Service with Docker image
|
|
24
|
+
# Suthep will pull the image and create the container
|
|
25
|
+
- name: webapp
|
|
26
|
+
port: 8080
|
|
27
|
+
docker:
|
|
28
|
+
image: nginx:latest
|
|
29
|
+
container: webapp-container
|
|
30
|
+
port: 80
|
|
31
|
+
domains:
|
|
32
|
+
- example.com
|
|
33
|
+
- www.example.com
|
|
34
|
+
healthCheck:
|
|
35
|
+
path: /
|
|
36
|
+
interval: 30
|
|
37
|
+
|
|
38
|
+
# Example 3: Service connecting to existing Docker container
|
|
39
|
+
# Useful when containers are managed separately (e.g., docker-compose)
|
|
40
|
+
- name: database-proxy
|
|
41
|
+
port: 5432
|
|
42
|
+
docker:
|
|
43
|
+
container: postgres-container
|
|
44
|
+
port: 5432
|
|
45
|
+
domains:
|
|
46
|
+
- db.example.com
|
|
47
|
+
|
|
48
|
+
# Example 4: Path-based routing
|
|
49
|
+
# Multiple services on the same domain using different paths
|
|
50
|
+
- name: api-v2
|
|
51
|
+
port: 3001
|
|
52
|
+
path: /api
|
|
53
|
+
domains:
|
|
54
|
+
- myapp.com
|
|
55
|
+
docker:
|
|
56
|
+
image: myapp/api:latest
|
|
57
|
+
container: api-v2-container
|
|
58
|
+
port: 3001
|
|
59
|
+
healthCheck:
|
|
60
|
+
path: /health
|
|
61
|
+
interval: 30
|
|
62
|
+
|
|
63
|
+
- name: frontend
|
|
64
|
+
port: 3000
|
|
65
|
+
path: /
|
|
66
|
+
domains:
|
|
67
|
+
- myapp.com
|
|
68
|
+
docker:
|
|
69
|
+
image: myapp/frontend:latest
|
|
70
|
+
container: frontend-container
|
|
71
|
+
port: 3000
|
|
72
|
+
healthCheck:
|
|
73
|
+
path: /
|
|
74
|
+
interval: 30
|
|
75
|
+
|
|
76
|
+
# Nginx Configuration
|
|
77
|
+
nginx:
|
|
78
|
+
configPath: /etc/nginx/sites-available
|
|
79
|
+
reloadCommand: sudo nginx -t && sudo systemctl reload nginx
|
|
80
|
+
|
|
81
|
+
# Certbot Configuration (SSL/TLS certificates)
|
|
82
|
+
certbot:
|
|
83
|
+
email: admin@example.com
|
|
84
|
+
staging: false # Set to true for testing to avoid rate limits
|
|
85
|
+
|
|
86
|
+
# Deployment Configuration
|
|
87
|
+
deployment:
|
|
88
|
+
strategy: rolling # Options: rolling, blue-green
|
|
89
|
+
healthCheckTimeout: 30000 # milliseconds
|
package/package.json
CHANGED
package/src/commands/deploy.ts
CHANGED
|
@@ -1,11 +1,23 @@
|
|
|
1
1
|
import chalk from 'chalk'
|
|
2
2
|
import fs from 'fs-extra'
|
|
3
|
-
import
|
|
4
|
-
import { requestCertificate } from '../utils/certbot'
|
|
3
|
+
import type { ServiceConfig } from '../types/config'
|
|
4
|
+
import { certificateExists, requestCertificate } from '../utils/certbot'
|
|
5
5
|
import { loadConfig } from '../utils/config-loader'
|
|
6
6
|
import { deployService, performHealthCheck } from '../utils/deployment'
|
|
7
|
-
import {
|
|
8
|
-
|
|
7
|
+
import {
|
|
8
|
+
cleanupTempContainer,
|
|
9
|
+
startDockerContainer,
|
|
10
|
+
startDockerContainerZeroDowntime,
|
|
11
|
+
swapContainersForZeroDowntime,
|
|
12
|
+
type ZeroDowntimeContainerInfo,
|
|
13
|
+
} from '../utils/docker'
|
|
14
|
+
import {
|
|
15
|
+
enableSite,
|
|
16
|
+
generateMultiServiceNginxConfig,
|
|
17
|
+
generateNginxConfig,
|
|
18
|
+
reloadNginx,
|
|
19
|
+
writeNginxConfig,
|
|
20
|
+
} from '../utils/nginx'
|
|
9
21
|
|
|
10
22
|
interface DeployOptions {
|
|
11
23
|
file: string
|
|
@@ -28,7 +40,24 @@ export async function deployCommand(options: DeployOptions): Promise<void> {
|
|
|
28
40
|
console.log(chalk.green(`ā
Configuration loaded for project: ${config.project.name}`))
|
|
29
41
|
console.log(chalk.dim(` Services: ${config.services.map((s) => s.name).join(', ')}\n`))
|
|
30
42
|
|
|
31
|
-
//
|
|
43
|
+
// Group services by domain
|
|
44
|
+
const domainToServices = new Map<string, ServiceConfig[]>()
|
|
45
|
+
const allDomains = new Set<string>()
|
|
46
|
+
|
|
47
|
+
for (const service of config.services) {
|
|
48
|
+
for (const domain of service.domains) {
|
|
49
|
+
allDomains.add(domain)
|
|
50
|
+
if (!domainToServices.has(domain)) {
|
|
51
|
+
domainToServices.set(domain, [])
|
|
52
|
+
}
|
|
53
|
+
domainToServices.get(domain)!.push(service)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Deploy each service (Docker, health checks, etc.)
|
|
58
|
+
// Track zero-downtime info for services that need it
|
|
59
|
+
const serviceTempInfo = new Map<string, ZeroDowntimeContainerInfo | null>()
|
|
60
|
+
|
|
32
61
|
for (const service of config.services) {
|
|
33
62
|
console.log(chalk.cyan(`\nš¦ Deploying service: ${service.name}`))
|
|
34
63
|
|
|
@@ -36,62 +65,42 @@ export async function deployCommand(options: DeployOptions): Promise<void> {
|
|
|
36
65
|
// Start Docker container if configured
|
|
37
66
|
if (service.docker) {
|
|
38
67
|
console.log(chalk.dim(' š³ Managing Docker container...'))
|
|
39
|
-
await startDockerContainer(service)
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// Deploy the service
|
|
43
|
-
await deployService(service, config.deployment)
|
|
44
68
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
await enableSite(service.name, config.nginx.configPath)
|
|
53
|
-
|
|
54
|
-
console.log(chalk.green(` ā
Nginx configured for ${service.domains.join(', ')}`))
|
|
55
|
-
}
|
|
69
|
+
// Use zero-downtime deployment if strategy is blue-green or rolling
|
|
70
|
+
if (
|
|
71
|
+
config.deployment.strategy === 'blue-green' ||
|
|
72
|
+
config.deployment.strategy === 'rolling'
|
|
73
|
+
) {
|
|
74
|
+
const tempInfo = await startDockerContainerZeroDowntime(service)
|
|
75
|
+
serviceTempInfo.set(service.name, tempInfo)
|
|
56
76
|
|
|
57
|
-
|
|
58
|
-
if (options.https && service.domains.length > 0) {
|
|
59
|
-
console.log(chalk.dim(' š Setting up HTTPS certificates...'))
|
|
60
|
-
|
|
61
|
-
for (const domain of service.domains) {
|
|
62
|
-
try {
|
|
63
|
-
await requestCertificate(domain, config.certbot.email, config.certbot.staging)
|
|
64
|
-
console.log(chalk.green(` ā
SSL certificate obtained for ${domain}`))
|
|
65
|
-
} catch (error) {
|
|
77
|
+
if (tempInfo && tempInfo.oldContainerExists) {
|
|
66
78
|
console.log(
|
|
67
|
-
chalk.
|
|
68
|
-
`
|
|
69
|
-
error instanceof Error ? error.message : error
|
|
70
|
-
}`
|
|
79
|
+
chalk.cyan(
|
|
80
|
+
` š Zero-downtime deployment: new container on port ${tempInfo.tempPort}`
|
|
71
81
|
)
|
|
72
82
|
)
|
|
73
83
|
}
|
|
84
|
+
} else {
|
|
85
|
+
// Fallback to regular deployment
|
|
86
|
+
await startDockerContainer(service)
|
|
87
|
+
serviceTempInfo.set(service.name, null)
|
|
74
88
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
if (options.nginx) {
|
|
78
|
-
const nginxConfigContent = generateNginxConfig(service, true)
|
|
79
|
-
const nginxConfigPath = path.join(config.nginx.configPath, `${service.name}.conf`)
|
|
80
|
-
await fs.writeFile(nginxConfigPath, nginxConfigContent)
|
|
81
|
-
}
|
|
89
|
+
} else {
|
|
90
|
+
serviceTempInfo.set(service.name, null)
|
|
82
91
|
}
|
|
83
92
|
|
|
84
|
-
//
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
await reloadNginx(config.nginx.reloadCommand)
|
|
88
|
-
}
|
|
93
|
+
// Deploy the service (with temp info for zero-downtime)
|
|
94
|
+
const tempInfo = serviceTempInfo.get(service.name) || null
|
|
95
|
+
await deployService(service, config.deployment, tempInfo)
|
|
89
96
|
|
|
90
|
-
// Perform health check
|
|
97
|
+
// Perform health check on appropriate port
|
|
91
98
|
if (service.healthCheck) {
|
|
92
99
|
console.log(chalk.dim(` š„ Performing health check...`))
|
|
100
|
+
const checkPort =
|
|
101
|
+
tempInfo && tempInfo.oldContainerExists ? tempInfo.tempPort : service.port
|
|
93
102
|
const isHealthy = await performHealthCheck(
|
|
94
|
-
`http://localhost:${
|
|
103
|
+
`http://localhost:${checkPort}${service.healthCheck.path}`,
|
|
95
104
|
config.deployment.healthCheckTimeout
|
|
96
105
|
)
|
|
97
106
|
|
|
@@ -102,7 +111,7 @@ export async function deployCommand(options: DeployOptions): Promise<void> {
|
|
|
102
111
|
}
|
|
103
112
|
}
|
|
104
113
|
|
|
105
|
-
console.log(chalk.green.bold(
|
|
114
|
+
console.log(chalk.green.bold(`⨠Service ${service.name} deployed successfully!`))
|
|
106
115
|
} catch (error) {
|
|
107
116
|
console.error(
|
|
108
117
|
chalk.red(`\nā Failed to deploy service ${service.name}:`),
|
|
@@ -112,6 +121,267 @@ export async function deployCommand(options: DeployOptions): Promise<void> {
|
|
|
112
121
|
}
|
|
113
122
|
}
|
|
114
123
|
|
|
124
|
+
// Helper function to generate nginx configs with optional port overrides
|
|
125
|
+
const generateNginxConfigsForDomain = (
|
|
126
|
+
domain: string,
|
|
127
|
+
withHttps: boolean,
|
|
128
|
+
portOverrides?: Map<string, number>
|
|
129
|
+
): string => {
|
|
130
|
+
const servicesForDomain = domainToServices.get(domain)!
|
|
131
|
+
if (servicesForDomain.length === 1) {
|
|
132
|
+
const service = servicesForDomain[0]
|
|
133
|
+
const portOverride = portOverrides?.get(service.name)
|
|
134
|
+
return generateNginxConfig(service, withHttps, portOverride)
|
|
135
|
+
} else {
|
|
136
|
+
return generateMultiServiceNginxConfig(servicesForDomain, domain, withHttps, portOverrides)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Check if we need zero-downtime nginx updates (any service has temp container)
|
|
141
|
+
const needsZeroDowntimeNginx = Array.from(serviceTempInfo.values()).some(
|
|
142
|
+
(info) => info !== null && info.oldContainerExists
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
// Configure Nginx per domain
|
|
146
|
+
if (options.nginx) {
|
|
147
|
+
// If zero-downtime, first update nginx to point to temp ports
|
|
148
|
+
if (needsZeroDowntimeNginx) {
|
|
149
|
+
console.log(chalk.cyan(`\nāļø Updating Nginx for zero-downtime deployment...`))
|
|
150
|
+
|
|
151
|
+
// Build port override map for temp ports
|
|
152
|
+
const tempPortOverrides = new Map<string, number>()
|
|
153
|
+
for (const service of config.services) {
|
|
154
|
+
const tempInfo = serviceTempInfo.get(service.name)
|
|
155
|
+
if (tempInfo && tempInfo.oldContainerExists) {
|
|
156
|
+
tempPortOverrides.set(service.name, tempInfo.tempPort)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
for (const domain of allDomains) {
|
|
161
|
+
const configName = domain.replace(/\./g, '_')
|
|
162
|
+
try {
|
|
163
|
+
const nginxConfigContent = generateNginxConfigsForDomain(
|
|
164
|
+
domain,
|
|
165
|
+
false,
|
|
166
|
+
tempPortOverrides
|
|
167
|
+
)
|
|
168
|
+
await writeNginxConfig(configName, config.nginx.configPath, nginxConfigContent)
|
|
169
|
+
await enableSite(configName, config.nginx.configPath)
|
|
170
|
+
console.log(chalk.green(` ā
Nginx updated for ${domain} (temporary ports)`))
|
|
171
|
+
} catch (error) {
|
|
172
|
+
console.error(
|
|
173
|
+
chalk.red(` ā Failed to update Nginx for ${domain}:`),
|
|
174
|
+
error instanceof Error ? error.message : error
|
|
175
|
+
)
|
|
176
|
+
throw error
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Reload nginx to switch to temp ports (graceful reload, no connection drops)
|
|
181
|
+
console.log(chalk.cyan(`\nš Reloading Nginx to switch to new containers...`))
|
|
182
|
+
await reloadNginx(config.nginx.reloadCommand)
|
|
183
|
+
console.log(chalk.green(` ā
Nginx reloaded, traffic now routed to new containers`))
|
|
184
|
+
|
|
185
|
+
// Now swap containers (stop old, promote new)
|
|
186
|
+
console.log(chalk.cyan(`\nš Swapping containers for zero-downtime...`))
|
|
187
|
+
for (const service of config.services) {
|
|
188
|
+
const tempInfo = serviceTempInfo.get(service.name)
|
|
189
|
+
if (tempInfo && tempInfo.oldContainerExists && service.docker) {
|
|
190
|
+
try {
|
|
191
|
+
await swapContainersForZeroDowntime(service, tempInfo)
|
|
192
|
+
console.log(chalk.green(` ā
Container swapped for ${service.name}`))
|
|
193
|
+
} catch (error) {
|
|
194
|
+
console.error(
|
|
195
|
+
chalk.red(` ā Failed to swap container for ${service.name}:`),
|
|
196
|
+
error instanceof Error ? error.message : error
|
|
197
|
+
)
|
|
198
|
+
throw error
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Update nginx back to original ports (before stopping temp containers)
|
|
204
|
+
console.log(chalk.cyan(`\nāļø Updating Nginx back to production ports...`))
|
|
205
|
+
for (const domain of allDomains) {
|
|
206
|
+
const configName = domain.replace(/\./g, '_')
|
|
207
|
+
try {
|
|
208
|
+
const nginxConfigContent = generateNginxConfigsForDomain(domain, false)
|
|
209
|
+
await writeNginxConfig(configName, config.nginx.configPath, nginxConfigContent)
|
|
210
|
+
await enableSite(configName, config.nginx.configPath)
|
|
211
|
+
console.log(chalk.green(` ā
Nginx updated for ${domain} (production ports)`))
|
|
212
|
+
} catch (error) {
|
|
213
|
+
console.error(
|
|
214
|
+
chalk.red(` ā Failed to update Nginx for ${domain}:`),
|
|
215
|
+
error instanceof Error ? error.message : error
|
|
216
|
+
)
|
|
217
|
+
throw error
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Reload nginx to switch to production ports (graceful reload)
|
|
222
|
+
console.log(chalk.cyan(`\nš Reloading Nginx to switch to production ports...`))
|
|
223
|
+
await reloadNginx(config.nginx.reloadCommand)
|
|
224
|
+
console.log(chalk.green(` ā
Nginx reloaded, traffic now routed to production containers`))
|
|
225
|
+
|
|
226
|
+
// Clean up temp containers (nginx already pointing to production, so safe to remove)
|
|
227
|
+
console.log(chalk.cyan(`\nš§¹ Cleaning up temporary containers...`))
|
|
228
|
+
for (const service of config.services) {
|
|
229
|
+
const tempInfo = serviceTempInfo.get(service.name)
|
|
230
|
+
if (tempInfo && tempInfo.oldContainerExists) {
|
|
231
|
+
await cleanupTempContainer(tempInfo.tempContainerName)
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
} else {
|
|
235
|
+
// Regular nginx configuration (no zero-downtime needed)
|
|
236
|
+
console.log(chalk.cyan(`\nāļø Configuring Nginx reverse proxy...`))
|
|
237
|
+
|
|
238
|
+
for (const domain of allDomains) {
|
|
239
|
+
const servicesForDomain = domainToServices.get(domain)!
|
|
240
|
+
const configName = domain.replace(/\./g, '_')
|
|
241
|
+
|
|
242
|
+
try {
|
|
243
|
+
// Log domain and services configuration
|
|
244
|
+
if (servicesForDomain.length > 1) {
|
|
245
|
+
console.log(
|
|
246
|
+
chalk.cyan(
|
|
247
|
+
` š Configuring ${domain} with ${
|
|
248
|
+
servicesForDomain.length
|
|
249
|
+
} services: ${servicesForDomain.map((s) => s.name).join(', ')}`
|
|
250
|
+
)
|
|
251
|
+
)
|
|
252
|
+
console.log(
|
|
253
|
+
chalk.dim(
|
|
254
|
+
` All services will share the same nginx config file: ${configName}.conf`
|
|
255
|
+
)
|
|
256
|
+
)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Generate Nginx config
|
|
260
|
+
const nginxConfigContent = generateNginxConfigsForDomain(domain, false)
|
|
261
|
+
|
|
262
|
+
// Check if config file already exists and write/override it
|
|
263
|
+
const wasOverridden = await writeNginxConfig(
|
|
264
|
+
configName,
|
|
265
|
+
config.nginx.configPath,
|
|
266
|
+
nginxConfigContent
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
if (wasOverridden) {
|
|
270
|
+
console.log(
|
|
271
|
+
chalk.yellow(
|
|
272
|
+
` š Nginx config "${configName}.conf" already exists, deleting and recreating with new configuration...`
|
|
273
|
+
)
|
|
274
|
+
)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
await enableSite(configName, config.nginx.configPath)
|
|
278
|
+
|
|
279
|
+
console.log(chalk.green(` ā
Nginx configured for ${domain}`))
|
|
280
|
+
} catch (error) {
|
|
281
|
+
console.error(
|
|
282
|
+
chalk.red(` ā Failed to configure Nginx for ${domain}:`),
|
|
283
|
+
error instanceof Error ? error.message : error
|
|
284
|
+
)
|
|
285
|
+
throw error
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Setup HTTPS with Certbot (per domain, not per service)
|
|
292
|
+
if (options.https && allDomains.size > 0) {
|
|
293
|
+
console.log(chalk.cyan(`\nš Setting up HTTPS certificates...`))
|
|
294
|
+
|
|
295
|
+
for (const domain of allDomains) {
|
|
296
|
+
try {
|
|
297
|
+
// Check if certificate already exists
|
|
298
|
+
const exists = await certificateExists(domain)
|
|
299
|
+
if (exists) {
|
|
300
|
+
console.log(
|
|
301
|
+
chalk.green(
|
|
302
|
+
` ā
SSL certificate already exists for ${domain}, skipping certificate creation`
|
|
303
|
+
)
|
|
304
|
+
)
|
|
305
|
+
console.log(
|
|
306
|
+
chalk.dim(` Using existing certificate from /etc/letsencrypt/live/${domain}/`)
|
|
307
|
+
)
|
|
308
|
+
} else {
|
|
309
|
+
// Request new certificate
|
|
310
|
+
console.log(chalk.cyan(` š Requesting SSL certificate for ${domain}...`))
|
|
311
|
+
try {
|
|
312
|
+
await requestCertificate(domain, config.certbot.email, config.certbot.staging)
|
|
313
|
+
console.log(chalk.green(` ā
SSL certificate obtained for ${domain}`))
|
|
314
|
+
} catch (error: any) {
|
|
315
|
+
// Check if error is because certificate already exists (race condition or check missed it)
|
|
316
|
+
const errorMessage = error?.message || String(error) || ''
|
|
317
|
+
if (
|
|
318
|
+
errorMessage.includes('already exists') ||
|
|
319
|
+
errorMessage.includes('Skipping certificate creation')
|
|
320
|
+
) {
|
|
321
|
+
console.log(
|
|
322
|
+
chalk.green(
|
|
323
|
+
` ā
SSL certificate already exists for ${domain} (detected during request), skipping...`
|
|
324
|
+
)
|
|
325
|
+
)
|
|
326
|
+
} else {
|
|
327
|
+
throw error // Re-throw if it's a different error
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
} catch (error) {
|
|
332
|
+
console.log(
|
|
333
|
+
chalk.yellow(
|
|
334
|
+
` ā ļø Failed to obtain SSL for ${domain}: ${
|
|
335
|
+
error instanceof Error ? error.message : error
|
|
336
|
+
}`
|
|
337
|
+
)
|
|
338
|
+
)
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Update Nginx configs with HTTPS
|
|
343
|
+
if (options.nginx) {
|
|
344
|
+
console.log(chalk.cyan(`\nš Updating Nginx configs with HTTPS...`))
|
|
345
|
+
for (const domain of allDomains) {
|
|
346
|
+
const configName = domain.replace(/\./g, '_')
|
|
347
|
+
|
|
348
|
+
try {
|
|
349
|
+
const nginxConfigContent = generateNginxConfigsForDomain(domain, true)
|
|
350
|
+
const wasOverridden = await writeNginxConfig(
|
|
351
|
+
configName,
|
|
352
|
+
config.nginx.configPath,
|
|
353
|
+
nginxConfigContent
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
if (wasOverridden) {
|
|
357
|
+
console.log(
|
|
358
|
+
chalk.yellow(
|
|
359
|
+
` š Nginx config "${configName}.conf" already exists, deleting and recreating with new HTTPS configuration...`
|
|
360
|
+
)
|
|
361
|
+
)
|
|
362
|
+
}
|
|
363
|
+
console.log(chalk.green(` ā
HTTPS config updated for ${domain}`))
|
|
364
|
+
} catch (error) {
|
|
365
|
+
console.error(
|
|
366
|
+
chalk.red(` ā Failed to update HTTPS config for ${domain}:`),
|
|
367
|
+
error instanceof Error ? error.message : error
|
|
368
|
+
)
|
|
369
|
+
throw error
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Final reload Nginx after all configurations (only if we didn't already reload for zero-downtime)
|
|
376
|
+
if (options.nginx && !needsZeroDowntimeNginx) {
|
|
377
|
+
console.log(chalk.cyan(`\nš Reloading Nginx...`))
|
|
378
|
+
await reloadNginx(config.nginx.reloadCommand)
|
|
379
|
+
} else if (options.nginx && needsZeroDowntimeNginx) {
|
|
380
|
+
// Final reload after HTTPS update
|
|
381
|
+
console.log(chalk.cyan(`\nš Final Nginx reload with HTTPS...`))
|
|
382
|
+
await reloadNginx(config.nginx.reloadCommand)
|
|
383
|
+
}
|
|
384
|
+
|
|
115
385
|
console.log(chalk.green.bold('\nš All services deployed successfully!\n'))
|
|
116
386
|
|
|
117
387
|
// Print service URLs
|
|
@@ -119,7 +389,9 @@ export async function deployCommand(options: DeployOptions): Promise<void> {
|
|
|
119
389
|
for (const service of config.services) {
|
|
120
390
|
for (const domain of service.domains) {
|
|
121
391
|
const protocol = options.https ? 'https' : 'http'
|
|
122
|
-
|
|
392
|
+
const servicePath = service.path || '/'
|
|
393
|
+
const fullPath = servicePath === '/' ? '' : servicePath
|
|
394
|
+
console.log(chalk.dim(` ${service.name}: ${protocol}://${domain}${fullPath}`))
|
|
123
395
|
}
|
|
124
396
|
}
|
|
125
397
|
console.log()
|