WIP: k3s remote setup and graphical installer

This commit is contained in:
chrisfu 2025-11-16 21:48:31 -08:00
parent 46ffe08783
commit 10e1113eea
13 changed files with 1972 additions and 6 deletions

79
INSTALLER_README.md Normal file
View File

@ -0,0 +1,79 @@
# Prole Service Dependencies Installer
A desktop application for installing, deploying, and validating Prole services.
## Features
### 1. Install Screen
- Check installation status of required dependencies:
- Docker
- Homebrew
- k3d
- kubectl
- Helm
- krew
- cmctl
- Download links to each project's homepage
- Generate installer script (`install_dependencies.sh`) with all installation commands
### 2. Deploy Screen
- Single "Build and Deploy" button that:
- Checks Docker is running
- Creates or restarts k3d `prole-service-cluster`
- Builds the local `prole-db` Docker image
- Tags the image for the registry
- Pushes the image to the registry
- Imports the image to the k3d cluster
- Real-time progress tracking with visual indicators (pending, running, completed, error)
- Green checkmarks for completed steps
### 3. Validate Screen
- Displays `kubectl cnpg status prole-db` output
- Shows k3d cluster list status
- Auto-refreshes every 10 seconds (can be toggled)
- Manual refresh button
- Link to Prometheus (http://localhost:9090)
## Usage
### Running the Installer
```bash
python3 install.py
```
Or make it executable and run directly:
```bash
chmod +x install.py
./install.py
```
### Requirements
- Python 3.6+
- tkinter (usually included with Python on macOS/Linux)
- macOS (for Homebrew installation)
### Installation Steps
1. **Install Dependencies**: Use the Install screen to check and install required dependencies
2. **Deploy Services**: Use the Deploy screen to build and deploy Prole services
3. **Validate**: Use the Validate screen to monitor the deployment status
## Notes
- The installer requires Docker Desktop to be running for deployment
- The k3d cluster name is `prole-service-cluster` as specified in README.md
- The Docker image version is `prole-db:17.5-027`
- The registry is configured as `k8s-prole-org-registry:k8s.prole.org:5000`
## Apple Silicon Support
The installer automatically detects if running on Apple Silicon (ARM64) and:
- Sets `--platform linux/amd64` flag for Docker builds
- Ensures mssql Docker images are built for AMD64 platform (required for MSSQL Server)
- Ensures prole-db images are built for AMD64 platform for compatibility
This ensures compatibility with services that require AMD64 architecture, such as MSSQL Server.

View File

@ -76,3 +76,76 @@ kubectl apply -f https://github.com/cloudnative-pg/plugin-barman-cloud/releases/
# prole logo cmd
# figlet -f small "prole" | boxes -d ansi-rounded | boxes -d shell
## K3s HA install for Prole (Pi cluster + local k3d agents)
This repository includes scripts to create an HA K3s control plane across Raspberry Pi hosts and attach local k3d agents.
Topology overview:
- raspberry.prole.org → first K3s server (embedded etcd)
- pi.prole.org → additional K3s server (recommended for HA)
- This host (developer machine) → local k3d agents that join the remote K3s server
Prerequisites:
- k3sup: https://github.com/alexellis/k3sup
- k3d: https://k3d.io/
- kubectl
- SSH access to the Pis (defaults assume user `pi` and key `~/.ssh/id_rsa`)
1) Install first K3s server on raspberry.prole.org
```bash
cd k3s
# Optional env overrides:
# export K3S_FIRST_SERVER_HOST=raspberry.prole.org
# export K3S_FIRST_SERVER_USER=pi
# export K3S_SSH_KEY=$HOME/.ssh/id_rsa
# export K3S_VERSION=v1.30.6+k3s1
# export K3S_EXTRA_ARGS="--cluster-init --tls-san raspberry.prole.org"
./install_k3s_first_server.sh
```
2) Join pi.prole.org as an additional HA server (optional but recommended)
```bash
cd k3s
# Optional env overrides:
# export K3S_EXISTING_SERVER_HOST=raspberry.prole.org
# export K3S_EXISTING_SERVER_USER=pi
# export K3S_ADDITIONAL_SERVER_HOST=pi.prole.org
# export K3S_ADDITIONAL_SERVER_USER=pi
# export K3S_SSH_KEY=$HOME/.ssh/id_rsa
# export K3S_VERSION=v1.30.6+k3s1
# export K3S_EXTRA_ARGS="--tls-san raspberry.prole.org --tls-san pi.prole.org"
./join_k3s_additional_server.sh
```
3) Get the K3s node token from the first server
```bash
cd k3s
# Optional env overrides:
# export K3S_SERVER_HOST=raspberry.prole.org
# export K3S_SERVER_USER=pi
# export K3S_SSH_KEY=$HOME/.ssh/id_rsa
TOKEN=$(./get_remote_k3s_node_token.sh)
echo "K3s token: $TOKEN"
```
4) Create a local k3d agents-only cluster that joins the remote K3s server
```bash
cd k3s
# Required: provide the token
export K3S_REMOTE_TOKEN="$TOKEN"
# Optional env overrides:
# export K3D_CLUSTER_NAME=prole-remote-agents
# export K3D_AGENTS=2
# export K3D_IMAGE=rancher/k3s:v1.30.6-k3s1
# export K3D_REGISTRY_CONFIG=$HOME/dev/prole/k3s/registries.yaml
# export K3S_REMOTE_SERVER_HOST=raspberry.prole.org
# export K3S_REMOTE_SERVER_PORT=6443
./create_k3d_join_remote_agents.sh
```
Notes:
- The scripts default to embedded etcd HA. For true quorum, use at least 3 servers where possible. With two servers, loss of one server can cause loss of quorum; consider adding a third server or running an external etcd.
- If you prefer an external datastore (e.g., MySQL/PG/etcd), adjust `K3S_EXTRA_ARGS` in the server install to set `--datastore-endpoint=...` and remove `--cluster-init`.
- The local k3d cluster creates only agents that connect to the remote control-plane; it does not run a separate control-plane locally.

781
install.py Executable file
View File

