AlinsRan commented on PR #13686:
URL: https://github.com/apache/apisix/pull/13686#issuecomment-5114732172

   Concrete sketch of the token approach, in case it helps the discussion. The 
exported API changes from `resolve_conf_var_in_text(text) -> text` to 
`load_yaml_conf(text) -> conf, err`, because resolution now happens after 
parsing; the two call sites are `apisix/cli/file.lua:321` and 
`apisix/core/config_yaml.lua:101`.
   
   ```lua
   local TOKEN_PREFIX = "__APISIX_ENV_VAR_"
   local TOKEN_SUFFIX = "__"
   
   
   -- pass 1: replace every `${{VAR}}` reference with a token that survives YAML
   -- parsing as a plain scalar. References inside comments become tokens too, 
but
   -- the YAML parser drops them, so an unset var in a comment never surfaces.
   local function tokenize_conf_var(text)
       local refs = {}
       local parts = {}
       local pos = 1
   
       while true do
           local from, to, body = str_find(text, "%$%{%{%s*(.-)%s*%}%}", pos)
           if not from then
               break
           end
   
           local var, default = body, nil
           local i = str_find(body, "%:%=")
           if i then
               var = str_sub(body, 1, i - 1)
               default = str_sub(body, i + 2):gsub("^%s*(.-)%s*$", "%1")
           end
   
           -- a reference wrapped in quotes stays a string, an unquoted one 
takes
           -- its type from the value (#13078)
           local quote = str_sub(text, from - 1, from - 1)
           local quoted = (quote == '"' or quote == "'")
                          and str_sub(text, to + 1, to + 1) == quote
   
           refs[#refs + 1] = {var = var, default = default, quoted = quoted}
           parts[#parts + 1] = str_sub(text, pos, from - 1)
           parts[#parts + 1] = TOKEN_PREFIX .. #refs .. TOKEN_SUFFIX
           pos = to + 1
       end
   
       parts[#parts + 1] = str_sub(text, pos)
       return table_concat(parts), refs
   end
   
   
   local function lookup(ref)
       local v = getenv(ref.var) or ref.default
       if not v then
           return nil, "failed to handle configuration: " ..
                       "can't find environment variable " .. ref.var
       end
   
       if not exported_vars then
           exported_vars = {}
       end
       exported_vars[ref.var] = v   -- still feeds get_exported_vars()
       return v
   end
   
   
   -- pass 2: resolve the tokens that made it into the parsed config
   local function resolve_token(str, refs)
       local idx = str:match("^" .. TOKEN_PREFIX .. "(%d+)" .. TOKEN_SUFFIX .. 
"$")
       if idx then
           local ref = refs[tonumber(idx)]
           local v, err = lookup(ref)
           if not v then
               return nil, err
           end
           if ref.quoted then
               return v
           end
           if tonumber(v) then
               return tonumber(v)
           elseif v == "true" then
               return true
           elseif v == "false" then
               return false
           end
           return v
       end
   
       local err
       local new_str = str:gsub(TOKEN_PREFIX .. "(%d+)" .. TOKEN_SUFFIX, 
function(i)
           local v, e = lookup(refs[tonumber(i)])
           if not v then
               err = e
               return ""
           end
           return v
       end)
       if err then
           return nil, err
       end
       return new_str
   end
   
   
   local function resolve_conf_var_in_tree(conf, refs)
       local renamed_keys
       for key, val in pairs(conf) do
           if type(val) == "table" then
               local ok, err = resolve_conf_var_in_tree(val, refs)
               if not ok then
                   return nil, err
               end
           elseif type(val) == "string" then
               local new_val, err = resolve_token(val, refs)
               if err then
                   return nil, err
               end
               conf[key] = new_val
           end
   
           if type(key) == "string" then
               local new_key, err = resolve_token(key, refs)
               if err then
                   return nil, err
               end
               if new_key ~= key then
                   renamed_keys = renamed_keys or {}
                   renamed_keys[#renamed_keys + 1] = {key, tostring(new_key)}
               end
           end
       end
   
       -- deferred, inserting a key while iterating with pairs() is undefined
       for _, pair in ipairs(renamed_keys or {}) do
           conf[pair[2]] = conf[pair[1]]
           conf[pair[1]] = nil
       end
       return true
   end
   
   
   function _M.load_yaml_conf(text)
       local tokenized, refs = tokenize_conf_var(text)
       local ok, conf = pcall(yaml.load, tokenized)
       if not ok then
           return nil, "invalid yaml: " .. tostring(conf)
       end
       if type(conf) ~= "table" then
           return conf
       end
   
       local resolved, err = resolve_conf_var_in_tree(conf, refs)
       if not resolved then
           return nil, err
       end
       return conf
   end
   ```
   
   Output of that sketch on the cases from my previous comment (`W=3 B=true 
K=mykey HASHY='secret # not-a-comment' MULTI=$'a\nb: 2' 
BIG=356002209726529540`):
   
   ```
   full-line comment      {"key":1}
   inline comment         {"key":1}
   missing var            ERR: failed to handle configuration: can't find 
environment variable UNSET_XYZ
   unquoted number        {"weight":3}
   quoted number          {"key":"3"}
   unquoted bool          {"flag":true}
   default fallback       {"key":"fallback"}
   var in key             {"mykey":1}
   embedded string        {"key":"pre-3-post"}
   value with #           {"key":"secret # not-a-comment"}
   multiline value        {"key":"a\nb: 2"}
   block scalar           {"body":"# 3 stays\n"}
   big int quoted         {"id":"356002209726529540"}
   two refs one line      {"key":"mykey-3"}
   exported_vars          
{"B":"true","BIG":"356002209726529540","HASHY":"secret # not-a-comment",
                           "K":"mykey","MULTI":"a\nb: 
2","NOPE":"fallback","W":"3"}
   ```
   
   Two notes on it:
   
   - `exported_vars` is still populated during pass 2, so `get_exported_vars()` 
and the generated nginx `env` directives keep working — the resolution just 
moves later.
   - token collision is the one thing to guard: if the raw text already 
contains `TOKEN_PREFIX`, bail out or pick another prefix.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to