Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 50 additions & 14 deletions Public/Replication/New-PfbFleetMember.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -10,44 +10,66 @@ function New-PfbFleetMember {
(generated on any array already in the fleet) and a reference to the joining array
itself.

There are two ways to supply that body:

-FleetKey takes the key straight from New-PfbFleetKey and builds the whole body for
you, resolving the joining array's own id with Get-PfbArray against the connection you
are already using. This is the common case, because the array running the cmdlet is by
definition the array that is joining.

-Members passes the request body through unaltered, for the cases the convenience path
does not cover -- enrolling several members in one call, or naming a member other than
the array the connection points at.

The two are mutually exclusive parameter sets, so the engine rejects supplying both
rather than having to pick one silently.

CONFIRMED WIRE-CONTRACT BUG (issue #38): this cmdlet previously sent `fleet_names` and
`member_names` as bare query parameters with no request body at all. `member_names` is
not a valid query parameter for this endpoint (only `fleet_ids`/`fleet_names` are, per
the OpenAPI spec's parameter list for POST /fleets/members) and there was no way to
supply the required fleet key or self-identification, so this cmdlet could never have
succeeded against a real array. -MemberName has been removed; -Members now exposes the
actual request body so a caller can supply the correct shape, e.g.:
`-Members @{ key = $fleetKey; member = @{ id = $thisArrayId } }`.

The fix is verified against the OpenAPI spec (FleetMemberPost schema and the POST
operation's parameter list) but has NOT been live-tested against a real fleet -- that
happens in a later task against a lab array. See docs in issue #38's
issue38-fleetmember-bug-comment.md for the full context.
succeeded against a real array. -MemberName has been removed; -Members and -FleetKey
are what expose the actual request body.
.PARAMETER FleetName
The fleet name to add the member to. Sent as the `fleet_names` query parameter.
.PARAMETER FleetId
The fleet ID to add the member to. Sent as the `fleet_ids` query parameter.
The fleet ID to add the member to. Sent as the `fleet_ids` query parameter. This is the
id form of the same fleet selector as -FleetName, and is unrelated to -FleetKey: the
selector says which fleet, the key authorises the join.
.PARAMETER FleetKey
The fleet key generated on an array already in the fleet, as returned by
New-PfbFleetKey. The joining array's own id is resolved with Get-PfbArray over the same
connection and the `members` body is built from the two, so this call is issued even
under -WhatIf -- it is a read, and without it there is no body to describe.
.PARAMETER Members
Info about the members being added to the fleet, as a hashtable or array of hashtables
-- for example @{ key = "<fleet key>"; member = @{ id = "<this array's own id>" } }.
The `key` is the fleet key generated on any array already in the fleet; `member` is a
reference to the array joining the fleet.
.PARAMETER Array
The FlashBlade connection object. If not specified, the default connection is used.
.EXAMPLE
$key = New-PfbFleetKey -Array $existingMember
New-PfbFleetMember -FleetName "fleet-prod" -FleetKey $key.fleet_key -Array $joiningArray

Joins $joiningArray to "fleet-prod". The joining array identifies itself, so only the
fleet and the key have to be supplied.
.EXAMPLE
New-PfbFleetMember -FleetName "fleet-prod" -Members @{ key = "1fc6297a-5183-4b7a-8d58-0182af1a2b64"; member = @{ id = "10314f42-020d-7080-8013-000ddt400012" } }

Adds this array to "fleet-prod" using the fleet key generated by an existing fleet member.
Adds a member by explicit id, for the cases -FleetKey does not cover.
.EXAMPLE
New-PfbFleetMember -FleetId "10314f42-020d-7080-8013-000ddt400099" -Members @{ key = "key-456"; member = @{ id = "this-array-id" } } -WhatIf

Shows what would happen without actually adding the member, identifying the fleet by ID.
#>
[CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')]
[CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium', DefaultParameterSetName = 'Members')]
param(
[Parameter()] [string]$FleetName,
[Parameter()] [string]$FleetId,
[Parameter()] [hashtable[]]$Members,
[Parameter(Mandatory, ParameterSetName = 'FleetKey')] [string]$FleetKey,
[Parameter(ParameterSetName = 'Members')] [hashtable[]]$Members,
[Parameter()] [PSCustomObject]$Array
)

Expand All @@ -61,9 +83,23 @@ function New-PfbFleetMember {
# outside {id, name, resource_type} -- this makes the array COMPOSITE, not an array of
# references, so it is passed straight through rather than projected into @{ name = ... }.
$body = @{}
if ($PSBoundParameters.ContainsKey('Members')) { $body['members'] = @($Members) }
$self = $null

if ($PSCmdlet.ParameterSetName -eq 'FleetKey') {
$self = Get-PfbArray -Array $Array | Select-Object -First 1
if (-not $self -or -not $self.id) {
throw ("Could not determine this array's own id from Get-PfbArray, so the fleet " +
'member body cannot be built. Supply the member reference explicitly with ' +
'-Members instead.')
}
$body['members'] = @(@{ key = $FleetKey; member = @{ id = $self.id } })
}
elseif ($PSBoundParameters.ContainsKey('Members')) {
$body['members'] = @($Members)
}

$target = if ($FleetName) { $FleetName } elseif ($FleetId) { $FleetId } else { 'fleet member' }
$fleet = if ($FleetName) { $FleetName } elseif ($FleetId) { $FleetId } else { 'fleet member' }
$target = if ($self -and $self.name) { "$($self.name) into $fleet" } else { $fleet }

if ($PSCmdlet.ShouldProcess($target, 'Add fleet member')) {
Invoke-PfbApiRequest -Array $Array -Method POST -Endpoint 'fleets/members' -Body $body -QueryParams $queryParams
Expand Down
4 changes: 2 additions & 2 deletions Reports/PfbDeadKeyReport.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
{
"specVersion": "2.28",
"counts": {
"parametersInventoried": 2167,
"parametersInventoried": 2168,
"keysEvaluated": 1747,
"ok": 1664,
"deadKey": 83,
"skipReasons": {
"wire name unresolved": 126,
"wire name unresolved": 127,
"body property": 280,
"endpoint/method ambiguous": 14,
"endpoint/verb absent from spec": 0
Expand Down
4 changes: 4 additions & 0 deletions Reports/PfbFieldCmdletMap.json
Original file line number Diff line number Diff line change
Expand Up @@ -20519,6 +20519,10 @@
"cmdlet": "New-PfbFileSystemSnapshot",
"parameter": "SourceName"
},
{
"cmdlet": "New-PfbFleetMember",
"parameter": "FleetKey"
},
{
"cmdlet": "New-PfbLocalGroupMember",
"parameter": "Member"
Expand Down
3 changes: 2 additions & 1 deletion Reports/PfbFieldCmdletMapping.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ Reporting only -- no `Public/` cmdlet is edited by this script. Every `matched`
- `Update-PfbSmbSharePolicy -Enabled`
- `Update-PfbUserGroupQuotaPolicy -Enabled`

## Typed but unresolved wire name (needs manual inspection): 51
## Typed but unresolved wire name (needs manual inspection): 52

- `Connect-PfbArray -AllArrays`
- `Connect-PfbArray -ApiToken`
Expand Down Expand Up @@ -155,6 +155,7 @@ Reporting only -- no `Public/` cmdlet is edited by this script. Every `matched`
- `Invoke-PfbInContext -ScriptBlock`
- `New-PfbDataEvictionPolicy -Disabled`
- `New-PfbFileSystemSnapshot -SourceName`
- `New-PfbFleetMember -FleetKey`
- `New-PfbLocalGroupMember -Member`
- `New-PfbWorkloadPlacementRecommendation -Inputs`
- `Remove-PfbBucket -Eradicate`
Expand Down
15 changes: 14 additions & 1 deletion Tests/CommittedDeadKeyReport.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,20 @@ BeforeAll {
# and `$queryParams['names'] = $filterNames -join ','`; it is unresolvable only because
# the AST resolver cannot trace that conditional. This is the `body property` case,
# not the coverage-loss case this ceiling guards against.
'wire name unresolved' = 126
#
# 126 -> 127 for New-PfbFleetMember|FleetKey, the same case again and for the same
# reason. It is a NEW parameter, so nothing that was evaluable stopped being evaluated,
# and it was never evaluable as a query key: its value is placed inside the request body
# at `members[].key`, which the resolver does not follow into. Live-verified against the
# lab fleet -- POST /fleets/members returns 200 for the member it names -- so the key
# demonstrably reaches the wire, exactly as with the entry above.
#
# A raise here needs that pair of facts, not just a passing test: the parameter is new
# (so no coverage was lost) AND its route to the wire is evidenced (so 'unresolved'
# means the resolver cannot see it, not that it goes nowhere). Absent either, a growing
# count is the coverage regression this ceiling exists to catch -- do not bump it to
# clear a red.
'wire name unresolved' = 127
'endpoint/method ambiguous' = 14
'endpoint/verb absent from spec' = 0
}
Expand Down
87 changes: 87 additions & 0 deletions Tests/New-PfbFleetMember.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -95,10 +95,97 @@ Describe 'New-PfbFleetMember - typed body/query parameters (#31, confirmed wire-
}
}

Context '-FleetKey convenience path (build the body from a New-PfbFleetKey key plus self-identification)' {
BeforeEach {
Mock -ModuleName PureStorageFlashBladePowerShell Get-PfbArray {
[PSCustomObject]@{ id = 'self-array-id'; name = 'fb-a' }
}
}

It 'builds the members body from -FleetKey and the joining array own id' {
New-PfbFleetMember -FleetName 'fleet-prod' -FleetKey 'fleet-key-abc' -Confirm:$false -Array $fakeArray

Should -Invoke -ModuleName PureStorageFlashBladePowerShell Invoke-PfbApiRequest -Times 1 -Exactly -ParameterFilter {
$Method -eq 'POST' -and $Endpoint -eq 'fleets/members' -and
$QueryParams['fleet_names'] -eq 'fleet-prod' -and
@($Body['members']).Count -eq 1 -and
@($Body['members'])[0]['key'] -eq 'fleet-key-abc' -and
@($Body['members'])[0]['member']['id'] -eq 'self-array-id'
}
}

It 'sends the same wire shape as the equivalent explicit -Members call, so the convenience path is a pure ergonomics layer' {
$script:captured = @()
Mock -ModuleName PureStorageFlashBladePowerShell Invoke-PfbApiRequest {
$script:captured += , ($Body | ConvertTo-Json -Depth 6 -Compress)
}

New-PfbFleetMember -FleetName 'fleet-prod' -FleetKey 'k' -Confirm:$false -Array $fakeArray
New-PfbFleetMember -FleetName 'fleet-prod' `
-Members @{ key = 'k'; member = @{ id = 'self-array-id' } } `
-Confirm:$false -Array $fakeArray

$script:captured.Count | Should -Be 2
$script:captured[0] | Should -Be $script:captured[1]
}

It 'resolves the joining array over the connection it was given, not the default connection' {
New-PfbFleetMember -FleetName 'fleet-prod' -FleetKey 'k' -Confirm:$false -Array $fakeArray

Should -Invoke -ModuleName PureStorageFlashBladePowerShell Get-PfbArray -Times 1 -Exactly -ParameterFilter {
$Array.Endpoint -eq 'fb.example.test'
}
}

It 'takes the first array record, since GET /arrays returns a list of one for the array being talked to' {
Mock -ModuleName PureStorageFlashBladePowerShell Get-PfbArray {
@([PSCustomObject]@{ id = 'first'; name = 'fb-a' }, [PSCustomObject]@{ id = 'second'; name = 'fb-b' })
}

New-PfbFleetMember -FleetName 'fleet-prod' -FleetKey 'k' -Confirm:$false -Array $fakeArray

Should -Invoke -ModuleName PureStorageFlashBladePowerShell Invoke-PfbApiRequest -Times 1 -Exactly -ParameterFilter {
@($Body['members'])[0]['member']['id'] -eq 'first'
}
}

It 'throws rather than POSTing a body with an empty member id when the array id cannot be resolved' {
Mock -ModuleName PureStorageFlashBladePowerShell Get-PfbArray { }

{ New-PfbFleetMember -FleetName 'fleet-prod' -FleetKey 'k' -Confirm:$false -Array $fakeArray } |
Should -Throw -ExpectedMessage '*-Members*'

Should -Invoke -ModuleName PureStorageFlashBladePowerShell Invoke-PfbApiRequest -Times 0 -Exactly
}

It 'accepts -FleetId as the selector alongside -FleetKey, because the selector says WHICH fleet and the key only authorises the join' {
New-PfbFleetMember -FleetId 'fleet-1' -FleetKey 'k' -Confirm:$false -Array $fakeArray

Should -Invoke -ModuleName PureStorageFlashBladePowerShell Invoke-PfbApiRequest -Times 1 -Exactly -ParameterFilter {
$QueryParams['fleet_ids'] -eq 'fleet-1' -and -not $QueryParams.ContainsKey('fleet_names') -and
@($Body['members'])[0]['key'] -eq 'k'
}
}

It 'rejects -FleetKey and -Members together at bind time rather than silently preferring one' {
{ New-PfbFleetMember -FleetName 'fleet-prod' -FleetKey 'k' `
-Members @{ key = 'k'; member = @{ id = 'i' } } -Confirm:$false -Array $fakeArray } |
Should -Throw
}

It 'never calls Get-PfbArray on the -Members path, so the passthrough shape stays exactly what the caller wrote' {
New-PfbFleetMember -FleetName 'fleet-prod' -Members @{ key = 'k'; member = @{ id = 'i' } } `
-Confirm:$false -Array $fakeArray

Should -Invoke -ModuleName PureStorageFlashBladePowerShell Get-PfbArray -Times 0 -Exactly
}
}

Context 'constraint compliance' {
It 'puts no ValidateSet on -<Parameter> (constraint 3, no spec enum)' -ForEach @(
@{ Parameter = 'FleetName' }
@{ Parameter = 'FleetId' }
@{ Parameter = 'FleetKey' }
@{ Parameter = 'Members' }
) {
$attrs = (Get-Command New-PfbFleetMember).Parameters[$Parameter].Attributes
Expand Down
Loading