Skip to content

[WIP] Salt Resources#68889

Open
dwoz wants to merge 8 commits intomasterfrom
resources
Open

[WIP] Salt Resources#68889
dwoz wants to merge 8 commits intomasterfrom
resources

Conversation

@dwoz
Copy link
Copy Markdown
Contributor

@dwoz dwoz commented Apr 2, 2026

What does this PR do?

What issues does this PR fix or reference?

Fixes #68888

@dwoz dwoz requested a review from a team as a code owner April 2, 2026 22:14
@dwoz dwoz added test:full Run the full test suite labels Apr 3, 2026
dwoz added 7 commits April 4, 2026 16:25
…d SSH resource

Adds the complete core infrastructure for Salt Resources (SRN), allowing
resources to be targeted and executed against independently from the
managing minion, along with a full SSH resource implementation.

Targeting layer:
- T@ and M@ compound match engines (resource_match, managing_minion_match)
- CkMinions._check_resource_minions and _augment_with_resources so that
  glob targets like '*' automatically include managed resource IDs
- Master-side resource registration (_register_resources in AESFuncs) and
  minion-side _register_resources_with_master on connect

Loader layer:
- salt.loader.resource(): loads salt/resource/*.py, packs as __resource_funcs__
- salt.loader.resource_modules(opts, resource_type): isolated per-type
  execution-module loader with opts["resource_type"] injected; one instance
  per managed type per minion process (not per device)

Minion dispatch:
- gen_modules() initialises self.resource_funcs and self.resource_loaders
- _thread_return routes resource jobs to the per-type loader, injects
  __resource__ and per-resource __grains__ before each call
- Unknown functions for a resource type fail loudly instead of silently
  executing on the managing minion
- _execute_job_function accepts an optional functions= loader parameter
- JID deduplication bypassed for resource_job loads (shared parent JID)
- _NO_RESOURCE_FUNS prevents internal Salt plumbing from dispatching to
  resources

Dummy resource (example/test implementation):
- salt/resource/dummy.py: full dummy resource (ping, grains, services, pkgs)
- salt/modules/dummyresource_test.py: Pattern B execution module gating on
  opts["resource_type"] == "dummy", routes test.ping to dummy.ping()

SSH resource:
- salt/resource/ssh.py: SSH resource module using salt-ssh Shell transport
- salt/modules/sshresource_cmd.py: cmd.* surface for SSH resources
- salt/modules/sshresource_pkg.py: pkg.* surface for SSH resources
- salt/modules/sshresource_state.py: state.* surface for SSH resources
- salt/modules/sshresource_test.py: test.ping for SSH resources

Also adds salt/grains/resources.py, design documents, example loader
scripts, srv/ pillar configuration, and a Docker environment for testing.

Made-with: Cursor
salt.loader.resource_modules was missing "__resource__" from its pack,
so LazyLoader never created a NamedLoaderContext for it and modules like
sshresource_state raised NameError when accessing __resource__["id"].
Added the sentinel entry so the loader wires __resource__ to
resource_ctxvar — the per-thread contextvar set by _thread_return before
each resource job executes.
Added a regression test (test_resource_modules_packs_resource_dunder)
and a full suite of unit and integration tests covering T@ targeting,
wildcard augmentation, resource_ctxvar thread isolation, and the
_resolve_resource_targets dispatch path. Fixed lint issues across the new
test files (blacklisted unittest.mock imports, f-string without
interpolation, unused import, broad exception catch).
…rces

Three bugs fixed and one new feature added:
1. salt '*' test.ping was incorrectly dispatching to resources when
   targeting a specific minion by name (e.g. salt 'minion' test.ping).
   _resolve_resource_targets now only dispatches to resources for wildcard
   glob patterns (* ? [).  The master-side _augment_with_resources guard
   is updated consistently.
2. Removing a resource type from the pillar and running sync_all left
   stale resource IDs in the master cache indefinitely, causing phantom
   entries in salt '*' output.  Three co-operating fixes:
   - _discover_resources now distinguishes "no resources key in pillar"
     (preserve opts for config-file deployments) from "resources key
     present but empty" (authoritative removal — clear opts).
   - _register_resources_with_master no longer short-circuits on an empty
     resource dict; it always sends the registration so the master cache
     is overwritten with the current (possibly empty) state.
   - pillar_refresh re-discovers resources and re-registers with the
     master after successfully compiling a new pillar, so a sync_all
     picks up removals without requiring a minion restart.
3. _augment_with_resources had no error handling; a cache driver failure
   would propagate into check_minions and silently discard all PKI-based
   minion IDs.  Cache errors are now caught, logged, and degraded
   gracefully.
New: saltutil.sync_resources / refresh_resources — explicit resource
sync modelled after sync_modules/refresh_modules.  sync_all now includes
ret["resources"] and fires a resource_refresh event that triggers
_discover_resources + _register_resources_with_master on the minion.
For state functions (state.apply, state.highstate, state.sls, etc.) run
against a wildcard glob, resource jobs are now executed inline by the
managing minion and their results folded into a single response block,
producing one Summary section instead of one per resource.
Key changes:
- salt/utils/minions.py: add _MERGE_RESOURCE_FUNS; check_minions accepts
  fun= and skips resource augmentation for merge-mode functions so the
  master does not wait for separate per-resource responses
- salt/master.py: pass fun= to check_minions at publish time
- salt/minion.py: add _MERGE_RESOURCE_FUNS and _prefix_resource_state_key
  (refactored to @staticmethod); _handle_payload skips separate resource
  dispatch for merge-mode functions; _thread_return executes resources
  inline and merges prefixed state keys into the managing minion's ret,
  reporting Result: False for unsupported functions and missing loaders
- salt/modules/sshresource_state.py: highstate() returns a no_|-states
  state dict (Result: False) when the top file has no match; _exec_state_pkg
  recovers valid state dicts from SSHCommandExecutionError instead of
  re-raising and losing the result
Tests:
- test_minions_resources.py: merge-fun skips augmentation, non-merge and
  no-fun still augment, compound targets unaffected
- test_minion_resources.py: _MERGE_RESOURCE_FUNS membership and sync
  invariant, _prefix_resource_state_key correctness and fallback, merge
  block no-loader/unsupported/dict/string branches
- test_sshresource_state.py (new): empty-chunks no-top-file return,
  SSHCommandExecutionError recovery and re-raise cases, normal envelope path
- salt/states/saltutil.py: add sync_resources() state function to match
  the sync_resources() module function added in saltutil.py; the existing
  test enforces a 1:1 correspondence between all sync_* module and state
  functions
- tests/pytests/unit/client/test_netapi.py: update test_run_log to use
  AsyncMock for process_manager.run(), which our netapi.py change wrapped
  in asyncio.run() and therefore requires a coroutine, not a plain Mock
…on job targeting

- salt/state.py: add __pub_resource_targets, __pub_minion_is_target,
  __pub_resource_target, and __pub_resource_job to STATE_REQUISITE_KEYWORDS
  so they are treated as internal pub metadata and stripped before being
  passed to state module functions (e.g. git.present, ssh_known_hosts.present)
- salt/utils/minions.py: guard the _MERGE_RESOURCE_FUNS membership check
  with isinstance(fun, str) — multifunction jobs pass fun as a list which
  is not hashable, causing a TypeError swallowed by the broad except that
  returned an empty minion list and "No minions matched the target"
- tests: add test_check_minions_list_fun_still_augments covering the list
  fun case, and CI-TRACKING.md to record known failures and regression tests
The salt-ssh relenv work merged into master introduced three changes that
required corresponding updates in our SSH resource module:
1. Single.cmd_block() now calls mod_data(self.fsclient) unconditionally to
   regenerate extension modules before every remote execution.  Passing
   fsclient=None (the default) crashes immediately at fsclient.opts.
   _exec_state_pkg() now creates a fresh FSClient from the cached master
   opts and passes it via the fsclient= kwarg to Single.
2. Single.__init__ renames thin_dir from the default *_salt suffix to
   *_salt_relenv when relenv=True, then writes the updated path back into
   opts["thin_dir"].  The previous code built the state.pkg argv string
   before constructing Single, so the baked-in path diverged from where
   the state tar was actually sent.  The argv is now set after __init__
   so both the send path and the command string use the finalized
   thin_dir.
3. _relenv_path() hardcoded linux/x86_64, which would silently deploy the
   wrong tarball to arm64 hosts.  It now probes x86_64 then arm64 and
   returns the first path that exists locally, or None.  A None return
   tells Single.__init__ to call detect_os_arch() itself — one extra SSH
   round-trip as a fallback, but always correct.
Also removes the leftover debug echo from SSH_SH_SHIM_RELENV that was
writing SALT_CALL_CMD diagnostics to stderr on every remote execution.
Tests added: TestRelenvPath (4 cases), fsclient mock added to all
_exec_state_pkg test helpers, and test_single_receives_fsclient verifies
the kwarg is threaded through.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

test:full Run the full test suite

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] Salt Resources

1 participant