kustodian 1.0.0
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/LICENSE +21 -0
- package/README.md +346 -0
- package/dist/cli/bin.d.ts +3 -0
- package/dist/cli/bin.d.ts.map +1 -0
- package/dist/cli/bin.js +67 -0
- package/dist/cli/bin.js.map +1 -0
- package/dist/cli/command.d.ts +57 -0
- package/dist/cli/command.d.ts.map +1 -0
- package/dist/cli/command.js +26 -0
- package/dist/cli/command.js.map +1 -0
- package/dist/cli/commands/apply.d.ts +8 -0
- package/dist/cli/commands/apply.d.ts.map +1 -0
- package/dist/cli/commands/apply.js +802 -0
- package/dist/cli/commands/apply.js.map +1 -0
- package/dist/cli/commands/init.d.ts +5 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/init.js +403 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/sources.d.ts +5 -0
- package/dist/cli/commands/sources.d.ts.map +1 -0
- package/dist/cli/commands/sources.js +377 -0
- package/dist/cli/commands/sources.js.map +1 -0
- package/dist/cli/commands/update.d.ts +5 -0
- package/dist/cli/commands/update.d.ts.map +1 -0
- package/dist/cli/commands/update.js +314 -0
- package/dist/cli/commands/update.js.map +1 -0
- package/dist/cli/commands/validate.d.ts +5 -0
- package/dist/cli/commands/validate.d.ts.map +1 -0
- package/dist/cli/commands/validate.js +126 -0
- package/dist/cli/commands/validate.js.map +1 -0
- package/dist/cli/container.d.ts +48 -0
- package/dist/cli/container.d.ts.map +1 -0
- package/dist/cli/container.js +49 -0
- package/dist/cli/container.js.map +1 -0
- package/dist/cli/index.d.ts +8 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +9 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/middleware.d.ts +118 -0
- package/dist/cli/middleware.d.ts.map +1 -0
- package/dist/cli/middleware.js +280 -0
- package/dist/cli/middleware.js.map +1 -0
- package/dist/cli/runner.d.ts +34 -0
- package/dist/cli/runner.d.ts.map +1 -0
- package/dist/cli/runner.js +156 -0
- package/dist/cli/runner.js.map +1 -0
- package/dist/cli/utils/defaults.d.ts +23 -0
- package/dist/cli/utils/defaults.d.ts.map +1 -0
- package/dist/cli/utils/defaults.js +43 -0
- package/dist/cli/utils/defaults.js.map +1 -0
- package/dist/core/error.d.ts +117 -0
- package/dist/core/error.d.ts.map +1 -0
- package/dist/core/error.js +210 -0
- package/dist/core/error.js.map +1 -0
- package/dist/core/index.d.ts +6 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +6 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/logger.d.ts +31 -0
- package/dist/core/logger.d.ts.map +1 -0
- package/dist/core/logger.js +78 -0
- package/dist/core/logger.js.map +1 -0
- package/dist/core/path.d.ts +57 -0
- package/dist/core/path.d.ts.map +1 -0
- package/dist/core/path.js +154 -0
- package/dist/core/path.js.map +1 -0
- package/dist/core/result.d.ts +80 -0
- package/dist/core/result.d.ts.map +1 -0
- package/dist/core/result.js +150 -0
- package/dist/core/result.js.map +1 -0
- package/dist/core/types.d.ts +80 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +5 -0
- package/dist/core/types.js.map +1 -0
- package/dist/generator/external-substitutions.d.ts +21 -0
- package/dist/generator/external-substitutions.d.ts.map +1 -0
- package/dist/generator/external-substitutions.js +49 -0
- package/dist/generator/external-substitutions.js.map +1 -0
- package/dist/generator/flux.d.ts +61 -0
- package/dist/generator/flux.d.ts.map +1 -0
- package/dist/generator/flux.js +294 -0
- package/dist/generator/flux.js.map +1 -0
- package/dist/generator/generator.d.ts +77 -0
- package/dist/generator/generator.d.ts.map +1 -0
- package/dist/generator/generator.js +183 -0
- package/dist/generator/generator.js.map +1 -0
- package/dist/generator/index.d.ts +9 -0
- package/dist/generator/index.d.ts.map +1 -0
- package/dist/generator/index.js +9 -0
- package/dist/generator/index.js.map +1 -0
- package/dist/generator/kustomization-resolution.d.ts +40 -0
- package/dist/generator/kustomization-resolution.d.ts.map +1 -0
- package/dist/generator/kustomization-resolution.js +59 -0
- package/dist/generator/kustomization-resolution.js.map +1 -0
- package/dist/generator/namespace.d.ts +74 -0
- package/dist/generator/namespace.d.ts.map +1 -0
- package/dist/generator/namespace.js +109 -0
- package/dist/generator/namespace.js.map +1 -0
- package/dist/generator/output.d.ts +56 -0
- package/dist/generator/output.d.ts.map +1 -0
- package/dist/generator/output.js +171 -0
- package/dist/generator/output.js.map +1 -0
- package/dist/generator/preservation.d.ts +45 -0
- package/dist/generator/preservation.d.ts.map +1 -0
- package/dist/generator/preservation.js +71 -0
- package/dist/generator/preservation.js.map +1 -0
- package/dist/generator/substitution.d.ts +79 -0
- package/dist/generator/substitution.d.ts.map +1 -0
- package/dist/generator/substitution.js +210 -0
- package/dist/generator/substitution.js.map +1 -0
- package/dist/generator/types.d.ts +162 -0
- package/dist/generator/types.d.ts.map +1 -0
- package/dist/generator/types.js +2 -0
- package/dist/generator/types.js.map +1 -0
- package/dist/generator/validation/cycle-detection.d.ts +25 -0
- package/dist/generator/validation/cycle-detection.d.ts.map +1 -0
- package/dist/generator/validation/cycle-detection.js +101 -0
- package/dist/generator/validation/cycle-detection.js.map +1 -0
- package/dist/generator/validation/enablement.d.ts +24 -0
- package/dist/generator/validation/enablement.d.ts.map +1 -0
- package/dist/generator/validation/enablement.js +69 -0
- package/dist/generator/validation/enablement.js.map +1 -0
- package/dist/generator/validation/graph.d.ts +29 -0
- package/dist/generator/validation/graph.d.ts.map +1 -0
- package/dist/generator/validation/graph.js +106 -0
- package/dist/generator/validation/graph.js.map +1 -0
- package/dist/generator/validation/index.d.ts +38 -0
- package/dist/generator/validation/index.d.ts.map +1 -0
- package/dist/generator/validation/index.js +64 -0
- package/dist/generator/validation/index.js.map +1 -0
- package/dist/generator/validation/reference.d.ts +45 -0
- package/dist/generator/validation/reference.d.ts.map +1 -0
- package/dist/generator/validation/reference.js +143 -0
- package/dist/generator/validation/reference.js.map +1 -0
- package/dist/generator/validation/requirements.d.ts +25 -0
- package/dist/generator/validation/requirements.d.ts.map +1 -0
- package/dist/generator/validation/requirements.js +64 -0
- package/dist/generator/validation/requirements.js.map +1 -0
- package/dist/generator/validation/types.d.ts +116 -0
- package/dist/generator/validation/types.d.ts.map +1 -0
- package/dist/generator/validation/types.js +13 -0
- package/dist/generator/validation/types.js.map +1 -0
- package/dist/k8s/exec.d.ts +20 -0
- package/dist/k8s/exec.d.ts.map +1 -0
- package/dist/k8s/exec.js +47 -0
- package/dist/k8s/exec.js.map +1 -0
- package/dist/k8s/flux.d.ts +57 -0
- package/dist/k8s/flux.d.ts.map +1 -0
- package/dist/k8s/flux.js +202 -0
- package/dist/k8s/flux.js.map +1 -0
- package/dist/k8s/index.d.ts +6 -0
- package/dist/k8s/index.d.ts.map +1 -0
- package/dist/k8s/index.js +6 -0
- package/dist/k8s/index.js.map +1 -0
- package/dist/k8s/kubeconfig.d.ts +45 -0
- package/dist/k8s/kubeconfig.d.ts.map +1 -0
- package/dist/k8s/kubeconfig.js +152 -0
- package/dist/k8s/kubeconfig.js.map +1 -0
- package/dist/k8s/kubectl.d.ts +68 -0
- package/dist/k8s/kubectl.d.ts.map +1 -0
- package/dist/k8s/kubectl.js +187 -0
- package/dist/k8s/kubectl.js.map +1 -0
- package/dist/k8s/types.d.ts +65 -0
- package/dist/k8s/types.d.ts.map +1 -0
- package/dist/k8s/types.js +2 -0
- package/dist/k8s/types.js.map +1 -0
- package/dist/loader/file.d.ts +39 -0
- package/dist/loader/file.d.ts.map +1 -0
- package/dist/loader/file.js +121 -0
- package/dist/loader/file.js.map +1 -0
- package/dist/loader/index.d.ts +5 -0
- package/dist/loader/index.d.ts.map +1 -0
- package/dist/loader/index.js +5 -0
- package/dist/loader/index.js.map +1 -0
- package/dist/loader/profile.d.ts +23 -0
- package/dist/loader/profile.d.ts.map +1 -0
- package/dist/loader/profile.js +84 -0
- package/dist/loader/profile.js.map +1 -0
- package/dist/loader/project.d.ts +76 -0
- package/dist/loader/project.d.ts.map +1 -0
- package/dist/loader/project.js +279 -0
- package/dist/loader/project.js.map +1 -0
- package/dist/loader/yaml.d.ts +15 -0
- package/dist/loader/yaml.d.ts.map +1 -0
- package/dist/loader/yaml.js +44 -0
- package/dist/loader/yaml.js.map +1 -0
- package/dist/nodes/index.d.ts +5 -0
- package/dist/nodes/index.d.ts.map +1 -0
- package/dist/nodes/index.js +5 -0
- package/dist/nodes/index.js.map +1 -0
- package/dist/nodes/kubectl-labeler.d.ts +7 -0
- package/dist/nodes/kubectl-labeler.d.ts.map +1 -0
- package/dist/nodes/kubectl-labeler.js +89 -0
- package/dist/nodes/kubectl-labeler.js.map +1 -0
- package/dist/nodes/labeler.d.ts +74 -0
- package/dist/nodes/labeler.d.ts.map +1 -0
- package/dist/nodes/labeler.js +118 -0
- package/dist/nodes/labeler.js.map +1 -0
- package/dist/nodes/profile.d.ts +58 -0
- package/dist/nodes/profile.d.ts.map +1 -0
- package/dist/nodes/profile.js +128 -0
- package/dist/nodes/profile.js.map +1 -0
- package/dist/nodes/types.d.ts +84 -0
- package/dist/nodes/types.d.ts.map +1 -0
- package/dist/nodes/types.js +77 -0
- package/dist/nodes/types.js.map +1 -0
- package/dist/plugins/generators.d.ts +49 -0
- package/dist/plugins/generators.d.ts.map +1 -0
- package/dist/plugins/generators.js +7 -0
- package/dist/plugins/generators.js.map +1 -0
- package/dist/plugins/hooks.d.ts +120 -0
- package/dist/plugins/hooks.d.ts.map +1 -0
- package/dist/plugins/hooks.js +41 -0
- package/dist/plugins/hooks.js.map +1 -0
- package/dist/plugins/index.d.ts +8 -0
- package/dist/plugins/index.d.ts.map +1 -0
- package/dist/plugins/index.js +15 -0
- package/dist/plugins/index.js.map +1 -0
- package/dist/plugins/loader.d.ts +40 -0
- package/dist/plugins/loader.d.ts.map +1 -0
- package/dist/plugins/loader.js +236 -0
- package/dist/plugins/loader.js.map +1 -0
- package/dist/plugins/object-types.d.ts +71 -0
- package/dist/plugins/object-types.d.ts.map +1 -0
- package/dist/plugins/object-types.js +65 -0
- package/dist/plugins/object-types.js.map +1 -0
- package/dist/plugins/registry.d.ts +87 -0
- package/dist/plugins/registry.d.ts.map +1 -0
- package/dist/plugins/registry.js +158 -0
- package/dist/plugins/registry.js.map +1 -0
- package/dist/plugins/substitution-providers.d.ts +48 -0
- package/dist/plugins/substitution-providers.d.ts.map +1 -0
- package/dist/plugins/substitution-providers.js +2 -0
- package/dist/plugins/substitution-providers.js.map +1 -0
- package/dist/plugins/types.d.ts +288 -0
- package/dist/plugins/types.d.ts.map +1 -0
- package/dist/plugins/types.js +25 -0
- package/dist/plugins/types.js.map +1 -0
- package/dist/registry/auth.d.ts +14 -0
- package/dist/registry/auth.d.ts.map +1 -0
- package/dist/registry/auth.js +40 -0
- package/dist/registry/auth.js.map +1 -0
- package/dist/registry/client.d.ts +24 -0
- package/dist/registry/client.d.ts.map +1 -0
- package/dist/registry/client.js +85 -0
- package/dist/registry/client.js.map +1 -0
- package/dist/registry/dockerhub.d.ts +6 -0
- package/dist/registry/dockerhub.d.ts.map +1 -0
- package/dist/registry/dockerhub.js +91 -0
- package/dist/registry/dockerhub.js.map +1 -0
- package/dist/registry/ghcr.d.ts +6 -0
- package/dist/registry/ghcr.d.ts.map +1 -0
- package/dist/registry/ghcr.js +95 -0
- package/dist/registry/ghcr.js.map +1 -0
- package/dist/registry/helm.d.ts +11 -0
- package/dist/registry/helm.d.ts.map +1 -0
- package/dist/registry/helm.js +124 -0
- package/dist/registry/helm.js.map +1 -0
- package/dist/registry/index.d.ts +8 -0
- package/dist/registry/index.d.ts.map +1 -0
- package/dist/registry/index.js +12 -0
- package/dist/registry/index.js.map +1 -0
- package/dist/registry/types.d.ts +55 -0
- package/dist/registry/types.d.ts.map +1 -0
- package/dist/registry/types.js +2 -0
- package/dist/registry/types.js.map +1 -0
- package/dist/registry/version.d.ts +22 -0
- package/dist/registry/version.d.ts.map +1 -0
- package/dist/registry/version.js +62 -0
- package/dist/registry/version.js.map +1 -0
- package/dist/schema/cluster.d.ts +2890 -0
- package/dist/schema/cluster.d.ts.map +1 -0
- package/dist/schema/cluster.js +231 -0
- package/dist/schema/cluster.js.map +1 -0
- package/dist/schema/common.d.ts +1059 -0
- package/dist/schema/common.d.ts.map +1 -0
- package/dist/schema/common.js +340 -0
- package/dist/schema/common.js.map +1 -0
- package/dist/schema/index.d.ts +8 -0
- package/dist/schema/index.d.ts.map +1 -0
- package/dist/schema/index.js +8 -0
- package/dist/schema/index.js.map +1 -0
- package/dist/schema/node-list.d.ts +361 -0
- package/dist/schema/node-list.d.ts.map +1 -0
- package/dist/schema/node-list.js +104 -0
- package/dist/schema/node-list.js.map +1 -0
- package/dist/schema/profile.d.ts +166 -0
- package/dist/schema/profile.d.ts.map +1 -0
- package/dist/schema/profile.js +61 -0
- package/dist/schema/profile.js.map +1 -0
- package/dist/schema/project.d.ts +168 -0
- package/dist/schema/project.d.ts.map +1 -0
- package/dist/schema/project.js +40 -0
- package/dist/schema/project.js.map +1 -0
- package/dist/schema/sources.d.ts +338 -0
- package/dist/schema/sources.d.ts.map +1 -0
- package/dist/schema/sources.js +111 -0
- package/dist/schema/sources.js.map +1 -0
- package/dist/schema/template.d.ts +2711 -0
- package/dist/schema/template.d.ts.map +1 -0
- package/dist/schema/template.js +97 -0
- package/dist/schema/template.js.map +1 -0
- package/dist/sources/cache/index.d.ts +9 -0
- package/dist/sources/cache/index.d.ts.map +1 -0
- package/dist/sources/cache/index.js +278 -0
- package/dist/sources/cache/index.js.map +1 -0
- package/dist/sources/cache/metadata.d.ts +36 -0
- package/dist/sources/cache/metadata.d.ts.map +1 -0
- package/dist/sources/cache/metadata.js +21 -0
- package/dist/sources/cache/metadata.js.map +1 -0
- package/dist/sources/cache/ttl.d.ts +20 -0
- package/dist/sources/cache/ttl.d.ts.map +1 -0
- package/dist/sources/cache/ttl.js +53 -0
- package/dist/sources/cache/ttl.js.map +1 -0
- package/dist/sources/fetchers/git.d.ts +6 -0
- package/dist/sources/fetchers/git.d.ts.map +1 -0
- package/dist/sources/fetchers/git.js +165 -0
- package/dist/sources/fetchers/git.js.map +1 -0
- package/dist/sources/fetchers/http.d.ts +6 -0
- package/dist/sources/fetchers/http.d.ts.map +1 -0
- package/dist/sources/fetchers/http.js +159 -0
- package/dist/sources/fetchers/http.js.map +1 -0
- package/dist/sources/fetchers/index.d.ts +11 -0
- package/dist/sources/fetchers/index.d.ts.map +1 -0
- package/dist/sources/fetchers/index.js +24 -0
- package/dist/sources/fetchers/index.js.map +1 -0
- package/dist/sources/fetchers/oci.d.ts +6 -0
- package/dist/sources/fetchers/oci.d.ts.map +1 -0
- package/dist/sources/fetchers/oci.js +156 -0
- package/dist/sources/fetchers/oci.js.map +1 -0
- package/dist/sources/fetchers/types.d.ts +25 -0
- package/dist/sources/fetchers/types.d.ts.map +1 -0
- package/dist/sources/fetchers/types.js +2 -0
- package/dist/sources/fetchers/types.js.map +1 -0
- package/dist/sources/index.d.ts +6 -0
- package/dist/sources/index.d.ts.map +1 -0
- package/dist/sources/index.js +10 -0
- package/dist/sources/index.js.map +1 -0
- package/dist/sources/loader.d.ts +34 -0
- package/dist/sources/loader.d.ts.map +1 -0
- package/dist/sources/loader.js +89 -0
- package/dist/sources/loader.js.map +1 -0
- package/dist/sources/resolver.d.ts +17 -0
- package/dist/sources/resolver.d.ts.map +1 -0
- package/dist/sources/resolver.js +125 -0
- package/dist/sources/resolver.js.map +1 -0
- package/dist/sources/types.d.ts +151 -0
- package/dist/sources/types.d.ts.map +1 -0
- package/dist/sources/types.js +2 -0
- package/dist/sources/types.js.map +1 -0
- package/package.json +101 -0
|
@@ -0,0 +1,802 @@
|
|
|
1
|
+
import { exec } from 'node:child_process';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { createInterface } from 'node:readline';
|
|
4
|
+
import { promisify } from 'node:util';
|
|
5
|
+
import { is_success, success } from '../../core/index.js';
|
|
6
|
+
import { validate_template_requirements } from '../../generator/index.js';
|
|
7
|
+
import { find_project_root, load_project } from '../../loader/index.js';
|
|
8
|
+
import { define_command } from '../command.js';
|
|
9
|
+
import { resolve_defaults } from '../utils/defaults.js';
|
|
10
|
+
const execAsync = promisify(exec);
|
|
11
|
+
/**
|
|
12
|
+
* Apply command - orchestrates full cluster setup:
|
|
13
|
+
* 1. Bootstrap nodes with k0s
|
|
14
|
+
* 2. Install Flux CD
|
|
15
|
+
* 3. Deploy templates
|
|
16
|
+
*/
|
|
17
|
+
export const apply_command = define_command({
|
|
18
|
+
name: 'apply',
|
|
19
|
+
description: 'Apply full cluster configuration (bootstrap + Flux + templates)',
|
|
20
|
+
options: [
|
|
21
|
+
{
|
|
22
|
+
name: 'cluster',
|
|
23
|
+
short: 'c',
|
|
24
|
+
description: 'Cluster name to apply',
|
|
25
|
+
type: 'string',
|
|
26
|
+
required: true,
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: 'provider',
|
|
30
|
+
short: 'P',
|
|
31
|
+
description: 'Cluster provider for bootstrap (default: k0s)',
|
|
32
|
+
type: 'string',
|
|
33
|
+
default_value: 'k0s',
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: 'project',
|
|
37
|
+
short: 'p',
|
|
38
|
+
description: 'Path to project root',
|
|
39
|
+
type: 'string',
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: 'dry-run',
|
|
43
|
+
short: 'd',
|
|
44
|
+
description: 'Preview what would happen without making changes',
|
|
45
|
+
type: 'boolean',
|
|
46
|
+
default_value: false,
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: 'skip-bootstrap',
|
|
50
|
+
description: 'Skip cluster bootstrap (use existing cluster)',
|
|
51
|
+
type: 'boolean',
|
|
52
|
+
default_value: false,
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: 'skip-flux',
|
|
56
|
+
description: 'Skip Flux CD installation',
|
|
57
|
+
type: 'boolean',
|
|
58
|
+
default_value: false,
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: 'skip-templates',
|
|
62
|
+
description: 'Skip template deployment',
|
|
63
|
+
type: 'boolean',
|
|
64
|
+
default_value: false,
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
handler: async (ctx) => {
|
|
68
|
+
const cluster_name = ctx.options['cluster'];
|
|
69
|
+
const provider_name = ctx.options['provider'];
|
|
70
|
+
const project_path = ctx.options['project'] || process.cwd();
|
|
71
|
+
const dry_run = ctx.options['dry-run'];
|
|
72
|
+
const skip_bootstrap = ctx.options['skip-bootstrap'];
|
|
73
|
+
const skip_flux = ctx.options['skip-flux'];
|
|
74
|
+
const skip_templates = ctx.options['skip-templates'];
|
|
75
|
+
if (!cluster_name) {
|
|
76
|
+
console.error('Error: --cluster is required');
|
|
77
|
+
return {
|
|
78
|
+
success: false,
|
|
79
|
+
error: { code: 'INVALID_ARGS', message: '--cluster is required' },
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
console.log('\n━━━ Kustodian Apply ━━━');
|
|
83
|
+
console.log(`Cluster: ${cluster_name}`);
|
|
84
|
+
console.log(`Provider: ${provider_name}`);
|
|
85
|
+
if (dry_run) {
|
|
86
|
+
console.log('Mode: DRY RUN\n');
|
|
87
|
+
}
|
|
88
|
+
// ===== PHASE 1: Load Project =====
|
|
89
|
+
console.log('\n[1/3] Loading project configuration...');
|
|
90
|
+
const root_result = await find_project_root(project_path);
|
|
91
|
+
if (!is_success(root_result)) {
|
|
92
|
+
console.error(` ✗ Error: ${root_result.error.message}`);
|
|
93
|
+
return root_result;
|
|
94
|
+
}
|
|
95
|
+
const project_root = root_result.value;
|
|
96
|
+
console.log(` → Project root: ${project_root}`);
|
|
97
|
+
const project_result = await load_project(project_root);
|
|
98
|
+
if (!is_success(project_result)) {
|
|
99
|
+
console.error(` ✗ Error: ${project_result.error.message}`);
|
|
100
|
+
return project_result;
|
|
101
|
+
}
|
|
102
|
+
const project = project_result.value;
|
|
103
|
+
const loaded_cluster = project.clusters.find((c) => c.cluster.metadata.name === cluster_name);
|
|
104
|
+
if (!loaded_cluster) {
|
|
105
|
+
console.error(` ✗ Error: Cluster '${cluster_name}' not found`);
|
|
106
|
+
return {
|
|
107
|
+
success: false,
|
|
108
|
+
error: { code: 'NOT_FOUND', message: `Cluster '${cluster_name}' not found` },
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
console.log(' ✓ Loaded cluster configuration');
|
|
112
|
+
console.log(` ✓ Loaded ${project.templates.length} templates`);
|
|
113
|
+
console.log(` ✓ Loaded ${loaded_cluster.nodes.length} nodes`);
|
|
114
|
+
// Resolve cluster defaults (Flux namespace, OCI secret names, etc.)
|
|
115
|
+
const defaults = resolve_defaults(loaded_cluster.cluster, project.config);
|
|
116
|
+
const FLUX_NAMESPACE = defaults.flux_namespace;
|
|
117
|
+
const OCI_REGISTRY_SECRET_NAME = defaults.oci_registry_secret_name;
|
|
118
|
+
// ===== PHASE 2: Bootstrap Cluster =====
|
|
119
|
+
if (!skip_bootstrap) {
|
|
120
|
+
console.log('\n[2/3] Checking cluster status...');
|
|
121
|
+
// Check if cluster is already accessible
|
|
122
|
+
const { exec } = await import('node:child_process');
|
|
123
|
+
const { promisify } = await import('node:util');
|
|
124
|
+
const execAsync = promisify(exec);
|
|
125
|
+
let cluster_exists = false;
|
|
126
|
+
try {
|
|
127
|
+
await execAsync('kubectl cluster-info', { timeout: 5000 });
|
|
128
|
+
console.log(' ✓ Cluster is already running and accessible');
|
|
129
|
+
cluster_exists = true;
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
console.log(' → No existing cluster detected');
|
|
133
|
+
}
|
|
134
|
+
if (!cluster_exists) {
|
|
135
|
+
console.log(' → Bootstrapping cluster with k0s...');
|
|
136
|
+
// Check if we have nodes to bootstrap
|
|
137
|
+
if (loaded_cluster.nodes.length === 0) {
|
|
138
|
+
console.error(' ✗ Error: No nodes defined for cluster');
|
|
139
|
+
console.error(' → Add nodes to cluster.yaml spec.nodes or create node files in nodes/ directory');
|
|
140
|
+
return {
|
|
141
|
+
success: false,
|
|
142
|
+
error: { code: 'NOT_FOUND', message: 'No nodes defined for cluster' },
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
// Build NodeListType for bootstrap workflow
|
|
146
|
+
const node_list = {
|
|
147
|
+
cluster: cluster_name,
|
|
148
|
+
nodes: loaded_cluster.nodes,
|
|
149
|
+
...(loaded_cluster.cluster.spec.node_defaults?.label_prefix && {
|
|
150
|
+
label_prefix: loaded_cluster.cluster.spec.node_defaults.label_prefix,
|
|
151
|
+
}),
|
|
152
|
+
...(loaded_cluster.cluster.spec.node_defaults?.ssh && {
|
|
153
|
+
ssh: loaded_cluster.cluster.spec.node_defaults.ssh,
|
|
154
|
+
}),
|
|
155
|
+
};
|
|
156
|
+
// Load k0s provider
|
|
157
|
+
const k0s_package = 'kustodian-k0s';
|
|
158
|
+
const { create_k0s_provider } = await import(k0s_package);
|
|
159
|
+
const provider = create_k0s_provider();
|
|
160
|
+
console.log(' → Validating cluster configuration...');
|
|
161
|
+
const validate_result = provider.validate(node_list);
|
|
162
|
+
if (!is_success(validate_result)) {
|
|
163
|
+
console.error(` ✗ Validation failed: ${validate_result.error.message}`);
|
|
164
|
+
return validate_result;
|
|
165
|
+
}
|
|
166
|
+
console.log(' ✓ Configuration valid');
|
|
167
|
+
console.log(' → Installing k0s cluster...');
|
|
168
|
+
if (dry_run) {
|
|
169
|
+
console.log(' [dry-run] Would run: k0sctl apply');
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
const install_result = await provider.install(node_list, { dry_run: false });
|
|
173
|
+
if (!is_success(install_result)) {
|
|
174
|
+
console.error(` ✗ Installation failed: ${install_result.error.message}`);
|
|
175
|
+
return install_result;
|
|
176
|
+
}
|
|
177
|
+
console.log(' ✓ k0s cluster installed');
|
|
178
|
+
console.log(' → Retrieving kubeconfig...');
|
|
179
|
+
const kubeconfig_result = await provider.get_kubeconfig(node_list);
|
|
180
|
+
if (!is_success(kubeconfig_result)) {
|
|
181
|
+
console.error(` ✗ Failed to get kubeconfig: ${kubeconfig_result.error.message}`);
|
|
182
|
+
return kubeconfig_result;
|
|
183
|
+
}
|
|
184
|
+
console.log(` ✓ Kubeconfig: ${kubeconfig_result.value}`);
|
|
185
|
+
console.log(' → Waiting for cluster nodes to be ready...');
|
|
186
|
+
try {
|
|
187
|
+
await execAsync('kubectl wait --for=condition=Ready node --all --timeout=300s', {
|
|
188
|
+
timeout: 320000,
|
|
189
|
+
});
|
|
190
|
+
console.log(' ✓ All nodes are ready');
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
console.log(' ⚠ Some nodes may not be ready yet');
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
console.log(' ✓ Cluster bootstrapped successfully');
|
|
197
|
+
if (!dry_run) {
|
|
198
|
+
console.log(' → Allowing control plane to stabilize...');
|
|
199
|
+
await new Promise((resolve) => setTimeout(resolve, 5000));
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
console.log('\n[2/3] Skipping bootstrap (using existing cluster)');
|
|
205
|
+
}
|
|
206
|
+
// ===== PHASE 3: Install Flux CD =====
|
|
207
|
+
if (!skip_flux) {
|
|
208
|
+
console.log('\n[3/3] Checking Flux CD status...');
|
|
209
|
+
const { exec } = await import('node:child_process');
|
|
210
|
+
const { promisify } = await import('node:util');
|
|
211
|
+
const execAsync = promisify(exec);
|
|
212
|
+
let flux_installed = false;
|
|
213
|
+
try {
|
|
214
|
+
const { stdout } = await execAsync('kubectl get namespace flux-system', { timeout: 5000 });
|
|
215
|
+
if (stdout.includes('flux-system')) {
|
|
216
|
+
console.log(' ✓ Flux CD is already installed');
|
|
217
|
+
flux_installed = true;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
console.log(' → Flux CD not detected');
|
|
222
|
+
}
|
|
223
|
+
if (!flux_installed) {
|
|
224
|
+
console.log(' → Installing Flux CD...');
|
|
225
|
+
// Check if flux CLI is available
|
|
226
|
+
try {
|
|
227
|
+
await execAsync('flux --version', { timeout: 5000 });
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
console.error(' ✗ Error: flux CLI not found');
|
|
231
|
+
console.error(' → Install with: brew install fluxcd/tap/flux');
|
|
232
|
+
return {
|
|
233
|
+
success: false,
|
|
234
|
+
error: { code: 'MISSING_DEPENDENCY', message: 'flux CLI not found' },
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
if (dry_run) {
|
|
238
|
+
console.log(' [dry-run] Would run: flux install');
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
try {
|
|
242
|
+
console.log(' Running: flux install');
|
|
243
|
+
const { stderr } = await execAsync('flux install', {
|
|
244
|
+
timeout: 300000, // 5 minutes timeout
|
|
245
|
+
});
|
|
246
|
+
if (stderr && !stderr.includes('successfully')) {
|
|
247
|
+
console.log(` ${stderr}`);
|
|
248
|
+
}
|
|
249
|
+
console.log(' ✓ Flux CD installed successfully');
|
|
250
|
+
}
|
|
251
|
+
catch (error) {
|
|
252
|
+
const err = error;
|
|
253
|
+
console.error(` ✗ Flux installation failed: ${err.message || err.stderr}`);
|
|
254
|
+
return {
|
|
255
|
+
success: false,
|
|
256
|
+
error: { code: 'FLUX_INSTALL_FAILED', message: 'Flux installation failed' },
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
// Wait for Flux to be ready
|
|
260
|
+
console.log(' Waiting for Flux components to be ready...');
|
|
261
|
+
try {
|
|
262
|
+
await execAsync('flux check --timeout=2m', { timeout: 150000 });
|
|
263
|
+
console.log(' ✓ Flux components are ready');
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
console.log(' ⚠ Flux components may not be fully ready yet');
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
console.log('\n[3/3] Skipping Flux CD installation');
|
|
273
|
+
}
|
|
274
|
+
// ===== PHASE 4: Configure Secrets =====
|
|
275
|
+
if (!skip_templates) {
|
|
276
|
+
console.log('\n[4/5] Configuring cluster secrets...');
|
|
277
|
+
// Check if Doppler is configured for this cluster
|
|
278
|
+
const doppler_config = loaded_cluster.cluster.spec.secrets?.doppler;
|
|
279
|
+
if (doppler_config?.cluster_secret?.enabled !== false) {
|
|
280
|
+
console.log(' → Checking Doppler token...');
|
|
281
|
+
await ensure_doppler_token_secret(dry_run, doppler_config?.cluster_secret);
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
console.log(' → Doppler cluster secret disabled, skipping');
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
// ===== PHASE 5: Deploy Templates =====
|
|
288
|
+
if (!skip_templates) {
|
|
289
|
+
console.log('\n[5/5] Deploying templates...');
|
|
290
|
+
// Validate template requirements
|
|
291
|
+
console.log(' → Validating template requirements...');
|
|
292
|
+
// All templates listed in cluster.yaml are deployed (opt-in model)
|
|
293
|
+
const enabled_template_refs = loaded_cluster.cluster.spec.templates || [];
|
|
294
|
+
if (enabled_template_refs.length > 0) {
|
|
295
|
+
const enabled_templates = project.templates
|
|
296
|
+
.filter((t) => enabled_template_refs.some((ref) => ref.name === t.template.metadata.name))
|
|
297
|
+
.map((t) => t.template);
|
|
298
|
+
const requirements_result = validate_template_requirements(enabled_templates, loaded_cluster.nodes);
|
|
299
|
+
if (!requirements_result.valid) {
|
|
300
|
+
console.error(' ✗ Template requirement validation failed:');
|
|
301
|
+
for (const error of requirements_result.errors) {
|
|
302
|
+
console.error(` - ${error.template}: ${error.message}`);
|
|
303
|
+
}
|
|
304
|
+
return {
|
|
305
|
+
success: false,
|
|
306
|
+
error: {
|
|
307
|
+
code: 'REQUIREMENT_VALIDATION_ERROR',
|
|
308
|
+
message: 'Template requirements not met',
|
|
309
|
+
},
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
console.log(' ✓ All template requirements satisfied');
|
|
313
|
+
}
|
|
314
|
+
if (loaded_cluster.cluster.spec.oci) {
|
|
315
|
+
// OCI Mode - generate in memory and apply directly
|
|
316
|
+
console.log(' → Cluster uses OCI deployment');
|
|
317
|
+
console.log(' → Generating Flux resources...');
|
|
318
|
+
const { create_generator, serialize_resource } = await import('../../generator/index.js');
|
|
319
|
+
const oci_repository_name = defaults.oci_repository_name;
|
|
320
|
+
// Build template paths map - maps template name to relative path from templates/
|
|
321
|
+
const templates_dir = path.join(project_root, 'templates');
|
|
322
|
+
const template_paths = new Map();
|
|
323
|
+
for (const t of project.templates) {
|
|
324
|
+
// Get relative path from templates directory
|
|
325
|
+
const relative_path = path.relative(templates_dir, t.path);
|
|
326
|
+
template_paths.set(t.template.metadata.name, relative_path);
|
|
327
|
+
}
|
|
328
|
+
const generator = create_generator({
|
|
329
|
+
flux_namespace: defaults.flux_namespace,
|
|
330
|
+
git_repository_name: oci_repository_name,
|
|
331
|
+
template_paths,
|
|
332
|
+
flux_reconciliation_interval: defaults.flux_reconciliation_interval,
|
|
333
|
+
flux_reconciliation_timeout: defaults.flux_reconciliation_timeout,
|
|
334
|
+
});
|
|
335
|
+
const gen_result = await generator.generate(loaded_cluster.cluster, project.templates.map((t) => t.template), {});
|
|
336
|
+
if (!is_success(gen_result)) {
|
|
337
|
+
console.error(` ✗ Generation failed: ${gen_result.error.message}`);
|
|
338
|
+
return gen_result;
|
|
339
|
+
}
|
|
340
|
+
const gen_data = gen_result.value;
|
|
341
|
+
console.log(` ✓ Generated ${gen_data.kustomizations.length} Flux Kustomizations`);
|
|
342
|
+
// Ensure OCI registry authentication
|
|
343
|
+
const oci_config = loaded_cluster.cluster.spec.oci;
|
|
344
|
+
console.log(' → Checking OCI registry authentication...');
|
|
345
|
+
const auth_result = await ensure_oci_registry_secret(oci_config.registry, dry_run, OCI_REGISTRY_SECRET_NAME, FLUX_NAMESPACE);
|
|
346
|
+
if (dry_run) {
|
|
347
|
+
console.log('\n [dry-run] Would push to OCI and apply Flux resources');
|
|
348
|
+
if (auth_result.has_auth) {
|
|
349
|
+
console.log(` → Secret: ${OCI_REGISTRY_SECRET_NAME} (registry auth)`);
|
|
350
|
+
}
|
|
351
|
+
if (gen_data.oci_repository) {
|
|
352
|
+
console.log(` → OCIRepository: ${gen_data.oci_repository.metadata.name}`);
|
|
353
|
+
}
|
|
354
|
+
for (const k of gen_data.kustomizations) {
|
|
355
|
+
console.log(` → Kustomization: ${k.name} (${k.path})`);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
else {
|
|
359
|
+
// Push to OCI registry
|
|
360
|
+
console.log(' → Pushing to OCI registry...');
|
|
361
|
+
const tag = await get_oci_tag(loaded_cluster.cluster, project_root);
|
|
362
|
+
const oci = loaded_cluster.cluster.spec.oci;
|
|
363
|
+
const oci_url = `oci://${oci.registry}/${oci.repository}:${tag}`;
|
|
364
|
+
try {
|
|
365
|
+
const git_source = await get_git_source(project_root);
|
|
366
|
+
const git_revision = await get_git_revision(project_root);
|
|
367
|
+
const push_cmd = `flux push artifact ${oci_url} --path="${project_root}" --source="${git_source}" --revision="${git_revision}"`;
|
|
368
|
+
await execAsync(push_cmd, { timeout: 120000 });
|
|
369
|
+
console.log(` ✓ Pushed to ${oci_url}`);
|
|
370
|
+
}
|
|
371
|
+
catch (error) {
|
|
372
|
+
const err = error;
|
|
373
|
+
console.error(` ✗ Push failed: ${err.message}`);
|
|
374
|
+
return {
|
|
375
|
+
success: false,
|
|
376
|
+
error: { code: 'PUSH_FAILED', message: err.message },
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
// Apply Flux resources directly (no file writes)
|
|
380
|
+
console.log(' → Applying Flux resources...');
|
|
381
|
+
try {
|
|
382
|
+
// Build combined YAML for all resources
|
|
383
|
+
const resources = [];
|
|
384
|
+
// Add OCIRepository with correct tag and auth
|
|
385
|
+
if (gen_data.oci_repository) {
|
|
386
|
+
const oci_repo = { ...gen_data.oci_repository };
|
|
387
|
+
oci_repo.spec = { ...oci_repo.spec, ref: { tag } };
|
|
388
|
+
// Add secretRef if auth is configured
|
|
389
|
+
if (auth_result.has_auth) {
|
|
390
|
+
oci_repo.spec = {
|
|
391
|
+
...oci_repo.spec,
|
|
392
|
+
secretRef: { name: defaults.oci_registry_secret_name },
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
resources.push(oci_repo);
|
|
396
|
+
}
|
|
397
|
+
// Add all Kustomizations
|
|
398
|
+
for (const k of gen_data.kustomizations) {
|
|
399
|
+
resources.push(k.flux_kustomization);
|
|
400
|
+
}
|
|
401
|
+
// Serialize and apply via stdin
|
|
402
|
+
const yaml_content = resources.map((r) => serialize_resource(r)).join('---\n');
|
|
403
|
+
const { spawn } = await import('node:child_process');
|
|
404
|
+
await new Promise((resolve, reject) => {
|
|
405
|
+
const kubectl = spawn('kubectl', ['apply', '-f', '-'], {
|
|
406
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
407
|
+
});
|
|
408
|
+
let stdout = '';
|
|
409
|
+
let stderr = '';
|
|
410
|
+
kubectl.stdout.on('data', (data) => {
|
|
411
|
+
stdout += data.toString();
|
|
412
|
+
});
|
|
413
|
+
kubectl.stderr.on('data', (data) => {
|
|
414
|
+
stderr += data.toString();
|
|
415
|
+
});
|
|
416
|
+
kubectl.on('close', (code) => {
|
|
417
|
+
if (code === 0) {
|
|
418
|
+
if (stdout)
|
|
419
|
+
console.log(` ${stdout.trim()}`);
|
|
420
|
+
resolve();
|
|
421
|
+
}
|
|
422
|
+
else {
|
|
423
|
+
reject(new Error(stderr || `kubectl exited with code ${code}`));
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
kubectl.stdin.write(yaml_content);
|
|
427
|
+
kubectl.stdin.end();
|
|
428
|
+
});
|
|
429
|
+
console.log(' ✓ Flux resources applied');
|
|
430
|
+
}
|
|
431
|
+
catch (error) {
|
|
432
|
+
const err = error;
|
|
433
|
+
console.error(` ✗ Apply failed: ${err.message}`);
|
|
434
|
+
return {
|
|
435
|
+
success: false,
|
|
436
|
+
error: { code: 'APPLY_FAILED', message: err.message },
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
console.log('\n ✓ Deployment complete - Flux will reconcile from OCI');
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
else {
|
|
443
|
+
console.error(' ✗ Error: Cluster must have spec.oci configured');
|
|
444
|
+
console.error(' → Git-based deployment has been removed');
|
|
445
|
+
return {
|
|
446
|
+
success: false,
|
|
447
|
+
error: { code: 'INVALID_CONFIG', message: 'spec.oci configuration required' },
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
else {
|
|
452
|
+
console.log('\n[4/5] Skipping secrets configuration');
|
|
453
|
+
console.log('\n[5/5] Skipping template deployment');
|
|
454
|
+
}
|
|
455
|
+
console.log('\n━━━ Apply Complete ━━━\n');
|
|
456
|
+
return success(undefined);
|
|
457
|
+
},
|
|
458
|
+
});
|
|
459
|
+
/**
|
|
460
|
+
* Resolves the OCI tag based on cluster strategy.
|
|
461
|
+
*/
|
|
462
|
+
async function get_oci_tag(cluster, project_root) {
|
|
463
|
+
if (!cluster.spec.oci) {
|
|
464
|
+
return 'latest';
|
|
465
|
+
}
|
|
466
|
+
const strategy = cluster.spec.oci.tag_strategy || 'git-sha';
|
|
467
|
+
switch (strategy) {
|
|
468
|
+
case 'cluster':
|
|
469
|
+
return cluster.metadata.name;
|
|
470
|
+
case 'manual':
|
|
471
|
+
return cluster.spec.oci.tag || 'latest';
|
|
472
|
+
case 'version': {
|
|
473
|
+
try {
|
|
474
|
+
const { stdout } = await execAsync('git describe --tags --abbrev=0', { cwd: project_root });
|
|
475
|
+
return stdout.trim();
|
|
476
|
+
}
|
|
477
|
+
catch {
|
|
478
|
+
return 'latest';
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
default: {
|
|
482
|
+
try {
|
|
483
|
+
const { stdout } = await execAsync('git rev-parse --short HEAD', { cwd: project_root });
|
|
484
|
+
return `sha1-${stdout.trim()}`;
|
|
485
|
+
}
|
|
486
|
+
catch {
|
|
487
|
+
return 'latest';
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* Gets the git remote URL for source metadata.
|
|
494
|
+
*/
|
|
495
|
+
async function get_git_source(project_root) {
|
|
496
|
+
try {
|
|
497
|
+
const { stdout } = await execAsync('git config --get remote.origin.url', { cwd: project_root });
|
|
498
|
+
return stdout.trim();
|
|
499
|
+
}
|
|
500
|
+
catch {
|
|
501
|
+
return 'unknown';
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* Gets the current git revision for source metadata.
|
|
506
|
+
*/
|
|
507
|
+
async function get_git_revision(project_root) {
|
|
508
|
+
try {
|
|
509
|
+
const { stdout } = await execAsync('git rev-parse HEAD', { cwd: project_root });
|
|
510
|
+
return `sha1:${stdout.trim()}`;
|
|
511
|
+
}
|
|
512
|
+
catch {
|
|
513
|
+
return 'unknown';
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Prompts for user input with hidden option for sensitive data.
|
|
518
|
+
*/
|
|
519
|
+
async function prompt_for_input(message, hidden = false) {
|
|
520
|
+
const rl = createInterface({
|
|
521
|
+
input: process.stdin,
|
|
522
|
+
output: process.stdout,
|
|
523
|
+
});
|
|
524
|
+
return new Promise((resolve) => {
|
|
525
|
+
if (hidden && process.stdin.isTTY) {
|
|
526
|
+
process.stdout.write(message);
|
|
527
|
+
const stdin = process.stdin;
|
|
528
|
+
stdin.setRawMode?.(true);
|
|
529
|
+
stdin.resume();
|
|
530
|
+
let input = '';
|
|
531
|
+
const onData = (char) => {
|
|
532
|
+
const c = char.toString();
|
|
533
|
+
if (c === '\n' || c === '\r') {
|
|
534
|
+
stdin.setRawMode?.(false);
|
|
535
|
+
stdin.removeListener('data', onData);
|
|
536
|
+
process.stdout.write('\n');
|
|
537
|
+
rl.close();
|
|
538
|
+
resolve(input);
|
|
539
|
+
}
|
|
540
|
+
else if (c === '\u0003') {
|
|
541
|
+
process.exit(1);
|
|
542
|
+
}
|
|
543
|
+
else if (c === '\u007F') {
|
|
544
|
+
if (input.length > 0)
|
|
545
|
+
input = input.slice(0, -1);
|
|
546
|
+
}
|
|
547
|
+
else {
|
|
548
|
+
input += c;
|
|
549
|
+
}
|
|
550
|
+
};
|
|
551
|
+
stdin.on('data', onData);
|
|
552
|
+
}
|
|
553
|
+
else {
|
|
554
|
+
rl.question(message, (answer) => {
|
|
555
|
+
rl.close();
|
|
556
|
+
resolve(answer);
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* Gets the OCI registry token from environment or prompts user.
|
|
563
|
+
*/
|
|
564
|
+
async function get_oci_registry_token(registry) {
|
|
565
|
+
if (registry === 'ghcr.io') {
|
|
566
|
+
// Check environment variables
|
|
567
|
+
const env_token = process.env['GITHUB_TOKEN'] || process.env['GH_TOKEN'];
|
|
568
|
+
if (env_token) {
|
|
569
|
+
console.log(' → Using GITHUB_TOKEN from environment');
|
|
570
|
+
return env_token;
|
|
571
|
+
}
|
|
572
|
+
// Try gh CLI
|
|
573
|
+
try {
|
|
574
|
+
const { stdout } = await execAsync('gh auth token', { timeout: 5000 });
|
|
575
|
+
const gh_token = stdout.trim();
|
|
576
|
+
if (gh_token) {
|
|
577
|
+
console.log(' → Using token from gh CLI');
|
|
578
|
+
return gh_token;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
catch {
|
|
582
|
+
// gh CLI not available
|
|
583
|
+
}
|
|
584
|
+
// Prompt user
|
|
585
|
+
console.log(' → No GHCR token found (checked: GITHUB_TOKEN, GH_TOKEN, gh CLI)');
|
|
586
|
+
console.log(' → Create a token at: https://github.com/settings/tokens');
|
|
587
|
+
console.log(' → Required scope: read:packages');
|
|
588
|
+
const input = await prompt_for_input(' Enter GitHub token (or Enter to skip): ', true);
|
|
589
|
+
return input || undefined;
|
|
590
|
+
}
|
|
591
|
+
// Generic registry
|
|
592
|
+
const username = process.env['REGISTRY_USERNAME'];
|
|
593
|
+
const password = process.env['REGISTRY_PASSWORD'];
|
|
594
|
+
if (username && password) {
|
|
595
|
+
return `${username}:${password}`;
|
|
596
|
+
}
|
|
597
|
+
return undefined;
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* Creates a dockerconfigjson Secret manifest for OCI registry auth.
|
|
601
|
+
*/
|
|
602
|
+
function create_registry_secret_manifest(registry, token, secret_name, namespace) {
|
|
603
|
+
const authString = token.includes(':')
|
|
604
|
+
? Buffer.from(token).toString('base64')
|
|
605
|
+
: Buffer.from(`_:${token}`).toString('base64');
|
|
606
|
+
return {
|
|
607
|
+
apiVersion: 'v1',
|
|
608
|
+
kind: 'Secret',
|
|
609
|
+
metadata: {
|
|
610
|
+
name: secret_name,
|
|
611
|
+
namespace: namespace,
|
|
612
|
+
},
|
|
613
|
+
type: 'kubernetes.io/dockerconfigjson',
|
|
614
|
+
data: {
|
|
615
|
+
'.dockerconfigjson': Buffer.from(JSON.stringify({ auths: { [registry]: { auth: authString } } })).toString('base64'),
|
|
616
|
+
},
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
/**
|
|
620
|
+
* Ensures OCI registry secret exists, creating if needed.
|
|
621
|
+
*/
|
|
622
|
+
async function ensure_oci_registry_secret(registry, dry_run, secret_name, namespace) {
|
|
623
|
+
// Check if secret exists
|
|
624
|
+
try {
|
|
625
|
+
await execAsync(`kubectl get secret ${secret_name} -n ${namespace}`, {
|
|
626
|
+
timeout: 5000,
|
|
627
|
+
});
|
|
628
|
+
console.log(' ✓ OCI registry secret exists');
|
|
629
|
+
return { has_auth: true };
|
|
630
|
+
}
|
|
631
|
+
catch {
|
|
632
|
+
// Need to create
|
|
633
|
+
}
|
|
634
|
+
const token = await get_oci_registry_token(registry);
|
|
635
|
+
if (!token) {
|
|
636
|
+
console.log(' → No credentials provided, OCI will be unauthenticated');
|
|
637
|
+
return { has_auth: false };
|
|
638
|
+
}
|
|
639
|
+
const secret = create_registry_secret_manifest(registry, token, secret_name, namespace);
|
|
640
|
+
if (dry_run) {
|
|
641
|
+
console.log(` [dry-run] Would create secret: ${secret_name}`);
|
|
642
|
+
return { has_auth: true, secret };
|
|
643
|
+
}
|
|
644
|
+
// Apply secret
|
|
645
|
+
try {
|
|
646
|
+
const { spawn } = await import('node:child_process');
|
|
647
|
+
const { serialize_resource } = await import('../../generator/index.js');
|
|
648
|
+
await new Promise((resolve, reject) => {
|
|
649
|
+
const kubectl = spawn('kubectl', ['apply', '-f', '-'], {
|
|
650
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
651
|
+
});
|
|
652
|
+
let stderr = '';
|
|
653
|
+
kubectl.stderr.on('data', (data) => {
|
|
654
|
+
stderr += data.toString();
|
|
655
|
+
});
|
|
656
|
+
kubectl.on('close', (code) => {
|
|
657
|
+
if (code === 0)
|
|
658
|
+
resolve();
|
|
659
|
+
else
|
|
660
|
+
reject(new Error(stderr || `kubectl exited with code ${code}`));
|
|
661
|
+
});
|
|
662
|
+
kubectl.stdin.write(serialize_resource(secret));
|
|
663
|
+
kubectl.stdin.end();
|
|
664
|
+
});
|
|
665
|
+
console.log(` ✓ Created secret: ${secret_name}`);
|
|
666
|
+
return { has_auth: true, secret };
|
|
667
|
+
}
|
|
668
|
+
catch (error) {
|
|
669
|
+
const err = error;
|
|
670
|
+
console.error(` ✗ Failed to create secret: ${err.message}`);
|
|
671
|
+
return { has_auth: false };
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
/**
|
|
675
|
+
* Gets the Doppler service token from environment or prompts user.
|
|
676
|
+
*/
|
|
677
|
+
async function get_doppler_token() {
|
|
678
|
+
// Check environment variable
|
|
679
|
+
const env_token = process.env['DOPPLER_TOKEN'];
|
|
680
|
+
if (env_token) {
|
|
681
|
+
console.log(' → Using DOPPLER_TOKEN from environment');
|
|
682
|
+
return env_token;
|
|
683
|
+
}
|
|
684
|
+
// Prompt user
|
|
685
|
+
console.log(' → No Doppler token found (checked: DOPPLER_TOKEN env var)');
|
|
686
|
+
console.log(' → Create a service token at: https://dashboard.doppler.com');
|
|
687
|
+
const input = await prompt_for_input(' Enter Doppler service token (or Enter to skip): ', true);
|
|
688
|
+
return input || undefined;
|
|
689
|
+
}
|
|
690
|
+
/**
|
|
691
|
+
* Creates an Opaque Secret manifest for Doppler token.
|
|
692
|
+
*/
|
|
693
|
+
function create_doppler_secret_manifest(token, config) {
|
|
694
|
+
return {
|
|
695
|
+
apiVersion: 'v1',
|
|
696
|
+
kind: 'Secret',
|
|
697
|
+
metadata: {
|
|
698
|
+
name: config.name,
|
|
699
|
+
namespace: config.namespace,
|
|
700
|
+
...(config.annotations && { annotations: config.annotations }),
|
|
701
|
+
},
|
|
702
|
+
type: 'Opaque',
|
|
703
|
+
stringData: {
|
|
704
|
+
[config.key]: token,
|
|
705
|
+
},
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
/**
|
|
709
|
+
* Ensures the Doppler namespace exists.
|
|
710
|
+
*/
|
|
711
|
+
async function ensure_doppler_namespace(namespace, dry_run) {
|
|
712
|
+
try {
|
|
713
|
+
await execAsync(`kubectl get namespace ${namespace}`, { timeout: 5000 });
|
|
714
|
+
return true;
|
|
715
|
+
}
|
|
716
|
+
catch {
|
|
717
|
+
// Need to create
|
|
718
|
+
}
|
|
719
|
+
if (dry_run) {
|
|
720
|
+
console.log(` [dry-run] Would create namespace: ${namespace}`);
|
|
721
|
+
return true;
|
|
722
|
+
}
|
|
723
|
+
try {
|
|
724
|
+
await execAsync(`kubectl create namespace ${namespace}`, { timeout: 10000 });
|
|
725
|
+
console.log(` ✓ Created namespace: ${namespace}`);
|
|
726
|
+
return true;
|
|
727
|
+
}
|
|
728
|
+
catch (error) {
|
|
729
|
+
const err = error;
|
|
730
|
+
console.error(` ✗ Failed to create namespace: ${err.message}`);
|
|
731
|
+
return false;
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
/**
|
|
735
|
+
* Ensures Doppler token secret exists, creating if needed.
|
|
736
|
+
*/
|
|
737
|
+
async function ensure_doppler_token_secret(dry_run, cluster_config) {
|
|
738
|
+
// Merge cluster config with defaults
|
|
739
|
+
const config = {
|
|
740
|
+
namespace: cluster_config?.namespace || 'doppler-operator-system',
|
|
741
|
+
name: cluster_config?.name || 'doppler-token',
|
|
742
|
+
key: cluster_config?.key || 'serviceToken',
|
|
743
|
+
...(cluster_config?.annotations && { annotations: cluster_config.annotations }),
|
|
744
|
+
};
|
|
745
|
+
// Check if secret exists
|
|
746
|
+
try {
|
|
747
|
+
await execAsync(`kubectl get secret ${config.name} -n ${config.namespace}`, {
|
|
748
|
+
timeout: 5000,
|
|
749
|
+
});
|
|
750
|
+
console.log(' ✓ Doppler token secret exists');
|
|
751
|
+
return true;
|
|
752
|
+
}
|
|
753
|
+
catch {
|
|
754
|
+
// Need to create
|
|
755
|
+
}
|
|
756
|
+
const token = await get_doppler_token();
|
|
757
|
+
if (!token) {
|
|
758
|
+
console.log(' → No Doppler token provided, skipping secret creation');
|
|
759
|
+
console.log(' ⚠ ExternalSecrets using Doppler will fail until this is configured');
|
|
760
|
+
return false;
|
|
761
|
+
}
|
|
762
|
+
// Ensure namespace exists
|
|
763
|
+
const ns_ok = await ensure_doppler_namespace(config.namespace, dry_run);
|
|
764
|
+
if (!ns_ok) {
|
|
765
|
+
return false;
|
|
766
|
+
}
|
|
767
|
+
const secret = create_doppler_secret_manifest(token, config);
|
|
768
|
+
if (dry_run) {
|
|
769
|
+
console.log(` [dry-run] Would create secret: ${config.name}`);
|
|
770
|
+
return true;
|
|
771
|
+
}
|
|
772
|
+
// Apply secret
|
|
773
|
+
try {
|
|
774
|
+
const { spawn } = await import('node:child_process');
|
|
775
|
+
const { serialize_resource } = await import('../../generator/index.js');
|
|
776
|
+
await new Promise((resolve, reject) => {
|
|
777
|
+
const kubectl = spawn('kubectl', ['apply', '-f', '-'], {
|
|
778
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
779
|
+
});
|
|
780
|
+
let stderr = '';
|
|
781
|
+
kubectl.stderr.on('data', (data) => {
|
|
782
|
+
stderr += data.toString();
|
|
783
|
+
});
|
|
784
|
+
kubectl.on('close', (code) => {
|
|
785
|
+
if (code === 0)
|
|
786
|
+
resolve();
|
|
787
|
+
else
|
|
788
|
+
reject(new Error(stderr || `kubectl exited with code ${code}`));
|
|
789
|
+
});
|
|
790
|
+
kubectl.stdin.write(serialize_resource(secret));
|
|
791
|
+
kubectl.stdin.end();
|
|
792
|
+
});
|
|
793
|
+
console.log(` ✓ Created secret: ${config.name} in ${config.namespace}`);
|
|
794
|
+
return true;
|
|
795
|
+
}
|
|
796
|
+
catch (error) {
|
|
797
|
+
const err = error;
|
|
798
|
+
console.error(` ✗ Failed to create Doppler secret: ${err.message}`);
|
|
799
|
+
return false;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
//# sourceMappingURL=apply.js.map
|