Skip to content

Commit ff6f8e4

Browse files
committed
feat: add OAuth credential setup and gateway output parsing to TUI deploy flow (#411)
* feat: add OAuth credential setup and gateway output parsing to TUI deploy flow * refactor: rename hasOwned* to has* identity provider helpers * fix: log gateway config errors instead of silently catching
1 parent 75843cf commit ff6f8e4

File tree

6 files changed

+103
-19
lines changed

6 files changed

+103
-19
lines changed

src/cli/commands/deploy/actions.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ import {
1010
checkBootstrapNeeded,
1111
checkStackDeployability,
1212
getAllCredentials,
13-
hasOwnedIdentityApiProviders,
14-
hasOwnedIdentityOAuthProviders,
13+
hasIdentityApiProviders,
14+
hasIdentityOAuthProviders,
1515
performStackTeardown,
1616
setupApiKeyProviders,
1717
setupOAuth2Providers,
@@ -181,7 +181,7 @@ export async function handleDeploy(options: ValidatedDeployOptions): Promise<Dep
181181
const runtimeCredentials =
182182
Object.keys(envCredentials).length > 0 ? new SecureCredentials(envCredentials) : undefined;
183183

184-
if (hasOwnedIdentityApiProviders(context.projectSpec)) {
184+
if (hasIdentityApiProviders(context.projectSpec)) {
185185
startStep('Creating credentials...');
186186

187187
const identityResult = await setupApiKeyProviders({
@@ -208,7 +208,7 @@ export async function handleDeploy(options: ValidatedDeployOptions): Promise<Dep
208208
string,
209209
{ credentialProviderArn: string; clientSecretArn?: string; callbackUrl?: string }
210210
> = {};
211-
if (hasOwnedIdentityOAuthProviders(context.projectSpec)) {
211+
if (hasIdentityOAuthProviders(context.projectSpec)) {
212212
startStep('Creating OAuth credentials...');
213213

214214
const oauthResult = await setupOAuth2Providers({

src/cli/operations/deploy/__tests__/pre-deploy-identity.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {
22
getAllCredentials,
3-
hasOwnedIdentityOAuthProviders,
3+
hasIdentityOAuthProviders,
44
setupApiKeyProviders,
55
setupOAuth2Providers,
66
} from '../pre-deploy-identity.js';
@@ -200,27 +200,27 @@ describe('setupApiKeyProviders - KMS key reuse via GetTokenVault', () => {
200200
});
201201
});
202202

203-
describe('hasOwnedIdentityOAuthProviders', () => {
203+
describe('hasIdentityOAuthProviders', () => {
204204
it('returns true when OAuthCredentialProvider exists', () => {
205205
const projectSpec = {
206206
credentials: [
207207
{ name: 'oauth-cred', type: 'OAuthCredentialProvider' },
208208
{ name: 'api-cred', type: 'ApiKeyCredentialProvider' },
209209
],
210210
};
211-
expect(hasOwnedIdentityOAuthProviders(projectSpec as any)).toBe(true);
211+
expect(hasIdentityOAuthProviders(projectSpec as any)).toBe(true);
212212
});
213213

214214
it('returns false when only ApiKey credentials exist', () => {
215215
const projectSpec = {
216216
credentials: [{ name: 'api-cred', type: 'ApiKeyCredentialProvider' }],
217217
};
218-
expect(hasOwnedIdentityOAuthProviders(projectSpec as any)).toBe(false);
218+
expect(hasIdentityOAuthProviders(projectSpec as any)).toBe(false);
219219
});
220220

221221
it('returns false when no credentials exist', () => {
222222
const projectSpec = { credentials: [] };
223-
expect(hasOwnedIdentityOAuthProviders(projectSpec as any)).toBe(false);
223+
expect(hasIdentityOAuthProviders(projectSpec as any)).toBe(false);
224224
});
225225
});
226226

src/cli/operations/deploy/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ export {
1717
export {
1818
setupApiKeyProviders,
1919
setupOAuth2Providers,
20-
hasOwnedIdentityApiProviders,
21-
hasOwnedIdentityOAuthProviders,
20+
hasIdentityApiProviders,
21+
hasIdentityOAuthProviders,
2222
getMissingCredentials,
2323
getAllCredentials,
2424
type SetupApiKeyProvidersOptions,

src/cli/operations/deploy/pre-deploy-identity.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ async function setupApiKeyCredentialProvider(
193193
/**
194194
* Check if the project has any API key credentials that need setup.
195195
*/
196-
export function hasOwnedIdentityApiProviders(projectSpec: AgentCoreProjectSpec): boolean {
196+
export function hasIdentityApiProviders(projectSpec: AgentCoreProjectSpec): boolean {
197197
return projectSpec.credentials.some(c => c.type === 'ApiKeyCredentialProvider');
198198
}
199199

@@ -307,7 +307,7 @@ export async function setupOAuth2Providers(options: SetupOAuth2ProvidersOptions)
307307
/**
308308
* Check if the project has any OAuth credentials that need setup.
309309
*/
310-
export function hasOwnedIdentityOAuthProviders(projectSpec: AgentCoreProjectSpec): boolean {
310+
export function hasIdentityOAuthProviders(projectSpec: AgentCoreProjectSpec): boolean {
311311
return projectSpec.credentials.some(c => c.type === 'OAuthCredentialProvider');
312312
}
313313

src/cli/tui/hooks/useCdkPreflight.ts

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@ import {
1313
checkStackDeployability,
1414
formatError,
1515
getAllCredentials,
16-
hasOwnedIdentityApiProviders,
16+
hasIdentityApiProviders,
17+
hasIdentityOAuthProviders,
1718
setupApiKeyProviders,
19+
setupOAuth2Providers,
1820
synthesizeCdk,
1921
validateProject,
2022
} from '../../operations/deploy';
@@ -65,6 +67,8 @@ export interface PreflightResult {
6567
missingCredentials: MissingCredential[];
6668
/** KMS key ARN used for identity token vault encryption */
6769
identityKmsKeyArn?: string;
70+
/** OAuth credential ARNs from pre-deploy setup */
71+
oauthCredentials: Record<string, { credentialProviderArn: string; clientSecretArn?: string; callbackUrl?: string }>;
6872
startPreflight: () => Promise<void>;
6973
confirmTeardown: () => void;
7074
cancelTeardown: () => void;
@@ -119,6 +123,9 @@ export function useCdkPreflight(options: PreflightOptions): PreflightResult {
119123
const [runtimeCredentials, setRuntimeCredentials] = useState<SecureCredentials | null>(null);
120124
const [skipIdentitySetup, setSkipIdentitySetup] = useState(false);
121125
const [identityKmsKeyArn, setIdentityKmsKeyArn] = useState<string | undefined>(undefined);
126+
const [oauthCredentials, setOauthCredentials] = useState<
127+
Record<string, { credentialProviderArn: string; clientSecretArn?: string; callbackUrl?: string }>
128+
>({});
122129
const [teardownConfirmed, setTeardownConfirmed] = useState(false);
123130

124131
// Guard against concurrent runs (React StrictMode, re-renders, etc.)
@@ -417,7 +424,7 @@ export function useCdkPreflight(options: PreflightOptions): PreflightResult {
417424

418425
// Check if API key providers need setup - always prompt user for credential source
419426
// Skip this check if skipIdentityCheck is true (e.g., plan command only synthesizes)
420-
const needsApiKeySetup = !skipIdentityCheck && hasOwnedIdentityApiProviders(preflightContext.projectSpec);
427+
const needsApiKeySetup = !skipIdentityCheck && hasIdentityApiProviders(preflightContext.projectSpec);
421428
if (needsApiKeySetup) {
422429
// Get all credentials for the prompt (not just missing ones)
423430
const allCredentials = getAllCredentials(preflightContext.projectSpec);
@@ -559,6 +566,62 @@ export function useCdkPreflight(options: PreflightOptions): PreflightResult {
559566
logger.endStep('success');
560567
setSteps(prev => prev.map((s, i) => (i === prev.length - 1 ? { ...s, status: 'success' } : s)));
561568

569+
// Set up OAuth credential providers if needed
570+
if (hasIdentityOAuthProviders(context.projectSpec)) {
571+
setSteps(prev => [...prev, { label: 'Set up OAuth providers', status: 'running' }]);
572+
logger.startStep('Set up OAuth providers');
573+
574+
const oauthResult = await setupOAuth2Providers({
575+
projectSpec: context.projectSpec,
576+
configBaseDir,
577+
region: target.region,
578+
runtimeCredentials: runtimeCredentials ?? undefined,
579+
});
580+
581+
for (const result of oauthResult.results) {
582+
if (result.status === 'created') {
583+
logger.log(`Created OAuth provider: ${result.providerName}`);
584+
} else if (result.status === 'updated') {
585+
logger.log(`Updated OAuth provider: ${result.providerName}`);
586+
} else if (result.status === 'skipped') {
587+
logger.log(`Skipped ${result.providerName}: ${result.error}`);
588+
} else if (result.status === 'error') {
589+
logger.log(`Error for ${result.providerName}: ${result.error}`);
590+
}
591+
}
592+
593+
if (oauthResult.hasErrors) {
594+
logger.endStep('error', 'Some OAuth providers failed to set up');
595+
setSteps(prev =>
596+
prev.map((s, i) =>
597+
i === prev.length - 1 ? { ...s, status: 'error', error: 'Some OAuth providers failed' } : s
598+
)
599+
);
600+
setPhase('error');
601+
isRunningRef.current = false;
602+
return;
603+
}
604+
605+
// Collect credential ARNs for deployed state
606+
const creds: Record<
607+
string,
608+
{ credentialProviderArn: string; clientSecretArn?: string; callbackUrl?: string }
609+
> = {};
610+
for (const result of oauthResult.results) {
611+
if (result.credentialProviderArn) {
612+
creds[result.providerName] = {
613+
credentialProviderArn: result.credentialProviderArn,
614+
clientSecretArn: result.clientSecretArn,
615+
callbackUrl: result.callbackUrl,
616+
};
617+
}
618+
}
619+
setOauthCredentials(creds);
620+
621+
logger.endStep('success');
622+
setSteps(prev => prev.map((s, i) => (i === prev.length - 1 ? { ...s, status: 'success' } : s)));
623+
}
624+
562625
// Clear runtime credentials
563626
setRuntimeCredentials(null);
564627

@@ -643,6 +706,7 @@ export function useCdkPreflight(options: PreflightOptions): PreflightResult {
643706
hasCredentialsError,
644707
missingCredentials,
645708
identityKmsKeyArn,
709+
oauthCredentials,
646710
startPreflight,
647711
confirmTeardown,
648712
cancelTeardown,

src/cli/tui/screens/deploy/useDeployFlow.ts

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { ConfigIO } from '../../../../lib';
22
import type { CdkToolkitWrapper, DeployMessage, SwitchableIoHost } from '../../../cdk/toolkit-lib';
3-
import { buildDeployedState, getStackOutputs, parseAgentOutputs } from '../../../cloudformation';
3+
import { buildDeployedState, getStackOutputs, parseAgentOutputs, parseGatewayOutputs } from '../../../cloudformation';
44
import { getErrorMessage, isChangesetInProgressError, isExpiredTokenError } from '../../../errors';
55
import { ExecLogger } from '../../../logging';
66
import { performStackTeardown } from '../../../operations/deploy';
@@ -28,6 +28,7 @@ export interface PreSynthesized {
2828
stackNames: string[];
2929
switchableIoHost?: SwitchableIoHost;
3030
identityKmsKeyArn?: string;
31+
oauthCredentials?: Record<string, { credentialProviderArn: string; clientSecretArn?: string; callbackUrl?: string }>;
3132
}
3233

3334
interface DeployFlowOptions {
@@ -88,6 +89,7 @@ export function useDeployFlow(options: DeployFlowOptions = {}): DeployFlowState
8889
const stackNames = preSynthesized?.stackNames ?? preflight.stackNames;
8990
const switchableIoHost = preSynthesized?.switchableIoHost ?? preflight.switchableIoHost;
9091
const identityKmsKeyArn = preSynthesized?.identityKmsKeyArn ?? preflight.identityKmsKeyArn;
92+
const oauthCredentials = preSynthesized?.oauthCredentials ?? preflight.oauthCredentials;
9193

9294
const [publishAssetsStep, setPublishAssetsStep] = useState<Step>({ label: 'Publish assets', status: 'pending' });
9395
const [deployStep, setDeployStep] = useState<Step>({ label: 'Deploy to AWS', status: 'pending' });
@@ -163,6 +165,23 @@ export function useDeployFlow(options: DeployFlowOptions = {}): DeployFlowState
163165
);
164166
}
165167

168+
// Parse gateway outputs from CDK stack
169+
let gateways: Record<string, { gatewayId: string; gatewayArn: string }> = {};
170+
try {
171+
const mcpSpec = await configIO.readMcpSpec();
172+
const gatewaySpecs =
173+
mcpSpec?.agentCoreGateways?.reduce(
174+
(acc: Record<string, unknown>, gateway: { name: string }) => {
175+
acc[gateway.name] = gateway;
176+
return acc;
177+
},
178+
{} as Record<string, unknown>
179+
) ?? {};
180+
gateways = parseGatewayOutputs(outputs, gatewaySpecs);
181+
} catch (error) {
182+
logger.log(`Failed to read gateway configuration: ${getErrorMessage(error)}`, 'warn');
183+
}
184+
166185
// Expose outputs to UI
167186
setStackOutputs(outputs);
168187

@@ -171,12 +190,13 @@ export function useDeployFlow(options: DeployFlowOptions = {}): DeployFlowState
171190
target.name,
172191
currentStackName,
173192
agents,
174-
{},
193+
gateways,
175194
existingState,
176-
identityKmsKeyArn
195+
identityKmsKeyArn,
196+
Object.keys(oauthCredentials).length > 0 ? oauthCredentials : undefined
177197
);
178198
await configIO.writeDeployedState(deployedState);
179-
}, [context, stackNames, logger, identityKmsKeyArn]);
199+
}, [context, stackNames, logger, identityKmsKeyArn, oauthCredentials]);
180200

181201
// Start deploy when preflight completes OR when shouldStartDeploy is set
182202
useEffect(() => {

0 commit comments

Comments
 (0)