suthep 0.1.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/.editorconfig +17 -0
- package/.prettierignore +6 -0
- package/.prettierrc +7 -0
- package/.vscode/settings.json +19 -0
- package/LICENSE +21 -0
- package/README.md +217 -0
- package/dist/commands/deploy.js +318 -0
- package/dist/commands/deploy.js.map +1 -0
- package/dist/commands/init.js +188 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/setup.js +90 -0
- package/dist/commands/setup.js.map +1 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/utils/certbot.js +64 -0
- package/dist/utils/certbot.js.map +1 -0
- package/dist/utils/config-loader.js +95 -0
- package/dist/utils/config-loader.js.map +1 -0
- package/dist/utils/deployment.js +76 -0
- package/dist/utils/deployment.js.map +1 -0
- package/dist/utils/docker.js +393 -0
- package/dist/utils/docker.js.map +1 -0
- package/dist/utils/nginx.js +303 -0
- package/dist/utils/nginx.js.map +1 -0
- package/docs/README.md +95 -0
- package/docs/TRANSLATIONS.md +211 -0
- package/docs/en/README.md +76 -0
- package/docs/en/api-reference.md +545 -0
- package/docs/en/architecture.md +369 -0
- package/docs/en/commands.md +273 -0
- package/docs/en/configuration.md +347 -0
- package/docs/en/developer-guide.md +588 -0
- package/docs/en/docker-ports-config.md +333 -0
- package/docs/en/examples.md +537 -0
- package/docs/en/getting-started.md +202 -0
- package/docs/en/port-binding.md +268 -0
- package/docs/en/troubleshooting.md +441 -0
- package/docs/th/README.md +64 -0
- package/docs/th/commands.md +202 -0
- package/docs/th/configuration.md +325 -0
- package/docs/th/getting-started.md +203 -0
- package/example/README.md +85 -0
- package/example/docker-compose.yml +76 -0
- package/example/docker-ports-example.yml +81 -0
- package/example/muacle.yml +47 -0
- package/example/port-binding-example.yml +45 -0
- package/example/suthep.yml +46 -0
- package/example/suthep=1.yml +46 -0
- package/package.json +45 -0
- package/src/commands/deploy.ts +405 -0
- package/src/commands/init.ts +214 -0
- package/src/commands/setup.ts +112 -0
- package/src/index.ts +42 -0
- package/src/types/config.ts +52 -0
- package/src/utils/certbot.ts +144 -0
- package/src/utils/config-loader.ts +121 -0
- package/src/utils/deployment.ts +157 -0
- package/src/utils/docker.ts +755 -0
- package/src/utils/nginx.ts +326 -0
- package/suthep-0.1.1.tgz +0 -0
- package/suthep.example.yml +98 -0
- package/test +0 -0
- package/todo.md +6 -0
- package/tsconfig.json +26 -0
- package/vite.config.ts +46 -0
package/.editorconfig
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Editor configuration, see http://editorconfig.org
|
|
2
|
+
root = true
|
|
3
|
+
|
|
4
|
+
[*]
|
|
5
|
+
charset = utf-8
|
|
6
|
+
indent_style = space
|
|
7
|
+
indent_size = 2
|
|
8
|
+
insert_final_newline = true
|
|
9
|
+
trim_trailing_whitespace = true
|
|
10
|
+
|
|
11
|
+
[*.md]
|
|
12
|
+
max_line_length = off
|
|
13
|
+
trim_trailing_whitespace = false
|
|
14
|
+
|
|
15
|
+
[{Makefile,**.mk}]
|
|
16
|
+
indent_style = tab
|
|
17
|
+
indent_size = 2
|
package/.prettierignore
ADDED
package/.prettierrc
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"eslint.workingDirectories": [
|
|
3
|
+
{
|
|
4
|
+
"mode": "auto"
|
|
5
|
+
}
|
|
6
|
+
],
|
|
7
|
+
"[typescript]": {
|
|
8
|
+
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
|
9
|
+
},
|
|
10
|
+
"editor.codeActionsOnSave": {
|
|
11
|
+
"source.organizeImports": "explicit"
|
|
12
|
+
},
|
|
13
|
+
"editor.formatOnSave": true,
|
|
14
|
+
"editor.insertSpaces": false,
|
|
15
|
+
"files.eol": "\n",
|
|
16
|
+
"files.insertFinalNewline": true,
|
|
17
|
+
"files.trimTrailingWhitespace": true
|
|
18
|
+
}
|
|
19
|
+
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Montol Saklor
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# Suthep
|
|
2
|
+
|
|
3
|
+
A powerful CLI tool for deploying projects with automatic Nginx reverse proxy setup, HTTPS with Certbot, and zero-downtime deployments.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- โ
Automatic Nginx reverse proxy setup
|
|
8
|
+
- โ
Automatic HTTPS with Certbot
|
|
9
|
+
- โ
Zero-downtime deployment
|
|
10
|
+
- โ
Docker container support
|
|
11
|
+
- โ
Multiple domain/subdomain support
|
|
12
|
+
- โ
Health check integration
|
|
13
|
+
- โ
YAML-based configuration
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install -g suthep
|
|
19
|
+
# or
|
|
20
|
+
yarn global add suthep
|
|
21
|
+
# or
|
|
22
|
+
pnpm add -g suthep
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The tool uses `tsx` to run TypeScript directly, so no build step is required for development. For production, you can still build:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm run build # Optional: compile to JavaScript
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Quick Start
|
|
32
|
+
|
|
33
|
+
1. **Initialize a configuration file:**
|
|
34
|
+
```bash
|
|
35
|
+
deploy init
|
|
36
|
+
```
|
|
37
|
+
This will create a `deploy.yml` file with interactive prompts.
|
|
38
|
+
|
|
39
|
+
2. **Or use the example configuration:**
|
|
40
|
+
```bash
|
|
41
|
+
cp example.yml deploy.yml
|
|
42
|
+
# Edit deploy.yml with your settings
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
3. **Setup prerequisites (Nginx and Certbot):**
|
|
46
|
+
```bash
|
|
47
|
+
deploy setup
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
4. **Deploy your project:**
|
|
51
|
+
```bash
|
|
52
|
+
deploy deploy
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Configuration
|
|
56
|
+
|
|
57
|
+
The configuration file (`deploy.yml`) supports the following structure:
|
|
58
|
+
|
|
59
|
+
```yaml
|
|
60
|
+
project:
|
|
61
|
+
name: my-app
|
|
62
|
+
version: 1.0.0
|
|
63
|
+
|
|
64
|
+
services:
|
|
65
|
+
- name: api
|
|
66
|
+
port: 3000
|
|
67
|
+
domains:
|
|
68
|
+
- api.example.com
|
|
69
|
+
- www.api.example.com
|
|
70
|
+
healthCheck:
|
|
71
|
+
path: /health
|
|
72
|
+
interval: 30
|
|
73
|
+
environment:
|
|
74
|
+
NODE_ENV: production
|
|
75
|
+
|
|
76
|
+
- name: webapp
|
|
77
|
+
port: 8080
|
|
78
|
+
docker:
|
|
79
|
+
image: nginx:latest
|
|
80
|
+
container: webapp-container
|
|
81
|
+
port: 80
|
|
82
|
+
domains:
|
|
83
|
+
- example.com
|
|
84
|
+
- www.example.com
|
|
85
|
+
|
|
86
|
+
nginx:
|
|
87
|
+
configPath: /etc/nginx/sites-available
|
|
88
|
+
reloadCommand: sudo nginx -t && sudo systemctl reload nginx
|
|
89
|
+
|
|
90
|
+
certbot:
|
|
91
|
+
email: admin@example.com
|
|
92
|
+
staging: false
|
|
93
|
+
|
|
94
|
+
deployment:
|
|
95
|
+
strategy: rolling
|
|
96
|
+
healthCheckTimeout: 30000
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Service Configuration
|
|
100
|
+
|
|
101
|
+
Each service can have:
|
|
102
|
+
|
|
103
|
+
- **name**: Unique service name
|
|
104
|
+
- **port**: Port the service runs on
|
|
105
|
+
- **domains**: Array of domain names or subdomains
|
|
106
|
+
- **docker** (optional):
|
|
107
|
+
- **image**: Docker image to use
|
|
108
|
+
- **container**: Container name
|
|
109
|
+
- **port**: Container's internal port
|
|
110
|
+
- **healthCheck** (optional):
|
|
111
|
+
- **path**: Health check endpoint path
|
|
112
|
+
- **interval**: Check interval in seconds
|
|
113
|
+
- **environment** (optional): Environment variables
|
|
114
|
+
|
|
115
|
+
## Commands
|
|
116
|
+
|
|
117
|
+
### `deploy init`
|
|
118
|
+
|
|
119
|
+
Initialize a new deployment configuration file.
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
deploy init [-f deploy.yml]
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### `deploy setup`
|
|
126
|
+
|
|
127
|
+
Setup Nginx and Certbot on the system.
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
deploy setup [--nginx-only] [--certbot-only]
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### `deploy deploy`
|
|
134
|
+
|
|
135
|
+
Deploy a project using the configuration file.
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
deploy deploy [-f deploy.yml] [--no-https] [--no-nginx]
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Options:
|
|
142
|
+
- `-f, --file`: Configuration file path (default: `deploy.yml`)
|
|
143
|
+
- `--no-https`: Skip HTTPS setup
|
|
144
|
+
- `--no-nginx`: Skip Nginx configuration
|
|
145
|
+
|
|
146
|
+
## Examples
|
|
147
|
+
|
|
148
|
+
### Simple Node.js Service
|
|
149
|
+
|
|
150
|
+
```yaml
|
|
151
|
+
services:
|
|
152
|
+
- name: api
|
|
153
|
+
port: 3000
|
|
154
|
+
domains:
|
|
155
|
+
- api.example.com
|
|
156
|
+
healthCheck:
|
|
157
|
+
path: /health
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Docker Container Service
|
|
161
|
+
|
|
162
|
+
```yaml
|
|
163
|
+
services:
|
|
164
|
+
- name: webapp
|
|
165
|
+
port: 8080
|
|
166
|
+
docker:
|
|
167
|
+
image: myapp/webapp:latest
|
|
168
|
+
container: webapp-container
|
|
169
|
+
port: 80
|
|
170
|
+
domains:
|
|
171
|
+
- example.com
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Multiple Domains
|
|
175
|
+
|
|
176
|
+
```yaml
|
|
177
|
+
services:
|
|
178
|
+
- name: dashboard
|
|
179
|
+
port: 5000
|
|
180
|
+
domains:
|
|
181
|
+
- dashboard.example.com
|
|
182
|
+
- admin.example.com
|
|
183
|
+
- app.example.com
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Connect to Existing Docker Container
|
|
187
|
+
|
|
188
|
+
```yaml
|
|
189
|
+
services:
|
|
190
|
+
- name: database-proxy
|
|
191
|
+
port: 5432
|
|
192
|
+
docker:
|
|
193
|
+
container: postgres-container
|
|
194
|
+
port: 5432
|
|
195
|
+
domains:
|
|
196
|
+
- db.example.com
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Requirements
|
|
200
|
+
|
|
201
|
+
- Node.js 16+
|
|
202
|
+
- Nginx (installed via `deploy setup`)
|
|
203
|
+
- Certbot (installed via `deploy setup`)
|
|
204
|
+
- Docker (optional, for Docker-based services)
|
|
205
|
+
- sudo access (for Nginx and Certbot operations)
|
|
206
|
+
|
|
207
|
+
## Cost Optimization
|
|
208
|
+
|
|
209
|
+
This tool helps save costs on VMs by:
|
|
210
|
+
- Efficiently managing multiple services on a single server
|
|
211
|
+
- Automatic reverse proxy setup reduces manual configuration
|
|
212
|
+
- Zero-downtime deployments reduce service interruptions
|
|
213
|
+
- Health checks ensure service reliability
|
|
214
|
+
|
|
215
|
+
## License
|
|
216
|
+
|
|
217
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import fs from "fs-extra";
|
|
3
|
+
import { certificateExists, requestCertificate } from "../utils/certbot.js";
|
|
4
|
+
import { loadConfig } from "../utils/config-loader.js";
|
|
5
|
+
import { deployService, performHealthCheck } from "../utils/deployment.js";
|
|
6
|
+
import { startDockerContainerZeroDowntime, startDockerContainer, swapContainersForZeroDowntime, cleanupTempContainer } from "../utils/docker.js";
|
|
7
|
+
import { generateNginxConfig, generateMultiServiceNginxConfig, writeNginxConfig, enableSite, reloadNginx } from "../utils/nginx.js";
|
|
8
|
+
async function deployCommand(options) {
|
|
9
|
+
console.log(chalk.blue.bold("\n๐ Deploying Services\n"));
|
|
10
|
+
try {
|
|
11
|
+
if (!await fs.pathExists(options.file)) {
|
|
12
|
+
throw new Error(`Configuration file not found: ${options.file}`);
|
|
13
|
+
}
|
|
14
|
+
console.log(chalk.cyan(`๐ Loading configuration from ${options.file}...`));
|
|
15
|
+
const config = await loadConfig(options.file);
|
|
16
|
+
console.log(chalk.green(`โ
Configuration loaded for project: ${config.project.name}`));
|
|
17
|
+
console.log(chalk.dim(` Services: ${config.services.map((s) => s.name).join(", ")}
|
|
18
|
+
`));
|
|
19
|
+
const domainToServices = /* @__PURE__ */ new Map();
|
|
20
|
+
const allDomains = /* @__PURE__ */ new Set();
|
|
21
|
+
for (const service of config.services) {
|
|
22
|
+
for (const domain of service.domains) {
|
|
23
|
+
allDomains.add(domain);
|
|
24
|
+
if (!domainToServices.has(domain)) {
|
|
25
|
+
domainToServices.set(domain, []);
|
|
26
|
+
}
|
|
27
|
+
domainToServices.get(domain).push(service);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const serviceTempInfo = /* @__PURE__ */ new Map();
|
|
31
|
+
for (const service of config.services) {
|
|
32
|
+
console.log(chalk.cyan(`
|
|
33
|
+
๐ฆ Deploying service: ${service.name}`));
|
|
34
|
+
try {
|
|
35
|
+
if (service.docker) {
|
|
36
|
+
console.log(chalk.dim(" ๐ณ Managing Docker container..."));
|
|
37
|
+
if (config.deployment.strategy === "blue-green" || config.deployment.strategy === "rolling") {
|
|
38
|
+
const tempInfo2 = await startDockerContainerZeroDowntime(service);
|
|
39
|
+
serviceTempInfo.set(service.name, tempInfo2);
|
|
40
|
+
if (tempInfo2 && tempInfo2.oldContainerExists) {
|
|
41
|
+
console.log(
|
|
42
|
+
chalk.cyan(
|
|
43
|
+
` ๐ Zero-downtime deployment: new container on port ${tempInfo2.tempPort}`
|
|
44
|
+
)
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
} else {
|
|
48
|
+
await startDockerContainer(service);
|
|
49
|
+
serviceTempInfo.set(service.name, null);
|
|
50
|
+
}
|
|
51
|
+
} else {
|
|
52
|
+
serviceTempInfo.set(service.name, null);
|
|
53
|
+
}
|
|
54
|
+
const tempInfo = serviceTempInfo.get(service.name) || null;
|
|
55
|
+
await deployService(service, config.deployment, tempInfo);
|
|
56
|
+
if (service.healthCheck) {
|
|
57
|
+
console.log(chalk.dim(` ๐ฅ Performing health check...`));
|
|
58
|
+
const checkPort = tempInfo && tempInfo.oldContainerExists ? tempInfo.tempPort : service.port;
|
|
59
|
+
const isHealthy = await performHealthCheck(
|
|
60
|
+
`http://localhost:${checkPort}${service.healthCheck.path}`,
|
|
61
|
+
config.deployment.healthCheckTimeout
|
|
62
|
+
);
|
|
63
|
+
if (isHealthy) {
|
|
64
|
+
console.log(chalk.green(` โ
Service ${service.name} is healthy`));
|
|
65
|
+
} else {
|
|
66
|
+
throw new Error(`Health check failed for service ${service.name}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
console.log(chalk.green.bold(`โจ Service ${service.name} deployed successfully!`));
|
|
70
|
+
} catch (error) {
|
|
71
|
+
console.error(
|
|
72
|
+
chalk.red(`
|
|
73
|
+
โ Failed to deploy service ${service.name}:`),
|
|
74
|
+
error instanceof Error ? error.message : error
|
|
75
|
+
);
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const generateNginxConfigsForDomain = (domain, withHttps, portOverrides) => {
|
|
80
|
+
const servicesForDomain = domainToServices.get(domain);
|
|
81
|
+
if (servicesForDomain.length === 1) {
|
|
82
|
+
const service = servicesForDomain[0];
|
|
83
|
+
const portOverride = portOverrides?.get(service.name);
|
|
84
|
+
return generateNginxConfig(service, withHttps, portOverride);
|
|
85
|
+
} else {
|
|
86
|
+
return generateMultiServiceNginxConfig(servicesForDomain, domain, withHttps, portOverrides);
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
const needsZeroDowntimeNginx = Array.from(serviceTempInfo.values()).some(
|
|
90
|
+
(info) => info !== null && info.oldContainerExists
|
|
91
|
+
);
|
|
92
|
+
if (options.nginx) {
|
|
93
|
+
if (needsZeroDowntimeNginx) {
|
|
94
|
+
console.log(chalk.cyan(`
|
|
95
|
+
โ๏ธ Updating Nginx for zero-downtime deployment...`));
|
|
96
|
+
const tempPortOverrides = /* @__PURE__ */ new Map();
|
|
97
|
+
for (const service of config.services) {
|
|
98
|
+
const tempInfo = serviceTempInfo.get(service.name);
|
|
99
|
+
if (tempInfo && tempInfo.oldContainerExists) {
|
|
100
|
+
tempPortOverrides.set(service.name, tempInfo.tempPort);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
for (const domain of allDomains) {
|
|
104
|
+
const configName = domain.replace(/\./g, "_");
|
|
105
|
+
try {
|
|
106
|
+
const nginxConfigContent = generateNginxConfigsForDomain(
|
|
107
|
+
domain,
|
|
108
|
+
false,
|
|
109
|
+
tempPortOverrides
|
|
110
|
+
);
|
|
111
|
+
await writeNginxConfig(configName, config.nginx.configPath, nginxConfigContent);
|
|
112
|
+
await enableSite(configName, config.nginx.configPath);
|
|
113
|
+
console.log(chalk.green(` โ
Nginx updated for ${domain} (temporary ports)`));
|
|
114
|
+
} catch (error) {
|
|
115
|
+
console.error(
|
|
116
|
+
chalk.red(` โ Failed to update Nginx for ${domain}:`),
|
|
117
|
+
error instanceof Error ? error.message : error
|
|
118
|
+
);
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
console.log(chalk.cyan(`
|
|
123
|
+
๐ Reloading Nginx to switch to new containers...`));
|
|
124
|
+
await reloadNginx(config.nginx.reloadCommand);
|
|
125
|
+
console.log(chalk.green(` โ
Nginx reloaded, traffic now routed to new containers`));
|
|
126
|
+
console.log(chalk.cyan(`
|
|
127
|
+
๐ Swapping containers for zero-downtime...`));
|
|
128
|
+
for (const service of config.services) {
|
|
129
|
+
const tempInfo = serviceTempInfo.get(service.name);
|
|
130
|
+
if (tempInfo && tempInfo.oldContainerExists && service.docker) {
|
|
131
|
+
try {
|
|
132
|
+
await swapContainersForZeroDowntime(service, tempInfo);
|
|
133
|
+
console.log(chalk.green(` โ
Container swapped for ${service.name}`));
|
|
134
|
+
} catch (error) {
|
|
135
|
+
console.error(
|
|
136
|
+
chalk.red(` โ Failed to swap container for ${service.name}:`),
|
|
137
|
+
error instanceof Error ? error.message : error
|
|
138
|
+
);
|
|
139
|
+
throw error;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
console.log(chalk.cyan(`
|
|
144
|
+
โ๏ธ Updating Nginx back to production ports...`));
|
|
145
|
+
for (const domain of allDomains) {
|
|
146
|
+
const configName = domain.replace(/\./g, "_");
|
|
147
|
+
try {
|
|
148
|
+
const nginxConfigContent = generateNginxConfigsForDomain(domain, false);
|
|
149
|
+
await writeNginxConfig(configName, config.nginx.configPath, nginxConfigContent);
|
|
150
|
+
await enableSite(configName, config.nginx.configPath);
|
|
151
|
+
console.log(chalk.green(` โ
Nginx updated for ${domain} (production ports)`));
|
|
152
|
+
} catch (error) {
|
|
153
|
+
console.error(
|
|
154
|
+
chalk.red(` โ Failed to update Nginx for ${domain}:`),
|
|
155
|
+
error instanceof Error ? error.message : error
|
|
156
|
+
);
|
|
157
|
+
throw error;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
console.log(chalk.cyan(`
|
|
161
|
+
๐ Reloading Nginx to switch to production ports...`));
|
|
162
|
+
await reloadNginx(config.nginx.reloadCommand);
|
|
163
|
+
console.log(chalk.green(` โ
Nginx reloaded, traffic now routed to production containers`));
|
|
164
|
+
console.log(chalk.cyan(`
|
|
165
|
+
๐งน Cleaning up temporary containers...`));
|
|
166
|
+
for (const service of config.services) {
|
|
167
|
+
const tempInfo = serviceTempInfo.get(service.name);
|
|
168
|
+
if (tempInfo && tempInfo.oldContainerExists) {
|
|
169
|
+
await cleanupTempContainer(tempInfo.tempContainerName);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
} else {
|
|
173
|
+
console.log(chalk.cyan(`
|
|
174
|
+
โ๏ธ Configuring Nginx reverse proxy...`));
|
|
175
|
+
for (const domain of allDomains) {
|
|
176
|
+
const servicesForDomain = domainToServices.get(domain);
|
|
177
|
+
const configName = domain.replace(/\./g, "_");
|
|
178
|
+
try {
|
|
179
|
+
if (servicesForDomain.length > 1) {
|
|
180
|
+
console.log(
|
|
181
|
+
chalk.cyan(
|
|
182
|
+
` ๐ Configuring ${domain} with ${servicesForDomain.length} services: ${servicesForDomain.map((s) => s.name).join(", ")}`
|
|
183
|
+
)
|
|
184
|
+
);
|
|
185
|
+
console.log(
|
|
186
|
+
chalk.dim(
|
|
187
|
+
` All services will share the same nginx config file: ${configName}.conf`
|
|
188
|
+
)
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
const nginxConfigContent = generateNginxConfigsForDomain(domain, false);
|
|
192
|
+
const wasOverridden = await writeNginxConfig(
|
|
193
|
+
configName,
|
|
194
|
+
config.nginx.configPath,
|
|
195
|
+
nginxConfigContent
|
|
196
|
+
);
|
|
197
|
+
if (wasOverridden) {
|
|
198
|
+
console.log(
|
|
199
|
+
chalk.yellow(
|
|
200
|
+
` ๐ Nginx config "${configName}.conf" already exists, deleting and recreating with new configuration...`
|
|
201
|
+
)
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
await enableSite(configName, config.nginx.configPath);
|
|
205
|
+
console.log(chalk.green(` โ
Nginx configured for ${domain}`));
|
|
206
|
+
} catch (error) {
|
|
207
|
+
console.error(
|
|
208
|
+
chalk.red(` โ Failed to configure Nginx for ${domain}:`),
|
|
209
|
+
error instanceof Error ? error.message : error
|
|
210
|
+
);
|
|
211
|
+
throw error;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
if (options.https && allDomains.size > 0) {
|
|
217
|
+
console.log(chalk.cyan(`
|
|
218
|
+
๐ Setting up HTTPS certificates...`));
|
|
219
|
+
for (const domain of allDomains) {
|
|
220
|
+
try {
|
|
221
|
+
const exists = await certificateExists(domain);
|
|
222
|
+
if (exists) {
|
|
223
|
+
console.log(
|
|
224
|
+
chalk.green(
|
|
225
|
+
` โ
SSL certificate already exists for ${domain}, skipping certificate creation`
|
|
226
|
+
)
|
|
227
|
+
);
|
|
228
|
+
console.log(
|
|
229
|
+
chalk.dim(` Using existing certificate from /etc/letsencrypt/live/${domain}/`)
|
|
230
|
+
);
|
|
231
|
+
} else {
|
|
232
|
+
console.log(chalk.cyan(` ๐ Requesting SSL certificate for ${domain}...`));
|
|
233
|
+
try {
|
|
234
|
+
await requestCertificate(domain, config.certbot.email, config.certbot.staging);
|
|
235
|
+
console.log(chalk.green(` โ
SSL certificate obtained for ${domain}`));
|
|
236
|
+
} catch (error) {
|
|
237
|
+
const errorMessage = error?.message || String(error) || "";
|
|
238
|
+
if (errorMessage.includes("already exists") || errorMessage.includes("Skipping certificate creation")) {
|
|
239
|
+
console.log(
|
|
240
|
+
chalk.green(
|
|
241
|
+
` โ
SSL certificate already exists for ${domain} (detected during request), skipping...`
|
|
242
|
+
)
|
|
243
|
+
);
|
|
244
|
+
} else {
|
|
245
|
+
throw error;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
} catch (error) {
|
|
250
|
+
console.log(
|
|
251
|
+
chalk.yellow(
|
|
252
|
+
` โ ๏ธ Failed to obtain SSL for ${domain}: ${error instanceof Error ? error.message : error}`
|
|
253
|
+
)
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (options.nginx) {
|
|
258
|
+
console.log(chalk.cyan(`
|
|
259
|
+
๐ Updating Nginx configs with HTTPS...`));
|
|
260
|
+
for (const domain of allDomains) {
|
|
261
|
+
const configName = domain.replace(/\./g, "_");
|
|
262
|
+
try {
|
|
263
|
+
const nginxConfigContent = generateNginxConfigsForDomain(domain, true);
|
|
264
|
+
const wasOverridden = await writeNginxConfig(
|
|
265
|
+
configName,
|
|
266
|
+
config.nginx.configPath,
|
|
267
|
+
nginxConfigContent
|
|
268
|
+
);
|
|
269
|
+
if (wasOverridden) {
|
|
270
|
+
console.log(
|
|
271
|
+
chalk.yellow(
|
|
272
|
+
` ๐ Nginx config "${configName}.conf" already exists, deleting and recreating with new HTTPS configuration...`
|
|
273
|
+
)
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
console.log(chalk.green(` โ
HTTPS config updated for ${domain}`));
|
|
277
|
+
} catch (error) {
|
|
278
|
+
console.error(
|
|
279
|
+
chalk.red(` โ Failed to update HTTPS config for ${domain}:`),
|
|
280
|
+
error instanceof Error ? error.message : error
|
|
281
|
+
);
|
|
282
|
+
throw error;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
if (options.nginx && !needsZeroDowntimeNginx) {
|
|
288
|
+
console.log(chalk.cyan(`
|
|
289
|
+
๐ Reloading Nginx...`));
|
|
290
|
+
await reloadNginx(config.nginx.reloadCommand);
|
|
291
|
+
} else if (options.nginx && needsZeroDowntimeNginx) {
|
|
292
|
+
console.log(chalk.cyan(`
|
|
293
|
+
๐ Final Nginx reload with HTTPS...`));
|
|
294
|
+
await reloadNginx(config.nginx.reloadCommand);
|
|
295
|
+
}
|
|
296
|
+
console.log(chalk.green.bold("\n๐ All services deployed successfully!\n"));
|
|
297
|
+
console.log(chalk.cyan("๐ Service URLs:"));
|
|
298
|
+
for (const service of config.services) {
|
|
299
|
+
for (const domain of service.domains) {
|
|
300
|
+
const protocol = options.https ? "https" : "http";
|
|
301
|
+
const servicePath = service.path || "/";
|
|
302
|
+
const fullPath = servicePath === "/" ? "" : servicePath;
|
|
303
|
+
console.log(chalk.dim(` ${service.name}: ${protocol}://${domain}${fullPath}`));
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
console.log();
|
|
307
|
+
} catch (error) {
|
|
308
|
+
console.error(
|
|
309
|
+
chalk.red("\nโ Deployment failed:"),
|
|
310
|
+
error instanceof Error ? error.message : error
|
|
311
|
+
);
|
|
312
|
+
process.exit(1);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
export {
|
|
316
|
+
deployCommand
|
|
317
|
+
};
|
|
318
|
+
//# sourceMappingURL=deploy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deploy.js","sources":["../../src/commands/deploy.ts"],"sourcesContent":["import chalk from 'chalk'\nimport fs from 'fs-extra'\nimport type { ServiceConfig } from '../types/config'\nimport { certificateExists, requestCertificate } from '../utils/certbot'\nimport { loadConfig } from '../utils/config-loader'\nimport { deployService, performHealthCheck } from '../utils/deployment'\nimport {\n cleanupTempContainer,\n startDockerContainer,\n startDockerContainerZeroDowntime,\n swapContainersForZeroDowntime,\n type ZeroDowntimeContainerInfo,\n} from '../utils/docker'\nimport {\n enableSite,\n generateMultiServiceNginxConfig,\n generateNginxConfig,\n reloadNginx,\n writeNginxConfig,\n} from '../utils/nginx'\n\ninterface DeployOptions {\n file: string\n https: boolean\n nginx: boolean\n}\n\nexport async function deployCommand(options: DeployOptions): Promise<void> {\n console.log(chalk.blue.bold('\\n๐ Deploying Services\\n'))\n\n try {\n // Load configuration\n if (!(await fs.pathExists(options.file))) {\n throw new Error(`Configuration file not found: ${options.file}`)\n }\n\n console.log(chalk.cyan(`๐ Loading configuration from ${options.file}...`))\n const config = await loadConfig(options.file)\n\n console.log(chalk.green(`โ
Configuration loaded for project: ${config.project.name}`))\n console.log(chalk.dim(` Services: ${config.services.map((s) => s.name).join(', ')}\\n`))\n\n // Group services by domain\n const domainToServices = new Map<string, ServiceConfig[]>()\n const allDomains = new Set<string>()\n\n for (const service of config.services) {\n for (const domain of service.domains) {\n allDomains.add(domain)\n if (!domainToServices.has(domain)) {\n domainToServices.set(domain, [])\n }\n domainToServices.get(domain)!.push(service)\n }\n }\n\n // Deploy each service (Docker, health checks, etc.)\n // Track zero-downtime info for services that need it\n const serviceTempInfo = new Map<string, ZeroDowntimeContainerInfo | null>()\n\n for (const service of config.services) {\n console.log(chalk.cyan(`\\n๐ฆ Deploying service: ${service.name}`))\n\n try {\n // Start Docker container if configured\n if (service.docker) {\n console.log(chalk.dim(' ๐ณ Managing Docker container...'))\n\n // Use zero-downtime deployment if strategy is blue-green or rolling\n if (\n config.deployment.strategy === 'blue-green' ||\n config.deployment.strategy === 'rolling'\n ) {\n const tempInfo = await startDockerContainerZeroDowntime(service)\n serviceTempInfo.set(service.name, tempInfo)\n\n if (tempInfo && tempInfo.oldContainerExists) {\n console.log(\n chalk.cyan(\n ` ๐ Zero-downtime deployment: new container on port ${tempInfo.tempPort}`\n )\n )\n }\n } else {\n // Fallback to regular deployment\n await startDockerContainer(service)\n serviceTempInfo.set(service.name, null)\n }\n } else {\n serviceTempInfo.set(service.name, null)\n }\n\n // Deploy the service (with temp info for zero-downtime)\n const tempInfo = serviceTempInfo.get(service.name) || null\n await deployService(service, config.deployment, tempInfo)\n\n // Perform health check on appropriate port\n if (service.healthCheck) {\n console.log(chalk.dim(` ๐ฅ Performing health check...`))\n const checkPort =\n tempInfo && tempInfo.oldContainerExists ? tempInfo.tempPort : service.port\n const isHealthy = await performHealthCheck(\n `http://localhost:${checkPort}${service.healthCheck.path}`,\n config.deployment.healthCheckTimeout\n )\n\n if (isHealthy) {\n console.log(chalk.green(` โ
Service ${service.name} is healthy`))\n } else {\n throw new Error(`Health check failed for service ${service.name}`)\n }\n }\n\n console.log(chalk.green.bold(`โจ Service ${service.name} deployed successfully!`))\n } catch (error) {\n console.error(\n chalk.red(`\\nโ Failed to deploy service ${service.name}:`),\n error instanceof Error ? error.message : error\n )\n throw error\n }\n }\n\n // Helper function to generate nginx configs with optional port overrides\n const generateNginxConfigsForDomain = (\n domain: string,\n withHttps: boolean,\n portOverrides?: Map<string, number>\n ): string => {\n const servicesForDomain = domainToServices.get(domain)!\n if (servicesForDomain.length === 1) {\n const service = servicesForDomain[0]\n const portOverride = portOverrides?.get(service.name)\n return generateNginxConfig(service, withHttps, portOverride)\n } else {\n return generateMultiServiceNginxConfig(servicesForDomain, domain, withHttps, portOverrides)\n }\n }\n\n // Check if we need zero-downtime nginx updates (any service has temp container)\n const needsZeroDowntimeNginx = Array.from(serviceTempInfo.values()).some(\n (info) => info !== null && info.oldContainerExists\n )\n\n // Configure Nginx per domain\n if (options.nginx) {\n // If zero-downtime, first update nginx to point to temp ports\n if (needsZeroDowntimeNginx) {\n console.log(chalk.cyan(`\\nโ๏ธ Updating Nginx for zero-downtime deployment...`))\n\n // Build port override map for temp ports\n const tempPortOverrides = new Map<string, number>()\n for (const service of config.services) {\n const tempInfo = serviceTempInfo.get(service.name)\n if (tempInfo && tempInfo.oldContainerExists) {\n tempPortOverrides.set(service.name, tempInfo.tempPort)\n }\n }\n\n for (const domain of allDomains) {\n const configName = domain.replace(/\\./g, '_')\n try {\n const nginxConfigContent = generateNginxConfigsForDomain(\n domain,\n false,\n tempPortOverrides\n )\n await writeNginxConfig(configName, config.nginx.configPath, nginxConfigContent)\n await enableSite(configName, config.nginx.configPath)\n console.log(chalk.green(` โ
Nginx updated for ${domain} (temporary ports)`))\n } catch (error) {\n console.error(\n chalk.red(` โ Failed to update Nginx for ${domain}:`),\n error instanceof Error ? error.message : error\n )\n throw error\n }\n }\n\n // Reload nginx to switch to temp ports (graceful reload, no connection drops)\n console.log(chalk.cyan(`\\n๐ Reloading Nginx to switch to new containers...`))\n await reloadNginx(config.nginx.reloadCommand)\n console.log(chalk.green(` โ
Nginx reloaded, traffic now routed to new containers`))\n\n // Now swap containers (stop old, promote new)\n console.log(chalk.cyan(`\\n๐ Swapping containers for zero-downtime...`))\n for (const service of config.services) {\n const tempInfo = serviceTempInfo.get(service.name)\n if (tempInfo && tempInfo.oldContainerExists && service.docker) {\n try {\n await swapContainersForZeroDowntime(service, tempInfo)\n console.log(chalk.green(` โ
Container swapped for ${service.name}`))\n } catch (error) {\n console.error(\n chalk.red(` โ Failed to swap container for ${service.name}:`),\n error instanceof Error ? error.message : error\n )\n throw error\n }\n }\n }\n\n // Update nginx back to original ports (before stopping temp containers)\n console.log(chalk.cyan(`\\nโ๏ธ Updating Nginx back to production ports...`))\n for (const domain of allDomains) {\n const configName = domain.replace(/\\./g, '_')\n try {\n const nginxConfigContent = generateNginxConfigsForDomain(domain, false)\n await writeNginxConfig(configName, config.nginx.configPath, nginxConfigContent)\n await enableSite(configName, config.nginx.configPath)\n console.log(chalk.green(` โ
Nginx updated for ${domain} (production ports)`))\n } catch (error) {\n console.error(\n chalk.red(` โ Failed to update Nginx for ${domain}:`),\n error instanceof Error ? error.message : error\n )\n throw error\n }\n }\n\n // Reload nginx to switch to production ports (graceful reload)\n console.log(chalk.cyan(`\\n๐ Reloading Nginx to switch to production ports...`))\n await reloadNginx(config.nginx.reloadCommand)\n console.log(chalk.green(` โ
Nginx reloaded, traffic now routed to production containers`))\n\n // Clean up temp containers (nginx already pointing to production, so safe to remove)\n console.log(chalk.cyan(`\\n๐งน Cleaning up temporary containers...`))\n for (const service of config.services) {\n const tempInfo = serviceTempInfo.get(service.name)\n if (tempInfo && tempInfo.oldContainerExists) {\n await cleanupTempContainer(tempInfo.tempContainerName)\n }\n }\n } else {\n // Regular nginx configuration (no zero-downtime needed)\n console.log(chalk.cyan(`\\nโ๏ธ Configuring Nginx reverse proxy...`))\n\n for (const domain of allDomains) {\n const servicesForDomain = domainToServices.get(domain)!\n const configName = domain.replace(/\\./g, '_')\n\n try {\n // Log domain and services configuration\n if (servicesForDomain.length > 1) {\n console.log(\n chalk.cyan(\n ` ๐ Configuring ${domain} with ${\n servicesForDomain.length\n } services: ${servicesForDomain.map((s) => s.name).join(', ')}`\n )\n )\n console.log(\n chalk.dim(\n ` All services will share the same nginx config file: ${configName}.conf`\n )\n )\n }\n\n // Generate Nginx config\n const nginxConfigContent = generateNginxConfigsForDomain(domain, false)\n\n // Check if config file already exists and write/override it\n const wasOverridden = await writeNginxConfig(\n configName,\n config.nginx.configPath,\n nginxConfigContent\n )\n\n if (wasOverridden) {\n console.log(\n chalk.yellow(\n ` ๐ Nginx config \"${configName}.conf\" already exists, deleting and recreating with new configuration...`\n )\n )\n }\n\n await enableSite(configName, config.nginx.configPath)\n\n console.log(chalk.green(` โ
Nginx configured for ${domain}`))\n } catch (error) {\n console.error(\n chalk.red(` โ Failed to configure Nginx for ${domain}:`),\n error instanceof Error ? error.message : error\n )\n throw error\n }\n }\n }\n }\n\n // Setup HTTPS with Certbot (per domain, not per service)\n if (options.https && allDomains.size > 0) {\n console.log(chalk.cyan(`\\n๐ Setting up HTTPS certificates...`))\n\n for (const domain of allDomains) {\n try {\n // Check if certificate already exists\n const exists = await certificateExists(domain)\n if (exists) {\n console.log(\n chalk.green(\n ` โ
SSL certificate already exists for ${domain}, skipping certificate creation`\n )\n )\n console.log(\n chalk.dim(` Using existing certificate from /etc/letsencrypt/live/${domain}/`)\n )\n } else {\n // Request new certificate\n console.log(chalk.cyan(` ๐ Requesting SSL certificate for ${domain}...`))\n try {\n await requestCertificate(domain, config.certbot.email, config.certbot.staging)\n console.log(chalk.green(` โ
SSL certificate obtained for ${domain}`))\n } catch (error: any) {\n // Check if error is because certificate already exists (race condition or check missed it)\n const errorMessage = error?.message || String(error) || ''\n if (\n errorMessage.includes('already exists') ||\n errorMessage.includes('Skipping certificate creation')\n ) {\n console.log(\n chalk.green(\n ` โ
SSL certificate already exists for ${domain} (detected during request), skipping...`\n )\n )\n } else {\n throw error // Re-throw if it's a different error\n }\n }\n }\n } catch (error) {\n console.log(\n chalk.yellow(\n ` โ ๏ธ Failed to obtain SSL for ${domain}: ${\n error instanceof Error ? error.message : error\n }`\n )\n )\n }\n }\n\n // Update Nginx configs with HTTPS\n if (options.nginx) {\n console.log(chalk.cyan(`\\n๐ Updating Nginx configs with HTTPS...`))\n for (const domain of allDomains) {\n const configName = domain.replace(/\\./g, '_')\n\n try {\n const nginxConfigContent = generateNginxConfigsForDomain(domain, true)\n const wasOverridden = await writeNginxConfig(\n configName,\n config.nginx.configPath,\n nginxConfigContent\n )\n\n if (wasOverridden) {\n console.log(\n chalk.yellow(\n ` ๐ Nginx config \"${configName}.conf\" already exists, deleting and recreating with new HTTPS configuration...`\n )\n )\n }\n console.log(chalk.green(` โ
HTTPS config updated for ${domain}`))\n } catch (error) {\n console.error(\n chalk.red(` โ Failed to update HTTPS config for ${domain}:`),\n error instanceof Error ? error.message : error\n )\n throw error\n }\n }\n }\n }\n\n // Final reload Nginx after all configurations (only if we didn't already reload for zero-downtime)\n if (options.nginx && !needsZeroDowntimeNginx) {\n console.log(chalk.cyan(`\\n๐ Reloading Nginx...`))\n await reloadNginx(config.nginx.reloadCommand)\n } else if (options.nginx && needsZeroDowntimeNginx) {\n // Final reload after HTTPS update\n console.log(chalk.cyan(`\\n๐ Final Nginx reload with HTTPS...`))\n await reloadNginx(config.nginx.reloadCommand)\n }\n\n console.log(chalk.green.bold('\\n๐ All services deployed successfully!\\n'))\n\n // Print service URLs\n console.log(chalk.cyan('๐ Service URLs:'))\n for (const service of config.services) {\n for (const domain of service.domains) {\n const protocol = options.https ? 'https' : 'http'\n const servicePath = service.path || '/'\n const fullPath = servicePath === '/' ? '' : servicePath\n console.log(chalk.dim(` ${service.name}: ${protocol}://${domain}${fullPath}`))\n }\n }\n console.log()\n } catch (error) {\n console.error(\n chalk.red('\\nโ Deployment failed:'),\n error instanceof Error ? error.message : error\n )\n process.exit(1)\n }\n}\n"],"names":["tempInfo"],"mappings":"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"}
|