@ -0,0 +1,781 @@
#!/usr/bin/env python3
"""
Prole Service Dependencies Installer
Desktop application for installing, deploying, and validating Prole services
"""
import tkinter as tk
from tkinter import ttk, scrolledtext, messagebox
import subprocess
import threading
import os
import sys
import webbrowser
import time
import platform
from pathlib import Path
# Get the project root directory
PROJECT_ROOT = Path(__file__).parent.absolute()
def is_apple_silicon():
"""Check if running on Apple Silicon (ARM64)"""
return platform.machine() == 'arm64' and platform.system() == 'Darwin'
def get_docker_build_platform_args():
"""Get Docker build platform arguments for Apple Silicon"""
if is_apple_silicon():
return ['--platform', 'linux/amd64']
return []
class ProleInstaller:
def __init__(self, root):
self.root = root
self.root.title("Prole Service Dependencies Installer")
# Center window on screen
window_width = 1000
window_height = 700
screen_width = root.winfo_screenwidth()
screen_height = root.winfo_screenheight()
center_x = int(screen_width / 2 - window_width / 2)
center_y = int(screen_height / 2 - window_height / 2)
self.root.geometry(f"{window_width}x{window_height}+{center_x}+{center_y}")
self.root.configure(bg='#1a1a1a')
# Style configuration
self.style = ttk.Style()
self.style.theme_use('clam')
self.configure_styles()
# Create main container
self.container = tk.Frame(root, bg='#1a1a1a')
self.container.pack(fill='both', expand=True, padx=20, pady=20)
# Initialize validation attributes before creating screens
self.validation_running = False
self.validation_thread = None
# Create navigation buttons
self.create_navigation()
# Create screens
self.screens = {}
self.create_install_screen()
self.create_deploy_screen()
self.create_validate_screen()
# Show initial screen
self.show_screen('install')
def configure_styles(self):
"""Configure ttk styles"""
self.style.configure('Title.TLabel',
background='#1a1a1a',
foreground='#4a9eff',
font=('Helvetica', 20, 'bold'))
self.style.configure('Heading.TLabel',
background='#1a1a1a',
foreground='#ffffff',
font=('Helvetica', 14, 'bold'))
self.style.configure('Dark.TFrame',
background='#1a1a1a')
self.style.configure('Card.TFrame',
background='#2a2a2a',
relief='flat')
self.style.configure('Primary.TButton',
background='#4a9eff',
foreground='white',
font=('Helvetica', 12, 'bold'))
self.style.map('Primary.TButton',
background=[('active', '#3a8eef')])
def create_navigation(self):
"""Create navigation buttons"""
nav_frame = tk.Frame(self.container, bg='#1a1a1a')
nav_frame.pack(fill='x', pady=(0, 20))
self.nav_buttons = {}
screens = [
('install', '1. Install'),
('deploy', '2. Deploy'),
('validate', '3. Validate')
]
for screen_id, label in screens:
btn = tk.Button(nav_frame,
text=label,
command=lambda s=screen_id: self.show_screen(s),
bg='#2a2a2a',
fg='#ffffff',
activebackground='#3a3a3a',
activeforeground='#ffffff',
font=('Helvetica', 11, 'bold'),
relief='flat',
padx=20,
pady=10,
cursor='hand2',
borderwidth=0,
highlightthickness=0)
btn.pack(side='left', padx=5)
self.nav_buttons[screen_id] = btn
def show_screen(self, screen_id):
"""Show the specified screen"""
# Hide all screens
for screen in self.screens.values():
screen.pack_forget()
# Show selected screen
if screen_id in self.screens:
self.screens[screen_id].pack(fill='both', expand=True)
# Update navigation button states
for sid, btn in self.nav_buttons.items():
if sid == screen_id:
btn.configure(bg='#4a9eff', fg='#ffffff', activebackground='#3a8eef', activeforeground='#ffffff')
else:
btn.configure(bg='#2a2a2a', fg='#ffffff', activebackground='#3a3a3a', activeforeground='#ffffff')
def create_install_screen(self):
"""Create the Install screen"""
frame = tk.Frame(self.container, bg='#1a1a1a')
self.screens['install'] = frame
# Title
title = ttk.Label(frame, text="Install Dependencies", style='Title.TLabel')
title.pack(pady=(0, 30))
# Instructions
instructions = tk.Label(frame,
text="Install the following dependencies to proceed with Prole deployment:",
bg='#1a1a1a',
fg='#aaaaaa',
font=('Helvetica', 11))
instructions.pack(pady=(0, 20))
# Dependencies list
deps_frame = tk.Frame(frame, bg='#1a1a1a')
deps_frame.pack(fill='both', expand=True)
dependencies = [
{
'name': 'Docker',
'description': 'Container platform for running Prole services',
'url': 'https://www.docker.com/products/docker-desktop',
'install_cmd': None,
'check_cmd': 'docker --version'
},
{
'name': 'Homebrew',
'description': 'Package manager for macOS',
'url': 'https://brew.sh',
'install_cmd': '/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"',
'check_cmd': 'brew --version'
},
{
'name': 'k3d',
'description': 'Lightweight wrapper to run k3s in Docker',
'url': 'https://k3d.io',
'install_cmd': 'brew install k3d',
'check_cmd': 'k3d --version'
},
{
'name': 'kubectl',
'description': 'Kubernetes command-line tool',
'url': 'https://kubernetes.io/docs/tasks/tools/',
'install_cmd': 'brew install kubectl',
'check_cmd': 'kubectl version --client'
},
{
'name': 'Helm',
'description': 'Kubernetes package manager',
'url': 'https://helm.sh',
'install_cmd': 'brew install helm',
'check_cmd': 'helm version'
},
{
'name': 'krew',
'description': 'Kubectl plugin manager',
'url': 'https://krew.sigs.k8s.io',
'install_cmd': 'brew install krew',
'check_cmd': 'kubectl krew version'
},
{
'name': 'cmctl',
'description': 'cert-manager CLI tool',
'url': 'https://cert-manager.io',
'install_cmd': 'brew install cmctl',
'check_cmd': 'cmctl version'
}
]
self.dep_status = {}
for dep in dependencies:
self.create_dependency_card(deps_frame, dep)
# Generate installer script button
script_btn = tk.Button(frame,
text="Generate Installer Script",
command=self.generate_installer_script,
bg='#4a9eff',
fg='white',
activebackground='#3a8eef',
font=('Helvetica', 12, 'bold'),
padx=30,
pady=15,
cursor='hand2',
relief='flat')
script_btn.pack(pady=20)
def create_dependency_card(self, parent, dep):
"""Create a dependency card with status and download link"""
card = tk.Frame(parent, bg='#2a2a2a', relief='flat', bd=1)
card.pack(fill='x', pady=5, padx=10)
# Left side - info
info_frame = tk.Frame(card, bg='#2a2a2a')
info_frame.pack(side='left', fill='both', expand=True, padx=15, pady=15)
name_label = tk.Label(info_frame,
text=dep['name'],
bg='#2a2a2a',
fg='#ffffff',
font=('Helvetica', 13, 'bold'),
anchor='w')
name_label.pack(fill='x')
desc_label = tk.Label(info_frame,
text=dep['description'],
bg='#2a2a2a',
fg='#aaaaaa',
font=('Helvetica', 10),
anchor='w')
desc_label.pack(fill='x', pady=(5, 0))
# Right side - status and actions
action_frame = tk.Frame(card, bg='#2a2a2a')
action_frame.pack(side='right', padx=15, pady=15)
# Status indicator
status_label = tk.Label(action_frame,
text="Checking...",
bg='#2a2a2a',
fg='#ffaa00',
font=('Helvetica', 10))
status_label.pack(side='left', padx=10)
self.dep_status[dep['name']] = {'label': status_label, 'dep': dep}
# Download button
download_btn = tk.Button(action_frame,
text="Download",
command=lambda url=dep['url']: webbrowser.open(url),
bg='#28a745',
fg='white',
activebackground='#218838',
font=('Helvetica', 10),
padx=15,
pady=5,
cursor='hand2',
relief='flat')
download_btn.pack(side='left', padx=5)
# Check status
self.check_dependency(dep['name'])
def check_dependency(self, name):
"""Check if a dependency is installed"""
dep_info = self.dep_status[name]
dep = dep_info['dep']
label = dep_info['label']
def check():
try:
result = subprocess.run(dep['check_cmd'].split(),
capture_output=True,
text=True,
timeout=5)
if result.returncode == 0:
label.configure(text="✓ Installed", fg='#28a745')
else:
label.configure(text="✗ Not Installed", fg='#dc3545')
except Exception:
label.configure(text="✗ Not Installed", fg='#dc3545')
threading.Thread(target=check, daemon=True).start()
def generate_installer_script(self):
"""Generate installer script with all commands"""
script_path = PROJECT_ROOT / 'install_dependencies.sh'
script_content = """#!/bin/bash
# Prole Dependencies Installer Script
# Generated by Prole Installer
set -e
echo "Installing Prole dependencies..."
# Check and install Homebrew
if ! command -v brew &> /dev/null; then
echo "Installing Homebrew..."
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
else
echo "Homebrew already installed. Updating..."
brew update
brew upgrade
fi
# Install k3d
if ! command -v k3d &> /dev/null; then
echo "Installing k3d..."
brew install k3d
fi
# Install kubectl
if ! command -v kubectl &> /dev/null; then
echo "Installing kubectl..."
brew install kubectl
fi
# Install Helm
if ! command -v helm &> /dev/null; then
echo "Installing Helm..."
brew install helm
fi
# Install krew
if ! command -v kubectl krew &> /dev/null; then
echo "Installing krew..."
brew install krew
kubectl krew update
fi
# Install cmctl
if ! command -v cmctl &> /dev/null; then
echo "Installing cmctl..."
brew install cmctl
fi
# Install kubectl plugins
echo "Installing kubectl plugins..."
kubectl krew install view-secret
echo "All dependencies installed successfully!"
"""
try:
with open(script_path, 'w') as f:
f.write(script_content)
os.chmod(script_path, 0o755)
messagebox.showinfo("Success",
f"Installer script generated at:\n{script_path}\n\n"
"You can run it with: ./install_dependencies.sh")
except Exception as e:
messagebox.showerror("Error", f"Failed to generate script: {str(e)}")
def create_deploy_screen(self):
"""Create the Deploy screen"""
frame = tk.Frame(self.container, bg='#1a1a1a')
self.screens['deploy'] = frame
# Title
title = ttk.Label(frame, text="Build and Deploy", style='Title.TLabel')
title.pack(pady=(0, 30))
# Instructions
instructions = tk.Label(frame,
text="Build and deploy Prole services to k3d cluster",
bg='#1a1a1a',
fg='#aaaaaa',
font=('Helvetica', 11))
instructions.pack(pady=(0, 20))
# Deploy button
deploy_btn = tk.Button(frame,
text="Build and Deploy",
command=self.start_deployment,
bg='#4a9eff',
fg='white',
activebackground='#3a8eef',
font=('Helvetica', 16, 'bold'),
padx=40,
pady=20,
cursor='hand2',
relief='flat')
deploy_btn.pack(pady=20)
# Progress frame
progress_frame = tk.Frame(frame, bg='#1a1a1a')
progress_frame.pack(fill='both', expand=True, pady=20)
# Progress list
self.deploy_steps = [
{'name': 'Check Docker is running', 'status': 'pending'},
{'name': 'Create or restart k3d prole-service-cluster', 'status': 'pending'},
{'name': 'Build prole-db Docker image', 'status': 'pending'},
{'name': 'Tag Docker image for registry', 'status': 'pending'},
{'name': 'Push image to registry', 'status': 'pending'},
{'name': 'Import image to k3d cluster', 'status': 'pending'},
]
self.deploy_widgets = {}
for step in self.deploy_steps:
self.create_deploy_step_widget(progress_frame, step)
def create_deploy_step_widget(self, parent, step):
"""Create a widget for a deployment step"""
step_frame = tk.Frame(parent, bg='#2a2a2a', relief='flat')
step_frame.pack(fill='x', pady=5, padx=10)
# Status indicator
status_canvas = tk.Canvas(step_frame, width=30, height=30, bg='#2a2a2a', highlightthickness=0)
status_canvas.pack(side='left', padx=15, pady=15)
# Step name
name_label = tk.Label(step_frame,
text=step['name'],
bg='#2a2a2a',
fg='#ffffff',
font=('Helvetica', 11),
anchor='w')
name_label.pack(side='left', fill='x', expand=True, padx=10)
# Status text
status_label = tk.Label(step_frame,
text="Pending",
bg='#2a2a2a',
fg='#aaaaaa',
font=('Helvetica', 10))
status_label.pack(side='right', padx=15)
self.deploy_widgets[step['name']] = {
'canvas': status_canvas,
'label': status_label,
'step': step
}
# Draw initial pending state
self.update_deploy_step_status(step['name'], 'pending')
def update_deploy_step_status(self, step_name, status):
"""Update the status of a deployment step"""
widget = self.deploy_widgets[step_name]
canvas = widget['canvas']
label = widget['label']
step = widget['step']
step['status'] = status
canvas.delete('all')
if status == 'pending':
canvas.create_oval(5, 5, 25, 25, outline='#666', width=2)
label.configure(text="Pending", fg='#aaaaaa')
elif status == 'running':
canvas.create_oval(5, 5, 25, 25, outline='#ffaa00', width=2, fill='#ffaa00')
label.configure(text="Running...", fg='#ffaa00')
elif status == 'completed':
canvas.create_oval(5, 5, 25, 25, outline='#28a745', width=2, fill='#28a745')
canvas.create_text(15, 15, text='', fill='white', font=('Helvetica', 16, 'bold'))
label.configure(text="Completed", fg='#28a745')
elif status == 'error':
canvas.create_oval(5, 5, 25, 25, outline='#dc3545', width=2, fill='#dc3545')
canvas.create_text(15, 15, text='', fill='white', font=('Helvetica', 16, 'bold'))
label.configure(text="Error", fg='#dc3545')
def start_deployment(self):
"""Start the deployment process"""
threading.Thread(target=self.run_deployment, daemon=True).start()
def run_deployment(self):
"""Run the deployment steps"""
try:
# Step 1: Check Docker
self.update_deploy_step_status('Check Docker is running', 'running')
if not self.check_docker_running():
self.update_deploy_step_status('Check Docker is running', 'error')
messagebox.showerror("Error", "Docker is not running. Please start Docker Desktop.")
return
self.update_deploy_step_status('Check Docker is running', 'completed')
# Step 2: Create or restart k3d cluster
self.update_deploy_step_status('Create or restart k3d prole-service-cluster', 'running')
self.create_k3d_cluster()
self.update_deploy_step_status('Create or restart k3d prole-service-cluster', 'completed')
# Step 3: Build Docker image
self.update_deploy_step_status('Build prole-db Docker image', 'running')
self.build_docker_image()
self.update_deploy_step_status('Build prole-db Docker image', 'completed')
# Step 4: Tag image
self.update_deploy_step_status('Tag Docker image for registry', 'running')
self.tag_docker_image()
self.update_deploy_step_status('Tag Docker image for registry', 'completed')
# Step 5: Push to registry
self.update_deploy_step_status('Push image to registry', 'running')
self.push_docker_image()
self.update_deploy_step_status('Push image to registry', 'completed')
# Step 6: Import to k3d
self.update_deploy_step_status('Import image to k3d cluster', 'running')
self.import_k3d_image()
self.update_deploy_step_status('Import image to k3d cluster', 'completed')
messagebox.showinfo("Success", "Deployment completed successfully!")
except Exception as e:
messagebox.showerror("Error", f"Deployment failed: {str(e)}")
def check_docker_running(self):
"""Check if Docker is running"""
try:
result = subprocess.run(['docker', 'ps'],
capture_output=True,
timeout=10)
return result.returncode == 0
except Exception:
return False
def create_k3d_cluster(self):
"""Create or restart k3d cluster"""
# Check if cluster exists
result = subprocess.run(['k3d', 'cluster', 'list'],
capture_output=True,
text=True)
cluster_exists = 'prole-service-cluster' in result.stdout
if cluster_exists:
# Delete existing cluster
subprocess.run(['k3d', 'cluster', 'delete', 'prole-service-cluster'],
check=True)
# Create new cluster
subprocess.run([
'k3d', 'cluster', 'create', 'prole-service-cluster',
'-a', '2',
'--registry-create', 'k8s-prole-org-registry:k8s.prole.org:5000',
'--timestamps'
], check=True, cwd=PROJECT_ROOT)
def build_docker_image(self):
"""Build prole-db Docker image"""
image_tag = 'prole-db:17.5-027'
build_cmd = ['docker', 'build', '-t', image_tag]
# Add platform flag for Apple Silicon (ARM64 needs amd64 for compatibility)
build_cmd.extend(get_docker_build_platform_args())
build_cmd.append('.')
result = subprocess.run(build_cmd,
check=True,
cwd=PROJECT_ROOT / 'prole-db',
capture_output=True,
text=True)
if result.returncode != 0:
raise Exception(f"Failed to build image: {result.stderr}")
def build_mssql_docker_image(self, image_tag='prole-mssql-db:latest'):
"""Build mssql Docker image (with platform detection for Apple Silicon)"""
build_cmd = ['docker', 'build', '-t', image_tag]
# Add platform flag for Apple Silicon (mssql requires amd64)
build_cmd.extend(get_docker_build_platform_args())
build_cmd.append('.')
result = subprocess.run(build_cmd,
check=True,
cwd=PROJECT_ROOT / 'mssql',
capture_output=True,
text=True)
if result.returncode != 0:
raise Exception(f"Failed to build mssql image: {result.stderr}")
return result
def tag_docker_image(self):
"""Tag Docker image for registry"""
subprocess.run([
'docker', 'tag',
'prole-db:17.5-027',
'localhost:5000/prole-db:17.5-027'
], check=True, capture_output=True)
def push_docker_image(self):
"""Push Docker image to registry"""
result = subprocess.run([
'docker', 'push',
'localhost:5000/prole-db:17.5-027'
], check=True, capture_output=True, text=True)
if result.returncode != 0:
raise Exception(f"Failed to push image: {result.stderr}")
def import_k3d_image(self):
"""Import image to k3d cluster"""
subprocess.run([
'k3d', 'image', 'import',
'prole-db:17.5-027',
'-c', 'prole-service-cluster'
], check=True, capture_output=True)
def create_validate_screen(self):
"""Create the Validate screen"""
frame = tk.Frame(self.container, bg='#1a1a1a')
self.screens['validate'] = frame
# Title
title = ttk.Label(frame, text="Validate Deployment", style='Title.TLabel')
title.pack(pady=(0, 20))
# Prometheus link
prometheus_frame = tk.Frame(frame, bg='#1a1a1a')
prometheus_frame.pack(pady=(0, 20))
prometheus_label = tk.Label(prometheus_frame,
text="Prometheus: ",
bg='#1a1a1a',
fg='#aaaaaa',
font=('Helvetica', 11))
prometheus_label.pack(side='left')
prometheus_link = tk.Label(prometheus_frame,
text="http://localhost:9090",
bg='#1a1a1a',
fg='#4a9eff',
font=('Helvetica', 11, 'underline'),
cursor='hand2')
prometheus_link.pack(side='left')
prometheus_link.bind('<Button-1>', lambda e: webbrowser.open('http://localhost:9090'))
# Status display
status_frame = tk.Frame(frame, bg='#1a1a1a')
status_frame.pack(fill='both', expand=True, pady=10)
status_label = ttk.Label(status_frame, text="Cluster Status", style='Heading.TLabel')
status_label.pack(anchor='w', pady=(0, 10))
# Status text area
self.status_text = scrolledtext.ScrolledText(status_frame,
bg='#0a0a0a',
fg='#4a9eff',
font=('Courier', 10),
wrap='word',
relief='flat',
bd=1)
self.status_text.pack(fill='both', expand=True)
# Auto-refresh checkbox
refresh_frame = tk.Frame(frame, bg='#1a1a1a')
refresh_frame.pack(pady=10)
self.auto_refresh_var = tk.BooleanVar(value=True)
refresh_check = tk.Checkbutton(refresh_frame,
text="Auto-refresh every 10 seconds",
variable=self.auto_refresh_var,
bg='#1a1a1a',
fg='#aaaaaa',
selectcolor='#2a2a2a',
activebackground='#1a1a1a',
activeforeground='#aaaaaa',
font=('Helvetica', 10),
command=self.toggle_auto_refresh)
refresh_check.pack(side='left', padx=10)
# Manual refresh button
refresh_btn = tk.Button(refresh_frame,
text="Refresh Now",
command=self.refresh_status,
bg='#4a9eff',
fg='white',
activebackground='#3a8eef',
font=('Helvetica', 10),
padx=15,
pady=5,
cursor='hand2',
relief='flat')
refresh_btn.pack(side='left', padx=10)
# Start auto-refresh
self.refresh_status()
self.toggle_auto_refresh()
def toggle_auto_refresh(self):
"""Toggle auto-refresh"""
if self.auto_refresh_var.get():
if not self.validation_running:
self.validation_running = True
self.validation_thread = threading.Thread(target=self.auto_refresh_loop, daemon=True)
self.validation_thread.start()
else:
self.validation_running = False
def auto_refresh_loop(self):
"""Auto-refresh loop"""
while self.validation_running:
time.sleep(10)
if self.validation_running:
self.root.after(0, self.refresh_status)
def refresh_status(self):
"""Refresh the cluster status"""
def update():
try:
full_status = ""
# Get k3d cluster list
try:
cluster_result = subprocess.run(['k3d', 'cluster', 'list'],
capture_output=True,
text=True,
timeout=5)
full_status += f"=== k3d Cluster Status ===\n{cluster_result.stdout}\n\n"
except Exception as e:
full_status += f"=== k3d Cluster Status ===\nError: {str(e)}\n\n"
# Get kubectl cnpg status
try:
result = subprocess.run(['kubectl', 'cnpg', 'status', 'prole-db'],
capture_output=True,
text=True,
timeout=10)
if result.returncode == 0:
status_output = result.stdout
else:
status_output = f"Error: {result.stderr}\n\nNote: Make sure kubectl cnpg plugin is installed:\n kubectl krew install cnpg"
full_status += f"=== CloudNativePG Status ===\n{status_output}\n"
except FileNotFoundError:
full_status += "=== CloudNativePG Status ===\nError: kubectl not found. Please install dependencies first.\n"
except subprocess.TimeoutExpired:
full_status += "=== CloudNativePG Status ===\nError: Command timed out\n"
except Exception as e:
full_status += f"=== CloudNativePG Status ===\nError: {str(e)}\n"
full_status += f"\nLast updated: {time.strftime('%Y-%m-%d %H:%M:%S')}"
self.status_text.delete('1.0', tk.END)
self.status_text.insert('1.0', full_status)
except Exception as e:
self.status_text.delete('1.0', tk.END)
self.status_text.insert('1.0', f"Error: {str(e)}")
threading.Thread(target=update, daemon=True).start()
def main():
root = tk.Tk()
app = ProleInstaller(root)
root.mainloop()
if __name__ == '__main__':
main()

