...
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
|
||||
!!hero_code.generate_installer
|
||||
name:''
|
||||
classname:'Kubernetes'
|
||||
classname:'KubeClient'
|
||||
singleton:0
|
||||
templates:1
|
||||
default:1
|
||||
|
||||
@@ -1,265 +1,59 @@
|
||||
module kubernetes
|
||||
|
||||
import incubaid.herolib.osal.core as osal
|
||||
import incubaid.herolib.ui.console
|
||||
import incubaid.herolib.core.texttools
|
||||
import time
|
||||
import incubaid.herolib.osal.startupmanager
|
||||
|
||||
@[params]
|
||||
pub struct DeployArgs {
|
||||
pub mut:
|
||||
deployment Deployment
|
||||
wait bool = true
|
||||
timeout_seconds int = 300
|
||||
poll_interval_ms int = 5000
|
||||
fn startupcmd() ![]startupmanager.ZProcessNewArgs {
|
||||
return []startupmanager.ZProcessNewArgs{}
|
||||
}
|
||||
|
||||
// Deploy a Deployment resource
|
||||
pub fn (mut c KubernetesClient) deploy(args DeployArgs) !Deployment {
|
||||
console.print_header('Deploying ${args.deployment.metadata.name}')
|
||||
|
||||
// Check if deployment already exists
|
||||
existing := c.get[Deployment](
|
||||
resource_type: 'deployments'
|
||||
name: args.deployment.metadata.name
|
||||
namespace: args.deployment.metadata.namespace
|
||||
) or { none }
|
||||
|
||||
mut deployed_deployment := match existing {
|
||||
Deployment { c.update[Deployment](args.deployment)! }
|
||||
else { c.create[Deployment](args.deployment)! }
|
||||
}
|
||||
|
||||
console.print_green('Deployment created/updated: ${args.deployment.metadata.name}')
|
||||
|
||||
if args.wait {
|
||||
c.wait_for_deployment_ready(
|
||||
name: args.deployment.metadata.name
|
||||
namespace: args.deployment.metadata.namespace
|
||||
timeout_seconds: args.timeout_seconds
|
||||
)!
|
||||
}
|
||||
|
||||
return deployed_deployment
|
||||
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
|
||||
}
|
||||
|
||||
@[params]
|
||||
pub struct WaitForDeploymentArgs {
|
||||
pub mut:
|
||||
name string
|
||||
namespace string = 'default'
|
||||
timeout_seconds int = 300
|
||||
poll_interval_ms int = 5000
|
||||
}
|
||||
|
||||
// Wait for deployment to be ready
|
||||
pub fn (mut c KubernetesClient) wait_for_deployment_ready(args WaitForDeploymentArgs) ! {
|
||||
start_time := time.now()
|
||||
|
||||
for {
|
||||
deployment := c.get[Deployment](
|
||||
resource_type: 'deployments'
|
||||
name: args.name
|
||||
namespace: args.namespace
|
||||
)!
|
||||
|
||||
if deployment.status != none {
|
||||
status := deployment.status!
|
||||
if status.observed_generation >= deployment.metadata.resource_version.int() {
|
||||
if status.updated_replicas == deployment.spec.replicas {
|
||||
if status.ready_replicas == deployment.spec.replicas {
|
||||
console.print_green('Deployment ${args.name} is ready')
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
elapsed := time.now().unix_time() - start_time.unix_time()
|
||||
if elapsed > args.timeout_seconds {
|
||||
return error('Timeout waiting for deployment ${args.name} to be ready')
|
||||
}
|
||||
|
||||
console.print_debug('Waiting for deployment ${args.name}...')
|
||||
time.sleep(args.poll_interval_ms * time.millisecond)
|
||||
fn start_pre() ! {
|
||||
console.print_header('Pre-start checks')
|
||||
if !osal.cmd_exists('kubectl') {
|
||||
return error('kubectl not found in PATH')
|
||||
}
|
||||
}
|
||||
|
||||
@[params]
|
||||
pub struct DeleteArgs {
|
||||
pub mut:
|
||||
resource_type string // 'pods', 'deployments', 'services', etc.
|
||||
name string
|
||||
namespace string = 'default'
|
||||
grace_period_seconds int = 30
|
||||
wait bool = true
|
||||
timeout_seconds int = 300
|
||||
fn start_post() ! {
|
||||
console.print_header('Post-start validation')
|
||||
}
|
||||
|
||||
// Delete a resource
|
||||
pub fn (mut c KubernetesClient) delete_resource(args DeleteArgs) ! {
|
||||
console.print_header('Deleting ${args.resource_type}/${args.name}')
|
||||
fn stop_pre() ! {
|
||||
}
|
||||
|
||||
c.delete(
|
||||
resource_type: args.resource_type
|
||||
name: args.name
|
||||
namespace: args.namespace
|
||||
)!
|
||||
fn stop_post() ! {
|
||||
}
|
||||
|
||||
console.print_green('${args.resource_type}/${args.name} deleted')
|
||||
fn installed() !bool {
|
||||
return osal.cmd_exists('kubectl')
|
||||
}
|
||||
|
||||
if args.wait {
|
||||
start_time := time.now()
|
||||
for {
|
||||
c.get[map[string]interface{}](
|
||||
resource_type: args.resource_type
|
||||
name: args.name
|
||||
namespace: args.namespace
|
||||
) or {
|
||||
console.print_green('${args.resource_type}/${args.name} fully removed')
|
||||
return
|
||||
}
|
||||
|
||||
elapsed := time.now().unix_time() - start_time.unix_time()
|
||||
if elapsed > args.timeout_seconds {
|
||||
return error('Timeout waiting for ${args.resource_type}/${args.name} to be deleted')
|
||||
}
|
||||
|
||||
time.sleep(5000 * time.millisecond)
|
||||
}
|
||||
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/')
|
||||
}
|
||||
}
|
||||
|
||||
@[params]
|
||||
pub struct ScaleArgs {
|
||||
pub mut:
|
||||
deployment_name string
|
||||
namespace string = 'default'
|
||||
replicas int
|
||||
wait bool = true
|
||||
timeout_seconds int = 300
|
||||
fn build() ! {
|
||||
// Not applicable for kubectl wrapper
|
||||
}
|
||||
|
||||
// Scale a deployment
|
||||
pub fn (mut c KubernetesClient) scale_deployment(args ScaleArgs) !Deployment {
|
||||
console.print_header('Scaling ${args.deployment_name} to ${args.replicas} replicas')
|
||||
|
||||
// Patch the deployment spec.replicas field
|
||||
patch_data := {
|
||||
'spec': {
|
||||
'replicas': args.replicas
|
||||
}
|
||||
}
|
||||
|
||||
mut deployment := c.patch[Deployment](
|
||||
resource_type: 'deployments'
|
||||
name: args.deployment_name
|
||||
namespace: args.namespace
|
||||
patch_data: patch_data
|
||||
)!
|
||||
|
||||
if args.wait {
|
||||
c.wait_for_deployment_ready(
|
||||
name: args.deployment_name
|
||||
namespace: args.namespace
|
||||
timeout_seconds: args.timeout_seconds
|
||||
)!
|
||||
}
|
||||
|
||||
return deployment
|
||||
fn destroy() ! {
|
||||
console.print_header('destroy kubernetes client')
|
||||
// No cleanup needed for kubectl wrapper
|
||||
}
|
||||
|
||||
@[params]
|
||||
pub struct RollingRestartArgs {
|
||||
pub mut:
|
||||
deployment_name string
|
||||
namespace string = 'default'
|
||||
wait bool = true
|
||||
timeout_seconds int = 600
|
||||
}
|
||||
|
||||
// Perform rolling restart of a deployment
|
||||
pub fn (mut c KubernetesClient) rolling_restart(args RollingRestartArgs) ! {
|
||||
console.print_header('Rolling restart of ${args.deployment_name}')
|
||||
|
||||
now_timestamp := time.now().format_rfc3339()
|
||||
patch_data := {
|
||||
'spec': {
|
||||
'template': {
|
||||
'metadata': {
|
||||
'annotations': {
|
||||
'kubectl.kubernetes.io/restartedAt': now_timestamp
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c.patch[Deployment](
|
||||
resource_type: 'deployments'
|
||||
name: args.deployment_name
|
||||
namespace: args.namespace
|
||||
patch_data: patch_data
|
||||
)!
|
||||
|
||||
if args.wait {
|
||||
c.wait_for_deployment_ready(
|
||||
name: args.deployment_name
|
||||
namespace: args.namespace
|
||||
timeout_seconds: args.timeout_seconds
|
||||
)!
|
||||
}
|
||||
|
||||
console.print_green('${args.deployment_name} rolling restart completed')
|
||||
}
|
||||
|
||||
@[params]
|
||||
pub struct GetLogsArgs {
|
||||
pub mut:
|
||||
pod_name string
|
||||
container_name string
|
||||
namespace string = 'default'
|
||||
tail_lines int = 100
|
||||
follow bool
|
||||
}
|
||||
|
||||
// Get pod logs
|
||||
pub fn (mut c KubernetesClient) get_pod_logs(args GetLogsArgs) !string {
|
||||
mut prefix := '/api/v1/namespaces/${args.namespace}/pods/${args.pod_name}/log'
|
||||
prefix += '?tailLines=${args.tail_lines}'
|
||||
if args.container_name.len > 0 {
|
||||
prefix += '&container=${args.container_name}'
|
||||
}
|
||||
|
||||
conn := c.connection()!
|
||||
return conn.get_text(prefix: prefix)!
|
||||
}
|
||||
|
||||
@[params]
|
||||
pub struct ExecArgs {
|
||||
pub mut:
|
||||
pod_name string
|
||||
container_name string
|
||||
command []string
|
||||
namespace string = 'default'
|
||||
}
|
||||
|
||||
// Execute command in pod (requires WebSocket support - future enhancement)
|
||||
pub fn (mut c KubernetesClient) exec_in_pod(args ExecArgs) ! {
|
||||
// TODO: Implement WebSocket-based exec
|
||||
return error('exec_in_pod requires WebSocket support - not yet implemented')
|
||||
}
|
||||
|
||||
@[params]
|
||||
pub struct PortForwardArgs {
|
||||
pub mut:
|
||||
pod_name string
|
||||
namespace string = 'default'
|
||||
local_port int
|
||||
remote_port int
|
||||
}
|
||||
|
||||
// Port forward to pod (requires WebSocket support - future enhancement)
|
||||
pub fn (mut c KubernetesClient) port_forward(args PortForwardArgs) ! {
|
||||
// TODO: Implement WebSocket-based port forward
|
||||
return error('port_forward requires WebSocket support - not yet implemented')
|
||||
fn configure() ! {
|
||||
console.print_debug('Kubernetes client configured')
|
||||
}
|
||||
|
||||
@@ -1,223 +1,231 @@
|
||||
module kubernetes
|
||||
|
||||
import incubaid.herolib.osal.core as osal
|
||||
import incubaid.herolib.core.httpconnection
|
||||
import net.http
|
||||
import incubaid.herolib.core.pathlib
|
||||
import incubaid.herolib.ui.console
|
||||
import json
|
||||
import encoding.base64
|
||||
import os
|
||||
|
||||
@[heap]
|
||||
pub struct KubernetesClient {
|
||||
// Execute kubectl command with proper error handling
|
||||
pub fn (mut k KubeClient) kubectl_exec(args KubectlExecArgs) !KubectlResult {
|
||||
mut cmd := '${k.kubectl_path} '
|
||||
|
||||
if !k.config.namespace.is_empty() {
|
||||
cmd += '--namespace=${k.config.namespace} '
|
||||
}
|
||||
|
||||
if !k.kubeconfig_path.is_empty() {
|
||||
cmd += '--kubeconfig=${k.kubeconfig_path} '
|
||||
}
|
||||
|
||||
if !k.config.context.is_empty() {
|
||||
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
|
||||
)!
|
||||
|
||||
return KubectlResult{
|
||||
exit_code: job.exit_code
|
||||
stdout: job.output
|
||||
stderr: job.error
|
||||
success: job.exit_code == 0
|
||||
}
|
||||
}
|
||||
|
||||
@[params]
|
||||
pub struct KubectlExecArgs {
|
||||
pub mut:
|
||||
config KubernetesConfig
|
||||
http_conn ?&httpconnection.HTTPConnection
|
||||
api_groups map[string]APIGroupVersion
|
||||
command string
|
||||
timeout int = 30
|
||||
retry int = 0
|
||||
}
|
||||
|
||||
pub struct APIGroupVersion {
|
||||
pub:
|
||||
group_version string
|
||||
resources []APIResource
|
||||
pub struct KubectlResult {
|
||||
pub mut:
|
||||
exit_code int
|
||||
stdout string
|
||||
stderr string
|
||||
success bool
|
||||
}
|
||||
|
||||
pub struct APIResource {
|
||||
pub:
|
||||
name string
|
||||
singularname string
|
||||
namespaced bool
|
||||
kind string
|
||||
verbs []string // get, list, create, update, delete, patch, watch, etc.
|
||||
}
|
||||
|
||||
pub struct APIResponse {
|
||||
pub:
|
||||
kind string
|
||||
api_version string
|
||||
metadata map[string]interface{}
|
||||
items []json.Any
|
||||
}
|
||||
|
||||
// Create new Kubernetes client
|
||||
pub fn new(config KubernetesConfig) !&KubernetesClient {
|
||||
mut cfg := config
|
||||
cfg.validate()!
|
||||
|
||||
mut client := KubernetesClient{
|
||||
config: cfg
|
||||
// Test connection to cluster
|
||||
pub fn (mut k KubeClient) test_connection() !bool {
|
||||
result := k.kubectl_exec(command: 'cluster-info')!
|
||||
if result.success {
|
||||
k.connected = true
|
||||
return true
|
||||
}
|
||||
client.http_conn = http_connection_create(cfg)!
|
||||
|
||||
return &client
|
||||
return false
|
||||
}
|
||||
|
||||
// Create HTTP connection with proper auth
|
||||
fn http_connection_create(cfg KubernetesConfig) !&httpconnection.HTTPConnection {
|
||||
mut conn := httpconnection.new(
|
||||
name: 'kubernetes-${cfg.name}'
|
||||
url: cfg.server
|
||||
retry: 3
|
||||
cache: false
|
||||
)!
|
||||
|
||||
// Setup authentication
|
||||
if cfg.token.len > 0 {
|
||||
mut header := http.new_header()
|
||||
header.add(.authorization, 'Bearer ${cfg.token}')
|
||||
conn.default_header = header
|
||||
} else if cfg.username.len > 0 && cfg.password.len > 0 {
|
||||
conn.basic_auth(cfg.username, cfg.password)
|
||||
// Get cluster info
|
||||
pub fn (mut k KubeClient) cluster_info() !ClusterInfo {
|
||||
// Get API server version
|
||||
result := k.kubectl_exec(command: 'version -o json')!
|
||||
if !result.success {
|
||||
return error('Failed to get cluster version: ${result.stderr}')
|
||||
}
|
||||
|
||||
// TLS configuration
|
||||
if cfg.insecure_skip_verify {
|
||||
// Skip verification (development only)
|
||||
} else if cfg.certificate_authority.len > 0 {
|
||||
// Load CA certificate
|
||||
// TODO: Configure TLS with CA cert
|
||||
}
|
||||
version_data := json.decode(map[string]interface{}, result.stdout)!
|
||||
server_version := version_data['serverVersion'] or { return error('No serverVersion') }
|
||||
|
||||
return conn
|
||||
}
|
||||
|
||||
// Get HTTP connection
|
||||
pub fn (mut c KubernetesClient) connection() !&httpconnection.HTTPConnection {
|
||||
if c.http_conn == none {
|
||||
c.http_conn = http_connection_create(c.config)!
|
||||
}
|
||||
return c.http_conn!
|
||||
}
|
||||
|
||||
// ==================== DISCOVERY API ====================
|
||||
|
||||
// Get available API groups
|
||||
pub fn (mut c KubernetesClient) get_api_groups() ![]APIGroupVersion {
|
||||
conn := c.connection()!
|
||||
response := conn.get_json_generic[map[string]interface{}](
|
||||
prefix: '/apis'
|
||||
)!
|
||||
// Parse and return API groups
|
||||
return []APIGroupVersion{}
|
||||
}
|
||||
|
||||
// ==================== CORE API METHODS ====================
|
||||
|
||||
// Generic GET for any resource
|
||||
pub fn (mut c KubernetesClient) get[T](
|
||||
resource_type string
|
||||
name string
|
||||
namespace string = ''
|
||||
) !T {
|
||||
mut prefix := build_api_path(resource_type, name, namespace)
|
||||
conn := c.connection()!
|
||||
return conn.get_json_generic[T](prefix: prefix)!
|
||||
}
|
||||
|
||||
// Generic LIST for any resource
|
||||
pub fn (mut c KubernetesClient) list[T](
|
||||
resource_type string
|
||||
namespace string = ''
|
||||
label_selector string = ''
|
||||
) ![]T {
|
||||
mut prefix := build_api_list_path(resource_type, namespace)
|
||||
if label_selector.len > 0 {
|
||||
prefix += '?labelSelector=${label_selector}'
|
||||
}
|
||||
conn := c.connection()!
|
||||
response := conn.get_json_generic[map[string]interface{}](prefix: prefix)!
|
||||
// Parse items array
|
||||
return []T{}
|
||||
}
|
||||
|
||||
// Generic CREATE for any resource
|
||||
pub fn (mut c KubernetesClient) create[T](resource T) !T {
|
||||
resource_type := get_resource_type[T]()
|
||||
namespace := get_namespace[T](resource)
|
||||
mut prefix := build_api_path_create(resource_type, namespace)
|
||||
|
||||
conn := c.connection()!
|
||||
return conn.post_json_generic[T](
|
||||
prefix: prefix
|
||||
params: json.decode_object(json.encode(resource))!
|
||||
)!
|
||||
}
|
||||
|
||||
// Generic UPDATE (PUT) for any resource
|
||||
pub fn (mut c KubernetesClient) update[T](resource T) !T {
|
||||
resource_type := get_resource_type[T]()
|
||||
name := get_resource_name[T](resource)
|
||||
namespace := get_namespace[T](resource)
|
||||
mut prefix := build_api_path(resource_type, name, namespace)
|
||||
|
||||
conn := c.connection()!
|
||||
return conn.put_json_generic[T](
|
||||
prefix: prefix
|
||||
params: json.decode_object(json.encode(resource))!
|
||||
)!
|
||||
}
|
||||
|
||||
// Generic PATCH for any resource
|
||||
pub fn (mut c KubernetesClient) patch[T](
|
||||
resource_type string
|
||||
name string
|
||||
namespace string
|
||||
patch_data map[string]interface{}
|
||||
) !T {
|
||||
mut prefix := build_api_path(resource_type, name, namespace)
|
||||
|
||||
conn := c.connection()!
|
||||
mut header := http.new_header()
|
||||
header.add(.content_type, 'application/merge-patch+json')
|
||||
|
||||
return conn.patch_json_generic[T](
|
||||
prefix: prefix
|
||||
params: patch_data
|
||||
header: header
|
||||
)!
|
||||
}
|
||||
|
||||
// Generic DELETE for any resource
|
||||
pub fn (mut c KubernetesClient) delete(
|
||||
resource_type string
|
||||
name string
|
||||
namespace string = ''
|
||||
) ! {
|
||||
mut prefix := build_api_path(resource_type, name, namespace)
|
||||
conn := c.connection()!
|
||||
conn.delete(prefix: prefix)!
|
||||
}
|
||||
|
||||
// Helper functions for building API paths
|
||||
fn build_api_path(resource_type string, name string, namespace string) string {
|
||||
if namespace.len > 0 {
|
||||
return '/api/v1/namespaces/${namespace}/${resource_type}/${name}'
|
||||
// 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 {
|
||||
return '/api/v1/${resource_type}/${name}'
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
fn build_api_list_path(resource_type string, namespace string) string {
|
||||
if namespace.len > 0 {
|
||||
return '/api/v1/namespaces/${namespace}/${resource_type}'
|
||||
// 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 {
|
||||
return '/api/v1/${resource_type}'
|
||||
0
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
fn build_api_path_create(resource_type string, namespace string) string {
|
||||
return build_api_list_path(resource_type, namespace)
|
||||
// Get resources (Pods, Deployments, Services, etc.)
|
||||
pub fn (mut k KubeClient) get_pods(namespace string) ![]map[string]interface{} {
|
||||
result := k.kubectl_exec(command: 'get pods -n ${namespace} -o json')!
|
||||
if !result.success {
|
||||
return error('Failed to get pods: ${result.stderr}')
|
||||
}
|
||||
|
||||
data := json.decode(map[string]interface{}, result.stdout)!
|
||||
items := data['items'] or { []interface{}{} }
|
||||
return items as []map[string]interface{}
|
||||
}
|
||||
|
||||
// Helper functions to extract metadata from generic types
|
||||
fn get_resource_type[T]() string {
|
||||
// TODO: Use compile-time reflection to get Kind from T
|
||||
return 'pods'
|
||||
pub fn (mut k KubeClient) get_deployments(namespace string) ![]map[string]interface{} {
|
||||
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{}
|
||||
}
|
||||
|
||||
fn get_resource_name[T](resource T) string {
|
||||
// TODO: Extract metadata.name from resource
|
||||
return ''
|
||||
pub fn (mut k KubeClient) get_services(namespace string) ![]map[string]interface{} {
|
||||
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{}
|
||||
}
|
||||
|
||||
fn get_namespace[T](resource T) string {
|
||||
// TODO: Extract metadata.namespace from resource
|
||||
return 'default'
|
||||
// Apply YAML file
|
||||
pub fn (mut k KubeClient) apply_yaml(yaml_path string) !KubectlResult {
|
||||
// Validate before applying
|
||||
validation := yaml_validate(yaml_path)!
|
||||
if !validation.valid {
|
||||
return error('YAML validation failed: ${validation.errors.join(", ")}')
|
||||
}
|
||||
|
||||
result := k.kubectl_exec(command: 'apply -f ${yaml_path}')!
|
||||
if result.success {
|
||||
console.print_green('Applied: ${validation.kind}/${validation.metadata.name}')
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// 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}')!
|
||||
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}')
|
||||
}
|
||||
return result.stdout
|
||||
}
|
||||
|
||||
// Port forward
|
||||
pub fn (mut k KubeClient) port_forward(pod_name string, local_port int, remote_port int, namespace string) !string {
|
||||
cmd := 'port-forward ${pod_name} ${local_port}:${remote_port} -n ${namespace}'
|
||||
result := k.kubectl_exec(command: cmd, timeout: 300)!
|
||||
return result.stdout
|
||||
}
|
||||
|
||||
// Get logs
|
||||
pub fn (mut k KubeClient) logs(pod_name string, namespace string, follow bool) !string {
|
||||
mut cmd := 'logs ${pod_name} -n ${namespace}'
|
||||
if follow {
|
||||
cmd += ' -f'
|
||||
}
|
||||
result := k.kubectl_exec(command: cmd, timeout: 300)!
|
||||
if !result.success {
|
||||
return error('Failed to get logs: ${result.stderr}')
|
||||
}
|
||||
return result.stdout
|
||||
}
|
||||
|
||||
// Exec into container
|
||||
pub fn (mut k KubeClient) exec_pod(pod_name string, namespace string, container string, cmd_args []string) !string {
|
||||
cmd := 'exec -it ${pod_name} -n ${namespace}'
|
||||
if !container.is_empty() {
|
||||
cmd += ' -c ${container}'
|
||||
}
|
||||
cmd += ' -- ${cmd_args.join(" ")}'
|
||||
|
||||
result := k.kubectl_exec(command: cmd, timeout: 300)!
|
||||
if !result.success {
|
||||
return error('Exec failed: ${result.stderr}')
|
||||
}
|
||||
return result.stdout
|
||||
}
|
||||
|
||||
// Create namespace
|
||||
pub fn (mut k KubeClient) create_namespace(namespace_name string) !KubectlResult {
|
||||
result := k.kubectl_exec(command: 'create namespace ${namespace_name}')!
|
||||
return result
|
||||
}
|
||||
|
||||
// Watch resources (returns status)
|
||||
pub fn (mut k KubeClient) watch_deployment(name string, namespace string, timeout_seconds int) !bool {
|
||||
cmd := 'rollout status deployment/${name} -n ${namespace} --timeout=${timeout_seconds}s'
|
||||
result := k.kubectl_exec(command: cmd, timeout: timeout_seconds + 10)!
|
||||
return result.success
|
||||
}
|
||||
@@ -2,47 +2,226 @@ module kubernetes
|
||||
|
||||
import incubaid.herolib.data.paramsparser
|
||||
import incubaid.herolib.data.encoderhero
|
||||
import incubaid.herolib.data.ourjson
|
||||
import os
|
||||
|
||||
pub const version = '0.0.0'
|
||||
pub const version = '1.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 Kubernetes {
|
||||
// K8s API Version and Kind tracking
|
||||
@[params]
|
||||
pub struct K8sMetadata {
|
||||
pub mut:
|
||||
name string = 'default'
|
||||
homedir string
|
||||
configpath string
|
||||
username string
|
||||
password string @[secret]
|
||||
title string
|
||||
host string
|
||||
port int
|
||||
name string
|
||||
namespace string = 'default'
|
||||
labels map[string]string
|
||||
annotations map[string]string
|
||||
owner_reference string
|
||||
}
|
||||
|
||||
// your checking & initialization code if needed
|
||||
fn obj_init(mycfg_ Kubernetes) !Kubernetes {
|
||||
mut mycfg := mycfg_
|
||||
// if mycfg.password == '' && mycfg.secret == '' {
|
||||
// return error('password or secret needs to be filled in for ${mycfg.name}')
|
||||
//}
|
||||
return mycfg
|
||||
// Pod Specification
|
||||
@[params]
|
||||
pub struct ContainerSpec {
|
||||
pub mut:
|
||||
name string
|
||||
image string
|
||||
image_pull_policy string = 'IfNotPresent'
|
||||
ports []ContainerPort
|
||||
env []EnvVar
|
||||
resources ResourceRequirements
|
||||
volume_mounts []VolumeMount
|
||||
command []string
|
||||
args []string
|
||||
}
|
||||
|
||||
@[params]
|
||||
pub struct ContainerPort {
|
||||
pub mut:
|
||||
name string
|
||||
container_port int
|
||||
protocol string = 'TCP'
|
||||
host_port int
|
||||
}
|
||||
|
||||
@[params]
|
||||
pub struct EnvVar {
|
||||
pub mut:
|
||||
name string
|
||||
value string
|
||||
}
|
||||
|
||||
@[params]
|
||||
pub struct ResourceRequirements {
|
||||
pub mut:
|
||||
requests map[string]string // cpu, memory
|
||||
limits map[string]string
|
||||
}
|
||||
|
||||
@[params]
|
||||
pub struct VolumeMount {
|
||||
pub mut:
|
||||
name string
|
||||
mount_path string
|
||||
read_only bool
|
||||
}
|
||||
|
||||
@[params]
|
||||
pub struct PodSpec {
|
||||
pub mut:
|
||||
metadata K8sMetadata
|
||||
containers []ContainerSpec
|
||||
restart_policy string = 'Always'
|
||||
service_account string
|
||||
volumes []Volume
|
||||
}
|
||||
|
||||
@[params]
|
||||
pub struct Volume {
|
||||
pub mut:
|
||||
name string
|
||||
config_map string
|
||||
secret string
|
||||
empty_dir bool
|
||||
}
|
||||
|
||||
// Deployment Specification
|
||||
@[params]
|
||||
pub struct DeploymentSpec {
|
||||
pub mut:
|
||||
metadata K8sMetadata
|
||||
replicas int = 1
|
||||
selector map[string]string
|
||||
template PodSpec
|
||||
strategy DeploymentStrategy
|
||||
progress_deadline_seconds int = 600
|
||||
}
|
||||
|
||||
@[params]
|
||||
pub struct DeploymentStrategy {
|
||||
pub mut:
|
||||
strategy_type string = 'RollingUpdate'
|
||||
rolling_update RollingUpdateStrategy
|
||||
}
|
||||
|
||||
@[params]
|
||||
pub struct RollingUpdateStrategy {
|
||||
pub mut:
|
||||
max_surge string = '25%'
|
||||
max_unavailable string = '25%'
|
||||
}
|
||||
|
||||
// Service Specification
|
||||
@[params]
|
||||
pub struct ServiceSpec {
|
||||
pub mut:
|
||||
metadata K8sMetadata
|
||||
service_type string = 'ClusterIP' // ClusterIP, NodePort, LoadBalancer
|
||||
selector map[string]string
|
||||
ports []ServicePort
|
||||
cluster_ip string
|
||||
external_ips []string
|
||||
session_affinity string
|
||||
}
|
||||
|
||||
@[params]
|
||||
pub struct ServicePort {
|
||||
pub mut:
|
||||
name string
|
||||
protocol string = 'TCP'
|
||||
port int
|
||||
target_port int
|
||||
node_port int
|
||||
}
|
||||
|
||||
// ConfigMap
|
||||
@[params]
|
||||
pub struct ConfigMapSpec {
|
||||
pub mut:
|
||||
metadata K8sMetadata
|
||||
data map[string]string
|
||||
}
|
||||
|
||||
// Secret
|
||||
@[params]
|
||||
pub struct SecretSpec {
|
||||
pub mut:
|
||||
metadata K8sMetadata
|
||||
secret_type string = 'Opaque'
|
||||
data map[string]string // base64 encoded
|
||||
}
|
||||
|
||||
// Kube Client Configuration
|
||||
@[params]
|
||||
pub struct KubeConfig {
|
||||
pub mut:
|
||||
kubeconfig_path string
|
||||
context string = ''
|
||||
namespace string = 'default'
|
||||
api_server string
|
||||
ca_cert_path string
|
||||
client_cert_path string
|
||||
client_key_path string
|
||||
token string
|
||||
insecure_skip_tls_verify bool
|
||||
}
|
||||
|
||||
@[heap]
|
||||
pub struct KubeClient {
|
||||
pub mut:
|
||||
name string = 'default'
|
||||
kubeconfig_path string
|
||||
config KubeConfig
|
||||
connected bool
|
||||
api_version string = 'v1'
|
||||
kubectl_path string = 'kubectl'
|
||||
cache_enabled bool = true
|
||||
cache_ttl_seconds int = 300
|
||||
}
|
||||
|
||||
// Validation result for YAML files
|
||||
pub struct K8sValidationResult {
|
||||
pub mut:
|
||||
valid bool
|
||||
kind string
|
||||
api_version string
|
||||
metadata K8sMetadata
|
||||
errors []string
|
||||
}
|
||||
|
||||
// Cluster info
|
||||
pub struct ClusterInfo {
|
||||
pub mut:
|
||||
version string
|
||||
nodes int
|
||||
namespaces int
|
||||
running_pods int
|
||||
api_server string
|
||||
}
|
||||
|
||||
// Initialization
|
||||
fn obj_init(mut cfg KubeClient) !KubeClient {
|
||||
// Resolve kubeconfig path
|
||||
if cfg.kubeconfig_path.is_empty() {
|
||||
home := os.home_dir()
|
||||
cfg.kubeconfig_path = '${home}/.kube/config'
|
||||
}
|
||||
|
||||
// Ensure kubeconfig exists
|
||||
if !os.path_exists(cfg.kubeconfig_path) {
|
||||
return error('kubeconfig not found at ${cfg.kubeconfig_path}')
|
||||
}
|
||||
|
||||
cfg.config.kubeconfig_path = cfg.kubeconfig_path
|
||||
|
||||
return cfg
|
||||
}
|
||||
|
||||
// called before start if done
|
||||
fn configure() ! {
|
||||
// mut installer := get()!
|
||||
// mut mycode := $tmpl('templates/atemplate.yaml')
|
||||
// mut path := pathlib.get_file(path: cfg.configpath, create: true)!
|
||||
// path.write(mycode)!
|
||||
// console.print_debug(mycode)
|
||||
// Configure any defaults or environment-specific settings
|
||||
}
|
||||
|
||||
/////////////NORMALLY NO NEED TO TOUCH
|
||||
|
||||
pub fn heroscript_loads(heroscript string) !Kubernetes {
|
||||
mut obj := encoderhero.decode[Kubernetes](heroscript)!
|
||||
return obj
|
||||
pub fn heroscript_loads(heroscript string) !KubeClient {
|
||||
mut obj := encoderhero.decode[KubeClient](heroscript)!
|
||||
return obj_init(obj)!
|
||||
}
|
||||
|
||||
74
lib/virt/kubernetes/kubernetes_test.v
Normal file
74
lib/virt/kubernetes/kubernetes_test.v
Normal file
@@ -0,0 +1,74 @@
|
||||
module kubernetes
|
||||
|
||||
import time
|
||||
import os
|
||||
|
||||
fn test_model_creation() ! {
|
||||
mut deployment := DeploymentSpec{
|
||||
metadata: K8sMetadata{
|
||||
name: 'test-app'
|
||||
namespace: 'default'
|
||||
}
|
||||
replicas: 3
|
||||
selector: {
|
||||
'app': 'test-app'
|
||||
}
|
||||
template: PodSpec{
|
||||
metadata: K8sMetadata{
|
||||
name: 'test-app-pod'
|
||||
namespace: 'default'
|
||||
}
|
||||
containers: [
|
||||
ContainerSpec{
|
||||
name: 'app'
|
||||
image: 'nginx:latest'
|
||||
ports: [
|
||||
ContainerPort{
|
||||
name: 'http'
|
||||
container_port: 80
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
yaml := yaml_from_deployment(deployment)!
|
||||
assert yaml.contains('apiVersion: apps/v1')
|
||||
assert yaml.contains('kind: Deployment')
|
||||
assert yaml.contains('test-app')
|
||||
}
|
||||
|
||||
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_file := '/tmp/test-deployment.yaml'
|
||||
os.write_file(test_file, test_yaml)!
|
||||
|
||||
result := yaml_validate(test_file)!
|
||||
assert result.valid
|
||||
assert result.kind == 'Deployment'
|
||||
assert result.metadata.name == 'test-deployment'
|
||||
|
||||
os.rm(test_file)!
|
||||
}
|
||||
@@ -1,53 +1,169 @@
|
||||
module kubernetes
|
||||
|
||||
import encoding.yaml
|
||||
import json
|
||||
import incubaid.herolib.data.markdown
|
||||
import incubaid.herolib.core.pathlib
|
||||
import os
|
||||
|
||||
// Serialize Kubernetes resource to YAML
|
||||
pub fn to_yaml[T](resource T) !string {
|
||||
// TODO: Use V's YAML encoding to serialize
|
||||
return encode_to_yaml(resource)!
|
||||
}
|
||||
// Parse YAML file and return validation result
|
||||
pub fn yaml_validate(yaml_path string) !K8sValidationResult {
|
||||
mut file := pathlib.get(yaml_path)
|
||||
|
||||
// Deserialize YAML to Kubernetes resource
|
||||
pub fn from_yaml[T](content string) !T {
|
||||
// TODO: Use V's YAML decoding to parse
|
||||
return decode_from_yaml[T](content)!
|
||||
}
|
||||
|
||||
// Helper to handle YAML document conversion
|
||||
fn encode_to_yaml[T](resource T) !string {
|
||||
return yaml.encode(resource)!
|
||||
}
|
||||
|
||||
fn decode_from_yaml[T](content string) !T {
|
||||
return yaml.decode[T](content)!
|
||||
}
|
||||
|
||||
// Multi-document YAML support
|
||||
pub fn to_yaml_multi[T](resources []T) !string {
|
||||
mut result := ''
|
||||
for i, resource in resources {
|
||||
result += '---\n'
|
||||
result += to_yaml(resource)!
|
||||
if i < resources.len - 1 {
|
||||
result += '\n'
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
pub fn from_yaml_multi[T](content string) ![]T {
|
||||
mut result := []T{}
|
||||
docs := content.split('---')
|
||||
|
||||
for doc in docs {
|
||||
trimmed := doc.trim_space()
|
||||
if trimmed.len == 0 {
|
||||
continue
|
||||
}
|
||||
resource := from_yaml[T](trimmed)!
|
||||
result << resource
|
||||
if !file.exists() {
|
||||
return error('YAML file not found: ${yaml_path}')
|
||||
}
|
||||
|
||||
return result
|
||||
yaml_content := file.read()!
|
||||
|
||||
// Extract kind and apiVersion from YAML
|
||||
lines := yaml_content.split('\n')
|
||||
mut kind := ''
|
||||
mut api_version := ''
|
||||
mut metadata_name := ''
|
||||
mut metadata_namespace := 'default'
|
||||
|
||||
for line in lines {
|
||||
if line.starts_with('kind:') {
|
||||
kind = line.replace('kind:', '').trim_space()
|
||||
}
|
||||
if line.starts_with('apiVersion:') {
|
||||
api_version = line.replace('apiVersion:', '').trim_space()
|
||||
}
|
||||
if line.contains('name:') && line.trim_space().starts_with('name:') {
|
||||
metadata_name = line.replace('name:', '').trim_space()
|
||||
}
|
||||
if line.contains('namespace:') && line.trim_space().starts_with('namespace:') {
|
||||
metadata_namespace = line.replace('namespace:', '').trim_space()
|
||||
}
|
||||
}
|
||||
|
||||
mut errors := []string{}
|
||||
|
||||
if kind.len == 0 {
|
||||
errors << 'Missing "kind" field'
|
||||
}
|
||||
if api_version.len == 0 {
|
||||
errors << 'Missing "apiVersion" field'
|
||||
}
|
||||
if metadata_name.len == 0 {
|
||||
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(", ")}'
|
||||
}
|
||||
|
||||
return K8sValidationResult{
|
||||
valid: errors.len == 0
|
||||
kind: kind
|
||||
api_version: api_version
|
||||
metadata: K8sMetadata{
|
||||
name: metadata_name
|
||||
namespace: metadata_namespace
|
||||
}
|
||||
errors: errors
|
||||
}
|
||||
}
|
||||
|
||||
// Generate YAML from model
|
||||
pub fn yaml_from_deployment(spec DeploymentSpec) !string {
|
||||
mut yaml := 'apiVersion: apps/v1\nkind: Deployment\nmetadata:\n'
|
||||
yaml += ' name: ${spec.metadata.name}\n'
|
||||
yaml += ' namespace: ${spec.metadata.namespace}\n'
|
||||
|
||||
if spec.metadata.labels.len > 0 {
|
||||
yaml += ' labels:\n'
|
||||
for key, value in spec.metadata.labels {
|
||||
yaml += ' ${key}: ${value}\n'
|
||||
}
|
||||
}
|
||||
|
||||
yaml += 'spec:\n'
|
||||
yaml += ' replicas: ${spec.replicas}\n'
|
||||
yaml += ' selector:\n'
|
||||
yaml += ' matchLabels:\n'
|
||||
for key, value in spec.selector {
|
||||
yaml += ' ${key}: ${value}\n'
|
||||
}
|
||||
|
||||
yaml += ' template:\n'
|
||||
yaml += ' metadata:\n'
|
||||
yaml += ' labels:\n'
|
||||
for key, value in spec.selector {
|
||||
yaml += ' ${key}: ${value}\n'
|
||||
}
|
||||
|
||||
yaml += ' spec:\n'
|
||||
yaml += ' containers:\n'
|
||||
for container in spec.template.containers {
|
||||
yaml += ' - name: ${container.name}\n'
|
||||
yaml += ' image: ${container.image}\n'
|
||||
yaml += ' imagePullPolicy: ${container.image_pull_policy}\n'
|
||||
|
||||
if container.ports.len > 0 {
|
||||
yaml += ' ports:\n'
|
||||
for port in container.ports {
|
||||
yaml += ' - name: ${port.name}\n'
|
||||
yaml += ' containerPort: ${port.container_port}\n'
|
||||
yaml += ' protocol: ${port.protocol}\n'
|
||||
}
|
||||
}
|
||||
|
||||
if container.env.len > 0 {
|
||||
yaml += ' env:\n'
|
||||
for env in container.env {
|
||||
yaml += ' - name: ${env.name}\n'
|
||||
yaml += ' value: "${env.value}"\n'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return yaml
|
||||
}
|
||||
|
||||
pub fn yaml_from_service(spec ServiceSpec) !string {
|
||||
mut yaml := 'apiVersion: v1\nkind: Service\nmetadata:\n'
|
||||
yaml += ' name: ${spec.metadata.name}\n'
|
||||
yaml += ' namespace: ${spec.metadata.namespace}\n'
|
||||
|
||||
yaml += 'spec:\n'
|
||||
yaml += ' type: ${spec.service_type}\n'
|
||||
yaml += ' selector:\n'
|
||||
for key, value in spec.selector {
|
||||
yaml += ' ${key}: ${value}\n'
|
||||
}
|
||||
|
||||
yaml += ' ports:\n'
|
||||
for port in spec.ports {
|
||||
yaml += ' - name: ${port.name}\n'
|
||||
yaml += ' protocol: ${port.protocol}\n'
|
||||
yaml += ' port: ${port.port}\n'
|
||||
yaml += ' targetPort: ${port.target_port}\n'
|
||||
if port.node_port > 0 {
|
||||
yaml += ' nodePort: ${port.node_port}\n'
|
||||
}
|
||||
}
|
||||
|
||||
return yaml
|
||||
}
|
||||
|
||||
pub fn yaml_from_configmap(spec ConfigMapSpec) !string {
|
||||
mut yaml := 'apiVersion: v1\nkind: ConfigMap\nmetadata:\n'
|
||||
yaml += ' name: ${spec.metadata.name}\n'
|
||||
yaml += ' namespace: ${spec.metadata.namespace}\n'
|
||||
yaml += 'data:\n'
|
||||
|
||||
for key, value in spec.data {
|
||||
yaml += ' ${key}: |\n'
|
||||
// Indent multiline values
|
||||
lines := value.split('\n')
|
||||
for line in lines {
|
||||
yaml += ' ${line}\n'
|
||||
}
|
||||
}
|
||||
|
||||
return yaml
|
||||
}
|
||||
@@ -1,44 +1,187 @@
|
||||
# kubernetes
|
||||
# Kubernetes Client
|
||||
|
||||
A comprehensive Kubernetes client for HeroLib that wraps `kubectl` with additional safety and validation features.
|
||||
|
||||
## Features
|
||||
|
||||
To get started
|
||||
- ✅ **Connection Testing**: Validate cluster connectivity before operations
|
||||
- ✅ **YAML Validation**: Validate K8s YAML before applying
|
||||
- ✅ **Resource Management**: Create, read, update, delete K8s resources
|
||||
- ✅ **Cluster Info**: Get cluster status and metrics
|
||||
- ✅ **Pod Management**: Logs, exec, port-forwarding
|
||||
- ✅ **Model-Based**: Type-safe resource specifications
|
||||
- ✅ **HeroScript Integration**: Full playbook support
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
# Install kubectl
|
||||
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
|
||||
sudo install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl
|
||||
|
||||
# Setup kubeconfig
|
||||
mkdir -p ~/.kube
|
||||
cp /path/to/kubeconfig ~/.kube/config
|
||||
chmod 600 ~/.kube/config
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Connection
|
||||
|
||||
```v
|
||||
import incubaid.herolib.virt.kubernetes
|
||||
|
||||
|
||||
import incubaid.herolib.installers.something.kubernetes as kubernetes_installer
|
||||
|
||||
heroscript:="
|
||||
!!kubernetes.configure name:'test'
|
||||
password: '1234'
|
||||
port: 7701
|
||||
|
||||
!!kubernetes.start name:'test' reset:1
|
||||
"
|
||||
|
||||
kubernetes_installer.play(heroscript=heroscript)!
|
||||
|
||||
//or we can call the default and do a start with reset
|
||||
//mut installer:= kubernetes_installer.get()!
|
||||
//installer.start(reset:true)!
|
||||
|
||||
|
||||
|
||||
|
||||
mut k8s := kubernetes.new(name: 'production')!
|
||||
k8s.start()!
|
||||
info := k8s.cluster_info()!
|
||||
println(info)
|
||||
```
|
||||
|
||||
## example heroscript
|
||||
### Apply YAML
|
||||
|
||||
```v
|
||||
mut k8s := kubernetes.get(name: 'production')!
|
||||
k8s.apply_yaml('deployment.yaml')!
|
||||
```
|
||||
|
||||
```hero
|
||||
### Validate YAML
|
||||
|
||||
```v
|
||||
result := kubernetes.yaml_validate('my-deployment.yaml')!
|
||||
if result.valid {
|
||||
println('YAML is valid: ${result.kind}/${result.metadata.name}')
|
||||
} else {
|
||||
println('Validation errors: ${result.errors}')
|
||||
}
|
||||
```
|
||||
|
||||
### Get Resources
|
||||
|
||||
```v
|
||||
mut k8s := kubernetes.get(name: 'production')!
|
||||
pods := k8s.get_pods('default')!
|
||||
for pod in pods {
|
||||
println(pod)
|
||||
}
|
||||
```
|
||||
|
||||
### HeroScript
|
||||
|
||||
```heroscript
|
||||
!!kubernetes.configure
|
||||
homedir: '/home/user/kubernetes'
|
||||
username: 'admin'
|
||||
password: 'secretpassword'
|
||||
title: 'Some Title'
|
||||
host: 'localhost'
|
||||
port: 8888
|
||||
name: 'production'
|
||||
kubeconfig_path: '~/.kube/config'
|
||||
|
||||
!!kubernetes.apply
|
||||
name: 'production'
|
||||
yaml_path: '/path/to/deployment.yaml'
|
||||
|
||||
!!kubernetes.info
|
||||
name: 'production'
|
||||
|
||||
!!kubernetes.delete
|
||||
name: 'production'
|
||||
kind: 'deployment'
|
||||
resource_name: 'my-app'
|
||||
namespace: 'default'
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### Configuration
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `name` | string | 'default' | Client instance name |
|
||||
| `kubeconfig_path` | string | ~/.kube/config | Path to kubeconfig |
|
||||
| `context` | string | '' | K8s context to use |
|
||||
| `namespace` | string | 'default' | Default namespace |
|
||||
| `kubectl_path` | string | 'kubectl' | Path to kubectl binary |
|
||||
|
||||
### Main Methods
|
||||
|
||||
- `test_connection() !bool` - Test cluster connectivity
|
||||
- `cluster_info() !ClusterInfo` - Get cluster status
|
||||
- `apply_yaml(path: string) !KubectlResult` - Apply YAML file
|
||||
- `delete_resource(kind, name, namespace) !KubectlResult` - Delete resource
|
||||
- `get_pods(namespace) ![]map` - List pods
|
||||
- `get_deployments(namespace) ![]map` - List deployments
|
||||
- `logs(pod, namespace) !string` - Get pod logs
|
||||
- `exec_pod(pod, namespace, cmd) !string` - Execute in pod
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Always validate YAML** before applying
|
||||
2. **Test connection** before operations
|
||||
3. **Use namespaces** to isolate workloads
|
||||
4. **Check rollout status** for deployments
|
||||
5. **Monitor logs** for troubleshooting
|
||||
|
||||
## Example: Complete Workflow
|
||||
|
||||
```v
|
||||
#!/usr/bin/env -S v -cg -gc none -cc tcc -d use_openssl -enable-globals run
|
||||
|
||||
import incubaid.herolib.virt.kubernetes as k8s_mod
|
||||
import incubaid.herolib.ui.console
|
||||
|
||||
// Create and connect
|
||||
mut k8s := k8s_mod.new(
|
||||
name: 'my-cluster'
|
||||
kubeconfig_path: '~/.kube/config'
|
||||
)!
|
||||
|
||||
k8s.start()!
|
||||
|
||||
// Validate YAML
|
||||
result := k8s_mod.yaml_validate('my-deployment.yaml')!
|
||||
if !result.valid {
|
||||
console.print_stderr('Invalid YAML: ${result.errors.join(", ")}')
|
||||
exit(1)
|
||||
}
|
||||
|
||||
// Apply
|
||||
k8s.apply_yaml('my-deployment.yaml')!
|
||||
|
||||
// Monitor rollout
|
||||
if k8s.watch_deployment('my-app', 'default', 300)! {
|
||||
console.print_green('Deployment successful!')
|
||||
} else {
|
||||
console.print_stderr('Deployment failed!')
|
||||
}
|
||||
|
||||
// Check status
|
||||
info := k8s.cluster_info()!
|
||||
console.print_debug('Cluster: ${info.nodes} nodes, ${info.running_pods} pods')
|
||||
|
||||
k8s.stop()!
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### kubectl not found
|
||||
```bash
|
||||
which kubectl
|
||||
# If not found, install kubectl or add to PATH
|
||||
export PATH=$PATH:/usr/local/bin
|
||||
```
|
||||
|
||||
### Kubeconfig not found
|
||||
```bash
|
||||
export KUBECONFIG=~/.kube/config
|
||||
# Or specify in code: kubeconfig_path: '/path/to/config'
|
||||
```
|
||||
|
||||
### Connection refused
|
||||
```bash
|
||||
# Check cluster is running
|
||||
kubectl cluster-info
|
||||
# Check credentials in kubeconfig
|
||||
kubectl config view
|
||||
```
|
||||
|
||||
### YAML validation errors
|
||||
```bash
|
||||
# Validate with kubectl
|
||||
kubectl apply -f myfile.yaml --dry-run=client
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: {{.Name}}
|
||||
namespace: {{.Namespace}}
|
||||
name: ${metadata.name}
|
||||
namespace: ${metadata.namespace}
|
||||
data:
|
||||
{{.Key}}: {{.Value}}
|
||||
config.yaml: |
|
||||
${config_content}
|
||||
@@ -1,22 +1,24 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{.Name}}
|
||||
namespace: {{.Namespace}}
|
||||
name: ${metadata.name}
|
||||
namespace: ${metadata.namespace}
|
||||
labels:
|
||||
app: {{.AppLabel}}
|
||||
app: ${metadata.name}
|
||||
spec:
|
||||
replicas: {{.Replicas}}
|
||||
replicas: ${replicas}
|
||||
selector:
|
||||
matchLabels:
|
||||
app: {{.AppLabel}}
|
||||
app: ${metadata.name}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: {{.AppLabel}}
|
||||
app: ${metadata.name}
|
||||
spec:
|
||||
containers:
|
||||
- name: {{.ContainerName}}
|
||||
image: {{.Image}}
|
||||
- name: ${container.name}
|
||||
image: ${container.image}
|
||||
imagePullPolicy: IfNotPresent
|
||||
ports:
|
||||
- containerPort: {{.ContainerPort}}
|
||||
- containerPort: ${container.port}
|
||||
name: http
|
||||
@@ -1,15 +1,13 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{.Name}}
|
||||
namespace: {{.Namespace}}
|
||||
labels:
|
||||
app: {{.AppLabel}}
|
||||
name: ${metadata.name}
|
||||
namespace: ${metadata.namespace}
|
||||
spec:
|
||||
type: ${service_type}
|
||||
selector:
|
||||
app: {{.AppLabel}}
|
||||
app: ${metadata.name}
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: {{.Port}}
|
||||
targetPort: {{.TargetPort}}
|
||||
type: {{.ServiceType}}
|
||||
- protocol: TCP
|
||||
port: ${port}
|
||||
targetPort: ${target_port}
|
||||
12
lib/virt/kubernetes_notgood/.heroscript
Normal file
12
lib/virt/kubernetes_notgood/.heroscript
Normal file
@@ -0,0 +1,12 @@
|
||||
|
||||
!!hero_code.generate_installer
|
||||
name:''
|
||||
classname:'Kubernetes'
|
||||
singleton:0
|
||||
templates:1
|
||||
default:1
|
||||
title:''
|
||||
supported_platforms:''
|
||||
startupmanager:1
|
||||
hasconfig:1
|
||||
build:1
|
||||
265
lib/virt/kubernetes_notgood/kubernetes_actions.v
Normal file
265
lib/virt/kubernetes_notgood/kubernetes_actions.v
Normal file
@@ -0,0 +1,265 @@
|
||||
module kubernetes
|
||||
|
||||
import incubaid.herolib.ui.console
|
||||
import incubaid.herolib.core.texttools
|
||||
import time
|
||||
|
||||
@[params]
|
||||
pub struct DeployArgs {
|
||||
pub mut:
|
||||
deployment Deployment
|
||||
wait bool = true
|
||||
timeout_seconds int = 300
|
||||
poll_interval_ms int = 5000
|
||||
}
|
||||
|
||||
// Deploy a Deployment resource
|
||||
pub fn (mut c KubernetesClient) deploy(args DeployArgs) !Deployment {
|
||||
console.print_header('Deploying ${args.deployment.metadata.name}')
|
||||
|
||||
// Check if deployment already exists
|
||||
existing := c.get[Deployment](
|
||||
resource_type: 'deployments'
|
||||
name: args.deployment.metadata.name
|
||||
namespace: args.deployment.metadata.namespace
|
||||
) or { none }
|
||||
|
||||
mut deployed_deployment := match existing {
|
||||
Deployment { c.update[Deployment](args.deployment)! }
|
||||
else { c.create[Deployment](args.deployment)! }
|
||||
}
|
||||
|
||||
console.print_green('Deployment created/updated: ${args.deployment.metadata.name}')
|
||||
|
||||
if args.wait {
|
||||
c.wait_for_deployment_ready(
|
||||
name: args.deployment.metadata.name
|
||||
namespace: args.deployment.metadata.namespace
|
||||
timeout_seconds: args.timeout_seconds
|
||||
)!
|
||||
}
|
||||
|
||||
return deployed_deployment
|
||||
}
|
||||
|
||||
@[params]
|
||||
pub struct WaitForDeploymentArgs {
|
||||
pub mut:
|
||||
name string
|
||||
namespace string = 'default'
|
||||
timeout_seconds int = 300
|
||||
poll_interval_ms int = 5000
|
||||
}
|
||||
|
||||
// Wait for deployment to be ready
|
||||
pub fn (mut c KubernetesClient) wait_for_deployment_ready(args WaitForDeploymentArgs) ! {
|
||||
start_time := time.now()
|
||||
|
||||
for {
|
||||
deployment := c.get[Deployment](
|
||||
resource_type: 'deployments'
|
||||
name: args.name
|
||||
namespace: args.namespace
|
||||
)!
|
||||
|
||||
if deployment.status != none {
|
||||
status := deployment.status!
|
||||
if status.observed_generation >= deployment.metadata.resource_version.int() {
|
||||
if status.updated_replicas == deployment.spec.replicas {
|
||||
if status.ready_replicas == deployment.spec.replicas {
|
||||
console.print_green('Deployment ${args.name} is ready')
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
elapsed := time.now().unix_time() - start_time.unix_time()
|
||||
if elapsed > args.timeout_seconds {
|
||||
return error('Timeout waiting for deployment ${args.name} to be ready')
|
||||
}
|
||||
|
||||
console.print_debug('Waiting for deployment ${args.name}...')
|
||||
time.sleep(args.poll_interval_ms * time.millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
@[params]
|
||||
pub struct DeleteArgs {
|
||||
pub mut:
|
||||
resource_type string // 'pods', 'deployments', 'services', etc.
|
||||
name string
|
||||
namespace string = 'default'
|
||||
grace_period_seconds int = 30
|
||||
wait bool = true
|
||||
timeout_seconds int = 300
|
||||
}
|
||||
|
||||
// Delete a resource
|
||||
pub fn (mut c KubernetesClient) delete_resource(args DeleteArgs) ! {
|
||||
console.print_header('Deleting ${args.resource_type}/${args.name}')
|
||||
|
||||
c.delete(
|
||||
resource_type: args.resource_type
|
||||
name: args.name
|
||||
namespace: args.namespace
|
||||
)!
|
||||
|
||||
console.print_green('${args.resource_type}/${args.name} deleted')
|
||||
|
||||
if args.wait {
|
||||
start_time := time.now()
|
||||
for {
|
||||
c.get[map[string]interface{}](
|
||||
resource_type: args.resource_type
|
||||
name: args.name
|
||||
namespace: args.namespace
|
||||
) or {
|
||||
console.print_green('${args.resource_type}/${args.name} fully removed')
|
||||
return
|
||||
}
|
||||
|
||||
elapsed := time.now().unix_time() - start_time.unix_time()
|
||||
if elapsed > args.timeout_seconds {
|
||||
return error('Timeout waiting for ${args.resource_type}/${args.name} to be deleted')
|
||||
}
|
||||
|
||||
time.sleep(5000 * time.millisecond)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@[params]
|
||||
pub struct ScaleArgs {
|
||||
pub mut:
|
||||
deployment_name string
|
||||
namespace string = 'default'
|
||||
replicas int
|
||||
wait bool = true
|
||||
timeout_seconds int = 300
|
||||
}
|
||||
|
||||
// Scale a deployment
|
||||
pub fn (mut c KubernetesClient) scale_deployment(args ScaleArgs) !Deployment {
|
||||
console.print_header('Scaling ${args.deployment_name} to ${args.replicas} replicas')
|
||||
|
||||
// Patch the deployment spec.replicas field
|
||||
patch_data := {
|
||||
'spec': {
|
||||
'replicas': args.replicas
|
||||
}
|
||||
}
|
||||
|
||||
mut deployment := c.patch[Deployment](
|
||||
resource_type: 'deployments'
|
||||
name: args.deployment_name
|
||||
namespace: args.namespace
|
||||
patch_data: patch_data
|
||||
)!
|
||||
|
||||
if args.wait {
|
||||
c.wait_for_deployment_ready(
|
||||
name: args.deployment_name
|
||||
namespace: args.namespace
|
||||
timeout_seconds: args.timeout_seconds
|
||||
)!
|
||||
}
|
||||
|
||||
return deployment
|
||||
}
|
||||
|
||||
@[params]
|
||||
pub struct RollingRestartArgs {
|
||||
pub mut:
|
||||
deployment_name string
|
||||
namespace string = 'default'
|
||||
wait bool = true
|
||||
timeout_seconds int = 600
|
||||
}
|
||||
|
||||
// Perform rolling restart of a deployment
|
||||
pub fn (mut c KubernetesClient) rolling_restart(args RollingRestartArgs) ! {
|
||||
console.print_header('Rolling restart of ${args.deployment_name}')
|
||||
|
||||
now_timestamp := time.now().format_rfc3339()
|
||||
patch_data := {
|
||||
'spec': {
|
||||
'template': {
|
||||
'metadata': {
|
||||
'annotations': {
|
||||
'kubectl.kubernetes.io/restartedAt': now_timestamp
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c.patch[Deployment](
|
||||
resource_type: 'deployments'
|
||||
name: args.deployment_name
|
||||
namespace: args.namespace
|
||||
patch_data: patch_data
|
||||
)!
|
||||
|
||||
if args.wait {
|
||||
c.wait_for_deployment_ready(
|
||||
name: args.deployment_name
|
||||
namespace: args.namespace
|
||||
timeout_seconds: args.timeout_seconds
|
||||
)!
|
||||
}
|
||||
|
||||
console.print_green('${args.deployment_name} rolling restart completed')
|
||||
}
|
||||
|
||||
@[params]
|
||||
pub struct GetLogsArgs {
|
||||
pub mut:
|
||||
pod_name string
|
||||
container_name string
|
||||
namespace string = 'default'
|
||||
tail_lines int = 100
|
||||
follow bool
|
||||
}
|
||||
|
||||
// Get pod logs
|
||||
pub fn (mut c KubernetesClient) get_pod_logs(args GetLogsArgs) !string {
|
||||
mut prefix := '/api/v1/namespaces/${args.namespace}/pods/${args.pod_name}/log'
|
||||
prefix += '?tailLines=${args.tail_lines}'
|
||||
if args.container_name.len > 0 {
|
||||
prefix += '&container=${args.container_name}'
|
||||
}
|
||||
|
||||
conn := c.connection()!
|
||||
return conn.get_text(prefix: prefix)!
|
||||
}
|
||||
|
||||
@[params]
|
||||
pub struct ExecArgs {
|
||||
pub mut:
|
||||
pod_name string
|
||||
container_name string
|
||||
command []string
|
||||
namespace string = 'default'
|
||||
}
|
||||
|
||||
// Execute command in pod (requires WebSocket support - future enhancement)
|
||||
pub fn (mut c KubernetesClient) exec_in_pod(args ExecArgs) ! {
|
||||
// TODO: Implement WebSocket-based exec
|
||||
return error('exec_in_pod requires WebSocket support - not yet implemented')
|
||||
}
|
||||
|
||||
@[params]
|
||||
pub struct PortForwardArgs {
|
||||
pub mut:
|
||||
pod_name string
|
||||
namespace string = 'default'
|
||||
local_port int
|
||||
remote_port int
|
||||
}
|
||||
|
||||
// Port forward to pod (requires WebSocket support - future enhancement)
|
||||
pub fn (mut c KubernetesClient) port_forward(args PortForwardArgs) ! {
|
||||
// TODO: Implement WebSocket-based port forward
|
||||
return error('port_forward requires WebSocket support - not yet implemented')
|
||||
}
|
||||
223
lib/virt/kubernetes_notgood/kubernetes_client.v
Normal file
223
lib/virt/kubernetes_notgood/kubernetes_client.v
Normal file
@@ -0,0 +1,223 @@
|
||||
module kubernetes
|
||||
|
||||
import incubaid.herolib.core.httpconnection
|
||||
import net.http
|
||||
import json
|
||||
import encoding.base64
|
||||
|
||||
@[heap]
|
||||
pub struct KubernetesClient {
|
||||
pub mut:
|
||||
config KubernetesConfig
|
||||
http_conn ?&httpconnection.HTTPConnection
|
||||
api_groups map[string]APIGroupVersion
|
||||
}
|
||||
|
||||
pub struct APIGroupVersion {
|
||||
pub:
|
||||
group_version string
|
||||
resources []APIResource
|
||||
}
|
||||
|
||||
pub struct APIResource {
|
||||
pub:
|
||||
name string
|
||||
singularname string
|
||||
namespaced bool
|
||||
kind string
|
||||
verbs []string // get, list, create, update, delete, patch, watch, etc.
|
||||
}
|
||||
|
||||
pub struct APIResponse {
|
||||
pub:
|
||||
kind string
|
||||
api_version string
|
||||
metadata map[string]interface{}
|
||||
items []json.Any
|
||||
}
|
||||
|
||||
// Create new Kubernetes client
|
||||
pub fn new(config KubernetesConfig) !&KubernetesClient {
|
||||
mut cfg := config
|
||||
cfg.validate()!
|
||||
|
||||
mut client := KubernetesClient{
|
||||
config: cfg
|
||||
}
|
||||
client.http_conn = http_connection_create(cfg)!
|
||||
|
||||
return &client
|
||||
}
|
||||
|
||||
// Create HTTP connection with proper auth
|
||||
fn http_connection_create(cfg KubernetesConfig) !&httpconnection.HTTPConnection {
|
||||
mut conn := httpconnection.new(
|
||||
name: 'kubernetes-${cfg.name}'
|
||||
url: cfg.server
|
||||
retry: 3
|
||||
cache: false
|
||||
)!
|
||||
|
||||
// Setup authentication
|
||||
if cfg.token.len > 0 {
|
||||
mut header := http.new_header()
|
||||
header.add(.authorization, 'Bearer ${cfg.token}')
|
||||
conn.default_header = header
|
||||
} else if cfg.username.len > 0 && cfg.password.len > 0 {
|
||||
conn.basic_auth(cfg.username, cfg.password)
|
||||
}
|
||||
|
||||
// TLS configuration
|
||||
if cfg.insecure_skip_verify {
|
||||
// Skip verification (development only)
|
||||
} else if cfg.certificate_authority.len > 0 {
|
||||
// Load CA certificate
|
||||
// TODO: Configure TLS with CA cert
|
||||
}
|
||||
|
||||
return conn
|
||||
}
|
||||
|
||||
// Get HTTP connection
|
||||
pub fn (mut c KubernetesClient) connection() !&httpconnection.HTTPConnection {
|
||||
if c.http_conn == none {
|
||||
c.http_conn = http_connection_create(c.config)!
|
||||
}
|
||||
return c.http_conn!
|
||||
}
|
||||
|
||||
// ==================== DISCOVERY API ====================
|
||||
|
||||
// Get available API groups
|
||||
pub fn (mut c KubernetesClient) get_api_groups() ![]APIGroupVersion {
|
||||
conn := c.connection()!
|
||||
response := conn.get_json_generic[map[string]interface{}](
|
||||
prefix: '/apis'
|
||||
)!
|
||||
// Parse and return API groups
|
||||
return []APIGroupVersion{}
|
||||
}
|
||||
|
||||
// ==================== CORE API METHODS ====================
|
||||
|
||||
// Generic GET for any resource
|
||||
pub fn (mut c KubernetesClient) get[T](
|
||||
resource_type string
|
||||
name string
|
||||
namespace string = ''
|
||||
) !T {
|
||||
mut prefix := build_api_path(resource_type, name, namespace)
|
||||
conn := c.connection()!
|
||||
return conn.get_json_generic[T](prefix: prefix)!
|
||||
}
|
||||
|
||||
// Generic LIST for any resource
|
||||
pub fn (mut c KubernetesClient) list[T](
|
||||
resource_type string
|
||||
namespace string = ''
|
||||
label_selector string = ''
|
||||
) ![]T {
|
||||
mut prefix := build_api_list_path(resource_type, namespace)
|
||||
if label_selector.len > 0 {
|
||||
prefix += '?labelSelector=${label_selector}'
|
||||
}
|
||||
conn := c.connection()!
|
||||
response := conn.get_json_generic[map[string]interface{}](prefix: prefix)!
|
||||
// Parse items array
|
||||
return []T{}
|
||||
}
|
||||
|
||||
// Generic CREATE for any resource
|
||||
pub fn (mut c KubernetesClient) create[T](resource T) !T {
|
||||
resource_type := get_resource_type[T]()
|
||||
namespace := get_namespace[T](resource)
|
||||
mut prefix := build_api_path_create(resource_type, namespace)
|
||||
|
||||
conn := c.connection()!
|
||||
return conn.post_json_generic[T](
|
||||
prefix: prefix
|
||||
params: json.decode_object(json.encode(resource))!
|
||||
)!
|
||||
}
|
||||
|
||||
// Generic UPDATE (PUT) for any resource
|
||||
pub fn (mut c KubernetesClient) update[T](resource T) !T {
|
||||
resource_type := get_resource_type[T]()
|
||||
name := get_resource_name[T](resource)
|
||||
namespace := get_namespace[T](resource)
|
||||
mut prefix := build_api_path(resource_type, name, namespace)
|
||||
|
||||
conn := c.connection()!
|
||||
return conn.put_json_generic[T](
|
||||
prefix: prefix
|
||||
params: json.decode_object(json.encode(resource))!
|
||||
)!
|
||||
}
|
||||
|
||||
// Generic PATCH for any resource
|
||||
pub fn (mut c KubernetesClient) patch[T](
|
||||
resource_type string
|
||||
name string
|
||||
namespace string
|
||||
patch_data map[string]interface{}
|
||||
) !T {
|
||||
mut prefix := build_api_path(resource_type, name, namespace)
|
||||
|
||||
conn := c.connection()!
|
||||
mut header := http.new_header()
|
||||
header.add(.content_type, 'application/merge-patch+json')
|
||||
|
||||
return conn.patch_json_generic[T](
|
||||
prefix: prefix
|
||||
params: patch_data
|
||||
header: header
|
||||
)!
|
||||
}
|
||||
|
||||
// Generic DELETE for any resource
|
||||
pub fn (mut c KubernetesClient) delete(
|
||||
resource_type string
|
||||
name string
|
||||
namespace string = ''
|
||||
) ! {
|
||||
mut prefix := build_api_path(resource_type, name, namespace)
|
||||
conn := c.connection()!
|
||||
conn.delete(prefix: prefix)!
|
||||
}
|
||||
|
||||
// Helper functions for building API paths
|
||||
fn build_api_path(resource_type string, name string, namespace string) string {
|
||||
if namespace.len > 0 {
|
||||
return '/api/v1/namespaces/${namespace}/${resource_type}/${name}'
|
||||
} else {
|
||||
return '/api/v1/${resource_type}/${name}'
|
||||
}
|
||||
}
|
||||
|
||||
fn build_api_list_path(resource_type string, namespace string) string {
|
||||
if namespace.len > 0 {
|
||||
return '/api/v1/namespaces/${namespace}/${resource_type}'
|
||||
} else {
|
||||
return '/api/v1/${resource_type}'
|
||||
}
|
||||
}
|
||||
|
||||
fn build_api_path_create(resource_type string, namespace string) string {
|
||||
return build_api_list_path(resource_type, namespace)
|
||||
}
|
||||
|
||||
// Helper functions to extract metadata from generic types
|
||||
fn get_resource_type[T]() string {
|
||||
// TODO: Use compile-time reflection to get Kind from T
|
||||
return 'pods'
|
||||
}
|
||||
|
||||
fn get_resource_name[T](resource T) string {
|
||||
// TODO: Extract metadata.name from resource
|
||||
return ''
|
||||
}
|
||||
|
||||
fn get_namespace[T](resource T) string {
|
||||
// TODO: Extract metadata.namespace from resource
|
||||
return 'default'
|
||||
}
|
||||
312
lib/virt/kubernetes_notgood/kubernetes_factory_.v
Normal file
312
lib/virt/kubernetes_notgood/kubernetes_factory_.v
Normal file
@@ -0,0 +1,312 @@
|
||||
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]&Kubernetes
|
||||
kubernetes_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) !&Kubernetes {
|
||||
mut obj := Kubernetes{
|
||||
name: args.name
|
||||
}
|
||||
set(obj)!
|
||||
return get(name: args.name)!
|
||||
}
|
||||
|
||||
pub fn get(args ArgsGet) !&Kubernetes {
|
||||
mut context := base.context()!
|
||||
kubernetes_default = args.name
|
||||
if args.fromdb || args.name !in kubernetes_global {
|
||||
mut r := context.redis()!
|
||||
if r.hexists('context:kubernetes', args.name)! {
|
||||
data := r.hget('context:kubernetes', args.name)!
|
||||
if data.len == 0 {
|
||||
print_backtrace()
|
||||
return error('Kubernetes with name: kubernetes does not exist, prob bug.')
|
||||
}
|
||||
mut obj := json.decode(Kubernetes, data)!
|
||||
set_in_mem(obj)!
|
||||
} else {
|
||||
if args.create {
|
||||
new(args)!
|
||||
} else {
|
||||
print_backtrace()
|
||||
return error("Kubernetes with name 'kubernetes' does not exist")
|
||||
}
|
||||
}
|
||||
return get(name: args.name)! // no longer from db nor create
|
||||
}
|
||||
return kubernetes_global[args.name] or {
|
||||
print_backtrace()
|
||||
return error('could not get config for kubernetes with name:kubernetes')
|
||||
}
|
||||
}
|
||||
|
||||
// register the config for the future
|
||||
pub fn set(o Kubernetes) ! {
|
||||
mut o2 := set_in_mem(o)!
|
||||
kubernetes_default = o2.name
|
||||
mut context := base.context()!
|
||||
mut r := context.redis()!
|
||||
r.hset('context:kubernetes', 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', args.name)!
|
||||
}
|
||||
|
||||
pub fn delete(args ArgsGet) ! {
|
||||
mut context := base.context()!
|
||||
mut r := context.redis()!
|
||||
r.hdel('context:kubernetes', 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) ![]&Kubernetes {
|
||||
mut res := []&Kubernetes{}
|
||||
mut context := base.context()!
|
||||
if args.fromdb {
|
||||
// reset what is in mem
|
||||
kubernetes_global = map[string]&Kubernetes{}
|
||||
kubernetes_default = ''
|
||||
}
|
||||
if args.fromdb {
|
||||
mut r := context.redis()!
|
||||
mut l := r.hkeys('context:kubernetes')!
|
||||
|
||||
for name in l {
|
||||
res << get(name: name, fromdb: true)!
|
||||
}
|
||||
return res
|
||||
} else {
|
||||
// load from memory
|
||||
for _, client in kubernetes_global {
|
||||
res << client
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// only sets in mem, does not set as config
|
||||
fn set_in_mem(o Kubernetes) !Kubernetes {
|
||||
mut o2 := obj_init(o)!
|
||||
kubernetes_global[o2.name] = &o2
|
||||
kubernetes_default = o2.name
|
||||
return o2
|
||||
}
|
||||
|
||||
pub fn play(mut plbook PlayBook) ! {
|
||||
if !plbook.exists(filter: 'kubernetes.') {
|
||||
return
|
||||
}
|
||||
mut install_actions := plbook.find(filter: 'kubernetes.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: '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 Kubernetes) reload() ! {
|
||||
switch(self.name)
|
||||
self = obj_init(self)!
|
||||
}
|
||||
|
||||
pub fn (mut self Kubernetes) 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 Kubernetes) install_start(args InstallArgs) ! {
|
||||
switch(self.name)
|
||||
self.install(args)!
|
||||
self.start()!
|
||||
}
|
||||
|
||||
pub fn (mut self Kubernetes) 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 Kubernetes) restart() ! {
|
||||
switch(self.name)
|
||||
self.stop()!
|
||||
self.start()!
|
||||
}
|
||||
|
||||
pub fn (mut self Kubernetes) 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 Kubernetes) install(args InstallArgs) ! {
|
||||
switch(self.name)
|
||||
if args.reset || (!installed()!) {
|
||||
install()!
|
||||
}
|
||||
}
|
||||
|
||||
pub fn (mut self Kubernetes) build() ! {
|
||||
switch(self.name)
|
||||
build()!
|
||||
}
|
||||
|
||||
pub fn (mut self Kubernetes) destroy() ! {
|
||||
switch(self.name)
|
||||
self.stop() or {}
|
||||
destroy()!
|
||||
}
|
||||
|
||||
// switch instance to be used for kubernetes
|
||||
pub fn switch(name string) {
|
||||
kubernetes_default = name
|
||||
}
|
||||
48
lib/virt/kubernetes_notgood/kubernetes_model.v
Normal file
48
lib/virt/kubernetes_notgood/kubernetes_model.v
Normal file
@@ -0,0 +1,48 @@
|
||||
module kubernetes
|
||||
|
||||
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 Kubernetes {
|
||||
pub mut:
|
||||
name string = 'default'
|
||||
homedir string
|
||||
configpath string
|
||||
username string
|
||||
password string @[secret]
|
||||
title string
|
||||
host string
|
||||
port int
|
||||
}
|
||||
|
||||
// your checking & initialization code if needed
|
||||
fn obj_init(mycfg_ Kubernetes) !Kubernetes {
|
||||
mut mycfg := mycfg_
|
||||
// if mycfg.password == '' && mycfg.secret == '' {
|
||||
// return error('password or secret needs to be filled in for ${mycfg.name}')
|
||||
//}
|
||||
return mycfg
|
||||
}
|
||||
|
||||
// called before start if done
|
||||
fn configure() ! {
|
||||
// mut installer := get()!
|
||||
// mut mycode := $tmpl('templates/atemplate.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) !Kubernetes {
|
||||
mut obj := encoderhero.decode[Kubernetes](heroscript)!
|
||||
return obj
|
||||
}
|
||||
53
lib/virt/kubernetes_notgood/kubernetes_yaml.v
Normal file
53
lib/virt/kubernetes_notgood/kubernetes_yaml.v
Normal file
@@ -0,0 +1,53 @@
|
||||
module kubernetes
|
||||
|
||||
import encoding.yaml
|
||||
|
||||
// Serialize Kubernetes resource to YAML
|
||||
pub fn to_yaml[T](resource T) !string {
|
||||
// TODO: Use V's YAML encoding to serialize
|
||||
return encode_to_yaml(resource)!
|
||||
}
|
||||
|
||||
// Deserialize YAML to Kubernetes resource
|
||||
pub fn from_yaml[T](content string) !T {
|
||||
// TODO: Use V's YAML decoding to parse
|
||||
return decode_from_yaml[T](content)!
|
||||
}
|
||||
|
||||
// Helper to handle YAML document conversion
|
||||
fn encode_to_yaml[T](resource T) !string {
|
||||
return yaml.encode(resource)!
|
||||
}
|
||||
|
||||
fn decode_from_yaml[T](content string) !T {
|
||||
return yaml.decode[T](content)!
|
||||
}
|
||||
|
||||
// Multi-document YAML support
|
||||
pub fn to_yaml_multi[T](resources []T) !string {
|
||||
mut result := ''
|
||||
for i, resource in resources {
|
||||
result += '---\n'
|
||||
result += to_yaml(resource)!
|
||||
if i < resources.len - 1 {
|
||||
result += '\n'
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
pub fn from_yaml_multi[T](content string) ![]T {
|
||||
mut result := []T{}
|
||||
docs := content.split('---')
|
||||
|
||||
for doc in docs {
|
||||
trimmed := doc.trim_space()
|
||||
if trimmed.len == 0 {
|
||||
continue
|
||||
}
|
||||
resource := from_yaml[T](trimmed)!
|
||||
result << resource
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
44
lib/virt/kubernetes_notgood/readme.md
Normal file
44
lib/virt/kubernetes_notgood/readme.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# kubernetes
|
||||
|
||||
|
||||
|
||||
To get started
|
||||
|
||||
```v
|
||||
|
||||
|
||||
import incubaid.herolib.installers.something.kubernetes as kubernetes_installer
|
||||
|
||||
heroscript:="
|
||||
!!kubernetes.configure name:'test'
|
||||
password: '1234'
|
||||
port: 7701
|
||||
|
||||
!!kubernetes.start name:'test' reset:1
|
||||
"
|
||||
|
||||
kubernetes_installer.play(heroscript=heroscript)!
|
||||
|
||||
//or we can call the default and do a start with reset
|
||||
//mut installer:= kubernetes_installer.get()!
|
||||
//installer.start(reset:true)!
|
||||
|
||||
|
||||
|
||||
|
||||
```
|
||||
|
||||
## example heroscript
|
||||
|
||||
|
||||
```hero
|
||||
!!kubernetes.configure
|
||||
homedir: '/home/user/kubernetes'
|
||||
username: 'admin'
|
||||
password: 'secretpassword'
|
||||
title: 'Some Title'
|
||||
host: 'localhost'
|
||||
port: 8888
|
||||
|
||||
```
|
||||
|
||||
5
lib/virt/kubernetes_notgood/templates/atemplate.yaml
Normal file
5
lib/virt/kubernetes_notgood/templates/atemplate.yaml
Normal file
@@ -0,0 +1,5 @@
|
||||
|
||||
|
||||
name: ${cfg.configpath}
|
||||
|
||||
|
||||
7
lib/virt/kubernetes_notgood/templates/configmap.yaml
Normal file
7
lib/virt/kubernetes_notgood/templates/configmap.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: {{.Name}}
|
||||
namespace: {{.Namespace}}
|
||||
data:
|
||||
{{.Key}}: {{.Value}}
|
||||
22
lib/virt/kubernetes_notgood/templates/deployment.yaml
Normal file
22
lib/virt/kubernetes_notgood/templates/deployment.yaml
Normal file
@@ -0,0 +1,22 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{.Name}}
|
||||
namespace: {{.Namespace}}
|
||||
labels:
|
||||
app: {{.AppLabel}}
|
||||
spec:
|
||||
replicas: {{.Replicas}}
|
||||
selector:
|
||||
matchLabels:
|
||||
app: {{.AppLabel}}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: {{.AppLabel}}
|
||||
spec:
|
||||
containers:
|
||||
- name: {{.ContainerName}}
|
||||
image: {{.Image}}
|
||||
ports:
|
||||
- containerPort: {{.ContainerPort}}
|
||||
15
lib/virt/kubernetes_notgood/templates/service.yaml
Normal file
15
lib/virt/kubernetes_notgood/templates/service.yaml
Normal file
@@ -0,0 +1,15 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{.Name}}
|
||||
namespace: {{.Namespace}}
|
||||
labels:
|
||||
app: {{.AppLabel}}
|
||||
spec:
|
||||
selector:
|
||||
app: {{.AppLabel}}
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: {{.Port}}
|
||||
targetPort: {{.TargetPort}}
|
||||
type: {{.ServiceType}}
|
||||
Reference in New Issue
Block a user