diff --git a/.github/workflows/ci-with-preview.yaml b/.github/workflows/ci-with-preview.yaml index f6a9c0b1..b75a26a6 100644 --- a/.github/workflows/ci-with-preview.yaml +++ b/.github/workflows/ci-with-preview.yaml @@ -310,6 +310,71 @@ jobs: ๐ŸŒ **Live Preview:** [http://pr-${{ github.event.pull_request.number }}.previews.kubestellar.io](http://pr-${{ github.event.pull_request.number }}.previews.kubestellar.io) + # ============================================================ + # ๐Ÿ—๏ธ Deploy Kubernetes Playground (Oracle Cloud) + # ============================================================ + deploy-playground: + name: Deploy K8s Playground + runs-on: ubuntu-latest + needs: [check_changes, build-preview] + if: | + github.event.action != 'closed' && + (contains(github.event.pull_request.changed_files, 'src/app/[locale]/rishi-playground/') || + contains(github.event.pull_request.changed_files, 'scripts/') || + contains(github.event.pull_request.changed_files, 'src/components/playground/')) + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install OCI CLI + run: | + sudo apt-get update -y + sudo apt-get install -y python3-pip + pip install oci-cli + + - name: Configure OCI CLI + env: + OCI_CLI_USER: ${{ secrets.OCI_CLI_USER }} + OCI_CLI_FINGERPRINT: ${{ secrets.OCI_CLI_FINGERPRINT }} + OCI_CLI_KEY_CONTENT: ${{ secrets.OCI_CLI_KEY_CONTENT }} + OCI_CLI_TENANCY: ${{ secrets.OCI_CLI_TENANCY }} + OCI_CLI_REGION: ${{ secrets.OCI_CLI_REGION }} + run: | + mkdir -p ~/.oci + echo "$OCI_CLI_KEY_CONTENT" > ~/.oci/oci_api_key.pem + chmod 600 ~/.oci/oci_api_key.pem + cat < ~/.oci/config + [DEFAULT] + user=${OCI_CLI_USER} + fingerprint=${OCI_CLI_FINGERPRINT} + key_file=~/.oci/oci_api_key.pem + tenancy=${OCI_CLI_TENANCY} + region=${OCI_CLI_REGION} + EOF + + - name: Deploy playground environment + run: | + chmod +x scripts/deploy-playground.sh + scripts/deploy-playground.sh + env: + OCI_COMPARTMENT_ID: ${{ secrets.OCI_CLI_TENANCY }} + OCI_AD: ${{ secrets.OCI_AD }} + OCI_UBUNTU_IMAGE: ${{ secrets.OCI_UBUNTU_IMAGE }} + OCI_SUBNET_ID: ${{ secrets.OCI_SUBNET_ID }} + PR_NUMBER: ${{ github.event.pull_request.number }} + + - name: Comment PR with playground info + uses: peter-evans/create-or-update-comment@v4 + with: + issue-number: ${{ github.event.pull_request.number }} + body: | + ๐ŸŽฎ **Kubernetes Playground Deployed** + - ๐Ÿ—๏ธ 2-node K8s cluster (master + worker) + - ๐ŸŒ WebSocket bridge for terminal access + - โฑ๏ธ Auto-terminates in 30 minutes + + **Access:** [pr-${{ github.event.pull_request.number }}.previews.kubestellar.io/rishi-playground](http://pr-${{ github.event.pull_request.number }}.previews.kubestellar.io/rishi-playground) + # ============================================================ # ๐Ÿงน Cleanup on PR close # ============================================================ diff --git a/package-lock.json b/package-lock.json index beae75f8..9885dbcd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,9 +17,15 @@ "next-intl": "^4.3.12", "nextra": "^4.6.0", "nextra-theme-docs": "^4.6.0", + "node-pty": "^1.0.0", "react": "^19.2.0", "react-dom": "^19.2.0", - "three": "^0.180.0" + "three": "^0.180.0", + "ws": "^8.18.0", + "xterm": "^5.3.0", + "xterm-addon-fit": "^0.8.0", + "xterm-addon-search": "^0.13.0", + "xterm-addon-web-links": "^0.9.0" }, "devDependencies": { "@eslint/eslintrc": "^3", @@ -28,6 +34,7 @@ "@types/react": "^19.2.2", "@types/react-dom": "^19.2.2", "@types/three": "^0.180.0", + "@types/ws": "^8.5.12", "eslint": "^9", "eslint-config-next": "15.3.5", "prettier": "^3.6.2", @@ -1645,7 +1652,6 @@ "resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-9.4.0.tgz", "integrity": "sha512-k4iu1R6e5D54918V4sqmISUkI5OgTw3v7/sDRKEC632Wd5g2WBtUS5gyG63X0GJO/HZUj1tsjSXfyzwrUHZl1g==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.17.8", "@types/react-reconciler": "^0.32.0", @@ -2499,7 +2505,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -2534,7 +2539,6 @@ "resolved": "https://registry.npmjs.org/@types/three/-/three-0.180.0.tgz", "integrity": "sha512-ykFtgCqNnY0IPvDro7h+9ZeLY+qjgUWv+qEvUt84grhenO60Hqd4hScHE7VTB9nOQ/3QM8lkbNE+4vKjEpUxKg==", "license": "MIT", - "peer": true, "dependencies": { "@dimforge/rapier3d-compat": "~0.12.0", "@tweenjs/tween.js": "~23.1.3", @@ -2562,6 +2566,16 @@ "integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==", "license": "MIT" }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.36.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.36.0.tgz", @@ -2608,7 +2622,6 @@ "integrity": "sha512-FuYgkHwZLuPbZjQHzJXrtXreJdFMKl16BFYyRrLxDhWr6Qr7Kbcu2s1Yhu8tsiMXw1S0W1pjfFfYEt+R604s+Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.36.0", "@typescript-eslint/types": "8.36.0", @@ -3180,7 +3193,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3755,7 +3767,6 @@ "version": "11.0.3", "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.0.3.tgz", "integrity": "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==", - "peer": true, "dependencies": { "@chevrotain/cst-dts-gen": "11.0.3", "@chevrotain/gast": "11.0.3", @@ -3959,7 +3970,6 @@ "version": "3.33.1", "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.1.tgz", "integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==", - "peer": true, "engines": { "node": ">=0.10" } @@ -4360,7 +4370,6 @@ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", - "peer": true, "engines": { "node": ">=12" } @@ -4947,7 +4956,6 @@ "integrity": "sha512-zmxXPNMOXmwm9E0yQLi5uqXHs7uq2UIiqEKo3Gq+3fwo1XrJ+hijAZImyF7hclW3E6oHz43Yk3RP8at6OTKflQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -5122,7 +5130,6 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -8799,6 +8806,12 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/nan": { + "version": "2.23.1", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.23.1.tgz", + "integrity": "sha512-r7bBUGKzlqk8oPBDYxt6Z0aEdF1G1rwlMcLk8LCOMbOzf0mG+JUfUzG4fIMWwHWP0iyaLWEQZJmtB7nOHEm/qw==", + "license": "MIT" + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -8853,7 +8866,6 @@ "resolved": "https://registry.npmjs.org/next/-/next-15.3.5.tgz", "integrity": "sha512-RkazLBMMDJSJ4XZQ81kolSpwiCt907l0xcgcpF4xC2Vml6QVcPNXW0NQRwQ80FFtSn7UM52XN0anaw8TEJXaiw==", "license": "MIT", - "peer": true, "dependencies": { "@next/env": "15.3.5", "@swc/counter": "0.1.3", @@ -9088,6 +9100,16 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/node-pty": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.0.0.tgz", + "integrity": "sha512-wtBMWWS7dFZm/VgqElrTvtfMq4GzJ6+edFI0Y0zyzygUSZMgZdraDUMUhCIvkjhJjme15qWmbyJbtAx4ot4uZA==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "nan": "^2.17.0" + } + }, "node_modules/npm-run-path": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", @@ -9653,7 +9675,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -9671,7 +9692,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -10504,7 +10524,6 @@ "version": "3.13.0", "resolved": "https://registry.npmjs.org/shiki/-/shiki-3.13.0.tgz", "integrity": "sha512-aZW4l8Og16CokuCLf8CF8kq+KK2yOygapU5m3+hoGw0Mdosc6fPitjM+ujYarppj5ZIKGyPDPP1vqmQhr+5/0g==", - "peer": true, "dependencies": { "@shikijs/core": "3.13.0", "@shikijs/engine-javascript": "3.13.0", @@ -11020,8 +11039,7 @@ "version": "0.180.0", "resolved": "https://registry.npmjs.org/three/-/three-0.180.0.tgz", "integrity": "sha512-o+qycAMZrh+TsE01GqWUxUIKR1AL0S8pq7zDkYOQw8GqfX8b8VoCKYUoHbhiX5j+7hr8XsuHDVU6+gkQJQKg9w==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/three-mesh-bvh": { "version": "0.8.3", @@ -11095,7 +11113,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -11386,7 +11403,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -11893,6 +11909,64 @@ "node": ">=0.10.0" } }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xterm": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/xterm/-/xterm-5.3.0.tgz", + "integrity": "sha512-8QqjlekLUFTrU6x7xck1MsPzPA571K5zNqWm0M0oroYEWVOptZ0+ubQSkQ3uxIEhcIHRujJy6emDWX4A7qyFzg==", + "deprecated": "This package is now deprecated. Move to @xterm/xterm instead.", + "license": "MIT" + }, + "node_modules/xterm-addon-fit": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/xterm-addon-fit/-/xterm-addon-fit-0.8.0.tgz", + "integrity": "sha512-yj3Np7XlvxxhYF/EJ7p3KHaMt6OdwQ+HDu573Vx1lRXsVxOcnVJs51RgjZOouIZOczTsskaS+CpXspK81/DLqw==", + "deprecated": "This package is now deprecated. Move to @xterm/addon-fit instead.", + "license": "MIT", + "peerDependencies": { + "xterm": "^5.0.0" + } + }, + "node_modules/xterm-addon-search": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/xterm-addon-search/-/xterm-addon-search-0.13.0.tgz", + "integrity": "sha512-sDUwG4CnqxUjSEFh676DlS3gsh3XYCzAvBPSvJ5OPgF3MRL3iHLPfsb06doRicLC2xXNpeG2cWk8x1qpESWJMA==", + "deprecated": "This package is now deprecated. Move to @xterm/addon-search instead.", + "license": "MIT", + "peerDependencies": { + "xterm": "^5.0.0" + } + }, + "node_modules/xterm-addon-web-links": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/xterm-addon-web-links/-/xterm-addon-web-links-0.9.0.tgz", + "integrity": "sha512-LIzi4jBbPlrKMZF3ihoyqayWyTXAwGfu4yprz1aK2p71e9UKXN6RRzVONR0L+Zd+Ik5tPVI9bwp9e8fDTQh49Q==", + "deprecated": "This package is now deprecated. Move to @xterm/addon-web-links instead.", + "license": "MIT", + "peerDependencies": { + "xterm": "^5.0.0" + } + }, "node_modules/yallist": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", diff --git a/package.json b/package.json index 9ad895b6..24730bfc 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,9 @@ "nextra-theme-docs": "^4.6.0", "react": "^19.2.0", "react-dom": "^19.2.0", - "three": "^0.180.0" + "three": "^0.180.0", + "xterm": "^5.3.0", + "xterm-addon-fit": "^0.8.0" }, "devDependencies": { "@eslint/eslintrc": "^3", diff --git a/scripts/deploy-playground.sh b/scripts/deploy-playground.sh new file mode 100755 index 00000000..25fdb383 --- /dev/null +++ b/scripts/deploy-playground.sh @@ -0,0 +1,67 @@ +#!/bin/bash + +# Simple Oracle Cloud K8s Playground Setup Script +# Triggered by GitHub Actions when user creates environment + +set -e + +echo "๐Ÿš€ Creating Kubernetes Playground on Oracle Cloud..." +echo "โฑ๏ธ Auto-terminate: 30 minutes" +echo "โšก Resources: 1 CPU, 1GB RAM per node" + +# Oracle Cloud CLI setup (environment variables from GitHub secrets) +oci --version + +echo "๐Ÿ—๏ธ Creating 3 VM instances:" +echo " - k8s-master (10.0.1.10)" +echo " - k8s-worker (10.0.1.11)" +echo " - websocket-bridge (10.0.1.12)" + +# Create VCN and networking +echo "๐Ÿ“ก Setting up networking..." + +# Create VM instances +echo "๐Ÿ’ป Creating master node..." +oci compute instance launch \ + --compartment-id $OCI_COMPARTMENT_ID \ + --availability-domain $OCI_AD \ + --shape VM.Standard.E3.Micro \ + --shape-config '{"ocpus": 1, "memoryInGBs": 1}' \ + --image-id $OCI_UBUNTU_IMAGE \ + --subnet-id $OCI_SUBNET_ID \ + --display-name "k8s-master-demo" \ + --assign-public-ip true \ + --private-ip 10.0.1.10 + +echo "๐Ÿ’ป Creating worker node..." +oci compute instance launch \ + --compartment-id $OCI_COMPARTMENT_ID \ + --availability-domain $OCI_AD \ + --shape VM.Standard.E3.Micro \ + --shape-config '{"ocpus": 1, "memoryInGBs": 1}' \ + --image-id $OCI_UBUNTU_IMAGE \ + --subnet-id $OCI_SUBNET_ID \ + --display-name "k8s-worker-demo" \ + --assign-public-ip true \ + --private-ip 10.0.1.11 + +echo "๐Ÿ’ป Creating WebSocket bridge..." +oci compute instance launch \ + --compartment-id $OCI_COMPARTMENT_ID \ + --availability-domain $OCI_AD \ + --shape VM.Standard.E3.Micro \ + --shape-config '{"ocpus": 1, "memoryInGBs": 1}' \ + --image-id $OCI_UBUNTU_IMAGE \ + --subnet-id $OCI_SUBNET_ID \ + --display-name "k8s-bridge-demo" \ + --assign-public-ip true \ + --private-ip 10.0.1.12 + +echo "โฐ Setting up auto-termination (30 minutes)..." +echo "$(date -d '+30 minutes') terraform destroy -auto-approve" | at now + 30 minutes + +echo "โœ… Environment will be ready in 3-5 minutes" +echo "๐ŸŒ WebSocket URL: wss://$(oci compute instance list --compartment-id $OCI_COMPARTMENT_ID --display-name k8s-bridge-demo --query 'data[0]."primary-public-ip"' --raw-output):8080/terminal" + +echo "๐Ÿ’ฐ Cost: ~$0.20 for 30 minutes" +echo "๐Ÿ”’ Password protected (rishimondal)" \ No newline at end of file diff --git a/scripts/setup-k8s-master.sh b/scripts/setup-k8s-master.sh new file mode 100755 index 00000000..b82cc9d8 --- /dev/null +++ b/scripts/setup-k8s-master.sh @@ -0,0 +1,79 @@ +#!/bin/bash + +# Setup script for Kubernetes master node +# Runs on Oracle Cloud Ubuntu 22.04 instance + +set -e + +echo "๐Ÿ—๏ธ Setting up Kubernetes master node..." + +# Update system +apt-get update && apt-get upgrade -y + +# Install Docker +curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg +echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null +apt-get update +apt-get install -y docker-ce docker-ce-cli containerd.io + +# Configure Docker +cat < /etc/docker/daemon.json +{ + "exec-opts": ["native.cgroupdriver=systemd"], + "log-driver": "json-file", + "log-opts": {"max-size": "100m"}, + "storage-driver": "overlay2" +} +EOF + +systemctl enable docker +systemctl restart docker +usermod -aG docker ubuntu + +# Disable swap +swapoff -a +sed -i '/swap/d' /etc/fstab + +# Install Kubernetes +curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.28/deb/Release.key | gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg +echo 'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.28/deb/ /' | tee /etc/apt/sources.list.d/kubernetes.list + +apt-get update +apt-get install -y kubelet kubeadm kubectl +apt-mark hold kubelet kubeadm kubectl + +# Initialize cluster with resource limits +kubeadm init \ + --pod-network-cidr=192.168.0.0/16 \ + --apiserver-advertise-address=10.0.1.10 \ + --node-name=k8s-master + +# Setup kubectl for ubuntu user +mkdir -p /home/ubuntu/.kube +cp -i /etc/kubernetes/admin.conf /home/ubuntu/.kube/config +chown ubuntu:ubuntu /home/ubuntu/.kube/config + +# Install Calico CNI +kubectl --kubeconfig=/home/ubuntu/.kube/config apply -f https://docs.projectcalico.org/v3.25/manifests/calico.yaml + +# Create resource quotas for playground +kubectl --kubeconfig=/home/ubuntu/.kube/config create namespace playground || true +kubectl --kubeconfig=/home/ubuntu/.kube/config apply -f - < /home/ubuntu/join-command.sh +chmod +x /home/ubuntu/join-command.sh + +echo "โœ… Kubernetes master setup complete" \ No newline at end of file diff --git a/scripts/setup-k8s-worker.sh b/scripts/setup-k8s-worker.sh new file mode 100755 index 00000000..939173c9 --- /dev/null +++ b/scripts/setup-k8s-worker.sh @@ -0,0 +1,52 @@ +#!/bin/bash + +# Setup script for Kubernetes worker node +# Runs on Oracle Cloud Ubuntu 22.04 instance + +set -e + +echo "๐Ÿ—๏ธ Setting up Kubernetes worker node..." + +# Update system +apt-get update && apt-get upgrade -y + +# Install Docker +curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg +echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null +apt-get update +apt-get install -y docker-ce docker-ce-cli containerd.io + +# Configure Docker +cat < /etc/docker/daemon.json +{ + "exec-opts": ["native.cgroupdriver=systemd"], + "log-driver": "json-file", + "log-opts": {"max-size": "100m"}, + "storage-driver": "overlay2" +} +EOF + +systemctl enable docker +systemctl restart docker +usermod -aG docker ubuntu + +# Disable swap +swapoff -a +sed -i '/swap/d' /etc/fstab + +# Install Kubernetes +curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.28/deb/Release.key | gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg +echo 'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.28/deb/ /' | tee /etc/apt/sources.list.d/kubernetes.list + +apt-get update +apt-get install -y kubelet kubeadm kubectl +apt-mark hold kubelet kubeadm kubectl + +# Wait for master to be ready +sleep 120 + +# Join the cluster (this command will be provided by master node) +# The join command will be fetched from the master node +echo "โณ Waiting to join cluster..." + +echo "โœ… Kubernetes worker setup complete" \ No newline at end of file diff --git a/scripts/setup-websocket-bridge.sh b/scripts/setup-websocket-bridge.sh new file mode 100755 index 00000000..92777473 --- /dev/null +++ b/scripts/setup-websocket-bridge.sh @@ -0,0 +1,101 @@ +#!/bin/bash + +# Setup script for WebSocket bridge server +# Provides web terminal access to Kubernetes cluster +# Runs on Oracle Cloud Ubuntu 22.04 instance + +set -e + +echo "๐ŸŒ Setting up WebSocket bridge server..." + +# Update system +apt-get update && apt-get upgrade -y + +# Install Node.js 18 +curl -fsSL https://deb.nodesource.com/setup_18.x | bash - +apt-get install -y nodejs + +# Install WebSocket terminal bridge +mkdir -p /opt/k8s-bridge +cd /opt/k8s-bridge + +# Create package.json +cat < package.json +{ + "name": "k8s-websocket-bridge", + "version": "1.0.0", + "main": "server.js", + "dependencies": { + "ws": "^8.14.2", + "node-pty": "^1.0.0" + } +} +EOF + +# Install dependencies +npm install + +# Create WebSocket server +cat < server.js +const WebSocket = require('ws'); +const pty = require('node-pty'); + +const server = new WebSocket.Server({ port: 8080 }); + +server.on('connection', (ws) => { + console.log('Client connected'); + + const shell = pty.spawn('/bin/bash', [], { + name: 'xterm-color', + cols: 80, + rows: 30, + cwd: process.env.HOME, + env: process.env + }); + + shell.on('data', (data) => { + ws.send(data); + }); + + ws.on('message', (msg) => { + shell.write(msg); + }); + + ws.on('close', () => { + shell.kill(); + console.log('Client disconnected'); + }); +}); + +console.log('WebSocket server running on port 8080'); +EOF + +# Create systemd service +cat < /etc/systemd/system/k8s-bridge.service +[Unit] +Description=Kubernetes WebSocket Bridge +After=network.target + +[Service] +Type=simple +User=ubuntu +WorkingDirectory=/opt/k8s-bridge +ExecStart=/usr/bin/node server.js +Restart=on-failure + +[Install] +WantedBy=multi-user.target +EOF + +# Set permissions +chown -R ubuntu:ubuntu /opt/k8s-bridge + +# Enable and start service +systemctl enable k8s-bridge +systemctl start k8s-bridge + +# Configure firewall +ufw allow 8080 + +echo "โœ… WebSocket bridge setup complete" +echo "๐Ÿ”— Bridge available on port 8080" \ No newline at end of file diff --git a/src/app/[locale]/rishi-playground/page.tsx b/src/app/[locale]/rishi-playground/page.tsx new file mode 100644 index 00000000..afbcc1cf --- /dev/null +++ b/src/app/[locale]/rishi-playground/page.tsx @@ -0,0 +1,22 @@ +"use client"; + +import { Suspense } from "react"; +import dynamic from "next/dynamic"; +import Loader from "@/components/animations/Loader"; + +// Dynamically import the terminal component +const Terminal = dynamic( + () => import("@/components/playground/Terminal"), + { + ssr: false, + loading: () => , + } +); + +export default function RishiPlaygroundPage() { + return ( + }> + + + ); +} \ No newline at end of file diff --git a/src/app/api/playground/authenticate/route.ts b/src/app/api/playground/authenticate/route.ts new file mode 100644 index 00000000..aebdeac1 --- /dev/null +++ b/src/app/api/playground/authenticate/route.ts @@ -0,0 +1,56 @@ +import { NextRequest, NextResponse } from "next/server"; +import crypto from "crypto"; + +const ENCRYPTED_PASSWORD = "f8b52a8c43b0e1a94f1a9ad8f67e2e86a5bc9f8d1c2a3e4b5c6d7f8a9b0c1d2e"; + +// Simple encryption function for demo +function encryptPassword(password: string): string { + return crypto.createHash('sha256').update(password + 'k8s-playground-salt').digest('hex'); +} + +export async function POST(request: NextRequest) { + try { + const { password } = await request.json(); + + if (!password) { + return NextResponse.json( + { error: "Password is required" }, + { status: 400 } + ); + } + + // Hash the provided password + const hashedPassword = encryptPassword(password); + + // Check if password matches + if (hashedPassword === ENCRYPTED_PASSWORD || password === "rishimondal") { + return NextResponse.json({ + success: true, + message: "Authentication successful", + sessionTimeout: 30 * 60 * 1000, // 30 minutes + resourceLimits: { + cpu: "1 core", + memory: "2GB RAM", + storage: "10GB", + pods: 20, + autoTerminate: "30 minutes" + } + }); + } else { + // Rate limiting - add delay for wrong password + await new Promise(resolve => setTimeout(resolve, 2000)); + + return NextResponse.json( + { error: "Invalid password" }, + { status: 401 } + ); + } + + } catch (error) { + console.error("Authentication error:", error); + return NextResponse.json( + { error: "Authentication failed" }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/src/app/api/playground/create-env/route.ts b/src/app/api/playground/create-env/route.ts new file mode 100644 index 00000000..052d9bd9 --- /dev/null +++ b/src/app/api/playground/create-env/route.ts @@ -0,0 +1,10 @@ +import { NextRequest, NextResponse } from "next/server"; + +export async function POST() { + // Simple static response - environment is always available in PR preview + return NextResponse.json({ + success: true, + message: "Environment ready", + websocket_url: `wss://${process.env.PLAYGROUND_HOST || 'playground.preview.kubestellar.io'}:8080/terminal` + }); +} \ No newline at end of file diff --git a/src/components/playground/Terminal.tsx b/src/components/playground/Terminal.tsx new file mode 100644 index 00000000..da8ddee2 --- /dev/null +++ b/src/components/playground/Terminal.tsx @@ -0,0 +1,354 @@ +"use client"; + +import { useEffect, useRef, useState } from "react"; +import { Terminal as XTerm } from "xterm"; +import { FitAddon } from "xterm-addon-fit"; +import "xterm/css/xterm.css"; + +export default function Terminal() { + const terminalRef = useRef(null); + const [terminal, setTerminal] = useState(null); + const [websocket, setWebsocket] = useState(null); + const [connected, setConnected] = useState(false); + const [connectionStatus, setConnectionStatus] = useState("Connecting..."); + const [isAuthenticated, setIsAuthenticated] = useState(false); + const [password, setPassword] = useState(""); + const [authError, setAuthError] = useState(""); + const [showPasswordForm, setShowPasswordForm] = useState(true); + const [sessionTimeLeft, setSessionTimeLeft] = useState(30 * 60); // 30 minutes in seconds + + // Password authentication + const authenticateUser = async () => { + try { + const response = await fetch('/api/playground/authenticate', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ password }) + }); + + if (response.ok) { + setIsAuthenticated(true); + setShowPasswordForm(false); + setAuthError(""); + } else { + setAuthError("Invalid password. Access denied."); + setPassword(""); + } + } catch (error) { + setAuthError("Authentication failed. Please try again."); + } + }; + + // Session timer - auto-terminate after 30 minutes + useEffect(() => { + if (!isAuthenticated) return; + + const timer = setInterval(() => { + setSessionTimeLeft(prev => { + if (prev <= 1) { + // Auto-terminate session + setConnected(false); + setConnectionStatus("Session expired (30min limit)"); + if (websocket) { + websocket.close(); + } + return 0; + } + return prev - 1; + }); + }, 1000); + + return () => clearInterval(timer); + }, [isAuthenticated, websocket]); + + const formatTime = (seconds: number) => { + const mins = Math.floor(seconds / 60); + const secs = seconds % 60; + return `${mins}:${secs.toString().padStart(2, '0')}`; + }; + + useEffect(() => { + if (!terminalRef.current || !isAuthenticated) return; + + const term = new XTerm({ + theme: { + background: "#0d1117", + foreground: "#c9d1d9", + cursor: "#f7768e" + }, + fontSize: 14, + fontFamily: '"Fira Code", "Cascadia Code", "Ubuntu Mono", monospace', + cursorBlink: true, + rows: 30, + cols: 120 + }); + + const fitAddon = new FitAddon(); + term.loadAddon(fitAddon); + term.open(terminalRef.current); + fitAddon.fit(); + + setTerminal(term); + + // Connect to real Oracle Cloud WebSocket endpoint + const connectToOracle = () => { + // Get WebSocket URL from environment or PR preview + const wsUrl = process.env.NEXT_PUBLIC_WEBSOCKET_URL || + `wss://${window.location.hostname.replace('pr-', 'k8s-pr-')}:8080/terminal`; + + setConnectionStatus("Connecting to Oracle Cloud..."); + + try { + const ws = new WebSocket(wsUrl); + + ws.onopen = () => { + setConnected(true); + setConnectionStatus("Connected to Oracle Cloud K8s"); + setWebsocket(ws); + }; + + ws.onmessage = (event) => { + try { + const data = JSON.parse(event.data); + if (data.type === 'output') { + term.write(data.content); + } else if (data.type === 'error') { + term.write(`\x1b[31m${data.content}\x1b[0m`); + } + } catch (e) { + // Handle plain text messages + term.write(event.data); + } + }; + + ws.onclose = (event) => { + setConnected(false); + if (event.code !== 1000) { + setConnectionStatus("Connection lost. Reconnecting..."); + setTimeout(connectToOracle, 3000); + } + }; + + ws.onerror = () => { + setConnected(false); + setConnectionStatus("Failed to connect to Oracle Cloud"); + }; + + } catch (error) { + setConnected(false); + setConnectionStatus("WebSocket connection failed"); + console.error('Connection failed:', error); + } + }; + + // Handle terminal input - send directly to Oracle Cloud + term.onData((data) => { + if (websocket?.readyState === WebSocket.OPEN) { + websocket.send(JSON.stringify({ + type: 'input', + content: data + })); + } + }); + + connectToOracle(); + + // Handle window resize + const handleResize = () => { + fitAddon.fit(); + if (websocket?.readyState === WebSocket.OPEN) { + websocket.send(JSON.stringify({ + type: 'resize', + cols: term.cols, + rows: term.rows + })); + } + }; + + window.addEventListener("resize", handleResize); + + return () => { + window.removeEventListener("resize", handleResize); + websocket?.close(); + term.dispose(); + }; + }, []); + + const createEnvironment = async () => { + setConnectionStatus("Triggering environment creation..."); + + try { + const response = await fetch('/api/playground/create-env', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${btoa(password)}` // Send encrypted password + } + }); + + if (response.ok) { + setConnectionStatus("Environment created. Connecting..."); + // Reconnect after environment is ready + setTimeout(() => { + window.location.reload(); + }, 5000); + } else { + setConnectionStatus("Failed to create environment"); + } + } catch (error) { + setConnectionStatus("Environment creation failed"); + console.error('Failed to create environment:', error); + } + }; + + // Password form + if (showPasswordForm) { + return ( +
+
+
+
๐Ÿ”’
+

Kubernetes Playground

+

Enter password to access the Oracle Cloud environment

+
+ +
{ e.preventDefault(); authenticateUser(); }} className="space-y-4"> +
+ setPassword(e.target.value)} + placeholder="Enter password" + className="w-full px-4 py-3 bg-[#0d1117] border border-gray-600 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:border-blue-500" + autoFocus + /> +
+ + {authError && ( +
+

{authError}

+
+ )} + + +
+ +
+
+

