You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@apisix.apache.org by mo...@apache.org on 2024/02/23 07:04:51 UTC

(apisix) branch master updated: feat(discovery): support endpointslices in kubernetes discovery (#10916)

This is an automated email from the ASF dual-hosted git repository.

monkeydluffy pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/apisix.git


The following commit(s) were added to refs/heads/master by this push:
     new 869d09515 feat(discovery): support endpointslices in kubernetes discovery (#10916)
869d09515 is described below

commit 869d095153df75fbe6e3edcf631340965fdd04ab
Author: dongjiang <do...@126.com>
AuthorDate: Fri Feb 23 15:04:46 2024 +0800

    feat(discovery): support endpointslices in kubernetes discovery (#10916)
---
 apisix/discovery/kubernetes/init.lua   |  98 ++++++++-
 apisix/discovery/kubernetes/schema.lua |   7 +
 docs/en/latest/discovery/kubernetes.md |   8 +-
 docs/zh/latest/discovery/kubernetes.md |   8 +-
 t/kubernetes/discovery/kubernetes.t    |  42 ++++
 t/kubernetes/discovery/kubernetes3.t   | 388 +++++++++++++++++++++++++++++++++
 6 files changed, 542 insertions(+), 9 deletions(-)

diff --git a/apisix/discovery/kubernetes/init.lua b/apisix/discovery/kubernetes/init.lua
index d16d4f4fc..84c6ddbcb 100644
--- a/apisix/discovery/kubernetes/init.lua
+++ b/apisix/discovery/kubernetes/init.lua
@@ -50,6 +50,69 @@ local function sort_nodes_cmp(left, right)
     return left.port < right.port
 end
 
+local function on_endpoint_slices_modified(handle, endpoint)
+    if handle.namespace_selector and
+            not handle:namespace_selector(endpoint.metadata.namespace) then
+        return
+    end
+
+    core.log.debug(core.json.delay_encode(endpoint))
+    core.table.clear(endpoint_buffer)
+
+    local endpointslices = endpoint.endpoints
+    for _, endpointslice in ipairs(endpointslices or {}) do
+        if endpointslice.addresses then
+            local addresses = endpointslices.addresses
+            for _, port in ipairs(endpoint.ports or {}) do
+                local port_name
+                if port.name then
+                    port_name = port.name
+                elseif port.targetPort then
+                    port_name = tostring(port.targetPort)
+                else
+                    port_name = tostring(port.port)
+                end
+
+                if endpointslice.conditions and endpointslice.condition.ready then
+                    local nodes = endpoint_buffer[port_name]
+                    if nodes == nil then
+                        nodes = core.table.new(0, #endpointslices * #addresses)
+                        endpoint_buffer[port_name] = nodes
+                    end
+
+                    for _, address in ipairs(endpointslices.addresses) do
+                        core.table.insert(nodes, {
+                            host = address.ip,
+                            port = port.port,
+                            weight = handle.default_weight
+                        })
+                    end
+                end
+            end
+        end
+    end
+
+    for _, ports in pairs(endpoint_buffer) do
+        for _, nodes in pairs(ports) do
+            core.table.sort(nodes, sort_nodes_cmp)
+        end
+    end
+    local endpoint_key = endpoint.metadata.namespace .. "/" .. endpoint.metadata.name
+    local endpoint_content = core.json.encode(endpoint_buffer, true)
+    local endpoint_version = ngx.crc32_long(endpoint_content)
+
+    local _, err
+    _, err = handle.endpoint_dict:safe_set(endpoint_key .. "#version", endpoint_version)
+    if err then
+        core.log.error("set endpoint version into discovery DICT failed, ", err)
+        return
+    end
+    _, err = handle.endpoint_dict:safe_set(endpoint_key, endpoint_content)
+    if err then
+        core.log.error("set endpoint into discovery DICT failed, ", err)
+        handle.endpoint_dict:delete(endpoint_key .. "#version")
+    end
+end
 
 local function on_endpoint_modified(handle, endpoint)
     if handle.namespace_selector and
@@ -367,8 +430,13 @@ local function single_mode_init(conf)
     end
 
     local default_weight = conf.default_weight
-
-    local endpoints_informer, err = informer_factory.new("", "v1", "Endpoints", "endpoints", "")
+    local endpoints_informer, err
+    if conf.watch_endpoint_slices_schema then
+        endpoints_informer, err = informer_factory.new("discovery.k8s.io", "v1",
+                                                       "EndpointSlice", "endpointslices", "")
+    else
+        endpoints_informer, err = informer_factory.new("", "v1", "Endpoints", "endpoints", "")
+    end
     if err then
         error(err)
         return
@@ -377,8 +445,13 @@ local function single_mode_init(conf)
     setup_namespace_selector(conf, endpoints_informer)
     setup_label_selector(conf, endpoints_informer)
 
-    endpoints_informer.on_added = on_endpoint_modified
-    endpoints_informer.on_modified = on_endpoint_modified
+    if conf.watch_endpoint_slices_schema then
+        endpoints_informer.on_added = on_endpoint_slices_modified
+        endpoints_informer.on_modified = on_endpoint_slices_modified
+    else
+        endpoints_informer.on_added = on_endpoint_modified
+        endpoints_informer.on_modified = on_endpoint_modified
+    end
     endpoints_informer.on_deleted = on_endpoint_deleted
     endpoints_informer.pre_list = pre_list
     endpoints_informer.post_list = post_list
@@ -463,7 +536,13 @@ local function multiple_mode_init(confs)
 
         local default_weight = conf.default_weight
 
-        local endpoints_informer, err = informer_factory.new("", "v1", "Endpoints", "endpoints", "")
+        local endpoints_informer, err
+        if conf.watch_endpoint_slices_schema then
+            endpoints_informer, err = informer_factory.new("discovery.k8s.io", "v1",
+                                                           "EndpointSlice", "endpointslices", "")
+        else
+            endpoints_informer, err = informer_factory.new("", "v1", "Endpoints", "endpoints", "")
+        end
         if err then
             error(err)
             return
@@ -472,8 +551,13 @@ local function multiple_mode_init(confs)
         setup_namespace_selector(conf, endpoints_informer)
         setup_label_selector(conf, endpoints_informer)
 
-        endpoints_informer.on_added = on_endpoint_modified
-        endpoints_informer.on_modified = on_endpoint_modified
+        if conf.watch_endpoint_slices_schema then
+            endpoints_informer.on_added = on_endpoint_slices_modified
+            endpoints_informer.on_modified = on_endpoint_slices_modified
+        else
+            endpoints_informer.on_added = on_endpoint_modified
+            endpoints_informer.on_modified = on_endpoint_modified
+        end
         endpoints_informer.on_deleted = on_endpoint_deleted
         endpoints_informer.pre_list = pre_list
         endpoints_informer.post_list = post_list
diff --git a/apisix/discovery/kubernetes/schema.lua b/apisix/discovery/kubernetes/schema.lua
index 170608f55..18df2a3b7 100644
--- a/apisix/discovery/kubernetes/schema.lua
+++ b/apisix/discovery/kubernetes/schema.lua
@@ -105,6 +105,11 @@ local shared_size_schema = {
     default = "1m",
 }
 
+local watch_endpoint_slices_schema = {
+    type = "boolean",
+    default = false,
+}
+
 return {
     anyOf = {
         {
@@ -160,6 +165,7 @@ return {
                 label_selector = label_selector_schema,
                 default_weight = default_weight_schema,
                 shared_size = shared_size_schema,
+                watch_endpoint_slices = watch_endpoint_slices_schema,
             },
         },
         {
@@ -202,6 +208,7 @@ return {
                     label_selector = label_selector_schema,
                     default_weight = default_weight_schema,
                     shared_size = shared_size_schema,
+                    watch_endpoint_slices = watch_endpoint_slices_schema,
                 },
                 required = { "id", "service", "client" }
             },
diff --git a/docs/en/latest/discovery/kubernetes.md b/docs/en/latest/discovery/kubernetes.md
index fb11aac67..5936613bf 100644
--- a/docs/en/latest/discovery/kubernetes.md
+++ b/docs/en/latest/discovery/kubernetes.md
@@ -91,6 +91,9 @@ discovery:
 
     # reserved lua shared memory size,1m memory can store about 1000 pieces of endpoint
     shared_size: 1m #default 1m
+
+    # if watch_endpoint_slices setting true, watch apiserver with endpointslices instead of endpoints
+    watch_endpoint_slices: false #default false
 ```
 
 If the Kubernetes service discovery runs inside a pod, you can use minimal configuration:
@@ -220,6 +223,9 @@ discovery:
 
     # reserved lua shared memory size,1m memory can store about 1000 pieces of endpoint
     shared_size: 1m #default 1m
+
+    # if watch_endpoint_slices setting true, watch apiserver with endpointslices instead of endpoints
+    watch_endpoint_slices: false #default false
 ```
 
 Multi-Kubernetes service discovery does not fill default values for service and client fields, you need to fill them according to the cluster configuration.
@@ -312,7 +318,7 @@ metadata:
  name: apisix-test
 rules:
 - apiGroups: [ "" ]
-  resources: [ endpoints ]
+  resources: [ endpoints,endpointslices ]
   verbs: [ get,list,watch ]
 ---
 
diff --git a/docs/zh/latest/discovery/kubernetes.md b/docs/zh/latest/discovery/kubernetes.md
index c4b751889..574e2c876 100644
--- a/docs/zh/latest/discovery/kubernetes.md
+++ b/docs/zh/latest/discovery/kubernetes.md
@@ -91,6 +91,9 @@ discovery:
 
     # reserved lua shared memory size, 1m memory can store about 1000 pieces of endpoint
     shared_size: 1m #default 1m
+
+    # if watch_endpoint_slices setting true, watch apiserver with endpointslices instead of endpoints
+    watch_endpoint_slices: false #default false
 ```
 
 如果 Kubernetes 服务发现运行在 Pod 内,你可以使用如下最简配置:
@@ -219,6 +222,9 @@ discovery:
 
     # reserved lua shared memory size,1m memory can store about 1000 pieces of endpoint
     shared_size: 1m #default 1m
+
+    # if watch_endpoint_slices setting true, watch apiserver with endpointslices instead of endpoints
+    watch_endpoint_slices: false #default false
 ```
 
 多集群模式 Kubernetes 服务发现没有为 `service` 和 `client` 域填充默认值,你需要根据集群配置情况自行填充。
@@ -310,7 +316,7 @@ metadata:
  name: apisix-test
 rules:
 - apiGroups: [ "" ]
-  resources: [ endpoints ]
+  resources: [ endpoints,endpointslices ]
   verbs: [ get,list,watch ]
 ---
 
diff --git a/t/kubernetes/discovery/kubernetes.t b/t/kubernetes/discovery/kubernetes.t
index 24bf6f426..c4158de21 100644
--- a/t/kubernetes/discovery/kubernetes.t
+++ b/t/kubernetes/discovery/kubernetes.t
@@ -128,6 +128,7 @@ GET /compare
   "client": {
     "token": "${KUBERNETES_CLIENT_TOKEN}"
   },
+  "watch_endpoint_slices": false,
   "shared_size": "1m",
   "default_weight": 50
 }
@@ -162,6 +163,7 @@ GET /compare
   "client": {
     "token": "${KUBERNETES_CLIENT_TOKEN}"
   },
+  "watch_endpoint_slices": false,
   "shared_size": "1m",
   "default_weight": 50
 }
@@ -198,6 +200,7 @@ GET /compare
   "client": {
     "token": "${KUBERNETES_CLIENT_TOKEN}"
   },
+  "watch_endpoint_slices": false,
   "shared_size": "2m",
   "default_weight": 50
 }
@@ -232,6 +235,7 @@ GET /compare
   "client": {
     "token": "${KUBERNETES_CLIENT_TOKEN}"
   },
+  "watch_endpoint_slices": false,
   "shared_size": "1m",
   "default_weight": 33
 }
@@ -283,6 +287,7 @@ GET /compare
     "client": {
       "token": "${KUBERNETES_CLIENT_TOKEN}"
     },
+    "watch_endpoint_slices": false,
     "default_weight": 50,
     "shared_size": "1m"
   },
@@ -296,6 +301,7 @@ GET /compare
     "client": {
       "token": "${KUBERNETES_CLIENT_TOKEN}"
     },
+    "watch_endpoint_slices": false,
     "default_weight": 33,
     "shared_size": "2m"
   }
