@akash-chowdhury-24/deployhub 2.0.4 → 2.0.6

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
+ };
@@ -0,0 +1,231 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import chalk from 'chalk';
4
+ import {
5
+ generateDockerfile,
6
+ generateDockerignore,
7
+ getDockerfileFrameworkLabel,
8
+ resolveDockerSettings,
9
+ } from './dockerfile.js';
10
+ import {
11
+ generateKubernetesManifests,
12
+ hasKubernetesManifests,
13
+ resolveKubernetesManifestOptions,
14
+ } from './kubernetes-manifests.js';
15
+
16
+ /**
17
+ * @param {Record<string, Record<string, unknown>>} environments
18
+ * @returns {Set<string>}
19
+ */
20
+ export function getDeployTypes(environments) {
21
+ return new Set(
22
+ Object.values(environments)
23
+ .map((env) => /** @type {string} */ (env.type))
24
+ .filter(Boolean)
25
+ );
26
+ }
27
+
28
+ /**
29
+ * @param {import('../core/config.js').DeployHubConfig} config
30
+ * @param {Record<string, Record<string, unknown>>} [environments]
31
+ */
32
+ export function needsDockerfile(config, environments = config.environments || {}) {
33
+ const deployTypes = getDeployTypes(environments);
34
+ return deployTypes.has('docker') || deployTypes.has('kubernetes');
35
+ }
36
+
37
+ /**
38
+ * @param {import('../core/config.js').DeployHubConfig} config
39
+ * @param {Record<string, Record<string, unknown>>} [environments]
40
+ */
41
+ export function needsKubernetesManifests(
42
+ config,
43
+ environments = config.environments || {}
44
+ ) {
45
+ return getDeployTypes(environments).has('kubernetes');
46
+ }
47
+
48
+ /**
49
+ * @param {string} cwd
50
+ * @param {import('../core/config.js').DeployHubConfig} config
51
+ * @param {{ silent?: boolean }} [options]
52
+ * @returns {Promise<{ generated: boolean }>}
53
+ */
54
+ export async function ensureDockerignore(cwd, config, options = {}) {
55
+ const dockerignorePath = path.join(cwd, '.dockerignore');
56
+ if (await fs.pathExists(dockerignorePath)) {
57
+ return { generated: false };
58
+ }
59
+
60
+ if (!needsDockerfile(config)) {
61
+ return { generated: false };
62
+ }
63
+
64
+ await fs.writeFile(dockerignorePath, generateDockerignore(config));
65
+
66
+ if (!options.silent) {
67
+ console.log(
68
+ chalk.yellow(
69
+ 'No .dockerignore found — generated one at ./.dockerignore to keep node_modules and build caches out of the Docker build context.'
70
+ )
71
+ );
72
+ }
73
+
74
+ return { generated: true };
75
+ }
76
+
77
+ /**
78
+ * @param {string} cwd
79
+ * @param {import('../core/config.js').DeployHubConfig} config
80
+ * @param {{ silent?: boolean }} [options]
81
+ * @returns {Promise<{ generated: boolean, framework?: string, dockerignoreGenerated?: boolean }>}
82
+ */
83
+ export async function ensureDockerfile(cwd, config, options = {}) {
84
+ const dockerfilePath = path.join(cwd, 'Dockerfile');
85
+ let generated = false;
86
+ /** @type {string|undefined} */
87
+ let framework;
88
+
89
+ if (!(await fs.pathExists(dockerfilePath)) && needsDockerfile(config)) {
90
+ const settings = resolveDockerSettings(config);
91
+ const content = generateDockerfile(config);
92
+ await fs.writeFile(dockerfilePath, content);
93
+ generated = true;
94
+ framework = settings.framework;
95
+
96
+ if (!options.silent) {
97
+ const label = getDockerfileFrameworkLabel(settings.framework);
98
+ console.log(
99
+ chalk.yellow(
100
+ `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.`
101
+ )
102
+ );
103
+ }
104
+ }
105
+
106
+ // Always pair Dockerfile generation path with .dockerignore (never overwrite existing)
107
+ const dockerignoreResult = await ensureDockerignore(cwd, config, options);
108
+
109
+ return {
110
+ generated,
111
+ framework,
112
+ dockerignoreGenerated: dockerignoreResult.generated,
113
+ };
114
+ }
115
+
116
+ /**
117
+ * @param {string} cwd
118
+ * @param {import('../core/config.js').DeployHubConfig} config
119
+ * @param {Record<string, Record<string, unknown>>} [environments]
120
+ * @param {{ silent?: boolean }} [options]
121
+ * @returns {Promise<{ generated: boolean }>}
122
+ */
123
+ export async function ensureKubernetesManifests(
124
+ cwd,
125
+ config,
126
+ environments = config.environments || {},
127
+ options = {}
128
+ ) {
129
+ if (!needsKubernetesManifests(config, environments)) {
130
+ return { generated: false };
131
+ }
132
+
133
+ if (await hasKubernetesManifests(cwd)) {
134
+ return { generated: false };
135
+ }
136
+
137
+ const manifestOptions = resolveKubernetesManifestOptions(config, environments);
138
+ const { deploymentYaml, serviceYaml } = generateKubernetesManifests(manifestOptions);
139
+
140
+ const k8sDir = path.join(cwd, 'k8s');
141
+ await fs.ensureDir(k8sDir);
142
+ await fs.writeFile(path.join(k8sDir, 'deployment.yaml'), deploymentYaml);
143
+ await fs.writeFile(path.join(k8sDir, 'service.yaml'), serviceYaml);
144
+
145
+ if (!options.silent) {
146
+ console.log(
147
+ chalk.yellow(
148
+ '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.'
149
+ )
150
+ );
151
+ }
152
+
153
+ return { generated: true };
154
+ }
155
+
156
+ /**
157
+ * @param {string} cwd
158
+ * @param {import('../core/config.js').DeployHubConfig} config
159
+ * @param {Record<string, Record<string, unknown>>} [environments]
160
+ * @param {{ silent?: boolean }} [options]
161
+ * @returns {Promise<{ dockerfile: boolean, kubernetes: boolean }>}
162
+ */
163
+ export async function ensureDeployScaffold(
164
+ cwd,
165
+ config,
166
+ environments = config.environments || {},
167
+ options = {}
168
+ ) {
169
+ const dockerResult = await ensureDockerfile(cwd, config, options);
170
+ const k8sResult = await ensureKubernetesManifests(cwd, config, environments, options);
171
+ return {
172
+ dockerfile: dockerResult.generated,
173
+ dockerignore: Boolean(dockerResult.dockerignoreGenerated),
174
+ kubernetes: k8sResult.generated,
175
+ };
176
+ }
177
+
178
+ /**
179
+ * @param {string} srcDir
180
+ * @param {string} destDir
181
+ */
182
+ export async function copyKubernetesManifestsIfPresent(srcDir, destDir) {
183
+ const k8sSrc = path.join(srcDir, 'k8s');
184
+ if (await fs.pathExists(k8sSrc)) {
185
+ await fs.copy(k8sSrc, path.join(destDir, 'k8s'));
186
+ return;
187
+ }
188
+
189
+ let files = [];
190
+ try {
191
+ files = await fs.readdir(srcDir);
192
+ } catch {
193
+ return;
194
+ }
195
+
196
+ for (const file of files) {
197
+ if (!/\.ya?ml$/i.test(file)) continue;
198
+ const srcFile = path.join(srcDir, file);
199
+ const content = await fs.readFile(srcFile, 'utf-8');
200
+ if (/^\s*apiVersion:/m.test(content) && /^\s*kind:/m.test(content)) {
201
+ await fs.copy(srcFile, path.join(destDir, file));
202
+ }
203
+ }
204
+ }
205
+
206
+ /**
207
+ * Copy deploy-time assets from staging into artifactDir (alongside artifact.zip).
208
+ * @param {string} stagingDir
209
+ * @param {string} artifactDir
210
+ */
211
+ export async function copyDeployAssetsToArtifactDir(stagingDir, artifactDir) {
212
+ for (const file of ['Dockerfile', 'docker-compose.yml']) {
213
+ const src = path.join(stagingDir, file);
214
+ if (await fs.pathExists(src)) {
215
+ await fs.copy(src, path.join(artifactDir, file));
216
+ }
217
+ }
218
+
219
+ await copyKubernetesManifestsIfPresent(stagingDir, artifactDir);
220
+ }
221
+
222
+ export default {
223
+ ensureDockerfile,
224
+ ensureDockerignore,
225
+ ensureKubernetesManifests,
226
+ ensureDeployScaffold,
227
+ needsDockerfile,
228
+ needsKubernetesManifests,
229
+ copyKubernetesManifestsIfPresent,
230
+ copyDeployAssetsToArtifactDir,
231
+ };