From 0382cf51953c37c70cf30aa21d3b66482c1abf64 Mon Sep 17 00:00:00 2001 From: Foo Bar Date: Thu, 15 May 2025 07:46:48 +0800 Subject: [PATCH 01/11] feat(standalone): support incremental synchronization --- apisix/admin/standalone.lua | 140 ++++++++++++++++++++++------- apisix/core/config_yaml.lua | 126 ++++++++++++++++++-------- docs/en/latest/deployment-modes.md | 115 ++++++++++++++---------- t/admin/standalone.spec.ts | 93 ++++++++++++++++--- t/admin/standalone.t | 47 +++++++++- 5 files changed, 395 insertions(+), 126 deletions(-) diff --git a/apisix/admin/standalone.lua b/apisix/admin/standalone.lua index c0e9b22883a0..8d772693d955 100644 --- a/apisix/admin/standalone.lua +++ b/apisix/admin/standalone.lua @@ -30,25 +30,61 @@ local events = require("apisix.events") local core = require("apisix.core") local config_yaml = require("apisix.core.config_yaml") local check_schema = require("apisix.core.schema").check +local tbl_deepcopy = require("apisix.core.table").deepcopy + local EVENT_UPDATE = "standalone-api-configuration-update" local _M = {} -local function update_and_broadcast_config(apisix_yaml, conf_version) - local config = core.json.encode({ + +local function get_config() + local config = shared_dict:get("config") + if not config then + return nil, "not found" + end + + local err + config, err = core.json.decode(config) + if not config then + return nil, "failed to decode json: " .. err + end + return config +end + + +local function update_and_broadcast_config(old_config, apisix_yaml, conf_version_kv) + local config = { conf = apisix_yaml, - conf_version = conf_version, - }) + conf_version = conf_version_kv + } + + if old_config then + local ori_config = tbl_deepcopy(old_config) + ori_config.conf_version = ori_config.conf_version or {} + ori_config.conf = ori_config.conf or {} + for key, v in pairs(conf_version_kv) do + if v ~= ori_config.conf_version[key] then + ori_config.conf[key] = apisix_yaml[key] + ori_config.conf_version[key] = v + end + end + config = ori_config + end + + local encoded_config, encode_err = core.json.encode(config) + if not encoded_config then + core.log.error("failed to encode json: ", encode_err) + return nil, "failed to encode json: " .. encode_err + end if shared_dict then - -- the worker that handles Admin API calls is responsible for writing the shared dict - local ok, err = shared_dict:set("config", config) + local ok, save_err = shared_dict:set("config", encoded_config) if not ok then - return nil, "failed to save config to shared dict: " .. err + return nil, "failed to save config to shared dict: " .. save_err end - core.log.info("standalone config updated: ", config) + core.log.info("standalone config updated: ", encoded_config) else core.log.crit(config_yaml.ERR_NO_SHARED_DICT) end @@ -59,24 +95,38 @@ end local function update(ctx) local content_type = core.request.header(nil, "content-type") or "application/json" - local conf_version - if ctx.var.arg_conf_version then - conf_version = tonumber(ctx.var.arg_conf_version) - if not conf_version then - return core.response.exit(400, {error_msg = "invalid conf_version: " - .. ctx.var.arg_conf_version - .. ", should be a integer" }) + + local update_keys + + local config, err = get_config() + if not config then + if err ~= "not found" then + core.log.error("failed to get config from shared dict: ", err) + return core.response.exit(500, { + error_msg = "failed to get config from shared dict: " .. err + }) end else - conf_version = ngx.time() - end - -- check if conf_version greater than the current version - local _, ver = config_yaml._get_config() - if conf_version <= ver then - return core.response.exit(400, {error_msg = "invalid conf_version: conf_version (" - .. conf_version - .. ") should be greater than the current version (" - .. ver .. ")"}) + for key, version in pairs(config.conf_version) do + local header = "x-apisix-conf-version" .. "-" .. key + local req_conf_version = core.request.header(ctx, header) + if req_conf_version then + if not tonumber(req_conf_version) then + return core.response.exit(400, {error_msg = "invalid header: [" .. header .. + ": " .. req_conf_version .. "]" .. + " should be a integer"}) + end + req_conf_version = tonumber(req_conf_version) + if req_conf_version <= version then + return core.response.exit(400, {error_msg = "invalid header: [" .. header .. + ": " .. req_conf_version .. "]" .. + " should be greater than the current version (" .. version .. ")"}) + else + update_keys = update_keys or {} + update_keys[key] = req_conf_version + end + end + end end -- read the request body @@ -107,9 +157,19 @@ local function update(ctx) -- check input by jsonschema local apisix_yaml = {} + local confi_version_kv = tbl_deepcopy(config and config.conf_version or {}) local created_objs = config_yaml.fetch_all_created_obj() + for key, obj in pairs(created_objs) do - if req_body[key] and #req_body[key] > 0 then + local updated = false + if not update_keys then + confi_version_kv[key] = confi_version_kv[key] and confi_version_kv[key] + 1 or 1 + updated = true + elseif update_keys[key] then + confi_version_kv[key] = update_keys[key] + updated = true + end + if updated and req_body[key] and #req_body[key] > 0 then apisix_yaml[key] = table_new(1, 0) local item_schema = obj.item_schema local item_checker = obj.checker @@ -137,12 +197,15 @@ local function update(ctx) end end - local ok, err = update_and_broadcast_config(apisix_yaml, conf_version) + local ok, err = update_and_broadcast_config(config, apisix_yaml, confi_version_kv) if not ok then core.response.exit(500, err) end - core.response.set_header("X-APISIX-Conf-Version", tostring(conf_version)) + for key, version in pairs(confi_version_kv) do + core.response.set_header("X-APISIX-Conf-Version-" .. key, tostring(version)) + end + return core.response.exit(202) end @@ -151,9 +214,26 @@ local function get(ctx) local accept = core.request.header(nil, "accept") or "application/json" local want_yaml_resp = core.string.has_prefix(accept, "application/yaml") - local _, ver, config = config_yaml._get_config() + local config, err = get_config() + if not config then + if err ~= "not found" then + core.log.error("failed to get config from shared dict: ", err) + return core.response.exit(500, { + error_msg = "failed to get config from shared dict: " .. err + }) + end + config = {} + local created_objs = config_yaml.fetch_all_created_obj() + for _, obj in pairs(created_objs) do + core.response.set_header("X-APISIX-Conf-Version-" .. obj.key, + tostring(obj.conf_version)) + end + else + for key, version in pairs(config.conf_version) do + core.response.set_header("X-APISIX-Conf-Version-" .. key, tostring(version)) + end + end - core.response.set_header("X-APISIX-Conf-Version", tostring(ver)) local resp, err if want_yaml_resp then core.response.set_header("Content-Type", "application/yaml") @@ -195,7 +275,7 @@ end function _M.init_worker() - local function update_config() + local function update_config(data, event, resource, pid) local config, err = shared_dict:get("config") if not config then core.log.error("failed to get config from shared dict: ", err) diff --git a/apisix/core/config_yaml.lua b/apisix/core/config_yaml.lua index 8cd16e9acd7f..521fa46c9665 100644 --- a/apisix/core/config_yaml.lua +++ b/apisix/core/config_yaml.lua @@ -27,13 +27,13 @@ local json = require("apisix.core.json") local new_tab = require("table.new") local check_schema = require("apisix.core.schema").check local profile = require("apisix.core.profile") -local tbl_deepcopy = require("apisix.core.table").deepcopy local lfs = require("lfs") local file = require("apisix.cli.file") local exiting = ngx.worker.exiting local insert_tab = table.insert local type = type local ipairs = ipairs +local pairs = pairs local setmetatable = setmetatable local ngx_sleep = require("apisix.core.utils").sleep local ngx_timer_at = ngx.timer.at @@ -70,11 +70,11 @@ local mt = { local apisix_yaml -local apisix_yaml_raw -- save a deepcopy of the latest configuration for API local apisix_yaml_mtime +local key_conf_version = {} -local function update_config(table, mtime) +local function update_config(table, conf_version) if not table then log.error("failed update config: empty table") return @@ -86,17 +86,20 @@ local function update_config(table, mtime) return end - apisix_yaml = table - apisix_yaml_raw = tbl_deepcopy(table) - apisix_yaml_mtime = mtime -end -_M._update_config = update_config - + if type(conf_version) ~= "table" then + apisix_yaml = table + apisix_yaml_mtime = conf_version + return + end -local function get_config() - return apisix_yaml, apisix_yaml_mtime, apisix_yaml_raw + for key, conf_version in pairs(conf_version) do + if key_conf_version[key] then + apisix_yaml[key] = table[key] + key_conf_version[key] = conf_version + end + end end -_M._get_config = get_config +_M._update_config = update_config local function is_use_admin_api() @@ -163,24 +166,53 @@ local function sync_data(self) return nil, "failed to read local file " .. apisix_yaml_path end - if self.conf_version == apisix_yaml_mtime then + local conf_version + if is_use_admin_api() then + conf_version = key_conf_version[self.key] + else + conf_version = apisix_yaml_mtime + end + + if conf_version == self.conf_version then return true end + + local items = apisix_yaml[self.key] - log.info(self.key, " items: ", json.delay_encode(items)) if not items then self.values = new_tab(8, 0) self.values_hash = new_tab(0, 8) - self.conf_version = apisix_yaml_mtime + self.conf_version = conf_version return true end - if self.values then - for _, item in ipairs(self.values) do - config_util.fire_all_clean_handlers(item) + if self.values and #self.values > 0 then + if is_use_admin_api() then + local clean_items = {} + for _, item in ipairs(items) do + if item.modifiedIndex then + clean_items[item.id] = item.modifiedIndex + end + end + local new_values = new_tab(8, 0) + self.values_hash = new_tab(0, 8) + for _, item in ipairs(self.values) do + local id = item.value.id + if not clean_items[id] then + config_util.fire_all_clean_handlers(item) + else + insert_tab(new_values, item) + self.values_hash[id] = #new_values + end + end + self.values = new_values + else + for _, item in ipairs(self.values) do + config_util.fire_all_clean_handlers(item) + end + self.values = nil end - self.values = nil end if self.single_item then @@ -189,7 +221,8 @@ local function sync_data(self) self.values_hash = new_tab(0, 1) local item = items - local conf_item = {value = item, modifiedIndex = apisix_yaml_mtime, + local modifiedIndex = item.modifiedIndex or conf_version + local conf_item = {value = item, modifiedIndex = modifiedIndex, key = "/" .. self.key} local data_valid = true @@ -221,23 +254,26 @@ local function sync_data(self) end else - self.values = new_tab(#items, 0) - self.values_hash = new_tab(0, #items) + if not self.values then + self.values = new_tab(8, 0) + self.values_hash = new_tab(0, 8) + end local err for i, item in ipairs(items) do - local id = tostring(i) + local idx = tostring(i) local data_valid = true if type(item) ~= "table" then data_valid = false - log.error("invalid item data of [", self.key .. "/" .. id, + log.error("invalid item data of [", self.key .. "/" .. idx, "], val: ", json.delay_encode(item), ", it should be an object") end - local key = item.id or "arr_" .. i - local conf_item = {value = item, modifiedIndex = apisix_yaml_mtime, - key = "/" .. self.key .. "/" .. key} + local id = item.id or ("arr_" .. idx) + local modifiedIndex = item.modifiedIndex or conf_version + local conf_item = {value = item, modifiedIndex = modifiedIndex, + key = "/" .. self.key .. "/" .. id} if data_valid and self.item_schema then data_valid, err = check_schema(self.item_schema, item) @@ -256,12 +292,25 @@ local function sync_data(self) end if data_valid then - insert_tab(self.values, conf_item) - local item_id = conf_item.value.id or self.key .. "#" .. id - item_id = tostring(item_id) - self.values_hash[item_id] = #self.values - conf_item.value.id = item_id - conf_item.clean_handlers = {} + local item_id = tostring(id) + local pre_index = self.values_hash[item_id] + if pre_index then + -- remove the old item + local pre_val = self.values[pre_index] + if pre_val and + (not pre_val.modifiedIndex or pre_val.modifiedIndex ~= modifiedIndex) then + log.error("fire all clean handlers for ", self.key, " id: ", id, + " modifiedIndex: ", modifiedIndex) + config_util.fire_all_clean_handlers(pre_val) + conf_item.clean_handlers = {} + self.values[pre_index] = conf_item + end + else + insert_tab(self.values, conf_item) + self.values_hash[item_id] = #self.values + conf_item.value.id = item_id + conf_item.clean_handlers = {} + end if self.filter then self.filter(conf_item) @@ -270,7 +319,7 @@ local function sync_data(self) end end - self.conf_version = apisix_yaml_mtime + self.conf_version = conf_version return true end @@ -395,6 +444,14 @@ function _M.new(key, opts) key = sub_str(key, 2) end + if is_use_admin_api() then + key_conf_version[key] = 0 + if item_schema then + item_schema.properties.modifiedIndex = { + type = "integer", + } + end + end local obj = setmetatable({ automatic = automatic, item_schema = item_schema, @@ -471,7 +528,6 @@ end function _M.init_worker() if is_use_admin_api() then apisix_yaml = {} - apisix_yaml_raw = {} apisix_yaml_mtime = 0 return true end diff --git a/docs/en/latest/deployment-modes.md b/docs/en/latest/deployment-modes.md index bc17c4366caa..73531ea3c304 100644 --- a/docs/en/latest/deployment-modes.md +++ b/docs/en/latest/deployment-modes.md @@ -115,73 +115,98 @@ This method is more suitable for two types of users: Now, we have two standalone running modes, file-driven and API-driven. -1. The file-driven mode is the kind APISIX has always supported. +#### File-driven - The routing rules in the `conf/apisix.yaml` file are loaded into memory immediately after the APISIX node service starts. At each interval (default: 1 second), APISIX checks for updates to the file. If changes are detected, it reloads the rules. +The file-driven mode is the kind APISIX has always supported. - *Note*: Reloading and updating routing rules are all hot memory updates. There is no replacement of working processes, since it's a hot update. +The routing rules in the `conf/apisix.yaml` file are loaded into memory immediately after the APISIX node service starts. At each interval (default: 1 second), APISIX checks for updates to the file. If changes are detected, it reloads the rules. - This requires us to set the APISIX role to data plane. That is, set `deployment.role` to `data_plane` and `deployment.role_data_plane.config_provider` to `yaml`. +*Note*: Reloading and updating routing rules are all hot memory updates. There is no replacement of working processes, since it's a hot update. - Refer to the example below: +This requires us to set the APISIX role to data plane. That is, set `deployment.role` to `data_plane` and `deployment.role_data_plane.config_provider` to `yaml`. - ```yaml - deployment: - role: data_plane - role_data_plane: - config_provider: yaml - #END - ``` +Refer to the example below: - This makes it possible to disable the Admin API and discover configuration changes and reloads based on the local file system. +```yaml +deployment: + role: data_plane + role_data_plane: + config_provider: yaml +``` + +This makes it possible to disable the Admin API and discover configuration changes and reloads based on the local file system. + +#### API-driven (Experimental) + +##### Overview + +The API-driven mode is an emerging paradigm for standalone deployment. The routing rules are entirely in memory and not in a file, requiring updates through the dedicated Standalone Admin API. Changes overwrite the entire configuration and take effect immediately without requiring a reboot, as it is hot updated. + +##### Configuration + +To enable this mode, set the APISIX role to `traditional` (to start both the API gateway and the Admin API endpoint) and use the YAML config provider. Example configuration: + +```yaml +deployment: + role: traditional + role_traditional: + config_provider: yamls +``` + +This disables the local file source of configuration in favor of the API. When APISIX starts, it uses an empty configuration until updated via the API. + +##### API Endpoints -2. The API-driven is an emerging paradigm for standalone. +* Per-resource-type version header - The routing rules will be entirely in memory and not in a file, and it will need to be updated using the dedicated Standalone Admin API. + Use `X-APISIX-Conf-Version-` to indicate the client’s current version for each resource type (e.g. routes, upstreams, services, etc.). - I.e. we need to send an HTTP PUT request to this API containing the configuration in JSON or YAML format, which will flush the configuration used by each worker in the current APISIX instance. + If no `X-APISIX-Conf-Version-` headers are provided, APISIX treats the request as a full sync, replacing all existing resources. - Changes will overwrite the entire configuration and take effect immediately without requiring a reboot, as it is hot updated. + If the supplied version for any resource type is ≤ the server’s current version, the request for that resource will be rejected. - This requires us to set the APISIX role to traditional (since we need to start both the API gateway and the Admin API endpoint) and use the yaml config provider. That is, set `deployment.role` to `traditional` and `deployment.role_traditional.config_provider` to `yaml`. +* modifiedIndex per resource - Refer to the example below: + Allow setting an index for each resource, APISIX compares it to its modifiedIndex to determine whether to accept the update. - ```yaml - deployment: - role: traditional - role_traditional: - config_provider: yaml - #END - ``` - This disables the local file source of configuration in favor of the API. When APISIX starts, it uses the empty configuration until you update it via the API. +**Example:** - The following are API endpoints: +1. get configuration - ```shell - ## Update configuration - ## The conf_version is not required, if it is not entered by the client, the current 10-digit epoch time is used by default. - curl -X PUT http://127.0.0.1:9180/apisix/admin/configs?conf_version=1234 \ - -H "X-API-KEY: " - -H "Content-Type: application/json" ## or application/yaml - --data-binary @config.json +```shell +curl -X GET http://127.0.0.1:9180/apisix/admin/configs \ + -H "X-API-KEY: " \ + -H "Accept: application/json" ## or application/yaml +``` + +2. full update - ## Get latest configuration - curl -X GET http://127.0.0.1:9180/apisix/admin/configs - -H "X-API-KEY: " - -H "Accept: application/json" ## or application/yaml - ``` +```shell +curl -X PUT http://127.0.0.1:9180/apisix/admin/configs \ + -H "X-API-KEY: " \ + -H "Content-Type: application/json" ## or application/yaml \ + -d '{}' +``` - The update API validates the input and returns an error if it is invalid. If the configuration is accepted, it responds with a `202 Accepted` status and includes the latest configuration version in the `X-APISIX-Conf-Version` header. +3. update based on resource type + +```shell +curl -X PUT http://127.0.0.1:9180/apisix/admin/configs \ + -H "X-API-KEY: ${API_KEY}" \ + -H "X-APISIX-Conf-Version-routes: 42" \ + -H "X-APISIX-Conf-Version-upstreams: 17" \ + -H "Content-Type: application/json" \ + -d '{"routes":[],"upstreams":[]}' +``` - The get API also returns the version number via the `X-APISIX-Conf-Version` header, and returns a response body containing the configuration in a specific format as requested by the client `Accept` header. +:::note - These APIs apply the same security requirements as the Admin API — such as API key, TLS/mTLS, CORS, and IP allowlist — no changes or additions. +These APIs apply the same security requirements as the Admin API, including API key, TLS/mTLS, CORS, and IP allowlist. - The API accepts input in the same format as the file-based mode described above, although it also allows the user to input JSON instead of just YAML. The following example still applies. However, the API does not rely on the `#END` suffix because HTTP will guarantee input integrity. +The API accepts input in the same format as the file-based mode, supporting both JSON and YAML. Unlike the file-based mode, the API does not rely on the `#END` suffix, as HTTP guarantees input integrity. - *Note*: In this case, the Admin API based on etcd is not available. The configuration can only be flushed as a whole, rather than modified partially, and the client must send a request containing the complete new configuration to the API. +::: ### How to configure rules diff --git a/t/admin/standalone.spec.ts b/t/admin/standalone.spec.ts index 5b8ed4a0bd13..72d0b8bbad83 100644 --- a/t/admin/standalone.spec.ts +++ b/t/admin/standalone.spec.ts @@ -45,6 +45,20 @@ const config2 = { }, ], }; +const routeWithModifiedIndex = { + routes: [ + { + id: "r1", + uri: "/r1", + modifiedIndex: 1, + upstream: { + nodes: { "127.0.0.1:1980": 1 }, + type: "roundrobin", + }, + plugins: { "proxy-rewrite": { uri: "/hello" } }, + }, + ], +}; const clientConfig = { baseURL: "http://localhost:1984", headers: { @@ -70,7 +84,10 @@ describe("Admin - Standalone", () => { const resp = await client.get(ENDPOINT); expect(resp.status).toEqual(200); expect(resp.headers["content-type"]).toEqual("application/json"); - expect(resp.headers["x-apisix-conf-version"]).toEqual("0"); + expect(resp.headers["x-apisix-conf-version-routes"]).toEqual("0"); + expect(resp.headers["x-apisix-conf-version-ssls"]).toEqual("0"); + expect(resp.headers["x-apisix-conf-version-services"]).toEqual("0"); + expect(resp.headers["x-apisix-conf-version-upstreams"]).toEqual("0"); expect(resp.data).toEqual({}); }); @@ -80,23 +97,24 @@ describe("Admin - Standalone", () => { }); expect(resp.status).toEqual(200); expect(resp.headers["content-type"]).toEqual("application/yaml"); - expect(resp.headers["x-apisix-conf-version"]).toEqual("0"); + expect(resp.headers["x-apisix-conf-version-routes"]).toEqual("0"); + expect(resp.headers["x-apisix-conf-version-ssls"]).toEqual("0"); + expect(resp.headers["x-apisix-conf-version-services"]).toEqual("0"); + expect(resp.headers["x-apisix-conf-version-upstreams"]).toEqual("0"); // The lyaml-encoded empty Lua table becomes an array, which is expected, but shouldn't be expect(resp.data).toEqual([]); }); it("update config (add routes, by json)", async () => { - const resp = await client.put(ENDPOINT, config1, { - params: { conf_version: 1 }, - }); + const resp = await client.put(ENDPOINT, config1); expect(resp.status).toEqual(202); }); it("dump config (json format)", async () => { const resp = await client.get(ENDPOINT); expect(resp.status).toEqual(200); - expect(resp.headers["x-apisix-conf-version"]).toEqual("1"); + expect(resp.headers["x-apisix-conf-version-routes"]).toEqual("1"); }); it("dump config (yaml format)", async () => { @@ -105,7 +123,7 @@ describe("Admin - Standalone", () => { responseType: 'text', }); expect(resp.status).toEqual(200); - expect(resp.headers["x-apisix-conf-version"]).toEqual("1"); + expect(resp.headers["x-apisix-conf-version-routes"]).toEqual("1"); expect(resp.data).toContain("routes:") expect(resp.data).toContain("id: r1") expect(resp.data.startsWith('---')).toBe(false); @@ -123,7 +141,6 @@ describe("Admin - Standalone", () => { ENDPOINT, YAML.stringify(config2), { - params: { conf_version: 2 }, headers: { "Content-Type": "application/yaml" }, } ); @@ -133,7 +150,7 @@ describe("Admin - Standalone", () => { it("dump config (json format)", async () => { const resp = await client.get(ENDPOINT); expect(resp.status).toEqual(200); - expect(resp.headers["x-apisix-conf-version"]).toEqual("2"); + expect(resp.headers["x-apisix-conf-version-routes"]).toEqual("2"); }); it('check route "r1"', () => @@ -173,14 +190,16 @@ describe("Admin - Standalone", () => { ENDPOINT, YAML.stringify(config2), { - params: { conf_version: 0 }, - headers: { "Content-Type": "application/yaml" }, + headers: { + "Content-Type": "application/yaml", + "x-apisix-conf-version-routes": 1, + }, } ); expect(resp.status).toEqual(400); expect(resp.data).toEqual({ error_msg: - "invalid conf_version: conf_version (0) should be greater than the current version (3)", + "invalid header: [x-apisix-conf-version-routes: 1] should be greater than the current version (3)", }); }); @@ -190,12 +209,15 @@ describe("Admin - Standalone", () => { YAML.stringify(config2), { params: { conf_version: "abc" }, - headers: { "Content-Type": "application/yaml" }, + headers: { + "Content-Type": "application/yaml", + "x-apisix-conf-version-routes": "adc", + }, } ); expect(resp.status).toEqual(400); expect(resp.data).toEqual({ - error_msg: "invalid conf_version: abc, should be a integer", + error_msg: "invalid header: [x-apisix-conf-version-routes: adc] should be a integer", }); }); @@ -228,5 +250,48 @@ describe("Admin - Standalone", () => { error_msg: "invalid request body: empty request body", }); }); + + it("control resource changes using modifiedIndex", async () => { + const c1 = structuredClone(routeWithModifiedIndex); + c1.routes[0].modifiedIndex = 1; + + const c2 = structuredClone(c1); + c2.routes[0].uri = "/r2"; + + const c3 = structuredClone(c2); + c3.routes[0].modifiedIndex = 2; + + // Update with c1 + const resp = await clientException.put(ENDPOINT, c1); + expect(resp.status).toEqual(202); + + // Check route /r1 exists + const resp_1 = await client.get("/r1"); + expect(resp_1.status).toEqual(200); + + // Update with c2 + const resp2 = await clientException.put(ENDPOINT, c2); + expect(resp2.status).toEqual(202); + + // Check route /r1 exists + const resp2_2 = await client.get("/r1"); + expect(resp2_2.status).toEqual(200); + + // Check route /r2 not exists + const resp2_1 = await client.get("/r2").catch((err) => err.response); + expect(resp2_1.status).toEqual(404); + + // Update with c3 + const resp3 = await clientException.put(ENDPOINT, c3); + expect(resp3.status).toEqual(202); + + // Check route /r1 not exists + const resp3_1 = await client.get("/r1").catch((err) => err.response); + expect(resp3_1.status).toEqual(404); + + // Check route /r2 exists + const resp3_2 = await client.get("/r2"); + expect(resp3_2.status).toEqual(200); + }); }); }); diff --git a/t/admin/standalone.t b/t/admin/standalone.t index b5c9726e00c1..0953d6330388 100644 --- a/t/admin/standalone.t +++ b/t/admin/standalone.t @@ -68,7 +68,7 @@ qr/PASS admin\/standalone.spec.ts/ --- config location /t {} # force the worker to restart by changing the configuration --- request -PUT /apisix/admin/configs?conf_version=101 +PUT /apisix/admin/configs {"routes":[{"id":"r1","uri":"/r1","upstream":{"nodes":{"127.0.0.1:1980":1},"type":"roundrobin"},"plugins":{"proxy-rewrite":{"uri":"/hello"}}}]} --- more_headers X-API-KEY: edd1c9f034335f136f87ad84b625c8f1 @@ -91,7 +91,7 @@ hello world --- config location /t2 {} --- request -PUT /apisix/admin/configs?conf_version=102 +PUT /apisix/admin/configs {} --- more_headers X-API-KEY: edd1c9f034335f136f87ad84b625c8f1 @@ -105,3 +105,46 @@ X-API-KEY: edd1c9f034335f136f87ad84b625c8f1 --- request GET /r1 --- error_code: 404 + + + +=== TEST 6: route references upstream, but only updates the route +--- config + location /t6 {} +--- pipelined_requests eval +[ + "PUT /apisix/admin/configs\n" . "{\"routes\":[{\"id\":\"r1\",\"uri\":\"/r1\",\"upstream_id\":\"u1\",\"plugins\":{\"proxy-rewrite\":{\"uri\":\"/hello\"}}}],\"upstreams\":[{\"id\":\"u1\",\"nodes\":{\"127.0.0.1:1980\":1},\"type\":\"roundrobin\"}]}", + "PUT /apisix/admin/configs\n" . "{\"routes\":[{\"id\":\"r1\",\"uri\":\"/r2\",\"upstream_id\":\"u1\",\"plugins\":{\"proxy-rewrite\":{\"uri\":\"/hello\"}}}]}" +] +--- more_headers eval +[ + "X-API-KEY: edd1c9f034335f136f87ad84b625c8f1", + "X-API-KEY: edd1c9f034335f136f87ad84b625c8f1\n" . "x-apisix-conf-version-routes: 100", +] +--- error_code eval +[202, 202] + + + +=== TEST 7: hit r2 +--- config + location /t3 {} +--- pipelined_requests eval +["GET /r1", "GET /r2"] +--- error_code eval +[404, 200] + + + +=== TEST 8: put invalid conf_version +--- config + location /t {} +--- request +PUT /apisix/admin/configs +{"routes":[{"id":"r1","uri":"/r2","upstream_id":"u1","plugins":{"proxy-rewrite":{"uri":"/hello"}}}]} +--- more_headers +X-API-KEY: edd1c9f034335f136f87ad84b625c8f1 +x-apisix-conf-version-routes: 100 +--- error_code: 400 +--- response_body +{"error_msg":"invalid header: [x-apisix-conf-version-routes: 100] should be greater than the current version (100)"} From 17927e2becd579e32a88bc037c8cfd9d6f2ffe50 Mon Sep 17 00:00:00 2001 From: Foo Bar Date: Fri, 16 May 2025 07:45:16 +0800 Subject: [PATCH 02/11] fix ci --- apisix/admin/standalone.lua | 132 ++++++++++------------------- apisix/core/config_yaml.lua | 49 ++++------- docs/en/latest/deployment-modes.md | 3 +- t/admin/standalone.spec.ts | 85 ++++++++++++++----- t/admin/standalone.t | 82 +++++++++++++++--- 5 files changed, 198 insertions(+), 153 deletions(-) diff --git a/apisix/admin/standalone.lua b/apisix/admin/standalone.lua index 8d772693d955..9b6ce3f6bae2 100644 --- a/apisix/admin/standalone.lua +++ b/apisix/admin/standalone.lua @@ -17,8 +17,6 @@ local type = type local pairs = pairs local ipairs = ipairs -local tonumber = tonumber -local tostring = tostring local str_lower = string.lower local ngx = ngx local get_method = ngx.req.get_method @@ -30,15 +28,12 @@ local events = require("apisix.events") local core = require("apisix.core") local config_yaml = require("apisix.core.config_yaml") local check_schema = require("apisix.core.schema").check -local tbl_deepcopy = require("apisix.core.table").deepcopy - local EVENT_UPDATE = "standalone-api-configuration-update" local _M = {} - local function get_config() local config = shared_dict:get("config") if not config then @@ -54,37 +49,20 @@ local function get_config() end -local function update_and_broadcast_config(old_config, apisix_yaml, conf_version_kv) - local config = { - conf = apisix_yaml, - conf_version = conf_version_kv - } - - if old_config then - local ori_config = tbl_deepcopy(old_config) - ori_config.conf_version = ori_config.conf_version or {} - ori_config.conf = ori_config.conf or {} - for key, v in pairs(conf_version_kv) do - if v ~= ori_config.conf_version[key] then - ori_config.conf[key] = apisix_yaml[key] - ori_config.conf_version[key] = v - end - end - config = ori_config - end - - local encoded_config, encode_err = core.json.encode(config) - if not encoded_config then - core.log.error("failed to encode json: ", encode_err) - return nil, "failed to encode json: " .. encode_err +local function update_and_broadcast_config(apisix_yaml) + local raw, err = core.json.encode(apisix_yaml) + if not raw then + core.log.error("failed to encode json: ", err) + return nil, "failed to encode json: " .. err end if shared_dict then - local ok, save_err = shared_dict:set("config", encoded_config) + -- the worker that handles Admin API calls is responsible for writing the shared dict + local ok, err = shared_dict:set("config", raw) if not ok then - return nil, "failed to save config to shared dict: " .. save_err + return nil, "failed to save config to shared dict: " .. err end - core.log.info("standalone config updated: ", encoded_config) + core.log.info("standalone config updated: ", raw) else core.log.crit(config_yaml.ERR_NO_SHARED_DICT) end @@ -95,40 +73,6 @@ end local function update(ctx) local content_type = core.request.header(nil, "content-type") or "application/json" - - local update_keys - - local config, err = get_config() - if not config then - if err ~= "not found" then - core.log.error("failed to get config from shared dict: ", err) - return core.response.exit(500, { - error_msg = "failed to get config from shared dict: " .. err - }) - end - else - for key, version in pairs(config.conf_version) do - local header = "x-apisix-conf-version" .. "-" .. key - local req_conf_version = core.request.header(ctx, header) - if req_conf_version then - if not tonumber(req_conf_version) then - return core.response.exit(400, {error_msg = "invalid header: [" .. header .. - ": " .. req_conf_version .. "]" .. - " should be a integer"}) - end - req_conf_version = tonumber(req_conf_version) - if req_conf_version <= version then - return core.response.exit(400, {error_msg = "invalid header: [" .. header .. - ": " .. req_conf_version .. "]" .. - " should be greater than the current version (" .. version .. ")"}) - else - update_keys = update_keys or {} - update_keys[key] = req_conf_version - end - end - end - end - -- read the request body local req_body, err = core.request.get_body() if err then @@ -155,21 +99,46 @@ local function update(ctx) end req_body = data + local config, err = get_config() + if not config then + if err ~= "not found" then + core.log.error("failed to get config from shared dict: ", err) + return core.response.exit(500, { + error_msg = "failed to get config from shared dict: " .. err + }) + end + end + -- check input by jsonschema local apisix_yaml = {} - local confi_version_kv = tbl_deepcopy(config and config.conf_version or {}) local created_objs = config_yaml.fetch_all_created_obj() for key, obj in pairs(created_objs) do - local updated = false - if not update_keys then - confi_version_kv[key] = confi_version_kv[key] and confi_version_kv[key] + 1 or 1 - updated = true - elseif update_keys[key] then - confi_version_kv[key] = update_keys[key] - updated = true + local conf_version_key = obj.conf_version_key + local conf_version = config and config[conf_version_key] or obj.conf_version + + local new_conf_version = req_body[conf_version_key] + if not new_conf_version then + new_conf_version = conf_version + 1 + else + if type(new_conf_version) ~= "number" then + return core.response.exit(400, { + error_msg = conf_version_key .. " must be a number", + }) + end + + if new_conf_version < conf_version then + return core.response.exit(400, { + error_msg = conf_version_key .. + " must be greater than or equal to (" .. conf_version .. ")", + }) + end end - if updated and req_body[key] and #req_body[key] > 0 then + + apisix_yaml[conf_version_key] = new_conf_version + if new_conf_version == conf_version then + apisix_yaml[key] = config and config[key] + elseif req_body[key] and #req_body[key] > 0 then apisix_yaml[key] = table_new(1, 0) local item_schema = obj.item_schema local item_checker = obj.checker @@ -197,15 +166,11 @@ local function update(ctx) end end - local ok, err = update_and_broadcast_config(config, apisix_yaml, confi_version_kv) + local ok, err = update_and_broadcast_config(apisix_yaml) if not ok then core.response.exit(500, err) end - for key, version in pairs(confi_version_kv) do - core.response.set_header("X-APISIX-Conf-Version-" .. key, tostring(version)) - end - return core.response.exit(202) end @@ -225,12 +190,7 @@ local function get(ctx) config = {} local created_objs = config_yaml.fetch_all_created_obj() for _, obj in pairs(created_objs) do - core.response.set_header("X-APISIX-Conf-Version-" .. obj.key, - tostring(obj.conf_version)) - end - else - for key, version in pairs(config.conf_version) do - core.response.set_header("X-APISIX-Conf-Version-" .. key, tostring(version)) + config[obj.conf_version_key] = obj.conf_version end end @@ -287,7 +247,7 @@ function _M.init_worker() core.log.error("failed to decode json: ", err) return end - config_yaml._update_config(config.conf, config.conf_version) + config_yaml._update_config(config) end events:register(update_config, EVENT_UPDATE, EVENT_UPDATE) end diff --git a/apisix/core/config_yaml.lua b/apisix/core/config_yaml.lua index 521fa46c9665..b3af918b4fa2 100644 --- a/apisix/core/config_yaml.lua +++ b/apisix/core/config_yaml.lua @@ -33,7 +33,6 @@ local exiting = ngx.worker.exiting local insert_tab = table.insert local type = type local ipairs = ipairs -local pairs = pairs local setmetatable = setmetatable local ngx_sleep = require("apisix.core.utils").sleep local ngx_timer_at = ngx.timer.at @@ -72,8 +71,6 @@ local mt = { local apisix_yaml local apisix_yaml_mtime -local key_conf_version = {} - local function update_config(table, conf_version) if not table then log.error("failed update config: empty table") @@ -86,18 +83,8 @@ local function update_config(table, conf_version) return end - if type(conf_version) ~= "table" then - apisix_yaml = table - apisix_yaml_mtime = conf_version - return - end - - for key, conf_version in pairs(conf_version) do - if key_conf_version[key] then - apisix_yaml[key] = table[key] - key_conf_version[key] = conf_version - end - end + apisix_yaml = table + apisix_yaml_mtime = conf_version end _M._update_config = update_config @@ -161,24 +148,21 @@ local function sync_data(self) return nil, "missing 'key' arguments" end - if not apisix_yaml_mtime then - log.warn("wait for more time") - return nil, "failed to read local file " .. apisix_yaml_path - end - local conf_version if is_use_admin_api() then - conf_version = key_conf_version[self.key] + conf_version = apisix_yaml[self.conf_version_key] or 0 else + if not apisix_yaml_mtime then + log.warn("wait for more time") + return nil, "failed to read local file " .. apisix_yaml_path + end conf_version = apisix_yaml_mtime end - if conf_version == self.conf_version then + if not conf_version or conf_version == self.conf_version then return true end - - local items = apisix_yaml[self.key] if not items then self.values = new_tab(8, 0) @@ -189,17 +173,17 @@ local function sync_data(self) if self.values and #self.values > 0 then if is_use_admin_api() then - local clean_items = {} + local exist_items = {} for _, item in ipairs(items) do if item.modifiedIndex then - clean_items[item.id] = item.modifiedIndex + exist_items[item.id] = item.modifiedIndex end end local new_values = new_tab(8, 0) self.values_hash = new_tab(0, 8) for _, item in ipairs(self.values) do local id = item.value.id - if not clean_items[id] then + if not exist_items[id] then config_util.fire_all_clean_handlers(item) else insert_tab(new_values, item) @@ -302,8 +286,9 @@ local function sync_data(self) log.error("fire all clean handlers for ", self.key, " id: ", id, " modifiedIndex: ", modifiedIndex) config_util.fire_all_clean_handlers(pre_val) - conf_item.clean_handlers = {} self.values[pre_index] = conf_item + conf_item.value.id = item_id + conf_item.clean_handlers = {} end else insert_tab(self.values, conf_item) @@ -366,6 +351,7 @@ local function _automatic_fetch(premature, self) log.info("no config found in shared dict") goto SKIP_SHARED_DICT end + log.info("startup config loaded from shared dict: ", config) config, err = json.decode(tostring(config)) if not config then @@ -373,7 +359,7 @@ local function _automatic_fetch(premature, self) goto SKIP_SHARED_DICT end - _M._update_config(config.conf, config.conf_version) + _M._update_config(config) log.info("config loaded from shared dict") ::SKIP_SHARED_DICT:: @@ -445,8 +431,8 @@ function _M.new(key, opts) end if is_use_admin_api() then - key_conf_version[key] = 0 - if item_schema then + if item_schema and item_schema.properties then + -- allow clients to specify modifiedIndex to control resource changes. item_schema.properties.modifiedIndex = { type = "integer", } @@ -465,6 +451,7 @@ function _M.new(key, opts) last_err = nil, last_err_time = nil, key = key, + conf_version_key = key and key .. "_conf_version", single_item = single_item, filter = filter_fun, }, mt) diff --git a/docs/en/latest/deployment-modes.md b/docs/en/latest/deployment-modes.md index 73531ea3c304..561c893e597a 100644 --- a/docs/en/latest/deployment-modes.md +++ b/docs/en/latest/deployment-modes.md @@ -163,13 +163,12 @@ This disables the local file source of configuration in favor of the API. When A If no `X-APISIX-Conf-Version-` headers are provided, APISIX treats the request as a full sync, replacing all existing resources. - If the supplied version for any resource type is ≤ the server’s current version, the request for that resource will be rejected. + If the supplied version for any resource type is ≤ the server’s current version, the request for that resource will be rejected. * modifiedIndex per resource Allow setting an index for each resource, APISIX compares it to its modifiedIndex to determine whether to accept the update. - **Example:** 1. get configuration diff --git a/t/admin/standalone.spec.ts b/t/admin/standalone.spec.ts index 72d0b8bbad83..17f2a790db2d 100644 --- a/t/admin/standalone.spec.ts +++ b/t/admin/standalone.spec.ts @@ -45,6 +45,12 @@ const config2 = { }, ], }; +const invalidConfVersionConfig1 = { + routes_conf_version: -1, +}; +const invalidConfVersionConfig2 = { + routes_conf_version: "adc", +}; const routeWithModifiedIndex = { routes: [ { @@ -83,12 +89,11 @@ describe("Admin - Standalone", () => { it("dump empty config (default json format)", async () => { const resp = await client.get(ENDPOINT); expect(resp.status).toEqual(200); - expect(resp.headers["content-type"]).toEqual("application/json"); - expect(resp.headers["x-apisix-conf-version-routes"]).toEqual("0"); - expect(resp.headers["x-apisix-conf-version-ssls"]).toEqual("0"); - expect(resp.headers["x-apisix-conf-version-services"]).toEqual("0"); - expect(resp.headers["x-apisix-conf-version-upstreams"]).toEqual("0"); - expect(resp.data).toEqual({}); + expect(resp.data.routes_conf_version).toEqual(0); + expect(resp.data.ssls_conf_version).toEqual(0); + expect(resp.data.services_conf_version).toEqual(0); + expect(resp.data.upstreams_conf_version).toEqual(0); + expect(resp.data.consumers_conf_version).toEqual(0); }); it("dump empty config (yaml format)", async () => { @@ -97,13 +102,11 @@ describe("Admin - Standalone", () => { }); expect(resp.status).toEqual(200); expect(resp.headers["content-type"]).toEqual("application/yaml"); - expect(resp.headers["x-apisix-conf-version-routes"]).toEqual("0"); - expect(resp.headers["x-apisix-conf-version-ssls"]).toEqual("0"); - expect(resp.headers["x-apisix-conf-version-services"]).toEqual("0"); - expect(resp.headers["x-apisix-conf-version-upstreams"]).toEqual("0"); - - // The lyaml-encoded empty Lua table becomes an array, which is expected, but shouldn't be - expect(resp.data).toEqual([]); + expect(resp.data.routes_conf_version).toEqual(0); + expect(resp.data.ssls_conf_version).toEqual(0); + expect(resp.data.services_conf_version).toEqual(0); + expect(resp.data.upstreams_conf_version).toEqual(0); + expect(resp.data.consumers_conf_version).toEqual(0); }); it("update config (add routes, by json)", async () => { @@ -114,7 +117,11 @@ describe("Admin - Standalone", () => { it("dump config (json format)", async () => { const resp = await client.get(ENDPOINT); expect(resp.status).toEqual(200); - expect(resp.headers["x-apisix-conf-version-routes"]).toEqual("1"); + expect(resp.data.routes_conf_version).toEqual(1); + expect(resp.data.ssls_conf_version).toEqual(1); + expect(resp.data.services_conf_version).toEqual(1); + expect(resp.data.upstreams_conf_version).toEqual(1); + expect(resp.data.consumers_conf_version).toEqual(1); }); it("dump config (yaml format)", async () => { @@ -123,7 +130,6 @@ describe("Admin - Standalone", () => { responseType: 'text', }); expect(resp.status).toEqual(200); - expect(resp.headers["x-apisix-conf-version-routes"]).toEqual("1"); expect(resp.data).toContain("routes:") expect(resp.data).toContain("id: r1") expect(resp.data.startsWith('---')).toBe(false); @@ -150,7 +156,11 @@ describe("Admin - Standalone", () => { it("dump config (json format)", async () => { const resp = await client.get(ENDPOINT); expect(resp.status).toEqual(200); - expect(resp.headers["x-apisix-conf-version-routes"]).toEqual("2"); + expect(resp.data.routes_conf_version).toEqual(2); + expect(resp.data.ssls_conf_version).toEqual(2); + expect(resp.data.services_conf_version).toEqual(2); + expect(resp.data.upstreams_conf_version).toEqual(2); + expect(resp.data.consumers_conf_version).toEqual(2); }); it('check route "r1"', () => @@ -188,36 +198,33 @@ describe("Admin - Standalone", () => { it("update config (lower conf_version)", async () => { const resp = await clientException.put( ENDPOINT, - YAML.stringify(config2), + YAML.stringify(invalidConfVersionConfig1), { headers: { "Content-Type": "application/yaml", - "x-apisix-conf-version-routes": 1, }, } ); expect(resp.status).toEqual(400); expect(resp.data).toEqual({ error_msg: - "invalid header: [x-apisix-conf-version-routes: 1] should be greater than the current version (3)", + "routes_conf_version must be greater than or equal to (3)", }); }); it("update config (invalid conf_version)", async () => { const resp = await clientException.put( ENDPOINT, - YAML.stringify(config2), + YAML.stringify(invalidConfVersionConfig2), { - params: { conf_version: "abc" }, headers: { "Content-Type": "application/yaml", - "x-apisix-conf-version-routes": "adc", }, } ); expect(resp.status).toEqual(400); expect(resp.data).toEqual({ - error_msg: "invalid header: [x-apisix-conf-version-routes: adc] should be a integer", + error_msg: "routes_conf_version must be a number", }); }); @@ -232,6 +239,38 @@ describe("Admin - Standalone", () => { }); }); + it("only set routes_conf_version", async () => { + const resp = await clientException.put( + ENDPOINT, + YAML.stringify({ routes_conf_version: 15 }), + {headers: {"Content-Type": "application/yaml"}, + }); + expect(resp.status).toEqual(202); + + const resp_1 = await client.get(ENDPOINT); + expect(resp_1.status).toEqual(200); + expect(resp_1.data.routes_conf_version).toEqual(15); + expect(resp_1.data.ssls_conf_version).toEqual(4); + expect(resp_1.data.services_conf_version).toEqual(4); + expect(resp_1.data.upstreams_conf_version).toEqual(4); + expect(resp_1.data.consumers_conf_version).toEqual(4); + + const resp2 = await clientException.put( + ENDPOINT, + YAML.stringify({ routes_conf_version: 17 }), + {headers: {"Content-Type": "application/yaml"}, + }); + expect(resp2.status).toEqual(202); + + const resp2_1 = await client.get(ENDPOINT); + expect(resp2_1.status).toEqual(200); + expect(resp2_1.data.routes_conf_version).toEqual(17); + expect(resp2_1.data.ssls_conf_version).toEqual(5); + expect(resp2_1.data.services_conf_version).toEqual(5); + expect(resp2_1.data.upstreams_conf_version).toEqual(5); + expect(resp2_1.data.consumers_conf_version).toEqual(5); + }); + it("update config (not compliant with jsonschema)", async () => { const data = structuredClone(config1); (data.routes[0].uri as unknown) = 123; diff --git a/t/admin/standalone.t b/t/admin/standalone.t index 0953d6330388..f34059f9746f 100644 --- a/t/admin/standalone.t +++ b/t/admin/standalone.t @@ -64,7 +64,67 @@ qr/PASS admin\/standalone.spec.ts/ -=== TEST 2: configure route +=== TEST 2: init conf_version +--- config + location /t {} # force the worker to restart by changing the configuration +--- request +PUT /apisix/admin/configs +{ + "consumer_groups_conf_version": 1000, + "consumers_conf_version": 1000, + "global_rules_conf_version": 1000, + "plugin_configs_conf_version": 1000, + "plugin_metadata_conf_version": 1000, + "protos_conf_version": 1000, + "routes_conf_version": 1000, + "secrets_conf_version": 1000, + "services_conf_version": 1000, + "ssls_conf_version": 1000, + "upstreams_conf_version": 1000 +} +--- more_headers +X-API-KEY: edd1c9f034335f136f87ad84b625c8f1 +--- error_code: 202 + + + +=== TEST 3: get config +--- config + location /t { + content_by_lua_block { + local json = require("toolkit.json") + local t = require("lib.test_admin") + local code, body = t.test('/apisix/admin/configs', + ngx.HTTP_GET, + nil, + [[{ + "consumer_groups_conf_version": 1000, + "consumers_conf_version": 1000, + "global_rules_conf_version": 1000, + "plugin_configs_conf_version": 1000, + "plugin_metadata_conf_version": 1000, + "protos_conf_version": 1000, + "routes_conf_version": 1000, + "secrets_conf_version": 1000, + "services_conf_version": 1000, + "ssls_conf_version": 1000, + "upstreams_conf_version": 1000 + }]], + { + ["X-API-KEY"] = "edd1c9f034335f136f87ad84b625c8f1" + } + ) + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed + + + +=== TEST 4: configure route --- config location /t {} # force the worker to restart by changing the configuration --- request @@ -76,7 +136,7 @@ X-API-KEY: edd1c9f034335f136f87ad84b625c8f1 -=== TEST 3: test route +=== TEST 5: test route --- config location /t1 {} --- request @@ -87,7 +147,7 @@ hello world -=== TEST 4: remove route +=== TEST 6: remove route --- config location /t2 {} --- request @@ -99,7 +159,7 @@ X-API-KEY: edd1c9f034335f136f87ad84b625c8f1 -=== TEST 5: test non-exist route +=== TEST 7: test non-exist route --- config location /t3 {} --- request @@ -108,13 +168,13 @@ GET /r1 -=== TEST 6: route references upstream, but only updates the route +=== TEST 8: route references upstream, but only updates the route --- config location /t6 {} --- pipelined_requests eval [ - "PUT /apisix/admin/configs\n" . "{\"routes\":[{\"id\":\"r1\",\"uri\":\"/r1\",\"upstream_id\":\"u1\",\"plugins\":{\"proxy-rewrite\":{\"uri\":\"/hello\"}}}],\"upstreams\":[{\"id\":\"u1\",\"nodes\":{\"127.0.0.1:1980\":1},\"type\":\"roundrobin\"}]}", - "PUT /apisix/admin/configs\n" . "{\"routes\":[{\"id\":\"r1\",\"uri\":\"/r2\",\"upstream_id\":\"u1\",\"plugins\":{\"proxy-rewrite\":{\"uri\":\"/hello\"}}}]}" + "PUT /apisix/admin/configs\n" . "{\"routes_conf_version\":1060,\"upstreams_conf_version\":1060,\"routes\":[{\"id\":\"r1\",\"uri\":\"/r1\",\"upstream_id\":\"u1\",\"plugins\":{\"proxy-rewrite\":{\"uri\":\"/hello\"}}}],\"upstreams\":[{\"id\":\"u1\",\"nodes\":{\"127.0.0.1:1980\":1},\"type\":\"roundrobin\"}]}", + "PUT /apisix/admin/configs\n" . "{\"routes_conf_version\":1062,\"upstreams_conf_version\":1060,\"routes\":[{\"id\":\"r1\",\"uri\":\"/r2\",\"upstream_id\":\"u1\",\"plugins\":{\"proxy-rewrite\":{\"uri\":\"/hello\"}}}],\"upstreams\":[{\"id\":\"u1\",\"nodes\":{\"127.0.0.1:1980\":1},\"type\":\"roundrobin\"}]}" ] --- more_headers eval [ @@ -126,7 +186,7 @@ GET /r1 -=== TEST 7: hit r2 +=== TEST 9: hit r2 --- config location /t3 {} --- pipelined_requests eval @@ -136,15 +196,15 @@ GET /r1 -=== TEST 8: put invalid conf_version +=== TEST 10: routes_conf_version < 1062 is not allowed --- config location /t {} --- request PUT /apisix/admin/configs -{"routes":[{"id":"r1","uri":"/r2","upstream_id":"u1","plugins":{"proxy-rewrite":{"uri":"/hello"}}}]} +{"routes_conf_version":1,"routes":[{"id":"r1","uri":"/r2","upstream_id":"u1","plugins":{"proxy-rewrite":{"uri":"/hello"}}}]} --- more_headers X-API-KEY: edd1c9f034335f136f87ad84b625c8f1 x-apisix-conf-version-routes: 100 --- error_code: 400 --- response_body -{"error_msg":"invalid header: [x-apisix-conf-version-routes: 100] should be greater than the current version (100)"} +{"error_msg":"routes_conf_version must be greater than or equal to (1062)"} From d0df1d2ade52455e3901e12a7f4e6353dd500737 Mon Sep 17 00:00:00 2001 From: Foo Bar Date: Fri, 16 May 2025 09:18:22 +0800 Subject: [PATCH 03/11] update doc --- docs/en/latest/deployment-modes.md | 77 +++++++++++++++++++++++++++--- 1 file changed, 70 insertions(+), 7 deletions(-) diff --git a/docs/en/latest/deployment-modes.md b/docs/en/latest/deployment-modes.md index 561c893e597a..48a7c0db3594 100644 --- a/docs/en/latest/deployment-modes.md +++ b/docs/en/latest/deployment-modes.md @@ -157,13 +157,29 @@ This disables the local file source of configuration in favor of the API. When A ##### API Endpoints -* Per-resource-type version header +* Per-resource-type conf_version - Use `X-APISIX-Conf-Version-` to indicate the client’s current version for each resource type (e.g. routes, upstreams, services, etc.). + Use `_conf_version` to indicate the client’s current version for each resource type (e.g. routes, upstreams, services, etc.). - If no `X-APISIX-Conf-Version-` headers are provided, APISIX treats the request as a full sync, replacing all existing resources. + ```json + { + "routes_conf_version": 12, + "upstreams_conf_version": 102, + "routes": [], + "upstreams": [] + } + ``` - If the supplied version for any resource type is ≤ the server’s current version, the request for that resource will be rejected. + APISIX compares each provided `_conf_version` against its in-memory `_conf_version` for that resource type: + + - **Greater than** the current `conf_version` + If your `_conf_version` is **higher**, APISIX will **rebuild/reset** that resource type’s data to match your payload. + + - **Equal to** the current `conf_version` + If it is **equal**, APISIX treats the resource as **unchanged** and **ignores** it (no data is rebuilt). + + - **Less than** the current `conf_version` + If it is **lower**, APISIX considers your update **stale** and **rejects** the request for that resource type with a **400 Bad Request**. * modifiedIndex per resource @@ -179,6 +195,24 @@ curl -X GET http://127.0.0.1:9180/apisix/admin/configs \ -H "Accept: application/json" ## or application/yaml ``` +This returns the current configuration in JSON or YAML format. + +```json +{ + "consumer_groups_conf_version": 1000, + "consumers_conf_version": 1000, + "global_rules_conf_version": 1000, + "plugin_configs_conf_version": 1000, + "plugin_metadata_conf_version": 1000, + "protos_conf_version": 1000, + "routes_conf_version": 1000, + "secrets_conf_version": 1000, + "services_conf_version": 1000, + "ssls_conf_version": 1000, + "upstreams_conf_version": 1000 +} +``` + 2. full update ```shell @@ -190,13 +224,42 @@ curl -X PUT http://127.0.0.1:9180/apisix/admin/configs \ 3. update based on resource type +In APISIX memory, the current configuration is: + +```json +{ + "routes_conf_version": 1000, + "upstreams_conf_version": 1000, +} + +Due `upstreams_conf_version: 1001` > `upstreams_conf_version: 1000`, APISIX will update the upstreams configuration: + ```shell curl -X PUT http://127.0.0.1:9180/apisix/admin/configs \ -H "X-API-KEY: ${API_KEY}" \ - -H "X-APISIX-Conf-Version-routes: 42" \ - -H "X-APISIX-Conf-Version-upstreams: 17" \ -H "Content-Type: application/json" \ - -d '{"routes":[],"upstreams":[]}' + -d ' +{ + "routes_conf_version": 1000, + "upstreams_conf_version": 1001, + "routes": [ + { + "id": "r1", + "uri": "/hello", + "upstream_id": "u1" + } + ], + "upstreams": [ + { + "id": "u1", + "nodes": { + "127.0.0.1:1980": 1, + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + } + ] +}' ``` :::note From 1322db9f1e08b69b42f8bdf5f1fb41696d8bbd2b Mon Sep 17 00:00:00 2001 From: Foo Bar Date: Fri, 16 May 2025 09:26:56 +0800 Subject: [PATCH 04/11] fix doc lint --- docs/en/latest/deployment-modes.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/en/latest/deployment-modes.md b/docs/en/latest/deployment-modes.md index 48a7c0db3594..f57553ee951c 100644 --- a/docs/en/latest/deployment-modes.md +++ b/docs/en/latest/deployment-modes.md @@ -172,13 +172,13 @@ This disables the local file source of configuration in favor of the API. When A APISIX compares each provided `_conf_version` against its in-memory `_conf_version` for that resource type: - - **Greater than** the current `conf_version` + - **Greater than** the current `conf_version` If your `_conf_version` is **higher**, APISIX will **rebuild/reset** that resource type’s data to match your payload. - - **Equal to** the current `conf_version` + - **Equal to** the current `conf_version` If it is **equal**, APISIX treats the resource as **unchanged** and **ignores** it (no data is rebuilt). - - **Less than** the current `conf_version` + - **Less than** the current `conf_version` If it is **lower**, APISIX considers your update **stale** and **rejects** the request for that resource type with a **400 Bad Request**. * modifiedIndex per resource From e654abaaaad4408b8f164720aa268d531ae2eefb Mon Sep 17 00:00:00 2001 From: Foo Bar Date: Fri, 16 May 2025 17:03:14 +0800 Subject: [PATCH 05/11] resolve comments --- apisix/admin/standalone.lua | 17 +++++++++-------- apisix/core/config_yaml.lua | 15 +++++++++++---- docs/en/latest/deployment-modes.md | 24 +++++++++++++----------- t/admin/standalone.spec.ts | 6 ++++++ 4 files changed, 39 insertions(+), 23 deletions(-) diff --git a/apisix/admin/standalone.lua b/apisix/admin/standalone.lua index 9b6ce3f6bae2..27de24d2bdd8 100644 --- a/apisix/admin/standalone.lua +++ b/apisix/admin/standalone.lua @@ -28,6 +28,7 @@ local events = require("apisix.events") local core = require("apisix.core") local config_yaml = require("apisix.core.config_yaml") local check_schema = require("apisix.core.schema").check +local tbl_deepcopy = require("apisix.core.table").deepcopy local EVENT_UPDATE = "standalone-api-configuration-update" @@ -116,7 +117,7 @@ local function update(ctx) for key, obj in pairs(created_objs) do local conf_version_key = obj.conf_version_key local conf_version = config and config[conf_version_key] or obj.conf_version - + local items = req_body[key] local new_conf_version = req_body[conf_version_key] if not new_conf_version then new_conf_version = conf_version + 1 @@ -126,7 +127,6 @@ local function update(ctx) error_msg = conf_version_key .. " must be a number", }) end - if new_conf_version < conf_version then return core.response.exit(400, { error_msg = conf_version_key .. @@ -138,24 +138,25 @@ local function update(ctx) apisix_yaml[conf_version_key] = new_conf_version if new_conf_version == conf_version then apisix_yaml[key] = config and config[key] - elseif req_body[key] and #req_body[key] > 0 then - apisix_yaml[key] = table_new(1, 0) + elseif items and #items > 0 then + apisix_yaml[key] = table_new(#items, 0) local item_schema = obj.item_schema local item_checker = obj.checker - for index, item in ipairs(req_body[key]) do + for index, item in ipairs(items) do + local item_temp = tbl_deepcopy(item) local valid, err -- need to recover to 0-based subscript local err_prefix = "invalid " .. key .. " at index " .. (index - 1) .. ", err: " if item_schema then - valid, err = check_schema(obj.item_schema, item) + valid, err = check_schema(obj.item_schema, item_temp) if not valid then core.log.error(err_prefix, err) core.response.exit(400, {error_msg = err_prefix .. err}) end end if item_checker then - valid, err = item_checker(item) + valid, err = item_checker(item_temp) if not valid then core.log.error(err_prefix, err) core.response.exit(400, {error_msg = err_prefix .. err}) @@ -235,7 +236,7 @@ end function _M.init_worker() - local function update_config(data, event, resource, pid) + local function update_config() local config, err = shared_dict:get("config") if not config then core.log.error("failed to get config from shared dict: ", err) diff --git a/apisix/core/config_yaml.lua b/apisix/core/config_yaml.lua index b3af918b4fa2..d4fc250f9123 100644 --- a/apisix/core/config_yaml.lua +++ b/apisix/core/config_yaml.lua @@ -25,6 +25,7 @@ local yaml = require("lyaml") local log = require("apisix.core.log") local json = require("apisix.core.json") local new_tab = require("table.new") +local tbl_deepcopy = require("apisix.core.table").deepcopy local check_schema = require("apisix.core.schema").check local profile = require("apisix.core.profile") local lfs = require("lfs") @@ -173,17 +174,21 @@ local function sync_data(self) if self.values and #self.values > 0 then if is_use_admin_api() then - local exist_items = {} + -- used to delete values that do not exist in the new list. + -- only when using modifiedIndex, old values need to be retained. + -- If modifiedIndex changes, old values need to be removed and cleaned up. + local exist_modifiedIndex_items = {} for _, item in ipairs(items) do if item.modifiedIndex then - exist_items[item.id] = item.modifiedIndex + exist_modifiedIndex_items[tostring(item.id)] = true end end + local new_values = new_tab(8, 0) self.values_hash = new_tab(0, 8) for _, item in ipairs(self.values) do local id = item.value.id - if not exist_items[id] then + if not exist_modifiedIndex_items[id] then config_util.fire_all_clean_handlers(item) else insert_tab(new_values, item) @@ -432,10 +437,12 @@ function _M.new(key, opts) if is_use_admin_api() then if item_schema and item_schema.properties then + local item_schema_cp = tbl_deepcopy(item_schema) -- allow clients to specify modifiedIndex to control resource changes. - item_schema.properties.modifiedIndex = { + item_schema_cp.properties.modifiedIndex = { type = "integer", } + item_schema = item_schema_cp end end local obj = setmetatable({ diff --git a/docs/en/latest/deployment-modes.md b/docs/en/latest/deployment-modes.md index f57553ee951c..c3b82e2a4bb3 100644 --- a/docs/en/latest/deployment-modes.md +++ b/docs/en/latest/deployment-modes.md @@ -199,17 +199,17 @@ This returns the current configuration in JSON or YAML format. ```json { - "consumer_groups_conf_version": 1000, - "consumers_conf_version": 1000, - "global_rules_conf_version": 1000, - "plugin_configs_conf_version": 1000, - "plugin_metadata_conf_version": 1000, - "protos_conf_version": 1000, - "routes_conf_version": 1000, - "secrets_conf_version": 1000, - "services_conf_version": 1000, - "ssls_conf_version": 1000, - "upstreams_conf_version": 1000 + "consumer_groups_conf_version": 0, + "consumers_conf_version": 0, + "global_rules_conf_version": 0, + "plugin_configs_conf_version": 0, + "plugin_metadata_conf_version": 0, + "protos_conf_version": 0, + "routes_conf_version": 0, + "secrets_conf_version": 0, + "services_conf_version": 0, + "ssls_conf_version": 0, + "upstreams_conf_version": 0 } ``` @@ -244,6 +244,7 @@ curl -X PUT http://127.0.0.1:9180/apisix/admin/configs \ "upstreams_conf_version": 1001, "routes": [ { + "modifiedIndex": 1000, "id": "r1", "uri": "/hello", "upstream_id": "u1" @@ -251,6 +252,7 @@ curl -X PUT http://127.0.0.1:9180/apisix/admin/configs \ ], "upstreams": [ { + "modifiedIndex": 1001, "id": "u1", "nodes": { "127.0.0.1:1980": 1, diff --git a/t/admin/standalone.spec.ts b/t/admin/standalone.spec.ts index 17f2a790db2d..d80223ca022a 100644 --- a/t/admin/standalone.spec.ts +++ b/t/admin/standalone.spec.ts @@ -124,6 +124,12 @@ describe("Admin - Standalone", () => { expect(resp.data.consumers_conf_version).toEqual(1); }); + it("check default value", async () => { + const resp = await client.get(ENDPOINT); + expect(resp.status).toEqual(200); + expect(resp.data.routes).toEqual(config1.routes); + }); + it("dump config (yaml format)", async () => { const resp = await client.get(ENDPOINT, { headers: { Accept: "application/yaml" }, From efdb387cf2b96e321b60e81af554d55dde48de52 Mon Sep 17 00:00:00 2001 From: Foo Bar Date: Mon, 19 May 2025 16:24:12 +0800 Subject: [PATCH 06/11] update comments --- apisix/core/config_yaml.lua | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/apisix/core/config_yaml.lua b/apisix/core/config_yaml.lua index d4fc250f9123..428cdfc0d93b 100644 --- a/apisix/core/config_yaml.lua +++ b/apisix/core/config_yaml.lua @@ -174,28 +174,26 @@ local function sync_data(self) if self.values and #self.values > 0 then if is_use_admin_api() then - -- used to delete values that do not exist in the new list. - -- only when using modifiedIndex, old values need to be retained. - -- If modifiedIndex changes, old values need to be removed and cleaned up. - local exist_modifiedIndex_items = {} + -- filter self.values to retain only those whose IDs exist in the new items list. + local exist_values = new_tab(8, 0) + self.values_hash = new_tab(0, 8) + + local exist_items = {} for _, item in ipairs(items) do - if item.modifiedIndex then - exist_modifiedIndex_items[tostring(item.id)] = true - end + exist_items[tostring(item.id)] = true end - - local new_values = new_tab(8, 0) - self.values_hash = new_tab(0, 8) + -- remove objects that exist in the self.values but do not exist in the new items. + -- for removed items, trigger cleanup handlers. for _, item in ipairs(self.values) do local id = item.value.id - if not exist_modifiedIndex_items[id] then + if not exist_items[id] then config_util.fire_all_clean_handlers(item) else - insert_tab(new_values, item) - self.values_hash[id] = #new_values + insert_tab(exist_values, item) + self.values_hash[id] = #exist_values end end - self.values = new_values + self.values = exist_values else for _, item in ipairs(self.values) do config_util.fire_all_clean_handlers(item) @@ -287,9 +285,7 @@ local function sync_data(self) -- remove the old item local pre_val = self.values[pre_index] if pre_val and - (not pre_val.modifiedIndex or pre_val.modifiedIndex ~= modifiedIndex) then - log.error("fire all clean handlers for ", self.key, " id: ", id, - " modifiedIndex: ", modifiedIndex) + (not item.modifiedIndex or pre_val.modifiedIndex ~= item.modifiedIndex) then config_util.fire_all_clean_handlers(pre_val) self.values[pre_index] = conf_item conf_item.value.id = item_id From 7f841d029eeedb4c946eb6660a45830c37be3208 Mon Sep 17 00:00:00 2001 From: Foo Bar Date: Tue, 20 May 2025 08:43:14 +0800 Subject: [PATCH 07/11] resolve comments --- docs/en/latest/deployment-modes.md | 3 + t/admin/standalone.spec.ts | 152 ++++++++++++++--------------- 2 files changed, 79 insertions(+), 76 deletions(-) diff --git a/docs/en/latest/deployment-modes.md b/docs/en/latest/deployment-modes.md index c3b82e2a4bb3..55264986f1d7 100644 --- a/docs/en/latest/deployment-modes.md +++ b/docs/en/latest/deployment-modes.md @@ -138,6 +138,9 @@ This makes it possible to disable the Admin API and discover configuration chang #### API-driven (Experimental) +> This mode is experimental, please do not rely on it in your production environment. +> We use it to validate certain specific workloads and if it is appropriate we will turn it into an officially supported feature, otherwise it will be removed. + ##### Overview The API-driven mode is an emerging paradigm for standalone deployment. The routing rules are entirely in memory and not in a file, requiring updates through the dedicated Standalone Admin API. Changes overwrite the entire configuration and take effect immediately without requiring a reboot, as it is hot updated. diff --git a/t/admin/standalone.spec.ts b/t/admin/standalone.spec.ts index d80223ca022a..fb4ae0a858f3 100644 --- a/t/admin/standalone.spec.ts +++ b/t/admin/standalone.spec.ts @@ -192,61 +192,10 @@ describe("Admin - Standalone", () => { it('check route "r2"', () => expect(client.get("/r2")).rejects.toThrow( "Request failed with status code 404" - )); - }); - - describe("Exceptions", () => { - const clientException = axios.create({ - ...clientConfig, - validateStatus: () => true, - }); - - it("update config (lower conf_version)", async () => { - const resp = await clientException.put( - ENDPOINT, - YAML.stringify(invalidConfVersionConfig1), - { - headers: { - "Content-Type": "application/yaml", - }, - } - ); - expect(resp.status).toEqual(400); - expect(resp.data).toEqual({ - error_msg: - "routes_conf_version must be greater than or equal to (3)", - }); - }); - - it("update config (invalid conf_version)", async () => { - const resp = await clientException.put( - ENDPOINT, - YAML.stringify(invalidConfVersionConfig2), - { - headers: { - "Content-Type": "application/yaml", - }, - } - ); - expect(resp.status).toEqual(400); - expect(resp.data).toEqual({ - error_msg: "routes_conf_version must be a number", - }); - }); - - it("update config (invalid json format)", async () => { - const resp = await clientException.put(ENDPOINT, "{abcd", { - params: { conf_version: 4 }, - }); - expect(resp.status).toEqual(400); - expect(resp.data).toEqual({ - error_msg: - "invalid request body: Expected object key string but found invalid token at character 2", - }); - }); + )); it("only set routes_conf_version", async () => { - const resp = await clientException.put( + const resp = await client.put( ENDPOINT, YAML.stringify({ routes_conf_version: 15 }), {headers: {"Content-Type": "application/yaml"}, @@ -261,7 +210,7 @@ describe("Admin - Standalone", () => { expect(resp_1.data.upstreams_conf_version).toEqual(4); expect(resp_1.data.consumers_conf_version).toEqual(4); - const resp2 = await clientException.put( + const resp2 = await client.put( ENDPOINT, YAML.stringify({ routes_conf_version: 17 }), {headers: {"Content-Type": "application/yaml"}, @@ -277,25 +226,6 @@ describe("Admin - Standalone", () => { expect(resp2_1.data.consumers_conf_version).toEqual(5); }); - it("update config (not compliant with jsonschema)", async () => { - const data = structuredClone(config1); - (data.routes[0].uri as unknown) = 123; - const resp = await clientException.put(ENDPOINT, data); - expect(resp.status).toEqual(400); - expect(resp.data).toMatchObject({ - error_msg: - 'invalid routes at index 0, err: property "uri" validation failed: wrong type: expected string, got number', - }); - }); - - it("update config (empty request body)", async () => { - const resp = await clientException.put(ENDPOINT, ""); - expect(resp.status).toEqual(400); - expect(resp.data).toEqual({ - error_msg: "invalid request body: empty request body", - }); - }); - it("control resource changes using modifiedIndex", async () => { const c1 = structuredClone(routeWithModifiedIndex); c1.routes[0].modifiedIndex = 1; @@ -307,7 +237,7 @@ describe("Admin - Standalone", () => { c3.routes[0].modifiedIndex = 2; // Update with c1 - const resp = await clientException.put(ENDPOINT, c1); + const resp = await client.put(ENDPOINT, c1); expect(resp.status).toEqual(202); // Check route /r1 exists @@ -315,7 +245,7 @@ describe("Admin - Standalone", () => { expect(resp_1.status).toEqual(200); // Update with c2 - const resp2 = await clientException.put(ENDPOINT, c2); + const resp2 = await client.put(ENDPOINT, c2); expect(resp2.status).toEqual(202); // Check route /r1 exists @@ -327,7 +257,7 @@ describe("Admin - Standalone", () => { expect(resp2_1.status).toEqual(404); // Update with c3 - const resp3 = await clientException.put(ENDPOINT, c3); + const resp3 = await client.put(ENDPOINT, c3); expect(resp3.status).toEqual(202); // Check route /r1 not exists @@ -339,4 +269,74 @@ describe("Admin - Standalone", () => { expect(resp3_2.status).toEqual(200); }); }); + + describe("Exceptions", () => { + const clientException = axios.create({ + ...clientConfig, + validateStatus: () => true, + }); + + it("update config (lower conf_version)", async () => { + const resp = await clientException.put( + ENDPOINT, + YAML.stringify(invalidConfVersionConfig1), + { + headers: { + "Content-Type": "application/yaml", + }, + } + ); + expect(resp.status).toEqual(400); + expect(resp.data).toEqual({ + error_msg: + "routes_conf_version must be greater than or equal to (20)", + }); + }); + + it("update config (invalid conf_version)", async () => { + const resp = await clientException.put( + ENDPOINT, + YAML.stringify(invalidConfVersionConfig2), + { + headers: { + "Content-Type": "application/yaml", + }, + } + ); + expect(resp.status).toEqual(400); + expect(resp.data).toEqual({ + error_msg: "routes_conf_version must be a number", + }); + }); + + it("update config (invalid json format)", async () => { + const resp = await clientException.put(ENDPOINT, "{abcd", { + params: { conf_version: 4 }, + }); + expect(resp.status).toEqual(400); + expect(resp.data).toEqual({ + error_msg: + "invalid request body: Expected object key string but found invalid token at character 2", + }); + }); + + it("update config (not compliant with jsonschema)", async () => { + const data = structuredClone(config1); + (data.routes[0].uri as unknown) = 123; + const resp = await clientException.put(ENDPOINT, data); + expect(resp.status).toEqual(400); + expect(resp.data).toMatchObject({ + error_msg: + 'invalid routes at index 0, err: property "uri" validation failed: wrong type: expected string, got number', + }); + }); + + it("update config (empty request body)", async () => { + const resp = await clientException.put(ENDPOINT, ""); + expect(resp.status).toEqual(400); + expect(resp.data).toEqual({ + error_msg: "invalid request body: empty request body", + }); + }); + }); }); From cd34b41d814ee5bd974686c80df7c31e2b18913b Mon Sep 17 00:00:00 2001 From: AlinsRan Date: Tue, 20 May 2025 11:03:54 +0800 Subject: [PATCH 08/11] Apply suggestions from code review Co-authored-by: Traky Deng --- docs/en/latest/deployment-modes.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/en/latest/deployment-modes.md b/docs/en/latest/deployment-modes.md index 55264986f1d7..17693f6ad030 100644 --- a/docs/en/latest/deployment-modes.md +++ b/docs/en/latest/deployment-modes.md @@ -143,7 +143,7 @@ This makes it possible to disable the Admin API and discover configuration chang ##### Overview -The API-driven mode is an emerging paradigm for standalone deployment. The routing rules are entirely in memory and not in a file, requiring updates through the dedicated Standalone Admin API. Changes overwrite the entire configuration and take effect immediately without requiring a reboot, as it is hot updated. +API-driven mode is an emerging paradigm for standalone deployment, where routing rules are stored entirely in memory rather than in a configuration file. Updates must be made through the dedicated Standalone Admin API. Each update replaces the full configuration and takes effect immediately through hot updates, without requiring a restart. ##### Configuration @@ -160,7 +160,7 @@ This disables the local file source of configuration in favor of the API. When A ##### API Endpoints -* Per-resource-type conf_version +* `conf_version` by resource type Use `_conf_version` to indicate the client’s current version for each resource type (e.g. routes, upstreams, services, etc.). @@ -173,7 +173,7 @@ This disables the local file source of configuration in favor of the API. When A } ``` - APISIX compares each provided `_conf_version` against its in-memory `_conf_version` for that resource type: + APISIX compares each provided `_conf_version` against its in-memory `_conf_version` for that resource type. If the provided `_conf_version` is: - **Greater than** the current `conf_version` If your `_conf_version` is **higher**, APISIX will **rebuild/reset** that resource type’s data to match your payload. @@ -184,11 +184,11 @@ This disables the local file source of configuration in favor of the API. When A - **Less than** the current `conf_version` If it is **lower**, APISIX considers your update **stale** and **rejects** the request for that resource type with a **400 Bad Request**. -* modifiedIndex per resource +* `modifiedIndex` by individual resource - Allow setting an index for each resource, APISIX compares it to its modifiedIndex to determine whether to accept the update. + Allow setting an index for each resource. APISIX compares this index to its modifiedIndex to determine whether to accept the update. -**Example:** +##### Example 1. get configuration @@ -235,7 +235,7 @@ In APISIX memory, the current configuration is: "upstreams_conf_version": 1000, } -Due `upstreams_conf_version: 1001` > `upstreams_conf_version: 1000`, APISIX will update the upstreams configuration: +Update the previous upstreams configuration by setting a higher version number, such as 1001, to replace the current version 1000: ```shell curl -X PUT http://127.0.0.1:9180/apisix/admin/configs \ From 5d5f25e78ac6e8c78041529dd0b49476017cea8b Mon Sep 17 00:00:00 2001 From: AlinsRan Date: Tue, 20 May 2025 11:07:25 +0800 Subject: [PATCH 09/11] Update t/admin/standalone.spec.ts Co-authored-by: Zeping Bai --- t/admin/standalone.spec.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/t/admin/standalone.spec.ts b/t/admin/standalone.spec.ts index fb4ae0a858f3..911fbfea1797 100644 --- a/t/admin/standalone.spec.ts +++ b/t/admin/standalone.spec.ts @@ -249,6 +249,7 @@ describe("Admin - Standalone", () => { expect(resp2.status).toEqual(202); // Check route /r1 exists + // But it is not applied because the modifiedIndex is the same as the old value const resp2_2 = await client.get("/r1"); expect(resp2_2.status).toEqual(200); From dc149f855b9e5849c2c224b0536b5783b5bd6c54 Mon Sep 17 00:00:00 2001 From: AlinsRan Date: Tue, 20 May 2025 03:13:40 +0000 Subject: [PATCH 10/11] fix doc --- docs/en/latest/deployment-modes.md | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/docs/en/latest/deployment-modes.md b/docs/en/latest/deployment-modes.md index 17693f6ad030..70444da5da16 100644 --- a/docs/en/latest/deployment-modes.md +++ b/docs/en/latest/deployment-modes.md @@ -153,7 +153,7 @@ To enable this mode, set the APISIX role to `traditional` (to start both the API deployment: role: traditional role_traditional: - config_provider: yamls + config_provider: yaml ``` This disables the local file source of configuration in favor of the API. When APISIX starts, it uses an empty configuration until updated via the API. @@ -175,14 +175,11 @@ This disables the local file source of configuration in favor of the API. When A APISIX compares each provided `_conf_version` against its in-memory `_conf_version` for that resource type. If the provided `_conf_version` is: - - **Greater than** the current `conf_version` - If your `_conf_version` is **higher**, APISIX will **rebuild/reset** that resource type’s data to match your payload. + - **Greater than** the current `conf_version`, APISIX will **rebuild/reset** that resource type’s data to match your payload. - - **Equal to** the current `conf_version` - If it is **equal**, APISIX treats the resource as **unchanged** and **ignores** it (no data is rebuilt). + - **Equal to** the current `conf_version`, APISIX treats the resource as **unchanged** and **ignores** it (no data is rebuilt). - - **Less than** the current `conf_version` - If it is **lower**, APISIX considers your update **stale** and **rejects** the request for that resource type with a **400 Bad Request**. + - **Less than** the current `conf_version`, APISIX considers your update **stale** and **rejects** the request for that resource type with a **400 Bad Request**. * `modifiedIndex` by individual resource @@ -234,7 +231,7 @@ In APISIX memory, the current configuration is: "routes_conf_version": 1000, "upstreams_conf_version": 1000, } - +``` Update the previous upstreams configuration by setting a higher version number, such as 1001, to replace the current version 1000: ```shell From 72682c7ad139cf49cb66a8fd106e9c1bd852ddfb Mon Sep 17 00:00:00 2001 From: AlinsRan Date: Tue, 20 May 2025 03:26:35 +0000 Subject: [PATCH 11/11] add blank lines for doc --- docs/en/latest/deployment-modes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/latest/deployment-modes.md b/docs/en/latest/deployment-modes.md index 70444da5da16..4e916ae148a1 100644 --- a/docs/en/latest/deployment-modes.md +++ b/docs/en/latest/deployment-modes.md @@ -232,6 +232,7 @@ In APISIX memory, the current configuration is: "upstreams_conf_version": 1000, } ``` + Update the previous upstreams configuration by setting a higher version number, such as 1001, to replace the current version 1000: ```shell