@akash-chowdhury-24/deployhub 2.0.3 → 2.0.5

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.
@@ -0,0 +1,168 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+
4
+ /**
5
+ * @param {string} name
6
+ * @returns {string}
7
+ */
8
+ export function sanitizeK8sName(name) {
9
+ const sanitized = String(name)
10
+ .toLowerCase()
11
+ .replace(/[^a-z0-9-]/g, '-')
12
+ .replace(/^-+|-+$/g, '')
13
+ .slice(0, 63);
14
+ return sanitized || 'app';
15
+ }
16
+
17
+ /**
18
+ * @param {string} cwd
19
+ * @returns {Promise<boolean>}
20
+ */
21
+ export async function hasKubernetesManifests(cwd) {
22
+ if (await fs.pathExists(path.join(cwd, 'k8s'))) {
23
+ return true;
24
+ }
25
+
26
+ let files = [];
27
+ try {
28
+ files = await fs.readdir(cwd);
29
+ } catch {
30
+ return false;
31
+ }
32
+
33
+ for (const file of files) {
34
+ if (!/\.ya?ml$/i.test(file)) continue;
35
+ const content = await fs.readFile(path.join(cwd, file), 'utf-8');
36
+ if (/^\s*apiVersion:/m.test(content) && /^\s*kind:/m.test(content)) {
37
+ return true;
38
+ }
39
+ }
40
+
41
+ return false;
42
+ }
43
+
44
+ /**
45
+ * @param {object} options
46
+ * @param {string} options.appName
47
+ * @param {string} options.imageName
48
+ * @param {string} [options.imageTag]
49
+ * @param {number} options.port
50
+ * @param {string} options.namespace
51
+ * @param {string} [options.imagePullSecret]
52
+ * @returns {{ deploymentYaml: string, serviceYaml: string }}
53
+ */
54
+ export function generateKubernetesManifests({
55
+ appName,
56
+ imageName,
57
+ imageTag = 'latest',
58
+ port,
59
+ namespace,
60
+ imagePullSecret = '',
61
+ }) {
62
+ const name = sanitizeK8sName(appName);
63
+ const image = imageName.includes(':') ? imageName : `${imageName}:${imageTag}`;
64
+ const pullSecretBlock = imagePullSecret
65
+ ? ` imagePullSecrets:\n - name: ${imagePullSecret}\n`
66
+ : '';
67
+
68
+ const deploymentYaml = `apiVersion: apps/v1
69
+ kind: Deployment
70
+ metadata:
71
+ name: ${name}
72
+ namespace: ${namespace}
73
+ spec:
74
+ # Adjust replica count as needed
75
+ replicas: 1
76
+ selector:
77
+ matchLabels:
78
+ app: ${name}
79
+ template:
80
+ metadata:
81
+ labels:
82
+ app: ${name}
83
+ spec:
84
+ ${pullSecretBlock} containers:
85
+ - name: ${name}
86
+ image: ${image}
87
+ ports:
88
+ - containerPort: ${port}
89
+ resources:
90
+ requests:
91
+ memory: "128Mi"
92
+ cpu: "100m"
93
+ limits:
94
+ memory: "512Mi"
95
+ cpu: "500m"
96
+ `;
97
+
98
+ const servicePort = port === 80 ? 80 : 80;
99
+ const targetPort = port;
100
+
101
+ const serviceYaml = `apiVersion: v1
102
+ kind: Service
103
+ metadata:
104
+ name: ${name}
105
+ namespace: ${namespace}
106
+ spec:
107
+ type: ClusterIP
108
+ selector:
109
+ app: ${name}
110
+ ports:
111
+ - port: ${servicePort}
112
+ targetPort: ${targetPort}
113
+ `;
114
+
115
+ return { deploymentYaml, serviceYaml };
116
+ }
117
+
118
+ /**
119
+ * @param {import('../core/config.js').DeployHubConfig} config
120
+ * @param {Record<string, Record<string, unknown>>} [environments]
121
+ * @returns {{ appName: string, imageName: string, imageTag: string, port: number, namespace: string, imagePullSecret: string }}
122
+ */
123
+ export function resolveKubernetesManifestOptions(config, environments = {}) {
124
+ const envList = Object.values(environments);
125
+ const k8sEnv = envList.find((env) => env.type === 'kubernetes') || {};
126
+
127
+ const appName = config.project || 'app';
128
+ const imageName =
129
+ /** @type {string} */ (k8sEnv.dockerImageName) ||
130
+ process.env.DOCKER_IMAGE_NAME ||
131
+ appName;
132
+ const imageTag =
133
+ process.env.DOCKER_IMAGE_TAG || config.version || 'latest';
134
+ const namespace =
135
+ /** @type {string} */ (k8sEnv.kubeNamespace) ||
136
+ process.env.KUBE_NAMESPACE ||
137
+ appName ||
138
+ 'default';
139
+ const imagePullSecret =
140
+ /** @type {string} */ (k8sEnv.kubeImagePullSecret) ||
141
+ process.env.KUBE_IMAGE_PULL_SECRET ||
142
+ '';
143
+
144
+ let port = 3000;
145
+ if (config.projectType === 'both' && config.backend?.port) {
146
+ port = config.backend.port;
147
+ } else if (config.port) {
148
+ port = config.port;
149
+ } else if (config.backend?.port) {
150
+ port = config.backend.port;
151
+ }
152
+
153
+ return {
154
+ appName,
155
+ imageName,
156
+ imageTag,
157
+ port,
158
+ namespace,
159
+ imagePullSecret,
160
+ };
161
+ }
162
+
163
+ export default {
164
+ sanitizeK8sName,
165
+ hasKubernetesManifests,
166
+ generateKubernetesManifests,
167
+ resolveKubernetesManifestOptions,
168
+ };
@@ -1,3 +1,12 @@
1
+ /**
2
+ * Sanitize a project name for use in Nginx config file paths.
3
+ * @param {string} projectName
4
+ * @returns {string}
5
+ */
6
+ export function sanitizeNginxProjectName(projectName) {
7
+ return projectName.replace(/[^a-zA-Z0-9_-]/g, '-');
8
+ }
9
+
1
10
  /**
2
11
  * Generate nginx server block config for SPA frontend deployments.
3
12
  *
@@ -22,13 +31,61 @@ export function generateNginxConfig(projectName, deployPath, buildOutput = 'dist
22
31
  `;
23
32
  }
24
33
 
34
+ /**
35
+ * Debian/Ubuntu: sites-available path for this project.
36
+ * @param {string} projectName
37
+ * @returns {string}
38
+ */
39
+ export function getNginxSitesAvailablePath(projectName) {
40
+ return `/etc/nginx/sites-available/${sanitizeNginxProjectName(projectName)}`;
41
+ }
42
+
43
+ /**
44
+ * Debian/Ubuntu: sites-enabled symlink path for this project.
45
+ * @param {string} projectName
46
+ * @returns {string}
47
+ */
48
+ export function getNginxSitesEnabledPath(projectName) {
49
+ return `/etc/nginx/sites-enabled/${sanitizeNginxProjectName(projectName)}`;
50
+ }
51
+
52
+ /**
53
+ * RHEL/Amazon Linux: conf.d drop-in path for this project.
54
+ * @param {string} projectName
55
+ * @returns {string}
56
+ */
57
+ export function getNginxConfDPath(projectName) {
58
+ return `/etc/nginx/conf.d/${sanitizeNginxProjectName(projectName)}.conf`;
59
+ }
60
+
25
61
  /**
26
62
  * @param {string} projectName
27
63
  * @returns {string}
28
64
  */
