Skip to content

Design spec: Fusion context_names injection - #22

Open
dmann000 wants to merge 2 commits into
mainfrom
design/fusion-context
Open

Design spec: Fusion context_names injection#22
dmann000 wants to merge 2 commits into
mainfrom
design/fusion-context

Conversation

@dmann000

Copy link
Copy Markdown
Owner

Draft design spec for exposing the Fusion fleet context_names query parameter, per Justin's request.

Core idea: don't add a -Context parameter to ~520 cmdlets. Instead:

  1. Connection default - Connect-PfbArray -Context <string[]>, stored on the connection object.
  2. Per-call override - Invoke-PfbInContext -Context 'x' { ... } ambient scope (auto-restores on exit).
  3. Central injection - inject context_names at the single Invoke-PfbApiRequest choke point, gated by the capability map so we only send it to endpoints that accept it. Assert-PfbApiCapability already covers the array-version check.

Precedence: explicit query param > Invoke-PfbInContext > connection default > none. Fail-safe throughout.

Open questions (param name, whether the cap map already records context_names per endpoint, multi-context fan-out) are called out at the bottom. Not for merge yet - posted for review/comment.

Draft proposal for how to expose the Fusion fleet context_names query
param without adding a parameter to ~520 cmdlets: a connection-level
default on Connect-PfbArray, an ambient per-call override
(Invoke-PfbInContext), and central capability-map-gated injection at
the single Invoke-PfbApiRequest choke point. For review.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@juemerson-at-purestorage juemerson-at-purestorage left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: "Design spec: Fusion context_names injection" (PR #22)

Reviewer: Justin Emerson (with live-testing assistance)
Reviewed: draft as of design/fusion-context branch, docs/design/fusion-context-injection.md
Status: feedback for Don ahead of merge — not yet posted to the PR

Summary of the proposal

The design adds Fusion fleet context_names support without touching ~520 public
cmdlet signatures:

  1. Connect-PfbArray -Context <string[]> stores a session default on the
    connection object (.DefaultContext).
  2. Invoke-PfbInContext -Context 'x' { ... } provides an ambient per-call
    override that auto-restores on scope exit.
  3. Invoke-PfbApiRequest (the single request choke point) resolves the
    effective context (explicit -QueryParams['context_names'] > ambient
    override > connection default > none) and injects context_names, gated
    by the capability map
    — if the map has no entry for the endpoint, or the
    endpoint doesn't list context_names, injection is skipped silently.
  4. Assert-PfbApiCapability (already wired into Invoke-PfbApiRequest) is
    relied on to catch the case where the connected array's version predates
    context_names for that endpoint.

The overall shape — one injection point, reusing the capability map as the
source of truth, zero change to public cmdlet signatures — is the right
architecture and matches how the module already works. The rest of this review
is about one specific behavioral choice in step 3, verified against real
arrays rather than just read from the code.

Gaps to flag

Four things worth raising before this goes further. The first three are
straightforward reads of the draft; the fourth (silent skip-injection) is the
one we've since backed with live testing — see the dedicated section below for
the evidence and recommendation.

1. Invoke-PfbInContext nesting behavior is unspecified

The doc only shows single-level usage. It doesn't say what happens here:

Invoke-PfbInContext -Array $fb -Context 'a' {
    Invoke-PfbInContext -Array $fb -Context 'b' {
        Get-PfbFileSystem -Array $fb    # 'b', presumably
    }
    Get-PfbFileSystem -Array $fb        # 'a'? or back to no override?
}

"Internally this sets a script-scoped override... always cleared, even on
error" reads like a single mutable slot (set on enter, clear on exit), not a
stack. If that's the actual implementation, the inner block's exit clears the
override entirely instead of restoring 'a' — so the last two lines above
would run with no context, silently, instead of 'a'. That's the same
silently-wrong-scope failure mode as the step-3 issue, just via nesting instead
of an unsupported endpoint. If nesting is meant to work, the override needs to
be a push/pop stack, not a single slot; if nesting isn't meant to be supported,
the doc should say so and Invoke-PfbInContext should detect and throw on
re-entry rather than silently behaving unexpectedly.

See "Object-model recommendation" below for a concrete fix that resolves this
without needing an explicit stack data structure.

2. No test-coverage plan

The doc has no test-plan section at all. At minimum this needs coverage for:
the three-level precedence resolution (explicit param > ambient > connection
default), the capability-map-gated injection on both the allow and deny paths,
Assert-PfbApiCapability's version check for context_names specifically, and
— this is the one that actually matters most given the design's own claim —
an exception-safety test proving Invoke-PfbInContext restores/clears its
override when the scriptblock throws partway through, not just on the happy
path. That claim ("always cleared, even on error") is exactly the kind of
thing that looks obviously true until someone finds the one code path that
skips the finally.

3. Runspace/parallelism caveat for the ambient scope

Invoke-PfbInContext's override is described as script-scoped state read by
Invoke-PfbApiRequest. Script scope lives in one module instance, which lives
in one runspace. If a caller does something like:

Invoke-PfbInContext -Array $fb -Context 'member-b' {
    1..10 | ForEach-Object -Parallel {
        Get-PfbFileSystem -Array $using:fb
    }
}

each parallel runspace gets its own independent copy of the module (and thus
its own independent, unset script-scoped override) — the ambient context set
in the calling runspace is invisible inside ForEach-Object -Parallel,
Start-ThreadJob, Start-Job, or any runspace-pool worker. Calls made from
inside parallel work would silently fall back to the connection's
.DefaultContext (or no context at all), not the ambient override the caller
thinks is in effect — again the silent-wrong-scope pattern, just triggered by
a runspace boundary instead of an unsupported endpoint or a nesting bug. This
may not be worth solving in code (thread-safe ambient state across runspaces
is a much bigger lift), but it should be called out explicitly in
Invoke-PfbInContext's help text/docs so it isn't discovered the hard way.

See "Object-model recommendation" below — storing context state on the
connection object rather than in module script scope turns out to mitigate
most of this for free, rather than only being a documentation problem.

4. Silent skip-injection (step 3) — see below

Covered in detail in the next section, now with live-testing evidence rather
than just a read of the doc.

Finding: silent skip-injection reproduces the exact failure mode the design set out to avoid

Step 3 states: "If the map has no entry for the endpoint, or the endpoint does
not list context_names, skip injection silently — fail safe, same philosophy
as Assert-PfbApiCapability."

That is not fail-safe for this feature, specifically because of what "fail" means
here. If a caller sets a context — session default or ambient override — expecting
a call to land on fleet member B, and the target endpoint doesn't support
context_names, silently skipping injection sends that call to the local
array
(whatever Invoke-PfbApiRequest would otherwise target) instead of
member B, with zero signal to the caller that their context request was
dropped. For a mutating call (e.g. creating a filesystem, snapshot, or policy),
that's not a failed request — it's a successful request that silently landed
in the wrong place. That's worse than an error.

This is exactly the risk raised in review discussion before any code was
written: a context-scoped write must either land where the caller asked or
throw — never silently execute unscoped.

Live verification (2026-07-22, against real arrays — not just the design doc)

Two hypotheses were on the table for how the array itself handles an
unsupported/unknown context_names:

  • If the array cleanly rejects it (HTTP 400, parseable error), a simpler
    compromise design — always send context_names and let
    ConvertTo-PfbApiError surface the array's own error — would work, and most
    of the local capability-map gating for this parameter could be dropped.
  • If the array silently accepts/ignores it, that compromise doesn't work, and
    local gating (with a hard throw, not a silent skip) is required.

Tested against FB-A (lab sim, REST 2.26) and, for the version-boundary case,
against a second sim discovered to be genuinely running old Purity//FB 4.6.5
(REST ceiling 2.22) — both raw REST and, for the second round, through the
actual module code path (Connect-PfbArray + the private Invoke-PfbApiRequest),
not synthetic HTTP calls:

Case How tested Result
Endpoint that has never supported context_names in any indexed spec version (GET/POST/PATCH/DELETE /alert-watchers) Real create + patch + delete round-trip on FB-A via raw REST, and a GET via the module's Invoke-PfbApiRequest against the REST-2.22 array Silently accepted. HTTP 200 every time; real mutations (create/update) actually applied; zero error or mention of context_names anywhere.
Endpoint where context_names was introduced at a later version than the one requested (GET /admins, introduced 2.22, called via /api/2.10/; GET /dns, introduced 2.25, called via /api/2.22/) Raw REST on FB-A at an old version prefix; also via the module with -ApiVersionOverride Cleanly rejected on the wire, HTTP 400, code 24, "Parameter context_name is not supported in the target version." At/after the introduction version, a different, unrelated error appears (code 42, "Cannot find array in fleet"), showing the param was actually parsed.
Same "too new for this version" case, but through Assert-PfbApiCapability directly (module-level, no network call) Invoke-PfbApiRequest against both the REST-2.22 array (real version) and FB-A forced to -ApiVersionOverride '2.22' Threw locally before any request left the machine, identical message in both cases: "parameter 'context_names' on GET /dns requires REST 2.25 (Purity//FB 4.8.0), but the connected array is running REST 2.22 (Purity//FB 4.6.5)."

Conclusions from this:

  1. The "always send, let the array's error surface" compromise doesn't work
    as a general strategy — it only works for the "recorded but array too old"
    case, which Assert-PfbApiCapability already catches locally today with a
    clear, well-formed error (see row 3). It does not work for the "endpoint
    never supports this param, in any version" case (row 1) — the array just
    swallows it. Since that's precisely the case where the design's step 3 also
    chooses silent behavior (skip injection instead of throwing), the two gaps
    compound: local logic stays silent, and the wire stays silent too. Nothing in
    the stack ever tells the caller their context was dropped.
  2. Assert-PfbApiCapability's version gate is a pure comparison against
    whatever version number it's given (real connection version or an explicit
    override) — it has no dependency on what the array's true firmware ceiling
    is. That means design point 4 ("Assert-PfbApiCapability already catches
    the version mismatch") is correct and doesn't need duplicate logic — but
    only covers the recorded-but-too-old case, not the never-recorded case,
    since Assert-PfbApiCapability returns early (no-op) when a param key is
    simply absent from an endpoint's map entry (if (-not $entry) { return } /
    if ($introducedIn -and ...) — no $introducedIn means no check at all).
    Important qualifier, caught in a later pass: this is only true if
    context_names lands in $QueryParams before Assert-PfbApiCapability
    runs. See "Implementation ordering" under the Recommendation below —
    as literally drafted (injection happens "immediately before the request is
    built," i.e. near the URL construction), it runs after the existing
    Assert call, which means Assert would never see the injected parameter at
    all. Our live test doesn't actually exercise the real ordering either: it
    called Invoke-PfbApiRequest with context_names already present in
    -QueryParams, which is not how the drafted injection delivers it.

Recommendation

Change step 3 from "skip injection silently" to a hard throw when a
context is set (session default or ambient override) and the target
endpoint's capability-map entry doesn't list context_names at all. Keep
Assert-PfbApiCapability doing exactly what it does today for the
"recorded but array-too-old" case — that's already correct and verified.
Concretely:

  • Context set + map has no entry for Method Endpoint, or the entry doesn't
    list context_names, and the connected array's version is within the
    map's scanned range
    (see the capability-map-staleness section below for
    why that qualifier matters) → throw, naming the endpoint and telling the
    caller their context could not be honored. Do not send the request.
  • Context set + map lists context_names (any version) → inject it and let
    Assert-PfbApiCapability's existing version check do its job (unchanged from
    the current design).
  • Context set + map has no entry for Method Endpoint, or the entry
    doesn't list context_names
    + the connected array's version exceeds
    anything the map has scanned → do not throw (see below); proceed
    permissively per the module's existing "never block what should work"
    philosophy, backed by the one-shot staleness warning at connect time. (This
    must mirror the first bullet's condition exactly — the likeliest real
    staleness case is an endpoint that exists today and gains context_names
    in some future version, i.e. entry present, param absent, which is an
    "entry doesn't list it" case, not an "endpoint absent from the map" case. An
    earlier draft of this recommendation used the narrower "endpoint absent"
    wording here, which would incorrectly throw on that exact scenario.)
  • No context set → unchanged, no injection, no check.

