-
Notifications
You must be signed in to change notification settings - Fork 8
SP136: Subsystem gateway registration is create-only, no partial-state recovery #1526
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: feature/aps-4652-carryover
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| import { v4 as uuidv4 } from 'uuid' | ||
|
|
||
| import { createRuntimeGroup, createSubsystem, uniqueSubsystemName } from '../../support/sdx-commands' | ||
|
|
||
| describe('SDX Subsystem Gateway Registration Recovery', () => { | ||
| let workingData: any | ||
|
|
||
| before(() => { | ||
| cy.buildOrgGatewayDatasetAndProduct().then((data) => { | ||
| workingData = data | ||
|
|
||
| const rg = uuidv4().replace(/-/g, '').toUpperCase().substring(0, 6) | ||
| workingData['runtimeGroupId'] = rg.toLowerCase() | ||
|
|
||
| createRuntimeGroup(workingData.org, workingData.runtimeGroupId, 'dev') | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. any way to predictably simulate a failed run to create partial state? |
||
| }) | ||
| }) | ||
|
|
||
| it('PUT /organizations/{org}/subsystems/{name}/gateway - retrying a registration for the same subsystem recovers instead of failing', () => { | ||
| const { org, runtimeGroupId } = workingData | ||
| const subsystemName = uniqueSubsystemName() | ||
|
|
||
| createSubsystem(org, subsystemName, () => { | ||
| cy.setRequestBody({ runtimeGroupName: runtimeGroupId }) | ||
| cy.callAPI( | ||
| `ds/api/sdx/v1/organizations/${org.name}/subsystems/${subsystemName}/gateway`, | ||
| 'PUT' | ||
| ).then(({ apiRes: { status, body } }: any) => { | ||
| expect(status, body.message).to.be.equal(200) | ||
| expect(body).to.have.property('gatewayId') | ||
| const gatewayId = body.gatewayId | ||
|
|
||
| // Simulate a caller retrying the same registration request - e.g. | ||
| // because it never saw the first response (timeout, dropped | ||
| // connection, portal restart). The subsystem's gateway/namespace ID | ||
| // is fixed and immutable, so this always targets the exact same | ||
| // namespace as the first call. | ||
| cy.setRequestBody({ runtimeGroupName: runtimeGroupId }) | ||
| cy.callAPI( | ||
| `ds/api/sdx/v1/organizations/${org.name}/subsystems/${subsystemName}/gateway`, | ||
| 'PUT' | ||
| ).then(({ apiRes: { status, body } }: any) => { | ||
| expect(status, body.message).to.be.equal(200) | ||
| expect(body).to.have.property('gatewayId') | ||
| expect(body.gatewayId).to.be.equal(gatewayId) | ||
|
|
||
| // The subsystem catalog should reflect a single, consistent | ||
| // registration - no duplicate/corrupted state left behind by the | ||
| // repeated registration attempt. | ||
| cy.callAPI( | ||
| `ds/api/sdx/v1/organizations/${org.name}/subsystems`, | ||
| 'GET' | ||
| ).then(({ apiRes: { status, body } }: any) => { | ||
| expect(status).to.be.equal(200) | ||
| const matches = body.filter((s: any) => s.name === subsystemName) | ||
| expect(matches.length).to.be.equal(1) | ||
| expect(matches[0].gatewayId).to.be.equal(gatewayId) | ||
| }) | ||
| }) | ||
| }) | ||
| }) | ||
| }) | ||
| }) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,7 +12,10 @@ import { CreateNamespace, CreateNamespaceArgs } from './create-namespace'; | |
| import assert from '../user-assert'; | ||
| import { EnvironmentContext, getEnvironmentContext } from './get-namespaces'; | ||
| import { lookupProductEnvironmentServicesBySlug } from '../keystone'; | ||
| import { createUmaPolicy, updateUmaPolicy } from './ns-uma-policy-access'; | ||
| import { | ||
| createUmaPolicyIfMissing, | ||
| updateUmaPolicy, | ||
| } from './ns-uma-policy-access'; | ||
| import { SysGroupAccessService } from '../org-groups/sys-group-access'; | ||
| import { GroupAccessService } from '../org-groups'; | ||
|
|
||
|
|
@@ -296,6 +299,7 @@ async function createSDXNamespace( | |
| 'GatewayPattern.Publish', | ||
| ]; | ||
| args.includeSDXScopes = true; | ||
| args.allowResume = true; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. PR only talks about subsystem, but this will apply to org and runtime group gateways too. Those all have fixed gateway names so should be fine too. |
||
|
|
||
| const resourceSet = await CreateNamespace(context, args); | ||
|
|
||
|
|
@@ -308,7 +312,7 @@ async function createSDXNamespace( | |
| scopes: ['GatewayConfig.Publish', 'Namespace.Manage'], | ||
| }; | ||
|
|
||
| const umaResult = await createUmaPolicy( | ||
| const umaResult = await createUmaPolicyIfMissing( | ||
| context, | ||
| envCtx, | ||
| resourceSet.id, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -39,6 +39,14 @@ export interface CreateNamespaceArgs { | |
| routePaths?: string[]; | ||
| assignedScopes?: string[]; | ||
| includeSDXScopes?: boolean; | ||
| /** | ||
| * When true, an existing namespace with this exact name is treated as an | ||
| * in-progress/partially-completed registration to resume rather than a | ||
| * conflict to reject. Only safe when `name` is a stable, pre-allocated | ||
| * identifier (e.g. a Subsystem's own `namespace`) that can never | ||
| * legitimately collide with a different owner's namespace. | ||
| */ | ||
| allowResume?: boolean; | ||
| } | ||
|
|
||
| export async function CreateNamespace( | ||
|
|
@@ -67,7 +75,15 @@ export async function CreateNamespace( | |
| envCtx.issuerEnvConfig.clientId, | ||
| envCtx.issuerEnvConfig.clientSecret | ||
| ); | ||
| await nsService.checkNamespaceAvailable(newNS); | ||
| const resuming = args.allowResume && (await nsService.namespaceExists(newNS)); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Resume is keyed only on the Keycloak Fix would involve treating 'already in progress' as the group or UMA resource already exists. |
||
| if (!resuming) { | ||
| await nsService.checkNamespaceAvailable(newNS); | ||
| } else { | ||
| logger.info( | ||
| '[CreateNamespace] Namespace %s already exists, resuming registration', | ||
| newNS | ||
| ); | ||
| } | ||
|
|
||
| // This function gets all resources but also sets the accessToken in envCtx | ||
| // which we need to create the resource set | ||
|
|
@@ -98,7 +114,10 @@ export async function CreateNamespace( | |
| ownerManagedAccess: true, | ||
| }; | ||
|
|
||
| const rset = await resourceApi.createResourceSet(res); | ||
| const existingRset = resuming | ||
| ? await resourceApi.findResourceByName(newNS) | ||
| : undefined; | ||
| const rset = existingRset || (await resourceApi.createResourceSet(res)); | ||
|
|
||
| if (isUserBasedResourceOwners(envCtx) == false) { | ||
| const permissionApi = new KeycloakPermissionTicketService( | ||
|
|
@@ -111,7 +130,7 @@ export async function CreateNamespace( | |
| 'GatewayConfig.Publish', | ||
| 'Access.Manage', | ||
| ]) { | ||
| await permissionApi.createPermission( | ||
| await permissionApi.createOrUpdatePermission( | ||
| rset.id, | ||
| envCtx.subjectUuid, | ||
| true, | ||
|
|
@@ -128,9 +147,13 @@ export async function CreateNamespace( | |
| envCtx.issuerEnvConfig.clientSecret | ||
| ); | ||
|
|
||
| const { id, created } = await kcGroupService.createIfMissing('ns', newNS); | ||
| const { id } = await kcGroupService.createIfMissing('ns', newNS); | ||
|
|
||
| if (created) { | ||
| { | ||
| // Reconcile attributes unconditionally (not just on first create) so a | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could we guard the resume path against a different requested configuration? A second subsystem gateway PUT can supply another |
||
| // resumed registration still lands on the fully-configured end state, | ||
| // even if an earlier attempt died after the group was created but | ||
| // before attributes were applied. | ||
| const gwGroup = await kcGroupService.getGroupById(id); | ||
| if (args.org) { | ||
| gwGroup.attributes['org'] = [args.org]; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could the existing duplicate-registration cases be updated as part of this change?
21-sdx-api/v1/05-gateways.tsis already included above and still expects the second runtime-group and subsystem gateway PUTs to return 422 withNamespace already exists, while the new99-sp136case expects the second PUT to return 200. With this pattern enabled, the full E2E suite has contradictory expectations and the older cases appear likely to fail.