29
65
  export function getNginxSitePath(projectName) {
30
- const safeName = projectName.replace(/[^a-zA-Z0-9_-]/g, '-');
31
- return `/etc/nginx/sites-available/${safeName}`;
66
+ return getNginxSitesAvailablePath(projectName);
67
+ }
68
+
69
+ /**
70
+ * @param {string} sshUser
71
+ * @returns {string}
72
+ */
73
+ export function formatPasswordlessSudoGuidance(sshUser) {
74
+ return (
75
+ `Passwordless sudo required to activate Nginx config during deploy.\n` +
76
+ ` On your server, run sudo visudo and add a line like:\n` +
77
+ ` ${sshUser} ALL=(ALL) NOPASSWD: /usr/sbin/nginx, /bin/cp, /usr/bin/cp, /bin/systemctl, /usr/bin/systemctl\n` +
78
+ ` (replace ${sshUser} with your SSH_USER)\n` +
79
+ ` Security: this grants broad cp/systemctl access — see README one-time server setup for a production-hardening note.`
80
+ );
32
81
  }
33
82
 
34
- export default { generateNginxConfig, getNginxSitePath };
83
+ export default {
84
+ generateNginxConfig,
85
+ sanitizeNginxProjectName,
86
+ getNginxSitesAvailablePath,
87
+ getNginxSitesEnabledPath,
88
+ getNginxConfDPath,
89
+ getNginxSitePath,
90
+ formatPasswordlessSudoGuidance,
91
+ };
@@ -0,0 +1,192 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import chalk from 'chalk';
4
+ import {
5
+ generateDockerfile,
6
+ getDockerfileFrameworkLabel,
7
+ resolveDockerSettings,
8
+ } from './dockerfile.js';
9
+ import {
10
+ generateKubernetesManifests,
11
+ hasKubernetesManifests,
12
+ resolveKubernetesManifestOptions,
13
+ } from './kubernetes-manifests.js';
14
+
15
+ /**
16
+ * @param {Record<string, Record<string, unknown>>} environments
17
+ * @returns {Set<string>}
18
+ */
19
+ export function getDeployTypes(environments) {
20
+ return new Set(
21
+ Object.values(environments)
22
+ .map((env) => /** @type {string} */ (env.type))
23
+ .filter(Boolean)
24
+ );
25
+ }
26
+
27
+ /**
28
+ * @param {import('../core/config.js').DeployHubConfig} config
29
+ * @param {Record<string, Record<string, unknown>>} [environments]
30
+ */
31
+ export function needsDockerfile(config, environments = config.environments || {}) {
32
+ const deployTypes = getDeployTypes(environments);
33
+ return deployTypes.has('docker') || deployTypes.has('kubernetes');
34
+ }
35
+
36
+ /**
37
+ * @param {import('../core/config.js').DeployHubConfig} config
38
+ * @param {Record<string, Record<string, unknown>>} [environments]
39
+ */
40
+ export function needsKubernetesManifests(
41
+ config,
42
+ environments = config.environments || {}
43
+ ) {
44
+ return getDeployTypes(environments).has('kubernetes');
45
+ }
46
+
47
+ /**
48
+ * @param {string} cwd
49
+ * @param {import('../core/config.js').DeployHubConfig} config
50
+ * @param {{ silent?: boolean }} [options]
51
+ * @returns {Promise<{ generated: boolean, framework?: string }>}
52
+ */
53
+ export async function ensureDockerfile(cwd, config, options = {}) {
54
+ const dockerfilePath = path.join(cwd, 'Dockerfile');
55
+ if (await fs.pathExists(dockerfilePath)) {
56
+ return { generated: false };
57
+ }
58
+
59
+ if (!needsDockerfile(config)) {
60
+ return { generated: false };
61
+ }
62
+
63
+ const settings = resolveDockerSettings(config);
64
+ const content = generateDockerfile(config);
65
+ await fs.writeFile(dockerfilePath, content);
66
+
67
+ if (!options.silent) {
68
+ const label = getDockerfileFrameworkLabel(settings.framework);
69
+ console.log(
70
+ chalk.yellow(
71
+ `No Dockerfile found — generated a starter Dockerfile at ./Dockerfile based on your detected ${label}. Review it before deploying, especially the exposed port and start command.`
72
+ )
73
+ );
74
+ }
75
+
76
+ return { generated: true, framework: settings.framework };
77
+ }
78
+
79
+ /**
80
+ * @param {string} cwd
81
+ * @param {import('../core/config.js').DeployHubConfig} config
82
+ * @param {Record<string, Record<string, unknown>>} [environments]
83
+ * @param {{ silent?: boolean }} [options]
84
+ * @returns {Promise<{ generated: boolean }>}
85
+ */
86
+ export async function ensureKubernetesManifests(
87
+ cwd,
88
+ config,
89
+ environments = config.environments || {},
90
+ options = {}
91
+ ) {
92
+ if (!needsKubernetesManifests(config, environments)) {
93
+ return { generated: false };
94
+ }
95
+
96
+ if (await hasKubernetesManifests(cwd)) {
97
+ return { generated: false };
98
+ }
99
+
100
+ const manifestOptions = resolveKubernetesManifestOptions(config, environments);
101
+ const { deploymentYaml, serviceYaml } = generateKubernetesManifests(manifestOptions);
102
+
103
+ const k8sDir = path.join(cwd, 'k8s');
104
+ await fs.ensureDir(k8sDir);
105
+ await fs.writeFile(path.join(k8sDir, 'deployment.yaml'), deploymentYaml);
106
+ await fs.writeFile(path.join(k8sDir, 'service.yaml'), serviceYaml);
107
+
108
+ if (!options.silent) {
109
+ console.log(
110
+ chalk.yellow(
111
+ 'No Kubernetes manifests found — generated starter manifests at ./k8s/deployment.yaml and ./k8s/service.yaml. Review resource limits, replica count, and any environment-specific settings before deploying.'
112
+ )
113
+ );
114
+ }
115
+
116
+ return { generated: true };
117
+ }
118
+
119
+ /**
120
+ * @param {string} cwd
121
+ * @param {import('../core/config.js').DeployHubConfig} config
122
+ * @param {Record<string, Record<string, unknown>>} [environments]
123
+ * @param {{ silent?: boolean }} [options]
124
+ * @returns {Promise<{ dockerfile: boolean, kubernetes: boolean }>}
125
+ */
126
+ export async function ensureDeployScaffold(
127
+ cwd,
128
+ config,
129
+ environments = config.environments || {},
130
+ options = {}
131
+ ) {
132
+ const dockerResult = await ensureDockerfile(cwd, config, options);
133
+ const k8sResult = await ensureKubernetesManifests(cwd, config, environments, options);
134
+ return {
135
+ dockerfile: dockerResult.generated,
136
+ kubernetes: k8sResult.generated,
137
+ };
138
+ }
139
+
140
+ /**
141
+ * @param {string} srcDir
142
+ * @param {string} destDir
143
+ */
144
+ export async function copyKubernetesManifestsIfPresent(srcDir, destDir) {
145
+ const k8sSrc = path.join(srcDir, 'k8s');
146
+ if (await fs.pathExists(k8sSrc)) {
147
+ await fs.copy(k8sSrc, path.join(destDir, 'k8s'));
148
+ return;
149
+ }
150
+
151
+ let files = [];
152
+ try {
153
+ files = await fs.readdir(srcDir);
154
+ } catch {
155
+ return;
156
+ }
157
+
158
+ for (const file of files) {
159
+ if (!/\.ya?ml$/i.test(file)) continue;
160
+ const srcFile = path.join(srcDir, file);
161
+ const content = await fs.readFile(srcFile, 'utf-8');
162
+ if (/^\s*apiVersion:/m.test(content) && /^\s*kind:/m.test(content)) {
163
+ await fs.copy(srcFile, path.join(destDir, file));
164
+ }
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Copy deploy-time assets from staging into artifactDir (alongside artifact.zip).
170
+ * @param {string} stagingDir
171
+ * @param {string} artifactDir
172
+ */
173
+ export async function copyDeployAssetsToArtifactDir(stagingDir, artifactDir) {
174
+ for (const file of ['Dockerfile', 'docker-compose.yml']) {
175
+ const src = path.join(stagingDir, file);
176
+ if (await fs.pathExists(src)) {
177
+ await fs.copy(src, path.join(artifactDir, file));
178
+ }
179
+ }
180
+
181
+ await copyKubernetesManifestsIfPresent(stagingDir, artifactDir);
182
+ }
183
+
184
+ export default {
185
+ ensureDockerfile,
186
+ ensureKubernetesManifests,
187
+ ensureDeployScaffold,
188
+ needsDockerfile,
189
+ needsKubernetesManifests,
190
+ copyKubernetesManifestsIfPresent,
191
+ copyDeployAssetsToArtifactDir,
192
+ };