"Context set" above means the effective resolved context is a non-empty
set of names.
Both $null (nothing ever set) and an explicit empty
override (Invoke-PfbInContext -Context @(), i.e. "run this one call
locally") resolve to "nothing to inject" and must never trigger the
hard-throw — only a real, non-empty context name list does. See the tri-state
note under the object-model section below; without this clarification, the
escape hatch this document recommends elsewhere would throw on exactly the
endpoint it's meant to bypass.

Implementation ordering: injection must precede both the existing Assert-PfbApiCapability call and the pagination loop

Two ordering details make the difference between this recommendation actually
working and silently not doing anything, and neither is obvious from reading
the design doc's prose alone:

  • Injection must happen before line 41's Assert-PfbApiCapability call in
    Invoke-PfbApiRequest, not "immediately before the request is built"
    (the
    design doc's phrasing, which reads as somewhere near the URL construction
    around line 77-80 — after Assert has already run and returned). If
    context_names is added to $QueryParams after Assert executes, Assert
    never sees it, and the "Assert-PfbApiCapability already catches the
    too-old-for-version case" guarantee this document leans on (Recommendation
    bullet 2, Finding conclusion #2) simply doesn't hold in the real code path.
    The resolve-and-inject step, and the new hard-throw check, both need to run
    ahead of the existing line-41 call.
  • Injection must mutate the $QueryParams hashtable itself, not the
    already-built query string.
    Invoke-PfbApiRequest's -AutoPaginate loop
    rebuilds the query string from $QueryParams on every page
    ($QueryParams['continuation_token'] = ... then
    ConvertTo-PfbQueryString -Parameters $QueryParams again). If
    context_names were appended only to the first page's built URI instead of
    written into $QueryParams, it would silently vanish starting on page 2.
    This isn't a hypothetical edge case — Get-PfbArraySpace, a representative
    public cmdlet, already calls Invoke-PfbApiRequest ... -AutoPaginate, so
    any context-aware read hitting a paginated endpoint would be affected.
    Mutating $QueryParams (rather than the string) means the existing
    pagination loop carries context_names forward for free on every
    subsequent page, with no extra code needed there.

This preserves the "zero code change across ~520 cmdlets" property and the
single-choke-point architecture — it only changes what happens in the one
branch that currently says "silently skip."

Apply this uniformly across verbs — including GET — not just to
mutating calls.
It's tempting to soften this to a Write-Warning for reads
(no side effect, so a wrong-scope read feels less dangerous than a wrong-scope
write). That doesn't hold up: -WarningAction SilentlyContinue/
$WarningPreference = 'SilentlyContinue' is routine in exactly the kind of
scripted, scheduled, or CI automation most likely to set a read-scoped context
in the first place, so a warning is easy to suppress right back into silence.
Worse, a single blocked write is one loud, obvious failure; a silently
wrong-scoped read inside a loop over many fleet members quietly corrupts a
result set with no way to tell which rows are wrong after the fact — arguably
more dangerous in aggregate than the write case, not less. And critically, a
GET's output is routinely piped or fed into a subsequent mutating call (list
then delete-by-name, get a filesystem then patch it) — a wrong-scope read
isn't just bad output, it's a live payload that can propagate straight into
the next write. Throwing on every verb, with Invoke-PfbInContext -Context @() (or Clear-PfbContext) as the explicit, low-friction way to opt out of
context for a specific call once someone actually wants to, covers the
legitimate "I don't want to be interrupted" case without reopening the silent-
wrongness hole this whole feature exists to close.

