diff --git a/src/cli/commands/deploy/actions.ts b/src/cli/commands/deploy/actions.ts index b9cfc538..bfa09a1f 100644 --- a/src/cli/commands/deploy/actions.ts +++ b/src/cli/commands/deploy/actions.ts @@ -10,8 +10,8 @@ import { checkBootstrapNeeded, checkStackDeployability, getAllCredentials, - hasOwnedIdentityApiProviders, - hasOwnedIdentityOAuthProviders, + hasIdentityApiProviders, + hasIdentityOAuthProviders, performStackTeardown, setupApiKeyProviders, setupOAuth2Providers, @@ -181,7 +181,7 @@ export async function handleDeploy(options: ValidatedDeployOptions): Promise 0 ? new SecureCredentials(envCredentials) : undefined; - if (hasOwnedIdentityApiProviders(context.projectSpec)) { + if (hasIdentityApiProviders(context.projectSpec)) { startStep('Creating credentials...'); const identityResult = await setupApiKeyProviders({ @@ -208,7 +208,7 @@ export async function handleDeploy(options: ValidatedDeployOptions): Promise = {}; - if (hasOwnedIdentityOAuthProviders(context.projectSpec)) { + if (hasIdentityOAuthProviders(context.projectSpec)) { startStep('Creating OAuth credentials...'); const oauthResult = await setupOAuth2Providers({ diff --git a/src/cli/operations/deploy/__tests__/pre-deploy-identity.test.ts b/src/cli/operations/deploy/__tests__/pre-deploy-identity.test.ts index cc49fe31..f0511319 100644 --- a/src/cli/operations/deploy/__tests__/pre-deploy-identity.test.ts +++ b/src/cli/operations/deploy/__tests__/pre-deploy-identity.test.ts @@ -1,6 +1,6 @@ import { getAllCredentials, - hasOwnedIdentityOAuthProviders, + hasIdentityOAuthProviders, setupApiKeyProviders, setupOAuth2Providers, } from '../pre-deploy-identity.js'; @@ -200,7 +200,7 @@ describe('setupApiKeyProviders - KMS key reuse via GetTokenVault', () => { }); }); -describe('hasOwnedIdentityOAuthProviders', () => { +describe('hasIdentityOAuthProviders', () => { it('returns true when OAuthCredentialProvider exists', () => { const projectSpec = { credentials: [ @@ -208,19 +208,19 @@ describe('hasOwnedIdentityOAuthProviders', () => { { name: 'api-cred', type: 'ApiKeyCredentialProvider' }, ], }; - expect(hasOwnedIdentityOAuthProviders(projectSpec as any)).toBe(true); + expect(hasIdentityOAuthProviders(projectSpec as any)).toBe(true); }); it('returns false when only ApiKey credentials exist', () => { const projectSpec = { credentials: [{ name: 'api-cred', type: 'ApiKeyCredentialProvider' }], }; - expect(hasOwnedIdentityOAuthProviders(projectSpec as any)).toBe(false); + expect(hasIdentityOAuthProviders(projectSpec as any)).toBe(false); }); it('returns false when no credentials exist', () => { const projectSpec = { credentials: [] }; - expect(hasOwnedIdentityOAuthProviders(projectSpec as any)).toBe(false); + expect(hasIdentityOAuthProviders(projectSpec as any)).toBe(false); }); }); diff --git a/src/cli/operations/deploy/index.ts b/src/cli/operations/deploy/index.ts index 68c1ed35..f5de068e 100644 --- a/src/cli/operations/deploy/index.ts +++ b/src/cli/operations/deploy/index.ts @@ -17,8 +17,8 @@ export { export { setupApiKeyProviders, setupOAuth2Providers, - hasOwnedIdentityApiProviders, - hasOwnedIdentityOAuthProviders, + hasIdentityApiProviders, + hasIdentityOAuthProviders, getMissingCredentials, getAllCredentials, type SetupApiKeyProvidersOptions, diff --git a/src/cli/operations/deploy/pre-deploy-identity.ts b/src/cli/operations/deploy/pre-deploy-identity.ts index aa0d507b..c23538c0 100644 --- a/src/cli/operations/deploy/pre-deploy-identity.ts +++ b/src/cli/operations/deploy/pre-deploy-identity.ts @@ -192,7 +192,7 @@ async function setupApiKeyCredentialProvider( /** * Check if the project has any API key credentials that need setup. */ -export function hasOwnedIdentityApiProviders(projectSpec: AgentCoreProjectSpec): boolean { +export function hasIdentityApiProviders(projectSpec: AgentCoreProjectSpec): boolean { return projectSpec.credentials.some(c => c.type === 'ApiKeyCredentialProvider'); } @@ -306,7 +306,7 @@ export async function setupOAuth2Providers(options: SetupOAuth2ProvidersOptions) /** * Check if the project has any OAuth credentials that need setup. */ -export function hasOwnedIdentityOAuthProviders(projectSpec: AgentCoreProjectSpec): boolean { +export function hasIdentityOAuthProviders(projectSpec: AgentCoreProjectSpec): boolean { return projectSpec.credentials.some(c => c.type === 'OAuthCredentialProvider'); } diff --git a/src/cli/tui/hooks/useCdkPreflight.ts b/src/cli/tui/hooks/useCdkPreflight.ts index 70ab6b12..785e60c9 100644 --- a/src/cli/tui/hooks/useCdkPreflight.ts +++ b/src/cli/tui/hooks/useCdkPreflight.ts @@ -13,8 +13,10 @@ import { checkStackDeployability, formatError, getAllCredentials, - hasOwnedIdentityApiProviders, + hasIdentityApiProviders, + hasIdentityOAuthProviders, setupApiKeyProviders, + setupOAuth2Providers, synthesizeCdk, validateProject, } from '../../operations/deploy'; @@ -65,6 +67,8 @@ export interface PreflightResult { missingCredentials: MissingCredential[]; /** KMS key ARN used for identity token vault encryption */ identityKmsKeyArn?: string; + /** OAuth credential ARNs from pre-deploy setup */ + oauthCredentials: Record; startPreflight: () => Promise; confirmTeardown: () => void; cancelTeardown: () => void; @@ -119,6 +123,9 @@ export function useCdkPreflight(options: PreflightOptions): PreflightResult { const [runtimeCredentials, setRuntimeCredentials] = useState(null); const [skipIdentitySetup, setSkipIdentitySetup] = useState(false); const [identityKmsKeyArn, setIdentityKmsKeyArn] = useState(undefined); + const [oauthCredentials, setOauthCredentials] = useState< + Record + >({}); const [teardownConfirmed, setTeardownConfirmed] = useState(false); // Guard against concurrent runs (React StrictMode, re-renders, etc.) @@ -417,7 +424,7 @@ export function useCdkPreflight(options: PreflightOptions): PreflightResult { // Check if API key providers need setup - always prompt user for credential source // Skip this check if skipIdentityCheck is true (e.g., plan command only synthesizes) - const needsApiKeySetup = !skipIdentityCheck && hasOwnedIdentityApiProviders(preflightContext.projectSpec); + const needsApiKeySetup = !skipIdentityCheck && hasIdentityApiProviders(preflightContext.projectSpec); if (needsApiKeySetup) { // Get all credentials for the prompt (not just missing ones) const allCredentials = getAllCredentials(preflightContext.projectSpec); @@ -559,6 +566,62 @@ export function useCdkPreflight(options: PreflightOptions): PreflightResult { logger.endStep('success'); setSteps(prev => prev.map((s, i) => (i === prev.length - 1 ? { ...s, status: 'success' } : s))); + // Set up OAuth credential providers if needed + if (hasIdentityOAuthProviders(context.projectSpec)) { + setSteps(prev => [...prev, { label: 'Set up OAuth providers', status: 'running' }]); + logger.startStep('Set up OAuth providers'); + + const oauthResult = await setupOAuth2Providers({ + projectSpec: context.projectSpec, + configBaseDir, + region: target.region, + runtimeCredentials: runtimeCredentials ?? undefined, + }); + + for (const result of oauthResult.results) { + if (result.status === 'created') { + logger.log(`Created OAuth provider: ${result.providerName}`); + } else if (result.status === 'updated') { + logger.log(`Updated OAuth provider: ${result.providerName}`); + } else if (result.status === 'skipped') { + logger.log(`Skipped ${result.providerName}: ${result.error}`); + } else if (result.status === 'error') { + logger.log(`Error for ${result.providerName}: ${result.error}`); + } + } + + if (oauthResult.hasErrors) { + logger.endStep('error', 'Some OAuth providers failed to set up'); + setSteps(prev => + prev.map((s, i) => + i === prev.length - 1 ? { ...s, status: 'error', error: 'Some OAuth providers failed' } : s + ) + ); + setPhase('error'); + isRunningRef.current = false; + return; + } + + // Collect credential ARNs for deployed state + const creds: Record< + string, + { credentialProviderArn: string; clientSecretArn?: string; callbackUrl?: string } + > = {}; + for (const result of oauthResult.results) { + if (result.credentialProviderArn) { + creds[result.providerName] = { + credentialProviderArn: result.credentialProviderArn, + clientSecretArn: result.clientSecretArn, + callbackUrl: result.callbackUrl, + }; + } + } + setOauthCredentials(creds); + + logger.endStep('success'); + setSteps(prev => prev.map((s, i) => (i === prev.length - 1 ? { ...s, status: 'success' } : s))); + } + // Clear runtime credentials setRuntimeCredentials(null); @@ -643,6 +706,7 @@ export function useCdkPreflight(options: PreflightOptions): PreflightResult { hasCredentialsError, missingCredentials, identityKmsKeyArn, + oauthCredentials, startPreflight, confirmTeardown, cancelTeardown, diff --git a/src/cli/tui/screens/deploy/useDeployFlow.ts b/src/cli/tui/screens/deploy/useDeployFlow.ts index eaf9b16b..e0783463 100644 --- a/src/cli/tui/screens/deploy/useDeployFlow.ts +++ b/src/cli/tui/screens/deploy/useDeployFlow.ts @@ -1,6 +1,6 @@ import { ConfigIO } from '../../../../lib'; import type { CdkToolkitWrapper, DeployMessage, SwitchableIoHost } from '../../../cdk/toolkit-lib'; -import { buildDeployedState, getStackOutputs, parseAgentOutputs } from '../../../cloudformation'; +import { buildDeployedState, getStackOutputs, parseAgentOutputs, parseGatewayOutputs } from '../../../cloudformation'; import { getErrorMessage, isChangesetInProgressError, isExpiredTokenError } from '../../../errors'; import { ExecLogger } from '../../../logging'; import { performStackTeardown } from '../../../operations/deploy'; @@ -28,6 +28,7 @@ export interface PreSynthesized { stackNames: string[]; switchableIoHost?: SwitchableIoHost; identityKmsKeyArn?: string; + oauthCredentials?: Record; } interface DeployFlowOptions { @@ -88,6 +89,7 @@ export function useDeployFlow(options: DeployFlowOptions = {}): DeployFlowState const stackNames = preSynthesized?.stackNames ?? preflight.stackNames; const switchableIoHost = preSynthesized?.switchableIoHost ?? preflight.switchableIoHost; const identityKmsKeyArn = preSynthesized?.identityKmsKeyArn ?? preflight.identityKmsKeyArn; + const oauthCredentials = preSynthesized?.oauthCredentials ?? preflight.oauthCredentials; const [publishAssetsStep, setPublishAssetsStep] = useState({ label: 'Publish assets', status: 'pending' }); const [deployStep, setDeployStep] = useState({ label: 'Deploy to AWS', status: 'pending' }); @@ -163,6 +165,23 @@ export function useDeployFlow(options: DeployFlowOptions = {}): DeployFlowState ); } + // Parse gateway outputs from CDK stack + let gateways: Record = {}; + try { + const mcpSpec = await configIO.readMcpSpec(); + const gatewaySpecs = + mcpSpec?.agentCoreGateways?.reduce( + (acc: Record, gateway: { name: string }) => { + acc[gateway.name] = gateway; + return acc; + }, + {} as Record + ) ?? {}; + gateways = parseGatewayOutputs(outputs, gatewaySpecs); + } catch (error) { + logger.log(`Failed to read gateway configuration: ${getErrorMessage(error)}`, 'warn'); + } + // Expose outputs to UI setStackOutputs(outputs); @@ -171,12 +190,13 @@ export function useDeployFlow(options: DeployFlowOptions = {}): DeployFlowState target.name, currentStackName, agents, - {}, + gateways, existingState, - identityKmsKeyArn + identityKmsKeyArn, + Object.keys(oauthCredentials).length > 0 ? oauthCredentials : undefined ); await configIO.writeDeployedState(deployedState); - }, [context, stackNames, logger, identityKmsKeyArn]); + }, [context, stackNames, logger, identityKmsKeyArn, oauthCredentials]); // Start deploy when preflight completes OR when shouldStartDeploy is set useEffect(() => {