-
Notifications
You must be signed in to change notification settings - Fork 264
hack: fix DNS on IPv6-only kind clusters #958
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,6 +21,7 @@ KIND_CLUSTER_NAME="${KIND_CLUSTER_NAME:-kind}" | |
| KUBECTL_CONTEXT="kind-${KIND_CLUSTER_NAME}" | ||
| reg_name="kind-registry" | ||
| reg_port="${KIND_REGISTRY_PORT:-5001}" | ||
| IPV6_DNS_UPSTREAM="${IPV6_DNS_UPSTREAM:-2001:4860:4860::8888 2001:4860:4860::8844}" | ||
|
|
||
| if [[ $# -gt 0 ]]; then | ||
| case "$1" in | ||
|
|
@@ -31,6 +32,8 @@ if [[ $# -gt 0 ]]; then | |
| echo "Configured through the environment:" | ||
| echo " KIND_CLUSTER_NAME Name of the cluster to create (default: kind)." | ||
| echo " IP_FAMILY Address families for pods and Services: ipv4, ipv6 or dual (default: ipv4)." | ||
| echo " IPV6_DNS_UPSTREAM Space-separated IPv6 resolvers CoreDNS forwards to when IP_FAMILY=ipv6" | ||
| echo " (default: Google Public DNS). Override where those are unreachable." | ||
| exit 0 | ||
| ;; | ||
| esac | ||
|
|
@@ -146,14 +149,18 @@ if [[ "${IP_FAMILY}" != "ipv4" && | |
| fi | ||
|
|
||
| # For ipv6 kind writes a kubeconfig pointing at [::1], the address it published | ||
| # the apiserver on, which only works for a client on the Docker host itself: a | ||
| # VM-hosted daemon (Lima on macOS) forwards the port to the *v4* loopback, so | ||
| # every kubectl below fails at connect. localhost is a SAN on the apiserver | ||
| # cert and lets the client pick a family that works from either side. | ||
| # the apiserver on. That works for a client on the Docker host itself but not | ||
| # behind a VM-hosted daemon (Lima on macOS), which forwards the port to the *v4* | ||
| # loopback. localhost is a SAN on the apiserver cert and covers that case -- but | ||
| # only where it resolves to ::1, and Debian-family hosts give ::1 the names | ||
| # ip6-localhost and ip6-loopback, not localhost. Repointing unconditionally | ||
| # therefore trades a working address for a broken one on a Linux host. Ask the | ||
| # apiserver instead of guessing from the platform. | ||
| if [[ "${IP_FAMILY}" == "ipv6" ]]; then | ||
| server="$(kubectl config view \ | ||
| -o jsonpath="{.clusters[?(@.name==\"${KUBECTL_CONTEXT}\")].cluster.server}")" | ||
| if [[ "${server}" == "https://[::1]:"* ]]; then | ||
| if [[ "${server}" == "https://[::1]:"* ]] && | ||
| ! kubectl --context="${KUBECTL_CONTEXT}" get --raw /healthz >/dev/null 2>&1; then | ||
| echo "Repointing the kubeconfig for '${KUBECTL_CONTEXT}' at localhost..." | ||
| kubectl config set-cluster "${KUBECTL_CONTEXT}" \ | ||
| --server="https://localhost:${server##*:}" >/dev/null | ||
|
|
@@ -196,6 +203,61 @@ if [ "$(docker inspect -f='{{json .NetworkSettings.Networks.kind}}' "${reg_name} | |
| docker network connect "kind" "${reg_name}" | ||
| fi | ||
|
|
||
| # 4.5. Give CoreDNS an IPv6 forwarder and a registry entry | ||
| # | ||
| # CoreDNS runs dnsPolicy: Default, inheriting the node's IPv4 resolver, which | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit. make comments more concise, briefly mention the issue and the fix. IIUC, what this logic did is to replace "forward . /etc/resolv.conf" with "forward . ${IPV6_DNS_UPSTREAM}", and add registry. |
||
| # no pod here can reach, so external lookups SERVFAIL. Step 3's registry | ||
| # wiring is node-side, so it misses atelet too: that pull runs in atelet's own | ||
| # netns, where "kind-registry" NXDOMAINs. | ||
| if [[ "${IP_FAMILY}" == "ipv6" ]]; then | ||
| echo "Repointing CoreDNS at an IPv6 resolver and teaching it '${reg_name}'..." | ||
| reg_v6="$(docker inspect "${reg_name}" \ | ||
| --format '{{.NetworkSettings.Networks.kind.GlobalIPv6Address}}')" | ||
| if [[ -z "${reg_v6}" ]]; then | ||
| echo "error: '${reg_name}' has no IPv6 address on the 'kind' network" >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| corefile="$(kubectl --context="${KUBECTL_CONTEXT}" -n kube-system get cm coredns \ | ||
| -o jsonpath='{.data.Corefile}')" | ||
| search="forward . /etc/resolv.conf" | ||
| replace="forward . ${IPV6_DNS_UPSTREAM}" | ||
| # $search unquoted: bash 3.2 splices the quotes in literally. Replacing just | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit. consider removing it |
||
| # the target leaves kind's trailing "{ max_concurrent 1000 }" in place. | ||
| patched="${corefile/$search/$replace}" | ||
| if [[ "${patched}" == "${corefile}" ]]; then | ||
| echo "error: '${search}' not found in the CoreDNS Corefile" >&2 | ||
| echo " the Corefile layout changed upstream; update this block" >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| # Its own server block, not a hosts entry in .:53. A query is served by the | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit. consider removing this |
||
| # one block whose zone is its longest suffix, so only "${reg_name}" arrives | ||
| # here -- which is why this hosts needs no fallthrough to avoid NXDOMAINing | ||
| # every other name. | ||
| patched="${patched} | ||
| ${reg_name}:53 { | ||
| errors | ||
| hosts { | ||
| ${reg_v6} ${reg_name} | ||
| } | ||
| }" | ||
|
|
||
| # A YAML patch file avoids escaping the Corefile's newlines into JSON. | ||
| { printf 'data:\n Corefile: |\n'; printf '%s\n' "${patched}" | sed 's/^/ /'; } \ | ||
| > "${ROOT}/bin/coredns-patch.yaml" | ||
| kubectl --context="${KUBECTL_CONTEXT}" -n kube-system patch cm coredns \ | ||
| --type=merge --patch-file "${ROOT}/bin/coredns-patch.yaml" | ||
| kubectl --context="${KUBECTL_CONTEXT}" -n kube-system rollout restart deploy/coredns | ||
| kubectl --context="${KUBECTL_CONTEXT}" -n kube-system rollout status deploy/coredns \ | ||
| --timeout=120s | ||
|
|
||
| # Its own script so it can be re-run against a live cluster: the hosts entry | ||
| # above is a snapshot of an address the registry can move off (#1049). | ||
| KUBECTL_CONTEXT="${KUBECTL_CONTEXT}" REG_NAME="${reg_name}" \ | ||
| IPV6_DNS_UPSTREAM="${IPV6_DNS_UPSTREAM}" "${ROOT}"/hack/verify-ipv6-dns.sh | ||
| fi | ||
|
|
||
| # 5. Document the local registry in kube-public ConfigMap | ||
| echo "Documenting local registry in cluster..." | ||
| cat <<EOF | kubectl --context="${KUBECTL_CONTEXT}" apply -f - | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,128 @@ | ||
| #!/usr/bin/env bash | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit. remove this file, we can rely on e2e tests to provide signals |
||
|
|
||
| # Copyright 2026 Google LLC | ||
| # | ||
| # 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. | ||
| # | ||
| # Preconditions: | ||
| # IP_FAMILY=ipv6 hack/create-kind-cluster.sh | ||
| # | ||
| # Checks the two things the CoreDNS rewrite in create-kind-cluster.sh provides: | ||
| # a pod can resolve an external name (a GCS one -- that is where atelet pulls | ||
| # sandbox tarballs from) and reach the local registry by name. That script runs | ||
| # this last; re-run it by hand after the registry moves (#1049). | ||
|
|
||
| set -o errexit -o nounset -o pipefail | ||
|
|
||
| KUBECTL_CONTEXT="${KUBECTL_CONTEXT:-kind-${KIND_CLUSTER_NAME:-kind}}" | ||
| REG_NAME="${REG_NAME:-kind-registry}" | ||
| IPV6_DNS_UPSTREAM="${IPV6_DNS_UPSTREAM:-2001:4860:4860::8888 2001:4860:4860::8844}" | ||
|
|
||
| if [[ $# -gt 0 ]]; then | ||
| case "$1" in | ||
| -h|--help) | ||
| echo "Usage: $0" | ||
| echo "Verifies pod DNS on the IPv6-only kind cluster '${KUBECTL_CONTEXT}'." | ||
| echo | ||
| echo "Configured through the environment:" | ||
| echo " KUBECTL_CONTEXT Context to check (default: kind-\${KIND_CLUSTER_NAME:-kind})." | ||
| echo " REG_NAME Name of the local registry container (default: kind-registry)." | ||
| echo " IPV6_DNS_UPSTREAM The resolvers CoreDNS was pointed at; reported on failure only." | ||
| exit 0 | ||
| ;; | ||
| *) | ||
| echo "error: unknown argument '$1'; see --help" >&2 | ||
| exit 1 | ||
| ;; | ||
| esac | ||
| fi | ||
|
|
||
| kube() { kubectl --context="${KUBECTL_CONTEXT}" "$@"; } | ||
|
|
||
| echo "Verifying DNS from a pod..." | ||
| # Probe from a pod, not the node: the node is dual-stack and passes either way. | ||
| # The registry leg fetches rather than resolves -- the hosts entry is AAAA-only, | ||
| # which fails nslookup's A query but satisfies getaddrinfo. Legs share one | ||
| # stream and only the last exit status survives, hence the markers; read the log | ||
| # after the pod terminates, because an attach drops lines. | ||
|
|
||
| # Ask each replica directly as well as through the Service, so one sick replica | ||
| # is named instead of showing up as a coin flip -- and take the addresses from | ||
| # the EndpointSlice, filtered on ready, not from the pod list. A rollout leaves | ||
| # the previous generation running for its 30s grace period: still a listed pod, | ||
| # still unable to resolve, but already cut out of the Service. Querying it would | ||
| # fail a cluster that works. | ||
| legs="" | ||
| for ip in $(kube -n kube-system get endpointslices -l kubernetes.io/service-name=kube-dns \ | ||
| -o jsonpath='{range .items[*].endpoints[?(@.conditions.ready==true)]}{.addresses[0]}{" "}{end}'); do | ||
| legs="${legs} | ||
| nslookup storage.googleapis.com ${ip} >/dev/null 2>&1 || echo \"REPLICA_BAD ${ip}\"" | ||
| done | ||
|
|
||
| probe_pod="coredns-probe-$$" | ||
| trap 'kube delete pod "${probe_pod}" --now --ignore-not-found --wait=false >/dev/null 2>&1 || true' EXIT | ||
| kube run "${probe_pod}" --restart=Never --image=busybox:1.36 --command -- \ | ||
| sh -c "if out=\$(nslookup storage.googleapis.com 2>&1); then | ||
| echo RESOLVE_OK | ||
| else | ||
| echo \"resolve failed: \$(echo \"\$out\" | tail -2 | tr '\n' ' ')\" | ||
| fi${legs} | ||
| if out=\$(wget -T10 -O/dev/null http://${REG_NAME}:5000/v2/ 2>&1); then | ||
| echo REGISTRY_OK | ||
| else | ||
| echo \"registry fetch failed: \$(echo \"\$out\" | tail -1)\" | ||
| fi" >/dev/null | ||
|
|
||
| if ! kube wait --for=jsonpath='{.status.phase}'=Succeeded \ | ||
| "pod/${probe_pod}" --timeout=120s >/dev/null; then | ||
| echo "error: the probe pod never finished, so CoreDNS is unverified" >&2 | ||
| kube describe pod "${probe_pod}" | sed 's/^/ /' >&2 | ||
| exit 1 | ||
| fi | ||
| probe="$(kube logs "${probe_pod}")" | ||
|
|
||
| # A replica that fails while the Service still answers is the case the per-replica | ||
| # legs exist to catch, so it has to fail the check too: the Service round-robins, | ||
| # so one sick replica out of two is not a warning, it is half the pods in the | ||
| # cluster unable to resolve. | ||
| bad_replicas="$(printf '%s\n' "${probe}" | sed -n 's/^REPLICA_BAD //p')" | ||
|
|
||
| if [[ "${probe}" != *RESOLVE_OK* || "${probe}" != *REGISTRY_OK* || -n "${bad_replicas}" ]]; then | ||
| if [[ "${probe}" != *RESOLVE_OK* ]]; then | ||
| echo "error: a pod cannot resolve an external name" >&2 | ||
| echo " IPV6_DNS_UPSTREAM is '${IPV6_DNS_UPSTREAM}'; set it to a reachable resolver" >&2 | ||
| elif [[ "${probe}" == *REGISTRY_OK* ]]; then | ||
| echo "error: the Service resolves, but these CoreDNS replicas do not:" >&2 | ||
| while read -r ip; do | ||
| echo " ${ip}" >&2 | ||
| done <<<"${bad_replicas}" | ||
| echo " resolution is a coin flip until they are replaced" >&2 | ||
| else | ||
| # The Corefile holds a snapshot of the address; the registry may have moved | ||
| # off it (#1049), which is not the same fault as a registry that is down. | ||
| want="$(kube -n kube-system get cm coredns -o jsonpath='{.data.Corefile}' \ | ||
| | awk -v n="${REG_NAME}" '$2 == n {print $1}')" | ||
| have="$(docker inspect "${REG_NAME}" \ | ||
| --format '{{.NetworkSettings.Networks.kind.GlobalIPv6Address}}' 2>/dev/null || true)" | ||
| echo "error: DNS works but a pod cannot reach '${REG_NAME}'" >&2 | ||
| if [[ -n "${have}" && "${want}" != "${have}" ]]; then | ||
| echo " the Corefile says [${want}] but the registry is at [${have}]" >&2 | ||
| echo " re-create the cluster, or patch the hosts entry to match" >&2 | ||
| else | ||
| echo " check the registry container is up and on the 'kind' network" >&2 | ||
| fi | ||
| fi | ||
| echo " probe output was:" >&2 | ||
| printf '%s\n' "${probe}" | sed 's/^/ /' >&2 | ||
| exit 1 | ||
| fi | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit. be more concise about the issue it tries to solve, and what the solution is