You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@apisix.apache.org by we...@apache.org on 2020/11/15 09:21:04 UTC

[apisix] branch master updated: feat: generate create/update_time automatically (#2740)

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

wenming 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 da4e12e  feat: generate create/update_time automatically (#2740)
da4e12e is described below

commit da4e12e0f7da9b6c1d03c1ce755f2ef227cbda0f
Author: 罗泽轩 <sp...@gmail.com>
AuthorDate: Sun Nov 15 17:20:51 2020 +0800

    feat: generate create/update_time automatically (#2740)
---
 apisix/admin/consumers.lua |   7 +++
 apisix/admin/routes.lua    |  10 ++++
 apisix/admin/services.lua  |  10 ++++
 apisix/admin/ssl.lua       |  10 ++++
 apisix/admin/upstreams.lua |  10 ++++
 apisix/admin/utils.lua     |  60 +++++++++++++++++++
 doc/admin-api.md           |  20 +++----
 doc/zh-cn/admin-api.md     |  20 +++----
 t/admin/consumers.t        |  13 +++++
 t/admin/routes.t           |  28 +++++++++
 t/admin/services.t         |  28 +++++++++
 t/admin/ssl.t              |  80 ++++++++++++++++++++++++++
 t/admin/upstream.t         | 140 ++++++++++++++++++++++++++++++++++++++++++++-
 13 files changed, 414 insertions(+), 22 deletions(-)

diff --git a/apisix/admin/consumers.lua b/apisix/admin/consumers.lua
index 11e21f9..1addea7 100644
--- a/apisix/admin/consumers.lua
+++ b/apisix/admin/consumers.lua
@@ -16,6 +16,7 @@
 --
 local core    = require("apisix.core")
 local plugins = require("apisix.admin.plugins")
+local utils   = require("apisix.admin.utils")
 local plugin  = require("apisix.plugin")
 local pairs   = pairs
 
@@ -76,6 +77,12 @@ function _M.put(consumer_name, conf)
 
     local key = "/consumers/" .. consumer_name
     core.log.info("key: ", key)
+
+    local ok, err = utils.inject_conf_with_prev_conf("consumer", key, conf)
+    if not ok then
+        return 500, {error_msg = err}
+    end
+
     local res, err = core.etcd.set(key, conf)
     if not res then
         core.log.error("failed to put consumer[", key, "]: ", err)
diff --git a/apisix/admin/routes.lua b/apisix/admin/routes.lua
index 85fdfdf..a375655 100644
--- a/apisix/admin/routes.lua
+++ b/apisix/admin/routes.lua
@@ -17,6 +17,7 @@
 local core = require("apisix.core")
 local schema_plugin = require("apisix.admin.plugins").check_schema
 local upstreams = require("apisix.admin.upstreams")
+local utils = require("apisix.admin.utils")
 local tostring = tostring
 local type = type
 local loadstring = loadstring
@@ -147,6 +148,12 @@ function _M.put(id, conf, sub_path, args)
     end
 
     local key = "/routes/" .. id
+
+    local ok, err = utils.inject_conf_with_prev_conf("route", key, conf)
+    if not ok then
+        return 500, {error_msg = err}
+    end
+
     local res, err = core.etcd.set(key, conf, args.ttl)
     if not res then
         core.log.error("failed to put route[", key, "] to etcd: ", err)
@@ -181,6 +188,7 @@ function _M.post(id, conf, sub_path, args)
 
     local key = "/routes"
     -- core.log.info("key: ", key)
+    utils.inject_timestamp(conf)
     local res, err = core.etcd.push("/routes", conf, args.ttl)
     if not res then
         core.log.error("failed to post route[", key, "] to etcd: ", err)
@@ -253,6 +261,8 @@ function _M.patch(id, conf, sub_path, args)
         node_value = core.table.merge(node_value, conf);
     end
 
+    utils.inject_timestamp(node_value, nil, conf)
+
     core.log.info("new conf: ", core.json.delay_encode(node_value, true))
 
     local id, err = check_conf(id, node_value, true)
diff --git a/apisix/admin/services.lua b/apisix/admin/services.lua
index 7d0540b..13347b9 100644
--- a/apisix/admin/services.lua
+++ b/apisix/admin/services.lua
@@ -18,6 +18,7 @@ local core = require("apisix.core")
 local get_routes = require("apisix.router").http_routes
 local schema_plugin = require("apisix.admin.plugins").check_schema
 local upstreams = require("apisix.admin.upstreams")
+local utils = require("apisix.admin.utils")
 local tostring = tostring
 local ipairs = ipairs
 local type = type
@@ -116,6 +117,12 @@ function _M.put(id, conf)
 
     local key = "/services/" .. id
     core.log.info("key: ", key)
+
+    local ok, err = utils.inject_conf_with_prev_conf("service", key, conf)
+    if not ok then
+        return 500, {error_msg = err}
+    end
+
     local res, err = core.etcd.set(key, conf)
     if not res then
         core.log.error("failed to put service[", key, "]: ", err)
@@ -149,6 +156,7 @@ function _M.post(id, conf)
     end
 
     local key = "/services"
+    utils.inject_timestamp(conf)
     local res, err = core.etcd.push(key, conf)
     if not res then
         core.log.error("failed to post service[", key, "]: ", err)
@@ -231,6 +239,8 @@ function _M.patch(id, conf, sub_path)
         node_value = core.table.merge(node_value, conf);
     end
 
+    utils.inject_timestamp(node_value, nil, conf)
+
     core.log.info("new value ", core.json.delay_encode(node_value, true))
 
     local id, err = check_conf(id, node_value, true)
diff --git a/apisix/admin/ssl.lua b/apisix/admin/ssl.lua
index d62178d..668f9ca 100644
--- a/apisix/admin/ssl.lua
+++ b/apisix/admin/ssl.lua
@@ -15,6 +15,7 @@
 -- limitations under the License.
 --
 local core              = require("apisix.core")
+local utils             = require("apisix.admin.utils")
 local tostring          = tostring
 local aes               = require "resty.aes"
 local ngx_encode_base64 = ngx.encode_base64
@@ -103,6 +104,12 @@ function _M.put(id, conf)
     end
 
     local key = "/ssl/" .. id
+
+    local ok, err = utils.inject_conf_with_prev_conf("ssl", key, conf)
+    if not ok then
+        return 500, {error_msg = err}
+    end
+
     local res, err = core.etcd.set(key, conf)
     if not res then
         core.log.error("failed to put ssl[", key, "]: ", err)
@@ -151,6 +158,7 @@ function _M.post(id, conf)
 
     local key = "/ssl"
     -- core.log.info("key: ", key)
+    utils.inject_timestamp(conf)
     local res, err = core.etcd.push("/ssl", conf)
     if not res then
         core.log.error("failed to post ssl[", key, "]: ", err)
@@ -214,6 +222,8 @@ function _M.patch(id, conf)
 
     node_value = core.table.merge(node_value, conf);
 
+    utils.inject_timestamp(node_value, nil, conf)
+
     core.log.info("new ssl conf: ", core.json.delay_encode(node_value, true))
 
     local id, err = check_conf(id, node_value, true)
diff --git a/apisix/admin/upstreams.lua b/apisix/admin/upstreams.lua
index b454d89..b1f862d 100644
--- a/apisix/admin/upstreams.lua
+++ b/apisix/admin/upstreams.lua
@@ -17,6 +17,7 @@
 local core = require("apisix.core")
 local get_routes = require("apisix.router").http_routes
 local get_services = require("apisix.http.service").services
+local utils = require("apisix.admin.utils")
 local tostring = tostring
 local ipairs = ipairs
 local type = type
@@ -135,6 +136,12 @@ function _M.put(id, conf)
 
     local key = "/upstreams/" .. id
     core.log.info("key: ", key)
+
+    local ok, err = utils.inject_conf_with_prev_conf("upstream", key, conf)
+    if not ok then
+        return 500, {error_msg = err}
+    end
+
     local res, err = core.etcd.set(key, conf)
     if not res then
         core.log.error("failed to put upstream[", key, "]: ", err)
@@ -168,6 +175,7 @@ function _M.post(id, conf)
     end
 
     local key = "/upstreams"
+    utils.inject_timestamp(conf)
     local res, err = core.etcd.push(key, conf)
     if not res then
         core.log.error("failed to post upstream[", key, "]: ", err)
@@ -265,6 +273,8 @@ function _M.patch(id, conf, sub_path)
         new_value = core.table.merge(new_value, conf);
     end
 
+    utils.inject_timestamp(new_value, nil, conf)
+
     core.log.info("new value ", core.json.delay_encode(new_value, true))
 
     local id, err = check_conf(id, new_value, true)
diff --git a/apisix/admin/utils.lua b/apisix/admin/utils.lua
new file mode 100644
index 0000000..4c0694a
--- /dev/null
+++ b/apisix/admin/utils.lua
@@ -0,0 +1,60 @@
+--
+-- 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.
+--
+local core    = require("apisix.core")
+local ngx_time = ngx.time
+
+
+local _M = {}
+
+
+local function inject_timestamp(conf, prev_conf, patch_conf)
+    if not conf.create_time then
+        if prev_conf and prev_conf.node.value.create_time then
+            conf.create_time = prev_conf.node.value.create_time
+        end
+
+        -- As we don't know existent data's create_time, we have to pretend
+        -- they are created now.
+        conf.create_time = ngx_time()
+    end
+
+    -- For PATCH request, the modification is passed as 'patch_conf'
+    if not conf.update_time or (patch_conf and patch_conf.update_time == nil) then
+        conf.update_time = ngx_time()
+    end
+end
+_M.inject_timestamp = inject_timestamp
+
+
+function _M.inject_conf_with_prev_conf(kind, key, conf)
+    local res, err = core.etcd.get(key)
+    if not res or (res.status ~= 200 and res.status ~= 404) then
+        core.log.error("failed to get " .. kind .. "[", key, "] from etcd: ", err or res.status)
+        return nil, err
+    end
+
+    if res.status == 404 then
+        inject_timestamp(conf)
+    else
+        inject_timestamp(conf, res.body)
+    end
+
+    return true
+end
+
+
+return _M
diff --git a/doc/admin-api.md b/doc/admin-api.md
index 94b9fb0..6f8b146 100644
--- a/doc/admin-api.md
+++ b/doc/admin-api.md
@@ -72,8 +72,8 @@
 |service_protocol|False|Upstream protocol type|only `grpc` and `http` are supported|`http` is the default value; Must set `grpc` if using `gRPC proxy` or `gRPC transcode`|
 |labels   |False |Match Rules|Key/value pairs to specify attributes|{"version":"v2","build":"16","env":"production"}|
 |enable_websocket|False|Auxiliary| enable `websocket`(boolean), default `false`.||
-|create_time|False| Auxiliary|epoch timestamp in second| 1602883670|
-|update_time|False| Auxiliary|epoch timestamp in second| 1602883670|
+|create_time|False| Auxiliary|epoch timestamp in second, will be created automatically if missing | 1602883670|
+|update_time|False| Auxiliary|epoch timestamp in second, will be created automatically if missing | 1602883670|
 
 For the same type of parameters, such as `host` and `hosts`, `remote_addr` and `remote_addrs` cannot exist at the same time, only one of them can be selected. If enabled at the same time, the API will response an error.
 
@@ -295,8 +295,8 @@ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f13
 |desc     |False |Auxiliary   |service usage scenarios, and more.|customer xxxx|
 |labels   |False |Match Rules|Key/value pairs to specify attributes|{"version":"v2","build":"16","env":"production"}|
 |enable_websocket|False|Auxiliary| enable `websocket`(boolean), default `false`.||
-|create_time|False| Auxiliary|epoch timestamp in second| 1602883670|
-|update_time|False| Auxiliary|epoch timestamp in second| 1602883670|
+|create_time|False| Auxiliary|epoch timestamp in second, will be created automatically if missing | 1602883670|
+|update_time|False| Auxiliary|epoch timestamp in second, will be created automatically if missing | 1602883670|
 
 Config Example:
 
@@ -437,8 +437,8 @@ Return response from etcd currently.
 |plugins  |False |Plugin|See [Plugin](architecture-design.md#plugin) for more ||
 |desc     |False |Auxiliary   |Identifies route names, usage scenarios, and more.|customer xxxx|
 |labels   |False |Match Rules|Key/value pairs to specify attributes|{"version":"v2","build":"16","env":"production"}|
-|create_time|False| Auxiliary|epoch timestamp in second| 1602883670|
-|update_time|False| Auxiliary|epoch timestamp in second| 1602883670|
+|create_time|False| Auxiliary|epoch timestamp in second, will be created automatically if missing | 1602883670|
+|update_time|False| Auxiliary|epoch timestamp in second, will be created automatically if missing | 1602883670|
 
 Config Example:
 
@@ -520,8 +520,8 @@ In addition to the basic complex equalization algorithm selection, APISIX's Upst
 |pass_host            |optional|`pass` pass the client request host, `node` not pass the client request host, using the upstream node host, `rewrite` rewrite host by the configured `upstream_host`.|
 |upstream_host    |optional|This option is only valid if the `pass_host` is `rewrite`.|
 |labels|optional |Key/value pairs to specify attributes|{"version":"v2","build":"16","env":"production"}|
-|create_time|optional| epoch timestamp in second, like `1602883670`|
-|update_time|optional| epoch timestamp in second, like `1602883670`|
+|create_time|optional| epoch timestamp in second, like `1602883670`, will be created automatically if missing|
+|update_time|optional| epoch timestamp in second, like `1602883670`, will be created automatically if missing|
 
 
 Config Example:
@@ -664,8 +664,8 @@ Return response from etcd currently.
 |key|True|Private key|https Private key||
 |sni|True|Match Rules|https SNI||
 |labels|False |Match Rules|Key/value pairs to specify attributes|{"version":"v2","build":"16","env":"production"}|
-|create_time|False| Auxiliary|epoch timestamp in second| 1602883670|
-|update_time|False| Auxiliary|epoch timestamp in second| 1602883670|
+|create_time|False| Auxiliary|epoch timestamp in second, will be created automatically if missing | 1602883670|
+|update_time|False| Auxiliary|epoch timestamp in second, will be created automatically if missing | 1602883670|
 
 Config Example:
 
diff --git a/doc/zh-cn/admin-api.md b/doc/zh-cn/admin-api.md
index 9df4de4..a8bb27a 100644
--- a/doc/zh-cn/admin-api.md
+++ b/doc/zh-cn/admin-api.md
@@ -76,8 +76,8 @@
 |filter_func|可选|匹配规则|用户自定义的过滤函数。可以使用它来实现特殊场景的匹配要求实现。该函数默认接受一个名为 vars 的输入参数,可以用它来获取 Nginx 变量。|function(vars) return vars["arg_name"] == "json" end|
 |labels   |可选 |匹配规则|标识附加属性的键值对|{"version":"v2","build":"16","env":"production"}|
 |enable_websocket|可选 |辅助| 是否启用 `websocket`(boolean), 缺省 `false`.||
-|create_time|可选|辅助|单位为秒的 epoch 时间戳|1602883670|
-|update_time|可选|辅助|单位为秒的 epoch 时间戳|1602883670|
+|create_time|可选|辅助|单位为秒的 epoch 时间戳,如果不指定则自动创建|1602883670|
+|update_time|可选|辅助|单位为秒的 epoch 时间戳,如果不指定则自动创建|1602883670|
 
 有两点需要特别注意:
 
@@ -305,8 +305,8 @@ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f13
 |desc     |可选 |辅助   |服务描述、使用场景等。||
 |labels   |可选 |匹配规则|标识附加属性的键值对|{"version":"v2","build":"16","env":"production"}|
 |enable_websocket|可选 |辅助| 是否启用 `websocket`(boolean), 缺省 `false`.||
-|create_time|可选|辅助|单位为秒的 epoch 时间戳|1602883670|
-|update_time|可选|辅助|单位为秒的 epoch 时间戳|1602883670|
+|create_time|可选|辅助|单位为秒的 epoch 时间戳,如果不指定则自动创建|1602883670|
+|update_time|可选|辅助|单位为秒的 epoch 时间戳,如果不指定则自动创建|1602883670|
 
 serivce 对象 json 配置内容:
 
@@ -450,8 +450,8 @@ HTTP/1.1 200 OK
 |plugins|可选|Plugin|该 Consumer 对应的插件配置,它的优先级是最高的:Consumer > Route > Service。对于具体插件配置,可以参考 [Plugins](#plugin) 章节。||
 |desc     |可选 |辅助|consumer描述||
 |labels   |可选 |匹配规则|标识附加属性的键值对|{"version":"v2","build":"16","env":"production"}|
-|create_time|可选|辅助|单位为秒的 epoch 时间戳|1602883670|
-|update_time|可选|辅助|单位为秒的 epoch 时间戳|1602883670|
+|create_time|可选|辅助|单位为秒的 epoch 时间戳,如果不指定则自动创建|1602883670|
+|update_time|可选|辅助|单位为秒的 epoch 时间戳,如果不指定则自动创建|1602883670|
 
 consumer 对象 json 配置内容:
 
@@ -534,8 +534,8 @@ APISIX 的 Upstream 除了基本的复杂均衡算法选择外,还支持对上
 |pass_host            |可选|枚举|`pass` 透传客户端请求的 host, `node` 不透传客户端请求的 host, 使用 upstream node 配置的 host, `rewrite` 使用 `upstream_host` 配置的值重写 host 。||
 |upstream_host    |可选|辅助|只在 `pass_host` 配置为 `rewrite` 时有效。||
 |labels   |可选 |匹配规则|标识附加属性的键值对|{"version":"v2","build":"16","env":"production"}|
-|create_time|可选|辅助|单位为秒的 epoch 时间戳|1602883670|
-|update_time|可选|辅助|单位为秒的 epoch 时间戳|1602883670|
+|create_time|可选|辅助|单位为秒的 epoch 时间戳,如果不指定则自动创建|1602883670|
+|update_time|可选|辅助|单位为秒的 epoch 时间戳,如果不指定则自动创建|1602883670|
 
 upstream 对象 json 配置内容:
 
@@ -676,8 +676,8 @@ HTTP/1.1 200 OK
 |key|必需|私钥|https 证书私钥||
 |sni|必需|匹配规则|https 证书SNI||
 |labels|可选|匹配规则|标识附加属性的键值对|{"version":"v2","build":"16","env":"production"}|
-|create_time|可选|辅助|单位为秒的 epoch 时间戳|1602883670|
-|update_time|可选|辅助|单位为秒的 epoch 时间戳|1602883670|
+|create_time|可选|辅助|单位为秒的 epoch 时间戳,如果不指定则自动创建|1602883670|
+|update_time|可选|辅助|单位为秒的 epoch 时间戳,如果不指定则自动创建|1602883670|
 
 ssl 对象 json 配置内容:
 
diff --git a/t/admin/consumers.t b/t/admin/consumers.t
index e621f62..2e48225 100644
--- a/t/admin/consumers.t
+++ b/t/admin/consumers.t
@@ -66,6 +66,13 @@ passed
     location /t {
         content_by_lua_block {
             local t = require("lib.test_admin").test
+            local etcd = require("apisix.core.etcd")
+            local res = assert(etcd.get('/consumers/jack'))
+            local prev_create_time = res.body.node.value.create_time
+            assert(prev_create_time ~= nil, "create_time is nil")
+            local update_time = res.body.node.value.update_time
+            assert(update_time ~= nil, "update_time is nil")
+
             local code, body = t('/apisix/admin/consumers',
                  ngx.HTTP_PUT,
                  [[{
@@ -95,6 +102,12 @@ passed
 
             ngx.status = code
             ngx.say(body)
+
+            local res = assert(etcd.get('/consumers/jack'))
+            local create_time = res.body.node.value.create_time
+            assert(prev_create_time == create_time, "create_time mismatched")
+            local update_time = res.body.node.value.update_time
+            assert(update_time ~= nil, "update_time is nil")
         }
     }
 --- request
diff --git a/t/admin/routes.t b/t/admin/routes.t
index 799e6bf..202545f 100644
--- a/t/admin/routes.t
+++ b/t/admin/routes.t
@@ -173,6 +173,7 @@ GET /t
     location /t {
         content_by_lua_block {
             local t = require("lib.test_admin").test
+            local etcd = require("apisix.core.etcd")
             local code, message, res = t('/apisix/admin/routes',
                  ngx.HTTP_POST,
                  [[{
@@ -213,6 +214,12 @@ GET /t
             ngx.say("[push] code: ", code, " message: ", message)
 
             local id = string.sub(res.node.key, #"/apisix/routes/" + 1)
+            local res = assert(etcd.get('/routes/' .. id))
+            local create_time = res.body.node.value.create_time
+            assert(create_time ~= nil, "create_time is nil")
+            local update_time = res.body.node.value.update_time
+            assert(update_time ~= nil, "update_time is nil")
+
             code, message = t('/apisix/admin/routes/' .. id,
                  ngx.HTTP_DELETE,
                  nil,
@@ -239,6 +246,7 @@ GET /t
         content_by_lua_block {
             local core = require("apisix.core")
             local t = require("lib.test_admin").test
+            local etcd = require("apisix.core.etcd")
             local code, message, res = t('/apisix/admin/routes/1',
                  ngx.HTTP_PUT,
                  [[{
@@ -273,6 +281,12 @@ GET /t
             end
 
             ngx.say("[push] code: ", code, " message: ", message)
+
+            local res = assert(etcd.get('/routes/1'))
+            local create_time = res.body.node.value.create_time
+            assert(create_time ~= nil, "create_time is nil")
+            local update_time = res.body.node.value.update_time
+            assert(update_time ~= nil, "update_time is nil")
         }
     }
 --- request
@@ -997,6 +1011,14 @@ passed
     location /t {
         content_by_lua_block {
             local t = require("lib.test_admin").test
+            local etcd = require("apisix.core.etcd")
+
+            local id = 1
+            local res = assert(etcd.get('/routes/' .. id))
+            local prev_create_time = res.body.node.value.create_time
+            local prev_update_time = res.body.node.value.update_time
+            ngx.sleep(1)
+
             local code, body = t('/apisix/admin/routes/1',
                 ngx.HTTP_PATCH,
                 [[{
@@ -1015,6 +1037,12 @@ passed
 
             ngx.status = code
             ngx.say(body)
+
+            local res = assert(etcd.get('/routes/' .. id))
+            local create_time = res.body.node.value.create_time
+            assert(prev_create_time == create_time, "create_time mismatched")
+            local update_time = res.body.node.value.update_time
+            assert(prev_update_time ~= update_time, "update_time should be changed")
         }
     }
 --- request
diff --git a/t/admin/services.t b/t/admin/services.t
index 8d4d269..fea6687 100644
--- a/t/admin/services.t
+++ b/t/admin/services.t
@@ -31,6 +31,7 @@ __DATA__
     location /t {
         content_by_lua_block {
             local t = require("lib.test_admin").test
+            local etcd = require("apisix.core.etcd")
             local code, body = t('/apisix/admin/services/1',
                  ngx.HTTP_PUT,
                  [[{
@@ -61,6 +62,12 @@ __DATA__
 
             ngx.status = code
             ngx.say(body)
+
+            local res = assert(etcd.get('/services/1'))
+            local create_time = res.body.node.value.create_time
+            assert(create_time ~= nil, "create_time is nil")
+            local update_time = res.body.node.value.update_time
+            assert(update_time ~= nil, "update_time is nil")
         }
     }
 --- request
@@ -164,6 +171,7 @@ GET /t
     location /t {
         content_by_lua_block {
             local t = require("lib.test_admin").test
+            local etcd = require("apisix.core.etcd")
             local code, message, res = t('/apisix/admin/services',
                  ngx.HTTP_POST,
                  [[{
@@ -198,6 +206,12 @@ GET /t
             ngx.say("[push] code: ", code, " message: ", message)
 
             local id = string.sub(res.node.key, #"/apisix/services/" + 1)
+            local res = assert(etcd.get('/services/' .. id))
+            local create_time = res.body.node.value.create_time
+            assert(create_time ~= nil, "create_time is nil")
+            local update_time = res.body.node.value.update_time
+            assert(update_time ~= nil, "update_time is nil")
+
             code, message = t('/apisix/admin/services/' .. id,
                  ngx.HTTP_DELETE,
                  nil,
@@ -633,6 +647,14 @@ GET /t
     location /t {
         content_by_lua_block {
             local t = require("lib.test_admin").test
+            local etcd = require("apisix.core.etcd")
+
+            local id = 1
+            local res = assert(etcd.get('/services/' .. id))
+            local prev_create_time = res.body.node.value.create_time
+            local prev_update_time = res.body.node.value.update_time
+            ngx.sleep(1)
+
             local code, body = t('/apisix/admin/services/1',
                 ngx.HTTP_PATCH,
                 [[{
@@ -663,6 +685,12 @@ GET /t
 
             ngx.status = code
             ngx.say(body)
+
+            local res = assert(etcd.get('/services/' .. id))
+            local create_time = res.body.node.value.create_time
+            assert(prev_create_time == create_time, "create_time mismatched")
+            local update_time = res.body.node.value.update_time
+            assert(prev_update_time ~= update_time, "update_time should be changed")
         }
     }
 --- request
diff --git a/t/admin/ssl.t b/t/admin/ssl.t
index a088193..c8bda4c 100644
--- a/t/admin/ssl.t
+++ b/t/admin/ssl.t
@@ -27,6 +27,7 @@ __DATA__
     location /t {
         content_by_lua_block {
             local core = require("apisix.core")
+            local etcd = require("apisix.core.etcd")
             local t = require("lib.test_admin")
 
             local ssl_cert = t.read_file("conf/cert/apisix.crt")
@@ -49,6 +50,13 @@ __DATA__
 
             ngx.status = code
             ngx.say(body)
+
+            local res = assert(etcd.get('/ssl/1'))
+            local prev_create_time = res.body.node.value.create_time
+            assert(prev_create_time ~= nil, "create_time is nil")
+            local update_time = res.body.node.value.update_time
+            assert(update_time ~= nil, "update_time is nil")
+
         }
     }
 --- request
@@ -691,3 +699,75 @@ GET /t
 [delete] code: 200 message: passed
 --- no_error_log
 [error]
+
+
+
+=== TEST 19: create/patch ssl
+--- config
+    location /t {
+        content_by_lua_block {
+            local core = require("apisix.core")
+            local etcd = require("apisix.core.etcd")
+            local t = require("lib.test_admin")
+
+            local ssl_cert = t.read_file("conf/cert/apisix.crt")
+            local ssl_key =  t.read_file("conf/cert/apisix.key")
+            local data = {cert = ssl_cert, key = ssl_key, sni = "test.com"}
+
+            local code, body, res = t.test('/apisix/admin/ssl',
+                ngx.HTTP_POST,
+                core.json.encode(data),
+                [[{
+                    "node": {
+                        "value": {
+                            "sni": "test.com"
+                        }
+                    },
+                    "action": "create"
+                }]]
+                )
+
+            if code ~= 200 then
+                ngx.status = code
+                ngx.say(body)
+                return
+            end
+
+            local id = string.sub(res.node.key, #"/apisix/ssl/" + 1)
+            local res = assert(etcd.get('/ssl/' .. id))
+            local prev_create_time = res.body.node.value.create_time
+            assert(prev_create_time ~= nil, "create_time is nil")
+            local update_time = res.body.node.value.update_time
+            assert(update_time ~= nil, "update_time is nil")
+
+            local code, body = t.test('/apisix/admin/ssl/' .. id,
+                ngx.HTTP_PATCH,
+                core.json.encode({create_time = 0, update_time = 1})
+                )
+
+            if code ~= 201 then
+                ngx.status = code
+                ngx.say(body)
+                return
+            end
+
+            local res = assert(etcd.get('/ssl/' .. id))
+            local create_time = res.body.node.value.create_time
+            assert(create_time == 0, "create_time mismatched")
+            local update_time = res.body.node.value.update_time
+            assert(update_time == 1, "update_time mismatched")
+
+            -- clean up
+            local code, body = t.test('/apisix/admin/ssl/' .. id,
+                ngx.HTTP_DELETE
+            )
+            ngx.status = code
+            ngx.say(body)
+        }
+    }
+--- request
+GET /t
+--- response_body
+passed
+--- no_error_log
+[error]
diff --git a/t/admin/upstream.t b/t/admin/upstream.t
index d8b883e..4467bab 100644
--- a/t/admin/upstream.t
+++ b/t/admin/upstream.t
@@ -31,6 +31,7 @@ __DATA__
     location /t {
         content_by_lua_block {
             local t = require("lib.test_admin").test
+            local etcd = require("apisix.core.etcd")
             local code, body = t('/apisix/admin/upstreams/1',
                 ngx.HTTP_PUT,
                 [[{
@@ -57,6 +58,12 @@ __DATA__
 
             ngx.status = code
             ngx.say(body)
+
+            local res = assert(etcd.get('/upstreams/1'))
+            local create_time = res.body.node.value.create_time
+            assert(create_time ~= nil, "create_time is nil")
+            local update_time = res.body.node.value.update_time
+            assert(update_time ~= nil, "update_time is nil")
         }
     }
 --- request
@@ -158,6 +165,7 @@ GET /t
     location /t {
         content_by_lua_block {
             local t = require("lib.test_admin").test
+            local etcd = require("apisix.core.etcd")
             local code, message, res = t('/apisix/admin/upstreams',
                  ngx.HTTP_POST,
                  [[{
@@ -188,6 +196,12 @@ GET /t
             ngx.say("[push] code: ", code, " message: ", message)
 
             local id = string.sub(res.node.key, #"/apisix/upstreams/" + 1)
+            local res = assert(etcd.get('/upstreams/' .. id))
+            local create_time = res.body.node.value.create_time
+            assert(create_time ~= nil, "create_time is nil")
+            local update_time = res.body.node.value.update_time
+            assert(update_time ~= nil, "update_time is nil")
+
             code, message = t('/apisix/admin/upstreams/' .. id,
                  ngx.HTTP_DELETE,
                  nil,
@@ -652,6 +666,14 @@ GET /t
     location /t {
         content_by_lua_block {
             local t = require("lib.test_admin").test
+            local etcd = require("apisix.core.etcd")
+
+            local id = 1
+            local res = assert(etcd.get('/upstreams/' .. id))
+            local prev_create_time = res.body.node.value.create_time
+            local prev_update_time = res.body.node.value.update_time
+            ngx.sleep(1)
+
             local code, body = t('/apisix/admin/upstreams/1',
                 ngx.HTTP_PATCH,
                 [[{
@@ -678,6 +700,12 @@ GET /t
 
             ngx.status = code
             ngx.say(body)
+
+            local res = assert(etcd.get('/upstreams/' .. id))
+            local create_time = res.body.node.value.create_time
+            assert(prev_create_time == create_time, "create_time mismatched")
+            local update_time = res.body.node.value.update_time
+            assert(prev_update_time ~= update_time, "update_time should be changed")
         }
     }
 --- request
@@ -1806,7 +1834,115 @@ GET /t
 
 
 
-=== TEST 54: create upstream with create_time and update_time(id: 1)
+=== TEST 54: patch upstream(whole, create_time)
+--- config
+    location /t {
+        content_by_lua_block {
+            local t = require("lib.test_admin").test
+            local etcd = require("apisix.core.etcd")
+
+            local code, body = t('/apisix/admin/upstreams/1',
+                ngx.HTTP_PATCH,
+                [[{
+                    "nodes": {
+                        "127.0.0.1:8080": 1
+                    },
+                    "type": "roundrobin",
+                    "desc": "new upstream",
+                    "create_time": 1705252779
+                }]],
+                [[{
+                    "node": {
+                        "value": {
+                            "nodes": {
+                                "127.0.0.1:8080": 1
+                            },
+                            "type": "roundrobin",
+                            "desc": "new upstream",
+                            "create_time": 1705252779
+                        },
+                        "key": "/apisix/upstreams/1"
+                    },
+                    "action": "compareAndSwap"
+                }]]
+            )
+
+            ngx.status = code
+            ngx.say(body)
+
+            if code >= 300 then
+                return
+            end
+
+            local res = assert(etcd.get('/upstreams/1'))
+            local create_time = res.body.node.value.create_time
+            assert(create_time == 1705252779, "create_time mismatched")
+        }
+    }
+--- request
+GET /t
+--- response_body
+passed
+--- no_error_log
+[error]
+
+
+
+=== TEST 55: patch upstream(whole, update_time)
+--- config
+    location /t {
+        content_by_lua_block {
+            local t = require("lib.test_admin").test
+            local etcd = require("apisix.core.etcd")
+
+            local code, body = t('/apisix/admin/upstreams/1',
+                ngx.HTTP_PATCH,
+                [[{
+                    "nodes": {
+                        "127.0.0.1:8080": 1
+                    },
+                    "type": "roundrobin",
+                    "desc": "new upstream",
+                    "update_time": 1705252779
+                }]],
+                [[{
+                    "node": {
+                        "value": {
+                            "nodes": {
+                                "127.0.0.1:8080": 1
+                            },
+                            "type": "roundrobin",
+                            "desc": "new upstream",
+                            "create_time": 1705252779
+                        },
+                        "key": "/apisix/upstreams/1"
+                    },
+                    "action": "compareAndSwap"
+                }]]
+            )
+
+            ngx.status = code
+            ngx.say(body)
+
+            if code >= 300 then
+                return
+            end
+
+            local res = assert(etcd.get('/upstreams/1'))
+            local update_time = res.body.node.value.update_time
+            assert(update_time == 1705252779, "update_time mismatched")
+        }
+    }
+--- request
+GET /t
+--- response_body
+passed
+--- no_error_log
+[error]
+
+
+
+=== TEST 56: create upstream with create_time and update_time(id: 1)
 --- config
     location /t {
         content_by_lua_block {
@@ -1850,7 +1986,7 @@ passed
 
 
 
-=== TEST 55: delete test upstream(id: 1)
+=== TEST 57: delete test upstream(id: 1)
 --- config
     location /t {
         content_by_lua_block {