From 742d22d2cc3c6bf2eb8cd2eb210b9b798d63616b Mon Sep 17 00:00:00 2001 From: dajiaohuang Date: Fri, 18 Sep 2026 01:32:26 +0800 Subject: [PATCH] Fix Kubernetes strategy crash on endpoint addresses without targetRef `EndpointAddress.targetRef` is optional in the Kubernetes API, so addresses which are not backed by a pod (manually managed endpoints, for example) do not carry one. Parsing those raised a FunctionClauseError inside the strategy's init, so the strategy could not start at all and the node never joined the cluster. Fall back to the namespace the endpoints were queried from, which is the namespace every address in the response belongs to. Addresses which do have a targetRef keep using its namespace, so existing behaviour is unchanged. --- CHANGELOG.md | 1 + lib/strategy/kubernetes.ex | 17 +++++---- .../kubernetes_no_target_ref.json | 34 ++++++++++++++++++ test/kubernetes_test.exs | 35 +++++++++++++++++++ 4 files changed, 81 insertions(+), 6 deletions(-) create mode 100644 test/fixtures/vcr_cassettes/kubernetes_no_target_ref.json diff --git a/CHANGELOG.md b/CHANGELOG.md index cf9acce..322d33a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Unreleased - Add `kubernetes_use_cached_resources` option to Kubernetes strategy +- Fix Kubernetes strategy crash when an endpoints address has no `targetRef` ## 3.4.1 diff --git a/lib/strategy/kubernetes.ex b/lib/strategy/kubernetes.ex index ae83791..eca94d4 100644 --- a/lib/strategy/kubernetes.ex +++ b/lib/strategy/kubernetes.ex @@ -409,7 +409,7 @@ defmodule Cluster.Strategy.Kubernetes do case :httpc.request(:get, {~c"https://#{master}/#{path}", headers}, http_options, []) do {:ok, {{_version, 200, _status}, _headers, body}} -> - parse_response(ip_lookup_mode, Jason.decode!(body)) + parse_response(ip_lookup_mode, Jason.decode!(body), namespace) |> Enum.map(fn node_info -> format_node( Keyword.get(config, :mode, :ip), @@ -462,7 +462,7 @@ defmodule Cluster.Strategy.Kubernetes do defp apply_param(params, _key, _value), do: params - defp parse_response(:endpoints, resp) do + defp parse_response(:endpoints, resp, namespace) do case resp do %{"items" => items} when is_list(items) -> Enum.reduce(items, [], fn @@ -470,9 +470,14 @@ defmodule Cluster.Strategy.Kubernetes do addrs = Enum.flat_map(subsets, fn %{"addresses" => addresses} when is_list(addresses) -> - Enum.map(addresses, fn %{"ip" => ip, "targetRef" => %{"namespace" => namespace}} = - address -> - %{ip: ip, namespace: namespace, hostname: address["hostname"]} + Enum.map(addresses, fn %{"ip" => ip} = address -> + # targetRef is optional, and endpoints which are not backed by a pod + # (manually managed ones, for example) do not have one + %{ + ip: ip, + namespace: get_in(address, ["targetRef", "namespace"]) || namespace, + hostname: address["hostname"] + } end) _ -> @@ -490,7 +495,7 @@ defmodule Cluster.Strategy.Kubernetes do end end - defp parse_response(:pods, resp) do + defp parse_response(:pods, resp, _namespace) do case resp do %{"items" => items} when is_list(items) -> Enum.map(items, fn diff --git a/test/fixtures/vcr_cassettes/kubernetes_no_target_ref.json b/test/fixtures/vcr_cassettes/kubernetes_no_target_ref.json new file mode 100644 index 0000000..0955b53 --- /dev/null +++ b/test/fixtures/vcr_cassettes/kubernetes_no_target_ref.json @@ -0,0 +1,34 @@ +[ + { + "request": { + "body": "", + "headers": { + "authorization": "***" + }, + "method": "get", + "options": { + "httpc_options": [], + "http_options": { + "ssl": "[verify: :verify_none]" + } + }, + "request_body": "", + "url": "https://cluster.localhost./api/v1/namespaces/__libcluster_test/endpoints?labelSelector=app=test_selector" + }, + "response": { + "binary": false, + "body": "{\"kind\": \"EndpointsList\", \"apiVersion\": \"v1\", \"metadata\": {\"selfLink\": \"SELFLINK_PLACEHOLDER\", \"resourceVersion\": \"17042410\"}, \"items\": [{\"metadata\": {\"name\": \"development-development\", \"namespace\": \"__libcluster_test\", \"selfLink\": \"SELFLINK_PLACEHOLDER\", \"uid\": \"7e3faf1e-0294-11e8-bcad-42010a9c01cc\", \"resourceVersion\": \"17037787\", \"creationTimestamp\": \"2018-01-26T12:29:03Z\", \"labels\": {\"app\": \"test_selector\", \"chart\": \"CHART_PLACEHOLDER\"}}, \"subsets\": [{\"addresses\": [{\"hostname\": \"my-hostname-0\", \"ip\": \"10.48.33.136\", \"nodeName\": \"gke-jshmrtn-cluster-default-pool-a61da41f-db9x\", \"targetRef\": {\"kind\": \"Pod\", \"namespace\": \"airatel-service-localization\", \"name\": \"development-4292695165-mgq9f\", \"uid\": \"eb0f3e80-0295-11e8-bcad-42010a9c01cc\", \"resourceVersion\": \"17037783\"}}, {\"ip\": \"10.48.33.137\", \"nodeName\": \"gke-jshmrtn-cluster-default-pool-a61da41f-db9x\"}], \"ports\": [{\"name\": \"web\", \"port\": 8443, \"protocol\": \"TCP\"}]}]}]}", + "headers": { + "date": "Fri, 26 Jan 2018 13:18:46 GMT", + "content-length": "932", + "content-type": "application/json" + }, + "status_code": [ + "HTTP/1.1", + 200, + "OK" + ], + "type": "ok" + } + } +] \ No newline at end of file diff --git a/test/kubernetes_test.exs b/test/kubernetes_test.exs index 512ad6d..8b87d7f 100644 --- a/test/kubernetes_test.exs +++ b/test/kubernetes_test.exs @@ -283,6 +283,41 @@ defmodule Cluster.Strategy.KubernetesTest do end end + test "works with endpoints whose addresses have no targetRef" do + use_cassette "kubernetes_no_target_ref", custom: true do + capture_log(fn -> + start_supervised!({Kubernetes, + [ + %Cluster.Strategy.State{ + topology: :name, + config: [ + kubernetes_node_basename: "test_basename", + kubernetes_selector: "app=test_selector", + mode: :dns, + # If you want to run the test freshly, you'll need to create a DNS Entry + kubernetes_master: "cluster.localhost.", + kubernetes_service_account_path: + Path.join([__DIR__, "fixtures", "kubernetes", "service_account"]) + ], + connect: {Nodes, :connect, [self()]}, + disconnect: {Nodes, :disconnect, [self()]}, + list_nodes: {Nodes, :list_nodes, [[]]} + } + ]}) + + # address with a targetRef keeps the namespace it points at + assert_receive {:connect, + :"test_basename@10-48-33-136.airatel-service-localization.pod.cluster.local"}, + 5_000 + + # address without a targetRef falls back to the queried namespace + assert_receive {:connect, + :"test_basename@10-48-33-137.__libcluster_test.pod.cluster.local"}, + 5_000 + end) + end + end + test "works with pods and dns" do use_cassette "kubernetes_pods", custom: true do capture_log(fn ->