View File

@ -0,0 +1,49 @@
#!/usr/bin/env bash
# Create a local k3d cluster with only agents that join a remote K3s server.
# This uses k3s args: --server and --token applied to all agent nodes.
set -euo pipefail
: "${K3D_CLUSTER_NAME:=prole-remote-agents}"
: "${K3D_AGENTS:=2}"
: "${K3D_IMAGE:=}" # e.g. rancher/k3s:v1.30.6-k3s1
: "${K3D_REGISTRY_CONFIG:=}" # optional path to registries.yaml
# Remote server details
: "${K3S_REMOTE_SERVER_HOST:=raspberry.prole.org}"
: "${K3S_REMOTE_SERVER_PORT:=6443}"
: "${K3S_REMOTE_TOKEN:=}" # required
if [[ -z "${K3S_REMOTE_TOKEN}" ]]; then
echo "[error] K3S_REMOTE_TOKEN is required. Obtain it using k3s/get_remote_k3s_node_token.sh" >&2
exit 1
fi
SERVER_URL="https://${K3S_REMOTE_SERVER_HOST}:${K3S_REMOTE_SERVER_PORT}"
K3D_ARGS=(cluster create "${K3D_CLUSTER_NAME}" \
--servers 0 \
--agents "${K3D_AGENTS}" \
--k3s-arg "--server=${SERVER_URL}@agent:*" \
--k3s-arg "--token=${K3S_REMOTE_TOKEN}@agent:*"
)
if [[ -n "${K3D_IMAGE}" ]]; then
K3D_ARGS+=(--image "${K3D_IMAGE}")
fi
if [[ -n "${K3D_REGISTRY_CONFIG}" ]]; then
if [[ ! -f "${K3D_REGISTRY_CONFIG}" ]]; then
echo "[error] Registry config not found: ${K3D_REGISTRY_CONFIG}" >&2
exit 1
fi
K3D_ARGS+=(--registry-config "${K3D_REGISTRY_CONFIG}")
fi
echo "[info] Creating k3d agents-only cluster '${K3D_CLUSTER_NAME}' joining ${SERVER_URL}"
set -x
k3d "${K3D_ARGS[@]}"
set +x
echo "[ok] k3d cluster '${K3D_CLUSTER_NAME}' created with ${K3D_AGENTS} agent(s) joining ${SERVER_URL}"

