diff --git a/.github/workflows/build-chart.yaml b/.github/workflows/build-chart.yaml new file mode 100644 index 000000000..2b24b4bde --- /dev/null +++ b/.github/workflows/build-chart.yaml @@ -0,0 +1,61 @@ +name: Operator Build + +on: + workflow_call: + +jobs: + operator-build: + runs-on: ubuntu-latest + env: + CLUSTER_NAME: paladin + NAMESPACE: paladin + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + submodules: recursive + + - name: Install pre-requisites + uses: ./.github/actions/setup + + - name: Install Kind + uses: helm/kind-action@v1 + with: + install_only: true # only install kind, the cluster creation is managed by the deploy step + ignore_failed_clean: true + + - name: Download docker artifacts + uses: actions/download-artifact@v4 + with: + path: /tmp # download all docker images to /tmp + pattern: paladin-* + merge-multiple: true + + - name: Load image + run: | + docker load --input /tmp/paladin-operator-${{ github.sha }}.tar + docker load --input /tmp/paladin-${{ github.sha }}.tar + docker image ls -a + + # The makefile uses kustomize + - uses: imranismail/setup-kustomize@v2 + + - name: Deploy Operator + run: | + ./gradlew deploy \ + -PclusterName=${{ env.CLUSTER_NAME }} \ + -Pnamespace=${{ env.NAMESPACE }} \ + -PbuildOperator=false \ + -PbuildPaladin=false \ + -PoperatorImageName=paladin.io/paladin-operator \ + -PoperatorImageTag=test \ + -PpaladinImageName=paladin.io/paladin \ + -PpaladinImageTag=test + + - name: Uninstall Operator + run: | + ./gradlew clean \ + -PclusterName=${{ env.CLUSTER_NAME }} \ + -Pnamespace=${{ env.NAMESPACE }} \ + -PdeleteCluster=true \ No newline at end of file diff --git a/.github/workflows/build-image.yaml b/.github/workflows/build-image.yaml index c161b5125..8b55258a0 100644 --- a/.github/workflows/build-image.yaml +++ b/.github/workflows/build-image.yaml @@ -106,3 +106,11 @@ jobs: tag=${{ steps.build_tag_generator.outputs.BUILD_TAG }} cache-from: type=gha cache-to: type=gha,mode=max + outputs: type=docker,dest=/tmp/${{ inputs.image }}-${{ github.sha }}.tar + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: ${{ inputs.image }}-${{ github.sha }} + path: /tmp/${{ inputs.image }}-${{ github.sha }}.tar + retention-days: 1 \ No newline at end of file diff --git a/.github/workflows/operator.yaml b/.github/workflows/operator.yaml deleted file mode 100644 index ffaecd993..000000000 --- a/.github/workflows/operator.yaml +++ /dev/null @@ -1,51 +0,0 @@ -name: Paladin Operator Build - -on: - pull_request: - paths: - - 'operator/**' - workflow_dispatch: - -# Ensure this workflow only runs for the most recent commit of a pull-request -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true - -jobs: - operator-build: - runs-on: ubuntu-latest - env: - CLUSTER_NAME: paladin - NAMESPACE: paladin - - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - submodules: recursive - - - name: Install pre-requisites - uses: ./.github/actions/setup - - # - name: Go Lint - # working-directory: operator - # run: make lint - - # - name: Unit Tests - # working-directory: operator - # run: make test - - - name: Install Kind - uses: helm/kind-action@v1 - with: - install_only: true # only install kind, the cluster creation is managed by the deploy step - ignore_failed_clean: true - - # The makefile uses kustomize - - uses: imranismail/setup-kustomize@v2 - - - name: Deploy Operator - run: ./gradlew deploy -PclusterName=${{ env.CLUSTER_NAME }} -Pnamespace=${{ env.NAMESPACE }} - - - name: Uninstall Operator - run: ./gradlew clean -PclusterName=${{ env.CLUSTER_NAME }} -Pnamespace=${{ env.NAMESPACE }} -PdeleteCluster=true \ No newline at end of file diff --git a/.github/workflows/paladin-PR-build.yml b/.github/workflows/paladin-PR-build.yml index 7ca48bc56..dce242773 100644 --- a/.github/workflows/paladin-PR-build.yml +++ b/.github/workflows/paladin-PR-build.yml @@ -13,7 +13,6 @@ on: pull_request: paths-ignore: - '**.md' - - 'operator/charts/**' workflow_dispatch: # Ensure this workflow only runs for the most recent commit of a pull-request @@ -73,6 +72,12 @@ jobs: platforms: linux/amd64 runs-on: ubuntu-latest + chart-build: + # run only if pull_request and the path operator/** was changed + if: github.event_name == 'pull_request' + needs: [core-image-build, operator-image-build] + uses: ./.github/workflows/build-chart.yaml + image-release: # run only on pushes to main or manual triggers if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' diff --git a/operator/Makefile b/operator/Makefile index 690cb6101..b22e803ca 100644 --- a/operator/Makefile +++ b/operator/Makefile @@ -1,11 +1,11 @@ CLUSTER_NAME ?= paladin NAMESPACE ?= default -OPERATOR_IMG_NAME ?= paladin-operator -OPERATOR_IMG_TAG ?= test -OPERATOR_IMG ?= ${OPERATOR_IMG_NAME}:${OPERATOR_IMG_TAG} -PALADIN_IMG_NAME ?= paladin -PALADIN_IMG_TAG ?= test +OPERATOR_IMAGE_NAME ?= paladin-operator +OPERATOR_IMAGE_TAG ?= test +OPERATOR_IMG ?= ${OPERATOR_IMAGE_NAME}:${OPERATOR_IMAGE_TAG} +PALADIN_IMAGE_NAME ?= paladin +PALADIN_IMAGE_TAG ?= test # USE_IMAGE_DIGESTS defines if images are resolved via tags or digests USE_IMAGE_DIGESTS ?= false @@ -112,7 +112,7 @@ kind-start: ## Create a Kind cluster. kind-promote: kind-start ## Load Docker images into Kind cluster. echo "Loading images into Kind cluster..." $(KIND_CLUSTER) load docker-image ${OPERATOR_IMG} --name "${CLUSTER_NAME}" - $(KIND_CLUSTER) load docker-image ${PALADIN_IMG_NAME}:${PALADIN_IMG_TAG} --name "${CLUSTER_NAME}" + $(KIND_CLUSTER) load docker-image ${PALADIN_IMAGE_NAME}:${PALADIN_IMAGE_TAG} --name "${CLUSTER_NAME}" .PHONY: kind-delete kind-delete: ## Delete the Kind cluster. @@ -205,11 +205,11 @@ helm-install: helm-install-dependencies ## Install operator using Helm. $(HELM) upgrade --install ${CHART_NAME_OPERATOR} ${CHART_PATH_OPERATOR} \ -n ${NAMESPACE} --create-namespace \ --set operator.namespace=${NAMESPACE} \ - --set operator.image.repository=${OPERATOR_IMG_NAME} \ + --set operator.image.repository=${OPERATOR_IMAGE_NAME} \ --set operator.image.pullPolicy=IfNotPresent \ - --set operator.image.tag=${OPERATOR_IMG_TAG} \ - --set paladin.image.repository=${PALADIN_IMG_NAME} \ - --set paladin.image.tag=${PALADIN_IMG_TAG} \ + --set operator.image.tag=${OPERATOR_IMAGE_TAG} \ + --set paladin.image.repository=${PALADIN_IMAGE_NAME} \ + --set paladin.image.tag=${PALADIN_IMAGE_TAG} \ --set paladin.image.pullPolicy=IfNotPresent \ --set prometheus.enabled=false diff --git a/operator/build.gradle b/operator/build.gradle index 62bc0f84d..89f452c55 100644 --- a/operator/build.gradle +++ b/operator/build.gradle @@ -56,6 +56,11 @@ ext { deleteCluster = project.hasProperty('deleteCluster') ? project.deleteCluster.toBoolean() : false // By default, do not delete the cluster buildPaladin = project.hasProperty('buildPaladin') ? project.buildPaladin == 'true' : true // Default is to build Paladin buildOperator = project.hasProperty('buildOperator') ? project.buildOperator == 'true' : true // Default is to build the operator + + operatorImageName = project.hasProperty('operatorImageName') ? project.operatorImageName : 'paladin-operator' + operatorImageTag = project.hasProperty('operatorImageTag') ? project.operatorImageTag : 'test' + paladinImageName = project.hasProperty('paladinImageName') ? project.paladinImageName : 'paladin' + paladinImageTag = project.hasProperty('paladinImageTag') ? project.paladinImageTag : 'test' } def printClusterStatus(String namespace) { @@ -177,6 +182,10 @@ task promoteKindImages(type: Exec, dependsOn: [ executable 'make' args 'kind-promote' args "CLUSTER_NAME=${clusterName}" + args "OPERATOR_IMAGE_NAME=${operatorImageName}" + args "OPERATOR_IMAGE_TAG=${operatorImageTag}" + args "PALADIN_IMAGE_NAME=${paladinImageName}" + args "PALADIN_IMAGE_TAG=${paladinImageTag}" } task prepareCRDsChart(type: Exec) { @@ -199,6 +208,10 @@ task installOperator(type: Exec, dependsOn: [installCrds, promoteKindImages, pre executable 'make' args 'helm-install' args "NAMESPACE=${namespace}" + args "OPERATOR_IMAGE_NAME=${operatorImageName}" + args "OPERATOR_IMAGE_TAG=${operatorImageTag}" + args "PALADIN_IMAGE_NAME=${paladinImageName}" + args "PALADIN_IMAGE_TAG=${paladinImageTag}" } diff --git a/operator/charts/paladin-operator/assets/config.json b/operator/charts/paladin-operator/assets/config.json index 66e0489ab..f313234b9 100644 --- a/operator/charts/paladin-operator/assets/config.json +++ b/operator/charts/paladin-operator/assets/config.json @@ -2,19 +2,30 @@ "paladin": { "image": "{{ .Values.paladin.image.repository }}:{{ .Values.paladin.image.tag }}", "imagePullPolicy": "{{ .Values.paladin.image.pullPolicy }}", - "labels": { - "app": "paladin" - } + "labels": {{ toJson .Values.paladin.labels }}, + "annotations": {{ toJson .Values.paladin.annotations }}, + "envs": {{ toJson .Values.paladin.envs }}, + "tolerations": {{ toJson .Values.paladin.tolerations }}, + "affinity": {{ toJson .Values.paladin.affinity }}, + "nodeSelector": {{ toJson .Values.paladin.nodeSelector }}, + "securityContext": {{ toJson .Values.paladin.securityContext }} }, "besu": { "image": "{{ .Values.besu.image.repository }}:{{ .Values.besu.image.tag }}", "imagePullPolicy": "{{ .Values.besu.image.pullPolicy }}", - "labels": { - "app": "besu" - } + "labels": {{ toJson .Values.besu.labels }}, + "annotations": {{ toJson .Values.besu.annotations }}, + "envs": {{ toJson .Values.besu.envs }}, + "tolerations": {{ toJson .Values.besu.tolerations }}, + "affinity": {{ toJson .Values.besu.affinity }}, + "nodeSelector": {{ toJson .Values.besu.nodeSelector }}, + "securityContext": {{ toJson .Values.besu.securityContext }} }, "postgres": { "image": "{{ .Values.postgres.image.repository }}:{{ .Values.postgres.image.tag }}", - "imagePullPolicy": "{{ .Values.postgres.image.pullPolicy }}" + "imagePullPolicy": "{{ .Values.postgres.image.pullPolicy }}", + "labels": {{ toJson .Values.postgres.labels }}, + "annotations": {{ toJson .Values.postgres.annotations }}, + "envs": {{ toJson .Values.postgres.envs }} } } \ No newline at end of file diff --git a/operator/charts/paladin-operator/values.yaml b/operator/charts/paladin-operator/values.yaml index 0c40605d8..61605ccdf 100644 --- a/operator/charts/paladin-operator/values.yaml +++ b/operator/charts/paladin-operator/values.yaml @@ -1,16 +1,17 @@ - - -# Install CRDs as part of the chart installation -# When this is set to false, the CRDs are expected to be installed separately -installCRDs: false +# Installation mode. This setting determines which Custom Resources (CRs) will be installed by default when deploying this chart. +# Supported modes: +# - devnet: Installs a default Paladin network (3 nodes) along with the related Smart Contracts. +# - smartcontractdeployment: Deploys the Smart Contracts without installing the Paladin network. +# - none (or left empty): Only the operator will be installed. +mode: devnet # Default values for paladin-operator operator: name: paladin-operator namespace: paladin image: - repository: kaleidoinc/paladin-operator - tag: release + repository: ghcr.io/lf-decentralized-trust-labs/paladin-operator + tag: main pullPolicy: Always serviceAccount: @@ -90,16 +91,25 @@ prometheus: paladin: image: - repository: kaleidoinc/paladin - tag: release + repository: ghcr.io/lf-decentralized-trust-labs/paladin + tag: main pullPolicy: Always + labels: + app: paladin + besu: image: repository: hyperledger/besu tag: latest pullPolicy: Always -postgres: + labels: + app: besu +postgres: # the postgres container runs as a sidecar to the paladin container image: repository: postgres tag: latest - pullPolicy: Always \ No newline at end of file + pullPolicy: Always + +# Install CRDs as part of the chart installation +# When this is set to false, the CRDs are expected to be installed separately +installCRDs: false diff --git a/operator/contractpkg/main.go b/operator/contractpkg/main.go index 662eb3cc6..d26064cc1 100644 --- a/operator/contractpkg/main.go +++ b/operator/contractpkg/main.go @@ -257,6 +257,27 @@ func template() error { // Perform the regex replacement newContent := pattern.ReplaceAllString(string(content), "{{ `{{${1}}}` }}") + // Add conditional wrapper around the content + conditions := []string{"(eq .Values.mode \"devnet\")"} + + if strings.Contains(file, "smartcontractdeployment") { + // Include additional condition if file contains "smartcontractdeployment" + conditions = append(conditions, "(eq .Values.mode \"smartcontractdeployment\")") + } + + // Build the condition string for the template + var condition string + if len(conditions) == 1 { + // Single condition doesn't need 'or' + condition = conditions[0] + } else { + // Multiple conditions use 'or' to combine them + condition = fmt.Sprintf("(or %s)", strings.Join(conditions, " ")) + } + + // Wrap newContent with the conditional template + newContent = fmt.Sprintf("{{- if %s }}\n\n%s\n{{- end }}", condition, newContent) + // Write the modified content back to the same file err = os.WriteFile(file, []byte(newContent), fs.FileMode(0644)) if err != nil { diff --git a/operator/gitops/flux/README.md b/operator/gitops/flux/README.md new file mode 100644 index 000000000..6ae9721a8 --- /dev/null +++ b/operator/gitops/flux/README.md @@ -0,0 +1,11 @@ +# Install paladin operator using flux + +## Install the cert-manager +``` +kubectl apply -f cert-manager.yaml +``` + +## Install paladin operator +``` +kubectl apply -f paladin-operator.yaml +``` \ No newline at end of file diff --git a/operator/gitops/flux/cert-manager.yaml b/operator/gitops/flux/cert-manager.yaml new file mode 100644 index 000000000..cb20c2caa --- /dev/null +++ b/operator/gitops/flux/cert-manager.yaml @@ -0,0 +1,33 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: cert-manager +--- +apiVersion: source.toolkit.fluxcd.io/v1 +kind: HelmRepository +metadata: + name: cert-manager + namespace: cert-manager +spec: + interval: 24h + url: https://charts.jetstack.io + +--- +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: cert-manager + namespace: cert-manager +spec: + interval: 30m + chart: + spec: + chart: cert-manager + version: "v1.16.1" + sourceRef: + kind: HelmRepository + name: cert-manager + namespace: cert-manager + interval: 12h + values: + installCRDs: true \ No newline at end of file diff --git a/operator/gitops/flux/paladin-operator.yaml b/operator/gitops/flux/paladin-operator.yaml new file mode 100644 index 000000000..75ea4f9a4 --- /dev/null +++ b/operator/gitops/flux/paladin-operator.yaml @@ -0,0 +1,93 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: paladin +--- +apiVersion: source.toolkit.fluxcd.io/v1 +kind: HelmRepository +metadata: + name: paladin + namespace: paladin +spec: + url: https://LF-Decentralized-Trust-labs.github.io/paladin + interval: 5m +--- +apiVersion: helm.toolkit.fluxcd.io/v2beta1 +kind: HelmRelease +metadata: + name: paladin-crds + namespace: paladin +spec: + releaseName: paladin-crds + interval: 5m + chart: + spec: + chart: paladin-operator-crd + sourceRef: + kind: HelmRepository + name: paladin + namespace: paladin + version: "*" + install: + remediation: + retries: 3 + upgrade: + remediation: + retries: 3 +--- +apiVersion: helm.toolkit.fluxcd.io/v2beta1 +kind: HelmRelease +metadata: + name: paladin + namespace: paladin # This value must match spec.values.operator.namespace +spec: + releaseName: paladin + interval: 5m + chart: + spec: + chart: paladin-operator + sourceRef: + kind: HelmRepository + name: paladin + namespace: paladin + version: "*" + install: + createNamespace: true + remediation: + retries: 3 + upgrade: + remediation: + retries: 3 + values: + mode: devnet # Installation mode. This setting determines which Custom Resources (CRs) will be installed by default when deploying this chart + operator: + namespace: paladin # This value must match metadata.namespace + image: + repository: ghcr.io/lf-decentralized-trust-labs/paladin-operator + tag: main + pullPolicy: Always + paladin: + image: + repository: ghcr.io/lf-decentralized-trust-labs/paladin + tag: main + pullPolicy: Always + # tollerations: + # nodeSelector: + # affinity: + # securityContext: + # besu: + # image: + # repository: hyperledger/besu + # tag: latest + # pullPolicy: IfNotPresent + # nodeSelector: + # affinity: + # securityContext: + # postgres: + # image: + # repository: postgres + # tag: latest + # pullPolicy: IfNotPresent + + + \ No newline at end of file diff --git a/operator/internal/controller/besu_controller.go b/operator/internal/controller/besu_controller.go index 281474a80..554b9567e 100644 --- a/operator/internal/controller/besu_controller.go +++ b/operator/internal/controller/besu_controller.go @@ -345,6 +345,9 @@ func (r *BesuReconciler) getLabels(node *corev1alpha1.Besu, extraLabels ...map[s } } l["app.kubernetes.io/name"] = generateBesuName(node.Name) + l["app.kubernetes.io/instance"] = node.Name + l["app.kubernetes.io/part-of"] = "paladin" + return l } @@ -551,7 +554,8 @@ func (r *BesuReconciler) generateStatefulSetTemplate(node *corev1alpha1.Besu, na { Name: "besu", Image: r.config.Besu.Image, // Use the image from the config - ImagePullPolicy: corev1.PullIfNotPresent, + ImagePullPolicy: r.config.Besu.ImagePullPolicy, + SecurityContext: r.config.Besu.SecurityContext, VolumeMounts: []corev1.VolumeMount{ { Name: "config", @@ -631,6 +635,9 @@ func (r *BesuReconciler) generateStatefulSetTemplate(node *corev1alpha1.Besu, na }, }, }, + Tolerations: r.config.Besu.Tolerations, + Affinity: r.config.Besu.Affinity, + NodeSelector: r.config.Besu.NodeSelector, Volumes: []corev1.Volume{ { Name: "config", diff --git a/operator/internal/controller/besu_controller_test.go b/operator/internal/controller/besu_controller_test.go index f6181b377..ae9b19385 100644 --- a/operator/internal/controller/besu_controller_test.go +++ b/operator/internal/controller/besu_controller_test.go @@ -29,7 +29,6 @@ import ( corev1alpha1 "github.com/kaleido-io/paladin/operator/api/v1alpha1" "github.com/kaleido-io/paladin/operator/pkg/config" - corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -74,13 +73,7 @@ var _ = Describe("Besu Controller", func() { It("should successfully reconcile the resource", func() { By("Reconciling the created resource") cfg := &config.Config{ - Paladin: struct { - Image string `json:"image"` - ImagePullPolicy corev1.PullPolicy `json:"imagePullPolicy"` - Labels map[string]string `json:"labels"` - Annotations map[string]string `json:"annotations"` - Envs map[string]string `json:"envs"` - }{ + Paladin: config.Template{ Labels: map[string]string{ "env": "production", "tier": "backend", @@ -106,13 +99,7 @@ var _ = Describe("Besu Controller", func() { func TestBesu_GetLabels(t *testing.T) { // Mock configuration config := config.Config{ - Besu: struct { - Image string `json:"image"` - ImagePullPolicy corev1.PullPolicy `json:"imagePullPolicy"` - Labels map[string]string `json:"labels"` - Annotations map[string]string `json:"annotations"` - Envs map[string]string `json:"envs"` - }{ + Paladin: config.Template{ Labels: map[string]string{ "env": "production", "tier": "backend", diff --git a/operator/internal/controller/paladin_controller.go b/operator/internal/controller/paladin_controller.go index 928d7d2c7..32b3d067e 100644 --- a/operator/internal/controller/paladin_controller.go +++ b/operator/internal/controller/paladin_controller.go @@ -412,8 +412,12 @@ func (r *PaladinReconciler) generateStatefulSetTemplate(node *corev1alpha1.Palad TimeoutSeconds: 2, PeriodSeconds: 5, }, + SecurityContext: r.config.Paladin.SecurityContext, }, }, + Tolerations: r.config.Paladin.Tolerations, + NodeSelector: r.config.Paladin.NodeSelector, + Affinity: r.config.Paladin.Affinity, Volumes: []corev1.Volume{ { Name: "config", @@ -439,6 +443,7 @@ func (r *PaladinReconciler) addPostgresSidecar(ss *appsv1.StatefulSet, passwordS Name: "postgres", Image: r.config.Postgres.Image, // Use the image from the config ImagePullPolicy: r.config.Postgres.ImagePullPolicy, + SecurityContext: r.config.Postgres.SecurityContext, VolumeMounts: []corev1.VolumeMount{ { Name: "pgdata", @@ -1203,6 +1208,8 @@ func (r *PaladinReconciler) getLabels(node *corev1alpha1.Paladin, extraLabels .. } } l["app.kubernetes.io/name"] = generatePaladinName(node.Name) + l["app.kubernetes.io/instance"] = node.Name + l["app.kubernetes.io/part-of"] = "paladin" return l } diff --git a/operator/internal/controller/paladin_controller_test.go b/operator/internal/controller/paladin_controller_test.go index f7be4fe66..293f07787 100644 --- a/operator/internal/controller/paladin_controller_test.go +++ b/operator/internal/controller/paladin_controller_test.go @@ -130,13 +130,7 @@ var _ = Describe("Paladin Controller", func() { It("should successfully reconcile the resource", func() { By("Reconciling the created resource") cfg := &config.Config{ - Paladin: struct { - Image string `json:"image"` - ImagePullPolicy corev1.PullPolicy `json:"imagePullPolicy"` - Labels map[string]string `json:"labels"` - Annotations map[string]string `json:"annotations"` - Envs map[string]string `json:"envs"` - }{ + Paladin: config.Template{ Labels: map[string]string{ "env": "production", "tier": "backend", @@ -162,13 +156,7 @@ var _ = Describe("Paladin Controller", func() { func TestPaladin_GetLabels(t *testing.T) { // Mock configuration config := config.Config{ - Paladin: struct { - Image string `json:"image"` - ImagePullPolicy corev1.PullPolicy `json:"imagePullPolicy"` - Labels map[string]string `json:"labels"` - Annotations map[string]string `json:"annotations"` - Envs map[string]string `json:"envs"` - }{ + Paladin: config.Template{ Labels: map[string]string{ "env": "production", "tier": "backend", diff --git a/operator/pkg/config/config.go b/operator/pkg/config/config.go index 9270ba325..c917ad025 100644 --- a/operator/pkg/config/config.go +++ b/operator/pkg/config/config.go @@ -9,30 +9,23 @@ import ( "github.com/spf13/viper" ) +type Template struct { + Image string `json:"image"` + ImagePullPolicy corev1.PullPolicy `json:"imagePullPolicy"` + Labels map[string]string `json:"labels"` + Annotations map[string]string `json:"annotations"` + Envs map[string]string `json:"envs"` + Tolerations []corev1.Toleration `json:"tolerations"` + Affinity *corev1.Affinity `json:"affinity"` + NodeSelector map[string]string `json:"nodeSelector"` + SecurityContext *corev1.SecurityContext `json:"securityContext"` +} + // Config represents the structure of the configuration type Config struct { - Paladin struct { - Image string `json:"image"` - ImagePullPolicy corev1.PullPolicy `json:"imagePullPolicy"` - Labels map[string]string `json:"labels"` - Annotations map[string]string `json:"annotations"` - Envs map[string]string `json:"envs"` - // TODO: Add more fields - } `json:"paladin"` - Besu struct { - Image string `json:"image"` - ImagePullPolicy corev1.PullPolicy `json:"imagePullPolicy"` - Labels map[string]string `json:"labels"` - Annotations map[string]string `json:"annotations"` - Envs map[string]string `json:"envs"` - // TODO: Add more fields - } `json:"besu"` - Postgres struct { - Image string `json:"image"` - ImagePullPolicy corev1.PullPolicy `json:"imagePullPolicy"` - Envs map[string]string `json:"envs"` - // TODO: Add more fields - } `json:"postgres"` + Paladin Template `json:"paladin"` + Besu Template `json:"besu"` + Postgres Template `json:"postgres"` } // LoadConfig sets up Viper to load the config file diff --git a/operator/pkg/config/config_test.go b/operator/pkg/config/config_test.go new file mode 100644 index 000000000..6084a1bea --- /dev/null +++ b/operator/pkg/config/config_test.go @@ -0,0 +1,118 @@ +package config + +import ( + "os" + "path/filepath" + "testing" + + corev1 "k8s.io/api/core/v1" + + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestLoadConfig_Success(t *testing.T) { + // Create a temporary directory to hold the config file + tempDir := t.TempDir() + + // Create a config file in the tempDir + configFilePath := filepath.Join(tempDir, "config.json") + configContent := `{ + "paladin": { + "image": "paladin-image", + "imagePullPolicy": "Always", + "labels": {"app": "paladin"}, + "annotations": {"key": "value"}, + "envs": {"ENV_VAR": "value"}, + "tolerations": [{"key": "key1", "operator": "Exists", "effect": "NoSchedule"}], + "nodeSelector": {"disktype": "ssd"}, + "securityContext": {"runAsUser": 1000} + }, + "besu": { + "image": "besu-image", + "imagePullPolicy": "IfNotPresent" + }, + "postgres": { + "image": "postgres-image", + "imagePullPolicy": "Always" + } + }` + + err := os.WriteFile(configFilePath, []byte(configContent), 0644) + require.NoError(t, err, "Failed to write config file") + + // Set CONFIG_PATH environment variable to tempDir + err = os.Setenv("CONFIG_PATH", tempDir) + require.NoError(t, err) + defer os.Unsetenv("CONFIG_PATH") + + // Reset viper to ensure a clean state + viper.Reset() + + // Call LoadConfig + config, err := LoadConfig() + require.NoError(t, err, "LoadConfig failed") + + // Validate the config + assert.Equal(t, "paladin-image", config.Paladin.Image, "Paladin.Image mismatch") + assert.Equal(t, corev1.PullAlways, config.Paladin.ImagePullPolicy, "Paladin.ImagePullPolicy mismatch") + assert.Equal(t, "paladin", config.Paladin.Labels["app"], "Paladin.Labels['app'] mismatch") + assert.Equal(t, "value", config.Paladin.Annotations["key"], "Paladin.Annotations['key'] mismatch") + + // viper does not support case-sensitivity: https://github.com/spf13/viper/issues/1014 + assert.Equal(t, "value", config.Paladin.Envs["env_var"], "Paladin.Envs['ENV_VAR'] mismatch") + + require.Len(t, config.Paladin.Tolerations, 1, "Expected 1 toleration") + tol := config.Paladin.Tolerations[0] + assert.Equal(t, "key1", tol.Key, "Toleration key mismatch") + assert.Equal(t, corev1.TolerationOperator("Exists"), tol.Operator, "Toleration operator mismatch") + assert.Equal(t, corev1.TaintEffect("NoSchedule"), tol.Effect, "Toleration effect mismatch") + + assert.Equal(t, "ssd", config.Paladin.NodeSelector["disktype"], "Paladin.NodeSelectors['disktype'] mismatch") + + require.NotNil(t, config.Paladin.SecurityContext, "Expected Paladin.SecurityContext to be set") + assert.NotNil(t, config.Paladin.SecurityContext.RunAsUser, "Expected Paladin.SecurityContext.RunAsUser to be set") + assert.Equal(t, int64(1000), *config.Paladin.SecurityContext.RunAsUser, "Paladin.SecurityContext.RunAsUser mismatch") +} + +func TestLoadConfig_MissingFile(t *testing.T) { + // Create a temporary directory without a config file + tempDir := t.TempDir() + + // Set CONFIG_PATH environment variable to tempDir + err := os.Setenv("CONFIG_PATH", tempDir) + require.NoError(t, err) + defer os.Unsetenv("CONFIG_PATH") + + // Reset viper to ensure a clean state + viper.Reset() + + // Call LoadConfig + _, err = LoadConfig() + require.Error(t, err, "Expected LoadConfig to fail due to missing config file") +} + +func TestLoadConfig_InvalidJSON(t *testing.T) { + // Create a temporary directory to hold the config file + tempDir := t.TempDir() + + // Create an invalid config file in the tempDir + configFilePath := filepath.Join(tempDir, "config.json") + configContent := `{ invalid json }` + + err := os.WriteFile(configFilePath, []byte(configContent), 0644) + require.NoError(t, err, "Failed to write config file") + + // Set CONFIG_PATH environment variable to tempDir + err = os.Setenv("CONFIG_PATH", tempDir) + require.NoError(t, err) + defer os.Unsetenv("CONFIG_PATH") + + // Reset viper to ensure a clean state + viper.Reset() + + // Call LoadConfig + _, err = LoadConfig() + require.Error(t, err, "Expected LoadConfig to fail due to invalid JSON") +}