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.
Files changed (72) hide show
  1. package/README.md +172 -71
  2. package/dist/commands/deploy.js +251 -37
  3. package/dist/commands/deploy.js.map +1 -1
  4. package/dist/commands/down.js +179 -0
  5. package/dist/commands/down.js.map +1 -0
  6. package/dist/commands/redeploy.js +59 -0
  7. package/dist/commands/redeploy.js.map +1 -0
  8. package/dist/commands/up.js +213 -0
  9. package/dist/commands/up.js.map +1 -0
  10. package/dist/index.js +36 -1
  11. package/dist/index.js.map +1 -1
  12. package/dist/utils/certbot.js +40 -3
  13. package/dist/utils/certbot.js.map +1 -1
  14. package/dist/utils/config-loader.js +30 -0
  15. package/dist/utils/config-loader.js.map +1 -1
  16. package/dist/utils/deployment.js +49 -16
  17. package/dist/utils/deployment.js.map +1 -1
  18. package/dist/utils/docker.js +396 -25
  19. package/dist/utils/docker.js.map +1 -1
  20. package/dist/utils/nginx.js +167 -8
  21. package/dist/utils/nginx.js.map +1 -1
  22. package/docs/README.md +25 -49
  23. package/docs/english/01-introduction.md +84 -0
  24. package/docs/english/02-installation.md +200 -0
  25. package/docs/english/03-quick-start.md +256 -0
  26. package/docs/english/04-configuration.md +358 -0
  27. package/docs/english/05-commands.md +363 -0
  28. package/docs/english/06-examples.md +456 -0
  29. package/docs/english/07-troubleshooting.md +417 -0
  30. package/docs/english/08-advanced.md +411 -0
  31. package/docs/english/README.md +48 -0
  32. package/docs/thai/01-introduction.md +84 -0
  33. package/docs/thai/02-installation.md +200 -0
  34. package/docs/thai/03-quick-start.md +256 -0
  35. package/docs/thai/04-configuration.md +358 -0
  36. package/docs/thai/05-commands.md +363 -0
  37. package/docs/thai/06-examples.md +456 -0
  38. package/docs/thai/07-troubleshooting.md +417 -0
  39. package/docs/thai/08-advanced.md +411 -0
  40. package/docs/thai/README.md +48 -0
  41. package/example/README.md +286 -53
  42. package/example/suthep-complete.yml +103 -0
  43. package/example/suthep-docker-only.yml +71 -0
  44. package/example/suthep-no-docker.yml +51 -0
  45. package/example/suthep-path-routing.yml +62 -0
  46. package/example/suthep.example.yml +89 -0
  47. package/package.json +1 -1
  48. package/src/commands/deploy.ts +322 -50
  49. package/src/commands/down.ts +240 -0
  50. package/src/commands/redeploy.ts +78 -0
  51. package/src/commands/up.ts +271 -0
  52. package/src/index.ts +62 -1
  53. package/src/types/config.ts +25 -24
  54. package/src/utils/certbot.ts +68 -6
  55. package/src/utils/config-loader.ts +40 -0
  56. package/src/utils/deployment.ts +61 -36
  57. package/src/utils/docker.ts +634 -30
  58. package/src/utils/nginx.ts +187 -4
  59. package/suthep-0.1.0-beta.1.tgz +0 -0
  60. package/suthep-0.1.1.tgz +0 -0
  61. package/suthep.example.yml +34 -0
  62. package/suthep.yml +39 -0
  63. package/test +0 -0
  64. package/docs/api-reference.md +0 -545
  65. package/docs/architecture.md +0 -367
  66. package/docs/commands.md +0 -273
  67. package/docs/configuration.md +0 -347
  68. package/docs/examples.md +0 -537
  69. package/docs/getting-started.md +0 -197
  70. package/docs/troubleshooting.md +0 -441
  71. package/example/docker-compose.yml +0 -72
  72. package/example/suthep.yml +0 -31
@@ -3,49 +3,50 @@
3
3
  */
4
4
 
5
5
  export interface ProjectConfig {
6
- name: string;
7
- version: string;
6
+ name: string
7
+ version: string
8
8
  }
9
9
 
10
10
  export interface HealthCheckConfig {
11
- path: string;
12
- interval: number;
11
+ path: string
12
+ interval: number
13
13
  }
14
14
 
15
15
  export interface DockerConfig {
16
- image?: string;
17
- container: string;
18
- port: number;
16
+ image?: string
17
+ container: string
18
+ port: number
19
19
  }
20
20
 
