mirror of
https://github.com/dredx/prole.git
synced 2026-09-23 10:13:58 +00:00
Installer refactor: k3d cluster lifecycle, kubeconfig handling, UI polish & cleanup - Fix KUBECONFIG not generated for dev/k3d clusters by merging k3d kubeconfig in _script_env_for_namespace() and after cluster creation - Fix Add/Delete button macOS black focus ring with highlightbackground/highlightthickness styling - Refactor milestones, actions, controller, deploy pipeline, and env helpers - Simplify prole.cfg and port-mapping.cfg defaults - Improve archive_garage_backup.py and init_cloudnative_pg.sh - Remove unused img/LOADING_GIF_README.md and img/generate_loading_gif.py - Add qodana.yaml for static analysis configuration
This commit is contained in:
parent
6e2d3e9011
commit
9094d10117
@ -6,11 +6,11 @@ supabase-studio: local=8088 remote=3000 ns=supabase svc=studio address=0.0.0.0
|
||||
supabase-auth: local=9999 remote=9999 ns=supabase svc=auth address=0.0.0.0
|
||||
supabase-rest: local=3001 remote=3000 ns=supabase svc=rest address=0.0.0.0
|
||||
supabase-realtime: local=4000 remote=4000 ns=supabase svc=realtime address=0.0.0.0
|
||||
garage: local=3900 remote=3900 ns=default svc=garage address=0.0.0.0
|
||||
openbao: local=8200 remote=8200 ns=default svc=openbao address=127.0.0.1
|
||||
opentofu: local=8080 remote=8080 ns=default svc=opentofu address=0.0.0.0
|
||||
garage: local=3900 remote=3900 ns=<MagicMock name='StringVar().get().strip().strip().strip()' id='4767978832'> svc=garage address=0.0.0.0
|
||||
openbao: local=8200 remote=8200 ns=<MagicMock name='StringVar().get().strip().strip().strip()' id='4767978832'> svc=openbao address=127.0.0.1
|
||||
opentofu: local=8080 remote=8080 ns=<MagicMock name='StringVar().get().strip().strip().strip()' id='4767978832'> svc=opentofu address=0.0.0.0
|
||||
dashboard: local=8443 remote=443 ns=kubernetes-dashboard svc=kubernetes-dashboard-kong-proxy address=127.0.0.1
|
||||
postgres: local=5432 remote=5432 ns=prole-db-a0001 svc=prole-db-rw address=0.0.0.0
|
||||
postgres: local=<MagicMock name='StringVar().get().strip().strip()' id='4766698528'> remote=5432 ns=<MagicMock name='StringVar().get().strip().strip()' id='4766698528'> svc=prole-db-rw address=0.0.0.0
|
||||
prometheus: local=9090 remote=9090 ns=monitoring svc=kps-kube-prometheus-stack-prometheus address=127.0.0.1
|
||||
grafana: local=3000 remote=80 ns=monitoring svc=kps-grafana address=0.0.0.0
|
||||
supabase-kong: local=8000 remote=8000 ns=supabase svc=kong address=0.0.0.0
|
||||
|
||||
@ -27,45 +27,45 @@ env_setup.PROLE_HOME = ${PROLE_HOME}
|
||||
env_setup.PROLE_LOGS = ${PROLE_LOGS}
|
||||
env_setup.PROLE_SERVICE = ${PROLE_SERVICE}
|
||||
init_cluster.at_rest_encryption_enabled = true
|
||||
init_cluster.cluster_env = k3d-prole-dev-cluster
|
||||
init_cluster.cluster_env = dev
|
||||
init_cluster.deployment_target = prole-dev-cluster
|
||||
init_cluster.k3s_server_url = https://myrddin.prole.org:6443
|
||||
init_cluster.k3s_token = K107c8c6000488eca4a067d8a73119bbae2f07b4ea1bac7d8d3dc9c500cbb8acb18::server:04572345810eae2f9619a6ed4239702b
|
||||
init_cluster.kerberos_enabled = true
|
||||
init_cluster.mode = k3d
|
||||
init_cluster.start_cluster = true
|
||||
init_cluster.supabase_enabled = true
|
||||
init_cluster.supabase_enabled = false
|
||||
init_cnpg_deploy.force_rollout = false
|
||||
init_cnpg_deploy.run_deploy = true
|
||||
init_db_build.run_build = true
|
||||
init_password.db_host_port = 5432
|
||||
init_password.db_password = ${OPENBAO:kv/prole/prole-db-a0001/db#password}
|
||||
init_password.db_password_confirm = ${OPENBAO:kv/prole/prole-db-a0001/db#password}
|
||||
init_password.db_username = chrisfu
|
||||
init_password.db_username = root
|
||||
init_password.generate_ssh_key = true
|
||||
init_scripts.run_scripts = true
|
||||
kerberos_config.enabled = true
|
||||
kerberos_config.kdc = 10.0.0.3
|
||||
kerberos_config.password = ${OPENBAO:kv/prole/prole-db-a0001/kerberos#password}
|
||||
kerberos_config.realm = PROLE.ORG
|
||||
kerberos_config.test_connection = true
|
||||
kerberos_config.user = chrisfu
|
||||
kerberos_config.test_connection = false
|
||||
kerberos_config.user =
|
||||
network_scan.run = true
|
||||
ollama_config.model =
|
||||
ollama_config.server_host = morgoth.prole.org
|
||||
ollama_config.server_host =
|
||||
ollama_config.server_port = 11434
|
||||
|
||||
[Global]
|
||||
CLUSTER_ENV = k3d-prole-dev-cluster
|
||||
CNPG_RELEASE_SERIES = 1.28
|
||||
CNPG_VERSION = 1.28.1
|
||||
CNPG_RELEASE_SERIES = 1.28
|
||||
CLUSTER_ENV = dev
|
||||
DB_HOST_PORT = 5432
|
||||
DB_PASSWORD = ${OPENBAO:kv/prole/prole-db-a0001/db#password}
|
||||
DEPLOYMENT_MODE = k3d
|
||||
DEPLOYMENT_TARGET = prole-dev-cluster
|
||||
DOCKER_IMPORT_DIR =
|
||||
NAMESPACE = ${NAMESPACE}
|
||||
PROLE_DB_USER = chrisfu
|
||||
PROLE_DB_USER = root
|
||||
PROLE_HOME = ${PROLE_HOME}
|
||||
PROLE_K3S_SERVER = https://myrddin.prole.org:6443
|
||||
PROLE_K3S_TOKEN = K107c8c6000488eca4a067d8a73119bbae2f07b4ea1bac7d8d3dc9c500cbb8acb18::server:04572345810eae2f9619a6ed4239702b
|
||||
@ -75,7 +75,6 @@ SERVICE_NAMESPACE = default
|
||||
[Welcome]
|
||||
|
||||
[Dependencies]
|
||||
STATUS = All installed
|
||||
|
||||
[Network]
|
||||
AD_DC_HOST = myrddin.prole.org
|
||||
@ -90,20 +89,6 @@ KDC_AUTO_DETECTED = 10.0.0.3
|
||||
KERBEROS_AUTO_ENABLED = True
|
||||
|
||||
[Port Forwards]
|
||||
PORT_FORWARD_K3D_MAPPING_1 = id=argocd;namespace=argocd;target=svc/argocd-server;address=0.0.0.0;hostPort=8081;servicePort=80;protocol=TCP;description=ArgoCD
|
||||
PORT_FORWARD_K3D_MAPPING_10 = id=supabase-studio;namespace=supabase;target=svc/studio;address=0.0.0.0;hostPort=8088;servicePort=3000;protocol=TCP;description=Supabase Studio
|
||||
PORT_FORWARD_K3D_MAPPING_11 = id=supabase-auth;namespace=supabase;target=svc/auth;address=0.0.0.0;hostPort=9999;servicePort=9999;protocol=TCP;description=Supabase Auth (GoTrue)
|
||||
PORT_FORWARD_K3D_MAPPING_12 = id=supabase-rest;namespace=supabase;target=svc/rest;address=0.0.0.0;hostPort=3001;servicePort=3000;protocol=TCP;description=Supabase REST (PostgREST)
|
||||
PORT_FORWARD_K3D_MAPPING_13 = id=supabase-realtime;namespace=supabase;target=svc/realtime;address=0.0.0.0;hostPort=4000;servicePort=4000;protocol=TCP;description=Supabase Realtime
|
||||
PORT_FORWARD_K3D_MAPPING_2 = id=garage;namespace=default;target=svc/garage;address=0.0.0.0;hostPort=3900;servicePort=3900;protocol=TCP;description=Garage S3
|
||||
PORT_FORWARD_K3D_MAPPING_3 = id=openbao;namespace=default;target=svc/openbao;address=127.0.0.1;hostPort=8200;servicePort=8200;protocol=TCP;description=OpenBao
|
||||
PORT_FORWARD_K3D_MAPPING_4 = id=opentofu;namespace=default;target=svc/opentofu;address=0.0.0.0;hostPort=8080;servicePort=8080;protocol=TCP;description=OpenTofu
|
||||
PORT_FORWARD_K3D_MAPPING_5 = id=dashboard;namespace=kubernetes-dashboard;target=svc/kubernetes-dashboard-kong-proxy;address=127.0.0.1;hostPort=8443;servicePort=443;protocol=TCP;description=Kubernetes Dashboard
|
||||
PORT_FORWARD_K3D_MAPPING_6 = id=postgres;namespace=${NAMESPACE};target=svc/prole-db-rw;address=0.0.0.0;hostPort=5432;servicePort=5432;protocol=TCP;description=PostgreSQL (primary)
|
||||
PORT_FORWARD_K3D_MAPPING_7 = id=prometheus;namespace=monitoring;target=svc/kps-kube-prometheus-stack-prometheus;address=127.0.0.1;hostPort=9090;servicePort=9090;protocol=TCP;description=Prometheus UI
|
||||
PORT_FORWARD_K3D_MAPPING_8 = id=grafana;namespace=monitoring;target=svc/kps-grafana;address=0.0.0.0;hostPort=3000;servicePort=80;protocol=TCP;description=Grafana UI
|
||||
PORT_FORWARD_K3D_MAPPING_9 = id=supabase-kong;namespace=supabase;target=svc/kong;address=0.0.0.0;hostPort=8000;servicePort=8000;protocol=TCP;description=Supabase API (Kong)
|
||||
STATUS = Failed
|
||||
|
||||
[System Environment]
|
||||
PROLE_CONF = ${PROLE_CONF}
|
||||
@ -116,42 +101,20 @@ PROLE_SERVICE = ${PROLE_SERVICE}
|
||||
GRAFANA_ADMIN_PASSWORD = ${OPENBAO:kv/prole/prole-db-a0001/monitoring#grafana_admin_password}
|
||||
|
||||
[Kerberos Authentication]
|
||||
AD_PORT_FORWARD = 1
|
||||
AD_PROXY_HOST_NETWORK = 1
|
||||
AD_PROXY_IMAGE = alpine/socat
|
||||
AD_PROXY_SERVICE = prole-kerberos-ad-dc
|
||||
AD_TCP_PORTS = 88 389 445 464 636
|
||||
AD_UDP_PORTS = 88 464
|
||||
ENABLED = True
|
||||
KDC = 10.0.0.3
|
||||
PASSWORD = ${OPENBAO:kv/prole/prole-db-a0001/kerberos#password}
|
||||
REALM = PROLE.ORG
|
||||
SERVER = 10.0.0.3
|
||||
USER = chrisfu
|
||||
|
||||
[Ollama]
|
||||
OLLAMA_HOST = http://morgoth.prole.org:11434
|
||||
OLLAMA_SERVER_HOST = morgoth.prole.org
|
||||
OLLAMA_SERVER_PORT = 11434
|
||||
|
||||
[Optional Features]
|
||||
AT_REST_ENCRYPTION_ENABLED = True
|
||||
KERBEROS_ENABLED = True
|
||||
SUPABASE_ENABLED = False
|
||||
|
||||
[Database Creation]
|
||||
DB_NAME = prole-db-a0001
|
||||
DB_USER = chrisfu
|
||||
|
||||
[Initialize Cluster]
|
||||
ENVIRONMENT = k3d-prole-dev-cluster
|
||||
K3S_SERVER_URL = https://myrddin.prole.org:6443
|
||||
K3S_TOKEN = ${PROLE_SECRET:v1:lmSCzQB0YbB3fQ32:KUlh4KkEvD18Q3UddLIRLfBddA_WhfPOHIoquMN6dboK0SUJf2BG4yJKEg8ZKjhEQXeOl7ciDzSEQcHeErBVL2F4ZMwcLGcsymCTN5icxqqzs3MUBkDDfTaFNg12RdiycO1rPEZlqd7RZSLsSqT2clRvaDR03UGhCLxFvw==}
|
||||
|
||||
[Dev Cluster (k3d)]
|
||||
CLUSTER_ENV = k3d-prole-dev-cluster
|
||||
CLUSTER_ENV = dev
|
||||
DISPLAY_NAME = prole-dev-cluster
|
||||
KUBECTL_CONTEXT = k3d-prole-dev-cluster
|
||||
KUBECTL_CONTEXT =
|
||||
MODE = k3d
|
||||
|
||||
[Service Cluster (k3s)]
|
||||
@ -163,23 +126,19 @@ MODE = k3s
|
||||
PIPELINE_URL = http://127.0.0.1:8080
|
||||
|
||||
[Prod Cluster (k8s)]
|
||||
ARTIFACTS_DIR = ${PROLE_DATA}/staging
|
||||
ARTIFACTS_DIR = /Users/chrisfu/dev/prole/data/staging
|
||||
CLUSTER_ENV = prole-prod-cluster
|
||||
DISPLAY_NAME = prole-prod-cluster
|
||||
MODE = k8s
|
||||
PIPELINE_URL = http://127.0.0.1:8080
|
||||
|
||||
[Docker Build]
|
||||
LOCAL_REGISTRY = localhost:5000
|
||||
LOCAL_REGISTRY_INTERNAL = k3d-prole-registry.localhost:5000
|
||||
|
||||
[Initialization Scripts]
|
||||
STATUS = Completed
|
||||
|
||||
[Deployment]
|
||||
MODE = k3d
|
||||
TARGET = prole-dev-cluster
|
||||
|
||||
[Install]
|
||||
STATUS = Finished
|
||||
|
||||
|
||||
@ -45,16 +45,17 @@ def _canonical_uri(path: str) -> str:
|
||||
return "/".join(parts)
|
||||
|
||||
|
||||
def _aws_request(method: str, endpoint: str, path: str, params: dict, access_key: str, secret_key: str, region: str):
|
||||
parsed = urlparse(endpoint)
|
||||
host = parsed.netloc
|
||||
canonical_uri = _canonical_uri(path)
|
||||
canonical_querystring = _canonical_query(params)
|
||||
payload_hash = hashlib.sha256(b"").hexdigest()
|
||||
|
||||
now = datetime.datetime.utcnow()
|
||||
def _sign_request(
|
||||
method: str, host: str, path: str, params: dict, access_key: str, secret_key: str, region: str, service: str
|
||||
) -> tuple[dict[str, str], str, str]:
|
||||
"""Sign an AWS request and return headers, canonical URI, and canonical query string."""
|
||||
now = datetime.datetime.now(datetime.timezone.utc)
|
||||
amz_date = now.strftime("%Y%m%dT%H%M%SZ")
|
||||
date_stamp = now.strftime("%Y%m%d")
|
||||
payload_hash = hashlib.sha256(b"").hexdigest()
|
||||
|
||||
canonical_uri = _canonical_uri(path)
|
||||
canonical_querystring = _canonical_query(params)
|
||||
|
||||
canonical_headers = (
|
||||
f"host:{host}\n"
|
||||
@ -63,24 +64,21 @@ def _aws_request(method: str, endpoint: str, path: str, params: dict, access_key
|
||||
)
|
||||
signed_headers = "host;x-amz-content-sha256;x-amz-date"
|
||||
|
||||
canonical_request = "\n".join(
|
||||
[
|
||||
canonical_request = "\n".join([
|
||||
method,
|
||||
canonical_uri,
|
||||
canonical_querystring,
|
||||
canonical_headers,
|
||||
signed_headers,
|
||||
payload_hash,
|
||||
]
|
||||
)
|
||||
])
|
||||
|
||||
algorithm = "AWS4-HMAC-SHA256"
|
||||
credential_scope = f"{date_stamp}/{region}/s3/aws4_request"
|
||||
string_to_sign = "\n".join(
|
||||
[algorithm, amz_date, credential_scope, hashlib.sha256(canonical_request.encode("utf-8")).hexdigest()]
|
||||
)
|
||||
credential_scope = f"{date_stamp}/{region}/{service}/aws4_request"
|
||||
hashed_request = hashlib.sha256(canonical_request.encode("utf-8")).hexdigest()
|
||||
string_to_sign = "\n".join([algorithm, amz_date, credential_scope, hashed_request])
|
||||
|
||||
signing_key = _signature_key(secret_key, date_stamp, region, "s3")
|
||||
signing_key = _signature_key(secret_key, date_stamp, region, service)
|
||||
signature = hmac.new(signing_key, string_to_sign.encode("utf-8"), hashlib.sha256).hexdigest()
|
||||
|
||||
authorization_header = (
|
||||
@ -88,16 +86,27 @@ def _aws_request(method: str, endpoint: str, path: str, params: dict, access_key
|
||||
f"SignedHeaders={signed_headers}, Signature={signature}"
|
||||
)
|
||||
|
||||
url = f"{parsed.scheme}://{host}{canonical_uri}"
|
||||
if canonical_querystring:
|
||||
url = f"{url}?{canonical_querystring}"
|
||||
|
||||
headers = {
|
||||
"x-amz-date": amz_date,
|
||||
"x-amz-content-sha256": payload_hash,
|
||||
"Authorization": authorization_header,
|
||||
}
|
||||
|
||||
return headers, canonical_uri, canonical_querystring
|
||||
|
||||
|
||||
def _aws_request(method: str, endpoint: str, path: str, params: dict, access_key: str, secret_key: str, region: str):
|
||||
parsed = urlparse(endpoint)
|
||||
host = parsed.netloc
|
||||
|
||||
headers, canonical_uri, canonical_querystring = _sign_request(
|
||||
method, host, path, params, access_key, secret_key, region, "s3"
|
||||
)
|
||||
|
||||
url = f"{parsed.scheme}://{host}{canonical_uri}"
|
||||
if canonical_querystring:
|
||||
url = f"{url}?{canonical_querystring}"
|
||||
|
||||
req = urllib.request.Request(url, method=method, headers=headers)
|
||||
return urllib.request.urlopen(req)
|
||||
|
||||
|
||||
@ -1101,6 +1101,11 @@ fetch_admin_keys_and_db_pass_from_bao_or_local() {
|
||||
fi
|
||||
fi
|
||||
|
||||
if ! kubectl -n "$NAMESPACE" get secret prole-db-user >/dev/null 2>&1; then
|
||||
echo "ERROR: 'prole-db-user' secret is missing in namespace '$NAMESPACE' and could not be resolved from OpenBao or DB_PASSWORD env." >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ -f "$ADMIN_PRIV_GENERIC" && -f "$ADMIN_PUB_GENERIC" ]]; then
|
||||
echo "Using local admin key pair at $SECRETS_DIR"
|
||||
return 0
|
||||
@ -1162,7 +1167,10 @@ initialize() {
|
||||
ensure_barman_plugin
|
||||
echo "Using namespace: $NAMESPACE"
|
||||
|
||||
fetch_admin_keys_and_db_pass_from_bao_or_local
|
||||
if ! fetch_admin_keys_and_db_pass_from_bao_or_local; then
|
||||
echo "ERROR: Failed to fetch/ensure database secrets and admin keys." >&2
|
||||
return 1
|
||||
fi
|
||||
apply_cnpg_admin_secret
|
||||
|
||||
ensure_prole_stack_resources
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
# Add project root to path
|
||||
|
||||
@ -1,84 +0,0 @@
|
||||
# Prole Loading Screen Animation
|
||||
|
||||
An animated GIF loader inspired by 1980s Electronic Arts floppy disk loaders, featuring:
|
||||
- **Crackling energy effects** on the magnetic field lines (fast animation, 8-frame cycle)
|
||||
- **Science mice from Douglas Adams** standing on the rings taking notes (slower animation, 30-frame cycle)
|
||||
- **Alternating animation speeds** to reflect installation progress
|
||||
|
||||
## Generated Files
|
||||
|
||||
- `proleLoading.gif` - The animated loading screen (1024x768, 60 frames)
|
||||
- `generate_loading_gif.py` - Script to generate/regenerate the animation
|
||||
|
||||
## Usage
|
||||
|
||||
### Generate the Animation
|
||||
|
||||
**Default (1024x768, 60 frames, ~53MB):**
|
||||
```bash
|
||||
cd img
|
||||
source ../bin/activate # Activate virtual environment
|
||||
python3 generate_loading_gif.py
|
||||
```
|
||||
|
||||
**Optimized for smaller file size:**
|
||||
```bash
|
||||
# Smaller resolution and fewer frames
|
||||
python3 generate_loading_gif.py --size 800x600 --frames 40 --colors 128 --output proleLoading_small.gif
|
||||
|
||||
# Even smaller
|
||||
python3 generate_loading_gif.py --size 640x480 --frames 30 --colors 64 --output proleLoading_tiny.gif
|
||||
```
|
||||
|
||||
**Command-line options:**
|
||||
- `--size WIDTHxHEIGHT`: Output resolution (default: 1024x768)
|
||||
- `--frames N`: Total animation frames (default: 60)
|
||||
- `--energy-frames N`: Energy animation cycle (default: 8)
|
||||
- `--mice-frames N`: Mice animation cycle (default: 30)
|
||||
- `--colors N`: Color palette size (default: 256, lower = smaller file)
|
||||
- `--output FILENAME`: Output filename (default: proleLoading.gif)
|
||||
|
||||
### Integration with Installer
|
||||
|
||||
The GIF can be displayed in the installer during deployment. The animation runs while `install.py` deploys Prole.app and configures local services.
|
||||
|
||||
## Animation Details
|
||||
|
||||
- **Resolution**: 1024x768
|
||||
- **Total Frames**: 60 (complete cycle)
|
||||
- **Energy Animation**: 8 frames (fast, cycles ~7.5 times per full cycle)
|
||||
- **Mice Animation**: 30 frames (slow, cycles 2 times per full cycle)
|
||||
- **Frame Duration**: 50-80ms (varies with energy intensity)
|
||||
- **Loop**: Infinite
|
||||
|
||||
## Customization
|
||||
|
||||
Edit `generate_loading_gif.py` to adjust:
|
||||
|
||||
- `OUTPUT_SIZE`: Change resolution (default: 1024x768)
|
||||
- `FRAMES_PER_CYCLE`: Total animation frames (default: 60)
|
||||
- `ENERGY_FRAMES`: Speed of energy animation (default: 8)
|
||||
- `MICE_FRAMES`: Speed of mice animation (default: 30)
|
||||
- Colors: Modify `BLUE_DARK`, `BLUE_MEDIUM`, `ENERGY_BRIGHT`, etc.
|
||||
|
||||
## File Size Optimization
|
||||
|
||||
The current GIF is ~53MB. To reduce file size:
|
||||
|
||||
1. **Reduce frames**: Lower `FRAMES_PER_CYCLE` (e.g., 30-40 frames)
|
||||
2. **Reduce resolution**: Lower `OUTPUT_SIZE` (e.g., 800x600)
|
||||
3. **Reduce color palette**: Change quantization from 256 to 128 colors
|
||||
4. **Use external tools**: Use `gifsicle` or `ffmpeg` for further compression
|
||||
|
||||
Example with gifsicle:
|
||||
```bash
|
||||
gifsicle -O3 --colors 128 proleLoading.gif -o proleLoading_optimized.gif
|
||||
```
|
||||
|
||||
## Design Notes
|
||||
|
||||
- The energy effects overlay on the existing field lines from the blueprint logo
|
||||
- Mice are positioned on the rings in an elliptical path matching the logo's perspective
|
||||
- Animation speeds are intentionally mismatched (like old EA loaders) to create visual interest
|
||||
- A "stuck" animation would indicate a failure, just like the old floppy loaders
|
||||
|
||||
@ -1,345 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Generate an animated loading GIF for Prole installer.
|
||||
Inspired by 1980s Electronic Arts floppy disk loaders with alternating
|
||||
animation speeds to reflect installation progress.
|
||||
"""
|
||||
|
||||
import os
|
||||
import argparse
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
import math
|
||||
import random
|
||||
|
||||
# Default Configuration
|
||||
DEFAULT_OUTPUT_SIZE = (1024, 768)
|
||||
DEFAULT_FRAMES = 60
|
||||
DEFAULT_ENERGY_FRAMES = 8
|
||||
DEFAULT_MICE_FRAMES = 30
|
||||
DEFAULT_COLORS = 256
|
||||
DEFAULT_OUTPUT_FILE = "proleLoading.gif"
|
||||
|
||||
# Colors (blueprint theme)
|
||||
BLUE_DARK = (30, 60, 120)
|
||||
BLUE_MEDIUM = (60, 120, 200)
|
||||
BLUE_LIGHT = (100, 150, 220)
|
||||
ENERGY_BRIGHT = (150, 200, 255)
|
||||
ENERGY_CRACKLE = (200, 230, 255)
|
||||
BG_COLOR = (245, 248, 252)
|
||||
|
||||
|
||||
def draw_mouse(draw, x, y, size=15, angle=0, note_taking=False):
|
||||
"""Draw a simple mouse figure (Douglas Adams style)"""
|
||||
# Mouse body (oval)
|
||||
body_width = size
|
||||
body_height = size * 0.7
|
||||
body_bbox = [
|
||||
x - body_width // 2,
|
||||
y - body_height // 2,
|
||||
x + body_width // 2,
|
||||
y + body_height // 2
|
||||
]
|
||||
draw.ellipse(body_bbox, fill=BLUE_DARK, outline=BLUE_MEDIUM, width=1)
|
||||
|
||||
# Mouse head
|
||||
head_size = size * 0.5
|
||||
head_x = x + int(math.cos(angle) * body_width * 0.3)
|
||||
head_y = y - int(math.sin(angle) * body_width * 0.3)
|
||||
head_bbox = [
|
||||
head_x - head_size // 2,
|
||||
head_y - head_size // 2,
|
||||
head_x + head_size // 2,
|
||||
head_y + head_size // 2
|
||||
]
|
||||
draw.ellipse(head_bbox, fill=BLUE_DARK, outline=BLUE_MEDIUM, width=1)
|
||||
|
||||
# Ears
|
||||
ear_size = size * 0.3
|
||||
ear1_x = head_x - head_size * 0.3
|
||||
ear1_y = head_y - head_size * 0.3
|
||||
ear2_x = head_x + head_size * 0.3
|
||||
ear2_y = head_y - head_size * 0.3
|
||||
draw.ellipse([ear1_x - ear_size//2, ear1_y - ear_size//2,
|
||||
ear1_x + ear_size//2, ear1_y + ear_size//2],
|
||||
fill=BLUE_MEDIUM, outline=BLUE_DARK, width=1)
|
||||
draw.ellipse([ear2_x - ear_size//2, ear2_y - ear_size//2,
|
||||
ear2_x + ear_size//2, ear2_y + ear_size//2],
|
||||
fill=BLUE_MEDIUM, outline=BLUE_DARK, width=1)
|
||||
|
||||
# Tail
|
||||
tail_points = [
|
||||
(x - body_width * 0.4, y),
|
||||
(x - body_width * 0.6, y + size * 0.3),
|
||||
(x - body_width * 0.8, y + size * 0.1)
|
||||
]
|
||||
draw.line(tail_points, fill=BLUE_DARK, width=2)
|
||||
|
||||
# If taking notes, draw a clipboard/notepad
|
||||
if note_taking:
|
||||
clipboard_x = x + body_width * 0.4
|
||||
clipboard_y = y - size * 0.2
|
||||
clipboard_size = size * 0.6
|
||||
# Clipboard
|
||||
draw.rectangle(
|
||||
[clipboard_x - clipboard_size//2, clipboard_y - clipboard_size//2,
|
||||
clipboard_x + clipboard_size//2, clipboard_y + clipboard_size//2],
|
||||
fill=(250, 250, 245), outline=BLUE_DARK, width=1
|
||||
)
|
||||
# Lines on clipboard
|
||||
for i in range(3):
|
||||
line_y = clipboard_y - clipboard_size//3 + i * (clipboard_size//3)
|
||||
draw.line(
|
||||
[clipboard_x - clipboard_size//3, line_y,
|
||||
clipboard_x + clipboard_size//3, line_y],
|
||||
fill=BLUE_MEDIUM, width=1
|
||||
)
|
||||
# Pencil/pen
|
||||
pencil_x = clipboard_x + clipboard_size * 0.3
|
||||
pencil_y = clipboard_y
|
||||
draw.line(
|
||||
[pencil_x, pencil_y - clipboard_size//2,
|
||||
pencil_x, pencil_y + clipboard_size//2],
|
||||
fill=BLUE_DARK, width=2
|
||||
)
|
||||
|
||||
|
||||
def draw_energy_crackle(draw, start_x, start_y, end_x, end_y, intensity, energy_frame, seed=None):
|
||||
"""Draw crackling energy along a field line"""
|
||||
if seed is not None:
|
||||
random.seed(seed + energy_frame) # Consistent randomness per field line
|
||||
|
||||
# Calculate points along the curve (field line) - curved path
|
||||
num_points = 25
|
||||
points = []
|
||||
for i in range(num_points + 1):
|
||||
t = i / num_points
|
||||
# Create a curved path (field line) - more pronounced curve
|
||||
curve_amount = 40 * math.sin(t * math.pi)
|
||||
x = (1 - t) * start_x + t * end_x
|
||||
y = (1 - t) * start_y + t * end_y + curve_amount
|
||||
points.append((x, y))
|
||||
|
||||
# Draw crackling energy along the line
|
||||
crackle_intensity = intensity * (0.4 + 0.6 * abs(math.sin(energy_frame * math.pi * 2 / DEFAULT_ENERGY_FRAMES)))
|
||||
|
||||
# Draw main energy path with varying intensity
|
||||
for i in range(len(points) - 1):
|
||||
p1 = points[i]
|
||||
p2 = points[i + 1]
|
||||
|
||||
# Vary line width based on intensity
|
||||
line_width = max(1, int(1 + crackle_intensity * 2))
|
||||
energy_color = tuple(int(c * (0.7 + 0.3 * crackle_intensity)) for c in ENERGY_BRIGHT)
|
||||
|
||||
# Main energy line
|
||||
draw.line([p1, p2], fill=energy_color, width=line_width)
|
||||
|
||||
# Add crackling branches (more frequent when intensity is high)
|
||||
if random.random() < crackle_intensity * 0.6:
|
||||
branch_length = 4 + random.random() * 12
|
||||
branch_angle = random.random() * math.pi * 2
|
||||
branch_start_x = (p1[0] + p2[0]) / 2
|
||||
branch_start_y = (p1[1] + p2[1]) / 2
|
||||
branch_end_x = branch_start_x + math.cos(branch_angle) * branch_length
|
||||
branch_end_y = branch_start_y + math.sin(branch_angle) * branch_length
|
||||
draw.line(
|
||||
[branch_start_x, branch_start_y,
|
||||
branch_end_x, branch_end_y],
|
||||
fill=ENERGY_CRACKLE, width=1
|
||||
)
|
||||
# Small spark at end
|
||||
spark_size = 1 + int(random.random() * 2)
|
||||
draw.ellipse(
|
||||
[branch_end_x - spark_size, branch_end_y - spark_size,
|
||||
branch_end_x + spark_size, branch_end_y + spark_size],
|
||||
fill=ENERGY_CRACKLE
|
||||
)
|
||||
|
||||
|
||||
def generate_frame(base_image, frame_num, energy_frame, mice_frame, mice_frames=30):
|
||||
"""Generate a single animation frame"""
|
||||
# Create a copy of the base image
|
||||
frame = base_image.copy()
|
||||
draw = ImageDraw.Draw(frame)
|
||||
|
||||
# Get image dimensions
|
||||
width, height = frame.size
|
||||
center_x, center_y = width // 2, height // 2
|
||||
|
||||
# Draw energy crackling on field lines
|
||||
# Top field lines (curved lines from top pole) - matching logo structure
|
||||
top_pole_x = center_x
|
||||
top_pole_y = center_y - 180 # Approximate top pole position
|
||||
|
||||
# Create several field lines from top pole (curved outward)
|
||||
num_top_lines = 7
|
||||
for i in range(num_top_lines):
|
||||
angle = (i - num_top_lines // 2) * 0.35 # Spread out symmetrically
|
||||
line_length = 140 + (i % 2) * 20 # Vary length
|
||||
end_x = center_x + math.cos(angle) * line_length
|
||||
end_y = top_pole_y + 80 + math.sin(angle) * 40
|
||||
intensity = 0.4 + (i % 3) * 0.2 # Vary intensity
|
||||
draw_energy_crackle(draw, top_pole_x, top_pole_y, end_x, end_y,
|
||||
intensity, energy_frame, seed=i)
|
||||
|
||||
# Bottom field lines
|
||||
bottom_pole_x = center_x
|
||||
bottom_pole_y = center_y + 180 # Approximate bottom pole position
|
||||
|
||||
num_bottom_lines = 7
|
||||
for i in range(num_bottom_lines):
|
||||
angle = (i - num_bottom_lines // 2) * 0.35
|
||||
line_length = 140 + (i % 2) * 20
|
||||
end_x = center_x + math.cos(angle) * line_length
|
||||
end_y = bottom_pole_y - 80 - math.sin(angle) * 40
|
||||
intensity = 0.4 + (i % 3) * 0.2
|
||||
draw_energy_crackle(draw, bottom_pole_x, bottom_pole_y, end_x, end_y,
|
||||
intensity, energy_frame, seed=i + 100)
|
||||
|
||||
# Draw science mice on the rings
|
||||
# Ring positions (elliptical, perspective)
|
||||
ring_center_y = center_y
|
||||
ring_radius_x = 180
|
||||
ring_radius_y = 50
|
||||
|
||||
# Place 3-4 mice around the rings
|
||||
num_mice = 4
|
||||
for i in range(num_mice):
|
||||
# Position along ring (ellipse)
|
||||
t = (i / num_mice + mice_frame / mice_frames) * 2 * math.pi
|
||||
mouse_x = center_x + ring_radius_x * math.cos(t)
|
||||
mouse_y = ring_center_y + ring_radius_y * math.sin(t)
|
||||
|
||||
# Mouse angle (facing outward from center)
|
||||
mouse_angle = t + math.pi / 2
|
||||
|
||||
# Alternate between taking notes and observing
|
||||
note_taking = (i + mice_frame // 10) % 2 == 0
|
||||
|
||||
draw_mouse(draw, int(mouse_x), int(mouse_y), size=18,
|
||||
angle=mouse_angle, note_taking=note_taking)
|
||||
|
||||
return frame
|
||||
|
||||
|
||||
def main():
|
||||
"""Generate the animated loading GIF"""
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Generate animated loading GIF for Prole installer"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--size", type=str, default="1024x768",
|
||||
help="Output size as WIDTHxHEIGHT (default: 1024x768)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--frames", type=int, default=DEFAULT_FRAMES,
|
||||
help=f"Total animation frames (default: {DEFAULT_FRAMES})"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--energy-frames", type=int, default=DEFAULT_ENERGY_FRAMES,
|
||||
help=f"Energy animation cycle length (default: {DEFAULT_ENERGY_FRAMES})"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--mice-frames", type=int, default=DEFAULT_MICE_FRAMES,
|
||||
help=f"Mice animation cycle length (default: {DEFAULT_MICE_FRAMES})"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--colors", type=int, default=DEFAULT_COLORS,
|
||||
help=f"Color palette size for optimization (default: {DEFAULT_COLORS})"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output", type=str, default=DEFAULT_OUTPUT_FILE,
|
||||
help=f"Output filename (default: {DEFAULT_OUTPUT_FILE})"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--optimize", action="store_true", default=True,
|
||||
help="Enable GIF optimization (default: True)"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Parse size
|
||||
try:
|
||||
width, height = map(int, args.size.split('x'))
|
||||
OUTPUT_SIZE = (width, height)
|
||||
except ValueError:
|
||||
print(f"Error: Invalid size format '{args.size}'. Use WIDTHxHEIGHT (e.g., 1024x768)")
|
||||
return
|
||||
|
||||
FRAMES_PER_CYCLE = args.frames
|
||||
ENERGY_FRAMES = args.energy_frames | DEFAULT_ENERGY_FRAMES
|
||||
MICE_FRAMES = args.mice_frames
|
||||
NUM_COLORS = args.colors
|
||||
OUTPUT_FILE = args.output
|
||||
|
||||
print("Loading base logo image...")
|
||||
base_path = os.path.join(os.path.dirname(__file__), "proleLogoBlueprint.png")
|
||||
|
||||
if not os.path.exists(base_path):
|
||||
print(f"Error: Base image not found at {base_path}")
|
||||
return
|
||||
|
||||
# Load and resize base image
|
||||
base_image = Image.open(base_path)
|
||||
base_image = base_image.convert("RGB") # Ensure RGB mode
|
||||
base_image = base_image.resize(OUTPUT_SIZE, Image.Resampling.LANCZOS)
|
||||
|
||||
print(f"Generating {FRAMES_PER_CYCLE} animation frames...")
|
||||
frames = []
|
||||
|
||||
for frame_num in range(FRAMES_PER_CYCLE):
|
||||
# Calculate sub-frame indices for different animation speeds
|
||||
energy_frame = frame_num % ENERGY_FRAMES # Fast energy animation
|
||||
mice_frame = frame_num % MICE_FRAMES # Slower mice animation
|
||||
|
||||
frame = generate_frame(base_image, frame_num, energy_frame, mice_frame, MICE_FRAMES)
|
||||
frames.append(frame)
|
||||
|
||||
if (frame_num + 1) % 10 == 0:
|
||||
print(f" Generated {frame_num + 1}/{FRAMES_PER_CYCLE} frames...")
|
||||
|
||||
print("Saving animated GIF...")
|
||||
# Save as animated GIF
|
||||
# Use different durations: faster for energy-heavy frames, slower for others
|
||||
durations = []
|
||||
for i in range(FRAMES_PER_CYCLE):
|
||||
# Faster frames when energy is more intense
|
||||
energy_intensity = abs(math.sin(i * math.pi * 2 / ENERGY_FRAMES))
|
||||
duration = 50 + int(energy_intensity * 30) # 50-80ms per frame
|
||||
durations.append(duration)
|
||||
|
||||
output_path = os.path.join(os.path.dirname(__file__), OUTPUT_FILE)
|
||||
|
||||
# Optimize: quantize to reduce colors and file size
|
||||
if args.optimize:
|
||||
print(f"Optimizing GIF (quantizing to {NUM_COLORS} colors)...")
|
||||
quantized_frames = []
|
||||
for frame in frames:
|
||||
# Quantize to reduce colors for better compression
|
||||
quantized = frame.quantize(colors=NUM_COLORS, method=Image.Quantize.MEDIANCUT)
|
||||
quantized_frames.append(quantized.convert("P"))
|
||||
frames_to_save = quantized_frames
|
||||
else:
|
||||
frames_to_save = frames
|
||||
|
||||
frames_to_save[0].save(
|
||||
output_path,
|
||||
save_all=True,
|
||||
append_images=frames_to_save[1:],
|
||||
duration=durations,
|
||||
loop=0, # Infinite loop
|
||||
optimize=args.optimize # Enable optimization
|
||||
)
|
||||
|
||||
file_size = os.path.getsize(output_path) / (1024 * 1024) # Size in MB
|
||||
print(f"✓ Animation saved to {output_path}")
|
||||
print(f" Size: {OUTPUT_SIZE[0]}x{OUTPUT_SIZE[1]}")
|
||||
print(f" Frames: {FRAMES_PER_CYCLE}")
|
||||
print(f" Energy cycle: {ENERGY_FRAMES} frames (fast)")
|
||||
print(f" Mice cycle: {MICE_FRAMES} frames (slow)")
|
||||
print(f" File size: {file_size:.1f} MB")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@ -4,22 +4,30 @@ Installer actions and unattended workflow helpers.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import configparser
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
import secrets
|
||||
import shlex
|
||||
import socket
|
||||
import string
|
||||
import threading
|
||||
import uuid
|
||||
|
||||
from installer import config as inst_config
|
||||
from installer.config import _expand_path, _collect_cfg_vars, _expand_cfg_value, _parse_bool, _is_openbao_ref, \
|
||||
_is_prole_secret, _decrypt_prole_secret, _encrypt_prole_secret, _write_k3s_kubeconfig, _encrypt_cfg_secret
|
||||
from installer.core.controller import ProleController
|
||||
from installer.core.env import * # noqa: F401,F403
|
||||
from installer.core.env import _parse_ollama_host, _bool_str, _deployment_mode_from_env, _openbao_placeholder, \
|
||||
_looks_like_k8s_bearer_token, _find_kubeconfig_file, _normalize_cluster_env, _pf_upsert_mapping, \
|
||||
_build_required_port_forwards, _local_registry_enabled, _collect_images_from_files, _http_ping_registry, \
|
||||
_resolve_supabase_home, _push_docker_image, _detect_ansible_topology, _deployment_target_label, \
|
||||
_default_opentofu_pipeline_url, _format_ollama_host, _render_prole_cfg, _k3d_prole_data_volume_args, \
|
||||
_sync_opentofu_pipeline
|
||||
from installer.core.milestones import (
|
||||
DependenciesMilestone, NetworkScanMilestone, EnvSetupMilestone,
|
||||
SecretManagementMilestone, DatabaseCreationMilestone, DockerBuildMilestone,
|
||||
ClusterLifecycleMilestone, InitializationScriptsMilestone, DeploymentMilestone
|
||||
)
|
||||
from installer.core.env import * # noqa: F401,F403
|
||||
|
||||
|
||||
def _configure_unbuffered_io():
|
||||
os.environ.setdefault('PYTHONUNBUFFERED', '1')
|
||||
@ -1691,11 +1699,18 @@ class ProleSilentInstaller:
|
||||
raise Exception("Invalid database namespace.")
|
||||
user = (self._get_input('init_password.db_username', '') or '').strip()
|
||||
if not user:
|
||||
raise Exception("Database owner cannot be empty.")
|
||||
import getpass
|
||||
try:
|
||||
user = getpass.getuser()
|
||||
except Exception:
|
||||
user = "prole"
|
||||
self.log(f"[INFO] No database owner provided; using default: {user}")
|
||||
p1 = self._get_input('init_password.db_password', '')
|
||||
p2 = self._get_input('init_password.db_password_confirm', '') or p1
|
||||
if not p1:
|
||||
raise Exception("Database password cannot be empty.")
|
||||
p1 = self._generate_db_password()
|
||||
p2 = p1
|
||||
self.log("[INFO] No database password provided; generated default password.")
|
||||
if p1 != p2:
|
||||
raise Exception("Database passwords do not match.")
|
||||
|
||||
@ -2213,7 +2228,22 @@ class ProleSilentInstaller:
|
||||
self.err(f"[ERROR] {script} failed (code {rc})")
|
||||
overall_success = False
|
||||
self._scripts_success = overall_success
|
||||
self.prole_cfg_data['Initialization Scripts']['STATUS'] = 'Completed' if overall_success else 'Attempted'
|
||||
|
||||
# Verify critical secrets
|
||||
self.log(f"==> Verifying critical secrets in namespace {ns}")
|
||||
critical_secrets = ["prole-db-user", "prole-db-superuser", "cnpg-admin-key"]
|
||||
missing_secrets = []
|
||||
for secret in critical_secrets:
|
||||
rc_s = self._run_cmd(["kubectl", "get", "secret", secret, "-n", ns])
|
||||
if rc_s != 0:
|
||||
missing_secrets.append(secret)
|
||||
|
||||
if missing_secrets:
|
||||
self.err(f"[CRITICAL] Missing secrets in namespace '{ns}': {', '.join(missing_secrets)}")
|
||||
self.err("Database initialization will fail without these secrets.")
|
||||
self._scripts_success = False
|
||||
|
||||
self.prole_cfg_data['Initialization Scripts']['STATUS'] = 'Completed' if self._scripts_success else 'Attempted'
|
||||
|
||||
def _step_cnpg_deploy(self) -> None:
|
||||
if not self._get_input_bool('init_cnpg_deploy.run_deploy', DEFAULT_ACTION_FLAGS.get('init_cnpg_deploy.run_deploy', True)):
|
||||
|
||||
@ -7,7 +7,6 @@ import logging
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any, Callable, Sequence
|
||||
|
||||
from installer import config as inst_config
|
||||
from installer.state import InstallerState
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
@ -8,15 +8,10 @@ import base64
|
||||
import configparser
|
||||
import getpass
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
import secrets
|
||||
import shlex
|
||||
import shutil
|
||||
import socket
|
||||
import string
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
@ -24,8 +19,6 @@ import time
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
||||
@ -627,11 +620,11 @@ def _normalize_cluster_env(env: str | None) -> str:
|
||||
if not env:
|
||||
return ''
|
||||
s = str(env).strip().lower()
|
||||
if s in ('dev', 'k3d', 'k3d-dev', 'k3d-prole-dev-cluster', 'prole-dev-cluster'):
|
||||
if s in ('dev', 'k3d', 'k3d-dev', 'k3d-prole-dev-cluster', 'prole-dev-cluster') or s.startswith('k3d-') or s.startswith('prole-dev-'):
|
||||
return 'dev'
|
||||
if s in ('service', 'k3s', 'k3s-service', 'prole-service-cluster'):
|
||||
if s in ('service', 'k3s', 'k3s-service', 'prole-service-cluster') or s.startswith('prole-service-'):
|
||||
return 'service'
|
||||
if s in ('prod', 'production', 'k8s', 'prole-prod-cluster'):
|
||||
if s in ('prod', 'production', 'k8s', 'prole-prod-cluster') or s.startswith('prole-prod-'):
|
||||
return 'prod'
|
||||
return s
|
||||
|
||||
@ -639,11 +632,11 @@ def _normalize_cluster_env(env: str | None) -> str:
|
||||
def _cluster_env_radio_value(env: str | None) -> str:
|
||||
key = _normalize_cluster_env(env)
|
||||
if key == 'dev':
|
||||
return 'k3d-prole-dev-cluster'
|
||||
return 'dev'
|
||||
if key == 'service':
|
||||
return 'prole-service-cluster'
|
||||
return 'service'
|
||||
if key == 'prod':
|
||||
return 'prole-prod-cluster'
|
||||
return 'prod'
|
||||
return (env or '')
|
||||
|
||||
def _deployment_target_label(env: str | None) -> str:
|
||||
|
||||
@ -238,6 +238,23 @@ class DatabaseCreationMilestone(Milestone):
|
||||
user = getpass.getuser()
|
||||
state.inputs['init_password.db_username'] = user
|
||||
|
||||
if not p1:
|
||||
import secrets
|
||||
import string
|
||||
alphabet = string.ascii_letters + string.digits
|
||||
p1 = ''.join(secrets.choice(alphabet) for i in range(24))
|
||||
state.inputs['init_password.db_password'] = p1
|
||||
state.inputs['init_password.db_password_confirm'] = p1
|
||||
self.logger.info("Generated default database password.")
|
||||
|
||||
if p1 and not p1.startswith('${') and not inst_config._is_prole_secret(p1):
|
||||
if 'Inputs' not in state.config_data:
|
||||
state.config_data['Inputs'] = {}
|
||||
# Encrypt and save to config data for persistence in prole.cfg
|
||||
encrypted_pw = inst_config._encrypt_cfg_secret(p1)
|
||||
state.config_data['Inputs']['init_password.db_password'] = encrypted_pw
|
||||
state.config_data['Inputs']['init_password.db_password_confirm'] = encrypted_pw
|
||||
|
||||
state.inputs['init_password.db_namespace'] = ns
|
||||
state.inputs['env_setup.NAMESPACE'] = ns
|
||||
|
||||
@ -444,8 +461,25 @@ class InitializationScriptsMilestone(Milestone):
|
||||
|
||||
rc = state.controller.run_script(script, args=args, env=env, on_line=_stream_line)
|
||||
if rc != 0:
|
||||
self.logger.error(f"Script {script} failed (code {rc})")
|
||||
# Should we stop? Silent installer continues but logs error.
|
||||
msg = f"Script {script} failed (code {rc})"
|
||||
self.logger.error(msg)
|
||||
raise Exception(msg)
|
||||
|
||||
# Verify critical secrets
|
||||
ns = env.get("NAMESPACE", "default")
|
||||
self.logger.info(f"Verifying critical secrets in namespace {ns}...")
|
||||
critical_secrets = ["prole-db-user", "prole-db-superuser", "cnpg-admin-key"]
|
||||
missing_secrets = []
|
||||
for secret in critical_secrets:
|
||||
if self._run_cmd(f"kubectl get secret {secret} -n {ns}") != 0:
|
||||
missing_secrets.append(secret)
|
||||
|
||||
if missing_secrets:
|
||||
msg = f"CRITICAL: Missing secrets in namespace '{ns}': {', '.join(missing_secrets)}. Database initialization will fail."
|
||||
self.logger.error(msg)
|
||||
if progress:
|
||||
progress(msg, 1.0)
|
||||
raise Exception(msg)
|
||||
|
||||
# For k3d: start port-forward daemon and verify Grafana port-forward.
|
||||
pf_ok = True
|
||||
@ -522,7 +556,9 @@ class DeploymentMilestone(Milestone):
|
||||
if rc == 0:
|
||||
self.logger.info("Deployment successful")
|
||||
else:
|
||||
self.logger.error(f"Deployment failed (code {rc})")
|
||||
msg = f"Deployment failed (code {rc})"
|
||||
self.logger.error(msg)
|
||||
raise Exception(msg)
|
||||
|
||||
if progress:
|
||||
progress("Deployment complete", 1.0)
|
||||
|
||||
@ -9,17 +9,16 @@ Also defines the Deploy page (final milestone):
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
import os
|
||||
import threading
|
||||
import platform
|
||||
import subprocess
|
||||
import shutil
|
||||
import subprocess
|
||||
import threading
|
||||
from pathlib import Path
|
||||
from typing import Callable
|
||||
|
||||
from . import config as cfg
|
||||
from .milestone import Milestone
|
||||
from .state import InstallerState
|
||||
from . import config as cfg
|
||||
|
||||
|
||||
def check_xcode_tools() -> bool:
|
||||
|
||||
@ -6,10 +6,6 @@ to feature modules. The legacy install.py will import and invoke run().
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, scrolledtext
|
||||
|
||||
from . import screen as ui
|
||||
from . import build as build_mod
|
||||
from . import deploy as deploy_mod
|
||||
|
||||
|
||||
@ -2,10 +2,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Callable, Iterable, Sequence
|
||||
from typing import Callable, Sequence
|
||||
|
||||
from .state import InstallerState
|
||||
from . import config as inst_config
|
||||
from .state import InstallerState
|
||||
|
||||
ProgressCallback = Callable[[str, float | None], None]
|
||||
ValidationResult = str | Sequence[str] | None
|
||||
|
||||
@ -7,18 +7,15 @@ Maintains the same screen order and operations.
|
||||
from __future__ import annotations
|
||||
|
||||
import curses
|
||||
import sys
|
||||
import threading
|
||||
import getpass
|
||||
import os
|
||||
import subprocess
|
||||
import getpass
|
||||
import json
|
||||
import tempfile
|
||||
import sys
|
||||
import threading
|
||||
from pathlib import Path
|
||||
from typing import Optional, Callable
|
||||
|
||||
from . import config as inst_config
|
||||
from .ncurses_ui import CursesWindow, TerminalConsole, NavFooter, InputField, Checkbox
|
||||
from .ncurses_ui import CursesWindow, NavFooter, InputField
|
||||
|
||||
|
||||
class ProleNcursesInstaller:
|
||||
|
||||
@ -7,7 +7,6 @@ Tk Canvas used by the installer, to reduce clutter in install.py.
|
||||
from __future__ import annotations
|
||||
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
import platform
|
||||
|
||||
|
||||
|
||||
@ -4,47 +4,33 @@ Prole Service Dependencies Installer
|
||||
Desktop application for installing, deploying, and validating Prole services
|
||||
"""
|
||||
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, scrolledtext, messagebox, filedialog
|
||||
import subprocess
|
||||
import threading
|
||||
import os
|
||||
import sys
|
||||
import webbrowser
|
||||
import time
|
||||
import platform
|
||||
from pathlib import Path
|
||||
import shlex
|
||||
import socket
|
||||
import signal
|
||||
import shutil
|
||||
import json
|
||||
import configparser
|
||||
import tempfile
|
||||
import getpass
|
||||
import uuid
|
||||
import re
|
||||
import base64
|
||||
import secrets
|
||||
import string
|
||||
from datetime import datetime
|
||||
import urllib.request
|
||||
import urllib.parse
|
||||
import socket
|
||||
import threading
|
||||
import tkinter as tk
|
||||
import urllib.error
|
||||
import logging
|
||||
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
import uuid
|
||||
import webbrowser
|
||||
from datetime import datetime
|
||||
from tkinter import ttk, messagebox, filedialog
|
||||
|
||||
# Refactor: import shared helpers from the root-level installer package
|
||||
from installer import config as inst_config
|
||||
from installer.build import get_build_command as inst_get_build_command
|
||||
from installer import deploy as inst_deploy
|
||||
from installer import screen as ui
|
||||
# Refactor: import shared helpers from the root-level installer package
|
||||
from installer.build import get_build_command as inst_get_build_command
|
||||
from installer.config import _expand_cfg_value, _collect_cfg_vars, _is_prole_secret, _decrypt_prole_secret, \
|
||||
_is_openbao_ref, _write_k3s_kubeconfig, _encrypt_cfg_secret, _encrypt_prole_secret
|
||||
from installer.core.controller import ProleController
|
||||
from installer.core.milestones import (
|
||||
DependenciesMilestone, NetworkScanMilestone, EnvSetupMilestone,
|
||||
SecretManagementMilestone, DatabaseCreationMilestone, DockerBuildMilestone,
|
||||
ClusterLifecycleMilestone, InitializationScriptsMilestone, DeploymentMilestone
|
||||
)
|
||||
from installer.core.env import _detect_ansible_topology, _format_ansible_topology_summary, _parse_ollama_host, \
|
||||
_normalize_k3s_token, _deployment_target_label, _cluster_env_radio_value, _find_kubeconfig_file, _bool_str, \
|
||||
_looks_like_k8s_bearer_token, _deployment_mode_from_env, _pf_extract_id, _ensure_ansible_vault_credentials, \
|
||||
_openbao_placeholder, _pf_upsert_mapping, _format_ollama_host, _default_opentofu_pipeline_url, \
|
||||
_build_required_port_forwards, _render_prole_cfg, _normalize_cluster_env, _k3d_prole_data_volume_args, \
|
||||
_push_docker_image, _http_ping_registry, _local_registry_enabled, _collect_images_from_files
|
||||
|
||||
# On macOS, set the process name as early as possible so the menu bar shows 'Prole Database Installer'
|
||||
if platform.system() == 'Darwin':
|
||||
@ -55,7 +41,7 @@ if platform.system() == 'Darwin':
|
||||
pass
|
||||
|
||||
from installer.core.env import * # noqa: F401,F403
|
||||
from installer.core.actions import ProleSilentInstaller, _prepare_k3s_pipeline
|
||||
from installer.core.actions import ProleSilentInstaller, _prepare_k3s_pipeline, _reset_k3s_namespace
|
||||
|
||||
# Initialize global frames list to keep them in memory
|
||||
GLOBAL_SCAN_FRAMES = []
|
||||
@ -329,7 +315,19 @@ class ProleInstaller:
|
||||
self.page_index = 0
|
||||
|
||||
# Initialize variables for new Initialize screens
|
||||
self.cluster_env = tk.StringVar(value='k3d-prole-dev-cluster')
|
||||
self.cluster_env = tk.StringVar(value='dev')
|
||||
self.k3d_cluster_name = tk.StringVar(value='prole-dev-cluster')
|
||||
self.k3d_cluster_list = tk.Variable(value=[])
|
||||
self.selected_k3d_cluster = tk.StringVar()
|
||||
|
||||
# Pre-populate selected_k3d_cluster from prole.cfg if applicable
|
||||
try:
|
||||
cfg_env = (self.prole_cfg_data.get('Initialize Cluster', {}) or {}).get('ENVIRONMENT', '')
|
||||
if _normalize_cluster_env(cfg_env) == 'dev':
|
||||
self.selected_k3d_cluster.set(cfg_env)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
self.deploy_target = tk.StringVar(value=_deployment_target_label(self.cluster_env.get()))
|
||||
self._syncing_deploy_target = False
|
||||
self._deploy_target_trace = self.deploy_target.trace_add('write', self._on_deploy_target_change)
|
||||
@ -791,7 +789,7 @@ class ProleInstaller:
|
||||
current = (self.cluster_env.get() or '').strip()
|
||||
except Exception:
|
||||
current = ''
|
||||
if not current or current == 'k3d-prole-dev-cluster':
|
||||
if not current or current == 'dev':
|
||||
try:
|
||||
self.cluster_env.set(_cluster_env_radio_value(cfg_env))
|
||||
except Exception:
|
||||
@ -1664,7 +1662,23 @@ class ProleInstaller:
|
||||
env["KRB5_USER"] = self.kerberos_user.get().strip()
|
||||
if self.kerberos_password.get().strip():
|
||||
env["KRB5_PASSWORD"] = self.kerberos_password.get().strip()
|
||||
if self._cluster_env_key() == 'service':
|
||||
if self._cluster_env_key() == 'dev':
|
||||
# For k3d dev clusters, use the default kubeconfig (~/.kube/config)
|
||||
if not (env.get("KUBECONFIG") or "").strip():
|
||||
cluster_name = (self.selected_k3d_cluster.get() or '').strip()
|
||||
if cluster_name:
|
||||
# Ensure k3d kubeconfig is merged and context is switched
|
||||
try:
|
||||
subprocess.run(
|
||||
['k3d', 'kubeconfig', 'merge', cluster_name, '--kubeconfig-switch-context'],
|
||||
capture_output=True, text=True, timeout=10
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
default_kube = str(Path.home() / '.kube' / 'config')
|
||||
if Path(default_kube).exists():
|
||||
env["KUBECONFIG"] = default_kube
|
||||
elif self._cluster_env_key() == 'service':
|
||||
raw_server, raw_token = self._k3s_connection_raw()
|
||||
if raw_server:
|
||||
env["PROLE_K3S_SERVER"] = raw_server
|
||||
@ -2151,9 +2165,9 @@ class ProleInstaller:
|
||||
_set_bool('init_db_build.run_build', _action('init_db_build.run_build', DEFAULT_ACTION_FLAGS.get('init_db_build.run_build', True)))
|
||||
|
||||
# Cluster init + optional features
|
||||
_set('init_cluster.cluster_env', _get_var(self.cluster_env, 'k3d-prole-dev-cluster'))
|
||||
_set('init_cluster.mode', _deployment_mode_from_env(_get_var(self.cluster_env, 'k3d-prole-dev-cluster')))
|
||||
_set('init_cluster.deployment_target', _deployment_target_label(_get_var(self.cluster_env, 'k3d-prole-dev-cluster')))
|
||||
_set('init_cluster.cluster_env', _get_var(self.cluster_env, 'dev'))
|
||||
_set('init_cluster.mode', _deployment_mode_from_env(_get_var(self.cluster_env, 'dev')))
|
||||
_set('init_cluster.deployment_target', _deployment_target_label(_get_var(self.cluster_env, 'dev')))
|
||||
_set('init_cluster.k3s_server_url', _get_var(self.k3s_server_url, ''))
|
||||
_set('init_cluster.k3s_token', _encrypt_cfg_secret(_get_var(self.k3s_token, '')))
|
||||
_set_bool('init_cluster.supabase_enabled', _get_var(self.supabase_enabled, False))
|
||||
@ -2685,7 +2699,7 @@ class ProleInstaller:
|
||||
sections['Dev Cluster (k3d)'] = {
|
||||
**self.prole_cfg_data.get('Dev Cluster (k3d)', {}),
|
||||
'MODE': 'k3d',
|
||||
'CLUSTER_ENV': 'k3d-prole-dev-cluster',
|
||||
'CLUSTER_ENV': 'dev',
|
||||
'DISPLAY_NAME': 'prole-dev-cluster',
|
||||
'KUBECTL_CONTEXT': (self.selected_kubectx.get() or '').strip()
|
||||
}
|
||||
@ -4438,11 +4452,11 @@ class ProleInstaller:
|
||||
target = ''
|
||||
key = _normalize_cluster_env(target)
|
||||
if key == 'dev':
|
||||
desired = 'k3d-prole-dev-cluster'
|
||||
desired = 'dev'
|
||||
elif key == 'service':
|
||||
desired = 'prole-service-cluster'
|
||||
desired = 'service'
|
||||
elif key == 'prod':
|
||||
desired = 'prole-prod-cluster'
|
||||
desired = 'prod'
|
||||
else:
|
||||
return
|
||||
self._syncing_deploy_target = True
|
||||
@ -4467,13 +4481,13 @@ class ProleInstaller:
|
||||
# Cluster Selection (Radio Buttons)
|
||||
x_label = 48
|
||||
y = 280
|
||||
self._canvas_items.append(ui.canvas_text(self, x_label, y, 'Select Cluster Name:', fill='black', font=('SF Pro Text', 14, 'bold')))
|
||||
self._canvas_items.append(ui.canvas_text(self, x_label, y, 'Select Environment:', fill='black', font=('SF Pro Text', 14, 'bold')))
|
||||
|
||||
y += 28
|
||||
cluster_options = [
|
||||
('k3d-prole-dev-cluster', 'prole-dev-cluster'),
|
||||
('prole-service-cluster', 'prole-service-cluster'),
|
||||
('prole-prod-cluster', 'prole-prod-cluster')
|
||||
('dev', 'Dev'),
|
||||
('service', 'Service'),
|
||||
('prod', 'Prod')
|
||||
]
|
||||
|
||||
# We need to trace cluster_env if not already traced
|
||||
@ -4481,7 +4495,7 @@ class ProleInstaller:
|
||||
self._cluster_env_trace = self.cluster_env.trace_add('write', self._on_cluster_env_change)
|
||||
|
||||
rb_x = x_label + 20
|
||||
rb_gap = 240
|
||||
rb_gap = 150
|
||||
for idx, (val, name) in enumerate(cluster_options):
|
||||
rb = tk.Radiobutton(self.bg_canvas, text=name, variable=self.cluster_env, value=val,
|
||||
bg='white', fg='black', activebackground='white', selectcolor='white',
|
||||
@ -4489,54 +4503,53 @@ class ProleInstaller:
|
||||
rb_window = self.bg_canvas.create_window(rb_x + idx * rb_gap, y, window=rb, anchor='nw')
|
||||
self._canvas_items.append(rb_window)
|
||||
self._overlay_widgets.append(rb)
|
||||
y += 30
|
||||
|
||||
save_x = content_width - 140
|
||||
save_y = y - 4
|
||||
btn = tk.Button(self.bg_canvas, text='Save', command=self._on_save_cluster_config,
|
||||
bg='#F5F5DC', fg='black', activebackground='#E5E5D5', activeforeground='black',
|
||||
highlightbackground='#F5F5DC', highlightthickness=0,
|
||||
relief='flat', font=('SF Pro Text', 11), padx=10, pady=2)
|
||||
self._init_cluster_button = btn
|
||||
btn_window = self.bg_canvas.create_window(save_x, save_y, window=btn, anchor='nw')
|
||||
self._canvas_items.append(btn_window)
|
||||
self._overlay_widgets.append(btn)
|
||||
|
||||
selected_env = self.cluster_env.get()
|
||||
selected_env_key = self._cluster_env_key(selected_env)
|
||||
y += 16
|
||||
selected_env_key = self._cluster_env_key()
|
||||
y += 44
|
||||
|
||||
if selected_env_key == 'dev':
|
||||
# kubectx dropdown
|
||||
self._canvas_items.append(ui.canvas_text(self, x_label, y, 'Available kubectx contexts:', fill='black', font=('SF Pro Text', 12, 'bold')))
|
||||
y += 24
|
||||
# Combined Select / Add Cluster widget
|
||||
self._canvas_items.append(ui.canvas_text(self, x_label, y, 'Cluster:', fill='black', font=('SF Pro Text', 12, 'bold')))
|
||||
y += 28
|
||||
|
||||
values = self._get_kubectx_list()
|
||||
combo = ttk.Combobox(self.bg_canvas, textvariable=self.selected_kubectx, values=values, state='readonly', width=40)
|
||||
# Preselect from prole.cfg (cluster env) if available and present in contexts
|
||||
try:
|
||||
desired_ctx = (self.cluster_env.get() or '').strip()
|
||||
if desired_ctx and desired_ctx in values:
|
||||
self.selected_kubectx.set(desired_ctx)
|
||||
elif not self.selected_kubectx.get() and values:
|
||||
self.selected_kubectx.set(values[0])
|
||||
except Exception:
|
||||
if not self.selected_kubectx.get() and values:
|
||||
self.selected_kubectx.set(values[0])
|
||||
clusters = self._get_k3d_cluster_list()
|
||||
combo = ttk.Combobox(self.bg_canvas, textvariable=self.selected_k3d_cluster, values=clusters, width=30)
|
||||
if not self.selected_k3d_cluster.get() and clusters:
|
||||
self.selected_k3d_cluster.set(clusters[0])
|
||||
|
||||
combo.bind('<<ComboboxSelected>>', lambda _evt: self._on_k3d_cluster_select())
|
||||
|
||||
combo_win = self.bg_canvas.create_window(x_label + 20, y, window=combo, anchor='nw')
|
||||
self._canvas_items.append(combo_win)
|
||||
self._overlay_widgets.append(combo)
|
||||
y += 34
|
||||
|
||||
elif selected_env_key == 'service':
|
||||
add_btn = tk.Button(self.bg_canvas, text='Add', command=self._on_create_k3d_cluster,
|
||||
bg='#F5F5DC', fg='black', activebackground='#E5E5D5', activeforeground='black',
|
||||
highlightbackground='#F5F5DC', highlightthickness=0,
|
||||
relief='flat', font=('SF Pro Text', 10), padx=10)
|
||||
add_win = self.bg_canvas.create_window(x_label + 290, y - 4, window=add_btn, anchor='nw')
|
||||
self._canvas_items.append(add_win)
|
||||
self._overlay_widgets.append(add_btn)
|
||||
|
||||
delete_btn = tk.Button(self.bg_canvas, text='Delete', command=self._on_delete_k3d_cluster,
|
||||
bg='#F5F5DC', fg='#cc0000', activebackground='#E5E5D5', activeforeground='#cc0000',
|
||||
highlightbackground='#F5F5DC', highlightthickness=0,
|
||||
relief='flat', font=('SF Pro Text', 10), padx=10)
|
||||
delete_win = self.bg_canvas.create_window(x_label + 350, y - 4, window=delete_btn, anchor='nw')
|
||||
self._canvas_items.append(delete_win)
|
||||
self._overlay_widgets.append(delete_btn)
|
||||
y += 40
|
||||
|
||||
else:
|
||||
# Service / Prod Section
|
||||
if selected_env_key == 'service':
|
||||
# K3s connection settings
|
||||
try:
|
||||
self._apply_k3s_defaults()
|
||||
except Exception:
|
||||
pass
|
||||
self._canvas_items.append(ui.canvas_text(self, x_label, y, 'K3s Connection:', fill='black', font=('SF Pro Text', 12, 'bold')))
|
||||
y += 24
|
||||
y += 28
|
||||
|
||||
ui.canvas_text(self, x_label + 20, y, 'Server URL:', fill='black', font=('SF Pro Text', 11))
|
||||
server_entry = tk.Entry(self.bg_canvas, textvariable=self.k3s_server_url, width=60)
|
||||
@ -4545,29 +4558,58 @@ class ProleInstaller:
|
||||
self._overlay_widgets.append(server_entry)
|
||||
y += 28
|
||||
|
||||
ui.canvas_text(self, x_label + 20, y, 'Token (vault_k3s_token):', fill='black', font=('SF Pro Text', 11))
|
||||
ui.canvas_text(self, x_label + 20, y, 'Token:', fill='black', font=('SF Pro Text', 11))
|
||||
token_entry = tk.Entry(self.bg_canvas, textvariable=self.k3s_token, show='*', width=60)
|
||||
token_win = self.bg_canvas.create_window(x_label + 200, y - 8, window=token_entry, anchor='nw')
|
||||
self._canvas_items.append(token_win)
|
||||
self._overlay_widgets.append(token_entry)
|
||||
y += 34
|
||||
|
||||
# Context selection for both Service and Prod
|
||||
self._canvas_items.append(ui.canvas_text(self, x_label, y, 'Kubernetes Context:', fill='black', font=('SF Pro Text', 12, 'bold')))
|
||||
y += 28
|
||||
values = self._get_kubectx_list()
|
||||
combo = ttk.Combobox(self.bg_canvas, textvariable=self.selected_kubectx, values=values, state='readonly', width=40)
|
||||
if not self.selected_kubectx.get() and values:
|
||||
self.selected_kubectx.set(values[0])
|
||||
combo_win = self.bg_canvas.create_window(x_label + 20, y, window=combo, anchor='nw')
|
||||
self._canvas_items.append(combo_win)
|
||||
self._overlay_widgets.append(combo)
|
||||
y += 40
|
||||
|
||||
service_label = ui.canvas_text(self, x_label, y, '', fill='black', font=('SF Pro Text', 12, 'bold'))
|
||||
if selected_env_key == 'prod':
|
||||
# Staging directory input
|
||||
self._canvas_items.append(ui.canvas_text(self, x_label, y, 'Local Artifact Staging Directory:', fill='black', font=('SF Pro Text', 12, 'bold')))
|
||||
y += 30
|
||||
|
||||
entry = tk.Entry(self.bg_canvas, textvariable=self.prod_artifacts_path, width=60)
|
||||
entry_win = self.bg_canvas.create_window(x_label + 20, y, window=entry, anchor='nw')
|
||||
self._canvas_items.append(entry_win)
|
||||
self._overlay_widgets.append(entry)
|
||||
|
||||
browse_btn = tk.Button(self.bg_canvas, text="Browse...", command=lambda: self.prod_artifacts_path.set(filedialog.askdirectory() or self.prod_artifacts_path.get()))
|
||||
browse_win = self.bg_canvas.create_window(x_label + 440, y - 5, window=browse_btn, anchor='nw')
|
||||
self._canvas_items.append(browse_win)
|
||||
self._overlay_widgets.append(browse_btn)
|
||||
y += 30
|
||||
|
||||
ui.canvas_text(self, x_label + 20, y, '(Used by etc/deploy_pipeline.sh --mode gcp)', fill='#6e6e73', font=('SF Pro Text', 10))
|
||||
y += 34
|
||||
|
||||
# Common Services Status Check (Notebook)
|
||||
# Show if in Service/Prod or if a cluster is selected in Dev
|
||||
show_notebook = False
|
||||
if selected_env_key == 'dev':
|
||||
if self.selected_k3d_cluster.get():
|
||||
show_notebook = True
|
||||
else:
|
||||
show_notebook = True
|
||||
|
||||
if show_notebook:
|
||||
service_label_text = f"Common Services (namespace: {self._get_service_namespace()})"
|
||||
service_label = ui.canvas_text(self, x_label, y, service_label_text, fill='black', font=('SF Pro Text', 12, 'bold'))
|
||||
self._canvas_items.append(service_label)
|
||||
|
||||
def _update_service_label(*_args):
|
||||
ns_val = 'default'
|
||||
if self.bg_canvas.winfo_exists():
|
||||
self.bg_canvas.itemconfig(service_label, text=f"Common Services (namespace: {ns_val})")
|
||||
|
||||
if hasattr(self, '_service_ns_trace'):
|
||||
try:
|
||||
self.service_namespace.trace_remove('write', self._service_ns_trace)
|
||||
except Exception:
|
||||
pass
|
||||
self._service_ns_trace = self.service_namespace.trace_add('write', lambda *_a: self.safe_after(_update_service_label))
|
||||
_update_service_label()
|
||||
y += 24
|
||||
y += 28
|
||||
|
||||
notebook = ttk.Notebook(self.bg_canvas)
|
||||
status_frame = tk.Frame(notebook, bg='white')
|
||||
@ -4593,6 +4635,7 @@ class ProleInstaller:
|
||||
)
|
||||
self._canvas_items.append(console_window)
|
||||
self._overlay_widgets.extend([notebook, status_frame, deploy_frame, status_console, deploy_console])
|
||||
|
||||
restored = self._restore_common_services_log(deploy_console)
|
||||
if restored:
|
||||
try:
|
||||
@ -4618,46 +4661,9 @@ class ProleInstaller:
|
||||
deploy_win = self.bg_canvas.create_window(x_label + 20, y, window=deploy_btn, anchor='nw')
|
||||
self._canvas_items.append(deploy_win)
|
||||
self._overlay_widgets.append(deploy_btn)
|
||||
y += 24
|
||||
y += 40
|
||||
self._verify_k3s_services()
|
||||
|
||||
elif selected_env_key == 'prod':
|
||||
# Staging directory input
|
||||
self._canvas_items.append(ui.canvas_text(self, x_label, y, 'Local Artifact Staging Directory:', fill='black', font=('SF Pro Text', 12, 'bold')))
|
||||
y += 30
|
||||
|
||||
entry = tk.Entry(self.bg_canvas, textvariable=self.prod_artifacts_path, width=60)
|
||||
entry_win = self.bg_canvas.create_window(x_label + 20, y, window=entry, anchor='nw')
|
||||
self._canvas_items.append(entry_win)
|
||||
self._overlay_widgets.append(entry)
|
||||
|
||||
browse_btn = tk.Button(self.bg_canvas, text="Browse...", command=lambda: self.prod_artifacts_path.set(filedialog.askdirectory() or self.prod_artifacts_path.get()))
|
||||
browse_win = self.bg_canvas.create_window(x_label + 20 + 400, y - 5, window=browse_btn, anchor='nw')
|
||||
self._canvas_items.append(browse_win)
|
||||
self._overlay_widgets.append(browse_btn)
|
||||
y += 40
|
||||
|
||||
ui.canvas_text(self, x_label + 20, y, '(Used by etc/deploy_pipeline.sh --mode gcp)', fill='#6e6e73', font=('SF Pro Text', 10))
|
||||
y += 30
|
||||
|
||||
# Cluster Status (only if dev)
|
||||
if selected_env_key == 'dev':
|
||||
y += 10
|
||||
self.k3d_status_label = ui.canvas_text(self, x_label, y, 'Cluster: Checking...', fill='#6e6e73', font=('SF Pro Text', 12))
|
||||
self._canvas_items.append(self.k3d_status_label)
|
||||
|
||||
y += 30
|
||||
self.common_services_status_label = ui.canvas_text(self, x_label, y, 'Common Services: Checking...', fill='#6e6e73', font=('SF Pro Text', 12))
|
||||
self._canvas_items.append(self.common_services_status_label)
|
||||
|
||||
y += 30
|
||||
self._cluster_env_message_label = ui.canvas_text(self, x_label, y, '', fill='#34c759', font=('SF Pro Text', 11))
|
||||
self._canvas_items.append(self._cluster_env_message_label)
|
||||
|
||||
# Async status check
|
||||
self.check_cluster_status_async()
|
||||
y += 40
|
||||
|
||||
def _openbao_url(self) -> str:
|
||||
url = (os.environ.get("PROLE_OPENBAO_URL") or "http://127.0.0.1:18200").strip()
|
||||
return url.rstrip('/')
|
||||
@ -4954,6 +4960,168 @@ class ProleInstaller:
|
||||
msg = f"{label}: Connected" if ok else f"{label}: Not reachable"
|
||||
return ok, msg
|
||||
|
||||
def _get_k3d_cluster_list(self) -> list[str]:
|
||||
"""Get list of k3d clusters."""
|
||||
try:
|
||||
res = subprocess.run(['k3d', 'cluster', 'list', '--no-headers'], capture_output=True, text=True, timeout=5)
|
||||
if res.returncode == 0:
|
||||
# Extract first column (cluster names)
|
||||
return [line.split()[0] for line in res.stdout.splitlines() if line.strip()]
|
||||
except Exception:
|
||||
pass
|
||||
return []
|
||||
|
||||
def _on_create_k3d_cluster(self):
|
||||
name = self.selected_k3d_cluster.get().strip()
|
||||
if not name:
|
||||
messagebox.showerror("Error", "Cluster name cannot be empty")
|
||||
return
|
||||
# Persist the typed name so the refreshed page keeps it selected
|
||||
self.k3d_cluster_name.set(name)
|
||||
|
||||
# If the cluster already exists just select it (triggers notebook display)
|
||||
existing = self._get_k3d_cluster_list()
|
||||
if name in existing:
|
||||
self.selected_k3d_cluster.set(name)
|
||||
self.show_page('init_cluster')
|
||||
return
|
||||
|
||||
# Refresh the page so the notebook/console is visible before streaming output
|
||||
self.selected_k3d_cluster.set(name)
|
||||
self.show_page('init_cluster')
|
||||
|
||||
def do_create():
|
||||
console = getattr(self, '_k3s_service_deploy_console', None)
|
||||
if console:
|
||||
console.clear()
|
||||
console.write(f"Creating k3d cluster '{name}' @ {time.strftime('%Y-%m-%d %H:%M:%S')}\n\n")
|
||||
try:
|
||||
if self._k3s_service_notebook and self._k3s_service_deploy_tab:
|
||||
self._k3s_service_notebook.select(self._k3s_service_deploy_tab)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
# Build the k3d create command
|
||||
prole_data = str(self._resolve_env_dir('PROLE_DATA', 'data'))
|
||||
volume_args = _k3d_prole_data_volume_args(prole_data)
|
||||
reg_args = []
|
||||
if getattr(self, 'registry_url', None):
|
||||
if self.registry_url.startswith('localhost:5000'):
|
||||
reg_args = ['--registry-create', 'prole-registry:0.0.0.0:5000']
|
||||
else:
|
||||
reg_args = ['--registry-use', self.registry_url]
|
||||
cmd = ['k3d', 'cluster', 'create', name, '-a', '2', '--wait'] + volume_args + reg_args + ['--timestamps']
|
||||
if console:
|
||||
console.write(f"$ {' '.join(cmd)}\n\n")
|
||||
|
||||
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
||||
text=True, cwd=PROJECT_ROOT)
|
||||
for line in proc.stdout:
|
||||
if console:
|
||||
console.write(line)
|
||||
proc.wait()
|
||||
if proc.returncode != 0:
|
||||
if console:
|
||||
console.write(f"\nk3d cluster create failed (exit {proc.returncode})\n")
|
||||
return
|
||||
if console:
|
||||
console.write(f"\nCluster '{name}' created successfully.\n\n")
|
||||
|
||||
# Merge kubeconfig so kubectl can reach the new cluster
|
||||
try:
|
||||
subprocess.run(
|
||||
['k3d', 'kubeconfig', 'merge', name, '--kubeconfig-switch-context'],
|
||||
capture_output=True, text=True, timeout=10
|
||||
)
|
||||
if console:
|
||||
console.write(f"Kubeconfig merged for cluster '{name}'.\n\n")
|
||||
except Exception as kce:
|
||||
if console:
|
||||
console.write(f"Warning: failed to merge kubeconfig: {kce}\n")
|
||||
|
||||
# Refresh UI and then deploy common services check
|
||||
self.safe_after(self._refresh_k3d_ui)
|
||||
# Trigger common services check in the deploy console
|
||||
self.safe_after(self._deploy_k3s_services)
|
||||
except Exception as e:
|
||||
if console:
|
||||
console.write(f"\nError: {e}\n")
|
||||
self.safe_after(lambda: messagebox.showerror("Error", f"Failed to create k3d cluster: {e}"))
|
||||
|
||||
threading.Thread(target=do_create, daemon=True).start()
|
||||
|
||||
def _refresh_k3d_ui(self):
|
||||
clusters = self._get_k3d_cluster_list()
|
||||
self.k3d_cluster_list.set(clusters)
|
||||
name = self.k3d_cluster_name.get().strip()
|
||||
if name in clusters:
|
||||
self.selected_k3d_cluster.set(name)
|
||||
self.show_page('init_cluster')
|
||||
|
||||
def _on_k3d_cluster_select(self, *args):
|
||||
"""Called when a k3d cluster is selected from the dropdown."""
|
||||
cluster_name = self.selected_k3d_cluster.get()
|
||||
if cluster_name:
|
||||
try:
|
||||
# Merge kubeconfig and switch context to the selected cluster
|
||||
subprocess.run(['k3d', 'kubeconfig', 'merge', cluster_name, '--switch-context'], capture_output=True, text=True)
|
||||
except Exception:
|
||||
pass
|
||||
self.show_page('init_cluster')
|
||||
|
||||
def _on_delete_k3d_cluster(self):
|
||||
"""Delete the selected k3d cluster after user confirmation."""
|
||||
name = self.selected_k3d_cluster.get().strip()
|
||||
if not name:
|
||||
messagebox.showerror("Error", "No cluster selected to delete.")
|
||||
return
|
||||
|
||||
confirmed = messagebox.askyesno("Confirm Delete", f"Are you sure you want to delete cluster '{name}'?")
|
||||
if not confirmed:
|
||||
return
|
||||
|
||||
# Show the notebook/console before streaming output
|
||||
self.show_page('init_cluster')
|
||||
|
||||
def do_delete():
|
||||
console = getattr(self, '_k3s_service_deploy_console', None)
|
||||
if console:
|
||||
console.clear()
|
||||
console.write(f"Deleting k3d cluster '{name}' @ {time.strftime('%Y-%m-%d %H:%M:%S')}\n\n")
|
||||
try:
|
||||
if self._k3s_service_notebook and self._k3s_service_deploy_tab:
|
||||
self._k3s_service_notebook.select(self._k3s_service_deploy_tab)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
cmd = ['k3d', 'cluster', 'delete', name]
|
||||
if console:
|
||||
console.write(f"$ {' '.join(cmd)}\n\n")
|
||||
|
||||
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
||||
text=True, cwd=PROJECT_ROOT)
|
||||
for line in proc.stdout:
|
||||
if console:
|
||||
console.write(line)
|
||||
proc.wait()
|
||||
if proc.returncode != 0:
|
||||
if console:
|
||||
console.write(f"\nk3d cluster delete failed (exit {proc.returncode})\n")
|
||||
return
|
||||
if console:
|
||||
console.write(f"\nCluster '{name}' deleted successfully.\n")
|
||||
|
||||
# Clear selection and refresh UI
|
||||
self.selected_k3d_cluster.set('')
|
||||
self.k3d_cluster_name.set('')
|
||||
self.safe_after(self._refresh_k3d_ui)
|
||||
except Exception as e:
|
||||
if console:
|
||||
console.write(f"\nError: {e}\n")
|
||||
self.safe_after(lambda: messagebox.showerror("Error", f"Failed to delete k3d cluster: {e}"))
|
||||
|
||||
threading.Thread(target=do_delete, daemon=True).start()
|
||||
|
||||
def _get_kubectx_list(self) -> list[str]:
|
||||
"""Get list of kubernetes contexts."""
|
||||
try:
|
||||
@ -4970,7 +5138,7 @@ class ProleInstaller:
|
||||
return ["default"]
|
||||
|
||||
def _verify_k3s_services(self):
|
||||
if self._cluster_env_key() != 'service' or not self._k3s_service_status_console:
|
||||
if not self._k3s_service_status_console:
|
||||
self._stop_k3s_service_status_updates()
|
||||
return
|
||||
self._start_k3s_service_status_updates()
|
||||
@ -5022,10 +5190,10 @@ class ProleInstaller:
|
||||
try:
|
||||
ns = self._get_service_namespace()
|
||||
env = self._script_env_for_namespace(ns)
|
||||
env["PROLE_MODE"] = "k3s"
|
||||
env["PROLE_MODE"] = self._deployment_mode()
|
||||
|
||||
if not env.get("KUBECONFIG"):
|
||||
console.write("KUBECONFIG not generated. Check K3s credentials in prole.cfg.\n")
|
||||
console.write("KUBECONFIG not generated. Check cluster credentials in prole.cfg.\n")
|
||||
return
|
||||
|
||||
status_args = ["-n", ns]
|
||||
@ -5059,7 +5227,7 @@ class ProleInstaller:
|
||||
|
||||
service_ns = self._get_service_namespace()
|
||||
env = self._script_env_for_namespace(service_ns)
|
||||
env["PROLE_MODE"] = "k3s"
|
||||
env["PROLE_MODE"] = self._deployment_mode()
|
||||
log_path = self._common_services_log_path()
|
||||
env["COMMON_SERVICES_INIT_LOG"] = str(log_path)
|
||||
try:
|
||||
@ -5073,10 +5241,10 @@ class ProleInstaller:
|
||||
|
||||
if not env.get("KUBECONFIG"):
|
||||
if console:
|
||||
console.write("KUBECONFIG not generated. Check K3s credentials in prole.cfg.\n")
|
||||
console.write("KUBECONFIG not generated. Check cluster credentials in prole.cfg.\n")
|
||||
if log_fp:
|
||||
try:
|
||||
log_fp.write("KUBECONFIG not generated. Check K3s credentials in prole.cfg.\n")
|
||||
log_fp.write("KUBECONFIG not generated. Check cluster credentials in prole.cfg.\n")
|
||||
log_fp.close()
|
||||
except Exception:
|
||||
pass
|
||||
@ -5121,86 +5289,72 @@ class ProleInstaller:
|
||||
|
||||
threading.Thread(target=_deploy, daemon=True).start()
|
||||
|
||||
def _on_save_cluster_config(self):
|
||||
def _validate_and_save_cluster_config(self) -> bool:
|
||||
"""Verify the config then write the values to prole.cfg."""
|
||||
# Verification logic
|
||||
env_key = self._cluster_env_key()
|
||||
if env_key == 'dev':
|
||||
ctx = self.selected_kubectx.get()
|
||||
if not ctx:
|
||||
messagebox.showwarning("Validation", "Please select a kubectx context.")
|
||||
return
|
||||
# Switch context
|
||||
cluster_val = self.selected_k3d_cluster.get() or 'prole-dev-cluster'
|
||||
if not cluster_val:
|
||||
messagebox.showwarning("Validation", "Please select or create a k3d cluster.")
|
||||
return False
|
||||
|
||||
# Context switch (if context matches cluster name)
|
||||
try:
|
||||
subprocess.run(['kubectx', ctx], check=True)
|
||||
subprocess.run(['kubectl', 'config', 'use-context', f"k3d-{cluster_val}"], capture_output=True, text=True)
|
||||
except Exception:
|
||||
try:
|
||||
subprocess.run(['kubectl', 'config', 'use-context', ctx], check=True)
|
||||
except Exception as e:
|
||||
messagebox.showerror("Error", f"Failed to switch to context {ctx}: {e}")
|
||||
return
|
||||
pass
|
||||
elif env_key == 'service':
|
||||
if not (self.k3s_server_url.get() or '').strip():
|
||||
messagebox.showwarning("Validation", "Please specify the K3s server URL.")
|
||||
return
|
||||
return False
|
||||
if not (self.k3s_token.get() or '').strip():
|
||||
messagebox.showwarning("Validation", "Please specify the K3s token.")
|
||||
return
|
||||
return False
|
||||
cluster_val = self.cluster_env.get()
|
||||
elif env_key == 'prod':
|
||||
path = self.prod_artifacts_path.get().strip()
|
||||
if not path:
|
||||
messagebox.showwarning("Validation", "Please specify an artifact staging directory.")
|
||||
return
|
||||
return False
|
||||
p = Path(path).expanduser()
|
||||
if not p.exists():
|
||||
try:
|
||||
p.mkdir(parents=True, exist_ok=True)
|
||||
except Exception as e:
|
||||
messagebox.showerror("Error", f"Failed to create directory {path}: {e}")
|
||||
return
|
||||
return False
|
||||
cluster_val = self.cluster_env.get()
|
||||
else:
|
||||
cluster_val = self.cluster_env.get()
|
||||
|
||||
# Capture cluster info for config
|
||||
self.prole_cfg_data['Initialize Cluster']['ENVIRONMENT'] = self.cluster_env.get()
|
||||
self.prole_cfg_data['Initialize Cluster']['ENVIRONMENT'] = cluster_val
|
||||
self.prole_cfg_data['Initialize Cluster']['K3S_SERVER_URL'] = (self.k3s_server_url.get() or '').strip()
|
||||
self.prole_cfg_data['Initialize Cluster']['K3S_TOKEN'] = _encrypt_cfg_secret(self.k3s_token.get() or '')
|
||||
self.prole_cfg_data['Global']['SERVICE_NAMESPACE'] = self._get_service_namespace()
|
||||
self.prole_cfg_data['Optional Features']['SUPABASE_ENABLED'] = str(self.supabase_enabled.get())
|
||||
self.prole_cfg_data['Optional Features']['KERBEROS_ENABLED'] = str(self.kerberos_enabled.get())
|
||||
self.prole_cfg_data['Optional Features']['AT_REST_ENCRYPTION_ENABLED'] = str(self.at_rest_encryption_enabled.get())
|
||||
|
||||
if env_key == 'prod':
|
||||
self.prole_cfg_data['Prod Cluster (k8s)']['STAGING_DIRECTORY'] = self.prod_artifacts_path.get()
|
||||
|
||||
# Save config
|
||||
self._save_prole_cfg()
|
||||
|
||||
# Apply common services configuration for service namespace, then refresh statuses
|
||||
ns = self._get_service_namespace()
|
||||
def worker():
|
||||
try:
|
||||
env_vars = self._script_env_for_namespace(ns)
|
||||
# Use 'update' to (re)apply manifests and config in the selected namespace
|
||||
svc_args = ["-n", ns]
|
||||
if self.kerberos_enabled.get():
|
||||
svc_args.append("-k")
|
||||
svc_args.append("update")
|
||||
self.controller.run_script("init_common_services.sh", args=svc_args, env=env_vars)
|
||||
except Exception as e:
|
||||
print(f"Cluster service update failed: {e}")
|
||||
finally:
|
||||
# Refresh status labels
|
||||
self.safe_after(self.check_cluster_status_async)
|
||||
|
||||
threading.Thread(target=worker, daemon=True).start()
|
||||
|
||||
messagebox.showinfo("Success", "Cluster configuration saved and common services applied.")
|
||||
return True
|
||||
|
||||
def _cluster_status_snapshot(self) -> dict:
|
||||
env = self._cluster_env_key()
|
||||
|
||||
cluster_ok = False
|
||||
if env == 'dev':
|
||||
cluster_name = "prole-dev-cluster"
|
||||
cluster_name = self.selected_k3d_cluster.get() or "prole-dev-cluster"
|
||||
try:
|
||||
res = subprocess.run(['k3d', 'cluster', 'list', '--no-headers'], capture_output=True, text=True)
|
||||
cluster_ok = cluster_name in (res.stdout or '')
|
||||
except Exception:
|
||||
cluster_ok = False
|
||||
cluster_msg = f"K3D Cluster ({cluster_name}): Running" if cluster_ok else f"K3D Cluster ({cluster_name}): Not found/stopped"
|
||||
cluster_msg = f"K3D Cluster ({cluster_name}): Running" if cluster_ok else f"K3D Cluster ({cluster_name}): Not found"
|
||||
else:
|
||||
cluster_ok, cluster_msg = self._check_k8s_cluster(env)
|
||||
cluster_fill = '#34c759' if cluster_ok else '#ff9f0a'
|
||||
@ -5221,17 +5375,12 @@ class ProleInstaller:
|
||||
|
||||
def _cluster_ready_for_navigation(self) -> bool:
|
||||
status = self._cluster_status_snapshot()
|
||||
env = status['env']
|
||||
|
||||
if not status['cluster_ok']:
|
||||
try:
|
||||
messagebox.showerror('Cluster', 'Cluster is not reachable. Please verify your cluster and try again.')
|
||||
except Exception:
|
||||
pass
|
||||
return False
|
||||
|
||||
# Refresh status display for any changes
|
||||
self.check_cluster_status_async()
|
||||
return True
|
||||
|
||||
def check_cluster_status_async(self):
|
||||
@ -6607,6 +6756,22 @@ class ProleInstaller:
|
||||
else:
|
||||
self.script_consoles[script].write("\nPort-forwards running (grafana ok).\n")
|
||||
|
||||
# Verify critical secrets
|
||||
ns = env.get("NAMESPACE", "default")
|
||||
self.script_consoles["init_cloudnative_pg.sh"].write(f"\nVerifying critical secrets in namespace {ns}...\n")
|
||||
critical_secrets = ["prole-db-user", "prole-db-superuser", "cnpg-admin-key"]
|
||||
missing_secrets = []
|
||||
for secret in critical_secrets:
|
||||
rc_s, _ = self._run_cmd_capture(["kubectl", "get", "secret", secret, "-n", ns])
|
||||
if rc_s != 0:
|
||||
missing_secrets.append(secret)
|
||||
|
||||
if missing_secrets:
|
||||
self.script_consoles["init_cloudnative_pg.sh"].write(f"CRITICAL: Missing secrets in namespace '{ns}': {', '.join(missing_secrets)}.\n")
|
||||
self.script_consoles["init_cloudnative_pg.sh"].write("Database initialization will fail without these secrets.\n")
|
||||
self.safe_after(lambda: self.bg_canvas.itemconfig(self._init_scripts_status_label, text="Critical secret missing.", fill='#ff3b30') if self.bg_canvas.winfo_exists() else None)
|
||||
overall_success = False
|
||||
|
||||
if overall_success:
|
||||
self.safe_after(lambda: self.bg_canvas.itemconfig(self._init_scripts_status_label, text="Initialization complete!", fill='#34c759') if self.bg_canvas.winfo_exists() else None)
|
||||
self._scripts_success = True
|
||||
@ -7124,15 +7289,8 @@ class ProleInstaller:
|
||||
return
|
||||
|
||||
if current_id == 'init_cluster':
|
||||
# Capture cluster info
|
||||
self.prole_cfg_data['Initialize Cluster']['ENVIRONMENT'] = self.cluster_env.get()
|
||||
self.prole_cfg_data['Initialize Cluster']['K3S_SERVER_URL'] = (self.k3s_server_url.get() or '').strip()
|
||||
self.prole_cfg_data['Initialize Cluster']['K3S_TOKEN'] = _encrypt_cfg_secret(self.k3s_token.get() or '')
|
||||
self.prole_cfg_data['Global']['SERVICE_NAMESPACE'] = self._get_service_namespace()
|
||||
self.prole_cfg_data['Optional Features']['SUPABASE_ENABLED'] = str(self.supabase_enabled.get())
|
||||
self.prole_cfg_data['Optional Features']['KERBEROS_ENABLED'] = str(self.kerberos_enabled.get())
|
||||
self.prole_cfg_data['Optional Features']['AT_REST_ENCRYPTION_ENABLED'] = str(self.at_rest_encryption_enabled.get())
|
||||
self._save_prole_cfg()
|
||||
if not self._validate_and_save_cluster_config():
|
||||
return
|
||||
if not self._cluster_ready_for_navigation():
|
||||
return
|
||||
if not getattr(self, '_common_services_success', False):
|
||||
@ -8595,6 +8753,7 @@ echo "-------------------------------------------------------------------";
|
||||
|
||||
def create_dmg(self):
|
||||
"""Create a DMG containing Prole Tools.app, a 'setup' binary, and an /Applications symlink."""
|
||||
global installer_name
|
||||
if platform.system() != 'Darwin':
|
||||
return
|
||||
|
||||
@ -9797,9 +9956,8 @@ esac
|
||||
else:
|
||||
raise Exception(f"Unknown environment: {env}")
|
||||
|
||||
def create_or_recreate_k3d_dev_cluster(self):
|
||||
"""Create or restart local k3d cluster named prole-dev-cluster and wire it to the chosen registry."""
|
||||
cluster_name = 'prole-dev-cluster'
|
||||
def create_or_recreate_k3d_dev_cluster(self, cluster_name='prole-dev-cluster'):
|
||||
"""Create or restart local k3d cluster and wire it to the chosen registry."""
|
||||
result = subprocess.run(['k3d', 'cluster', 'list'], capture_output=True, text=True)
|
||||
if cluster_name in (result.stdout or ''):
|
||||
subprocess.run(['k3d', 'cluster', 'delete', cluster_name], check=True)
|
||||
|
||||
48
qodana.yaml
Normal file
48
qodana.yaml
Normal file
@ -0,0 +1,48 @@
|
||||
#-------------------------------------------------------------------------------#
|
||||
# Qodana analysis is configured by qodana.yaml file #
|
||||
# https://www.jetbrains.com/help/qodana/qodana-yaml.html #
|
||||
#-------------------------------------------------------------------------------#
|
||||
|
||||
#################################################################################
|
||||
# WARNING: Do not store sensitive information in this file, #
|
||||
# as its contents will be included in the Qodana report. #
|
||||
#################################################################################
|
||||
version: "1.0"
|
||||
|
||||
#Specify inspection profile for code analysis
|
||||
profile:
|
||||
name: qodana.starter
|
||||
|
||||
#Enable inspections
|
||||
#include:
|
||||
# - name: <SomeEnabledInspectionId>
|
||||
|
||||
#Disable inspections
|
||||
#exclude:
|
||||
# - name: <SomeDisabledInspectionId>
|
||||
# paths:
|
||||
# - <path/where/not/run/inspection>
|
||||
|
||||
projectJDK: "25" #(Applied in CI/CD pipeline)
|
||||
|
||||
#Execute shell command before Qodana execution (Applied in CI/CD pipeline)
|
||||
#bootstrap: sh ./prepare-qodana.sh
|
||||
|
||||
#Install IDE plugins before Qodana execution (Applied in CI/CD pipeline)
|
||||
#plugins:
|
||||
# - id: <plugin.id> #(plugin id can be found at https://plugins.jetbrains.com)
|
||||
|
||||
# Quality gate. Will fail the CI/CD pipeline if any condition is not met
|
||||
# severityThresholds - configures maximum thresholds for different problem severities
|
||||
# testCoverageThresholds - configures minimum code coverage on a whole project and newly added code
|
||||
# Code Coverage is available in Ultimate and Ultimate Plus plans
|
||||
#failureConditions:
|
||||
# severityThresholds:
|
||||
# any: 15
|
||||
# critical: 5
|
||||
# testCoverageThresholds:
|
||||
# fresh: 70
|
||||
# total: 50
|
||||
|
||||
#Specify Qodana linter for analysis (Applied in CI/CD pipeline)
|
||||
linter: jetbrains/qodana-jvm:2025.3
|
||||
Loading…
Reference in New Issue
Block a user