View File

@ -0,0 +1,20 @@
#!/usr/bin/env bash
# Fetch the K3s cluster node-token from the first server via SSH.
# Default server: raspberry.prole.org
set -euo pipefail
: "${K3S_SERVER_HOST:=raspberry.prole.org}"
: "${K3S_SERVER_USER:=pi}"
: "${K3S_SSH_KEY:=$HOME/.ssh/id_rsa}"
echo "[info] Retrieving node-token from ${K3S_SERVER_USER}@${K3S_SERVER_HOST}"
CMD='sudo cat /var/lib/rancher/k3s/server/node-token'
set -x
TOKEN=$(ssh -i "${K3S_SSH_KEY}" -o StrictHostKeyChecking=no "${K3S_SERVER_USER}@${K3S_SERVER_HOST}" "${CMD}")
set +x
echo "${TOKEN}"

View File

@ -0,0 +1,46 @@
#!/usr/bin/env bash
# Bootstrap the first K3s server (embedded etcd) on raspberry.prole.org using k3sup.
# This script is idempotent: re-running will upgrade/ensure k3s is present.
# Requirements:
# - k3sup installed locally (https://github.com/alexellis/k3sup)
# - SSH access to the target host
# - The target host must have ports 6443/tcp, 10250/tcp open locally
set -euo pipefail
# Configurable via env vars; provide sensible defaults
: "${K3S_FIRST_SERVER_HOST:=raspberry.prole.org}"
: "${K3S_FIRST_SERVER_USER:=pi}"
: "${K3S_SSH_KEY:=$HOME/.ssh/id_rsa}"
: "${K3S_VERSION:=}"
# Extra args to pass directly to k3s server
K3S_EXTRA_ARGS_DEFAULT="--cluster-init"
: "${K3S_EXTRA_ARGS:=${K3S_EXTRA_ARGS_DEFAULT}}"
echo "[info] Installing first K3s server on ${K3S_FIRST_SERVER_HOST} (user=${K3S_FIRST_SERVER_USER})"
if ! command -v k3sup >/dev/null 2>&1; then
echo "[error] k3sup is not installed. See https://github.com/alexellis/k3sup" >&2
exit 1
fi
K3SUP_ARGS=(install \
--ip "${K3S_FIRST_SERVER_HOST}" \
--user "${K3S_FIRST_SERVER_USER}" \
--ssh-key "${K3S_SSH_KEY}" \
--k3s-extra-args "${K3S_EXTRA_ARGS}"
)
if [[ -n "${K3S_VERSION}" ]]; then
K3SUP_ARGS+=(--k3s-version "${K3S_VERSION}")
fi
# Create/merge kubeconfig in ~/.kube/config with context named after host
K3SUP_ARGS+=(--local-path "$HOME/.kube/config" --context "k3s-${K3S_FIRST_SERVER_HOST}")
set -x
k3sup "${K3SUP_ARGS[@]}"
set +x
echo "[ok] First K3s server installed with embedded etcd and context 'k3s-${K3S_FIRST_SERVER_HOST}'"

