assay-lua 0.10.2

General-purpose enhanced Lua runtime. Batteries-included scripting, automation, and web services.
Documentation
--- @module assay.eso
--- @description External Secrets Operator. ExternalSecrets, SecretStores, ClusterSecretStores sync status.
--- @keywords eso, external-secrets, secretstores, kubernetes, secrets, sync, store, readiness, wait, cluster, external-secret
--- @quickref c.external_secrets:list(namespace) -> {items} | List ExternalSecrets in namespace
--- @quickref c.external_secrets:get(namespace, name) -> es|nil | Get ExternalSecret by name
--- @quickref c.external_secrets:status(namespace, name) -> {ready, status, sync_hash} | Get sync status
--- @quickref c.external_secrets:is_synced(namespace, name) -> bool | Check if ExternalSecret is synced
--- @quickref c.external_secrets:wait_synced(namespace, name, timeout_secs?) -> true | Wait for sync
--- @quickref c.external_secrets:all_synced(namespace) -> {synced, failed, total} | Check all secrets sync status
--- @quickref c.secret_stores:list(namespace) -> {items} | List SecretStores in namespace
--- @quickref c.secret_stores:get(namespace, name) -> store|nil | Get SecretStore by name
--- @quickref c.secret_stores:status(namespace, name) -> {ready, conditions} | Get store status
--- @quickref c.secret_stores:is_ready(namespace, name) -> bool | Check if SecretStore is ready
--- @quickref c.secret_stores:all_ready(namespace) -> {ready, not_ready, total} | Check all stores readiness
--- @quickref c.cluster_secret_stores:list() -> {items} | List ClusterSecretStores
--- @quickref c.cluster_secret_stores:get(name) -> store|nil | Get ClusterSecretStore by name
--- @quickref c.cluster_secret_stores:is_ready(name) -> bool | Check if ClusterSecretStore is ready
--- @quickref c.cluster_external_secrets:list() -> {items} | List ClusterExternalSecrets
--- @quickref c.cluster_external_secrets:get(name) -> es|nil | Get ClusterExternalSecret by name

local M = {}

local API_BASE = "/apis/external-secrets.io/v1beta1"

