@akash-chowdhury-24/deployhub 2.0.8 → 2.0.9
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/package.json
CHANGED
package/src/commands/doctor.js
CHANGED
|
@@ -2,6 +2,7 @@ import chalk from 'chalk';
|
|
|
2
2
|
import { execa } from 'execa';
|
|
3
3
|
import fs from 'fs-extra';
|
|
4
4
|
import path from 'path';
|
|
5
|
+
import os from 'os';
|
|
5
6
|
import axios from 'axios';
|
|
6
7
|
import { loadConfig, loadEnv } from '../core/config.js';
|
|
7
8
|
import { testProvider } from '../storage/index.js';
|
|
@@ -20,6 +21,7 @@ import {
|
|
|
20
21
|
} from '../utils/shell-quote.js';
|
|
21
22
|
import { formatPasswordlessSudoGuidance } from '../utils/nginx.js';
|
|
22
23
|
import { checkImagePullability } from '../utils/docker-image-deploy.js';
|
|
24
|
+
import { namespaceExists } from '../utils/kubernetes-namespace.js';
|
|
23
25
|
|
|
24
26
|
/**
|
|
25
27
|
* @typedef {{ name: string, pass: boolean, message: string }} CheckResult
|
|
@@ -412,6 +414,42 @@ async function runDeploymentChecks(config, envName, envConfig) {
|
|
|
412
414
|
})
|
|
413
415
|
);
|
|
414
416
|
|
|
417
|
+
checks.push(
|
|
418
|
+
await runCheck('Kubernetes namespace', async () => {
|
|
419
|
+
const ns = process.env.KUBE_NAMESPACE || config.project || 'default';
|
|
420
|
+
const kubeconfig =
|
|
421
|
+
process.env.KUBECONFIG || path.join(os.homedir(), '.kube', 'config');
|
|
422
|
+
const context = process.env.KUBE_CONTEXT || '';
|
|
423
|
+
const expanded = kubeconfig.replace(/^~/, os.homedir());
|
|
424
|
+
const kubectlEnv = { ...process.env, KUBECONFIG: path.resolve(expanded) };
|
|
425
|
+
|
|
426
|
+
/** @param {string[]} baseArgs */
|
|
427
|
+
function kubectlClusterArgs(baseArgs) {
|
|
428
|
+
const args = [...baseArgs];
|
|
429
|
+
if (context) args.push('--context', context);
|
|
430
|
+
return args;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const exists = await namespaceExists(ns, {
|
|
434
|
+
kubectlArgs: kubectlClusterArgs,
|
|
435
|
+
getKubectlEnv: () => kubectlEnv,
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
if (exists) {
|
|
439
|
+
return {
|
|
440
|
+
name: 'Kubernetes namespace',
|
|
441
|
+
pass: true,
|
|
442
|
+
message: `Namespace '${ns}' exists`,
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
return {
|
|
446
|
+
name: 'Kubernetes namespace',
|
|
447
|
+
pass: true,
|
|
448
|
+
message: `Namespace '${ns}' does not exist yet — deploy will prompt locally or auto-create in CI (kubectl create namespace ${ns})`,
|
|
449
|
+
};
|
|
450
|
+
})
|
|
451
|
+
);
|
|
452
|
+
|
|
415
453
|
checks.push(
|
|
416
454
|
await runCheck('Container image pullable', async () => {
|
|
417
455
|
const result = await checkImagePullability(config, process.env);
|
|
@@ -309,7 +309,7 @@ export const DEPLOYMENT_ENV_DEFS = {
|
|
|
309
309
|
optionalReason: 'defaults to your project name or "default" if unset',
|
|
310
310
|
comment: [
|
|
311
311
|
'Kubernetes namespace for your deployment.',
|
|
312
|
-
'
|
|
312
|
+
'If missing, deployhub deploy prompts to create it locally (or auto-creates in CI).',
|
|
313
313
|
],
|
|
314
314
|
example: 'my-app',
|
|
315
315
|
when: 'optional',
|
|
@@ -720,7 +720,7 @@ export const DEPLOYMENT_GUIDE = {
|
|
|
720
720
|
],
|
|
721
721
|
after: [
|
|
722
722
|
'Ensure your kubeconfig context points to the correct cluster.',
|
|
723
|
-
'
|
|
723
|
+
'Namespace is created on first deploy if missing (prompt locally; auto-create in CI). Or: kubectl create namespace YOUR_NAMESPACE',
|
|
724
724
|
'Copy .env.example to .env and set DOCKER_IMAGE_NAME, DOCKER_REGISTRY_USERNAME, and DOCKER_REGISTRY_TOKEN.',
|
|
725
725
|
'Skipping registry credentials will very likely cause ImagePullBackOff — the cluster cannot see local Docker images.',
|
|
726
726
|
'For private registries: also create kubectl create secret docker-registry ... and set KUBE_IMAGE_PULL_SECRET.',
|
|
@@ -5,6 +5,7 @@ import os from 'os';
|
|
|
5
5
|
import { createLogger } from '../../logger/index.js';
|
|
6
6
|
import { sanitizeK8sName } from '../../utils/kubernetes-manifests.js';
|
|
7
7
|
import { createDockerImageDeployContext } from '../../utils/docker-image-deploy.js';
|
|
8
|
+
import { ensureKubernetesNamespace } from '../../utils/kubernetes-namespace.js';
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* @param {import('../../core/config.js').DeployHubConfig} config
|
|
@@ -26,12 +27,22 @@ export function createKubernetesProvider(config, envName, env = process.env) {
|
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
/**
|
|
30
|
+
* Cluster-scoped kubectl args (context only). Used for Namespace get/create.
|
|
29
31
|
* @param {string[]} baseArgs
|
|
30
32
|
*/
|
|
31
|
-
function
|
|
33
|
+
function kubectlClusterArgs(baseArgs) {
|
|
32
34
|
/** @type {string[]} */
|
|
33
35
|
const args = [...baseArgs];
|
|
34
36
|
if (context) args.push('--context', context);
|
|
37
|
+
return args;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @param {string[]} baseArgs
|
|
42
|
+
*/
|
|
43
|
+
function kubectlArgs(baseArgs) {
|
|
44
|
+
/** @type {string[]} */
|
|
45
|
+
const args = kubectlClusterArgs(baseArgs);
|
|
35
46
|
if (namespace) args.push('--namespace', namespace);
|
|
36
47
|
return args;
|
|
37
48
|
}
|
|
@@ -61,6 +72,13 @@ export function createKubernetesProvider(config, envName, env = process.env) {
|
|
|
61
72
|
);
|
|
62
73
|
}
|
|
63
74
|
|
|
75
|
+
await ensureKubernetesNamespace({
|
|
76
|
+
namespace,
|
|
77
|
+
log,
|
|
78
|
+
kubectlArgs: kubectlClusterArgs,
|
|
79
|
+
getKubectlEnv,
|
|
80
|
+
});
|
|
81
|
+
|
|
64
82
|
const applyTarget = (await fs.pathExists(path.join(manifestDir, 'k8s')))
|
|
65
83
|
? path.join(manifestDir, 'k8s')
|
|
66
84
|
: manifestDir;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detect whether the CLI can safely prompt the user.
|
|
3
|
+
* Non-interactive when stdin is not a TTY, or when common CI env vars are set.
|
|
4
|
+
*
|
|
5
|
+
* @param {{ env?: NodeJS.ProcessEnv, stdinIsTTY?: boolean|null }} [options]
|
|
6
|
+
* @returns {boolean}
|
|
7
|
+
*/
|
|
8
|
+
export function isInteractive(options = {}) {
|
|
9
|
+
const env = options.env || process.env;
|
|
10
|
+
const stdinIsTTY =
|
|
11
|
+
options.stdinIsTTY !== undefined ? options.stdinIsTTY : Boolean(process.stdin.isTTY);
|
|
12
|
+
|
|
13
|
+
if (env.CI === 'true' || env.CI === '1') return false;
|
|
14
|
+
if (env.GITHUB_ACTIONS === 'true' || env.GITHUB_ACTIONS === '1') return false;
|
|
15
|
+
if (!stdinIsTTY) return false;
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param {{ env?: NodeJS.ProcessEnv, stdinIsTTY?: boolean|null }} [options]
|
|
21
|
+
* @returns {boolean}
|
|
22
|
+
*/
|
|
23
|
+
export function isNonInteractive(options = {}) {
|
|
24
|
+
return !isInteractive(options);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export default { isInteractive, isNonInteractive };
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { execa } from 'execa';
|
|
2
|
+
import inquirer from 'inquirer';
|
|
3
|
+
import { isInteractive } from './interactive.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @param {unknown} err
|
|
7
|
+
* @returns {string}
|
|
8
|
+
*/
|
|
9
|
+
function kubectlErrorDetail(err) {
|
|
10
|
+
if (err && typeof err === 'object' && 'stderr' in err) {
|
|
11
|
+
const stderr = String(/** @type {{ stderr?: unknown }} */ (err).stderr || '').trim();
|
|
12
|
+
if (stderr) return stderr;
|
|
13
|
+
}
|
|
14
|
+
if (err instanceof Error && err.message) return err.message;
|
|
15
|
+
return String(err);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Check whether a namespace exists using a cluster-scoped get that exits 0 for
|
|
20
|
+
* both found and missing. Non-zero exits (auth, connectivity, bad kubeconfig)
|
|
21
|
+
* are rethrown — they must not be treated as "not found".
|
|
22
|
+
*
|
|
23
|
+
* @param {string} namespace
|
|
24
|
+
* @param {{
|
|
25
|
+
* kubectlArgs?: (args: string[]) => string[],
|
|
26
|
+
* getKubectlEnv?: () => NodeJS.ProcessEnv,
|
|
27
|
+
* execaFn?: typeof execa,
|
|
28
|
+
* }} [options]
|
|
29
|
+
* @returns {Promise<boolean>}
|
|
30
|
+
*/
|
|
31
|
+
export async function namespaceExists(namespace, options = {}) {
|
|
32
|
+
const kubectlArgs = options.kubectlArgs || ((args) => args);
|
|
33
|
+
const getKubectlEnv = options.getKubectlEnv || (() => process.env);
|
|
34
|
+
const execaFn = options.execaFn || execa;
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const { stdout } = await execaFn(
|
|
38
|
+
'kubectl',
|
|
39
|
+
kubectlArgs(['get', 'namespace', namespace, '--ignore-not-found', '-o', 'name']),
|
|
40
|
+
{
|
|
41
|
+
stdio: 'pipe',
|
|
42
|
+
env: getKubectlEnv(),
|
|
43
|
+
}
|
|
44
|
+
);
|
|
45
|
+
return Boolean(stdout && String(stdout).trim());
|
|
46
|
+
} catch (err) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
`Failed to check whether namespace '${namespace}' exists: ${kubectlErrorDetail(err)}`,
|
|
49
|
+
{ cause: err instanceof Error ? err : undefined }
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @param {string} namespace
|
|
56
|
+
* @returns {Promise<boolean>}
|
|
57
|
+
*/
|
|
58
|
+
async function defaultConfirmCreate(namespace) {
|
|
59
|
+
const { create } = await inquirer.prompt([
|
|
60
|
+
{
|
|
61
|
+
type: 'confirm',
|
|
62
|
+
name: 'create',
|
|
63
|
+
message: `Namespace '${namespace}' does not exist. Create it now?`,
|
|
64
|
+
default: true,
|
|
65
|
+
},
|
|
66
|
+
]);
|
|
67
|
+
return Boolean(create);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Ensure the target namespace exists before kubectl apply.
|
|
72
|
+
* Interactive: prompt to create. CI / non-TTY: auto-create with a clear log.
|
|
73
|
+
* Connectivity/auth failures from the existence check abort before create/prompt.
|
|
74
|
+
*
|
|
75
|
+
* @param {{
|
|
76
|
+
* namespace: string,
|
|
77
|
+
* log: { info: Function, warn: Function, success: Function, error?: Function },
|
|
78
|
+
* kubectlArgs?: (args: string[]) => string[],
|
|
79
|
+
* getKubectlEnv?: () => NodeJS.ProcessEnv,
|
|
80
|
+
* execaFn?: typeof execa,
|
|
81
|
+
* confirmFn?: (namespace: string) => Promise<boolean>,
|
|
82
|
+
* interactive?: boolean,
|
|
83
|
+
* }} options
|
|
84
|
+
* @returns {Promise<{ existed: boolean, created: boolean }>}
|
|
85
|
+
*/
|
|
86
|
+
export async function ensureKubernetesNamespace(options) {
|
|
87
|
+
const {
|
|
88
|
+
namespace,
|
|
89
|
+
log,
|
|
90
|
+
kubectlArgs = (args) => args,
|
|
91
|
+
getKubectlEnv = () => process.env,
|
|
92
|
+
execaFn = execa,
|
|
93
|
+
confirmFn = defaultConfirmCreate,
|
|
94
|
+
interactive = isInteractive(),
|
|
95
|
+
} = options;
|
|
96
|
+
|
|
97
|
+
if (!namespace) {
|
|
98
|
+
throw new Error('Kubernetes namespace is empty — set KUBE_NAMESPACE or config.project.');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Throws on auth/connectivity/kubeconfig errors — only false means genuine NotFound.
|
|
102
|
+
if (await namespaceExists(namespace, { kubectlArgs, getKubectlEnv, execaFn })) {
|
|
103
|
+
return { existed: true, created: false };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
log.warn(`Namespace '${namespace}' was not found on the cluster.`);
|
|
107
|
+
|
|
108
|
+
let shouldCreate = true;
|
|
109
|
+
if (interactive) {
|
|
110
|
+
shouldCreate = await confirmFn(namespace);
|
|
111
|
+
if (!shouldCreate) {
|
|
112
|
+
throw new Error(
|
|
113
|
+
`Namespace '${namespace}' does not exist. Create it manually with: kubectl create namespace ${namespace}`
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
} else {
|
|
117
|
+
log.info(
|
|
118
|
+
`Non-interactive session detected — creating namespace '${namespace}' automatically.`
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
await execaFn('kubectl', kubectlArgs(['create', 'namespace', namespace]), {
|
|
123
|
+
stdio: 'inherit',
|
|
124
|
+
env: getKubectlEnv(),
|
|
125
|
+
});
|
|
126
|
+
log.success(`Created namespace '${namespace}'`);
|
|
127
|
+
return { existed: false, created: true };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export default { namespaceExists, ensureKubernetesNamespace };
|