diff --git a/.github/workflows/lint-test.yaml b/.github/workflows/lint-test.yaml index 8ca7864e..276c54cb 100644 --- a/.github/workflows/lint-test.yaml +++ b/.github/workflows/lint-test.yaml @@ -36,6 +36,9 @@ jobs: - name: Run chart-testing (lint) run: ct lint --config ct.yaml + - name: Run challenger RPC secret-reference tests + run: bash charts/challenger/ci/test-rpc-secret-refs.sh + - name: Create kind cluster uses: helm/kind-action@ef37e7f390d99f746eb8b610417061a60e82a6cc if: steps.list-changed.outputs.changed == 'true' diff --git a/charts/challenger/Chart.yaml b/charts/challenger/Chart.yaml index 90c4c918..71dd2e84 100644 --- a/charts/challenger/Chart.yaml +++ b/charts/challenger/Chart.yaml @@ -23,7 +23,7 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.1.1 +version: 0.2.0 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to diff --git a/charts/challenger/README.md b/charts/challenger/README.md index 27c0a12e..52fb2eb0 100644 --- a/charts/challenger/README.md +++ b/charts/challenger/README.md @@ -1,6 +1,6 @@ # challenger -![Version: 0.1.1](https://img.shields.io/badge/Version-0.1.1-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) +![Version: 0.2.0](https://img.shields.io/badge/Version-0.2.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 deploying the OpPoke Challenger Bot (go and rust) in Kubernetes @@ -43,6 +43,7 @@ A Helm chart for deploying the OpPoke Challenger Bot (go and rust) in Kubernetes | readinessProbe.tcpSocket.port | int | `9090` | | | replicaCount | int | `1` | how many replicas to run (default 1) | | resources | object | `{}` | | +| rpcSecrets | object | `{}` | Optional existing Secret references for RPC endpoints. Do not combine an entry with its plaintext counterpart. | | securityContext | object | `{}` | | | service.ports.challenger.port | int | `9090` | | | service.ports.challenger.protocol | string | `"TCP"` | | diff --git a/charts/challenger/ci/challenger-go-flashbots-values.yaml b/charts/challenger/ci/challenger-go-flashbots-values.yaml index adf53088..5699f724 100644 --- a/charts/challenger/ci/challenger-go-flashbots-values.yaml +++ b/charts/challenger/ci/challenger-go-flashbots-values.yaml @@ -1,4 +1,5 @@ implementation: go +replicaCount: 0 ethRpcUrl: "https://eth.drpc.org" flashbotsRpcUrl: "https://rpc.flashbots.net/fast" diff --git a/charts/challenger/ci/challenger-go-values.yaml b/charts/challenger/ci/challenger-go-values.yaml index c209755f..a653fc23 100644 --- a/charts/challenger/ci/challenger-go-values.yaml +++ b/charts/challenger/ci/challenger-go-values.yaml @@ -1,4 +1,5 @@ implementation: go +replicaCount: 0 ethRpcUrl: "https://eth.drpc.org" diff --git a/charts/challenger/ci/challenger-rs-flashbots-values.yaml b/charts/challenger/ci/challenger-rs-flashbots-values.yaml index c4f668da..372a0c99 100644 --- a/charts/challenger/ci/challenger-rs-flashbots-values.yaml +++ b/charts/challenger/ci/challenger-rs-flashbots-values.yaml @@ -1,4 +1,5 @@ implementation: rs +replicaCount: 0 ethRpcUrl: "https://eth.drpc.org" flashbotsRpcUrl: "https://rpc.flashbots.net/fast" diff --git a/charts/challenger/ci/challenger-rs-values.yaml b/charts/challenger/ci/challenger-rs-values.yaml index 54d0804a..096306ec 100644 --- a/charts/challenger/ci/challenger-rs-values.yaml +++ b/charts/challenger/ci/challenger-rs-values.yaml @@ -1,4 +1,5 @@ implementation: rs +replicaCount: 0 ethRpcUrl: "https://eth.drpc.org" diff --git a/charts/challenger/ci/test-rpc-secret-refs.sh b/charts/challenger/ci/test-rpc-secret-refs.sh new file mode 100644 index 00000000..265feaeb --- /dev/null +++ b/charts/challenger/ci/test-rpc-secret-refs.sh @@ -0,0 +1,198 @@ +#!/usr/bin/env bash + +set -euo pipefail + +chart_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +tmp_dir="$(mktemp -d)" +trap 'rm -rf "${tmp_dir}"' EXIT + +assert_contains() { + local file="$1" + local expected="$2" + + if ! grep -Fq -- "${expected}" "${file}"; then + echo "expected rendered chart to contain: ${expected}" >&2 + exit 1 + fi +} + +assert_not_contains() { + local file="$1" + local unexpected="$2" + + if grep -Fq -- "${unexpected}" "${file}"; then + echo "rendered chart leaked plaintext test endpoint: ${unexpected}" >&2 + exit 1 + fi +} + +eth_rpc_placeholder="\$(ETH_RPC_URL)" +secret_render="${tmp_dir}/secret-render.yaml" + +helm template challenger "${chart_dir}" \ + --set implementation=rs \ + --set-string rpcSecrets.ethRpcUrl.existingSecret=challenger-rpc \ + --set-string rpcSecrets.ethRpcUrl.key=ethRpcUrl \ + >"${secret_render}" + +assert_contains "${secret_render}" "${eth_rpc_placeholder}" +assert_contains "${secret_render}" 'name: ETH_RPC_URL' +assert_contains "${secret_render}" 'name: "challenger-rpc"' +assert_contains "${secret_render}" 'key: "ethRpcUrl"' +assert_not_contains "${secret_render}" 'https://' + +flashbots_render="${tmp_dir}/flashbots-render.yaml" +flashbots_rpc_placeholder="\$(FLASHBOTS_RPC_URL)" + +helm template challenger "${chart_dir}" \ + --set implementation=rs \ + --set-string ethRpcUrl=https://public.example.invalid \ + --set-string rpcSecrets.flashbotsRpcUrl.existingSecret=challenger-rpc \ + --set-string rpcSecrets.flashbotsRpcUrl.key=flashbotsRpcUrl \ + >"${flashbots_render}" + +assert_contains "${flashbots_render}" '--flashbot-rpc-url' +assert_contains "${flashbots_render}" "${flashbots_rpc_placeholder}" +assert_contains "${flashbots_render}" 'name: FLASHBOTS_RPC_URL' +assert_contains "${flashbots_render}" 'key: "flashbotsRpcUrl"' + +mixed_env_render="${tmp_dir}/mixed-env-render.yaml" +helm template challenger "${chart_dir}" \ + --set implementation=rs \ + --set-string rpcSecrets.ethRpcUrl.existingSecret=challenger-rpc \ + --set-string rpcSecrets.ethRpcUrl.key=ethRpcUrl \ + --set-string rpcSecrets.flashbotsRpcUrl.existingSecret=challenger-rpc \ + --set-string rpcSecrets.flashbotsRpcUrl.key=flashbotsRpcUrl \ + --set-string env.normal.FOO=bar \ + >"${mixed_env_render}" + +assert_contains "${mixed_env_render}" 'name: ETH_RPC_URL' +assert_contains "${mixed_env_render}" 'name: FLASHBOTS_RPC_URL' +assert_contains "${mixed_env_render}" 'name: FOO' +assert_contains "${mixed_env_render}" 'value: "bar"' + +private_key_render="${tmp_dir}/private-key-render.yaml" +eth_secret_key_placeholder="\$(ETH_SECRET_KEY)" +helm template challenger "${chart_dir}" \ + --set implementation=rs \ + --set-string ethConfig.ethPrivateKey.existingSecret=challenger-eth \ + --set-string ethConfig.ethPrivateKey.key=ethPrivateKey \ + --set-string env.normal.FOO=bar \ + >"${private_key_render}" + +assert_contains "${private_key_render}" "${eth_secret_key_placeholder}" +assert_contains "${private_key_render}" 'name: ETH_SECRET_KEY' +assert_contains "${private_key_render}" 'name: FOO' + +inline_rpc="https://public-backward-compatible.example.invalid" +inline_render="${tmp_dir}/inline-render.yaml" + +helm template challenger "${chart_dir}" \ + --set implementation=rs \ + --set-string "ethRpcUrl=${inline_rpc}" \ + >"${inline_render}" + +assert_contains "${inline_render}" "${inline_rpc}" + +zero_replicas_render="${tmp_dir}/zero-replicas-render.yaml" +helm template challenger "${chart_dir}" \ + --set implementation=rs \ + --set replicaCount=0 \ + >"${zero_replicas_render}" + +assert_contains "${zero_replicas_render}" 'replicas: 0' + +invalid_render="${tmp_dir}/invalid-render.yaml" +plaintext_rpc="https://plaintext-should-not-render.example.invalid/rpc" +if helm template challenger "${chart_dir}" \ + --set implementation=rs \ + --set-string "ethRpcUrl=${plaintext_rpc}" \ + --set-string rpcSecrets.ethRpcUrl.existingSecret=challenger-rpc \ + --set-string rpcSecrets.ethRpcUrl.key=ethRpcUrl \ + >"${invalid_render}" 2>&1; then + echo "expected plaintext and Secret-backed ETH RPC sources to conflict" >&2 + exit 1 +fi + +assert_contains "${invalid_render}" 'ethRpcUrl and rpcSecrets.ethRpcUrl cannot both be set' + +if helm template challenger "${chart_dir}" \ + --set implementation=rs \ + --set-string rpcSecrets.ethRpcUrl.key=ethRpcUrl \ + >"${invalid_render}" 2>&1; then + echo "expected an incomplete RPC secret reference to fail rendering" >&2 + exit 1 +fi + +assert_contains "${invalid_render}" 'rpcSecrets.ethRpcUrl.existingSecret is required' + +if helm template challenger "${chart_dir}" \ + --set implementation=rs \ + --set-string rpcSecrets.ethRpcUrl.existingSecret=challenger-rpc \ + >"${invalid_render}" 2>&1; then + echo "expected an RPC secret reference without a key to fail rendering" >&2 + exit 1 +fi + +assert_contains "${invalid_render}" 'rpcSecrets.ethRpcUrl.key is required' + +flashbots_plaintext="https://plaintext-flashbots-should-not-render.example.invalid" +if helm template challenger "${chart_dir}" \ + --set implementation=rs \ + --set-string ethRpcUrl=https://public.example.invalid \ + --set-string "flashbotsRpcUrl=${flashbots_plaintext}" \ + --set-string rpcSecrets.flashbotsRpcUrl.existingSecret=challenger-rpc \ + --set-string rpcSecrets.flashbotsRpcUrl.key=flashbotsRpcUrl \ + >"${invalid_render}" 2>&1; then + echo "expected plaintext and Secret-backed Flashbots RPC sources to conflict" >&2 + exit 1 +fi + +assert_contains "${invalid_render}" 'flashbotsRpcUrl and rpcSecrets.flashbotsRpcUrl cannot both be set' + +if helm template challenger "${chart_dir}" \ + --set implementation=rs \ + --set-string ethRpcUrl=https://public.example.invalid \ + --set-string rpcSecrets.flashbotsRpcUrl.existingSecret=challenger-rpc \ + >"${invalid_render}" 2>&1; then + echo "expected a Flashbots RPC secret reference without a key to fail rendering" >&2 + exit 1 +fi + +assert_contains "${invalid_render}" 'rpcSecrets.flashbotsRpcUrl.key is required' + +if helm template challenger "${chart_dir}" \ + --set implementation=rs \ + --set-string ethRpcUrl=https://public.example.invalid \ + --set-string rpcSecrets.flashbotsRpcUrl.key=flashbotsRpcUrl \ + >"${invalid_render}" 2>&1; then + echo "expected a Flashbots RPC secret reference without a Secret name to fail rendering" >&2 + exit 1 +fi + +assert_contains "${invalid_render}" 'rpcSecrets.flashbotsRpcUrl.existingSecret is required' + +if helm template challenger "${chart_dir}" \ + --set implementation=rs \ + --set-string rpcSecrets.ethRpcUrl.existingSecret=challenger-rpc \ + --set-string rpcSecrets.ethRpcUrl.key=ethRpcUrl \ + --set-string env.normal.ETH_RPC_URL=shadowed \ + >"${invalid_render}" 2>&1; then + echo "expected env.normal to reject the reserved ETH_RPC_URL name" >&2 + exit 1 +fi + +assert_contains "${invalid_render}" 'env.normal.ETH_RPC_URL is reserved when rpcSecrets.ethRpcUrl is set' + +if helm template challenger "${chart_dir}" \ + --set implementation=rs \ + --set-string ethRpcUrl=https://public.example.invalid \ + --set-string rpcSecrets.flashbotsRpcUrl.existingSecret=challenger-rpc \ + --set-string rpcSecrets.flashbotsRpcUrl.key=flashbotsRpcUrl \ + --set-string env.normal.FLASHBOTS_RPC_URL=shadowed \ + >"${invalid_render}" 2>&1; then + echo "expected env.normal to reject the reserved FLASHBOTS_RPC_URL name" >&2 + exit 1 +fi + +assert_contains "${invalid_render}" 'env.normal.FLASHBOTS_RPC_URL is reserved when rpcSecrets.flashbotsRpcUrl is set' diff --git a/charts/challenger/templates/deployment.yaml b/charts/challenger/templates/deployment.yaml index 7df48f4c..6c9feebb 100644 --- a/charts/challenger/templates/deployment.yaml +++ b/charts/challenger/templates/deployment.yaml @@ -1,3 +1,19 @@ +{{- $rpcSecrets := .Values.rpcSecrets | default dict -}} +{{- $ethRpcSecret := get $rpcSecrets "ethRpcUrl" | default dict -}} +{{- $flashbotsRpcSecret := get $rpcSecrets "flashbotsRpcUrl" | default dict -}} +{{- $normalEnv := .Values.env.normal | default dict -}} +{{- if and $ethRpcSecret .Values.ethRpcUrl -}} +{{- fail "ethRpcUrl and rpcSecrets.ethRpcUrl cannot both be set" -}} +{{- end -}} +{{- if and $flashbotsRpcSecret .Values.flashbotsRpcUrl -}} +{{- fail "flashbotsRpcUrl and rpcSecrets.flashbotsRpcUrl cannot both be set" -}} +{{- end -}} +{{- if and $ethRpcSecret (hasKey $normalEnv "ETH_RPC_URL") -}} +{{- fail "env.normal.ETH_RPC_URL is reserved when rpcSecrets.ethRpcUrl is set" -}} +{{- end -}} +{{- if and $flashbotsRpcSecret (hasKey $normalEnv "FLASHBOTS_RPC_URL") -}} +{{- fail "env.normal.FLASHBOTS_RPC_URL is reserved when rpcSecrets.flashbotsRpcUrl is set" -}} +{{- end -}} apiVersion: apps/v1 kind: Deployment metadata: @@ -5,7 +21,7 @@ metadata: labels: {{- include "challenger.labels" . | nindent 4 }} spec: - replicas: {{ .Values.replicaCount | default 1 }} + replicas: {{ .Values.replicaCount }} selector: matchLabels: {{- include "challenger.selectorLabels" . | nindent 6 }} @@ -48,14 +64,22 @@ spec: - "{{ $address }}" {{- end }} - "--rpc-url" + {{- if $ethRpcSecret }} + - "$(ETH_RPC_URL)" + {{- else }} - "{{ .Values.ethRpcUrl }}" - {{- if .Values.flashbotsRpcUrl }} + {{- end }} + {{- if or .Values.flashbotsRpcUrl $flashbotsRpcSecret }} - "--flashbot-rpc-url" + {{- if $flashbotsRpcSecret }} + - "$(FLASHBOTS_RPC_URL)" + {{- else }} - "{{ .Values.flashbotsRpcUrl }}" + {{- end }} {{- end }} {{- if .Values.ethConfig.ethPrivateKey }} - "--secret-key" - - "${ETH_SECRET_KEY}" + - "$(ETH_SECRET_KEY)" {{- end }} {{- if .Values.ethConfig.ethKeys }} - "--keystore" @@ -79,12 +103,26 @@ spec: env: + {{- if $ethRpcSecret }} + - name: ETH_RPC_URL + valueFrom: + secretKeyRef: + name: {{ required "rpcSecrets.ethRpcUrl.existingSecret is required" $ethRpcSecret.existingSecret | quote }} + key: {{ required "rpcSecrets.ethRpcUrl.key is required" $ethRpcSecret.key | quote }} + {{- end }} + {{- if $flashbotsRpcSecret }} + - name: FLASHBOTS_RPC_URL + valueFrom: + secretKeyRef: + name: {{ required "rpcSecrets.flashbotsRpcUrl.existingSecret is required" $flashbotsRpcSecret.existingSecret | quote }} + key: {{ required "rpcSecrets.flashbotsRpcUrl.key is required" $flashbotsRpcSecret.key | quote }} + {{- end }} {{- if .Values.ethConfig.ethPrivateKey }} - - name: ETH_SECRET_KEY - valueFrom: - secretKeyRef: - name: {{ .Values.ethConfig.ethPrivateKey.existingSecret }} - key: {{ .Values.ethConfig.ethPrivateKey.key }} + - name: ETH_SECRET_KEY + valueFrom: + secretKeyRef: + name: {{ .Values.ethConfig.ethPrivateKey.existingSecret }} + key: {{ .Values.ethConfig.ethPrivateKey.key }} {{- end }} {{- include "helpers.list-env-variables" . | indent 12 }} resources: diff --git a/charts/challenger/values.yaml b/charts/challenger/values.yaml index e283c053..ff8d1478 100644 --- a/charts/challenger/values.yaml +++ b/charts/challenger/values.yaml @@ -40,6 +40,14 @@ ethChainId: "1" ethRpcUrl: "" # -- provide flashbots RPC flashbotsRpcUrl: "" +# -- Optional existing Secret references for RPC endpoints. Do not combine an entry with its plaintext counterpart. +rpcSecrets: {} + # ethRpcUrl: + # existingSecret: "challenger-rpc" + # key: "ethRpcUrl" + # flashbotsRpcUrl: + # existingSecret: "challenger-rpc" + # key: "flashbotsRpcUrl" # -- Transaction type definition, possible values are: legacy, eip1559 or none (default "none") transactionType: ""