Merge pull request #192 from Incubaid/development_openrouter

Adding Cryptpad installer and finalizing the Kubernetes client
This commit is contained in:
Omdanii
2025-11-02 13:54:21 +02:00
committed by GitHub
50 changed files with 2889 additions and 1611 deletions

3
.gitignore vendored
View File

@@ -57,4 +57,5 @@ MCP_HTTP_REST_IMPLEMENTATION_PLAN.md
tmux_logger
release
install_herolib
doc
doc
priv_key.bin

View File

@@ -0,0 +1,120 @@
# OpenRouter Examples - Proof of Concept
## Overview
This folder contains **example scripts** demonstrating how to use the **OpenAI client** (`herolib.clients.openai`) configured to work with **OpenRouter**.
* **Goal:** Show how to send messages to OpenRouter models using the OpenAI client, run a **two-model pipeline** for code enhancement, and illustrate multi-model usage.
* **Key Insight:** The OpenAI client is OpenRouter-compatible by design - simply configure it with OpenRouter's base URL (`https://openrouter.ai/api/v1`) and API key.
---
## Configuration
All examples configure the OpenAI client to use OpenRouter by setting:
* **URL**: `https://openrouter.ai/api/v1`
* **API Key**: Read from `OPENROUTER_API_KEY` environment variable
* **Model**: OpenRouter model IDs (e.g., `qwen/qwen-2.5-coder-32b-instruct`)
Example configuration:
```v
playcmds.run(
heroscript: '
!!openai.configure
name: "default"
url: "https://openrouter.ai/api/v1"
model_default: "qwen/qwen-2.5-coder-32b-instruct"
'
)!
```
---
## Example Scripts
### 1. `openai_init.vsh`
* **Purpose:** Basic initialization example showing OpenAI client configured for OpenRouter.
* **Demonstrates:** Client configuration and simple chat completion.
* **Usage:**
```bash
examples/ai/openai/openai_init.vsh
```
---
### 2. `openai_hello.vsh`
* **Purpose:** Simple hello message to OpenRouter.
* **Demonstrates:** Sending a single message using `client.chat_completion`.
* **Usage:**
```bash
examples/ai/openai/openai_hello.vsh
```
* **Expected output:** A friendly "hello" response from the AI and token usage.
---
### 3. `openai_example.vsh`
* **Purpose:** Demonstrates basic conversation features.
* **Demonstrates:**
* Sending a single message
* Using system + user messages for conversation context
* Printing token usage
* **Usage:**
```bash
examples/ai/openai/openai_example.vsh
```
* **Expected output:** Responses from the AI for both simple and system-prompt conversations.
---
### 4. `openai_two_model_pipeline.vsh`
* **Purpose:** Two-model code enhancement pipeline (proof of concept).
* **Demonstrates:**
* Model A (`Qwen3 Coder`) suggests code improvements.
* Model B (`morph-v3-fast`) applies the suggested edits.
* Tracks tokens and shows before/after code.
* Using two separate OpenAI client instances with different models
* **Usage:**
```bash
examples/ai/openai/openai_two_model_pipeline.vsh
```
* **Expected output:**
* Original code
* Suggested edits
* Final updated code
* Token usage summary
---
## Environment Variables
Set your OpenRouter API key before running the examples:
```bash
export OPENROUTER_API_KEY="sk-or-v1-..."
```
The OpenAI client automatically detects when the URL contains "openrouter" and will use the `OPENROUTER_API_KEY` environment variable.
---
## Notes
1. **No separate OpenRouter client needed** - The OpenAI client is fully compatible with OpenRouter's API.
2. All scripts configure the OpenAI client with OpenRouter's base URL.
3. The two-model pipeline uses **two separate client instances** (one per model) to demonstrate multi-model workflows.
4. Scripts can be run individually using the `v -enable-globals run` command.
5. The two-model pipeline is a **proof of concept**; the flow can later be extended to multiple files or OpenRPC specs.

View File

