@rulebricks/cli 1.9.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/README.md +62 -0
- package/dist/commands/clone.d.ts +6 -0
- package/dist/commands/clone.js +60 -0
- package/dist/commands/deploy.d.ts +8 -0
- package/dist/commands/deploy.js +409 -0
- package/dist/commands/destroy.d.ts +8 -0
- package/dist/commands/destroy.js +298 -0
- package/dist/commands/init.d.ts +7 -0
- package/dist/commands/init.js +201 -0
- package/dist/commands/logs.d.ts +9 -0
- package/dist/commands/logs.js +222 -0
- package/dist/commands/open.d.ts +7 -0
- package/dist/commands/open.js +139 -0
- package/dist/commands/status.d.ts +5 -0
- package/dist/commands/status.js +125 -0
- package/dist/commands/upgrade.d.ts +7 -0
- package/dist/commands/upgrade.js +239 -0
- package/dist/components/DNSWaitScreen.d.ts +9 -0
- package/dist/components/DNSWaitScreen.js +73 -0
- package/dist/components/Wizard/WizardContext.d.ts +176 -0
- package/dist/components/Wizard/WizardContext.js +346 -0
- package/dist/components/Wizard/index.d.ts +2 -0
- package/dist/components/Wizard/index.js +2 -0
- package/dist/components/Wizard/steps/CloudProviderStep.d.ts +6 -0
- package/dist/components/Wizard/steps/CloudProviderStep.js +210 -0
- package/dist/components/Wizard/steps/CredentialsStep.d.ts +6 -0
- package/dist/components/Wizard/steps/CredentialsStep.js +22 -0
- package/dist/components/Wizard/steps/DatabaseStep.d.ts +6 -0
- package/dist/components/Wizard/steps/DatabaseStep.js +80 -0
- package/dist/components/Wizard/steps/DeploymentModeStep.d.ts +5 -0
- package/dist/components/Wizard/steps/DeploymentModeStep.js +26 -0
- package/dist/components/Wizard/steps/DomainStep.d.ts +6 -0
- package/dist/components/Wizard/steps/DomainStep.js +126 -0
- package/dist/components/Wizard/steps/FeatureConfigStep.d.ts +6 -0
- package/dist/components/Wizard/steps/FeatureConfigStep.js +765 -0
- package/dist/components/Wizard/steps/FeaturesStep.d.ts +6 -0
- package/dist/components/Wizard/steps/FeaturesStep.js +119 -0
- package/dist/components/Wizard/steps/ReviewStep.d.ts +6 -0
- package/dist/components/Wizard/steps/ReviewStep.js +56 -0
- package/dist/components/Wizard/steps/SMTPStep.d.ts +6 -0
- package/dist/components/Wizard/steps/SMTPStep.js +191 -0
- package/dist/components/Wizard/steps/SupabaseCredentialsStep.d.ts +6 -0
- package/dist/components/Wizard/steps/SupabaseCredentialsStep.js +76 -0
- package/dist/components/Wizard/steps/TierStep.d.ts +6 -0
- package/dist/components/Wizard/steps/TierStep.js +29 -0
- package/dist/components/Wizard/steps/VersionStep.d.ts +6 -0
- package/dist/components/Wizard/steps/VersionStep.js +113 -0
- package/dist/components/Wizard/steps/index.d.ts +12 -0
- package/dist/components/Wizard/steps/index.js +12 -0
- package/dist/components/common/AppShell.d.ts +31 -0
- package/dist/components/common/AppShell.js +31 -0
- package/dist/components/common/Box.d.ts +20 -0
- package/dist/components/common/Box.js +20 -0
- package/dist/components/common/Logo.d.ts +7 -0
- package/dist/components/common/Logo.js +22 -0
- package/dist/components/common/Spinner.d.ts +12 -0
- package/dist/components/common/Spinner.js +28 -0
- package/dist/components/common/index.d.ts +6 -0
- package/dist/components/common/index.js +5 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +202 -0
- package/dist/lib/cloudCli.d.ts +156 -0
- package/dist/lib/cloudCli.js +691 -0
- package/dist/lib/config.d.ts +91 -0
- package/dist/lib/config.js +278 -0
- package/dist/lib/dns.d.ts +41 -0
- package/dist/lib/dns.js +235 -0
- package/dist/lib/dockerHub.d.ts +57 -0
- package/dist/lib/dockerHub.js +128 -0
- package/dist/lib/helm.d.ts +53 -0
- package/dist/lib/helm.js +209 -0
- package/dist/lib/helmValues.d.ts +17 -0
- package/dist/lib/helmValues.js +693 -0
- package/dist/lib/kubernetes.d.ts +161 -0
- package/dist/lib/kubernetes.js +755 -0
- package/dist/lib/terraform.d.ts +44 -0
- package/dist/lib/terraform.js +230 -0
- package/dist/lib/theme.d.ts +81 -0
- package/dist/lib/theme.js +115 -0
- package/dist/lib/validation.d.ts +47 -0
- package/dist/lib/validation.js +164 -0
- package/dist/lib/versions.d.ts +69 -0
- package/dist/lib/versions.js +139 -0
- package/dist/types/index.d.ts +718 -0
- package/dist/types/index.js +556 -0
- package/email-templates/email_change.html +325 -0
- package/email-templates/invite.html +383 -0
- package/email-templates/password_change.html +414 -0
- package/email-templates/verify.html +396 -0
- package/package.json +78 -0
- package/terraform/aws/main.tf +327 -0
- package/terraform/azure/main.tf +326 -0
- package/terraform/gcp/main.tf +369 -0
package/README.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+

