diff --git a/Public/Replication/New-PfbFleetMember.ps1 b/Public/Replication/New-PfbFleetMember.ps1 index 8605e28..0e4123d 100644 --- a/Public/Replication/New-PfbFleetMember.ps1 +++ b/Public/Replication/New-PfbFleetMember.ps1 @@ -10,23 +10,38 @@ 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 = ""; member = @{ id = "" } }. @@ -34,20 +49,27 @@ function New-PfbFleetMember { 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 ) @@ -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 diff --git a/Reports/PfbDeadKeyReport.json b/Reports/PfbDeadKeyReport.json index 9d17494..84eb043 100644 --- a/Reports/PfbDeadKeyReport.json +++ b/Reports/PfbDeadKeyReport.json @@ -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 diff --git a/Reports/PfbFieldCmdletMap.json b/Reports/PfbFieldCmdletMap.json index 3794bb0..5cb5e74 100644 --- a/Reports/PfbFieldCmdletMap.json +++ b/Reports/PfbFieldCmdletMap.json @@ -20519,6 +20519,10 @@ "cmdlet": "New-PfbFileSystemSnapshot", "parameter": "SourceName" }, + { + "cmdlet": "New-PfbFleetMember", + "parameter": "FleetKey" + }, { "cmdlet": "New-PfbLocalGroupMember", "parameter": "Member" diff --git a/Reports/PfbFieldCmdletMapping.md b/Reports/PfbFieldCmdletMapping.md index a0e3e99..516aac5 100644 --- a/Reports/PfbFieldCmdletMapping.md +++ b/Reports/PfbFieldCmdletMapping.md @@ -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` @@ -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` diff --git a/Tests/CommittedDeadKeyReport.Tests.ps1 b/Tests/CommittedDeadKeyReport.Tests.ps1 index f79983c..241dbad 100644 --- a/Tests/CommittedDeadKeyReport.Tests.ps1 +++ b/Tests/CommittedDeadKeyReport.Tests.ps1 @@ -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 } diff --git a/Tests/New-PfbFleetMember.Tests.ps1 b/Tests/New-PfbFleetMember.Tests.ps1 index b6af0c7..b2a0159 100644 --- a/Tests/New-PfbFleetMember.Tests.ps1 +++ b/Tests/New-PfbFleetMember.Tests.ps1 @@ -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 - (constraint 3, no spec enum)' -ForEach @( @{ Parameter = 'FleetName' } @{ Parameter = 'FleetId' } + @{ Parameter = 'FleetKey' } @{ Parameter = 'Members' } ) { $attrs = (Get-Command New-PfbFleetMember).Parameters[$Parameter].Attributes