Capability-map staleness: don't let the hard-throw contradict the module's own philosophy

Assert-PfbApiCapability's docstring states the module's founding principle
plainly: "A capability check must never be the reason a call that would
otherwise succeed gets blocked."
The existing logic backs this up everywhere
— absence from the map is always treated as permissive, never as confirmed
rejection. The hard-throw recommendation above, applied naively, breaks that
principle: if a customer's array is running firmware newer than anything the
capability map has ever been built from, and that firmware happens to have
added context_names support to an endpoint the map has never scanned, a flat
"absent from map → throw" would block a call that would have actually
succeeded — exactly the failure mode the module was designed never to cause.

The fix doesn't require abandoning the hard-throw; it requires making it
conditional on something the map already records but Assert-PfbApiCapability
currently ignores: Data/PfbCapabilityMap.json's generatedFrom array (e.g.
["2.0", "2.1", ..., "2.27"]), which lists exactly which REST versions were
scanned to build it.

  • Connected array's version is within the map's scanned range (e.g. REST
    2.20, map scanned through 2.27) and the endpoint has no context_names
    entry → that absence is confirmed, not just unknown. We looked at that
    exact version's spec and it wasn't there. Throwing here blocks nothing that
    should work.
  • Connected array's version exceeds the map's scanned range (e.g. REST
    2.28, map only ever scanned through 2.27) → we have no evidence either way.
    The map simply hasn't caught up to that firmware. This must stay
    permissive — proceed without injecting/throwing — to honor the module's
    existing philosophy.

A connect-time warning, generalized beyond context_names

The permissive fallback above trades a hard block for silent uncertainty,
which isn't fully satisfying either — so surface it instead of staying quiet
about it. And this problem isn't actually specific to context_names: it's
the general fact that the module's capability knowledge can lag behind a
customer's actual Purity//FB release, and context_names is simply the
parameter where that lag has a silent (rather than a clean wire-level 400)
failure mode, so it's the sharpest instance of a broader issue, not the whole
of it.

Recommended mechanism:

  • Check once, at Connect-PfbArray, unconditionally — regardless of
    whether -Context is passed. Compare the negotiated $Array.ApiVersion
    against the map's generatedFrom max. This is a single cheap comparison,
    computed once and cached on the connection object (e.g.
    .ExceedsCapabilityMapCoverage), not recomputed per call.
  • One-shot per connection. Track a flag (e.g.
    .ContextMapCoverageWarned) so it fires exactly once for the life of a
    connection object, no matter how many calls, Set-PfbContext invocations,
    or Invoke-PfbInContext blocks follow.
  • Firing at Connect-PfbArray universally (not gated on -Context being
    set) means Set-PfbContext/Invoke-PfbInContext don't need their own
    separate trigger
    — by the time either is used, the connection has already
    been checked and, if relevant, already warned. The only reason to keep a
    defensive check at those two points is in case a connection object somehow
    bypassed the connect-time logic (e.g. one cached from before an upgrade);
    cheap insurance, not a separate design pillar.
  • Message content is concrete, not generic, and names the sharp case
    explicitly, e.g.: "Connected array is running REST 2.28; this module's
    capability map only covers through REST 2.27 — capability checks for
    anything newer than 2.27, including context scoping, can't be fully
    verified against known data and may not error even if unsupported. Check
    the PowerShell Gallery for a newer release of this module (Update-Module <ModuleName>)."
  • No live PowerShell Gallery lookup. Don't call Find-Module to check
    whether a newer version is actually published — that's a network
    dependency with real latency and failure modes, and this module gets used
    against air-gapped lab/customer arrays where a Gallery call could hang or
    error for reasons unrelated to the actual warning. Point at the mechanism
    (Update-Module) as a suggestion; don't try to auto-detect whether an
    update genuinely exists.

Object-model recommendation: Connect-/Set-/Clear-PfbContext + Invoke-PfbInContext

