Files
flowfish/backend/routers/clusters.py
T
taylanbakircioglu 3467d52639 fix: preserve existing events-buffer-length on large clusters
The upgrade script now defaults to the current buffer value when it is
>= 8192, instead of always recommending 8192. Reducing buffer on busy
clusters causes eBPF ring buffer overflow and lost events. Also warns
if user manually enters a value below the minimum.

Made-with: Cursor
2026-04-08 13:01:39 +03:00

2929 lines
125 KiB
Python

"""
Clusters router - Simplified for MVP
"""
from fastapi import APIRouter, HTTPException, status, Query
from fastapi.responses import PlainTextResponse
from pydantic import BaseModel
from typing import Optional, List
from datetime import datetime
from pathlib import Path
import structlog
import asyncio
from database.postgresql import database
from services.cluster_cache_service import cluster_cache_service
from services.cluster_connection_manager import cluster_connection_manager
from utils.encryption import encrypt_data
from config import settings as app_settings
logger = structlog.get_logger()
router = APIRouter()
# Path to kubernetes manifests
MANIFESTS_PATH = Path(__file__).parent.parent.parent / "deployment" / "kubernetes-manifests"
# Pydantic schemas
class ClusterCreate(BaseModel):
name: str
description: Optional[str] = None
environment: Optional[str] = "production"
provider: Optional[str] = "kubernetes"
region: Optional[str] = "default"
connection_type: str # 'in-cluster', 'kubeconfig', 'token'
api_server_url: Optional[str] = None
kubeconfig: Optional[str] = None
token: Optional[str] = None
ca_cert: Optional[str] = None
gadget_namespace: str # Namespace where gadget is deployed (REQUIRED from UI)
gadget_endpoint: Optional[str] = None # Deprecated - not used anymore
skip_tls_verify: Optional[bool] = False
class ClusterUpdate(BaseModel):
"""
Schema for updating cluster configuration.
All fields are optional - only provided fields will be updated.
Sensitive fields (token, kubeconfig, ca_cert) are only updated if explicitly provided.
"""
name: Optional[str] = None
description: Optional[str] = None
environment: Optional[str] = None # production, staging, development
provider: Optional[str] = None # kubernetes, openshift, eks, gke, aks
region: Optional[str] = None
api_server_url: Optional[str] = None
gadget_namespace: Optional[str] = None # Namespace where gadget is deployed
status: Optional[str] = None # 'active', 'inactive', 'maintenance'
skip_tls_verify: Optional[bool] = None
# Sensitive fields - only update if explicitly provided (not empty string)
token: Optional[str] = None
kubeconfig: Optional[str] = None
ca_cert: Optional[str] = None
class ClusterResponse(BaseModel):
id: int
name: str = "unnamed"
description: Optional[str] = None
environment: Optional[str] = None
provider: Optional[str] = None
region: Optional[str] = None
connection_type: Optional[str] = None
api_server_url: Optional[str] = None
gadget_namespace: Optional[str] = None
gadget_endpoint: Optional[str] = None # Deprecated
gadget_health_status: Optional[str] = None
gadget_version: Optional[str] = None
status: Optional[str] = None
total_nodes: Optional[int] = None
total_pods: Optional[int] = None
total_namespaces: Optional[int] = None
k8s_version: Optional[str] = None
created_at: Optional[datetime] = None
updated_at: Optional[datetime] = None
@router.get("/clusters")
async def get_clusters(is_active: Optional[bool] = None):
"""Get list of clusters"""
try:
query = """
SELECT id, name, description, environment, provider, region,
connection_type, api_server_url, gadget_namespace, gadget_endpoint,
gadget_health_status, gadget_version, status,
total_nodes, total_pods, total_namespaces,
k8s_version, created_at, updated_at
FROM clusters
WHERE status != 'deleted'
"""
params = {}
if is_active is not None:
query += " AND status = 'active'"
query += " ORDER BY created_at DESC"
clusters = await database.fetch_all(query, params)
logger.info("Retrieved clusters", count=len(clusters))
return {
"clusters": [dict(cluster) for cluster in clusters],
"count": len(clusters),
"supported_gadget_version": app_settings.GADGET_SUPPORTED_VERSION
}
except Exception as e:
logger.error("Get clusters failed", error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to retrieve clusters: {str(e)}"
)
@router.post("/clusters", status_code=status.HTTP_201_CREATED)
async def create_cluster(cluster_data: ClusterCreate):
"""Create new cluster and fetch its information"""
try:
# Check if cluster with same name exists
existing = await database.fetch_one(
"SELECT id, name, status FROM clusters WHERE name = :name",
{"name": cluster_data.name}
)
if existing:
if existing["status"] == "deleted":
# Reactivate deleted cluster - update instead of insert
logger.info("Reactivating deleted cluster",
cluster_id=existing["id"], name=cluster_data.name)
update_query = """
UPDATE clusters SET
description = :description,
environment = :environment,
provider = :provider,
region = :region,
connection_type = :connection_type,
api_server_url = :api_server_url,
kubeconfig_encrypted = :kubeconfig,
token_encrypted = :token,
ca_cert_encrypted = :ca_cert,
gadget_namespace = :gadget_namespace,
skip_tls_verify = :skip_tls_verify,
status = 'active',
gadget_health_status = 'unknown',
updated_at = NOW()
WHERE id = :cluster_id
RETURNING id
"""
# Encrypt sensitive data before saving
encrypted_kubeconfig = encrypt_data(cluster_data.kubeconfig) if cluster_data.kubeconfig else None
encrypted_token = encrypt_data(cluster_data.token) if cluster_data.token else None
encrypted_ca_cert = encrypt_data(cluster_data.ca_cert) if cluster_data.ca_cert else None
params = {
"cluster_id": existing["id"],
"description": cluster_data.description or "",
"environment": cluster_data.environment or "production",
"provider": cluster_data.provider or "kubernetes",
"region": cluster_data.region or "default",
"connection_type": cluster_data.connection_type,
"api_server_url": cluster_data.api_server_url,
"kubeconfig": encrypted_kubeconfig,
"token": encrypted_token,
"ca_cert": encrypted_ca_cert,
"gadget_namespace": cluster_data.gadget_namespace, # Required from UI
"skip_tls_verify": cluster_data.skip_tls_verify or False
}
result = await database.fetch_one(update_query, params)
cluster_id = result['id']
else:
# Active cluster with same name exists
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail=f"Cluster with name '{cluster_data.name}' already exists (ID: {existing['id']}). Use a different name or delete the existing cluster first."
)
else:
# Insert new cluster
insert_query = """
INSERT INTO clusters (
name, description, environment, provider, region,
connection_type, api_server_url, kubeconfig_encrypted,
token_encrypted, ca_cert_encrypted, gadget_namespace,
skip_tls_verify, status, gadget_health_status, created_at
)
VALUES (
:name, :description, :environment, :provider, :region,
:connection_type, :api_server_url, :kubeconfig,
:token, :ca_cert, :gadget_namespace,
:skip_tls_verify, 'active', 'unknown', NOW()
)
RETURNING id
"""
# Encrypt sensitive data before saving
encrypted_kubeconfig = encrypt_data(cluster_data.kubeconfig) if cluster_data.kubeconfig else None
encrypted_token = encrypt_data(cluster_data.token) if cluster_data.token else None
encrypted_ca_cert = encrypt_data(cluster_data.ca_cert) if cluster_data.ca_cert else None
params = {
"name": cluster_data.name,
"description": cluster_data.description or "",
"environment": cluster_data.environment or "production",
"provider": cluster_data.provider or "kubernetes",
"region": cluster_data.region or "default",
"connection_type": cluster_data.connection_type,
"api_server_url": cluster_data.api_server_url,
"kubeconfig": encrypted_kubeconfig,
"token": encrypted_token,
"ca_cert": encrypted_ca_cert,
"gadget_namespace": cluster_data.gadget_namespace, # Required from UI
"skip_tls_verify": cluster_data.skip_tls_verify or False
}
result = await database.fetch_one(insert_query, params)
cluster_id = result['id']
logger.info("Cluster created/updated", cluster_id=cluster_id, name=cluster_data.name)
# Schedule background task to fetch cluster info (non-blocking)
# This prevents health check timeouts during cluster creation
async def _fetch_cluster_info_background(cid: int, name: str):
"""Background task to fetch cluster info without blocking the request"""
try:
logger.info("Background: Fetching cluster info", cluster_id=cid, cluster_name=name)
cluster_info = await cluster_connection_manager.get_cluster_info(cid)
gadget_health = await cluster_connection_manager.check_gadget_health(cid)
if not cluster_info.get("error"):
update_query = """
UPDATE clusters
SET total_nodes = :total_nodes,
total_pods = :total_pods,
total_namespaces = :total_namespaces,
k8s_version = :k8s_version,
gadget_health_status = :gadget_health_status,
gadget_version = :gadget_version,
updated_at = NOW()
WHERE id = :cluster_id
"""
await database.execute(update_query, {
"cluster_id": cid,
"total_nodes": cluster_info.get("total_nodes", 0),
"total_pods": cluster_info.get("total_pods", 0),
"total_namespaces": cluster_info.get("total_namespaces", 0),
"k8s_version": cluster_info.get("k8s_version"),
"gadget_health_status": gadget_health.get("health_status", "unknown"),
"gadget_version": gadget_health.get("version")
})
logger.info("Background: Cluster info updated", cluster_id=cid)
else:
error_msg = cluster_info.get("error", "Unknown error")
logger.warning("Background: Cluster info fetch returned error",
cluster_id=cid, error=error_msg)
try:
await database.execute(
"""UPDATE clusters SET
gadget_health_status = 'unknown',
error_message = :error_msg,
updated_at = NOW()
WHERE id = :cluster_id""",
{"cluster_id": cid, "error_msg": str(error_msg)[:500]}
)
except Exception:
pass
except Exception as e:
logger.error("Background: Failed to fetch cluster info",
cluster_id=cid,
error=str(e))
try:
await database.execute(
"""UPDATE clusters SET
gadget_health_status = 'unknown',
error_message = :error_msg,
updated_at = NOW()
WHERE id = :cluster_id""",
{"cluster_id": cid, "error_msg": str(e)[:500]}
)
except Exception:
pass
# Start background task - don't await, let it run independently
asyncio.create_task(_fetch_cluster_info_background(cluster_id, cluster_data.name))
# Fetch the complete cluster record
cluster = await database.fetch_one(
"""SELECT id, name, description, environment, provider, region,
connection_type, api_server_url, gadget_namespace, gadget_endpoint,
gadget_health_status, gadget_version, status,
total_nodes, total_pods, total_namespaces,
k8s_version, created_at, updated_at
FROM clusters WHERE id = :cluster_id""",
{"cluster_id": cluster_id}
)
if not cluster:
logger.error("Cluster not found after creation", cluster_id=cluster_id)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Cluster created but could not be retrieved"
)
return {
"message": "Cluster created successfully",
"cluster": dict(cluster)
}
except HTTPException:
raise
except Exception as e:
logger.error("Create cluster failed", error=str(e), exc_info=True)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to create cluster: {str(e)}"
)
# NOTE: Static paths like /clusters/gadget-install-script and /clusters/test-connection
# MUST be defined BEFORE dynamic paths like /clusters/{cluster_id}
# Otherwise FastAPI will try to match "gadget-install-script" as a cluster_id
def generate_uninstall_script(cli_tool: str, provider_upper: str) -> str:
"""Generate uninstall script for Flowfish components"""
return f'''#!/bin/bash
#
# ╔═══════════════════════════════════════════════════════════════════════════╗
# ║ Flowfish Uninstall Script for {provider_upper:<12} ║
# ║ ║
# ║ This script SAFELY removes ONLY: ║
# ║ - Inspector Gadget (DaemonSet, Service, ConfigMap, RBAC) ║
# ║ - Flowfish ServiceAccount and RBAC ║
# ║ - SCC (OpenShift only, if not used by other namespaces) ║
# ║ ║
# ║ ✅ SAFE: The NAMESPACE will NOT be deleted! ║
# ║ ✅ SAFE: Other workloads in the namespace will NOT be affected! ║
# ╚═══════════════════════════════════════════════════════════════════════════╝
#
# Usage:
# chmod +x uninstall-flowfish.sh
# ./uninstall-flowfish.sh <namespace>
# ./uninstall-flowfish.sh # Interactive mode
#
CLI_TOOL="{cli_tool}"
# Colors
RED='\\033[0;31m'
GREEN='\\033[0;32m'
YELLOW='\\033[1;33m'
BLUE='\\033[0;34m'
CYAN='\\033[0;36m'
BOLD='\\033[1m'
NC='\\033[0m'
print_status() {{ echo -e "${{BLUE}}[INFO]${{NC}} $1"; }}
print_success() {{ echo -e "${{GREEN}}[SUCCESS]${{NC}} $1"; }}
print_warning() {{ echo -e "${{YELLOW}}[WARNING]${{NC}} $1"; }}
print_error() {{ echo -e "${{RED}}[ERROR]${{NC}} $1"; }}
print_header() {{ echo -e "\\n${{CYAN}}${{BOLD}}═══ $1 ═══${{NC}}\\n"; }}
echo ""
echo "╔═══════════════════════════════════════════════════════════════════════════╗"
echo "║ Flowfish Uninstall Script for {provider_upper:<12} ║"
echo "║ ║"
echo "║ ⚠️ This will remove Inspector Gadget and Flowfish ServiceAccount ║"
echo "╚═══════════════════════════════════════════════════════════════════════════╝"
echo ""
# ═══════════════════════════════════════════════════════════════════════════
# Pre-flight Checks
# ═══════════════════════════════════════════════════════════════════════════
print_header "Pre-flight Checks"
if ! command -v $CLI_TOOL &> /dev/null; then
print_error "$CLI_TOOL CLI is not installed or not in PATH"
exit 1
fi
print_success "$CLI_TOOL CLI found"
if ! $CLI_TOOL whoami &> /dev/null; then
print_error "Not logged in. Please run '$CLI_TOOL login' first."
exit 1
fi
CURRENT_USER=$($CLI_TOOL whoami)
print_success "Logged in as: $CURRENT_USER"
# Get namespace
if [ -n "$1" ]; then
NAMESPACE="$1"
print_status "Using namespace from parameter: $NAMESPACE"
else
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
read -p "Enter namespace to uninstall Flowfish from: " NAMESPACE
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
fi
if [ -z "$NAMESPACE" ]; then
print_error "Namespace cannot be empty!"
exit 1
fi
if ! $CLI_TOOL get namespace "$NAMESPACE" &> /dev/null; then
print_error "Namespace '$NAMESPACE' does not exist!"
exit 1
fi
# ═══════════════════════════════════════════════════════════════════════════
# Safety Check - Verify this is a Flowfish namespace
# ═══════════════════════════════════════════════════════════════════════════
print_header "Safety Verification"
# Check if Inspector Gadget or Flowfish resources exist in this namespace
GADGET_EXISTS=$($CLI_TOOL get daemonset inspektor-gadget -n $NAMESPACE 2>/dev/null && echo "yes" || echo "no")
SA_EXISTS=$($CLI_TOOL get sa flowfish-remote-reader -n $NAMESPACE 2>/dev/null && echo "yes" || echo "no")
if [ "$GADGET_EXISTS" = "no" ] && [ "$SA_EXISTS" = "no" ]; then
print_error "No Flowfish or Inspector Gadget resources found in namespace '$NAMESPACE'"
print_warning "This namespace does not appear to have Flowfish installed."
print_warning "Aborting to prevent accidental deletion of unrelated resources."
exit 1
fi
print_success "Flowfish resources detected in namespace '$NAMESPACE'"
# Confirmation
echo ""
echo "┌─────────────────────────────────────────────────────────────────────────────┐"
echo "│ ⚠️ WARNING: The following will be DELETED from namespace '$NAMESPACE': │"
echo "├─────────────────────────────────────────────────────────────────────────────┤"
if [ "$GADGET_EXISTS" = "yes" ]; then
echo "│ - DaemonSet: inspektor-gadget │"
echo "│ - Service: inspektor-gadget │"
echo "│ - ConfigMap: inspektor-gadget-config │"
echo "│ - ServiceAccount: inspektor-gadget │"
fi
if [ "$SA_EXISTS" = "yes" ]; then
echo "│ - ServiceAccount: flowfish-remote-reader │"
echo "│ - Secret: flowfish-remote-reader-token │"
fi
echo "│ - ClusterRole: inspektor-gadget (if no other bindings) │"
echo "│ - ClusterRole: flowfish-remote-reader (if no other bindings) │"
echo "│ - ClusterRoleBinding: inspektor-gadget-$NAMESPACE │"
echo "│ - ClusterRoleBinding: flowfish-remote-reader-$NAMESPACE │"
echo "├─────────────────────────────────────────────────────────────────────────────┤"
echo "│ ✅ SAFE: The namespace '$NAMESPACE' will NOT be deleted! │"
echo "│ ✅ SAFE: Other workloads in this namespace will NOT be affected! │"
echo "└─────────────────────────────────────────────────────────────────────────────┘"
echo ""
read -p "Are you sure you want to proceed? (yes/no): " CONFIRM
if [ "$CONFIRM" != "yes" ]; then
print_warning "Uninstall cancelled."
exit 0
fi
# ═══════════════════════════════════════════════════════════════════════════
# Uninstall Process
# ═══════════════════════════════════════════════════════════════════════════
print_header "Removing Flowfish Components"
# Remove namespace-scoped resources first
print_status "Removing Inspector Gadget DaemonSet..."
$CLI_TOOL delete daemonset inspektor-gadget -n $NAMESPACE --ignore-not-found=true 2>/dev/null
print_success "DaemonSet removed"
print_status "Removing Inspector Gadget Service..."
$CLI_TOOL delete service inspektor-gadget -n $NAMESPACE --ignore-not-found=true 2>/dev/null
print_success "Service removed"
print_status "Removing Inspector Gadget ConfigMap..."
$CLI_TOOL delete configmap inspektor-gadget-config -n $NAMESPACE --ignore-not-found=true 2>/dev/null
print_success "ConfigMap removed"
print_status "Removing Inspector Gadget ServiceAccount..."
$CLI_TOOL delete sa inspektor-gadget -n $NAMESPACE --ignore-not-found=true 2>/dev/null
print_success "Gadget ServiceAccount removed"
print_status "Removing Flowfish ServiceAccount..."
$CLI_TOOL delete sa flowfish-remote-reader -n $NAMESPACE --ignore-not-found=true 2>/dev/null
print_success "Flowfish ServiceAccount removed"
print_status "Removing Flowfish Token Secret..."
$CLI_TOOL delete secret flowfish-remote-reader-token -n $NAMESPACE --ignore-not-found=true 2>/dev/null
print_success "Token Secret removed"
print_status "Removing Flowfish Gadget Access Role..."
$CLI_TOOL delete role flowfish-gadget-access -n $NAMESPACE --ignore-not-found=true 2>/dev/null
$CLI_TOOL delete rolebinding flowfish-gadget-access -n $NAMESPACE --ignore-not-found=true 2>/dev/null
print_success "Gadget Access Role removed"
# Remove cluster-scoped resources
print_header "Removing Cluster-scoped Resources"
print_status "Removing ClusterRoleBinding for Gadget..."
$CLI_TOOL delete clusterrolebinding inspektor-gadget-$NAMESPACE --ignore-not-found=true 2>/dev/null
print_success "Gadget ClusterRoleBinding removed"
print_status "Removing ClusterRoleBinding for Flowfish..."
$CLI_TOOL delete clusterrolebinding flowfish-remote-reader-$NAMESPACE --ignore-not-found=true 2>/dev/null
print_success "Flowfish ClusterRoleBinding removed"
# Check if ClusterRoles are still in use by other namespaces
GADGET_BINDINGS=$($CLI_TOOL get clusterrolebindings -o jsonpath='{{.items[?(@.roleRef.name=="inspektor-gadget")].metadata.name}}' 2>/dev/null)
if [ -z "$GADGET_BINDINGS" ]; then
print_status "Removing ClusterRole inspektor-gadget (no longer in use)..."
$CLI_TOOL delete clusterrole inspektor-gadget --ignore-not-found=true 2>/dev/null
print_success "Gadget ClusterRole removed"
else
print_warning "ClusterRole inspektor-gadget still in use by other namespaces, skipping..."
fi
FLOWFISH_BINDINGS=$($CLI_TOOL get clusterrolebindings -o jsonpath='{{.items[?(@.roleRef.name=="flowfish-remote-reader")].metadata.name}}' 2>/dev/null)
if [ -z "$FLOWFISH_BINDINGS" ]; then
print_status "Removing ClusterRole flowfish-remote-reader (no longer in use)..."
$CLI_TOOL delete clusterrole flowfish-remote-reader --ignore-not-found=true 2>/dev/null
print_success "Flowfish ClusterRole removed"
else
print_warning "ClusterRole flowfish-remote-reader still in use by other namespaces, skipping..."
fi
# Remove SCC (OpenShift only) - check if still in use
if [ "$CLI_TOOL" = "oc" ]; then
# Check if SCC is used by other ServiceAccounts
SCC_USERS=$($CLI_TOOL get scc inspektor-gadget-scc -o jsonpath='{{.users}}' 2>/dev/null | grep -v "$NAMESPACE" || echo "")
if [ -z "$SCC_USERS" ]; then
print_status "Removing SCC inspektor-gadget-scc (no longer in use)..."
$CLI_TOOL adm policy remove-scc-from-user inspektor-gadget-scc -z inspektor-gadget -n $NAMESPACE 2>/dev/null || true
$CLI_TOOL delete scc inspektor-gadget-scc --ignore-not-found=true 2>/dev/null
print_success "SCC removed"
else
print_warning "SCC inspektor-gadget-scc still in use by other namespaces, skipping..."
fi
fi
# ═══════════════════════════════════════════════════════════════════════════
# Completion
# ═══════════════════════════════════════════════════════════════════════════
echo ""
echo "╔═══════════════════════════════════════════════════════════════════════════╗"
echo "║ ║"
echo "║ ✅ UNINSTALL COMPLETE ║"
echo "║ ║"
echo "║ Flowfish components have been removed from namespace '$NAMESPACE' ║"
echo "║ ║"
echo "║ ✅ Namespace '$NAMESPACE' was preserved (NOT deleted) ║"
echo "║ ✅ Other workloads in this namespace were NOT affected ║"
echo "║ ║"
echo "║ Note: The Trace CRD was NOT removed as it may be used by other ║"
echo "║ Inspector Gadget installations. To remove it manually: ║"
echo "║ $CLI_TOOL delete crd traces.gadget.kinvolk.io ║"
echo "║ ║"
echo "╚═══════════════════════════════════════════════════════════════════════════╝"
echo ""
'''
@router.get("/clusters/gadget-install-script", response_class=PlainTextResponse)
async def get_gadget_install_script(
provider: str = Query("openshift", description="Kubernetes provider: openshift, kubernetes"),
mode: str = Query("install", description="Script mode: install or uninstall"),
registry: str = Query("ghcr.io/inspektor-gadget/inspektor-gadget", description="Gadget image registry (e.g., harbor.example.com/flowfish/inspektor-gadget)"),
version: str = Query("v0.50.1", description="Gadget version tag"),
storage_class: str = Query("", description="StorageClass name for persistent gadget data (e.g., standard, gp2, managed-premium). If empty, uses emptyDir (data lost on pod restart)")
):
"""
Generate setup or uninstall script for remote cluster integration.
Install mode:
1. Installs Inspector Gadget for eBPF event collection
2. Creates a read-only ServiceAccount for Flowfish
3. Generates authentication token (1 year validity)
4. Outputs all connection details for Flowfish UI
5. (Optional) Creates PersistentVolumeClaims with specified StorageClass
Uninstall mode:
- Safely removes only Flowfish-related resources
- Validates namespace before deletion
Registry examples:
- ghcr.io/inspektor-gadget/inspektor-gadget (default, official)
- harbor.example.com/flowfish/inspektor-gadget (internal Harbor)
StorageClass examples:
- standard (GKE default)
- gp2, gp3 (AWS EKS)
- managed-premium (Azure AKS)
- thin, thick (vSphere)
- Leave empty to use emptyDir (ephemeral storage)
"""
try:
is_openshift = provider.lower() == "openshift"
cli_tool = "oc" if is_openshift else "kubectl"
# Return uninstall script if requested
if mode == "uninstall":
return generate_uninstall_script(cli_tool, provider.upper())
# Embedded YAML contents - no file dependencies
yaml_contents = {
"crds": """---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: traces.gadget.kinvolk.io
annotations:
controller-gen.kubebuilder.io/version: v0.9.2
spec:
group: gadget.kinvolk.io
names:
kind: Trace
listKind: TraceList
plural: traces
singular: trace
scope: Cluster
versions:
- name: v1alpha1
schema:
openAPIV3Schema:
description: Trace is the Schema for the traces API
properties:
apiVersion:
type: string
kind:
type: string
metadata:
type: object
spec:
properties:
filter:
properties:
containerName:
type: string
labels:
additionalProperties:
type: string
type: object
namespace:
type: string
podname:
type: string
type: object
gadget:
type: string
node:
type: string
output:
properties:
mode:
type: string
type: object
parameters:
additionalProperties:
type: string
type: object
runMode:
type: string
required:
- gadget
type: object
status:
properties:
operationError:
type: string
operationWarning:
type: string
output:
type: string
state:
type: string
type: object
type: object
served: true
storage: true
subresources:
status: {}
""",
"config": """---
apiVersion: v1
kind: ConfigMap
metadata:
name: inspektor-gadget-config
namespace: NAMESPACE_PLACEHOLDER
labels:
app: inspektor-gadget
data:
config.yaml: |
events-buffer-length: 16384
# Auto-detected by init container at pod startup
containerd-socketpath: CONTAINERD_SOCKET_AUTO
crio-socketpath: /run/crio/crio.sock
docker-socketpath: /run/docker.sock
podman-socketpath: /run/podman/podman.sock
gadget-namespace: "NAMESPACE_PLACEHOLDER"
daemon-log-level: info
operator:
kubemanager:
fallback-podinformer: true
hook-mode: auto
oci:
allowed-gadgets: []
disallow-pulling: false
verify-image: false
otel-metrics:
otel-metrics-listen: false
otel-metrics-listen-address: 0.0.0.0:2224
""",
"rbac": """---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: inspektor-gadget
rules:
# Core resources - for kubernetes enricher
- apiGroups: [""]
resources: ["pods", "nodes", "namespaces", "configmaps", "services", "events"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["configmaps"]
verbs: ["create", "update", "patch", "delete"]
# Apps resources - for owner reference enrichment
- apiGroups: ["apps"]
resources: ["deployments", "daemonsets", "replicasets", "statefulsets"]
verbs: ["get", "list", "watch"]
# Batch resources - REQUIRED for kubernetes enricher to resolve owner references
# Without this, gadget crashes when processing containers from Jobs/CronJobs
# causing core dump files (core-ocihookgadget-*) that fill up node disks
- apiGroups: ["batch"]
resources: ["jobs", "cronjobs"]
verbs: ["get", "list", "watch"]
# Gadget traces CRD
- apiGroups: ["gadget.kinvolk.io"]
resources: ["traces"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: ["apiextensions.k8s.io"]
resources: ["customresourcedefinitions"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: inspektor-gadget-NAMESPACE_PLACEHOLDER
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: inspektor-gadget
subjects:
- kind: ServiceAccount
name: inspektor-gadget
namespace: NAMESPACE_PLACEHOLDER
""",
"daemonset": """---
apiVersion: v1
kind: ServiceAccount
metadata:
name: inspektor-gadget
namespace: NAMESPACE_PLACEHOLDER
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: inspektor-gadget
namespace: NAMESPACE_PLACEHOLDER
labels:
app: inspektor-gadget
k8s-app: inspektor-gadget
spec:
selector:
matchLabels:
app: inspektor-gadget
updateStrategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 1
template:
metadata:
labels:
app: inspektor-gadget
k8s-app: gadget
annotations:
# NOTE: AppArmor annotation deprecated in K8s 1.30+, using securityContext.appArmorProfile
prometheus.io/scrape: "true"
prometheus.io/port: "2223"
prometheus.io/path: "/metrics"
spec:
hostPID: true
hostNetwork: true
dnsPolicy: ClusterFirstWithHostNet
serviceAccountName: inspektor-gadget
nodeSelector:
kubernetes.io/os: linux
# Exclude master/control-plane/infra nodes (CSI storage typically not available)
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: node-role.kubernetes.io/control-plane
operator: DoesNotExist
- key: node-role.kubernetes.io/master
operator: DoesNotExist
- key: node-role.kubernetes.io/infra
operator: DoesNotExist
tolerations:
- effect: NoSchedule
operator: Exists
- effect: NoExecute
operator: Exists
initContainers:
- name: detect-runtime
image: busybox:1.36
command: ['sh', '-c']
args:
- |
if [ -S /host/run/k3s/containerd/containerd.sock ]; then
SOCKET="/run/k3s/containerd/containerd.sock"
echo "Detected K3s/RKE2 containerd socket"
elif [ -S /host/run/containerd/containerd.sock ]; then
SOCKET="/run/containerd/containerd.sock"
echo "Detected standard containerd socket"
elif [ -S /host/var/snap/microk8s/common/run/containerd.sock ]; then
SOCKET="/host/var/snap/microk8s/common/run/containerd.sock"
echo "Detected MicroK8s containerd socket"
else
SOCKET="/run/containerd/containerd.sock"
echo "WARNING: No containerd socket found at known paths, using default"
fi
echo "Using containerd socket: $SOCKET"
sed "s|CONTAINERD_SOCKET_AUTO|$SOCKET|g" /config-template/config.yaml > /config-generated/config.yaml
volumeMounts:
- name: run
mountPath: /host/run
readOnly: true
- name: var
mountPath: /host/var
readOnly: true
- name: config
mountPath: /config-template
readOnly: true
- name: config-generated
mountPath: /config-generated
containers:
- name: gadget
image: GADGET_IMAGE_PLACEHOLDER
imagePullPolicy: Always
terminationMessagePolicy: FallbackToLogsOnError
# NOTE: Only -serve flag! -service-host breaks kubectl gadget connectivity
# kubectl gadget connects via Kubernetes API, not gRPC port
# This applies to BOTH in-cluster and remote clusters
command:
- /bin/gadgettracermanager
- -serve
lifecycle:
preStop:
exec:
command:
- /cleanup
env:
- name: NODE_NAME
valueFrom:
fieldRef:
fieldPath: spec.nodeName
- name: GADGET_POD_UID
valueFrom:
fieldRef:
fieldPath: metadata.uid
- name: GADGET_IMAGE
value: "GADGET_IMAGE_PLACEHOLDER"
- name: HOST_ROOT
value: "/host"
- name: IG_EXPERIMENTAL
value: "false"
securityContext:
readOnlyRootFilesystem: true
# AppArmor profile - K8s 1.30+ format (replaces deprecated annotation)
appArmorProfile:
type: Unconfined
seLinuxOptions:
type: spc_t
capabilities:
drop:
- ALL
add:
- SYS_ADMIN
- SYSLOG
- SYS_PTRACE
- SYS_RESOURCE
- IPC_LOCK
- NET_RAW
- NET_ADMIN
startupProbe:
exec:
command:
- /bin/gadgettracermanager
- -liveness
failureThreshold: 12
periodSeconds: 5
readinessProbe:
exec:
command:
- /bin/gadgettracermanager
- -liveness
periodSeconds: 5
timeoutSeconds: 2
livenessProbe:
exec:
command:
- /bin/gadgettracermanager
- -liveness
periodSeconds: 5
timeoutSeconds: 2
resources:
requests:
cpu: 100m
memory: 512Mi
limits:
cpu: "1"
memory: 12Gi
volumeMounts:
- name: bin
mountPath: /host/bin
readOnly: true
- name: etc
mountPath: /host/etc
- name: opt
mountPath: /host/opt
- name: usr
mountPath: /host/usr
readOnly: true
- name: run
mountPath: /host/run
readOnly: true
- name: var
mountPath: /host/var
readOnly: true
- name: proc
mountPath: /host/proc
readOnly: true
- name: run
mountPath: /run
- name: debugfs
mountPath: /sys/kernel/debug
- name: cgroup
mountPath: /sys/fs/cgroup
readOnly: true
- name: bpffs
mountPath: /sys/fs/bpf
- name: oci
mountPath: /var/lib/ig
- name: config-generated
mountPath: /etc/ig
readOnly: true
- name: wasm-cache
mountPath: /var/run/ig/wasm-cache
volumes:
- name: bin
hostPath:
path: /bin
- name: etc
hostPath:
path: /etc
- name: opt
hostPath:
path: /opt
- name: usr
hostPath:
path: /usr
- name: proc
hostPath:
path: /proc
- name: run
hostPath:
path: /run
- name: var
hostPath:
path: /var
- name: cgroup
hostPath:
path: /sys/fs/cgroup
- name: bpffs
hostPath:
path: /sys/fs/bpf
- name: debugfs
hostPath:
path: /sys/kernel/debug
- name: oci
emptyDir: {}
- name: config
configMap:
name: inspektor-gadget-config
defaultMode: 0400
- name: config-generated
emptyDir: {}
- name: wasm-cache
emptyDir: {}
---
# ClusterIP service (optional - kubectl gadget uses K8s API, not this service)
apiVersion: v1
kind: Service
metadata:
name: inspektor-gadget
namespace: NAMESPACE_PLACEHOLDER
labels:
app: inspektor-gadget
spec:
type: ClusterIP
ports:
- name: grpc
port: 16060
targetPort: 16060
protocol: TCP
selector:
app: inspektor-gadget
"""
}
provider_upper = provider.upper()
# Generate comprehensive setup script
# Determine storage class parameter for script
storage_class_default = storage_class if storage_class else ""
script = f'''#!/bin/bash
#
# ╔═══════════════════════════════════════════════════════════════════════════╗
# ║ Flowfish Remote Cluster Setup Script for {provider_upper:<12} ║
# ║ ║
# ║ This script: ║
# ║ 1. Installs Inspector Gadget for eBPF event collection ║
# ║ 2. Creates a READ-ONLY ServiceAccount for Flowfish ║
# ║ 3. Generates authentication token ║
# ║ 4. Outputs connection details for Flowfish UI ║
# ║ 5. (Optional) Configures persistent storage with StorageClass ║
# ╚═══════════════════════════════════════════════════════════════════════════╝
#
# Usage:
# chmod +x setup-flowfish-remote.sh
# ./setup-flowfish-remote.sh <namespace> [registry] [version] [storage_class]
# ./setup-flowfish-remote.sh # Interactive mode
#
# Examples:
# # Using official registry with emptyDir (default - ephemeral storage):
# ./setup-flowfish-remote.sh flowfish
#
# # Using internal Harbor registry with persistent storage:
# ./setup-flowfish-remote.sh flowfish harbor.example.com/flowfish/inspektor-gadget v0.50.1 standard
#
# # Using only storage class (with default registry and version):
# ./setup-flowfish-remote.sh flowfish "" "" gp2
#
# Arguments:
# namespace - Target namespace (required)
# registry - Gadget image registry (default: {registry})
# version - Gadget version tag (default: {version})
# storage_class - StorageClass for persistent data (optional, uses emptyDir if not specified)
#
# StorageClass Examples:
# - standard (GKE default)
# - gp2, gp3 (AWS EKS)
# - managed-premium (Azure AKS)
# - thin, thick (vSphere)
#
# Requirements:
# - {cli_tool} CLI installed and logged in
# - cluster-admin privileges (for RBAC and CRD creation)
# - Target namespace must exist
#
# Don't use set -e, we handle errors manually for better UX
# ═══════════════════════════════════════════════════════════════════════════
# Configuration
# ═══════════════════════════════════════════════════════════════════════════
CLI_TOOL="{cli_tool}"
SA_NAME="flowfish-remote-reader"
# Default values (can be overridden by arguments)
DEFAULT_REGISTRY="{registry}"
DEFAULT_VERSION="{version}"
DEFAULT_STORAGE_CLASS="{storage_class_default}"
# Parse arguments
NAMESPACE="${{1:-}}"
GADGET_REGISTRY="${{2:-$DEFAULT_REGISTRY}}"
GADGET_VERSION="${{3:-$DEFAULT_VERSION}}"
STORAGE_CLASS="${{4:-$DEFAULT_STORAGE_CLASS}}"
# Colors
RED='\\033[0;31m'
GREEN='\\033[0;32m'
YELLOW='\\033[1;33m'
BLUE='\\033[0;34m'
CYAN='\\033[0;36m'
BOLD='\\033[1m'
NC='\\033[0m'
print_status() {{ echo -e "${{BLUE}}[INFO]${{NC}} $1"; }}
print_success() {{ echo -e "${{GREEN}}[SUCCESS]${{NC}} $1"; }}
print_warning() {{ echo -e "${{YELLOW}}[WARNING]${{NC}} $1"; }}
print_error() {{ echo -e "${{RED}}[ERROR]${{NC}} $1"; }}
print_header() {{ echo -e "\\n${{CYAN}}${{BOLD}}═══ $1 ═══${{NC}}\\n"; }}
echo ""
echo "╔═══════════════════════════════════════════════════════════════════════════╗"
echo "║ Flowfish Remote Cluster Setup for {provider_upper:<12} ║"
echo "║ ║"
echo "║ 🔒 Security: Creates READ-ONLY access (no write permissions) ║"
echo "╚═══════════════════════════════════════════════════════════════════════════╝"
echo ""
# ═══════════════════════════════════════════════════════════════════════════
# Pre-flight Checks
# ═══════════════════════════════════════════════════════════════════════════
print_header "Pre-flight Checks"
if ! command -v $CLI_TOOL &> /dev/null; then
print_error "$CLI_TOOL CLI is not installed or not in PATH"
exit 1
fi
print_success "$CLI_TOOL CLI found"
if ! $CLI_TOOL whoami &> /dev/null; then
print_error "Not logged in. Please run '$CLI_TOOL login' first."
exit 1
fi
CURRENT_USER=$($CLI_TOOL whoami)
print_success "Logged in as: $CURRENT_USER"
# Check for cluster-admin
if ! $CLI_TOOL auth can-i create clusterrole &> /dev/null; then
print_error "You need cluster-admin privileges to run this script"
exit 1
fi
print_success "Cluster-admin privileges confirmed"
# Interactive mode if arguments not provided
if [ -z "$NAMESPACE" ]; then
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo -e "${{CYAN}}Namespace:${{NC}} Target namespace where Flowfish components will be installed"
echo -e " Example: flowfish, prod-flowfish"
read -p "Enter namespace: " NAMESPACE
echo ""
echo -e "${{CYAN}}Registry:${{NC}} Container registry for Inspektor Gadget image"
echo -e " Example: harbor.example.com/flowfish/inspektor-gadget"
echo -e " Default: $DEFAULT_REGISTRY"
read -p "Enter registry (press Enter for default): " INPUT_REGISTRY
GADGET_REGISTRY="${{INPUT_REGISTRY:-$DEFAULT_REGISTRY}}"
echo ""
echo -e "${{CYAN}}Version:${{NC}} Inspektor Gadget version tag"
echo -e " Example: v0.46.0, v0.50.1"
echo -e " Default: $DEFAULT_VERSION"
read -p "Enter version (press Enter for default): " INPUT_VERSION
GADGET_VERSION="${{INPUT_VERSION:-$DEFAULT_VERSION}}"
echo ""
echo -e "${{CYAN}}Storage Class:${{NC}} StorageClass for persistent gadget data (OCI images, WASM cache)"
echo -e " This prevents gadget from filling up node's local disk (emptyDir)"
echo -e " Leave empty to use emptyDir (ephemeral storage - data lost on pod restart)"
echo -e " Examples: standard (GKE), gp2/gp3 (AWS), managed-premium (Azure)"
if [ -n "$DEFAULT_STORAGE_CLASS" ]; then
echo -e " Default: $DEFAULT_STORAGE_CLASS"
fi
read -p "Enter storage class (press Enter for emptyDir): " INPUT_STORAGE_CLASS
STORAGE_CLASS="${{INPUT_STORAGE_CLASS:-$DEFAULT_STORAGE_CLASS}}"
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
fi
echo ""
print_status "Configuration:"
print_status " Namespace: $NAMESPACE"
print_status " Registry: $GADGET_REGISTRY"
print_status " Version: $GADGET_VERSION"
if [ -n "$STORAGE_CLASS" ]; then
print_status " Storage Class: $STORAGE_CLASS (persistent storage)"
else
print_status " Storage Class: (none - using emptyDir)"
fi
echo ""
if [ -z "$NAMESPACE" ]; then
print_error "Namespace cannot be empty!"
exit 1
fi
if ! $CLI_TOOL get namespace "$NAMESPACE" &> /dev/null; then
print_error "Namespace '$NAMESPACE' does not exist!"
print_status "Create it with: $CLI_TOOL create namespace $NAMESPACE"
exit 1
fi
print_success "Namespace '$NAMESPACE' exists"
# ═══════════════════════════════════════════════════════════════════════════
# Storage Class Validation (if specified)
# ═══════════════════════════════════════════════════════════════════════════
USE_PERSISTENT_STORAGE=false
if [ -n "$STORAGE_CLASS" ]; then
print_header "Storage Class Validation"
# Check Kubernetes version (ephemeral volumes require 1.23+)
print_status "Checking Kubernetes version compatibility..."
K8S_VERSION=$($CLI_TOOL version -o json 2>/dev/null | grep -o '"gitVersion": "[^"]*"' | head -1 | cut -d'"' -f4)
if [ -z "$K8S_VERSION" ]; then
K8S_VERSION=$($CLI_TOOL version --short 2>/dev/null | grep -i server | awk '{{print $NF}}')
fi
if [ -n "$K8S_VERSION" ]; then
print_status "Kubernetes version: $K8S_VERSION"
K8S_MAJOR=$(echo "$K8S_VERSION" | sed 's/v//' | cut -d. -f1)
K8S_MINOR=$(echo "$K8S_VERSION" | cut -d. -f2)
if [ "$K8S_MAJOR" -lt 1 ] || ([ "$K8S_MAJOR" -eq 1 ] && [ "$K8S_MINOR" -lt 23 ]); then
print_error "Kubernetes version $K8S_VERSION does not support ephemeral volumes!"
print_error "Ephemeral volumes require Kubernetes 1.23 or newer."
print_warning "Falling back to emptyDir storage..."
STORAGE_CLASS=""
else
print_success "Kubernetes version is compatible (1.23+ required)"
fi
else
print_warning "Could not determine Kubernetes version. Proceeding with StorageClass..."
fi
fi
if [ -n "$STORAGE_CLASS" ]; then
print_status "Checking if StorageClass '$STORAGE_CLASS' exists..."
if ! $CLI_TOOL get storageclass "$STORAGE_CLASS" &> /dev/null; then
print_error "StorageClass '$STORAGE_CLASS' not found!"
echo ""
print_status "Available StorageClasses in cluster:"
$CLI_TOOL get storageclass -o custom-columns=NAME:.metadata.name,PROVISIONER:.provisioner,RECLAIMPOLICY:.reclaimPolicy,DEFAULT:.metadata.annotations."storageclass\.kubernetes\.io/is-default-class" 2>/dev/null || \
$CLI_TOOL get storageclass 2>/dev/null || echo " (unable to list storage classes)"
echo ""
print_warning "Options:"
print_warning " 1. Use a valid StorageClass from the list above"
print_warning " 2. Re-run without storage class (uses emptyDir)"
exit 1
fi
print_success "StorageClass '$STORAGE_CLASS' exists"
# Check if StorageClass supports dynamic provisioning
PROVISIONER=$($CLI_TOOL get storageclass "$STORAGE_CLASS" -o jsonpath='{{.provisioner}}' 2>/dev/null)
print_status "Provisioner: $PROVISIONER"
# Check volume binding mode
BINDING_MODE=$($CLI_TOOL get storageclass "$STORAGE_CLASS" -o jsonpath='{{.volumeBindingMode}}' 2>/dev/null)
if [ "$BINDING_MODE" = "WaitForFirstConsumer" ]; then
print_success "Volume binding mode: WaitForFirstConsumer (recommended for DaemonSet)"
elif [ -n "$BINDING_MODE" ]; then
print_warning "Volume binding mode: $BINDING_MODE"
print_warning "Consider using WaitForFirstConsumer for better node affinity"
fi
USE_PERSISTENT_STORAGE=true
print_success "Persistent storage will be configured with StorageClass: $STORAGE_CLASS"
echo ""
fi
# ═══════════════════════════════════════════════════════════════════════════
# PART 1: Inspector Gadget Installation
# ═══════════════════════════════════════════════════════════════════════════
print_header "Part 1: Inspector Gadget Installation"
# Step 1: Apply Trace CRD
print_status "1/6 - Applying Trace CRD..."
cat <<'CRD_EOF' | $CLI_TOOL apply -f -
{yaml_contents["crds"]}
CRD_EOF
print_success "Trace CRD applied"
# Step 2: Create Security Context Constraint (OpenShift only)
if [ "$CLI_TOOL" = "oc" ]; then
print_status "2/6 - Creating Security Context Constraint (SCC)..."
cat <<'SCC_EOF' | $CLI_TOOL apply -f -
apiVersion: security.openshift.io/v1
kind: SecurityContextConstraints
metadata:
name: inspektor-gadget-scc
labels:
app.kubernetes.io/name: inspektor-gadget
allowHostDirVolumePlugin: true
allowHostIPC: false
allowHostNetwork: true
allowHostPID: true
allowHostPorts: true
allowPrivilegeEscalation: true
allowPrivilegedContainer: true
allowedCapabilities:
- SYS_ADMIN
- SYSLOG
- SYS_PTRACE
- SYS_RESOURCE
- IPC_LOCK
- NET_RAW
- NET_ADMIN
defaultAddCapabilities: null
fsGroup:
type: RunAsAny
priority: null
readOnlyRootFilesystem: true
requiredDropCapabilities: null
runAsUser:
type: RunAsAny
seLinuxContext:
type: RunAsAny
supplementalGroups:
type: RunAsAny
volumes:
- configMap
- downwardAPI
- emptyDir
- ephemeral
- hostPath
- persistentVolumeClaim
- projected
- secret
SCC_EOF
print_success "SCC created"
else
print_status "2/6 - Skipping SCC (not OpenShift)..."
fi
# Step 3: Apply RBAC for Gadget
print_status "3/6 - Applying Gadget RBAC..."
cat <<'RBAC_EOF' | sed "s/NAMESPACE_PLACEHOLDER/$NAMESPACE/g" | $CLI_TOOL apply -f -
{yaml_contents["rbac"]}
RBAC_EOF
print_success "Gadget RBAC applied"
# Step 4: Bind SCC to ServiceAccount (OpenShift only)
if [ "$CLI_TOOL" = "oc" ]; then
print_status "4/6 - Binding SCC to ServiceAccount..."
$CLI_TOOL adm policy add-scc-to-user inspektor-gadget-scc -z inspektor-gadget -n $NAMESPACE 2>/dev/null || true
print_success "SCC bound to ServiceAccount"
else
print_status "4/6 - Skipping SCC binding (not OpenShift)..."
fi
# Step 5: Create ConfigMap
print_status "5/6 - Creating ConfigMap..."
cat <<'CONFIG_EOF' | sed "s/NAMESPACE_PLACEHOLDER/$NAMESPACE/g" | $CLI_TOOL apply -f -
{yaml_contents["config"]}
CONFIG_EOF
print_success "ConfigMap created"
# Step 6: Deploy DaemonSet
print_status "6/6 - Deploying DaemonSet..."
GADGET_IMAGE="${{GADGET_REGISTRY}}:${{GADGET_VERSION}}"
print_status "Using Gadget image: $GADGET_IMAGE"
if [ "$USE_PERSISTENT_STORAGE" = true ]; then
print_status "Configuring with persistent storage (StorageClass: $STORAGE_CLASS)"
cat <<DAEMONSET_PVC_EOF | sed "s/NAMESPACE_PLACEHOLDER/$NAMESPACE/g" | sed "s|GADGET_IMAGE_PLACEHOLDER|$GADGET_IMAGE|g" | sed "s|STORAGE_CLASS_PLACEHOLDER|$STORAGE_CLASS|g" | $CLI_TOOL apply -f -
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: inspektor-gadget
namespace: NAMESPACE_PLACEHOLDER
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: inspektor-gadget
namespace: NAMESPACE_PLACEHOLDER
labels:
app: inspektor-gadget
k8s-app: inspektor-gadget
spec:
selector:
matchLabels:
app: inspektor-gadget
updateStrategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 1
template:
metadata:
labels:
app: inspektor-gadget
k8s-app: gadget
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "2223"
prometheus.io/path: "/metrics"
spec:
hostPID: true
hostNetwork: true
dnsPolicy: ClusterFirstWithHostNet
serviceAccountName: inspektor-gadget
nodeSelector:
kubernetes.io/os: linux
# Exclude master/control-plane/infra nodes (CSI storage typically not available)
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: node-role.kubernetes.io/control-plane
operator: DoesNotExist
- key: node-role.kubernetes.io/master
operator: DoesNotExist
- key: node-role.kubernetes.io/infra
operator: DoesNotExist
tolerations:
- effect: NoSchedule
operator: Exists
- effect: NoExecute
operator: Exists
initContainers:
- name: detect-runtime
image: busybox:1.36
command: ['sh', '-c']
args:
- |
if [ -S /host/run/k3s/containerd/containerd.sock ]; then
SOCKET="/run/k3s/containerd/containerd.sock"
echo "Detected K3s/RKE2 containerd socket"
elif [ -S /host/run/containerd/containerd.sock ]; then
SOCKET="/run/containerd/containerd.sock"
echo "Detected standard containerd socket"
elif [ -S /host/var/snap/microk8s/common/run/containerd.sock ]; then
SOCKET="/host/var/snap/microk8s/common/run/containerd.sock"
echo "Detected MicroK8s containerd socket"
else
SOCKET="/run/containerd/containerd.sock"
echo "WARNING: No containerd socket found at known paths, using default"
fi
echo "Using containerd socket: $SOCKET"
sed "s|CONTAINERD_SOCKET_AUTO|$SOCKET|g" /config-template/config.yaml > /config-generated/config.yaml
volumeMounts:
- name: run
mountPath: /host/run
readOnly: true
- name: var
mountPath: /host/var
readOnly: true
- name: config
mountPath: /config-template
readOnly: true
- name: config-generated
mountPath: /config-generated
containers:
- name: gadget
image: GADGET_IMAGE_PLACEHOLDER
imagePullPolicy: Always
terminationMessagePolicy: FallbackToLogsOnError
command:
- /bin/gadgettracermanager
- -serve
lifecycle:
preStop:
exec:
command:
- /cleanup
env:
- name: NODE_NAME
valueFrom:
fieldRef:
fieldPath: spec.nodeName
- name: GADGET_POD_UID
valueFrom:
fieldRef:
fieldPath: metadata.uid
- name: GADGET_IMAGE
value: "GADGET_IMAGE_PLACEHOLDER"
- name: HOST_ROOT
value: "/host"
- name: IG_EXPERIMENTAL
value: "false"
securityContext:
readOnlyRootFilesystem: true
appArmorProfile:
type: Unconfined
seLinuxOptions:
type: spc_t
capabilities:
drop:
- ALL
add:
- SYS_ADMIN
- SYSLOG
- SYS_PTRACE
- SYS_RESOURCE
- IPC_LOCK
- NET_RAW
- NET_ADMIN
startupProbe:
exec:
command:
- /bin/gadgettracermanager
- -liveness
failureThreshold: 12
periodSeconds: 5
readinessProbe:
exec:
command:
- /bin/gadgettracermanager
- -liveness
periodSeconds: 5
timeoutSeconds: 2
livenessProbe:
exec:
command:
- /bin/gadgettracermanager
- -liveness
periodSeconds: 5
timeoutSeconds: 2
resources:
requests:
cpu: 100m
memory: 512Mi
limits:
cpu: "1"
memory: 12Gi
volumeMounts:
- name: bin
mountPath: /host/bin
readOnly: true
- name: etc
mountPath: /host/etc
- name: opt
mountPath: /host/opt
- name: usr
mountPath: /host/usr
readOnly: true
- name: run
mountPath: /host/run
readOnly: true
- name: var
mountPath: /host/var
readOnly: true
- name: proc
mountPath: /host/proc
readOnly: true
- name: run
mountPath: /run
- name: debugfs
mountPath: /sys/kernel/debug
- name: cgroup
mountPath: /sys/fs/cgroup
readOnly: true
- name: bpffs
mountPath: /sys/fs/bpf
- name: oci
mountPath: /var/lib/ig
- name: config-generated
mountPath: /etc/ig
readOnly: true
- name: wasm-cache
mountPath: /var/run/ig/wasm-cache
volumes:
- name: bin
hostPath:
path: /bin
- name: etc
hostPath:
path: /etc
- name: opt
hostPath:
path: /opt
- name: usr
hostPath:
path: /usr
- name: proc
hostPath:
path: /proc
- name: run
hostPath:
path: /run
- name: var
hostPath:
path: /var
- name: cgroup
hostPath:
path: /sys/fs/cgroup
- name: bpffs
hostPath:
path: /sys/fs/bpf
- name: debugfs
hostPath:
path: /sys/kernel/debug
# Persistent storage for OCI images (gadget programs)
- name: oci
ephemeral:
volumeClaimTemplate:
metadata:
labels:
app: inspektor-gadget
volume-type: oci-storage
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: "STORAGE_CLASS_PLACEHOLDER"
resources:
requests:
storage: 10Gi
- name: config
configMap:
name: inspektor-gadget-config
defaultMode: 0400
- name: config-generated
emptyDir: {{}}
# Persistent storage for WASM cache
- name: wasm-cache
ephemeral:
volumeClaimTemplate:
metadata:
labels:
app: inspektor-gadget
volume-type: wasm-cache
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: "STORAGE_CLASS_PLACEHOLDER"
resources:
requests:
storage: 5Gi
---
apiVersion: v1
kind: Service
metadata:
name: inspektor-gadget
namespace: NAMESPACE_PLACEHOLDER
labels:
app: inspektor-gadget
spec:
type: ClusterIP
ports:
- name: grpc
port: 16060
targetPort: 16060
protocol: TCP
selector:
app: inspektor-gadget
DAEMONSET_PVC_EOF
print_success "DaemonSet deployed with persistent storage"
print_status "Ephemeral PVCs will be created automatically per pod"
else
print_status "Configuring with emptyDir (ephemeral storage)"
cat <<'DAEMONSET_EOF' | sed "s/NAMESPACE_PLACEHOLDER/$NAMESPACE/g" | sed "s|GADGET_IMAGE_PLACEHOLDER|$GADGET_IMAGE|g" | $CLI_TOOL apply -f -
{yaml_contents["daemonset"]}
DAEMONSET_EOF
print_success "DaemonSet deployed with emptyDir storage"
fi
# Restart pods to pick up SCC
print_status "Restarting Gadget pods to apply SCC..."
$CLI_TOOL delete pods -l app=inspektor-gadget -n $NAMESPACE --ignore-not-found=true 2>/dev/null || true
sleep 3
# Wait for pods
print_status "Waiting for Gadget pods to be ready (timeout: 180s)..."
if $CLI_TOOL wait --for=condition=ready pod -l app=inspektor-gadget -n "$NAMESPACE" --timeout=180s 2>/dev/null; then
print_success "All Gadget pods are ready!"
else
print_warning "Timeout waiting for pods. Check with: $CLI_TOOL get pods -l app=inspektor-gadget -n $NAMESPACE"
fi
# ═══════════════════════════════════════════════════════════════════════════
# PART 2: Flowfish Read-Only ServiceAccount
# ═══════════════════════════════════════════════════════════════════════════
print_header "Part 2: Flowfish Read-Only ServiceAccount"
print_status "Creating ServiceAccount '$SA_NAME'..."
cat <<EOF | $CLI_TOOL apply -f -
apiVersion: v1
kind: ServiceAccount
metadata:
name: $SA_NAME
namespace: $NAMESPACE
labels:
app.kubernetes.io/name: flowfish
app.kubernetes.io/component: remote-reader
app.kubernetes.io/purpose: readonly-access
EOF
print_success "ServiceAccount created"
print_status "Creating ClusterRole with READ-ONLY permissions..."
cat <<EOF | $CLI_TOOL apply -f -
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: flowfish-remote-reader
labels:
app.kubernetes.io/name: flowfish
app.kubernetes.io/purpose: readonly-access
rules:
# Core resources - READ ONLY
- apiGroups: [""]
resources: ["pods", "nodes", "namespaces", "services", "events", "endpoints"]
verbs: ["get", "list", "watch"]
# Apps resources - READ ONLY
- apiGroups: ["apps"]
resources: ["deployments", "daemonsets", "replicasets", "statefulsets"]
verbs: ["get", "list", "watch"]
# Batch resources - READ ONLY
- apiGroups: ["batch"]
resources: ["jobs", "cronjobs"]
verbs: ["get", "list", "watch"]
# Networking - READ ONLY
- apiGroups: ["networking.k8s.io"]
resources: ["networkpolicies", "ingresses"]
verbs: ["get", "list", "watch"]
# Inspector Gadget traces - READ ONLY
- apiGroups: ["gadget.kinvolk.io"]
resources: ["traces"]
verbs: ["get", "list", "watch"]
EOF
print_success "ClusterRole created (READ-ONLY permissions only)"
# Create namespace-scoped Role for kubectl gadget access
# This allows portforward/exec ONLY in the gadget namespace (not cluster-wide)
print_status "Creating namespace-scoped Role for kubectl gadget access..."
cat <<EOF | $CLI_TOOL apply -f -
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: flowfish-gadget-access
namespace: $NAMESPACE
labels:
app.kubernetes.io/name: flowfish
app.kubernetes.io/purpose: gadget-communication
rules:
# Required for kubectl gadget to communicate with Inspector Gadget pods
# These permissions are ONLY valid in this namespace (not cluster-wide)
- apiGroups: [""]
resources: ["pods/portforward"]
verbs: ["create"]
- apiGroups: [""]
resources: ["pods/exec"]
verbs: ["create"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: flowfish-gadget-access
namespace: $NAMESPACE
labels:
app.kubernetes.io/name: flowfish
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: flowfish-gadget-access
subjects:
- kind: ServiceAccount
name: $SA_NAME
namespace: $NAMESPACE
EOF
print_success "Namespace-scoped Role created (pods/portforward, pods/exec in $NAMESPACE only)"
print_status "Creating ClusterRoleBinding..."
cat <<EOF | $CLI_TOOL apply -f -
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: flowfish-remote-reader-$NAMESPACE
labels:
app.kubernetes.io/name: flowfish
app.kubernetes.io/purpose: readonly-access
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: flowfish-remote-reader
subjects:
- kind: ServiceAccount
name: $SA_NAME
namespace: $NAMESPACE
EOF
print_success "ClusterRoleBinding created"
# ═══════════════════════════════════════════════════════════════════════════
# PART 3: Generate Connection Details
# ═══════════════════════════════════════════════════════════════════════════
print_header "Part 3: Generating Connection Details"
# Get API Server URL
print_status "Getting API Server URL..."
API_SERVER=$($CLI_TOOL config view --minify -o jsonpath='{{.clusters[0].cluster.server}}')
print_success "API Server: $API_SERVER"
# Generate Token - use Secret-based method for OpenShift compatibility
print_status "Creating token Secret for ServiceAccount..."
cat <<EOF | $CLI_TOOL apply -f -
apiVersion: v1
kind: Secret
metadata:
name: $SA_NAME-token
namespace: $NAMESPACE
labels:
app.kubernetes.io/managed-by: flowfish
app.kubernetes.io/component: remote-reader
annotations:
kubernetes.io/service-account.name: $SA_NAME
type: kubernetes.io/service-account-token
EOF
print_status "Waiting for token to be generated..."
sleep 5
SA_TOKEN=$($CLI_TOOL get secret $SA_NAME-token -n $NAMESPACE -o jsonpath='{{.data.token}}' 2>/dev/null | base64 -d 2>/dev/null)
if [ -z "$SA_TOKEN" ]; then
print_warning "Token not in Secret yet. Trying oc create token..."
SA_TOKEN=$($CLI_TOOL create token $SA_NAME -n $NAMESPACE --duration=8760h 2>/dev/null || echo "")
fi
if [ -z "$SA_TOKEN" ]; then
print_error "Could not generate ServiceAccount token!"
echo ""
echo "Manual token generation command:"
echo " $CLI_TOOL create token $SA_NAME -n $NAMESPACE --duration=8760h"
echo ""
else
print_success "Token generated successfully"
fi
# Get CA Certificate
print_status "Getting CA Certificate..."
# First, get the API server URL to extract CA from connection
API_SERVER=$($CLI_TOOL config view --minify -o jsonpath='{{.clusters[0].cluster.server}}' 2>/dev/null || echo "")
API_HOST=$(echo "$API_SERVER" | sed -e 's|https://||' -e 's|:.*||')
API_PORT=$(echo "$API_SERVER" | sed -e 's|.*:||' -e 's|/.*||')
[ -z "$API_PORT" ] && API_PORT="6443"
# We'll collect CA certs from multiple sources and combine them
# This ensures compatibility with different cluster configurations:
# - Self-signed (OpenShift internal CAs)
# - Corporate PKI (external CA like BankSubCA2)
# - Public CA (DigiCert, Let's Encrypt - usually in system trust store)
# - Hybrid (both internal and external CAs needed)
CA_CERTS_COLLECTED=""
# ─────────────────────────────────────────────────────────────────────────────
# Source 1: API Server TLS Connection (for corporate/external PKI)
# ─────────────────────────────────────────────────────────────────────────────
# This gets the ACTUAL CA chain that signed the API server certificate
# Critical for corporate PKI where internal CAs don't match external certs
if command -v openssl &> /dev/null && [ -n "$API_HOST" ]; then
print_status "Checking API server certificate chain..."
# Get ALL certificates from the TLS handshake (full chain)
ALL_CERTS=$(echo | openssl s_client -connect "$API_HOST:$API_PORT" -servername "$API_HOST" -showcerts 2>/dev/null | \
sed -n '/-----BEGIN CERTIFICATE-----/,/-----END CERTIFICATE-----/p')
# Count certificates in chain
CERT_COUNT=$(echo "$ALL_CERTS" | grep -c "BEGIN CERTIFICATE" || echo "0")
if [ "$CERT_COUNT" -gt 1 ]; then
# Multiple certs = chain provided, skip first (leaf) and keep CA certs
CHAIN_CERTS=$(echo "$ALL_CERTS" | awk 'BEGIN{{n=0}} /-----BEGIN CERTIFICATE-----/{{n++}} n>1{{print}}')
if [ -n "$CHAIN_CERTS" ]; then
CA_CERTS_COLLECTED="$CHAIN_CERTS"
print_success "CA chain retrieved from API server ($((CERT_COUNT-1)) CA cert(s))"
fi
elif [ "$CERT_COUNT" -eq 1 ]; then
# Single cert = server doesn't send chain, try to get CA from AIA extension
ISSUER=$(echo "$ALL_CERTS" | openssl x509 -noout -issuer 2>/dev/null | sed 's/issuer=//')
print_warning "API server sends only leaf certificate"
print_warning "Issuer: $ISSUER"
# Try to extract CA URL from AIA (Authority Information Access) extension
AIA_URL=$(echo "$ALL_CERTS" | openssl x509 -noout -text 2>/dev/null | \
grep -A1 "CA Issuers" | grep -oE 'http://[^[:space:]]+\.crt' | head -1)
if [ -n "$AIA_URL" ]; then
print_status "Found CA download URL in certificate: $AIA_URL"
print_status "Attempting to download issuer CA..."
# Download the CA certificate (might be DER or PEM format)
CA_TEMP_FILE=$(mktemp)
if curl -sSf -o "$CA_TEMP_FILE" "$AIA_URL" 2>/dev/null; then
# Check if it's DER format (binary) and convert to PEM
if file "$CA_TEMP_FILE" 2>/dev/null | grep -q "data"; then
# Binary/DER format - convert to PEM
ISSUER_CA=$(openssl x509 -inform DER -in "$CA_TEMP_FILE" -outform PEM 2>/dev/null)
else
# Already PEM format
ISSUER_CA=$(cat "$CA_TEMP_FILE")
fi
if [ -n "$ISSUER_CA" ] && echo "$ISSUER_CA" | grep -q "BEGIN CERTIFICATE"; then
CA_CERTS_COLLECTED="$ISSUER_CA"
ISSUER_CN=$(echo "$ISSUER_CA" | openssl x509 -noout -subject 2>/dev/null | sed 's/.*CN = //' | sed 's/,.*//')
print_success "Downloaded issuer CA: $ISSUER_CN"
# Try to get the root CA if this is an intermediate
ROOT_AIA=$(echo "$ISSUER_CA" | openssl x509 -noout -text 2>/dev/null | \
grep -A1 "CA Issuers" | grep -oE 'http://[^[:space:]]+\.crt' | head -1)
if [ -n "$ROOT_AIA" ] && [ "$ROOT_AIA" != "$AIA_URL" ]; then
print_status "Found root CA URL: $ROOT_AIA"
ROOT_TEMP=$(mktemp)
if curl -sSf -o "$ROOT_TEMP" "$ROOT_AIA" 2>/dev/null; then
if file "$ROOT_TEMP" 2>/dev/null | grep -q "data"; then
ROOT_CA=$(openssl x509 -inform DER -in "$ROOT_TEMP" -outform PEM 2>/dev/null)
else
ROOT_CA=$(cat "$ROOT_TEMP")
fi
if [ -n "$ROOT_CA" ] && echo "$ROOT_CA" | grep -q "BEGIN CERTIFICATE"; then
CA_CERTS_COLLECTED="$CA_CERTS_COLLECTED
$ROOT_CA"
ROOT_CN=$(echo "$ROOT_CA" | openssl x509 -noout -subject 2>/dev/null | sed 's/.*CN = //' | sed 's/,.*//')
print_success "Downloaded root CA: $ROOT_CN"
fi
fi
rm -f "$ROOT_TEMP" 2>/dev/null
fi
else
print_warning "Downloaded file is not a valid certificate"
fi
else
print_warning "Could not download CA from $AIA_URL"
print_warning "You may need to manually provide the CA certificate"
fi
rm -f "$CA_TEMP_FILE" 2>/dev/null
else
print_warning "No CA download URL found in certificate"
print_warning "Will try other methods to find CA..."
fi
fi
fi
# ─────────────────────────────────────────────────────────────────────────────
# Source 2: Kubeconfig certificate-authority-data (OpenShift internal CAs)
# ─────────────────────────────────────────────────────────────────────────────
# This contains the CAs that the cluster administrator configured
# Usually OpenShift internal CAs for self-signed setups
KUBECONFIG_CA=""
CA_DATA=$($CLI_TOOL config view --raw --minify -o jsonpath='{{.clusters[0].cluster.certificate-authority-data}}' 2>/dev/null || echo "")
if [ -n "$CA_DATA" ]; then
KUBECONFIG_CA=$(echo "$CA_DATA" | base64 -d 2>/dev/null)
if [ -n "$KUBECONFIG_CA" ]; then
if [ -z "$CA_CERTS_COLLECTED" ]; then
CA_CERTS_COLLECTED="$KUBECONFIG_CA"
print_success "CA Certificate retrieved from kubeconfig"
else
# Append if not already included (avoid duplicates)
if ! echo "$CA_CERTS_COLLECTED" | grep -q "$(echo "$KUBECONFIG_CA" | head -5)"; then
CA_CERTS_COLLECTED="$CA_CERTS_COLLECTED
$KUBECONFIG_CA"
print_success "Added kubeconfig CA to bundle"
fi
fi
fi
fi
# ─────────────────────────────────────────────────────────────────────────────
# Source 3: ServiceAccount token secret (cluster internal CA)
# ─────────────────────────────────────────────────────────────────────────────
SA_CA=""
SA_CA_DATA=$($CLI_TOOL get secret $SA_NAME-token -n $NAMESPACE -o jsonpath='{{.data.ca\\.crt}}' 2>/dev/null || echo "")
if [ -n "$SA_CA_DATA" ]; then
SA_CA=$(echo "$SA_CA_DATA" | base64 -d 2>/dev/null)
if [ -n "$SA_CA" ]; then
if [ -z "$CA_CERTS_COLLECTED" ]; then
CA_CERTS_COLLECTED="$SA_CA"
print_success "CA Certificate retrieved from token secret"
else
# Check if this adds new certs
if ! echo "$CA_CERTS_COLLECTED" | grep -q "$(echo "$SA_CA" | head -5)"; then
CA_CERTS_COLLECTED="$CA_CERTS_COLLECTED
$SA_CA"
print_success "Added ServiceAccount CA to bundle"
fi
fi
fi
fi
# ─────────────────────────────────────────────────────────────────────────────
# Final CA Bundle
# ─────────────────────────────────────────────────────────────────────────────
if [ -n "$CA_CERTS_COLLECTED" ]; then
CA_CERT_DECODED="$CA_CERTS_COLLECTED"
# Count total CA certs in bundle
TOTAL_CAS=$(echo "$CA_CERT_DECODED" | grep -c "BEGIN CERTIFICATE" || echo "0")
print_success "CA bundle ready with $TOTAL_CAS certificate(s)"
else
print_warning "Could not retrieve CA Certificate automatically."
print_warning "Options:"
print_warning " 1. Enable 'Skip TLS Verify' in Flowfish UI"
print_warning " 2. Manually provide CA certificate (see commands below)"
fi
# Inspector Gadget Namespace (used by Flowfish for kubectl gadget commands)
# NOTE: gadget_endpoint is no longer needed - Flowfish uses kubectl gadget via K8s API
print_success "Inspector Gadget namespace: $NAMESPACE"
# ═══════════════════════════════════════════════════════════════════════════
# PART 4: Output Connection Details for Flowfish UI
# ═══════════════════════════════════════════════════════════════════════════
echo ""
echo ""
echo "╔═══════════════════════════════════════════════════════════════════════════╗"
echo "║ ║"
echo "║ ✅ SETUP COMPLETE - Copy these values to Flowfish UI ║"
echo "║ ║"
echo "╚═══════════════════════════════════════════════════════════════════════════╝"
echo ""
echo "┌─────────────────────────────────────────────────────────────────────────────┐"
echo "│ 📌 FLOWFISH UI - ADD CLUSTER FORM │"
echo "├─────────────────────────────────────────────────────────────────────────────┤"
echo "│ │"
echo "│ Connection Type: Token │"
echo "│ │"
echo "│ ─────────────────────────────────────────────────────────────────────────── │"
echo "│ │"
echo "│ API Server URL: │"
echo "├─────────────────────────────────────────────────────────────────────────────┤"
echo "$API_SERVER"
echo "├─────────────────────────────────────────────────────────────────────────────┤"
echo "│ │"
echo "│ Service Account Token: │"
echo "├─────────────────────────────────────────────────────────────────────────────┤"
echo "$SA_TOKEN"
echo "├─────────────────────────────────────────────────────────────────────────────┤"
echo "│ │"
echo "│ Inspector Gadget Namespace: │"
echo "├─────────────────────────────────────────────────────────────────────────────┤"
echo "$NAMESPACE"
echo "├─────────────────────────────────────────────────────────────────────────────┤"
echo "│ │"
echo "│ CA Certificate: │"
echo "├─────────────────────────────────────────────────────────────────────────────┤"
if [ -n "$CA_CERT_DECODED" ]; then
echo "$CA_CERT_DECODED"
else
echo "(Not retrieved automatically - use manual command below or enable 'Skip TLS Verify')"
fi
echo "└─────────────────────────────────────────────────────────────────────────────┘"
echo ""
echo "┌─────────────────────────────────────────────────────────────────────────────┐"
echo "│ 🔒 SECURITY SUMMARY │"
echo "├─────────────────────────────────────────────────────────────────────────────┤"
echo "│ ✅ ServiceAccount: $SA_NAME (READ-ONLY)"
echo "│ ✅ Permissions: GET, LIST, WATCH only (no write access)"
echo "│ ✅ Token Validity: 1 year"
echo "│ ✅ Namespace: $NAMESPACE"
if [ "$USE_PERSISTENT_STORAGE" = true ]; then
echo "│ ✅ Storage: Persistent (StorageClass: $STORAGE_CLASS)"
echo "│ - OCI volume: 10Gi per node"
echo "│ - WASM cache: 5Gi per node"
else
echo "│ ⚠️ Storage: emptyDir (ephemeral - data lost on pod restart)"
fi
echo "└─────────────────────────────────────────────────────────────────────────────┘"
echo ""
echo "┌─────────────────────────────────────────────────────────────────────────────┐"
echo "│ 📝 MANUAL COMMANDS (if values above are empty) │"
echo "├─────────────────────────────────────────────────────────────────────────────┤"
echo "│ │"
echo "│ Get Token: │"
echo "│ $CLI_TOOL create token $SA_NAME -n $NAMESPACE --duration=8760h │"
echo "│ │"
echo "│ Get CA Certificate (choose based on your environment): │"
echo "│ │"
echo "│ Option 1 - From API Server connection (Corporate PKI): │"
echo "│ echo | openssl s_client -connect $API_HOST:$API_PORT -showcerts 2>/dev/null | sed -n '/BEGIN/,/END/p'"
echo "│ │"
echo "│ Option 2 - From kubeconfig (Self-signed/Internal): │"
echo "│ $CLI_TOOL config view --raw -o jsonpath='{{.clusters[0].cluster.certificate-authority-data}}' | base64 -d"
echo "│ │"
echo "│ Option 3 - From ConfigMap (Kubernetes internal): │"
echo "│ $CLI_TOOL get configmap kube-root-ca.crt -n $NAMESPACE -o jsonpath='{{.data.ca\\.crt}}'"
echo "│ │"
echo "│ Option 4 - From ServiceAccount secret: │"
echo "│ $CLI_TOOL get secret $SA_NAME-token -n $NAMESPACE -o jsonpath='{{.data.ca\\.crt}}' | base64 -d"
echo "│ │"
echo "└─────────────────────────────────────────────────────────────────────────────┘"
echo ""
echo "┌─────────────────────────────────────────────────────────────────────────────┐"
echo "│ 🔍 VERIFICATION COMMANDS │"
echo "├─────────────────────────────────────────────────────────────────────────────┤"
echo "│ Check Gadget pods: $CLI_TOOL get pods -l app=inspektor-gadget -n $NAMESPACE"
echo "│ Check ServiceAccount: $CLI_TOOL get sa $SA_NAME -n $NAMESPACE"
echo "│ View Gadget logs: $CLI_TOOL logs -l app=inspektor-gadget -n $NAMESPACE --tail=20"
echo "└─────────────────────────────────────────────────────────────────────────────┘"
echo ""
print_success "Setup complete! Copy the values above to Flowfish UI and click 'Test Connection'"
echo ""
'''
logger.info("Generated complete setup script", provider=provider)
return script
except Exception as e:
logger.error("Failed to generate setup script", error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to generate script: {str(e)}"
)
@router.get("/clusters/{cluster_id}")
async def get_cluster(cluster_id: int):
"""Get cluster by ID"""
try:
query = """
SELECT id, name, description, environment, provider, region,
connection_type, api_server_url, gadget_namespace, gadget_endpoint,
gadget_health_status, gadget_version, status,
total_nodes, total_pods, total_namespaces,
k8s_version, created_at, updated_at
FROM clusters
WHERE id = :cluster_id
"""
cluster = await database.fetch_one(query, {"cluster_id": cluster_id})
if not cluster:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Cluster {cluster_id} not found"
)
return dict(cluster)
except HTTPException:
raise
except Exception as e:
logger.error("Get cluster failed", error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to retrieve cluster: {str(e)}"
)
@router.patch("/clusters/{cluster_id}")
async def update_cluster(cluster_id: int, cluster_data: ClusterUpdate):
"""
Update cluster configuration.
All fields are optional - only provided fields will be updated.
Sensitive fields (token, kubeconfig, ca_cert) are only updated if explicitly provided.
Empty strings for sensitive fields are ignored to prevent accidental clearing.
"""
try:
# Check if cluster exists
existing = await database.fetch_one(
"SELECT id, name, connection_type FROM clusters WHERE id = :cluster_id",
{"cluster_id": cluster_id}
)
if not existing:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Cluster {cluster_id} not found"
)
# Build update query dynamically
updates = []
params = {"cluster_id": cluster_id}
# Basic fields
if cluster_data.name is not None:
# Check if new name conflicts with another cluster
if cluster_data.name != existing["name"]:
name_check = await database.fetch_one(
"SELECT id FROM clusters WHERE name = :name AND id != :cluster_id AND status != 'deleted'",
{"name": cluster_data.name, "cluster_id": cluster_id}
)
if name_check:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Cluster with name '{cluster_data.name}' already exists"
)
updates.append("name = :name")
params["name"] = cluster_data.name
if cluster_data.description is not None:
updates.append("description = :description")
params["description"] = cluster_data.description
if cluster_data.environment is not None:
updates.append("environment = :environment")
params["environment"] = cluster_data.environment
if cluster_data.provider is not None:
updates.append("provider = :provider")
params["provider"] = cluster_data.provider
if cluster_data.region is not None:
updates.append("region = :region")
params["region"] = cluster_data.region
if cluster_data.api_server_url is not None:
updates.append("api_server_url = :api_server_url")
params["api_server_url"] = cluster_data.api_server_url
if cluster_data.gadget_namespace is not None:
updates.append("gadget_namespace = :gadget_namespace")
params["gadget_namespace"] = cluster_data.gadget_namespace
if cluster_data.status is not None:
updates.append("status = :status")
params["status"] = cluster_data.status
if cluster_data.skip_tls_verify is not None:
updates.append("skip_tls_verify = :skip_tls_verify")
params["skip_tls_verify"] = cluster_data.skip_tls_verify
# Sensitive fields - only update if non-empty value provided
# This prevents accidental clearing of credentials
if cluster_data.token is not None and cluster_data.token.strip():
updates.append("token = :token")
params["token"] = cluster_data.token
if cluster_data.kubeconfig is not None and cluster_data.kubeconfig.strip():
updates.append("kubeconfig = :kubeconfig")
params["kubeconfig"] = cluster_data.kubeconfig
if cluster_data.ca_cert is not None and cluster_data.ca_cert.strip():
updates.append("ca_cert = :ca_cert")
params["ca_cert"] = cluster_data.ca_cert
if not updates:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="No fields to update"
)
# Always update updated_at
updates.append("updated_at = NOW()")
query = f"""
UPDATE clusters
SET {', '.join(updates)}
WHERE id = :cluster_id
"""
await database.execute(query, params)
# Return updated cluster (without sensitive fields)
updated = await database.fetch_one(
"""SELECT id, name, description, environment, provider, region,
connection_type, api_server_url, gadget_namespace, gadget_endpoint,
gadget_health_status, gadget_version, status,
total_nodes, total_pods, total_namespaces, k8s_version,
skip_tls_verify, created_at, updated_at
FROM clusters WHERE id = :cluster_id""",
{"cluster_id": cluster_id}
)
logger.info("Cluster updated", cluster_id=cluster_id, updated_fields=list(params.keys()))
return {
"message": "Cluster updated successfully",
"cluster": dict(updated) if updated else None
}
except HTTPException:
raise
except Exception as e:
logger.error("Update cluster failed", error=str(e), cluster_id=cluster_id)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to update cluster: {str(e)}"
)
@router.delete("/clusters/{cluster_id}")
async def delete_cluster(cluster_id: int):
"""Delete cluster (soft delete)"""
try:
# Check if cluster exists
existing = await database.fetch_one(
"SELECT id, name FROM clusters WHERE id = :cluster_id",
{"cluster_id": cluster_id}
)
if not existing:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Cluster {cluster_id} not found"
)
# Soft delete - set status to 'deleted'
await database.execute(
"UPDATE clusters SET status = 'deleted', updated_at = NOW() WHERE id = :cluster_id",
{"cluster_id": cluster_id}
)
logger.info("Cluster deleted", cluster_id=cluster_id, cluster_name=existing["name"])
return {
"message": f"Cluster '{existing['name']}' deleted successfully"
}
except HTTPException:
raise
except Exception as e:
logger.error("Delete cluster failed", error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to delete cluster: {str(e)}"
)
@router.post("/clusters/{cluster_id}/sync")
async def sync_cluster(cluster_id: int):
"""Sync cluster information (workloads, nodes, etc.)"""
try:
cluster = await database.fetch_one(
"""SELECT id, name, connection_type, api_server_url, kubeconfig_encrypted,
token_encrypted, ca_cert_encrypted, skip_tls_verify, gadget_namespace
FROM clusters WHERE id = :cluster_id AND status = 'active'""",
{"cluster_id": cluster_id}
)
if not cluster:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Active cluster {cluster_id} not found"
)
logger.info("Starting cluster sync", cluster_id=cluster_id, cluster_name=cluster['name'])
# Use unified ClusterConnectionManager for all connection types
cluster_info = await cluster_connection_manager.get_cluster_info(cluster_id)
logger.info("Starting gadget health check via ClusterConnectionManager", cluster_id=cluster_id)
try:
gadget_health = await cluster_connection_manager.check_gadget_health(cluster_id)
logger.info("Gadget health check result",
health_status=gadget_health.get("health_status"),
version=gadget_health.get("version"),
error=gadget_health.get("error"),
pods_ready=gadget_health.get("pods_ready"),
pods_total=gadget_health.get("pods_total"))
except Exception as health_err:
logger.error("Gadget health check exception", error=str(health_err))
gadget_health = {"health_status": "unknown", "error": str(health_err)}
# Update cluster with fetched info
# Even if cluster_info has error, we still update gadget health
cluster_info_error = cluster_info.get("error")
if not cluster_info_error:
# Full sync - both cluster info and gadget health available
await database.execute(
"""UPDATE clusters SET
total_nodes = :total_nodes,
total_pods = :total_pods,
total_namespaces = :total_namespaces,
k8s_version = :k8s_version,
gadget_health_status = :gadget_health_status,
gadget_version = :gadget_version,
updated_at = NOW()
WHERE id = :cluster_id""",
{
"cluster_id": cluster_id,
"total_nodes": cluster_info.get("total_nodes", 0),
"total_pods": cluster_info.get("total_pods", 0),
"total_namespaces": cluster_info.get("total_namespaces", 0),
"k8s_version": cluster_info.get("k8s_version"),
"gadget_health_status": gadget_health.get("health_status", "unknown"),
"gadget_version": gadget_health.get("version")
}
)
logger.info("Cluster sync completed",
cluster_id=cluster_id,
nodes=cluster_info.get("total_nodes", 0),
pods=cluster_info.get("total_pods", 0),
namespaces=cluster_info.get("total_namespaces", 0))
# Proactively refresh cache after successful sync
try:
await cluster_cache_service.refresh_cluster_cache(cluster_id)
logger.info("Cache refreshed after sync", cluster_id=cluster_id)
except Exception as cache_err:
logger.warning("Cache refresh failed after sync",
cluster_id=cluster_id,
error=str(cache_err))
return {
"message": f"Cluster '{cluster['name']}' synced successfully",
"status": "completed",
"resources": {
"nodes": cluster_info.get("total_nodes", 0),
"pods": cluster_info.get("total_pods", 0),
"namespaces": cluster_info.get("total_namespaces", 0)
},
"gadget_health": gadget_health.get("health_status", "unknown"),
"gadget_details": {
"version": gadget_health.get("version"),
"error": gadget_health.get("error"),
"pods_ready": gadget_health.get("pods_ready", 0),
"pods_total": gadget_health.get("pods_total", 0),
"details": gadget_health.get("details", {})
}
}
else:
# Partial sync - cluster info failed but gadget health may be available
logger.warning("Cluster info fetch failed, updating gadget health only",
cluster_id=cluster_id,
error=cluster_info_error)
# Still update gadget health even if cluster info failed
await database.execute(
"""UPDATE clusters SET
gadget_health_status = :gadget_health_status,
gadget_version = :gadget_version,
updated_at = NOW()
WHERE id = :cluster_id""",
{
"cluster_id": cluster_id,
"gadget_health_status": gadget_health.get("health_status", "unknown"),
"gadget_version": gadget_health.get("version")
}
)
# Return partial success instead of 500 error
return {
"message": f"Cluster '{cluster['name']}' partially synced - cluster info unavailable",
"status": "partial",
"warning": f"Cluster info fetch failed: {cluster_info_error}",
"resources": None,
"gadget_health": gadget_health.get("health_status", "unknown"),
"gadget_details": {
"version": gadget_health.get("version"),
"error": gadget_health.get("error"),
"pods_ready": gadget_health.get("pods_ready", 0),
"pods_total": gadget_health.get("pods_total", 0),
"details": gadget_health.get("details", {})
}
}
except HTTPException:
raise
except Exception as e:
logger.error("Cluster sync failed", error=str(e), exc_info=True)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to sync cluster: {str(e)}"
)
class ConnectionTestRequest(BaseModel):
"""Request model for connection test"""
connection_type: str # 'in-cluster', 'kubeconfig', 'token'
api_server_url: Optional[str] = None
token: Optional[str] = None
ca_cert: Optional[str] = None
skip_tls_verify: Optional[bool] = False
gadget_namespace: str # Namespace where gadget is deployed (REQUIRED from UI)
@router.post("/clusters/test-connection")
async def test_cluster_connection(test_data: ConnectionTestRequest):
"""
Test cluster connection before creating.
This endpoint allows users to verify their cluster credentials
and Inspector Gadget endpoint before creating a cluster.
Uses ClusterConnectionManager.test_connection() for unified logic.
Returns detailed connection status and any errors.
"""
try:
# Validate required fields based on connection type
normalized_type = test_data.connection_type.replace('_', '-').lower() if test_data.connection_type else ""
if normalized_type == "token":
if not test_data.api_server_url:
raise ValueError("API Server URL is required for token authentication")
if not test_data.token:
raise ValueError("Token is required for token authentication")
# Use ClusterConnectionManager for unified connection testing
test_result = await cluster_connection_manager.test_connection(
connection_type=test_data.connection_type,
api_server_url=test_data.api_server_url,
token=test_data.token,
ca_cert=test_data.ca_cert,
kubeconfig=test_data.token if normalized_type == "kubeconfig" else None,
skip_tls_verify=test_data.skip_tls_verify or False,
gadget_namespace=test_data.gadget_namespace
)
# Add recommendations based on errors
result = {
"cluster_connection": test_result["cluster_connection"],
"gadget_connection": test_result["gadget_connection"],
"overall_status": test_result["overall_status"],
"recommendations": []
}
# Add cluster connection recommendations
if result["cluster_connection"]["status"] == "failed":
error = result["cluster_connection"].get("error", "").lower()
if "certificate" in error or "ssl" in error:
result["recommendations"].append(
"Certificate verification failed. Try enabling 'Skip TLS Verify' or provide a valid CA certificate."
)
elif "unauthorized" in error or "401" in error:
result["recommendations"].append(
"Authentication failed. Verify your token has the correct permissions."
)
elif "connection" in error or "timeout" in error:
result["recommendations"].append(
"Cannot connect to the API server. Verify the URL and network connectivity."
)
elif "token" in error or "required" in error:
result["recommendations"].append(
"Please provide the required authentication credentials."
)
# Add gadget connection recommendations
if result["gadget_connection"]["status"] == "failed":
error = result["gadget_connection"].get("error", "")
result["recommendations"].append(
f"Inspector Gadget pods not healthy in namespace '{test_data.gadget_namespace}'. Check DaemonSet status."
)
elif result["gadget_connection"]["status"] == "warning":
result["recommendations"].append(
"Inspector Gadget may be degraded. Check the DaemonSet status on the cluster."
)
elif result["gadget_connection"]["status"] == "skipped":
result["recommendations"].append(
"Provide an Inspector Gadget endpoint for full functionality."
)
logger.info("Connection test completed",
connection_type=test_data.connection_type,
overall_status=result["overall_status"])
return result
except Exception as e:
logger.error("Connection test failed", error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Connection test failed: {str(e)}"
)
@router.get("/clusters/{cluster_id}/gadget-upgrade-script", response_class=PlainTextResponse)
async def get_gadget_upgrade_script(
cluster_id: int,
target_version: str = Query("v0.50.1", description="Target gadget version"),
memory_limit: str = Query("6Gi", description="Memory limit for gadget containers"),
):
"""
Generate a cluster-specific upgrade script for Inspektor Gadget DaemonSet.
Pre-fills parameters from the cluster's current configuration.
"""
try:
query = """
SELECT id, name, gadget_namespace, gadget_version, connection_type
FROM clusters WHERE id = :id AND status != 'deleted'
"""
cluster = await database.fetch_one(query, {"id": cluster_id})
if not cluster:
raise HTTPException(status_code=404, detail="Cluster not found")
namespace = cluster['gadget_namespace'] or 'flowfish'
current_version = cluster['gadget_version'] or 'unknown'
cluster_name = cluster['name']
script = f'''#!/bin/bash
# =========================================================================
# Inspektor Gadget Upgrade Script
# Generated for cluster: {cluster_name} (ID: {cluster_id})
# Current version: {current_version}
# Target version: {target_version}
# =========================================================================
#
# Usage:
# chmod +x upgrade-gadget.sh
# ./upgrade-gadget.sh
#
# The script will interactively ask for configuration and validate
# each step before making changes. Safe to run - nothing changes
# without your explicit confirmation.
#
# =========================================================================
# Colors
RED='\\033[0;31m'
GREEN='\\033[0;32m'
YELLOW='\\033[1;33m'
BLUE='\\033[0;34m'
CYAN='\\033[0;36m'
BOLD='\\033[1m'
NC='\\033[0m'
print_info() {{ echo -e "${{BLUE}}[INFO]${{NC}} $1"; }}
print_ok() {{ echo -e "${{GREEN}}[OK]${{NC}} $1"; }}
print_warn() {{ echo -e "${{YELLOW}}[WARN]${{NC}} $1"; }}
print_err() {{ echo -e "${{RED}}[ERROR]${{NC}} $1"; }}
print_header() {{ echo -e "\\n${{CYAN}}${{BOLD}}=== $1 ===${{NC}}\\n"; }}
confirm_step() {{
local msg="${{1:-Continue?}}"
read -p "$msg (y/N): " ans
if [[ ! "$ans" =~ ^[Yy]$ ]]; then
echo "Cancelled by user."
exit 0
fi
}}
echo ""
echo "======================================================================="
echo " Inspektor Gadget Upgrade"
echo " Cluster: {cluster_name}"
echo "======================================================================="
echo ""
# =========================================================================
# Step 1: Pre-flight Checks
# =========================================================================
print_header "Pre-flight Checks"
# Detect CLI tool
if command -v oc &>/dev/null; then
CLI_TOOL="oc"
elif command -v kubectl &>/dev/null; then
CLI_TOOL="kubectl"
else
print_err "Neither oc nor kubectl found in PATH"
exit 1
fi
print_ok "CLI tool: $CLI_TOOL"
# Check cluster connectivity and auth
if [ "$CLI_TOOL" = "oc" ]; then
if ! $CLI_TOOL whoami &>/dev/null 2>&1; then
print_err "Not logged in. Run 'oc login' first."
exit 1
fi
CURRENT_USER=$($CLI_TOOL whoami 2>/dev/null)
else
if ! $CLI_TOOL cluster-info &>/dev/null 2>&1; then
print_err "Cannot reach cluster. Check your kubeconfig and context."
exit 1
fi
CURRENT_USER=$($CLI_TOOL config current-context 2>/dev/null || echo "unknown")
fi
print_ok "Logged in as: $CURRENT_USER"
# Check permissions
if ! $CLI_TOOL auth can-i patch daemonset -n "{namespace}" &>/dev/null 2>&1; then
print_warn "May not have permission to patch DaemonSets in {namespace}"
print_warn "Upgrade might fail at the apply step"
fi
# =========================================================================
# Step 2: Interactive Configuration
# =========================================================================
print_header "Configuration"
# Namespace
DEFAULT_NAMESPACE="{namespace}"
echo -e "${{CYAN}}Namespace:${{NC}} Where Inspektor Gadget is deployed"
echo -e " Default: $DEFAULT_NAMESPACE"
read -p "Enter namespace (press Enter for default): " INPUT_NS
NAMESPACE="${{INPUT_NS:-$DEFAULT_NAMESPACE}}"
# Validate namespace exists
if ! $CLI_TOOL get namespace "$NAMESPACE" &>/dev/null 2>&1; then
print_err "Namespace '$NAMESPACE' does not exist!"
exit 1
fi
print_ok "Namespace '$NAMESPACE' exists"
# Validate DaemonSet exists
if ! $CLI_TOOL get daemonset inspektor-gadget -n "$NAMESPACE" &>/dev/null 2>&1; then
print_err "DaemonSet 'inspektor-gadget' not found in namespace '$NAMESPACE'"
print_info "Check: $CLI_TOOL get daemonset -n $NAMESPACE"
exit 1
fi
print_ok "DaemonSet 'inspektor-gadget' found"
echo ""
# Read current state from cluster
CURRENT_IMAGE=$($CLI_TOOL get daemonset inspektor-gadget -n "$NAMESPACE" \\
-o jsonpath='{{.spec.template.spec.containers[0].image}}' 2>/dev/null || echo "unknown")
CURRENT_REGISTRY=$(echo "$CURRENT_IMAGE" | sed "s|:.*||")
CURRENT_VERSION=$(echo "$CURRENT_IMAGE" | grep -oE 'v[0-9]+\\.[0-9]+\\.[0-9]+' || echo "unknown")
CURRENT_MEM=$($CLI_TOOL get daemonset inspektor-gadget -n "$NAMESPACE" \\
-o jsonpath='{{.spec.template.spec.containers[0].resources.limits.memory}}' 2>/dev/null || echo "unknown")
CURRENT_READY=$($CLI_TOOL get daemonset inspektor-gadget -n "$NAMESPACE" \\
-o jsonpath='{{.status.numberReady}}' 2>/dev/null || echo "0")
CURRENT_DESIRED=$($CLI_TOOL get daemonset inspektor-gadget -n "$NAMESPACE" \\
-o jsonpath='{{.status.desiredNumberScheduled}}' 2>/dev/null || echo "0")
print_info "Current state:"
print_info " Image: $CURRENT_IMAGE"
print_info " Version: $CURRENT_VERSION"
print_info " Registry: $CURRENT_REGISTRY"
print_info " Memory Limit: $CURRENT_MEM"
print_info " Pods Ready: $CURRENT_READY / $CURRENT_DESIRED"
echo ""
# Check if pods are healthy before upgrade
if [ "$CURRENT_READY" != "$CURRENT_DESIRED" ] || [ "$CURRENT_READY" = "0" ]; then
print_warn "Not all pods are ready ($CURRENT_READY / $CURRENT_DESIRED)"
print_warn "Upgrading an unhealthy DaemonSet may cause issues"
confirm_step "Continue anyway?"
fi
# Target version
DEFAULT_TARGET="{target_version}"
echo -e "${{CYAN}}Target Version:${{NC}} Version to upgrade to"
echo -e " Current: $CURRENT_VERSION"
echo -e " Default: $DEFAULT_TARGET"
read -p "Enter target version (press Enter for default): " INPUT_VER
TARGET_VERSION="${{INPUT_VER:-$DEFAULT_TARGET}}"
if [ "$TARGET_VERSION" = "$CURRENT_VERSION" ]; then
print_warn "Target version ($TARGET_VERSION) is the same as current ($CURRENT_VERSION)"
confirm_step "Continue anyway?"
fi
# Registry
echo ""
echo -e "${{CYAN}}Image Registry:${{NC}} Where to pull the gadget image from"
echo -e " Current: $CURRENT_REGISTRY"
echo -e " Press Enter to keep current registry (recommended)"
read -p "Enter registry (press Enter to keep current): " INPUT_REG
REGISTRY="${{INPUT_REG:-$CURRENT_REGISTRY}}"
# Memory limit
DEFAULT_MEM="{memory_limit}"
echo ""
echo -e "${{CYAN}}Memory Limit:${{NC}} Memory limit for gadget containers"
echo -e " Current: $CURRENT_MEM"
echo -e " Recommended: $DEFAULT_MEM"
read -p "Enter memory limit (press Enter for recommended): " INPUT_MEM
MEMORY_LIMIT="${{INPUT_MEM:-$DEFAULT_MEM}}"
# Events buffer
MIN_BUFFER=8192
CURRENT_BUFFER_VAL=""
if $CLI_TOOL get configmap inspektor-gadget-config -n "$NAMESPACE" &>/dev/null 2>&1; then
CURRENT_BUFFER_VAL=$($CLI_TOOL get configmap inspektor-gadget-config -n "$NAMESPACE" \\
-o jsonpath='{{.data.config\\.yaml}}' 2>/dev/null | grep "events-buffer-length" | grep -oE '[0-9]+' || echo "")
fi
echo ""
echo -e "${{CYAN}}Events Buffer Length:${{NC}} eBPF ring buffer size"
if [ -n "$CURRENT_BUFFER_VAL" ]; then
echo -e " Current: $CURRENT_BUFFER_VAL"
if [ "$CURRENT_BUFFER_VAL" -ge "$MIN_BUFFER" ] 2>/dev/null; then
DEFAULT_BUFFER=$CURRENT_BUFFER_VAL
echo -e " Keeping current value (reducing may cause event loss on busy clusters)"
else
DEFAULT_BUFFER=$MIN_BUFFER
echo -e " Recommended minimum: $MIN_BUFFER"
fi
else
DEFAULT_BUFFER=$MIN_BUFFER
echo -e " Recommended: $DEFAULT_BUFFER"
fi
echo -e " Default: $DEFAULT_BUFFER"
read -p "Enter buffer length (press Enter for default, 0 to skip): " INPUT_BUF
EVENTS_BUFFER_LENGTH="${{INPUT_BUF:-$DEFAULT_BUFFER}}"
if [ "$EVENTS_BUFFER_LENGTH" -lt "$MIN_BUFFER" ] 2>/dev/null && [ "$EVENTS_BUFFER_LENGTH" != "0" ]; then
print_warn "Buffer $EVENTS_BUFFER_LENGTH is below minimum ($MIN_BUFFER), may cause event loss"
confirm_step "Continue with this value?"
fi
# =========================================================================
# Step 3: Review & Confirm
# =========================================================================
print_header "Upgrade Plan"
echo "The following changes will be applied:"
echo ""
echo " Namespace: $NAMESPACE"
echo " Image: $REGISTRY:$TARGET_VERSION"
echo " Version: $CURRENT_VERSION -> $TARGET_VERSION"
echo " Memory Limit: $CURRENT_MEM -> $MEMORY_LIMIT"
if [ "$EVENTS_BUFFER_LENGTH" = "0" ]; then
echo " Events Buffer: (skipped)"
elif [ -n "$CURRENT_BUFFER_VAL" ] && [ "$CURRENT_BUFFER_VAL" != "$EVENTS_BUFFER_LENGTH" ] 2>/dev/null; then
echo " Events Buffer: $CURRENT_BUFFER_VAL -> $EVENTS_BUFFER_LENGTH"
else
echo " Events Buffer: (no change)"
fi
echo ""
echo " Rollback command (save this):"
echo " $CLI_TOOL set image daemonset/inspektor-gadget -n $NAMESPACE gadget=$CURRENT_IMAGE"
echo ""
print_warn "All gadget pods will be restarted during the upgrade."
print_warn "Ensure no active analyses are running on this cluster."
echo ""
confirm_step "Apply upgrade?"
# =========================================================================
# Step 4: Apply Changes
# =========================================================================
print_header "Applying Upgrade"
# 4a: Update container image
print_info "[1/5] Updating DaemonSet image to $REGISTRY:$TARGET_VERSION ..."
if ! $CLI_TOOL set image daemonset/inspektor-gadget -n "$NAMESPACE" gadget="$REGISTRY:$TARGET_VERSION"; then
print_err "Failed to update image!"
print_info "Rollback: $CLI_TOOL set image daemonset/inspektor-gadget -n $NAMESPACE gadget=$CURRENT_IMAGE"
exit 1
fi
print_ok "Image updated"
# 4b: Update GADGET_IMAGE env var (used by IG for OCI image pulls)
print_info "[2/5] Updating GADGET_IMAGE environment variable..."
if ! $CLI_TOOL set env daemonset/inspektor-gadget -n "$NAMESPACE" -c gadget \\
GADGET_IMAGE="$REGISTRY:$TARGET_VERSION"; then
print_warn "Failed to update GADGET_IMAGE env var (non-fatal, continuing)"
fi
print_ok "GADGET_IMAGE env updated"
# 4c: Update memory limit
print_info "[3/5] Setting memory limit to $MEMORY_LIMIT ..."
if ! $CLI_TOOL patch daemonset inspektor-gadget -n "$NAMESPACE" --type=json \\
-p="[{{\\"op\\": \\"replace\\", \\"path\\": \\"/spec/template/spec/containers/0/resources/limits/memory\\", \\"value\\": \\"$MEMORY_LIMIT\\"}}]"; then
print_warn "Failed to update memory limit (non-fatal, continuing)"
fi
print_ok "Memory limit set"
# 4d: Update events buffer
if [ "$EVENTS_BUFFER_LENGTH" != "0" ]; then
print_info "[4/5] Updating events buffer..."
if $CLI_TOOL get configmap inspektor-gadget-config -n "$NAMESPACE" &>/dev/null 2>&1; then
CUR_CFG=$($CLI_TOOL get configmap inspektor-gadget-config -n "$NAMESPACE" \\
-o jsonpath='{{.data.config\\.yaml}}' 2>/dev/null || echo "")
if [ -n "$CUR_CFG" ]; then
CUR_BUF=$(echo "$CUR_CFG" | grep "events-buffer-length" | grep -oE '[0-9]+' || echo "0")
if [ "$CUR_BUF" != "$EVENTS_BUFFER_LENGTH" ] 2>/dev/null; then
UPD_CFG=$(echo "$CUR_CFG" | sed "s/events-buffer-length:.*/events-buffer-length: $EVENTS_BUFFER_LENGTH/")
$CLI_TOOL create configmap inspektor-gadget-config -n "$NAMESPACE" \\
--from-literal="config.yaml=$UPD_CFG" --dry-run=client -o yaml \\
| $CLI_TOOL apply -f -
print_ok "Buffer updated: $CUR_BUF -> $EVENTS_BUFFER_LENGTH"
else
print_ok "Buffer unchanged (${{CUR_BUF:-unknown}})"
fi
else
print_warn "ConfigMap has no config.yaml data, skipping"
fi
else
print_warn "ConfigMap not found, skipping buffer update"
fi
else
print_info "[4/5] Events buffer update skipped (user choice)"
fi
# 4e: Wait for rollout
print_info "[5/5] Waiting for rollout (timeout: 5 minutes)..."
if ! $CLI_TOOL rollout status daemonset/inspektor-gadget -n "$NAMESPACE" --timeout=300s; then
print_err "Rollout timed out or failed!"
print_warn "Check pod status: $CLI_TOOL get pods -l app=inspektor-gadget -n $NAMESPACE"
print_warn "Check events: $CLI_TOOL get events -n $NAMESPACE --sort-by='.lastTimestamp' | tail -20"
print_info "Rollback: $CLI_TOOL set image daemonset/inspektor-gadget -n $NAMESPACE gadget=$CURRENT_IMAGE"
exit 1
fi
print_ok "Rollout complete"
# =========================================================================
# Step 5: Verification
# =========================================================================
print_header "Verification"
NEW_IMAGE=$($CLI_TOOL get daemonset inspektor-gadget -n "$NAMESPACE" \\
-o jsonpath='{{.spec.template.spec.containers[0].image}}' 2>/dev/null || echo "unknown")
NEW_VERSION_ACTUAL=$(echo "$NEW_IMAGE" | grep -oE 'v[0-9]+\\.[0-9]+\\.[0-9]+' || echo "unknown")
NEW_MEM=$($CLI_TOOL get daemonset inspektor-gadget -n "$NAMESPACE" \\
-o jsonpath='{{.spec.template.spec.containers[0].resources.limits.memory}}' 2>/dev/null || echo "unknown")
READY=$($CLI_TOOL get daemonset inspektor-gadget -n "$NAMESPACE" \\
-o jsonpath='{{.status.numberReady}}' 2>/dev/null || echo "0")
DESIRED=$($CLI_TOOL get daemonset inspektor-gadget -n "$NAMESPACE" \\
-o jsonpath='{{.status.desiredNumberScheduled}}' 2>/dev/null || echo "0")
# Verify version
if [ "$NEW_VERSION_ACTUAL" = "$TARGET_VERSION" ]; then
print_ok "Version: $NEW_VERSION_ACTUAL"
else
print_warn "Version mismatch: expected $TARGET_VERSION, got $NEW_VERSION_ACTUAL"
fi
# Verify pods
if [ "$READY" = "$DESIRED" ] && [ "$READY" != "0" ]; then
print_ok "Pods: $READY / $DESIRED ready"
else
print_warn "Pods: $READY / $DESIRED ready"
print_info "Some pods may still be starting. Check: $CLI_TOOL get pods -l app=inspektor-gadget -n $NAMESPACE"
fi
print_ok "Memory Limit: $NEW_MEM"
print_ok "Image: $NEW_IMAGE"
echo ""
echo "======================================================================="
echo " Upgrade Complete"
echo "======================================================================="
echo ""
echo " Next steps:"
echo " 1. Sync the cluster in Flowfish UI to update version info"
echo " 2. Start a test analysis to verify gadget functionality"
echo ""
echo " Rollback (if needed):"
echo " $CLI_TOOL set image daemonset/inspektor-gadget -n $NAMESPACE gadget=$CURRENT_IMAGE"
echo "======================================================================="
'''
return script
except HTTPException:
raise
except Exception as e:
logger.error("Failed to generate upgrade script", error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to generate upgrade script: {str(e)}"
)