21
21
  export interface ServiceConfig {
22
- name: string;
23
- port: number;
24
- domains: string[];
25
- docker?: DockerConfig;
26
- healthCheck?: HealthCheckConfig;
27
- environment?: Record<string, string>;
22
+ name: string
23
+ port: number
24
+ domains: string[]
25
+ path?: string // Path prefix for this service (e.g., '/api', '/'). Defaults to '/'
26
+ docker?: DockerConfig
27
+ healthCheck?: HealthCheckConfig
28
+ environment?: Record<string, string>
28
29
  }
29
30
 
30
31
  export interface NginxConfig {
31
- configPath: string;
32
- reloadCommand: string;
32
+ configPath: string
33
+ reloadCommand: string
33
34
  }
34
35
 
35
36
  export interface CertbotConfig {
36
- email: string;
37
- staging: boolean;
37
+ email: string
38
+ staging: boolean
38
39
  }
39
40
 
40
41
  export interface DeploymentConfig {
41
- strategy: 'rolling' | 'blue-green';
42
- healthCheckTimeout: number;
42
+ strategy: 'rolling' | 'blue-green'
43
+ healthCheckTimeout: number
43
44
  }
44
45
 
45
46
  export interface DeployConfig {
46
- project: ProjectConfig;
47
- services: ServiceConfig[];
48
- nginx: NginxConfig;
49
- certbot: CertbotConfig;
50
- deployment: DeploymentConfig;
47
+ project: ProjectConfig
48
+ services: ServiceConfig[]
49
+ nginx: NginxConfig
50
+ certbot: CertbotConfig
51
+ deployment: DeploymentConfig
51
52
  }
