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
8 changes: 4 additions & 4 deletions src/cli/commands/deploy/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import {
checkBootstrapNeeded,
checkStackDeployability,
getAllCredentials,
hasOwnedIdentityApiProviders,
hasOwnedIdentityOAuthProviders,
hasIdentityApiProviders,
hasIdentityOAuthProviders,
performStackTeardown,
setupApiKeyProviders,
setupOAuth2Providers,
Expand Down Expand Up @@ -181,7 +181,7 @@ export async function handleDeploy(options: ValidatedDeployOptions): Promise<Dep
const runtimeCredentials =
Object.keys(envCredentials).length > 0 ? new SecureCredentials(envCredentials) : undefined;

if (hasOwnedIdentityApiProviders(context.projectSpec)) {
if (hasIdentityApiProviders(context.projectSpec)) {
startStep('Creating credentials...');

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

const oauthResult = await setupOAuth2Providers({
Expand Down
10 changes: 5 additions & 5 deletions src/cli/operations/deploy/__tests__/pre-deploy-identity.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {
getAllCredentials,
hasOwnedIdentityOAuthProviders,
hasIdentityOAuthProviders,
setupApiKeyProviders,
setupOAuth2Providers,
} from '../pre-deploy-identity.js';
Expand Down Expand Up @@ -200,27 +200,27 @@ describe('setupApiKeyProviders - KMS key reuse via GetTokenVault', () => {
});
});

describe('hasOwnedIdentityOAuthProviders', () => {
describe('hasIdentityOAuthProviders', () => {
it('returns true when OAuthCredentialProvider exists', () => {
const projectSpec = {
credentials: [
{ name: 'oauth-cred', type: 'OAuthCredentialProvider' },
{ 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);
});
});

Expand Down
4 changes: 2 additions & 2 deletions src/cli/operations/deploy/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ export {
export {
setupApiKeyProviders,
setupOAuth2Providers,
hasOwnedIdentityApiProviders,
hasOwnedIdentityOAuthProviders,
hasIdentityApiProviders,
hasIdentityOAuthProviders,
getMissingCredentials,
getAllCredentials,
type SetupApiKeyProvidersOptions,
Expand Down
4 changes: 2 additions & 2 deletions src/cli/operations/deploy/pre-deploy-identity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}

Expand Down Expand Up @@ -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');
}

Expand Down
68 changes: 66 additions & 2 deletions src/cli/tui/hooks/useCdkPreflight.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ import {
checkStackDeployability,
formatError,
getAllCredentials,
hasOwnedIdentityApiProviders,
hasIdentityApiProviders,
hasIdentityOAuthProviders,
setupApiKeyProviders,
setupOAuth2Providers,
synthesizeCdk,
validateProject,
} from '../../operations/deploy';
Expand Down Expand Up @@ -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<string, { credentialProviderArn: string; clientSecretArn?: string; callbackUrl?: string }>;
startPreflight: () => Promise<void>;
confirmTeardown: () => void;
cancelTeardown: () => void;
Expand Down Expand Up @@ -119,6 +123,9 @@ export function useCdkPreflight(options: PreflightOptions): PreflightResult {
const [runtimeCredentials, setRuntimeCredentials] = useState<SecureCredentials | null>(null);
const [skipIdentitySetup, setSkipIdentitySetup] = useState(false);
const [identityKmsKeyArn, setIdentityKmsKeyArn] = useState<string | undefined>(undefined);
const [oauthCredentials, setOauthCredentials] = useState<
Record<string, { credentialProviderArn: string; clientSecretArn?: string; callbackUrl?: string }>
>({});
const [teardownConfirmed, setTeardownConfirmed] = useState(false);

// Guard against concurrent runs (React StrictMode, re-renders, etc.)
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -643,6 +706,7 @@ export function useCdkPreflight(options: PreflightOptions): PreflightResult {
hasCredentialsError,
missingCredentials,
identityKmsKeyArn,
oauthCredentials,
startPreflight,
confirmTeardown,
cancelTeardown,
Expand Down
28 changes: 24 additions & 4 deletions src/cli/tui/screens/deploy/useDeployFlow.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -28,6 +28,7 @@ export interface PreSynthesized {
stackNames: string[];
switchableIoHost?: SwitchableIoHost;
identityKmsKeyArn?: string;
oauthCredentials?: Record<string, { credentialProviderArn: string; clientSecretArn?: string; callbackUrl?: string }>;
}

interface DeployFlowOptions {
Expand Down Expand Up @@ -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<Step>({ label: 'Publish assets', status: 'pending' });
const [deployStep, setDeployStep] = useState<Step>({ label: 'Deploy to AWS', status: 'pending' });
Expand Down Expand Up @@ -163,6 +165,23 @@ export function useDeployFlow(options: DeployFlowOptions = {}): DeployFlowState
);
}

// Parse gateway outputs from CDK stack
let gateways: Record<string, { gatewayId: string; gatewayArn: string }> = {};
try {
const mcpSpec = await configIO.readMcpSpec();
const gatewaySpecs =
mcpSpec?.agentCoreGateways?.reduce(
(acc: Record<string, unknown>, gateway: { name: string }) => {
acc[gateway.name] = gateway;
return acc;
},
{} as Record<string, unknown>
) ?? {};
gateways = parseGatewayOutputs(outputs, gatewaySpecs);
} catch (error) {
logger.log(`Failed to read gateway configuration: ${getErrorMessage(error)}`, 'warn');
}

// Expose outputs to UI
setStackOutputs(outputs);

Expand All @@ -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(() => {
Expand Down
Loading