@@ -304,3 +310,39 @@ GET /compare
 Content-type: application/json
 --- response_body
 true
+
+
+
+=== TEST 6: set watch_endpoint_slices true and use kubernetes endpointslices api
+--- yaml_config
+apisix:
+  node_listen: 1984
+deployment:
+  role: data_plane
+  role_data_plane:
+    config_provider: yaml
+discovery:
+  kubernetes:
+    client:
+        token: ${KUBERNETES_CLIENT_TOKEN}
+    default_weight: 33
+    watch_endpoint_slices: true
+--- request
+GET /compare
+{
+  "service": {
+    "schema": "https",
+    "host": "${KUBERNETES_SERVICE_HOST}",
+    "port": "${KUBERNETES_SERVICE_PORT}"
+  },
+  "client": {
+    "token": "${KUBERNETES_CLIENT_TOKEN}"
+  },
+  "watch_endpoint_slices": true,
+  "shared_size": "1m",
+  "default_weight": 33
+}
+--- more_headers
+Content-type: application/json
+--- response_body
+true
diff --git a/t/kubernetes/discovery/kubernetes3.t b/t/kubernetes/discovery/kubernetes3.t
new file mode 100644
index 000000000..cebab5ba2
--- /dev/null
+++ b/t/kubernetes/discovery/kubernetes3.t
@@ -0,0 +1,388 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+#
+
+BEGIN {
+    our $token_file = "/tmp/var/run/secrets/kubernetes.io/serviceaccount/token";
+    our $token_value = eval {`cat $token_file 2>/dev/null`};
+
+    our $yaml_config = <<_EOC_;
+apisix:
+  node_listen: 1984
+deployment:
+  role: data_plane
+  role_data_plane:
+    config_provider: yaml
+discovery:
+  kubernetes:
+    - id: first
+      service:
+        host: "127.0.0.1"
+        port: "6443"
+      client:
+        token_file: "/tmp/var/run/secrets/kubernetes.io/serviceaccount/token"
+      watch_endpoint_slices: true
+    - id: second
+      service:
+        schema: "http",
+        host: "127.0.0.1",
+        port: "6445"
+      client:
+        token_file: "/tmp/var/run/secrets/kubernetes.io/serviceaccount/token"
+      watch_endpoint_slices: true
+
+_EOC_
+
+    our $scale_ns_c = <<_EOC_;
+[
+  {
+    "op": "replace_endpointslices",
+    "name": "ep",
+    "namespace": "ns-c",
+    "endpoints": [
+      {
+        "addresses": [
+            "10.0.0.1"
+        ],
+        "conditions": {
+           "ready": true,
+           "serving": true,
+           "terminating": false
+        },
+        "nodeName": "kind-control-plane"
+      }
+    ]
+    "ports": [
+      {
+        "name": "p1",
+        "port": 5001
+      }
+    ]
+  }
+]
+_EOC_
+
+}
+
+use t::APISIX 'no_plan';
+
+repeat_each(1);
+log_level('warn');
+no_root_location();
+no_shuffle();
+workers(4);
+
+add_block_preprocessor(sub {
+    my ($block) = @_;
+
+    my $apisix_yaml = $block->apisix_yaml // <<_EOC_;
+routes: []
+#END
+_EOC_
+
+    $block->set_value("apisix_yaml", $apisix_yaml);
+
+    my $main_config = $block->main_config // <<_EOC_;
+env KUBERNETES_SERVICE_HOST=127.0.0.1;
+env KUBERNETES_SERVICE_PORT=6443;
+env KUBERNETES_CLIENT_TOKEN=$::token_value;
+env KUBERNETES_CLIENT_TOKEN_FILE=$::token_file;
+_EOC_
+
+    $block->set_value("main_config", $main_config);
+
+    my $config = $block->config // <<_EOC_;
+        location /queries {
+            content_by_lua_block {
+              local core = require("apisix.core")
+              local d = require("apisix.discovery.kubernetes")
+
+              ngx.sleep(1)
+
+              ngx.req.read_body()
+              local request_body = ngx.req.get_body_data()
+              local queries = core.json.decode(request_body)
+              local response_body = "{"
+              for _,query in ipairs(queries) do
+                local nodes = d.nodes(query)
+                if nodes==nil or #nodes==0 then
+                    response_body=response_body.." "..0
+                else
+                    response_body=response_body.." "..#nodes
+                end
+              end
+              ngx.say(response_body.." }")
+            }
+        }
+
+        location /operators {
+            content_by_lua_block {
+                local http = require("resty.http")
+                local core = require("apisix.core")
+                local ipairs = ipairs
+
+                ngx.req.read_body()
+                local request_body = ngx.req.get_body_data()
+                local operators = core.json.decode(request_body)
+
+                core.log.info("get body ", request_body)
+                core.log.info("get operators ", #operators)
+                for _, op in ipairs(operators) do
+                    local method, path, body
+                    local headers = {
+                        ["Host"] = "127.0.0.1:6445"
+                    }
+
+                    if op.op == "replace_endpointslices" then
+                        method = "PATCH"
+                        path = "/apis/discovery.k8s.io/namespaces/" .. op.namespace .. "/endpointslices/" .. op.name
+                        if #op.endpoints == 0 then
+                            body = '[{"path":"/endpoints","op":"replace","value":[]}]'
+                        else
+                            local t = { { op = "replace", path = "/endpoints", value = op.endpoints } }
+                            body = core.json.encode(t, true)
+                        end
+                        headers["Content-Type"] = "application/json-patch+json"
+                    end
+
+                    if op.op == "replace_labels" then
+                        method = "PATCH"
+                        path = "/apis/discovery.k8s.io/namespaces/" .. op.namespace .. "/endpointslices/" .. op.name
+                        local t = { { op = "replace", path = "/metadata/labels", value = op.labels } }
+                        body = core.json.encode(t, true)
+                        headers["Content-Type"] = "application/json-patch+json"
+                    end
+
+                    local httpc = http.new()
+                    core.log.info("begin to connect ", "127.0.0.1:6445")
+                    local ok, message = httpc:connect({
+                        scheme = "http",
+                        host = "127.0.0.1",
+                        port = 6445,
+                    })
+                    if not ok then
+                        core.log.error("connect 127.0.0.1:6445 failed, message : ", message)
+                        ngx.say("FAILED")
+                    end
+                    local res, err = httpc:request({
+                        method = method,
+                        path = path,
+                        headers = headers,
+                        body = body,
+                    })
+                    if err ~= nil then
+                        core.log.err("operator k8s cluster error: ", err)
+                        return 500
+                    end
+                    if res.status ~= 200 and res.status ~= 201 and res.status ~= 409 then
+                        return res.status
+                    end
+                end
+                ngx.say("DONE")
+            }
+        }
+
+_EOC_
+
+    $block->set_value("config", $config);
+
+});
+
+run_tests();
+
+__DATA__
+
+=== TEST 1: create namespace and endpoints
+--- yaml_config eval: $::yaml_config
+--- request
+POST /operators
+[
+    {
+        "op": "replace_endpointslices",
+        "namespace": "ns-a",
+        "name": "ep",
+        "endpoints": [
+            {
+                "addresses": [
+                    "10.0.0.1",
+                    "10.0.0.2"
+                ],
+                "conditions": {
+                    "ready": true,
+                    "serving": true,
+                    "terminating": false
+                },
+                "nodeName": "kind-control-plane"
+            },
+            {
+                "addresses": [
+                    "20.0.0.1",
+                    "20.0.0.2"
+                ],
+                "conditions": {
+                    "ready": false,
+                    "serving": false,
+                    "terminating": false
+                },
+                "nodeName": "kind-control-plane"
+            }
+        ],
+        "ports": [
+            {
+                "name": "p",
+                "port": 5001
+            }
+        ]
+    },
+    {
+        "op": "create_namespace",
+        "name": "ns-b"
+    },
+    {
+        "op": "replace_endpointslices",
+        "namespace": "ns-b",
+        "name": "ep",
+        "endpoints": [
+            {
+                "addresses": [
+                    "10.0.0.1",
+                    "10.0.0.2"
+                ],
+                "conditions": {
+                    "ready": true,
+                    "serving": true,
+                    "terminating": false
+                },
+                "nodeName": "kind-control-plane"
+            },
+            {
+                "addresses": [
+                    "20.0.0.1",
+                    "20.0.0.2"
+                ],
+                "conditions": {
+                    "ready": false,
+                    "serving": true,
+                    "terminating": false
+                },
+                "nodeName": "kind-control-plane"
+            }
+        ],
+        "ports": [
+            {
+                "name": "p",
+                "port": 5002
+            }
+        ]
+    },
+    {
+        "op": "create_namespace",
+        "name": "ns-c"
+    },
+    {
+        "op": "replace_endpointslices",
+        "namespace": "ns-c",
+        "name": "ep",
+        "endpoints": [
+            {
+                "addresses": [
+                    "10.0.0.1",
+                    "10.0.0.2"
+                ],
+                "conditions": {
+                    "ready": true,
+                    "serving": true,
+                    "terminating": false
+                },
+                "nodeName": "kind-control-plane"
+            },
+            {
+                "addresses": [
+                    "20.0.0.1",
+                    "20.0.0.2"
+                ],
+                "conditions": {
+                    "ready": true,
+                    "serving": true,
+                    "terminating": false
+                },
+                "nodeName": "kind-control-plane"
+            }
+        ],
+        "ports": [
+            {
+                "name": "p",
+                "port": 5003
+            }
+        ]
+    }
+]
+--- more_headers
+Content-type: application/json
+
+
+
+=== TEST 2: use default parameters
+--- yaml_config eval: $::yaml_config
+--- request
+GET /queries
+[
+  "first/ns-a/ep:p1","first/ns-a/ep:p2","first/ns-b/ep:p1","first/ns-b/ep:p2","first/ns-c/ep:5001","first/ns-c/ep:5002",
+  "second/ns-a/ep:p1","second/ns-a/ep:p2","second/ns-b/ep:p1","second/ns-b/ep:p2","second/ns-c/ep:5001","second/ns-c/ep:5002"
+]
+--- more_headers
+Content-type: application/json
+--- response_body eval
+qr{ 0 0 2 2 0 0 0 0 2 2 0 0 }
+
+
+
+=== TEST 3: use specify environment parameters
+--- yaml_config
+apisix:
+  node_listen: 1984
+deployment:
+  role: data_plane
+  role_data_plane:
+    config_provider: yaml
+discovery:
+  kubernetes:
+    - id: first
+      service:
+        host: ${KUBERNETES_SERVICE_HOST}
+        port: ${KUBERNETES_SERVICE_PORT}
+      client:
+        token: ${KUBERNETES_CLIENT_TOKEN}
+      watch_endpoint_slices: true
+    - id: second
+      service:
+        schema: "http",
+        host: "127.0.0.1",
+        port: "6445"
+      client:
+        token: ${KUBERNETES_CLIENT_TOKEN}
+      watch_endpoint_slices: true
+
+--- request
+GET /queries
+[
+  "first/ns-a/ep:p1","first/ns-a/ep:p2","first/ns-b/ep:p1","first/ns-b/ep:p2","first/ns-c/ep:5001","first/ns-c/ep:5002",
+  "second/ns-a/ep:p1","second/ns-a/ep:p2","second/ns-b/ep:p1","second/ns-b/ep:p2","second/ns-c/ep:5001","second/ns-c/ep:5002"
+]
+--- more_headers
+Content-type: application/json
+--- response_body eval
+qr{ 0 0 2 2 0 0 0 0 2 2 0 0 }