fix: add aws-local mode - validates AWS CloudFormation templates, executes via local-docker

This commit is contained in:
frostebite
2026-01-28 05:35:46 +00:00
parent d318481e85
commit 3f7c3323f2
4 changed files with 190 additions and 27 deletions

View File

@@ -28,6 +28,9 @@ class CloudRunner {
private static cloudRunnerEnvironmentVariables: CloudRunnerEnvironmentVariable[];
static lockedWorkspace: string = ``;
public static readonly retainedWorkspacePrefix: string = `retained-workspace`;
// When true, validates AWS CloudFormation templates even when using local-docker execution
// This is set by AWS_FORCE_PROVIDER=aws-local mode
public static validateAwsTemplates: boolean = false;
public static get isCloudRunnerEnvironment() {
return process.env[`GITHUB_ACTIONS`] !== `true`;
}
@@ -70,10 +73,14 @@ class CloudRunner {
private static async setupSelectedBuildPlatform() {
CloudRunnerLogger.log(`Cloud Runner platform selected ${CloudRunner.buildParameters.providerStrategy}`);
// Detect LocalStack endpoints and reroute AWS provider to local-docker for CI tests that only need S3
// However, if AWS_FORCE_PROVIDER is set to 'aws', we should use AWS provider even with LocalStack
// This is needed for integrity tests that validate AWS functionality (ECS, CloudFormation, etc.) with LocalStack
const forceAwsProvider = process.env.AWS_FORCE_PROVIDER === 'aws' || process.env.AWS_FORCE_PROVIDER === 'true';
// Detect LocalStack endpoints and handle AWS provider appropriately
// AWS_FORCE_PROVIDER options:
// - 'aws': Force AWS provider (requires LocalStack Pro with ECS support)
// - 'aws-local': Validate AWS templates/config but execute via local-docker (for CI without ECS)
// - unset/other: Auto-fallback to local-docker when LocalStack detected
const awsForceProvider = process.env.AWS_FORCE_PROVIDER || '';
const forceAwsProvider = awsForceProvider === 'aws' || awsForceProvider === 'true';
const useAwsLocalMode = awsForceProvider === 'aws-local';
const endpointsToCheck = [
process.env.AWS_ENDPOINT,
process.env.AWS_S3_ENDPOINT,
@@ -92,18 +99,33 @@ class CloudRunner {
.join(' ');
const isLocalStack = /localstack|localhost|127\.0\.0\.1/i.test(endpointsToCheck);
let provider = CloudRunner.buildParameters.providerStrategy;
if (provider === 'aws' && isLocalStack && !forceAwsProvider) {
CloudRunnerLogger.log('LocalStack endpoints detected; routing provider to local-docker for this run');
CloudRunnerLogger.log(
'Note: Set AWS_FORCE_PROVIDER=aws to force AWS provider with LocalStack for AWS functionality tests',
);
provider = 'local-docker';
} else if (provider === 'aws' && isLocalStack && forceAwsProvider) {
CloudRunnerLogger.log(
'LocalStack endpoints detected but AWS_FORCE_PROVIDER is set; using AWS provider to validate AWS functionality',
);
let validateAwsTemplates = false;
if (provider === 'aws' && isLocalStack) {
if (useAwsLocalMode) {
// aws-local mode: Validate AWS templates but execute via local-docker
// This provides confidence in AWS CloudFormation without requiring LocalStack Pro
CloudRunnerLogger.log('AWS_FORCE_PROVIDER=aws-local: Validating AWS templates, executing via local-docker');
validateAwsTemplates = true;
provider = 'local-docker';
} else if (forceAwsProvider) {
// Force full AWS provider (requires LocalStack Pro with ECS support)
CloudRunnerLogger.log(
'LocalStack endpoints detected but AWS_FORCE_PROVIDER=aws; using full AWS provider (requires ECS support)',
);
} else {
// Auto-fallback to local-docker
CloudRunnerLogger.log('LocalStack endpoints detected; routing provider to local-docker for this run');
CloudRunnerLogger.log(
'Note: Set AWS_FORCE_PROVIDER=aws-local to validate AWS templates with local-docker execution',
);
provider = 'local-docker';
}
}
// Store whether we should validate AWS templates (used by aws-local mode)
CloudRunner.validateAwsTemplates = validateAwsTemplates;
switch (provider) {
case 'k8s':
CloudRunner.Provider = new Kubernetes(CloudRunner.buildParameters);
@@ -157,6 +179,12 @@ class CloudRunner {
throw new Error(`baseImage is undefined`);
}
await CloudRunner.setup(buildParameters);
// When aws-local mode is enabled, validate AWS CloudFormation templates
// This ensures AWS templates are correct even when executing via local-docker
if (CloudRunner.validateAwsTemplates) {
await CloudRunner.validateAwsCloudFormationTemplates();
}
await CloudRunner.Provider.setupWorkflow(
CloudRunner.buildParameters.buildGuid,
CloudRunner.buildParameters,
@@ -252,5 +280,62 @@ class CloudRunner {
const jsonContent = JSON.stringify(content, undefined, 4);
await GitHub.updateGitHubCheck(jsonContent, CloudRunner.buildParameters.buildGuid);
}
/**
* Validates AWS CloudFormation templates without deploying them.
* Used by aws-local mode to ensure AWS templates are correct when executing via local-docker.
* This provides confidence that AWS ECS deployments would work with the generated templates.
*/
private static async validateAwsCloudFormationTemplates() {
CloudRunnerLogger.log('=== AWS CloudFormation Template Validation (aws-local mode) ===');
try {
// Import AWS template formations
const { BaseStackFormation } = await import('./providers/aws/cloud-formations/base-stack-formation');
const { TaskDefinitionFormation } = await import('./providers/aws/cloud-formations/task-definition-formation');
// Validate base stack template
const baseTemplate = BaseStackFormation.formation;
CloudRunnerLogger.log(`✓ Base stack template generated (${baseTemplate.length} chars)`);
// Check for required resources in base stack
const requiredBaseResources = ['AWS::EC2::VPC', 'AWS::ECS::Cluster', 'AWS::S3::Bucket', 'AWS::IAM::Role'];
for (const resource of requiredBaseResources) {
if (baseTemplate.includes(resource)) {
CloudRunnerLogger.log(` ✓ Contains ${resource}`);
} else {
throw new Error(`Base stack template missing required resource: ${resource}`);
}
}
// Validate task definition template
const taskTemplate = TaskDefinitionFormation.formation;
CloudRunnerLogger.log(`✓ Task definition template generated (${taskTemplate.length} chars)`);
// Check for required resources in task definition
const requiredTaskResources = ['AWS::ECS::TaskDefinition', 'AWS::Logs::LogGroup'];
for (const resource of requiredTaskResources) {
if (taskTemplate.includes(resource)) {
CloudRunnerLogger.log(` ✓ Contains ${resource}`);
} else {
throw new Error(`Task definition template missing required resource: ${resource}`);
}
}
// Validate YAML syntax by checking for common patterns
if (!baseTemplate.includes('AWSTemplateFormatVersion')) {
throw new Error('Base stack template missing AWSTemplateFormatVersion');
}
if (!taskTemplate.includes('AWSTemplateFormatVersion')) {
throw new Error('Task definition template missing AWSTemplateFormatVersion');
}
CloudRunnerLogger.log('=== AWS CloudFormation templates validated successfully ===');
CloudRunnerLogger.log('Note: Actual execution will use local-docker provider');
} catch (error: any) {
CloudRunnerLogger.log(`AWS CloudFormation template validation failed: ${error.message}`);
throw error;
}
}
}
export default CloudRunner;