Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
A
Adrian Duke
simulation: Switch externalAddress config parsing to jq
CI — 0 passed, 1 failed
z6Mki2ag...Tyuy failure
9 days ago ac6759b74b220d3d9ac70cc0a665715405eca338 History
heartwood simulation modules radicle-node templates config.cue
package templates

import (
	timoniv1 "timoni.sh/core/v1alpha1"
)

#NodeGroup: {
	role:       string
	replicas:   int | *1
	repository: string | *"quay.io/radicle_garden/radicle-node"
	pullPolicy: string | *"IfNotPresent"
	version:    string | *"latest"
	nodeIdSeed: string | *""

	storage: {
		className: string | *"local-path"
		size:      string | *"1Gi"
	}

	resources: {...}

	sidecars: {
		events: bool | *true
	}

	scripts: {
		init: string | *"""
			#!/bin/sh
			set -e

			KUBE_CONFIG_DIR=/tmp/config-source
			RAD_HOME=/home/radicle/.radicle
			RAD_CONFIG=${RAD_HOME}/config.json

			echo "[INIT] Hostname: $(hostname)"
			# --- STANDARD INIT LOGIC (User 11011) ---
			mkdir -p "${RAD_HOME}"

			if [ -f "${KUBE_CONFIG_DIR}/config.json" ]; then
			  cp "${KUBE_CONFIG_DIR}/config.json" "${RAD_CONFIG}"
			  echo "[INIT] Config copied successfully."
			else
			  echo "[INIT] ERROR: Source config not found."
			  exit 1
			fi
			"""
		start: string | *"""
			#!/bin/sh
			set -e

			RAD_HOME=/home/radicle/.radicle
			RAD_ALIAS=$(hostname)
			RAD_KEY=${RAD_HOME}/keys/radicle
			RAD_CONFIG=${RAD_HOME}/config.json

			# Configure the external address by prepending the pod's hostname.
			# We only do this for seeds and bootstraps to ensure proper routing.
			configure_external_address() {
			  # Extract the first external address, stripping JSON formatting
			  EXT_ADDRESS=$(rad config | jq -r '.node.externalAddresses[0]')
			  
			  if [ -n "$EXT_ADDRESS" ]; then
			    # Check if it already starts with the pod's hostname to prevent stuttering
			    case "$EXT_ADDRESS" in
			      ${RAD_ALIAS}.*)
			        echo "[START] External address already correct: ${EXT_ADDRESS}"
			        ;;
			      *)
			        rad config remove node.externalAddresses "${EXT_ADDRESS}"
			        NEW_ADDRESS="${RAD_ALIAS}.${EXT_ADDRESS}"
			        rad config push node.externalAddresses "${NEW_ADDRESS}"
			        echo "[START] Node's external address updated to: ${NEW_ADDRESS}"
			        ;;
			    esac
			  fi
			}

			#
			# Generate keys
			#
			if [ ! -f "${RAD_KEY}" ]; then
			  echo "[START] Generating identity for: ${RAD_ALIAS}..."
			  # We move the config out of the way so 'rad auth' doesn't complain
			  if [ -f "${RAD_CONFIG}" ]; then
			     mv "${RAD_CONFIG}" "${RAD_CONFIG}.bak"
			  fi

			  # RAD_KEYGEN_SEED requires a 32-byte hex string.
			  # We hash either the injected NODE_ID_SEED or the hostname to generate it.
			  if [ -n "${NODE_ID_SEED}" ]; then
			    export RAD_KEYGEN_SEED=$(echo "${NODE_ID_SEED}" | sha256sum | tr -d "\\n *-")
			  else
			    export RAD_KEYGEN_SEED=$(hostname | sha256sum | tr -d "\\n *-")
			  fi

			  rad auth --alias "${RAD_ALIAS}"

			  if [ -f "${RAD_CONFIG}.bak" ]; then
			     mv "${RAD_CONFIG}.bak" "${RAD_CONFIG}"
			  fi
			  echo "[START] Identity generated"
			fi

			#
			# Update config settings
			#
			echo "[START] Node's alias set to: $(rad config set node.alias "${RAD_ALIAS}")"

			if [ "\(role)" = "seed" ] || [ "\(role)" = "bootstrap" ]; then
			  configure_external_address
			fi

			#
			# Start node
			#
			echo "[START] Starting Radicle node..."
			exec rad node start --foreground
			"""
		events: string | *"""
			#!/bin/sh

			RAD_HOME=/home/radicle/.radicle
			RAD_NODE_SOCKET=${RAD_HOME}/node/control.sock

			echo "[EVENTS] Waiting for node socket..."
			while [ ! -S ${RAD_NODE_SOCKET} ]; do
			  sleep 1
			done
			echo "[EVENTS] Socket found. Streaming events..."
			exec rad node events
			"""
	}

	radicleConfig: {
		node: {
			// Automatically generate the base external address using the role.
			// The start.sh script will dynamically prepend the pod's hostname to this at boot.
			externalAddresses: [...string] | *["\(role).default.svc.cluster.local:8776"]
			// Set network to "test" by default.
			network: "test"
			...
		}
		...
	}
}

// Config defines the schema and defaults for the Instance values.
#Config: {
	kubeVersion!: string
	clusterVersion: timoniv1.#SemVer & {#Version: kubeVersion, #Minimum: "1.20.0"}
	moduleVersion!: string
	metadata: timoniv1.#Metadata & {#Version: moduleVersion}
	metadata: labels: timoniv1.#Labels
	metadata: annotations?: timoniv1.#Annotations

	// The topology map is merged with the #NodeGroup schema
	topology: [string]: #NodeGroup

	// Helper to generate metadata with a specific name
	#Meta: {
		name: string
		out: {
			"name": name
			namespace: metadata.namespace
			if metadata.annotations != _|_ {
				annotations: metadata.annotations
			}
		}
	}
}

// Instance takes the config values and outputs the Kubernetes objects.
#Instance: {
	config: #Config

	// Extract unique roles to create headless services
	let _roles = {
		for name, group in config.topology {
			"\(group.role)": true
		}
	}

	objects: {
		// Generate one Headless Service per role (e.g. seed, peer, bootstrap)
		for roleName, _ in _roles {
			"svc-\(roleName)": #Service & {
				#config: config
				#role:   roleName
			}
		}

		// Generate a StatefulSet and ConfigMap for each group in the topology
		for name, group in config.topology {
			"cm-\(name)": #ConfigMap & {
				#config: config
				#name:   name
				#group:  group
			}
			"sts-\(name)": #StatefulSet & {
				#config: config
				#name:   name
				#group:  group
				#cmName: name + "-config"
			}
		}
	}
}