diff --git a/.dockerignore b/.dockerignore index 2f7896d..c4c33f4 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1 +1 @@ -target/ +!target/ \ No newline at end of file diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index bdbe035..8a25017 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -1,81 +1,82 @@ name: docker on: - push: - branches: - - main + push: + branches: + - main + - stable + - unstable + tags: + - v* concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true env: - DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} - DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} + DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} + DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} + DOCKER_BUILD_SUMMARY: true + REPO_NAME: $GITHUB_REPOSITORY + IMAGE_NAME: ${{ github.repository }}:${{ github.ref_name }} jobs: - build-docker-single-arch: - name: build-docker-${{ matrix.cpu_arch }} + extract-version: + uses: ./.github/workflows/extract-version.yml + build-docker: + name: build-docker runs-on: ubuntu-22.04 strategy: - matrix: - cpu_arch: [aarch64, x86_64] + fail-fast: false steps: - - uses: actions/checkout@v4 - - name: Update Rust - run: rustup update stable - - name: Dockerhub login - run: | - echo "${DOCKER_PASSWORD}" | docker login --username ${DOCKER_USERNAME} --password-stdin - - name: Cross build binaries - run: | - cargo install cross - make build-${{ matrix.cpu_arch }} - - name: Make bin dir - run: mkdir ./bin + - uses: actions/checkout@v4 + - name: Update Rust + run: rustup update stable + - name: Login to DockerHub + uses: docker/login-action@v3 + with: + username: ${{ env.DOCKER_USERNAME }} + password: ${{ env.DOCKER_PASSWORD }} - - name: Move cross-built binary into Docker scope - run: mv ./target/${{ matrix.cpu_arch }}-unknown-linux-gnu/release/eleel ./bin + - name: Debug IMAGE_NAME + run: echo "IMAGE_NAME=${{ env.IMAGE_NAME }}" + + - name: Cross build binaries + run: | + cargo install cross + make build + timeout-minutes: 100 - - name: Map aarch64 to arm64 short arch - if: startsWith(matrix.cpu_arch, 'aarch64') - run: echo "SHORT_ARCH=arm64" >> $GITHUB_ENV + - name: Install QEMU + run: sudo apt-get update && sudo apt-get install -y qemu-user-static - - name: Map x86_64 to amd64 short arch - if: startsWith(matrix.cpu_arch, 'x86_64') - run: echo "SHORT_ARCH=amd64" >> $GITHUB_ENV + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 - - name: Install QEMU - run: sudo apt-get update && sudo apt-get install -y qemu-user-static + - name: Build and push using Bake + uses: docker/bake-action@v6 + with: + files: ./docker-bake.hcl + source: . + push: true + load: true + set: | + *.context=. + *.tags=${{ env.IMAGE_NAME }} + env: + GITHUB_REPO: ${{ github.repository }} + DOCKER_BUILD_RECORD_UPLOAD: true - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + - name: Login to DockerHub + uses: docker/login-action@v3 + with: + username: ${{ env.DOCKER_USERNAME }} + password: ${{ env.DOCKER_PASSWORD }} - - name: Build and push - uses: docker/build-push-action@v5 - with: - file: ./Dockerfile.cross - context: . - platforms: linux/${{ env.SHORT_ARCH }} - push: true - tags: | - ${{ github.repository_owner }}/eleel:latest-${{ env.SHORT_ARCH }} + - name: Create and push multiarch manifest + run: | + docker buildx imagetools create -t ${{ env.IMAGE_NAME }} --append --debug \ + ${{ github.repository }}-arm64 \ + ${{ github.repository }}-amd64 - build-docker-multiarch: - name: build-docker-eleel-multiarch - runs-on: ubuntu-22.04 - needs: [build-docker-single-arch] - steps: - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Dockerhub login - run: | - echo "${DOCKER_PASSWORD}" | docker login --username ${DOCKER_USERNAME} --password-stdin - - - name: Create and push multiarch manifests - run: | - docker buildx imagetools create -t ${{ github.repository_owner}}/eleel:latest \ - ${{ github.repository_owner}}/eleel:latest-arm64 \ - ${{ github.repository_owner}}/eleel:latest-amd64; diff --git a/.github/workflows/extract-version.yml b/.github/workflows/extract-version.yml new file mode 100644 index 0000000..ca348cd --- /dev/null +++ b/.github/workflows/extract-version.yml @@ -0,0 +1,49 @@ +name: Extract Version + +on: + workflow_call: + outputs: + VERSION: + description: "The extracted version (latest or vX.Y.Z)" + value: ${{ jobs.extract-version.outputs.VERSION || 'latest' }} + VERSION_SUFFIX: + description: "The version suffix (empty or -unstable)" + value: ${{ jobs.extract-version.outputs.VERSION_SUFFIX }} + +jobs: + extract-version: + runs-on: ubuntu-22.04 + outputs: + VERSION: ${{ env.VERSION }} + VERSION_SUFFIX: ${{ env.VERSION_SUFFIX }} + steps: + + + - name: Extract version (if stable) + if: github.event.ref == 'refs/heads/stable' + run: | + echo "VERSION=latest" >> $GITHUB_ENV + echo "VERSION_SUFFIX=" >> $GITHUB_ENV + + - name: Extract version (if unstable) + if: github.event.ref == 'refs/heads/unstable' + run: | + echo "VERSION=latest" >> $GITHUB_ENV + echo "VERSION_SUFFIX=-unstable" >> $GITHUB_ENV + + - name: Extract version (if other branch) + if: github.event.ref != 'refs/heads/stable' && github.event.ref != 'refs/heads/unstable' && !startsWith(github.event.ref, 'refs/tags') + run: | + echo "VERSION=${GITHUB_REF#refs/heads/}" >> $GITHUB_ENV + echo "VERSION_SUFFIX=" >> $GITHUB_ENV + + - name: Extract version (if tagged release) + if: startsWith(github.event.ref, 'refs/tags') + run: | + echo "VERSION=$(echo ${GITHUB_REF#refs/tags/})" >> $GITHUB_ENV + echo "VERSION_SUFFIX=" >> $GITHUB_ENV + + - name: Debug - Print environment variables + run: | + echo "[debug] VERSION=${{ env.VERSION }}" + echo "[debug] VERSION_SUFFIX=${{ env.VERSION_SUFFIX }}" \ No newline at end of file diff --git a/.github/workflows/helm-tests.yml b/.github/workflows/helm-tests.yml new file mode 100644 index 0000000..2d063cf --- /dev/null +++ b/.github/workflows/helm-tests.yml @@ -0,0 +1,19 @@ +name: Helm Chart Tests + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Install helm-unittest plugin + run: helm plugin install https://github.com/helm-unittest/helm-unittest.git + + - name: Run Helm unit tests + run: helm unittest charts/eleel -f "charts/eleel/tests/*_test.yaml" \ No newline at end of file diff --git a/.gitignore b/.gitignore index ba5a49f..90f7262 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /target *.swp *.toml +.qodo diff --git a/Dockerfile.cross b/Dockerfile.cross index 25adf89..932763c 100644 --- a/Dockerfile.cross +++ b/Dockerfile.cross @@ -2,9 +2,10 @@ # It assumes the eleel binary has already been # compiled for `$TARGETPLATFORM` and moved to `./bin`. FROM --platform=$TARGETPLATFORM ubuntu:24.04 +ARG TARGET_ARCH RUN apt-get update && apt-get install -y --no-install-recommends \ - libssl-dev \ - ca-certificates \ - && apt-get clean \ - && rm -rf /var/lib/apt/lists/* -COPY ./bin/eleel /usr/local/bin/eleel + libssl-dev \ + ca-certificates \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* +ADD ./target/$TARGET_ARCH/release/eleel /usr/local/bin/eleel diff --git a/Makefile b/Makefile index 71f8a1d..ea7d115 100644 --- a/Makefile +++ b/Makefile @@ -4,4 +4,6 @@ build-x86_64: build-aarch64: cross build --bin eleel --target aarch64-unknown-linux-gnu --profile release --locked +build: build-x86_64 build-aarch64 + .PHONY: build-x86_64 build-aarch64 diff --git a/README.md b/README.md index da29187..260d477 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,14 @@ Eleel is written in Rust and makes use of components from [Lighthouse][]. A binary will be installed to `~/.cargo/bin/eleel`. +### Build docker images + +### Using `bake` + +See https://docs.docker.com/build/building/multi-platform/. + +`docker buildx bake -f docker-bake.hcl` + ## Getting started Eleel needs to connect to a _real_ execution node, e.g. Geth/Nethermind/Besu. You diff --git a/charts/eleel/Chart.yaml b/charts/eleel/Chart.yaml new file mode 100644 index 0000000..35a5597 --- /dev/null +++ b/charts/eleel/Chart.yaml @@ -0,0 +1,7 @@ +apiVersion: v2 +name: eleel +description: A Helm chart for Kubernetes to deploy Eleel Ethereum execution engine multiplexer +type: application +version: 0.1.0 +appVersion: "1.0.0" +kubeVersion: ">=1.19.0-0" \ No newline at end of file diff --git a/charts/eleel/rendered.yaml b/charts/eleel/rendered.yaml new file mode 100644 index 0000000..54742c1 --- /dev/null +++ b/charts/eleel/rendered.yaml @@ -0,0 +1,66 @@ +--- +# Source: eleel/templates/statefulset.yaml +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: eleel-test + labels: + app.kubernetes.io/name: eleel + app.kubernetes.io/instance: eleel-test + app.kubernetes.io/managed-by: Helm + helm.sh/chart: eleel-0.1.0 + app.kubernetes.io/version: "1.0.0" +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: eleel + app.kubernetes.io/instance: eleel-test + serviceName: eleel-test + template: + metadata: + labels: + app.kubernetes.io/name: eleel + app.kubernetes.io/instance: eleel-test + spec: + containers: + - name: eleel + image: "eleel:test" + imagePullPolicy: IfNotPresent + ports: + - name: http + containerPort: 8552 + protocol: TCP + env: + - name: RUST_LOG + value: "eleel=debug" + - name: LISTEN_ADDRESS + value: "0.0.0.0" + - name: LISTEN_PORT + value: "8552" + - name: EE_URL + value: "http://test-engine:8551" + - name: NETWORK + value: "mainnet" + - name: EE_JWT_SECRET + value: "/test-secrets/ee.jwt" + - name: CONTROLLER_JWT_SECRET + value: "/test-secrets/controller.jwt" + - name: CLIENT_JWT_SECRETS + value: "/test-secrets/clients.toml" + volumeMounts: + - name: jwt-secrets + mountPath: /test-secrets + readOnly: true + livenessProbe: + httpGet: + path: /health + port: http + readinessProbe: + httpGet: + path: /health + port: http + volumes: + - name: jwt-secrets + secret: + secretName: "eleel-test-jwt-secrets" diff --git a/charts/eleel/templates/_helpers.tpl b/charts/eleel/templates/_helpers.tpl new file mode 100644 index 0000000..ed0b2fa --- /dev/null +++ b/charts/eleel/templates/_helpers.tpl @@ -0,0 +1,63 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Expand the name of the chart. +*/}} +{{- define "eleel.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 "eleel.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 "eleel.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Common labels +*/}} +{{- define "eleel.labels" -}} +helm.sh/chart: {{ include "eleel.chart" . }} +{{ include "eleel.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end -}} + +{{/* +Selector labels +*/}} +{{- define "eleel.selectorLabels" -}} +app.kubernetes.io/name: {{ include "eleel.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end -}} + +{{/* +Create the name of the service account to use +*/}} +{{- define "eleel.serviceAccountName" -}} +{{- if .Values.serviceAccount.create -}} + {{ default (include "eleel.fullname" .) .Values.serviceAccount.name }} +{{- else -}} + {{ default "default" .Values.serviceAccount.name }} +{{- end -}} +{{- end -}} \ No newline at end of file diff --git a/charts/eleel/templates/statefulset.yaml b/charts/eleel/templates/statefulset.yaml new file mode 100644 index 0000000..4d46892 --- /dev/null +++ b/charts/eleel/templates/statefulset.yaml @@ -0,0 +1,64 @@ +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: {{ include "eleel.fullname" . }} + labels: + app.kubernetes.io/name: {{ include "eleel.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + helm.sh/chart: {{ include "eleel.chart" . }} + app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +spec: + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + app.kubernetes.io/name: {{ include "eleel.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + serviceName: {{ include "eleel.fullname" . }} + template: + metadata: + labels: + app.kubernetes.io/name: {{ include "eleel.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + spec: + containers: + - name: {{ .Chart.Name }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: {{ .Values.service.port }} + protocol: TCP + env: + - name: RUST_LOG + value: "eleel=debug" + - name: LISTEN_ADDRESS + value: "{{ .Values.config.listenAddress }}" + - name: LISTEN_PORT + value: "{{ .Values.config.listenPort }}" + - name: EE_URL + value: "{{ .Values.config.eeUrl }}" + - name: NETWORK + value: "{{ .Values.config.network }}" + - name: EE_JWT_SECRET + value: "{{ .Values.secrets.eeJwtSecret.mountPath }}/{{ .Values.secrets.eeJwtSecret.fileName }}" + - name: CONTROLLER_JWT_SECRET + value: "{{ .Values.secrets.controllerJwtSecret.mountPath }}/{{ .Values.secrets.controllerJwtSecret.fileName }}" + - name: CLIENT_JWT_SECRETS + value: "{{ .Values.secrets.clientJwtSecrets.mountPath }}/{{ .Values.secrets.clientJwtSecrets.fileName }}" + volumeMounts: + - name: jwt-secrets + mountPath: {{ .Values.secrets.eeJwtSecret.mountPath }} + readOnly: true + livenessProbe: + httpGet: + path: /health + port: http + readinessProbe: + httpGet: + path: /health + port: http + volumes: + - name: jwt-secrets + secret: + secretName: "{{ .Release.Name }}-jwt-secrets" \ No newline at end of file diff --git a/charts/eleel/tests/container_test.yaml b/charts/eleel/tests/container_test.yaml new file mode 100644 index 0000000..6022865 --- /dev/null +++ b/charts/eleel/tests/container_test.yaml @@ -0,0 +1,18 @@ +suite: test eleel container +templates: + - templates/statefulset.yaml +tests: + - it: should configure container correctly + asserts: + - equal: + path: spec.template.spec.containers[0].name + value: eleel + - equal: + path: spec.template.spec.containers[0].ports[0].containerPort + value: 8552 + - equal: + path: spec.template.spec.containers[0].env[0].name + value: RUST_LOG + - equal: + path: spec.template.spec.containers[0].env[0].value + value: eleel=debug \ No newline at end of file diff --git a/charts/eleel/tests/deployment_test.yaml b/charts/eleel/tests/deployment_test.yaml new file mode 100644 index 0000000..e87152e --- /dev/null +++ b/charts/eleel/tests/deployment_test.yaml @@ -0,0 +1,20 @@ +suite: test statefulset +templates: + - statefulset.yaml +tests: + - it: should create statefulset with correct properties + asserts: + - isKind: + of: StatefulSet + - matchRegex: + path: metadata.name + pattern: .+-eleel$ + - matchRegex: + path: spec.serviceName + pattern: .+-eleel$ + - equal: + path: spec.replicas + value: 1 + - equal: + path: spec.template.spec.containers[0].name + value: eleel \ No newline at end of file diff --git a/charts/eleel/tests/secrets_test.yaml b/charts/eleel/tests/secrets_test.yaml new file mode 100644 index 0000000..9fe5962 --- /dev/null +++ b/charts/eleel/tests/secrets_test.yaml @@ -0,0 +1,20 @@ +suite: test secrets +templates: + - statefulset.yaml +values: + - test-values.yaml +tests: + - it: should mount jwt secrets + asserts: + - equal: + path: spec.template.spec.volumes[0].name + value: jwt-secrets + - matchRegex: + path: spec.template.spec.volumes[0].secret.secretName + pattern: .+-jwt-secrets$ + - equal: + path: spec.template.spec.containers[0].volumeMounts[0].mountPath + value: /test-secrets + - equal: + path: spec.template.spec.containers[0].volumeMounts[0].readOnly + value: true \ No newline at end of file diff --git a/charts/eleel/tests/test-values.yaml b/charts/eleel/tests/test-values.yaml new file mode 100644 index 0000000..d11ee75 --- /dev/null +++ b/charts/eleel/tests/test-values.yaml @@ -0,0 +1,26 @@ +replicaCount: 1 + +image: + repository: eleel + tag: test + pullPolicy: IfNotPresent + +service: + port: 8552 + +config: + listenAddress: "0.0.0.0" + listenPort: "8552" + eeUrl: "http://test-engine:8551" + network: "mainnet" + +secrets: + eeJwtSecret: + mountPath: "/test-secrets" + fileName: "ee.jwt" + controllerJwtSecret: + mountPath: "/test-secrets" + fileName: "controller.jwt" + clientJwtSecrets: + mountPath: "/test-secrets" + fileName: "clients.toml" \ No newline at end of file diff --git a/charts/eleel/values.yaml b/charts/eleel/values.yaml new file mode 100644 index 0000000..2f6bdc5 --- /dev/null +++ b/charts/eleel/values.yaml @@ -0,0 +1,48 @@ +# Default values for eleel. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 1 + +image: + repository: eleel + tag: latest + pullPolicy: IfNotPresent + +service: + port: 8552 + +config: + listenAddress: "127.0.0.1" + listenPort: "8552" + eeUrl: "http://localhost:8551" + network: "mainnet" + +secrets: + eeJwtSecret: + mountPath: "/secrets" + fileName: "ee.jwt" + controllerJwtSecret: + mountPath: "/secrets" + fileName: "controller.jwt" + clientJwtSecrets: + mountPath: "/secrets" + fileName: "clients.toml" + +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 + +nodeSelector: {} + +tolerations: [] + +affinity: {} \ No newline at end of file diff --git a/docker-bake.hcl b/docker-bake.hcl new file mode 100644 index 0000000..81e8555 --- /dev/null +++ b/docker-bake.hcl @@ -0,0 +1,52 @@ +variable "GITHUB_REPO" { + default = "sigp/eleel" +} + +variable "DESCRIPTION" { + default = "Eleel is a multiplexer for Ethereum execution clients. It allows multiple consensus clients to connect to a single execution client." +} + +group "default" { + targets = ["binary-amd64", "binary-arm64"] + + context = "." + + + +} + +target "binary-amd64" { + dockerfile = "Dockerfile.cross" + context = "." + platforms = ["linux/amd64"] + description = "${DESCRIPTION}" + tags = ["${GITHUB_REPO}-amd64"] + labels = { + "org.opencontainers.image.source" = "https://github.com/${GITHUB_REPO}" + } + + args = { + TARGET_ARCH = "x86_64-unknown-linux-gnu" + } +} + +target "binary-arm64" { + dockerfile = "Dockerfile.cross" + description = "${DESCRIPTION}" + context = "." + platforms = ["linux/arm64"] + tags = ["${GITHUB_REPO}-arm64"] + labels = { + "org.opencontainers.image.source" = "https://github.com/${GITHUB_REPO}" + } + + args = { + TARGET_ARCH = "aarch64-unknown-linux-gnu" + } +} + +target "manifest" { + inherits = ["binary"] + output = ["type=registry"] + description = "Eleel is a multiplexer for Ethereum execution clients. It allows multiple consensus clients to connect to a single execution client." +}