Merge pull request 'removed netcat dependency with /dev/tcp' (#2) from bug/fix-missing-dependencies-k8s-bootstrap into main
Reviewed-on: #2
This commit was merged in pull request #2.
This commit is contained in:
@@ -85,31 +85,3 @@ jobs:
|
||||
cloud-url: ${{ secrets.PULUMI_BACKEND_URL }}
|
||||
env:
|
||||
PULUMI_CONFIG_PASSPHRASE: ${{ secrets.PULUMI_CONFIG_PASSPHRASE }}
|
||||
|
||||
# Propagate kubeconfig to the downstream stacks so their next deploy picks it up
|
||||
- name: Propagate kubeconfig to k8s-infra
|
||||
run: |
|
||||
echo "${{ secrets.K8S_INFRA_DEV_YAML }}" | base64 -d > k8s-infra/Pulumi.dev.yaml
|
||||
cd k8s-infra && npm install
|
||||
KUBECONFIG=$(cd ../k8s-bootstrap && pulumi stack output kubeconfig --show-secrets \
|
||||
--cloud-url "${{ secrets.PULUMI_BACKEND_URL }}" -s dev)
|
||||
pulumi config set --secret kubeconfig "$KUBECONFIG" \
|
||||
--cloud-url "${{ secrets.PULUMI_BACKEND_URL }}" -s dev
|
||||
# Re-encode updated config for the secret (update manually in Gitea after first run)
|
||||
base64 -w 0 Pulumi.dev.yaml
|
||||
working-directory: .
|
||||
env:
|
||||
PULUMI_CONFIG_PASSPHRASE: ${{ secrets.PULUMI_CONFIG_PASSPHRASE }}
|
||||
|
||||
- name: Propagate kubeconfig to k8s-apps
|
||||
run: |
|
||||
echo "${{ secrets.K8S_APPS_DEV_YAML }}" | base64 -d > k8s-apps/Pulumi.dev.yaml
|
||||
cd k8s-apps && npm install
|
||||
KUBECONFIG=$(cd ../k8s-bootstrap && pulumi stack output kubeconfig --show-secrets \
|
||||
--cloud-url "${{ secrets.PULUMI_BACKEND_URL }}" -s dev)
|
||||
pulumi config set --secret kubeconfig "$KUBECONFIG" \
|
||||
--cloud-url "${{ secrets.PULUMI_BACKEND_URL }}" -s dev
|
||||
base64 -w 0 Pulumi.dev.yaml
|
||||
working-directory: .
|
||||
env:
|
||||
PULUMI_CONFIG_PASSPHRASE: ${{ secrets.PULUMI_CONFIG_PASSPHRASE }}
|
||||
|
||||
@@ -5,7 +5,7 @@ Bootstraps a k3s cluster on the 5 Proxmox VMs created by `proxmox-infra`. Starts
|
||||
## How it works
|
||||
|
||||
1. Starts all 5 VMs via `POST /api2/json/nodes/{node}/qemu/{vmid}/status/start`
|
||||
2. Waits for port 22 to open on each VM (`nc -z`)
|
||||
2. Waits for port 22 to open on each VM (bash `/dev/tcp`)
|
||||
3. Installs k3s on `k3s-master-1` with `--cluster-init --tls-san <master1Ip>`
|
||||
4. Joins `k3s-master-2` and `k3s-master-3` as embedded etcd nodes
|
||||
5. Joins `k3s-worker-1` and `k3s-worker-2` as agent nodes
|
||||
@@ -18,12 +18,6 @@ VM IDs and the CI runner SSH private key are read automatically from the `proxmo
|
||||
Run in this directory after `pulumi stack init dev`:
|
||||
|
||||
```bash
|
||||
# Same as proxmox-infra
|
||||
pulumi config set --secret pve1Endpoint "https://192.168.1.x:8006"
|
||||
pulumi config set --secret pve1ApiToken "root@pam!pulumi=<token>"
|
||||
pulumi config set --secret pve2Endpoint "https://192.168.1.y:8006"
|
||||
pulumi config set --secret pve2ApiToken "root@pam!pulumi=<token>"
|
||||
|
||||
# Pre-shared k3s token — any strong random string
|
||||
pulumi config set --secret k3sToken "$(openssl rand -hex 32)"
|
||||
|
||||
@@ -35,6 +29,14 @@ pulumi config set worker1Ip "192.168.1.x"
|
||||
pulumi config set worker2Ip "192.168.1.x"
|
||||
```
|
||||
|
||||
Proxmox credentials (`pve1Endpoint`, `pve1ApiToken`, `pve2Endpoint`, `pve2ApiToken`) are read automatically from the `proxmox-infra` stack outputs via StackReference — do **not** set them here.
|
||||
|
||||
## Deployment order
|
||||
|
||||
`proxmox-infra` must be deployed **before** k8s-bootstrap. The proxmox-infra stack exports the Proxmox credentials and VM IDs that k8s-bootstrap reads via StackReference. If proxmox-infra hasn't been re-deployed after its latest code changes, those outputs won't exist and k8s-bootstrap will fail.
|
||||
|
||||
In CI, both workflows run as previews on PRs (no deploy). Trigger `workflow_dispatch` on `deploy-proxmox-infra` first, then run k8s-bootstrap.
|
||||
|
||||
After setting config, re-encode `Pulumi.dev.yaml` and update the Gitea secret `K8S_BOOTSTRAP_DEV_YAML`:
|
||||
```bash
|
||||
base64 -w 0 Pulumi.dev.yaml
|
||||
|
||||
+134
-56
@@ -33,123 +33,201 @@ const worker2Ip = config.require("worker2Ip");
|
||||
const k3sToken = config.requireSecret("k3sToken");
|
||||
|
||||
// VM IDs and CI runner SSH key — read from proxmox-infra stack outputs
|
||||
const vmIdsOutput = infraRef.requireOutput("vmIds") as pulumi.Output<Record<string, number>>;
|
||||
const ciRunnerPrivateKey = infraRef.requireOutput("ciRunnerPrivateKey") as pulumi.Output<string>;
|
||||
const vmIdsOutput = infraRef.requireOutput("vmIds") as pulumi.Output<
|
||||
Record<string, number>
|
||||
>;
|
||||
const ciRunnerPrivateKey = infraRef.requireOutput(
|
||||
"ciRunnerPrivateKey",
|
||||
) as pulumi.Output<string>;
|
||||
|
||||
const master1VmId = vmIdsOutput.apply(ids => String(ids.master1));
|
||||
const master2VmId = vmIdsOutput.apply(ids => String(ids.master2));
|
||||
const master3VmId = vmIdsOutput.apply(ids => String(ids.master3));
|
||||
const worker1VmId = vmIdsOutput.apply(ids => String(ids.worker1));
|
||||
const worker2VmId = vmIdsOutput.apply(ids => String(ids.worker2));
|
||||
const master1VmId = vmIdsOutput.apply((ids) => String(ids.master1));
|
||||
const master2VmId = vmIdsOutput.apply((ids) => String(ids.master2));
|
||||
const master3VmId = vmIdsOutput.apply((ids) => String(ids.master3));
|
||||
const worker1VmId = vmIdsOutput.apply((ids) => String(ids.worker1));
|
||||
const worker2VmId = vmIdsOutput.apply((ids) => String(ids.worker2));
|
||||
|
||||
// SSH connection helper
|
||||
function conn(ip: string): command.types.input.remote.ConnectionArgs {
|
||||
return { host: ip, user: "ubuntu", privateKey: ciRunnerPrivateKey };
|
||||
return { host: ip, user: "ubuntu", privateKey: ciRunnerPrivateKey };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Step 1: Start all VMs (fire-and-forget — VMs are owned by proxmox-infra)
|
||||
// triggers: vmId — forces re-run when VMs are deleted and recreated
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const startMaster1 = new command.local.Command("start-master-1", {
|
||||
create: pulumi.interpolate`curl -sf -k -X POST -H "Authorization: PVEAPIToken=${pve1ApiToken}" "${pve1Endpoint}/api2/json/nodes/pve/qemu/${master1VmId}/status/start" 2>/dev/null || true`,
|
||||
interpreter: ["/bin/bash", "-c"],
|
||||
create: pulumi.interpolate`curl -sf -k -X POST -H "Authorization: PVEAPIToken=${pve1ApiToken}" "${pve1Endpoint}/api2/json/nodes/pve/qemu/${master1VmId}/status/start" 2>/dev/null || true`,
|
||||
triggers: [master1VmId],
|
||||
interpreter: ["/bin/bash", "-c"],
|
||||
});
|
||||
const startMaster2 = new command.local.Command("start-master-2", {
|
||||
create: pulumi.interpolate`curl -sf -k -X POST -H "Authorization: PVEAPIToken=${pve1ApiToken}" "${pve1Endpoint}/api2/json/nodes/pve/qemu/${master2VmId}/status/start" 2>/dev/null || true`,
|
||||
interpreter: ["/bin/bash", "-c"],
|
||||
create: pulumi.interpolate`curl -sf -k -X POST -H "Authorization: PVEAPIToken=${pve1ApiToken}" "${pve1Endpoint}/api2/json/nodes/pve/qemu/${master2VmId}/status/start" 2>/dev/null || true`,
|
||||
triggers: [master2VmId],
|
||||
interpreter: ["/bin/bash", "-c"],
|
||||
});
|
||||
const startWorker1 = new command.local.Command("start-worker-1", {
|
||||
create: pulumi.interpolate`curl -sf -k -X POST -H "Authorization: PVEAPIToken=${pve1ApiToken}" "${pve1Endpoint}/api2/json/nodes/pve/qemu/${worker1VmId}/status/start" 2>/dev/null || true`,
|
||||
interpreter: ["/bin/bash", "-c"],
|
||||
create: pulumi.interpolate`curl -sf -k -X POST -H "Authorization: PVEAPIToken=${pve1ApiToken}" "${pve1Endpoint}/api2/json/nodes/pve/qemu/${worker1VmId}/status/start" 2>/dev/null || true`,
|
||||
triggers: [worker1VmId],
|
||||
interpreter: ["/bin/bash", "-c"],
|
||||
});
|
||||
const startMaster3 = new command.local.Command("start-master-3", {
|
||||
create: pulumi.interpolate`curl -sf -k -X POST -H "Authorization: PVEAPIToken=${pve2ApiToken}" "${pve2Endpoint}/api2/json/nodes/pve-bckp/qemu/${master3VmId}/status/start" 2>/dev/null || true`,
|
||||
interpreter: ["/bin/bash", "-c"],
|
||||
create: pulumi.interpolate`curl -sf -k -X POST -H "Authorization: PVEAPIToken=${pve2ApiToken}" "${pve2Endpoint}/api2/json/nodes/pve-bckp/qemu/${master3VmId}/status/start" 2>/dev/null || true`,
|
||||
triggers: [master3VmId],
|
||||
interpreter: ["/bin/bash", "-c"],
|
||||
});
|
||||
const startWorker2 = new command.local.Command("start-worker-2", {
|
||||
create: pulumi.interpolate`curl -sf -k -X POST -H "Authorization: PVEAPIToken=${pve2ApiToken}" "${pve2Endpoint}/api2/json/nodes/pve-bckp/qemu/${worker2VmId}/status/start" 2>/dev/null || true`,
|
||||
interpreter: ["/bin/bash", "-c"],
|
||||
create: pulumi.interpolate`curl -sf -k -X POST -H "Authorization: PVEAPIToken=${pve2ApiToken}" "${pve2Endpoint}/api2/json/nodes/pve-bckp/qemu/${worker2VmId}/status/start" 2>/dev/null || true`,
|
||||
triggers: [worker2VmId],
|
||||
interpreter: ["/bin/bash", "-c"],
|
||||
});
|
||||
|
||||
const allStarts = [startMaster1, startMaster2, startMaster3, startWorker1, startWorker2];
|
||||
const allStarts = [
|
||||
startMaster1,
|
||||
startMaster2,
|
||||
startMaster3,
|
||||
startWorker1,
|
||||
startWorker2,
|
||||
];
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Step 2: Wait for SSH on master-1, install k3s
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const waitMaster1Ssh = new command.local.Command("wait-ssh-master-1", {
|
||||
create: `for i in $(seq 1 60); do nc -z -w 5 ${master1Ip} 22 && exit 0; sleep 5; done; exit 1`,
|
||||
const waitMaster1Ssh = new command.local.Command(
|
||||
"wait-ssh-master-1",
|
||||
{
|
||||
create: `for i in $(seq 1 60); do (timeout 5 bash -c "echo > /dev/tcp/${master1Ip}/22") 2>/dev/null && exit 0; sleep 5; done; exit 1`,
|
||||
triggers: [master1VmId],
|
||||
interpreter: ["/bin/bash", "-c"],
|
||||
}, { dependsOn: allStarts });
|
||||
},
|
||||
{ dependsOn: allStarts },
|
||||
);
|
||||
|
||||
const installMaster1 = new command.remote.Command("install-k3s-master-1", {
|
||||
const installMaster1 = new command.remote.Command(
|
||||
"install-k3s-master-1",
|
||||
{
|
||||
connection: conn(master1Ip),
|
||||
create: pulumi.interpolate`curl -sfL https://get.k3s.io | K3S_TOKEN='${k3sToken}' sh -s - server --cluster-init --tls-san ${master1Ip}`,
|
||||
}, { dependsOn: [waitMaster1Ssh] });
|
||||
create: pulumi.interpolate`curl -sfL https://get.k3s.io | sudo K3S_TOKEN='${k3sToken}' sh -s - server --cluster-init --tls-san ${master1Ip} --node-name k3s-master-1`,
|
||||
triggers: [master1VmId],
|
||||
},
|
||||
{ dependsOn: [waitMaster1Ssh] },
|
||||
);
|
||||
|
||||
const waitK3sMaster1Ready = new command.remote.Command("wait-k3s-master-1-ready", {
|
||||
const waitK3sMaster1Ready = new command.remote.Command(
|
||||
"wait-k3s-master-1-ready",
|
||||
{
|
||||
connection: conn(master1Ip),
|
||||
create: `for i in $(seq 1 60); do kubectl --kubeconfig /etc/rancher/k3s/k3s.yaml get node k3s-master-1 --no-headers 2>/dev/null | grep -q " Ready" && exit 0; sleep 10; done; exit 1`,
|
||||
}, { dependsOn: [installMaster1] });
|
||||
create: `for i in $(seq 1 60); do sudo kubectl --kubeconfig /etc/rancher/k3s/k3s.yaml get node k3s-master-1 --no-headers 2>/dev/null | grep -q " Ready" && exit 0; sleep 10; done; exit 1`,
|
||||
triggers: [master1VmId],
|
||||
},
|
||||
{ dependsOn: [installMaster1] },
|
||||
);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Step 3: Wait for SSH on remaining masters, join cluster
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const waitMaster2Ssh = new command.local.Command("wait-ssh-master-2", {
|
||||
create: `for i in $(seq 1 60); do nc -z -w 5 ${master2Ip} 22 && exit 0; sleep 5; done; exit 1`,
|
||||
const waitMaster2Ssh = new command.local.Command(
|
||||
"wait-ssh-master-2",
|
||||
{
|
||||
create: `for i in $(seq 1 60); do (timeout 5 bash -c "echo > /dev/tcp/${master2Ip}/22") 2>/dev/null && exit 0; sleep 5; done; exit 1`,
|
||||
triggers: [master2VmId],
|
||||
interpreter: ["/bin/bash", "-c"],
|
||||
}, { dependsOn: [waitK3sMaster1Ready] });
|
||||
},
|
||||
{ dependsOn: [waitK3sMaster1Ready] },
|
||||
);
|
||||
|
||||
const waitMaster3Ssh = new command.local.Command("wait-ssh-master-3", {
|
||||
create: `for i in $(seq 1 60); do nc -z -w 5 ${master3Ip} 22 && exit 0; sleep 5; done; exit 1`,
|
||||
const waitMaster3Ssh = new command.local.Command(
|
||||
"wait-ssh-master-3",
|
||||
{
|
||||
create: `for i in $(seq 1 60); do (timeout 5 bash -c "echo > /dev/tcp/${master3Ip}/22") 2>/dev/null && exit 0; sleep 5; done; exit 1`,
|
||||
triggers: [master3VmId],
|
||||
interpreter: ["/bin/bash", "-c"],
|
||||
}, { dependsOn: [waitK3sMaster1Ready] });
|
||||
},
|
||||
{ dependsOn: [waitK3sMaster1Ready] },
|
||||
);
|
||||
|
||||
const joinMaster2 = new command.remote.Command("join-k3s-master-2", {
|
||||
const joinMaster2 = new command.remote.Command(
|
||||
"join-k3s-master-2",
|
||||
{
|
||||
connection: conn(master2Ip),
|
||||
create: pulumi.interpolate`curl -sfL https://get.k3s.io | K3S_TOKEN='${k3sToken}' sh -s - server --server https://${master1Ip}:6443`,
|
||||
}, { dependsOn: [waitMaster2Ssh] });
|
||||
create: pulumi.interpolate`curl -sfL https://get.k3s.io | sudo K3S_TOKEN='${k3sToken}' sh -s - server --server https://${master1Ip}:6443 --node-name k3s-master-2`,
|
||||
triggers: [master2VmId],
|
||||
},
|
||||
{ dependsOn: [waitMaster2Ssh] },
|
||||
);
|
||||
|
||||
const joinMaster3 = new command.remote.Command("join-k3s-master-3", {
|
||||
const joinMaster3 = new command.remote.Command(
|
||||
"join-k3s-master-3",
|
||||
{
|
||||
connection: conn(master3Ip),
|
||||
create: pulumi.interpolate`curl -sfL https://get.k3s.io | K3S_TOKEN='${k3sToken}' sh -s - server --server https://${master1Ip}:6443`,
|
||||
}, { dependsOn: [waitMaster3Ssh, joinMaster2] });
|
||||
create: pulumi.interpolate`curl -sfL https://get.k3s.io | sudo K3S_TOKEN='${k3sToken}' sh -s - server --server https://${master1Ip}:6443 --node-name k3s-master-3`,
|
||||
triggers: [master3VmId],
|
||||
},
|
||||
{ dependsOn: [waitMaster3Ssh, joinMaster2] },
|
||||
);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Step 4: Join worker nodes
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const waitWorker1Ssh = new command.local.Command("wait-ssh-worker-1", {
|
||||
create: `for i in $(seq 1 60); do nc -z -w 5 ${worker1Ip} 22 && exit 0; sleep 5; done; exit 1`,
|
||||
const waitWorker1Ssh = new command.local.Command(
|
||||
"wait-ssh-worker-1",
|
||||
{
|
||||
create: `for i in $(seq 1 60); do (timeout 5 bash -c "echo > /dev/tcp/${worker1Ip}/22") 2>/dev/null && exit 0; sleep 5; done; exit 1`,
|
||||
triggers: [worker1VmId],
|
||||
interpreter: ["/bin/bash", "-c"],
|
||||
}, { dependsOn: [joinMaster3] });
|
||||
},
|
||||
{ dependsOn: [joinMaster3] },
|
||||
);
|
||||
|
||||
const waitWorker2Ssh = new command.local.Command("wait-ssh-worker-2", {
|
||||
create: `for i in $(seq 1 60); do nc -z -w 5 ${worker2Ip} 22 && exit 0; sleep 5; done; exit 1`,
|
||||
const waitWorker2Ssh = new command.local.Command(
|
||||
"wait-ssh-worker-2",
|
||||
{
|
||||
create: `for i in $(seq 1 60); do (timeout 5 bash -c "echo > /dev/tcp/${worker2Ip}/22") 2>/dev/null && exit 0; sleep 5; done; exit 1`,
|
||||
triggers: [worker2VmId],
|
||||
interpreter: ["/bin/bash", "-c"],
|
||||
}, { dependsOn: [joinMaster3] });
|
||||
},
|
||||
{ dependsOn: [joinMaster3] },
|
||||
);
|
||||
|
||||
const joinWorker1 = new command.remote.Command("join-k3s-worker-1", {
|
||||
const joinWorker1 = new command.remote.Command(
|
||||
"join-k3s-worker-1",
|
||||
{
|
||||
connection: conn(worker1Ip),
|
||||
create: pulumi.interpolate`curl -sfL https://get.k3s.io | K3S_URL=https://${master1Ip}:6443 K3S_TOKEN='${k3sToken}' sh -s -`,
|
||||
}, { dependsOn: [waitWorker1Ssh] });
|
||||
create: pulumi.interpolate`curl -sfL https://get.k3s.io | sudo K3S_URL=https://${master1Ip}:6443 K3S_TOKEN='${k3sToken}' sh -s - --node-name k3s-worker-1`,
|
||||
triggers: [worker1VmId],
|
||||
},
|
||||
{ dependsOn: [waitWorker1Ssh] },
|
||||
);
|
||||
|
||||
const joinWorker2 = new command.remote.Command("join-k3s-worker-2", {
|
||||
const joinWorker2 = new command.remote.Command(
|
||||
"join-k3s-worker-2",
|
||||
{
|
||||
connection: conn(worker2Ip),
|
||||
create: pulumi.interpolate`curl -sfL https://get.k3s.io | K3S_URL=https://${master1Ip}:6443 K3S_TOKEN='${k3sToken}' sh -s -`,
|
||||
}, { dependsOn: [waitWorker2Ssh] });
|
||||
create: pulumi.interpolate`curl -sfL https://get.k3s.io | sudo K3S_URL=https://${master1Ip}:6443 K3S_TOKEN='${k3sToken}' sh -s - --node-name k3s-worker-2`,
|
||||
triggers: [worker2VmId],
|
||||
},
|
||||
{ dependsOn: [waitWorker2Ssh] },
|
||||
);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Step 5: Read kubeconfig from master-1, patch server URL for external access
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const getKubeconfig = new command.remote.Command("get-kubeconfig", {
|
||||
const getKubeconfig = new command.remote.Command(
|
||||
"get-kubeconfig",
|
||||
{
|
||||
connection: conn(master1Ip),
|
||||
create: `cat /etc/rancher/k3s/k3s.yaml`,
|
||||
}, { dependsOn: [joinWorker1, joinWorker2] });
|
||||
create: `sudo cat /etc/rancher/k3s/k3s.yaml`,
|
||||
triggers: [master1VmId],
|
||||
},
|
||||
{ dependsOn: [joinWorker1, joinWorker2] },
|
||||
);
|
||||
|
||||
export const kubeconfig = pulumi.secret(
|
||||
getKubeconfig.stdout.apply(kc => kc.replace(/127\.0\.0\.1/g, master1Ip).trim()),
|
||||
getKubeconfig.stdout.apply((kc) =>
|
||||
kc.replace(/127\.0\.0\.1/g, master1Ip).trim(),
|
||||
),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user