View File

@ -0,0 +1,45 @@
#!/usr/bin/env bash
# Join an additional K3s server (pi.prole.org) to the existing cluster using k3sup.
# Default topology: HA embedded etcd with at least two servers.
set -euo pipefail
: "${K3S_EXISTING_SERVER_HOST:=raspberry.prole.org}"
: "${K3S_EXISTING_SERVER_USER:=pi}"
: "${K3S_ADDITIONAL_SERVER_HOST:=pi.prole.org}"
: "${K3S_ADDITIONAL_SERVER_USER:=pi}"
: "${K3S_SSH_KEY:=$HOME/.ssh/id_rsa}"
: "${K3S_VERSION:=}"
# You likely want your public name(s) here so clients can connect via hostname
# You may include multiple --tls-san by space-separating them.
K3S_EXTRA_ARGS_DEFAULT="--tls-san ${K3S_EXISTING_SERVER_HOST} --tls-san ${K3S_ADDITIONAL_SERVER_HOST}"
: "${K3S_EXTRA_ARGS:=${K3S_EXTRA_ARGS_DEFAULT}}"
echo "[info] Joining ${K3S_ADDITIONAL_SERVER_HOST} as additional K3s server to cluster at ${K3S_EXISTING_SERVER_HOST}"
if ! command -v k3sup >/dev/null 2>&1; then
echo "[error] k3sup is not installed. See https://github.com/alexellis/k3sup" >&2
exit 1
fi
K3SUP_ARGS=(join \
--server \
--ip "${K3S_ADDITIONAL_SERVER_HOST}" \
--user "${K3S_ADDITIONAL_SERVER_USER}" \
--ssh-key "${K3S_SSH_KEY}" \
--server-ip "${K3S_EXISTING_SERVER_HOST}" \
--server-user "${K3S_EXISTING_SERVER_USER}" \
--k3s-extra-args "${K3S_EXTRA_ARGS}"
)
if [[ -n "${K3S_VERSION}" ]]; then
K3SUP_ARGS+=(--k3s-version "${K3S_VERSION}")
fi
set -x
k3sup "${K3SUP_ARGS[@]}"
set +x
echo "[ok] ${K3S_ADDITIONAL_SERVER_HOST} joined as an additional K3s server"

