Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 128 additions & 0 deletions .github/actions/federation-bootstrap/action.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
name: federation-bootstrap
description: >
Stand up a Karmada federation topology (hub kind cluster + N member kind
clusters, joined and labeled) and expose the kubeconfig bundle. Any Datum
service e2e that needs a hub + members gets it from one action. Unlike
kind-bootstrap this does NOT set KUBECONFIG in the environment — there is no
single sensible default among the hub / karmada / member kubeconfigs; consume
the emitted outputs instead.

# ---------- inputs ----------
inputs:
hub_cluster:
description: "kind cluster name hosting the Karmada control plane (the hub)"
required: false
default: federation-hub
members:
description: >
Space-separated `<name>[=<label>]` member entries (newlines tolerated),
e.g. "compute-pop-dfw=dfw compute-pop-ord=ord".
required: true
member_label_key:
description: "Label key applied to each member carrying its label value"
required: false
default: topology.datum.net/city-code
karmada_api_nodeport:
description: "NodePort + host port for the Karmada apiserver"
required: false
default: "32443"
karmada_version:
description: "Karmada Helm chart version (empty ⇒ Taskfile default)"
required: false
default: ""
wait_timeout:
description: "Timeout for shorter waits such as member Ready (e.g. 300s, 5m)"
required: false
default: "300s"
karmada_wait_timeout:
description: "Timeout for Karmada install/converge waits (e.g. 10m)"
required: false
default: "10m"
ref:
description: >
test-infra ref to check out. Empty ⇒ github.action_ref, so the inner
checkout matches the pin on the `uses:` line.
required: false
default: ""
images:
description: "Space-separated images to load into the hub + all members (optional)"
required: false
default: ""

# ---------- outputs ----------
outputs:
kubeconfig_dir:
description: "Directory holding the federation kubeconfig bundle"
value: ${{ steps.emit.outputs.kubeconfig_dir }}
hub_kubeconfig:
description: "Host-side kubeconfig for the hub cluster"
value: ${{ steps.emit.outputs.hub_kubeconfig }}
karmada_kubeconfig:
description: "Karmada apiserver kubeconfig (localhost:NodePort)"
value: ${{ steps.emit.outputs.karmada_kubeconfig }}
karmada_internal_kubeconfig:
description: "Karmada apiserver kubeconfig (docker-bridge IP:NodePort)"
value: ${{ steps.emit.outputs.karmada_internal_kubeconfig }}

runs:
using: composite
steps:
- name: Checkout test-infra
uses: actions/checkout@v6
with:
repository: datum-cloud/test-infra
path: test-infra
ref: ${{ inputs.ref != '' && inputs.ref || github.action_ref }}

- name: Install Task
uses: arduino/setup-task@v2
with:
version: 3.x
repo-token: ${{ github.token }}

- name: Stand up federation topology
shell: bash
working-directory: test-infra
run: |
# Newlines in the members input are tolerated: fold them to spaces.
MEMBERS=$(printf '%s' "${{ inputs.members }}" | tr '\n' ' ')
# Only pass KARMADA_VERSION when set: Task treats a CLI-provided empty
# value as authoritative and skips the Taskfile's `| default`, so passing
# an empty string here would zero out the chart + karmadactl versions.
VERSION_ARG=()
if [ -n "${{ inputs.karmada_version }}" ]; then
VERSION_ARG=(KARMADA_VERSION="${{ inputs.karmada_version }}")
fi
task federation-up \
FEDERATION_HUB_CLUSTER="${{ inputs.hub_cluster }}" \
FEDERATION_MEMBERS="$MEMBERS" \
FEDERATION_MEMBER_LABEL_KEY="${{ inputs.member_label_key }}" \
KARMADA_API_NODEPORT="${{ inputs.karmada_api_nodeport }}" \
WAIT_TIMEOUT="${{ inputs.wait_timeout }}" \
KARMADA_WAIT_TIMEOUT="${{ inputs.karmada_wait_timeout }}" \
"${VERSION_ARG[@]}"

- name: Load images into hub + members
if: ${{ inputs.images != '' }}
shell: bash
working-directory: test-infra
run: |
MEMBERS=$(printf '%s' "${{ inputs.members }}" | tr '\n' ' ')
IMG_LIST=$(printf '%s' "${{ inputs.images }}" | tr '\n' ' ')
task federation-load-image \
FEDERATION_HUB_CLUSTER="${{ inputs.hub_cluster }}" \
FEDERATION_MEMBERS="$MEMBERS" \
IMAGES="$IMG_LIST"

- id: emit
name: Export kubeconfig paths
shell: bash
working-directory: test-infra
run: |
DIR="$PWD/kubeconfigs/federation"
{
echo "kubeconfig_dir=$DIR"
echo "hub_kubeconfig=$DIR/${{ inputs.hub_cluster }}.yaml"
echo "karmada_kubeconfig=$DIR/karmada.yaml"
echo "karmada_internal_kubeconfig=$DIR/karmada-internal.yaml"
} >> "$GITHUB_OUTPUT"
165 changes: 165 additions & 0 deletions .github/workflows/federation-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
name: federation-topology-test
on:
pull_request:
branches: [ main ]
types: [opened, synchronize, reopened]
push:
branches:
- main

jobs:
federation:
runs-on: ubuntu-latest
timeout-minutes: 30
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

env:
HUB: dogfood-hub
# Two members exercises the join loop and both label paths.
MEMBERS: dogfood-member-a=dfw dogfood-member-b=ord

steps:
- uses: actions/checkout@v6
with: { fetch-depth: 0 }