|
|
2
|
+
|
|
3
|
+
The Rulebricks CLI is a management utility that automates the creation and maintenance of private Rulebricks clusters, helping you deploy Rulebricks in customizable, high-throughput configurations on AWS, GCP, or Azure.
|
|
4
|
+
|
|
5
|
+
You can choose how much you would like the CLI to automate for you– use it to generate valid configuration values, automate infrastructure provisioning (via Terraform), software deployment (via Helm), or all of the above.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
Try the quick install script (macOS/Linux):
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
curl -fsSL https://raw.githubusercontent.com/rulebricks/cli/main/install.sh | bash
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Standalone binaries are available on the [Releases page](https://github.com/rulebricks/cli/releases).
|
|
16
|
+
|
|
17
|
+
## Prerequisites
|
|
18
|
+
|
|
19
|
+
You must have a valid **Rulebricks license key**
|
|
20
|
+
to deploy using this CLI. You will be
|
|
21
|
+
requested for this key during project
|
|
22
|
+
configuration.
|
|
23
|
+
|
|
24
|
+
Rulebricks requires TLS. You will require either external-dns on your cluster to automatically add DNS records, or you will need **access** to manually add **DNS records** for the subdomain(s) where you would like to access your private deployment from.
|
|
25
|
+
|
|
26
|
+
Finally, you will need to have the following tools installed and ready on your machine:
|
|
27
|
+
|
|
28
|
+
- **Node.js** >= 20
|
|
29
|
+
- **kubectl** - Kubernetes CLI
|
|
30
|
+
- **Helm** >= 3.0
|
|
31
|
+
- **Terraform** >= 1.0 (for infrastructure provisioning)
|
|
32
|
+
- Cloud CLI (`aws`, `gcloud`, or `az`) configured for your provider
|
|
33
|
+
|
|
34
|
+
## Quick Start
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
# Configuration wizard (generates values.yaml)
|
|
38
|
+
rulebricks init
|
|
39
|
+
|
|
40
|
+
# Provision and/or deploy to your cluster
|
|
41
|
+
rulebricks deploy my-deployment
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Commands
|
|
45
|
+
|
|
46
|
+
| Command | Description |
|
|
47
|
+
| --------------------------- | -------------------------------------- |
|
|
48
|
+
| `rulebricks init` | Interactive setup wizard |
|
|
49
|
+
| `rulebricks deploy [name]` | Deploy to Kubernetes |
|
|
50
|
+
| `rulebricks upgrade [name]` | Upgrade to a new version |
|
|
51
|
+
| `rulebricks destroy [name]` | Remove a deployment |
|
|
52
|
+
| `rulebricks status [name]` | Show deployment health |
|
|
53
|
+
| `rulebricks logs [name]` | Inspect services |
|
|
54
|
+
| `rulebricks open [name]` | Open the generated configuration files |
|
|
55
|
+
|
|
56
|
+
Add `-h` to any command to learn more about its options.
|
|
57
|
+
|
|
58
|
+
## Notes
|
|
59
|
+
|
|
60
|
+
There are a uniquely wide variety of customization options this CLI makes available (multi-cloud, hybrid vs. self-hosted database deployment, custom email templates, etc.), and not all combinations have been validated.
|
|
61
|
+
|
|
62
|
+
If you encounter any issue deploying your private Rulebricks cluster, please [email us](mailto:support@rulebricks.com) or [open an issue](https://github.com/rulebricks/cli/issues) and we will follow up promptly.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useEffect } from "react";
|
|
3
|
+
import { Box, Text, useApp } from "ink";
|
|
4
|
+
import { BorderBox, Spinner, ThemeProvider, useTheme, Logo, } from "../components/common/index.js";
|
|
5
|
+
import { deploymentExists, cloneDeploymentConfig, getDeploymentDir, } from "../lib/config.js";
|
|
6
|
+
import { generateHelmValues } from "../lib/helmValues.js";
|
|
7
|
+
function CloneCommandInner({ source, target }) {
|
|
8
|
+
const { exit } = useApp();
|
|
9
|
+
const { colors } = useTheme();
|
|
10
|
+
const [step, setStep] = useState("validating");
|
|
11
|
+
const [error, setError] = useState(null);
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
(async () => {
|
|
14
|
+
try {
|
|
15
|
+
// Validate source exists
|
|
16
|
+
const sourceExists = await deploymentExists(source);
|
|
17
|
+
if (!sourceExists) {
|
|
18
|
+
setError(`Source deployment "${source}" not found`);
|
|
19
|
+
setStep("error");
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
// Validate target doesn't exist
|
|
23
|
+
const targetExists = await deploymentExists(target);
|
|
24
|
+
if (targetExists) {
|
|
25
|
+
setError(`Target deployment "${target}" already exists`);
|
|
26
|
+
setStep("error");
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
// Clone the configuration
|
|
30
|
+
setStep("cloning");
|
|
31
|
+
const clonedConfig = await cloneDeploymentConfig(source, target);
|
|
32
|
+
// Generate fresh Helm values from the cloned config
|
|
33
|
+
await generateHelmValues(clonedConfig);
|
|
34
|
+
setStep("complete");
|
|
35
|
+
setTimeout(() => exit(), 3000);
|
|
36
|
+
}
|
|
37
|
+
catch (err) {
|
|
38
|
+
setError(err instanceof Error ? err.message : "Failed to clone deployment");
|
|
39
|
+
setStep("error");
|
|
40
|
+
}
|
|
41
|
+
})();
|
|
42
|
+
}, [source, target, exit]);
|
|
43
|
+
// Validating screen
|
|
44
|
+
if (step === "validating") {
|
|
45
|
+
return (_jsx(BorderBox, { title: "Clone Deployment", children: _jsx(Box, { flexDirection: "column", marginY: 1, children: _jsx(Spinner, { label: "Validating deployments..." }) }) }));
|
|
46
|
+
}
|
|
47
|
+
// Error screen
|
|
48
|
+
if (step === "error") {
|
|
49
|
+
return (_jsx(BorderBox, { title: "Clone Failed", children: _jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { color: colors.error, bold: true, children: "\u2717 Error" }), _jsx(Text, { color: colors.error, children: error }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: colors.muted, dimColor: true, children: ["Use ", _jsx(Text, { color: colors.accent, children: "rulebricks list" }), " to see available deployments."] }) })] }) }));
|
|
50
|
+
}
|
|
51
|
+
// Cloning screen
|
|
52
|
+
if (step === "cloning") {
|
|
53
|
+
return (_jsx(BorderBox, { title: "Clone Deployment", children: _jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsxs(Text, { color: colors.muted, children: ["Source: ", _jsx(Text, { color: colors.accent, children: source })] }), _jsxs(Text, { color: colors.muted, children: ["Target: ", _jsx(Text, { color: colors.accent, children: target })] }), _jsx(Box, { marginTop: 1, children: _jsx(Spinner, { label: "Cloning configuration..." }) })] }) }));
|
|
54
|
+
}
|
|
55
|
+
// Complete screen
|
|
56
|
+
return (_jsx(BorderBox, { title: "Clone Complete", children: _jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsxs(Text, { color: colors.success, bold: true, children: ["\u2713 Successfully cloned \"", source, "\" to \"", target, "\""] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: colors.muted, children: "Created files:" }), _jsx(Text, { color: colors.muted, children: " \u2022 config.yaml" }), _jsx(Text, { color: colors.muted, children: " \u2022 values.yaml" })] }), _jsx(Box, { marginTop: 1, flexDirection: "column", children: _jsxs(Text, { color: colors.muted, dimColor: true, children: ["Location: ", getDeploymentDir(target)] }) }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: colors.muted, children: "Next steps:" }), _jsxs(Text, { color: colors.accent, children: [" rulebricks deploy ", target] })] })] }) }));
|
|
57
|
+
}
|
|
58
|
+
export function CloneCommand(props) {
|
|
59
|
+
return (_jsxs(ThemeProvider, { theme: "init", children: [_jsx(Logo, {}), _jsx(CloneCommandInner, { ...props })] }));
|
|
60
|
+
}
|
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useEffect, useCallback, useRef } from "react";
|
|
3
|
+
import { Box, Text, useApp, useInput } from "ink";
|
|
4
|
+
import { BorderBox, Spinner, StatusLine, ThemeProvider, useTheme, Logo, } from "../components/common/index.js";
|
|
5
|
+
import { DNSWaitScreen } from "../components/DNSWaitScreen.js";
|
|
6
|
+
import { loadDeploymentConfig, loadDeploymentState, saveDeploymentState, updateDeploymentStatus, } from "../lib/config.js";
|
|
7
|
+
import { setupTerraformWorkspace, terraformInit, terraformPlan, terraformApply, terraformDestroy, updateKubeconfig, hasTerraformState, isTerraformInstalled, } from "../lib/terraform.js";
|
|
8
|
+
import { installChart, upgradeChart, isHelmInstalled } from "../lib/helm.js";
|
|
9
|
+
import { isKubectlInstalled, checkClusterAccessible, } from "../lib/kubernetes.js";
|
|
10
|
+
import { generateHelmValues, updateHelmValuesForTLS, } from "../lib/helmValues.js";
|
|
11
|
+
import { isSupportedDnsProvider, getNamespace, getReleaseName, } from "../types/index.js";
|
|
12
|
+
function DeployCommandInner({ name, skipInfra, skipDns, version, }) {
|
|
13
|
+
const { exit } = useApp();
|
|
14
|
+
const { colors } = useTheme();
|
|
15
|
+
const [step, setStep] = useState("loading");
|
|
16
|
+
const [config, setConfig] = useState(null);
|
|
17
|
+
const [error, setError] = useState(null);
|
|
18
|
+
const [useExternalDns, setUseExternalDns] = useState(false);
|
|
19
|
+
const infraStartedRef = useRef(false); // Track if we started infra provisioning (ref for sync access)
|
|
20
|
+
const [cleanupError, setCleanupError] = useState(null);
|
|
21
|
+
const [status, setStatus] = useState({
|
|
22
|
+
preflight: "pending",
|
|
23
|
+
infrastructure: "pending",
|
|
24
|
+
kubeconfig: "pending",
|
|
25
|
+
helmInstall: "pending",
|
|
26
|
+
dnsConfig: "pending",
|
|
27
|
+
helmUpgradeTls: "pending",
|
|
28
|
+
});
|
|
29
|
+
// Handle cleanup prompt responses
|
|
30
|
+
const handleCleanup = useCallback(async () => {
|
|
31
|
+
setStep("cleanup-running");
|
|
32
|
+
try {
|
|
33
|
+
await terraformDestroy(name);
|
|
34
|
+
setStep("cleanup-complete");
|
|
35
|
+
setTimeout(() => exit(), 3000);
|
|
36
|
+
}
|
|
37
|
+
catch (err) {
|
|
38
|
+
setCleanupError(err instanceof Error ? err.message : "Cleanup failed");
|
|
39
|
+
setStep("cleanup-complete");
|
|
40
|
+
setTimeout(() => exit(), 5000);
|
|
41
|
+
}
|
|
42
|
+
}, [name, exit]);
|
|
43
|
+
const skipCleanup = useCallback(() => {
|
|
44
|
+
setStep("error");
|
|
45
|
+
}, []);
|
|
46
|
+
useInput((input, key) => {
|
|
47
|
+
if (step === "cleanup-prompt") {
|
|
48
|
+
if (input === "y" || input === "Y") {
|
|
49
|
+
handleCleanup();
|
|
50
|
+
}
|
|
51
|
+
else if (input === "n" || input === "N" || key.escape) {
|
|
52
|
+
skipCleanup();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
else if (key.escape &&
|
|
56
|
+
(step === "error" || step === "cleanup-complete")) {
|
|
57
|
+
exit();
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
// Resume after DNS wait (manual DNS flow)
|
|
61
|
+
const handleDnsComplete = useCallback(async () => {
|
|
62
|
+
if (!config)
|
|
63
|
+
return;
|
|
64
|
+
try {
|
|
65
|
+
setStep("helm-upgrade-tls");
|
|
66
|
+
setStatus((s) => ({
|
|
67
|
+
...s,
|
|
68
|
+
dnsConfig: "success",
|
|
69
|
+
helmUpgradeTls: "running",
|
|
70
|
+
}));
|
|
71
|
+
// Update helm values to enable TLS
|
|
72
|
+
await updateHelmValuesForTLS(name, true);
|
|
73
|
+
const namespace = getNamespace(config.name);
|
|
74
|
+
const releaseName = getReleaseName(config.name);
|
|
75
|
+
// Upgrade the chart with TLS enabled
|
|
76
|
+
await upgradeChart(name, { releaseName, namespace, version, wait: true });
|
|
77
|
+
setStatus((s) => ({ ...s, helmUpgradeTls: "success" }));
|
|
78
|
+
// Update state
|
|
79
|
+
await updateDeploymentStatus(name, "running", {
|
|
80
|
+
application: {
|
|
81
|
+
appVersion: config.appVersion || "latest",
|
|
82
|
+
hpsVersion: config.hpsVersion || config.appVersion || "latest",
|
|
83
|
+
chartVersion: version || "latest",
|
|
84
|
+
namespace,
|
|
85
|
+
url: `https://${config.domain}`,
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
setStep("complete");
|
|
89
|
+
setTimeout(() => exit(), 5000);
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
const message = err instanceof Error ? err.message : "TLS upgrade failed";
|
|
93
|
+
setError(message);
|
|
94
|
+
setStep("error");
|
|
95
|
+
setStatus((s) => ({ ...s, helmUpgradeTls: "error" }));
|
|
96
|
+
await updateDeploymentStatus(name, "failed");
|
|
97
|
+
}
|
|
98
|
+
}, [config, name, version, exit]);
|
|
99
|
+
// Skip DNS validation (manual DNS flow)
|
|
100
|
+
const handleDnsSkip = useCallback(async () => {
|
|
101
|
+
if (!config)
|
|
102
|
+
return;
|
|
103
|
+
setStatus((s) => ({
|
|
104
|
+
...s,
|
|
105
|
+
dnsConfig: "skipped",
|
|
106
|
+
helmUpgradeTls: "skipped",
|
|
107
|
+
}));
|
|
108
|
+
const namespace = getNamespace(config.name);
|
|
109
|
+
// Mark as running without TLS upgrade
|
|
110
|
+
await updateDeploymentStatus(name, "running", {
|
|
111
|
+
application: {
|
|
112
|
+
appVersion: config.appVersion || "latest",
|
|
113
|
+
hpsVersion: config.hpsVersion || config.appVersion || "latest",
|
|
114
|
+
chartVersion: version || "latest",
|
|
115
|
+
namespace,
|
|
116
|
+
url: `https://${config.domain}`,
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
setStep("complete");
|
|
120
|
+
setTimeout(() => exit(), 5000);
|
|
121
|
+
}, [config, name, version, exit]);
|
|
122
|
+
useEffect(() => {
|
|
123
|
+
runDeployment();
|
|
124
|
+
}, []);
|
|
125
|
+
async function runDeployment() {
|
|
126
|
+
try {
|
|
127
|
+
// Load configuration
|
|
128
|
+
const cfg = await loadDeploymentConfig(name);
|
|
129
|
+
setConfig(cfg);
|
|
130
|
+
// Determine if External DNS is enabled
|
|
131
|
+
// External DNS = supported provider + auto-manage enabled
|
|
132
|
+
const externalDnsEnabled = cfg.dns.autoManage && isSupportedDnsProvider(cfg.dns.provider);
|
|
133
|
+
setUseExternalDns(externalDnsEnabled);
|
|
134
|
+
// Initialize deployment state
|
|
135
|
+
const existingState = await loadDeploymentState(name);
|
|
136
|
+
const state = existingState || {
|
|
137
|
+
name,
|
|
138
|
+
version: version || "latest",
|
|
139
|
+
createdAt: new Date().toISOString(),
|
|
140
|
+
updatedAt: new Date().toISOString(),
|
|
141
|
+
status: "deploying",
|
|
142
|
+
};
|
|
143
|
+
await saveDeploymentState(name, { ...state, status: "deploying" });
|
|
144
|
+
// Preflight checks
|
|
145
|
+
setStep("preflight");
|
|
146
|
+
setStatus((s) => ({ ...s, preflight: "running" }));
|
|
147
|
+
await runPreflightChecks(cfg);
|
|
148
|
+
setStatus((s) => ({ ...s, preflight: "success" }));
|
|
149
|
+
// Infrastructure provisioning
|
|
150
|
+
const needsInfra = cfg.infrastructure.mode === "provision" && !skipInfra;
|
|
151
|
+
if (needsInfra) {
|
|
152
|
+
setStatus((s) => ({ ...s, infrastructure: "running" }));
|
|
153
|
+
infraStartedRef.current = true; // Mark that we're doing infrastructure work
|
|
154
|
+
// Check if already provisioned
|
|
155
|
+
const hasState = await hasTerraformState(name);
|
|
156
|
+
if (!hasState) {
|
|
157
|
+
setStep("infra-setup");
|
|
158
|
+
await setupTerraformWorkspace(name, cfg.infrastructure.provider);
|
|
159
|
+
}
|
|
160
|
+
setStep("infra-init");
|
|
161
|
+
await terraformInit(name);
|
|
162
|
+
setStep("infra-plan");
|
|
163
|
+
await terraformPlan(name);
|
|
164
|
+
setStep("infra-apply");
|
|
165
|
+
await terraformApply(name);
|
|
166
|
+
setStatus((s) => ({ ...s, infrastructure: "success" }));
|
|
167
|
+
// Update kubeconfig
|
|
168
|
+
setStep("kubeconfig");
|
|
169
|
+
setStatus((s) => ({ ...s, kubeconfig: "running" }));
|
|
170
|
+
await updateKubeconfig(cfg.infrastructure.provider, cfg.infrastructure.clusterName || `${name}-cluster`, cfg.infrastructure.region, {
|
|
171
|
+
gcpProjectId: cfg.infrastructure.gcpProjectId,
|
|
172
|
+
azureResourceGroup: cfg.infrastructure.azureResourceGroup,
|
|
173
|
+
});
|
|
174
|
+
// Note: StorageClass is managed by the Helm chart, not the CLI
|
|
175
|
+
// This avoids conflicts where kubectl-created resources lack Helm ownership labels
|
|
176
|
+
setStatus((s) => ({ ...s, kubeconfig: "success" }));
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
// For existing infrastructure, infrastructure is always skipped
|
|
180
|
+
// kubeconfig may have been updated during preflight if cluster wasn't accessible
|
|
181
|
+
// (in that case, it's already set to 'success'), otherwise mark as skipped
|
|
182
|
+
setStatus((s) => ({
|
|
183
|
+
...s,
|
|
184
|
+
infrastructure: "skipped",
|
|
185
|
+
kubeconfig: s.kubeconfig === "success" ? "success" : "skipped",
|
|
186
|
+
}));
|
|
187
|
+
}
|
|
188
|
+
// Helm Chart Installation
|
|
189
|
+
setStep("helm-install");
|
|
190
|
+
setStatus((s) => ({ ...s, helmInstall: "running" }));
|
|
191
|
+
const namespace = getNamespace(cfg.name);
|
|
192
|
+
const releaseName = getReleaseName(cfg.name);
|
|
193
|
+
if (externalDnsEnabled) {
|
|
194
|
+
// SINGLE-PHASE DEPLOYMENT (External DNS)
|
|
195
|
+
// Install with TLS enabled from the start - external-dns handles DNS records
|
|
196
|
+
await generateHelmValues(cfg, { tlsEnabled: true });
|
|
197
|
+
await installChart(name, {
|
|
198
|
+
releaseName,
|
|
199
|
+
namespace,
|
|
200
|
+
version,
|
|
201
|
+
wait: true,
|
|
202
|
+
});
|
|
203
|
+
setStatus((s) => ({
|
|
204
|
+
...s,
|
|
205
|
+
helmInstall: "success",
|
|
206
|
+
dnsConfig: "skipped", // External DNS handles this
|
|
207
|
+
helmUpgradeTls: "skipped", // TLS enabled from start
|
|
208
|
+
}));
|
|
209
|
+
// Update state to running
|
|
210
|
+
await updateDeploymentStatus(name, "running", {
|
|
211
|
+
application: {
|
|
212
|
+
appVersion: cfg.appVersion || "latest",
|
|
213
|
+
hpsVersion: cfg.hpsVersion || cfg.appVersion || "latest",
|
|
214
|
+
chartVersion: version || "latest",
|
|
215
|
+
namespace,
|
|
216
|
+
url: `https://${cfg.domain}`,
|
|
217
|
+
},
|
|
218
|
+
});
|
|
219
|
+
setStep("complete");
|
|
220
|
+
setTimeout(() => exit(), 5000);
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
// TWO-PHASE DEPLOYMENT (Manual DNS)
|
|
224
|
+
// Phase 1: Install without TLS
|
|
225
|
+
await generateHelmValues(cfg, { tlsEnabled: false });
|
|
226
|
+
await installChart(name, {
|
|
227
|
+
releaseName,
|
|
228
|
+
namespace,
|
|
229
|
+
version,
|
|
230
|
+
wait: true,
|
|
231
|
+
});
|
|
232
|
+
setStatus((s) => ({ ...s, helmInstall: "success" }));
|
|
233
|
+
// If skipping DNS, go straight to complete
|
|
234
|
+
if (skipDns) {
|
|
235
|
+
setStatus((s) => ({
|
|
236
|
+
...s,
|
|
237
|
+
dnsConfig: "skipped",
|
|
238
|
+
helmUpgradeTls: "skipped",
|
|
239
|
+
}));
|
|
240
|
+
await updateDeploymentStatus(name, "waiting-dns", {
|
|
241
|
+
application: {
|
|
242
|
+
appVersion: cfg.appVersion || "latest",
|
|
243
|
+
hpsVersion: cfg.hpsVersion || cfg.appVersion || "latest",
|
|
244
|
+
chartVersion: version || "latest",
|
|
245
|
+
namespace,
|
|
246
|
+
url: `https://${cfg.domain}`,
|
|
247
|
+
},
|
|
248
|
+
});
|
|
249
|
+
setStep("complete");
|
|
250
|
+
setTimeout(() => exit(), 5000);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
// Update state to waiting for DNS
|
|
254
|
+
await updateDeploymentStatus(name, "waiting-dns");
|
|
255
|
+
// Phase 2: DNS configuration wait
|
|
256
|
+
setStep("dns-wait");
|
|
257
|
+
setStatus((s) => ({ ...s, dnsConfig: "running" }));
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
catch (err) {
|
|
261
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
262
|
+
setError(message);
|
|
263
|
+
await updateDeploymentStatus(name, "failed");
|
|
264
|
+
// If we started infrastructure provisioning but failed, offer cleanup
|
|
265
|
+
if (infraStartedRef.current) {
|
|
266
|
+
setStep("cleanup-prompt");
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
setStep("error");
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
async function runPreflightChecks(cfg) {
|
|
274
|
+
// Check required tools
|
|
275
|
+
const [helm, kubectl, terraform] = await Promise.all([
|
|
276
|
+
isHelmInstalled(),
|
|
277
|
+
isKubectlInstalled(),
|
|
278
|
+
isTerraformInstalled(),
|
|
279
|
+
]);
|
|
280
|
+
if (!helm) {
|
|
281
|
+
throw new Error("Helm is not installed. Please install Helm first.");
|
|
282
|
+
}
|
|
283
|
+
if (!kubectl) {
|
|
284
|
+
throw new Error("kubectl is not installed. Please install kubectl first.");
|
|
285
|
+
}
|
|
286
|
+
if (cfg.infrastructure.mode === "provision" && !terraform) {
|
|
287
|
+
throw new Error("Terraform is not installed. Required for infrastructure provisioning.");
|
|
288
|
+
}
|
|
289
|
+
// Check cluster access if using existing infrastructure
|
|
290
|
+
if (cfg.infrastructure.mode === "existing") {
|
|
291
|
+
let clusterError = await checkClusterAccessible();
|
|
292
|
+
// If cluster not accessible but we have provider details, try updating kubeconfig
|
|
293
|
+
if (clusterError &&
|
|
294
|
+
cfg.infrastructure.provider &&
|
|
295
|
+
cfg.infrastructure.region &&
|
|
296
|
+
cfg.infrastructure.clusterName) {
|
|
297
|
+
try {
|
|
298
|
+
// Show visual feedback for kubeconfig update
|
|
299
|
+
setStep("kubeconfig");
|
|
300
|
+
setStatus((s) => ({
|
|
301
|
+
...s,
|
|
302
|
+
preflight: "success",
|
|
303
|
+
kubeconfig: "running",
|
|
304
|
+
}));
|
|
305
|
+
await updateKubeconfig(cfg.infrastructure.provider, cfg.infrastructure.clusterName, cfg.infrastructure.region, {
|
|
306
|
+
gcpProjectId: cfg.infrastructure.gcpProjectId,
|
|
307
|
+
azureResourceGroup: cfg.infrastructure.azureResourceGroup,
|
|
308
|
+
});
|
|
309
|
+
// Retry cluster access check
|
|
310
|
+
clusterError = await checkClusterAccessible();
|
|
311
|
+
if (!clusterError) {
|
|
312
|
+
setStatus((s) => ({ ...s, kubeconfig: "success" }));
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
catch (kubeconfigError) {
|
|
316
|
+
// Kubeconfig update failed, include both errors
|
|
317
|
+
const kubeconfigMsg = kubeconfigError instanceof Error
|
|
318
|
+
? kubeconfigError.message
|
|
319
|
+
: "Unknown error";
|
|
320
|
+
throw new Error(`Cannot access Kubernetes cluster and kubeconfig update failed:\n` +
|
|
321
|
+
`Cluster error: ${clusterError}\n` +
|
|
322
|
+
`Kubeconfig update error: ${kubeconfigMsg}`);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
if (clusterError) {
|
|
326
|
+
// Provide helpful message based on whether provider details are missing
|
|
327
|
+
if (!cfg.infrastructure.provider ||
|
|
328
|
+
!cfg.infrastructure.region ||
|
|
329
|
+
!cfg.infrastructure.clusterName) {
|
|
330
|
+
throw new Error(`Cannot access Kubernetes cluster:\n${clusterError}\n\n` +
|
|
331
|
+
`Tip: Re-run 'rulebricks init' and provide your cloud provider, region, and cluster name ` +
|
|
332
|
+
`to enable automatic kubeconfig updates.`);
|
|
333
|
+
}
|
|
334
|
+
throw new Error(`Cannot access Kubernetes cluster:\n${clusterError}`);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
// Cleanup prompt screen
|
|
339
|
+
if (step === "cleanup-prompt") {
|
|
340
|
+
return (_jsx(BorderBox, { title: "Deployment Failed", children: _jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { color: colors.error, bold: true, children: "\u2717 Infrastructure provisioning failed" }), _jsx(Text, { color: colors.error, children: error }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: colors.warning, bold: true, children: "Partial infrastructure may have been created." }), _jsx(Text, { color: colors.muted, children: "Would you like to clean up to avoid orphaned resources?" })] }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: colors.accent, bold: true, children: "[Y]" }), _jsxs(Text, { color: colors.muted, children: [" ", "Yes, destroy partial infrastructure"] })] }), _jsxs(Box, { children: [_jsx(Text, { color: colors.accent, bold: true, children: "[N]" }), _jsxs(Text, { color: colors.muted, children: [" ", "No, keep for debugging (you can run `rulebricks destroy --cluster` later)"] })] })] }) }));
|
|
341
|
+
}
|
|
342
|
+
// Cleanup running screen
|
|
343
|
+
if (step === "cleanup-running") {
|
|
344
|
+
return (_jsx(BorderBox, { title: "Cleaning Up", children: _jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Spinner, { label: "Destroying partial infrastructure..." }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.muted, children: "This may take several minutes..." }) })] }) }));
|
|
345
|
+
}
|
|
346
|
+
// Cleanup complete screen
|
|
347
|
+
if (step === "cleanup-complete") {
|
|
348
|
+
return (_jsx(BorderBox, { title: "Cleanup Complete", children: _jsxs(Box, { flexDirection: "column", marginY: 1, children: [cleanupError ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: colors.warning, bold: true, children: "\u26A0 Cleanup encountered issues" }), _jsx(Text, { color: colors.warning, children: cleanupError }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: colors.muted, children: ["Some resources may remain. Run `rulebricks destroy ", name, " ", "--cluster` to retry."] }) })] })) : (_jsxs(_Fragment, { children: [_jsx(Text, { color: colors.success, bold: true, children: "\u2713 Infrastructure cleaned up successfully" }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.muted, children: "All partial resources have been destroyed. You can try deploying again." }) })] })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.muted, dimColor: true, children: "Press Esc to exit" }) })] }) }));
|
|
349
|
+
}
|
|
350
|
+
// Error screen (non-infra failures or when user skips cleanup)
|
|
351
|
+
if (step === "error") {
|
|
352
|
+
// Format error message, preserving newlines for multi-line errors
|
|
353
|
+
const errorLines = error?.split("\n") || ["Unknown error"];
|
|
354
|
+
return (_jsx(BorderBox, { title: "Deployment Failed", children: _jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { color: colors.error, bold: true, children: "\u2717 Error" }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: errorLines.map((line, i) => (_jsx(Text, { color: line.startsWith(" •") ? colors.muted : colors.error, children: line }, i))) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.muted, dimColor: true, children: "Press Esc to exit" }) })] }) }));
|
|
355
|
+
}
|
|
356
|
+
// DNS wait screen (only for manual DNS flow)
|
|
357
|
+
if (step === "dns-wait" && config) {
|
|
358
|
+
return (_jsx(DNSWaitScreen, { domain: config.domain, selfHostedSupabase: config.database.type === "self-hosted", namespace: getNamespace(config.name), onComplete: handleDnsComplete, onSkip: handleDnsSkip }));
|
|
359
|
+
}
|
|
360
|
+
// Complete screen
|
|
361
|
+
if (step === "complete") {
|
|
362
|
+
const tlsSkipped = status.helmUpgradeTls === "skipped" && !useExternalDns;
|
|
363
|
+
return (_jsx(BorderBox, { title: "Deployment Complete", children: _jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { color: colors.success, bold: true, children: "\u2713 Rulebricks deployed successfully!" }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { children: ["URL:", " ", _jsxs(Text, { color: colors.accent, children: ["https://", config?.domain, "/auth/signup"] })] }), useExternalDns && (_jsx(Text, { color: colors.muted, children: "DNS records will be created automatically by external-dns" })), tlsSkipped && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: colors.warning, children: ["\u26A0 TLS not configured. Run `rulebricks deploy ", name, "` again after DNS setup."] }) }))] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Next steps:" }), _jsxs(Text, { color: colors.muted, children: [" ", "\u2022 Visit the URL to complete initial setup"] }), _jsxs(Text, { color: colors.muted, children: [" ", "\u2022 Run `rulebricks status ", name, "` to check deployment health"] }), tlsSkipped && (_jsxs(Text, { color: colors.muted, children: [" ", "\u2022 Configure DNS and re-run deploy for TLS"] }))] })] }) }));
|
|
364
|
+
}
|
|
365
|
+
// Progress screen
|
|
366
|
+
const helmInstallLabel = useExternalDns
|
|
367
|
+
? "Helm chart installation (with TLS)"
|
|
368
|
+
: "Helm chart installation";
|
|
369
|
+
return (_jsx(BorderBox, { title: `Deploying ${name}`, children: _jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(StatusLine, { status: status.preflight, label: "Preflight checks" }), _jsx(StatusLine, { status: status.infrastructure, label: "Infrastructure provisioning", detail: step === "infra-setup"
|
|
370
|
+
? "Setting up workspace"
|
|
371
|
+
: step === "infra-init"
|
|
372
|
+
? "Initializing Terraform"
|
|
373
|
+
: step === "infra-plan"
|
|
374
|
+
? "Planning changes"
|
|
375
|
+
: step === "infra-apply"
|
|
376
|
+
? "Applying infrastructure"
|
|
377
|
+
: undefined }), _jsx(StatusLine, { status: status.kubeconfig, label: "Kubernetes configuration" }), _jsx(StatusLine, { status: status.helmInstall, label: helmInstallLabel }), !useExternalDns && (_jsxs(_Fragment, { children: [_jsx(StatusLine, { status: status.dnsConfig, label: "DNS configuration" }), _jsx(StatusLine, { status: status.helmUpgradeTls, label: "TLS configuration" })] })), step !== "dns-wait" && (_jsx(Box, { marginTop: 1, children: _jsx(Spinner, { label: getStepLabel(step, useExternalDns) }) }))] }) }));
|
|
378
|
+
}
|
|
379
|
+
function getStepLabel(step, useExternalDns) {
|
|
380
|
+
switch (step) {
|
|
381
|
+
case "loading":
|
|
382
|
+
return "Loading configuration...";
|
|
383
|
+
case "preflight":
|
|
384
|
+
return "Running preflight checks...";
|
|
385
|
+
case "infra-setup":
|
|
386
|
+
return "Setting up Terraform workspace...";
|
|
387
|
+
case "infra-init":
|
|
388
|
+
return "Initializing Terraform...";
|
|
389
|
+
case "infra-plan":
|
|
390
|
+
return "Planning infrastructure changes...";
|
|
391
|
+
case "infra-apply":
|
|
392
|
+
return "Creating infrastructure (may take up to 15 minutes)...";
|
|
393
|
+
case "kubeconfig":
|
|
394
|
+
return "Updating kubeconfig...";
|
|
395
|
+
case "helm-install":
|
|
396
|
+
return useExternalDns
|
|
397
|
+
? "Installing Helm chart with TLS..."
|
|
398
|
+
: "Installing Helm chart...";
|
|
399
|
+
case "dns-wait":
|
|
400
|
+
return "Waiting for DNS configuration...";
|
|
401
|
+
case "helm-upgrade-tls":
|
|
402
|
+
return "Enabling TLS certificates...";
|
|
403
|
+
default:
|
|
404
|
+
return "Processing...";
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
export function DeployCommand(props) {
|
|
408
|
+
return (_jsxs(ThemeProvider, { theme: "deploy", children: [_jsx(Logo, {}), _jsx(DeployCommandInner, { ...props })] }));
|
|
409
|
+
}
|