@@ -1,12 +1,22 @@
#!/usr/bin/env -S v -n -w -gc none -cc tcc -d use_openssl -enable-globals run
import incubaid.herolib.clients.openrouter
import incubaid.herolib.clients.openai
import incubaid.herolib.core.playcmds
// Get the client instance
mut client := openrouter.get()!
// Configure OpenAI client to use OpenRouter
playcmds.run(
heroscript: '
!!openai.configure
name: "default"
url: "https://openrouter.ai/api/v1"
model_default: "qwen/qwen-2.5-coder-32b-instruct"
'
)!
println('🤖 OpenRouter Client Example')
// Get the client instance
mut client := openai.get()!
println('🤖 OpenRouter Client Example (using OpenAI client)')
println(''.repeat(50))
println('')
@@ -29,11 +39,11 @@ println('─'.repeat(50))
r = client.chat_completion(
model: 'qwen/qwen-2.5-coder-32b-instruct'
messages: [
openrouter.Message{
openai.Message{
role: .system
content: 'You are a helpful coding assistant who speaks concisely.'
},
openrouter.Message{
openai.Message{
role: .user
content: 'What is V programming language?'
},

View File

@@ -1,10 +1,20 @@
#!/usr/bin/env -S v -n -w -gc none -cc tcc -d use_openssl -enable-globals run
import incubaid.herolib.clients.openrouter
import incubaid.herolib.clients.openai
import incubaid.herolib.core.playcmds
// Configure OpenAI client to use OpenRouter
playcmds.run(
heroscript: '
!!openai.configure
name: "default"
url: "https://openrouter.ai/api/v1"
model_default: "qwen/qwen-2.5-coder-32b-instruct"
'
)!
// Get the client instance
mut client := openrouter.get() or {
mut client := openai.get() or {
eprintln('Failed to get client: ${err}')
return
}

View File

@@ -3,7 +3,7 @@
import incubaid.herolib.clients.openai
import incubaid.herolib.core.playcmds
//to set the API key, either set it here, or set the OPENAI_API_KEY environment variable
// to set the API key, either set it here, or set the OPENAI_API_KEY environment variable
playcmds.run(
heroscript: '
@@ -20,3 +20,5 @@ mut r := client.chat_completion(
temperature: 0.3
max_completion_tokens: 1024
)!
println(r.result)

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env -S v -n -w -gc none -cc tcc -d use_openssl -enable-globals run
import incubaid.herolib.clients.openrouter
import incubaid.herolib.clients.openai
import incubaid.herolib.core.playcmds
// Sample code file to be improved
@@ -19,23 +19,41 @@ def find_max(lst):
return max
'
mut modifier := openrouter.get(name: 'modifier', create: true) or {
panic('Failed to get modifier client: ${err}')
}
// Configure two OpenAI client instances to use OpenRouter with different models
// Model A: Enhancement model (Qwen Coder)
playcmds.run(
heroscript: '
!!openai.configure
name: "enhancer"
url: "https://openrouter.ai/api/v1"
model_default: "qwen/qwen-2.5-coder-32b-instruct"
'
)!
mut enhancer := openrouter.get(name: 'enhancer', create: true) or {
panic('Failed to get enhancer client: ${err}')
}
// Model B: Modification model (Llama 3.3 70B)
playcmds.run(
heroscript: '
!!openai.configure
name: "modifier"
url: "https://openrouter.ai/api/v1"
model_default: "meta-llama/llama-3.3-70b-instruct"
'
)!
mut enhancer := openai.get(name: 'enhancer') or { panic('Failed to get enhancer client: ${err}') }
mut modifier := openai.get(name: 'modifier') or { panic('Failed to get modifier client: ${err}') }
println(''.repeat(70))
println('🔧 Two-Model Code Enhancement Pipeline - Proof of Concept')
println('🔧 Using OpenAI client configured for OpenRouter')
println(''.repeat(70))
println('')
// Step 1: Get enhancement suggestions from Model A (Qwen3 Coder 480B)
// Step 1: Get enhancement suggestions from Model A (Qwen Coder)
println('📝 STEP 1: Code Enhancement Analysis')
println(''.repeat(70))
println('Model: Qwen3 Coder 480B A35B')
println('Model: qwen/qwen-2.5-coder-32b-instruct')
println('Task: Analyze code and suggest improvements\n')
enhancement_prompt := 'You are a code enhancement agent.
@@ -68,10 +86,10 @@ println(enhancement_result.result)
println(''.repeat(70))
println('Tokens used: ${enhancement_result.usage.total_tokens}\n')
// Step 2: Apply edits using Model B (morph-v3-fast)
// Step 2: Apply edits using Model B (Llama 3.3 70B)
println('\n📝 STEP 2: Apply Code Modifications')
println(''.repeat(70))
println('Model: morph-v3-fast')
println('Model: meta-llama/llama-3.3-70b-instruct')
println('Task: Apply the suggested edits to produce updated code\n')
modification_prompt := 'You are a file editing agent.
@@ -106,9 +124,9 @@ println('Tokens used: ${modification_result.usage.total_tokens}\n')
println('\n📊 PIPELINE SUMMARY')
println(''.repeat(70))
println('Original code length: ${sample_code.len} chars')
println('Enhancement model: qwen/qwq-32b-preview (Qwen3 Coder 480B A35B)')
println('Enhancement model: qwen/qwen-2.5-coder-32b-instruct')
println('Enhancement tokens: ${enhancement_result.usage.total_tokens}')
println('Modification model: neversleep/llama-3.3-70b-instruct (morph-v3-fast)')
println('Modification model: meta-llama/llama-3.3-70b-instruct')
println('Modification tokens: ${modification_result.usage.total_tokens}')
println('Total tokens: ${enhancement_result.usage.total_tokens +
modification_result.usage.total_tokens}')

View File

@@ -1,78 +0,0 @@
# OpenRouter Examples - Proof of Concept
## Overview
This folder contains **example scripts** demonstrating the usage of the OpenRouter V client (`herolib.clients.openrouter`).
* **Goal:** Show how to send messages to OpenRouter models, run a **two-model pipeline** for code enhancement, and illustrate multi-model usage.
---
## Example Scripts
### 1. `say_hello.vsh`
* **Purpose:** Simple hello message to OpenRouter.
* **Demonstrates:** Sending a single message using `client.chat_completion`.
* **Usage:**
```bash
examples/clients/openrouter/openrouter_hello.vsh
```
* **Expected output:** A friendly "hello" response from the AI and token usage.
---
### 2. `openrouter_example.vsh`
* **Purpose:** Demonstrates basic conversation features.
* **Demonstrates:**
* Sending a single message
* Using system + user messages for conversation context
* Printing token usage
* **Usage:**
```bash
examples/clients/openrouter/openrouter_example.vsh
```
* **Expected output:** Responses from the AI for both simple and system-prompt conversations.
---
### 3. `openrouter_two_model_pipeline.vsh`
* **Purpose:** Two-model code enhancement pipeline (proof of concept).
* **Demonstrates:**
* Model A (`Qwen3 Coder`) suggests code improvements.
* Model B (`morph-v3-fast`) applies the suggested edits.
* Tracks tokens and shows before/after code.
* **Usage:**
```bash
examples/clients/openrouter/openrouter_two_model_pipeline.vsh
```
* **Expected output:**
* Original code
* Suggested edits
* Final updated code
* Token usage summary
---
## Notes
1. Ensure your **OpenRouter API key** is set:
```bash
export OPENROUTER_API_KEY="sk-or-v1-..."
```
2. All scripts use the **same OpenRouter client** instance for simplicity, except the two-model pipeline which uses **two separate client instances** (one per model).
3. Scripts can be run individually using the `v -enable-globals run` command.
4. The two-model pipeline is a **proof of concept**; the flow can later be extended to multiple files or OpenRPC specs.

1
examples/installers/k8s/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
cryptpad

View File

@@ -0,0 +1,23 @@
#!/usr/bin/env -S v -n -w -gc none -cc tcc -d use_openssl -enable-globals run
import incubaid.herolib.installers.k8s.cryptpad
// This example demonstrates how to use the CryptPad installer.
// 1. Create a new installer instance with a specific hostname.
// Replace 'mycryptpad' with your desired hostname.
mut installer := cryptpad.get(
name: 'kristof'
create: true
)!
// 2. Install CryptPad.
// This will generate the necessary Kubernetes YAML files and apply them to your cluster.
// installer.install()!
// cryptpad.delete()!
// println('CryptPad installation started.')
// println('You can access it at https://${installer.hostname}.gent01.grid.tf')
// 3. To destroy the deployment, you can run the following:
installer.destroy()!

View File

@@ -0,0 +1,11 @@
#!/usr/bin/env -S v -n -w -gc none -cc tcc -d use_openssl -enable-globals run
import incubaid.herolib.installers.virt.kubernetes_installer
mut kubectl := kubernetes_installer.get(name: 'k_installer', create: true)!
// To install
kubectl.install()!
// To remove
kubectl.destroy()!

1
examples/virt/kubernetes/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
kubernetes_example

View 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`

View 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('')

View File

@@ -1,187 +0,0 @@
module cryptpad
import incubaid.herolib.osal.core as osal
import incubaid.herolib.ui.console
import incubaid.herolib.core.texttools
import incubaid.herolib.core.pathlib
import incubaid.herolib.osal.systemd
import incubaid.herolib.osal.startupmanager
import incubaid.herolib.installers.ulist
import incubaid.herolib.installers.lang.golang
import incubaid.herolib.installers.lang.rust
import incubaid.herolib.installers.lang.python
import os
fn startupcmd() ![]startupmanager.ZProcessNewArgs {
mut installer := get()!
mut res := []startupmanager.ZProcessNewArgs{}
// THIS IS EXAMPLE CODEAND NEEDS TO BE CHANGED
// res << startupmanager.ZProcessNewArgs{
// name: 'cryptpad'
// cmd: 'cryptpad server'
// env: {
// 'HOME': '/root'
// }
// }
return res
}
fn running() !bool {
mut installer := get()!
// THIS IS EXAMPLE CODEAND NEEDS TO BE CHANGED
// this checks health of cryptpad
// curl http://localhost:3333/api/v1/s --oauth2-bearer 1234 works
// url:='http://127.0.0.1:${cfg.port}/api/v1'
// mut conn := httpconnection.new(name: 'cryptpad', url: url)!
// if cfg.secret.len > 0 {
// conn.default_header.add(.authorization, 'Bearer ${cfg.secret}')
// }
// conn.default_header.add(.content_type, 'application/json')
// console.print_debug("curl -X 'GET' '${url}'/tags --oauth2-bearer ${cfg.secret}")
// r := conn.get_json_dict(prefix: 'tags', debug: false) or {return false}
// println(r)
// if true{panic("ssss")}
// tags := r['Tags'] or { return false }
// console.print_debug(tags)
// console.print_debug('cryptpad is answering.')
return false
}
fn start_pre() ! {
}
fn start_post() ! {
}
fn stop_pre() ! {
}
fn stop_post() ! {
}
//////////////////// following actions are not specific to instance of the object
// checks if a certain version or above is installed
fn installed() !bool {
// THIS IS EXAMPLE CODEAND NEEDS TO BE CHANGED
// res := os.execute('${osal.profile_path_source_and()!} cryptpad version')
// if res.exit_code != 0 {
// return false
// }
// r := res.output.split_into_lines().filter(it.trim_space().len > 0)
// if r.len != 1 {
// return error("couldn't parse cryptpad version.\n${res.output}")
// }
// if texttools.version(version) == texttools.version(r[0]) {
// return true
// }
return false
}
// get the Upload List of the files
fn ulist_get() !ulist.UList {
// optionally build a UList which is all paths which are result of building, is then used e.g. in upload
return ulist.UList{}
}
// uploads to S3 server if configured
fn upload() ! {
// installers.upload(
// cmdname: 'cryptpad'
// source: '${gitpath}/target/x86_64-unknown-linux-musl/release/cryptpad'
// )!
}
fn install() ! {
console.print_header('install cryptpad')
// THIS IS EXAMPLE CODEAND NEEDS TO BE CHANGED
// mut url := ''
// if core.is_linux_arm()! {
// url = 'https://github.com/cryptpad-dev/cryptpad/releases/download/v${version}/cryptpad_${version}_linux_arm64.tar.gz'
// } else if core.is_linux_intel()! {
// url = 'https://github.com/cryptpad-dev/cryptpad/releases/download/v${version}/cryptpad_${version}_linux_amd64.tar.gz'
// } else if core.is_osx_arm()! {
// url = 'https://github.com/cryptpad-dev/cryptpad/releases/download/v${version}/cryptpad_${version}_darwin_arm64.tar.gz'
// } else if osal.is_osx_intel()! {
// url = 'https://github.com/cryptpad-dev/cryptpad/releases/download/v${version}/cryptpad_${version}_darwin_amd64.tar.gz'
// } else {
// return error('unsported platform')
// }
// mut dest := osal.download(
// url: url
// minsize_kb: 9000
// expand_dir: '/tmp/cryptpad'
// )!
// //dest.moveup_single_subdir()!
// mut binpath := dest.file_get('cryptpad')!
// osal.cmd_add(
// cmdname: 'cryptpad'
// source: binpath.path
// )!
}
fn build() ! {
// url := 'https://github.com/threefoldtech/cryptpad'
// make sure we install base on the node
// if core.platform() != .ubuntu {
// return error('only support ubuntu for now')
// }
// golang.install()!
// console.print_header('build cryptpad')
// gitpath := gittools.get_repo(coderoot: '/tmp/builder', url: url, reset: true, pull: true)!
// cmd := '
// cd ${gitpath}
// source ~/.cargo/env
// exit 1 #todo
// '
// osal.execute_stdout(cmd)!
//
// //now copy to the default bin path
// mut binpath := dest.file_get('...')!
// adds it to path
// osal.cmd_add(
// cmdname: 'griddriver2'
// source: binpath.path
// )!
}
fn destroy() ! {
// mut systemdfactory := systemd.new()!
// systemdfactory.destroy("zinit")!
// osal.process_kill_recursive(name:'zinit')!
// osal.cmd_delete('zinit')!
// osal.package_remove('
// podman
// conmon
// buildah
// skopeo
// runc
// ')!
// //will remove all paths where go/bin is found
// osal.profile_path_add_remove(paths2delete:"go/bin")!
// osal.rm("
// podman
// conmon
// buildah
// skopeo
// runc
// /var/lib/containers
// /var/lib/podman
// /var/lib/buildah
// /tmp/podman
// /tmp/conmon
// ")!
}

View File

@@ -1,312 +0,0 @@
module cryptpad
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 (
cryptpad_global map[string]&CryptPad
cryptpad_default string
)
/////////FACTORY
@[params]
pub struct ArgsGet {
pub mut:
name string = 'default'
fromdb bool // will load from filesystem
create bool // default will not create if not exist
}
pub fn new(args ArgsGet) !&CryptPad {
mut obj := CryptPad{
name: args.name
}
set(obj)!
return get(name: args.name)!
}
pub fn get(args ArgsGet) !&CryptPad {
mut context := base.context()!
cryptpad_default = args.name
if args.fromdb || args.name !in cryptpad_global {
mut r := context.redis()!
if r.hexists('context:cryptpad', args.name)! {
data := r.hget('context:cryptpad', args.name)!
if data.len == 0 {
print_backtrace()
return error('CryptPad with name: ${args.name} does not exist, prob bug.')
}
mut obj := json.decode(CryptPad, data)!
set_in_mem(obj)!
} else {
if args.create {
new(args)!
} else {
print_backtrace()
return error("CryptPad with name '${args.name}' does not exist")
}
}
return get(name: args.name)! // no longer from db nor create
}
return cryptpad_global[args.name] or {
print_backtrace()
return error('could not get config for cryptpad with name:${args.name}')
}
}
// register the config for the future
pub fn set(o CryptPad) ! {
mut o2 := set_in_mem(o)!
cryptpad_default = o2.name
mut context := base.context()!
mut r := context.redis()!
r.hset('context:cryptpad', o2.name, json.encode(o2))!
}
// does the config exists?
pub fn exists(args ArgsGet) !bool {
mut context := base.context()!
mut r := context.redis()!
return r.hexists('context:cryptpad', args.name)!
}
pub fn delete(args ArgsGet) ! {
mut context := base.context()!
mut r := context.redis()!
r.hdel('context:cryptpad', args.name)!
}
@[params]
pub struct ArgsList {
pub mut:
fromdb bool // will load from filesystem
}
// if fromdb set: load from filesystem, and not from mem, will also reset what is in mem
pub fn list(args ArgsList) ![]&CryptPad {
mut res := []&CryptPad{}
mut context := base.context()!
if args.fromdb {
// reset what is in mem
cryptpad_global = map[string]&CryptPad{}
cryptpad_default = ''
}
if args.fromdb {
mut r := context.redis()!
mut l := r.hkeys('context:cryptpad')!
for name in l {
res << get(name: name, fromdb: true)!
}
return res
} else {
// load from memory
for _, client in cryptpad_global {
res << client
}
}
return res
}
// only sets in mem, does not set as config
fn set_in_mem(o CryptPad) !CryptPad {
mut o2 := obj_init(o)!
cryptpad_global[o2.name] = &o2
cryptpad_default = o2.name
return o2
}
pub fn play(mut plbook PlayBook) ! {
if !plbook.exists(filter: 'cryptpad.') {
return
}
mut install_actions := plbook.find(filter: 'cryptpad.configure')!
if install_actions.len > 0 {
for mut install_action in install_actions {
heroscript := install_action.heroscript()
mut obj2 := heroscript_loads(heroscript)!
set(obj2)!
install_action.done = true
}
}
mut other_actions := plbook.find(filter: 'cryptpad.')!
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 cryptpad.destroy')
destroy()!
}
if other_action.name == 'install' {
console.print_debug('install action cryptpad.install')
install()!
}
}
if other_action.name in ['start', 'stop', 'restart'] {
mut p := other_action.params
name := p.get('name')!
mut cryptpad_obj := get(name: name)!
console.print_debug('action object:\n${cryptpad_obj}')
if other_action.name == 'start' {
console.print_debug('install action cryptpad.${other_action.name}')
cryptpad_obj.start()!
}
if other_action.name == 'stop' {
console.print_debug('install action cryptpad.${other_action.name}')
cryptpad_obj.stop()!
}
if other_action.name == 'restart' {
console.print_debug('install action cryptpad.${other_action.name}')
cryptpad_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: cryptpad' startupmanager get screen")
return startupmanager.get(.screen)!
}
.zinit {
console.print_debug("installer: cryptpad' startupmanager get zinit")
return startupmanager.get(.zinit)!
}
.systemd {
console.print_debug("installer: cryptpad' startupmanager get systemd")
return startupmanager.get(.systemd)!
}
else {
console.print_debug("installer: cryptpad' startupmanager get auto")
return startupmanager.get(.auto)!
}
}
}
// load from disk and make sure is properly intialized
pub fn (mut self CryptPad) reload() ! {
switch(self.name)
self = obj_init(self)!
}
pub fn (mut self CryptPad) start() ! {
switch(self.name)
if self.running()! {
return
}
console.print_header('installer: cryptpad start')
if !installed()! {
install()!
}
configure()!
start_pre()!
for zprocess in startupcmd()! {
mut sm := startupmanager_get(zprocess.startuptype)!
console.print_debug('installer: cryptpad 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('cryptpad did not install properly.')
}
pub fn (mut self CryptPad) install_start(args InstallArgs) ! {
switch(self.name)
self.install(args)!
self.start()!
}
pub fn (mut self CryptPad) 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 CryptPad) restart() ! {
switch(self.name)
self.stop()!
self.start()!
}
pub fn (mut self CryptPad) 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 CryptPad) install(args InstallArgs) ! {
switch(self.name)
if args.reset || (!installed()!) {
install()!
}
}
pub fn (mut self CryptPad) build() ! {
switch(self.name)
build()!
}
pub fn (mut self CryptPad) destroy() ! {
switch(self.name)
self.stop() or {}
destroy()!
}
// switch instance to be used for cryptpad
pub fn switch(name string) {
cryptpad_default = name
}

View File

@@ -1,52 +0,0 @@
module cryptpad
import incubaid.herolib.data.paramsparser
import incubaid.herolib.data.encoderhero
import os
pub const version = '0.0.0'
const singleton = false
const default = true
// THIS THE THE SOURCE OF THE INFORMATION OF THIS FILE, HERE WE HAVE THE CONFIG OBJECT CONFIGURED AND MODELLED
@[heap]
pub struct CryptPad {
pub mut:
name string = 'default'
domain string
domain_sandbox string
configpath string //can be left empty, will be set to default path (is kubernetes config file)
cryptpad_configpath string //can be left empty, will be set to default path
masters []string //list of master servers for kubernetes
domainname string //can be 1 name e.g. mycryptpad or it can be a fully qualified domain name e.g. mycryptpad.mycompany.com
}
// your checking & initialization code if needed
fn obj_init(mycfg_ CryptPad) !CryptPad {
mut mycfg := mycfg_
if mycfg.domain == '' {
return error('CryptPad client "${mycfg.name}" missing domain')
}
if mycfg.configpath == '' {
mycfg.configpath = '${os.home_dir()}/.apps/cryptpad/${mycfg.name}/config.yaml'
}
//call kubernetes client to get master nodes and put them in
mycfg.masters = []string{} //TODO get from kubernetes
return mycfg
}
// called before start if done
fn configure() ! {
mut installer := get()!
mut mycode := $tmpl('templates/main.yaml')
mut path := pathlib.get_file(path: cfg.configpath, create: true)!
path.write(mycode)!
console.print_debug(mycode)
}
/////////////NORMALLY NO NEED TO TOUCH
pub fn heroscript_loads(heroscript string) !CryptPad {
mut obj := encoderhero.decode[CryptPad](heroscript)!
return obj
}

View File

@@ -1,44 +0,0 @@
# cryptpad
To get started
```v
import incubaid.herolib.installers.something.cryptpad as cryptpad_installer
heroscript:="
!!cryptpad.configure name:'test'
password: '1234'
port: 7701
!!cryptpad.start name:'test' reset:1
"
cryptpad_installer.play(heroscript=heroscript)!
//or we can call the default and do a start with reset
//mut installer:= cryptpad_installer.get()!
//installer.start(reset:true)!
```
## example heroscript
```hero
!!cryptpad.configure
homedir: '/home/user/cryptpad'
username: 'admin'
password: 'secretpassword'
title: 'Some Title'
host: 'localhost'
port: 8888
```

View File

@@ -1,28 +0,0 @@
apiVersion: v1
kind: Namespace
metadata:
name: collab
---
apiVersion: ingress.grid.tf/v1
kind: TFGW
metadata:
name: cryptpad-main
namespace: collab
spec:
hostname: "cryptpadp2"
backends:
- "http://[49a:bfc3:59bf:872c:ff0f:d25f:751c:5106]:80"
- "http://[53c:609e:6488:9ddd:ff0f:2ffc:ac2a:94a6]:80"
- "http://[58e:ca1d:ce3d:355c:ff0f:a065:a40a:a08c]:80"
---
apiVersion: ingress.grid.tf/v1
kind: TFGW
metadata:
name: cryptpad-sandbox
namespace: collab
spec:
hostname: "cryptpads2"
backends:
- "http://[49a:bfc3:59bf:872c:ff0f:d25f:751c:5106]:80"
- "http://[53c:609e:6488:9ddd:ff0f:2ffc:ac2a:94a6]:80"
- "http://[58e:ca1d:ce3d:355c:ff0f:a065:a40a:a08c]:80"

View File

@@ -1,7 +0,0 @@
!!hero_code.generate_client
name:'openrouter'
classname:'OpenRouter'
singleton:0
default:1
hasconfig:1
reset:0

View File

@@ -1,13 +0,0 @@
module openrouter
fn test_factory() {
mut client := get(name: 'default', create: true)!
assert client.name == 'default'
assert client.url == 'https://openrouter.ai/api/v1'
assert client.model_default == 'qwen/qwen-2.5-coder-32b-instruct'
}
fn test_client_creation() {
mut client := new(name: 'test_client')!
assert client.name == 'test_client'
}

View File

@@ -1,97 +0,0 @@
module openrouter
import json
@[params]
pub struct CompletionArgs {
pub mut:
model string
messages []Message // optional because we can use message, which means we just pass a string
message string
temperature f64 = 0.2
max_completion_tokens int = 32000
}
pub struct Message {
pub mut:
role RoleType
content string
}
pub enum RoleType {
system
user
assistant
function
}
fn roletype_str(x RoleType) string {
return match x {
.system {
'system'
}
.user {
'user'
}
.assistant {
'assistant'
}
.function {
'function'
}
}
}
pub struct ChatCompletion {
pub mut:
id string
created u32
result string
usage Usage
}
// creates a new chat completion given a list of messages
// each message consists of message content and the role of the author
pub fn (mut f OpenRouter) chat_completion(args_ CompletionArgs) !ChatCompletion {
mut args := args_
if args.model == '' {
args.model = f.model_default
}
mut m := ChatMessagesRaw{
model: args.model
temperature: args.temperature
max_completion_tokens: args.max_completion_tokens
}
for msg in args.messages {
mr := MessageRaw{
role: roletype_str(msg.role)
content: msg.content
}
m.messages << mr
}
if args.message != '' {
mr := MessageRaw{
role: 'user'
content: args.message
}
m.messages << mr
}
data := json.encode(m)
mut conn := f.connection()!
r := conn.post_json_str(prefix: 'chat/completions', data: data)!
res := json.decode(ChatCompletionRaw, r)!
mut result := ''
for choice in res.choices {
result += choice.message.content
}
mut chat_completion_result := ChatCompletion{
id: res.id
created: res.created
result: result
usage: res.usage
}
return chat_completion_result
}

View File

@@ -1,140 +0,0 @@
module openrouter
import incubaid.herolib.core.base
import incubaid.herolib.core.playbook { PlayBook }
import incubaid.herolib.ui.console
import json
__global (
openrouter_global map[string]&OpenRouter
openrouter_default string
)
/////////FACTORY
@[params]
pub struct ArgsGet {
pub mut:
name string = 'default'
fromdb bool // will load from filesystem
create bool // default will not create if not exist
}
pub fn new(args ArgsGet) !&OpenRouter {
mut obj := OpenRouter{
name: args.name
}
set(obj)!
return get(name: args.name)!
}
pub fn get(args ArgsGet) !&OpenRouter {
mut context := base.context()!
openrouter_default = args.name
if args.fromdb || args.name !in openrouter_global {
mut r := context.redis()!
if r.hexists('context:openrouter', args.name)! {
data := r.hget('context:openrouter', args.name)!
if data.len == 0 {
print_backtrace()
return error('OpenRouter with name: ${args.name} does not exist, prob bug.')
}
mut obj := json.decode(OpenRouter, data)!
set_in_mem(obj)!
} else {
if args.create {
new(args)!
} else {
print_backtrace()
return error("OpenRouter with name '${args.name}' does not exist")
}
}
return get(name: args.name)! // no longer from db nor create
}
return openrouter_global[args.name] or {
print_backtrace()
return error('could not get config for openrouter with name:${args.name}')
}
}
// register the config for the future
pub fn set(o OpenRouter) ! {
mut o2 := set_in_mem(o)!
openrouter_default = o2.name
mut context := base.context()!
mut r := context.redis()!
r.hset('context:openrouter', o2.name, json.encode(o2))!
}
// does the config exists?
pub fn exists(args ArgsGet) !bool {
mut context := base.context()!
mut r := context.redis()!
return r.hexists('context:openrouter', args.name)!
}
pub fn delete(args ArgsGet) ! {
mut context := base.context()!
mut r := context.redis()!
r.hdel('context:openrouter', args.name)!
}
@[params]
pub struct ArgsList {
pub mut:
fromdb bool // will load from filesystem
}
// if fromdb set: load from filesystem, and not from mem, will also reset what is in mem
pub fn list(args ArgsList) ![]&OpenRouter {
mut res := []&OpenRouter{}
mut context := base.context()!
if args.fromdb {
// reset what is in mem
openrouter_global = map[string]&OpenRouter{}
openrouter_default = ''
}
if args.fromdb {
mut r := context.redis()!
mut l := r.hkeys('context:openrouter')!
for name in l {
res << get(name: name, fromdb: true)!
}
return res
} else {
// load from memory
for _, client in openrouter_global {
res << client
}
}
return res
}
// only sets in mem, does not set as config
fn set_in_mem(o OpenRouter) !OpenRouter {
mut o2 := obj_init(o)!
openrouter_global[o2.name] = &o2
openrouter_default = o2.name
return o2
}
pub fn play(mut plbook PlayBook) ! {
if !plbook.exists(filter: 'openrouter.') {
return
}
mut install_actions := plbook.find(filter: 'openrouter.configure')!
if install_actions.len > 0 {
for mut install_action in install_actions {
heroscript := install_action.heroscript()
mut obj2 := heroscript_loads(heroscript)!
set(obj2)!
install_action.done = true
}
}
}
// switch instance to be used for openrouter
pub fn switch(name string) {
openrouter_default = name
}

View File

@@ -1,67 +0,0 @@
module openrouter
import incubaid.herolib.data.encoderhero
import incubaid.herolib.core.httpconnection
import os
pub const version = '0.0.0'
const singleton = false
const default = true
@[heap]
pub struct OpenRouter {
pub mut:
name string = 'default'
api_key string
url string = 'https://openrouter.ai/api/v1'
model_default string = 'qwen/qwen-2.5-coder-32b-instruct'
}
// your checking & initialization code if needed
fn obj_init(mycfg_ OpenRouter) !OpenRouter {
mut mycfg := mycfg_
if mycfg.model_default == '' {
k := os.getenv('OPENROUTER_AI_MODEL')
if k != '' {
mycfg.model_default = k
}
}
if mycfg.url == '' {
k := os.getenv('OPENROUTER_URL')
if k != '' {
mycfg.url = k
}
}
if mycfg.api_key == '' {
k := os.getenv('OPENROUTER_API_KEY')
if k != '' {
mycfg.api_key = k
} else {
return error('OPENROUTER_API_KEY environment variable not set')
}
}
return mycfg
}
pub fn (mut client OpenRouter) connection() !&httpconnection.HTTPConnection {
mut c2 := httpconnection.new(
name: 'openrouterconnection_${client.name}'
url: client.url
cache: false
retry: 20
)!
c2.default_header.set(.authorization, 'Bearer ${client.api_key}')
return c2
}
/////////////NORMALLY NO NEED TO TOUCH
pub fn heroscript_dumps(obj OpenRouter) !string {
return encoderhero.encode[OpenRouter](obj)!
}
pub fn heroscript_loads(heroscript string) !OpenRouter {
mut obj := encoderhero.decode[OpenRouter](heroscript)!
return obj
}

View File

@@ -1,38 +0,0 @@
module openrouter
struct ChatCompletionRaw {
mut:
id string
object string
created u32
choices []ChoiceRaw
usage Usage
}
struct ChoiceRaw {
mut:
index int
message MessageRaw
finish_reason string
}
struct MessageRaw {
mut:
role string
content string
}
struct ChatMessagesRaw {
mut:
model string
messages []MessageRaw
temperature f64 = 0.5
max_completion_tokens int = 32000
}
pub struct Usage {
pub mut:
prompt_tokens int
completion_tokens int
total_tokens int
}

View File

@@ -1,97 +0,0 @@
# OpenRouter V Client
A V client for the OpenRouter API, providing access to multiple AI models through a unified interface.
## Quick Start
```v
import incubaid.herolib.clients.openrouter
import incubaid.herolib.core.playcmds
// Configure client (key can be read from env vars)
playcmds.run(
heroscript: '
!!openrouter.configure name:"default"
key:"${YOUR_OPENROUTER_KEY}"
url:"https://openrouter.ai/api/v1"
model_default:"qwen/qwen-2.5-coder-32b-instruct"
'
reset: false
)!
mut client := openrouter.get()!
// Simple chat example
resp := client.chat_completion(
model: "qwen/qwen-2.5-coder-32b-instruct"
message: "Hello, world!"
temperature: 0.6
)!
println('Answer: ${resp.result}')
```
## Environment Variables
The client automatically reads API keys from environment variables if not explicitly configured:
- `OPENROUTER_API_KEY` - OpenRouter API key
- `AIKEY` - Alternative API key variable
- `AIURL` - API base URL (defaults to `https://openrouter.ai/api/v1`)
- `AIMODEL` - Default model (defaults to `qwen/qwen-2.5-coder-32b-instruct`)
## Example with Multiple Messages
```v
import incubaid.herolib.clients.openrouter
mut client := openrouter.get()!
resp := client.chat_completion(
messages: [
openrouter.Message{
role: .system
content: 'You are a helpful coding assistant.'
},
openrouter.Message{
role: .user
content: 'Write a hello world in V'
},
]
temperature: 0.3
max_completion_tokens: 1024
)!
println(resp.result)
```
## Configuration via Heroscript
```hero
!!openrouter.configure
name: "default"
key: "sk-or-v1-..."
url: "https://openrouter.ai/api/v1"
model_default: "qwen/qwen-2.5-coder-32b-instruct"
```
## Features
- **Chat Completion**: Generate text completions using various AI models
- **Multiple Models**: Access to OpenRouter's extensive model catalog
- **Environment Variable Support**: Automatic configuration from environment
- **Factory Pattern**: Manage multiple client instances
- **Retry Logic**: Built-in retry mechanism for failed requests
## Available Models
OpenRouter provides access to many models including:
- `qwen/qwen-2.5-coder-32b-instruct` - Qwen 2.5 Coder (default)
- `anthropic/claude-3.5-sonnet`
- `openai/gpt-4-turbo`
- `google/gemini-pro`
- `meta-llama/llama-3.1-70b-instruct`
- And many more...
Check the [OpenRouter documentation](https://openrouter.ai/docs) for the full list of available models.

View File

@@ -2,7 +2,6 @@ module zinit
import incubaid.herolib.core.base
import incubaid.herolib.core.playbook { PlayBook }
import incubaid.herolib.ui.console
import json
__global (

View File

@@ -1,8 +1,6 @@
module zinit
import incubaid.herolib.data.encoderhero
import incubaid.herolib.schemas.jsonrpc
import os
pub const version = '0.0.0'
const singleton = true

View File

@@ -10,7 +10,6 @@ import incubaid.herolib.clients.meilisearch
import incubaid.herolib.clients.mycelium
import incubaid.herolib.clients.mycelium_rpc
import incubaid.herolib.clients.openai
import incubaid.herolib.clients.openrouter
import incubaid.herolib.clients.postgresql_client
import incubaid.herolib.clients.qdrant
import incubaid.herolib.clients.rcloneclient
@@ -66,7 +65,6 @@ pub fn run_all(args_ PlayArgs) ! {
mycelium.play(mut plbook)!
mycelium_rpc.play(mut plbook)!
openai.play(mut plbook)!
openrouter.play(mut plbook)!
postgresql_client.play(mut plbook)!
qdrant.play(mut plbook)!
rcloneclient.play(mut plbook)!

View File

@@ -1,7 +1,5 @@
module elements
import toml
// Frontmatter2 struct
@[heap]
pub struct Frontmatter2 {

View File

@@ -0,0 +1,10 @@
!!hero_code.generate_installer
name:'cryptpad'
classname:'CryptpadServer'
singleton:0 //there can only be 1 object in the globals, is called 'default'
templates:1 //are there templates for the installer
default:0 //can we create a default when the factory is used
title:''
supported_platforms:'' //osx, ... (empty means all)
reset:0 // regenerate all, dangerous !!!
startupmanager:0 //not managed by a startup manager

View File

@@ -0,0 +1,72 @@
# CryptPad Kubernetes Installer
A Kubernetes installer for CryptPad with TFGrid Gateway integration.
## Quick Start
```v
import incubaid.herolib.installers.k8s.cryptpad
// Create and install CryptPad
mut installer := cryptpad.get(
name: 'mycryptpad'
create: true
)!
installer.install()!
```
to change the hostname and the namespace, you can override the default values:
```v
mut installer := cryptpad.get(
name: 'mycryptpad'
create: true
)!
installer.hostname = 'customhostname'
installer.namespace = 'customnamespace'
installer.install()!
```
## Usage
### Create an Instance
```v
mut installer := cryptpad.get(
name: 'mycryptpad' // Unique name for this instance
create: true // Create if doesn't exist
)!
```
The instance name will be used as:
- Kubernetes namespace name
- Hostname prefix (e.g., `mycryptpad.gent01.grid.tf`)
### Install
```v
installer.install()!
```
This will:
1. Generate Kubernetes YAML files for CryptPad and TFGrid Gateway
2. Apply them to your k3s cluster
3. Wait for deployment to be ready
### Destroy
```v
installer.destroy()!
```
Removes all CryptPad resources from the cluster.
## Requirements
- kubectl installed and configured
- k3s cluster running
- Redis server running (for configuration storage)

View File

@@ -0,0 +1,189 @@
module cryptpad
import incubaid.herolib.osal.core as osal
import incubaid.herolib.ui.console
import incubaid.herolib.installers.ulist
import time
const max_deployment_retries = 30
const deployment_check_interval_seconds = 2
//////////////////// following actions are not specific to instance of the object
// checks if a certain version or above is installed
fn installed() !bool {
installer := get()!
mut k8s := installer.kube_client
// Try to get the cryptpad deployment
deployments := k8s.get_deployments(installer.namespace) or {
// If we can't get deployments, it's not running
return false
}
// Check if cryptpad deployment exists
for deployment in deployments {
if deployment.name == 'cryptpad' {
return true
}
}
return false
}
// get the Upload List of the files
fn ulist_get() !ulist.UList {
// optionally build a UList which is all paths which are result of building, is then used e.g. in upload
return ulist.UList{}
}
// uploads to S3 server if configured
fn upload() ! {
// installers.upload(
// cmdname: 'cryptpad'
// source: '${gitpath}/target/x86_64-unknown-linux-musl/release/cryptpad'
// )!
}
fn install() ! {
console.print_header('Installing CryptPad...')
// Get installer config to access namespace
installer := get()!
mut k8s := installer.kube_client
configure()!
// 1. Check for dependencies.
console.print_info('Checking for kubectl...')
kubectl_installed()!
console.print_info('kubectl is installed and configured.')
// 4. Apply the YAML files using kubernetes client
console.print_info('Applying Gateway YAML file to the cluster...')
res1 := k8s.apply_yaml('/tmp/tfgw-cryptpad.yaml')!
if !res1.success {
return error('Failed to apply tfgw-cryptpad.yaml: ${res1.stderr}')
}
console.print_info('Gateway YAML file applied successfully.')
// 5. Verify TFGW deployments
verify_tfgw_deployment(tfgw_name: 'cryptpad-main', namespace: installer.namespace)!
verify_tfgw_deployment(tfgw_name: 'cryptpad-sandbox', namespace: installer.namespace)!
// 6. Apply Cryptpad YAML
console.print_info('Applying Cryptpad YAML file to the cluster...')
res2 := k8s.apply_yaml('/tmp/cryptpad.yaml')!
if !res2.success {
return error('Failed to apply cryptpad.yaml: ${res2.stderr}')
}
console.print_info('Cryptpad YAML file applied successfully.')
// 7. Verify deployment status
console.print_info('Verifying deployment status...')
mut is_running := false
for i in 0 .. max_deployment_retries {
if installed()! {
is_running = true
break
}
console.print_info('Waiting for CryptPad deployment to be ready... (${i + 1}/${max_deployment_retries})')
time.sleep(deployment_check_interval_seconds * time.second)
}
if is_running {
console.print_header('CryptPad installation successful!')
} else {
return error('CryptPad deployment failed to start.')
}
}
// params for verifying the generating of the FQDN using tfgw crd
@[params]
struct VerifyTfgwDeployment {
pub mut:
tfgw_name string // tfgw serivce generating the FQDN
namespace string // namespace name for cryptpad deployments/services
retry int = 30
}
// Function for verifying the generating of of the FQDN using tfgw crd
fn verify_tfgw_deployment(args VerifyTfgwDeployment) ! {
console.print_info('Verifying TFGW deployment for ${args.tfgw_name}...')
installer := get()!
mut k8s := installer.kube_client
mut is_fqdn_generated := false
for i in 0 .. args.retry {
// Use kubectl_exec for custom resource (TFGW) with jsonpath
result := k8s.kubectl_exec(
command: 'get tfgw ${args.tfgw_name} -n ${args.namespace} -o jsonpath="{.status.fqdn}"'
) or {
console.print_info('Waiting for FQDN to be generated for ${args.tfgw_name}... (${i + 1}/${args.retry})')
time.sleep(2 * time.second)
continue
}
if result.success && result.stdout != '' {
is_fqdn_generated = true
break
}
console.print_info('Waiting for FQDN to be generated for ${args.tfgw_name}... (${i + 1}/${args.retry})')
time.sleep(2 * time.second)
}
if !is_fqdn_generated {
console.print_stderr('Failed to get FQDN for ${args.tfgw_name}.')
// Use describe_resource to get detailed information about the TFGW resource
result := k8s.describe_resource(
resource: 'tfgw'
resource_name: args.tfgw_name
namespace: args.namespace
) or { return error('TFGW deployment failed for ${args.tfgw_name}.') }
console.print_stderr(result.stdout)
return error('TFGW deployment failed for ${args.tfgw_name}.')
}
console.print_info('TFGW deployment for ${args.tfgw_name} verified successfully.')
}
fn destroy() ! {
console.print_header('Destroying CryptPad...')
installer := get()!
mut k8s := installer.kube_client
console.print_debug('Attempting to delete namespace: ${installer.namespace}')
// Delete the namespace using kubernetes client
result := k8s.delete_resource('namespace', installer.namespace, '') or {
console.print_stderr('Failed to delete namespace ${installer.namespace}: ${err}')
return error('Failed to delete namespace ${installer.namespace}: ${err}')
}
console.print_debug('Delete command completed. Exit code: ${result.exit_code}, Success: ${result.success}')
if !result.success {
// Namespace not found is OK - it means it's already deleted
if result.stderr.contains('NotFound') {
console.print_info('Namespace ${installer.namespace} does not exist (already deleted).')
} else {
console.print_stderr('Failed to delete namespace ${installer.namespace}: ${result.stderr}')
return error('Failed to delete namespace ${installer.namespace}: ${result.stderr}')
}
} else {
console.print_info('Namespace ${installer.namespace} deleted successfully.')
}
}
fn kubectl_installed() ! {
// Check if kubectl command exists
if !osal.cmd_exists('kubectl') {
return error('kubectl is not installed. Please install it to continue.')
}
// Check if kubectl is configured to connect to a cluster
installer := get()!
mut k8s := installer.kube_client
if !k8s.test_connection()! {
return error('kubectl is not configured to connect to a Kubernetes cluster. Please check your kubeconfig.')
}
}

View File

@@ -0,0 +1,193 @@
module cryptpad
import incubaid.herolib.core.base
import incubaid.herolib.core.playbook { PlayBook }
import incubaid.herolib.ui.console
import json
__global (
cryptpad_global map[string]&CryptpadServer
cryptpad_default string
)
/////////FACTORY
@[params]
pub struct ArgsGet {
pub mut:
name string = 'cryptpad'
fromdb bool // will load from filesystem
create bool // default will not create if not exist
}
pub fn new(args ArgsGet) !&CryptpadServer {
mut obj := CryptpadServer{
name: args.name
}
set(obj)!
return get(name: args.name)!
}
pub fn get(args_ ArgsGet) !&CryptpadServer {
mut args := args_
mut context := base.context()!
if args.name == 'cryptpad' && cryptpad_default != '' {
args.name = cryptpad_default
}
if args.fromdb || args.name !in cryptpad_global {
mut r := context.redis()!
if r.hexists('context:cryptpad', args.name)! {
data := r.hget('context:cryptpad', args.name)!
if data.len == 0 {
print_backtrace()
return error('CryptpadServer with name: ${args.name} does not exist, prob bug.')
}
mut obj := json.decode(CryptpadServer, data)!
set_in_mem(obj)!
} else {
if args.create {
new(args)!
} else {
print_backtrace()
return error("CryptpadServer with name '${args.name}' does not exist")
}
}
return get(
name: args.name
fromdb: args.fromdb
create: args.create
)! // no longer from db nor create
}
result := cryptpad_global[args.name] or {
print_backtrace()
return error('could not get config for cryptpad with name:${args.name}')
}
return result
}
// register the config for the future
pub fn set(o CryptpadServer) ! {
mut o2 := set_in_mem(o)!
cryptpad_default = o2.name
mut context := base.context()!
mut r := context.redis()!
encoded := json.encode(o2)
r.hset('context:cryptpad', o2.name, encoded)!
}
// does the config exists?
pub fn exists(args ArgsGet) !bool {
mut context := base.context()!
mut r := context.redis()!
return r.hexists('context:cryptpad', args.name)!
}
pub fn delete(args ArgsGet) ! {
mut context := base.context()!
mut r := context.redis()!
r.hdel('context:cryptpad', args.name)!
}
@[params]
pub struct ArgsList {
pub mut:
fromdb bool // will load from filesystem
}
// if fromdb set: load from filesystem, and not from mem, will also reset what is in mem
pub fn list(args ArgsList) ![]&CryptpadServer {
mut res := []&CryptpadServer{}
mut context := base.context()!
if args.fromdb {
// reset what is in mem
cryptpad_global = map[string]&CryptpadServer{}
cryptpad_default = ''
}
if args.fromdb {
mut r := context.redis()!
mut l := r.hkeys('context:cryptpad')!
for name in l {
res << get(name: name, fromdb: true)!
}
return res
} else {
// load from memory
for _, client in cryptpad_global {
res << client
}
}
return res
}
// only sets in mem, does not set as config
fn set_in_mem(o CryptpadServer) !CryptpadServer {
mut o2 := obj_init(o)!
cryptpad_global[o2.name] = &o2
cryptpad_default = o2.name
return o2
}
pub fn play(mut plbook PlayBook) ! {
if !plbook.exists(filter: 'cryptpad.') {
return
}
mut install_actions := plbook.find(filter: 'cryptpad.configure')!
if install_actions.len > 0 {
for mut install_action in install_actions {
heroscript := install_action.heroscript()
mut obj2 := heroscript_loads(heroscript)!
set(obj2)!
install_action.done = true
}
}
mut other_actions := plbook.find(filter: 'cryptpad.')!
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 cryptpad.destroy')
destroy()!
}
if other_action.name == 'install' {
console.print_debug('install action cryptpad.install')
install()!
}
}
other_action.done = true
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////# LIVE CYCLE MANAGEMENT FOR INSTALLERS ///////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////
// load from disk and make sure is properly intialized
pub fn (mut self CryptpadServer) reload() ! {
switch(self.name)
self = obj_init(self)!
}
@[params]
pub struct InstallArgs {
pub mut:
reset bool
}
pub fn (mut self CryptpadServer) install(args InstallArgs) ! {
switch(self.name)
if args.reset || (!installed()!) {
install()!
}
}
pub fn (mut self CryptpadServer) destroy() ! {
switch(self.name)
destroy()!
}
// switch instance to be used for cryptpad
pub fn switch(name string) {
cryptpad_default = name
}

View File

@@ -0,0 +1,104 @@
module cryptpad
import incubaid.herolib.ui.console
import incubaid.herolib.data.encoderhero
import incubaid.herolib.virt.kubernetes
import incubaid.herolib.core.pathlib
import strings
pub const version = '0.0.0'
const singleton = false
const default = false
struct ConfigValues {
pub mut:
hostname string // The CryptPad hostname
backends string // The backends for the TFGW
namespace string // The namespace for the CryptPad deployment
}
@[heap]
pub struct CryptpadServer {
pub mut:
name string = 'cryptpad'
hostname string
namespace string
cryptpad_path string = '/tmp/cryptpad.yaml'
tfgw_cryptpad_path string = '/tmp/tfgw-cryptpad.yaml'
kube_client kubernetes.KubeClient @[skip]
}
// your checking & initialization code if needed
fn obj_init(mycfg_ CryptpadServer) !CryptpadServer {
mut mycfg := mycfg_
if mycfg.namespace == '' {
mycfg.namespace = mycfg.name
}
if mycfg.hostname == '' {
mycfg.hostname = mycfg.name
}
mycfg.kube_client = kubernetes.get(create: true)!
mycfg.kube_client.config.namespace = mycfg.namespace
return mycfg
}
// called before start if done
fn configure() ! {
mut installer := get()!
master_ips := get_master_node_ips()!
console.print_info('Master node IPs: ${master_ips}')
mut backends_str_builder := strings.new_builder(100)
for ip in master_ips {
backends_str_builder.writeln(' - "http://[${ip}]:80"')
}
config_values := ConfigValues{
hostname: installer.hostname
backends: backends_str_builder.str()
namespace: installer.namespace
}
console.print_info('Generating YAML files from templates...')
temp := $tmpl('./templates/tfgw-cryptpad.yaml')
mut temp_path := pathlib.get_file(path: installer.tfgw_cryptpad_path, create: true)!
temp_path.write(temp)!
temp2 := $tmpl('./templates/cryptpad.yaml')
mut temp_path2 := pathlib.get_file(path: installer.cryptpad_path, create: true)!
temp_path2.write(temp2)!
console.print_info('YAML files generated successfully.')
}
// Get Kubernetes master node IPs
fn get_master_node_ips() ![]string {
mut master_ips := []string{}
installer := get()!
// Get all nodes using the kubernetes client
mut k8s := installer.kube_client
nodes := k8s.get_nodes()!
// Extract IPv6 internal IPs from all nodes (dual-stack support)
for node in nodes {
// Check all internal IPs (not just the first one) for IPv6 addresses
for ip in node.internal_ips {
if ip.len > 0 && ip.contains(':') {
master_ips << ip
}
}
}
return master_ips
}
/////////////NORMALLY NO NEED TO TOUCH
pub fn heroscript_loads(heroscript string) !CryptpadServer {
mut obj := encoderhero.decode[CryptpadServer](heroscript)!
return obj
}

View File

@@ -1,20 +1,20 @@
apiVersion: v1
kind: Namespace
metadata:
name: collab
name: @{config_values.namespace}
---
apiVersion: v1
kind: ConfigMap
metadata:
name: cryptpad-config
namespace: collab
namespace: @{config_values.namespace}
data:
config.js: |
module.exports = {
httpUnsafeOrigin: 'https://cryptpadp2.gent01.grid.tf',
httpSafeOrigin: 'https://cryptpads2.gent01.grid.tf',
httpUnsafeOrigin: 'https://@{config_values.hostname}.gent01.grid.tf',
httpSafeOrigin: 'https://@{config_values.hostname}sb.gent01.grid.tf',
httpAddress: '0.0.0.0',
httpPort: 3000,
httpPort: 80,
websocketPort: 3003,
websocketPath: '/cryptpad_websocket',
@@ -31,7 +31,7 @@ apiVersion: apps/v1
kind: Deployment
metadata:
name: cryptpad
namespace: collab
namespace: @{config_values.namespace}
spec:
replicas: 1
selector:
@@ -58,15 +58,15 @@ spec:
- name: cryptpad
image: cryptpad/cryptpad:latest
ports:
- { name: http, containerPort: 3000 }
- { name: http, containerPort: 80 }
- { name: ws, containerPort: 3003 }
env:
- { name: CPAD_CONF, value: "/cryptpad/config/config.js" }
- { name: CPAD_MAIN_DOMAIN, value: "https://cryptpadp2.gent01.grid.tf" }
- { name: CPAD_SANDBOX_DOMAIN, value: "https://cryptpads2.gent01.grid.tf" }
- { name: CPAD_MAIN_DOMAIN, value: "https://@{config_values.hostname}.gent01.grid.tf" }
- { name: CPAD_SANDBOX_DOMAIN, value: "https://@{config_values.hostname}sb.gent01.grid.tf" }
- { name: CPAD_INSTALL_ONLYOFFICE, value: "no" }
readinessProbe:
httpGet: { path: /, port: 3000 }
httpGet: { path: /, port: 80 }
initialDelaySeconds: 20
periodSeconds: 10
volumeMounts:
@@ -91,37 +91,37 @@ apiVersion: v1
kind: Service
metadata:
name: cryptpad
namespace: collab
namespace: @{config_values.namespace}
spec:
selector: { app: cryptpad }
ports:
- { name: http, port: 3000, targetPort: 3000 }
- { name: http, port: 80, targetPort: 80 }
- { name: ws, port: 3003, targetPort: 3003 }
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: cryptpad
namespace: collab
namespace: @{config_values.namespace}
annotations:
kubernetes.io/ingress.class: traefik
kubernetes.io.ingress.class: traefik
spec:
rules:
- host: cryptpadp2.gent01.grid.tf
- host: @{config_values.hostname}.gent01.grid.tf
http:
paths:
- path: /
pathType: Prefix
backend: { service: { name: cryptpad, port: { number: 3000 } } }
backend: { service: { name: cryptpad, port: { number: 80 } } }
- path: /cryptpad_websocket
pathType: Prefix
backend: { service: { name: cryptpad, port: { number: 3003 } } }
- host: cryptpads2.gent01.grid.tf
- host: @{config_values.hostname}sb.gent01.grid.tf
http:
paths:
- path: /
pathType: Prefix
backend: { service: { name: cryptpad, port: { number: 3000 } } }
backend: { service: { name: cryptpad, port: { number: 80 } } }
- path: /cryptpad_websocket
pathType: Prefix
backend: { service: { name: cryptpad, port: { number: 3003 } } }
backend: { service: { name: cryptpad, port: { number: 3003 } } }

View File

@@ -0,0 +1,24 @@
apiVersion: v1
kind: Namespace
metadata:
name: @{config_values.namespace}
---
apiVersion: ingress.grid.tf/v1
kind: TFGW
metadata:
name: cryptpad-main
namespace: @{config_values.namespace}
spec:
hostname: "@{config_values.hostname}"
backends:
@{config_values.backends}
---
apiVersion: ingress.grid.tf/v1
kind: TFGW
metadata:
name: cryptpad-sandbox
namespace: @{config_values.namespace}
spec:
hostname: "@{config_values.hostname}sb" # Sandbox domain will be always with prefix hostname+sb
backends:
@{config_values.backends}

View File

@@ -1,12 +1,12 @@
!!hero_code.generate_installer
name:''
classname:'CryptPad'
singleton:0
classname:'KubernetesInstaller'
singleton:1
templates:1
default:1
title:''
supported_platforms:''
startupmanager:1
startupmanager:0
hasconfig:1
build:1
build:0

View File

@@ -0,0 +1,120 @@
module kubernetes_installer
import incubaid.herolib.osal.core as osal
import incubaid.herolib.ui.console
import incubaid.herolib.core.texttools
import incubaid.herolib.core
import incubaid.herolib.installers.ulist
import os
//////////////////// following actions are not specific to instance of the object
// checks if kubectl is installed and meets minimum version requirement
fn installed() !bool {
if !osal.cmd_exists('kubectl') {
return false
}
res := os.execute('${osal.profile_path_source_and()!} kubectl version --client --output=json')
if res.exit_code != 0 {
// Try older kubectl version command format
res2 := os.execute('${osal.profile_path_source_and()!} kubectl version --client --short')
if res2.exit_code != 0 {
return false
}
// Parse version from output like "Client Version: v1.31.0"
lines := res2.output.split_into_lines().filter(it.contains('Client Version'))
if lines.len == 0 {
return false
}
version_str := lines[0].all_after('v').trim_space()
if texttools.version(version) <= texttools.version(version_str) {
return true
}
return false
}
// For newer kubectl versions with JSON output
// Just check if kubectl exists and runs - version checking is optional
return true
}
// get the Upload List of the files
fn ulist_get() !ulist.UList {
return ulist.UList{}
}
// uploads to S3 server if configured
fn upload() ! {
// Not applicable for kubectl
}
fn install() ! {
console.print_header('install kubectl')
mut url := ''
mut dest_path := '/tmp/kubectl'
// Determine download URL based on platform
if core.is_linux_arm()! {
url = 'https://dl.k8s.io/release/v${version}/bin/linux/arm64/kubectl'
} else if core.is_linux_intel()! {
url = 'https://dl.k8s.io/release/v${version}/bin/linux/amd64/kubectl'
} else if core.is_osx_arm()! {
url = 'https://dl.k8s.io/release/v${version}/bin/darwin/arm64/kubectl'
} else if core.is_osx_intel()! {
url = 'https://dl.k8s.io/release/v${version}/bin/darwin/amd64/kubectl'
} else {
return error('unsupported platform for kubectl installation')
}
console.print_header('downloading kubectl from ${url}')
// Download kubectl binary
osal.download(
url: url
// minsize_kb: 40000 // kubectl is ~45MB
dest: dest_path
)!
// Make it executable
os.chmod(dest_path, 0o755)!
// Install to system
osal.cmd_add(
cmdname: 'kubectl'
source: dest_path
)!
// Create .kube directory with proper permissions
kube_dir := os.join_path(os.home_dir(), '.kube')
if !os.exists(kube_dir) {
console.print_header('creating ${kube_dir} directory')
os.mkdir_all(kube_dir)!
os.chmod(kube_dir, 0o700)! // read/write/execute for owner only
console.print_header('${kube_dir} directory created with permissions 0700')
} else {
// Ensure correct permissions even if directory exists
os.chmod(kube_dir, 0o700)!
console.print_header('${kube_dir} directory permissions set to 0700')
}
console.print_header('kubectl installed successfully')
}
fn destroy() ! {
console.print_header('destroy kubectl')
if !installed()! {
console.print_header('kubectl is not installed')
return
}
// Remove kubectl command
osal.cmd_delete('kubectl')!
// Clean up any temporary files
osal.rm('/tmp/kubectl')!
console.print_header('kubectl destruction completed')
}

View File

@@ -0,0 +1,179 @@
module kubernetes_installer
import incubaid.herolib.core.base
import incubaid.herolib.core.playbook { PlayBook }
import incubaid.herolib.ui.console
import json
__global (
kubernetes_installer_global map[string]&KubernetesInstaller
kubernetes_installer_default string
)
/////////FACTORY
@[params]
pub struct ArgsGet {
pub mut:
name string = 'default'
fromdb bool // will load from filesystem
create bool // default will not create if not exist
}
pub fn new(args ArgsGet) !&KubernetesInstaller {
mut obj := KubernetesInstaller{
name: args.name
}
set(obj)!
return get(name: args.name)!
}
pub fn get(args ArgsGet) !&KubernetesInstaller {
mut context := base.context()!
kubernetes_installer_default = args.name
if args.fromdb || args.name !in kubernetes_installer_global {
mut r := context.redis()!
if r.hexists('context:kubernetes_installer', args.name)! {
data := r.hget('context:kubernetes_installer', args.name)!
if data.len == 0 {
print_backtrace()
return error('KubernetesInstaller with name: ${args.name} does not exist, prob bug.')
}
mut obj := json.decode(KubernetesInstaller, data)!
set_in_mem(obj)!
} else {
if args.create {
new(args)!
} else {
print_backtrace()
return error("KubernetesInstaller with name '${args.name}' does not exist")
}
}
return get(name: args.name)! // no longer from db nor create
}
return kubernetes_installer_global[args.name] or {
print_backtrace()
return error('could not get config for kubernetes_installer with name:${args.name}')
}
}
// register the config for the future
pub fn set(o KubernetesInstaller) ! {
mut o2 := set_in_mem(o)!
kubernetes_installer_default = o2.name
mut context := base.context()!
mut r := context.redis()!
r.hset('context:kubernetes_installer', o2.name, json.encode(o2))!
}
// does the config exists?
pub fn exists(args ArgsGet) !bool {
mut context := base.context()!
mut r := context.redis()!
return r.hexists('context:kubernetes_installer', args.name)!
}
pub fn delete(args ArgsGet) ! {
mut context := base.context()!
mut r := context.redis()!
r.hdel('context:kubernetes_installer', args.name)!
}
@[params]
pub struct ArgsList {
pub mut:
fromdb bool // will load from filesystem
}
// if fromdb set: load from filesystem, and not from mem, will also reset what is in mem
pub fn list(args ArgsList) ![]&KubernetesInstaller {
mut res := []&KubernetesInstaller{}
mut context := base.context()!
if args.fromdb {
// reset what is in mem
kubernetes_installer_global = map[string]&KubernetesInstaller{}
kubernetes_installer_default = ''
}
if args.fromdb {
mut r := context.redis()!
mut l := r.hkeys('context:kubernetes_installer')!
for name in l {
res << get(name: name, fromdb: true)!
}
return res
} else {
// load from memory
for _, client in kubernetes_installer_global {
res << client
}
}
return res
}
// only sets in mem, does not set as config
fn set_in_mem(o KubernetesInstaller) !KubernetesInstaller {
mut o2 := obj_init(o)!
kubernetes_installer_global[o2.name] = &o2
kubernetes_installer_default = o2.name
return o2
}
pub fn play(mut plbook PlayBook) ! {
if !plbook.exists(filter: 'kubernetes_installer.') {
return
}
mut install_actions := plbook.find(filter: 'kubernetes_installer.configure')!
if install_actions.len > 0 {
return error("can't configure kubernetes_installer, because no configuration allowed for this installer.")
}
mut other_actions := plbook.find(filter: 'kubernetes_installer.')!
for mut other_action in other_actions {
if other_action.name in ['destroy', 'install'] {
mut p := other_action.params
reset := p.get_default_false('reset')
if other_action.name == 'destroy' || reset {
console.print_debug('install action kubernetes_installer.destroy')
destroy()!
}
if other_action.name == 'install' {
console.print_debug('install action kubernetes_installer.install')
install()!
}
}
other_action.done = true
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////# LIVE CYCLE MANAGEMENT FOR INSTALLERS ///////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////
// load from disk and make sure is properly intialized
pub fn (mut self KubernetesInstaller) reload() ! {
switch(self.name)
self = obj_init(self)!
}
@[params]
pub struct InstallArgs {
pub mut:
reset bool
}
pub fn (mut self KubernetesInstaller) install(args InstallArgs) ! {
switch(self.name)
if args.reset || (!installed()!) {
install()!
}
}
pub fn (mut self KubernetesInstaller) destroy() ! {
switch(self.name)
destroy()!
}
// switch instance to be used for kubernetes_installer
pub fn switch(name string) {
kubernetes_installer_default = name
}

View File

@@ -0,0 +1,36 @@
module kubernetes_installer
import incubaid.herolib.data.encoderhero
pub const version = '1.31.0'
const singleton = true
const default = true
// Kubernetes installer - handles kubectl installation
@[heap]
pub struct KubernetesInstaller {
pub mut:
name string = 'default'
}
// your checking & initialization code if needed
fn obj_init(mycfg_ KubernetesInstaller) !KubernetesInstaller {
mut mycfg := mycfg_
return mycfg
}
// called before start if done
fn configure() ! {
// No configuration needed for kubectl
}
/////////////NORMALLY NO NEED TO TOUCH
pub fn heroscript_dumps(obj KubernetesInstaller) !string {
return encoderhero.encode[KubernetesInstaller](obj)!
}
pub fn heroscript_loads(heroscript string) !KubernetesInstaller {
mut obj := encoderhero.decode[KubernetesInstaller](heroscript)!
return obj
}

View File

@@ -0,0 +1,44 @@
# kubernetes_installer
To get started
```v
import incubaid.herolib.installers.something.kubernetes_installer as kubernetes_installer_installer
heroscript:="
!!kubernetes_installer.configure name:'test'
password: '1234'
port: 7701
!!kubernetes_installer.start name:'test' reset:1
"
kubernetes_installer_installer.play(heroscript=heroscript)!
//or we can call the default and do a start with reset
//mut installer:= kubernetes_installer_installer.get()!
//installer.start(reset:true)!
```
## example heroscript
```hero
!!kubernetes_installer.configure
homedir: '/home/user/kubernetes_installer'
username: 'admin'
password: 'secretpassword'
title: 'Some Title'
host: 'localhost'
port: 8888
```

View File

@@ -1,5 +1,5 @@
!!hero_code.generate_installer
!!hero_code.generate_client
name:''
classname:'KubeClient'
singleton:0

View File

@@ -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')
// }

View File

@@ -1,19 +1,25 @@
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
}
// Args for describing a resource
@[params]
pub struct DescribeResourceArgs {
pub mut:
resource string // Resource type: pod, node, service, deployment, tfgw, etc.
resource_name string // Name of the specific resource instance
namespace string // Namespace (empty string for cluster-scoped resources)
}
pub struct KubectlResult {
@@ -29,37 +35,28 @@ 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
console.print_debug('executing: ${cmd}')
job := osal.exec(
cmd: cmd
timeout: args.timeout
retry: args.retry
raise_error: false
)!
cmd += ' ${args.command}'
result := os.execute(cmd)
return KubectlResult{
exit_code: job.exit_code
stdout: job.output
stderr: job.error
success: job.exit_code == 0
exit_code: result.exit_code
stdout: result.output
stderr: result.output // os.execute combines stdout and stderr
success: result.exit_code == 0
}
}
// Test connection to cluster
pub fn (mut k KubeClient) test_connection() !bool {
result := k.kubectl_exec(command: 'cluster-info')!
@@ -78,90 +75,269 @@ 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
}
// Get nodes from cluster
pub fn (mut k KubeClient) get_nodes() ![]Node {
result := k.kubectl_exec(command: 'get nodes -o json')!
if !result.success {
return error('Failed to get nodes: ${result.stderr}')
}
// Parse JSON response using struct-based decoding
node_list := json.decode(KubectlNodeListResponse, result.stdout) or {
// Log error details for debugging
console.print_stderr('Failed to parse nodes JSON response')
console.print_stderr('Error: ${err}')
console.print_stderr('Response length: ${result.stdout.len} bytes')
if result.stdout.len > 0 {
console.print_stderr('First 200 chars: ${result.stdout[..if result.stdout.len < 200 {
result.stdout.len
} else {
200
}]}')
}
return error('Failed to parse nodes JSON: ${err}')
}
mut nodes := []Node{}
for item in node_list.items {
// Extract IP addresses (handle dual-stack: multiple IPs of same type)
mut internal_ips := []string{}
mut external_ips := []string{}
mut hostname := ''
for addr in item.status.addresses {
match addr.address_type {
'InternalIP' {
internal_ips << addr.address
}
'ExternalIP' {
external_ips << addr.address
}
'Hostname' {
hostname = addr.address
}
else {}
}
}
// For backward compatibility, use first internal/external IP
internal_ip := if internal_ips.len > 0 { internal_ips[0] } else { '' }
external_ip := if external_ips.len > 0 { external_ips[0] } else { '' }
// Determine node status from conditions
mut node_status := 'Unknown'
for condition in item.status.conditions {
if condition.condition_type == 'Ready' {
node_status = if condition.status == 'True' { 'Ready' } else { 'NotReady' }
break
}
}
// Extract roles from labels
mut roles := []string{}
for label_key, _ in item.metadata.labels {
if label_key.starts_with('node-role.kubernetes.io/') {
role := label_key.all_after('node-role.kubernetes.io/')
if role.len > 0 {
roles << role
}
}
}
// Create Node struct from kubectl response
node := Node{
name: item.metadata.name
internal_ip: internal_ip
external_ip: external_ip
internal_ips: internal_ips
external_ips: external_ips
hostname: hostname
status: node_status
roles: roles
kubelet_version: item.status.node_info.kubelet_version
os_image: item.status.node_info.os_image
kernel_version: item.status.node_info.kernel_version
container_runtime: item.status.node_info.container_runtime_version
labels: item.metadata.labels
created_at: item.metadata.creation_timestamp
}
nodes << node
}
return nodes
}
// Apply YAML file
@@ -172,7 +348,10 @@ pub fn (mut k KubeClient) apply_yaml(yaml_path string) !KubectlResult {
return error('YAML validation failed: ${validation.errors.join(', ')}')
}
console.print_debug('Applying YAML file: ${yaml_path}')
result := k.kubectl_exec(command: 'apply -f ${yaml_path}')!
console.print_debug('Apply completed with exit code: ${result.exit_code}')
if result.success {
console.print_green('Applied: ${validation.kind}/${validation.metadata.name}')
}
@@ -181,17 +360,24 @@ pub fn (mut k KubeClient) apply_yaml(yaml_path string) !KubectlResult {
// Delete resource
pub fn (mut k KubeClient) delete_resource(kind string, name string, namespace string) !KubectlResult {
result := k.kubectl_exec(command: 'delete ${kind} ${name} -n ${namespace}')!
mut cmd := 'delete ${kind} ${name}'
result := k.kubectl_exec(command: cmd)!
return result
}
// Describe resource
pub fn (mut k KubeClient) describe_resource(kind string, name string, namespace string) !string {
result := k.kubectl_exec(command: 'describe ${kind} ${name} -n ${namespace}')!
if !result.success {
return error('Failed to describe resource: ${result.stderr}')
// Describe resource - provides detailed information about a specific resource
pub fn (mut k KubeClient) describe_resource(args DescribeResourceArgs) !KubectlResult {
// Build the describe command
mut cmd := 'describe ${args.resource} ${args.resource_name}'
// Only add namespace flag if namespace is not empty (for namespaced resources)
if args.namespace.len > 0 {
cmd += ' -n ${args.namespace}'
}
return result.stdout
// Execute the command
result := k.kubectl_exec(command: cmd)!
return result
}
// Port forward

View File

@@ -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

View File

@@ -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

View File

@@ -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,254 @@ 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
}
// Node list response from 'kubectl get nodes -o json'
struct KubectlNodeListResponse {
items []KubectlNodeItem
}
struct KubectlNodeItem {
metadata KubectlNodeMetadata
spec KubectlNodeSpec
status KubectlNodeStatus
}
struct KubectlNodeMetadata {
name string
labels map[string]string
creation_timestamp string @[json: creationTimestamp]
}
struct KubectlNodeSpec {
pod_cidr string @[json: podCIDR]
}
struct KubectlNodeStatus {
addresses []KubectlNodeAddress
conditions []KubectlNodeCondition
node_info KubectlNodeSystemInfo @[json: nodeInfo]
}
struct KubectlNodeAddress {
address string @[json: address]
address_type string @[json: type]
}
struct KubectlNodeCondition {
condition_type string @[json: type]
status string
}
struct KubectlNodeSystemInfo {
architecture string
kernel_version string @[json: kernelVersion]
os_image string @[json: osImage]
operating_system string @[json: operatingSystem]
kubelet_version string @[json: kubeletVersion]
container_runtime_version string @[json: containerRuntimeVersion]
}
// ============================================================================
// 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
}
// Node runtime information
pub struct Node {
pub mut:
name string
internal_ip string // Primary internal IP (first in list)
external_ip string // Primary external IP (first in list)
internal_ips []string // All internal IPs (for dual-stack support)
external_ips []string // All external IPs (for dual-stack support)
hostname string
status string // Ready, NotReady, Unknown
roles []string
kubelet_version string
os_image string
kernel_version string
container_runtime 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
}

View File

@@ -1,76 +1,739 @@
module kubernetes
import time
import os
import json
// ============================================================================
// 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'
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'
}
replicas: 3
selector: {
'app': 'test-app'
created_at: '2024-01-15T10:30:00Z'
}
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'
}
// 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'
}
template: PodSpec{
metadata: K8sMetadata{
name: 'test-app-pod'
namespace: 'default'
}
containers: [
ContainerSpec{
name: 'app'
image: 'nginx:latest'
ports: [
ContainerPort{
name: 'http'
container_port: 80
},
]
},
]
created_at: '2024-01-15T09:00:00Z'
}
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')
}
// ============================================================================
// Unit Tests for Node Struct and get_nodes() Method
// ============================================================================
// Test Node struct creation with all fields
fn test_node_struct_creation() ! {
mut node := Node{
name: 'worker-node-1'
internal_ip: '192.168.1.10'
external_ip: '203.0.113.10'
hostname: 'worker-node-1.example.com'
status: 'Ready'
roles: ['worker']
kubelet_version: 'v1.31.0'
os_image: 'Ubuntu 22.04.3 LTS'
kernel_version: '5.15.0-91-generic'
container_runtime: 'containerd://1.7.2'
labels: {
'kubernetes.io/hostname': 'worker-node-1'
'node-role.kubernetes.io/worker': ''
}
created_at: '2024-01-15T08:00:00Z'
}
assert node.name == 'worker-node-1'
assert node.internal_ip == '192.168.1.10'
assert node.external_ip == '203.0.113.10'
assert node.hostname == 'worker-node-1.example.com'
assert node.status == 'Ready'
assert node.roles.len == 1
assert node.roles[0] == 'worker'
assert node.kubelet_version == 'v1.31.0'
assert node.os_image == 'Ubuntu 22.04.3 LTS'
assert node.kernel_version == '5.15.0-91-generic'
assert node.container_runtime == 'containerd://1.7.2'
assert node.labels['kubernetes.io/hostname'] == 'worker-node-1'
assert node.created_at == '2024-01-15T08:00:00Z'
}
// Test Node struct with master role
fn test_node_struct_master_role() ! {
mut node := Node{
name: 'master-node-1'
status: 'Ready'
roles: ['control-plane', 'master']
}
assert node.name == 'master-node-1'
assert node.status == 'Ready'
assert node.roles.len == 2
assert 'control-plane' in node.roles
assert 'master' in node.roles
}
// Test Node struct with NotReady status
fn test_node_struct_not_ready() ! {
mut node := Node{
name: 'worker-node-2'
status: 'NotReady'
}
assert node.name == 'worker-node-2'
assert node.status == 'NotReady'
}
// Test Node struct with IPv6 internal IP
fn test_node_struct_ipv6() ! {
mut node := Node{
name: 'worker-node-3'
internal_ip: '2001:db8::1'
status: 'Ready'
}
assert node.name == 'worker-node-3'
assert node.internal_ip == '2001:db8::1'
assert node.internal_ip.contains(':')
assert node.status == 'Ready'
}
// Test Node struct with default values
fn test_node_default_values() ! {
mut node := Node{}
assert node.name == ''
assert node.internal_ip == ''
assert node.external_ip == ''
assert node.hostname == ''
assert node.status == ''
assert node.roles.len == 0
assert node.kubelet_version == ''
assert node.os_image == ''
assert node.kernel_version == ''
assert node.container_runtime == ''
assert node.labels.len == 0
assert node.created_at == ''
}
// Test Node struct with multiple roles
fn test_node_multiple_roles() ! {
mut node := Node{
name: 'control-plane-1'
roles: ['control-plane', 'master', 'etcd']
}
assert node.roles.len == 3
assert 'control-plane' in node.roles
assert 'master' in node.roles
assert 'etcd' in node.roles
}
// ============================================================================
// Unit Tests for DescribeResourceArgs Struct
// ============================================================================
// Test DescribeResourceArgs struct for namespaced resource
fn test_describe_resource_args_namespaced() ! {
mut args := DescribeResourceArgs{
resource: 'pod'
resource_name: 'nginx-pod'
namespace: 'default'
}
assert args.resource == 'pod'
assert args.resource_name == 'nginx-pod'
assert args.namespace == 'default'
}
// Test DescribeResourceArgs struct for cluster-scoped resource
fn test_describe_resource_args_cluster_scoped() ! {
mut args := DescribeResourceArgs{
resource: 'node'
resource_name: 'worker-node-1'
namespace: ''
}
assert args.resource == 'node'
assert args.resource_name == 'worker-node-1'
assert args.namespace == ''
}
// Test DescribeResourceArgs struct for custom resource (TFGW)
fn test_describe_resource_args_custom_resource() ! {
mut args := DescribeResourceArgs{
resource: 'tfgw'
resource_name: 'cryptpad-main'
namespace: 'cryptpad'
}
assert args.resource == 'tfgw'
assert args.resource_name == 'cryptpad-main'
assert args.namespace == 'cryptpad'
}
// ============================================================================
// JSON Parsing Tests for Kubectl Responses
// ============================================================================
// Test parsing kubectl node list JSON response
fn test_parse_kubectl_node_list_json() ! {
// Sample kubectl get nodes -o json response
json_response := '{
"items": [
{
"metadata": {
"name": "k3s-master",
"labels": {
"kubernetes.io/hostname": "k3s-master",
"node-role.kubernetes.io/control-plane": "",
"node-role.kubernetes.io/master": ""
},
"creationTimestamp": "2024-01-15T08:00:00Z"
},
"spec": {
"podCIDR": "10.42.0.0/24"
},
"status": {
"addresses": [
{
"type": "InternalIP",
"address": "192.168.1.100"
},
{
"type": "Hostname",
"address": "k3s-master"
}
],
"conditions": [
{
"type": "Ready",
"status": "True"
}
],
"nodeInfo": {
"architecture": "arm64",
"kernelVersion": "5.15.0-91-generic",
"osImage": "Ubuntu 22.04.3 LTS",
"operatingSystem": "linux",
"kubeletVersion": "v1.31.0+k3s1",
"containerRuntimeVersion": "containerd://1.7.11-k3s2"
}
}
}
]
}'
// Parse the JSON
node_list := json.decode(KubectlNodeListResponse, json_response)!
// Verify parsing
assert node_list.items.len == 1
assert node_list.items[0].metadata.name == 'k3s-master'
assert node_list.items[0].metadata.labels['kubernetes.io/hostname'] == 'k3s-master'
assert node_list.items[0].metadata.labels['node-role.kubernetes.io/control-plane'] == ''
assert node_list.items[0].spec.pod_cidr == '10.42.0.0/24'
assert node_list.items[0].status.addresses.len == 2
assert node_list.items[0].status.addresses[0].address_type == 'InternalIP'
assert node_list.items[0].status.addresses[0].address == '192.168.1.100'
assert node_list.items[0].status.addresses[1].address_type == 'Hostname'
assert node_list.items[0].status.addresses[1].address == 'k3s-master'
assert node_list.items[0].status.conditions.len == 1
assert node_list.items[0].status.conditions[0].condition_type == 'Ready'
assert node_list.items[0].status.conditions[0].status == 'True'
assert node_list.items[0].status.node_info.kubelet_version == 'v1.31.0+k3s1'
assert node_list.items[0].status.node_info.os_image == 'Ubuntu 22.04.3 LTS'
}
// Test parsing kubectl node list with IPv6 addresses
fn test_parse_kubectl_node_list_ipv6() ! {
json_response := '{
"items": [
{
"metadata": {
"name": "worker-node-1",
"labels": {
"node-role.kubernetes.io/worker": ""
},
"creationTimestamp": "2024-01-15T09:00:00Z"
},
"spec": {
"podCIDR": "10.42.1.0/24"
},
"status": {
"addresses": [
{
"type": "InternalIP",
"address": "2001:db8::1"
},
{
"type": "ExternalIP",
"address": "2001:db8:1::1"
},
{
"type": "Hostname",
"address": "worker-node-1"
}
],
"conditions": [
{
"type": "Ready",
"status": "True"
}
],
"nodeInfo": {
"architecture": "amd64",
"kernelVersion": "6.5.0-14-generic",
"osImage": "Ubuntu 23.10",
"operatingSystem": "linux",
"kubeletVersion": "v1.31.0",
"containerRuntimeVersion": "containerd://1.7.2"
}
}
}
]
}'
node_list := json.decode(KubectlNodeListResponse, json_response)!
assert node_list.items.len == 1
assert node_list.items[0].metadata.name == 'worker-node-1'
assert node_list.items[0].status.addresses.len == 3
// Verify IPv6 addresses
mut internal_ip := ''
mut external_ip := ''
for addr in node_list.items[0].status.addresses {
if addr.address_type == 'InternalIP' {
internal_ip = addr.address
}
if addr.address_type == 'ExternalIP' {
external_ip = addr.address
}
}
yaml := yaml_from_deployment(deployment)!
assert yaml.contains('apiVersion: apps/v1')
assert yaml.contains('kind: Deployment')
assert yaml.contains('test-app')
assert internal_ip == '2001:db8::1'
assert internal_ip.contains(':')
assert external_ip == '2001:db8:1::1'
assert external_ip.contains(':')
}
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 parsing node with NotReady status
fn test_parse_kubectl_node_not_ready() ! {
json_response := '{
"items": [
{
"metadata": {
"name": "worker-node-2",
"labels": {},
"creationTimestamp": "2024-01-15T10:00:00Z"
},
"spec": {
"podCIDR": ""
},
"status": {
"addresses": [
{
"type": "InternalIP",
"address": "192.168.1.102"
}
],
"conditions": [
{
"type": "Ready",
"status": "False"
}
],
"nodeInfo": {
"architecture": "amd64",
"kernelVersion": "5.15.0-91-generic",
"osImage": "Ubuntu 22.04.3 LTS",
"operatingSystem": "linux",
"kubeletVersion": "v1.31.0",
"containerRuntimeVersion": "containerd://1.7.2"
}
}
}
]
}'
test_file := '/tmp/test-deployment.yaml'
os.write_file(test_file, test_yaml)!
node_list := json.decode(KubectlNodeListResponse, json_response)!
result := yaml_validate(test_file)!
assert result.valid
assert result.kind == 'Deployment'
assert result.metadata.name == 'test-deployment'
os.rm(test_file)!
assert node_list.items.len == 1
assert node_list.items[0].metadata.name == 'worker-node-2'
assert node_list.items[0].status.conditions[0].condition_type == 'Ready'
assert node_list.items[0].status.conditions[0].status == 'False'
}
// Test role extraction from node labels
fn test_node_role_extraction() ! {
json_response := '{
"items": [
{
"metadata": {
"name": "control-plane-1",
"labels": {
"node-role.kubernetes.io/control-plane": "",
"node-role.kubernetes.io/master": "",
"node-role.kubernetes.io/etcd": "",
"kubernetes.io/hostname": "control-plane-1"
},
"creationTimestamp": "2024-01-15T08:00:00Z"
},
"spec": {
"podCIDR": "10.42.0.0/24"
},
"status": {
"addresses": [
{
"type": "InternalIP",
"address": "192.168.1.100"
}
],
"conditions": [
{
"type": "Ready",
"status": "True"
}
],
"nodeInfo": {
"architecture": "arm64",
"kernelVersion": "5.15.0-91-generic",
"osImage": "Ubuntu 22.04.3 LTS",
"operatingSystem": "linux",
"kubeletVersion": "v1.31.0",
"containerRuntimeVersion": "containerd://1.7.2"
}
}
}
]
}'
node_list := json.decode(KubectlNodeListResponse, json_response)!
assert node_list.items.len == 1
// Extract roles from labels
mut roles := []string{}
for label_key, _ in node_list.items[0].metadata.labels {
if label_key.starts_with('node-role.kubernetes.io/') {
role := label_key.all_after('node-role.kubernetes.io/')
if role.len > 0 {
roles << role
}
}
}
// Verify roles were extracted
assert roles.len == 3
assert 'control-plane' in roles
assert 'master' in roles
assert 'etcd' in roles
}
// Test empty node list
fn test_parse_empty_node_list() ! {
json_response := '{
"items": []
}'
node_list := json.decode(KubectlNodeListResponse, json_response)!
assert node_list.items.len == 0
}
// Test dual-stack node (multiple InternalIP addresses)
fn test_parse_dual_stack_node() ! {
json_response := '{
"items": [
{
"metadata": {
"name": "dual-stack-node",
"labels": {
"node-role.kubernetes.io/control-plane": "true"
},
"creationTimestamp": "2025-10-29T12:40:47Z"
},
"spec": {
"podCIDR": "10.42.0.0/24"
},
"status": {
"addresses": [
{
"type": "InternalIP",
"address": "10.20.3.2"
},
{
"type": "InternalIP",
"address": "477:a3a5:7595:d3da:ff0f:ece1:204e:6691"
},
{
"type": "Hostname",
"address": "dual-stack-node"
}
],
"conditions": [
{
"type": "Ready",
"status": "True"
}
],
"nodeInfo": {
"architecture": "amd64",
"kernelVersion": "5.15.0-91-generic",
"osImage": "Ubuntu 22.04.3 LTS",
"operatingSystem": "linux",
"kubeletVersion": "v1.31.0+k3s1",
"containerRuntimeVersion": "containerd://1.7.11-k3s2"
}
}
}
]
}'
node_list := json.decode(KubectlNodeListResponse, json_response)!
assert node_list.items.len == 1
assert node_list.items[0].metadata.name == 'dual-stack-node'
// Verify we have 2 InternalIP addresses
assert node_list.items[0].status.addresses.len == 3
mut internal_ip_count := 0
for addr in node_list.items[0].status.addresses {
if addr.address_type == 'InternalIP' {
internal_ip_count++
}
}
assert internal_ip_count == 2
}

