From 472e4bfaaa782c582654da49d0a09cb7320b8519 Mon Sep 17 00:00:00 2001 From: Mahmoud-Emad Date: Mon, 3 Nov 2025 16:25:21 +0200 Subject: [PATCH 1/2] feat: Add Gitea Kubernetes installer - Add Gitea installer module and types - Implement installation and destruction logic - Integrate with Kubernetes and TFGW - Add example usage and documentation --- examples/installers/k8s/.gitignore | 3 +- examples/installers/k8s/gitea.vsh | 34 ++++ lib/installers/k8s/gitea/.heroscript | 12 ++ lib/installers/k8s/gitea/README.md | 105 ++++++++++ lib/installers/k8s/gitea/gitea_actions.v | 188 ++++++++++++++++++ lib/installers/k8s/gitea/gitea_factory_.v | 188 ++++++++++++++++++ lib/installers/k8s/gitea/gitea_model.v | 155 +++++++++++++++ lib/installers/k8s/gitea/templates/gitea.yaml | 97 +++++++++ lib/installers/k8s/gitea/templates/tfgw.yaml | 15 ++ 9 files changed, 796 insertions(+), 1 deletion(-) create mode 100755 examples/installers/k8s/gitea.vsh create mode 100644 lib/installers/k8s/gitea/.heroscript create mode 100644 lib/installers/k8s/gitea/README.md create mode 100644 lib/installers/k8s/gitea/gitea_actions.v create mode 100644 lib/installers/k8s/gitea/gitea_factory_.v create mode 100644 lib/installers/k8s/gitea/gitea_model.v create mode 100644 lib/installers/k8s/gitea/templates/gitea.yaml create mode 100644 lib/installers/k8s/gitea/templates/tfgw.yaml diff --git a/examples/installers/k8s/.gitignore b/examples/installers/k8s/.gitignore index 05081403..78d2be8b 100644 --- a/examples/installers/k8s/.gitignore +++ b/examples/installers/k8s/.gitignore @@ -1,2 +1,3 @@ cryptpad -element_chat \ No newline at end of file +element_chat +gitea \ No newline at end of file diff --git a/examples/installers/k8s/gitea.vsh b/examples/installers/k8s/gitea.vsh new file mode 100755 index 00000000..3acb3d08 --- /dev/null +++ b/examples/installers/k8s/gitea.vsh @@ -0,0 +1,34 @@ +#!/usr/bin/env -S v -n -w -gc none -cc tcc -d use_openssl -enable-globals run + +import incubaid.herolib.installers.k8s.gitea + +// This example demonstrates how to use the Gitea installer. + +// 1. Create a new installer instance with a specific hostname. +// Replace 'mygitea' with your desired hostname. +// Note: Use only alphanumeric characters (no underscores or dashes). +mut installer := gitea.get( + name: 'kristof' + create: true +)! + +// 2. Configure the installer (all settings are optional with sensible defaults) +// installer.hostname = 'giteaapp' // Default: 'giteaapp' +// installer.namespace = 'forge' // Default: 'forge' + +// // Gitea server configuration +// installer.http_port = 3000 // Default: 3000 +// installer.disable_registration = false // Default: false (allow new user registration) +// installer.db_type = 'sqlite3' // Default: 'sqlite3' (can be 'postgres', 'mysql') +// installer.db_path = '/data/gitea/gitea.db' // Default: '/data/gitea/gitea.db' +// installer.storage_size = '5Gi' // Default: '5Gi' (PVC storage size) + +// 3. Install Gitea. +// This will generate the necessary Kubernetes YAML files and apply them to your cluster. +installer.install()! + +// println('Gitea installation started.') +// println('You can access it at: https://${installer.hostname}.gent01.grid.tf') + +// 4. To destroy the deployment, you can run the following: +// installer.destroy()! diff --git a/lib/installers/k8s/gitea/.heroscript b/lib/installers/k8s/gitea/.heroscript new file mode 100644 index 00000000..e14e18a9 --- /dev/null +++ b/lib/installers/k8s/gitea/.heroscript @@ -0,0 +1,12 @@ + +!!hero_code.generate_installer + name:'' + classname:'GiteaK8SInstaller' + singleton:0 + templates:1 + default:0 + title:'' + supported_platforms:'' + startupmanager:0 + hasconfig:1 + build:0 \ No newline at end of file diff --git a/lib/installers/k8s/gitea/README.md b/lib/installers/k8s/gitea/README.md new file mode 100644 index 00000000..9b3bc435 --- /dev/null +++ b/lib/installers/k8s/gitea/README.md @@ -0,0 +1,105 @@ +# Gitea Kubernetes Installer + +A Kubernetes installer for Gitea with TFGrid Gateway integration. + +## Overview + +This installer deploys a complete Git hosting solution: + +- **Gitea**: A lightweight self-hosted Git service +- **TFGW (ThreeFold Gateway)**: Provides public FQDNs with TLS termination + +## Quick Start + +```v +import incubaid.herolib.installers.k8s.gitea + +// Create and install Gitea with defaults +mut installer := gitea.get( + name: 'mygitea' + create: true +)! + +installer.install()! +``` + +## Configuration Options + +All configuration options are optional and have sensible defaults: + +### Hostname and Namespace + +```v +installer.hostname = 'giteaapp' // Default: 'giteaapp' +installer.namespace = 'forge' // Default: '${installer.name}-gitea-namespace' +``` + +**Note**: Use only alphanumeric characters in hostnames (no underscores or dashes). + +### Gitea Server Configuration + +```v +// Server port +installer.http_port = 3000 // Default: 3000 + +// Database configuration +installer.db_type = 'sqlite3' // Default: 'sqlite3' (options: 'sqlite3', 'postgres', 'mysql') +installer.db_path = '/data/gitea/gitea.db' // Default: '/data/gitea/gitea.db' + +// Registration +installer.disable_registration = false // Default: false (allow new user registration) + +// Storage +installer.storage_size = '5Gi' // Default: '5Gi' (PVC storage size) +``` + +## Full Example + +```v +import incubaid.herolib.installers.k8s.gitea + +mut installer := gitea.get( + name: 'mygitea' + create: true +)! + +// Configure hostname and namespace +installer.hostname = 'mygit' +installer.namespace = 'forge' + +// Configure Gitea +installer.http_port = 3000 +installer.db_type = 'sqlite3' +installer.disable_registration = true // Disable public registration +installer.storage_size = '10Gi' // Increase storage + +// Install +installer.install()! + +println('Gitea: https://${installer.hostname}.gent01.grid.tf') +``` + +## Management + +### Check Installation Status + +```v +if gitea.installed()! { + println('Gitea is installed') +} else { + println('Gitea is not installed') +} +``` + +### Destroy Deployment + +```v +installer.destroy()! +``` + +This will delete the entire namespace and all resources within it. + +## See Also + +- [Gitea Documentation](https://docs.gitea.io/) +- [Gitea GitHub Repository](https://github.com/go-gitea/gitea) diff --git a/lib/installers/k8s/gitea/gitea_actions.v b/lib/installers/k8s/gitea/gitea_actions.v new file mode 100644 index 00000000..a905094d --- /dev/null +++ b/lib/installers/k8s/gitea/gitea_actions.v @@ -0,0 +1,188 @@ +module gitea + +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 +pub fn installed() !bool { + installer := get()! + mut k8s := installer.kube_client + + // Try to get the gitea deployment + deployments := k8s.get_deployments(installer.namespace) or { + // If we can't get deployments, it's not running + return false + } + + // Check if gitea deployment exists + for deployment in deployments { + if deployment.name == 'gitea' { + 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: 'gitea' + // source: '${gitpath}/target/x86_64-unknown-linux-musl/release/gitea' + // )! +} + +fn install() ! { + console.print_header('Installing Gitea...') + + // 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.') + + // 2. Apply the YAML files using kubernetes client + console.print_info('Applying Gateway YAML file to the cluster...') + res1 := k8s.apply_yaml('/tmp/gitea/tfgw-gitea.yaml')! + if !res1.success { + return error('Failed to apply tfgw-gitea.yaml: ${res1.stderr}') + } + console.print_info('Gateway YAML file applied successfully.') + + // 3. Verify TFGW deployment + verify_tfgw_deployment(tfgw_name: 'gitea', namespace: installer.namespace)! + + // 4. Apply Gitea App YAML + console.print_info('Applying Gitea App YAML file to the cluster...') + res2 := k8s.apply_yaml('/tmp/gitea/gitea.yaml')! + if !res2.success { + return error('Failed to apply gitea.yaml: ${res2.stderr}') + } + console.print_info('Gitea App YAML file applied successfully.') + + // 5. 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 Gitea deployment to be ready... (${i + 1}/${max_deployment_retries})') + time.sleep(deployment_check_interval_seconds * time.second) + } + + if is_running { + console.print_header('Gitea installation successful!') + console.print_header('You can access Gitea at https://${installer.hostname}.gent01.grid.tf') + } else { + return error('Gitea 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 gitea deployments/services +} + +// 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 .. max_deployment_retries { + // 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}/${max_deployment_retries})') + time.sleep(deployment_check_interval_seconds * 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}/${max_deployment_retries})') + time.sleep(deployment_check_interval_seconds * 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 Gitea...') + 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.') + } +} diff --git a/lib/installers/k8s/gitea/gitea_factory_.v b/lib/installers/k8s/gitea/gitea_factory_.v new file mode 100644 index 00000000..712c0356 --- /dev/null +++ b/lib/installers/k8s/gitea/gitea_factory_.v @@ -0,0 +1,188 @@ +module gitea + +import incubaid.herolib.core.base +import incubaid.herolib.core.playbook { PlayBook } +import incubaid.herolib.ui.console +import json + +__global ( + gitea_global map[string]&GiteaK8SInstaller + gitea_default string +) + +/////////FACTORY + +@[params] +pub struct ArgsGet { +pub mut: + name string = 'gitea' + fromdb bool // will load from filesystem + create bool // default will not create if not exist +} + +pub fn new(args ArgsGet) !&GiteaK8SInstaller { + mut obj := GiteaK8SInstaller{ + name: args.name + } + set(obj)! + return get(name: args.name)! +} + +pub fn get(args_ ArgsGet) !&GiteaK8SInstaller { + mut context := base.context()! + mut args := args_ + if args.name == 'gitea' && gitea_default != '' { + args.name = gitea_default + } + + if args.fromdb || args.name !in gitea_global { + mut r := context.redis()! + if r.hexists('context:gitea', args.name)! { + data := r.hget('context:gitea', args.name)! + if data.len == 0 { + print_backtrace() + return error('GiteaK8SInstaller with name: ${args.name} does not exist, prob bug.') + } + mut obj := json.decode(GiteaK8SInstaller, data)! + set_in_mem(obj)! + } else { + if args.create { + new(args)! + } else { + print_backtrace() + return error("GiteaK8SInstaller with name '${args.name}' does not exist") + } + } + return get(name: args.name)! // no longer from db nor create + } + return gitea_global[args.name] or { + print_backtrace() + return error('could not get config for gitea with name:${args.name}') + } +} + +// register the config for the future +pub fn set(o GiteaK8SInstaller) ! { + mut o2 := set_in_mem(o)! + gitea_default = o2.name + mut context := base.context()! + mut r := context.redis()! + r.hset('context:gitea', 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:gitea', args.name)! +} + +pub fn delete(args ArgsGet) ! { + mut context := base.context()! + mut r := context.redis()! + r.hdel('context:gitea', 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) ![]&GiteaK8SInstaller { + mut res := []&GiteaK8SInstaller{} + mut context := base.context()! + if args.fromdb { + // reset what is in mem + gitea_global = map[string]&GiteaK8SInstaller{} + gitea_default = '' + } + if args.fromdb { + mut r := context.redis()! + mut l := r.hkeys('context:gitea')! + + for name in l { + res << get(name: name, fromdb: true)! + } + return res + } else { + // load from memory + for _, client in gitea_global { + res << client + } + } + return res +} + +// only sets in mem, does not set as config +fn set_in_mem(o GiteaK8SInstaller) !GiteaK8SInstaller { + mut o2 := obj_init(o)! + gitea_global[o2.name] = &o2 + gitea_default = o2.name + return o2 +} + +pub fn play(mut plbook PlayBook) ! { + if !plbook.exists(filter: 'gitea.') { + return + } + mut install_actions := plbook.find(filter: 'gitea.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: 'gitea.')! + 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 gitea.destroy') + destroy()! + } + if other_action.name == 'install' { + console.print_debug('install action gitea.install') + install()! + } + } + other_action.done = true + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +//////////////////////////# LIVE CYCLE MANAGEMENT FOR INSTALLERS /////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////////////////////////// + +// load from disk and make sure is properly intialized +pub fn (mut self GiteaK8SInstaller) reload() ! { + switch(self.name) + self = obj_init(self)! +} + +@[params] +pub struct InstallArgs { +pub mut: + reset bool +} + +pub fn (mut self GiteaK8SInstaller) install(args InstallArgs) ! { + switch(self.name) + if args.reset || (!installed()!) { + install()! + } +} + +pub fn (mut self GiteaK8SInstaller) destroy() ! { + switch(self.name) + destroy()! +} + +// switch instance to be used for gitea +pub fn switch(name string) { + gitea_default = name +} diff --git a/lib/installers/k8s/gitea/gitea_model.v b/lib/installers/k8s/gitea/gitea_model.v new file mode 100644 index 00000000..5e1df788 --- /dev/null +++ b/lib/installers/k8s/gitea/gitea_model.v @@ -0,0 +1,155 @@ +module gitea + +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 Gitea hostname + backends string // The backends for the TFGW + namespace string // The namespace for the Gitea deployment + root_url string // Gitea ROOT_URL + domain string // Gitea domain + http_port int // Gitea HTTP port + disable_registration bool // Disable user registration + db_type string // Database type (sqlite3, postgres, mysql) + db_path string // Database path for SQLite + storage_size string // PVC storage size +} + +@[heap] +pub struct GiteaK8SInstaller { +pub mut: + name string = 'gitea' + hostname string // Gitea hostname for TFGW + namespace string // Kubernetes namespace + // Gitea configuration + root_url string + domain string + http_port int = 3000 + disable_registration bool + db_type string = 'sqlite3' + db_path string = '/data/gitea/gitea.db' + storage_size string = '5Gi' + // Internal paths + gitea_app_path string = '/tmp/gitea/gitea.yaml' + tfgw_path string = '/tmp/gitea/tfgw-gitea.yaml' + kube_client kubernetes.KubeClient @[skip] +} + +// your checking & initialization code if needed +fn obj_init(mycfg_ GiteaK8SInstaller) !GiteaK8SInstaller { + mut mycfg := mycfg_ + + if mycfg.name == '' { + mycfg.name = 'gitea' + } + + // Replace the dashes, dots, and underscores with nothing + mycfg.name = mycfg.name.replace('_', '') + mycfg.name = mycfg.name.replace('-', '') + mycfg.name = mycfg.name.replace('.', '') + + if mycfg.namespace == '' { + mycfg.namespace = '${mycfg.name}gitea-namespace' + } + + if mycfg.namespace.contains('_') || mycfg.namespace.contains('.') { + console.print_stderr('namespace cannot contain _, was: ${mycfg.namespace}, use dashes instead.') + return error('namespace cannot contain _, was: ${mycfg.namespace}') + } + + if mycfg.hostname == '' { + mycfg.hostname = '${mycfg.name}giteaapp' + } + + 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"') + } + + console.print_info('Generating configuration files from templates...') + + // Get FQDN for root_url and domain + fqdn := '${installer.hostname}.gent01.grid.tf' + + // Create config_values for template generation + mut config_values := ConfigValues{ + hostname: installer.hostname + backends: backends_str_builder.str() + namespace: installer.namespace + root_url: 'https://${fqdn}/' + domain: fqdn + http_port: installer.http_port + disable_registration: installer.disable_registration + db_type: installer.db_type + db_path: installer.db_path + storage_size: installer.storage_size + } + + // Ensure the output directory exists + _ := pathlib.get_dir(path: '/tmp/gitea', create: true)! + + // Generate TFGW YAML + tfgw_yaml := $tmpl('./templates/tfgw.yaml') + mut tfgw_path := pathlib.get_file( + path: installer.tfgw_path + create: true + check: true + )! + tfgw_path.write(tfgw_yaml)! + + // Generate gitea-app YAML + gitea_app_yaml := $tmpl('./templates/gitea.yaml') + mut gitea_app_path := pathlib.get_file(path: installer.gitea_app_path, create: true)! + gitea_app_path.write(gitea_app_yaml)! + + console.print_info('Configuration 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) !GiteaK8SInstaller { + mut obj := encoderhero.decode[GiteaK8SInstaller](heroscript)! + return obj +} diff --git a/lib/installers/k8s/gitea/templates/gitea.yaml b/lib/installers/k8s/gitea/templates/gitea.yaml new file mode 100644 index 00000000..f72276b2 --- /dev/null +++ b/lib/installers/k8s/gitea/templates/gitea.yaml @@ -0,0 +1,97 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: @{config_values.namespace} +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: gitea-data + namespace: @{config_values.namespace} +spec: + accessModes: [ "ReadWriteOnce" ] + resources: { requests: { storage: @{config_values.storage_size} } } +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: gitea + namespace: @{config_values.namespace} +spec: + replicas: 1 + selector: + matchLabels: { app: gitea } + template: + metadata: + labels: { app: gitea } + spec: + securityContext: + fsGroup: 1000 + containers: + - name: gitea + image: gitea/gitea:1.22-rootless + imagePullPolicy: IfNotPresent + ports: + - name: http + containerPort: @{config_values.http_port} + env: + - name: GITEA__server__ROOT_URL + value: "@{config_values.root_url}" + - name: GITEA__server__PROTOCOL + value: "http" + - name: GITEA__server__HTTP_PORT + value: "@{config_values.http_port}" + - name: GITEA__server__DOMAIN + value: "@{config_values.domain}" + - name: GITEA__server__START_SSH_SERVER + value: "false" + - name: GITEA__database__DB_TYPE + value: "@{config_values.db_type}" + - name: GITEA__database__PATH + value: "@{config_values.db_path}" + - name: GITEA__service__DISABLE_REGISTRATION + value: "@{config_values.disable_registration}" + volumeMounts: + - name: data + mountPath: /data + readinessProbe: + httpGet: { path: /, port: @{config_values.http_port} } + initialDelaySeconds: 20 + periodSeconds: 10 + volumes: + - name: data + persistentVolumeClaim: + claimName: gitea-data +--- +apiVersion: v1 +kind: Service +metadata: + name: gitea + namespace: @{config_values.namespace} +spec: + selector: { app: gitea } + ports: + - name: http + port: @{config_values.http_port} + targetPort: @{config_values.http_port} + type: ClusterIP +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: gitea + namespace: @{config_values.namespace} +spec: + ingressClassName: traefik + rules: + - host: @{config_values.domain} + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: gitea + port: + number: @{config_values.http_port} + diff --git a/lib/installers/k8s/gitea/templates/tfgw.yaml b/lib/installers/k8s/gitea/templates/tfgw.yaml new file mode 100644 index 00000000..9d262d7c --- /dev/null +++ b/lib/installers/k8s/gitea/templates/tfgw.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: @{config_values.namespace} +--- +apiVersion: ingress.grid.tf/v1 +kind: TFGW +metadata: + name: gitea + namespace: @{config_values.namespace} +spec: + hostname: "@{config_values.hostname}" + backends: +@{config_values.backends} + From bafc519cd7f8c18ec1189386f21077d5a3e5984e Mon Sep 17 00:00:00 2001 From: Mahmoud-Emad Date: Mon, 3 Nov 2025 17:04:40 +0200 Subject: [PATCH 2/2] feat: Add PostgreSQL support for Gitea installer - Add PostgreSQL configuration options - Generate PostgreSQL YAML when selected - Verify PostgreSQL pod readiness - Update documentation for PostgreSQL usage - Add PostgreSQL service and pod definitions --- examples/installers/k8s/gitea.vsh | 14 ++++- lib/installers/k8s/gitea/README.md | 40 ++++++++++++- lib/installers/k8s/gitea/gitea_actions.v | 56 ++++++++++++++++++- lib/installers/k8s/gitea/gitea_model.v | 32 ++++++++++- lib/installers/k8s/gitea/templates/gitea.yaml | 12 ++++ .../k8s/gitea/templates/postgres.yaml | 56 +++++++++++++++++++ 6 files changed, 203 insertions(+), 7 deletions(-) create mode 100644 lib/installers/k8s/gitea/templates/postgres.yaml diff --git a/examples/installers/k8s/gitea.vsh b/examples/installers/k8s/gitea.vsh index 3acb3d08..2ce6518f 100755 --- a/examples/installers/k8s/gitea.vsh +++ b/examples/installers/k8s/gitea.vsh @@ -19,9 +19,19 @@ mut installer := gitea.get( // // Gitea server configuration // installer.http_port = 3000 // Default: 3000 // installer.disable_registration = false // Default: false (allow new user registration) -// installer.db_type = 'sqlite3' // Default: 'sqlite3' (can be 'postgres', 'mysql') + +// // Database configuration - Option 1: SQLite (default) +// installer.db_type = 'sqlite3' // Default: 'sqlite3' // installer.db_path = '/data/gitea/gitea.db' // Default: '/data/gitea/gitea.db' -// installer.storage_size = '5Gi' // Default: '5Gi' (PVC storage size) + +// // Database configuration - Option 2: PostgreSQL +// // When using postgres, a PostgreSQL pod will be automatically deployed +installer.db_type = 'postgres' // Use PostgreSQL instead of SQLite +installer.db_host = 'postgres' // Default: 'postgres' (PostgreSQL service name) +installer.db_name = 'gitea' // Default: 'gitea' (database name) +installer.db_user = 'gitea' // Default: 'gitea' (database user) +installer.db_password = 'gitea' // Default: 'gitea' (database password) +installer.storage_size = '5Gi' // Default: '5Gi' (PVC storage size) // 3. Install Gitea. // This will generate the necessary Kubernetes YAML files and apply them to your cluster. diff --git a/lib/installers/k8s/gitea/README.md b/lib/installers/k8s/gitea/README.md index 9b3bc435..d42575c7 100644 --- a/lib/installers/k8s/gitea/README.md +++ b/lib/installers/k8s/gitea/README.md @@ -43,8 +43,14 @@ installer.namespace = 'forge' // Default: '${installer.name}-gitea-namespa installer.http_port = 3000 // Default: 3000 // Database configuration -installer.db_type = 'sqlite3' // Default: 'sqlite3' (options: 'sqlite3', 'postgres', 'mysql') -installer.db_path = '/data/gitea/gitea.db' // Default: '/data/gitea/gitea.db' +installer.db_type = 'sqlite3' // Default: 'sqlite3' (options: 'sqlite3', 'postgres') +installer.db_path = '/data/gitea/gitea.db' // Default: '/data/gitea/gitea.db' (for sqlite3) + +// PostgreSQL configuration (only used when db_type = 'postgres') +installer.db_host = 'postgres' // Default: 'postgres' (PostgreSQL service name) +installer.db_name = 'gitea' // Default: 'gitea' (PostgreSQL database name) +installer.db_user = 'gitea' // Default: 'gitea' (PostgreSQL user) +installer.db_password = 'gitea' // Default: 'gitea' (PostgreSQL password) // Registration installer.disable_registration = false // Default: false (allow new user registration) @@ -53,6 +59,8 @@ installer.disable_registration = false // Default: false (allow new user regist installer.storage_size = '5Gi' // Default: '5Gi' (PVC storage size) ``` +**Note**: When using `db_type = 'postgres'`, a PostgreSQL pod will be automatically deployed in the same namespace. The installer only supports `sqlite3` and `postgres` database types. + ## Full Example ```v @@ -79,6 +87,34 @@ installer.install()! println('Gitea: https://${installer.hostname}.gent01.grid.tf') ``` +## PostgreSQL Example + +To use PostgreSQL instead of SQLite: + +```v +import incubaid.herolib.installers.k8s.gitea + +mut installer := gitea.get( + name: 'mygitea' + create: true +)! + +// Configure to use PostgreSQL +installer.db_type = 'postgres' // Use PostgreSQL +installer.storage_size = '10Gi' // Storage for both Gitea and PostgreSQL + +// Optional: customize PostgreSQL settings +installer.db_host = 'postgres' // PostgreSQL service name +installer.db_name = 'gitea' // Database name +installer.db_user = 'gitea' // Database user +installer.db_password = 'securepassword' // Database password + +// Install (PostgreSQL pod will be deployed automatically) +installer.install()! + +println('Gitea with PostgreSQL: https://${installer.hostname}.gent01.grid.tf') +``` + ## Management ### Check Installation Status diff --git a/lib/installers/k8s/gitea/gitea_actions.v b/lib/installers/k8s/gitea/gitea_actions.v index a905094d..d2a1188b 100644 --- a/lib/installers/k8s/gitea/gitea_actions.v +++ b/lib/installers/k8s/gitea/gitea_actions.v @@ -69,7 +69,20 @@ fn install() ! { // 3. Verify TFGW deployment verify_tfgw_deployment(tfgw_name: 'gitea', namespace: installer.namespace)! - // 4. Apply Gitea App YAML + // 4. Apply PostgreSQL YAML if postgres is selected + if installer.db_type == 'postgres' { + console.print_info('Applying PostgreSQL YAML file to the cluster...') + res_postgres := k8s.apply_yaml('/tmp/gitea/postgres.yaml')! + if !res_postgres.success { + return error('Failed to apply postgres.yaml: ${res_postgres.stderr}') + } + console.print_info('PostgreSQL YAML file applied successfully.') + + // Verify PostgreSQL pod is ready + verify_postgres_pod(namespace: installer.namespace)! + } + + // 5. Apply Gitea App YAML console.print_info('Applying Gitea App YAML file to the cluster...') res2 := k8s.apply_yaml('/tmp/gitea/gitea.yaml')! if !res2.success { @@ -77,7 +90,7 @@ fn install() ! { } console.print_info('Gitea App YAML file applied successfully.') - // 5. Verify deployment status + // 6. Verify deployment status console.print_info('Verifying deployment status...') mut is_running := false for i in 0 .. max_deployment_retries { @@ -105,6 +118,45 @@ pub mut: namespace string // namespace name for gitea deployments/services } +// params for verifying postgres pod is ready +@[params] +struct VerifyPostgresPod { +pub mut: + namespace string // namespace name for postgres pod +} + +// Function for verifying postgres pod is ready +fn verify_postgres_pod(args VerifyPostgresPod) ! { + console.print_info('Verifying PostgreSQL pod is ready...') + installer := get()! + mut k8s := installer.kube_client + mut is_ready := false + + for i in 0 .. max_deployment_retries { + // Check if postgres pod exists and is running + result := k8s.kubectl_exec( + command: 'get pod ${installer.db_host} -n ${args.namespace} -o jsonpath="{.status.phase}"' + ) or { + console.print_info('Waiting for PostgreSQL pod to be created... (${i + 1}/${max_deployment_retries})') + time.sleep(deployment_check_interval_seconds * time.second) + continue + } + + if result.success && result.stdout == 'Running' { + is_ready = true + break + } + console.print_info('Waiting for PostgreSQL pod to be ready... (${i + 1}/${max_deployment_retries})') + time.sleep(deployment_check_interval_seconds * time.second) + } + + if !is_ready { + console.print_stderr('PostgreSQL pod failed to become ready.') + return error('PostgreSQL pod failed to become ready.') + } + console.print_info('PostgreSQL pod is ready.') +} + // 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}...') diff --git a/lib/installers/k8s/gitea/gitea_model.v b/lib/installers/k8s/gitea/gitea_model.v index 5e1df788..54eb36b6 100644 --- a/lib/installers/k8s/gitea/gitea_model.v +++ b/lib/installers/k8s/gitea/gitea_model.v @@ -22,6 +22,11 @@ pub mut: db_type string // Database type (sqlite3, postgres, mysql) db_path string // Database path for SQLite storage_size string // PVC storage size + // Postgres-specific settings + db_host string // Database host (for postgres) + db_name string // Database name (for postgres) + db_user string // Database user (for postgres) + db_password string // Database password (for postgres) } @[heap] @@ -38,9 +43,15 @@ pub mut: db_type string = 'sqlite3' db_path string = '/data/gitea/gitea.db' storage_size string = '5Gi' + // PostgreSQL configuration (only used when db_type = 'postgres') + db_host string = 'postgres' // PostgreSQL host (service name) + db_name string = 'gitea' // PostgreSQL database name + db_user string = 'gitea' // PostgreSQL user + db_password string = 'gitea' // PostgreSQL password // Internal paths gitea_app_path string = '/tmp/gitea/gitea.yaml' tfgw_path string = '/tmp/gitea/tfgw-gitea.yaml' + postgres_path string = '/tmp/gitea/postgres.yaml' kube_client kubernetes.KubeClient @[skip] } @@ -58,7 +69,7 @@ fn obj_init(mycfg_ GiteaK8SInstaller) !GiteaK8SInstaller { mycfg.name = mycfg.name.replace('.', '') if mycfg.namespace == '' { - mycfg.namespace = '${mycfg.name}gitea-namespace' + mycfg.namespace = '${mycfg.name}-gitea-namespace' } if mycfg.namespace.contains('_') || mycfg.namespace.contains('.') { @@ -70,6 +81,12 @@ fn obj_init(mycfg_ GiteaK8SInstaller) !GiteaK8SInstaller { mycfg.hostname = '${mycfg.name}giteaapp' } + // Validate database type + if mycfg.db_type !in ['sqlite3', 'postgres'] { + console.print_stderr('Only sqlite3 and postgres databases are supported. Got: ${mycfg.db_type}') + return error('Unsupported database type: ${mycfg.db_type}. Only sqlite3 and postgres are supported.') + } + mycfg.kube_client = kubernetes.get(create: true)! mycfg.kube_client.config.namespace = mycfg.namespace return mycfg @@ -104,6 +121,11 @@ fn configure() ! { db_type: installer.db_type db_path: installer.db_path storage_size: installer.storage_size + // Postgres connection details (use full DNS name for service) + db_host: '${installer.db_host}.${installer.namespace}.svc.cluster.local' + db_name: installer.db_name + db_user: installer.db_user + db_password: installer.db_password } // Ensure the output directory exists @@ -123,6 +145,14 @@ fn configure() ! { mut gitea_app_path := pathlib.get_file(path: installer.gitea_app_path, create: true)! gitea_app_path.write(gitea_app_yaml)! + // Generate postgres YAML if postgres is selected + if installer.db_type == 'postgres' { + postgres_yaml := $tmpl('./templates/postgres.yaml') + mut postgres_path := pathlib.get_file(path: installer.postgres_path, create: true)! + postgres_path.write(postgres_yaml)! + console.print_info('PostgreSQL configuration file generated.') + } + console.print_info('Configuration files generated successfully.') } diff --git a/lib/installers/k8s/gitea/templates/gitea.yaml b/lib/installers/k8s/gitea/templates/gitea.yaml index f72276b2..89a4e748 100644 --- a/lib/installers/k8s/gitea/templates/gitea.yaml +++ b/lib/installers/k8s/gitea/templates/gitea.yaml @@ -47,8 +47,20 @@ spec: value: "false" - name: GITEA__database__DB_TYPE value: "@{config_values.db_type}" +@if config_values.db_type == 'sqlite3' - name: GITEA__database__PATH value: "@{config_values.db_path}" +@end +@if config_values.db_type == 'postgres' + - name: GITEA__database__HOST + value: "@{config_values.db_host}" + - name: GITEA__database__NAME + value: "@{config_values.db_name}" + - name: GITEA__database__USER + value: "@{config_values.db_user}" + - name: GITEA__database__PASSWD + value: "@{config_values.db_password}" +@end - name: GITEA__service__DISABLE_REGISTRATION value: "@{config_values.disable_registration}" volumeMounts: diff --git a/lib/installers/k8s/gitea/templates/postgres.yaml b/lib/installers/k8s/gitea/templates/postgres.yaml new file mode 100644 index 00000000..a8766610 --- /dev/null +++ b/lib/installers/k8s/gitea/templates/postgres.yaml @@ -0,0 +1,56 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: postgres-data + namespace: @{config_values.namespace} +spec: + accessModes: [ReadWriteOnce] + resources: + requests: + storage: @{config_values.storage_size} +--- +apiVersion: v1 +kind: Pod +metadata: + name: @{config_values.db_host} + namespace: @{config_values.namespace} + labels: + app: @{config_values.db_host} +spec: + containers: + - name: postgres + image: postgres:16-alpine + env: + - name: POSTGRES_DB + value: @{config_values.db_name} + - name: POSTGRES_USER + value: @{config_values.db_user} + - name: POSTGRES_PASSWORD + value: @{config_values.db_password} + - name: PGDATA + value: /var/lib/postgresql/data/pgdata + ports: + - containerPort: 5432 + name: postgres + volumeMounts: + - name: postgres-storage + mountPath: /var/lib/postgresql/data + volumes: + - name: postgres-storage + persistentVolumeClaim: + claimName: postgres-data +--- +apiVersion: v1 +kind: Service +metadata: + name: @{config_values.db_host} + namespace: @{config_values.namespace} +spec: + selector: + app: @{config_values.db_host} + ports: + - port: 5432 + targetPort: 5432 + name: postgres + type: ClusterIP +