View File

@ -1,6 +0,0 @@
#!/bin/bash
export PROLE_HOME=/Users/chrisfu/dev/prole
PROLE_PASSWD=`cat $PROLE_HOME/prole-db/.prole_user_password | base64 -d`
k3d cluster create prole-data-cluster \
--k3s-arg "--datastore-endpoint=mysql://prole:$PROLE_PASSWD\@tcp(10.0.0.203:3306)/k3s@server:*"

View File

@ -0,0 +1,11 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: prole-mssql-db-data001
spec:
storageClassName: synology-iscsi-storage
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 2Gi

View File

@ -0,0 +1,31 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: prole-mssql-db
spec:
replicas: 1
selector:
matchLabels:
service: db
template:
metadata:
labels:
service: db
spec:
containers:
- name: prole-mssql-db
image: prole-mssql-db:015
ports:
- containerPort: 1433
volumeMounts:
- mountPath: "/var/opt/mssql/data"
name: prole-db-mssql-data
env:
- name: ACCEPT_EULA
value: 'Y'
- name: MSSQL_SA_PASSWORD
value: 'QGFYaC3!pqWy'
volumes:
- name: prole-db-mssql-data
persistentVolumeClaim:
claimName: prole-mssql-db-nfs-data001

BIN
www/images/prole-type.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 599 B

78
www/index.html Normal file
View File

@ -0,0 +1,78 @@
<!DOCTYPE html>
<html class="img-no-display"><head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"><meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no, width=device-width">
<title>prole.org</title>
<style>
html {
height: 100%;
overflow: auto;
padding: 0;
margin: 0;
}
body {
height: 100%;
padding: 0;
margin: 0;
}
div#outer {
display: table;
height: 100%;
width: 100%;
}
div#inner {
display: table-cell;
text-align: center;
vertical-align: middle;
}
div#container {
display: flex;
flex-direction: column;
justify-content: center;
min-width: 800px;
min-height: 580px;
}
img {
width: 500px;
height: 330px;
margin: 30px 0;
}
p#header {
font-family: Roboto-Medium;
font-size: 28px;
color: #323C46;
text-align: center;
line-height: 36px;
margin-top: 0;
margin-bottom: 12px;
}
p#paragraph {
font-family: Roboto-Regular;
font-size: 13px;
color: #323C46;
text-align: center;
line-height: 20px;
margin: 0 auto;
}
</style>
<link href="../help.css" type="text/css" rel="stylesheet" />
<link href="../scrollbar/flexcroll.css" type="text/css" rel="stylesheet" />
<script type="text/javascript" src="../scrollbar/flexcroll.js"></script>
<script type="text/javascript" src="../scrollbar/initFlexcroll.js"></script>
</head>
<body>
<div id="outer">
<div id="inner">
<div id="container">
<div>
<p align=center>
<img src="images/prole-type.gif" border=0>
</p>
</div>
</div>
</div>
</div>
</body>
</html>

759
www/installer.html Normal file
View File