function M.client(url, token)
  local base_url = url:gsub("/+$", "")

  -- Shared HTTP helpers (captured by all sub-object methods as upvalues)

  local function headers()
    return { Authorization = "Bearer " .. token }
  end

  local function api_get(path)
    local resp = http.get(base_url .. path, { headers = headers() })
    if resp.status == 404 then return nil end
    if resp.status ~= 200 then
      error("eso: GET " .. path .. " HTTP " .. resp.status .. ": " .. resp.body)
    end
    return json.parse(resp.body)
  end

  local function api_list(path)
    local resp = http.get(base_url .. path, { headers = headers() })
    if resp.status ~= 200 then
      error("eso: LIST " .. path .. " HTTP " .. resp.status .. ": " .. resp.body)
    end
    return json.parse(resp.body)
  end

  local function find_condition(conditions, cond_type)
    for _, cond in ipairs(conditions or {}) do
      if cond.type == cond_type then
        return cond
      end
    end
    return nil
  end

  -- ===== Client =====

  local c = {}

  -- ===== ExternalSecrets (namespaced) =====

  c.external_secrets = {}

  function c.external_secrets:list(namespace)
    return api_list(API_BASE .. "/namespaces/" .. namespace .. "/externalsecrets")
  end

  function c.external_secrets:get(namespace, name)
    return api_get(API_BASE .. "/namespaces/" .. namespace .. "/externalsecrets/" .. name)
  end

  function c.external_secrets:status(namespace, name)
    local es = api_get(API_BASE .. "/namespaces/" .. namespace .. "/externalsecrets/" .. name)
    if not es then
      error("eso: ExternalSecret " .. namespace .. "/" .. name .. " not found")
    end
    local st = es.status or {}
    local conditions = st.conditions or {}
    local ready_cond = find_condition(conditions, "Ready")
    return {
      ready = ready_cond ~= nil and ready_cond.status == "True",
      status = ready_cond and ready_cond.reason or "Unknown",
      sync_hash = st.syncedResourceVersion or "",
      conditions = conditions,
    }
  end

  function c.external_secrets:is_synced(namespace, name)
    local es = api_get(API_BASE .. "/namespaces/" .. namespace .. "/externalsecrets/" .. name)
    if not es then return false end
    local conditions = (es.status or {}).conditions or {}
    local ready_cond = find_condition(conditions, "Ready")
    return ready_cond ~= nil and ready_cond.status == "True"
  end

  function c.external_secrets:wait_synced(namespace, name, timeout_secs)
    timeout_secs = timeout_secs or 60
    local elapsed = 0
    while elapsed < timeout_secs do
      local synced = c.external_secrets:is_synced(namespace, name)
      if synced then return true end
      sleep(5)
      elapsed = elapsed + 5
    end
    error("eso: ExternalSecret " .. namespace .. "/" .. name .. " not synced after " .. timeout_secs .. "s")
  end

  function c.external_secrets:all_synced(namespace)
    local list = c.external_secrets:list(namespace)
    local synced = 0
    local failed = 0
    local total = 0
    local failed_names = {}
    for _, es in ipairs(list.items or {}) do
      total = total + 1
      local conditions = (es.status or {}).conditions or {}
      local ready_cond = find_condition(conditions, "Ready")
      if ready_cond and ready_cond.status == "True" then
        synced = synced + 1
      else
        failed = failed + 1
        failed_names[#failed_names + 1] = es.metadata.name
      end
    end
    return { synced = synced, failed = failed, total = total, failed_names = failed_names }
  end

  -- ===== SecretStores (namespaced) =====

  c.secret_stores = {}

  function c.secret_stores:list(namespace)
    return api_list(API_BASE .. "/namespaces/" .. namespace .. "/secretstores")
  end

  function c.secret_stores:get(namespace, name)
    return api_get(API_BASE .. "/namespaces/" .. namespace .. "/secretstores/" .. name)
  end

  function c.secret_stores:status(namespace, name)
    local ss = api_get(API_BASE .. "/namespaces/" .. namespace .. "/secretstores/" .. name)
    if not ss then
      error("eso: SecretStore " .. namespace .. "/" .. name .. " not found")
    end
    local conditions = (ss.status or {}).conditions or {}
    local ready_cond = find_condition(conditions, "Ready")
    return {
      ready = ready_cond ~= nil and ready_cond.status == "True",
      conditions = conditions,
    }
  end

  function c.secret_stores:is_ready(namespace, name)
    local ss = api_get(API_BASE .. "/namespaces/" .. namespace .. "/secretstores/" .. name)
    if not ss then return false end
    local conditions = (ss.status or {}).conditions or {}
    local ready_cond = find_condition(conditions, "Ready")
    return ready_cond ~= nil and ready_cond.status == "True"
  end

  function c.secret_stores:all_ready(namespace)
    local list = c.secret_stores:list(namespace)
    local ready = 0
    local not_ready = 0
    local total = 0
    local not_ready_names = {}
    for _, ss in ipairs(list.items or {}) do
      total = total + 1
      local conditions = (ss.status or {}).conditions or {}
      local ready_cond = find_condition(conditions, "Ready")
      if ready_cond and ready_cond.status == "True" then
        ready = ready + 1
      else
        not_ready = not_ready + 1
        not_ready_names[#not_ready_names + 1] = ss.metadata.name
      end
    end
    return { ready = ready, not_ready = not_ready, total = total, not_ready_names = not_ready_names }
  end

  -- ===== ClusterSecretStores (cluster-scoped) =====

  c.cluster_secret_stores = {}

  function c.cluster_secret_stores:list()
    return api_list(API_BASE .. "/clustersecretstores")
  end

  function c.cluster_secret_stores:get(name)
    return api_get(API_BASE .. "/clustersecretstores/" .. name)
  end

  function c.cluster_secret_stores:is_ready(name)
    local css = api_get(API_BASE .. "/clustersecretstores/" .. name)
    if not css then return false end
    local conditions = (css.status or {}).conditions or {}
    local ready_cond = find_condition(conditions, "Ready")
    return ready_cond ~= nil and ready_cond.status == "True"
  end

  -- ===== ClusterExternalSecrets (cluster-scoped) =====

  c.cluster_external_secrets = {}

  function c.cluster_external_secrets:list()
    return api_list(API_BASE .. "/clusterexternalsecrets")
  end

  function c.cluster_external_secrets:get(name)
    return api_get(API_BASE .. "/clusterexternalsecrets/" .. name)
  end

  return c
end

return M