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
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -58,3 +58,4 @@ tmux_logger
|
|||||||
release
|
release
|
||||||
install_herolib
|
install_herolib
|
||||||
doc
|
doc
|
||||||
|
priv_key.bin
|
||||||
1
examples/virt/kubernetes/.gitignore
vendored
Normal file
1
examples/virt/kubernetes/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
kubernetes_example
|
||||||
177
examples/virt/kubernetes/README.md
Normal file
177
examples/virt/kubernetes/README.md
Normal file
@@ -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`
|
||||||
231
examples/virt/kubernetes/kubernetes_example.vsh
Executable file
231
examples/virt/kubernetes/kubernetes_example.vsh
Executable file
@@ -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('╚════════════════════════════════════════════════════════════════╝')
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
module elements
|
module elements
|
||||||
|
|
||||||
import toml
|
|
||||||
|
|
||||||
// Frontmatter2 struct
|
// Frontmatter2 struct
|
||||||
@[heap]
|
@[heap]
|
||||||
pub struct Frontmatter2 {
|
pub struct Frontmatter2 {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
|
||||||
!!hero_code.generate_installer
|
!!hero_code.generate_client
|
||||||
name:''
|
name:''
|
||||||
classname:'KubeClient'
|
classname:'KubeClient'
|
||||||
singleton:0
|
singleton:0
|
||||||
|
|||||||
@@ -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')
|
|
||||||
// }
|
|
||||||
@@ -1,19 +1,15 @@
|
|||||||
module kubernetes
|
module kubernetes
|
||||||
|
|
||||||
import incubaid.herolib.osal.core as osal
|
import incubaid.herolib.osal.core as osal
|
||||||
import incubaid.herolib.core.httpconnection
|
|
||||||
import incubaid.herolib.core.pathlib
|
|
||||||
import incubaid.herolib.ui.console
|
import incubaid.herolib.ui.console
|
||||||
import json
|
import json
|
||||||
import os
|
|
||||||
|
|
||||||
|
|
||||||
@[params]
|
@[params]
|
||||||
pub struct KubectlExecArgs {
|
pub struct KubectlExecArgs {
|
||||||
pub mut:
|
pub mut:
|
||||||
command string
|
command string
|
||||||
timeout int = 30
|
timeout int = 30
|
||||||
retry int = 0
|
retry int
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct KubectlResult {
|
pub struct KubectlResult {
|
||||||
@@ -29,18 +25,18 @@ pub fn (mut k KubeClient) kubectl_exec(args KubectlExecArgs) !KubectlResult {
|
|||||||
mut cmd := 'kubectl'
|
mut cmd := 'kubectl'
|
||||||
|
|
||||||
if k.config.namespace.len > 0 {
|
if k.config.namespace.len > 0 {
|
||||||
cmd += '--namespace=${k.config.namespace} '
|
cmd += ' --namespace=${k.config.namespace}'
|
||||||
}
|
}
|
||||||
|
|
||||||
if k.kubeconfig_path.len > 0 {
|
if k.kubeconfig_path.len > 0 {
|
||||||
cmd += '--kubeconfig=${k.kubeconfig_path} '
|
cmd += ' --kubeconfig=${k.kubeconfig_path}'
|
||||||
}
|
}
|
||||||
|
|
||||||
if k.config.context.len > 0 {
|
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}')
|
console.print_debug('executing: ${cmd}')
|
||||||
|
|
||||||
@@ -59,7 +55,6 @@ pub fn (mut k KubeClient) kubectl_exec(args KubectlExecArgs) !KubectlResult {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Test connection to cluster
|
// Test connection to cluster
|
||||||
pub fn (mut k KubeClient) test_connection() !bool {
|
pub fn (mut k KubeClient) test_connection() !bool {
|
||||||
result := k.kubectl_exec(command: 'cluster-info')!
|
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}')
|
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;
|
// Get node count
|
||||||
// version_data := json.decode(map[string]interface{}, result.stdout)!
|
nodes_result := k.kubectl_exec(command: 'get nodes -o json')!
|
||||||
// server_version := version_data['serverVersion'] or { return error('No serverVersion') }
|
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
|
// Get namespace count
|
||||||
// nodes_result := k.kubectl_exec(command: 'get nodes -o json')!
|
ns_result := k.kubectl_exec(command: 'get namespaces -o json')!
|
||||||
// nodes_count := if nodes_result.success {
|
mut ns_count := 0
|
||||||
// nodes_data := json.decode(map[string]interface{}, nodes_result.stdout)!
|
if ns_result.success {
|
||||||
// items := nodes_data['items'] or { []interface{}{} }
|
ns_list := json.decode(KubectlListResponse, ns_result.stdout) or {
|
||||||
// items.len
|
console.print_debug('Failed to parse namespaces JSON: ${err}')
|
||||||
// } else {
|
KubectlListResponse{}
|
||||||
// 0
|
}
|
||||||
// }
|
ns_count = ns_list.items.len
|
||||||
|
}
|
||||||
|
|
||||||
// // Get namespace count
|
// Get running pods count
|
||||||
// ns_result := k.kubectl_exec(command: 'get namespaces -o json')!
|
pods_result := k.kubectl_exec(command: 'get pods --all-namespaces -o json')!
|
||||||
// ns_count := if ns_result.success {
|
mut pods_count := 0
|
||||||
// ns_data := json.decode(map[string]interface{}, ns_result.stdout)!
|
if pods_result.success {
|
||||||
// items := ns_data['items'] or { []interface{}{} }
|
pods_list := json.decode(KubectlListResponse, pods_result.stdout) or {
|
||||||
// items.len
|
console.print_debug('Failed to parse pods JSON: ${err}')
|
||||||
// } else {
|
KubectlListResponse{}
|
||||||
// 0
|
}
|
||||||
// }
|
pods_count = pods_list.items.len
|
||||||
|
}
|
||||||
|
|
||||||
// // Get running pods count
|
return ClusterInfo{
|
||||||
// pods_result := k.kubectl_exec(command: 'get pods --all-namespaces -o json')!
|
version: version_str
|
||||||
// pods_count := if pods_result.success {
|
nodes: nodes_count
|
||||||
// pods_data := json.decode(map[string]interface{}, pods_result.stdout)!
|
namespaces: ns_count
|
||||||
// items := pods_data['items'] or { []interface{}{} }
|
running_pods: pods_count
|
||||||
// items.len
|
api_server: k.config.api_server
|
||||||
// } 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{}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get resources (Pods, Deployments, Services, etc.)
|
// 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')!
|
result := k.kubectl_exec(command: 'get pods -n ${namespace} -o json')!
|
||||||
if !result.success {
|
if !result.success {
|
||||||
return error('Failed to get pods: ${result.stderr}')
|
return error('Failed to get pods: ${result.stderr}')
|
||||||
}
|
}
|
||||||
|
|
||||||
println(result.stdout)
|
// Parse JSON response using struct-based decoding
|
||||||
$dbg;
|
pod_list := json.decode(KubectlPodListResponse, result.stdout) or {
|
||||||
// data := json.decode(map[string]interface{}, result.stdout)!
|
return error('Failed to parse pods JSON: ${err}')
|
||||||
// items := data['items'] or { []interface{}{} }
|
}
|
||||||
// return items as []map[string]interface{}
|
|
||||||
|
|
||||||
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')!
|
result := k.kubectl_exec(command: 'get deployments -n ${namespace} -o json')!
|
||||||
if !result.success {
|
if !result.success {
|
||||||
return error('Failed to get deployments: ${result.stderr}')
|
return error('Failed to get deployments: ${result.stderr}')
|
||||||
}
|
}
|
||||||
|
|
||||||
// data := json.decode(map[string]interface{}, result.stdout)!
|
// Parse JSON response using struct-based decoding
|
||||||
// items := data['items'] or { []interface{}{} }
|
deployment_list := json.decode(KubectlDeploymentListResponse, result.stdout) or {
|
||||||
// return items as []map[string]interface{}
|
return error('Failed to parse deployments JSON: ${err}')
|
||||||
panic('Not implemented')
|
}
|
||||||
|
|
||||||
|
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')!
|
result := k.kubectl_exec(command: 'get services -n ${namespace} -o json')!
|
||||||
if !result.success {
|
if !result.success {
|
||||||
return error('Failed to get services: ${result.stderr}')
|
return error('Failed to get services: ${result.stderr}')
|
||||||
}
|
}
|
||||||
|
|
||||||
// data := json.decode(map[string]interface{}, result.stdout)!
|
// Parse JSON response using struct-based decoding
|
||||||
// items := data['items'] or { []interface{}{} }
|
service_list := json.decode(KubectlServiceListResponse, result.stdout) or {
|
||||||
// return items as []map[string]interface{}
|
return error('Failed to parse services JSON: ${err}')
|
||||||
panic('Not implemented')
|
}
|
||||||
|
|
||||||
|
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
|
// Apply YAML file
|
||||||
|
|||||||
@@ -2,10 +2,7 @@ module kubernetes
|
|||||||
|
|
||||||
import incubaid.herolib.core.base
|
import incubaid.herolib.core.base
|
||||||
import incubaid.herolib.core.playbook { PlayBook }
|
import incubaid.herolib.core.playbook { PlayBook }
|
||||||
import incubaid.herolib.ui.console
|
|
||||||
import json
|
import json
|
||||||
import incubaid.herolib.osal.startupmanager
|
|
||||||
import time
|
|
||||||
|
|
||||||
__global (
|
__global (
|
||||||
kubernetes_global map[string]&KubeClient
|
kubernetes_global map[string]&KubeClient
|
||||||
@@ -134,176 +131,6 @@ pub fn play(mut plbook PlayBook) ! {
|
|||||||
install_action.done = true
|
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
|
// switch instance to be used for kubernetes
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
module kubernetes
|
module kubernetes
|
||||||
|
|
||||||
import incubaid.herolib.data.paramsparser
|
|
||||||
import incubaid.herolib.data.encoderhero
|
import incubaid.herolib.data.encoderhero
|
||||||
import os
|
|
||||||
|
|
||||||
pub const version = '0.0.0'
|
pub const version = '0.0.0'
|
||||||
const singleton = false
|
const singleton = false
|
||||||
@@ -12,9 +10,9 @@ const default = true
|
|||||||
pub struct KubeClient {
|
pub struct KubeClient {
|
||||||
pub mut:
|
pub mut:
|
||||||
name string = 'default'
|
name string = 'default'
|
||||||
kubeconfig_path string
|
kubeconfig_path string // Path to kubeconfig file
|
||||||
config KubeConfig
|
config KubeConfig // Kubernetes configuration
|
||||||
connected bool
|
connected bool // Connection status
|
||||||
api_version string = 'v1'
|
api_version string = 'v1'
|
||||||
cache_enabled bool = true
|
cache_enabled bool = true
|
||||||
cache_ttl_seconds int = 300
|
cache_ttl_seconds int = 300
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
module kubernetes
|
module kubernetes
|
||||||
|
|
||||||
import incubaid.herolib.data.encoderhero
|
|
||||||
import os
|
|
||||||
|
|
||||||
// K8s API Version and Kind tracking
|
// K8s API Version and Kind tracking
|
||||||
@[params]
|
@[params]
|
||||||
pub struct K8sMetadata {
|
pub struct K8sMetadata {
|
||||||
@@ -150,7 +147,7 @@ pub mut:
|
|||||||
pub struct KubeConfig {
|
pub struct KubeConfig {
|
||||||
pub mut:
|
pub mut:
|
||||||
kubeconfig_path string
|
kubeconfig_path string
|
||||||
context string = ''
|
context string
|
||||||
namespace string = 'default'
|
namespace string = 'default'
|
||||||
api_server string
|
api_server string
|
||||||
ca_cert_path string
|
ca_cert_path string
|
||||||
@@ -179,3 +176,189 @@ pub mut:
|
|||||||
running_pods int
|
running_pods int
|
||||||
api_server string
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,76 +1,249 @@
|
|||||||
module kubernetes
|
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() ! {
|
fn test_model_creation() ! {
|
||||||
mut deployment := DeploymentSpec{
|
mut client := new(name: 'test-cluster')!
|
||||||
metadata: K8sMetadata{
|
assert client.name == 'test-cluster'
|
||||||
name: 'test-app'
|
}
|
||||||
namespace: 'default'
|
|
||||||
}
|
// ============================================================================
|
||||||
replicas: 3
|
// Unit Tests for Data Structures and JSON Parsing
|
||||||
selector: {
|
// ============================================================================
|
||||||
'app': 'test-app'
|
|
||||||
}
|
// Test Pod struct creation and field access
|
||||||
template: PodSpec{
|
fn test_pod_struct_creation() ! {
|
||||||
metadata: K8sMetadata{
|
mut pod := Pod{
|
||||||
name: 'test-app-pod'
|
name: 'test-pod'
|
||||||
namespace: 'default'
|
namespace: 'default'
|
||||||
}
|
status: 'Running'
|
||||||
containers: [
|
node: 'node-1'
|
||||||
ContainerSpec{
|
ip: '10.244.0.5'
|
||||||
name: 'app'
|
containers: ['nginx', 'sidecar']
|
||||||
image: 'nginx:latest'
|
labels: {
|
||||||
ports: [
|
'app': 'web'
|
||||||
ContainerPort{
|
'env': 'prod'
|
||||||
name: 'http'
|
|
||||||
container_port: 80
|
|
||||||
},
|
|
||||||
]
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
created_at: '2024-01-15T10:30:00Z'
|
||||||
}
|
}
|
||||||
|
|
||||||
yaml := yaml_from_deployment(deployment)!
|
assert pod.name == 'test-pod'
|
||||||
assert yaml.contains('apiVersion: apps/v1')
|
assert pod.namespace == 'default'
|
||||||
assert yaml.contains('kind: Deployment')
|
assert pod.status == 'Running'
|
||||||
assert yaml.contains('test-app')
|
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() ! {
|
// Test Deployment struct creation
|
||||||
// Create test YAML file
|
fn test_deployment_struct_creation() ! {
|
||||||
test_yaml := ''
|
mut deployment := Deployment{
|
||||||
'
|
name: 'nginx-deployment'
|
||||||
apiVersion: apps/v1
|
namespace: 'default'
|
||||||
kind: Deployment
|
replicas: 3
|
||||||
metadata:
|
ready_replicas: 3
|
||||||
name: test-deployment
|
available_replicas: 3
|
||||||
namespace: default
|
updated_replicas: 3
|
||||||
spec:
|
labels: {
|
||||||
replicas: 1
|
'app': 'nginx'
|
||||||
selector:
|
}
|
||||||
matchLabels:
|
created_at: '2024-01-15T09:00:00Z'
|
||||||
app: test
|
}
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
app: test
|
|
||||||
spec:
|
|
||||||
containers:
|
|
||||||
- name: app
|
|
||||||
image: nginx:latest
|
|
||||||
'
|
|
||||||
''
|
|
||||||
|
|
||||||
test_file := '/tmp/test-deployment.yaml'
|
assert deployment.name == 'nginx-deployment'
|
||||||
os.write_file(test_file, test_yaml)!
|
assert deployment.namespace == 'default'
|
||||||
|
assert deployment.replicas == 3
|
||||||
result := yaml_validate(test_file)!
|
assert deployment.ready_replicas == 3
|
||||||
assert result.valid
|
assert deployment.available_replicas == 3
|
||||||
assert result.kind == 'Deployment'
|
assert deployment.updated_replicas == 3
|
||||||
assert result.metadata.name == 'test-deployment'
|
assert deployment.labels['app'] == 'nginx'
|
||||||
|
assert deployment.created_at == '2024-01-15T09:00:00Z'
|
||||||
os.rm(test_file)!
|
}
|
||||||
|
|
||||||
|
// 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')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
module kubernetes
|
module kubernetes
|
||||||
|
|
||||||
import json
|
|
||||||
import incubaid.herolib.data.markdown
|
|
||||||
import incubaid.herolib.core.pathlib
|
import incubaid.herolib.core.pathlib
|
||||||
import os
|
|
||||||
|
|
||||||
// Parse YAML file and return validation result
|
// Parse YAML file and return validation result
|
||||||
pub fn yaml_validate(yaml_path string) !K8sValidationResult {
|
pub fn yaml_validate(yaml_path string) !K8sValidationResult {
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||
name: ${cfg.configpath}
|
|
||||||
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user