- name: Stand up federation topology
id: fed
uses: ./.github/actions/federation-bootstrap
with:
hub_cluster: dogfood-hub
members: ${{ env.MEMBERS }}
# Dogfood the Taskfile at THIS commit, not test-infra's default branch.
ref: ${{ github.event.pull_request.head.sha || github.sha }}

- name: Assert both members are Ready
run: |
set -euo pipefail
KARMADA="${{ steps.fed.outputs.karmada_kubeconfig }}"
for m in dogfood-member-a dogfood-member-b; do
kubectl --kubeconfig "$KARMADA" wait --for=condition=Ready \
"cluster/$m" --timeout=120s
echo "✅ $m is Ready"
done

- name: Assert city-code labels
run: |
set -euo pipefail
KARMADA="${{ steps.fed.outputs.karmada_kubeconfig }}"
check() {
local cluster="$1" want="$2"
got=$(kubectl --kubeconfig "$KARMADA" get cluster "$cluster" \
-o jsonpath='{.metadata.labels.topology\.datum\.net/city-code}')
if [ "$got" != "$want" ]; then
echo "❌ $cluster city-code=[$got], expected [$want]"; exit 1
fi
echo "✅ $cluster labeled city-code=$got"
}
check dogfood-member-a dfw
check dogfood-member-b ord

- name: PropagationPolicy round-trip (dfw only)
run: |
set -euo pipefail
KARMADA="${{ steps.fed.outputs.karmada_kubeconfig }}"
DIR="${{ steps.fed.outputs.kubeconfig_dir }}"
MEMBER_A="$DIR/dogfood-member-a.yaml"
MEMBER_B="$DIR/dogfood-member-b.yaml"

# A ConfigMap plus a PropagationPolicy that targets only members labeled
# city-code=dfw (member-a). This is the one assertion that proves the
# patched member Secret + apiEndpoint actually carry traffic — Karmada
# can only place the object if it can reach member-a's API server.
kubectl --kubeconfig "$KARMADA" apply -f - <<'EOF'
apiVersion: v1
kind: ConfigMap
metadata:
name: dogfood-cm
namespace: default
data:
hello: federation
---
apiVersion: policy.karmada.io/v1alpha1
kind: PropagationPolicy
metadata:
name: dogfood-cm
namespace: default
spec:
resourceSelectors:
- apiVersion: v1
kind: ConfigMap
name: dogfood-cm
placement:
clusterAffinity:
labelSelector:
matchLabels:
topology.datum.net/city-code: dfw
EOF

echo "⏳ Waiting for dogfood-cm to land on member-a (dfw)..."
for i in $(seq 1 30); do
if kubectl --kubeconfig "$MEMBER_A" -n default get configmap dogfood-cm >/dev/null 2>&1; then
echo "✅ dogfood-cm present on member-a"; break
fi
[ "$i" = "30" ] && { echo "❌ dogfood-cm never propagated to member-a"; exit 1; }
sleep 5
done

# member-b (ord) must NOT receive it — proves label-scoped placement.
if kubectl --kubeconfig "$MEMBER_B" -n default get configmap dogfood-cm >/dev/null 2>&1; then
echo "❌ dogfood-cm unexpectedly present on member-b (ord)"; exit 1
fi
echo "✅ dogfood-cm absent on member-b, as expected"

# Delete and confirm it is withdrawn from member-a.
kubectl --kubeconfig "$KARMADA" delete configmap dogfood-cm -n default
kubectl --kubeconfig "$KARMADA" delete propagationpolicy dogfood-cm -n default
echo "⏳ Waiting for dogfood-cm to be withdrawn from member-a..."
for i in $(seq 1 30); do
if ! kubectl --kubeconfig "$MEMBER_A" -n default get configmap dogfood-cm >/dev/null 2>&1; then
echo "✅ dogfood-cm withdrawn from member-a"; break
fi
[ "$i" = "30" ] && { echo "❌ dogfood-cm never withdrawn from member-a"; exit 1; }
sleep 5
done

- name: Federation status
run: |
cd test-infra
task federation-status \
FEDERATION_HUB_CLUSTER=dogfood-hub \
FEDERATION_MEMBERS="${{ env.MEMBERS }}"

- name: Collect diagnostics on failure
if: failure()
run: |
set +e
mkdir -p /tmp/fed-diagnostics
for c in dogfood-hub dogfood-member-a dogfood-member-b; do
kind export logs --name "$c" "/tmp/fed-diagnostics/$c" 2>&1 || true
done
KARMADA="${{ steps.fed.outputs.karmada_kubeconfig }}"
HUB="${{ steps.fed.outputs.hub_kubeconfig }}"
kubectl --kubeconfig "$HUB" -n karmada-system get pods -o wide \
> /tmp/fed-diagnostics/karmada-pods.txt 2>&1 || true
kubectl --kubeconfig "$HUB" -n karmada-system logs -l app=karmada-controller-manager --tail=500 \
> /tmp/fed-diagnostics/karmada-controller-manager.log 2>&1 || true
kubectl --kubeconfig "$KARMADA" get clusters -o yaml \
> /tmp/fed-diagnostics/karmada-clusters.yaml 2>&1 || true

- name: Upload diagnostics
if: failure()
uses: actions/upload-artifact@v4
with:
name: federation-diagnostics
path: /tmp/fed-diagnostics
if-no-files-found: ignore

- name: Tear down
if: always()
run: |
cd test-infra
task federation-down \
FEDERATION_HUB_CLUSTER=dogfood-hub \
FEDERATION_MEMBERS="${{ env.MEMBERS }}"
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,7 @@
*.tgz
# Well known location for the kubeconfig to use talk to the test-infra cluster.
kubeconfig
# Federation kubeconfig bundle (hub/member/karmada configs + federation.env)
kubeconfigs/
# Repo-local pinned tools (e.g. karmadactl)
bin/
Loading
Loading