|
| 1 | +#!/usr/bin/env bash |
| 2 | + |
| 3 | +# Copyright 2026 The Kube Bind Authors. |
| 4 | +# |
| 5 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
| 6 | +# you may not use this file except in compliance with the License. |
| 7 | +# You may obtain a copy of the License at |
| 8 | +# |
| 9 | +# http://www.apache.org/licenses/LICENSE-2.0 |
| 10 | +# |
| 11 | +# Unless required by applicable law or agreed to in writing, software |
| 12 | +# distributed under the License is distributed on an "AS IS" BASIS, |
| 13 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 14 | +# See the License for the specific language governing permissions and |
| 15 | +# limitations under the License. |
| 16 | + |
| 17 | +# End-to-end test of the v2 slim-core konnector as a real in-cluster deployment. |
| 18 | +# |
| 19 | +# Unlike hack/demo.sh (which runs the konnector on the host via `go run`), this |
| 20 | +# script exercises the shipping artifact: it builds the container image, loads |
| 21 | +# it into kind, installs the konnector with Helm into the consumer cluster, and |
| 22 | +# then asserts the full bind/sync flow end to end. |
| 23 | +# |
| 24 | +# Steps: |
| 25 | +# 1. Create two kind clusters (provider + consumer). |
| 26 | +# 2. Build the konnector image and load it into the consumer cluster. |
| 27 | +# 3. Provider: install the exported Widget CRD. |
| 28 | +# 4. Consumer: helm install the konnector (chart bundles the core CRDs). |
| 29 | +# 5. Consumer: store the provider kubeconfig as a Secret and apply the bundle |
| 30 | +# (Connection + ClusterBinding). |
| 31 | +# 6. Assert: Connection Ready, Widget CRD pulled, ClusterBinding Synced. |
| 32 | +# 7. Assert: a Widget created on the consumer syncs UP to the provider. |
| 33 | +# 8. Assert: status set on the provider flows DOWN to the consumer. |
| 34 | +# |
| 35 | +# Usage: |
| 36 | +# hack/e2e.sh # full run, tears the clusters down at the end |
| 37 | +# KEEP=1 hack/e2e.sh # leave the clusters and konnector running afterwards |
| 38 | +# NO_BUILD=1 hack/e2e.sh # reuse an already-built/loaded image (faster reruns) |
| 39 | +# |
| 40 | +# Env overrides: PROVIDER, CONSUMER, NAMESPACE, IMAGE, RELEASE, TIMEOUT. |
| 41 | +set -euo pipefail |
| 42 | + |
| 43 | +PROVIDER=${PROVIDER:-kbind-e2e-provider} |
| 44 | +CONSUMER=${CONSUMER:-kbind-e2e-consumer} |
| 45 | +NAMESPACE=${NAMESPACE:-kbind} |
| 46 | +IMAGE=${IMAGE:-kbind/konnector:e2e} |
| 47 | +RELEASE=${RELEASE:-konnector} |
| 48 | +TIMEOUT=${TIMEOUT:-180} |
| 49 | +KEEP=${KEEP:-0} |
| 50 | +NO_BUILD=${NO_BUILD:-0} |
| 51 | + |
| 52 | +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" |
| 53 | +ROOT="$(cd "${HERE}/.." && pwd)" |
| 54 | +SAMPLES="${ROOT}/config/samples" |
| 55 | +CHART="${ROOT}/deploy/charts/konnector" |
| 56 | +PROVIDER_KC="$(mktemp -t kbind-e2e-provider.XXXXXX)" |
| 57 | +CONSUMER_KC="$(mktemp -t kbind-e2e-consumer.XXXXXX)" |
| 58 | +# Kubeconfig reachable from inside the consumer cluster (server = provider node's |
| 59 | +# IP on the docker "kind" network, not the host-mapped 127.0.0.1 port). |
| 60 | +PROVIDER_INTERNAL_KC="$(mktemp -t kbind-e2e-provider-internal.XXXXXX)" |
| 61 | + |
| 62 | +IMAGE_REPO="${IMAGE%:*}" |
| 63 | +IMAGE_TAG="${IMAGE##*:}" |
| 64 | + |
| 65 | +kp() { kubectl --kubeconfig "${PROVIDER_KC}" "$@"; } |
| 66 | +kc() { kubectl --kubeconfig "${CONSUMER_KC}" "$@"; } |
| 67 | + |
| 68 | +RED=$'\033[31m'; GREEN=$'\033[32m'; BOLD=$'\033[1m'; RESET=$'\033[0m' |
| 69 | +info() { echo "${BOLD}==>${RESET} $*"; } |
| 70 | +pass() { echo "${GREEN} ✓${RESET} $*"; } |
| 71 | +fail() { echo "${RED} ✗ $*${RESET}" >&2; exit 1; } |
| 72 | + |
| 73 | +# retry <attempts> <delay-seconds> <cmd...> — succeeds as soon as <cmd> does. |
| 74 | +retry() { |
| 75 | + local attempts=$1 delay=$2; shift 2 |
| 76 | + local i |
| 77 | + for ((i = 1; i <= attempts; i++)); do |
| 78 | + if "$@"; then return 0; fi |
| 79 | + sleep "${delay}" |
| 80 | + done |
| 81 | + return 1 |
| 82 | +} |
| 83 | + |
| 84 | +cleanup() { |
| 85 | + rm -f "${PROVIDER_KC}" "${CONSUMER_KC}" "${PROVIDER_INTERNAL_KC}" |
| 86 | + if [[ "${KEEP}" == "1" ]]; then |
| 87 | + cat <<EOF |
| 88 | +
|
| 89 | +${BOLD}Clusters left running (KEEP=1).${RESET} Tear down with: |
| 90 | + kind delete cluster --name ${PROVIDER} |
| 91 | + kind delete cluster --name ${CONSUMER} |
| 92 | +EOF |
| 93 | + return |
| 94 | + fi |
| 95 | + info "Tearing down kind clusters" |
| 96 | + kind delete cluster --name "${PROVIDER}" >/dev/null 2>&1 || true |
| 97 | + kind delete cluster --name "${CONSUMER}" >/dev/null 2>&1 || true |
| 98 | +} |
| 99 | +trap cleanup EXIT |
| 100 | + |
| 101 | +# ---------------------------------------------------------------------------- |
| 102 | +info "Creating kind clusters (${PROVIDER}, ${CONSUMER})" |
| 103 | +kind create cluster --name "${PROVIDER}" >/dev/null 2>&1 || true |
| 104 | +kind create cluster --name "${CONSUMER}" >/dev/null 2>&1 || true |
| 105 | +kind get kubeconfig --name "${PROVIDER}" > "${PROVIDER_KC}" |
| 106 | +kind get kubeconfig --name "${CONSUMER}" > "${CONSUMER_KC}" |
| 107 | + |
| 108 | +# A kind cluster can report "created" before its API server is reachable, and a |
| 109 | +# memory-starved docker host can leave a node NotReady. Gate on real readiness |
| 110 | +# so later steps fail loudly here instead of with a cryptic "connection refused". |
| 111 | +info "Waiting for both clusters' API servers and nodes to be Ready" |
| 112 | +retry "${TIMEOUT}" 2 kp get --raw=/readyz >/dev/null 2>&1 \ |
| 113 | + || fail "provider API server never became ready (check 'docker ps' — the kind node may have been OOM-killed)" |
| 114 | +retry "${TIMEOUT}" 2 kc get --raw=/readyz >/dev/null 2>&1 \ |
| 115 | + || fail "consumer API server never became ready (check 'docker ps' — the kind node may have been OOM-killed)" |
| 116 | +kp wait --for=condition=Ready nodes --all --timeout="${TIMEOUT}s" >/dev/null |
| 117 | +kc wait --for=condition=Ready nodes --all --timeout="${TIMEOUT}s" >/dev/null |
| 118 | +pass "both clusters are reachable and Ready" |
| 119 | + |
| 120 | +# The konnector runs as a pod in the consumer cluster, so it cannot reach the |
| 121 | +# provider via the host-mapped 127.0.0.1 port. Rewrite the server to the |
| 122 | +# provider node's IP on the shared docker "kind" network; the kind API server |
| 123 | +# cert includes that internal IP as a SAN, so TLS still verifies. |
| 124 | +PROVIDER_IP="$(docker inspect -f '{{(index .NetworkSettings.Networks "kind").IPAddress}}' "${PROVIDER}-control-plane")" |
| 125 | +[[ -n "${PROVIDER_IP}" ]] || fail "could not determine provider node IP on the kind network" |
| 126 | +sed -E "s#server: https://127\.0\.0\.1:[0-9]+#server: https://${PROVIDER_IP}:6443#" \ |
| 127 | + "${PROVIDER_KC}" > "${PROVIDER_INTERNAL_KC}" |
| 128 | + |
| 129 | +# ---------------------------------------------------------------------------- |
| 130 | +if [[ "${NO_BUILD}" == "1" ]]; then |
| 131 | + info "Skipping image build (NO_BUILD=1); loading ${IMAGE} into ${CONSUMER}" |
| 132 | +else |
| 133 | + info "Building konnector image ${IMAGE}" |
| 134 | + docker build -t "${IMAGE}" "${ROOT}" |
| 135 | +fi |
| 136 | +info "Loading ${IMAGE} into the consumer cluster" |
| 137 | +kind load docker-image "${IMAGE}" --name "${CONSUMER}" |
| 138 | + |
| 139 | +# ---------------------------------------------------------------------------- |
| 140 | +info "Provider: installing the exported Widget CRD" |
| 141 | +kp apply -f "${SAMPLES}/provider-widget-crd.yaml" |
| 142 | + |
| 143 | +# ---------------------------------------------------------------------------- |
| 144 | +info "Consumer: helm install the konnector (chart bundles the core CRDs)" |
| 145 | +kc create namespace "${NAMESPACE}" --dry-run=client -o yaml | kc apply -f - |
| 146 | +helm --kubeconfig "${CONSUMER_KC}" upgrade --install "${RELEASE}" "${CHART}" \ |
| 147 | + --namespace "${NAMESPACE}" \ |
| 148 | + --set image.repository="${IMAGE_REPO}" \ |
| 149 | + --set image.tag="${IMAGE_TAG}" \ |
| 150 | + --set image.pullPolicy=IfNotPresent \ |
| 151 | + --wait --timeout "${TIMEOUT}s" |
| 152 | + |
| 153 | +DEPLOY="$(kc -n "${NAMESPACE}" get deploy -l app.kubernetes.io/instance="${RELEASE}" -o name | head -n1)" |
| 154 | +[[ -n "${DEPLOY}" ]] || fail "konnector deployment not found" |
| 155 | +kc -n "${NAMESPACE}" rollout status "${DEPLOY}" --timeout="${TIMEOUT}s" |
| 156 | +pass "konnector deployment is available" |
| 157 | + |
| 158 | +# ---------------------------------------------------------------------------- |
| 159 | +info "Consumer: store the provider kubeconfig and apply the bundle" |
| 160 | +kc -n "${NAMESPACE}" delete secret demo-provider-kubeconfig --ignore-not-found >/dev/null |
| 161 | +kc -n "${NAMESPACE}" create secret generic demo-provider-kubeconfig \ |
| 162 | + --from-file=kubeconfig="${PROVIDER_INTERNAL_KC}" |
| 163 | +kc apply -f "${SAMPLES}/binding.yaml" |
| 164 | + |
| 165 | +# ---------------------------------------------------------------------------- |
| 166 | +info "Asserting the bind flow" |
| 167 | +kc wait --for=condition=Ready connection/demo-provider --timeout="${TIMEOUT}s" \ |
| 168 | + || fail "Connection did not become Ready" |
| 169 | +pass "Connection demo-provider is Ready" |
| 170 | + |
| 171 | +retry "${TIMEOUT}" 1 kc get crd widgets.example.org >/dev/null 2>&1 \ |
| 172 | + || fail "Widget CRD was not pulled onto the consumer" |
| 173 | +kc wait --for=condition=Established crd/widgets.example.org --timeout="${TIMEOUT}s" >/dev/null |
| 174 | +pass "Widget CRD pulled onto the consumer and Established" |
| 175 | + |
| 176 | +kc wait --for=condition=Synced clusterbinding/widgets --timeout="${TIMEOUT}s" \ |
| 177 | + || fail "ClusterBinding did not become Synced" |
| 178 | +pass "ClusterBinding widgets is Synced" |
| 179 | + |
| 180 | +# ---------------------------------------------------------------------------- |
| 181 | +info "Asserting spec sync UP (consumer -> provider)" |
| 182 | +kc apply -f "${SAMPLES}/widget.yaml" |
| 183 | +retry "${TIMEOUT}" 1 kp -n default get widget my-widget >/dev/null 2>&1 \ |
| 184 | + || fail "Widget did not sync up to the provider" |
| 185 | +SIZE="$(kp -n default get widget my-widget -o jsonpath='{.spec.size}')" |
| 186 | +[[ "${SIZE}" == "large" ]] || fail "Widget synced up but spec.size=${SIZE:-<empty>} (want large)" |
| 187 | +pass "Widget synced up to the provider (spec.size=large)" |
| 188 | + |
| 189 | +# ---------------------------------------------------------------------------- |
| 190 | +info "Asserting status sync DOWN (provider -> consumer)" |
| 191 | +kp -n default patch widget my-widget --subresource=status --type=merge \ |
| 192 | + -p '{"status":{"phase":"Running"}}' |
| 193 | +phase_is_running() { |
| 194 | + [[ "$(kc -n default get widget my-widget -o jsonpath='{.status.phase}' 2>/dev/null)" == "Running" ]] |
| 195 | +} |
| 196 | +retry "${TIMEOUT}" 1 phase_is_running \ |
| 197 | + || fail "provider status did not flow down to the consumer" |
| 198 | +pass "status flowed down to the consumer (status.phase=Running)" |
| 199 | + |
| 200 | +echo |
| 201 | +echo "${GREEN}${BOLD}E2E PASSED${RESET}" |
0 commit comments