@ -0,0 +1,759 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Prole Service Dependencies Installer</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: #1a1a1a;
color: #e0e0e0;
overflow: hidden;
height: 100vh;
position: relative;
}
#background-image {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image: url('images/prole-type.gif');
background-size: cover;
background-position: center;
background-repeat: no-repeat;
opacity: 0.15;
z-index: 0;
}
.installer-container {
position: relative;
z-index: 1;
height: 100vh;
display: flex;
flex-direction: column;
background: rgba(26, 26, 26, 0.85);
backdrop-filter: blur(10px);
}
.installer-header {
padding: 30px;
text-align: center;
border-bottom: 2px solid #333;
}
.installer-header h1 {
font-size: 32px;
color: #4a9eff;
margin-bottom: 10px;
}
.installer-header p {
color: #aaa;
font-size: 14px;
}
.installer-content {
flex: 1;
padding: 40px;
overflow-y: auto;
}
.step {
display: none;
max-width: 900px;
margin: 0 auto;
}
.step.active {
display: block;
}
.checklist-item {
background: rgba(255, 255, 255, 0.05);
border: 1px solid #333;
border-radius: 8px;
padding: 20px;
margin-bottom: 15px;
display: flex;
align-items: center;
justify-content: space-between;
}
.checklist-item-content {
flex: 1;
}
.checklist-item-title {
font-size: 18px;
font-weight: 600;
color: #fff;
margin-bottom: 8px;
display: flex;
align-items: center;
gap: 10px;
}
.checklist-item-description {
font-size: 14px;
color: #aaa;
margin-top: 5px;
}
.sub-checklist {
margin-top: 15px;
margin-left: 30px;
padding-left: 20px;
border-left: 2px solid #444;
}
.sub-checklist-item {
background: rgba(255, 255, 255, 0.03);
border: 1px solid #2a2a2a;
border-radius: 6px;
padding: 12px;
margin-bottom: 10px;
display: flex;
align-items: center;
justify-content: space-between;
}
.status-indicator {
display: flex;
align-items: center;
gap: 15px;
}
.checkmark {
width: 24px;
height: 24px;
border-radius: 50%;
border: 2px solid #4a9eff;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.checkmark.installed {
background: #4a9eff;
border-color: #4a9eff;
}
.checkmark.installed::after {
content: '✓';
color: white;
font-size: 16px;
font-weight: bold;
}
.checkmark.not-installed {
background: transparent;
border-color: #666;
}
.checkmark.not-installed::after {
content: '✗';
color: #666;
font-size: 16px;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
text-decoration: none;
display: inline-block;
}
.btn-primary {
background: #4a9eff;
color: white;
}
.btn-primary:hover {
background: #3a8eef;
transform: translateY(-2px);
}
.btn-secondary {
background: #555;
color: white;
}
.btn-secondary:hover {
background: #666;
}
.btn-download {
background: #28a745;
color: white;
}
.btn-download:hover {
background: #218838;
}
.btn-update {
background: #ffc107;
color: #1a1a1a;
}
.btn-update:hover {
background: #e0a800;
}
.command-display {
background: #0a0a0a;
border: 1px solid #333;
border-radius: 6px;
padding: 20px;
margin: 20px 0;
font-family: 'Courier New', monospace;
font-size: 14px;
color: #4a9eff;
overflow-x: auto;
}
.command-display code {
color: #4a9eff;
white-space: pre-wrap;
}
.installer-footer {
padding: 20px 40px;
border-top: 2px solid #333;
display: flex;
justify-content: space-between;
align-items: center;
}
.btn-large {
padding: 12px 30px;
font-size: 16px;
}
.terminal-window {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 80%;
max-width: 800px;
height: 500px;
background: rgba(10, 10, 10, 0.95);
border: 2px solid #4a9eff;
border-radius: 8px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
z-index: 1000;
display: none;
flex-direction: column;
backdrop-filter: blur(10px);
}
.terminal-window.active {
display: flex;
}
.terminal-window::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image: url('images/prole-type.gif');
background-size: cover;
background-position: center;
opacity: 0.2;
z-index: -1;
border-radius: 6px;
}
.terminal-header {
padding: 15px;
background: rgba(74, 158, 255, 0.2);
border-bottom: 1px solid #4a9eff;
display: flex;
justify-content: space-between;
align-items: center;
}
.terminal-title {
color: #4a9eff;
font-weight: 600;
}
.terminal-close {
background: #ff4444;
color: white;
border: none;
width: 24px;
height: 24px;
border-radius: 50%;
cursor: pointer;
font-size: 14px;
display: flex;
align-items: center;
justify-content: center;
}
.terminal-body {
flex: 1;
padding: 20px;
overflow-y: auto;
font-family: 'Courier New', monospace;
font-size: 14px;
color: #4a9eff;
}
.terminal-prompt {
color: #4a9eff;
margin-bottom: 10px;
}
.terminal-input {
background: transparent;
border: none;
color: #4a9eff;
font-family: 'Courier New', monospace;
font-size: 14px;
width: 100%;
outline: none;
}
.terminal-output {
color: #aaa;
margin-top: 10px;
}
.cluster-status {
background: #0a0a0a;
border: 1px solid #333;
border-radius: 6px;
padding: 20px;
margin: 20px 0;
font-family: 'Courier New', monospace;
font-size: 14px;
color: #4a9eff;
min-height: 200px;
max-height: 400px;
overflow-y: auto;
}
.cluster-status pre {
margin: 0;
color: #4a9eff;
}
.status-text {
color: #aaa;
font-style: italic;
}
</style>
</head>
<body>
<div id="background-image"></div>
<div class="installer-container">
<div class="installer-header">
<h1>Prole Service Dependencies Installer</h1>
<p>Install and configure required dependencies for Prole services</p>
</div>
<div class="installer-content">
<!-- Step 1: Dependency Installation -->
<div class="step active" id="step1">
<h2 style="margin-bottom: 30px; color: #4a9eff;">Step 1: Install Dependencies</h2>
<!-- Docker Check -->
<div class="checklist-item">
<div class="checklist-item-content">
<div class="checklist-item-title">
<span>Docker</span>
</div>
<div class="checklist-item-description">
Container platform required for running Prole services
</div>
</div>
<div class="status-indicator">
<div class="checkmark not-installed" id="docker-check"></div>
<a href="https://www.docker.com/products/docker-desktop" target="_blank" class="btn btn-download">Click here to download</a>
</div>
</div>
<!-- Homebrew Check -->
<div class="checklist-item">
<div class="checklist-item-content">
<div class="checklist-item-title">
<span>Homebrew</span>
</div>
<div class="checklist-item-description">
Package manager for macOS (required for k3d installation)
</div>
<div class="sub-checklist">
<div class="sub-checklist-item">
<div class="checklist-item-content">
<div class="checklist-item-title" style="font-size: 16px;">
<span>Homebrew Installed & Current</span>
</div>
</div>
<div class="status-indicator">
<div class="checkmark not-installed" id="homebrew-check"></div>
<button class="btn btn-update" id="update-homebrew-btn">Update Homebrew</button>
</div>
</div>
</div>
</div>
</div>
<!-- k3d Check -->
<div class="checklist-item">
<div class="checklist-item-content">
<div class="checklist-item-title">
<span>k3d</span>
</div>
<div class="checklist-item-description">
Lightweight wrapper to run k3s in Docker (installed via Homebrew)
</div>
</div>
<div class="status-indicator">
<div class="checkmark not-installed" id="k3d-check"></div>
<a href="https://k3d.io/" target="_blank" class="btn btn-download">Click here to download</a>
</div>
</div>
<!-- Command Display -->
<div class="command-display">
<strong style="color: #fff; margin-bottom: 10px; display: block;">Commands to execute:</strong>
<code id="install-commands"># Commands will appear here after checking dependencies</code>
</div>
<div style="text-align: center; margin-top: 30px;">
<button class="btn btn-primary btn-large" id="install-btn">Install</button>
</div>
</div>
<!-- Step 2: Create Home Cluster -->
<div class="step" id="step2">
<h2 style="margin-bottom: 30px; color: #4a9eff;">Step 2: Create Home Cluster</h2>
<!-- Docker Running Check -->
<div class="checklist-item">
<div class="checklist-item-content">
<div class="checklist-item-title">
<span>Docker Installed and Running</span>
</div>
<div class="checklist-item-description">
Verify Docker is installed and the daemon is running
</div>
</div>
<div class="status-indicator">
<div class="checkmark not-installed" id="docker-running-check"></div>
</div>
</div>
<!-- k3d Ready Check -->
<div class="checklist-item">
<div class="checklist-item-content">
<div class="checklist-item-title">
<span>k3d Command and Dependencies Ready</span>
</div>
<div class="checklist-item-description">
Verify k3d and all related dependencies are installed and ready
</div>
</div>
<div class="status-indicator">
<div class="checkmark not-installed" id="k3d-ready-check"></div>
</div>
</div>
<!-- k3d Cluster List -->
<div class="cluster-status">
<strong style="color: #fff; margin-bottom: 10px; display: block;">k3d cluster list output:</strong>
<pre id="cluster-list-output" class="status-text">Running k3d cluster list...</pre>
</div>
</div>
</div>
<div class="installer-footer">
<button class="btn btn-secondary" id="prev-btn" style="display: none;">Previous</button>
<button class="btn btn-primary btn-large" id="next-btn">Next</button>
</div>
</div>
<!-- Terminal Window -->
<div class="terminal-window" id="terminal-window">
<div class="terminal-header">
<div class="terminal-title">Prole Terminal</div>
<button class="terminal-close" id="terminal-close">×</button>
</div>
<div class="terminal-body" id="terminal-body">
<div class="terminal-prompt">$ what is my name?</div>
<div class="terminal-output" id="terminal-output"></div>
<div class="terminal-prompt" id="terminal-input-line" style="display: none;">
$ <input type="text" class="terminal-input" id="terminal-input" autocomplete="off">
</div>
</div>
</div>
<script>
let currentStep = 1;
let userName = '';
let terminalActive = false;
// Check dependencies on load
async function checkDependencies() {
// Simulate checking dependencies
// In a real implementation, this would call a backend API
// Check Docker
try {
const dockerCheck = await checkCommand('docker --version');
updateCheckmark('docker-check', dockerCheck);
} catch (e) {
updateCheckmark('docker-check', false);
}
// Check Homebrew
try {
const brewCheck = await checkCommand('brew --version');
updateCheckmark('homebrew-check', brewCheck);
} catch (e) {
updateCheckmark('homebrew-check', false);
}
// Check k3d
try {
const k3dCheck = await checkCommand('k3d --version');
updateCheckmark('k3d-check', k3dCheck);
} catch (e) {
updateCheckmark('k3d-check', false);
}
updateInstallCommands();
}
// Simulate command checking (in real app, this would use a backend)
async function checkCommand(command) {
// This is a simulation - in a real app, you'd call a backend API
// For now, we'll simulate based on common scenarios
return new Promise((resolve) => {
setTimeout(() => {
// Simulate: docker and brew are often installed, k3d less so
if (command.includes('docker')) {
resolve(Math.random() > 0.3); // 70% chance installed
} else if (command.includes('brew')) {
resolve(Math.random() > 0.2); // 80% chance installed
} else if (command.includes('k3d')) {
resolve(Math.random() > 0.7); // 30% chance installed
} else {
resolve(false);
}
}, 500);
});
}
function updateCheckmark(id, installed) {
const checkmark = document.getElementById(id);
if (installed) {
checkmark.classList.remove('not-installed');
checkmark.classList.add('installed');
} else {
checkmark.classList.remove('installed');
checkmark.classList.add('not-installed');
}
}
function updateInstallCommands() {
const dockerInstalled = document.getElementById('docker-check').classList.contains('installed');
const brewInstalled = document.getElementById('homebrew-check').classList.contains('installed');
const k3dInstalled = document.getElementById('k3d-check').classList.contains('installed');
let commands = [];
if (!brewInstalled) {
commands.push('/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"');
} else {
commands.push('brew update');
commands.push('brew upgrade');
}
if (!k3dInstalled && brewInstalled) {
commands.push('brew install k3d');
}
const commandsDisplay = document.getElementById('install-commands');
if (commands.length > 0) {
commandsDisplay.textContent = commands.join('\n');
} else {
commandsDisplay.textContent = '# All dependencies are already installed!';
}
}
// Update Homebrew button
document.getElementById('update-homebrew-btn').addEventListener('click', () => {
const commandsDisplay = document.getElementById('install-commands');
commandsDisplay.textContent = 'brew update\nbrew upgrade';
alert('Click the Install button to update Homebrew');
});
// Install button
document.getElementById('install-btn').addEventListener('click', () => {
const commands = document.getElementById('install-commands').textContent;
if (commands.includes('All dependencies')) {
alert('All dependencies are already installed!');
return;
}
// Show commands that will be executed
const confirmed = confirm(`The following commands will be executed:\n\n${commands}\n\nProceed?`);
if (confirmed) {
// In a real app, this would execute the commands via a backend
alert('Installation commands executed! (This is a simulation - in production, commands would run via backend)');
// Simulate installation success
setTimeout(() => {
checkDependencies();
}, 2000);
}
});
// Next button
document.getElementById('next-btn').addEventListener('click', () => {
if (currentStep === 1) {
// Move to step 2
document.getElementById('step1').classList.remove('active');
document.getElementById('step2').classList.add('active');
document.getElementById('prev-btn').style.display = 'block';
currentStep = 2;
// Check cluster status
checkClusterStatus();
} else if (currentStep === 2) {
// Open terminal window
openTerminal();
}
});
// Previous button
document.getElementById('prev-btn').addEventListener('click', () => {
if (currentStep === 2) {
document.getElementById('step2').classList.remove('active');
document.getElementById('step1').classList.add('active');
document.getElementById('prev-btn').style.display = 'none';
currentStep = 1;
}
});
// Check cluster status
async function checkClusterStatus() {
// Check Docker running
try {
const dockerRunning = await checkCommand('docker ps');
updateCheckmark('docker-running-check', dockerRunning);
} catch (e) {
updateCheckmark('docker-running-check', false);
}
// Check k3d ready
try {
const k3dReady = await checkCommand('k3d --version');
updateCheckmark('k3d-ready-check', k3dReady);
} catch (e) {
updateCheckmark('k3d-ready-check', false);
}
// Run k3d cluster list
const clusterOutput = document.getElementById('cluster-list-output');
clusterOutput.textContent = 'Running: k3d cluster list\n\n';
// Simulate k3d cluster list output
setTimeout(() => {
const output = `$ k3d cluster list
NAME CLUSTER SERVERS AGENTS LOADBALANCER
prole-data-cluster running 1 0 true
Clusters found: 1`;
clusterOutput.textContent = output;
}, 1000);
}
// Terminal functions
function openTerminal() {
const terminal = document.getElementById('terminal-window');
terminal.classList.add('active');
terminalActive = true;
// Start the prompt sequence
setTimeout(() => {
const inputLine = document.getElementById('terminal-input-line');
inputLine.style.display = 'block';
document.getElementById('terminal-input').focus();
}, 500);
}
document.getElementById('terminal-close').addEventListener('click', () => {
document.getElementById('terminal-window').classList.remove('active');
terminalActive = false;
userName = '';
document.getElementById('terminal-output').innerHTML = '';
document.getElementById('terminal-input-line').style.display = 'none';
document.getElementById('terminal-input').value = '';
});
document.getElementById('terminal-input').addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
const input = document.getElementById('terminal-input');
const value = input.value.trim();
const output = document.getElementById('terminal-output');
if (!userName) {
// First question: "what is my name?"
userName = value || 'Prole';
output.innerHTML = `<div style="color: #4a9eff;">$ ${value}</div>`;
output.innerHTML += `<div style="color: #aaa; margin-top: 10px;">Hi, I'm ${userName}. What should I call you?</div>`;
input.value = '';
// Update prompt
setTimeout(() => {
const promptLine = document.querySelector('.terminal-prompt');
if (promptLine) {
promptLine.textContent = `$ `;
}
}, 100);
} else {
// Second question: "What should I call you?"
const userResponse = value || 'User';
output.innerHTML += `<div style="color: #4a9eff;">$ ${value}</div>`;
output.innerHTML += `<div style="color: #aaa; margin-top: 10px;">Nice to meet you, ${userResponse}! I'm ${userName}.</div>`;
input.value = '';
// Close terminal after a moment
setTimeout(() => {
document.getElementById('terminal-window').classList.remove('active');
terminalActive = false;
}, 2000);
}
}
});
// Initialize
checkDependencies();
</script>
</body>
</html>