@build-astron-co/nimbus 0.2.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 +628 -0
- package/bin/nimbus +38 -0
- package/package.json +80 -0
- package/src/__tests__/app.test.ts +76 -0
- package/src/__tests__/audit.test.ts +877 -0
- package/src/__tests__/circuit-breaker.test.ts +116 -0
- package/src/__tests__/cli-run.test.ts +115 -0
- package/src/__tests__/context-manager.test.ts +502 -0
- package/src/__tests__/context.test.ts +242 -0
- package/src/__tests__/enterprise.test.ts +401 -0
- package/src/__tests__/generator.test.ts +433 -0
- package/src/__tests__/hooks.test.ts +582 -0
- package/src/__tests__/init.test.ts +436 -0
- package/src/__tests__/intent-parser.test.ts +229 -0
- package/src/__tests__/llm-router.test.ts +209 -0
- package/src/__tests__/lsp.test.ts +293 -0
- package/src/__tests__/modes.test.ts +336 -0
- package/src/__tests__/permissions.test.ts +338 -0
- package/src/__tests__/serve.test.ts +275 -0
- package/src/__tests__/sessions.test.ts +227 -0
- package/src/__tests__/sharing.test.ts +288 -0
- package/src/__tests__/snapshots.test.ts +581 -0
- package/src/__tests__/state-db.test.ts +334 -0
- package/src/__tests__/stream-with-tools.test.ts +732 -0
- package/src/__tests__/subagents.test.ts +176 -0
- package/src/__tests__/system-prompt.test.ts +169 -0
- package/src/__tests__/tool-converter.test.ts +256 -0
- package/src/__tests__/tool-schemas.test.ts +397 -0
- package/src/__tests__/tools.test.ts +143 -0
- package/src/__tests__/version.test.ts +49 -0
- package/src/agent/compaction-agent.ts +227 -0
- package/src/agent/context-manager.ts +435 -0
- package/src/agent/context.ts +427 -0
- package/src/agent/deploy-preview.ts +426 -0
- package/src/agent/index.ts +68 -0
- package/src/agent/loop.ts +717 -0
- package/src/agent/modes.ts +429 -0
- package/src/agent/permissions.ts +466 -0
- package/src/agent/subagents/base.ts +116 -0
- package/src/agent/subagents/cost.ts +51 -0
- package/src/agent/subagents/explore.ts +42 -0
- package/src/agent/subagents/general.ts +54 -0
- package/src/agent/subagents/index.ts +102 -0
- package/src/agent/subagents/infra.ts +59 -0
- package/src/agent/subagents/security.ts +69 -0
- package/src/agent/system-prompt.ts +436 -0
- package/src/app.ts +122 -0
- package/src/audit/activity-log.ts +290 -0
- package/src/audit/compliance-checker.ts +540 -0
- package/src/audit/cost-tracker.ts +318 -0
- package/src/audit/index.ts +23 -0
- package/src/audit/security-scanner.ts +596 -0
- package/src/auth/guard.ts +75 -0
- package/src/auth/index.ts +56 -0
- package/src/auth/oauth.ts +455 -0
- package/src/auth/providers.ts +470 -0
- package/src/auth/sso.ts +113 -0
- package/src/auth/store.ts +505 -0
- package/src/auth/types.ts +187 -0
- package/src/build.ts +141 -0
- package/src/cli/index.ts +16 -0
- package/src/cli/init.ts +854 -0
- package/src/cli/openapi-spec.ts +356 -0
- package/src/cli/run.ts +237 -0
- package/src/cli/serve-auth.ts +80 -0
- package/src/cli/serve.ts +462 -0
- package/src/cli/web.ts +67 -0
- package/src/cli.ts +1417 -0
- package/src/clients/core-engine-client.ts +227 -0
- package/src/clients/enterprise-client.ts +334 -0
- package/src/clients/generator-client.ts +351 -0
- package/src/clients/git-client.ts +627 -0
- package/src/clients/github-client.ts +410 -0
- package/src/clients/helm-client.ts +504 -0
- package/src/clients/index.ts +80 -0
- package/src/clients/k8s-client.ts +497 -0
- package/src/clients/llm-client.ts +161 -0
- package/src/clients/rest-client.ts +130 -0
- package/src/clients/service-discovery.ts +33 -0
- package/src/clients/terraform-client.ts +482 -0
- package/src/clients/tools-client.ts +1843 -0
- package/src/clients/ws-client.ts +115 -0
- package/src/commands/analyze/index.ts +352 -0
- package/src/commands/apply/helm.ts +473 -0
- package/src/commands/apply/index.ts +213 -0
- package/src/commands/apply/k8s.ts +454 -0
- package/src/commands/apply/terraform.ts +582 -0
- package/src/commands/ask.ts +167 -0
- package/src/commands/audit/index.ts +238 -0
- package/src/commands/auth-cloud.ts +294 -0
- package/src/commands/auth-list.ts +134 -0
- package/src/commands/auth-profile.ts +121 -0
- package/src/commands/auth-status.ts +141 -0
- package/src/commands/aws/ec2.ts +501 -0
- package/src/commands/aws/iam.ts +397 -0
- package/src/commands/aws/index.ts +133 -0
- package/src/commands/aws/lambda.ts +396 -0
- package/src/commands/aws/rds.ts +439 -0
- package/src/commands/aws/s3.ts +439 -0
- package/src/commands/aws/vpc.ts +393 -0
- package/src/commands/aws-discover.ts +649 -0
- package/src/commands/aws-terraform.ts +805 -0
- package/src/commands/azure/aks.ts +376 -0
- package/src/commands/azure/functions.ts +253 -0
- package/src/commands/azure/index.ts +116 -0
- package/src/commands/azure/storage.ts +478 -0
- package/src/commands/azure/vm.ts +355 -0
- package/src/commands/billing/index.ts +256 -0
- package/src/commands/chat.ts +314 -0
- package/src/commands/config.ts +346 -0
- package/src/commands/cost/cloud-cost-estimator.ts +266 -0
- package/src/commands/cost/estimator.ts +79 -0
- package/src/commands/cost/index.ts +594 -0
- package/src/commands/cost/parsers/terraform.ts +273 -0
- package/src/commands/cost/parsers/types.ts +25 -0
- package/src/commands/cost/pricing/aws.ts +544 -0
- package/src/commands/cost/pricing/azure.ts +499 -0
- package/src/commands/cost/pricing/gcp.ts +396 -0
- package/src/commands/cost/pricing/index.ts +40 -0
- package/src/commands/demo.ts +250 -0
- package/src/commands/doctor.ts +794 -0
- package/src/commands/drift/index.ts +439 -0
- package/src/commands/explain.ts +277 -0
- package/src/commands/feedback.ts +389 -0
- package/src/commands/fix.ts +324 -0
- package/src/commands/fs/index.ts +402 -0
- package/src/commands/gcp/compute.ts +325 -0
- package/src/commands/gcp/functions.ts +271 -0
- package/src/commands/gcp/gke.ts +438 -0
- package/src/commands/gcp/iam.ts +344 -0
- package/src/commands/gcp/index.ts +129 -0
- package/src/commands/gcp/storage.ts +284 -0
- package/src/commands/generate-helm.ts +1249 -0
- package/src/commands/generate-k8s.ts +1560 -0
- package/src/commands/generate-terraform.ts +1460 -0
- package/src/commands/gh/index.ts +863 -0
- package/src/commands/git/index.ts +1343 -0
- package/src/commands/helm/index.ts +1126 -0
- package/src/commands/help.ts +539 -0
- package/src/commands/history.ts +142 -0
- package/src/commands/import.ts +868 -0
- package/src/commands/index.ts +367 -0
- package/src/commands/init.ts +1046 -0
- package/src/commands/k8s/index.ts +1137 -0
- package/src/commands/login.ts +631 -0
- package/src/commands/logout.ts +83 -0
- package/src/commands/onboarding.ts +228 -0
- package/src/commands/plan/display.ts +279 -0
- package/src/commands/plan/index.ts +599 -0
- package/src/commands/preview.ts +452 -0
- package/src/commands/questionnaire.ts +1270 -0
- package/src/commands/resume.ts +55 -0
- package/src/commands/team/index.ts +346 -0
- package/src/commands/template.ts +232 -0
- package/src/commands/tf/index.ts +1034 -0
- package/src/commands/upgrade.ts +550 -0
- package/src/commands/usage/index.ts +134 -0
- package/src/commands/version.ts +170 -0
- package/src/compat/index.ts +2 -0
- package/src/compat/runtime.ts +12 -0
- package/src/compat/sqlite.ts +107 -0
- package/src/config/index.ts +17 -0
- package/src/config/manager.ts +530 -0
- package/src/config/safety-policy.ts +358 -0
- package/src/config/schema.ts +125 -0
- package/src/config/types.ts +527 -0
- package/src/context/context-db.ts +199 -0
- package/src/demo/index.ts +349 -0
- package/src/demo/scenarios/full-journey.ts +229 -0
- package/src/demo/scenarios/getting-started.ts +127 -0
- package/src/demo/scenarios/helm-release.ts +341 -0
- package/src/demo/scenarios/k8s-deployment.ts +194 -0
- package/src/demo/scenarios/terraform-vpc.ts +170 -0
- package/src/demo/types.ts +92 -0
- package/src/engine/cost-estimator.ts +438 -0
- package/src/engine/diagram-generator.ts +256 -0
- package/src/engine/drift-detector.ts +902 -0
- package/src/engine/executor.ts +1035 -0
- package/src/engine/index.ts +76 -0
- package/src/engine/orchestrator.ts +636 -0
- package/src/engine/planner.ts +720 -0
- package/src/engine/safety.ts +743 -0
- package/src/engine/verifier.ts +770 -0
- package/src/enterprise/audit.ts +348 -0
- package/src/enterprise/auth.ts +270 -0
- package/src/enterprise/billing.ts +822 -0
- package/src/enterprise/index.ts +17 -0
- package/src/enterprise/teams.ts +443 -0
- package/src/generator/best-practices.ts +1608 -0
- package/src/generator/helm.ts +630 -0
- package/src/generator/index.ts +37 -0
- package/src/generator/intent-parser.ts +514 -0
- package/src/generator/kubernetes.ts +976 -0
- package/src/generator/terraform.ts +1867 -0
- package/src/history/index.ts +8 -0
- package/src/history/manager.ts +322 -0
- package/src/history/types.ts +34 -0
- package/src/hooks/config.ts +432 -0
- package/src/hooks/engine.ts +391 -0
- package/src/hooks/index.ts +4 -0
- package/src/llm/auth-bridge.ts +198 -0
- package/src/llm/circuit-breaker.ts +140 -0
- package/src/llm/config-loader.ts +201 -0
- package/src/llm/cost-calculator.ts +171 -0
- package/src/llm/index.ts +8 -0
- package/src/llm/model-aliases.ts +115 -0
- package/src/llm/provider-registry.ts +63 -0
- package/src/llm/providers/anthropic.ts +433 -0
- package/src/llm/providers/bedrock.ts +477 -0
- package/src/llm/providers/google.ts +405 -0
- package/src/llm/providers/ollama.ts +767 -0
- package/src/llm/providers/openai-compatible.ts +340 -0
- package/src/llm/providers/openai.ts +328 -0
- package/src/llm/providers/openrouter.ts +338 -0
- package/src/llm/router.ts +1035 -0
- package/src/llm/types.ts +232 -0
- package/src/lsp/client.ts +298 -0
- package/src/lsp/languages.ts +116 -0
- package/src/lsp/manager.ts +278 -0
- package/src/mcp/client.ts +402 -0
- package/src/mcp/index.ts +5 -0
- package/src/mcp/manager.ts +133 -0
- package/src/nimbus.ts +214 -0
- package/src/plugins/index.ts +27 -0
- package/src/plugins/loader.ts +334 -0
- package/src/plugins/manager.ts +376 -0
- package/src/plugins/types.ts +284 -0
- package/src/scanners/cicd-scanner.ts +258 -0
- package/src/scanners/cloud-scanner.ts +466 -0
- package/src/scanners/framework-scanner.ts +469 -0
- package/src/scanners/iac-scanner.ts +388 -0
- package/src/scanners/index.ts +539 -0
- package/src/scanners/language-scanner.ts +276 -0
- package/src/scanners/package-manager-scanner.ts +277 -0
- package/src/scanners/types.ts +172 -0
- package/src/sessions/manager.ts +365 -0
- package/src/sessions/types.ts +44 -0
- package/src/sharing/sync.ts +296 -0
- package/src/sharing/viewer.ts +97 -0
- package/src/snapshots/index.ts +2 -0
- package/src/snapshots/manager.ts +530 -0
- package/src/state/artifacts.ts +147 -0
- package/src/state/audit.ts +137 -0
- package/src/state/billing.ts +240 -0
- package/src/state/checkpoints.ts +117 -0
- package/src/state/config.ts +67 -0
- package/src/state/conversations.ts +14 -0
- package/src/state/credentials.ts +154 -0
- package/src/state/db.ts +58 -0
- package/src/state/index.ts +26 -0
- package/src/state/messages.ts +115 -0
- package/src/state/projects.ts +123 -0
- package/src/state/schema.ts +236 -0
- package/src/state/sessions.ts +147 -0
- package/src/state/teams.ts +200 -0
- package/src/telemetry.ts +108 -0
- package/src/tools/aws-ops.ts +952 -0
- package/src/tools/azure-ops.ts +579 -0
- package/src/tools/file-ops.ts +593 -0
- package/src/tools/gcp-ops.ts +625 -0
- package/src/tools/git-ops.ts +773 -0
- package/src/tools/github-ops.ts +799 -0
- package/src/tools/helm-ops.ts +943 -0
- package/src/tools/index.ts +17 -0
- package/src/tools/k8s-ops.ts +819 -0
- package/src/tools/schemas/converter.ts +184 -0
- package/src/tools/schemas/devops.ts +612 -0
- package/src/tools/schemas/index.ts +73 -0
- package/src/tools/schemas/standard.ts +1144 -0
- package/src/tools/schemas/types.ts +705 -0
- package/src/tools/terraform-ops.ts +862 -0
- package/src/types/ambient.d.ts +193 -0
- package/src/types/config.ts +83 -0
- package/src/types/drift.ts +116 -0
- package/src/types/enterprise.ts +335 -0
- package/src/types/index.ts +20 -0
- package/src/types/plan.ts +44 -0
- package/src/types/request.ts +65 -0
- package/src/types/response.ts +54 -0
- package/src/types/service.ts +51 -0
- package/src/ui/App.tsx +997 -0
- package/src/ui/DeployPreview.tsx +169 -0
- package/src/ui/Header.tsx +68 -0
- package/src/ui/InputBox.tsx +350 -0
- package/src/ui/MessageList.tsx +585 -0
- package/src/ui/PermissionPrompt.tsx +151 -0
- package/src/ui/StatusBar.tsx +158 -0
- package/src/ui/ToolCallDisplay.tsx +409 -0
- package/src/ui/chat-ui.ts +853 -0
- package/src/ui/index.ts +33 -0
- package/src/ui/ink/index.ts +711 -0
- package/src/ui/streaming.ts +176 -0
- package/src/ui/types.ts +57 -0
- package/src/utils/analytics.ts +72 -0
- package/src/utils/cost-warning.ts +27 -0
- package/src/utils/env.ts +46 -0
- package/src/utils/errors.ts +69 -0
- package/src/utils/event-bus.ts +38 -0
- package/src/utils/index.ts +24 -0
- package/src/utils/logger.ts +171 -0
- package/src/utils/rate-limiter.ts +121 -0
- package/src/utils/service-auth.ts +49 -0
- package/src/utils/validation.ts +53 -0
- package/src/version.ts +4 -0
- package/src/watcher/index.ts +163 -0
- package/src/wizard/approval.ts +383 -0
- package/src/wizard/index.ts +25 -0
- package/src/wizard/prompts.ts +338 -0
- package/src/wizard/types.ts +171 -0
- package/src/wizard/ui.ts +556 -0
- package/src/wizard/wizard.ts +304 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,1560 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate Kubernetes Manifests Command
|
|
3
|
+
*
|
|
4
|
+
* Interactive wizard for generating K8s resources
|
|
5
|
+
*
|
|
6
|
+
* Usage: nimbus generate k8s [options]
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { logger } from '../utils';
|
|
10
|
+
import { RestClient } from '../clients';
|
|
11
|
+
import {
|
|
12
|
+
createWizard,
|
|
13
|
+
ui,
|
|
14
|
+
select,
|
|
15
|
+
multiSelect,
|
|
16
|
+
confirm,
|
|
17
|
+
input,
|
|
18
|
+
pathInput,
|
|
19
|
+
type WizardStep,
|
|
20
|
+
type StepResult,
|
|
21
|
+
} from '../wizard';
|
|
22
|
+
|
|
23
|
+
// Generator Service client
|
|
24
|
+
const generatorUrl = process.env.GENERATOR_SERVICE_URL || 'http://localhost:3003';
|
|
25
|
+
const generatorClient = new RestClient(generatorUrl);
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Workload types for Kubernetes
|
|
29
|
+
*/
|
|
30
|
+
export type K8sWorkloadType = 'deployment' | 'statefulset' | 'daemonset' | 'job' | 'cronjob';
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Service types for Kubernetes
|
|
34
|
+
*/
|
|
35
|
+
export type K8sServiceType = 'ClusterIP' | 'NodePort' | 'LoadBalancer' | 'None';
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Command options from CLI arguments
|
|
39
|
+
*/
|
|
40
|
+
export interface GenerateK8sOptions {
|
|
41
|
+
workloadType?: K8sWorkloadType;
|
|
42
|
+
namespace?: string;
|
|
43
|
+
name?: string;
|
|
44
|
+
image?: string;
|
|
45
|
+
replicas?: number;
|
|
46
|
+
port?: number;
|
|
47
|
+
serviceType?: K8sServiceType;
|
|
48
|
+
output?: string;
|
|
49
|
+
nonInteractive?: boolean;
|
|
50
|
+
includeIngress?: boolean;
|
|
51
|
+
includeHpa?: boolean;
|
|
52
|
+
includePdb?: boolean;
|
|
53
|
+
includeConfigMap?: boolean;
|
|
54
|
+
includeSecret?: boolean;
|
|
55
|
+
cpuRequest?: string;
|
|
56
|
+
cpuLimit?: string;
|
|
57
|
+
memoryRequest?: string;
|
|
58
|
+
memoryLimit?: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Wizard context for K8s generation
|
|
63
|
+
*/
|
|
64
|
+
export interface K8sWizardContext {
|
|
65
|
+
// Workload configuration
|
|
66
|
+
workloadType?: K8sWorkloadType;
|
|
67
|
+
name?: string;
|
|
68
|
+
namespace?: string;
|
|
69
|
+
image?: string;
|
|
70
|
+
imageTag?: string;
|
|
71
|
+
|
|
72
|
+
// Replica configuration
|
|
73
|
+
replicas?: number;
|
|
74
|
+
minReplicas?: number;
|
|
75
|
+
maxReplicas?: number;
|
|
76
|
+
targetCPUUtilization?: number;
|
|
77
|
+
|
|
78
|
+
// Port & Service configuration
|
|
79
|
+
containerPort?: number;
|
|
80
|
+
serviceType?: K8sServiceType;
|
|
81
|
+
servicePort?: number;
|
|
82
|
+
|
|
83
|
+
// Resource limits
|
|
84
|
+
cpuRequest?: string;
|
|
85
|
+
cpuLimit?: string;
|
|
86
|
+
memoryRequest?: string;
|
|
87
|
+
memoryLimit?: string;
|
|
88
|
+
|
|
89
|
+
// Additional resources
|
|
90
|
+
includeService?: boolean;
|
|
91
|
+
includeIngress?: boolean;
|
|
92
|
+
includeHpa?: boolean;
|
|
93
|
+
includePdb?: boolean;
|
|
94
|
+
includeConfigMap?: boolean;
|
|
95
|
+
includeSecret?: boolean;
|
|
96
|
+
|
|
97
|
+
// Ingress configuration
|
|
98
|
+
ingressHost?: string;
|
|
99
|
+
ingressPath?: string;
|
|
100
|
+
ingressTls?: boolean;
|
|
101
|
+
|
|
102
|
+
// PDB configuration
|
|
103
|
+
minAvailable?: number | string;
|
|
104
|
+
|
|
105
|
+
// Job/CronJob specific
|
|
106
|
+
schedule?: string;
|
|
107
|
+
backoffLimit?: number;
|
|
108
|
+
completions?: number;
|
|
109
|
+
parallelism?: number;
|
|
110
|
+
|
|
111
|
+
// Health checks
|
|
112
|
+
includeProbes?: boolean;
|
|
113
|
+
livenessPath?: string;
|
|
114
|
+
readinessPath?: string;
|
|
115
|
+
|
|
116
|
+
// Output
|
|
117
|
+
outputPath?: string;
|
|
118
|
+
outputFormat?: 'multiple' | 'single' | 'kustomize';
|
|
119
|
+
generatedFiles?: string[];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Run the generate k8s command
|
|
124
|
+
*/
|
|
125
|
+
export async function generateK8sCommand(options: GenerateK8sOptions = {}): Promise<void> {
|
|
126
|
+
logger.info('Starting Kubernetes manifest generation wizard');
|
|
127
|
+
|
|
128
|
+
// Non-interactive mode
|
|
129
|
+
if (options.nonInteractive) {
|
|
130
|
+
await runNonInteractive(options);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Interactive wizard mode
|
|
135
|
+
const wizard = createWizard<K8sWizardContext>({
|
|
136
|
+
title: 'nimbus generate k8s',
|
|
137
|
+
description: 'Generate Kubernetes manifests for your application',
|
|
138
|
+
initialContext: {
|
|
139
|
+
workloadType: options.workloadType,
|
|
140
|
+
namespace: options.namespace,
|
|
141
|
+
name: options.name,
|
|
142
|
+
image: options.image,
|
|
143
|
+
replicas: options.replicas,
|
|
144
|
+
containerPort: options.port,
|
|
145
|
+
serviceType: options.serviceType,
|
|
146
|
+
outputPath: options.output,
|
|
147
|
+
includeIngress: options.includeIngress,
|
|
148
|
+
includeHpa: options.includeHpa,
|
|
149
|
+
includePdb: options.includePdb,
|
|
150
|
+
includeConfigMap: options.includeConfigMap,
|
|
151
|
+
includeSecret: options.includeSecret,
|
|
152
|
+
cpuRequest: options.cpuRequest,
|
|
153
|
+
cpuLimit: options.cpuLimit,
|
|
154
|
+
memoryRequest: options.memoryRequest,
|
|
155
|
+
memoryLimit: options.memoryLimit,
|
|
156
|
+
},
|
|
157
|
+
steps: createWizardSteps(),
|
|
158
|
+
onEvent: event => {
|
|
159
|
+
logger.debug('Wizard event', { type: event.type });
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const result = await wizard.run();
|
|
164
|
+
|
|
165
|
+
if (result.success) {
|
|
166
|
+
ui.newLine();
|
|
167
|
+
ui.box({
|
|
168
|
+
title: 'Complete!',
|
|
169
|
+
content: [
|
|
170
|
+
'Your Kubernetes manifests have been generated.',
|
|
171
|
+
'',
|
|
172
|
+
'Generated files:',
|
|
173
|
+
...(result.context.generatedFiles?.map(f => ` - ${f}`) || [' - (manifests generated)']),
|
|
174
|
+
'',
|
|
175
|
+
'Next steps:',
|
|
176
|
+
` 1. Review the generated files in ${result.context.outputPath}`,
|
|
177
|
+
' 2. Customize values as needed for your environment',
|
|
178
|
+
' 3. Run "kubectl apply -f <path>" or "nimbus apply k8s <path>"',
|
|
179
|
+
],
|
|
180
|
+
style: 'rounded',
|
|
181
|
+
borderColor: 'green',
|
|
182
|
+
padding: 1,
|
|
183
|
+
});
|
|
184
|
+
} else {
|
|
185
|
+
ui.error(`Wizard failed: ${result.error?.message || 'Unknown error'}`);
|
|
186
|
+
process.exit(1);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Create wizard steps
|
|
192
|
+
*/
|
|
193
|
+
function createWizardSteps(): WizardStep<K8sWizardContext>[] {
|
|
194
|
+
return [
|
|
195
|
+
// Step 1: Workload Type Selection
|
|
196
|
+
{
|
|
197
|
+
id: 'workload-type',
|
|
198
|
+
title: 'Workload Type',
|
|
199
|
+
description: 'Select the type of Kubernetes workload to generate',
|
|
200
|
+
execute: workloadTypeStep,
|
|
201
|
+
},
|
|
202
|
+
|
|
203
|
+
// Step 2: Basic Configuration
|
|
204
|
+
{
|
|
205
|
+
id: 'basic-config',
|
|
206
|
+
title: 'Basic Configuration',
|
|
207
|
+
description: 'Configure name, namespace, and image',
|
|
208
|
+
execute: basicConfigStep,
|
|
209
|
+
},
|
|
210
|
+
|
|
211
|
+
// Step 3: Replica Configuration (not for DaemonSet or Job)
|
|
212
|
+
{
|
|
213
|
+
id: 'replicas',
|
|
214
|
+
title: 'Replica Configuration',
|
|
215
|
+
description: 'Configure replicas and scaling options',
|
|
216
|
+
condition: ctx => !['daemonset', 'job'].includes(ctx.workloadType || ''),
|
|
217
|
+
execute: replicaConfigStep,
|
|
218
|
+
},
|
|
219
|
+
|
|
220
|
+
// Step 4: Job/CronJob Configuration
|
|
221
|
+
{
|
|
222
|
+
id: 'job-config',
|
|
223
|
+
title: 'Job Configuration',
|
|
224
|
+
description: 'Configure job-specific settings',
|
|
225
|
+
condition: ctx => ['job', 'cronjob'].includes(ctx.workloadType || ''),
|
|
226
|
+
execute: jobConfigStep,
|
|
227
|
+
},
|
|
228
|
+
|
|
229
|
+
// Step 5: Port & Service Configuration
|
|
230
|
+
{
|
|
231
|
+
id: 'service-config',
|
|
232
|
+
title: 'Port & Service Configuration',
|
|
233
|
+
description: 'Configure container ports and service exposure',
|
|
234
|
+
execute: serviceConfigStep,
|
|
235
|
+
},
|
|
236
|
+
|
|
237
|
+
// Step 6: Resource Limits
|
|
238
|
+
{
|
|
239
|
+
id: 'resources',
|
|
240
|
+
title: 'Resource Limits',
|
|
241
|
+
description: 'Configure CPU and memory requests/limits',
|
|
242
|
+
execute: resourceLimitsStep,
|
|
243
|
+
},
|
|
244
|
+
|
|
245
|
+
// Step 7: Additional Resources
|
|
246
|
+
{
|
|
247
|
+
id: 'additional-resources',
|
|
248
|
+
title: 'Additional Resources',
|
|
249
|
+
description: 'Select additional Kubernetes resources to generate',
|
|
250
|
+
execute: additionalResourcesStep,
|
|
251
|
+
},
|
|
252
|
+
|
|
253
|
+
// Step 8: Health Checks
|
|
254
|
+
{
|
|
255
|
+
id: 'health-checks',
|
|
256
|
+
title: 'Health Checks',
|
|
257
|
+
description: 'Configure liveness and readiness probes',
|
|
258
|
+
condition: ctx => !['job', 'cronjob'].includes(ctx.workloadType || ''),
|
|
259
|
+
execute: healthChecksStep,
|
|
260
|
+
},
|
|
261
|
+
|
|
262
|
+
// Step 9: Output Configuration
|
|
263
|
+
{
|
|
264
|
+
id: 'output',
|
|
265
|
+
title: 'Output Configuration',
|
|
266
|
+
description: 'Configure where and how to save the manifests',
|
|
267
|
+
execute: outputConfigStep,
|
|
268
|
+
},
|
|
269
|
+
|
|
270
|
+
// Step 10: Generate
|
|
271
|
+
{
|
|
272
|
+
id: 'generate',
|
|
273
|
+
title: 'Generate Manifests',
|
|
274
|
+
description: 'Generating your Kubernetes manifests...',
|
|
275
|
+
execute: generateStep,
|
|
276
|
+
},
|
|
277
|
+
];
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Step 1: Workload Type Selection
|
|
282
|
+
*/
|
|
283
|
+
async function workloadTypeStep(ctx: K8sWizardContext): Promise<StepResult> {
|
|
284
|
+
const workloadType = await select<K8sWorkloadType>({
|
|
285
|
+
message: 'Select workload type:',
|
|
286
|
+
options: [
|
|
287
|
+
{
|
|
288
|
+
value: 'deployment',
|
|
289
|
+
label: 'Deployment',
|
|
290
|
+
description: 'Stateless application with rolling updates (most common)',
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
value: 'statefulset',
|
|
294
|
+
label: 'StatefulSet',
|
|
295
|
+
description: 'Stateful application with stable network identity and storage',
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
value: 'daemonset',
|
|
299
|
+
label: 'DaemonSet',
|
|
300
|
+
description: 'Run a pod on every node (e.g., monitoring agents)',
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
value: 'job',
|
|
304
|
+
label: 'Job',
|
|
305
|
+
description: 'Run a task to completion',
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
value: 'cronjob',
|
|
309
|
+
label: 'CronJob',
|
|
310
|
+
description: 'Run a job on a schedule',
|
|
311
|
+
},
|
|
312
|
+
],
|
|
313
|
+
defaultValue: ctx.workloadType || 'deployment',
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
if (!workloadType) {
|
|
317
|
+
return { success: false, error: 'No workload type selected' };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return {
|
|
321
|
+
success: true,
|
|
322
|
+
data: { workloadType },
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Step 2: Basic Configuration
|
|
328
|
+
*/
|
|
329
|
+
async function basicConfigStep(ctx: K8sWizardContext): Promise<StepResult> {
|
|
330
|
+
// Application name
|
|
331
|
+
const name = await input({
|
|
332
|
+
message: 'Application name:',
|
|
333
|
+
defaultValue: ctx.name || 'my-app',
|
|
334
|
+
validate: value => {
|
|
335
|
+
if (!value) {
|
|
336
|
+
return 'Name is required';
|
|
337
|
+
}
|
|
338
|
+
if (!/^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/.test(value)) {
|
|
339
|
+
return 'Name must be lowercase alphanumeric with dashes only';
|
|
340
|
+
}
|
|
341
|
+
return true;
|
|
342
|
+
},
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
if (!name) {
|
|
346
|
+
return { success: false, error: 'Name is required' };
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Namespace
|
|
350
|
+
ui.newLine();
|
|
351
|
+
const namespace = await input({
|
|
352
|
+
message: 'Namespace:',
|
|
353
|
+
defaultValue: ctx.namespace || 'default',
|
|
354
|
+
validate: value => {
|
|
355
|
+
if (!value) {
|
|
356
|
+
return 'Namespace is required';
|
|
357
|
+
}
|
|
358
|
+
if (!/^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/.test(value)) {
|
|
359
|
+
return 'Namespace must be lowercase alphanumeric with dashes only';
|
|
360
|
+
}
|
|
361
|
+
return true;
|
|
362
|
+
},
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
if (!namespace) {
|
|
366
|
+
return { success: false, error: 'Namespace is required' };
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Container image
|
|
370
|
+
ui.newLine();
|
|
371
|
+
const image = await input({
|
|
372
|
+
message: 'Container image (e.g., nginx:latest, myregistry/myapp):',
|
|
373
|
+
defaultValue: ctx.image || '',
|
|
374
|
+
validate: value => {
|
|
375
|
+
if (!value) {
|
|
376
|
+
return 'Image is required';
|
|
377
|
+
}
|
|
378
|
+
return true;
|
|
379
|
+
},
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
if (!image) {
|
|
383
|
+
return { success: false, error: 'Image is required' };
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Parse image and tag
|
|
387
|
+
const imageParts = image.split(':');
|
|
388
|
+
const imageBase = imageParts[0];
|
|
389
|
+
const imageTag = imageParts[1] || 'latest';
|
|
390
|
+
|
|
391
|
+
return {
|
|
392
|
+
success: true,
|
|
393
|
+
data: {
|
|
394
|
+
name,
|
|
395
|
+
namespace,
|
|
396
|
+
image: imageBase,
|
|
397
|
+
imageTag,
|
|
398
|
+
},
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Step 3: Replica Configuration
|
|
404
|
+
*/
|
|
405
|
+
async function replicaConfigStep(ctx: K8sWizardContext): Promise<StepResult> {
|
|
406
|
+
const replicas = await input({
|
|
407
|
+
message: 'Number of replicas:',
|
|
408
|
+
defaultValue: String(ctx.replicas || 2),
|
|
409
|
+
validate: value => {
|
|
410
|
+
const num = parseInt(value, 10);
|
|
411
|
+
if (isNaN(num) || num < 1) {
|
|
412
|
+
return 'Must be a positive number';
|
|
413
|
+
}
|
|
414
|
+
return true;
|
|
415
|
+
},
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
if (!replicas) {
|
|
419
|
+
return { success: false, error: 'Replicas required' };
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Ask about HPA
|
|
423
|
+
ui.newLine();
|
|
424
|
+
const includeHpa =
|
|
425
|
+
ctx.includeHpa ??
|
|
426
|
+
(await confirm({
|
|
427
|
+
message: 'Include Horizontal Pod Autoscaler (HPA)?',
|
|
428
|
+
defaultValue: false,
|
|
429
|
+
}));
|
|
430
|
+
|
|
431
|
+
let minReplicas: number | undefined;
|
|
432
|
+
let maxReplicas: number | undefined;
|
|
433
|
+
let targetCPUUtilization: number | undefined;
|
|
434
|
+
|
|
435
|
+
if (includeHpa) {
|
|
436
|
+
ui.newLine();
|
|
437
|
+
const min = await input({
|
|
438
|
+
message: 'Minimum replicas:',
|
|
439
|
+
defaultValue: String(ctx.minReplicas || Math.max(1, parseInt(replicas, 10) - 1)),
|
|
440
|
+
validate: value => {
|
|
441
|
+
const num = parseInt(value, 10);
|
|
442
|
+
if (isNaN(num) || num < 1) {
|
|
443
|
+
return 'Must be a positive number';
|
|
444
|
+
}
|
|
445
|
+
return true;
|
|
446
|
+
},
|
|
447
|
+
});
|
|
448
|
+
minReplicas = parseInt(min || '1', 10);
|
|
449
|
+
|
|
450
|
+
const max = await input({
|
|
451
|
+
message: 'Maximum replicas:',
|
|
452
|
+
defaultValue: String(ctx.maxReplicas || parseInt(replicas, 10) * 2),
|
|
453
|
+
validate: value => {
|
|
454
|
+
const num = parseInt(value, 10);
|
|
455
|
+
if (isNaN(num) || num < minReplicas!) {
|
|
456
|
+
return `Must be >= ${minReplicas}`;
|
|
457
|
+
}
|
|
458
|
+
return true;
|
|
459
|
+
},
|
|
460
|
+
});
|
|
461
|
+
maxReplicas = parseInt(max || '4', 10);
|
|
462
|
+
|
|
463
|
+
const cpu = await input({
|
|
464
|
+
message: 'Target CPU utilization (%):',
|
|
465
|
+
defaultValue: String(ctx.targetCPUUtilization || 70),
|
|
466
|
+
validate: value => {
|
|
467
|
+
const num = parseInt(value, 10);
|
|
468
|
+
if (isNaN(num) || num < 1 || num > 100) {
|
|
469
|
+
return 'Must be between 1 and 100';
|
|
470
|
+
}
|
|
471
|
+
return true;
|
|
472
|
+
},
|
|
473
|
+
});
|
|
474
|
+
targetCPUUtilization = parseInt(cpu || '70', 10);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
return {
|
|
478
|
+
success: true,
|
|
479
|
+
data: {
|
|
480
|
+
replicas: parseInt(replicas, 10),
|
|
481
|
+
includeHpa,
|
|
482
|
+
minReplicas,
|
|
483
|
+
maxReplicas,
|
|
484
|
+
targetCPUUtilization,
|
|
485
|
+
},
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Step 4: Job/CronJob Configuration
|
|
491
|
+
*/
|
|
492
|
+
async function jobConfigStep(ctx: K8sWizardContext): Promise<StepResult> {
|
|
493
|
+
let schedule: string | undefined;
|
|
494
|
+
|
|
495
|
+
if (ctx.workloadType === 'cronjob') {
|
|
496
|
+
const scheduleInput = await input({
|
|
497
|
+
message: 'Cron schedule (e.g., "*/5 * * * *" for every 5 minutes):',
|
|
498
|
+
defaultValue: ctx.schedule || '0 * * * *',
|
|
499
|
+
validate: value => {
|
|
500
|
+
if (!value) {
|
|
501
|
+
return 'Schedule is required for CronJob';
|
|
502
|
+
}
|
|
503
|
+
// Basic cron validation (5 fields)
|
|
504
|
+
const parts = value.trim().split(/\s+/);
|
|
505
|
+
if (parts.length !== 5) {
|
|
506
|
+
return 'Schedule must have 5 fields (min hour day month weekday)';
|
|
507
|
+
}
|
|
508
|
+
return true;
|
|
509
|
+
},
|
|
510
|
+
});
|
|
511
|
+
schedule = scheduleInput;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Backoff limit
|
|
515
|
+
ui.newLine();
|
|
516
|
+
const backoffInput = await input({
|
|
517
|
+
message: 'Backoff limit (retries on failure):',
|
|
518
|
+
defaultValue: String(ctx.backoffLimit || 6),
|
|
519
|
+
validate: value => {
|
|
520
|
+
const num = parseInt(value, 10);
|
|
521
|
+
if (isNaN(num) || num < 0) {
|
|
522
|
+
return 'Must be a non-negative number';
|
|
523
|
+
}
|
|
524
|
+
return true;
|
|
525
|
+
},
|
|
526
|
+
});
|
|
527
|
+
const backoffLimit = parseInt(backoffInput || '6', 10);
|
|
528
|
+
|
|
529
|
+
// Completions
|
|
530
|
+
ui.newLine();
|
|
531
|
+
const completionsInput = await input({
|
|
532
|
+
message: 'Number of completions (total successful pods):',
|
|
533
|
+
defaultValue: String(ctx.completions || 1),
|
|
534
|
+
validate: value => {
|
|
535
|
+
const num = parseInt(value, 10);
|
|
536
|
+
if (isNaN(num) || num < 1) {
|
|
537
|
+
return 'Must be a positive number';
|
|
538
|
+
}
|
|
539
|
+
return true;
|
|
540
|
+
},
|
|
541
|
+
});
|
|
542
|
+
const completions = parseInt(completionsInput || '1', 10);
|
|
543
|
+
|
|
544
|
+
// Parallelism
|
|
545
|
+
ui.newLine();
|
|
546
|
+
const parallelismInput = await input({
|
|
547
|
+
message: 'Parallelism (concurrent pods):',
|
|
548
|
+
defaultValue: String(ctx.parallelism || 1),
|
|
549
|
+
validate: value => {
|
|
550
|
+
const num = parseInt(value, 10);
|
|
551
|
+
if (isNaN(num) || num < 1) {
|
|
552
|
+
return 'Must be a positive number';
|
|
553
|
+
}
|
|
554
|
+
return true;
|
|
555
|
+
},
|
|
556
|
+
});
|
|
557
|
+
const parallelism = parseInt(parallelismInput || '1', 10);
|
|
558
|
+
|
|
559
|
+
return {
|
|
560
|
+
success: true,
|
|
561
|
+
data: {
|
|
562
|
+
schedule,
|
|
563
|
+
backoffLimit,
|
|
564
|
+
completions,
|
|
565
|
+
parallelism,
|
|
566
|
+
},
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Step 5: Port & Service Configuration
|
|
572
|
+
*/
|
|
573
|
+
async function serviceConfigStep(ctx: K8sWizardContext): Promise<StepResult> {
|
|
574
|
+
// Container port
|
|
575
|
+
const portInput = await input({
|
|
576
|
+
message: 'Container port:',
|
|
577
|
+
defaultValue: String(ctx.containerPort || 8080),
|
|
578
|
+
validate: value => {
|
|
579
|
+
const num = parseInt(value, 10);
|
|
580
|
+
if (isNaN(num) || num < 1 || num > 65535) {
|
|
581
|
+
return 'Must be between 1 and 65535';
|
|
582
|
+
}
|
|
583
|
+
return true;
|
|
584
|
+
},
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
if (!portInput) {
|
|
588
|
+
return { success: false, error: 'Port is required' };
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
const containerPort = parseInt(portInput, 10);
|
|
592
|
+
|
|
593
|
+
// Service exposure (not for Jobs)
|
|
594
|
+
let includeService = false;
|
|
595
|
+
let serviceType: K8sServiceType = 'ClusterIP';
|
|
596
|
+
let servicePort = containerPort;
|
|
597
|
+
|
|
598
|
+
if (!['job', 'cronjob'].includes(ctx.workloadType || '')) {
|
|
599
|
+
ui.newLine();
|
|
600
|
+
includeService = await confirm({
|
|
601
|
+
message: 'Create a Service to expose this workload?',
|
|
602
|
+
defaultValue: true,
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
if (includeService) {
|
|
606
|
+
ui.newLine();
|
|
607
|
+
serviceType =
|
|
608
|
+
(await select<K8sServiceType>({
|
|
609
|
+
message: 'Service type:',
|
|
610
|
+
options: [
|
|
611
|
+
{
|
|
612
|
+
value: 'ClusterIP',
|
|
613
|
+
label: 'ClusterIP',
|
|
614
|
+
description: 'Internal cluster access only (default)',
|
|
615
|
+
},
|
|
616
|
+
{
|
|
617
|
+
value: 'NodePort',
|
|
618
|
+
label: 'NodePort',
|
|
619
|
+
description: "Expose on each node's IP at a static port",
|
|
620
|
+
},
|
|
621
|
+
{
|
|
622
|
+
value: 'LoadBalancer',
|
|
623
|
+
label: 'LoadBalancer',
|
|
624
|
+
description: 'External load balancer (cloud provider)',
|
|
625
|
+
},
|
|
626
|
+
],
|
|
627
|
+
defaultValue: ctx.serviceType || 'ClusterIP',
|
|
628
|
+
})) || 'ClusterIP';
|
|
629
|
+
|
|
630
|
+
const servicePortInput = await input({
|
|
631
|
+
message: 'Service port:',
|
|
632
|
+
defaultValue: String(ctx.servicePort || containerPort),
|
|
633
|
+
validate: value => {
|
|
634
|
+
const num = parseInt(value, 10);
|
|
635
|
+
if (isNaN(num) || num < 1 || num > 65535) {
|
|
636
|
+
return 'Must be between 1 and 65535';
|
|
637
|
+
}
|
|
638
|
+
return true;
|
|
639
|
+
},
|
|
640
|
+
});
|
|
641
|
+
servicePort = parseInt(servicePortInput || String(containerPort), 10);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
return {
|
|
646
|
+
success: true,
|
|
647
|
+
data: {
|
|
648
|
+
containerPort,
|
|
649
|
+
includeService,
|
|
650
|
+
serviceType,
|
|
651
|
+
servicePort,
|
|
652
|
+
},
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* Step 6: Resource Limits
|
|
658
|
+
*/
|
|
659
|
+
async function resourceLimitsStep(ctx: K8sWizardContext): Promise<StepResult> {
|
|
660
|
+
const setLimits = await confirm({
|
|
661
|
+
message: 'Configure resource requests and limits? (recommended for production)',
|
|
662
|
+
defaultValue: true,
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
if (!setLimits) {
|
|
666
|
+
return { success: true, data: {} };
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
ui.newLine();
|
|
670
|
+
ui.info('Resource requests (guaranteed resources):');
|
|
671
|
+
|
|
672
|
+
const cpuRequest = await input({
|
|
673
|
+
message: 'CPU request (e.g., 100m, 0.5):',
|
|
674
|
+
defaultValue: ctx.cpuRequest || '100m',
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
const memoryRequest = await input({
|
|
678
|
+
message: 'Memory request (e.g., 128Mi, 1Gi):',
|
|
679
|
+
defaultValue: ctx.memoryRequest || '128Mi',
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
ui.newLine();
|
|
683
|
+
ui.info('Resource limits (maximum allowed):');
|
|
684
|
+
|
|
685
|
+
const cpuLimit = await input({
|
|
686
|
+
message: 'CPU limit (e.g., 500m, 1):',
|
|
687
|
+
defaultValue: ctx.cpuLimit || '500m',
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
const memoryLimit = await input({
|
|
691
|
+
message: 'Memory limit (e.g., 256Mi, 2Gi):',
|
|
692
|
+
defaultValue: ctx.memoryLimit || '256Mi',
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
return {
|
|
696
|
+
success: true,
|
|
697
|
+
data: {
|
|
698
|
+
cpuRequest,
|
|
699
|
+
cpuLimit,
|
|
700
|
+
memoryRequest,
|
|
701
|
+
memoryLimit,
|
|
702
|
+
},
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/**
|
|
707
|
+
* Step 7: Additional Resources
|
|
708
|
+
*/
|
|
709
|
+
async function additionalResourcesStep(ctx: K8sWizardContext): Promise<StepResult> {
|
|
710
|
+
const resourceOptions = [
|
|
711
|
+
{ value: 'configmap', label: 'ConfigMap', description: 'Environment configuration' },
|
|
712
|
+
{ value: 'secret', label: 'Secret', description: 'Sensitive data (passwords, tokens)' },
|
|
713
|
+
];
|
|
714
|
+
|
|
715
|
+
// Only show Ingress for services
|
|
716
|
+
if (ctx.includeService) {
|
|
717
|
+
resourceOptions.push({
|
|
718
|
+
value: 'ingress',
|
|
719
|
+
label: 'Ingress',
|
|
720
|
+
description: 'HTTP/HTTPS routing (requires ingress controller)',
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// Only show PDB for Deployments/StatefulSets
|
|
725
|
+
if (['deployment', 'statefulset'].includes(ctx.workloadType || '')) {
|
|
726
|
+
resourceOptions.push({
|
|
727
|
+
value: 'pdb',
|
|
728
|
+
label: 'PodDisruptionBudget',
|
|
729
|
+
description: 'Ensure availability during disruptions',
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
const selectedResources = (await multiSelect({
|
|
734
|
+
message: 'Select additional resources to generate:',
|
|
735
|
+
options: resourceOptions,
|
|
736
|
+
required: false,
|
|
737
|
+
})) as string[];
|
|
738
|
+
|
|
739
|
+
const includeConfigMap = selectedResources.includes('configmap') || ctx.includeConfigMap;
|
|
740
|
+
const includeSecret = selectedResources.includes('secret') || ctx.includeSecret;
|
|
741
|
+
const includeIngress = selectedResources.includes('ingress') || ctx.includeIngress;
|
|
742
|
+
const includePdb = selectedResources.includes('pdb') || ctx.includePdb;
|
|
743
|
+
|
|
744
|
+
// Ingress configuration
|
|
745
|
+
let ingressHost: string | undefined;
|
|
746
|
+
let ingressPath: string | undefined;
|
|
747
|
+
let ingressTls = false;
|
|
748
|
+
|
|
749
|
+
if (includeIngress) {
|
|
750
|
+
ui.newLine();
|
|
751
|
+
ui.info('Ingress Configuration:');
|
|
752
|
+
|
|
753
|
+
ingressHost = await input({
|
|
754
|
+
message: 'Hostname (e.g., app.example.com):',
|
|
755
|
+
defaultValue: ctx.ingressHost || `${ctx.name}.example.com`,
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
ingressPath = await input({
|
|
759
|
+
message: 'Path:',
|
|
760
|
+
defaultValue: ctx.ingressPath || '/',
|
|
761
|
+
});
|
|
762
|
+
|
|
763
|
+
ingressTls = await confirm({
|
|
764
|
+
message: 'Enable TLS?',
|
|
765
|
+
defaultValue: ctx.ingressTls ?? true,
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// PDB configuration
|
|
770
|
+
let minAvailable: number | string | undefined;
|
|
771
|
+
|
|
772
|
+
if (includePdb) {
|
|
773
|
+
ui.newLine();
|
|
774
|
+
const minAvailableInput = await input({
|
|
775
|
+
message: 'Minimum available pods (number or percentage like "50%"):',
|
|
776
|
+
defaultValue: String(ctx.minAvailable || 1),
|
|
777
|
+
});
|
|
778
|
+
minAvailable = minAvailableInput?.includes('%')
|
|
779
|
+
? minAvailableInput
|
|
780
|
+
: parseInt(minAvailableInput || '1', 10);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
return {
|
|
784
|
+
success: true,
|
|
785
|
+
data: {
|
|
786
|
+
includeConfigMap,
|
|
787
|
+
includeSecret,
|
|
788
|
+
includeIngress,
|
|
789
|
+
includePdb,
|
|
790
|
+
ingressHost,
|
|
791
|
+
ingressPath,
|
|
792
|
+
ingressTls,
|
|
793
|
+
minAvailable,
|
|
794
|
+
},
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
/**
|
|
799
|
+
* Step 8: Health Checks
|
|
800
|
+
*/
|
|
801
|
+
async function healthChecksStep(ctx: K8sWizardContext): Promise<StepResult> {
|
|
802
|
+
const includeProbes = await confirm({
|
|
803
|
+
message: 'Configure health check probes? (recommended for production)',
|
|
804
|
+
defaultValue: true,
|
|
805
|
+
});
|
|
806
|
+
|
|
807
|
+
if (!includeProbes) {
|
|
808
|
+
return { success: true, data: { includeProbes: false } };
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
ui.newLine();
|
|
812
|
+
const livenessPath = await input({
|
|
813
|
+
message: 'Liveness probe path (e.g., /healthz):',
|
|
814
|
+
defaultValue: ctx.livenessPath || '/healthz',
|
|
815
|
+
});
|
|
816
|
+
|
|
817
|
+
const readinessPath = await input({
|
|
818
|
+
message: 'Readiness probe path (e.g., /ready):',
|
|
819
|
+
defaultValue: ctx.readinessPath || '/ready',
|
|
820
|
+
});
|
|
821
|
+
|
|
822
|
+
return {
|
|
823
|
+
success: true,
|
|
824
|
+
data: {
|
|
825
|
+
includeProbes,
|
|
826
|
+
livenessPath,
|
|
827
|
+
readinessPath,
|
|
828
|
+
},
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
/**
|
|
833
|
+
* Step 9: Output Configuration
|
|
834
|
+
*/
|
|
835
|
+
async function outputConfigStep(ctx: K8sWizardContext): Promise<StepResult> {
|
|
836
|
+
const outputFormat = await select<'multiple' | 'single' | 'kustomize'>({
|
|
837
|
+
message: 'Output format:',
|
|
838
|
+
options: [
|
|
839
|
+
{
|
|
840
|
+
value: 'multiple',
|
|
841
|
+
label: 'Multiple files',
|
|
842
|
+
description: 'One file per resource (deployment.yaml, service.yaml, etc.)',
|
|
843
|
+
},
|
|
844
|
+
{
|
|
845
|
+
value: 'single',
|
|
846
|
+
label: 'Single file',
|
|
847
|
+
description: 'All resources in one file with document separators',
|
|
848
|
+
},
|
|
849
|
+
{
|
|
850
|
+
value: 'kustomize',
|
|
851
|
+
label: 'Kustomize structure',
|
|
852
|
+
description: 'Base with kustomization.yaml for overlays',
|
|
853
|
+
},
|
|
854
|
+
],
|
|
855
|
+
defaultValue: ctx.outputFormat || 'multiple',
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
ui.newLine();
|
|
859
|
+
const outputPath = await pathInput('Output directory:', ctx.outputPath || `./${ctx.name}-k8s`);
|
|
860
|
+
|
|
861
|
+
if (!outputPath) {
|
|
862
|
+
return { success: false, error: 'Output path is required' };
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
return {
|
|
866
|
+
success: true,
|
|
867
|
+
data: {
|
|
868
|
+
outputFormat,
|
|
869
|
+
outputPath,
|
|
870
|
+
},
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
/**
|
|
875
|
+
* Step 10: Generate Manifests
|
|
876
|
+
*/
|
|
877
|
+
async function generateStep(ctx: K8sWizardContext): Promise<StepResult> {
|
|
878
|
+
ui.startSpinner({ message: 'Generating Kubernetes manifests...' });
|
|
879
|
+
|
|
880
|
+
try {
|
|
881
|
+
// Build the generation request
|
|
882
|
+
const request = buildGenerationRequest(ctx);
|
|
883
|
+
|
|
884
|
+
// Call generator service
|
|
885
|
+
const response = await generatorClient.post<{
|
|
886
|
+
success: boolean;
|
|
887
|
+
files: Array<{ name: string; content: string; path: string }>;
|
|
888
|
+
error?: string;
|
|
889
|
+
}>('/api/generate/k8s', request);
|
|
890
|
+
|
|
891
|
+
if (!response.success || !response.data?.success) {
|
|
892
|
+
// If generator service is not available, generate locally
|
|
893
|
+
ui.stopSpinnerFail('Generator service unavailable');
|
|
894
|
+
ui.info('Generating manifests locally...');
|
|
895
|
+
|
|
896
|
+
const files = generateManifestsLocally(ctx);
|
|
897
|
+
await writeFilesToDisk(files, ctx.outputPath!);
|
|
898
|
+
|
|
899
|
+
ui.newLine();
|
|
900
|
+
ui.success(`Generated ${files.length} manifest(s)`);
|
|
901
|
+
|
|
902
|
+
return {
|
|
903
|
+
success: true,
|
|
904
|
+
data: {
|
|
905
|
+
generatedFiles: files.map(f => f.path),
|
|
906
|
+
},
|
|
907
|
+
};
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
// Write files from generator service response
|
|
911
|
+
await writeFilesToDisk(response.data.files, ctx.outputPath!);
|
|
912
|
+
|
|
913
|
+
ui.stopSpinnerSuccess(`Generated ${response.data.files.length} manifest(s)`);
|
|
914
|
+
|
|
915
|
+
return {
|
|
916
|
+
success: true,
|
|
917
|
+
data: {
|
|
918
|
+
generatedFiles: response.data.files.map(f => f.path),
|
|
919
|
+
},
|
|
920
|
+
};
|
|
921
|
+
} catch (error: any) {
|
|
922
|
+
// Fall back to local generation
|
|
923
|
+
ui.stopSpinnerFail('Generator service error');
|
|
924
|
+
ui.info('Generating manifests locally...');
|
|
925
|
+
|
|
926
|
+
try {
|
|
927
|
+
const files = generateManifestsLocally(ctx);
|
|
928
|
+
await writeFilesToDisk(files, ctx.outputPath!);
|
|
929
|
+
|
|
930
|
+
ui.newLine();
|
|
931
|
+
ui.success(`Generated ${files.length} manifest(s)`);
|
|
932
|
+
|
|
933
|
+
return {
|
|
934
|
+
success: true,
|
|
935
|
+
data: {
|
|
936
|
+
generatedFiles: files.map(f => f.path),
|
|
937
|
+
},
|
|
938
|
+
};
|
|
939
|
+
} catch (localError: any) {
|
|
940
|
+
return {
|
|
941
|
+
success: false,
|
|
942
|
+
error: localError.message,
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
/**
|
|
949
|
+
* Build the generation request from context
|
|
950
|
+
*/
|
|
951
|
+
function buildGenerationRequest(ctx: K8sWizardContext): Record<string, unknown> {
|
|
952
|
+
return {
|
|
953
|
+
workloadType: ctx.workloadType,
|
|
954
|
+
name: ctx.name,
|
|
955
|
+
namespace: ctx.namespace,
|
|
956
|
+
image: ctx.image,
|
|
957
|
+
imageTag: ctx.imageTag,
|
|
958
|
+
replicas: ctx.replicas,
|
|
959
|
+
containerPort: ctx.containerPort,
|
|
960
|
+
serviceType: ctx.serviceType,
|
|
961
|
+
servicePort: ctx.servicePort,
|
|
962
|
+
cpuRequest: ctx.cpuRequest,
|
|
963
|
+
cpuLimit: ctx.cpuLimit,
|
|
964
|
+
memoryRequest: ctx.memoryRequest,
|
|
965
|
+
memoryLimit: ctx.memoryLimit,
|
|
966
|
+
includeService: ctx.includeService,
|
|
967
|
+
includeIngress: ctx.includeIngress,
|
|
968
|
+
includeHpa: ctx.includeHpa,
|
|
969
|
+
includePdb: ctx.includePdb,
|
|
970
|
+
includeConfigMap: ctx.includeConfigMap,
|
|
971
|
+
includeSecret: ctx.includeSecret,
|
|
972
|
+
includeProbes: ctx.includeProbes,
|
|
973
|
+
livenessPath: ctx.livenessPath,
|
|
974
|
+
readinessPath: ctx.readinessPath,
|
|
975
|
+
ingressHost: ctx.ingressHost,
|
|
976
|
+
ingressPath: ctx.ingressPath,
|
|
977
|
+
ingressTls: ctx.ingressTls,
|
|
978
|
+
minAvailable: ctx.minAvailable,
|
|
979
|
+
minReplicas: ctx.minReplicas,
|
|
980
|
+
maxReplicas: ctx.maxReplicas,
|
|
981
|
+
targetCPUUtilization: ctx.targetCPUUtilization,
|
|
982
|
+
schedule: ctx.schedule,
|
|
983
|
+
backoffLimit: ctx.backoffLimit,
|
|
984
|
+
completions: ctx.completions,
|
|
985
|
+
parallelism: ctx.parallelism,
|
|
986
|
+
outputFormat: ctx.outputFormat,
|
|
987
|
+
outputPath: ctx.outputPath,
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
/**
|
|
992
|
+
* Generate manifests locally when service is unavailable
|
|
993
|
+
*/
|
|
994
|
+
function generateManifestsLocally(
|
|
995
|
+
ctx: K8sWizardContext
|
|
996
|
+
): Array<{ name: string; content: string; path: string }> {
|
|
997
|
+
const files: Array<{ name: string; content: string; path: string }> = [];
|
|
998
|
+
const labels: Record<string, string> = {
|
|
999
|
+
'app.kubernetes.io/name': ctx.name || 'unnamed',
|
|
1000
|
+
'app.kubernetes.io/instance': ctx.name || 'unnamed',
|
|
1001
|
+
'app.kubernetes.io/managed-by': 'nimbus',
|
|
1002
|
+
};
|
|
1003
|
+
|
|
1004
|
+
// Generate main workload
|
|
1005
|
+
const workloadManifest = generateWorkloadManifest(ctx, labels);
|
|
1006
|
+
files.push({
|
|
1007
|
+
name: `${ctx.workloadType}.yaml`,
|
|
1008
|
+
content: workloadManifest,
|
|
1009
|
+
path: `${ctx.outputPath}/${ctx.workloadType}.yaml`,
|
|
1010
|
+
});
|
|
1011
|
+
|
|
1012
|
+
// Generate Service
|
|
1013
|
+
if (ctx.includeService) {
|
|
1014
|
+
files.push({
|
|
1015
|
+
name: 'service.yaml',
|
|
1016
|
+
content: generateServiceManifest(ctx, labels),
|
|
1017
|
+
path: `${ctx.outputPath}/service.yaml`,
|
|
1018
|
+
});
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
// Generate Ingress
|
|
1022
|
+
if (ctx.includeIngress) {
|
|
1023
|
+
files.push({
|
|
1024
|
+
name: 'ingress.yaml',
|
|
1025
|
+
content: generateIngressManifest(ctx, labels),
|
|
1026
|
+
path: `${ctx.outputPath}/ingress.yaml`,
|
|
1027
|
+
});
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
// Generate HPA
|
|
1031
|
+
if (ctx.includeHpa) {
|
|
1032
|
+
files.push({
|
|
1033
|
+
name: 'hpa.yaml',
|
|
1034
|
+
content: generateHpaManifest(ctx, labels),
|
|
1035
|
+
path: `${ctx.outputPath}/hpa.yaml`,
|
|
1036
|
+
});
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
// Generate PDB
|
|
1040
|
+
if (ctx.includePdb) {
|
|
1041
|
+
files.push({
|
|
1042
|
+
name: 'pdb.yaml',
|
|
1043
|
+
content: generatePdbManifest(ctx, labels),
|
|
1044
|
+
path: `${ctx.outputPath}/pdb.yaml`,
|
|
1045
|
+
});
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
// Generate ConfigMap
|
|
1049
|
+
if (ctx.includeConfigMap) {
|
|
1050
|
+
files.push({
|
|
1051
|
+
name: 'configmap.yaml',
|
|
1052
|
+
content: generateConfigMapManifest(ctx, labels),
|
|
1053
|
+
path: `${ctx.outputPath}/configmap.yaml`,
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
// Generate Secret
|
|
1058
|
+
if (ctx.includeSecret) {
|
|
1059
|
+
files.push({
|
|
1060
|
+
name: 'secret.yaml',
|
|
1061
|
+
content: generateSecretManifest(ctx, labels),
|
|
1062
|
+
path: `${ctx.outputPath}/secret.yaml`,
|
|
1063
|
+
});
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
return files;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
/**
|
|
1070
|
+
* Generate main workload manifest
|
|
1071
|
+
*/
|
|
1072
|
+
function generateWorkloadManifest(ctx: K8sWizardContext, labels: Record<string, string>): string {
|
|
1073
|
+
const { workloadType, name, namespace, image, imageTag, replicas, containerPort } = ctx;
|
|
1074
|
+
|
|
1075
|
+
// Build container spec
|
|
1076
|
+
const containerSpec: Record<string, unknown> = {
|
|
1077
|
+
name,
|
|
1078
|
+
image: `${image}:${imageTag || 'latest'}`,
|
|
1079
|
+
ports: containerPort ? [{ containerPort }] : undefined,
|
|
1080
|
+
resources:
|
|
1081
|
+
ctx.cpuRequest || ctx.memoryRequest
|
|
1082
|
+
? {
|
|
1083
|
+
requests: {
|
|
1084
|
+
...(ctx.cpuRequest && { cpu: ctx.cpuRequest }),
|
|
1085
|
+
...(ctx.memoryRequest && { memory: ctx.memoryRequest }),
|
|
1086
|
+
},
|
|
1087
|
+
limits: {
|
|
1088
|
+
...(ctx.cpuLimit && { cpu: ctx.cpuLimit }),
|
|
1089
|
+
...(ctx.memoryLimit && { memory: ctx.memoryLimit }),
|
|
1090
|
+
},
|
|
1091
|
+
}
|
|
1092
|
+
: undefined,
|
|
1093
|
+
};
|
|
1094
|
+
|
|
1095
|
+
// Add probes if configured
|
|
1096
|
+
if (ctx.includeProbes) {
|
|
1097
|
+
containerSpec.livenessProbe = {
|
|
1098
|
+
httpGet: {
|
|
1099
|
+
path: ctx.livenessPath || '/healthz',
|
|
1100
|
+
port: containerPort,
|
|
1101
|
+
},
|
|
1102
|
+
initialDelaySeconds: 10,
|
|
1103
|
+
periodSeconds: 10,
|
|
1104
|
+
};
|
|
1105
|
+
containerSpec.readinessProbe = {
|
|
1106
|
+
httpGet: {
|
|
1107
|
+
path: ctx.readinessPath || '/ready',
|
|
1108
|
+
port: containerPort,
|
|
1109
|
+
},
|
|
1110
|
+
initialDelaySeconds: 5,
|
|
1111
|
+
periodSeconds: 5,
|
|
1112
|
+
};
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
// Add envFrom if ConfigMap or Secret
|
|
1116
|
+
const envFrom = [];
|
|
1117
|
+
if (ctx.includeConfigMap) {
|
|
1118
|
+
envFrom.push({ configMapRef: { name: `${name}-config` } });
|
|
1119
|
+
}
|
|
1120
|
+
if (ctx.includeSecret) {
|
|
1121
|
+
envFrom.push({ secretRef: { name: `${name}-secret` } });
|
|
1122
|
+
}
|
|
1123
|
+
if (envFrom.length > 0) {
|
|
1124
|
+
containerSpec.envFrom = envFrom;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
// Clean up undefined values
|
|
1128
|
+
Object.keys(containerSpec).forEach(key => {
|
|
1129
|
+
if (containerSpec[key] === undefined) {
|
|
1130
|
+
delete containerSpec[key];
|
|
1131
|
+
}
|
|
1132
|
+
});
|
|
1133
|
+
|
|
1134
|
+
const podSpec = {
|
|
1135
|
+
containers: [containerSpec],
|
|
1136
|
+
};
|
|
1137
|
+
|
|
1138
|
+
let manifest: Record<string, unknown>;
|
|
1139
|
+
|
|
1140
|
+
switch (workloadType) {
|
|
1141
|
+
case 'deployment':
|
|
1142
|
+
manifest = {
|
|
1143
|
+
apiVersion: 'apps/v1',
|
|
1144
|
+
kind: 'Deployment',
|
|
1145
|
+
metadata: { name, namespace, labels },
|
|
1146
|
+
spec: {
|
|
1147
|
+
replicas,
|
|
1148
|
+
selector: { matchLabels: { 'app.kubernetes.io/name': name } },
|
|
1149
|
+
template: {
|
|
1150
|
+
metadata: { labels },
|
|
1151
|
+
spec: podSpec,
|
|
1152
|
+
},
|
|
1153
|
+
},
|
|
1154
|
+
};
|
|
1155
|
+
break;
|
|
1156
|
+
|
|
1157
|
+
case 'statefulset':
|
|
1158
|
+
manifest = {
|
|
1159
|
+
apiVersion: 'apps/v1',
|
|
1160
|
+
kind: 'StatefulSet',
|
|
1161
|
+
metadata: { name, namespace, labels },
|
|
1162
|
+
spec: {
|
|
1163
|
+
replicas,
|
|
1164
|
+
serviceName: name,
|
|
1165
|
+
selector: { matchLabels: { 'app.kubernetes.io/name': name } },
|
|
1166
|
+
template: {
|
|
1167
|
+
metadata: { labels },
|
|
1168
|
+
spec: podSpec,
|
|
1169
|
+
},
|
|
1170
|
+
},
|
|
1171
|
+
};
|
|
1172
|
+
break;
|
|
1173
|
+
|
|
1174
|
+
case 'daemonset':
|
|
1175
|
+
manifest = {
|
|
1176
|
+
apiVersion: 'apps/v1',
|
|
1177
|
+
kind: 'DaemonSet',
|
|
1178
|
+
metadata: { name, namespace, labels },
|
|
1179
|
+
spec: {
|
|
1180
|
+
selector: { matchLabels: { 'app.kubernetes.io/name': name } },
|
|
1181
|
+
template: {
|
|
1182
|
+
metadata: { labels },
|
|
1183
|
+
spec: podSpec,
|
|
1184
|
+
},
|
|
1185
|
+
},
|
|
1186
|
+
};
|
|
1187
|
+
break;
|
|
1188
|
+
|
|
1189
|
+
case 'job':
|
|
1190
|
+
manifest = {
|
|
1191
|
+
apiVersion: 'batch/v1',
|
|
1192
|
+
kind: 'Job',
|
|
1193
|
+
metadata: { name, namespace, labels },
|
|
1194
|
+
spec: {
|
|
1195
|
+
backoffLimit: ctx.backoffLimit,
|
|
1196
|
+
completions: ctx.completions,
|
|
1197
|
+
parallelism: ctx.parallelism,
|
|
1198
|
+
template: {
|
|
1199
|
+
metadata: { labels },
|
|
1200
|
+
spec: {
|
|
1201
|
+
...podSpec,
|
|
1202
|
+
restartPolicy: 'Never',
|
|
1203
|
+
},
|
|
1204
|
+
},
|
|
1205
|
+
},
|
|
1206
|
+
};
|
|
1207
|
+
break;
|
|
1208
|
+
|
|
1209
|
+
case 'cronjob':
|
|
1210
|
+
manifest = {
|
|
1211
|
+
apiVersion: 'batch/v1',
|
|
1212
|
+
kind: 'CronJob',
|
|
1213
|
+
metadata: { name, namespace, labels },
|
|
1214
|
+
spec: {
|
|
1215
|
+
schedule: ctx.schedule,
|
|
1216
|
+
jobTemplate: {
|
|
1217
|
+
spec: {
|
|
1218
|
+
backoffLimit: ctx.backoffLimit,
|
|
1219
|
+
template: {
|
|
1220
|
+
metadata: { labels },
|
|
1221
|
+
spec: {
|
|
1222
|
+
...podSpec,
|
|
1223
|
+
restartPolicy: 'Never',
|
|
1224
|
+
},
|
|
1225
|
+
},
|
|
1226
|
+
},
|
|
1227
|
+
},
|
|
1228
|
+
},
|
|
1229
|
+
};
|
|
1230
|
+
break;
|
|
1231
|
+
|
|
1232
|
+
default:
|
|
1233
|
+
manifest = {};
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
return toYaml(manifest);
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
/**
|
|
1240
|
+
* Generate Service manifest
|
|
1241
|
+
*/
|
|
1242
|
+
function generateServiceManifest(ctx: K8sWizardContext, labels: Record<string, string>): string {
|
|
1243
|
+
const manifest = {
|
|
1244
|
+
apiVersion: 'v1',
|
|
1245
|
+
kind: 'Service',
|
|
1246
|
+
metadata: {
|
|
1247
|
+
name: ctx.name,
|
|
1248
|
+
namespace: ctx.namespace,
|
|
1249
|
+
labels,
|
|
1250
|
+
},
|
|
1251
|
+
spec: {
|
|
1252
|
+
type: ctx.serviceType,
|
|
1253
|
+
selector: { 'app.kubernetes.io/name': ctx.name },
|
|
1254
|
+
ports: [
|
|
1255
|
+
{
|
|
1256
|
+
port: ctx.servicePort,
|
|
1257
|
+
targetPort: ctx.containerPort,
|
|
1258
|
+
protocol: 'TCP',
|
|
1259
|
+
},
|
|
1260
|
+
],
|
|
1261
|
+
},
|
|
1262
|
+
};
|
|
1263
|
+
|
|
1264
|
+
return toYaml(manifest);
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
/**
|
|
1268
|
+
* Generate Ingress manifest
|
|
1269
|
+
*/
|
|
1270
|
+
function generateIngressManifest(ctx: K8sWizardContext, labels: Record<string, string>): string {
|
|
1271
|
+
const manifest: Record<string, unknown> = {
|
|
1272
|
+
apiVersion: 'networking.k8s.io/v1',
|
|
1273
|
+
kind: 'Ingress',
|
|
1274
|
+
metadata: {
|
|
1275
|
+
name: ctx.name,
|
|
1276
|
+
namespace: ctx.namespace,
|
|
1277
|
+
labels,
|
|
1278
|
+
annotations: {
|
|
1279
|
+
'kubernetes.io/ingress.class': 'nginx',
|
|
1280
|
+
},
|
|
1281
|
+
},
|
|
1282
|
+
spec: {
|
|
1283
|
+
rules: [
|
|
1284
|
+
{
|
|
1285
|
+
host: ctx.ingressHost,
|
|
1286
|
+
http: {
|
|
1287
|
+
paths: [
|
|
1288
|
+
{
|
|
1289
|
+
path: ctx.ingressPath || '/',
|
|
1290
|
+
pathType: 'Prefix',
|
|
1291
|
+
backend: {
|
|
1292
|
+
service: {
|
|
1293
|
+
name: ctx.name,
|
|
1294
|
+
port: { number: ctx.servicePort },
|
|
1295
|
+
},
|
|
1296
|
+
},
|
|
1297
|
+
},
|
|
1298
|
+
],
|
|
1299
|
+
},
|
|
1300
|
+
},
|
|
1301
|
+
],
|
|
1302
|
+
},
|
|
1303
|
+
};
|
|
1304
|
+
|
|
1305
|
+
if (ctx.ingressTls) {
|
|
1306
|
+
(manifest.spec as Record<string, unknown>).tls = [
|
|
1307
|
+
{
|
|
1308
|
+
hosts: [ctx.ingressHost],
|
|
1309
|
+
secretName: `${ctx.name}-tls`,
|
|
1310
|
+
},
|
|
1311
|
+
];
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
return toYaml(manifest);
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
/**
|
|
1318
|
+
* Generate HPA manifest
|
|
1319
|
+
*/
|
|
1320
|
+
function generateHpaManifest(ctx: K8sWizardContext, labels: Record<string, string>): string {
|
|
1321
|
+
const manifest = {
|
|
1322
|
+
apiVersion: 'autoscaling/v2',
|
|
1323
|
+
kind: 'HorizontalPodAutoscaler',
|
|
1324
|
+
metadata: {
|
|
1325
|
+
name: ctx.name,
|
|
1326
|
+
namespace: ctx.namespace,
|
|
1327
|
+
labels,
|
|
1328
|
+
},
|
|
1329
|
+
spec: {
|
|
1330
|
+
scaleTargetRef: {
|
|
1331
|
+
apiVersion: 'apps/v1',
|
|
1332
|
+
kind: ctx.workloadType === 'statefulset' ? 'StatefulSet' : 'Deployment',
|
|
1333
|
+
name: ctx.name,
|
|
1334
|
+
},
|
|
1335
|
+
minReplicas: ctx.minReplicas,
|
|
1336
|
+
maxReplicas: ctx.maxReplicas,
|
|
1337
|
+
metrics: [
|
|
1338
|
+
{
|
|
1339
|
+
type: 'Resource',
|
|
1340
|
+
resource: {
|
|
1341
|
+
name: 'cpu',
|
|
1342
|
+
target: {
|
|
1343
|
+
type: 'Utilization',
|
|
1344
|
+
averageUtilization: ctx.targetCPUUtilization,
|
|
1345
|
+
},
|
|
1346
|
+
},
|
|
1347
|
+
},
|
|
1348
|
+
],
|
|
1349
|
+
},
|
|
1350
|
+
};
|
|
1351
|
+
|
|
1352
|
+
return toYaml(manifest);
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
/**
|
|
1356
|
+
* Generate PDB manifest
|
|
1357
|
+
*/
|
|
1358
|
+
function generatePdbManifest(ctx: K8sWizardContext, labels: Record<string, string>): string {
|
|
1359
|
+
const manifest = {
|
|
1360
|
+
apiVersion: 'policy/v1',
|
|
1361
|
+
kind: 'PodDisruptionBudget',
|
|
1362
|
+
metadata: {
|
|
1363
|
+
name: ctx.name,
|
|
1364
|
+
namespace: ctx.namespace,
|
|
1365
|
+
labels,
|
|
1366
|
+
},
|
|
1367
|
+
spec: {
|
|
1368
|
+
minAvailable: ctx.minAvailable,
|
|
1369
|
+
selector: { matchLabels: { 'app.kubernetes.io/name': ctx.name } },
|
|
1370
|
+
},
|
|
1371
|
+
};
|
|
1372
|
+
|
|
1373
|
+
return toYaml(manifest);
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
/**
|
|
1377
|
+
* Generate ConfigMap manifest
|
|
1378
|
+
*/
|
|
1379
|
+
function generateConfigMapManifest(ctx: K8sWizardContext, labels: Record<string, string>): string {
|
|
1380
|
+
const manifest = {
|
|
1381
|
+
apiVersion: 'v1',
|
|
1382
|
+
kind: 'ConfigMap',
|
|
1383
|
+
metadata: {
|
|
1384
|
+
name: `${ctx.name}-config`,
|
|
1385
|
+
namespace: ctx.namespace,
|
|
1386
|
+
labels,
|
|
1387
|
+
},
|
|
1388
|
+
data: {
|
|
1389
|
+
// Placeholder values - user should customize
|
|
1390
|
+
APP_ENV: 'production',
|
|
1391
|
+
LOG_LEVEL: 'info',
|
|
1392
|
+
},
|
|
1393
|
+
};
|
|
1394
|
+
|
|
1395
|
+
return toYaml(manifest);
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
/**
|
|
1399
|
+
* Generate Secret manifest
|
|
1400
|
+
*/
|
|
1401
|
+
function generateSecretManifest(ctx: K8sWizardContext, labels: Record<string, string>): string {
|
|
1402
|
+
const manifest = {
|
|
1403
|
+
apiVersion: 'v1',
|
|
1404
|
+
kind: 'Secret',
|
|
1405
|
+
metadata: {
|
|
1406
|
+
name: `${ctx.name}-secret`,
|
|
1407
|
+
namespace: ctx.namespace,
|
|
1408
|
+
labels,
|
|
1409
|
+
},
|
|
1410
|
+
type: 'Opaque',
|
|
1411
|
+
stringData: {
|
|
1412
|
+
// Placeholder values - user should customize
|
|
1413
|
+
'example-key': 'example-value',
|
|
1414
|
+
},
|
|
1415
|
+
};
|
|
1416
|
+
|
|
1417
|
+
return toYaml(manifest);
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
/**
|
|
1421
|
+
* Convert object to YAML string
|
|
1422
|
+
*/
|
|
1423
|
+
function toYaml(obj: Record<string, unknown>, indent = 0): string {
|
|
1424
|
+
const lines: string[] = [];
|
|
1425
|
+
const spaces = ' '.repeat(indent);
|
|
1426
|
+
|
|
1427
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
1428
|
+
if (value === undefined || value === null) {
|
|
1429
|
+
continue;
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
if (Array.isArray(value)) {
|
|
1433
|
+
if (value.length === 0) {
|
|
1434
|
+
continue;
|
|
1435
|
+
}
|
|
1436
|
+
lines.push(`${spaces}${key}:`);
|
|
1437
|
+
for (const item of value) {
|
|
1438
|
+
if (typeof item === 'object' && item !== null) {
|
|
1439
|
+
const itemYaml = toYaml(item as Record<string, unknown>, indent + 1);
|
|
1440
|
+
const itemLines = itemYaml.split('\n').filter(l => l.trim());
|
|
1441
|
+
lines.push(`${spaces}- ${itemLines[0].trim()}`);
|
|
1442
|
+
for (let i = 1; i < itemLines.length; i++) {
|
|
1443
|
+
lines.push(`${spaces} ${itemLines[i].trim()}`);
|
|
1444
|
+
}
|
|
1445
|
+
} else {
|
|
1446
|
+
lines.push(`${spaces}- ${item}`);
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
} else if (typeof value === 'object') {
|
|
1450
|
+
lines.push(`${spaces}${key}:`);
|
|
1451
|
+
lines.push(toYaml(value as Record<string, unknown>, indent + 1));
|
|
1452
|
+
} else if (
|
|
1453
|
+
typeof value === 'string' &&
|
|
1454
|
+
(value.includes(':') || value.includes('#') || value.includes('\n'))
|
|
1455
|
+
) {
|
|
1456
|
+
lines.push(`${spaces}${key}: "${value}"`);
|
|
1457
|
+
} else {
|
|
1458
|
+
lines.push(`${spaces}${key}: ${value}`);
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
return lines.join('\n');
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
/**
|
|
1466
|
+
* Write files to disk
|
|
1467
|
+
*/
|
|
1468
|
+
async function writeFilesToDisk(
|
|
1469
|
+
files: Array<{ name: string; content: string; path: string }>,
|
|
1470
|
+
outputPath: string
|
|
1471
|
+
): Promise<void> {
|
|
1472
|
+
const fs = await import('fs/promises');
|
|
1473
|
+
const path = await import('path');
|
|
1474
|
+
|
|
1475
|
+
// Create output directory
|
|
1476
|
+
await fs.mkdir(outputPath, { recursive: true });
|
|
1477
|
+
|
|
1478
|
+
// Write each file
|
|
1479
|
+
for (const file of files) {
|
|
1480
|
+
const filePath = path.join(outputPath, file.name);
|
|
1481
|
+
await fs.writeFile(filePath, file.content, 'utf-8');
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
/**
|
|
1486
|
+
* Run in non-interactive mode
|
|
1487
|
+
*/
|
|
1488
|
+
async function runNonInteractive(options: GenerateK8sOptions): Promise<void> {
|
|
1489
|
+
ui.header('nimbus generate k8s', 'Non-interactive mode');
|
|
1490
|
+
|
|
1491
|
+
// Validate required options
|
|
1492
|
+
if (!options.name) {
|
|
1493
|
+
ui.error('Name is required in non-interactive mode (--name)');
|
|
1494
|
+
process.exit(1);
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
if (!options.image) {
|
|
1498
|
+
ui.error('Image is required in non-interactive mode (--image)');
|
|
1499
|
+
process.exit(1);
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
ui.info(`Workload type: ${options.workloadType || 'deployment'}`);
|
|
1503
|
+
ui.info(`Name: ${options.name}`);
|
|
1504
|
+
ui.info(`Namespace: ${options.namespace || 'default'}`);
|
|
1505
|
+
ui.info(`Image: ${options.image}`);
|
|
1506
|
+
ui.info(`Output: ${options.output || `./${options.name}-k8s`}`);
|
|
1507
|
+
|
|
1508
|
+
// Build context from options
|
|
1509
|
+
const ctx: K8sWizardContext = {
|
|
1510
|
+
workloadType: options.workloadType || 'deployment',
|
|
1511
|
+
name: options.name,
|
|
1512
|
+
namespace: options.namespace || 'default',
|
|
1513
|
+
image: options.image.split(':')[0],
|
|
1514
|
+
imageTag: options.image.split(':')[1] || 'latest',
|
|
1515
|
+
replicas: options.replicas ?? 2,
|
|
1516
|
+
containerPort: options.port ?? 8080,
|
|
1517
|
+
serviceType: options.serviceType || 'ClusterIP',
|
|
1518
|
+
includeService: true,
|
|
1519
|
+
includeIngress: options.includeIngress ?? false,
|
|
1520
|
+
includeHpa: options.includeHpa ?? false,
|
|
1521
|
+
includePdb: options.includePdb ?? false,
|
|
1522
|
+
includeConfigMap: options.includeConfigMap ?? false,
|
|
1523
|
+
includeSecret: options.includeSecret ?? false,
|
|
1524
|
+
cpuRequest: options.cpuRequest,
|
|
1525
|
+
cpuLimit: options.cpuLimit,
|
|
1526
|
+
memoryRequest: options.memoryRequest,
|
|
1527
|
+
memoryLimit: options.memoryLimit,
|
|
1528
|
+
outputPath: options.output || `./${options.name}-k8s`,
|
|
1529
|
+
outputFormat: 'multiple',
|
|
1530
|
+
};
|
|
1531
|
+
|
|
1532
|
+
ui.newLine();
|
|
1533
|
+
ui.startSpinner({ message: 'Generating manifests...' });
|
|
1534
|
+
|
|
1535
|
+
try {
|
|
1536
|
+
const files = generateManifestsLocally(ctx);
|
|
1537
|
+
await writeFilesToDisk(files, ctx.outputPath!);
|
|
1538
|
+
|
|
1539
|
+
ui.stopSpinnerSuccess(`Generated ${files.length} manifest(s)`);
|
|
1540
|
+
|
|
1541
|
+
ui.newLine();
|
|
1542
|
+
ui.box({
|
|
1543
|
+
title: 'Complete!',
|
|
1544
|
+
content: [
|
|
1545
|
+
`Generated ${files.length} manifest(s) in ${ctx.outputPath}:`,
|
|
1546
|
+
...files.map(f => ` - ${f.name}`),
|
|
1547
|
+
],
|
|
1548
|
+
style: 'rounded',
|
|
1549
|
+
borderColor: 'green',
|
|
1550
|
+
padding: 1,
|
|
1551
|
+
});
|
|
1552
|
+
} catch (error: any) {
|
|
1553
|
+
ui.stopSpinnerFail('Generation failed');
|
|
1554
|
+
ui.error(error.message);
|
|
1555
|
+
process.exit(1);
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
// Export as default command
|
|
1560
|
+
export default generateK8sCommand;
|