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
1 change: 0 additions & 1 deletion src/datastore/DataStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ export enum StoreName {
public_schemas = 'public_schemas',
sam_schemas = 'sam_schemas',
private_schemas = 'private_schemas',
combined_schemas = 'combined_schemas',
}

export interface DataStore {
Expand Down
3 changes: 1 addition & 2 deletions src/handlers/Initialize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,16 @@ const logger = LoggerFactory.getLogger('InitializedHandler');

export function initializedHandler(workspace: LspWorkspace, components: ServerComponents): () => void {
return (): void => {
// Sync configuration from LSP workspace first, then initialize CfnLintService
components.settingsManager
.syncConfiguration()
.then(() => {
components.schemaRetriever.initialize();
return components.cfnLintService.initialize();
})
.then(async () => {
// Process folders sequentially to avoid overwhelming the system
for (const folder of workspace.getAllWorkspaceFolders()) {
try {
// Properly await the async mountFolder method
await components.cfnLintService.mountFolder(folder);
} catch (error) {
logger.error(error, `Failed to mount folder ${folder.name}`);
Expand Down
14 changes: 6 additions & 8 deletions src/schema/CombinedSchemas.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import { LoggerFactory } from '../telemetry/LoggerFactory';
import { PrivateSchemas, PrivateSchemasType } from './PrivateSchemas';
import { RegionalSchemas, RegionalSchemasType } from './RegionalSchemas';
import { ResourceSchema } from './ResourceSchema';
import { SamSchemas, SamSchemasType } from './SamSchemas';

export class CombinedSchemas {
private static readonly log = LoggerFactory.getLogger('CombinedSchemas');
readonly numSchemas: number;
readonly schemas: Map<string, ResourceSchema>;

constructor(regionalSchemas?: RegionalSchemas, privateSchemas?: PrivateSchemas, samSchemas?: SamSchemas) {
constructor(
readonly regionalSchemas?: RegionalSchemas,
readonly privateSchemas?: PrivateSchemas,
readonly samSchemas?: SamSchemas,
) {
this.schemas = new Map<string, ResourceSchema>([
...(privateSchemas?.schemas ?? []),
...(regionalSchemas?.schemas ?? []),
Expand All @@ -22,14 +24,10 @@ export class CombinedSchemas {
regionalSchemas?: RegionalSchemasType,
privateSchemas?: PrivateSchemasType,
samSchemas?: SamSchemasType,
) {
): CombinedSchemas {
const regionalSchema = regionalSchemas === undefined ? undefined : RegionalSchemas.from(regionalSchemas);
const privateSchema = privateSchemas === undefined ? undefined : PrivateSchemas.from(privateSchemas);
const samSchema = samSchemas === undefined ? undefined : SamSchemas.from(samSchemas);

CombinedSchemas.log.info(
`Combined schemas from public=${regionalSchemas?.schemas.length}, private=${privateSchema?.schemas.size}, SAM=${samSchema?.schemas.size}`,
);
return new CombinedSchemas(regionalSchema, privateSchema, samSchema);
}
}
10 changes: 6 additions & 4 deletions src/schema/GetSamSchemaTask.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import { Logger } from 'pino';
import { DataStore } from '../datastore/DataStore';
import { LoggerFactory } from '../telemetry/LoggerFactory';
import { Measure } from '../telemetry/TelemetryDecorator';
import { downloadJson } from '../utils/RemoteDownload';
import { GetSchemaTask } from './GetSchemaTask';
import { SamSchemas, SamSchemasType, SamStoreKey } from './SamSchemas';
import { CloudFormationResourceSchema, SamSchema, SamSchemaTransformer } from './SamSchemaTransformer';

export class GetSamSchemaTask extends GetSchemaTask {
private readonly logger = LoggerFactory.getLogger(GetSamSchemaTask);

constructor(private readonly getSamSchemas: () => Promise<Map<string, CloudFormationResourceSchema>>) {
super();
}

@Measure({ name: 'getSchemas' })
protected override async runImpl(dataStore: DataStore, logger?: Logger): Promise<void> {
protected override async runImpl(dataStore: DataStore): Promise<void> {
try {
const resourceSchemas = await this.getSamSchemas();

Expand All @@ -32,9 +34,9 @@ export class GetSamSchemaTask extends GetSchemaTask {

await dataStore.put(SamStoreKey, samSchemasData);

logger?.info(`${resourceSchemas.size} SAM schemas downloaded and stored`);
this.logger.info(`${resourceSchemas.size} SAM schemas downloaded and stored`);
} catch (error) {
logger?.error(error, 'Failed to download SAM schema');
this.logger.error(error, 'Failed to download SAM schemas');
throw error;
}
}
Expand Down
52 changes: 28 additions & 24 deletions src/schema/GetSchemaTask.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,27 @@
import { DescribeTypeOutput } from '@aws-sdk/client-cloudformation';
import { Logger } from 'pino';
import { AwsCredentials } from '../auth/AwsCredentials';
import { DataStore } from '../datastore/DataStore';
import { CfnService } from '../services/CfnService';
import { LoggerFactory } from '../telemetry/LoggerFactory';
import { ScopedTelemetry } from '../telemetry/ScopedTelemetry';
import { Measure, Telemetry } from '../telemetry/TelemetryDecorator';
import { AwsRegion } from '../utils/Region';
import { downloadFile } from '../utils/RemoteDownload';
import { PrivateSchemas, PrivateSchemasType } from './PrivateSchemas';
import { PrivateSchemas, PrivateSchemasType, PrivateStoreKey } from './PrivateSchemas';
import { RegionalSchemas, RegionalSchemasType, SchemaFileType } from './RegionalSchemas';
import { cfnResourceSchemaLink, unZipFile } from './RemoteSchemaHelper';

export abstract class GetSchemaTask {
protected abstract runImpl(dataStore: DataStore, logger?: Logger): Promise<void>;
protected abstract runImpl(dataStore: DataStore): Promise<void>;

async run(dataStore: DataStore, logger?: Logger) {
await this.runImpl(dataStore, logger);
async run(dataStore: DataStore) {
await this.runImpl(dataStore);
}
}

export class GetPublicSchemaTask extends GetSchemaTask {
private readonly logger = LoggerFactory.getLogger(GetPublicSchemaTask);

@Telemetry()
private readonly telemetry!: ScopedTelemetry;

Expand All @@ -35,9 +37,20 @@ export class GetPublicSchemaTask extends GetSchemaTask {
}

@Measure({ name: 'getSchemas' })
protected override async runImpl(dataStore: DataStore, logger?: Logger) {
protected override async runImpl(dataStore: DataStore) {
this.telemetry.count(`getSchemas.maxAttempt.fault`, 0, {
attributes: {
region: this.region,
},
});

if (this.attempts >= GetPublicSchemaTask.MaxAttempts) {
logger?.error(`Reached max attempts for retrieving schemas for ${this.region} without success`);
this.telemetry.count(`getSchemas.maxAttempt.fault`, 1, {
attributes: {
region: this.region,
},
});
this.logger.error(`Reached max attempts for retrieving schemas for ${this.region} without success`);
return;
}

Expand All @@ -53,44 +66,35 @@ export class GetPublicSchemaTask extends GetSchemaTask {
};

await dataStore.put<RegionalSchemasType>(this.region, value);
logger?.info(`${schemas.length} public schemas retrieved for ${this.region}`);
this.logger.info(`${schemas.length} public schemas retrieved for ${this.region}`);
}
}

export class GetPrivateSchemasTask extends GetSchemaTask {
private readonly processedProfiles = new Set<string>();
private readonly logger = LoggerFactory.getLogger(GetPrivateSchemasTask);

constructor(
private readonly getSchemas: () => Promise<DescribeTypeOutput[]>,
private readonly getProfile: () => string,
) {
constructor(private readonly getSchemas: () => Promise<DescribeTypeOutput[]>) {
super();
}

@Measure({ name: 'getSchemas' })
protected override async runImpl(dataStore: DataStore, logger?: Logger) {
protected override async runImpl(dataStore: DataStore) {
try {
const profile = this.getProfile();
if (this.processedProfiles.has(profile)) {
return;
}

const schemas: DescribeTypeOutput[] = await this.getSchemas();

const value: PrivateSchemasType = {
version: PrivateSchemas.V1,
identifier: profile,
identifier: PrivateStoreKey,
schemas: schemas,
firstCreatedMs: Date.now(),
lastModifiedMs: Date.now(),
};

await dataStore.put<PrivateSchemasType>(profile, value);
await dataStore.put<PrivateSchemasType>(PrivateStoreKey, value);

this.processedProfiles.add(profile);
logger?.info(`${schemas.length} private schemas retrieved`);
this.logger.info(`${schemas.length} private schemas retrieved`);
} catch (error) {
logger?.error(error, `Failed to get private schemas`);
this.logger.error(error, 'Failed to get private schemas');
throw error;
}
}
Expand Down
131 changes: 36 additions & 95 deletions src/schema/GetSchemaTaskManager.ts
Original file line number Diff line number Diff line change
@@ -1,132 +1,73 @@
import { DescribeTypeOutput } from '@aws-sdk/client-cloudformation';
import { SettingsConfigurable, ISettingsSubscriber, SettingsSubscription } from '../settings/ISettingsSubscriber';
import { DefaultSettings } from '../settings/Settings';
import { LoggerFactory } from '../telemetry/LoggerFactory';
import { Closeable } from '../utils/Closeable';
import { AwsRegion } from '../utils/Region';
import { AwsRegion, getRegion } from '../utils/Region';
import { GetSamSchemaTask } from './GetSamSchemaTask';
import { GetPrivateSchemasTask, GetPublicSchemaTask } from './GetSchemaTask';
import { SchemaFileType } from './RegionalSchemas';
import { CloudFormationResourceSchema } from './SamSchemaTransformer';
import { SchemaStore } from './SchemaStore';

const TenSeconds = 10 * 1000;
const OneHour = 60 * 60 * 1000;

export class GetSchemaTaskManager implements SettingsConfigurable, Closeable {
export class GetSchemaTaskManager {
private readonly processedRegions = new Set<AwsRegion>();
private readonly tasks: GetPublicSchemaTask[] = [];
private readonly privateTask: GetPrivateSchemasTask;
private readonly samTask: GetSamSchemaTask;
private settingsSubscription?: SettingsSubscription;
private readonly log = LoggerFactory.getLogger(GetSchemaTaskManager);

private isRunning: boolean = false;

private readonly timeout: NodeJS.Timeout;
private readonly interval: NodeJS.Timeout;
private isRunning = false;

constructor(
private readonly schemas: SchemaStore,
private readonly getPublicSchemas: (region: AwsRegion) => Promise<SchemaFileType[]>,
getPrivateResources: () => Promise<DescribeTypeOutput[]>,
getSamSchemas: () => Promise<Map<string, CloudFormationResourceSchema>>,
private profile: string = DefaultSettings.profile.profile,
private readonly onSchemaUpdate: (region?: string, profile?: string) => void,
) {
this.privateTask = new GetPrivateSchemasTask(getPrivateResources, () => this.profile);
this.privateTask = new GetPrivateSchemasTask(getPrivateResources);
this.samTask = new GetSamSchemaTask(getSamSchemas);

this.timeout = setTimeout(() => {
// Wait before trying to call CFN APIs so that credentials have time to update
this.runPrivateTask();
}, TenSeconds);

this.interval = setInterval(() => {
// Keep private schemas up to date with credential changes if profile has not already ben loaded
this.runPrivateTask();
}, OneHour);
}

configure(settingsManager: ISettingsSubscriber): void {
// Clean up existing subscription if present
if (this.settingsSubscription) {
this.settingsSubscription.unsubscribe();
}

// Set initial settings
this.profile = settingsManager.getCurrentSettings().profile.profile;
addTask(reg: string, regionFirstCreatedMs?: number) {
const region = getRegion(reg);

// Subscribe to profile settings changes
this.settingsSubscription = settingsManager.subscribe('profile', (newProfileSettings) => {
this.onSettingsChanged(newProfileSettings.profile);
});
}
if (!this.processedRegions.has(region)) {
this.tasks.push(new GetPublicSchemaTask(region, this.getPublicSchemas, regionFirstCreatedMs));
this.processedRegions.add(region);
}

private onSettingsChanged(newProfile: string): void {
this.profile = newProfile;
if (!this.isRunning) {
this.runNextTask();
}
}

addTask(region: AwsRegion, regionFirstCreatedMs?: number) {
if (!this.currentRegionalTasks().has(region)) {
this.tasks.push(new GetPublicSchemaTask(region, this.getPublicSchemas, regionFirstCreatedMs));
private runNextTask() {
const task = this.tasks.shift();
if (!task) {
this.isRunning = false;
return;
}
this.startProcessing();

this.isRunning = true;
task.run(this.schemas.publicSchemas)
.catch((err) => {
this.log.error(err);
this.tasks.push(task);
})
.finally(() => {
this.isRunning = false;
this.runNextTask();
});
}

runPrivateTask() {
this.privateTask
.run(this.schemas.privateSchemas, this.log)
.then(() => {
this.onSchemaUpdate(undefined, this.profile);
})
.catch(() => {});
.run(this.schemas.privateSchemas)
.then(() => this.schemas.invalidate())
.catch(this.log.error);
}

runSamTask() {
this.samTask
.run(this.schemas.samSchemas, this.log)
.then(() => {
this.onSchemaUpdate(); // No params = SAM update
})
.catch(() => {});
}

public currentRegionalTasks() {
return new Set(this.tasks.map((task) => task.region));
}

private startProcessing() {
if (!this.isRunning && this.tasks.length > 0) {
this.isRunning = true;
this.run();
}
}

private run() {
const task = this.tasks.shift();
if (task) {
task.run(this.schemas.publicSchemas, this.log)
.then(() => {
this.onSchemaUpdate(task.region);
})
.catch(() => {
this.tasks.push(task);
})
.finally(() => {
this.isRunning = false;
this.startProcessing();
});
}
}

public close() {
// Unsubscribe from settings changes
if (this.settingsSubscription) {
this.settingsSubscription.unsubscribe();
this.settingsSubscription = undefined;
}

clearTimeout(this.timeout);
clearInterval(this.interval);
.run(this.schemas.samSchemas)
.then(() => this.schemas.invalidate())
.catch(this.log.error);
}
}
1 change: 1 addition & 0 deletions src/schema/PrivateSchemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export type PrivateSchemasType = {
lastModifiedMs: number;
};

export const PrivateStoreKey = 'PrivateSchemas';
export class PrivateSchemas {
static readonly V1 = 'v1';

Expand Down
7 changes: 1 addition & 6 deletions src/schema/SamSchemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,7 @@ export class SamSchemas {
readonly lastModifiedMs: number;
readonly schemas: Map<string, ResourceSchema>;

constructor(
version: string,
schemas: { name: string; content: string; createdMs: number }[],
firstCreatedMs: number,
lastModifiedMs: number,
) {
constructor(version: string, schemas: SchemaFileType[], firstCreatedMs: number, lastModifiedMs: number) {
this.version = version;
this.firstCreatedMs = firstCreatedMs;
this.lastModifiedMs = lastModifiedMs;
Expand Down
Loading
Loading