From cef60cc394eb87bfbbccac9df3afe9d1d226a198 Mon Sep 17 00:00:00 2001 From: Roderik van der Veer Date: Wed, 17 Sep 2025 09:36:09 +0200 Subject: [PATCH 01/16] feat: add a chart for the network mapper --- .github/workflows/qa.yml | 4 + biome.jsonc | 6 + bun.lock | 3 + charts/network-bootstrapper/.helmignore | 23 + charts/network-bootstrapper/Chart.yaml | 6 + .../network-bootstrapper/templates/NOTES.txt | 35 ++ .../templates/_helpers.tpl | 62 +++ .../templates/deployment.yaml | 78 +++ .../network-bootstrapper/templates/hpa.yaml | 32 ++ .../templates/httproute.yaml | 38 ++ .../templates/ingress.yaml | 43 ++ .../templates/service.yaml | 15 + .../templates/serviceaccount.yaml | 13 + .../templates/tests/test-connection.yaml | 15 + charts/network-bootstrapper/values.yaml | 161 +++++++ package.json | 3 +- tools/version.ts | 444 ++++++++++++++++++ 17 files changed, 980 insertions(+), 1 deletion(-) create mode 100644 charts/network-bootstrapper/.helmignore create mode 100644 charts/network-bootstrapper/Chart.yaml create mode 100644 charts/network-bootstrapper/templates/NOTES.txt create mode 100644 charts/network-bootstrapper/templates/_helpers.tpl create mode 100644 charts/network-bootstrapper/templates/deployment.yaml create mode 100644 charts/network-bootstrapper/templates/hpa.yaml create mode 100644 charts/network-bootstrapper/templates/httproute.yaml create mode 100644 charts/network-bootstrapper/templates/ingress.yaml create mode 100644 charts/network-bootstrapper/templates/service.yaml create mode 100644 charts/network-bootstrapper/templates/serviceaccount.yaml create mode 100644 charts/network-bootstrapper/templates/tests/test-connection.yaml create mode 100644 charts/network-bootstrapper/values.yaml create mode 100644 tools/version.ts diff --git a/.github/workflows/qa.yml b/.github/workflows/qa.yml index d2d55e5..4b08deb 100644 --- a/.github/workflows/qa.yml +++ b/.github/workflows/qa.yml @@ -120,6 +120,10 @@ jobs: if: github.event_name == 'pull_request' || github.event_name == 'push' run: bun typecheck + - name: Set version + if: github.event_name == 'pull_request' || github.event_name == 'push' + run: bun run tools/version.ts + - name: Docker meta if: github.event_name == 'pull_request' || github.event_name == 'push' id: meta diff --git a/biome.jsonc b/biome.jsonc index cbd6fa0..1e2f179 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -5,6 +5,12 @@ "rules": { "suspicious": { "noConsole": "off" + }, + "complexity": { + "noExcessiveCognitiveComplexity": "off" + }, + "nursery": { + "useMaxParams": "off" } } }, diff --git a/bun.lock b/bun.lock index 4e28c8c..470237f 100644 --- a/bun.lock +++ b/bun.lock @@ -9,6 +9,7 @@ "commander": "14.0.1", "ox": "0.9.6", "viem": "2.37.6", + "yaml": "^2.8.1", "zod": "4.1.9", }, "devDependencies": { @@ -490,6 +491,8 @@ "ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="], + "yaml": ["yaml@2.8.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw=="], + "yoctocolors-cjs": ["yoctocolors-cjs@2.1.3", "", {}, "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw=="], "zod": ["zod@4.1.9", "", {}, "sha512-HI32jTq0AUAC125z30E8bQNz0RQ+9Uc+4J7V97gLYjZVKRjeydPgGt6dvQzFrav7MYOUGFqqOGiHpA/fdbd0cQ=="], diff --git a/charts/network-bootstrapper/.helmignore b/charts/network-bootstrapper/.helmignore new file mode 100644 index 0000000..0e8a0eb --- /dev/null +++ b/charts/network-bootstrapper/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/charts/network-bootstrapper/Chart.yaml b/charts/network-bootstrapper/Chart.yaml new file mode 100644 index 0000000..b23f6a9 --- /dev/null +++ b/charts/network-bootstrapper/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +name: network-bootstrapper +description: A Helm chart for Kubernetes +type: application +version: 0.1.0 +appVersion: "0.1.0" diff --git a/charts/network-bootstrapper/templates/NOTES.txt b/charts/network-bootstrapper/templates/NOTES.txt new file mode 100644 index 0000000..5eed516 --- /dev/null +++ b/charts/network-bootstrapper/templates/NOTES.txt @@ -0,0 +1,35 @@ +1. Get the application URL by running these commands: +{{- if .Values.httpRoute.enabled }} +{{- if .Values.httpRoute.hostnames }} + export APP_HOSTNAME={{ .Values.httpRoute.hostnames | first }} +{{- else }} + export APP_HOSTNAME=$(kubectl get --namespace {{(first .Values.httpRoute.parentRefs).namespace | default .Release.Namespace }} gateway/{{ (first .Values.httpRoute.parentRefs).name }} -o jsonpath="{.spec.listeners[0].hostname}") + {{- end }} +{{- if and .Values.httpRoute.rules (first .Values.httpRoute.rules).matches (first (first .Values.httpRoute.rules).matches).path.value }} + echo "Visit http://$APP_HOSTNAME{{ (first (first .Values.httpRoute.rules).matches).path.value }} to use your application" + + NOTE: Your HTTPRoute depends on the listener configuration of your gateway and your HTTPRoute rules. + The rules can be set for path, method, header and query parameters. + You can check the gateway configuration with 'kubectl get --namespace {{(first .Values.httpRoute.parentRefs).namespace | default .Release.Namespace }} gateway/{{ (first .Values.httpRoute.parentRefs).name }} -o yaml' +{{- end }} +{{- else if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "network-bootstrapper.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch its status by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "network-bootstrapper.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "network-bootstrapper.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "network-bootstrapper.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} diff --git a/charts/network-bootstrapper/templates/_helpers.tpl b/charts/network-bootstrapper/templates/_helpers.tpl new file mode 100644 index 0000000..c65af42 --- /dev/null +++ b/charts/network-bootstrapper/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "network-bootstrapper.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "network-bootstrapper.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "network-bootstrapper.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "network-bootstrapper.labels" -}} +helm.sh/chart: {{ include "network-bootstrapper.chart" . }} +{{ include "network-bootstrapper.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "network-bootstrapper.selectorLabels" -}} +app.kubernetes.io/name: {{ include "network-bootstrapper.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "network-bootstrapper.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "network-bootstrapper.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/charts/network-bootstrapper/templates/deployment.yaml b/charts/network-bootstrapper/templates/deployment.yaml new file mode 100644 index 0000000..19bae7a --- /dev/null +++ b/charts/network-bootstrapper/templates/deployment.yaml @@ -0,0 +1,78 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "network-bootstrapper.fullname" . }} + labels: + {{- include "network-bootstrapper.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "network-bootstrapper.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "network-bootstrapper.labels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "network-bootstrapper.serviceAccountName" . }} + {{- with .Values.podSecurityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: {{ .Chart.Name }} + {{- with .Values.securityContext }} + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: {{ .Values.service.port }} + protocol: TCP + {{- with .Values.livenessProbe }} + livenessProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.readinessProbe }} + readinessProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.volumeMounts }} + volumeMounts: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.volumes }} + volumes: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/charts/network-bootstrapper/templates/hpa.yaml b/charts/network-bootstrapper/templates/hpa.yaml new file mode 100644 index 0000000..67f44d1 --- /dev/null +++ b/charts/network-bootstrapper/templates/hpa.yaml @@ -0,0 +1,32 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "network-bootstrapper.fullname" . }} + labels: + {{- include "network-bootstrapper.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "network-bootstrapper.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/charts/network-bootstrapper/templates/httproute.yaml b/charts/network-bootstrapper/templates/httproute.yaml new file mode 100644 index 0000000..d0226c4 --- /dev/null +++ b/charts/network-bootstrapper/templates/httproute.yaml @@ -0,0 +1,38 @@ +{{- if .Values.httpRoute.enabled -}} +{{- $fullName := include "network-bootstrapper.fullname" . -}} +{{- $svcPort := .Values.service.port -}} +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: {{ $fullName }} + labels: + {{- include "network-bootstrapper.labels" . | nindent 4 }} + {{- with .Values.httpRoute.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + parentRefs: + {{- with .Values.httpRoute.parentRefs }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.httpRoute.hostnames }} + hostnames: + {{- toYaml . | nindent 4 }} + {{- end }} + rules: + {{- range .Values.httpRoute.rules }} + {{- with .matches }} + - matches: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .filters }} + filters: + {{- toYaml . | nindent 8 }} + {{- end }} + backendRefs: + - name: {{ $fullName }} + port: {{ $svcPort }} + weight: 1 + {{- end }} +{{- end }} diff --git a/charts/network-bootstrapper/templates/ingress.yaml b/charts/network-bootstrapper/templates/ingress.yaml new file mode 100644 index 0000000..a38abf1 --- /dev/null +++ b/charts/network-bootstrapper/templates/ingress.yaml @@ -0,0 +1,43 @@ +{{- if .Values.ingress.enabled -}} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "network-bootstrapper.fullname" . }} + labels: + {{- include "network-bootstrapper.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- with .Values.ingress.className }} + ingressClassName: {{ . }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- with .pathType }} + pathType: {{ . }} + {{- end }} + backend: + service: + name: {{ include "network-bootstrapper.fullname" $ }} + port: + number: {{ $.Values.service.port }} + {{- end }} + {{- end }} +{{- end }} diff --git a/charts/network-bootstrapper/templates/service.yaml b/charts/network-bootstrapper/templates/service.yaml new file mode 100644 index 0000000..8932949 --- /dev/null +++ b/charts/network-bootstrapper/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "network-bootstrapper.fullname" . }} + labels: + {{- include "network-bootstrapper.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "network-bootstrapper.selectorLabels" . | nindent 4 }} diff --git a/charts/network-bootstrapper/templates/serviceaccount.yaml b/charts/network-bootstrapper/templates/serviceaccount.yaml new file mode 100644 index 0000000..46da387 --- /dev/null +++ b/charts/network-bootstrapper/templates/serviceaccount.yaml @@ -0,0 +1,13 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "network-bootstrapper.serviceAccountName" . }} + labels: + {{- include "network-bootstrapper.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.serviceAccount.automount }} +{{- end }} diff --git a/charts/network-bootstrapper/templates/tests/test-connection.yaml b/charts/network-bootstrapper/templates/tests/test-connection.yaml new file mode 100644 index 0000000..cb29e2e --- /dev/null +++ b/charts/network-bootstrapper/templates/tests/test-connection.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "network-bootstrapper.fullname" . }}-test-connection" + labels: + {{- include "network-bootstrapper.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['{{ include "network-bootstrapper.fullname" . }}:{{ .Values.service.port }}'] + restartPolicy: Never diff --git a/charts/network-bootstrapper/values.yaml b/charts/network-bootstrapper/values.yaml new file mode 100644 index 0000000..cbc2f7f --- /dev/null +++ b/charts/network-bootstrapper/values.yaml @@ -0,0 +1,161 @@ +# Default values for network-bootstrapper. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +# This will set the replicaset count more information can be found here: https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/ +replicaCount: 1 + +# This sets the container image more information can be found here: https://kubernetes.io/docs/concepts/containers/images/ +image: + repository: nginx + # This sets the pull policy for images. + pullPolicy: IfNotPresent + # Overrides the image tag whose default is the chart appVersion. + tag: "" + +# This is for the secrets for pulling an image from a private repository more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ +imagePullSecrets: [] +# This is to override the chart name. +nameOverride: "" +fullnameOverride: "" + +# This section builds out the service account more information can be found here: https://kubernetes.io/docs/concepts/security/service-accounts/ +serviceAccount: + # Specifies whether a service account should be created + create: true + # Automatically mount a ServiceAccount's API credentials? + automount: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +# This is for setting Kubernetes Annotations to a Pod. +# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ +podAnnotations: {} +# This is for setting Kubernetes Labels to a Pod. +# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ +podLabels: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +# This is for setting up a service more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/ +service: + # This sets the service type more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types + type: ClusterIP + # This sets the ports more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#field-spec-ports + port: 80 + +# This block is for setting up the ingress for more information can be found here: https://kubernetes.io/docs/concepts/services-networking/ingress/ +ingress: + enabled: false + className: "" + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: chart-example.local + paths: + - path: / + pathType: ImplementationSpecific + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +# -- Expose the service via gateway-api HTTPRoute +# Requires Gateway API resources and suitable controller installed within the cluster +# (see: https://gateway-api.sigs.k8s.io/guides/) +httpRoute: + # HTTPRoute enabled. + enabled: false + # HTTPRoute annotations. + annotations: {} + # Which Gateways this Route is attached to. + parentRefs: + - name: gateway + sectionName: http + # namespace: default + # Hostnames matching HTTP header. + hostnames: + - chart-example.local + # List of rules and filters applied. + rules: + - matches: + - path: + type: PathPrefix + value: /headers + # filters: + # - type: RequestHeaderModifier + # requestHeaderModifier: + # set: + # - name: My-Overwrite-Header + # value: this-is-the-only-value + # remove: + # - User-Agent + # - matches: + # - path: + # type: PathPrefix + # value: /echo + # headers: + # - name: version + # value: v2 + +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +# This is to setup the liveness and readiness probes more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/ +livenessProbe: + httpGet: + path: / + port: http +readinessProbe: + httpGet: + path: / + port: http + +# This section is for setting up autoscaling more information can be found here: https://kubernetes.io/docs/concepts/workloads/autoscaling/ +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +# Additional volumes on the output Deployment definition. +volumes: [] +# - name: foo +# secret: +# secretName: mysecret +# optional: false + +# Additional volumeMounts on the output Deployment definition. +volumeMounts: [] +# - name: foo +# mountPath: "/etc/foo" +# readOnly: true + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/package.json b/package.json index c0bc41a..6d8d9d5 100644 --- a/package.json +++ b/package.json @@ -33,11 +33,12 @@ "typescript": "^5" }, "dependencies": { - "@kubernetes/client-node": "1.3.0", "@inquirer/prompts": "7.8.6", + "@kubernetes/client-node": "1.3.0", "commander": "14.0.1", "ox": "0.9.6", "viem": "2.37.6", + "yaml": "^2.8.1", "zod": "4.1.9" } } diff --git a/tools/version.ts b/tools/version.ts new file mode 100644 index 0000000..2a4dc33 --- /dev/null +++ b/tools/version.ts @@ -0,0 +1,444 @@ +#!/usr/bin/env bun + +import { relative } from "node:path"; +import { Glob } from "bun"; +import { parse, stringify } from "yaml"; + +interface VersionInfo { + tag: "latest" | "main" | "pr"; + version: string; +} + +interface VersionParams { + refSlug?: string; + refName?: string; + shaShort?: string; + buildId?: string; + startPath?: string; +} + +interface PackageJson { + version: string; + [key: string]: unknown; +} + +interface ChartYaml { + version: string; + appVersion: string; + dependencies?: Array<{ + name: string; + version: string; + repository?: string; + [key: string]: unknown; + }>; + [key: string]: unknown; +} + +/** + * Reads and parses the root package.json file + * @param startPath - Starting path for finding the monorepo root + * @returns The parsed package.json content + */ +async function readRootPackageJson(startPath?: string): Promise { + const packageJsonFile = Bun.file("package.json"); + + if (!(await packageJsonFile.exists())) { + throw new Error("Package.json not found at package.json"); + } + + const packageJson = (await packageJsonFile.json()) as PackageJson; + + if (!packageJson.version) { + throw new Error("No version found in package.json"); + } + + return packageJson; +} + +/** + * Generates version string based on Git ref information and base version + * @param refSlug - Git ref slug + * @param refName - Git ref name + * @param shaShort - Short SHA + * @param baseVersion - Base version from package.json + * @returns Object containing version and tag + */ +function generateVersionInfo( + refSlug: string, + refName: string, + shaShort: string, + baseVersion: string, + buildId?: string +): VersionInfo { + // Check if ref slug matches version pattern (v?[0-9]+\.[0-9]+\.[0-9]+$) + const versionPattern = /^v?[0-9]+\.[0-9]+\.[0-9]+$/; + + if (versionPattern.test(refSlug)) { + // Remove 'v' prefix if present + const version = refSlug.replace(/^v/, ""); + return { + tag: "latest", + version, + }; + } + + if (refName === "main") { + // Prefer numeric/strict BUILD_ID for better Renovate sorting + // Fallback to short SHA, and finally a timestamp to ensure uniqueness + const sanitize = (s: string) => s.replace(/[^0-9A-Za-z-]/g, ""); + const id = + sanitize(buildId || "") || sanitize(shaShort || "") || `${Date.now()}`; + // Use SemVer pre-release with dot-separated identifiers: -main. + const version = `${baseVersion}-main.${id}`; + return { + tag: "main", + version, + }; + } + + // Default case (PR or other branches) + const version = `${baseVersion}-pr${shaShort.replace(/^v/, "")}`; + return { + tag: "pr", + version, + }; +} + +/** + * Gets version and tag information based on Git ref information + * @param params - Configuration object with Git ref information + * @returns Object containing version and tag + */ +export async function getVersionInfo( + params: VersionParams = {} +): Promise { + const { + refSlug = process.env.GITHUB_REF_SLUG || "", + refName = process.env.GITHUB_REF_NAME || "", + shaShort = process.env.GITHUB_SHA_SHORT || "", + buildId = process.env.BUILD_ID || "", + startPath, + } = params; + + const packageJson = await readRootPackageJson(startPath); + + return generateVersionInfo( + refSlug, + refName, + shaShort, + packageJson.version, + buildId + ); +} + +/** + * Gets version info and logs the result (useful for CI/CD) + * @param params - Configuration object with Git ref information + * @returns Object containing version and tag with console output + */ +export async function getVersionInfoWithLogging( + params: VersionParams = {} +): Promise { + const result = await getVersionInfo(params); + + console.log(`TAG=${result.tag}`); + console.log(`VERSION=${result.version}`); + + return result; +} + +/** + * Updates workspace dependencies in a dependencies object + * @param deps - Dependencies object to update + * @param depType - Type of dependencies (for logging) + * @param newVersion - New version to use + * @returns Number of workspace dependencies updated + */ +function updateWorkspaceDependencies( + deps: Record | undefined, + depType: string, + newVersion: string +): number { + if (!deps) return 0; + + let workspaceCount = 0; + for (const [depName, depVersion] of Object.entries(deps)) { + // Skip @atk/* packages - not published to npm + if (depVersion === "workspace:*" && !depName.startsWith("@atk/")) { + deps[depName] = newVersion; + workspaceCount++; + } + } + + if (workspaceCount > 0) { + console.log( + ` Updated ${workspaceCount} workspace:* references in ${depType}` + ); + } + + return workspaceCount; +} + +/** + * Updates chart dependencies with version "*" + * @param dependencies - Chart dependencies array to update + * @param newVersion - New version to use + * @returns Number of chart dependencies updated + */ +function updateChartDependencies( + dependencies: + | Array<{ name: string; version: string; [key: string]: unknown }> + | undefined, + newVersion: string +): number { + if (!dependencies) return 0; + + let dependencyCount = 0; + for (const dep of dependencies) { + if (dep.version === "*") { + dep.version = newVersion; + dependencyCount++; + } + } + + if (dependencyCount > 0) { + console.log( + ` Updated ${dependencyCount} "*" version references in chart dependencies` + ); + } + + return dependencyCount; +} + +/** + * Updates all package.json files in the workspace with the new version using glob pattern + * Also replaces "workspace:*" references with the actual version + * @param startPath - Starting path for finding package.json files (defaults to current working directory) + * @returns Promise that resolves when all updates are complete + */ +export async function updatePackageVersion(startPath?: string): Promise { + try { + // Get the current version info + const versionInfo = await getVersionInfo({ startPath }); + const newVersion = versionInfo.version; + + console.log(`Updating all package.json files to version: ${newVersion}`); + + // Find all package.json files in the workspace, excluding node_modules + const glob = new Glob("**/package.json"); + const packageFiles: string[] = []; + + for await (const file of glob.scan(startPath || ".")) { + // Skip files in node_modules and kit/contracts/dependencies directories + if ( + file.includes("node_modules/") || + file.includes("kit/contracts/dependencies/") + ) { + continue; + } + packageFiles.push(file); + } + + if (packageFiles.length === 0) { + console.warn("No package.json files found"); + return; + } + + console.log(`Found ${packageFiles.length} package.json files:`); + + let updatedCount = 0; + + for (const packagePath of packageFiles) { + try { + console.log(` Processing: ${packagePath}`); + + // Read the current package.json file + const packageJsonFile = Bun.file(packagePath); + if (!(await packageJsonFile.exists())) { + console.warn(" Skipping: File does not exist"); + continue; + } + + const packageJson = (await packageJsonFile.json()) as PackageJson; + + if (!packageJson.version) { + console.warn(" Skipping: No version field found"); + continue; + } + + const oldVersion = packageJson.version; + let hasChanges = false; + + // Update the main version + packageJson.version = newVersion; + hasChanges = true; + + // Update workspace dependencies in all dependency types + const workspaceUpdates = [ + updateWorkspaceDependencies( + packageJson.dependencies as Record, + "dependencies", + newVersion + ), + updateWorkspaceDependencies( + packageJson.devDependencies as Record, + "devDependencies", + newVersion + ), + updateWorkspaceDependencies( + packageJson.peerDependencies as Record, + "peerDependencies", + newVersion + ), + updateWorkspaceDependencies( + packageJson.optionalDependencies as Record, + "optionalDependencies", + newVersion + ), + ]; + + const totalWorkspaceUpdates = workspaceUpdates.reduce( + (sum, count) => sum + count, + 0 + ); + + if (hasChanges) { + // Write the updated package.json back to disk + await Bun.write( + packagePath, + JSON.stringify(packageJson, null, 2) + "\n" + ); + + console.log(` Updated version: ${oldVersion} -> ${newVersion}`); + if (totalWorkspaceUpdates > 0) { + console.log( + ` Updated ${totalWorkspaceUpdates} total workspace:* references` + ); + } + updatedCount++; + } else { + console.log(" No changes needed"); + } + } catch (err) { + console.error(` Error processing ${packagePath}:`, err); + } + } + + console.log(`\nSuccessfully updated ${updatedCount} package.json files`); + } catch (err) { + console.error("Failed to update package versions:", err); + process.exit(1); + } +} + +/** + * Updates all Chart.yaml files in the ATK directory with the current version + */ +async function updateChartVersions(): Promise { + try { + // Get the current version info + const versionInfo = await getVersionInfo(); + const newVersion = versionInfo.version; + + console.log(`Updating charts to version: ${newVersion}`); + + // Find all Chart.yaml files in the ATK directory + const glob = new Glob("charts/**/Chart.yaml"); + const chartFiles: string[] = []; + + for await (const file of glob.scan(".")) { + chartFiles.push(file); + } + + if (chartFiles.length === 0) { + console.warn("No Chart.yaml files found in charts/"); + return; + } + + console.log(`Found ${chartFiles.length} Chart.yaml files:`); + + let updatedCount = 0; + + for (const chartPath of chartFiles) { + try { + const relativePath = relative(process.cwd(), chartPath); + console.log(` Processing: ${relativePath}`); + + // Read the current Chart.yaml file + const file = Bun.file(chartPath); + if (!(await file.exists())) { + console.warn(" Skipping: File does not exist"); + continue; + } + + const content = await file.text(); + const chart = parse(content) as ChartYaml; + + // Check if version fields exist + if (!(chart.version || chart.appVersion)) { + console.warn(" Skipping: No version or appVersion fields found"); + continue; + } + + const oldVersion = chart.version; + const oldAppVersion = chart.appVersion; + let hasChanges = false; + + // Update the version fields + if (chart.version) { + chart.version = newVersion; + hasChanges = true; + } + if (chart.appVersion) { + chart.appVersion = newVersion; + hasChanges = true; + } + + // Update chart dependencies with version "*" + const dependencyUpdates = updateChartDependencies( + chart.dependencies, + newVersion + ); + + if (dependencyUpdates > 0) { + hasChanges = true; + } + + if (hasChanges) { + // Convert back to YAML and write + const updatedContent = stringify(chart); + await Bun.write(chartPath, updatedContent); + + console.log(` Updated version: ${oldVersion} -> ${newVersion}`); + if (oldAppVersion !== oldVersion) { + console.log( + ` Updated appVersion: ${oldAppVersion} -> ${newVersion}` + ); + } + updatedCount++; + } else { + console.log(" No changes needed"); + } + } catch (err) { + console.error(` Error processing ${chartPath}:`, err); + } + } + + console.log(`\nSuccessfully updated ${updatedCount} Chart.yaml files`); + } catch (err) { + console.error("Failed to update chart versions:", err); + process.exit(1); + } +} + +// Run the script if called directly +if (import.meta.main) { + // Check if running in CI environment + if (!process.env.CI) { + console.log("Set the CI environment variable to run this script."); + process.exit(0); + } + + await updateChartVersions(); + await updatePackageVersion(); +} From 0364ec5de87efc6ea0b322810d7fcb3950753495 Mon Sep 17 00:00:00 2001 From: Roderik van der Veer Date: Wed, 17 Sep 2025 09:48:27 +0200 Subject: [PATCH 02/16] chore(deps): update dependencies in package.json and bun.lock --- .github/workflows/qa.yml | 4 + README.md | 31 +++++-- README.tpl | 9 ++ bun.lock | 23 +++++ charts/network-bootstrapper/README.md | 49 +++++++++++ lefthook.yml | 11 +++ package.json | 8 +- tools/version.ts | 117 +++++++++++++++++--------- 8 files changed, 201 insertions(+), 51 deletions(-) create mode 100644 README.tpl create mode 100644 charts/network-bootstrapper/README.md create mode 100644 lefthook.yml diff --git a/.github/workflows/qa.yml b/.github/workflows/qa.yml index 4b08deb..4b78fa9 100644 --- a/.github/workflows/qa.yml +++ b/.github/workflows/qa.yml @@ -124,6 +124,10 @@ jobs: if: github.event_name == 'pull_request' || github.event_name == 'push' run: bun run tools/version.ts + - name: Run docs + if: github.event_name == 'pull_request' || github.event_name == 'push' + run: bun run docs:helm + - name: Docker meta if: github.event_name == 'pull_request' || github.event_name == 'push' id: meta diff --git a/README.md b/README.md index d73cfcf..9db703c 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,30 @@ # network-bootstrapper -To install dependencies: +Generate node identities, configure consensus, and emit a Besu genesis. -```bash -bun install -``` +## Helm chart + +The helm chart to run this on Kubernetes / OpenShift can be found [here](./charts/network-bootstrapper/README.md) -To run: +## CLI usage -```bash -bun run src/index.ts ``` +Usage: network-bootstrapper [options] -This project was created using `bun init` in bun v1.2.22. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime. +Generate node identities, configure consensus, and emit a Besu genesis. + +Options: + -v, --validators Number of validator nodes to generate. + -r, --rpc-nodes Number of RPC nodes to generate. + -a, --allocations Path to a genesis allocations JSON file. + -o, --outputType Output target (screen, file, kubernetes). + (default: "screen") + --consensus Consensus algorithm (IBFTv2, QBFT). + --chain-id Chain ID for the genesis config. + --seconds-per-block Block time in seconds. + --gas-limit Block gas limit in decimal form. + --gas-price Base gas price (wei). + --evm-stack-size EVM stack size limit. + --contract-size-limit Contract size limit in bytes. + -h, --help display help for command +``` diff --git a/README.tpl b/README.tpl new file mode 100644 index 0000000..50a9612 --- /dev/null +++ b/README.tpl @@ -0,0 +1,9 @@ +# network-bootstrapper + +Generate node identities, configure consensus, and emit a Besu genesis. + +## Helm chart + +The helm chart to run this on Kubernetes / OpenShift can be found [here](./charts/network-bootstrapper/README.md) + +## CLI usage diff --git a/bun.lock b/bun.lock index 470237f..6b7c4d3 100644 --- a/bun.lock +++ b/bun.lock @@ -7,6 +7,7 @@ "@inquirer/prompts": "7.8.6", "@kubernetes/client-node": "1.3.0", "commander": "14.0.1", + "lefthook": "^1.13.0", "ox": "0.9.6", "viem": "2.37.6", "yaml": "^2.8.1", @@ -367,6 +368,28 @@ "jsonpath-plus": ["jsonpath-plus@10.3.0", "", { "dependencies": { "@jsep-plugin/assignment": "^1.3.0", "@jsep-plugin/regex": "^1.0.4", "jsep": "^1.4.0" }, "bin": { "jsonpath": "bin/jsonpath-cli.js", "jsonpath-plus": "bin/jsonpath-cli.js" } }, "sha512-8TNmfeTCk2Le33A3vRRwtuworG/L5RrgMvdjhKZxvyShO+mBu2fP50OWUjRLNtvw344DdDarFh9buFAZs5ujeA=="], + "lefthook": ["lefthook@1.13.0", "", { "optionalDependencies": { "lefthook-darwin-arm64": "1.13.0", "lefthook-darwin-x64": "1.13.0", "lefthook-freebsd-arm64": "1.13.0", "lefthook-freebsd-x64": "1.13.0", "lefthook-linux-arm64": "1.13.0", "lefthook-linux-x64": "1.13.0", "lefthook-openbsd-arm64": "1.13.0", "lefthook-openbsd-x64": "1.13.0", "lefthook-windows-arm64": "1.13.0", "lefthook-windows-x64": "1.13.0" }, "bin": { "lefthook": "bin/index.js" } }, "sha512-6pno+NjfBrKKt3XQmFUvwDdKXzBVh5JvzAIwcCOu9mqg81nAMCZd2FtTuU1fmDzXFNdsxjW8mwwKB+S8t5ucOQ=="], + + "lefthook-darwin-arm64": ["lefthook-darwin-arm64@1.13.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-mhD4zOj2VRx34tptEc/lP643n5YAAVP95f/TiP6geQz4kpLwUrsTwQxzoXUIauU2DGSNbFtp9hVSE++0e4ESEA=="], + + "lefthook-darwin-x64": ["lefthook-darwin-x64@1.13.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-uspgWrhh9Xoyb+x0hVeMnYkSA1K/cEov4QHxcBBTIvTvjEuijSLIQEzULsHvg7a6xNM/8E3SBzOwBRK44jM2Mw=="], + + "lefthook-freebsd-arm64": ["lefthook-freebsd-arm64@1.13.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-UUY+UlGuwAkO8hEY4+SGYfM1OeXSI4i2/8ROwBpu6fz0LrTL1OUYRVhLIRNJvWrF2XabfgXVUrnjGY7YSq4zpg=="], + + "lefthook-freebsd-x64": ["lefthook-freebsd-x64@1.13.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-wdF/Cwmbiblz+UaLb3a0trSKEmaY5z20latrmhim98M1H48iBHhUyUUJWaSEauyFMJWPwu7rSVZl5KktPxCxVA=="], + + "lefthook-linux-arm64": ["lefthook-linux-arm64@1.13.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-tpg4pA0JTeLxGAZDFJVOGyIMjQAE7F8HcM31tj+3KOogahspOffpmSoS1SlHzUSZ8Jm+Bvoqcis/sW68HkmWHw=="], + + "lefthook-linux-x64": ["lefthook-linux-x64@1.13.0", "", { "os": "linux", "cpu": "x64" }, "sha512-5JUhlDaYqt9vBTSQ5gkA00+0ktUSRyL60AhZID6OR4ML39SidzMTu/GrgHscPT4sD3TfSODEdGZ28sNKdLg6jA=="], + + "lefthook-openbsd-arm64": ["lefthook-openbsd-arm64@1.13.0", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-UNCoKrbH0Yv61jCCUIPRr7ErS3yYt2VNCFdzLf752O9K0yrfn9FzYUsyxQFEn1Ah/kq+TNgZw90gVLg5fv1t4g=="], + + "lefthook-openbsd-x64": ["lefthook-openbsd-x64@1.13.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-iyvE+jgHYnLvOoHsLykgf98lftewsQzEBciYxygna9sLZ9nLvfbwp9mWUk09yMRmPCFGDeeDecERaUa2SICWLA=="], + + "lefthook-windows-arm64": ["lefthook-windows-arm64@1.13.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-+u0GyvZouKGcecFsayIbzq1KIoDcrSqVhivLfJUq7vpMXbSHV5HbhrkdkfqkuGjGgGnWulQY29/bDubTQoqfOA=="], + + "lefthook-windows-x64": ["lefthook-windows-x64@1.13.0", "", { "os": "win32", "cpu": "x64" }, "sha512-RG8dfOkszk6BaOA7k26NO0R1/vy1tno7/wgdg+Wjt0pYFiBo0DhmPMoAVB4kzjObqBKDd1KWidzsEv4/R0oFIg=="], + "loupe": ["loupe@3.2.1", "", {}, "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ=="], "magic-string": ["magic-string@0.30.19", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw=="], diff --git a/charts/network-bootstrapper/README.md b/charts/network-bootstrapper/README.md new file mode 100644 index 0000000..64ceb2a --- /dev/null +++ b/charts/network-bootstrapper/README.md @@ -0,0 +1,49 @@ +# network-bootstrapper + +![Version: 0.1.0](https://img.shields.io/badge/Version-0.1.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 0.1.0](https://img.shields.io/badge/AppVersion-0.1.0-informational?style=flat-square) + +A Helm chart for Kubernetes + +## Values + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| affinity | object | `{}` | | +| autoscaling.enabled | bool | `false` | | +| autoscaling.maxReplicas | int | `100` | | +| autoscaling.minReplicas | int | `1` | | +| autoscaling.targetCPUUtilizationPercentage | int | `80` | | +| fullnameOverride | string | `""` | | +| httpRoute | object | `{"annotations":{},"enabled":false,"hostnames":["chart-example.local"],"parentRefs":[{"name":"gateway","sectionName":"http"}],"rules":[{"matches":[{"path":{"type":"PathPrefix","value":"/headers"}}]}]}` | Expose the service via gateway-api HTTPRoute Requires Gateway API resources and suitable controller installed within the cluster (see: https://gateway-api.sigs.k8s.io/guides/) | +| image.pullPolicy | string | `"IfNotPresent"` | | +| image.repository | string | `"nginx"` | | +| image.tag | string | `""` | | +| imagePullSecrets | list | `[]` | | +| ingress.annotations | object | `{}` | | +| ingress.className | string | `""` | | +| ingress.enabled | bool | `false` | | +| ingress.hosts[0].host | string | `"chart-example.local"` | | +| ingress.hosts[0].paths[0].path | string | `"/"` | | +| ingress.hosts[0].paths[0].pathType | string | `"ImplementationSpecific"` | | +| ingress.tls | list | `[]` | | +| livenessProbe.httpGet.path | string | `"/"` | | +| livenessProbe.httpGet.port | string | `"http"` | | +| nameOverride | string | `""` | | +| nodeSelector | object | `{}` | | +| podAnnotations | object | `{}` | | +| podLabels | object | `{}` | | +| podSecurityContext | object | `{}` | | +| readinessProbe.httpGet.path | string | `"/"` | | +| readinessProbe.httpGet.port | string | `"http"` | | +| replicaCount | int | `1` | | +| resources | object | `{}` | | +| securityContext | object | `{}` | | +| service.port | int | `80` | | +| service.type | string | `"ClusterIP"` | | +| serviceAccount.annotations | object | `{}` | | +| serviceAccount.automount | bool | `true` | | +| serviceAccount.create | bool | `true` | | +| serviceAccount.name | string | `""` | | +| tolerations | list | `[]` | | +| volumeMounts | list | `[]` | | +| volumes | list | `[]` | | diff --git a/lefthook.yml b/lefthook.yml new file mode 100644 index 0000000..793d802 --- /dev/null +++ b/lefthook.yml @@ -0,0 +1,11 @@ +pre-commit: + commands: + check_fix: + run: bun run check:fix || true + stage_fixed: true + docs_cli: + run: bun run docs:cli || true + stage_fixed: true + docs_helm: + run: bun run docs:helm || true + stage_fixed: true diff --git a/package.json b/package.json index 6d8d9d5..9d97475 100644 --- a/package.json +++ b/package.json @@ -21,8 +21,11 @@ }, "scripts": { "typecheck": "tsc --noEmit", - "check": "biome check src --write", - "test": "bun test" + "check": "ultracite check", + "check:fix": "ultracite fix", + "test": "bun test", + "docs:helm": "helm-docs --chart-search-root=. --skip-version-footer", + "docs:cli": "cat README.tpl > README.md && echo '\n```' >> README.md && bun src/index.ts --help >> README.md && echo '```' >> README.md" }, "devDependencies": { "@biomejs/biome": "2.2.4", @@ -36,6 +39,7 @@ "@inquirer/prompts": "7.8.6", "@kubernetes/client-node": "1.3.0", "commander": "14.0.1", + "lefthook": "^1.13.0", "ox": "0.9.6", "viem": "2.37.6", "yaml": "^2.8.1", diff --git a/tools/version.ts b/tools/version.ts index 2a4dc33..b0d0d4f 100644 --- a/tools/version.ts +++ b/tools/version.ts @@ -1,28 +1,32 @@ #!/usr/bin/env bun -import { relative } from "node:path"; +import { dirname, join, relative, resolve } from "node:path"; import { Glob } from "bun"; import { parse, stringify } from "yaml"; -interface VersionInfo { +const RELEASE_TAG_PATTERN = /^v?[0-9]+\.[0-9]+\.[0-9]+$/; +const LEADING_V_PATTERN = /^v/; +const NON_ALPHANUMERIC_PATTERN = /[^0-9A-Za-z-]/g; + +type VersionInfo = { tag: "latest" | "main" | "pr"; version: string; -} +}; -interface VersionParams { +type VersionParams = { refSlug?: string; refName?: string; shaShort?: string; buildId?: string; startPath?: string; -} +}; -interface PackageJson { +type PackageJson = { version: string; [key: string]: unknown; -} +}; -interface ChartYaml { +type ChartYaml = { version: string; appVersion: string; dependencies?: Array<{ @@ -32,6 +36,29 @@ interface ChartYaml { [key: string]: unknown; }>; [key: string]: unknown; +}; + +async function findPackageJsonPath(startPath?: string): Promise { + const resolvedPath = resolve(startPath ?? "."); + let currentDir = resolvedPath; + let parentDir = dirname(currentDir); + + while (currentDir !== parentDir) { + const candidate = join(currentDir, "package.json"); + if (await Bun.file(candidate).exists()) { + return candidate; + } + + currentDir = parentDir; + parentDir = dirname(currentDir); + } + + const rootCandidate = join(currentDir, "package.json"); + if (await Bun.file(rootCandidate).exists()) { + return rootCandidate; + } + + throw new Error(`package.json not found when searching from ${resolvedPath}`); } /** @@ -40,16 +67,13 @@ interface ChartYaml { * @returns The parsed package.json content */ async function readRootPackageJson(startPath?: string): Promise { - const packageJsonFile = Bun.file("package.json"); - - if (!(await packageJsonFile.exists())) { - throw new Error("Package.json not found at package.json"); - } + const packageJsonPath = await findPackageJsonPath(startPath); + const packageJsonFile = Bun.file(packageJsonPath); const packageJson = (await packageJsonFile.json()) as PackageJson; if (!packageJson.version) { - throw new Error("No version found in package.json"); + throw new Error(`No version found in ${packageJsonPath}`); } return packageJson; @@ -70,12 +94,9 @@ function generateVersionInfo( baseVersion: string, buildId?: string ): VersionInfo { - // Check if ref slug matches version pattern (v?[0-9]+\.[0-9]+\.[0-9]+$) - const versionPattern = /^v?[0-9]+\.[0-9]+\.[0-9]+$/; - - if (versionPattern.test(refSlug)) { + if (RELEASE_TAG_PATTERN.test(refSlug)) { // Remove 'v' prefix if present - const version = refSlug.replace(/^v/, ""); + const version = refSlug.replace(LEADING_V_PATTERN, ""); return { tag: "latest", version, @@ -85,7 +106,8 @@ function generateVersionInfo( if (refName === "main") { // Prefer numeric/strict BUILD_ID for better Renovate sorting // Fallback to short SHA, and finally a timestamp to ensure uniqueness - const sanitize = (s: string) => s.replace(/[^0-9A-Za-z-]/g, ""); + const sanitize = (value: string) => + value.replace(NON_ALPHANUMERIC_PATTERN, ""); const id = sanitize(buildId || "") || sanitize(shaShort || "") || `${Date.now()}`; // Use SemVer pre-release with dot-separated identifiers: -main. @@ -97,7 +119,7 @@ function generateVersionInfo( } // Default case (PR or other branches) - const version = `${baseVersion}-pr${shaShort.replace(/^v/, "")}`; + const version = `${baseVersion}-pr${shaShort.replace(LEADING_V_PATTERN, "")}`; return { tag: "pr", version, @@ -159,7 +181,9 @@ function updateWorkspaceDependencies( depType: string, newVersion: string ): number { - if (!deps) return 0; + if (!deps) { + return 0; + } let workspaceCount = 0; for (const [depName, depVersion] of Object.entries(deps)) { @@ -191,7 +215,9 @@ function updateChartDependencies( | undefined, newVersion: string ): number { - if (!dependencies) return 0; + if (!dependencies) { + return 0; + } let dependencyCount = 0; for (const dep of dependencies) { @@ -267,11 +293,11 @@ export async function updatePackageVersion(startPath?: string): Promise { } const oldVersion = packageJson.version; - let hasChanges = false; + const versionChanged = oldVersion !== newVersion; - // Update the main version - packageJson.version = newVersion; - hasChanges = true; + if (versionChanged) { + packageJson.version = newVersion; + } // Update workspace dependencies in all dependency types const workspaceUpdates = [ @@ -302,14 +328,20 @@ export async function updatePackageVersion(startPath?: string): Promise { 0 ); - if (hasChanges) { + const shouldWrite = versionChanged || totalWorkspaceUpdates > 0; + + if (shouldWrite) { // Write the updated package.json back to disk await Bun.write( packagePath, - JSON.stringify(packageJson, null, 2) + "\n" + `${JSON.stringify(packageJson, null, 2)}\n` ); - console.log(` Updated version: ${oldVersion} -> ${newVersion}`); + if (versionChanged) { + console.log(` Updated version: ${oldVersion} -> ${newVersion}`); + } else { + console.log(` Version already at ${newVersion}`); + } if (totalWorkspaceUpdates > 0) { console.log( ` Updated ${totalWorkspaceUpdates} total workspace:* references` @@ -382,16 +414,18 @@ async function updateChartVersions(): Promise { const oldVersion = chart.version; const oldAppVersion = chart.appVersion; - let hasChanges = false; + const versionChanged = Boolean( + chart.version && chart.version !== newVersion + ); + const appVersionChanged = Boolean( + chart.appVersion && chart.appVersion !== newVersion + ); - // Update the version fields - if (chart.version) { + if (versionChanged && chart.version) { chart.version = newVersion; - hasChanges = true; } - if (chart.appVersion) { + if (appVersionChanged && chart.appVersion) { chart.appVersion = newVersion; - hasChanges = true; } // Update chart dependencies with version "*" @@ -400,17 +434,18 @@ async function updateChartVersions(): Promise { newVersion ); - if (dependencyUpdates > 0) { - hasChanges = true; - } + const hasChanges = + versionChanged || appVersionChanged || dependencyUpdates > 0; if (hasChanges) { // Convert back to YAML and write const updatedContent = stringify(chart); await Bun.write(chartPath, updatedContent); - console.log(` Updated version: ${oldVersion} -> ${newVersion}`); - if (oldAppVersion !== oldVersion) { + if (oldVersion && oldVersion !== newVersion) { + console.log(` Updated version: ${oldVersion} -> ${newVersion}`); + } + if (oldAppVersion && oldAppVersion !== newVersion) { console.log( ` Updated appVersion: ${oldAppVersion} -> ${newVersion}` ); From 445531fe8c3ba126d707c1b820d27b863d67e5ba Mon Sep 17 00:00:00 2001 From: Roderik van der Veer Date: Wed, 17 Sep 2025 10:24:17 +0200 Subject: [PATCH 03/16] chore: update build and test commands in documentation and configuration files --- .github/ct.yaml | 4 + .github/workflows/qa.yml | 46 ++++++ AGENTS.md | 2 +- .../network-bootstrapper/templates/NOTES.txt | 35 ---- .../network-bootstrapper/templates/hpa.yaml | 32 ---- .../templates/httproute.yaml | 38 ----- .../templates/ingress.yaml | 43 ----- .../templates/{deployment.yaml => job.yaml} | 60 ++++--- .../templates/service.yaml | 15 -- .../templates/tests/test-connection.yaml | 15 -- charts/network-bootstrapper/values.yaml | 122 ++++---------- package.json | 1 + src/cli/build-command.test.ts | 76 ++++++++- src/cli/build-command.ts | 104 ++++++++---- src/cli/genesis-prompts.test.ts | 55 +++++++ src/cli/genesis-prompts.ts | 149 +++++++++++------- 16 files changed, 421 insertions(+), 376 deletions(-) create mode 100644 .github/ct.yaml delete mode 100644 charts/network-bootstrapper/templates/NOTES.txt delete mode 100644 charts/network-bootstrapper/templates/hpa.yaml delete mode 100644 charts/network-bootstrapper/templates/httproute.yaml delete mode 100644 charts/network-bootstrapper/templates/ingress.yaml rename charts/network-bootstrapper/templates/{deployment.yaml => job.yaml} (55%) delete mode 100644 charts/network-bootstrapper/templates/service.yaml delete mode 100644 charts/network-bootstrapper/templates/tests/test-connection.yaml diff --git a/.github/ct.yaml b/.github/ct.yaml new file mode 100644 index 0000000..dd514f0 --- /dev/null +++ b/.github/ct.yaml @@ -0,0 +1,4 @@ +chart-dirs: + - charts +remote: origin +target-branch: main diff --git a/.github/workflows/qa.yml b/.github/workflows/qa.yml index 4b78fa9..ae446de 100644 --- a/.github/workflows/qa.yml +++ b/.github/workflows/qa.yml @@ -156,6 +156,52 @@ jobs: provenance: mode=max sbom: true + - name: Set up Python + if: github.event_name == 'pull_request' || github.event_name == 'push' + uses: actions/setup-python@v5 + with: + python-version: "3.x" + check-latest: true + + - name: Set up Helm + if: github.event_name == 'pull_request' || github.event_name == 'push' + uses: azure/setup-helm@v4 + + - name: Set up chart-testing + if: github.event_name == 'pull_request' || github.event_name == 'push' + uses: helm/chart-testing-action@v2.7.0 + + - name: Determine chart changes + if: github.event_name == 'pull_request' || github.event_name == 'push' + id: ct-changed + env: + CT_CONFIG: .github/ct.yaml + run: | + changed=$(ct list-changed --config "$CT_CONFIG") + if [[ -n "$changed" ]]; then + printf "changed=true\n" >> "$GITHUB_OUTPUT" + echo "$changed" + else + printf "changed=false\n" >> "$GITHUB_OUTPUT" + echo "No chart changes detected" + fi + + - name: Run chart-testing (lint) + if: (github.event_name == 'pull_request' || github.event_name == 'push') && steps.ct-changed.outputs.changed == 'true' + env: + CT_CONFIG: .github/ct.yaml + run: ct lint --config "$CT_CONFIG" + + - name: Create kind cluster + if: (github.event_name == 'pull_request' || github.event_name == 'push') && steps.ct-changed.outputs.changed == 'true' + uses: helm/kind-action@v1.12.0 + + - name: Run chart-testing (install) + if: (github.event_name == 'pull_request' || github.event_name == 'push') && steps.ct-changed.outputs.changed == 'true' + env: + CT_CONFIG: .github/ct.yaml + run: ct install --config "$CT_CONFIG" + # Label QA results (PR only) - name: Label QA build status if: | diff --git a/AGENTS.md b/AGENTS.md index 1ab9666..19de531 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,7 +6,7 @@ The TypeScript sources live in `src/`. CLI flows under `src/cli/` orchestrate pr ## Build, Test, and Development Commands -Install dependencies with `bun install`. Use `bun run src/index.ts` to execute the bootstrapper locally. Run the full test suite with `bun test`. Type safety is enforced by `bun run typecheck`, and formatting plus lint rules are auto-fixed with `bun run check` (Biome). Combine these commands before pushing to catch regressions early. +Install dependencies with `bun install`. Use `bun run src/index.ts` to execute the bootstrapper locally. Run the full test suite with `bun test`. Type safety is enforced by `bun run typecheck`, and formatting plus lint rules are auto-fixed with `bun run check:fix` (Biome). Combine these commands before pushing to catch regressions early. ## Coding Style & Naming Conventions diff --git a/charts/network-bootstrapper/templates/NOTES.txt b/charts/network-bootstrapper/templates/NOTES.txt deleted file mode 100644 index 5eed516..0000000 --- a/charts/network-bootstrapper/templates/NOTES.txt +++ /dev/null @@ -1,35 +0,0 @@ -1. Get the application URL by running these commands: -{{- if .Values.httpRoute.enabled }} -{{- if .Values.httpRoute.hostnames }} - export APP_HOSTNAME={{ .Values.httpRoute.hostnames | first }} -{{- else }} - export APP_HOSTNAME=$(kubectl get --namespace {{(first .Values.httpRoute.parentRefs).namespace | default .Release.Namespace }} gateway/{{ (first .Values.httpRoute.parentRefs).name }} -o jsonpath="{.spec.listeners[0].hostname}") - {{- end }} -{{- if and .Values.httpRoute.rules (first .Values.httpRoute.rules).matches (first (first .Values.httpRoute.rules).matches).path.value }} - echo "Visit http://$APP_HOSTNAME{{ (first (first .Values.httpRoute.rules).matches).path.value }} to use your application" - - NOTE: Your HTTPRoute depends on the listener configuration of your gateway and your HTTPRoute rules. - The rules can be set for path, method, header and query parameters. - You can check the gateway configuration with 'kubectl get --namespace {{(first .Values.httpRoute.parentRefs).namespace | default .Release.Namespace }} gateway/{{ (first .Values.httpRoute.parentRefs).name }} -o yaml' -{{- end }} -{{- else if .Values.ingress.enabled }} -{{- range $host := .Values.ingress.hosts }} - {{- range .paths }} - http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} - {{- end }} -{{- end }} -{{- else if contains "NodePort" .Values.service.type }} - export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "network-bootstrapper.fullname" . }}) - export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") - echo http://$NODE_IP:$NODE_PORT -{{- else if contains "LoadBalancer" .Values.service.type }} - NOTE: It may take a few minutes for the LoadBalancer IP to be available. - You can watch its status by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "network-bootstrapper.fullname" . }}' - export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "network-bootstrapper.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") - echo http://$SERVICE_IP:{{ .Values.service.port }} -{{- else if contains "ClusterIP" .Values.service.type }} - export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "network-bootstrapper.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") - export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") - echo "Visit http://127.0.0.1:8080 to use your application" - kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT -{{- end }} diff --git a/charts/network-bootstrapper/templates/hpa.yaml b/charts/network-bootstrapper/templates/hpa.yaml deleted file mode 100644 index 67f44d1..0000000 --- a/charts/network-bootstrapper/templates/hpa.yaml +++ /dev/null @@ -1,32 +0,0 @@ -{{- if .Values.autoscaling.enabled }} -apiVersion: autoscaling/v2 -kind: HorizontalPodAutoscaler -metadata: - name: {{ include "network-bootstrapper.fullname" . }} - labels: - {{- include "network-bootstrapper.labels" . | nindent 4 }} -spec: - scaleTargetRef: - apiVersion: apps/v1 - kind: Deployment - name: {{ include "network-bootstrapper.fullname" . }} - minReplicas: {{ .Values.autoscaling.minReplicas }} - maxReplicas: {{ .Values.autoscaling.maxReplicas }} - metrics: - {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} - - type: Resource - resource: - name: cpu - target: - type: Utilization - averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} - {{- end }} - {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} - - type: Resource - resource: - name: memory - target: - type: Utilization - averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} - {{- end }} -{{- end }} diff --git a/charts/network-bootstrapper/templates/httproute.yaml b/charts/network-bootstrapper/templates/httproute.yaml deleted file mode 100644 index d0226c4..0000000 --- a/charts/network-bootstrapper/templates/httproute.yaml +++ /dev/null @@ -1,38 +0,0 @@ -{{- if .Values.httpRoute.enabled -}} -{{- $fullName := include "network-bootstrapper.fullname" . -}} -{{- $svcPort := .Values.service.port -}} -apiVersion: gateway.networking.k8s.io/v1 -kind: HTTPRoute -metadata: - name: {{ $fullName }} - labels: - {{- include "network-bootstrapper.labels" . | nindent 4 }} - {{- with .Values.httpRoute.annotations }} - annotations: - {{- toYaml . | nindent 4 }} - {{- end }} -spec: - parentRefs: - {{- with .Values.httpRoute.parentRefs }} - {{- toYaml . | nindent 4 }} - {{- end }} - {{- with .Values.httpRoute.hostnames }} - hostnames: - {{- toYaml . | nindent 4 }} - {{- end }} - rules: - {{- range .Values.httpRoute.rules }} - {{- with .matches }} - - matches: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .filters }} - filters: - {{- toYaml . | nindent 8 }} - {{- end }} - backendRefs: - - name: {{ $fullName }} - port: {{ $svcPort }} - weight: 1 - {{- end }} -{{- end }} diff --git a/charts/network-bootstrapper/templates/ingress.yaml b/charts/network-bootstrapper/templates/ingress.yaml deleted file mode 100644 index a38abf1..0000000 --- a/charts/network-bootstrapper/templates/ingress.yaml +++ /dev/null @@ -1,43 +0,0 @@ -{{- if .Values.ingress.enabled -}} -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: {{ include "network-bootstrapper.fullname" . }} - labels: - {{- include "network-bootstrapper.labels" . | nindent 4 }} - {{- with .Values.ingress.annotations }} - annotations: - {{- toYaml . | nindent 4 }} - {{- end }} -spec: - {{- with .Values.ingress.className }} - ingressClassName: {{ . }} - {{- end }} - {{- if .Values.ingress.tls }} - tls: - {{- range .Values.ingress.tls }} - - hosts: - {{- range .hosts }} - - {{ . | quote }} - {{- end }} - secretName: {{ .secretName }} - {{- end }} - {{- end }} - rules: - {{- range .Values.ingress.hosts }} - - host: {{ .host | quote }} - http: - paths: - {{- range .paths }} - - path: {{ .path }} - {{- with .pathType }} - pathType: {{ . }} - {{- end }} - backend: - service: - name: {{ include "network-bootstrapper.fullname" $ }} - port: - number: {{ $.Values.service.port }} - {{- end }} - {{- end }} -{{- end }} diff --git a/charts/network-bootstrapper/templates/deployment.yaml b/charts/network-bootstrapper/templates/job.yaml similarity index 55% rename from charts/network-bootstrapper/templates/deployment.yaml rename to charts/network-bootstrapper/templates/job.yaml index 19bae7a..1a6b0f9 100644 --- a/charts/network-bootstrapper/templates/deployment.yaml +++ b/charts/network-bootstrapper/templates/job.yaml @@ -1,16 +1,12 @@ -apiVersion: apps/v1 -kind: Deployment +apiVersion: batch/v1 +kind: Job metadata: name: {{ include "network-bootstrapper.fullname" . }} labels: {{- include "network-bootstrapper.labels" . | nindent 4 }} spec: - {{- if not .Values.autoscaling.enabled }} - replicas: {{ .Values.replicaCount }} - {{- end }} - selector: - matchLabels: - {{- include "network-bootstrapper.selectorLabels" . | nindent 6 }} + backoffLimit: 3 + completions: 1 template: metadata: {{- with .Values.podAnnotations }} @@ -23,6 +19,7 @@ spec: {{- toYaml . | nindent 8 }} {{- end }} spec: + restartPolicy: Never {{- with .Values.imagePullSecrets }} imagePullSecrets: {{- toYaml . | nindent 8 }} @@ -40,18 +37,41 @@ spec: {{- end }} image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" imagePullPolicy: {{ .Values.image.pullPolicy }} - ports: - - name: http - containerPort: {{ .Values.service.port }} - protocol: TCP - {{- with .Values.livenessProbe }} - livenessProbe: - {{- toYaml . | nindent 12 }} - {{- end }} - {{- with .Values.readinessProbe }} - readinessProbe: - {{- toYaml . | nindent 12 }} - {{- end }} + args: + {{- with .Values.settings.validators }} + - --validators={{ . | quote }} + {{- end }} + {{- with .Values.settings.rpcNodes }} + - --rpc-nodes={{ . | quote }} + {{- end }} + {{- with .Values.settings.allocations }} + - --allocations={{ . | quote }} + {{- end }} + {{- with .Values.settings.outputType }} + - --outputType={{ . | quote }} + {{- end }} + {{- with .Values.settings.consensus }} + - --consensus={{ . | quote }} + {{- end }} + {{- with .Values.settings.chainId }} + - --chain-id={{ . | quote }} + {{- end }} + {{- with .Values.settings.secondsPerBlock }} + - --seconds-per-block={{ . | quote }} + {{- end }} + {{- with .Values.settings.gasLimit }} + - --gas-limit={{ . | quote }} + {{- end }} + {{- with .Values.settings.gasPrice }} + - --gas-price={{ . | quote }} + {{- end }} + {{- with .Values.settings.evmStackSize }} + - --evm-stack-size={{ . | quote }} + {{- end }} + {{- with .Values.settings.contractSizeLimit }} + - --contract-size-limit={{ . | quote }} + {{- end }} + - --accept-defaults {{- with .Values.resources }} resources: {{- toYaml . | nindent 12 }} diff --git a/charts/network-bootstrapper/templates/service.yaml b/charts/network-bootstrapper/templates/service.yaml deleted file mode 100644 index 8932949..0000000 --- a/charts/network-bootstrapper/templates/service.yaml +++ /dev/null @@ -1,15 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: {{ include "network-bootstrapper.fullname" . }} - labels: - {{- include "network-bootstrapper.labels" . | nindent 4 }} -spec: - type: {{ .Values.service.type }} - ports: - - port: {{ .Values.service.port }} - targetPort: http - protocol: TCP - name: http - selector: - {{- include "network-bootstrapper.selectorLabels" . | nindent 4 }} diff --git a/charts/network-bootstrapper/templates/tests/test-connection.yaml b/charts/network-bootstrapper/templates/tests/test-connection.yaml deleted file mode 100644 index cb29e2e..0000000 --- a/charts/network-bootstrapper/templates/tests/test-connection.yaml +++ /dev/null @@ -1,15 +0,0 @@ -apiVersion: v1 -kind: Pod -metadata: - name: "{{ include "network-bootstrapper.fullname" . }}-test-connection" - labels: - {{- include "network-bootstrapper.labels" . | nindent 4 }} - annotations: - "helm.sh/hook": test -spec: - containers: - - name: wget - image: busybox - command: ['wget'] - args: ['{{ include "network-bootstrapper.fullname" . }}:{{ .Values.service.port }}'] - restartPolicy: Never diff --git a/charts/network-bootstrapper/values.yaml b/charts/network-bootstrapper/values.yaml index cbc2f7f..17d2f7f 100644 --- a/charts/network-bootstrapper/values.yaml +++ b/charts/network-bootstrapper/values.yaml @@ -1,13 +1,6 @@ -# Default values for network-bootstrapper. -# This is a YAML-formatted file. -# Declare variables to be passed into your templates. - -# This will set the replicaset count more information can be found here: https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/ -replicaCount: 1 - # This sets the container image more information can be found here: https://kubernetes.io/docs/concepts/containers/images/ image: - repository: nginx + repository: ghcr.io/settlemint/network-bootstrapper # This sets the pull policy for images. pullPolicy: IfNotPresent # Overrides the image tag whose default is the chart appVersion. @@ -38,10 +31,12 @@ podAnnotations: {} # For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ podLabels: {} -podSecurityContext: {} +podSecurityContext: + {} # fsGroup: 2000 -securityContext: {} +securityContext: + {} # capabilities: # drop: # - ALL @@ -49,69 +44,8 @@ securityContext: {} # runAsNonRoot: true # runAsUser: 1000 -# This is for setting up a service more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/ -service: - # This sets the service type more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types - type: ClusterIP - # This sets the ports more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#field-spec-ports - port: 80 - -# This block is for setting up the ingress for more information can be found here: https://kubernetes.io/docs/concepts/services-networking/ingress/ -ingress: - enabled: false - className: "" - annotations: {} - # kubernetes.io/ingress.class: nginx - # kubernetes.io/tls-acme: "true" - hosts: - - host: chart-example.local - paths: - - path: / - pathType: ImplementationSpecific - tls: [] - # - secretName: chart-example-tls - # hosts: - # - chart-example.local - -# -- Expose the service via gateway-api HTTPRoute -# Requires Gateway API resources and suitable controller installed within the cluster -# (see: https://gateway-api.sigs.k8s.io/guides/) -httpRoute: - # HTTPRoute enabled. - enabled: false - # HTTPRoute annotations. - annotations: {} - # Which Gateways this Route is attached to. - parentRefs: - - name: gateway - sectionName: http - # namespace: default - # Hostnames matching HTTP header. - hostnames: - - chart-example.local - # List of rules and filters applied. - rules: - - matches: - - path: - type: PathPrefix - value: /headers - # filters: - # - type: RequestHeaderModifier - # requestHeaderModifier: - # set: - # - name: My-Overwrite-Header - # value: this-is-the-only-value - # remove: - # - User-Agent - # - matches: - # - path: - # type: PathPrefix - # value: /echo - # headers: - # - name: version - # value: v2 - -resources: {} +resources: + {} # We usually recommend not to specify default resources and to leave this as a conscious # choice for the user. This also increases chances charts run on environments with little # resources, such as Minikube. If you do want to specify resources, uncomment the following @@ -123,24 +57,6 @@ resources: {} # cpu: 100m # memory: 128Mi -# This is to setup the liveness and readiness probes more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/ -livenessProbe: - httpGet: - path: / - port: http -readinessProbe: - httpGet: - path: / - port: http - -# This section is for setting up autoscaling more information can be found here: https://kubernetes.io/docs/concepts/workloads/autoscaling/ -autoscaling: - enabled: false - minReplicas: 1 - maxReplicas: 100 - targetCPUUtilizationPercentage: 80 - # targetMemoryUtilizationPercentage: 80 - # Additional volumes on the output Deployment definition. volumes: [] # - name: foo @@ -159,3 +75,27 @@ nodeSelector: {} tolerations: [] affinity: {} + +settings: + # Number of validator nodes to generate. (default: 4) + validators: + # Number of RPC nodes to generate. (default: 2) + rpcNodes: + # Path to a genesis allocations JSON file. (default: none) + allocations: + # Output target (screen, file, kubernetes). (default: "screen") + outputType: + # Consensus algorithm (IBFTv2, QBFT). (default: "QBFT") + consensus: + # Chain ID for the genesis config. (default: random between 40000 and 50000) + chainId: + # Block time in seconds. (default: 2) + secondsPerBlock: + # Block gas limit in decimal form. (default: 9007199254740991) + gasLimit: + # Base gas price (wei). (default: 0) + gasPrice: + # EVM stack size limit. (default: 2048) + evmStackSize: + # Contract size limit in bytes. (default: 2147483647) + contractSizeLimit: diff --git a/package.json b/package.json index 9d97475..3ead8c7 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "check": "ultracite check", "check:fix": "ultracite fix", "test": "bun test", + "helm": "helm upgrade --install networkbootstrapper ./charts/network-bootstrapper -n networkbootstrapper --create-namespace --timeout 15m", "docs:helm": "helm-docs --chart-search-root=. --skip-version-footer", "docs:cli": "cat README.tpl > README.md && echo '\n```' >> README.md && bun src/index.ts --help >> README.md && echo '```' >> README.md" }, diff --git a/src/cli/build-command.test.ts b/src/cli/build-command.test.ts index 175ccc1..3bc7383 100644 --- a/src/cli/build-command.test.ts +++ b/src/cli/build-command.test.ts @@ -99,7 +99,13 @@ describe("CLI command bootstrap", () => { }, promptForGenesis: ( _service, - { allocations, validatorAddresses, faucetAddress, preset } + { + allocations, + validatorAddresses, + faucetAddress, + preset, + autoAcceptDefaults, + } ) => { expect(validatorAddresses).toEqual([ expectedAddress(FIRST_VALIDATOR_INDEX), @@ -109,6 +115,7 @@ describe("CLI command bootstrap", () => { expect(allocations).toEqual({ [expectedAddress(FAUCET_INDEX)]: { balance: "0x01" }, }); + expect(autoAcceptDefaults).toBe(false); expect(preset).toEqual({ algorithm: undefined, chainId: undefined, @@ -232,10 +239,7 @@ describe("CLI command bootstrap", () => { { from: "node" } ); - expect(promptCalls).toEqual([ - [VALIDATOR_LABEL, 2, EXPECTED_DEFAULT_VALIDATOR], - [RPC_LABEL, 1, EXPECTED_DEFAULT_RPC], - ]); + expect(promptCalls).toEqual([]); expect(stdout.read()).toContain("Genesis"); expect(stdout.read()).toContain(GENESIS_MARKER); }); @@ -252,7 +256,8 @@ describe("CLI command bootstrap", () => { } return Promise.resolve(provided); }, - promptForGenesis: (_service, { preset }) => { + promptForGenesis: (_service, { preset, autoAcceptDefaults }) => { + expect(autoAcceptDefaults).toBe(false); expect(preset).toEqual({ algorithm: ALGORITHM.IBFTv2, chainId: 1234, @@ -342,4 +347,63 @@ describe("CLI command bootstrap", () => { `Consensus must be one of: ${Object.values(ALGORITHM).join(", ")}.` ); }); + + test("runBootstrap accepts defaults without prompting when flag provided", async () => { + const factory = createFactoryStub(); + let promptCountInvocations = 0; + let loadAllocationsInvoked = false; + + const deps: BootstrapDependencies = { + factory, + promptForCount: () => { + promptCountInvocations += 1; + return Promise.resolve(0); + }, + promptForGenesis: (_service, options) => { + expect(options.autoAcceptDefaults).toBe(true); + expect(options.preset).toEqual({ + algorithm: undefined, + chainId: undefined, + secondsPerBlock: undefined, + gasLimit: undefined, + gasPrice: undefined, + evmStackSize: undefined, + contractSizeLimit: undefined, + }); + + return Promise.resolve({ + algorithm: ALGORITHM.QBFT, + config: { + chainId: 1, + faucetWalletAddress: expectedAddress( + EXPECTED_DEFAULT_VALIDATOR + EXPECTED_DEFAULT_RPC + 1 + ), + gasLimit: "0x1", + secondsPerBlock: 2, + }, + genesis: { config: {}, extraData: "0xextra" } as any, + }); + }, + service: {} as any, + loadAllocations: () => { + loadAllocationsInvoked = true; + return Promise.resolve({} as Record); + }, + outputResult: (_type, payload) => { + expect(payload.validators).toHaveLength(EXPECTED_DEFAULT_VALIDATOR); + expect(payload.rpcNodes).toHaveLength(EXPECTED_DEFAULT_RPC); + return Promise.resolve(); + }, + }; + + await runBootstrap( + { + acceptDefaults: true, + }, + deps + ); + + expect(promptCountInvocations).toBe(0); + expect(loadAllocationsInvoked).toBe(false); + }); }); diff --git a/src/cli/build-command.ts b/src/cli/build-command.ts index 0e7f6a2..5751ea5 100644 --- a/src/cli/build-command.ts +++ b/src/cli/build-command.ts @@ -18,6 +18,7 @@ import { createCountParser, promptForCount } from "./prompt-helpers.ts"; type CliOptions = { allocations?: string; + acceptDefaults?: boolean; chainId?: number; consensus?: Algorithm; contractSizeLimit?: number; @@ -82,14 +83,43 @@ const runBootstrap = async ( options: CliOptions, deps: BootstrapDependencies ): Promise => { - const validatorsCount = await deps.promptForCount( + const { + acceptDefaults = false, + allocations, + chainId, + consensus, + contractSizeLimit, + evmStackSize, + gasLimit, + gasPrice, + outputType, + rpcNodes: rpcNodeOption, + secondsPerBlock, + validators: validatorOption, + } = options; + + const resolveCount = ( + label: string, + provided: number | undefined, + defaultValue: number + ): Promise => { + if (provided !== undefined) { + return Promise.resolve(provided); + } + if (acceptDefaults) { + return Promise.resolve(defaultValue); + } + return deps.promptForCount(label, undefined, defaultValue); + }; + + const validatorsCount = await resolveCount( "validator nodes", - options.validators, + validatorOption, DEFAULT_VALIDATOR_COUNT ); - const rpcNodeCount = await deps.promptForCount( + const rpcNodeCount = await resolveCount( "RPC nodes", - options.rpcNodes, + rpcNodeOption, DEFAULT_RPC_COUNT ); @@ -101,26 +131,26 @@ const runBootstrap = async ( const faucetAddress: HexAddress = faucet.address; - const allocationOverrides = options.allocations - ? await deps.loadAllocations(options.allocations) + const allocationOverrides = allocations + ? await deps.loadAllocations(allocations) : {}; const { genesis } = await deps.promptForGenesis(deps.service, { faucetAddress, allocations: allocationOverrides, preset: { - algorithm: options.consensus, - chainId: options.chainId, - secondsPerBlock: options.secondsPerBlock, - gasLimit: options.gasLimit, - gasPrice: options.gasPrice, - evmStackSize: options.evmStackSize, - contractSizeLimit: options.contractSizeLimit, + algorithm: consensus, + chainId, + secondsPerBlock, + gasLimit, + gasPrice, + evmStackSize, + contractSizeLimit, }, + autoAcceptDefaults: acceptDefaults, validatorAddresses, }); - const outputType = options.outputType ?? "screen"; const payload: OutputPayload = { faucet, genesis, @@ -128,7 +158,7 @@ const runBootstrap = async ( validators, }; - await deps.outputResult(outputType, payload); + await deps.outputResult(outputType ?? "screen", payload); }; /* c8 ignore start */ @@ -155,16 +185,18 @@ const createCliCommand = ( .option( "-v, --validators ", "Number of validator nodes to generate.", - createCountParser("Validators") + createCountParser("Validators"), + DEFAULT_VALIDATOR_COUNT ) .option( "-r, --rpc-nodes ", "Number of RPC nodes to generate.", - createCountParser("RPC nodes") + createCountParser("RPC nodes"), + DEFAULT_RPC_COUNT ) .option( "-a, --allocations ", - "Path to a genesis allocations JSON file." + "Path to a genesis allocations JSON file. (default: none)" ) .option( "-o, --outputType ", @@ -182,7 +214,9 @@ const createCliCommand = ( ) .option( "--consensus ", - `Consensus algorithm (${Object.values(ALGORITHM).join(", ")}).`, + `Consensus algorithm (${Object.values(ALGORITHM).join(", ")}). (default: ${ + ALGORITHM.QBFT + })`, (value: string): Algorithm => { const normalized = value.trim().toLowerCase(); const match = Object.values(ALGORITHM).find( @@ -198,39 +232,55 @@ const createCliCommand = ( ) .option( "--chain-id ", - "Chain ID for the genesis config.", + "Chain ID for the genesis config. (default: random between 40000 and 50000)", (value: string): number => parsePositiveInteger(value, "Chain ID") ) .option( "--seconds-per-block ", - "Block time in seconds.", + "Block time in seconds. (default: 2)", (value: string): number => parsePositiveInteger(value, "Seconds per block") ) .option( "--gas-limit ", - "Block gas limit in decimal form.", + "Block gas limit in decimal form. (default: 9007199254740991)", (value: string): string => parsePositiveBigInt(value, "Gas limit") ) .option( "--gas-price ", - "Base gas price (wei).", + "Base gas price (wei). (default: 0)", (value: string): number => parseNonNegativeInteger(value, "Gas price") ) .option( "--evm-stack-size ", - "EVM stack size limit.", + "EVM stack size limit. (default: 2048)", (value: string): number => parsePositiveInteger(value, "EVM stack size") ) .option( "--contract-size-limit ", - "Contract size limit in bytes.", + "Contract size limit in bytes. (default: 2147483647)", (value: string): number => parsePositiveInteger(value, "Contract size limit") + ) + .option( + "--accept-defaults", + "Accept default values for all prompts when CLI flags are omitted. (default: disabled)" ); - command.action(async (options: CliOptions) => { - await runBootstrap(options, deps); + command.action(async (options: CliOptions, cmd: Command) => { + const normalizedOptions: CliOptions = { + ...options, + validators: + cmd.getOptionValueSource("validators") === "default" + ? undefined + : options.validators, + rpcNodes: + cmd.getOptionValueSource("rpcNodes") === "default" + ? undefined + : options.rpcNodes, + }; + + await runBootstrap(normalizedOptions, deps); }); return command; diff --git a/src/cli/genesis-prompts.test.ts b/src/cli/genesis-prompts.test.ts index 19988dc..0b78f3c 100644 --- a/src/cli/genesis-prompts.test.ts +++ b/src/cli/genesis-prompts.test.ts @@ -26,6 +26,11 @@ const CONTRACT_SIZE_LIMIT = 10_000; const NEGATIVE_PRESET_INT = -1; const NEGATIVE_BIG_VALUE = "-1"; const NON_NUMERIC_BIG_VALUE = "not-a-number"; +const MIN_CHAIN_ID = 40_000; +const CHAIN_ID_RANGE = 10_000; +const DEFAULT_EVM_STACK_SIZE = 2048; +const DEFAULT_CONTRACT_SIZE_LIMIT = 2_147_483_647; +const RANDOM_HALF = 0.5; const withCancel = (value: T) => { const promise = Promise.resolve(value) as Promise & { cancel: () => void }; @@ -323,4 +328,54 @@ describe("promptForGenesisConfig", () => { }) ).rejects.toThrow("Gas limit must be a positive integer."); }); + + test("autoAcceptDefaults uses defaults without prompting", async () => { + const { service, generated } = createServiceStub(); + const originalRandom = Math.random; + Math.random = () => RANDOM_HALF; + + const overrides: PromptOverrides = { + selectPrompt: () => { + throw new Error( + "Select prompt should not be called when accepting defaults." + ); + }, + inputPrompt: () => { + throw new Error( + "Input prompt should not be called when accepting defaults." + ); + }, + }; + + try { + const result = await promptForGenesisConfig( + service as unknown as BesuGenesisService, + { + allocations: {} as Record, + faucetAddress: faucet, + overrides, + autoAcceptDefaults: true, + validatorAddresses: validators, + } + ); + + const expectedChainId = + Math.floor(RANDOM_HALF * CHAIN_ID_RANGE) + MIN_CHAIN_ID; + expect(result.algorithm).toBe(ALGORITHM.QBFT); + expect(result.config.chainId).toBe(expectedChainId); + expect(result.config.secondsPerBlock).toBe(2); + expect(result.config.gasLimit).toBe( + `0x${BigInt(GAS_LIMIT_DECIMAL).toString(HEX_RADIX)}` + ); + expect(result.config.gasPrice).toBeUndefined(); + expect(result.config.evmStackSize).toBe(DEFAULT_EVM_STACK_SIZE); + expect(result.config.contractSizeLimit).toBe(DEFAULT_CONTRACT_SIZE_LIMIT); + expect(result.genesis).toEqual({ + ...generated, + extraData: "0xextra", + }); + } finally { + Math.random = originalRandom; + } + }); }); diff --git a/src/cli/genesis-prompts.ts b/src/cli/genesis-prompts.ts index 7b9e1db..a057057 100644 --- a/src/cli/genesis-prompts.ts +++ b/src/cli/genesis-prompts.ts @@ -61,6 +61,7 @@ type GenesisPromptPreset = { type GenesisPromptOptions = { allocations?: Record; + autoAcceptDefaults?: boolean; faucetAddress: HexAddress; overrides?: Partial; preset?: GenesisPromptPreset; @@ -103,6 +104,7 @@ const promptForGenesisConfig = async ( service: BesuGenesisService, { allocations = {}, + autoAcceptDefaults = false, faucetAddress, overrides = {}, preset, @@ -116,6 +118,8 @@ const promptForGenesisConfig = async ( const defaults = createDefaultNetworkSettings(); + const fallbackAlgorithm = ALGORITHM.QBFT; + let resolvedAlgorithm: Algorithm; if (preset?.algorithm) { if (!Object.values(ALGORITHM).includes(preset.algorithm)) { @@ -124,6 +128,8 @@ const promptForGenesisConfig = async ( ); } resolvedAlgorithm = preset.algorithm; + } else if (autoAcceptDefaults) { + resolvedAlgorithm = fallbackAlgorithm; } else { const algorithmSelection = await selectFn({ message: accent("Select consensus algorithm"), @@ -142,66 +148,103 @@ const promptForGenesisConfig = async ( resolvedAlgorithm = algorithmSelection; } - const chainId = preset?.chainId - ? ensurePositiveInteger(preset.chainId, "Chain ID") - : await promptForInteger({ - defaultValue: defaults.chainId, - labelText: "Chain ID", - message: "Chain ID", - min: 1, - prompt: inputFn, - }); - - const secondsPerBlock = preset?.secondsPerBlock - ? ensurePositiveInteger(preset.secondsPerBlock, "Seconds per block") - : await promptForInteger({ - defaultValue: defaults.secondsPerBlock, - labelText: "Seconds per block", - message: "Seconds per block", - min: 1, - prompt: inputFn, - }); - - const gasLimitInput = preset?.gasLimit - ? ensurePositiveBigIntString(preset.gasLimit, "Gas limit") - : await promptForBigIntString({ - defaultValue: defaults.gasLimit, - labelText: "Block gas limit", - message: "Block gas limit (decimal)", - prompt: inputFn, - }); - - const gasPrice = - preset?.gasPrice ?? - (await promptForInteger({ + let chainId: number; + if (preset?.chainId !== undefined) { + chainId = ensurePositiveInteger(preset.chainId, "Chain ID"); + } else if (autoAcceptDefaults) { + chainId = defaults.chainId; + } else { + chainId = await promptForInteger({ + defaultValue: defaults.chainId, + labelText: "Chain ID", + message: "Chain ID", + min: 1, + prompt: inputFn, + }); + } + + let secondsPerBlock: number; + if (preset?.secondsPerBlock !== undefined) { + secondsPerBlock = ensurePositiveInteger( + preset.secondsPerBlock, + "Seconds per block" + ); + } else if (autoAcceptDefaults) { + secondsPerBlock = defaults.secondsPerBlock; + } else { + secondsPerBlock = await promptForInteger({ + defaultValue: defaults.secondsPerBlock, + labelText: "Seconds per block", + message: "Seconds per block", + min: 1, + prompt: inputFn, + }); + } + + let gasLimitInput: string; + if (preset?.gasLimit !== undefined) { + gasLimitInput = ensurePositiveBigIntString(preset.gasLimit, "Gas limit"); + } else if (autoAcceptDefaults) { + gasLimitInput = defaults.gasLimit; + } else { + gasLimitInput = await promptForBigIntString({ + defaultValue: defaults.gasLimit, + labelText: "Block gas limit", + message: "Block gas limit (decimal)", + prompt: inputFn, + }); + } + + let gasPrice: number; + if (preset?.gasPrice !== undefined) { + gasPrice = ensureNonNegativeInteger(preset.gasPrice, "Gas price"); + } else if (autoAcceptDefaults) { + gasPrice = defaults.gasPrice; + } else { + gasPrice = await promptForInteger({ defaultValue: defaults.gasPrice, labelText: "Base gas price", message: "Base gas price (wei)", min: 0, prompt: inputFn, - })); + }); + gasPrice = ensureNonNegativeInteger(gasPrice, "Gas price"); + } - const normalizedGasPrice = ensureNonNegativeInteger(gasPrice, "Gas price"); + let evmStackSize: number; + if (preset?.evmStackSize !== undefined) { + evmStackSize = ensurePositiveInteger(preset.evmStackSize, "EVM stack size"); + } else if (autoAcceptDefaults) { + evmStackSize = defaults.evmStackSize; + } else { + evmStackSize = await promptForInteger({ + defaultValue: defaults.evmStackSize, + labelText: "EVM stack size", + message: "EVM stack size", + min: 1, + prompt: inputFn, + }); + } - const evmStackSize = preset?.evmStackSize - ? ensurePositiveInteger(preset.evmStackSize, "EVM stack size") - : await promptForInteger({ - defaultValue: defaults.evmStackSize, - labelText: "EVM stack size", - message: "EVM stack size", - min: 1, - prompt: inputFn, - }); - - const contractSizeLimit = preset?.contractSizeLimit - ? ensurePositiveInteger(preset.contractSizeLimit, "Contract size limit") - : await promptForInteger({ - defaultValue: defaults.contractSizeLimit, - labelText: "Contract size limit", - message: "Contract size limit (bytes)", - min: 1, - prompt: inputFn, - }); + let contractSizeLimit: number; + if (preset?.contractSizeLimit !== undefined) { + contractSizeLimit = ensurePositiveInteger( + preset.contractSizeLimit, + "Contract size limit" + ); + } else if (autoAcceptDefaults) { + contractSizeLimit = defaults.contractSizeLimit; + } else { + contractSizeLimit = await promptForInteger({ + defaultValue: defaults.contractSizeLimit, + labelText: "Contract size limit", + message: "Contract size limit (bytes)", + min: 1, + prompt: inputFn, + }); + } + + const normalizedGasPrice = ensureNonNegativeInteger(gasPrice, "Gas price"); const config: BesuNetworkConfig = { chainId, From 3f3def9b46b0e54323632f89bb44c784f4b44c05 Mon Sep 17 00:00:00 2001 From: Roderik van der Veer Date: Wed, 17 Sep 2025 10:24:27 +0200 Subject: [PATCH 04/16] docs: update README and chart documentation with default values for CLI options --- README.md | 24 +++++++++++++------- charts/network-bootstrapper/README.md | 32 ++++++++++----------------- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 9db703c..ced1781 100644 --- a/README.md +++ b/README.md @@ -15,16 +15,24 @@ Generate node identities, configure consensus, and emit a Besu genesis. Options: -v, --validators Number of validator nodes to generate. - -r, --rpc-nodes Number of RPC nodes to generate. + (default: 4) + -r, --rpc-nodes Number of RPC nodes to generate. (default: 2) -a, --allocations Path to a genesis allocations JSON file. + (default: none) -o, --outputType Output target (screen, file, kubernetes). (default: "screen") - --consensus Consensus algorithm (IBFTv2, QBFT). - --chain-id Chain ID for the genesis config. - --seconds-per-block Block time in seconds. - --gas-limit Block gas limit in decimal form. - --gas-price Base gas price (wei). - --evm-stack-size EVM stack size limit. - --contract-size-limit Contract size limit in bytes. + --consensus Consensus algorithm (IBFTv2, QBFT). (default: + QBFT) + --chain-id Chain ID for the genesis config. (default: + random between 40000 and 50000) + --seconds-per-block Block time in seconds. (default: 2) + --gas-limit Block gas limit in decimal form. (default: + 9007199254740991) + --gas-price Base gas price (wei). (default: 0) + --evm-stack-size EVM stack size limit. (default: 2048) + --contract-size-limit Contract size limit in bytes. (default: + 2147483647) + --accept-defaults Accept default values for all prompts when CLI + flags are omitted. (default: disabled) -h, --help display help for command ``` diff --git a/charts/network-bootstrapper/README.md b/charts/network-bootstrapper/README.md index 64ceb2a..f80f31c 100644 --- a/charts/network-bootstrapper/README.md +++ b/charts/network-bootstrapper/README.md @@ -9,41 +9,33 @@ A Helm chart for Kubernetes | Key | Type | Default | Description | |-----|------|---------|-------------| | affinity | object | `{}` | | -| autoscaling.enabled | bool | `false` | | -| autoscaling.maxReplicas | int | `100` | | -| autoscaling.minReplicas | int | `1` | | -| autoscaling.targetCPUUtilizationPercentage | int | `80` | | | fullnameOverride | string | `""` | | -| httpRoute | object | `{"annotations":{},"enabled":false,"hostnames":["chart-example.local"],"parentRefs":[{"name":"gateway","sectionName":"http"}],"rules":[{"matches":[{"path":{"type":"PathPrefix","value":"/headers"}}]}]}` | Expose the service via gateway-api HTTPRoute Requires Gateway API resources and suitable controller installed within the cluster (see: https://gateway-api.sigs.k8s.io/guides/) | | image.pullPolicy | string | `"IfNotPresent"` | | -| image.repository | string | `"nginx"` | | +| image.repository | string | `"ghcr.io/settlemint/network-bootstrapper"` | | | image.tag | string | `""` | | | imagePullSecrets | list | `[]` | | -| ingress.annotations | object | `{}` | | -| ingress.className | string | `""` | | -| ingress.enabled | bool | `false` | | -| ingress.hosts[0].host | string | `"chart-example.local"` | | -| ingress.hosts[0].paths[0].path | string | `"/"` | | -| ingress.hosts[0].paths[0].pathType | string | `"ImplementationSpecific"` | | -| ingress.tls | list | `[]` | | -| livenessProbe.httpGet.path | string | `"/"` | | -| livenessProbe.httpGet.port | string | `"http"` | | | nameOverride | string | `""` | | | nodeSelector | object | `{}` | | | podAnnotations | object | `{}` | | | podLabels | object | `{}` | | | podSecurityContext | object | `{}` | | -| readinessProbe.httpGet.path | string | `"/"` | | -| readinessProbe.httpGet.port | string | `"http"` | | -| replicaCount | int | `1` | | | resources | object | `{}` | | | securityContext | object | `{}` | | -| service.port | int | `80` | | -| service.type | string | `"ClusterIP"` | | | serviceAccount.annotations | object | `{}` | | | serviceAccount.automount | bool | `true` | | | serviceAccount.create | bool | `true` | | | serviceAccount.name | string | `""` | | +| settings.allocations | string | `nil` | | +| settings.chainId | string | `nil` | | +| settings.consensus | string | `nil` | | +| settings.contractSizeLimit | string | `nil` | | +| settings.evmStackSize | string | `nil` | | +| settings.gasLimit | string | `nil` | | +| settings.gasPrice | string | `nil` | | +| settings.outputType | string | `nil` | | +| settings.rpcNodes | string | `nil` | | +| settings.secondsPerBlock | string | `nil` | | +| settings.validators | string | `nil` | | | tolerations | list | `[]` | | | volumeMounts | list | `[]` | | | volumes | list | `[]` | | From 5aca0d57d2d54ced370107e3215cea1d2b23db5b Mon Sep 17 00:00:00 2001 From: Roderik van der Veer Date: Wed, 17 Sep 2025 10:35:46 +0200 Subject: [PATCH 05/16] chore: update Chart.yaml and README with maintainer information --- charts/network-bootstrapper/Chart.yaml | 4 ++ charts/network-bootstrapper/README.md | 71 ++++++++++++++----------- charts/network-bootstrapper/values.yaml | 63 ++++++++++++---------- tools/version.ts | 37 +++++++------ 4 files changed, 97 insertions(+), 78 deletions(-) diff --git a/charts/network-bootstrapper/Chart.yaml b/charts/network-bootstrapper/Chart.yaml index b23f6a9..50084a3 100644 --- a/charts/network-bootstrapper/Chart.yaml +++ b/charts/network-bootstrapper/Chart.yaml @@ -4,3 +4,7 @@ description: A Helm chart for Kubernetes type: application version: 0.1.0 appVersion: "0.1.0" +maintainers: + - name: SettleMint + email: support@settlemint.com + url: https://settlemint.com diff --git a/charts/network-bootstrapper/README.md b/charts/network-bootstrapper/README.md index f80f31c..62c3c03 100644 --- a/charts/network-bootstrapper/README.md +++ b/charts/network-bootstrapper/README.md @@ -4,38 +4,47 @@ A Helm chart for Kubernetes +## Maintainers + +| Name | Email | Url | +| ---- | ------ | --- | +| SettleMint | | | + ## Values | Key | Type | Default | Description | |-----|------|---------|-------------| -| affinity | object | `{}` | | -| fullnameOverride | string | `""` | | -| image.pullPolicy | string | `"IfNotPresent"` | | -| image.repository | string | `"ghcr.io/settlemint/network-bootstrapper"` | | -| image.tag | string | `""` | | -| imagePullSecrets | list | `[]` | | -| nameOverride | string | `""` | | -| nodeSelector | object | `{}` | | -| podAnnotations | object | `{}` | | -| podLabels | object | `{}` | | -| podSecurityContext | object | `{}` | | -| resources | object | `{}` | | -| securityContext | object | `{}` | | -| serviceAccount.annotations | object | `{}` | | -| serviceAccount.automount | bool | `true` | | -| serviceAccount.create | bool | `true` | | -| serviceAccount.name | string | `""` | | -| settings.allocations | string | `nil` | | -| settings.chainId | string | `nil` | | -| settings.consensus | string | `nil` | | -| settings.contractSizeLimit | string | `nil` | | -| settings.evmStackSize | string | `nil` | | -| settings.gasLimit | string | `nil` | | -| settings.gasPrice | string | `nil` | | -| settings.outputType | string | `nil` | | -| settings.rpcNodes | string | `nil` | | -| settings.secondsPerBlock | string | `nil` | | -| settings.validators | string | `nil` | | -| tolerations | list | `[]` | | -| volumeMounts | list | `[]` | | -| volumes | list | `[]` | | +| affinity | object | `{}` | Affinity and anti-affinity rules influencing pod placement. | +| fullnameOverride | string | `""` | Fully qualified name override for resources created by this release. | +| image | object | `{"pullPolicy":"IfNotPresent","repository":"ghcr.io/settlemint/network-bootstrapper","tag":""}` | Container image settings for the network bootstrapper workload. See https://kubernetes.io/docs/concepts/containers/images/ for background. | +| image.pullPolicy | string | `"IfNotPresent"` | Image pull policy controlling when Kubernetes re-fetches the image layer manifest. | +| image.repository | string | `"ghcr.io/settlemint/network-bootstrapper"` | OCI repository hosting the network bootstrapper image. | +| image.tag | string | `""` | Image tag override. Defaults to the chart's `.appVersion` when left empty. | +| imagePullSecrets | list | `[]` | Image pull secrets enabling access to private registries. See https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ for usage. | +| nameOverride | string | `""` | Short name override applied to chart-scoped resource names. | +| nodeSelector | object | `{}` | Node selector constraints for scheduling the bootstrapper pod. | +| podAnnotations | object | `{}` | Pod-level annotations merged onto the generated pod template metadata. See https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/. | +| podLabels | object | `{}` | Pod-level labels applied to the pod template metadata. See https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/. | +| podSecurityContext | object | `{}` | Pod-level security context applied to all containers in the pod. | +| resources | object | `{}` | Resource requests and limits for the bootstrapper container. | +| securityContext | object | `{}` | Container security context applied to the bootstrapper container. | +| serviceAccount | object | `{"annotations":{},"automount":true,"create":true,"name":""}` | Service account configuration for the bootstrapper pod. See https://kubernetes.io/docs/concepts/security/service-accounts/ for details. | +| serviceAccount.annotations | object | `{}` | Additional metadata annotations applied to the service account object. | +| serviceAccount.automount | bool | `true` | Automatically mount the service account token into the pod. | +| serviceAccount.create | bool | `true` | Whether to create a service account automatically. | +| serviceAccount.name | string | `""` | Existing service account name to use instead of one generated by the chart. If unset and `serviceAccount.create` is true, a name is derived from the chart fullname. | +| settings | object | `{"allocations":null,"chainId":null,"consensus":null,"contractSizeLimit":null,"evmStackSize":null,"gasLimit":null,"gasPrice":null,"outputType":null,"rpcNodes":null,"secondsPerBlock":null,"validators":null}` | Network bootstrapper CLI settings translated into command-line flags. | +| settings.allocations | string | `nil` | Filesystem path, accessible to the job, pointing to a JSON file with initial account allocations. Omit to skip pre-funded accounts. | +| settings.chainId | int | `nil` | Explicit chain ID applied to the genesis configuration. Defaults to a random value in the 40000-50000 range when omitted. | +| settings.consensus | string | `nil` | Consensus engine to configure for the network (IBFTv2 or QBFT). Default: "QBFT". | +| settings.contractSizeLimit | int | `nil` | Contract size limit in bytes enforced by the EVM. Default: 2147483647. | +| settings.evmStackSize | int | `nil` | Maximum EVM stack size allowed for contract execution. Default: 2048. | +| settings.gasLimit | int | `nil` | Genesis block gas limit value expressed in decimal. Default: 9007199254740991. | +| settings.gasPrice | int | `nil` | Base gas price in wei applied to the chain. Default: 0. | +| settings.outputType | string | `nil` | Destination for generated artefacts: `screen` (stdout), `file` (write to volume), or `kubernetes` (persist as Kubernetes secrets/configmaps). Default: "screen". | +| settings.rpcNodes | int | `nil` | Number of RPC node definitions included in the output topology. Default: 2. | +| settings.secondsPerBlock | int | `nil` | Target block time in seconds encoded into genesis. Default: 2. | +| settings.validators | int | `nil` | Number of validator node definitions the bootstrapper generates. Default: 4. | +| tolerations | list | `[]` | Kubernetes tolerations assigned to the bootstrapper pod. | +| volumeMounts | list | `[]` | Additional volume mounts added to the bootstrapper container. | +| volumes | list | `[]` | Additional volumes injected into the deployment pod spec. | diff --git a/charts/network-bootstrapper/values.yaml b/charts/network-bootstrapper/values.yaml index 17d2f7f..e25fdbc 100644 --- a/charts/network-bootstrapper/values.yaml +++ b/charts/network-bootstrapper/values.yaml @@ -1,40 +1,42 @@ -# This sets the container image more information can be found here: https://kubernetes.io/docs/concepts/containers/images/ +# -- Container image settings for the network bootstrapper workload. See https://kubernetes.io/docs/concepts/containers/images/ for background. image: + # -- (string) OCI repository hosting the network bootstrapper image. repository: ghcr.io/settlemint/network-bootstrapper - # This sets the pull policy for images. + # -- (string) Image pull policy controlling when Kubernetes re-fetches the image layer manifest. pullPolicy: IfNotPresent - # Overrides the image tag whose default is the chart appVersion. + # -- (string) Image tag override. Defaults to the chart's `.appVersion` when left empty. tag: "" -# This is for the secrets for pulling an image from a private repository more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ +# -- (list) Image pull secrets enabling access to private registries. See https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ for usage. imagePullSecrets: [] -# This is to override the chart name. +# -- (string) Short name override applied to chart-scoped resource names. nameOverride: "" +# -- (string) Fully qualified name override for resources created by this release. fullnameOverride: "" -# This section builds out the service account more information can be found here: https://kubernetes.io/docs/concepts/security/service-accounts/ +# -- (object) Service account configuration for the bootstrapper pod. See https://kubernetes.io/docs/concepts/security/service-accounts/ for details. serviceAccount: - # Specifies whether a service account should be created + # -- (bool) Whether to create a service account automatically. create: true - # Automatically mount a ServiceAccount's API credentials? + # -- (bool) Automatically mount the service account token into the pod. automount: true - # Annotations to add to the service account + # -- (object) Additional metadata annotations applied to the service account object. annotations: {} - # The name of the service account to use. - # If not set and create is true, a name is generated using the fullname template + # -- (string) Existing service account name to use instead of one generated by the chart. + # If unset and `serviceAccount.create` is true, a name is derived from the chart fullname. name: "" -# This is for setting Kubernetes Annotations to a Pod. -# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ +# -- (object) Pod-level annotations merged onto the generated pod template metadata. See https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/. podAnnotations: {} -# This is for setting Kubernetes Labels to a Pod. -# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ +# -- (object) Pod-level labels applied to the pod template metadata. See https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/. podLabels: {} +# -- (object) Pod-level security context applied to all containers in the pod. podSecurityContext: {} # fsGroup: 2000 +# -- (object) Container security context applied to the bootstrapper container. securityContext: {} # capabilities: @@ -44,6 +46,7 @@ securityContext: # runAsNonRoot: true # runAsUser: 1000 +# -- (object) Resource requests and limits for the bootstrapper container. resources: {} # We usually recommend not to specify default resources and to leave this as a conscious @@ -57,45 +60,49 @@ resources: # cpu: 100m # memory: 128Mi -# Additional volumes on the output Deployment definition. +# -- (list) Additional volumes injected into the deployment pod spec. volumes: [] # - name: foo # secret: # secretName: mysecret # optional: false -# Additional volumeMounts on the output Deployment definition. +# -- (list) Additional volume mounts added to the bootstrapper container. volumeMounts: [] # - name: foo # mountPath: "/etc/foo" # readOnly: true +# -- (object) Node selector constraints for scheduling the bootstrapper pod. nodeSelector: {} +# -- (list) Kubernetes tolerations assigned to the bootstrapper pod. tolerations: [] +# -- (object) Affinity and anti-affinity rules influencing pod placement. affinity: {} +# -- (object) Network bootstrapper CLI settings translated into command-line flags. settings: - # Number of validator nodes to generate. (default: 4) + # -- (int) Number of validator node definitions the bootstrapper generates. Default: 4. validators: - # Number of RPC nodes to generate. (default: 2) + # -- (int) Number of RPC node definitions included in the output topology. Default: 2. rpcNodes: - # Path to a genesis allocations JSON file. (default: none) + # -- (string) Filesystem path, accessible to the job, pointing to a JSON file with initial account allocations. Omit to skip pre-funded accounts. allocations: - # Output target (screen, file, kubernetes). (default: "screen") + # -- (string) Destination for generated artefacts: `screen` (stdout), `file` (write to volume), or `kubernetes` (persist as Kubernetes secrets/configmaps). Default: "screen". outputType: - # Consensus algorithm (IBFTv2, QBFT). (default: "QBFT") + # -- (string) Consensus engine to configure for the network (IBFTv2 or QBFT). Default: "QBFT". consensus: - # Chain ID for the genesis config. (default: random between 40000 and 50000) + # -- (int) Explicit chain ID applied to the genesis configuration. Defaults to a random value in the 40000-50000 range when omitted. chainId: - # Block time in seconds. (default: 2) + # -- (int) Target block time in seconds encoded into genesis. Default: 2. secondsPerBlock: - # Block gas limit in decimal form. (default: 9007199254740991) + # -- (int) Genesis block gas limit value expressed in decimal. Default: 9007199254740991. gasLimit: - # Base gas price (wei). (default: 0) + # -- (int) Base gas price in wei applied to the chain. Default: 0. gasPrice: - # EVM stack size limit. (default: 2048) + # -- (int) Maximum EVM stack size allowed for contract execution. Default: 2048. evmStackSize: - # Contract size limit in bytes. (default: 2147483647) + # -- (int) Contract size limit in bytes enforced by the EVM. Default: 2147483647. contractSizeLimit: diff --git a/tools/version.ts b/tools/version.ts index b0d0d4f..5c03b42 100644 --- a/tools/version.ts +++ b/tools/version.ts @@ -8,6 +8,9 @@ const RELEASE_TAG_PATTERN = /^v?[0-9]+\.[0-9]+\.[0-9]+$/; const LEADING_V_PATTERN = /^v/; const NON_ALPHANUMERIC_PATTERN = /[^0-9A-Za-z-]/g; +const sanitizeIdentifier = (value: string) => + value.replace(NON_ALPHANUMERIC_PATTERN, ""); + type VersionInfo = { tag: "latest" | "main" | "pr"; version: string; @@ -16,7 +19,6 @@ type VersionInfo = { type VersionParams = { refSlug?: string; refName?: string; - shaShort?: string; buildId?: string; startPath?: string; }; @@ -83,14 +85,13 @@ async function readRootPackageJson(startPath?: string): Promise { * Generates version string based on Git ref information and base version * @param refSlug - Git ref slug * @param refName - Git ref name - * @param shaShort - Short SHA * @param baseVersion - Base version from package.json + * @param buildId - Optional build identifier (GitHub run counter or similar) * @returns Object containing version and tag */ function generateVersionInfo( refSlug: string, refName: string, - shaShort: string, baseVersion: string, buildId?: string ): VersionInfo { @@ -104,12 +105,9 @@ function generateVersionInfo( } if (refName === "main") { - // Prefer numeric/strict BUILD_ID for better Renovate sorting - // Fallback to short SHA, and finally a timestamp to ensure uniqueness - const sanitize = (value: string) => - value.replace(NON_ALPHANUMERIC_PATTERN, ""); - const id = - sanitize(buildId || "") || sanitize(shaShort || "") || `${Date.now()}`; + // Prefer numeric/strict BUILD_ID (or GitHub run counters) for Renovate sorting + // Fall back to a timestamp to ensure uniqueness outside CI + const id = sanitizeIdentifier(buildId || "") || `${Date.now()}`; // Use SemVer pre-release with dot-separated identifiers: -main. const version = `${baseVersion}-main.${id}`; return { @@ -119,7 +117,8 @@ function generateVersionInfo( } // Default case (PR or other branches) - const version = `${baseVersion}-pr${shaShort.replace(LEADING_V_PATTERN, "")}`; + const identifier = sanitizeIdentifier(buildId || "") || `${Date.now()}`; + const version = `${baseVersion}-pr.${identifier}`; return { tag: "pr", version, @@ -137,20 +136,20 @@ export async function getVersionInfo( const { refSlug = process.env.GITHUB_REF_SLUG || "", refName = process.env.GITHUB_REF_NAME || "", - shaShort = process.env.GITHUB_SHA_SHORT || "", - buildId = process.env.BUILD_ID || "", + buildId: providedBuildId, startPath, } = params; + const buildId = + providedBuildId || + process.env.BUILD_ID || + process.env.GITHUB_RUN_NUMBER || + process.env.GITHUB_RUN_ID || + ""; + const packageJson = await readRootPackageJson(startPath); - return generateVersionInfo( - refSlug, - refName, - shaShort, - packageJson.version, - buildId - ); + return generateVersionInfo(refSlug, refName, packageJson.version, buildId); } /** From 1c10c4fa94ec91bc4a2e98e067c8b848674fc7b5 Mon Sep 17 00:00:00 2001 From: Roderik van der Veer Date: Wed, 17 Sep 2025 10:52:54 +0200 Subject: [PATCH 06/16] chore: update GitHub Actions workflow for versioning and build process --- .github/workflows/qa.yml | 2 ++ tools/version.ts | 51 +++++++++++++++++++++++++++++++++++----- 2 files changed, 47 insertions(+), 6 deletions(-) diff --git a/.github/workflows/qa.yml b/.github/workflows/qa.yml index ae446de..ec4db66 100644 --- a/.github/workflows/qa.yml +++ b/.github/workflows/qa.yml @@ -121,6 +121,7 @@ jobs: run: bun typecheck - name: Set version + id: version if: github.event_name == 'pull_request' || github.event_name == 'push' run: bun run tools/version.ts @@ -143,6 +144,7 @@ jobs: type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}} type=sha + type=raw,value=${{ steps.version.outputs.version }} - name: Build and push if: github.event_name == 'pull_request' || github.event_name == 'push' diff --git a/tools/version.ts b/tools/version.ts index 5c03b42..f02cf66 100644 --- a/tools/version.ts +++ b/tools/version.ts @@ -1,5 +1,6 @@ #!/usr/bin/env bun +import { appendFile } from "node:fs/promises"; import { dirname, join, relative, resolve } from "node:path"; import { Glob } from "bun"; import { parse, stringify } from "yaml"; @@ -241,10 +242,14 @@ function updateChartDependencies( * @param startPath - Starting path for finding package.json files (defaults to current working directory) * @returns Promise that resolves when all updates are complete */ -export async function updatePackageVersion(startPath?: string): Promise { +export async function updatePackageVersion( + startPath?: string, + versionInfoOverride?: VersionInfo +): Promise { try { // Get the current version info - const versionInfo = await getVersionInfo({ startPath }); + const versionInfo = + versionInfoOverride ?? (await getVersionInfo({ startPath })); const newVersion = versionInfo.version; console.log(`Updating all package.json files to version: ${newVersion}`); @@ -365,10 +370,12 @@ export async function updatePackageVersion(startPath?: string): Promise { /** * Updates all Chart.yaml files in the ATK directory with the current version */ -async function updateChartVersions(): Promise { +async function updateChartVersions( + versionInfoOverride?: VersionInfo +): Promise { try { // Get the current version info - const versionInfo = await getVersionInfo(); + const versionInfo = versionInfoOverride ?? (await getVersionInfo()); const newVersion = versionInfo.version; console.log(`Updating charts to version: ${newVersion}`); @@ -465,6 +472,35 @@ async function updateChartVersions(): Promise { } } +/** + * Exports version and tag information to GitHub Outputs/Env for downstream steps + */ +async function persistGithubContext(versionInfo: VersionInfo): Promise { + const { version, tag } = versionInfo; + + const appendLines = async ( + filePath: string | undefined, + lines: string[] + ): Promise => { + if (!filePath || lines.length === 0) { + return; + } + + const content = `${lines.join("\n")}\n`; + await appendFile(filePath, content, "utf8"); + }; + + await appendLines(process.env.GITHUB_OUTPUT, [ + `version=${version}`, + `tag=${tag}`, + ]); + + await appendLines(process.env.GITHUB_ENV, [ + `NETWORK_BOOTSTRAPPER_VERSION=${version}`, + `NETWORK_BOOTSTRAPPER_TAG=${tag}`, + ]); +} + // Run the script if called directly if (import.meta.main) { // Check if running in CI environment @@ -473,6 +509,9 @@ if (import.meta.main) { process.exit(0); } - await updateChartVersions(); - await updatePackageVersion(); + const versionInfo = await getVersionInfo(); + + await updateChartVersions(versionInfo); + await updatePackageVersion(undefined, versionInfo); + await persistGithubContext(versionInfo); } From 2b87e791cdb370b43426b87a723adb6f49c0a54d Mon Sep 17 00:00:00 2001 From: Roderik van der Veer Date: Wed, 17 Sep 2025 11:16:22 +0200 Subject: [PATCH 07/16] chore: update Helm chart values and CLI command handling for output type --- .github/ct.yaml | 1 + .../network-bootstrapper/templates/job.yaml | 22 +++++------ .../network-bootstrapper/templates/role.yaml | 17 +++++++++ .../templates/rolebinding.yaml | 16 ++++++++ charts/network-bootstrapper/values.yaml | 7 +++- src/cli/build-command.test.ts | 37 +++++++++++++++++++ src/cli/build-command.ts | 37 ++++++++++++++++--- 7 files changed, 119 insertions(+), 18 deletions(-) create mode 100644 charts/network-bootstrapper/templates/role.yaml create mode 100644 charts/network-bootstrapper/templates/rolebinding.yaml diff --git a/.github/ct.yaml b/.github/ct.yaml index dd514f0..a4b4882 100644 --- a/.github/ct.yaml +++ b/.github/ct.yaml @@ -2,3 +2,4 @@ chart-dirs: - charts remote: origin target-branch: main +helm-extra-args: '--wait --timeout 600s' diff --git a/charts/network-bootstrapper/templates/job.yaml b/charts/network-bootstrapper/templates/job.yaml index 1a6b0f9..19486f0 100644 --- a/charts/network-bootstrapper/templates/job.yaml +++ b/charts/network-bootstrapper/templates/job.yaml @@ -39,37 +39,37 @@ spec: imagePullPolicy: {{ .Values.image.pullPolicy }} args: {{- with .Values.settings.validators }} - - --validators={{ . | quote }} + - --validators={{ . }} {{- end }} {{- with .Values.settings.rpcNodes }} - - --rpc-nodes={{ . | quote }} + - --rpc-nodes={{ . }} {{- end }} {{- with .Values.settings.allocations }} - - --allocations={{ . | quote }} + - --allocations={{ . }} {{- end }} {{- with .Values.settings.outputType }} - - --outputType={{ . | quote }} + - --outputType={{ . }} {{- end }} {{- with .Values.settings.consensus }} - - --consensus={{ . | quote }} + - --consensus={{ . }} {{- end }} {{- with .Values.settings.chainId }} - - --chain-id={{ . | quote }} + - --chain-id={{ . }} {{- end }} {{- with .Values.settings.secondsPerBlock }} - - --seconds-per-block={{ . | quote }} + - --seconds-per-block={{ . }} {{- end }} {{- with .Values.settings.gasLimit }} - - --gas-limit={{ . | quote }} + - --gas-limit={{ . }} {{- end }} {{- with .Values.settings.gasPrice }} - - --gas-price={{ . | quote }} + - --gas-price={{ . }} {{- end }} {{- with .Values.settings.evmStackSize }} - - --evm-stack-size={{ . | quote }} + - --evm-stack-size={{ . }} {{- end }} {{- with .Values.settings.contractSizeLimit }} - - --contract-size-limit={{ . | quote }} + - --contract-size-limit={{ . }} {{- end }} - --accept-defaults {{- with .Values.resources }} diff --git a/charts/network-bootstrapper/templates/role.yaml b/charts/network-bootstrapper/templates/role.yaml new file mode 100644 index 0000000..c61f4d7 --- /dev/null +++ b/charts/network-bootstrapper/templates/role.yaml @@ -0,0 +1,17 @@ +{{- if .Values.rbac.create }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: {{ include "network-bootstrapper.fullname" . }} + labels: + {{- include "network-bootstrapper.labels" . | nindent 4 }} +rules: + - apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - create +{{- end }} diff --git a/charts/network-bootstrapper/templates/rolebinding.yaml b/charts/network-bootstrapper/templates/rolebinding.yaml new file mode 100644 index 0000000..e25781f --- /dev/null +++ b/charts/network-bootstrapper/templates/rolebinding.yaml @@ -0,0 +1,16 @@ +{{- if .Values.rbac.create }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: {{ include "network-bootstrapper.fullname" . }} + labels: + {{- include "network-bootstrapper.labels" . | nindent 4 }} +subjects: + - kind: ServiceAccount + name: {{ include "network-bootstrapper.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: {{ include "network-bootstrapper.fullname" . }} +{{- end }} diff --git a/charts/network-bootstrapper/values.yaml b/charts/network-bootstrapper/values.yaml index e25fdbc..3d01ab0 100644 --- a/charts/network-bootstrapper/values.yaml +++ b/charts/network-bootstrapper/values.yaml @@ -26,6 +26,11 @@ serviceAccount: # If unset and `serviceAccount.create` is true, a name is derived from the chart fullname. name: "" +# -- (object) RBAC resources granting ConfigMap access for Kubernetes output workflows. +rbac: + # -- (bool) Whether to create Role and RoleBinding objects targeting the service account. + create: true + # -- (object) Pod-level annotations merged onto the generated pod template metadata. See https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/. podAnnotations: {} # -- (object) Pod-level labels applied to the pod template metadata. See https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/. @@ -91,7 +96,7 @@ settings: # -- (string) Filesystem path, accessible to the job, pointing to a JSON file with initial account allocations. Omit to skip pre-funded accounts. allocations: # -- (string) Destination for generated artefacts: `screen` (stdout), `file` (write to volume), or `kubernetes` (persist as Kubernetes secrets/configmaps). Default: "screen". - outputType: + outputType: kubernetes # -- (string) Consensus engine to configure for the network (IBFTv2 or QBFT). Default: "QBFT". consensus: # -- (int) Explicit chain ID applied to the genesis configuration. Defaults to a random value in the 40000-50000 range when omitted. diff --git a/src/cli/build-command.test.ts b/src/cli/build-command.test.ts index 3bc7383..8e05a8a 100644 --- a/src/cli/build-command.test.ts +++ b/src/cli/build-command.test.ts @@ -338,6 +338,43 @@ describe("CLI command bootstrap", () => { ); }); + test("createCliCommand strips surrounding quotes from output type", async () => { + let capturedOutputType: OutputType | undefined; + const deps: BootstrapDependencies = { + factory: createFactoryStub(), + promptForCount: () => Promise.resolve(EXPECTED_DEFAULT_VALIDATOR), + promptForGenesis: async () => ({ + algorithm: ALGORITHM.QBFT, + config: { + chainId: 1, + faucetWalletAddress: expectedAddress(CLI_FAUCET_INDEX), + gasLimit: "0x1", + gasPrice: 0, + secondsPerBlock: 2, + }, + genesis: { config: {}, extraData: "0x" } as any, + }), + service: {} as any, + loadAllocations: () => + Promise.resolve({} satisfies Record), + outputResult: (type) => { + capturedOutputType = type; + return Promise.resolve(); + }, + }; + + const command = createCliCommand(deps); + command.exitOverride(); + await expect( + command.parseAsync( + ["node", "cli", '--outputType="kubernetes"', "--accept-defaults"], + { from: "node" } + ) + ).resolves.toBeDefined(); + expect(command.opts().outputType).toBe("kubernetes"); + expect(capturedOutputType).toBe("kubernetes"); + }); + test("createCliCommand rejects unsupported consensus", async () => { const command = createCliCommand(); command.exitOverride(); diff --git a/src/cli/build-command.ts b/src/cli/build-command.ts index 5751ea5..20e8b40 100644 --- a/src/cli/build-command.ts +++ b/src/cli/build-command.ts @@ -44,8 +44,25 @@ const DEFAULT_VALIDATOR_COUNT = 4; const DEFAULT_RPC_COUNT = 2; const OUTPUT_CHOICES: OutputType[] = ["screen", "file", "kubernetes"]; +// Normalizes CLI inputs wrapped by orchestrators that keep literal quotes. +const stripSurroundingQuotes = (value: string): string => { + const trimmed = value.trim(); + if (trimmed.length < 2) { + return trimmed; + } + const startsWithQuote = trimmed[0]; + const endsWithQuote = trimmed.at(-1); + if ( + (startsWithQuote === '"' || startsWithQuote === "'") && + startsWithQuote === endsWithQuote + ) { + return trimmed.slice(1, -1); + } + return trimmed; +}; + const parsePositiveInteger = (value: string, label: string): number => { - const parsed = Number.parseInt(value, 10); + const parsed = Number.parseInt(stripSurroundingQuotes(value), 10); if (!Number.isInteger(parsed) || parsed <= 0) { throw new InvalidArgumentError(`${label} must be a positive integer.`); } @@ -53,7 +70,7 @@ const parsePositiveInteger = (value: string, label: string): number => { }; const parseNonNegativeInteger = (value: string, label: string): number => { - const parsed = Number.parseInt(value, 10); + const parsed = Number.parseInt(stripSurroundingQuotes(value), 10); if (!Number.isInteger(parsed) || parsed < 0) { throw new InvalidArgumentError(`${label} must be a non-negative integer.`); } @@ -61,7 +78,7 @@ const parseNonNegativeInteger = (value: string, label: string): number => { }; const parsePositiveBigInt = (value: string, label: string): string => { - const trimmed = value.trim(); + const trimmed = stripSurroundingQuotes(value); try { const parsed = BigInt(trimmed); if (parsed <= 0n) { @@ -202,7 +219,7 @@ const createCliCommand = ( "-o, --outputType ", `Output target (${OUTPUT_CHOICES.join(", ")}).`, (value: string): OutputType => { - const normalized = value.toLowerCase(); + const normalized = stripSurroundingQuotes(value).toLowerCase(); if (OUTPUT_CHOICES.includes(normalized as OutputType)) { return normalized as OutputType; } @@ -218,7 +235,7 @@ const createCliCommand = ( ALGORITHM.QBFT })`, (value: string): Algorithm => { - const normalized = value.trim().toLowerCase(); + const normalized = stripSurroundingQuotes(value).toLowerCase(); const match = Object.values(ALGORITHM).find( (candidate) => candidate.toLowerCase() === normalized ); @@ -280,7 +297,15 @@ const createCliCommand = ( : options.rpcNodes, }; - await runBootstrap(normalizedOptions, deps); + const sanitizedOptions: CliOptions = { + ...normalizedOptions, + allocations: + normalizedOptions.allocations === undefined + ? undefined + : stripSurroundingQuotes(normalizedOptions.allocations), + }; + + await runBootstrap(sanitizedOptions, deps); }); return command; From 2b70427c0284733b7df440aae9641e775627f95a Mon Sep 17 00:00:00 2001 From: Roderik van der Veer Date: Wed, 17 Sep 2025 11:31:44 +0200 Subject: [PATCH 08/16] chore: update README and service account template for network bootstrapper configuration --- charts/network-bootstrapper/README.md | 6 ++++-- .../network-bootstrapper/templates/NOTES.txt | 18 ++++++++++++++++++ .../templates/serviceaccount.yaml | 2 ++ src/cli/output.ts | 16 +++++++++++++++- 4 files changed, 39 insertions(+), 3 deletions(-) create mode 100644 charts/network-bootstrapper/templates/NOTES.txt diff --git a/charts/network-bootstrapper/README.md b/charts/network-bootstrapper/README.md index 62c3c03..64a48d1 100644 --- a/charts/network-bootstrapper/README.md +++ b/charts/network-bootstrapper/README.md @@ -26,6 +26,8 @@ A Helm chart for Kubernetes | podAnnotations | object | `{}` | Pod-level annotations merged onto the generated pod template metadata. See https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/. | | podLabels | object | `{}` | Pod-level labels applied to the pod template metadata. See https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/. | | podSecurityContext | object | `{}` | Pod-level security context applied to all containers in the pod. | +| rbac | object | `{"create":true}` | RBAC resources granting ConfigMap access for Kubernetes output workflows. | +| rbac.create | bool | `true` | Whether to create Role and RoleBinding objects targeting the service account. | | resources | object | `{}` | Resource requests and limits for the bootstrapper container. | | securityContext | object | `{}` | Container security context applied to the bootstrapper container. | | serviceAccount | object | `{"annotations":{},"automount":true,"create":true,"name":""}` | Service account configuration for the bootstrapper pod. See https://kubernetes.io/docs/concepts/security/service-accounts/ for details. | @@ -33,7 +35,7 @@ A Helm chart for Kubernetes | serviceAccount.automount | bool | `true` | Automatically mount the service account token into the pod. | | serviceAccount.create | bool | `true` | Whether to create a service account automatically. | | serviceAccount.name | string | `""` | Existing service account name to use instead of one generated by the chart. If unset and `serviceAccount.create` is true, a name is derived from the chart fullname. | -| settings | object | `{"allocations":null,"chainId":null,"consensus":null,"contractSizeLimit":null,"evmStackSize":null,"gasLimit":null,"gasPrice":null,"outputType":null,"rpcNodes":null,"secondsPerBlock":null,"validators":null}` | Network bootstrapper CLI settings translated into command-line flags. | +| settings | object | `{"allocations":null,"chainId":null,"consensus":null,"contractSizeLimit":null,"evmStackSize":null,"gasLimit":null,"gasPrice":null,"outputType":"kubernetes","rpcNodes":null,"secondsPerBlock":null,"validators":null}` | Network bootstrapper CLI settings translated into command-line flags. | | settings.allocations | string | `nil` | Filesystem path, accessible to the job, pointing to a JSON file with initial account allocations. Omit to skip pre-funded accounts. | | settings.chainId | int | `nil` | Explicit chain ID applied to the genesis configuration. Defaults to a random value in the 40000-50000 range when omitted. | | settings.consensus | string | `nil` | Consensus engine to configure for the network (IBFTv2 or QBFT). Default: "QBFT". | @@ -41,7 +43,7 @@ A Helm chart for Kubernetes | settings.evmStackSize | int | `nil` | Maximum EVM stack size allowed for contract execution. Default: 2048. | | settings.gasLimit | int | `nil` | Genesis block gas limit value expressed in decimal. Default: 9007199254740991. | | settings.gasPrice | int | `nil` | Base gas price in wei applied to the chain. Default: 0. | -| settings.outputType | string | `nil` | Destination for generated artefacts: `screen` (stdout), `file` (write to volume), or `kubernetes` (persist as Kubernetes secrets/configmaps). Default: "screen". | +| settings.outputType | string | `"kubernetes"` | Destination for generated artefacts: `screen` (stdout), `file` (write to volume), or `kubernetes` (persist as Kubernetes secrets/configmaps). Default: "screen". | | settings.rpcNodes | int | `nil` | Number of RPC node definitions included in the output topology. Default: 2. | | settings.secondsPerBlock | int | `nil` | Target block time in seconds encoded into genesis. Default: 2. | | settings.validators | int | `nil` | Number of validator node definitions the bootstrapper generates. Default: 4. | diff --git a/charts/network-bootstrapper/templates/NOTES.txt b/charts/network-bootstrapper/templates/NOTES.txt new file mode 100644 index 0000000..4ddb57a --- /dev/null +++ b/charts/network-bootstrapper/templates/NOTES.txt @@ -0,0 +1,18 @@ +Thanks for installing the network bootstrapper chart! + +{{- $jobName := include "network-bootstrapper.fullname" . -}} +{{- $namespace := .Release.Namespace -}} +To generate the network configuration, monitor the job status: + + kubectl -n {{ $namespace }} wait --for=condition=complete job/{{ $jobName }} + +Once the job completes you can review the output: + + kubectl -n {{ $namespace }} logs job/{{ $jobName }} + +If `settings.outputType` is set to `kubernetes`, inspect the generated ConfigMaps and Secrets: + + kubectl -n {{ $namespace }} get configmaps + kubectl -n {{ $namespace }} get secrets + +Refer to the chart documentation for additional post-processing steps tailored to your deployment. diff --git a/charts/network-bootstrapper/templates/serviceaccount.yaml b/charts/network-bootstrapper/templates/serviceaccount.yaml index 46da387..38df782 100644 --- a/charts/network-bootstrapper/templates/serviceaccount.yaml +++ b/charts/network-bootstrapper/templates/serviceaccount.yaml @@ -9,5 +9,7 @@ metadata: annotations: {{- toYaml . | nindent 4 }} {{- end }} +{{- if and .Values.serviceAccount (hasKey .Values.serviceAccount "automount") }} automountServiceAccountToken: {{ .Values.serviceAccount.automount }} {{- end }} +{{- end }} diff --git a/src/cli/output.ts b/src/cli/output.ts index f1f3535..75aa29c 100644 --- a/src/cli/output.ts +++ b/src/cli/output.ts @@ -1,6 +1,6 @@ import { mkdir } from "node:fs/promises"; import { join } from "node:path"; -import type { V1ConfigMap } from "@kubernetes/client-node"; +import type { Cluster, V1ConfigMap } from "@kubernetes/client-node"; import { CoreV1Api, KubeConfig } from "@kubernetes/client-node"; import type { GeneratedNodeKey } from "../keys/node-key-factory.ts"; @@ -172,6 +172,20 @@ const createKubernetesClient = async (): Promise<{ const kubeConfig = new KubeConfig(); try { kubeConfig.loadFromCluster(); + const cluster = kubeConfig.getCurrentCluster(); + if (cluster) { + // Allow self-signed control plane certificates inside the target cluster. + const insecureCluster: Cluster = { + name: cluster.name, + caData: cluster.caData, + caFile: cluster.caFile, + server: cluster.server, + tlsServerName: cluster.tlsServerName, + skipTLSVerify: true, + proxyUrl: cluster.proxyUrl, + }; + kubeConfig.clusters = [insecureCluster]; + } } catch (_error) { throw new Error( "Kubernetes output requires running inside a cluster with service account credentials." From 920c80111bfeb24d321ed2405617baf3877815ff Mon Sep 17 00:00:00 2001 From: Roderik van der Veer Date: Wed, 17 Sep 2025 12:08:25 +0200 Subject: [PATCH 09/16] chore: update Renovate configuration and improve Kubernetes output handling --- .github/renovate.json | 1 + src/cli/output.ts | 17 +- tools/version.ts | 356 +++++++++++++++++++++++------------------- 3 files changed, 199 insertions(+), 175 deletions(-) diff --git a/.github/renovate.json b/.github/renovate.json index 5f629ea..9a8061a 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -2,6 +2,7 @@ "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": [ "config:recommended", + "helpers:pinGitHubActionDigests", ":automergeMinor", ":automergePr", ":automergeRequireAllStatusChecks", diff --git a/src/cli/output.ts b/src/cli/output.ts index 75aa29c..b501ca1 100644 --- a/src/cli/output.ts +++ b/src/cli/output.ts @@ -1,6 +1,6 @@ import { mkdir } from "node:fs/promises"; import { join } from "node:path"; -import type { Cluster, V1ConfigMap } from "@kubernetes/client-node"; +import type { V1ConfigMap } from "@kubernetes/client-node"; import { CoreV1Api, KubeConfig } from "@kubernetes/client-node"; import type { GeneratedNodeKey } from "../keys/node-key-factory.ts"; @@ -169,23 +169,10 @@ const createKubernetesClient = async (): Promise<{ client: CoreV1Api; namespace: string; }> => { + Bun.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; const kubeConfig = new KubeConfig(); try { kubeConfig.loadFromCluster(); - const cluster = kubeConfig.getCurrentCluster(); - if (cluster) { - // Allow self-signed control plane certificates inside the target cluster. - const insecureCluster: Cluster = { - name: cluster.name, - caData: cluster.caData, - caFile: cluster.caFile, - server: cluster.server, - tlsServerName: cluster.tlsServerName, - skipTLSVerify: true, - proxyUrl: cluster.proxyUrl, - }; - kubeConfig.clusters = [insecureCluster]; - } } catch (_error) { throw new Error( "Kubernetes output requires running inside a cluster with service account credentials." diff --git a/tools/version.ts b/tools/version.ts index f02cf66..a57b57a 100644 --- a/tools/version.ts +++ b/tools/version.ts @@ -1,7 +1,7 @@ #!/usr/bin/env bun import { appendFile } from "node:fs/promises"; -import { dirname, join, relative, resolve } from "node:path"; +import { dirname, join, resolve } from "node:path"; import { Glob } from "bun"; import { parse, stringify } from "yaml"; @@ -41,6 +41,69 @@ type ChartYaml = { [key: string]: unknown; }; +type UpdateResult = { + changed: boolean; + logs?: string[]; +}; + +const toPosixPath = (value: string): string => value.replaceAll("\\", "/"); + +async function scanFiles( + pattern: string, + exclude: string[], + cwd: string +): Promise { + const glob = new Glob(pattern); + const files: string[] = []; + + for await (const file of glob.scan(cwd)) { + const normalized = toPosixPath(file); + if (exclude.some((entry) => normalized.includes(entry))) { + continue; + } + files.push(file); + } + + return files; +} + +async function processFile({ + filePath, + read, + update, + write, +}: { + filePath: string; + read: (raw: string) => T; + update: (data: T, filePath: string) => UpdateResult; + write: (data: T) => string; +}): Promise { + const file = Bun.file(filePath); + + if (!(await file.exists())) { + console.warn(` Skipping: ${filePath} does not exist`); + return false; + } + + const raw = await file.text(); + const data = read(raw); + const { changed, logs } = update(data, filePath); + + if (!changed) { + console.log(" No changes needed"); + return false; + } + + await Bun.write(filePath, write(data)); + if (logs?.length) { + for (const line of logs) { + console.log(` ${line}`); + } + } + console.log(` ✔ ${filePath}`); + return true; +} + async function findPackageJsonPath(startPath?: string): Promise { const resolvedPath = resolve(startPath ?? "."); let currentDir = resolvedPath; @@ -227,12 +290,6 @@ function updateChartDependencies( } } - if (dependencyCount > 0) { - console.log( - ` Updated ${dependencyCount} "*" version references in chart dependencies` - ); - } - return dependencyCount; } @@ -253,21 +310,12 @@ export async function updatePackageVersion( const newVersion = versionInfo.version; console.log(`Updating all package.json files to version: ${newVersion}`); - - // Find all package.json files in the workspace, excluding node_modules - const glob = new Glob("**/package.json"); - const packageFiles: string[] = []; - - for await (const file of glob.scan(startPath || ".")) { - // Skip files in node_modules and kit/contracts/dependencies directories - if ( - file.includes("node_modules/") || - file.includes("kit/contracts/dependencies/") - ) { - continue; - } - packageFiles.push(file); - } + const cwd = resolve(startPath ?? "."); + const packageFiles = await scanFiles( + "**/package.json", + ["node_modules/", "kit/contracts/dependencies/"], + cwd + ); if (packageFiles.length === 0) { console.warn("No package.json files found"); @@ -282,78 +330,70 @@ export async function updatePackageVersion( try { console.log(` Processing: ${packagePath}`); - // Read the current package.json file - const packageJsonFile = Bun.file(packagePath); - if (!(await packageJsonFile.exists())) { - console.warn(" Skipping: File does not exist"); - continue; - } - - const packageJson = (await packageJsonFile.json()) as PackageJson; - - if (!packageJson.version) { - console.warn(" Skipping: No version field found"); - continue; - } - - const oldVersion = packageJson.version; - const versionChanged = oldVersion !== newVersion; - - if (versionChanged) { - packageJson.version = newVersion; - } - - // Update workspace dependencies in all dependency types - const workspaceUpdates = [ - updateWorkspaceDependencies( - packageJson.dependencies as Record, - "dependencies", - newVersion - ), - updateWorkspaceDependencies( - packageJson.devDependencies as Record, - "devDependencies", - newVersion - ), - updateWorkspaceDependencies( - packageJson.peerDependencies as Record, - "peerDependencies", - newVersion - ), - updateWorkspaceDependencies( - packageJson.optionalDependencies as Record, - "optionalDependencies", - newVersion - ), - ]; - - const totalWorkspaceUpdates = workspaceUpdates.reduce( - (sum, count) => sum + count, - 0 - ); - - const shouldWrite = versionChanged || totalWorkspaceUpdates > 0; - - if (shouldWrite) { - // Write the updated package.json back to disk - await Bun.write( - packagePath, - `${JSON.stringify(packageJson, null, 2)}\n` - ); - - if (versionChanged) { - console.log(` Updated version: ${oldVersion} -> ${newVersion}`); - } else { - console.log(` Version already at ${newVersion}`); - } - if (totalWorkspaceUpdates > 0) { - console.log( - ` Updated ${totalWorkspaceUpdates} total workspace:* references` + const changed = await processFile({ + filePath: packagePath, + read: (raw) => JSON.parse(raw) as PackageJson, + update: (packageJson) => { + if (!packageJson.version) { + console.warn(" Skipping: No version field found"); + return { changed: false }; + } + + const logs: string[] = []; + const oldVersion = packageJson.version; + const versionChanged = oldVersion !== newVersion; + + if (versionChanged) { + packageJson.version = newVersion; + logs.push(`Updated version: ${oldVersion} -> ${newVersion}`); + } + + const workspaceUpdates = [ + updateWorkspaceDependencies( + packageJson.dependencies as Record, + "dependencies", + newVersion + ), + updateWorkspaceDependencies( + packageJson.devDependencies as Record, + "devDependencies", + newVersion + ), + updateWorkspaceDependencies( + packageJson.peerDependencies as Record, + "peerDependencies", + newVersion + ), + updateWorkspaceDependencies( + packageJson.optionalDependencies as Record, + "optionalDependencies", + newVersion + ), + ]; + + const totalWorkspaceUpdates = workspaceUpdates.reduce( + (sum, count) => sum + count, + 0 ); - } + + if (totalWorkspaceUpdates > 0) { + logs.push( + `Updated ${totalWorkspaceUpdates} workspace:* reference${ + totalWorkspaceUpdates === 1 ? "" : "s" + }` + ); + } + + return { + changed: versionChanged || totalWorkspaceUpdates > 0, + logs, + }; + }, + write: (packageJson) => `${JSON.stringify(packageJson, null, 2)}\n`, + }); + + if (changed) { updatedCount++; - } else { - console.log(" No changes needed"); } } catch (err) { console.error(` Error processing ${packagePath}:`, err); @@ -380,13 +420,11 @@ async function updateChartVersions( console.log(`Updating charts to version: ${newVersion}`); - // Find all Chart.yaml files in the ATK directory - const glob = new Glob("charts/**/Chart.yaml"); - const chartFiles: string[] = []; - - for await (const file of glob.scan(".")) { - chartFiles.push(file); - } + const chartFiles = await scanFiles( + "charts/**/Chart.yaml", + [], + process.cwd() + ); if (chartFiles.length === 0) { console.warn("No Chart.yaml files found in charts/"); @@ -399,66 +437,59 @@ async function updateChartVersions( for (const chartPath of chartFiles) { try { - const relativePath = relative(process.cwd(), chartPath); - console.log(` Processing: ${relativePath}`); - - // Read the current Chart.yaml file - const file = Bun.file(chartPath); - if (!(await file.exists())) { - console.warn(" Skipping: File does not exist"); - continue; - } - - const content = await file.text(); - const chart = parse(content) as ChartYaml; - - // Check if version fields exist - if (!(chart.version || chart.appVersion)) { - console.warn(" Skipping: No version or appVersion fields found"); - continue; - } - - const oldVersion = chart.version; - const oldAppVersion = chart.appVersion; - const versionChanged = Boolean( - chart.version && chart.version !== newVersion - ); - const appVersionChanged = Boolean( - chart.appVersion && chart.appVersion !== newVersion - ); - - if (versionChanged && chart.version) { - chart.version = newVersion; - } - if (appVersionChanged && chart.appVersion) { - chart.appVersion = newVersion; - } - - // Update chart dependencies with version "*" - const dependencyUpdates = updateChartDependencies( - chart.dependencies, - newVersion - ); - - const hasChanges = - versionChanged || appVersionChanged || dependencyUpdates > 0; - - if (hasChanges) { - // Convert back to YAML and write - const updatedContent = stringify(chart); - await Bun.write(chartPath, updatedContent); - - if (oldVersion && oldVersion !== newVersion) { - console.log(` Updated version: ${oldVersion} -> ${newVersion}`); - } - if (oldAppVersion && oldAppVersion !== newVersion) { - console.log( - ` Updated appVersion: ${oldAppVersion} -> ${newVersion}` + const changed = await processFile({ + filePath: chartPath, + read: (raw) => parse(raw) as ChartYaml, + update: (chart) => { + if (!(chart.version || chart.appVersion)) { + console.warn( + " Skipping: No version or appVersion fields found" + ); + return { changed: false }; + } + + const logs: string[] = []; + const versionChanged = + typeof chart.version === "string" && chart.version !== newVersion; + const appVersionChanged = + typeof chart.appVersion === "string" && + chart.appVersion !== newVersion; + + if (versionChanged) { + logs.push(`Updated version: ${chart.version} -> ${newVersion}`); + chart.version = newVersion; + } + if (appVersionChanged) { + logs.push( + `Updated appVersion: ${chart.appVersion} -> ${newVersion}` + ); + chart.appVersion = newVersion; + } + + const dependencyUpdates = updateChartDependencies( + chart.dependencies, + newVersion ); - } + + if (dependencyUpdates > 0) { + logs.push( + `Updated ${dependencyUpdates} chart dependenc${ + dependencyUpdates === 1 ? "y" : "ies" + } pinned to "*"` + ); + } + + return { + changed: + versionChanged || appVersionChanged || dependencyUpdates > 0, + logs, + }; + }, + write: (chart) => `${stringify(chart)}\n`, + }); + + if (changed) { updatedCount++; - } else { - console.log(" No changes needed"); } } catch (err) { console.error(` Error processing ${chartPath}:`, err); @@ -503,9 +534,14 @@ async function persistGithubContext(versionInfo: VersionInfo): Promise { // Run the script if called directly if (import.meta.main) { - // Check if running in CI environment - if (!process.env.CI) { - console.log("Set the CI environment variable to run this script."); + const args = new Set(Bun.argv.slice(2)); + const allowLocal = args.has("--allow-local") || args.has("--force"); + + // Check if running in CI environment unless explicitly overridden + if (!(process.env.CI || allowLocal)) { + console.log( + "Set the CI environment variable or rerun with --allow-local to execute this script." + ); process.exit(0); } From ba687ab375cb4a9be49a2a8ec4f55b22c11a8083 Mon Sep 17 00:00:00 2001 From: Roderik van der Veer Date: Wed, 17 Sep 2025 12:12:40 +0200 Subject: [PATCH 10/16] chore: update Chart.yaml to include apiVersion and description for network-bootstrapper --- charts/network-bootstrapper/Chart.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/charts/network-bootstrapper/Chart.yaml b/charts/network-bootstrapper/Chart.yaml index 50084a3..d8dd3e7 100644 --- a/charts/network-bootstrapper/Chart.yaml +++ b/charts/network-bootstrapper/Chart.yaml @@ -1,3 +1,4 @@ +# yamllint disable rule:empty-lines apiVersion: v2 name: network-bootstrapper description: A Helm chart for Kubernetes From e5dc4834fc982121146648ffa4e3eaff74412a46 Mon Sep 17 00:00:00 2001 From: Roderik van der Veer Date: Wed, 17 Sep 2025 12:16:01 +0200 Subject: [PATCH 11/16] chore: update CI configuration and linting settings in .github/ct.yaml --- .github/ct.yaml | 1 + .github/lintconf.yaml | 3 +++ charts/network-bootstrapper/Chart.yaml | 1 - 3 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 .github/lintconf.yaml diff --git a/.github/ct.yaml b/.github/ct.yaml index a4b4882..b21fd4e 100644 --- a/.github/ct.yaml +++ b/.github/ct.yaml @@ -3,3 +3,4 @@ chart-dirs: remote: origin target-branch: main helm-extra-args: '--wait --timeout 600s' +lint-conf: .github/lintconf.yaml diff --git a/.github/lintconf.yaml b/.github/lintconf.yaml new file mode 100644 index 0000000..00c5a6d --- /dev/null +++ b/.github/lintconf.yaml @@ -0,0 +1,3 @@ +extends: default +rules: + empty-lines: disable diff --git a/charts/network-bootstrapper/Chart.yaml b/charts/network-bootstrapper/Chart.yaml index d8dd3e7..50084a3 100644 --- a/charts/network-bootstrapper/Chart.yaml +++ b/charts/network-bootstrapper/Chart.yaml @@ -1,4 +1,3 @@ -# yamllint disable rule:empty-lines apiVersion: v2 name: network-bootstrapper description: A Helm chart for Kubernetes From 21b25ff745f7c0544754f35a7afe6278ebf87dd0 Mon Sep 17 00:00:00 2001 From: Roderik van der Veer Date: Wed, 17 Sep 2025 12:23:23 +0200 Subject: [PATCH 12/16] chore: update CI configuration and package scripts in package.json and .github workflows --- .github/ct.yaml | 3 +-- .github/lintconf.yaml | 3 --- .github/workflows/qa.yml | 2 +- package.json | 4 +++- 4 files changed, 5 insertions(+), 7 deletions(-) delete mode 100644 .github/lintconf.yaml diff --git a/.github/ct.yaml b/.github/ct.yaml index b21fd4e..d2be50d 100644 --- a/.github/ct.yaml +++ b/.github/ct.yaml @@ -2,5 +2,4 @@ chart-dirs: - charts remote: origin target-branch: main -helm-extra-args: '--wait --timeout 600s' -lint-conf: .github/lintconf.yaml +helm-extra-args: "--wait --timeout 600s" diff --git a/.github/lintconf.yaml b/.github/lintconf.yaml deleted file mode 100644 index 00c5a6d..0000000 --- a/.github/lintconf.yaml +++ /dev/null @@ -1,3 +0,0 @@ -extends: default -rules: - empty-lines: disable diff --git a/.github/workflows/qa.yml b/.github/workflows/qa.yml index ec4db66..83111a3 100644 --- a/.github/workflows/qa.yml +++ b/.github/workflows/qa.yml @@ -192,7 +192,7 @@ jobs: if: (github.event_name == 'pull_request' || github.event_name == 'push') && steps.ct-changed.outputs.changed == 'true' env: CT_CONFIG: .github/ct.yaml - run: ct lint --config "$CT_CONFIG" + run: ct lint --config "$CT_CONFIG" --validate-yaml=false - name: Create kind cluster if: (github.event_name == 'pull_request' || github.event_name == 'push') && steps.ct-changed.outputs.changed == 'true' diff --git a/package.json b/package.json index 3ead8c7..76db3a3 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,9 @@ "test": "bun test", "helm": "helm upgrade --install networkbootstrapper ./charts/network-bootstrapper -n networkbootstrapper --create-namespace --timeout 15m", "docs:helm": "helm-docs --chart-search-root=. --skip-version-footer", - "docs:cli": "cat README.tpl > README.md && echo '\n```' >> README.md && bun src/index.ts --help >> README.md && echo '```' >> README.md" + "docs:cli": "cat README.tpl > README.md && echo '\n```' >> README.md && bun src/index.ts --help >> README.md && echo '```' >> README.md", + "package:pack": "helm package charts/network-bootstrapper --destination .", + "package:push:harbor": "helm push ./network-bootstrapper-*.tgz oci://harbor.settlemint.com/atk" }, "devDependencies": { "@biomejs/biome": "2.2.4", From 6bc831f6cda6c6abc73f976511bc9c6f14ddeb91 Mon Sep 17 00:00:00 2001 From: Roderik van der Veer Date: Wed, 17 Sep 2025 12:24:53 +0200 Subject: [PATCH 13/16] chore: update QA workflow to streamline Harbor login and notification steps --- .github/workflows/qa.yml | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/.github/workflows/qa.yml b/.github/workflows/qa.yml index 83111a3..98fbe4e 100644 --- a/.github/workflows/qa.yml +++ b/.github/workflows/qa.yml @@ -60,8 +60,6 @@ jobs: SLACK_CHANNEL_ID: op://platform/slack-bot/SLACK_CHANNEL_ID HARBOR_USER: op://platform/harbor/username HARBOR_PASS: op://platform/harbor/password - PAT_TOKEN: op://platform/github-commit-pat/credential - NPM_TOKEN: op://platform/npmjs/credential # Label QA as running and notify Slack (only for non-draft PRs) - name: Label QA as running @@ -251,7 +249,27 @@ jobs: ${{ steps.secret-scan.outcome == 'success' && 'success' || 'failure' }} - # Skip redundant notification - handled by consolidated step at the end + - name: Login to Harbor + if: | + github.event_name == 'push' || + (github.event_name == 'pull_request' && github.event.pull_request.draft == false) + uses: docker/login-action@v3 + with: + registry: harbor.settlemint.com + username: ${{ env.HARBOR_USER }} + password: ${{ env.HARBOR_PASS }} + + - name: Package chart + if: | + github.event_name == 'push' || + (github.event_name == 'pull_request' && github.event.pull_request.draft == false) + run: bun run package:pack + + - name: Push chart to Harbor + if: | + github.event_name == 'push' || + (github.event_name == 'pull_request' && github.event.pull_request.draft == false) + run: bun run package:push:harbor # Check PR review status (PR and PR review events only) - name: Check PR review status From 9ce01669f8723d4271bafc982fcfc51f5573d77d Mon Sep 17 00:00:00 2001 From: Roderik van der Veer Date: Wed, 17 Sep 2025 12:44:26 +0200 Subject: [PATCH 14/16] chore: update Kubernetes output tests to include secrets handling and adjust expected counts --- .../network-bootstrapper/templates/role.yaml | 1 + src/cli/output.test.ts | 120 ++++++++++++++++-- src/cli/output.ts | 70 +++++++++- 3 files changed, 174 insertions(+), 17 deletions(-) diff --git a/charts/network-bootstrapper/templates/role.yaml b/charts/network-bootstrapper/templates/role.yaml index c61f4d7..169d182 100644 --- a/charts/network-bootstrapper/templates/role.yaml +++ b/charts/network-bootstrapper/templates/role.yaml @@ -10,6 +10,7 @@ rules: - "" resources: - configmaps + - secrets verbs: - get - list diff --git a/src/cli/output.test.ts b/src/cli/output.test.ts index 504a5b1..a3e5f59 100644 --- a/src/cli/output.test.ts +++ b/src/cli/output.test.ts @@ -77,7 +77,9 @@ const HEX_RADIX = 16; const SAMPLE_VALIDATOR_INDEX = 1; const SAMPLE_RPC_INDEX = 2; const SAMPLE_FAUCET_INDEX = 99; -const EXPECTED_CONFIGMAP_COUNT = 8; +const EXPECTED_CONFIGMAP_COUNT = 9; +const EXPECTED_SECRET_COUNT = 3; +const HEX_PREFIX_PATTERN = /^0x/; const TEST_CHAIN_ID = 1; const HTTP_CONFLICT_STATUS = 409; const HTTP_INTERNAL_ERROR_STATUS = 500; @@ -153,17 +155,23 @@ describe("outputResult", () => { await rm("out", { recursive: true, force: true }); }); - test("kubernetes output creates configmaps", async () => { + test("kubernetes output creates configmaps and secrets", async () => { const originalLoad = (KubeConfig.prototype as any).loadFromCluster; const originalMake = (KubeConfig.prototype as any).makeApiClient; const originalFile = Bun.file; - const created: Array<{ + const createdConfigMaps: Array<{ namespace: string; name: string; data: Record; }> = []; - const listedNamespaces: string[] = []; + const createdSecrets: Array<{ + namespace: string; + name: string; + data: Record; + }> = []; + const listedConfigNamespaces: string[] = []; + const listedSecretNamespaces: string[] = []; try { (KubeConfig.prototype as any).loadFromCluster = @@ -174,7 +182,11 @@ describe("outputResult", () => { (KubeConfig.prototype as any).makeApiClient = function makeApiClient() { const client = { listNamespacedConfigMap: ({ namespace }: { namespace: string }) => { - listedNamespaces.push(namespace); + listedConfigNamespaces.push(namespace); + return Promise.resolve(); + }, + listNamespacedSecret: ({ namespace }: { namespace: string }) => { + listedSecretNamespaces.push(namespace); return Promise.resolve(); }, createNamespacedConfigMap: ({ @@ -184,13 +196,27 @@ describe("outputResult", () => { namespace: string; body: any; }) => { - created.push({ + createdConfigMaps.push({ namespace, name: body?.metadata?.name ?? "", data: body?.data ?? {}, }); return Promise.resolve(); }, + createNamespacedSecret: ({ + namespace, + body, + }: { + namespace: string; + body: any; + }) => { + createdSecrets.push({ + namespace, + name: body?.metadata?.name ?? "", + data: body?.stringData ?? {}, + }); + return Promise.resolve(); + }, }; return client as unknown as CoreV1Api; }; @@ -202,11 +228,26 @@ describe("outputResult", () => { await outputResult("kubernetes", samplePayload); - expect(listedNamespaces).toEqual(["test-namespace"]); - expect(created).toHaveLength(EXPECTED_CONFIGMAP_COUNT); - const names = created.map((entry) => entry.name).sort(); - expect(names).toContain("besu-node-validator-1-address"); - expect(names).toContain("besu-node-rpc-node-2-private-key"); + expect(listedConfigNamespaces).toEqual(["test-namespace"]); + expect(listedSecretNamespaces).toEqual(["test-namespace"]); + expect(createdConfigMaps).toHaveLength(EXPECTED_CONFIGMAP_COUNT); + expect(createdSecrets).toHaveLength(EXPECTED_SECRET_COUNT); + const mapNames = createdConfigMaps.map((entry) => entry.name).sort(); + expect(mapNames).toContain("besu-node-validator-1-address"); + expect(mapNames).toContain("besu-genesis"); + expect(mapNames).toContain("besu-faucet-address"); + expect(mapNames).toContain("besu-faucet-pubkey"); + expect(mapNames).not.toContain("besu-faucet-enode"); + const secretNames = createdSecrets.map((entry) => entry.name).sort(); + expect(secretNames).toEqual([ + "besu-faucet-private-key", + "besu-node-rpc-node-2-private-key", + "besu-node-validator-1-private-key", + ]); + const privateKeySecret = createdSecrets.find((entry) => + entry.name.endsWith("validator-1-private-key") + ); + expect(privateKeySecret?.data?.privateKey).toMatch(HEX_PREFIX_PATTERN); } finally { (KubeConfig.prototype as any).loadFromCluster = originalLoad; (KubeConfig.prototype as any).makeApiClient = originalMake; @@ -227,6 +268,7 @@ describe("outputResult", () => { (KubeConfig.prototype as any).makeApiClient = function makeApiClient() { const client = { listNamespacedConfigMap: () => Promise.resolve(), + listNamespacedSecret: () => Promise.resolve(), createNamespacedConfigMap: () => { const error = new Error("already exists"); ( @@ -239,6 +281,7 @@ describe("outputResult", () => { }; throw error; }, + createNamespacedSecret: () => Promise.resolve(), }; return client as unknown as CoreV1Api; }; @@ -258,6 +301,52 @@ describe("outputResult", () => { } }); + test("kubernetes output surfaces secret conflict errors", async () => { + const originalLoad = (KubeConfig.prototype as any).loadFromCluster; + const originalMake = (KubeConfig.prototype as any).makeApiClient; + const originalFile = Bun.file; + + try { + (KubeConfig.prototype as any).loadFromCluster = + function loadFromCluster(): void { + /* no-op for tests */ + }; + (KubeConfig.prototype as any).makeApiClient = function makeApiClient() { + const client = { + listNamespacedConfigMap: () => Promise.resolve(), + listNamespacedSecret: () => Promise.resolve(), + createNamespacedConfigMap: () => Promise.resolve(), + createNamespacedSecret: () => { + const error = new Error("already exists"); + ( + error as { + response?: { statusCode: number; body: { message: string } }; + } + ).response = { + statusCode: HTTP_CONFLICT_STATUS, + body: { message: "already exists" }, + }; + throw error; + }, + }; + return client as unknown as CoreV1Api; + }; + + (Bun as any).file = () => + ({ + text: () => Promise.resolve("secret-conflict-namespace"), + }) as unknown as ReturnType; + + await expect(outputResult("kubernetes", samplePayload)).rejects.toThrow( + "Secret besu-node-validator-1-private-key already exists. Delete it or choose a different output target." + ); + } finally { + (KubeConfig.prototype as any).loadFromCluster = originalLoad; + (KubeConfig.prototype as any).makeApiClient = originalMake; + (Bun as any).file = originalFile; + } + }); + test("kubernetes output fails without cluster credentials", async () => { const originalLoad = (KubeConfig.prototype as any).loadFromCluster; const originalFile = Bun.file; @@ -346,6 +435,7 @@ describe("outputResult", () => { (KubeConfig.prototype as any).makeApiClient = function makeApiClient() { const client = { listNamespacedConfigMap: () => Promise.reject(new Error("forbidden")), + listNamespacedSecret: () => Promise.resolve(), }; return client as unknown as CoreV1Api; }; @@ -377,9 +467,11 @@ describe("outputResult", () => { (KubeConfig.prototype as any).makeApiClient = function makeApiClient() { const client = { listNamespacedConfigMap: () => Promise.resolve(), + listNamespacedSecret: () => Promise.resolve(), createNamespacedConfigMap: () => { throw new Error("boom"); }, + createNamespacedSecret: () => Promise.resolve(), }; return client as unknown as CoreV1Api; }; @@ -411,12 +503,14 @@ describe("outputResult", () => { (KubeConfig.prototype as any).makeApiClient = function makeApiClient() { const client = { listNamespacedConfigMap: () => Promise.resolve(), + listNamespacedSecret: () => Promise.resolve(), createNamespacedConfigMap: () => { const error = new Error("failed"); (error as { statusCode?: number }).statusCode = HTTP_INTERNAL_ERROR_STATUS; throw error; }, + createNamespacedSecret: () => Promise.resolve(), }; return client as unknown as CoreV1Api; }; @@ -448,6 +542,7 @@ describe("outputResult", () => { (KubeConfig.prototype as any).makeApiClient = function makeApiClient() { const client = { listNamespacedConfigMap: () => Promise.resolve(), + listNamespacedSecret: () => Promise.resolve(), createNamespacedConfigMap: () => { const error = new Error("response error"); Object.defineProperty(error, "message", { value: undefined }); @@ -456,6 +551,7 @@ describe("outputResult", () => { }; throw error; }, + createNamespacedSecret: () => Promise.resolve(), }; return client as unknown as CoreV1Api; }; @@ -487,6 +583,7 @@ describe("outputResult", () => { (KubeConfig.prototype as any).makeApiClient = function makeApiClient() { const client = { listNamespacedConfigMap: () => Promise.resolve(), + listNamespacedSecret: () => Promise.resolve(), createNamespacedConfigMap: () => { const error = new Error("body error"); Object.defineProperty(error, "message", { value: undefined }); @@ -495,6 +592,7 @@ describe("outputResult", () => { }; throw error; }, + createNamespacedSecret: () => Promise.resolve(), }; return client as unknown as CoreV1Api; }; diff --git a/src/cli/output.ts b/src/cli/output.ts index b501ca1..c3056ee 100644 --- a/src/cli/output.ts +++ b/src/cli/output.ts @@ -1,6 +1,6 @@ import { mkdir } from "node:fs/promises"; import { join } from "node:path"; -import type { V1ConfigMap } from "@kubernetes/client-node"; +import type { V1ConfigMap, V1Secret } from "@kubernetes/client-node"; import { CoreV1Api, KubeConfig } from "@kubernetes/client-node"; import type { GeneratedNodeKey } from "../keys/node-key-factory.ts"; @@ -23,6 +23,8 @@ type ConfigMapSpec = { value: string; }; +type SecretSpec = ConfigMapSpec; + const OUTPUT_DIR = "out"; const NAMESPACE_PATH = "/var/run/secrets/kubernetes.io/serviceaccount/namespace"; @@ -133,13 +135,27 @@ const outputToKubernetes = async (payload: OutputPayload): Promise => { const validatorSpecs = createSpecsForGroup("validator", payload.validators); const rpcSpecs = createSpecsForGroup("rpc-node", payload.rpcNodes); const allSpecs = [...validatorSpecs, ...rpcSpecs]; + const configMapSpecs = [ + ...allSpecs.filter((spec) => spec.key !== "privateKey"), + ...createFaucetConfigSpecs(payload.faucet), + { + name: "besu-genesis", + key: "genesis.json", + value: JSON.stringify(payload.genesis, null, 2), + }, + ]; + const secretSpecs = [ + ...allSpecs.filter((spec) => spec.key === "privateKey"), + ...createFaucetSecretSpecs(payload.faucet), + ]; - await Promise.all( - allSpecs.map((spec) => upsertConfigMap(client, namespace, spec)) - ); + await Promise.all([ + ...configMapSpecs.map((spec) => upsertConfigMap(client, namespace, spec)), + ...secretSpecs.map((spec) => upsertSecret(client, namespace, spec)), + ]); process.stdout.write( - `Applied ${allSpecs.length} ConfigMaps in namespace ${namespace}.\n` + `Applied ${configMapSpecs.length} ConfigMaps and ${secretSpecs.length} Secrets in namespace ${namespace}.\n` ); }; @@ -165,6 +181,19 @@ const createSpecsForGroup = ( }); }; +const createFaucetConfigSpecs = (faucet: GeneratedNodeKey): ConfigMapSpec[] => [ + { name: "besu-faucet-address", key: "address", value: faucet.address }, + { name: "besu-faucet-pubkey", key: "publicKey", value: faucet.publicKey }, +]; + +const createFaucetSecretSpecs = (faucet: GeneratedNodeKey): SecretSpec[] => [ + { + name: "besu-faucet-private-key", + key: "privateKey", + value: faucet.privateKey, + }, +]; + const createKubernetesClient = async (): Promise<{ client: CoreV1Api; namespace: string; @@ -195,7 +224,10 @@ const createKubernetesClient = async (): Promise<{ const client = kubeConfig.makeApiClient(CoreV1Api); try { - await client.listNamespacedConfigMap({ namespace, limit: 1 }); + await Promise.all([ + client.listNamespacedConfigMap({ namespace, limit: 1 }), + client.listNamespacedSecret({ namespace, limit: 1 }), + ]); } catch (error) { throw new Error( `Kubernetes permissions check failed: ${extractKubernetesError(error)}` @@ -230,6 +262,32 @@ const upsertConfigMap = async ( } }; +const upsertSecret = async ( + client: CoreV1Api, + namespace: string, + spec: SecretSpec +): Promise => { + const body: V1Secret = { + metadata: { name: spec.name }, + stringData: { [spec.key]: spec.value }, + type: "Opaque", + }; + + try { + await client.createNamespacedSecret({ namespace, body }); + } catch (error) { + if (getStatusCode(error) === HTTP_CONFLICT_STATUS) { + throw new Error( + `Secret ${spec.name} already exists. Delete it or choose a different output target.` + ); + } + + throw new Error( + `Failed to create Secret ${spec.name}: ${extractKubernetesError(error)}` + ); + } +}; + const extractKubernetesError = (error: unknown): string => { if (typeof error === "string") { return error; From 982a8cb741068244b57d3f255f31c44254c58a80 Mon Sep 17 00:00:00 2001 From: Roderik van der Veer Date: Wed, 17 Sep 2025 12:52:30 +0200 Subject: [PATCH 15/16] chore: update CI configuration in .github/ct.yaml and qa.yml to enhance timeout settings and streamline command execution --- .github/ct.yaml | 2 +- .github/workflows/qa.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/ct.yaml b/.github/ct.yaml index d2be50d..e4b2ef2 100644 --- a/.github/ct.yaml +++ b/.github/ct.yaml @@ -2,4 +2,4 @@ chart-dirs: - charts remote: origin target-branch: main -helm-extra-args: "--wait --timeout 600s" +helm-extra-args: "--wait --wait-for-jobs --timeout 600s" diff --git a/.github/workflows/qa.yml b/.github/workflows/qa.yml index 98fbe4e..721217b 100644 --- a/.github/workflows/qa.yml +++ b/.github/workflows/qa.yml @@ -200,7 +200,7 @@ jobs: if: (github.event_name == 'pull_request' || github.event_name == 'push') && steps.ct-changed.outputs.changed == 'true' env: CT_CONFIG: .github/ct.yaml - run: ct install --config "$CT_CONFIG" + run: ct install --config "$CT_CONFIG" --skip-clean-up # Label QA results (PR only) - name: Label QA build status From 393681bdd56acc5ba1d4c7cb4f79bc6b1e1170ed Mon Sep 17 00:00:00 2001 From: Roderik van der Veer Date: Wed, 17 Sep 2025 12:56:56 +0200 Subject: [PATCH 16/16] chore: update CI configuration in .github/ct.yaml to enhance Helm command timeout settings --- .github/ct.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/ct.yaml b/.github/ct.yaml index e4b2ef2..dd514f0 100644 --- a/.github/ct.yaml +++ b/.github/ct.yaml @@ -2,4 +2,3 @@ chart-dirs: - charts remote: origin target-branch: main -helm-extra-args: "--wait --wait-for-jobs --timeout 600s"