@@ -8,6 +8,14 @@ export async function requestCertificate(
8
8
  email: string,
9
9
  staging: boolean = false
10
10
  ): Promise<void> {
11
+ // Check if certificate already exists before requesting
12
+ const exists = await certificateExists(domain)
13
+ if (exists) {
14
+ throw new Error(
15
+ `Certificate for ${domain} already exists. Use certificateExists() to check before calling this function.`
16
+ )
17
+ }
18
+
11
19
  const args = [
12
20
  'certonly',
13
21
  '--nginx',
@@ -25,12 +33,20 @@ export async function requestCertificate(
25
33
 
26
34
  try {
27
35
  await execa('sudo', ['certbot', ...args])
28
- } catch (error) {
29
- throw new Error(
30
- `Failed to obtain SSL certificate for ${domain}: ${
31
- error instanceof Error ? error.message : error
32
- }`
33
- )
36
+ } catch (error: any) {
37
+ const errorMessage = error?.stderr || error?.message || String(error) || 'Unknown error'
38
+ const errorLower = errorMessage.toLowerCase()
39
+
40
+ // Check if error is due to certificate already existing
41
+ if (
42
+ errorLower.includes('certificate already exists') ||
43
+ errorLower.includes('already have a certificate') ||
44
+ errorLower.includes('duplicate certificate')
45
+ ) {
46
+ throw new Error(`Certificate for ${domain} already exists. Skipping certificate creation.`)
47
+ }
48
+
49
+ throw new Error(`Failed to obtain SSL certificate for ${domain}: ${errorMessage}`)
34
50
  }
35
51
  }
36
52
 
@@ -47,6 +63,52 @@ export async function renewCertificates(): Promise<void> {
47
63
  }
48
64
  }
49
65
 
66
+ /**
67
+ * Check if a certificate exists for a domain
68
+ */
69
+ export async function certificateExists(domain: string): Promise<boolean> {
70
+ try {
71
+ // First, check if certificate files exist using test command (most reliable)
72
+ try {
73
+ await execa('sudo', ['test', '-f', `/etc/letsencrypt/live/${domain}/fullchain.pem`])
74
+ await execa('sudo', ['test', '-f', `/etc/letsencrypt/live/${domain}/privkey.pem`])
75
+ // Both files exist
76
+ return true
77
+ } catch {
78
+ // Files don't exist, continue to certbot check
79
+ }
80
+
81
+ // Fallback: Check using certbot certificates command
82
+ try {
83
+ const { stdout } = await execa('sudo', ['certbot', 'certificates'])
84
+
85
+ // Check if the domain appears in the certificates list
86
+ const lines = stdout.split('\n')
87
+ for (let i = 0; i < lines.length; i++) {
88
+ const line = lines[i]
89
+ // Check if this line contains "Domains:" and includes our domain
90
+ if (line.includes('Domains:') && line.includes(domain)) {
91
+ return true
92
+ }
93
+ // Also check for the domain in certificate paths
94
+ if (
95
+ line.includes(domain) &&
96
+ (line.includes('/live/') || line.includes('Certificate Name:'))
97
+ ) {
98
+ return true
99
+ }
100
+ }
101
+ } catch {
102
+ // If certbot command fails, assume no certificate exists
103
+ }
104
+
105
+ return false
106
+ } catch (error) {
107
+ // If all checks fail, assume no certificate exists
108
+ return false
109
+ }
110
+ }
111
+
50
112
  /**
51
113
  * Check certificate expiration for a domain
52
114
  */
@@ -33,6 +33,10 @@ function validateConfig(config: any): asserts config is DeployConfig {
33
33
  throw new Error('Configuration must include at least one service')
34
34
  }
35
35
 
36
+ // Track ports and container names to detect conflicts
37
+ const usedPorts = new Map<number, string[]>()
38
+ const usedContainers = new Map<string, string>()
39
+
36
40
  for (const service of config.services) {
37
41
  if (!service.name) {
38
42
  throw new Error('Each service must have a name')
@@ -43,6 +47,42 @@ function validateConfig(config: any): asserts config is DeployConfig {
43
47
  if (!service.domains || !Array.isArray(service.domains) || service.domains.length === 0) {
44
48
  throw new Error(`Service ${service.name} must have at least one domain`)
45
49
  }
50
+
51
+ // Check for port conflicts
52
+ if (usedPorts.has(service.port)) {
53
+ const conflictingServices = usedPorts.get(service.port)!
54
+ throw new Error(
55
+ `Port conflict: Service "${service.name}" uses port ${
56
+ service.port
57
+ } which is already used by: ${conflictingServices.join(
58
+ ', '
59
+ )}. Each service must use a unique port.`
60
+ )
61
+ }
62
+ usedPorts.set(service.port, [service.name])
63
+
64
+ // Check for Docker container name conflicts
65
+ if (service.docker) {
66
+ const containerName = service.docker.container
67
+ if (usedContainers.has(containerName)) {
68
+ const conflictingService = usedContainers.get(containerName)!
69
+ throw new Error(
70
+ `Docker container name conflict: Service "${service.name}" uses container name "${containerName}" which is already used by service "${conflictingService}". Each Docker container must have a unique name.`
71
+ )
72
+ }
73
+ usedContainers.set(containerName, service.name)
74
+ }
75
+ }
76
+
77
+ // Check for duplicate service names
78
+ const serviceNames = new Set<string>()
79
+ for (const service of config.services) {
80
+ if (serviceNames.has(service.name)) {
81
+ throw new Error(
82
+ `Duplicate service name: "${service.name}" is used multiple times. Each service must have a unique name.`
83
+ )
84
+ }
85
+ serviceNames.add(service.name)
46
86
  }
47
87
 
48
88
  if (!config.nginx) {
@@ -1,4 +1,5 @@
1
1
  import type { DeploymentConfig, ServiceConfig } from '../types/config'
2
+ import type { ZeroDowntimeContainerInfo } from './docker'
2
3
 
3
4
  /**
4
5
  * Perform a health check on a service endpoint
@@ -33,12 +34,13 @@ export async function performHealthCheck(url: string, timeout: number = 30000):
33
34
  */
34
35
  export async function deployService(
35
36
  service: ServiceConfig,
36
- deploymentConfig: DeploymentConfig
37
+ deploymentConfig: DeploymentConfig,
38
+ tempInfo: ZeroDowntimeContainerInfo | null = null
37
39
  ): Promise<void> {
38
40
  if (deploymentConfig.strategy === 'rolling') {
39
- await rollingDeploy(service, deploymentConfig)
41
+ await rollingDeploy(service, deploymentConfig, tempInfo)
40
42
  } else if (deploymentConfig.strategy === 'blue-green') {
41
- await blueGreenDeploy(service, deploymentConfig)
43
+ await blueGreenDeploy(service, deploymentConfig, tempInfo)
42
44
  } else {
43
45
  throw new Error(`Unknown deployment strategy: ${deploymentConfig.strategy}`)
44
46
  }
@@ -46,27 +48,37 @@ export async function deployService(
46
48
 
47
49
  /**
48
50
  * Rolling deployment strategy
49
- * Gradually replaces old instances with new ones
51
+ * For single instance, uses zero-downtime approach similar to blue-green
50
52
  */
51
53
  async function rollingDeploy(
52
54
  service: ServiceConfig,
53
- deploymentConfig: DeploymentConfig
55
+ deploymentConfig: DeploymentConfig,
56
+ tempInfo: ZeroDowntimeContainerInfo | null
54
57
  ): Promise<void> {
55
- // For rolling deployment:
56
- // 1. Start new service instance
57
- // 2. Wait for health check
58
- // 3. Switch traffic to new instance
59
- // 4. Stop old instance (if applicable)
58
+ // For rolling deployment with single instance:
59
+ // Similar to blue-green - use temporary container and port
60
60
 
61
- // In this implementation, we assume the service is already running
62
- // and we're just verifying it's healthy before proceeding
61
+ if (!tempInfo || !tempInfo.oldContainerExists) {
62
+ // No existing container, just check health on the new container
63
+ if (service.healthCheck) {
64
+ const healthUrl = `http://localhost:${service.port}${service.healthCheck.path}`
65
+ const isHealthy = await performHealthCheck(healthUrl, deploymentConfig.healthCheckTimeout)
63
66
 
64
- if (service.healthCheck) {
65
- const healthUrl = `http://localhost:${service.port}${service.healthCheck.path}`
66
- const isHealthy = await performHealthCheck(healthUrl, deploymentConfig.healthCheckTimeout)
67
-
68
- if (!isHealthy) {
69
- throw new Error(`Service ${service.name} failed health check during rolling deployment`)
67
+ if (!isHealthy) {
68
+ throw new Error(`Service ${service.name} failed health check during rolling deployment`)
69
+ }
70
+ }
71
+ } else {
72
+ // Check health on temporary port
73
+ if (service.healthCheck) {
74
+ const healthUrl = `http://localhost:${tempInfo.tempPort}${service.healthCheck.path}`
75
+ const isHealthy = await performHealthCheck(healthUrl, deploymentConfig.healthCheckTimeout)
76
+
77
+ if (!isHealthy) {
78
+ throw new Error(
79
+ `Service ${service.name} failed health check on temporary container during rolling deployment`
80
+ )
81
+ }
70
82
  }
71
83
  }
72
84
 
@@ -75,28 +87,41 @@ async function rollingDeploy(
75
87
  }
76
88
 
77
89
  /**
78
- * Blue-green deployment strategy
79
- * Maintains two identical environments and switches between them
90
+ * Blue-green deployment strategy for single instance
91
+ * Uses temporary container and port for zero-downtime deployment
80
92
  */
81
93
  async function blueGreenDeploy(
82
94
  service: ServiceConfig,
83
- deploymentConfig: DeploymentConfig
95
+ deploymentConfig: DeploymentConfig,
96
+ tempInfo: ZeroDowntimeContainerInfo | null
84
97
  ): Promise<void> {
85
- // For blue-green deployment:
86
- // 1. Deploy to "green" environment
87
- // 2. Run health checks on green
88
- // 3. Switch router/load balancer to green
89
- // 4. Keep blue as backup
90
-
91
- // This is a simplified implementation
92
- // In production, you'd manage multiple service instances
93
-
94
- if (service.healthCheck) {
95
- const healthUrl = `http://localhost:${service.port}${service.healthCheck.path}`
96
- const isHealthy = await performHealthCheck(healthUrl, deploymentConfig.healthCheckTimeout)
97
-
98
- if (!isHealthy) {
99
- throw new Error(`Service ${service.name} failed health check during blue-green deployment`)
98
+ // For blue-green deployment with single instance:
99
+ // 1. New container is already started on temporary port (handled in deploy command)
100
+ // 2. Run health checks on new container
101
+ // 3. Switch nginx to new port (handled in deploy command)
102
+ // 4. Stop old container and promote new one (handled in deploy command)
103
+
104
+ if (!tempInfo || !tempInfo.oldContainerExists) {
105
+ // No existing container, just check health on the new container
106
+ if (service.healthCheck) {
107
+ const healthUrl = `http://localhost:${service.port}${service.healthCheck.path}`
108
+ const isHealthy = await performHealthCheck(healthUrl, deploymentConfig.healthCheckTimeout)
109
+
110
+ if (!isHealthy) {
111
+ throw new Error(`Service ${service.name} failed health check during blue-green deployment`)
112
+ }
113
+ }
114
+ } else {
115
+ // Check health on temporary port
116
+ if (service.healthCheck) {
117
+ const healthUrl = `http://localhost:${tempInfo.tempPort}${service.healthCheck.path}`
118
+ const isHealthy = await performHealthCheck(healthUrl, deploymentConfig.healthCheckTimeout)
119
+
120
+ if (!isHealthy) {
121
+ throw new Error(
122
+ `Service ${service.name} failed health check on temporary container during blue-green deployment`
123
+ )
124
+ }
100
125
  }
101
126
  }
102
127
  }