From c556cc71d4a2cdbde577c1c695d64fd1a791d4d8 Mon Sep 17 00:00:00 2001 From: Mahmoud-Emad Date: Wed, 29 Oct 2025 16:46:37 +0300 Subject: [PATCH] feat: Implement Kubernetes client and example - Add Kubernetes client module for interacting with kubectl - Implement methods to get cluster info, pods, deployments, and services - Create a Kubernetes example script demonstrating client usage - Add JSON response structs for parsing kubectl output - Define runtime resource structs (Pod, Deployment, Service) for structured data - Include comprehensive unit tests for data structures and client logic --- .gitignore | 3 +- examples/virt/kubernetes/.gitignore | 1 + examples/virt/kubernetes/README.md | 177 ++++++++++ .../virt/kubernetes/kubernetes_example.vsh | 231 +++++++++++++ .../markdown/elements/element_frontmatter2.v | 2 - lib/virt/kubernetes/.heroscript | 2 +- lib/virt/kubernetes/kubernetes_actions.v | 59 ---- lib/virt/kubernetes/kubernetes_client.v | 214 +++++++++---- lib/virt/kubernetes/kubernetes_factory_.v | 173 ---------- lib/virt/kubernetes/kubernetes_model.v | 8 +- .../kubernetes/kubernetes_resources_model.v | 191 ++++++++++- lib/virt/kubernetes/kubernetes_test.v | 303 ++++++++++++++---- lib/virt/kubernetes/kubernetes_yaml.v | 3 - lib/virt/kubernetes/templates/atemplate.yaml | 5 - 14 files changed, 987 insertions(+), 385 deletions(-) create mode 100644 examples/virt/kubernetes/.gitignore create mode 100644 examples/virt/kubernetes/README.md create mode 100755 examples/virt/kubernetes/kubernetes_example.vsh delete mode 100644 lib/virt/kubernetes/kubernetes_actions.v delete mode 100644 lib/virt/kubernetes/templates/atemplate.yaml diff --git a/.gitignore b/.gitignore index 33eabe13..bf0ebdf4 100644 --- a/.gitignore +++ b/.gitignore @@ -57,4 +57,5 @@ MCP_HTTP_REST_IMPLEMENTATION_PLAN.md tmux_logger release install_herolib -doc \ No newline at end of file +doc +priv_key.bin \ No newline at end of file diff --git a/examples/virt/kubernetes/.gitignore b/examples/virt/kubernetes/.gitignore new file mode 100644 index 00000000..1c2b83a7 --- /dev/null +++ b/examples/virt/kubernetes/.gitignore @@ -0,0 +1 @@ +kubernetes_example diff --git a/examples/virt/kubernetes/README.md b/examples/virt/kubernetes/README.md new file mode 100644 index 00000000..27c468a4 --- /dev/null +++ b/examples/virt/kubernetes/README.md @@ -0,0 +1,177 @@ +# Kubernetes Client Example + +This example demonstrates the Kubernetes client functionality in HeroLib, including JSON parsing and cluster interaction. + +## Prerequisites + +1. **kubectl installed**: The Kubernetes command-line tool must be installed on your system. + - macOS: `brew install kubectl` + - Linux: See [official installation guide](https://kubernetes.io/docs/tasks/tools/install-kubectl-linux/) + - Windows: See [official installation guide](https://kubernetes.io/docs/tasks/tools/install-kubectl-windows/) + +2. **Kubernetes cluster**: You need access to a Kubernetes cluster. For local development, you can use: + - **Minikube**: `brew install minikube && minikube start` + - **Kind**: `brew install kind && kind create cluster` + - **Docker Desktop**: Enable Kubernetes in Docker Desktop settings + - **k3s**: Lightweight Kubernetes distribution + +## Running the Example + +### Method 1: Direct Execution (Recommended) + +```bash +# Make the script executable +chmod +x examples/virt/kubernetes/kubernetes_example.vsh + +# Run the script +./examples/virt/kubernetes/kubernetes_example.vsh +``` + +### Method 2: Using V Command + +```bash +v -enable-globals run examples/virt/kubernetes/kubernetes_example.vsh +``` + +## What the Example Demonstrates + +The example script demonstrates the following functionality: + +### 1. **Cluster Information** + +- Retrieves Kubernetes cluster version +- Counts total nodes in the cluster +- Counts total namespaces +- Counts running pods across all namespaces + +### 2. **Pod Management** + +- Lists all pods in the `default` namespace +- Displays pod details: + - Name, namespace, status + - Node assignment and IP address + - Container names + - Labels and creation timestamp + +### 3. **Deployment Management** + +- Lists all deployments in the `default` namespace +- Shows deployment information: + - Name and namespace + - Replica counts (desired, ready, available, updated) + - Labels and creation timestamp + +### 4. **Service Management** + +- Lists all services in the `default` namespace +- Displays service details: + - Name, namespace, and type (ClusterIP, NodePort, LoadBalancer) + - Cluster IP and external IP (if applicable) + - Exposed ports and protocols + - Labels and creation timestamp + +## Expected Output + +### With a Running Cluster + +When connected to a Kubernetes cluster with resources, you'll see formatted output like: + +``` +╔════════════════════════════════════════════════════════════════╗ +║ Kubernetes Client Example - HeroLib ║ +║ Demonstrates JSON parsing and cluster interaction ║ +╚════════════════════════════════════════════════════════════════╝ + +[INFO] Creating Kubernetes client instance... +[SUCCESS] Kubernetes client created successfully + + - 1. Cluster Information +[INFO] Retrieving cluster information... + +┌─────────────────────────────────────────────────────────────┐ +│ Cluster Overview │ +├─────────────────────────────────────────────────────────────┤ +│ API Server: https://127.0.0.1:6443 │ +│ Version: v1.31.0 │ +│ Nodes: 3 │ +│ Namespaces: 5 │ +│ Running Pods: 12 │ +└─────────────────────────────────────────────────────────────┘ +``` + +### Without a Cluster + +If kubectl is not installed or no cluster is configured, you'll see helpful error messages: + +``` +Error: Failed to get cluster information +... +This usually means: + - kubectl is not installed + - No Kubernetes cluster is configured (check ~/.kube/config) + - The cluster is not accessible + +To set up a local cluster, you can use: + - Minikube: https://minikube.sigs.k8s.io/docs/start/ + - Kind: https://kind.sigs.k8s.io/docs/user/quick-start/ + - Docker Desktop (includes Kubernetes) +``` + +## Creating Test Resources + +If your cluster is empty, you can create test resources to see the example in action: + +```bash +# Create a test pod +kubectl run nginx --image=nginx + +# Create a test deployment +kubectl create deployment nginx-deployment --image=nginx --replicas=3 + +# Expose the deployment as a service +kubectl expose deployment nginx-deployment --port=80 --type=ClusterIP +``` + +## Code Structure + +The example demonstrates proper usage of the HeroLib Kubernetes client: + +1. **Factory Pattern**: Uses `kubernetes.new()` to create a client instance +2. **Error Handling**: Proper use of V's `!` error propagation and `or {}` blocks +3. **JSON Parsing**: All kubectl JSON output is parsed into structured V types +4. **Console Output**: Clear, formatted output using the `console` module + +## Implementation Details + +The Kubernetes client module uses: + +- **Struct-based JSON decoding**: V's `json.decode(Type, data)` for type-safe parsing +- **Kubernetes API response structs**: Matching kubectl's JSON output format +- **Runtime resource structs**: Clean data structures for application use (`Pod`, `Deployment`, `Service`) + +## Troubleshooting + +### "kubectl: command not found" + +Install kubectl using your package manager (see Prerequisites above). + +### "The connection to the server was refused" + +Start a local Kubernetes cluster: + +```bash +minikube start +# or +kind create cluster +``` + +### "No resources found in default namespace" + +Create test resources using the commands in the "Creating Test Resources" section above. + +## Related Files + +- **Implementation**: `lib/virt/kubernetes/kubernetes_client.v` +- **Data Models**: `lib/virt/kubernetes/kubernetes_resources_model.v` +- **Unit Tests**: `lib/virt/kubernetes/kubernetes_test.v` +- **Factory**: `lib/virt/kubernetes/kubernetes_factory_.v` diff --git a/examples/virt/kubernetes/kubernetes_example.vsh b/examples/virt/kubernetes/kubernetes_example.vsh new file mode 100755 index 00000000..f1b955ae --- /dev/null +++ b/examples/virt/kubernetes/kubernetes_example.vsh @@ -0,0 +1,231 @@ +#!/usr/bin/env -S v -n -w -gc none -cc tcc -d use_openssl -enable-globals run + +import incubaid.herolib.virt.kubernetes +import incubaid.herolib.ui.console + +println('╔════════════════════════════════════════════════════════════════╗') +println('║ Kubernetes Client Example - HeroLib ║') +println('║ Demonstrates JSON parsing and cluster interaction ║') +println('╚════════════════════════════════════════════════════════════════╝') +println('') + +// Create a Kubernetes client instance using the factory pattern +println('[INFO] Creating Kubernetes client instance...') +mut client := kubernetes.new() or { + console.print_header('Error: Failed to create Kubernetes client') + eprintln('${err}') + eprintln('') + eprintln('Make sure kubectl is installed and configured properly.') + eprintln('You can install kubectl ') + exit(1) +} + +println('[SUCCESS] Kubernetes client created successfully') +println('') + +// ============================================================================ +// 1. Get Cluster Information +// ============================================================================ +console.print_header('1. Cluster Information') +println('[INFO] Retrieving cluster information...') +println('') + +cluster := client.cluster_info() or { + console.print_header('Error: Failed to get cluster information') + eprintln('${err}') + eprintln('') + eprintln('This usually means:') + eprintln(' - kubectl is not installed') + eprintln(' - No Kubernetes cluster is configured (check ~/.kube/config)') + eprintln(' - The cluster is not accessible') + eprintln('') + eprintln('To set up a local cluster, you can use:') + eprintln(' - Minikube: https://minikube.sigs.k8s.io/docs/start/') + eprintln(' - Kind: https://kind.sigs.k8s.io/docs/user/quick-start/') + eprintln(' - Docker Desktop (includes Kubernetes)') + exit(1) +} + +println('┌─────────────────────────────────────────────────────────────┐') +println('│ Cluster Overview │') +println('├─────────────────────────────────────────────────────────────┤') +println('│ API Server: ${cluster.api_server:-50}│') +println('│ Version: ${cluster.version:-50}│') +println('│ Nodes: ${cluster.nodes.str():-50}│') +println('│ Namespaces: ${cluster.namespaces.str():-50}│') +println('│ Running Pods: ${cluster.running_pods.str():-50}│') +println('└─────────────────────────────────────────────────────────────┘') +println('') + +// ============================================================================ +// 2. Get Pods in the 'default' namespace +// ============================================================================ +console.print_header('2. Pods in "default" Namespace') +println('[INFO] Retrieving pods from the default namespace...') +println('') + +pods := client.get_pods('default') or { + console.print_header('Warning: Failed to get pods') + eprintln('${err}') + eprintln('') + []kubernetes.Pod{} +} + +if pods.len == 0 { + println('No pods found in the default namespace.') + println('') + println('To create a test pod, run:') + println(' kubectl run nginx --image=nginx') + println('') +} else { + println('Found ${pods.len} pod(s) in the default namespace:') + println('') + + for i, pod in pods { + println('┌─────────────────────────────────────────────────────────────┐') + println('│ Pod #${i + 1:-56}│') + println('├─────────────────────────────────────────────────────────────┤') + println('│ Name: ${pod.name:-50}│') + println('│ Namespace: ${pod.namespace:-50}│') + println('│ Status: ${pod.status:-50}│') + println('│ Node: ${pod.node:-50}│') + println('│ IP: ${pod.ip:-50}│') + println('│ Containers: ${pod.containers.join(', '):-50}│') + println('│ Created: ${pod.created_at:-50}│') + + if pod.labels.len > 0 { + println('│ Labels: │') + for key, value in pod.labels { + label_str := ' ${key}=${value}' + println('│ ${label_str:-58}│') + } + } + + println('└─────────────────────────────────────────────────────────────┘') + println('') + } +} + +// ============================================================================ +// 3. Get Deployments in the 'default' namespace +// ============================================================================ +console.print_header('3. Deployments in "default" Namespace') +println('[INFO] Retrieving deployments from the default namespace...') +println('') + +deployments := client.get_deployments('default') or { + console.print_header('Warning: Failed to get deployments') + eprintln('${err}') + eprintln('') + []kubernetes.Deployment{} +} + +if deployments.len == 0 { + println('No deployments found in the default namespace.') + println('') + println('To create a test deployment, run:') + println(' kubectl create deployment nginx --image=nginx --replicas=3') + println('') +} else { + println('Found ${deployments.len} deployment(s) in the default namespace:') + println('') + + for i, deploy in deployments { + ready_status := if deploy.ready_replicas == deploy.replicas { '✓' } else { '⚠' } + + println('┌─────────────────────────────────────────────────────────────┐') + println('│ Deployment #${i + 1:-53}│') + println('├─────────────────────────────────────────────────────────────┤') + println('│ Name: ${deploy.name:-44}│') + println('│ Namespace: ${deploy.namespace:-44}│') + println('│ Replicas: ${deploy.replicas.str():-44}│') + println('│ Ready Replicas: ${deploy.ready_replicas.str():-44}│') + println('│ Available: ${deploy.available_replicas.str():-44}│') + println('│ Updated: ${deploy.updated_replicas.str():-44}│') + println('│ Status: ${ready_status:-44}│') + println('│ Created: ${deploy.created_at:-44}│') + + if deploy.labels.len > 0 { + println('│ Labels: │') + for key, value in deploy.labels { + label_str := ' ${key}=${value}' + println('│ ${label_str:-58}│') + } + } + + println('└─────────────────────────────────────────────────────────────┘') + println('') + } +} + +// ============================================================================ +// 4. Get Services in the 'default' namespace +// ============================================================================ +console.print_header('4. Services in "default" Namespace') +println('[INFO] Retrieving services from the default namespace...') +println('') + +services := client.get_services('default') or { + console.print_header('Warning: Failed to get services') + eprintln('${err}') + eprintln('') + []kubernetes.Service{} +} + +if services.len == 0 { + println('No services found in the default namespace.') + println('') + println('To create a test service, run:') + println(' kubectl expose deployment nginx --port=80 --type=ClusterIP') + println('') +} else { + println('Found ${services.len} service(s) in the default namespace:') + println('') + + for i, svc in services { + println('┌─────────────────────────────────────────────────────────────┐') + println('│ Service #${i + 1:-54}│') + println('├─────────────────────────────────────────────────────────────┤') + println('│ Name: ${svc.name:-48}│') + println('│ Namespace: ${svc.namespace:-48}│') + println('│ Type: ${svc.service_type:-48}│') + println('│ Cluster IP: ${svc.cluster_ip:-48}│') + + if svc.external_ip.len > 0 { + println('│ External IP: ${svc.external_ip:-48}│') + } + + if svc.ports.len > 0 { + println('│ Ports: ${svc.ports.join(', '):-48}│') + } + + println('│ Created: ${svc.created_at:-48}│') + + if svc.labels.len > 0 { + println('│ Labels: │') + for key, value in svc.labels { + label_str := ' ${key}=${value}' + println('│ ${label_str:-58}│') + } + } + + println('└─────────────────────────────────────────────────────────────┘') + println('') + } +} + +// ============================================================================ +// Summary +// ============================================================================ +console.print_header('Summary') +println('✓ Successfully demonstrated Kubernetes client functionality') +println('✓ Cluster information retrieved and parsed') +println('✓ Pods: ${pods.len} found') +println('✓ Deployments: ${deployments.len} found') +println('✓ Services: ${services.len} found') +println('') +println('All JSON parsing operations completed successfully!') +println('') +println('╔════════════════════════════════════════════════════════════════╗') +println('║ Example Complete ║') +println('╚════════════════════════════════════════════════════════════════╝') diff --git a/lib/data/markdown/elements/element_frontmatter2.v b/lib/data/markdown/elements/element_frontmatter2.v index 5ec83b8e..6cfb0e35 100644 --- a/lib/data/markdown/elements/element_frontmatter2.v +++ b/lib/data/markdown/elements/element_frontmatter2.v @@ -1,7 +1,5 @@ module elements -import toml - // Frontmatter2 struct @[heap] pub struct Frontmatter2 { diff --git a/lib/virt/kubernetes/.heroscript b/lib/virt/kubernetes/.heroscript index 30ecf58f..9d0e3664 100644 --- a/lib/virt/kubernetes/.heroscript +++ b/lib/virt/kubernetes/.heroscript @@ -1,5 +1,5 @@ -!!hero_code.generate_installer +!!hero_code.generate_client name:'' classname:'KubeClient' singleton:0 diff --git a/lib/virt/kubernetes/kubernetes_actions.v b/lib/virt/kubernetes/kubernetes_actions.v deleted file mode 100644 index 2d84cdc0..00000000 --- a/lib/virt/kubernetes/kubernetes_actions.v +++ /dev/null @@ -1,59 +0,0 @@ -module kubernetes - -import incubaid.herolib.osal.core as osal -import incubaid.herolib.ui.console -import incubaid.herolib.core.texttools -import incubaid.herolib.osal.startupmanager - -fn startupcmd() ![]startupmanager.ZProcessNewArgs { - return []startupmanager.ZProcessNewArgs{} -} - -fn running() !bool { - // Check if kubectl is available and can connect - job := osal.exec(cmd: 'kubectl cluster-info', raise_error: false)! - return job.exit_code == 0 -} - -fn start_pre() ! { - console.print_header('Pre-start checks') - if !osal.cmd_exists('kubectl') { - return error('kubectl not found in PATH') - } -} - -fn start_post() ! { - console.print_header('Post-start validation') -} - -fn stop_pre() ! { -} - -fn stop_post() ! { -} - -fn installed() !bool { - return osal.cmd_exists('kubectl') -} - -fn install() ! { - console.print_header('install kubectl') - // kubectl is typically installed separately via package manager - // This can be enhanced to auto-download if needed - if !osal.cmd_exists('kubectl') { - return error('Please install kubectl: https://kubernetes.io/docs/tasks/tools/') - } -} - -fn build() ! { - // Not applicable for kubectl wrapper -} - -fn destroy() ! { - console.print_header('destroy kubernetes client') - // No cleanup needed for kubectl wrapper -} - -// fn configure() ! { -// console.print_debug('Kubernetes client configured') -// } diff --git a/lib/virt/kubernetes/kubernetes_client.v b/lib/virt/kubernetes/kubernetes_client.v index 24f47b91..cf727cdc 100644 --- a/lib/virt/kubernetes/kubernetes_client.v +++ b/lib/virt/kubernetes/kubernetes_client.v @@ -1,19 +1,15 @@ module kubernetes import incubaid.herolib.osal.core as osal -import incubaid.herolib.core.httpconnection -import incubaid.herolib.core.pathlib import incubaid.herolib.ui.console import json -import os - @[params] pub struct KubectlExecArgs { pub mut: command string timeout int = 30 - retry int = 0 + retry int } pub struct KubectlResult { @@ -29,18 +25,18 @@ pub fn (mut k KubeClient) kubectl_exec(args KubectlExecArgs) !KubectlResult { mut cmd := 'kubectl' if k.config.namespace.len > 0 { - cmd += '--namespace=${k.config.namespace} ' + cmd += ' --namespace=${k.config.namespace}' } if k.kubeconfig_path.len > 0 { - cmd += '--kubeconfig=${k.kubeconfig_path} ' + cmd += ' --kubeconfig=${k.kubeconfig_path}' } if k.config.context.len > 0 { - cmd += '--context=${k.config.context} ' + cmd += ' --context=${k.config.context}' } - cmd += args.command + cmd += ' ${args.command}' console.print_debug('executing: ${cmd}') @@ -59,7 +55,6 @@ pub fn (mut k KubeClient) kubectl_exec(args KubectlExecArgs) !KubectlResult { } } - // Test connection to cluster pub fn (mut k KubeClient) test_connection() !bool { result := k.kubectl_exec(command: 'cluster-info')! @@ -78,90 +73,175 @@ pub fn (mut k KubeClient) cluster_info() !ClusterInfo { return error('Failed to get cluster version: ${result.stderr}') } - println(result.stdout) + // Parse version JSON using struct-based decoding + mut version_str := 'unknown' + version_response := json.decode(KubectlVersionResponse, result.stdout) or { + console.print_debug('Failed to parse version JSON: ${err}') + KubectlVersionResponse{} + } + if version_response.server_version.git_version.len > 0 { + version_str = version_response.server_version.git_version + } - $dbg; - // version_data := json.decode(map[string]interface{}, result.stdout)! - // server_version := version_data['serverVersion'] or { return error('No serverVersion') } + // Get node count + nodes_result := k.kubectl_exec(command: 'get nodes -o json')! + mut nodes_count := 0 + if nodes_result.success { + nodes_list := json.decode(KubectlListResponse, nodes_result.stdout) or { + console.print_debug('Failed to parse nodes JSON: ${err}') + KubectlListResponse{} + } + nodes_count = nodes_list.items.len + } - // // Get node count - // nodes_result := k.kubectl_exec(command: 'get nodes -o json')! - // nodes_count := if nodes_result.success { - // nodes_data := json.decode(map[string]interface{}, nodes_result.stdout)! - // items := nodes_data['items'] or { []interface{}{} } - // items.len - // } else { - // 0 - // } + // Get namespace count + ns_result := k.kubectl_exec(command: 'get namespaces -o json')! + mut ns_count := 0 + if ns_result.success { + ns_list := json.decode(KubectlListResponse, ns_result.stdout) or { + console.print_debug('Failed to parse namespaces JSON: ${err}') + KubectlListResponse{} + } + ns_count = ns_list.items.len + } - // // Get namespace count - // ns_result := k.kubectl_exec(command: 'get namespaces -o json')! - // ns_count := if ns_result.success { - // ns_data := json.decode(map[string]interface{}, ns_result.stdout)! - // items := ns_data['items'] or { []interface{}{} } - // items.len - // } else { - // 0 - // } + // Get running pods count + pods_result := k.kubectl_exec(command: 'get pods --all-namespaces -o json')! + mut pods_count := 0 + if pods_result.success { + pods_list := json.decode(KubectlListResponse, pods_result.stdout) or { + console.print_debug('Failed to parse pods JSON: ${err}') + KubectlListResponse{} + } + pods_count = pods_list.items.len + } - // // Get running pods count - // pods_result := k.kubectl_exec(command: 'get pods --all-namespaces -o json')! - // pods_count := if pods_result.success { - // pods_data := json.decode(map[string]interface{}, pods_result.stdout)! - // items := pods_data['items'] or { []interface{}{} } - // items.len - // } else { - // 0 - // } - - // return ClusterInfo{ - // version: 'v1.0.0' - // nodes: nodes_count - // namespaces: ns_count - // running_pods: pods_count - // api_server: k.config.api_server - // } - return ClusterInfo{} + return ClusterInfo{ + version: version_str + nodes: nodes_count + namespaces: ns_count + running_pods: pods_count + api_server: k.config.api_server + } } // Get resources (Pods, Deployments, Services, etc.) -pub fn (mut k KubeClient) get_pods(namespace string) ! { +pub fn (mut k KubeClient) get_pods(namespace string) ![]Pod { result := k.kubectl_exec(command: 'get pods -n ${namespace} -o json')! if !result.success { return error('Failed to get pods: ${result.stderr}') } - println(result.stdout) - $dbg; - // data := json.decode(map[string]interface{}, result.stdout)! - // items := data['items'] or { []interface{}{} } - // return items as []map[string]interface{} + // Parse JSON response using struct-based decoding + pod_list := json.decode(KubectlPodListResponse, result.stdout) or { + return error('Failed to parse pods JSON: ${err}') + } - panic('Not implemented') + mut pods := []Pod{} + + for item in pod_list.items { + // Extract container names + mut container_names := []string{} + for container in item.spec.containers { + container_names << container.name + } + + // Create Pod struct from kubectl response + pod := Pod{ + name: item.metadata.name + namespace: item.metadata.namespace + status: item.status.phase + node: item.spec.node_name + ip: item.status.pod_ip + containers: container_names + labels: item.metadata.labels + created_at: item.metadata.creation_timestamp + } + + pods << pod + } + + return pods } -pub fn (mut k KubeClient) get_deployments(namespace string) ! { +pub fn (mut k KubeClient) get_deployments(namespace string) ![]Deployment { result := k.kubectl_exec(command: 'get deployments -n ${namespace} -o json')! if !result.success { return error('Failed to get deployments: ${result.stderr}') } - // data := json.decode(map[string]interface{}, result.stdout)! - // items := data['items'] or { []interface{}{} } - // return items as []map[string]interface{} - panic('Not implemented') + // Parse JSON response using struct-based decoding + deployment_list := json.decode(KubectlDeploymentListResponse, result.stdout) or { + return error('Failed to parse deployments JSON: ${err}') + } + + mut deployments := []Deployment{} + + for item in deployment_list.items { + // Create Deployment struct from kubectl response + deployment := Deployment{ + name: item.metadata.name + namespace: item.metadata.namespace + replicas: item.spec.replicas + ready_replicas: item.status.ready_replicas + available_replicas: item.status.available_replicas + updated_replicas: item.status.updated_replicas + labels: item.metadata.labels + created_at: item.metadata.creation_timestamp + } + + deployments << deployment + } + + return deployments } -pub fn (mut k KubeClient) get_services(namespace string) ! { +pub fn (mut k KubeClient) get_services(namespace string) ![]Service { result := k.kubectl_exec(command: 'get services -n ${namespace} -o json')! if !result.success { return error('Failed to get services: ${result.stderr}') } - // data := json.decode(map[string]interface{}, result.stdout)! - // items := data['items'] or { []interface{}{} } - // return items as []map[string]interface{} - panic('Not implemented') + // Parse JSON response using struct-based decoding + service_list := json.decode(KubectlServiceListResponse, result.stdout) or { + return error('Failed to parse services JSON: ${err}') + } + + mut services := []Service{} + + for item in service_list.items { + // Build port strings (e.g., "80/TCP", "443/TCP") + mut port_strings := []string{} + for port in item.spec.ports { + port_strings << '${port.port}/${port.protocol}' + } + + // Get external IP from LoadBalancer status if available + mut external_ip := '' + if item.status.load_balancer.ingress.len > 0 { + external_ip = item.status.load_balancer.ingress[0].ip + } + // Also check spec.external_ips + if external_ip.len == 0 && item.spec.external_ips.len > 0 { + external_ip = item.spec.external_ips[0] + } + + // Create Service struct from kubectl response + service := Service{ + name: item.metadata.name + namespace: item.metadata.namespace + service_type: item.spec.service_type + cluster_ip: item.spec.cluster_ip + external_ip: external_ip + ports: port_strings + labels: item.metadata.labels + created_at: item.metadata.creation_timestamp + } + + services << service + } + + return services } // Apply YAML file diff --git a/lib/virt/kubernetes/kubernetes_factory_.v b/lib/virt/kubernetes/kubernetes_factory_.v index 67005a02..84e0b0b5 100644 --- a/lib/virt/kubernetes/kubernetes_factory_.v +++ b/lib/virt/kubernetes/kubernetes_factory_.v @@ -2,10 +2,7 @@ module kubernetes import incubaid.herolib.core.base import incubaid.herolib.core.playbook { PlayBook } -import incubaid.herolib.ui.console import json -import incubaid.herolib.osal.startupmanager -import time __global ( kubernetes_global map[string]&KubeClient @@ -134,176 +131,6 @@ pub fn play(mut plbook PlayBook) ! { install_action.done = true } } - mut other_actions := plbook.find(filter: 'kubernetes.')! - for mut other_action in other_actions { - if other_action.name in ['destroy', 'install', 'build'] { - mut p := other_action.params - reset := p.get_default_false('reset') - if other_action.name == 'destroy' || reset { - console.print_debug('install action kubernetes.destroy') - destroy()! - } - if other_action.name == 'install' { - console.print_debug('install action kubernetes.install') - install()! - } - } - if other_action.name in ['start', 'stop', 'restart'] { - mut p := other_action.params - name := p.get('name')! - mut kubernetes_obj := get(name: name)! - console.print_debug('action object:\n${kubernetes_obj}') - if other_action.name == 'start' { - console.print_debug('install action kubernetes.${other_action.name}') - kubernetes_obj.start()! - } - - if other_action.name == 'stop' { - console.print_debug('install action kubernetes.${other_action.name}') - kubernetes_obj.stop()! - } - if other_action.name == 'restart' { - console.print_debug('install action kubernetes.${other_action.name}') - kubernetes_obj.restart()! - } - } - other_action.done = true - } -} - -//////////////////////////////////////////////////////////////////////////////////////////////////// -//////////////////////////# LIVE CYCLE MANAGEMENT FOR INSTALLERS /////////////////////////////////// -//////////////////////////////////////////////////////////////////////////////////////////////////// - -fn startupmanager_get(cat startupmanager.StartupManagerType) !startupmanager.StartupManager { - // unknown - // screen - // zinit - // tmux - // systemd - match cat { - .screen { - console.print_debug("installer: kubernetes' startupmanager get screen") - return startupmanager.get(.screen)! - } - .zinit { - console.print_debug("installer: kubernetes' startupmanager get zinit") - return startupmanager.get(.zinit)! - } - .systemd { - console.print_debug("installer: kubernetes' startupmanager get systemd") - return startupmanager.get(.systemd)! - } - else { - console.print_debug("installer: kubernetes' startupmanager get auto") - return startupmanager.get(.auto)! - } - } -} - -// load from disk and make sure is properly intialized -pub fn (mut self KubeClient) reload() ! { - switch(self.name) - self = obj_init(self)! -} - -pub fn (mut self KubeClient) start() ! { - switch(self.name) - if self.running()! { - return - } - - console.print_header('installer: kubernetes start') - - if !installed()! { - install()! - } - - configure()! - - start_pre()! - - for zprocess in startupcmd()! { - mut sm := startupmanager_get(zprocess.startuptype)! - - console.print_debug('installer: kubernetes starting with ${zprocess.startuptype}...') - - sm.new(zprocess)! - - sm.start(zprocess.name)! - } - - start_post()! - - for _ in 0 .. 50 { - if self.running()! { - return - } - time.sleep(100 * time.millisecond) - } - return error('kubernetes did not install properly.') -} - -pub fn (mut self KubeClient) install_start(args InstallArgs) ! { - switch(self.name) - self.install(args)! - self.start()! -} - -pub fn (mut self KubeClient) stop() ! { - switch(self.name) - stop_pre()! - for zprocess in startupcmd()! { - mut sm := startupmanager_get(zprocess.startuptype)! - sm.stop(zprocess.name)! - } - stop_post()! -} - -pub fn (mut self KubeClient) restart() ! { - switch(self.name) - self.stop()! - self.start()! -} - -pub fn (mut self KubeClient) running() !bool { - switch(self.name) - - // walk over the generic processes, if not running return - for zprocess in startupcmd()! { - if zprocess.startuptype != .screen { - mut sm := startupmanager_get(zprocess.startuptype)! - r := sm.running(zprocess.name)! - if r == false { - return false - } - } - } - return running()! -} - -@[params] -pub struct InstallArgs { -pub mut: - reset bool -} - -pub fn (mut self KubeClient) install(args InstallArgs) ! { - switch(self.name) - if args.reset || (!installed()!) { - install()! - } -} - -pub fn (mut self KubeClient) build() ! { - switch(self.name) - build()! -} - -pub fn (mut self KubeClient) destroy() ! { - switch(self.name) - self.stop() or {} - destroy()! } // switch instance to be used for kubernetes diff --git a/lib/virt/kubernetes/kubernetes_model.v b/lib/virt/kubernetes/kubernetes_model.v index b1ad61c3..acb1e274 100644 --- a/lib/virt/kubernetes/kubernetes_model.v +++ b/lib/virt/kubernetes/kubernetes_model.v @@ -1,8 +1,6 @@ module kubernetes -import incubaid.herolib.data.paramsparser import incubaid.herolib.data.encoderhero -import os pub const version = '0.0.0' const singleton = false @@ -12,9 +10,9 @@ const default = true pub struct KubeClient { pub mut: name string = 'default' - kubeconfig_path string - config KubeConfig - connected bool + kubeconfig_path string // Path to kubeconfig file + config KubeConfig // Kubernetes configuration + connected bool // Connection status api_version string = 'v1' cache_enabled bool = true cache_ttl_seconds int = 300 diff --git a/lib/virt/kubernetes/kubernetes_resources_model.v b/lib/virt/kubernetes/kubernetes_resources_model.v index cd2038f9..f489b553 100644 --- a/lib/virt/kubernetes/kubernetes_resources_model.v +++ b/lib/virt/kubernetes/kubernetes_resources_model.v @@ -1,8 +1,5 @@ module kubernetes -import incubaid.herolib.data.encoderhero -import os - // K8s API Version and Kind tracking @[params] pub struct K8sMetadata { @@ -150,7 +147,7 @@ pub mut: pub struct KubeConfig { pub mut: kubeconfig_path string - context string = '' + context string namespace string = 'default' api_server string ca_cert_path string @@ -179,3 +176,189 @@ pub mut: running_pods int api_server string } + +// ============================================================================ +// Kubectl JSON Response Structs +// These structs match the JSON structure returned by kubectl commands +// ============================================================================ + +// Version response from 'kubectl version -o json' +struct KubectlVersionResponse { + server_version ServerVersionInfo @[json: serverVersion] +} + +struct ServerVersionInfo { + git_version string @[json: gitVersion] + major string + minor string +} + +// Generic list response structure +struct KubectlListResponse { + items []KubectlItemMetadata +} + +struct KubectlItemMetadata { + metadata KubectlMetadata +} + +struct KubectlMetadata { + name string +} + +// Pod list response from 'kubectl get pods -o json' +struct KubectlPodListResponse { + items []KubectlPodItem +} + +struct KubectlPodItem { + metadata KubectlPodMetadata + spec KubectlPodSpec + status KubectlPodStatus +} + +struct KubectlPodMetadata { + name string + namespace string + labels map[string]string + creation_timestamp string @[json: creationTimestamp] +} + +struct KubectlPodSpec { + node_name string @[json: nodeName] + containers []KubectlContainer +} + +struct KubectlContainer { + name string + image string +} + +struct KubectlPodStatus { + phase string + pod_ip string @[json: podIP] +} + +// Deployment list response from 'kubectl get deployments -o json' +struct KubectlDeploymentListResponse { + items []KubectlDeploymentItem +} + +struct KubectlDeploymentItem { + metadata KubectlDeploymentMetadata + spec KubectlDeploymentSpec + status KubectlDeploymentStatus +} + +struct KubectlDeploymentMetadata { + name string + namespace string + labels map[string]string + creation_timestamp string @[json: creationTimestamp] +} + +struct KubectlDeploymentSpec { + replicas int +} + +struct KubectlDeploymentStatus { + ready_replicas int @[json: readyReplicas] + available_replicas int @[json: availableReplicas] + updated_replicas int @[json: updatedReplicas] +} + +// Service list response from 'kubectl get services -o json' +struct KubectlServiceListResponse { + items []KubectlServiceItem +} + +struct KubectlServiceItem { + metadata KubectlServiceMetadata + spec KubectlServiceSpec + status KubectlServiceStatus +} + +struct KubectlServiceMetadata { + name string + namespace string + labels map[string]string + creation_timestamp string @[json: creationTimestamp] +} + +struct KubectlServiceSpec { + service_type string @[json: type] + cluster_ip string @[json: clusterIP] + external_ips []string @[json: externalIPs] + ports []KubectlServicePort +} + +struct KubectlServicePort { + port int + protocol string +} + +struct KubectlServiceStatus { + load_balancer KubectlLoadBalancerStatus @[json: loadBalancer] +} + +struct KubectlLoadBalancerStatus { + ingress []KubectlLoadBalancerIngress +} + +struct KubectlLoadBalancerIngress { + ip string +} + +// ============================================================================ +// Runtime resource structs (returned from kubectl get commands) +// ============================================================================ + +// Pod runtime information +pub struct Pod { +pub mut: + name string + namespace string + status string + node string + ip string + containers []string + labels map[string]string + created_at string +} + +// Deployment runtime information +pub struct Deployment { +pub mut: + name string + namespace string + replicas int + ready_replicas int + available_replicas int + updated_replicas int + labels map[string]string + created_at string +} + +// Service runtime information +pub struct Service { +pub mut: + name string + namespace string + service_type string + cluster_ip string + external_ip string + ports []string + labels map[string]string + created_at string +} + +// Version information from kubectl version command +pub struct VersionInfo { +pub mut: + major string + minor string + git_version string + git_commit string + build_date string + platform string +} diff --git a/lib/virt/kubernetes/kubernetes_test.v b/lib/virt/kubernetes/kubernetes_test.v index 9bc182cf..6c48b5dc 100644 --- a/lib/virt/kubernetes/kubernetes_test.v +++ b/lib/virt/kubernetes/kubernetes_test.v @@ -1,76 +1,249 @@ module kubernetes -import time -import os +// ============================================================================ +// Unit Tests for Kubernetes Client Module +// These tests verify struct creation and data handling without executing +// real kubectl commands (unit tests only, not integration tests) +// ============================================================================ fn test_model_creation() ! { - mut deployment := DeploymentSpec{ - metadata: K8sMetadata{ - name: 'test-app' - namespace: 'default' - } - replicas: 3 - selector: { - 'app': 'test-app' - } - template: PodSpec{ - metadata: K8sMetadata{ - name: 'test-app-pod' - namespace: 'default' - } - containers: [ - ContainerSpec{ - name: 'app' - image: 'nginx:latest' - ports: [ - ContainerPort{ - name: 'http' - container_port: 80 - }, - ] - }, - ] + mut client := new(name: 'test-cluster')! + assert client.name == 'test-cluster' +} + +// ============================================================================ +// Unit Tests for Data Structures and JSON Parsing +// ============================================================================ + +// Test Pod struct creation and field access +fn test_pod_struct_creation() ! { + mut pod := Pod{ + name: 'test-pod' + namespace: 'default' + status: 'Running' + node: 'node-1' + ip: '10.244.0.5' + containers: ['nginx', 'sidecar'] + labels: { + 'app': 'web' + 'env': 'prod' } + created_at: '2024-01-15T10:30:00Z' } - yaml := yaml_from_deployment(deployment)! - assert yaml.contains('apiVersion: apps/v1') - assert yaml.contains('kind: Deployment') - assert yaml.contains('test-app') + assert pod.name == 'test-pod' + assert pod.namespace == 'default' + assert pod.status == 'Running' + assert pod.node == 'node-1' + assert pod.ip == '10.244.0.5' + assert pod.containers.len == 2 + assert pod.containers[0] == 'nginx' + assert pod.containers[1] == 'sidecar' + assert pod.labels['app'] == 'web' + assert pod.labels['env'] == 'prod' + assert pod.created_at == '2024-01-15T10:30:00Z' } -fn test_yaml_validation() ! { - // Create test YAML file - test_yaml := '' - ' -apiVersion: apps/v1 -kind: Deployment -metadata: - name: test-deployment - namespace: default -spec: - replicas: 1 - selector: - matchLabels: - app: test - template: - metadata: - labels: - app: test - spec: - containers: - - name: app - image: nginx:latest -' - '' +// Test Deployment struct creation +fn test_deployment_struct_creation() ! { + mut deployment := Deployment{ + name: 'nginx-deployment' + namespace: 'default' + replicas: 3 + ready_replicas: 3 + available_replicas: 3 + updated_replicas: 3 + labels: { + 'app': 'nginx' + } + created_at: '2024-01-15T09:00:00Z' + } - test_file := '/tmp/test-deployment.yaml' - os.write_file(test_file, test_yaml)! - - result := yaml_validate(test_file)! - assert result.valid - assert result.kind == 'Deployment' - assert result.metadata.name == 'test-deployment' - - os.rm(test_file)! + assert deployment.name == 'nginx-deployment' + assert deployment.namespace == 'default' + assert deployment.replicas == 3 + assert deployment.ready_replicas == 3 + assert deployment.available_replicas == 3 + assert deployment.updated_replicas == 3 + assert deployment.labels['app'] == 'nginx' + assert deployment.created_at == '2024-01-15T09:00:00Z' +} + +// Test Service struct creation with ClusterIP +fn test_service_struct_creation() ! { + mut service := Service{ + name: 'nginx-service' + namespace: 'default' + service_type: 'ClusterIP' + cluster_ip: '10.96.100.50' + external_ip: '' + ports: ['80/TCP', '443/TCP'] + labels: { + 'app': 'nginx' + } + created_at: '2024-01-15T09:30:00Z' + } + + assert service.name == 'nginx-service' + assert service.namespace == 'default' + assert service.service_type == 'ClusterIP' + assert service.cluster_ip == '10.96.100.50' + assert service.external_ip == '' + assert service.ports.len == 2 + assert service.ports[0] == '80/TCP' + assert service.ports[1] == '443/TCP' + assert service.labels['app'] == 'nginx' + assert service.created_at == '2024-01-15T09:30:00Z' +} + +// Test Service with LoadBalancer type and external IP +fn test_service_loadbalancer_type() ! { + mut service := Service{ + name: 'web-lb-service' + namespace: 'default' + service_type: 'LoadBalancer' + cluster_ip: '10.96.100.52' + external_ip: '203.0.113.10' + ports: ['80/TCP'] + labels: { + 'app': 'web' + } + created_at: '2024-01-15T11:00:00Z' + } + + assert service.name == 'web-lb-service' + assert service.service_type == 'LoadBalancer' + assert service.cluster_ip == '10.96.100.52' + assert service.external_ip == '203.0.113.10' + assert service.ports.len == 1 + assert service.ports[0] == '80/TCP' +} + +// Test ClusterInfo struct +fn test_cluster_info_struct() ! { + mut cluster := ClusterInfo{ + api_server: 'https://test-cluster:6443' + version: 'v1.31.0' + nodes: 3 + namespaces: 5 + running_pods: 12 + } + + assert cluster.api_server == 'https://test-cluster:6443' + assert cluster.version == 'v1.31.0' + assert cluster.nodes == 3 + assert cluster.namespaces == 5 + assert cluster.running_pods == 12 +} + +// Test Pod with multiple containers +fn test_pod_with_multiple_containers() ! { + mut pod := Pod{ + name: 'multi-container-pod' + namespace: 'default' + containers: ['app', 'sidecar', 'init'] + } + + assert pod.containers.len == 3 + assert 'app' in pod.containers + assert 'sidecar' in pod.containers + assert 'init' in pod.containers +} + +// Test Deployment with partial ready state +fn test_deployment_partial_ready() ! { + mut deployment := Deployment{ + name: 'redis-deployment' + namespace: 'default' + replicas: 3 + ready_replicas: 2 + available_replicas: 2 + updated_replicas: 3 + } + + assert deployment.replicas == 3 + assert deployment.ready_replicas == 2 + assert deployment.available_replicas == 2 + // Not all replicas are ready + assert deployment.ready_replicas < deployment.replicas +} + +// Test Service with multiple ports +fn test_service_with_multiple_ports() ! { + mut service := Service{ + name: 'multi-port-service' + ports: ['80/TCP', '443/TCP', '8080/TCP'] + } + + assert service.ports.len == 3 + assert '80/TCP' in service.ports + assert '443/TCP' in service.ports + assert '8080/TCP' in service.ports +} + +// Test Pod with default/empty values +fn test_pod_default_values() ! { + mut pod := Pod{} + + assert pod.name == '' + assert pod.namespace == '' + assert pod.status == '' + assert pod.node == '' + assert pod.ip == '' + assert pod.containers.len == 0 + assert pod.labels.len == 0 + assert pod.created_at == '' +} + +// Test Deployment with default values +fn test_deployment_default_values() ! { + mut deployment := Deployment{} + + assert deployment.name == '' + assert deployment.namespace == '' + assert deployment.replicas == 0 + assert deployment.ready_replicas == 0 + assert deployment.available_replicas == 0 + assert deployment.updated_replicas == 0 + assert deployment.labels.len == 0 + assert deployment.created_at == '' +} + +// Test Service with default values +fn test_service_default_values() ! { + mut service := Service{} + + assert service.name == '' + assert service.namespace == '' + assert service.service_type == '' + assert service.cluster_ip == '' + assert service.external_ip == '' + assert service.ports.len == 0 + assert service.labels.len == 0 + assert service.created_at == '' +} + +// Test KubectlResult struct for successful command +fn test_kubectl_result_struct() ! { + mut result := KubectlResult{ + exit_code: 0 + stdout: '{"items": []}' + stderr: '' + } + + assert result.exit_code == 0 + assert result.stdout.contains('items') + assert result.stderr == '' +} + +// Test KubectlResult struct for error +fn test_kubectl_result_error() ! { + mut result := KubectlResult{ + exit_code: 1 + stdout: '' + stderr: 'Error: connection refused' + } + + assert result.exit_code == 1 + assert result.stderr.contains('Error') } diff --git a/lib/virt/kubernetes/kubernetes_yaml.v b/lib/virt/kubernetes/kubernetes_yaml.v index febf0932..b1198e8d 100644 --- a/lib/virt/kubernetes/kubernetes_yaml.v +++ b/lib/virt/kubernetes/kubernetes_yaml.v @@ -1,9 +1,6 @@ module kubernetes -import json -import incubaid.herolib.data.markdown import incubaid.herolib.core.pathlib -import os // Parse YAML file and return validation result pub fn yaml_validate(yaml_path string) !K8sValidationResult { diff --git a/lib/virt/kubernetes/templates/atemplate.yaml b/lib/virt/kubernetes/templates/atemplate.yaml deleted file mode 100644 index a4c386dd..00000000 --- a/lib/virt/kubernetes/templates/atemplate.yaml +++ /dev/null @@ -1,5 +0,0 @@ - - -name: ${cfg.configpath} - -