Skip to content

Commit d276347

Browse files
authored
Add e2e kind high level test (#567)
1 parent 9c044dd commit d276347

4 files changed

Lines changed: 227 additions & 2 deletions

File tree

.github/workflows/ci.yaml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,20 @@ jobs:
6060
--platform linux/amd64,linux/arm64 \
6161
--provenance=false \
6262
.
63+
64+
e2e-kind:
65+
name: e2e-kind
66+
runs-on: ubuntu-latest
67+
steps:
68+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
69+
# kubectl and docker are preinstalled on ubuntu-latest; pin kind and helm via
70+
# direct downloads to keep the toolchain reproducible without extra actions.
71+
- name: Install kind and helm
72+
run: |
73+
curl -fsSLo ./kind https://kind.sigs.k8s.io/dl/v0.27.0/kind-linux-amd64
74+
chmod +x ./kind && sudo mv ./kind /usr/local/bin/kind
75+
curl -fsSL https://get.helm.sh/helm-v3.17.1-linux-amd64.tar.gz | tar -xz
76+
sudo mv linux-amd64/helm /usr/local/bin/helm
77+
# Builds the image, loads it into a kind consumer cluster, helm-installs the
78+
# konnector, and asserts the full bind/sync flow against a kind provider.
79+
- run: make test-e2e-kind

Makefile

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,13 @@ test-e2e:
7171
KUBEBUILDER_ASSETS="$$($(SETUP_ENVTEST) use $(ENVTEST_K8S_VERSION) -p path)" \
7272
go test ./test/e2e/... -count=1 -timeout 600s
7373

74+
# Full kind-based e2e: build the image, load it into kind, helm-install the
75+
# konnector, and assert the bind/sync flow end to end. Pass KEEP=1 to leave the
76+
# clusters running, NO_BUILD=1 to reuse an already-loaded image.
77+
.PHONY: test-e2e-kind
78+
test-e2e-kind:
79+
IMAGE=$(IMAGE) ./hack/e2e.sh
80+
7481
.PHONY: vet
7582
vet:
7683
go vet ./...

hack/demo.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ PROVIDER=kbind-provider
2323
CONSUMER=kbind-consumer
2424
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
2525
V2="$(cd "${HERE}/.." && pwd)"
26-
SAMPLES="${V2}/konnector/config/samples"
26+
SAMPLES="${V2}/config/samples"
2727
PROVIDER_KC=/tmp/${PROVIDER}.kubeconfig
2828
CONSUMER_KC=/tmp/${CONSUMER}.kubeconfig
2929

@@ -59,7 +59,7 @@ cat <<EOF
5959
================================================================================
6060
Demo wired. Now run the konnector against the consumer cluster:
6161
62-
cd ${V2}/konnector
62+
cd ${V2}
6363
KUBECONFIG=${CONSUMER_KC} go run ./cmd/konnector
6464
6565
Then, in another terminal, watch it work:

hack/e2e.sh

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
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

Comments
 (0)