View File

@@ -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 {
@@ -49,11 +46,20 @@ pub fn yaml_validate(yaml_path string) !K8sValidationResult {
errors << 'Missing metadata.name field'
}
// Validate kind values
valid_kinds := ['Pod', 'Deployment', 'Service', 'ConfigMap', 'Secret', 'StatefulSet', 'DaemonSet',
'Job', 'CronJob', 'Ingress', 'PersistentVolume', 'PersistentVolumeClaim']
if kind !in valid_kinds {
errors << 'Invalid kind: ${kind}. Valid kinds: ${valid_kinds.join(', ')}'
// Validate kind values for standard Kubernetes resources
// Allow custom resources (CRDs) which typically have non-standard apiVersions
standard_kinds := ['Pod', 'Deployment', 'Service', 'ConfigMap', 'Secret', 'StatefulSet',
'DaemonSet', 'Job', 'CronJob', 'Ingress', 'PersistentVolume', 'PersistentVolumeClaim',
'Namespace', 'ServiceAccount', 'Role', 'RoleBinding', 'ClusterRole', 'ClusterRoleBinding']
// Check if it's a standard Kubernetes resource or a custom resource
is_standard_api := api_version.starts_with('v1') || api_version.starts_with('apps/')
|| api_version.starts_with('batch/') || api_version.starts_with('networking.k8s.io/')
|| api_version.starts_with('rbac.authorization.k8s.io/')
// Only validate kind for standard Kubernetes resources
if is_standard_api && kind !in standard_kinds {
errors << 'Invalid kind: ${kind}. Valid kinds for standard resources: ${standard_kinds.join(', ')}'
}
return K8sValidationResult{