The finding above focused on step 3's silent-skip behavior. Working through the
surrounding cmdlet surface in discussion surfaced four more structural
questions, which we ran past an independent architecture consult rather than
just guessing:

  1. Set-PfbContext/Clear-PfbContext are deferred to "Phase 3, only if
    asked for" with no other stated justification. But without them, once a
    caller does Connect-PfbArray -Context 'member-b', every call on that
    connection is stuck scoped to 'member-b' (or some other explicit
    context) for the life of the object — the only way to run something
    unscoped on the same connection is to reconnect. That argues for pulling
    them into Phase 1. (It's also, if anything, less work than
    Invoke-PfbInContext — Phase 2 — since it doesn't need scriptblock scoping
    or exception-safety, which makes the current Phase 2/3 ordering look like
    it wasn't driven by implementation effort.)
  2. If Set-PfbContext mutates .DefaultContext in place on the shared
    $Array object, any other code holding the same reference (a helper
    function, a concurrent loop iteration, a caller further up the stack)
    silently sees its scope change out from under it — the identical
    "silently wrong scope" failure mode this whole feature exists to prevent,
    just relocated from the REST layer into object mutation.
  3. The doc describes Invoke-PfbInContext's override as "a script-scoped
    override." Taken literally, that's a single global slot, not one per
    connection and not stack-shaped — so nested Invoke-PfbInContext calls
    would restore to "no override" instead of the outer override, and using it
    on one connection could leak onto an unrelated second connection active in
    the same block.
  4. Whatever storage the ambient override uses lives in one module instance in
    one runspace. ForEach-Object -Parallel, Start-ThreadJob, Start-Job,
    and runspace-pool workers each get independent module instances/session
    state, so an ambient override set in the calling runspace is invisible to
    work dispatched into another one, even via $using:fb.

The core decision: context state lives on the connection object, not module script scope

Two separate properties on $Array, governed by two different mutation
policies:

  • .DefaultContext — durable session default. Copy-on-write.
    Set-PfbContext/Clear-PfbContext return a new connection object; they
    never mutate .DefaultContext on the object the caller passed in.
  • .ContextOverride — the ambient, block-scoped value
    Invoke-PfbInContext sets. Mutable, but scoped by construction and restored
    via try/finally.

Invoke-PfbApiRequest resolves the effective context exactly once, at the top
of the choke point, reading only from the $Array it was handed:

explicit -QueryParams['context_names']  >  $Array.ContextOverride  >  $Array.DefaultContext  >  none

Putting this state on the object rather than in $script:-scoped module
state
is the single decision that resolves problems 2–4 above:

  • Cross-connection leakage (problem 3) disappears structurally. A call
    made with $fb1 reads $fb1.ContextOverride; a call made with $fb2 reads
    $fb2.ContextOverride — physically different slots, no keying logic to get
    wrong. A side-channel keyed by connection identity (e.g. a hashtable in
    module scope keyed by a GUID) would also fix this, but only if the keying is
    implemented correctly, and it does nothing for the next point.
  • Runspace portability (problem 4) gets real mitigation, not just a
    documented limitation.
    $using:fb passes the same live object instance
    into ForEach-Object -Parallel/Start-ThreadJob (in-process) — those
    workers see .ContextOverride/.DefaultContext correctly. Start-Job /
    remoting CliXml-clones the object at dispatch time, so the child sees the
    context as it stood at that moment — exactly what's intended for a fan-out.
    This only works because the state travels with the object; the drafted
    script-scoped design carries nothing across either boundary and silently
    falls back to none. One residual caveat remains and should be documented:
    concurrent workers mutating .ContextOverride on the same shared object
    in parallel would still race — the guidance is "set context before forking
    parallel work, don't push ambient overrides from inside concurrent workers
    on a shared connection."

This also isn't a new pattern for the module — Invoke-PfbApiRequest already
writes AuthToken/BearerToken/TokenExpiresAt back onto the same $Array
instance during auto-reconnect. The distinction that resolves problem 2 is
that token refresh is a transparent mutation (doesn't change where a call
lands), while context is a targeting mutation (changes which fleet member a
write hits) — different risk classes, different mutation policy, not an
inconsistency.

Set-PfbContext / Clear-PfbContext: copy-on-write, always-return, no -PassThru

Set-PfbContext
    [-Array] <PSCustomObject>   # pipeline input; defaults to the current default connection
    [-Context] <string[]>       # Mandatory
    → always returns the new connection object (not opt-in via -PassThru)

Clear-PfbContext
    [-Array] <PSCustomObject>   # pipeline input; defaults to the current default connection
    → always returns the new connection object
  • Copy, not mutate. Closes problem 2: a helper function, an outer stack
    frame, or a loop iteration holding the old $fb reference keeps its
    original scope; only the caller who captures the return value
    ($fb = Set-PfbContext -Array $fb -Context 'b') sees the change.
  • Always return the new object; skip -PassThru. -PassThru is the
    convention for cmdlets whose primary effect is a side effect and whose
    output is optional. Here the output is the effect — making it mandatory
    is the honest signature, even though it's a less familiar idiom than a
    typical in-place Set-* cmdlet.
  • Must swap the cache pointer, not just return a new object: if the
    module tracks connections in a $script:PfbArrays endpoint-keyed cache and
    a $script:PfbDefaultArray pointer (as Connect-PfbArray already does for
    reconnect/token-refresh bookkeeping), Set-PfbContext/Clear-PfbContext
    need to update those pointers to the new copy. Otherwise callers using the
    implicit default connection (no -Array on their cmdlet call) would keep
    hitting the old, unchanged object even after Set-PfbContext "succeeded."
  • Clear-PfbContext ships as its own cmdlet, not Set-PfbContext -Context $null. Two reasons: it matches the module's own existing
    Set-PfbCredential/Clear-PfbCredential naming precedent, and $null/@()
    needs to mean something distinct — "explicit no-context for this scope" —
    at the Invoke-PfbInContext layer (see tri-state note below), so
    overloading it as "clear" at the Set-PfbContext layer too is ambiguous.

Invoke-PfbInContext: correct nesting without an explicit stack

function Invoke-PfbInContext {
    param(
        [Parameter(Mandatory)][PSCustomObject]$Array,
        [Parameter(Mandatory)][AllowEmptyCollection()][string[]]$Context,
        [Parameter(Mandatory, Position=0)][scriptblock]$ScriptBlock
    )
    $previous = $Array.ContextOverride
    $Array.ContextOverride = $Context
    try     { & $ScriptBlock }
    finally { $Array.ContextOverride = $previous }
}
  • Nesting works with no explicit Stack[] object — each invocation
    captures its own $previous local before overwriting, so the PowerShell
    call stack itself provides push/pop discipline. The inner block's finally
    restores the outer block's value, not "cleared." This directly answers gap
    #1 above.
  • Exception-safe via finally — this is the specific behavior the test
    plan (gap #2) needs to prove: throw from inside a block, and from inside a
    nested block, and assert .ContextOverride is restored correctly at each
    level, not just on the happy path.
  • Per-connection by construction — fixes the leakage case in gap #3 with
    no keying logic, per the object-placement decision above.

Tri-state "none" matters

Distinguish unset from explicitly no-context, so a caller can say "session
default is member-b, but run this one block locally":

  • unset → .ContextOverride is $null
  • explicit no-context → [string[]]@() (non-null, empty) — hence
    [AllowEmptyCollection()] on Invoke-PfbInContext -Context, and the
    resolver checks -ne $null, not truthiness.

An empty-array resolution means "resolved to local, deliberately" — a
decision, not the silent skip the design's step 3 (and our main finding above)
needs to stop doing. Both this case and the unset ($null) case resolve to
"nothing to inject" and must be excluded from the hard-throw gate — only a
non-empty resolved context list is subject to the capability-gated hard-throw

from the Recommendation section above. This distinction is what makes the
Invoke-PfbInContext -Context @() escape hatch actually work as an escape
hatch: using it on an endpoint that doesn't support context_names must
resolve to "nothing to inject, run locally, no throw," not accidentally
re-trigger the very check it exists to bypass.

Test-plan additions (beyond gap #2's list)

  • Three-level precedence resolution, including the empty-array "explicit
    none" case.
  • Nested Invoke-PfbInContext restoring the outer value, including when the
    inner block throws.
  • Cross-connection isolation: an override set via $fb1 must not affect calls
    made with $fb2 inside the same block.
  • Set-PfbContext leaving the original object's .DefaultContext untouched
    while the cache/default pointer moves to the new copy.

Answers to the doc's open questions (from this investigation)

  • Q2 ("does the capability map already record context_names per
    endpoint?"): Yes, confirmed directly — Data/PfbCapabilityMap.json has
    real context_names entries with introduction versions ranging from 2.17
    through 2.26 across dozens of endpoints, populated automatically by
    tools/Build-PfbCapabilityMap.ps1's existing full-rescan-every-version
    process. No new map-building work is needed for this design.

Not pursued further

An attempt to isolate whether Assert-PfbApiCapability's clean-rejection
behavior on the wire (row 2 above) requires the array's live firmware to
already know about a later version's field, versus being independent of true
firmware capability, hit a dead end: the only pre-existing (not
brand-new-in-2.26) endpoint where context_names was introduced at exactly
REST 2.26 (POST/PATCH/DELETE /management-access-policies) turned out to be
RBAC-restricted — a 403 Access Denied, verb-independent, identical regardless
of which admin account was used. That narrow question was dropped as
inconclusive; it doesn't change any of the recommendations above, since the
module-level behavior (the part that actually matters for this design) was
verified directly and doesn't depend on the answer.

Follow-up questions for Don

  • "Explicit query param wins" — wins over what, concretely? The precedence
    order is explicit -QueryParams['context_names'] > Invoke-PfbInContext > connection default > none, but today nothing in the public API surface can
    populate that top tier: every one of the ~520 public cmdlets builds its own
    fixed, named QueryParams hashtable internally (checked Get-PfbArraySpace.ps1
    as a representative example), and none of them expose context_names or any
    generic passthrough parameter. Invoke-PfbApiRequest is private, so the only
    way to hit that precedence tier is either module-internal code calling it
    directly, or some future cmdlet that doesn't exist yet. Was this written with
    a specific future cmdlet in mind (e.g. something fleet-membership-aware that
    takes its own -Context and deliberately bypasses the ambient/session
    mechanisms), or is it defensive/future-proofing layering with no concrete
    plan behind it yet? If the latter, worth saying so explicitly in the doc so a
    reader doesn't assume there's already a way to reach it.

Notes for a later pass (not blocking, worth addressing before merge)

  • Error surfacing for fleet-membership failures is undefined. Our own
    live test showed the array returns code 42 "Cannot find array in fleet"
    for a context name it can't resolve. As drafted, that flows through
    ConvertTo-PfbApiError and surfaces as a bare
    "FlashBlade API error: Cannot find array in fleet" — no indication of
    which context name caused it, or that it came from a session default the
    caller may have set several calls (or scripts) earlier and forgotten about.
    Worth deciding whether the injection layer should annotate context-targeting
    failures with the active context name, or leave this as a known rough edge.
  • The security/authorization model of targeting an arbitrary fleet member
    isn't discussed anywhere in the design.
    context_names is not itself an
    auth boundary — any caller with a valid management connection can attempt
    to target any fleet member by name, and whatever protection exists (or
    doesn't) is entirely server-side (the RBAC 403 we hit while testing
    /management-access-policies being one example of that server-side
    enforcement in action). Worth a sentence in the design doc stating this
    explicitly, so nobody reading it assumes the client enforces anything about
    which contexts a caller is allowed to target.
  • Invoke-PfbInContext's scriptblock form can't be used in a pipeline
    (Get-Pfb… | Invoke-PfbInContext doesn't make sense with the drafted
    signature). Worth noting explicitly — and worth pointing out that pulling
    Set-PfbContext into Phase 1 (already recommended above) actually gives
    callers a pipeline-friendly alternative for the durable case
    ($fb = $fb | Set-PfbContext -Context 'b'), even though the ambient,
    scriptblock-scoped mechanism will never be pipeline-friendly by its nature.

Multi-value context_names: a real API split the design doc treats as an open policy choice

Open question #3 in the draft asks: "Multiple contexts: the REST param is
plural. Do we support fan-out to several members in one call, or constrain to
one for now?"
— phrased as if this is a free design decision. It isn't. The
OpenAPI spec (tools/specs/fb2.27.json) defines two distinct parameter
components
, both surfacing as context_names at the wire level, with
different cardinality contracts:

  • Context_names_get (139 references — GET, plus exactly one DELETE):
    "Performs the operation on the unique contexts specified... each
    context name must be the name of an array in the same fleet... Enter
    multiple names in comma-separated format."
    Real, documented multi-target
    fan-out.
  • Context_names (234 references — POST, PATCH, and almost every
    DELETE): "Performs the operation on the context specified. If
    specified, the context names must be an array of size 1..."
    Same JSON
    Schema (type: array, items: string) as the multi-value variant — the
    size-1 restriction is expressed only in free-text description, not a
    structured maxItems constraint, so it isn't mechanically detectable the
    way the rest of the capability map's data is.

The split correlates almost perfectly with verb — every POST/PATCH uses
the size-1 variant, and every DELETE does too except one:
DELETE /management-access-policies, which uses the multi-value variant. That
endpoint is also the one this review already flagged in "Not pursued further"
as the sole pre-existing endpoint where context_names was introduced at
exactly REST 2.26 and turned out to be RBAC-restricted — so it's already a bit
of an outlier. Given it's such a recent addition, this may well be a
documentation/spec error rather than an intentional exception — not something
we can safely resolve by testing (we can't tell "the array honored 2 context
names correctly" apart from "the array silently used only the first one and
ignored the second" without much more invasive verification than seems
warranted for one edge case). Flagging for Don to confirm directly whether
this is real or a spec/doc bug
, rather than guessing further.

Recommended interim design, pending that confirmation: treat GET as
multi-context-capable and POST/PATCH/DELETE as single-context-only,
uniformly, with the one known DELETE exception called out in code comments so
it isn't silently mishandled either way once Don confirms which way it goes.
This also surfaces a real, additional gap in the capability map itself: it
currently records only paramName: introducedVersion per endpoint, with no
concept of a cardinality constraint at all — closing that properly would mean
teaching tools/Build-PfbCapabilityMap.ps1 to distinguish which of the two
spec components an endpoint references (or, short of that, hardcoding the
verb-based heuristic above with the one exception as a documented special
case).

Follow-up question for Don: should mutating calls with a multi-value context silently fan out, or fail loud?

If a caller sets a multi-value context (session default via Connect-PfbArray -Context @('a','b'), or Set-PfbContext) and then invokes a cmdlet that maps
to a size-1-only endpoint, should the module silently issue one API call per
context name to make the write-side limitation invisible, or should it throw
and tell the caller to narrow to one context for that call (e.g. via
Invoke-PfbInContext -Context 'a' { ... })?

Leaning toward throw, not silent fan-out, for three reasons:

  1. It silently changes the return shape. Every mutating cmdlet today
    returns one object per call. Client-side fan-out would turn that into an
    array of N objects the moment someone upstream sets a multi-value context
    — a return-type change triggered by session state the calling code may
    have no visibility into, breaking any script written assuming a scalar
    result.
  2. Partial failure has no clean silent answer. If the write succeeds on
    one context and fails on another, there's no non-terminating way to report
    that doesn't either discard a real failure or require the caller to
    defensively inspect every result — which isn't actually invisible, it just
    moves the burden.
  3. It isn't actually equivalent to the server's native read-side fan-out.
    The array resolves a multi-context GET as one atomic request. N separate
    client-issued write requests have no such atomicity, no all-or-nothing
    guarantee, and (per the object-model section's retry discussion) no
    protection against a retry re-attempting whichever ones already succeeded.
    Presenting client-side looping as equivalent to the server's real
    capability would be misleading.

Alternative if the ergonomics matter enough to Don: an explicit, separately
named opt-in
(a distinct helper cmdlet, not automatic behavior triggered by
cmdlet-plus-context-shape alone) that returns a clearly multi-object,
per-context-annotated result and surfaces partial failures loudly rather than
folding them into a plain return value — rather than "throw" being the only
option on the table.

@juemerson-at-purestorage

Copy link
Copy Markdown
Collaborator

Follow-up: empirical audit of endpoints missing context_names from the capability map

As a sanity check on the "silent skip-injection" failure mode discussed above, I live-tested all 113 GET endpoints that have never had context_names recorded in the capability map (at REST ≤2.26) against a lab array, each called with a bogus context_names value.

Result: 90 of 113 silently accept the parameter (no error, no mention of context/fleet anywhere in the response) — consistent with the map-gap failure mode this thread is about. This spans config, hardware, security, and even the fleet-management surface itself (e.g. /fleets, /fleets/members).

2 endpoints actually process context_names despite the map never recording it for them — i.e., real, confirmed map gaps, not just staleness:

  • GET /audits — returns code:42, "Cannot find array in fleet".
  • GET /snmp-managers/test — a bogus context_names changes the response from the normal "names or ids query parameter is required" error to "Filtering and sorting are not supported for this request", showing the endpoint specifically recognizes and rejects the param (a different rejection style than /audits, but the same underlying gap).

These reinforce the point made earlier: the map can be wrong even within its fully-scanned version range, not just behind on newer versions — which is a factor for however staleness-detection/warning ends up being scoped.

21 endpoints were inconclusive — each returned an error unrelated to context_names (verified identical with/without the parameter), usually a missing required parameter, a product/feature gate, or a pre-existing bug on that endpoint. Listed for completeness, no action implied:

  • /file-systems/groups/performance
  • /file-systems/users/performance
  • /logs
  • /logs-async/download
  • /network-interfaces/ping
  • /network-interfaces/trace
  • /rapid-data-locking/test
  • /arrays/space/storage-classes
  • /file-systems/space/storage-classes
  • /realms/space/storage-classes
  • /fleets/fleet-key
  • /remote-arrays
  • /object-store-roles/object-store-trust-policies/download
  • /nodes
  • /node-groups
  • /node-groups/nodes
  • /node-groups/uses
  • /resiliency-groups
  • /resiliency-groups/members
  • /alert-watchers/test
  • /network-interfaces/connectors/settings

(Only GET endpoints were tested so far; POST/PATCH/DELETE remain unaudited.)

@juemerson-at-purestorage

Copy link
Copy Markdown
Collaborator

Pipeline ergonomics: Get-PfbFleetMember → context

A natural thing a caller will reach for is discovering fleet members and piping
them straight into a context cmdlet:

Get-PfbFleetMember -FleetName 'fleet-prod' | Set-PfbContext
Get-PfbFleetMember -FleetName 'fleet-prod' | Invoke-PfbInContext { Get-PfbFileSystem }

Neither works with the cmdlet shapes drafted above, for structural reasons worth
recording — and the fix points at a small change to Get-PfbFleetMember itself.

Why it doesn't bind today

  1. The name you want is nested, so nothing binds by property name.
    Get-PfbFleetMember returns raw FleetMember objects whose top-level
    properties are coordinator_of, fleet, member, status, and
    status_details. The array name a context needs lives at .member.name,
    one level down. ValueFromPipelineByPropertyName only matches top-level
    property names against a parameter (or its aliases); there is no top-level
    string property for -Context [string[]] to grab, so the pipe binds nothing
    and silently no-ops — the same silent-wrong-scope family this whole review is
    trying to close off.
  2. Set-PfbContext as drafted spends its pipeline slot on the connection, not
    the context.
    The object-model section above gives Set-PfbContext an
    -Array <PSCustomObject> that takes pipeline input (to support
    $fb | Set-PfbContext -Context 'b'). A FleetMember is also a
    PSCustomObject, so piping members in would bind each member object ByValue
    to -Array (wrong) and never reach -Context. Because the module only does
    property-name pipeline binding — PSTypeName-based typing was deliberately
    deferred (see the pipeline typed-object-model decision) — there is no
    type-based way to disambiguate two PSCustomObjects arriving on the same
    pipeline.
  3. Invoke-PfbInContext can't be a pipeline target at all. Its pipeline
    payload would have to be the scriptblock, and Get-PfbFleetMember doesn't
    emit one. This matches the "Notes for a later pass" observation that the
    ambient/scriptblock form "will never be pipeline-friendly by its nature."

Works today, no code change (document as the canonical form)

Member enumeration projects the name out of every returned object, which covers
both cmdlets without any new binding:

$fb = Set-PfbContext -Array $fb -Context (Get-PfbFleetMember -FleetName 'fleet-prod').member.name

Invoke-PfbInContext -Array $fb -Context (Get-PfbFleetMember -FleetName 'fleet-prod').member.name {
    Get-PfbFileSystem -Array $fb
}

This is the blessed form for Invoke-PfbInContext, which stays non-pipeable by
design. It's also the fallback for Set-PfbContext regardless of what we do
below.

Recommendation: make Get-PfbFleetMember | Set-PfbContext a first-class flow

Three coordinated pieces, all small:

  • (a) Teach Get-PfbFleetMember to emit more useful, top-level properties.
    This is the crux, and it's worth doing on its own merits, not just for context
    binding. As written, the cmdlet hands back the raw FleetMember with the two
    values anyone actually wants (member.name, fleet.name) buried one level
    down and awkward at the console. Decorate each emitted object with top-level
    MemberName (= $_.member.name), FleetName (= $_.fleet.name), and a
    surfaced IsLocal (= $_.member.is_local) NoteProperty. That single change
    makes the object readable in a table view and gives the pipeline something
    top-level to bind — the "useful properties" fix and the "bind at the pipeline"
    fix are the same fix. (_fixedReferenceWithIsLocal is the ref schema behind
    both member and fleet, so is_local is genuinely available to surface.)

  • (b) Set-PfbContext -Context binds by property name, with aliases:

    [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName)]
    [Alias('MemberName','Name')]
    [string[]]$Context

    With (a) in place, MemberName on the piped object binds straight to
    -Context.

  • (c) Give the pipeline slot to the context, drop it from -Array, and
    aggregate.
    -Array becomes an ordinary parameter defaulting to the current
    default connection (pass -Array $fb explicitly for a non-default
    connection), freeing the ByValue pipeline slot for context names/objects.
    Because Set-PfbContext is copy-on-write, it must accumulate piped items in
    process{} and emit exactly one new connection in end{}
    , scoped to the
    union — otherwise N piped members yield N connection objects (last-one-wins
    confusion) instead of one connection scoped to all of them.

Result:

$fb = Get-PfbFleetMember -FleetName 'fleet-prod' | Set-PfbContext          # default connection
$fb = Get-PfbFleetMember -FleetName 'fleet-prod' -Array $fb |
        Set-PfbContext -Array $fb                                          # explicit connection

Preference: drop $fb | Set-PfbContext as redundant

Piece (c) trades away the $fb | Set-PfbContext -Context 'b' connection-piping
idiom the object-model section proposed. That trade is the right call — take
it.
Piping the connection object into Set-PfbContext is redundant with two
forms that already exist and read at least as clearly: Set-PfbContext -Array $fb -Context 'b' (explicit) and the implicit default-connection form. It buys
nothing that isn't already covered, and it's the only thing standing between
us and the far more valuable Get-PfbFleetMember | Set-PfbContext ergonomic.
Keeping a redundant connection-pipe just to preserve a Set-*-takes-object
convention would be trading a real, discoverable workflow for a stylistic nicety
callers won't miss. Drop it, and hand the pipeline to the context.

Two caveats that fall out of the object shape

  • Filter out the local array. Get-PfbFleetMember returns the local array
    as a member too. Piping all members sets context to include the local array,
    which is redundant at best. Surfacing IsLocal per (a) makes the idiomatic
    form easy: Get-PfbFleetMember -FleetName x | Where-Object { -not $_.IsLocal } | Set-PfbContext.
  • Multi-value hits the write-side limit. Piping many members yields a
    multi-value context, which is valid for GET (server-side fan-out) but
    size-1-only for POST/PATCH/DELETE (see the multi-value section above). A
    subsequent mutating call under a many-member context would trip the
    "throw, narrow to one" behavior. So "pipe all members into a durable context"
    is really a read-scoping ergonomic, and should be documented as such.

Net

Adopt the .member.name projection form as the documented answer for both
cmdlets now (needs nothing new); build pieces (a)–(c) so
Get-PfbFleetMember | Set-PfbContext becomes first-class; drop
$fb | Set-PfbContext as redundant; leave Invoke-PfbInContext non-pipeable by
design with the projection form as its blessed equivalent. Piece (a) —
Get-PfbFleetMember emitting MemberName/FleetName/IsLocal at top level —
is worth doing independently of the context feature and is the enabler for the
whole pipeline flow.

@juemerson-at-purestorage

Copy link
Copy Markdown
Collaborator

Follow-up: folding allow_errors into the context design

While looking at the multi-value context_names question above, it's worth calling out allow_errors as a closely related parameter that the design should account for now rather than bolt on later.

Empirically, it's a sibling of the multi-value context parameter, not a general-purpose flag. Across the 2.27 spec, allow_errors appears on 135 endpoints, and 134 of those 135 also carry Context_names_get (the multi-value fan-out variant). The mismatches are informative rather than noise:

  • 1 exception on the allow_errors side (PATCH /directory-services/test — no multi-value context) — minor, likely just a test-endpoint quirk.
  • 5 exceptions on the context_names_get side (topology-groups, topology-groups/arrays, topology-groups/members, presets/workload) — these describe the fleet's topology itself rather than fanning out N independent array-level reads, so there's no "one array errored" case for allow_errors to apply to. Makes sense they'd be excluded.

(Also: since confirming DELETE /management-access-policies's multi-value context_names is a documentation bug that's being fixed — good to know, thanks for chasing that down — it's worth noting that endpoint also lacked allow_errors, consistent with it not actually being a real multi-context-capable delete.)

Recommendation: treat allow_errors as a sibling of -Context, surfaced through the same object model, not a separate feature.

  1. Same two-tier surfacing as context. Rather than a bare per-cmdlet switch, add a durable connection-level default (e.g. .DefaultAllowErrors, set via Set-PfbContext -AllowErrors) plus a block-scoped override through Invoke-PfbInContext. "Tolerate individual array failures while exploring the fleet" is a session-level mode a user sets once, not something to restate on every call — exactly the reasoning already applied to -Context itself.

  2. Default $true only when a multi-value context is actually being sent; never send it otherwise. This deliberately diverges from the API's own raw default (false), matching PowerShell's native idiom for multi-target operations — Get-ChildItem with several -Path values, Get-Process -ComputerName with several hosts — where one bad target degrades gracefully instead of failing the whole call. Consistent with the "don't inject a parameter that wasn't asked for" principle already in this design: with a single-value or no context, allow_errors is irrelevant and shouldn't be sent.

  3. Surface per-array failures loudly but non-fatally. When allow_errors=true and the response body's errors array has entries, the request layer should emit one non-terminating Write-Error per failed array (naming the array/context in the error record) while still passing the successful arrays' results down the pipeline. This is the same throw-vs-silent principle from earlier in this review, just applied at per-array granularity within a single fan-out call instead of at the whole-call granularity.

Implication for the object model: this pushes -Context away from being a bare string/array parameter and toward a small PfbContext value object — names plus AllowErrors — since the two are wire-coupled closely enough (134/135) that designing them independently would likely mean revisiting the object model shape again once allow_errors support gets added. Worth deciding the shape of that object now, even if AllowErrors support isn't implemented in the first pass.

@juemerson-at-purestorage

juemerson-at-purestorage commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

Live verification: context_names / allow_errors / topology-group design, confirmed against a real 3-array fleet

The big shift that prompted this round of testing

In a conversation and design review with Wes, it came out that context_names isn't only ever an array — it can also target a Fleet or a Topology Group. The Workload Preset cmdlets target a Fleet, and the Topology Group cmdlets target both a Fleet and (optionally) a parent Topology Group. Critically, targeting a Fleet or Topology Group is not a shorthand for "fan out to all arrays in it" — it addresses the group/fleet object itself (e.g., a Workload Preset is stored at the fleet level, not on any one array). Fanning out to member arrays turns out to be a distinct, separate mechanism (the undocumented .arrays context suffix).

That reframes -Context from "a single array-shaped value" to a small family of context kinds with different fan-out semantics — which is what all of the testing below was built to pin down empirically, rather than continuing to guess from the spec text (which doesn't document the .arrays suffix at all).

Built a real Fusion fleet (cc-test-fleet, spanning three lab arrays) to test this live instead of continuing to reason from docs alone. Results confirm the proposed design and surface two more things worth folding in.

1. Bare name vs. .arrays suffix — confirmed, exactly as discussed

context_names value on an array-scoped endpoint (/arrays) on a fleet-scoped endpoint (/presets/workload)
(not specified) self empty result (no presets defined)
bare fleet name rejected"Cannot specify context that is a fleet" works — targets the fleet-level object itself
bare array name works — switches context to that array rejected"Invalid context."
bare topology-group name rejected"Cannot specify parameter <name> for remote execution requests" n/a
<fleet-name>.arrays works — fans out to every array in the fleet n/a
<topology-group-name>.arrays (tested with a 2-of-3-array group) works — fans out to exactly the group's members, correctly excluding the array not in the group n/a

This confirms the two-part shape already proposed: a Kind (Array / Fleet / TopologyGroup) plus a separate fan-out flag (the .arrays suffix), rather than trying to infer fan-out from Kind alone. It also confirms each resource type only accepts the context kind it's actually scoped to — array-scoped endpoints flatly reject a bare fleet or topology-group name, and fleet-scoped endpoints flatly reject a bare array name. Worth having the object model (or the request layer) validate this client-side, since the server-side error messages are inconsistent in phrasing (a fleet gets a specific "is a fleet" message; a topology group gets a generic "not valid for remote execution" message) even though both are being rejected for the same underlying reason.

(Every "works" row above, including the single bare-array-name row, was run under an LDAP-authenticated admin — see finding 2 below for why that matters.)

2. New finding: any use of context_names requires a dynamic-authorization-model admin

Correction to an earlier version of this comment: it previously stated that a single-value context switch to a different array worked fine under the local pureuser account. That's wrong — rechecked against the raw test output, that call fails too, with the same "Operation not permitted" as the multi-array/.arrays cases. There's no single-vs-multi distinction here at all: every context_names call that targets anything other than the connected array's own local context failed under pureuser, and succeeded with an identical call once connected as an LDAP-authenticated admin.

This turns out to be documented, not just an empirical quirk — a Fusion architecture doc states it plainly: "Only AD/LDAP authenticated users are allowed to execute fleet commands or do remote provisioning... Non-LDAP users (pureuser, puresupport, etc.) are disallowed to issue cross-array requests." So this is a blanket rule, not a multi-context-specific one.

Checked whether this is really a "pureuser vs. everyone else" distinction, since that would be simple to special-case but wrong: since 4.5.0, admins can create additional named local users, and those have the exact same privileges as pureuser; the 4.8.1 service-account admin type is also local. All three of these — pureuser, a custom local user, and a service account — share the same authorization_model: static. Only LDAP/SAML remote-user admins get authorization_model: dynamic. So the real gate isn't "is this account named pureuser," it's "is this admin's authorization model static or dynamic" — any static-model admin, not just pureuser, will hit the same wall, on any cross-array context at all.

Recommend a proactive client-side check rather than letting this surface as an opaque REST 400: Connect-PfbArray/Set-PfbContext can read the connected identity's authorization_model (already available via GET /admins) and throw immediately — "targeting a context other than the local array requires a dynamic-authorization-model (LDAP/SAML) admin; static-model admins, including pureuser and other local accounts, are not permitted" — rather than waiting for the array to reject the underlying request with a generic "Operation not permitted." This is a separate precondition from the REST-version gate already in the design doc, and worth documenting explicitly since a user hitting that message has no obvious reason to suspect their auth method, rather than their array or fleet setup, is the cause.

3. allow_errors — confirmed shape, and a concrete implementation requirement it surfaces

Reproduced a genuine partial-fleet failure (a resource that exists on only one of the three arrays) and queried it across all three:

  • allow_errors=false (or omitted): the whole call throws, even though one array had the resource.
  • allow_errors=true: the call succeeds. The response contains an items array (the successful results) and a separate errors array, with each entry carrying a location_context field naming the array where that particular lookup failed.

This matches the recommendation above (surface allow_errors as a sibling of -Context, default $true only for multi-value context, non-terminating Write-Error per failed array). It also surfaces a concrete requirement for the request layer: Private/Invoke-PfbApiRequest.ps1's response handling (~lines 197-206) currently only reads $response.items — it has no branch for $response.errors at all. Right now that's dead code (no shipped cmdlet sends multi-value context_names or allow_errors, so the shape never occurs in practice), but it's a real gap the implementation will need to close: today, if this response shape ever occurred, per-array failures would be silently dropped with no indication to the caller. Flagging it here rather than as a standalone bug, since it's not reachable by anything currently shipped — it's a requirement for this feature's implementation, not an independent defect to fix in isolation.

Incorporates Justin's live-tested review of rev 1:
- Hard-throw instead of silent skip when a non-empty context can't be
  honored (silent skip was proven to land calls on the local array).
- Context state moves to the connection object (.DefaultContext
  copy-on-write, .ContextOverride ambient) instead of module script
  scope, fixing nesting / multi-connection / parallel-runspace leakage.
- Set-/Clear-PfbContext pulled into Phase 1; Invoke-PfbInContext nests
  correctly via captured $previous + try/finally.
- Injection ordering (before Assert-PfbApiCapability, mutate QueryParams
  for AutoPaginate), tri-state none, connect-time staleness warning.
- Multi-value GET vs size-1 write split documented; 3 open questions
  flagged for Don to settle.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants