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
10 changes: 7 additions & 3 deletions src/cli/aws/account.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { getAwsLoginGuidance } from '../external-requirements/checks';
import { GetCallerIdentityCommand, STSClient } from '@aws-sdk/client-sts';
import { fromEnv, fromNodeProviderChain } from '@aws-sdk/credential-providers';
import type { AwsCredentialIdentityProvider } from '@smithy/types';
Expand Down Expand Up @@ -46,16 +47,18 @@ export async function detectAccount(): Promise<string | null> {
const code = (err as { name?: string })?.name ?? (err as { Code?: string })?.Code;

if (code === 'ExpiredTokenException' || code === 'ExpiredToken') {
const guidance = await getAwsLoginGuidance();
throw new AwsCredentialsError(
'AWS credentials expired.',
'AWS credentials expired.\n\nTo fix this:\n Run: aws login'
`AWS credentials expired.\n\nTo fix this:\n ${guidance}`
);
}

if (code === 'InvalidClientTokenId' || code === 'SignatureDoesNotMatch') {
const guidance = await getAwsLoginGuidance();
throw new AwsCredentialsError(
'AWS credentials are invalid.',
'AWS credentials are invalid.\n\nTo fix this:\n 1. Check your AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY\n 2. Or run: aws login'
`AWS credentials are invalid.\n\nTo fix this:\n 1. Check your AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY\n 2. Or ${guidance}`
);
}

Expand All @@ -77,11 +80,12 @@ export async function detectAccount(): Promise<string | null> {
export async function validateAwsCredentials(): Promise<void> {
const account = await detectAccount();
if (!account) {
const guidance = await getAwsLoginGuidance();
throw new AwsCredentialsError(
'No AWS credentials configured.',
'No AWS credentials configured.\n\n' +
'To fix this:\n' +
' 1. Run: aws login\n' +
` 1. ${guidance}\n` +
' 2. Or set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables'
);
}
Expand Down
71 changes: 69 additions & 2 deletions src/cli/external-requirements/checks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import { checkSubprocess, isWindows, runSubprocessCapture } from '../../lib';
import type { AgentCoreProjectSpec, TargetLanguage } from '../../schema';
import { detectContainerRuntime } from './detect';
import { NODE_MIN_VERSION, formatSemVer, parseSemVer, semVerGte } from './versions';
import { AWS_CLI_MIN_VERSION, NODE_MIN_VERSION, formatSemVer, parseSemVer, semVerGte } from './versions';

/**
* Result of a version check.
Expand Down Expand Up @@ -70,6 +70,73 @@ export async function checkUvVersion(): Promise<VersionCheckResult> {
return { satisfied: true, current, required: 'any', binary: 'uv' };
}

const AWS_CLI_INSTALL_URL = 'https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html';

/**
* Extract version from `aws --version` output.
* Expected format: "aws-cli/2.32.0 Python/3.11.6 Darwin/23.3.0 ..."
*/
function parseAwsCliVersion(output: string): string | null {
const match = /aws-cli\/(\d+\.\d+\.\d+)/.exec(output.trim());
return match?.[1] ?? null;
}

/**
* Check that AWS CLI meets minimum version requirement for `aws login`.
*/
export async function checkAwsCliVersion(): Promise<VersionCheckResult> {
const required = formatSemVer(AWS_CLI_MIN_VERSION);

const result = await runSubprocessCapture('aws', ['--version']);
if (result.code !== 0) {
return { satisfied: false, current: null, required, binary: 'aws' };
}

const versionStr = parseAwsCliVersion(result.stdout);
if (!versionStr) {
return { satisfied: false, current: null, required, binary: 'aws' };
}

const current = parseSemVer(versionStr);
if (!current) {
return { satisfied: false, current: versionStr, required, binary: 'aws' };
}

return {
satisfied: semVerGte(current, AWS_CLI_MIN_VERSION),
current: versionStr,
required,
binary: 'aws',
};
}

/** Cached result for getAwsLoginGuidance */
let _awsLoginGuidance: string | null = null;

/**
* Get version-aware guidance for authenticating with AWS.
* Checks if AWS CLI is installed and whether it supports `aws login`.
* Result is cached for the lifetime of the process.
*/
export async function getAwsLoginGuidance(): Promise<string> {
if (_awsLoginGuidance) return _awsLoginGuidance;

const check = await checkAwsCliVersion();

if (check.current === null) {
// AWS CLI not installed
_awsLoginGuidance = `Install AWS CLI (v${formatSemVer(AWS_CLI_MIN_VERSION)}+) from ${AWS_CLI_INSTALL_URL} and run: aws login`;
} else if (!check.satisfied) {
// AWS CLI installed but too old for `aws login`
_awsLoginGuidance = `Update AWS CLI from v${check.current} to v${formatSemVer(AWS_CLI_MIN_VERSION)}+ (${AWS_CLI_INSTALL_URL}) and run: aws login`;
} else {
// AWS CLI is new enough
_awsLoginGuidance = 'Run: aws login';
}

return _awsLoginGuidance;
}

/**
* Format a version check failure as a user-friendly error message.
*/
Expand Down Expand Up @@ -234,7 +301,7 @@ export async function checkCreateDependencies(
});
if (!awsAvailable) {
warnings.push(
"'aws' CLI not found. Required for 'aws sso login' and profile configuration. Install from https://aws.amazon.com/cli/"
`'aws' CLI not found. Required for 'aws login'. Install v${formatSemVer(AWS_CLI_MIN_VERSION)}+ from ${AWS_CLI_INSTALL_URL}`
);
}

Expand Down
12 changes: 11 additions & 1 deletion src/cli/external-requirements/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
export { parseSemVer, compareSemVer, semVerGte, formatSemVer, NODE_MIN_VERSION, type SemVer } from './versions';
export {
parseSemVer,
compareSemVer,
semVerGte,
formatSemVer,
NODE_MIN_VERSION,
AWS_CLI_MIN_VERSION,
type SemVer,
} from './versions';

export {
checkNodeVersion,
checkUvVersion,
checkAwsCliVersion,
getAwsLoginGuidance,
formatVersionError,
requiresUv,
requiresContainerRuntime,
Expand Down
3 changes: 3 additions & 0 deletions src/cli/external-requirements/versions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,6 @@ export function formatSemVer(v: SemVer): string {

/** Minimum Node.js version required for CDK synth (ES2022 target) */
export const NODE_MIN_VERSION: SemVer = { major: 18, minor: 0, patch: 0 };

/** Minimum AWS CLI version required for `aws login` */
export const AWS_CLI_MIN_VERSION: SemVer = { major: 2, minor: 32, patch: 0 };
3 changes: 2 additions & 1 deletion src/cli/operations/deploy/pre-deploy-identity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { SecureCredentials, readEnvFile } from '../../../lib';
import type { AgentCoreProjectSpec, Credential } from '../../../schema';
import { getCredentialProvider } from '../../aws';
import { isNoCredentialsError } from '../../errors';
import { getAwsLoginGuidance } from '../../external-requirements/checks';
import { apiKeyProviderExists, createApiKeyProvider, setTokenVaultKmsKey, updateApiKeyProvider } from '../identity';
import { computeDefaultCredentialEnvVarName } from '../identity/create-identity';
import { BedrockAgentCoreControlClient, GetTokenVaultCommand } from '@aws-sdk/client-bedrock-agentcore-control';
Expand Down Expand Up @@ -171,7 +172,7 @@ async function setupApiKeyCredentialProvider(
// Provide clearer error message for AWS credentials issues
let errorMessage: string;
if (isNoCredentialsError(error)) {
errorMessage = 'AWS credentials not found. Run `aws sso login` or set AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY.';
errorMessage = `AWS credentials not found. ${await getAwsLoginGuidance()}`;
} else {
errorMessage = error instanceof Error ? error.message : String(error);
}
Expand Down
Loading