gitops-ai 1.1.0 → 1.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/README.md +58 -27
- package/dist/commands/bootstrap.js +184 -64
- package/dist/commands/bootstrap.js.map +1 -1
- package/dist/commands/template-sync-wizard.d.ts +1 -0
- package/dist/commands/template-sync-wizard.js +169 -0
- package/dist/commands/template-sync-wizard.js.map +1 -0
- package/dist/commands/template-sync.d.ts +8 -0
- package/dist/commands/template-sync.js +41 -0
- package/dist/commands/template-sync.js.map +1 -0
- package/dist/core/bootstrap-runner.js +1 -1
- package/dist/core/bootstrap-runner.js.map +1 -1
- package/dist/core/cloudflare-oauth.js +6 -2
- package/dist/core/cloudflare-oauth.js.map +1 -1
- package/dist/core/encryption.js +1 -1
- package/dist/core/encryption.js.map +1 -1
- package/dist/core/github-oauth.js +4 -2
- package/dist/core/github-oauth.js.map +1 -1
- package/dist/core/gitlab-oauth.js +6 -2
- package/dist/core/gitlab-oauth.js.map +1 -1
- package/dist/core/template-sync.d.ts +46 -0
- package/dist/core/template-sync.js +249 -0
- package/dist/core/template-sync.js.map +1 -0
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/schemas.d.ts +2 -0
- package/dist/schemas.js.map +1 -1
- package/package.json +32 -2
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# GitOps AI - Bootstrapper
|
|
2
2
|
|
|
3
|
+
[](https://gitops-ai.vercel.app) [](https://gitops-ai.vercel.app/#docs/prerequisites)
|
|
4
|
+
|
|
3
5
|
GitOps-managed Kubernetes infrastructure for AI-powered applications powered by the [Flux Operator](https://fluxoperator.dev/) and [Flux CD](https://fluxcd.io/). A single bootstrap application provisions a Kubernetes cluster, installs all infrastructure components, and enables continuous delivery from Git.
|
|
4
6
|
|
|
5
7
|
## Why GitOps for your infrastructure
|
|
@@ -14,7 +16,8 @@ GitOps-managed Kubernetes infrastructure for AI-powered applications powered by
|
|
|
14
16
|
|
|
15
17
|
## Quick Start
|
|
16
18
|
|
|
17
|
-
|
|
19
|
+
On your Mac or Linux machine:
|
|
20
|
+
|
|
18
21
|
```bash
|
|
19
22
|
npx gitops-ai bootstrap
|
|
20
23
|
```
|
|
@@ -31,17 +34,17 @@ Or, if you already have Node.js >= 18:
|
|
|
31
34
|
npx gitops-ai bootstrap
|
|
32
35
|
```
|
|
33
36
|
|
|
34
|
-
The interactive wizard will prompt for your GitLab
|
|
37
|
+
The interactive wizard will prompt for your Git provider (GitHub or GitLab), create or use a repository from the [GitOps AI Template](https://gitlab.com/everythings-gonna-be-alright/gitops_ai_template), and run the full bootstrap.
|
|
35
38
|
|
|
36
39
|
## Requirements
|
|
37
40
|
|
|
38
|
-
| Resource
|
|
39
|
-
|
|
40
|
-
| **CPU**
|
|
41
|
-
| **Memory**
|
|
42
|
-
| **Disk**
|
|
43
|
-
| **OS**
|
|
44
|
-
| **Node.js**
|
|
41
|
+
| Resource | Minimum |
|
|
42
|
+
|-------------|-----------------------------------------------|
|
|
43
|
+
| **CPU** | 2+ cores |
|
|
44
|
+
| **Memory** | 4+ GB |
|
|
45
|
+
| **Disk** | 20+ GB free |
|
|
46
|
+
| **OS** | Ubuntu 25.04+ or macOS |
|
|
47
|
+
| **Node.js** | 18+ (installed automatically by `install.sh`) |
|
|
45
48
|
|
|
46
49
|
You will also need a [GitLab PAT](docs/prerequisites.md#1-gitlab-personal-access-token), a [Cloudflare API Token](docs/prerequisites.md#2-cloudflare-api-token) (if using automatic DNS/TLS), and an [OpenAI API Key](docs/prerequisites.md#3-openai-api-key) (if using OpenClaw). See [Prerequisites](docs/prerequisites.md) for full details.
|
|
47
50
|
|
|
@@ -67,9 +70,21 @@ Keeping the template in a separate repository means:
|
|
|
67
70
|
- **Clean separation** -- the bootstrapper CLI handles provisioning logic; the template holds pure infrastructure declarations. Each can be versioned and tested independently.
|
|
68
71
|
- **Customisation without lock-in** -- after the fork you own the repo. Add namespaces, swap Helm charts, or restructure directories to fit your needs.
|
|
69
72
|
|
|
73
|
+
### Repository layout (template → your repo)
|
|
74
|
+
|
|
75
|
+
The upstream template (and your bootstrapped repo) is organised roughly as:
|
|
76
|
+
|
|
77
|
+
| Path | Role |
|
|
78
|
+
|--------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|
79
|
+
| `templates/<category>/…` | Shared Helm bases and component manifests (e.g. `templates/system/`, `templates/ai/`, `templates/monitoring/`). |
|
|
80
|
+
| `clusters/_template/` | Prototype cluster layout; the CLI copies this to `clusters/<your-cluster-name>/` during bootstrap. |
|
|
81
|
+
| `clusters/<name>/` | Your live cluster overlay (`cluster-sync.yaml`, `components/`, encrypted secrets). |
|
|
82
|
+
|
|
83
|
+
See [Architecture](docs/architecture.md) for diagrams and a fuller tree.
|
|
84
|
+
|
|
70
85
|
## CLI Commands
|
|
71
86
|
|
|
72
|
-
The CLI provides
|
|
87
|
+
The CLI provides these commands:
|
|
73
88
|
|
|
74
89
|
### `bootstrap` (alias: `install`)
|
|
75
90
|
|
|
@@ -89,16 +104,16 @@ SOPS secret encryption management. Run without arguments for an interactive menu
|
|
|
89
104
|
npx gitops-ai sops [subcommand] [file]
|
|
90
105
|
```
|
|
91
106
|
|
|
92
|
-
| Subcommand | Description
|
|
93
|
-
|
|
107
|
+
| Subcommand | Description |
|
|
108
|
+
|------------------|------------------------------------------------------------------------|
|
|
94
109
|
| `init` | First-time setup: generate age key, create `.sops.yaml` and K8s secret |
|
|
95
|
-
| `encrypt` | Encrypt all unencrypted secret files
|
|
96
|
-
| `encrypt <file>` | Encrypt a specific file
|
|
97
|
-
| `decrypt <file>` | Decrypt a file for viewing (re-encrypt before commit)
|
|
98
|
-
| `edit <file>` | Open encrypted file in `$EDITOR` (auto re-encrypts on save)
|
|
99
|
-
| `status` | Show encryption status of all secret files
|
|
100
|
-
| `import` | Import an existing age key into a new cluster
|
|
101
|
-
| `rotate` | Rotate to a new age key and re-encrypt everything
|
|
110
|
+
| `encrypt` | Encrypt all unencrypted secret files |
|
|
111
|
+
| `encrypt <file>` | Encrypt a specific file |
|
|
112
|
+
| `decrypt <file>` | Decrypt a file for viewing (re-encrypt before commit) |
|
|
113
|
+
| `edit <file>` | Open encrypted file in `$EDITOR` (auto re-encrypts on save) |
|
|
114
|
+
| `status` | Show encryption status of all secret files |
|
|
115
|
+
| `import` | Import an existing age key into a new cluster |
|
|
116
|
+
| `rotate` | Rotate to a new age key and re-encrypt everything |
|
|
102
117
|
|
|
103
118
|
### `openclaw-pair`
|
|
104
119
|
|
|
@@ -108,6 +123,18 @@ Pair an OpenClaw device with the cluster after bootstrap:
|
|
|
108
123
|
npx gitops-ai openclaw-pair
|
|
109
124
|
```
|
|
110
125
|
|
|
126
|
+
### `template sync`
|
|
127
|
+
|
|
128
|
+
Fetch the upstream GitOps template and merge changes into your current branch. Run without flags for an **interactive wizard** (tag picker, diff preview with risk classification, merge/dry-run/cancel), or pass flags for non-interactive use:
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
npx gitops-ai template sync # interactive wizard
|
|
132
|
+
npx gitops-ai template sync --ref v1.0.0 # non-interactive merge
|
|
133
|
+
npx gitops-ai template sync --ref main --dry-run # non-interactive preview
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
See [Template synchronization](docs/template-sync.md).
|
|
137
|
+
|
|
111
138
|
## Components
|
|
112
139
|
|
|
113
140
|
The bootstrap wizard lets you select which components to install:
|
|
@@ -128,23 +155,27 @@ Components marked **DNS/TLS** are automatically enabled when you opt into automa
|
|
|
128
155
|
|
|
129
156
|
## Documentation
|
|
130
157
|
|
|
131
|
-
| Document
|
|
132
|
-
|
|
133
|
-
| [Prerequisites](docs/prerequisites.md)
|
|
134
|
-
| [Bootstrap](docs/bootstrap.md)
|
|
135
|
-
| [Architecture](docs/architecture.md)
|
|
136
|
-
| [Configuration](docs/configuration.md)
|
|
158
|
+
| Document | Description |
|
|
159
|
+
|---------------------------------------------------|---------------------------------------------------------------------|
|
|
160
|
+
| [Prerequisites](docs/prerequisites.md) | Node.js, Docker (macOS), Git provider, optional Cloudflare / OpenAI |
|
|
161
|
+
| [Bootstrap](docs/bootstrap.md) | What the bootstrap does, wizard walkthrough, resume capability |
|
|
162
|
+
| [Architecture](docs/architecture.md) | Repositories, bootstrap flow, Flux Operator & Instance, repo tree |
|
|
163
|
+
| [Configuration](docs/configuration.md) | Cluster variables, SOPS defaults, post-bootstrap changes |
|
|
164
|
+
| [Template synchronization](docs/template-sync.md) | Upstream merges, `template sync`, CI parity, risk tiers |
|
|
165
|
+
| [Scaling](docs/scaling.md) | Adding k3s worker and server nodes (Linux) |
|
|
166
|
+
| [Security](docs/security.md) | SOPS, Git auth, hardening, network |
|
|
137
167
|
|
|
138
168
|
## Development
|
|
139
169
|
|
|
140
170
|
```bash
|
|
141
|
-
git clone
|
|
171
|
+
git clone https://gitlab.com/everythings-gonna-be-alright/gitops_ai_bootstrapper.git
|
|
172
|
+
cd gitops_ai_bootstrapper
|
|
142
173
|
npm install
|
|
143
174
|
|
|
144
175
|
npm run dev # Run CLI locally via tsx
|
|
145
176
|
npm run build # Compile TypeScript to dist/
|
|
146
177
|
npm run typecheck # Type-check without emitting
|
|
147
|
-
npm run test:
|
|
178
|
+
npm run test:sync # Unit tests for template sync logic
|
|
148
179
|
npm run test:integration # Full k3d + Flux integration test (requires Docker)
|
|
149
180
|
```
|
|
150
181
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { execSync } from "node:child_process";
|
|
3
|
+
import { networkInterfaces } from "node:os";
|
|
3
4
|
import { resolve } from "node:path";
|
|
4
5
|
import * as p from "@clack/prompts";
|
|
5
6
|
import pc from "picocolors";
|
|
@@ -13,6 +14,7 @@ import * as k8s from "../core/kubernetes.js";
|
|
|
13
14
|
import * as flux from "../core/flux.js";
|
|
14
15
|
import { getProvider, } from "../core/git-provider.js";
|
|
15
16
|
import { COMPONENTS, REQUIRED_COMPONENT_IDS, DNS_TLS_COMPONENT_IDS, MONITORING_COMPONENT_IDS, OPTIONAL_COMPONENTS, SOURCE_GITLAB_HOST, SOURCE_PROJECT_PATH, } from "../schemas.js";
|
|
17
|
+
import { fetchTemplateTags, readPackageVersion } from "../core/template-sync.js";
|
|
16
18
|
import { stepWizard, back, maskSecret, } from "../utils/wizard.js";
|
|
17
19
|
import { loginAndCreateCloudflareToken } from "../core/cloudflare-oauth.js";
|
|
18
20
|
// ---------------------------------------------------------------------------
|
|
@@ -40,20 +42,7 @@ function openclawEnabled(state) {
|
|
|
40
42
|
function componentLabel(id) {
|
|
41
43
|
return COMPONENTS.find((c) => c.id === id)?.label ?? id;
|
|
42
44
|
}
|
|
43
|
-
|
|
44
|
-
const encoded = encodeURIComponent(SOURCE_PROJECT_PATH);
|
|
45
|
-
const url = `https://${SOURCE_GITLAB_HOST}/api/v4/projects/${encoded}/repository/tags?per_page=50&order_by=version&sort=desc`;
|
|
46
|
-
try {
|
|
47
|
-
const res = await fetch(url);
|
|
48
|
-
if (!res.ok)
|
|
49
|
-
return [];
|
|
50
|
-
const tags = (await res.json());
|
|
51
|
-
return tags.map((t) => t.name);
|
|
52
|
-
}
|
|
53
|
-
catch {
|
|
54
|
-
return [];
|
|
55
|
-
}
|
|
56
|
-
}
|
|
45
|
+
// fetchTemplateTags is imported from core/template-sync.ts
|
|
57
46
|
function providerLabel(type) {
|
|
58
47
|
return type === "gitlab" ? "GitLab" : "GitHub";
|
|
59
48
|
}
|
|
@@ -447,6 +436,8 @@ function buildFields(detectedIp, hasSavedPlan) {
|
|
|
447
436
|
hint: "Victoria Metrics + Grafana Operator (dashboards, alerting, metrics)",
|
|
448
437
|
};
|
|
449
438
|
const hasMonitoring = MONITORING_COMPONENT_IDS.every((id) => state.selectedComponents.includes(id));
|
|
439
|
+
const monitoringExplicitlyRemoved = !hasMonitoring
|
|
440
|
+
&& state.selectedComponents.some((id) => !REQUIRED_COMPONENT_IDS.includes(id));
|
|
450
441
|
const selected = await p.multiselect({
|
|
451
442
|
message: pc.bold("Optional components to install"),
|
|
452
443
|
options: [
|
|
@@ -458,7 +449,7 @@ function buildFields(detectedIp, hasSavedPlan) {
|
|
|
458
449
|
})),
|
|
459
450
|
],
|
|
460
451
|
initialValues: [
|
|
461
|
-
...(hasMonitoring ? [MONITORING_GROUP_ID] : []),
|
|
452
|
+
...((hasMonitoring || !monitoringExplicitlyRemoved) ? [MONITORING_GROUP_ID] : []),
|
|
462
453
|
...state.selectedComponents.filter((id) => OPTIONAL_COMPONENTS.some((c) => c.id === id)),
|
|
463
454
|
],
|
|
464
455
|
required: false,
|
|
@@ -640,7 +631,10 @@ function buildFields(detectedIp, hasSavedPlan) {
|
|
|
640
631
|
` 3. Name it (e.g. ${pc.cyan("gitops-ai")})\n` +
|
|
641
632
|
` 4. Copy the key value\n\n` +
|
|
642
633
|
pc.dim("The key starts with sk-… and is only shown once."), "OpenAI API Key");
|
|
643
|
-
p.
|
|
634
|
+
await p.text({
|
|
635
|
+
message: pc.dim("Press ") + pc.bold(pc.yellow("Enter")) + pc.dim(" to open browser…"),
|
|
636
|
+
defaultValue: "",
|
|
637
|
+
});
|
|
644
638
|
openUrl(OPENAI_API_KEYS_URL);
|
|
645
639
|
}
|
|
646
640
|
const v = await p.password({
|
|
@@ -661,45 +655,143 @@ function buildFields(detectedIp, hasSavedPlan) {
|
|
|
661
655
|
},
|
|
662
656
|
// ── Network ──────────────────────────────────────────────────────────
|
|
663
657
|
{
|
|
664
|
-
id: "
|
|
658
|
+
id: "networkAccessMode",
|
|
665
659
|
section: "Network",
|
|
666
|
-
skip: (state) => saved(state, "ingressAllowedIps"),
|
|
660
|
+
skip: (state) => saved(state, "ingressAllowedIps") && saved(state, "clusterPublicIp"),
|
|
667
661
|
run: async (state) => {
|
|
668
|
-
const
|
|
669
|
-
message: pc.bold("
|
|
670
|
-
|
|
671
|
-
|
|
662
|
+
const mode = await p.select({
|
|
663
|
+
message: pc.bold("How will you access the cluster?"),
|
|
664
|
+
options: [
|
|
665
|
+
{
|
|
666
|
+
value: "public",
|
|
667
|
+
label: "Public",
|
|
668
|
+
hint: "auto-detects your public IP",
|
|
669
|
+
},
|
|
670
|
+
{
|
|
671
|
+
value: "local",
|
|
672
|
+
label: "Local only (localhost / LAN)",
|
|
673
|
+
hint: "uses 127.0.0.1 or a private IP",
|
|
674
|
+
},
|
|
675
|
+
],
|
|
672
676
|
});
|
|
673
|
-
if (p.isCancel(
|
|
677
|
+
if (p.isCancel(mode))
|
|
674
678
|
return back();
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
679
|
+
const m = mode;
|
|
680
|
+
async function resolvePublicIp() {
|
|
681
|
+
if (detectedIp)
|
|
682
|
+
return detectedIp;
|
|
683
|
+
const services = ["ifconfig.me", "api.ipify.org", "icanhazip.com"];
|
|
684
|
+
await withSpinner("Detecting public IP", async () => {
|
|
685
|
+
for (const svc of services) {
|
|
686
|
+
try {
|
|
687
|
+
const ip = (await execAsync(`curl -s --max-time 4 ${svc}`)).trim();
|
|
688
|
+
if (/^\d{1,3}(\.\d{1,3}){3}$/.test(ip)) {
|
|
689
|
+
detectedIp = ip;
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
catch { /* try next */ }
|
|
694
|
+
}
|
|
695
|
+
});
|
|
696
|
+
return detectedIp;
|
|
697
|
+
}
|
|
698
|
+
if (m === "public") {
|
|
699
|
+
const publicIp = await resolvePublicIp();
|
|
700
|
+
const confirmIp = await p.text({
|
|
701
|
+
message: pc.bold("Public IP of your cluster"),
|
|
702
|
+
...(publicIp
|
|
703
|
+
? { initialValue: publicIp }
|
|
704
|
+
: { placeholder: "x.x.x.x" }),
|
|
705
|
+
validate: (v) => { if (!v)
|
|
706
|
+
return "Required"; },
|
|
707
|
+
});
|
|
708
|
+
if (p.isCancel(confirmIp))
|
|
709
|
+
return back();
|
|
710
|
+
const restriction = await p.select({
|
|
711
|
+
message: pc.bold("Restrict ingress access?"),
|
|
712
|
+
options: [
|
|
713
|
+
{
|
|
714
|
+
value: "open",
|
|
715
|
+
label: "Open to everyone (0.0.0.0/0)",
|
|
716
|
+
hint: "any IP can reach the cluster",
|
|
717
|
+
},
|
|
718
|
+
{
|
|
719
|
+
value: "restrict",
|
|
720
|
+
label: "Restrict to specific IPs",
|
|
721
|
+
hint: "only listed CIDRs can reach the cluster",
|
|
722
|
+
},
|
|
723
|
+
],
|
|
724
|
+
});
|
|
725
|
+
if (p.isCancel(restriction))
|
|
726
|
+
return back();
|
|
727
|
+
if (restriction === "open") {
|
|
728
|
+
return { ...state, clusterPublicIp: confirmIp, ingressAllowedIps: "0.0.0.0/0" };
|
|
729
|
+
}
|
|
730
|
+
const allowedCidrs = await p.text({
|
|
731
|
+
message: pc.bold("Allowed CIDRs (comma-separated)"),
|
|
732
|
+
placeholder: "203.0.113.0/24,198.51.100.5/32",
|
|
733
|
+
validate: (v) => { if (!v)
|
|
734
|
+
return "At least one CIDR is required"; },
|
|
735
|
+
});
|
|
736
|
+
if (p.isCancel(allowedCidrs))
|
|
737
|
+
return back();
|
|
738
|
+
return { ...state, clusterPublicIp: confirmIp, ingressAllowedIps: allowedCidrs };
|
|
739
|
+
}
|
|
740
|
+
// local — detect LAN IPs from network interfaces
|
|
741
|
+
const lanIps = [];
|
|
742
|
+
const ifaces = networkInterfaces();
|
|
743
|
+
for (const [name, addrs] of Object.entries(ifaces)) {
|
|
744
|
+
for (const addr of addrs ?? []) {
|
|
745
|
+
if (addr.family === "IPv4" && !addr.internal) {
|
|
746
|
+
lanIps.push({ ip: addr.address, iface: name });
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
let localIp;
|
|
751
|
+
if (lanIps.length > 0) {
|
|
752
|
+
localIp = await p.select({
|
|
753
|
+
message: pc.bold("Cluster IP"),
|
|
754
|
+
options: [
|
|
755
|
+
...lanIps.map((l) => ({
|
|
756
|
+
value: l.ip,
|
|
757
|
+
label: l.ip,
|
|
758
|
+
hint: l.iface,
|
|
759
|
+
})),
|
|
760
|
+
{ value: "127.0.0.1", label: "127.0.0.1", hint: "localhost" },
|
|
761
|
+
{ value: "__custom__", label: "Enter manually" },
|
|
762
|
+
],
|
|
763
|
+
});
|
|
764
|
+
if (p.isCancel(localIp))
|
|
765
|
+
return back();
|
|
766
|
+
if (localIp === "__custom__") {
|
|
767
|
+
localIp = await p.text({
|
|
768
|
+
message: pc.bold("Cluster IP"),
|
|
769
|
+
placeholder: "192.168.x.x",
|
|
770
|
+
validate: (v) => { if (!v)
|
|
771
|
+
return "Required"; },
|
|
772
|
+
});
|
|
773
|
+
if (p.isCancel(localIp))
|
|
774
|
+
return back();
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
else {
|
|
778
|
+
localIp = await p.text({
|
|
779
|
+
message: pc.bold("Cluster IP") + pc.dim(" (127.0.0.1 for localhost, or your LAN IP)"),
|
|
780
|
+
defaultValue: "127.0.0.1",
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
if (p.isCancel(localIp))
|
|
784
|
+
return back();
|
|
785
|
+
const allowedIps = await p.text({
|
|
786
|
+
message: pc.bold("IPs allowed to access the cluster (CIDR)"),
|
|
787
|
+
defaultValue: "10.0.0.0/8,172.16.0.0/12,192.168.0.0/16",
|
|
788
|
+
placeholder: "10.0.0.0/8,172.16.0.0/12,192.168.0.0/16",
|
|
697
789
|
});
|
|
698
|
-
if (p.isCancel(
|
|
790
|
+
if (p.isCancel(allowedIps))
|
|
699
791
|
return back();
|
|
700
|
-
return { ...state, clusterPublicIp:
|
|
792
|
+
return { ...state, clusterPublicIp: localIp, ingressAllowedIps: allowedIps };
|
|
701
793
|
},
|
|
702
|
-
review: (state) => ["
|
|
794
|
+
review: (state) => ["Network", `${state.clusterPublicIp} allowed: ${state.ingressAllowedIps}`],
|
|
703
795
|
},
|
|
704
796
|
];
|
|
705
797
|
}
|
|
@@ -837,10 +929,26 @@ export async function bootstrap() {
|
|
|
837
929
|
}
|
|
838
930
|
});
|
|
839
931
|
console.log();
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
932
|
+
const version = readPackageVersion();
|
|
933
|
+
const logo = [
|
|
934
|
+
" ▄█████▄ ",
|
|
935
|
+
" ██ ◆ ██ ",
|
|
936
|
+
" ██ ██ ",
|
|
937
|
+
" ▀██ ██▀ ",
|
|
938
|
+
" ▀█▀ ",
|
|
939
|
+
];
|
|
940
|
+
const taglines = [
|
|
941
|
+
"💅 Secure, isolated & flexible GitOps infrastructure",
|
|
942
|
+
"🤖 Manage it yourself — or delegate to AI",
|
|
943
|
+
"🔐 Encrypted secrets, hardened containers,",
|
|
944
|
+
" continuous delivery",
|
|
945
|
+
pc.dim(`v${version}`),
|
|
946
|
+
];
|
|
947
|
+
const banner = logo
|
|
948
|
+
.map((l, i) => pc.cyan(l) + " " + (taglines[i] ?? ""))
|
|
949
|
+
.join("\n");
|
|
950
|
+
p.box(banner, pc.bold("Welcome to GitOps AI Bootstrapper"), {
|
|
951
|
+
contentAlign: "left",
|
|
844
952
|
titleAlign: "center",
|
|
845
953
|
rounded: true,
|
|
846
954
|
formatBorder: (text) => pc.cyan(text),
|
|
@@ -879,14 +987,18 @@ export async function bootstrap() {
|
|
|
879
987
|
return;
|
|
880
988
|
}
|
|
881
989
|
}
|
|
882
|
-
// ── Detect public IP (silent)
|
|
883
|
-
let detectedIp =
|
|
884
|
-
if (!
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
990
|
+
// ── Detect public IP (silent, try multiple services) ─────────────────
|
|
991
|
+
let detectedIp = "";
|
|
992
|
+
if (!prev.clusterPublicIp) {
|
|
993
|
+
for (const svc of ["ifconfig.me", "api.ipify.org", "icanhazip.com"]) {
|
|
994
|
+
try {
|
|
995
|
+
const ip = (await execAsync(`curl -s --max-time 4 ${svc}`)).trim();
|
|
996
|
+
if (/^\d{1,3}(\.\d{1,3}){3}$/.test(ip)) {
|
|
997
|
+
detectedIp = ip;
|
|
998
|
+
break;
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
catch { /* try next */ }
|
|
890
1002
|
}
|
|
891
1003
|
}
|
|
892
1004
|
// ── Run interactive wizard ───────────────────────────────────────────
|
|
@@ -898,6 +1010,7 @@ export async function bootstrap() {
|
|
|
898
1010
|
: [
|
|
899
1011
|
...REQUIRED_COMPONENT_IDS,
|
|
900
1012
|
...(savedDnsTls ? DNS_TLS_COMPONENT_IDS : []),
|
|
1013
|
+
...MONITORING_COMPONENT_IDS,
|
|
901
1014
|
...OPTIONAL_COMPONENTS.map((c) => c.id),
|
|
902
1015
|
];
|
|
903
1016
|
const initialState = {
|
|
@@ -1035,6 +1148,8 @@ export async function bootstrap() {
|
|
|
1035
1148
|
? wizard.openclawGatewayToken
|
|
1036
1149
|
: undefined,
|
|
1037
1150
|
selectedComponents,
|
|
1151
|
+
templateRef: wizard.templateTag?.trim() ||
|
|
1152
|
+
(isNewRepo(wizard) ? "main" : undefined),
|
|
1038
1153
|
};
|
|
1039
1154
|
// ── Check macOS prerequisites ────────────────────────────────────────
|
|
1040
1155
|
if (isMacOS()) {
|
|
@@ -1055,20 +1170,25 @@ export async function bootstrap() {
|
|
|
1055
1170
|
log.error(`Bootstrap failed\n${formatError(err)}`);
|
|
1056
1171
|
return process.exit(1);
|
|
1057
1172
|
}
|
|
1058
|
-
// ── /etc/hosts suggestion (no DNS management)
|
|
1059
|
-
|
|
1173
|
+
// ── /etc/hosts suggestion (local IP or no DNS management) ───────────
|
|
1174
|
+
const isLocalIp = /^(127\.|10\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.)/.test(fullConfig.clusterPublicIp)
|
|
1175
|
+
|| fullConfig.clusterPublicIp === "localhost";
|
|
1176
|
+
if (isLocalIp || !wizard.manageDnsAndTls) {
|
|
1060
1177
|
const hostsEntries = selectedComponents
|
|
1061
1178
|
.map((id) => COMPONENTS.find((c) => c.id === id))
|
|
1062
1179
|
.filter((c) => !!c?.subdomain)
|
|
1063
1180
|
.map((c) => `${fullConfig.clusterPublicIp} ${c.subdomain}.${fullConfig.clusterDomain}`);
|
|
1064
1181
|
if (hostsEntries.length > 0) {
|
|
1065
1182
|
const hostsBlock = hostsEntries.join("\n");
|
|
1066
|
-
|
|
1183
|
+
const reason = isLocalIp
|
|
1184
|
+
? "Your cluster uses a local/private IP, so DNS won't resolve publicly."
|
|
1185
|
+
: "Automatic DNS is disabled.";
|
|
1186
|
+
p.note(`${pc.dim(reason + " Add these to")} ${pc.bold("/etc/hosts")}${pc.dim(":")}\n\n` +
|
|
1067
1187
|
hostsEntries.map((e) => pc.cyan(e)).join("\n"), "Local DNS");
|
|
1068
1188
|
const addHosts = await p.confirm({
|
|
1069
1189
|
message: pc.bold("Append these entries to /etc/hosts now?") +
|
|
1070
1190
|
pc.dim(" (requires sudo — macOS will prompt for your password)"),
|
|
1071
|
-
initialValue:
|
|
1191
|
+
initialValue: true,
|
|
1072
1192
|
});
|
|
1073
1193
|
if (!p.isCancel(addHosts) && addHosts) {
|
|
1074
1194
|
try {
|