| .. 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. |
| |
| .. include:: ../../../common.defs |
| |
| .. default-domain:: cpp |
| |
| TSCfgRegister |
| ************* |
| |
| Register a plugin's configuration file with the |TS| reload framework |
| and drive the per-reload context object delivered to the handler. |
| |
| Synopsis |
| ======== |
| |
| .. code-block:: cpp |
| |
| #include <ts/ts.h> |
| |
| .. enum:: TSCfgSourceType |
| |
| What content sources a plugin's config handler supports. |
| |
| .. enumerator:: TS_CFG_SOURCE_FILE_ONLY |
| |
| Handler reloads only from a file on disk. RPC-supplied YAML is |
| rejected. |
| |
| .. enumerator:: TS_CFG_SOURCE_FILE_AND_RPC |
| |
| Handler can also process YAML content supplied via RPC. |
| |
| .. type:: TSCfgRegistrationInfo |
| |
| Options struct passed to :func:`TSCfgRegister`. Fields are set with |
| C++20 designated initializers; new fields may be appended in future |
| |TS| versions without breaking source compatibility for existing |
| plugins. |
| |
| .. var:: std::string_view key |
| |
| Unique registry key, also used as the YAML node name when an |
| operator reloads via |
| ``traffic_ctl config reload --data '{<key>: {...}}'``. |
| Must be unique across all registered configs (core and plugin). |
| |
| **Convention.** Use the plugin name (``my_plugin``). Keep the key |
| alphanumeric + underscore; avoid dots, since |
| ``traffic_ctl --directive my_plugin.dry_run=true`` parses the |
| first ``.`` as the directive separator. |
| |
| **Plugins with multiple files:** |
| |
| - If the files share the same logical config and should reload |
| together, register once and add the rest via |
| :func:`TSCfgAddFileDependency` (single task per reload). |
| - If the files are genuinely independent and reloadable on their |
| own, register each separately with a disambiguating key |
| (e.g. ``my_plugin_main``, ``my_plugin_aux``). Each becomes its |
| own task entry in :option:`traffic_ctl config status`. |
| |
| Required. |
| |
| .. var:: std::string_view config_path |
| |
| Default config file path (absolute, or relative to ``sysconfdir``). |
| Required. |
| |
| .. var:: std::string_view filename_record |
| |
| Fully-qualified record name that holds the active config filename, |
| or ``{}`` (default) if the path is fixed. When set, the operator |
| can change the active config file at runtime by editing this |
| record; ``config_path`` is used as the fallback when the record is |
| empty or unset. Optional. |
| |
| .. var:: TSCfgLoadCb handler |
| |
| Reload callback invoked when the file changes, when an attached |
| trigger record changes, or when the operator triggers an RPC |
| reload. Required. |
| |
| .. var:: void *data |
| |
| Opaque pointer passed unmodified to ``handler`` on every invocation. |
| The plugin owns it; ATS does not copy or free it. Must outlive the |
| registration - typically a ``static`` or once-allocated heap object |
| stashed in ``TSPluginInit``:: |
| |
| static PluginState state; |
| state.config_path = ...; |
| |
| TSCfgRegistrationInfo info{}; |
| info.handler = config_reload; |
| info.data = &state; |
| TSCfgRegister(&info); |
| |
| .. var:: TSCfgSourceType source |
| |
| Whether ``handler`` can also process YAML content supplied via RPC. |
| Defaults to :enumerator:`TSCfgSourceType::TS_CFG_SOURCE_FILE_ONLY`. |
| |
| .. var:: bool is_required |
| |
| Hint propagated to FileManager: marks the file as "required" in |
| catalog/inspection output and emits a ``Dbg`` line if the file |
| is missing at registration time. The framework does **not** |
| enforce this flag at reload-time today: the handler is still |
| invoked even when the file is missing or invalid, and the |
| plugin chooses whether to fail the reload. Defaults to ``false``. |
| |
| .. type:: TSCfgFileDependencyInfo |
| |
| Options struct passed to :func:`TSCfgAddFileDependency`. Only ``key`` |
| and ``config_path`` are required. |
| |
| .. var:: std::string_view key |
| |
| Parent registry key as passed to :func:`TSCfgRegister`. Required. |
| |
| .. var:: std::string_view config_path |
| |
| Default companion file path (absolute, or relative to ``sysconfdir``). |
| Required. |
| |
| .. var:: std::string_view filename_record |
| |
| Fully-qualified record name that holds the filename, or ``{}`` if |
| the path is fixed. Optional. |
| |
| .. var:: std::string_view dep_key |
| |
| Routing key for inline YAML supplied via JSONRPC. When non-empty, |
| content delivered under this top-level node is routed to the parent |
| entry's handler, giving the plugin parity with core |
| composite-config patterns. When empty (default), the dependency is |
| file-change-only. |
| |
| .. var:: bool is_required |
| |
| Hint propagated to FileManager (catalog/inspection only); see the |
| equivalent field on :type:`TSCfgRegistrationInfo`. Defaults to |
| ``false``. |
| |
| .. type:: TSCfgLoadCtx |
| |
| Opaque handle to a per-reload context. The context tracks the |
| lifecycle of a single reload attempt for one registered config: it |
| carries the reload token, supplied YAML (for RPC reloads), the |
| filename (for file reloads), and the task state machine the framework |
| uses to aggregate results. |
| |
| .. type:: void (*TSCfgLoadCb)(TSCfgLoadCtx ctx, void *data) |
| |
| Plugin reload callback signature. ``data`` is the opaque pointer the |
| plugin supplied via :var:`TSCfgRegistrationInfo::data`. |
| |
| .. enum:: TSCfgLogLevel |
| |
| Severity for log entries emitted via :func:`TSCfgLoadCtxAddLog`. |
| |
| .. enumerator:: TS_CFG_LOG_NOTE |
| |
| Informational. Default level. Maps to ``DL_Note`` internally. |
| |
| .. enumerator:: TS_CFG_LOG_WARNING |
| |
| Concerning; the reload may still complete. Maps to ``DL_Warning``. |
| |
| .. enumerator:: TS_CFG_LOG_ERROR |
| |
| Failure cause; pair with a subsequent :func:`TSCfgLoadCtxFail`. |
| Maps to ``DL_Error``. |
| |
| The framework deliberately does not expose the fatal-class diagnostics |
| levels here: a reload handler should not be able to terminate the |
| process. Plugins that truly need debug-only output should use |
| ``Dbg(ctl, ...)`` instead of this API. |
| |
| .. function:: TSReturnCode TSCfgRegister(const TSCfgRegistrationInfo *info) |
| .. function:: bool TSCfgIsRegistered(std::string_view key) |
| .. function:: TSReturnCode TSCfgAttachReloadTrigger(std::string_view key, std::string_view record_name) |
| .. function:: TSReturnCode TSCfgAddFileDependency(const TSCfgFileDependencyInfo *info) |
| .. function:: void TSCfgLoadCtxInProgress(TSCfgLoadCtx ctx, std::string_view msg) |
| .. function:: void TSCfgLoadCtxComplete(TSCfgLoadCtx ctx, std::string_view msg) |
| .. function:: void TSCfgLoadCtxFail(TSCfgLoadCtx ctx, std::string_view msg) |
| .. function:: void TSCfgLoadCtxAddLog(TSCfgLoadCtx ctx, TSCfgLogLevel level, std::string_view msg) |
| .. function:: TSCfgLoadCtx TSCfgLoadCtxAddSubtask(TSCfgLoadCtx ctx, std::string_view description) |
| .. function:: std::string_view TSCfgLoadCtxGetFilename(TSCfgLoadCtx ctx) |
| .. function:: std::string_view TSCfgLoadCtxGetReloadToken(TSCfgLoadCtx ctx) |
| .. function:: TSYaml TSCfgLoadCtxGetSuppliedYaml(TSCfgLoadCtx ctx) |
| .. function:: TSYaml TSCfgLoadCtxGetReloadDirectives(TSCfgLoadCtx ctx) |
| |
| Description |
| =========== |
| |
| These functions are the plugin-facing entry points to the |TS| |
| configuration reload framework. A plugin registers a config file |
| together with a reload callback, and the framework drives the callback |
| whenever the file changes, an attached trigger record changes, or an |
| operator triggers a reload via JSONRPC. |
| |
| The plugin name (set via :func:`TSPluginRegister`) is automatically |
| attached to every registered entry, so reload-trace logs and |
| :option:`traffic_ctl config status` output identify which plugin owns |
| which entry as ``[plugin: <name>]``. Plugins do not pass their name |
| explicitly. |
| |
| Registration |
| ------------ |
| |
| :func:`TSCfgRegister` |
| Registers a config file and reload handler. |
| |
| Must be called from :func:`TSPluginInit`, **after** |
| :func:`TSPluginRegister`. Returns ``TS_ERROR`` if called outside |
| ``TSPluginInit``, before ``TSPluginRegister``, or with a null or |
| incomplete ``info`` struct. |
| |
| If another plugin (or core) has already registered the same key, |
| the duplicate registration is rejected, a warning is logged |
| identifying both owners, and ``TS_ERROR`` is returned. The colliding |
| registration is **not** installed, so the handler would never run; |
| the plugin can detect this and log or bail out. Plugins that may be |
| loaded more than once can probe first with :func:`TSCfgIsRegistered` |
| to avoid the duplicate attempt entirely. |
| |
| :func:`TSCfgIsRegistered` |
| Returns ``true`` if ``key`` is already registered (by any plugin or |
| core), ``false`` otherwise (including for an empty ``key``). Plugins |
| that may be loaded more than once - e.g. by an admin retry, or as a |
| remap plugin instance - can call this before :func:`TSCfgRegister` |
| to skip a duplicate registration that would otherwise be rejected. |
| |
| :func:`TSCfgAttachReloadTrigger` |
| Wires a record so that changing its value re-runs the reload handler |
| registered for ``key``. Internally registers a record-change |
| callback (``RecRegisterConfigUpdateCb``) and routes the event back |
| into the same reload pipeline as file changes: the plugin's |
| :type:`TSCfgLoadCb` is invoked with the resolved file path |
| available via :func:`TSCfgLoadCtxGetFilename`, exactly as it would |
| be for an on-disk file change. May be called multiple times to |
| attach more than one trigger record to the same entry. |
| |
| **Core analog.** :cpp:func:`!ConfigRegistry::register_config()` |
| accepts a ``trigger_records`` initializer-list at registration time |
| (e.g. ``ssl_multicert`` lists ~10 record names there). The |
| post-registration form is :cpp:func:`!ConfigRegistry::attach`. This |
| function is the plugin-facing wrapper for the latter; the |
| initializer-list shape is intentionally not exposed on |
| :type:`TSCfgRegistrationInfo` so the option struct stays |
| ABI-stable. |
| |
| **It triggers a reload, nothing else.** Specifically the plugin |
| cannot: |
| |
| - Register a free-form record-change callback. There is no |
| ``TSRecordRegisterChangeCb`` today; the underlying primitive |
| (``RecRegisterConfigUpdateCb``) is internal-only. |
| - Receive record-change details. The handler gets no record name, |
| no old/new value, no event payload. Use ``TSMgmt*Get()`` inside |
| the handler if it needs the value. |
| - Subscribe a record without a registered config key. Calling with |
| an unknown ``key`` returns ``TS_ERROR``. |
| - Multiplex one record across two keys, or attach in any shape |
| other than (one record, one config key, per call). |
| |
| The reload is always treated as file-driven (no RPC payload): |
| :func:`TSCfgLoadCtxGetSuppliedYaml` and |
| :func:`TSCfgLoadCtxGetReloadDirectives` return ``nullptr`` for |
| record-triggered invocations. Standalone record changes (i.e. |
| ``traffic_ctl config set`` outside an active reload cycle) invoke |
| the handler with an empty context that does not surface in |
| :option:`traffic_ctl config status` - same as core record-triggered |
| reloads outside a reload cycle. |
| |
| :func:`TSCfgAddFileDependency` |
| Adds a companion file to a previously registered key. When the |
| companion file changes on disk - or when an operator submits inline |
| YAML under :var:`TSCfgFileDependencyInfo::dep_key` via JSONRPC, when |
| set - the plugin's handler is invoked. With ``dep_key`` empty the |
| dependency is file-change-only. |
| |
| Per-reload context (TSCfgLoadCtx) |
| --------------------------------- |
| |
| A :type:`TSCfgLoadCtx` is created by the framework before invoking the |
| plugin's :type:`TSCfgLoadCb`. The handler must drive the context to a |
| **terminal state** - either :func:`TSCfgLoadCtxComplete` or |
| :func:`TSCfgLoadCtxFail` - so the reload tree can finish. See |
| :ref:`config-context-terminal-state` for the complete contract. |
| |
| State transitions: |
| |
| :func:`TSCfgLoadCtxInProgress` |
| Set the task to ``IN_PROGRESS`` and emit an optional progress |
| message. Calling it more than once on the same context is |
| discouraged - use :func:`TSCfgLoadCtxAddLog` for periodic log |
| output. Pass ``{}`` for ``msg`` if no message is needed. |
| |
| :func:`TSCfgLoadCtxComplete` |
| Mark the task as successfully completed. Pass ``{}`` for ``msg`` if |
| no message is needed. This call finalizes and invalidates ``ctx``: |
| **do not use** ``ctx`` afterwards. A later call on the same handle |
| (a second Complete/Fail, or any accessor) is ignored and logged as |
| misuse rather than acting on freed state, but plugins must not rely |
| on that as normal control flow. |
| |
| :func:`TSCfgLoadCtxFail` |
| Mark the task as failed. Pass ``{}`` for ``msg`` if no message is |
| needed. This call finalizes and invalidates ``ctx`` with the same |
| post-finalize semantics as :func:`TSCfgLoadCtxComplete`. |
| |
| :func:`TSCfgLoadCtxAddLog` |
| Append a log entry at ``level`` to the task. Visible in |
| :option:`traffic_ctl config status` output and in the |
| :ref:`get_reload_config_status` JSONRPC response. Does not change the |
| task state. |
| |
| :func:`TSCfgLoadCtxAddSubtask` |
| Create a child subtask. Returns a new context that the plugin must |
| independently drive to a terminal state. Use this when a single |
| config reload spawns several distinct units of work that should be |
| tracked separately. The parent's status aggregates from its |
| children: any child failing causes the parent to be marked failed. |
| |
| The subtask is born in ``CREATED`` and follows the same state |
| machine as the parent. Two transition patterns are valid: |
| |
| - ``CREATED -> IN_PROGRESS -> SUCCESS / FAILED`` (recommended for |
| substantive work) - call :func:`TSCfgLoadCtxInProgress` once you |
| start, then :func:`TSCfgLoadCtxComplete` / |
| :func:`TSCfgLoadCtxFail` when done. The "in progress" phase is |
| visible in :option:`traffic_ctl config status`. |
| - ``CREATED -> SUCCESS / FAILED`` (shortcut for trivial subtasks) - |
| call :func:`TSCfgLoadCtxComplete` / :func:`TSCfgLoadCtxFail` |
| directly on the returned handle. |
| |
| Inputs: |
| |
| :func:`TSCfgLoadCtxGetFilename` |
| Returns the path the framework expects this handler to read. |
| Two-step resolution: |
| |
| 1. If the registration's ``filename_record`` was set AND the record |
| currently has a non-empty value, that value is returned (the |
| operator can override the filename at runtime via |
| ``traffic_ctl config set <record>``). |
| 2. Otherwise, returns ``config_path`` as-registered. |
| |
| Most plugins don't set ``filename_record`` and could equivalently |
| use their own stashed copy of the registered path - this function |
| is the canonical way to get the filename only when |
| ``filename_record`` is in play. |
| |
| **Always populated for plugin handlers**, including on RPC reloads. |
| To detect RPC content, check :func:`TSCfgLoadCtxGetSuppliedYaml` - |
| not this function. |
| |
| **Core analog.** :cpp:func:`!ConfigReloadTask::get_filename`. For |
| core handlers, the value is empty on RPC reloads; the plugin |
| wrapper always populates it so plugins can use |
| :func:`TSCfgLoadCtxGetSuppliedYaml` as the canonical RPC-detection |
| signal. |
| |
| :func:`TSCfgLoadCtxGetReloadToken` |
| Returns the reload-cycle correlation token (e.g. |
| ``rldtk-<timestamp>``). Useful for plugin-side log lines that need |
| to match the operator's |
| ``traffic_ctl config status -t <token>`` view. |
| |
| :func:`TSCfgLoadCtxGetSuppliedYaml` |
| Returns the YAML content supplied via the JSONRPC reload payload as |
| an opaque :type:`TSYaml` handle (a ``YAML::Node *``). The |
| framework-reserved ``_reload`` directives are stripped before |
| delivery. The returned node is undefined when the reload was driven |
| by file change rather than RPC. Only meaningful when the entry was |
| registered with :enumerator:`TSCfgSourceType::TS_CFG_SOURCE_FILE_AND_RPC`. |
| |
| :func:`TSCfgLoadCtxGetReloadDirectives` |
| Returns the YAML map extracted from the ``_reload`` key in the |
| RPC-supplied content as an opaque :type:`TSYaml` handle. Used by |
| handlers that need scoped reload behavior (for example, reload only |
| one entry, dry-run mode, or a version constraint). See |
| :ref:`config-reload-framework` for the directive convention. |
| |
| The two ``std::string_view`` accessors return views whose backing |
| storage is owned by the framework and is valid for the lifetime of the |
| handler call. Copy the data if you need to retain it past the callback |
| return. The :type:`TSYaml` accessors return handles whose lifetime is |
| also tied to the handler call. |
| |
| Lifecycle and threading |
| ----------------------- |
| |
| The handler runs on an ``ET_TASK`` thread. It does **not** have to |
| complete synchronously: the plugin may stash ``ctx`` in its own state, |
| return from the callback, finish work on a different thread, and then |
| call :func:`TSCfgLoadCtxComplete` or :func:`TSCfgLoadCtxFail` from |
| there. This is the standard pattern for plugins that perform |
| asynchronous I/O during reload. |
| |
| If the handler never reaches a terminal state, the per-reload progress |
| checker eventually marks the task as ``TIMEOUT`` and unblocks the |
| overall reload (see :ref:`config-context-terminal-state`). The context |
| handle then leaks for the process lifetime, so reaching a terminal |
| state on every code path is mandatory. |
| |
| Recommended pattern: parse first, apply second |
| ---------------------------------------------- |
| |
| The framework does not provide a separate validation phase. The |
| handler is the single point where the plugin parses the new |
| configuration and applies it. To avoid leaving live state half-mutated |
| on a partial parse, plugins should follow a two-step pattern inside |
| the same handler: |
| |
| 1. **Parse** the file (or supplied YAML) into a fresh, staging-side |
| structure. Validate fully. Do not touch live state during this |
| step. On parse failure, call :func:`TSCfgLoadCtxFail` and return - |
| live state remains untouched. |
| |
| 2. **Swap** the staging structure into place atomically. This is |
| typically a pointer swap into a ``std::shared_ptr`` / |
| ``std::atomic`` slot the request path reads from. After the swap |
| succeeds, call :func:`TSCfgLoadCtxComplete`. |
| |
| This pattern matches what core configs already do (for example, |
| ``ip_allow``, ``remap.config``) and gives operators predictable |
| behavior on a malformed reload: the previous configuration stays in |
| effect until a fully valid one is loaded. |
| |
| Restrictions |
| ============ |
| |
| - Not supported for remap plugins (:func:`TSRemapInit` / |
| :func:`TSRemapNewInstance`). |
| - :func:`TSCfgRegister`, :func:`TSCfgAttachReloadTrigger`, and |
| :func:`TSCfgAddFileDependency` must all be called from |
| :func:`TSPluginInit`, after :func:`TSPluginRegister`. |
| |
| Example - registration and synchronous handler |
| ============================================== |
| |
| .. code-block:: cpp |
| |
| #include <ts/ts.h> |
| #include <string> |
| |
| namespace |
| { |
| constexpr char PLUGIN_NAME[] = "my_plugin"; |
| |
| struct PluginState { |
| std::string config_path; |
| }; |
| |
| void |
| config_reload(TSCfgLoadCtx ctx, void *data) |
| { |
| auto *state = static_cast<PluginState *>(data); |
| |
| std::string_view filename = TSCfgLoadCtxGetFilename(ctx); |
| if (filename.empty()) { |
| TSCfgLoadCtxFail(ctx, "no filename available"); |
| return; |
| } |
| |
| if (!parse_and_apply(state, std::string{filename})) { |
| TSCfgLoadCtxFail(ctx, "parse failed"); |
| return; |
| } |
| |
| TSCfgLoadCtxComplete(ctx, "Reloaded my_plugin"); |
| } |
| } // anonymous namespace |
| |
| void |
| TSPluginInit(int /* argc */, const char * /* argv */[]) |
| { |
| TSPluginRegistrationInfo plugin{}; |
| plugin.plugin_name = PLUGIN_NAME; |
| plugin.vendor_name = "Example Inc."; |
| plugin.support_email = "support@example.com"; |
| |
| if (TSPluginRegister(&plugin) != TS_SUCCESS) { |
| TSError("[%s] plugin registration failed", PLUGIN_NAME); |
| return; |
| } |
| |
| static PluginState state; |
| state.config_path = std::string{TSConfigDirGet()} + "/my_plugin.yaml"; |
| |
| TSCfgRegistrationInfo info{}; |
| info.key = PLUGIN_NAME; |
| info.config_path = state.config_path; |
| info.handler = config_reload; |
| info.data = &state; |
| info.source = TS_CFG_SOURCE_FILE_AND_RPC; |
| info.is_required = false; |
| if (TSCfgRegister(&info) != TS_SUCCESS) { |
| TSError("[%s] TSCfgRegister failed for '%s'", PLUGIN_NAME, |
| state.config_path.c_str()); |
| return; |
| } |
| |
| // Optional: attach a record so changing it fires the handler. |
| TSCfgAttachReloadTrigger(PLUGIN_NAME, "proxy.config.my_plugin.enabled"); |
| } |
| |
| Example - RPC content with directives |
| ===================================== |
| |
| .. code-block:: cpp |
| |
| #include <yaml-cpp/yaml.h> |
| |
| void |
| config_reload(TSCfgLoadCtx ctx, void * /* data */) |
| { |
| auto *yaml = static_cast<YAML::Node *>(TSCfgLoadCtxGetSuppliedYaml(ctx)); |
| auto *dirs = static_cast<YAML::Node *>(TSCfgLoadCtxGetReloadDirectives(ctx)); |
| |
| if (dirs && dirs->IsDefined()) { |
| // operator passed --directive ... |
| } |
| if (yaml && yaml->IsDefined()) { |
| // RPC-supplied content; do not read the file. |
| } else { |
| // file-driven reload; read TSCfgLoadCtxGetFilename(ctx). |
| } |
| TSCfgLoadCtxComplete(ctx, {}); |
| } |
| |
| Example - deferred completion |
| ============================= |
| |
| A plugin that schedules background work returns from the callback |
| without calling Complete or Fail; it stashes ``ctx`` and finishes from |
| another thread: |
| |
| .. code-block:: cpp |
| |
| struct Work { TSCfgLoadCtx ctx; PluginState *state; }; |
| |
| void |
| config_reload(TSCfgLoadCtx ctx, void *data) |
| { |
| auto *work = new Work{ctx, static_cast<PluginState *>(data)}; |
| // schedule heavy_work(work) on a worker thread; it will eventually |
| // call TSCfgLoadCtxComplete(work->ctx, {}) (or Fail) and delete work. |
| schedule_async(work); |
| } |
| |
| A runnable example of the deferred pattern lives in |
| ``tests/gold_tests/jsonrpc/plugins/cfg_plugin_deferred_test.cc``. |
| |
| See Also |
| ======== |
| |
| :ref:`config-reload-framework` is the complete framework guide, |
| covering the registry, terminal-state rule, RPC reload payload format, |
| ``[plugin: <name>]`` attribution in :option:`traffic_ctl config status`, |
| diagnostic macros, and ``diags.log`` summaries. |