diff --git a/.github/actions/install-ci-deps/action.yml b/.github/actions/install-ci-deps/action.yml new file mode 100644 index 0000000..7c091a8 --- /dev/null +++ b/.github/actions/install-ci-deps/action.yml @@ -0,0 +1,36 @@ +name: Install CI dependencies +description: Install system packages and tools for ARC/Kubernetes GitHub Actions runners + +inputs: + profile: + description: Dependency set to install (k8s-runner or golang-container) + required: false + default: k8s-runner + install_kind: + description: Install kind + required: false + default: "false" + install_kubectl: + description: Install kubectl + required: false + default: "false" + install_helm: + description: Install Helm + required: false + default: "false" + install_yq: + description: Install yq + required: false + default: "false" + +runs: + using: composite + steps: + - name: Install dependencies + shell: bash + env: + INSTALL_KIND: ${{ inputs.install_kind }} + INSTALL_KUBECTL: ${{ inputs.install_kubectl }} + INSTALL_HELM: ${{ inputs.install_helm }} + INSTALL_YQ: ${{ inputs.install_yq }} + run: bash "${{ github.action_path }}/install-ci-deps.sh" "${{ inputs.profile }}" diff --git a/.github/actions/install-ci-deps/install-ci-deps.sh b/.github/actions/install-ci-deps/install-ci-deps.sh new file mode 100755 index 0000000..76c6975 --- /dev/null +++ b/.github/actions/install-ci-deps/install-ci-deps.sh @@ -0,0 +1,184 @@ +#!/usr/bin/env bash +# Install CI dependencies for GitHub Actions on ARC/Kubernetes runners. +# +# ARC runner pods use the actions-runner image, which is minimal and typically +# runs as a non-root user without sudo. Job-level `container:` directives are +# not supported unless the runner scale set is configured for container jobs, so +# workflows install what they need directly into the runner pod. +set -euo pipefail + +PROFILE="${1:-k8s-runner}" +LOCAL_ROOT="${HOME}/.local" +LOCAL_BIN="${LOCAL_ROOT}/bin" +mkdir -p "$LOCAL_BIN" + +add_to_path() { + local dir="$1" + if [ -d "$dir" ] && [[ ":${PATH}:" != *":${dir}:"* ]]; then + export PATH="${dir}:${PATH}" + if [ -n "${GITHUB_PATH:-}" ]; then + echo "$dir" >> "$GITHUB_PATH" + fi + fi +} + +add_to_path "$LOCAL_BIN" +add_to_path "${LOCAL_ROOT}/usr/bin" + +install_apt_packages() { + local packages=("$@") + if [ "${#packages[@]}" -eq 0 ]; then + return 0 + fi + + if [ "$(id -u)" -eq 0 ] && command -v apt-get >/dev/null 2>&1; then + apt-get update + DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends "${packages[@]}" + return 0 + fi + + if command -v sudo >/dev/null 2>&1 && sudo -n true 2>/dev/null && command -v apt-get >/dev/null 2>&1; then + sudo apt-get update + sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends "${packages[@]}" + return 0 + fi + + if ! command -v dpkg-deb >/dev/null 2>&1; then + echo "dpkg-deb is required to install packages without root." >&2 + exit 1 + fi + + local workdir + workdir="$(mktemp -d)" + trap 'rm -rf "$workdir"' RETURN + + cd "$workdir" + for pkg in "${packages[@]}"; do + if apt-get download "$pkg" 2>/dev/null; then + dpkg-deb -x "${pkg}"_*.deb "$LOCAL_ROOT" + rm -f "${pkg}"_*.deb + continue + fi + + case "$pkg" in + make) + curl -fsSL "http://archive.ubuntu.com/ubuntu/pool/main/m/make-dfsg/make_4.3-4.1build1_amd64.deb" -o make.deb + dpkg-deb -x make.deb "$LOCAL_ROOT" + ;; + *) + echo "Failed to install ${pkg} without root." >&2 + exit 1 + ;; + esac + done +} + +ensure_command() { + command -v "$1" >/dev/null 2>&1 +} + +install_kind() { + ensure_command kind && return 0 + local arch="amd64" + case "$(uname -m)" in + x86_64) arch="amd64" ;; + aarch64|arm64) arch="arm64" ;; + *) + echo "Unsupported architecture for kind: $(uname -m)" >&2 + exit 1 + ;; + esac + curl -fsSL "https://kind.sigs.k8s.io/dl/latest/kind-linux-${arch}" -o "${LOCAL_BIN}/kind" + chmod +x "${LOCAL_BIN}/kind" +} + +install_kubectl() { + ensure_command kubectl && return 0 + local arch="amd64" + case "$(uname -m)" in + x86_64) arch="amd64" ;; + aarch64|arm64) arch="arm64" ;; + *) + echo "Unsupported architecture for kubectl: $(uname -m)" >&2 + exit 1 + ;; + esac + local version + version="$(curl -fsSL https://dl.k8s.io/release/stable.txt)" + curl -fsSL "https://dl.k8s.io/release/${version}/bin/linux/${arch}/kubectl" -o "${LOCAL_BIN}/kubectl" + chmod +x "${LOCAL_BIN}/kubectl" +} + +install_helm() { + ensure_command helm && return 0 + local arch="amd64" + case "$(uname -m)" in + x86_64) arch="amd64" ;; + aarch64|arm64) arch="arm64" ;; + *) + echo "Unsupported architecture for helm: $(uname -m)" >&2 + exit 1 + ;; + esac + local version + version="$(curl -fsSL https://get.helm.sh/helm-latest-version)" + curl -fsSL "https://get.helm.sh/helm-${version}-linux-${arch}.tar.gz" | tar xz -C "${RUNNER_TEMP:-/tmp}" + mv "${RUNNER_TEMP:-/tmp}/linux-${arch}/helm" "${LOCAL_BIN}/helm" + chmod +x "${LOCAL_BIN}/helm" +} + +install_yq() { + ensure_command yq && return 0 + local arch="amd64" + case "$(uname -m)" in + x86_64) arch="amd64" ;; + aarch64|arm64) arch="arm64" ;; + *) + echo "Unsupported architecture for yq: $(uname -m)" >&2 + exit 1 + ;; + esac + curl -fsSL "https://github.com/mikefarah/yq/releases/latest/download/yq_linux_${arch}" -o "${LOCAL_BIN}/yq" + chmod +x "${LOCAL_BIN}/yq" +} + +case "$PROFILE" in + k8s-runner) + missing=() + ensure_command make || missing+=(make) + ensure_command curl || missing+=(curl) + ensure_command git || missing+=(git) + install_apt_packages "${missing[@]}" + add_to_path "${LOCAL_ROOT}/usr/bin" + ;; + golang-container) + install_apt_packages make git curl ca-certificates + add_to_path "${LOCAL_ROOT}/usr/bin" + ;; + *) + echo "Unknown profile: ${PROFILE}" >&2 + exit 1 + ;; +esac + +if [ "${INSTALL_KIND:-false}" = "true" ]; then + install_kind +fi +if [ "${INSTALL_KUBECTL:-false}" = "true" ]; then + install_kubectl +fi +if [ "${INSTALL_HELM:-false}" = "true" ]; then + install_helm +fi +if [ "${INSTALL_YQ:-false}" = "true" ]; then + install_yq +fi + +echo "Installed CI dependencies (profile=${PROFILE})" +command -v make >/dev/null && make --version | head -1 || true +command -v go >/dev/null && go version || true +command -v kind >/dev/null && kind version || true +command -v kubectl >/dev/null && kubectl version --client=true || true +command -v helm >/dev/null && helm version --short || true +command -v yq >/dev/null && yq --version || true +command -v docker >/dev/null && docker version --format '{{.Client.Version}}' 2>/dev/null || true diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 6472813..d139270 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1,32 +1,43 @@ name: Lint on: - push: pull_request: concurrency: group: lint-${{ github.ref }} cancel-in-progress: true +permissions: + contents: read + jobs: lint: name: Lint - runs-on: self-hosted - container: - image: golang:1.25-bookworm + runs-on: hyperbytedb-operator-controller steps: - name: Clone the code uses: actions/checkout@v4 - # The job container runs as root, but the checkout files are owned by the - # runner user. Without marking the workspace as a safe directory, any - # `git` invocation (including the one Go uses for VCS stamping during - # typecheck) fails with `exit status 128`, which surfaces as a typecheck - # error from golangci-lint. + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: "1.25" + cache-dependency-path: go.sum + + # Checkout files are owned by the runner user. Without marking the workspace + # as a safe directory, any `git` invocation (including the one Go uses for + # VCS stamping during typecheck) fails with `exit status 128`, which + # surfaces as a typecheck error from golangci-lint. - name: Configure git safe directory run: git config --global --add safe.directory "$GITHUB_WORKSPACE" + - name: Install CI dependencies + uses: ./.github/actions/install-ci-deps + with: + profile: k8s-runner + - name: Check linter configuration run: make lint-config + - name: Run linter run: make lint diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2c91663..5455a9a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -40,9 +40,7 @@ env: jobs: versions: name: Compute versions - runs-on: self-hosted - container: - image: debian:bookworm-slim + runs-on: hyperbytedb-operator-controller outputs: chart_version: ${{ steps.compute.outputs.chart_version }} image_tag: ${{ steps.compute.outputs.image_tag }} @@ -67,22 +65,16 @@ jobs: image: name: Build & push operator image needs: versions - runs-on: self-hosted - container: - image: golang:1.25-bookworm - volumes: - - /var/run/docker.sock:/var/run/docker.sock + runs-on: hyperbytedb-operator-controller steps: - - name: Install Docker CLI - run: curl -fsSL https://get.docker.com | sh - - uses: actions/checkout@v4 - - name: Configure git safe directory - run: git config --global --add safe.directory "$GITHUB_WORKSPACE" - - name: Login to GHCR - run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login "$REGISTRY" -u "${{ github.actor }}" --password-stdin + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} - name: Build & push image env: @@ -101,21 +93,16 @@ jobs: chart: name: Package & push helm chart needs: [versions, image] - runs-on: self-hosted - container: - image: alpine:3.20 + runs-on: hyperbytedb-operator-controller steps: - - name: Install dependencies - run: apk add --no-cache bash ca-certificates curl git tar yq - - - name: Install Helm - run: | - curl -fsSL https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash - - uses: actions/checkout@v4 - - name: Configure git safe directory - run: git config --global --add safe.directory "$GITHUB_WORKSPACE" + - name: Install CI dependencies + uses: ./.github/actions/install-ci-deps + with: + profile: k8s-runner + install_helm: "true" + install_yq: "true" - name: Login to GHCR run: echo "${{ secrets.GITHUB_TOKEN }}" | helm registry login "$REGISTRY" -u "${{ github.actor }}" --password-stdin @@ -160,15 +147,8 @@ jobs: name: GitHub Release needs: [versions, chart] if: needs.versions.outputs.is_release == 'true' - runs-on: self-hosted - container: - image: debian:bookworm-slim + runs-on: hyperbytedb-operator-controller steps: - - name: Install dependencies - run: | - apt-get update - apt-get install -y ca-certificates curl - - uses: actions/download-artifact@v4 with: name: hyperbytedb-operator-chart diff --git a/.github/workflows/test-chart.yml b/.github/workflows/test-chart.yml index 9d0f8ba..13e55fc 100644 --- a/.github/workflows/test-chart.yml +++ b/.github/workflows/test-chart.yml @@ -1,142 +1,50 @@ name: Test Chart on: - push: pull_request: concurrency: group: test-chart-${{ github.ref }} cancel-in-progress: true +permissions: + contents: read + jobs: test-chart: name: Test Chart - runs-on: self-hosted - container: - image: golang:1.25-bookworm - volumes: - - /var/run/docker.sock:/var/run/docker.sock + runs-on: hyperbytedb-operator-controller + timeout-minutes: 30 env: KIND_CLUSTER_NAME: hyperbytedb-chart-${{ github.run_id }}-${{ github.run_attempt }} - # Image tag is run-scoped so concurrent test-chart jobs sharing the host - # docker daemon don't race on `docker build -t ` and load each - # other's half-built image into their kind cluster. OPERATOR_IMG: hyperbytedb-operator:ci-${{ github.run_id }}-${{ github.run_attempt }} steps: - - name: Install Docker CLI - run: curl -fsSL https://get.docker.com | sh - - name: Clone the code uses: actions/checkout@v4 - - name: Configure git safe directory - run: git config --global --add safe.directory "$GITHUB_WORKSPACE" - - - name: Install kind - run: | - curl -Lo ./kind https://kind.sigs.k8s.io/dl/latest/kind-linux-$(go env GOARCH) - chmod +x ./kind - mv ./kind /usr/local/bin/kind - - - name: Verify kind installation - run: kind version - - - name: Install kubectl - run: | - ARCH=$(go env GOARCH) - KUBECTL_VERSION=$(curl -Ls https://dl.k8s.io/release/stable.txt) - curl -Lo ./kubectl "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/${ARCH}/kubectl" - chmod +x ./kubectl - mv ./kubectl /usr/local/bin/kubectl - kubectl version --client + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: "1.25" + cache-dependency-path: go.sum - # Kind creates its node containers on the host's docker daemon, but the - # default kubeconfig points at 127.0.0.1, which is the *job container's* - # loopback (not the host's). We instead place the kind nodes on the same - # docker network as this job container and use the in-network kubeconfig - # so kubectl/helm can reach the API server by container hostname. - - name: Detect job container docker network - id: network - run: | - NETWORK=$(docker inspect "$(hostname)" \ - --format '{{range $k,$v := .NetworkSettings.Networks}}{{$k}}{{"\n"}}{{end}}' \ - | head -n1) - if [ -z "$NETWORK" ]; then - echo "Failed to detect docker network for $(hostname)" >&2 - exit 1 - fi - echo "Detected network: $NETWORK" - echo "KIND_EXPERIMENTAL_DOCKER_NETWORK=$NETWORK" >> "$GITHUB_ENV" - - # Reap obviously-stale kind clusters left behind by previous runs while - # leaving anything from a *concurrent* run untouched. Deleting another - # in-flight job's cluster races with its kubectl/helm calls and - # produces `dial tcp: lookup -control-plane: no such host` - # failures mid-run. - # - # Rules: - # * The default "kind" cluster is always safe to remove. - # * Anything matching our run-scoped prefix (hyperbytedb-chart-*) - # only gets removed if its control-plane container is older than - # STALE_AFTER_SECONDS or the container is gone entirely (orphaned - # kind metadata). - - name: Cleanup leftover chart kind clusters - env: - STALE_AFTER_SECONDS: "21600" # 6h - run: | - set -eu - NOW=$(date -u +%s) - for cluster in $(kind get clusters 2>/dev/null || true); do - case "$cluster" in - kind) - echo "Deleting default kind cluster: $cluster" - kind delete cluster --name "$cluster" || true - ;; - hyperbytedb-chart-*) - if [ "$cluster" = "$KIND_CLUSTER_NAME" ]; then - continue - fi - started_at=$(docker inspect "${cluster}-control-plane" \ - --format '{{.State.StartedAt}}' 2>/dev/null || true) - if [ -z "$started_at" ]; then - echo "Deleting orphaned kind cluster (no control-plane container): $cluster" - kind delete cluster --name "$cluster" || true - continue - fi - started_epoch=$(date -u -d "$started_at" +%s 2>/dev/null || echo 0) - age=$((NOW - started_epoch)) - if [ "$age" -gt "$STALE_AFTER_SECONDS" ]; then - echo "Deleting stale kind cluster (age=${age}s > ${STALE_AFTER_SECONDS}s): $cluster" - kind delete cluster --name "$cluster" || true - else - echo "Skipping recent kind cluster (age=${age}s, likely concurrent run): $cluster" - fi - ;; - *) - echo "Skipping unrelated kind cluster: $cluster" - ;; - esac - done + - name: Install CI dependencies + uses: ./.github/actions/install-ci-deps + with: + profile: k8s-runner + install_kind: "true" + install_kubectl: "true" + install_helm: "true" - name: Create kind cluster run: kind create cluster --name "$KIND_CLUSTER_NAME" - - name: Configure kubeconfig for in-network access - run: | - mkdir -p "$HOME/.kube" - kind get kubeconfig --name "$KIND_CLUSTER_NAME" --internal > "$HOME/.kube/config" - kubectl version || true - kubectl get nodes - - name: Prepare hyperbytedb-operator run: | go mod tidy make docker-build IMG="$OPERATOR_IMG" kind load docker-image "$OPERATOR_IMG" --name "$KIND_CLUSTER_NAME" - - name: Install Helm - run: make install-helm - - name: Lint Helm Chart run: helm lint ./dist/chart @@ -149,10 +57,3 @@ jobs: - name: Cleanup kind cluster if: always() run: kind delete cluster --name "$KIND_CLUSTER_NAME" || true - - # Remove the run-scoped operator image from the shared host docker - # daemon so we don't accumulate one image per CI run on the self-hosted - # runner. - - name: Cleanup operator image - if: always() - run: docker image rm -f "$OPERATOR_IMG" || true diff --git a/.github/workflows/test-e2e.yml b/.github/workflows/test-e2e.yml index f9805c3..cc97d6f 100644 --- a/.github/workflows/test-e2e.yml +++ b/.github/workflows/test-e2e.yml @@ -1,129 +1,40 @@ name: E2E Tests on: - push: pull_request: concurrency: group: test-e2e-${{ github.ref }} cancel-in-progress: true +permissions: + contents: read + jobs: test-e2e: name: E2E Tests - runs-on: self-hosted - container: - image: golang:1.25-bookworm - volumes: - - /var/run/docker.sock:/var/run/docker.sock + runs-on: hyperbytedb-operator-controller + timeout-minutes: 30 env: KIND_CLUSTER: hyperbytedb-e2e-${{ github.run_id }}-${{ github.run_attempt }} steps: - - name: Install Docker CLI - run: curl -fsSL https://get.docker.com | sh - - name: Clone the code uses: actions/checkout@v4 - - name: Configure git safe directory - run: git config --global --add safe.directory "$GITHUB_WORKSPACE" - - - name: Install kind - run: | - curl -Lo ./kind https://kind.sigs.k8s.io/dl/latest/kind-linux-$(go env GOARCH) - chmod +x ./kind - mv ./kind /usr/local/bin/kind - - - name: Verify kind installation - run: kind version - - - name: Install kubectl - run: | - ARCH=$(go env GOARCH) - KUBECTL_VERSION=$(curl -Ls https://dl.k8s.io/release/stable.txt) - curl -Lo ./kubectl "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/${ARCH}/kubectl" - chmod +x ./kubectl - mv ./kubectl /usr/local/bin/kubectl - kubectl version --client + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: "1.25" + cache-dependency-path: go.sum - # Place the kind nodes on the same docker network as this job container - # so the in-network kubeconfig (used below) is reachable. Without this - # the kubeconfig points at 127.0.0.1, which is the job container's - # loopback (not the host's docker daemon), and kubectl cannot reach the - # cluster. - - name: Detect job container docker network - run: | - NETWORK=$(docker inspect "$(hostname)" \ - --format '{{range $k,$v := .NetworkSettings.Networks}}{{$k}}{{"\n"}}{{end}}' \ - | head -n1) - if [ -z "$NETWORK" ]; then - echo "Failed to detect docker network for $(hostname)" >&2 - exit 1 - fi - echo "Detected network: $NETWORK" - echo "KIND_EXPERIMENTAL_DOCKER_NETWORK=$NETWORK" >> "$GITHUB_ENV" - - # Reap obviously-stale kind clusters left behind by previous runs while - # leaving anything from a *concurrent* run untouched. Deleting another - # in-flight job's cluster races with its kubectl calls and produces - # `dial tcp: lookup -control-plane: no such host` failures - # mid-test. - # - # Rules: - # * The default "kind" cluster and the legacy fixed-name cluster are - # always safe to remove. - # * Anything matching our run-scoped prefix (hyperbytedb-e2e-*) only - # gets removed if its control-plane container is older than the - # STALE_AFTER_SECONDS threshold OR the container is gone entirely - # (orphaned kind metadata). - - name: Cleanup leftover e2e kind clusters - env: - STALE_AFTER_SECONDS: "21600" # 6h - run: | - set -eu - NOW=$(date -u +%s) - for cluster in $(kind get clusters 2>/dev/null || true); do - case "$cluster" in - kind|hyperbytedb-operator-test-e2e) - echo "Deleting legacy/default kind cluster: $cluster" - kind delete cluster --name "$cluster" || true - ;; - hyperbytedb-e2e-*) - if [ "$cluster" = "$KIND_CLUSTER" ]; then - continue - fi - started_at=$(docker inspect "${cluster}-control-plane" \ - --format '{{.State.StartedAt}}' 2>/dev/null || true) - if [ -z "$started_at" ]; then - echo "Deleting orphaned kind cluster (no control-plane container): $cluster" - kind delete cluster --name "$cluster" || true - continue - fi - started_epoch=$(date -u -d "$started_at" +%s 2>/dev/null || echo 0) - age=$((NOW - started_epoch)) - if [ "$age" -gt "$STALE_AFTER_SECONDS" ]; then - echo "Deleting stale kind cluster (age=${age}s > ${STALE_AFTER_SECONDS}s): $cluster" - kind delete cluster --name "$cluster" || true - else - echo "Skipping recent kind cluster (age=${age}s, likely concurrent run): $cluster" - fi - ;; - *) - echo "Skipping unrelated kind cluster: $cluster" - ;; - esac - done - - - name: Create kind cluster - run: kind create cluster --name "$KIND_CLUSTER" - - - name: Configure kubeconfig for in-network access - run: | - mkdir -p "$HOME/.kube" - kind get kubeconfig --name "$KIND_CLUSTER" --internal > "$HOME/.kube/config" - kubectl get nodes + - name: Install CI dependencies + uses: ./.github/actions/install-ci-deps + with: + profile: k8s-runner + install_kind: "true" + install_kubectl: "true" - - name: Running Test e2e + - name: Run e2e tests run: | go mod tidy make test-e2e KIND_CLUSTER="$KIND_CLUSTER" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b91086f..a8896d2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,28 +1,39 @@ name: Tests on: - push: pull_request: concurrency: group: test-${{ github.ref }} cancel-in-progress: true +permissions: + contents: read + jobs: test: name: Test - runs-on: self-hosted - container: - image: golang:1.25-bookworm + runs-on: hyperbytedb-operator-controller steps: - name: Clone the code uses: actions/checkout@v4 + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: "1.25" + cache-dependency-path: go.sum + # Required so that `go list`/`go test` can run `git` for VCS stamping - # inside the container without hitting "dubious ownership" errors. + # without hitting "dubious ownership" errors. - name: Configure git safe directory run: git config --global --add safe.directory "$GITHUB_WORKSPACE" + - name: Install CI dependencies + uses: ./.github/actions/install-ci-deps + with: + profile: k8s-runner + - name: Running Tests run: | go mod tidy diff --git a/.gitignore b/.gitignore index 9f0f3a1..2215a37 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,22 @@ go.work # Kubeconfig might contain secrets *.kubeconfig +kubeconfig +.kube/config + +# Local env and credential files +.env +.env.* +!.env.example +credentials.json +*.pem +*.p12 +*.pfx +id_rsa +id_rsa.* +id_ed25519 +id_ed25519.* +known_hosts + +# Stray native artifacts (not part of this Go operator) +chdb.hpp diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..678d860 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works, within the Source form or + documentation, if provided along with the Derivative Works, or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2026 Hyperbyte Cloud a brand of H&A Digital Services + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/PROJECT b/PROJECT index 450c4df..68e955a 100644 --- a/PROJECT +++ b/PROJECT @@ -3,7 +3,7 @@ # and allow the plugins properly work. # More info: https://book.kubebuilder.io/reference/project-config.html cliVersion: 4.13.0 -domain: hyperbytedb.io +domain: hyperbyte.cloud layout: - go.kubebuilder.io/v4 plugins: @@ -17,7 +17,7 @@ resources: crdVersion: v1 namespaced: true controller: true - domain: hyperbytedb.io + domain: hyperbyte.cloud group: hyperbytedb kind: HyperbytedbCluster path: github.com/hyperbyte-cloud/hyperbytedb-operator/api/v1alpha1 @@ -26,7 +26,7 @@ resources: crdVersion: v1 namespaced: true controller: true - domain: hyperbytedb.io + domain: hyperbyte.cloud group: hyperbytedb kind: HyperbytedbBackup path: github.com/hyperbyte-cloud/hyperbytedb-operator/api/v1alpha1 @@ -35,7 +35,7 @@ resources: crdVersion: v1 namespaced: true controller: true - domain: hyperbytedb.io + domain: hyperbyte.cloud group: hyperbytedb kind: HyperbytedbRestore path: github.com/hyperbyte-cloud/hyperbytedb-operator/api/v1alpha1 diff --git a/chdb.hpp b/chdb.hpp deleted file mode 100644 index 71d18e1..0000000 --- a/chdb.hpp +++ /dev/null @@ -1,561 +0,0 @@ -#pragma once - -#include "chdb.h" -#include -#include -#include -#include -#include -#include -#include - -namespace CHDB -{ - -extern chdb_connection * connect_chdb_with_exception(int argc, char ** argv); - -/** - * These codes provide detailed error classification for better error handling - * and debugging. Each error code corresponds to a specific failure scenario. - */ -enum class ChdbErrorCode : std::uint8_t -{ - Success = 0, ///< Operation completed successfully - InvalidResultHandle, ///< Invalid or null result handle provided - ConnectionFailed, ///< Failed to establish database connection - ConnectionClosed, ///< Operation attempted on closed connection - QueryExecutionFailed, ///< SQL query execution failed - StreamingQueryFailed, ///< Streaming query initialization failed - StreamFetchFailed, ///< Failed to fetch streaming data - CommandLineQueryFailed, ///< Command line query execution failed - UnknownError ///< Unspecified error occurred -}; - -/** - * This exception class extends std::runtime_error to provide additional - * context through error codes. It maintains both human-readable error - * messages and machine-readable error codes for programmatic handling. - */ -class ChdbError : public std::runtime_error { -public: - /** - * Constructs ChdbError with specific error code and message - * @param code The specific error code identifying the failure type - * @param message Human-readable error description - */ - explicit ChdbError(ChdbErrorCode code, const char * message) - : std::runtime_error(message) - , error_code_(code) - { - } - - /** - * Constructs ChdbError with message only (UnknownError code) - * @param message Human-readable error description - */ - explicit ChdbError(const char * message) - : std::runtime_error(message) - , error_code_(ChdbErrorCode::UnknownError) - { - } - - /** - * Gets the specific error code - * @return The error code associated with this exception - */ - ChdbErrorCode code() const noexcept { return error_code_; } - -private: - ChdbErrorCode error_code_; -}; - -/** - * RAII wrapper for ChDB query results - * - * The Result class provides safe and convenient access to query results - * with automatic resource management. It wraps the C API result handle - * and ensures proper cleanup when the object is destroyed. - */ -class Result { -public: - /** - * Constructs Result from C API result handle - * @param result Raw result handle from ChDB C API - * @throws ChdbError if result handle is null - */ - explicit Result(chdb_result* result) : result_(result) { - if (!result_) { - throw ChdbError(ChdbErrorCode::InvalidResultHandle, "Invalid result handle"); - } - } - - /** - * Destructor - automatically cleans up result resources - */ - ~Result() { - if (result_) { - chdb_destroy_query_result(result_); - } - } - - /// Copy constructor is deleted (move-only semantics) - Result(const Result&) = delete; - /// Copy assignment is deleted (move-only semantics) - Result& operator=(const Result&) = delete; - - /** - * Move constructor - transfers ownership of result handle - * @param other Source Result object (will be left in valid but empty state) - */ - Result(Result&& other) noexcept : result_(other.result_) { - other.result_ = nullptr; - } - - /** - * Move assignment operator - transfers ownership of result handle - * @param other Source Result object (will be left in valid but empty state) - * @return Reference to this object - */ - Result& operator=(Result&& other) noexcept { - if (this != &other) { - if (result_) { - chdb_destroy_query_result(result_); - } - result_ = other.result_; - other.result_ = nullptr; - } - return *this; - } - - /** - * Gets result data as string_view (zero-copy) - * @return String view of the result data, empty if result is invalid - * @note The returned view is valid only while this Result object exists - */ - std::string_view data() const { - if (!result_) return {}; - char* buffer = chdb_result_buffer(result_); - size_t length = chdb_result_length(result_); - return std::string_view(buffer, length); - } - - /** - * Gets result data as byte span (zero-copy) - * @return Span of bytes representing the result data - * @note The returned span is valid only while this Result object exists - */ - std::span bytes() const { - if (!result_) return {}; - char* buffer = chdb_result_buffer(result_); - size_t length = chdb_result_length(result_); - return std::span(reinterpret_cast(buffer), length); - } - - /** - * Gets result data as string (copies data) - * @return String copy of the result data - * @note This method creates a copy, use data() for zero-copy access - */ - std::string str() const { - auto view = data(); - return std::string(view); - } - - /** - * Gets the size of result data in bytes - * @return Size of result data, or 0 if result is invalid - */ - size_t size() const { - return result_ ? chdb_result_length(result_) : 0; - } - - /** - * Gets query execution time - * @return Elapsed time for query execution, or 0.0 if result is invalid - */ - double elapsed() const { - return result_ ? chdb_result_elapsed(result_) : 0.0; - } - - /** - * Gets number of rows processed by the query - * @return Number of rows read/processed, or 0 if result is invalid - */ - uint64_t rows_read() const { - return result_ ? chdb_result_rows_read(result_) : 0; - } - - /** - * Gets number of bytes processed by the query - * @return Number of bytes read/processed, or 0 if result is invalid - */ - uint64_t bytes_read() const { - return result_ ? chdb_result_bytes_read(result_) : 0; - } - - /** - * Gets number of rows read from storage - * @return Number of rows read from underlying storage, or 0 if result is invalid - */ - uint64_t storage_rows_read() const { - return result_ ? chdb_result_storage_rows_read(result_) : 0; - } - - /** - * Gets number of bytes read from storage - * @return Number of bytes read from underlying storage, or 0 if result is invalid - */ - uint64_t storage_bytes_read() const { - return result_ ? chdb_result_storage_bytes_read(result_) : 0; - } - - /** - * Gets error message if query failed - * @return Optional containing error message, or nullopt if no error occurred - */ - std::optional error() const { - if (!result_) return std::nullopt; - const char* error_msg = chdb_result_error(result_); - return error_msg ? std::optional(error_msg) : std::nullopt; - } - - /** - * Checks if result contains an error - * @return true if an error occurred, false otherwise - */ - bool has_error() const { - return error().has_value(); - } - - /** - * Throws ChdbError if result contains an error - * @throws ChdbError if the result indicates an error occurred - * @note This is useful for converting error results to exceptions - */ - void throw_if_error() const { - auto err = error(); - if (err) { - throw ChdbError(err->c_str()); - } - } - - /** - * Gets the raw C API result handle - * @return Raw chdb_result pointer (for internal use) - * @warning This is an internal method - use with caution - */ - chdb_result * get() const { return result_; } - -private: - chdb_result * result_; ///< Raw C API result handle -}; - -/** - * The Connection class provides a high-level interface to ChDB database - * operations. It manages the connection lifecycle automatically and provides - * both regular and streaming query capabilities. - */ -class Connection { -public: - /** - * Constructs connection with custom arguments - */ - explicit Connection(const std::vector & args = {}) - { - std::vector argv; - argv.reserve(args.size() + 1); - static std::string chdb_program_name = "chdb"; - argv.push_back(chdb_program_name.data()); - for (const auto & arg : args) - { - argv.push_back(const_cast(arg.data())); - } - chdb_connection * conn_ptr = connect_chdb_with_exception(static_cast(argv.size()), argv.data()); - if (!conn_ptr) - { - throw ChdbError(ChdbErrorCode::ConnectionFailed, "Failed to create database connection"); - } - conn_ = *conn_ptr; - } - - /** - * Constructs connection to file-based database - */ - explicit Connection(const std::string& path) : Connection(std::vector{"--path=" + path}) {} - - ~Connection() { - if (conn_) { - chdb_close_conn(&conn_); - } - } - - /// Copy constructor is deleted (move-only semantics) - Connection(const Connection&) = delete; - /// Copy assignment is deleted (move-only semantics) - Connection& operator=(const Connection&) = delete; - - /** Move constructor - transfers connection ownership */ - Connection(Connection&& other) noexcept : conn_(other.conn_) { - other.conn_ = nullptr; - } - - /** Move assignment - transfers connection ownership */ - Connection& operator=(Connection&& other) noexcept { - if (this != &other) { - if (conn_) { - chdb_close_conn(&conn_); - } - conn_ = other.conn_; - other.conn_ = nullptr; - } - return *this; - } - - /** Execute SQL query and return complete result */ - Result query(const std::string & sql, const std::string & format = "TabSeparated") const - { - if (!conn_) { - throw ChdbError(ChdbErrorCode::ConnectionClosed, "Connection is closed"); - } - chdb_result * result = chdb_query(conn_, sql.c_str(), format.c_str()); - if (!result) { - throw ChdbError(ChdbErrorCode::QueryExecutionFailed, "Query execution failed"); - } - - return Result(result); - } - - /** Initialize streaming query for large datasets */ - Result stream_query(const std::string & sql, const std::string & format = "TabSeparated") const - { - if (!conn_) { - throw ChdbError(ChdbErrorCode::ConnectionClosed, "Connection is closed"); - } - - chdb_result * result = chdb_stream_query(conn_, sql.c_str(), format.c_str()); - if (!result) { - throw ChdbError(ChdbErrorCode::StreamingQueryFailed, "Streaming query initialization failed"); - } - - return Result(result); - } - - /** Fetch next batch from streaming query */ - Result stream_fetch(Result& stream_result) const { - if (!conn_) { - throw ChdbError(ChdbErrorCode::ConnectionClosed, "Connection is closed"); - } - - chdb_result * result = chdb_stream_fetch_result(conn_, stream_result.get()); - if (!result) { - throw ChdbError(ChdbErrorCode::StreamFetchFailed, "Stream fetch failed"); - } - - return Result(result); - } - - /** Cancel ongoing streaming query */ - void stream_cancel(Result& stream_result) const { - if (!conn_) { - throw ChdbError(ChdbErrorCode::ConnectionClosed, "Connection is closed"); - } - - chdb_stream_cancel_query(conn_, stream_result.get()); - } - -private: - chdb_connection conn_; -}; - -/** Iterator for streaming query results */ -class StreamIterator { -public: - /** Construct iterator for active streaming query */ - StreamIterator(const Connection & conn, Result & stream_result) - : conn_(&conn) - , stream_result_(&stream_result) - , finished_(false) - { - try - { - advance(); - } - catch (...) - { - finished_ = true; - throw; - } - } - - /** Construct end iterator */ - StreamIterator() - : conn_(nullptr) - , stream_result_(nullptr) - , finished_(true) - { - } - - StreamIterator(StreamIterator && other) noexcept - : conn_(other.conn_) - , stream_result_(other.stream_result_) - , current_result_(std::move(other.current_result_)) - , finished_(other.finished_) - { - other.conn_ = nullptr; - other.stream_result_ = nullptr; - other.finished_ = true; - } - - StreamIterator & operator=(StreamIterator && other) noexcept - { - if (this != &other) - { - conn_ = other.conn_; - stream_result_ = other.stream_result_; - current_result_ = std::move(other.current_result_); - finished_ = other.finished_; - other.conn_ = nullptr; - other.stream_result_ = nullptr; - other.finished_ = true; - } - return *this; - } - - // No need for custom destructor - std::optional handles cleanup automatically - ~StreamIterator() = default; - - StreamIterator(const StreamIterator&) = delete; - StreamIterator& operator=(const StreamIterator&) = delete; - - Result & operator*() - { - if (!current_result_) - { - throw ChdbError("Dereferencing invalid iterator"); - } - return current_result_.value(); - } - - const Result & operator*() const - { - if (!current_result_) - { - throw ChdbError("Dereferencing invalid iterator"); - } - return current_result_.value(); - } - - Result * operator->() - { - if (!current_result_) - { - throw ChdbError("Dereferencing invalid iterator"); - } - return ¤t_result_.value(); - } - - const Result * operator->() const - { - if (!current_result_) - { - throw ChdbError("Dereferencing invalid iterator"); - } - return ¤t_result_.value(); - } - - StreamIterator& operator++() { - advance(); - return *this; - } - bool operator==(const StreamIterator & other) const - { - return finished_ == other.finished_ && (finished_ || (conn_ == other.conn_ && stream_result_ == other.stream_result_)); - } - - bool operator!=(const StreamIterator& other) const { - return !(*this == other); - } - -private: - void advance() { - if (finished_ || !conn_ || !stream_result_) - return; - - current_result_ = conn_->stream_fetch(*stream_result_); - if (!current_result_ || current_result_->rows_read() == 0 || current_result_->has_error()) - { - finished_ = true; - } - } - - const Connection* conn_; - Result * stream_result_; - std::optional current_result_; - bool finished_; -}; - -/** Range-based for loop support for streaming queries */ -class Stream { -public: - /** Construct stream wrapper for range-based iteration */ - Stream(const Connection & conn, Result & stream_result) - : conn_(conn) - , stream_result_(stream_result) - { - } - - /** Get iterator to start of stream */ - StreamIterator begin() { return StreamIterator(conn_, stream_result_); } - - /** Get iterator representing end of stream */ - StreamIterator end() { - return StreamIterator(); - } - -private: - const Connection& conn_; - Result & stream_result_; -}; - -/** Execute query using command-line style arguments */ -inline Result query_cmdline(const std::vector& args) { - std::vector argv; - std::vector arg_storage; - - arg_storage.reserve(args.size()); - argv.reserve(args.size()); - - for (const auto& arg : args) { - arg_storage.push_back(arg); - argv.push_back(arg_storage.back().data()); - } - - chdb_result* result = chdb_query_cmdline(static_cast(argv.size()), argv.data()); - if (!result) { - throw ChdbError(ChdbErrorCode::CommandLineQueryFailed, "Command line query execution failed"); - } - - return Result(result); -} - -/** Execute query using C-style command-line arguments */ -inline Result query_cmdline(int argc, char** argv) { - chdb_result* result = chdb_query_cmdline(argc, argv); - if (!result) { - throw ChdbError(ChdbErrorCode::CommandLineQueryFailed, "Command line query execution failed"); - } - - return Result(result); -} - -/** Convenience function to create database connection */ -inline Connection connect(const std::string& path = ":memory:") { - return Connection(path); -} - -/** Convenience function to create connection with custom arguments */ -inline Connection connect(const std::vector& args) { - return Connection(args); -} - -} // namespace CHDB diff --git a/config/manager/kustomization.yaml b/config/manager/kustomization.yaml index ad13e96..380704c 100644 --- a/config/manager/kustomization.yaml +++ b/config/manager/kustomization.yaml @@ -4,5 +4,5 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization images: - name: controller - newName: controller - newTag: latest + newName: hyperbytedb-operator + newTag: dev diff --git a/hack/e2e/hyperbytedb-stub/Dockerfile b/hack/e2e/hyperbytedb-stub/Dockerfile new file mode 100644 index 0000000..43ab47e --- /dev/null +++ b/hack/e2e/hyperbytedb-stub/Dockerfile @@ -0,0 +1,15 @@ +FROM golang:1.25-bookworm AS builder + +WORKDIR /src +COPY main.go . +RUN go mod init hyperbytedb-e2e-stub \ + && CGO_ENABLED=0 go build -o hyperbytedb . + +FROM debian:bookworm-slim + +RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=builder /src/hyperbytedb /usr/local/bin/hyperbytedb + +EXPOSE 8086 diff --git a/hack/e2e/hyperbytedb-stub/main.go b/hack/e2e/hyperbytedb-stub/main.go new file mode 100644 index 0000000..e54d0d4 --- /dev/null +++ b/hack/e2e/hyperbytedb-stub/main.go @@ -0,0 +1,33 @@ +// Minimal hyperbytedb stand-in for operator e2e tests. +package main + +import ( + "flag" + "fmt" + "net/http" + "os" +) + +func main() { + var configPath string + flag.StringVar(&configPath, "config", "", "config file path") + flag.Parse() + + if len(flag.Args()) != 1 || flag.Arg(0) != "serve" { + fmt.Fprintln(os.Stderr, "usage: hyperbytedb --config serve") + os.Exit(1) + } + + mux := http.NewServeMux() + mux.HandleFunc("/ping", func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusOK) + }) + mux.HandleFunc("/health", func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusOK) + }) + + if err := http.ListenAndServe(":8086", mux); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} diff --git a/internal/controller/hyperbytedbcluster_controller.go b/internal/controller/hyperbytedbcluster_controller.go index e5f9b27..b66acbf 100644 --- a/internal/controller/hyperbytedbcluster_controller.go +++ b/internal/controller/hyperbytedbcluster_controller.go @@ -19,6 +19,7 @@ package controller import ( "context" "fmt" + "maps" "time" appsv1 "k8s.io/api/apps/v1" @@ -172,6 +173,9 @@ func (r *HyperbytedbClusterReconciler) Reconcile(ctx context.Context, req ctrl.R // 6. StatefulSet stsResult, err := r.reconcileStatefulSet(ctx, cluster, configHash) if err != nil { + if apierrors.IsConflict(err) { + return ctrl.Result{Requeue: true}, nil + } return r.setFailedStatus(ctx, cluster, "StatefulSetFailed", err) } @@ -697,12 +701,8 @@ func (r *HyperbytedbClusterReconciler) reconcileProxy(ctx context.Context, clust // annotation, and the Deployment would scale the new ReplicaSet back // to zero — silently undoing the rollout. mergedTemplateAnnotations := map[string]string{} - for k, v := range existingDep.Spec.Template.Annotations { - mergedTemplateAnnotations[k] = v - } - for k, v := range desiredDep.Spec.Template.Annotations { - mergedTemplateAnnotations[k] = v - } + maps.Copy(mergedTemplateAnnotations, existingDep.Spec.Template.Annotations) + maps.Copy(mergedTemplateAnnotations, desiredDep.Spec.Template.Annotations) if len(mergedTemplateAnnotations) > 0 { desiredDep.Spec.Template.Annotations = mergedTemplateAnnotations } diff --git a/internal/hyperbytedb/configmap.go b/internal/hyperbytedb/configmap.go index b3f8c01..935cd77 100644 --- a/internal/hyperbytedb/configmap.go +++ b/internal/hyperbytedb/configmap.go @@ -265,8 +265,8 @@ func writeClusterSection(b *strings.Builder, cluster *v1alpha1.HyperbytedbCluste b.WriteString("\n[cluster]\n") fmt.Fprintf(b, "enabled = %t\n", clusterEnabled) b.WriteString("peers = \"\"\n") - b.WriteString(fmt.Sprintf("replication_log_dir = \"%s\"\n", defaultReplLogDir)) - b.WriteString(fmt.Sprintf("raft_dir = \"%s\"\n", defaultRaftDir)) + fmt.Fprintf(b, "replication_log_dir = \"%s\"\n", defaultReplLogDir) + fmt.Fprintf(b, "raft_dir = \"%s\"\n", defaultRaftDir) heartbeatInterval := int32(2) if spec.Cluster.HeartbeatIntervalSecs > 0 { diff --git a/test/e2e/e2e_suite_test.go b/test/e2e/e2e_suite_test.go index 5820605..4ec68b2 100644 --- a/test/e2e/e2e_suite_test.go +++ b/test/e2e/e2e_suite_test.go @@ -31,7 +31,8 @@ import ( ) var ( - managerImage = "hyperbytedb-operator:dev" + managerImage = "hyperbytedb-operator:dev" + hyperbytedbImage = "hyperbytedb:latest" ) func TestE2E(t *testing.T) { @@ -46,7 +47,20 @@ var _ = BeforeSuite(func() { _, err := utils.Run(cmd) ExpectWithOffset(1, err).NotTo(HaveOccurred(), "Failed to build the manager image") + By("building the hyperbytedb e2e stub image") + cmd = exec.Command("docker", "build", + "-t", hyperbytedbImage, + "-f", "hack/e2e/hyperbytedb-stub/Dockerfile", + "hack/e2e/hyperbytedb-stub", + ) + _, err = utils.Run(cmd) + ExpectWithOffset(1, err).NotTo(HaveOccurred(), "Failed to build the hyperbytedb e2e stub image") + By("loading the manager image on Kind") err = utils.LoadImageToKindClusterWithName(managerImage) ExpectWithOffset(1, err).NotTo(HaveOccurred(), "Failed to load the manager image into Kind") + + By("loading the hyperbytedb image on Kind") + err = utils.LoadImageToKindClusterWithName(hyperbytedbImage) + ExpectWithOffset(1, err).NotTo(HaveOccurred(), "Failed to load the hyperbytedb image into Kind") }) diff --git a/test/e2e/helpers_test.go b/test/e2e/helpers_test.go index e90429d..935eb6c 100644 --- a/test/e2e/helpers_test.go +++ b/test/e2e/helpers_test.go @@ -17,10 +17,10 @@ metadata: spec: replicas: 1 image: hyperbytedb:latest + imagePullPolicy: Never server: port: 8086 storage: - backend: local volumeClaimTemplate: size: 1Gi logging: @@ -47,10 +47,10 @@ metadata: spec: replicas: %d image: hyperbytedb:latest + imagePullPolicy: Never server: port: 8086 storage: - backend: local volumeClaimTemplate: size: 1Gi logging: diff --git a/test/utils/utils.go b/test/utils/utils.go index ced92c7..45fefcc 100644 --- a/test/utils/utils.go +++ b/test/utils/utils.go @@ -32,7 +32,7 @@ const ( certmanagerURLTmpl = "https://github.com/cert-manager/cert-manager/releases/download/%s/cert-manager.yaml" defaultKindBinary = "kind" - defaultKindCluster = "kind" + defaultKindCluster = "hyperbytedb-operator-test-e2e" ) func warnError(err error) {