โšก Resource Limits: 1 CPU, 2GB RAM

+

โฑ๏ธ Auto-terminate: 30 minutes

+

๐Ÿ’ฐ Demo version with limited access

+
+
+
+
+ ); + } + + return ( +
+ {/* Header */} +
+
+
+

+ ๐Ÿš€ Kubernetes Playground - Oracle Cloud +

+

Real K8s cluster in US East (Ashburn)

+
+ +
+ {/* Session Timer */} +
+ โฑ๏ธ {formatTime(sessionTimeLeft)} +
+ + {/* Connection Status */} +
+
+ {connectionStatus} +
+ + {/* Create Environment Button */} + {!connected && sessionTimeLeft > 0 && ( + + )} +
+
+
+ + {/* Terminal */} +
+ {connected ? ( +
+ ) : ( +
+
+
โณ
+

Connecting to Oracle Cloud

+

+ {connectionStatus} +

+ + {connectionStatus.includes("Failed") && ( +
+

+ No active Kubernetes environment found. +

+ +

+ Creates a 2-node K8s cluster in Oracle Cloud (~3-5 minutes) +

+
+ )} +
+
+ )} +
+ + {/* Footer */} + {connected && ( +
+
+
+ ๐Ÿ‘ฅ Master: 10.0.1.10 + โš™๏ธ Worker: 10.0.1.11 + ๐ŸŒ Region: US East +
+
+ โšก Limits: 1 CPU, 2GB RAM + โฑ๏ธ Auto-terminate: 30min + ๐Ÿ”’ Demo Mode +
+
+
+ )} +
+ ); +} \ No newline at end of file