Merge pull request #192 from Incubaid/development_openrouter
Adding Cryptpad installer and finalizing the Kubernetes client
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -57,4 +57,5 @@ MCP_HTTP_REST_IMPLEMENTATION_PLAN.md
|
||||
tmux_logger
|
||||
release
|
||||
install_herolib
|
||||
doc
|
||||
doc
|
||||
priv_key.bin
|
||||
120
examples/ai/openai/README.md
Normal file
120
examples/ai/openai/README.md
Normal 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.
|
||||
@@ -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?'
|
||||
},
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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}')
|
||||
@@ -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
1
examples/installers/k8s/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
cryptpad
|
||||
23
examples/installers/k8s/cryptpad.vsh
Executable file
23
examples/installers/k8s/cryptpad.vsh
Executable 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()!
|
||||
11
examples/installers/virt/kubernetes.vsh
Executable file
11
examples/installers/virt/kubernetes.vsh
Executable 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
1
examples/virt/kubernetes/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
kubernetes_example
|
||||
177
examples/virt/kubernetes/README.md
Normal file
177
examples/virt/kubernetes/README.md
Normal file
@@ -0,0 +1,177 @@
|
||||
# Kubernetes Client Example
|
||||
|
||||
This example demonstrates the Kubernetes client functionality in HeroLib, including JSON parsing and cluster interaction.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. **kubectl installed**: The Kubernetes command-line tool must be installed on your system.
|
||||
- macOS: `brew install kubectl`
|
||||
- Linux: See [official installation guide](https://kubernetes.io/docs/tasks/tools/install-kubectl-linux/)
|
||||
- Windows: See [official installation guide](https://kubernetes.io/docs/tasks/tools/install-kubectl-windows/)
|
||||
|
||||
2. **Kubernetes cluster**: You need access to a Kubernetes cluster. For local development, you can use:
|
||||
- **Minikube**: `brew install minikube && minikube start`
|
||||
- **Kind**: `brew install kind && kind create cluster`
|
||||
- **Docker Desktop**: Enable Kubernetes in Docker Desktop settings
|
||||
- **k3s**: Lightweight Kubernetes distribution
|
||||
|
||||
## Running the Example
|
||||
|
||||
### Method 1: Direct Execution (Recommended)
|
||||
|
||||
```bash
|
||||
# Make the script executable
|
||||
chmod +x examples/virt/kubernetes/kubernetes_example.vsh
|
||||
|
||||
# Run the script
|
||||
./examples/virt/kubernetes/kubernetes_example.vsh
|
||||
```
|
||||
|
||||
### Method 2: Using V Command
|
||||
|
||||
```bash
|
||||
v -enable-globals run examples/virt/kubernetes/kubernetes_example.vsh
|
||||
```
|
||||
|
||||
## What the Example Demonstrates
|
||||
|
||||
The example script demonstrates the following functionality:
|
||||
|
||||
### 1. **Cluster Information**
|
||||
|
||||
- Retrieves Kubernetes cluster version
|
||||
- Counts total nodes in the cluster
|
||||
- Counts total namespaces
|
||||
- Counts running pods across all namespaces
|
||||
|
||||
### 2. **Pod Management**
|
||||
|
||||
- Lists all pods in the `default` namespace
|
||||
- Displays pod details:
|
||||
- Name, namespace, status
|
||||
- Node assignment and IP address
|
||||
- Container names
|
||||
- Labels and creation timestamp
|
||||
|
||||
### 3. **Deployment Management**
|
||||
|
||||
- Lists all deployments in the `default` namespace
|
||||
- Shows deployment information:
|
||||
- Name and namespace
|
||||
- Replica counts (desired, ready, available, updated)
|
||||
- Labels and creation timestamp
|
||||
|
||||
### 4. **Service Management**
|
||||
|
||||
- Lists all services in the `default` namespace
|
||||
- Displays service details:
|
||||
- Name, namespace, and type (ClusterIP, NodePort, LoadBalancer)
|
||||
- Cluster IP and external IP (if applicable)
|
||||
- Exposed ports and protocols
|
||||
- Labels and creation timestamp
|
||||
|
||||
## Expected Output
|
||||
|
||||
### With a Running Cluster
|
||||
|
||||
When connected to a Kubernetes cluster with resources, you'll see formatted output like:
|
||||
|
||||
```
|
||||
╔════════════════════════════════════════════════════════════════╗
|
||||
║ Kubernetes Client Example - HeroLib ║
|
||||
║ Demonstrates JSON parsing and cluster interaction ║
|
||||
╚════════════════════════════════════════════════════════════════╝
|
||||
|
||||
[INFO] Creating Kubernetes client instance...
|
||||
[SUCCESS] Kubernetes client created successfully
|
||||
|
||||
- 1. Cluster Information
|
||||
[INFO] Retrieving cluster information...
|
||||
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Cluster Overview │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ API Server: https://127.0.0.1:6443 │
|
||||
│ Version: v1.31.0 │
|
||||
│ Nodes: 3 │
|
||||
│ Namespaces: 5 │
|
||||
│ Running Pods: 12 │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Without a Cluster
|
||||
|
||||
If kubectl is not installed or no cluster is configured, you'll see helpful error messages:
|
||||
|
||||
```
|
||||
Error: Failed to get cluster information
|
||||
...
|
||||
This usually means:
|
||||
- kubectl is not installed
|
||||
- No Kubernetes cluster is configured (check ~/.kube/config)
|
||||
- The cluster is not accessible
|
||||
|
||||
To set up a local cluster, you can use:
|
||||
- Minikube: https://minikube.sigs.k8s.io/docs/start/
|
||||
- Kind: https://kind.sigs.k8s.io/docs/user/quick-start/
|
||||
- Docker Desktop (includes Kubernetes)
|
||||
```
|
||||
|
||||
## Creating Test Resources
|
||||
|
||||
If your cluster is empty, you can create test resources to see the example in action:
|
||||
|
||||
```bash
|
||||
# Create a test pod
|
||||
kubectl run nginx --image=nginx
|
||||
|
||||
# Create a test deployment
|
||||
kubectl create deployment nginx-deployment --image=nginx --replicas=3
|
||||
|
||||
# Expose the deployment as a service
|
||||
kubectl expose deployment nginx-deployment --port=80 --type=ClusterIP
|
||||
```
|
||||
|
||||
## Code Structure
|
||||
|
||||
The example demonstrates proper usage of the HeroLib Kubernetes client:
|
||||
|
||||
1. **Factory Pattern**: Uses `kubernetes.new()` to create a client instance
|
||||
2. **Error Handling**: Proper use of V's `!` error propagation and `or {}` blocks
|
||||
3. **JSON Parsing**: All kubectl JSON output is parsed into structured V types
|
||||
4. **Console Output**: Clear, formatted output using the `console` module
|
||||
|
||||
## Implementation Details
|
||||
|
||||
The Kubernetes client module uses:
|
||||
|
||||
- **Struct-based JSON decoding**: V's `json.decode(Type, data)` for type-safe parsing
|
||||
- **Kubernetes API response structs**: Matching kubectl's JSON output format
|
||||
- **Runtime resource structs**: Clean data structures for application use (`Pod`, `Deployment`, `Service`)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "kubectl: command not found"
|
||||
|
||||
Install kubectl using your package manager (see Prerequisites above).
|
||||
|
||||
### "The connection to the server was refused"
|
||||
|
||||
Start a local Kubernetes cluster:
|
||||
|
||||
```bash
|
||||
minikube start
|
||||
# or
|
||||
kind create cluster
|
||||
```
|
||||
|
||||
### "No resources found in default namespace"
|
||||
|
||||
Create test resources using the commands in the "Creating Test Resources" section above.
|
||||
|
||||
## Related Files
|
||||
|
||||
- **Implementation**: `lib/virt/kubernetes/kubernetes_client.v`
|
||||
- **Data Models**: `lib/virt/kubernetes/kubernetes_resources_model.v`
|
||||
- **Unit Tests**: `lib/virt/kubernetes/kubernetes_test.v`
|
||||
- **Factory**: `lib/virt/kubernetes/kubernetes_factory_.v`
|
||||
231
examples/virt/kubernetes/kubernetes_example.vsh
Executable file
231
examples/virt/kubernetes/kubernetes_example.vsh
Executable file
@@ -0,0 +1,231 @@
|
||||
#!/usr/bin/env -S v -n -w -gc none -cc tcc -d use_openssl -enable-globals run
|
||||
|
||||
import incubaid.herolib.virt.kubernetes
|
||||
import incubaid.herolib.ui.console
|
||||
|
||||
println('╔════════════════════════════════════════════════════════════════╗')
|
||||
println('║ Kubernetes Client Example - HeroLib ║')
|
||||
println('║ Demonstrates JSON parsing and cluster interaction ║')
|
||||
println('╚════════════════════════════════════════════════════════════════╝')
|
||||
println('')
|
||||
|
||||
// Create a Kubernetes client instance using the factory pattern
|
||||
println('[INFO] Creating Kubernetes client instance...')
|
||||
mut client := kubernetes.new() or {
|
||||
console.print_header('Error: Failed to create Kubernetes client')
|
||||
eprintln('${err}')
|
||||
eprintln('')
|
||||
eprintln('Make sure kubectl is installed and configured properly.')
|
||||
eprintln('You can install kubectl ')
|
||||
exit(1)
|
||||
}
|
||||
|
||||
println('[SUCCESS] Kubernetes client created successfully')
|
||||
println('')
|
||||
|
||||
// ============================================================================
|
||||
// 1. Get Cluster Information
|
||||
// ============================================================================
|
||||
console.print_header('1. Cluster Information')
|
||||
println('[INFO] Retrieving cluster information...')
|
||||
println('')
|
||||
|
||||
cluster := client.cluster_info() or {
|
||||
console.print_header('Error: Failed to get cluster information')
|
||||
eprintln('${err}')
|
||||
eprintln('')
|
||||
eprintln('This usually means:')
|
||||
eprintln(' - kubectl is not installed')
|
||||
eprintln(' - No Kubernetes cluster is configured (check ~/.kube/config)')
|
||||
eprintln(' - The cluster is not accessible')
|
||||
eprintln('')
|
||||
eprintln('To set up a local cluster, you can use:')
|
||||
eprintln(' - Minikube: https://minikube.sigs.k8s.io/docs/start/')
|
||||
eprintln(' - Kind: https://kind.sigs.k8s.io/docs/user/quick-start/')
|
||||
eprintln(' - Docker Desktop (includes Kubernetes)')
|
||||
exit(1)
|
||||
}
|
||||
|
||||
println('┌─────────────────────────────────────────────────────────────┐')
|
||||
println('│ Cluster Overview │')
|
||||
println('├─────────────────────────────────────────────────────────────┤')
|
||||
println('│ API Server: ${cluster.api_server:-50}│')
|
||||
println('│ Version: ${cluster.version:-50}│')
|
||||
println('│ Nodes: ${cluster.nodes.str():-50}│')
|
||||
println('│ Namespaces: ${cluster.namespaces.str():-50}│')
|
||||
println('│ Running Pods: ${cluster.running_pods.str():-50}│')
|
||||
println('└─────────────────────────────────────────────────────────────┘')
|
||||
println('')
|
||||
|
||||
// ============================================================================
|
||||
// 2. Get Pods in the 'default' namespace
|
||||
// ============================================================================
|
||||
console.print_header('2. Pods in "default" Namespace')
|
||||
println('[INFO] Retrieving pods from the default namespace...')
|
||||
println('')
|
||||
|
||||
pods := client.get_pods('default') or {
|
||||
console.print_header('Warning: Failed to get pods')
|
||||
eprintln('${err}')
|
||||
eprintln('')
|
||||
[]kubernetes.Pod{}
|
||||
}
|
||||
|
||||
if pods.len == 0 {
|
||||
println('No pods found in the default namespace.')
|
||||
println('')
|
||||
println('To create a test pod, run:')
|
||||
println(' kubectl run nginx --image=nginx')
|
||||
println('')
|
||||
} else {
|
||||
println('Found ${pods.len} pod(s) in the default namespace:')
|
||||
println('')
|
||||
|
||||
for i, pod in pods {
|
||||
println('┌─────────────────────────────────────────────────────────────┐')
|
||||
println('│ Pod #${i + 1:-56}│')
|
||||
println('├─────────────────────────────────────────────────────────────┤')
|
||||
println('│ Name: ${pod.name:-50}│')
|
||||
println('│ Namespace: ${pod.namespace:-50}│')
|
||||
println('│ Status: ${pod.status:-50}│')
|
||||
println('│ Node: ${pod.node:-50}│')
|
||||
println('│ IP: ${pod.ip:-50}│')
|
||||
println('│ Containers: ${pod.containers.join(', '):-50}│')
|
||||
println('│ Created: ${pod.created_at:-50}│')
|
||||
|
||||
if pod.labels.len > 0 {
|
||||
println('│ Labels: │')
|
||||
for key, value in pod.labels {
|
||||
label_str := ' ${key}=${value}'
|
||||
println('│ ${label_str:-58}│')
|
||||
}
|
||||
}
|
||||
|
||||
println('└─────────────────────────────────────────────────────────────┘')
|
||||
println('')
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 3. Get Deployments in the 'default' namespace
|
||||
// ============================================================================
|
||||
console.print_header('3. Deployments in "default" Namespace')
|
||||
println('[INFO] Retrieving deployments from the default namespace...')
|
||||
println('')
|
||||
|
||||
deployments := client.get_deployments('default') or {
|
||||
console.print_header('Warning: Failed to get deployments')
|
||||
eprintln('${err}')
|
||||
eprintln('')
|
||||
[]kubernetes.Deployment{}
|
||||
}
|
||||
|
||||
if deployments.len == 0 {
|
||||
println('No deployments found in the default namespace.')
|
||||
println('')
|
||||
println('To create a test deployment, run:')
|
||||
println(' kubectl create deployment nginx --image=nginx --replicas=3')
|
||||
println('')
|
||||
} else {
|
||||
println('Found ${deployments.len} deployment(s) in the default namespace:')
|
||||
println('')
|
||||
|
||||
for i, deploy in deployments {
|
||||
ready_status := if deploy.ready_replicas == deploy.replicas { '✓' } else { '⚠' }
|
||||
|
||||
println('┌─────────────────────────────────────────────────────────────┐')
|
||||
println('│ Deployment #${i + 1:-53}│')
|
||||
println('├─────────────────────────────────────────────────────────────┤')
|
||||
println('│ Name: ${deploy.name:-44}│')
|
||||
println('│ Namespace: ${deploy.namespace:-44}│')
|
||||
println('│ Replicas: ${deploy.replicas.str():-44}│')
|
||||
println('│ Ready Replicas: ${deploy.ready_replicas.str():-44}│')
|
||||
println('│ Available: ${deploy.available_replicas.str():-44}│')
|
||||
println('│ Updated: ${deploy.updated_replicas.str():-44}│')
|
||||
println('│ Status: ${ready_status:-44}│')
|
||||
println('│ Created: ${deploy.created_at:-44}│')
|
||||
|
||||
if deploy.labels.len > 0 {
|
||||
println('│ Labels: │')
|
||||
for key, value in deploy.labels {
|
||||
label_str := ' ${key}=${value}'
|
||||
println('│ ${label_str:-58}│')
|
||||
}
|
||||
}
|
||||
|
||||
println('└─────────────────────────────────────────────────────────────┘')
|
||||
println('')
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 4. Get Services in the 'default' namespace
|
||||
// ============================================================================
|
||||
console.print_header('4. Services in "default" Namespace')
|
||||
println('[INFO] Retrieving services from the default namespace...')
|
||||
println('')
|
||||
|
||||
services := client.get_services('default') or {
|
||||
console.print_header('Warning: Failed to get services')
|
||||
eprintln('${err}')
|
||||
eprintln('')
|
||||
[]kubernetes.Service{}
|
||||
}
|
||||
|
||||
if services.len == 0 {
|
||||
println('No services found in the default namespace.')
|
||||
println('')
|
||||
println('To create a test service, run:')
|
||||
println(' kubectl expose deployment nginx --port=80 --type=ClusterIP')
|
||||
println('')
|
||||
} else {
|
||||
println('Found ${services.len} service(s) in the default namespace:')
|
||||
println('')
|
||||
|
||||
for i, svc in services {
|
||||
println('┌─────────────────────────────────────────────────────────────┐')
|
||||
println('│ Service #${i + 1:-54}│')
|
||||
println('├─────────────────────────────────────────────────────────────┤')
|
||||
println('│ Name: ${svc.name:-48}│')
|
||||
println('│ Namespace: ${svc.namespace:-48}│')
|
||||
println('│ Type: ${svc.service_type:-48}│')
|
||||
println('│ Cluster IP: ${svc.cluster_ip:-48}│')
|
||||
|
||||
if svc.external_ip.len > 0 {
|
||||
println('│ External IP: ${svc.external_ip:-48}│')
|
||||
}
|
||||
|
||||
if svc.ports.len > 0 {
|
||||
println('│ Ports: ${svc.ports.join(', '):-48}│')
|
||||
}
|
||||
|
||||
println('│ Created: ${svc.created_at:-48}│')
|
||||
|
||||
if svc.labels.len > 0 {
|
||||
println('│ Labels: │')
|
||||
for key, value in svc.labels {
|
||||
label_str := ' ${key}=${value}'
|
||||
println('│ ${label_str:-58}│')
|
||||
}
|
||||
}
|
||||
|
||||
println('└─────────────────────────────────────────────────────────────┘')
|
||||
println('')
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Summary
|
||||
// ============================================================================
|
||||
console.print_header('Summary')
|
||||
println('✓ Successfully demonstrated Kubernetes client functionality')
|
||||
println('✓ Cluster information retrieved and parsed')
|
||||
println('✓ Pods: ${pods.len} found')
|
||||
println('✓ Deployments: ${deployments.len} found')
|
||||
println('✓ Services: ${services.len} found')
|
||||
println('')
|
||||
println('All JSON parsing operations completed successfully!')
|
||||
println('')
|
||||
println('╔════════════════════════════════════════════════════════════════╗')
|
||||
println('║ Example Complete ║')
|
||||
println('╚════════════════════════════════════════════════════════════════╝')
|
||||
@@ -1,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
|
||||
// ")!
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
```
|
||||
|
||||
@@ -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"
|
||||
@@ -1,7 +0,0 @@
|
||||
!!hero_code.generate_client
|
||||
name:'openrouter'
|
||||
classname:'OpenRouter'
|
||||
singleton:0
|
||||
default:1
|
||||
hasconfig:1
|
||||
reset:0
|
||||
@@ -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'
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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 (
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)!
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
module elements
|
||||
|
||||
import toml
|
||||
|
||||
// Frontmatter2 struct
|
||||
@[heap]
|
||||
pub struct Frontmatter2 {
|
||||
|
||||
10
lib/installers/k8s/cryptpad/.heroscript
Normal file
10
lib/installers/k8s/cryptpad/.heroscript
Normal 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
|
||||
72
lib/installers/k8s/cryptpad/README.md
Normal file
72
lib/installers/k8s/cryptpad/README.md
Normal 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)
|
||||
189
lib/installers/k8s/cryptpad/cryptpad_actions.v
Normal file
189
lib/installers/k8s/cryptpad/cryptpad_actions.v
Normal 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.')
|
||||
}
|
||||
}
|
||||
193
lib/installers/k8s/cryptpad/cryptpad_factory_.v
Normal file
193
lib/installers/k8s/cryptpad/cryptpad_factory_.v
Normal 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
|
||||
}
|
||||
104
lib/installers/k8s/cryptpad/cryptpad_model.v
Normal file
104
lib/installers/k8s/cryptpad/cryptpad_model.v
Normal 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
|
||||
}
|
||||
@@ -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 } } }
|
||||
24
lib/installers/k8s/cryptpad/templates/tfgw-cryptpad.yaml
Normal file
24
lib/installers/k8s/cryptpad/templates/tfgw-cryptpad.yaml
Normal 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}
|
||||
@@ -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
|
||||
@@ -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')
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
44
lib/installers/virt/kubernetes_installer/readme.md
Normal file
44
lib/installers/virt/kubernetes_installer/readme.md
Normal 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
|
||||
|
||||
```
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
|
||||
!!hero_code.generate_installer
|
||||
!!hero_code.generate_client
|
||||
name:''
|
||||
classname:'KubeClient'
|
||||
singleton:0
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
module kubernetes
|
||||
|
||||
import incubaid.herolib.osal.core as osal
|
||||
import incubaid.herolib.ui.console
|
||||
import incubaid.herolib.core.texttools
|
||||
import incubaid.herolib.osal.startupmanager
|
||||
|
||||
fn startupcmd() ![]startupmanager.ZProcessNewArgs {
|
||||
return []startupmanager.ZProcessNewArgs{}
|
||||
}
|
||||
|
||||
fn running() !bool {
|
||||
// Check if kubectl is available and can connect
|
||||
job := osal.exec(cmd: 'kubectl cluster-info', raise_error: false)!
|
||||
return job.exit_code == 0
|
||||
}
|
||||
|
||||
fn start_pre() ! {
|
||||
console.print_header('Pre-start checks')
|
||||
if !osal.cmd_exists('kubectl') {
|
||||
return error('kubectl not found in PATH')
|
||||
}
|
||||
}
|
||||
|
||||
fn start_post() ! {
|
||||
console.print_header('Post-start validation')
|
||||
}
|
||||
|
||||
fn stop_pre() ! {
|
||||
}
|
||||
|
||||
fn stop_post() ! {
|
||||
}
|
||||
|
||||
fn installed() !bool {
|
||||
return osal.cmd_exists('kubectl')
|
||||
}
|
||||
|
||||
fn install() ! {
|
||||
console.print_header('install kubectl')
|
||||
// kubectl is typically installed separately via package manager
|
||||
// This can be enhanced to auto-download if needed
|
||||
if !osal.cmd_exists('kubectl') {
|
||||
return error('Please install kubectl: https://kubernetes.io/docs/tasks/tools/')
|
||||
}
|
||||
}
|
||||
|
||||
fn build() ! {
|
||||
// Not applicable for kubectl wrapper
|
||||
}
|
||||
|
||||
fn destroy() ! {
|
||||
console.print_header('destroy kubernetes client')
|
||||
// No cleanup needed for kubectl wrapper
|
||||
}
|
||||
|
||||
// fn configure() ! {
|
||||
// console.print_debug('Kubernetes client configured')
|
||||
// }
|
||||
@@ -1,19 +1,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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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{
|
||||
|
||||
Reference in New Issue
Block a user