Skip to content

Commit 09d8b0f

Browse files
authored
Create new feature flag and make schemas offline for e2e tests (#258)
Create hardware feature flag and make schemas offline for e2e test
1 parent e28424a commit 09d8b0f

17 files changed

+349
-64
lines changed

src/featureFlag/FeatureFlagProvider.ts

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@ export class FeatureFlagProvider implements Closeable {
1616

1717
private readonly timeout: NodeJS.Timeout;
1818

19-
constructor(private readonly localFile = join(__dirname, 'assets', 'featureFlag', `${AwsEnv.toLowerCase()}.json`)) {
19+
constructor(
20+
private readonly getLatestFeatureFlags: (env: string) => Promise<unknown>,
21+
private readonly localFile = join(__dirname, 'assets', 'featureFlag', `${AwsEnv.toLowerCase()}.json`),
22+
) {
2023
this.config = JSON.parse(readFileSync(localFile, 'utf8'));
2124

2225
this.supplier = new FeatureFlagSupplier(() => {
@@ -49,18 +52,16 @@ export class FeatureFlagProvider implements Closeable {
4952
}
5053

5154
private async refresh() {
52-
const newConfig = await this.getFromOnline(AwsEnv);
55+
const newConfig = await this.getFeatureFlags(AwsEnv);
5356
this.config = newConfig;
5457
writeFileSync(this.localFile, JSON.stringify(newConfig, undefined, 2));
5558

5659
this.log();
5760
}
5861

59-
@Measure({ name: 'getFromOnline' })
60-
private async getFromOnline(env: string): Promise<unknown> {
61-
return await downloadJson(
62-
`https://raw.githubusercontent.com/aws-cloudformation/cloudformation-languageserver/refs/head/main/assets/featureFlag/${env.toLowerCase()}.json`,
63-
);
62+
@Measure({ name: 'getFeatureFlags' })
63+
private async getFeatureFlags(env: string): Promise<unknown> {
64+
return await this.getLatestFeatureFlags(env);
6465
}
6566

6667
private log() {
@@ -79,3 +80,9 @@ export class FeatureFlagProvider implements Closeable {
7980
clearInterval(this.timeout);
8081
}
8182
}
83+
84+
export function getFromGitHub(env: string): Promise<unknown> {
85+
return downloadJson(
86+
`https://raw.githubusercontent.com/aws-cloudformation/cloudformation-languageserver/refs/heads/main/assets/featureFlag/${env.toLowerCase()}.json`,
87+
);
88+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { arch, platform, release, version } from 'os';
2+
import { FeatureFlag } from './FeatureFlagI';
3+
4+
type HardwareMatch = {
5+
arch?: string | string[];
6+
platform?: string | string[];
7+
release?: string | string[];
8+
version?: string | string[];
9+
nodeVersion?: string | string[];
10+
processArch?: string | string[];
11+
processPlatform?: string | string[];
12+
};
13+
14+
export class HardwareFeatureFlag implements FeatureFlag {
15+
private readonly enabled: boolean;
16+
17+
constructor(
18+
private readonly featureName: string,
19+
private readonly match: HardwareMatch,
20+
private readonly partial: boolean = false,
21+
) {
22+
const checks = [
23+
this.matchProperty(arch(), this.match.arch),
24+
this.matchProperty(platform(), this.match.platform),
25+
this.matchProperty(release(), this.match.release),
26+
this.matchProperty(version(), this.match.version),
27+
this.matchProperty(process.version, this.match.nodeVersion),
28+
this.matchProperty(process.arch, this.match.processArch),
29+
this.matchProperty(process.platform, this.match.processPlatform),
30+
];
31+
32+
this.enabled = checks.every(Boolean);
33+
}
34+
35+
private matchProperty(actual: string, expected?: string | string[]): boolean {
36+
if (expected === undefined) {
37+
return true;
38+
}
39+
40+
const patterns = Array.isArray(expected) ? expected : [expected];
41+
42+
return patterns.some((pattern) => {
43+
return this.partial ? actual.includes(pattern) : actual === pattern;
44+
});
45+
}
46+
47+
isEnabled(): boolean {
48+
return this.enabled;
49+
}
50+
51+
describe(): string {
52+
const matchStr = JSON.stringify(this.match);
53+
return `HardwareFeatureFlag(feature=${this.featureName}, match=${matchStr}, partial=${this.partial}, enabled=${this.enabled})`;
54+
}
55+
}

src/schema/GetSamSchemaTask.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,19 @@ import { Measure } from '../telemetry/TelemetryDecorator';
44
import { downloadJson } from '../utils/RemoteDownload';
55
import { GetSchemaTask } from './GetSchemaTask';
66
import { SamSchemas, SamSchemasType, SamStoreKey } from './SamSchemas';
7-
import { SamSchemaTransformer, SamSchema } from './SamSchemaTransformer';
7+
import { CloudFormationResourceSchema, SamSchema, SamSchemaTransformer } from './SamSchemaTransformer';
88

99
const logger = LoggerFactory.getLogger('GetSamSchemaTask');
1010

1111
export class GetSamSchemaTask extends GetSchemaTask {
12-
private static readonly SAM_SCHEMA_URL =
13-
'https://raw.githubusercontent.com/aws/serverless-application-model/refs/heads/develop/samtranslator/schema/schema.json';
12+
constructor(private readonly getSamSchemas: () => Promise<Map<string, CloudFormationResourceSchema>>) {
13+
super();
14+
}
1415

1516
@Measure({ name: 'getSchemas' })
1617
override async runImpl(dataStore: DataStore): Promise<void> {
1718
try {
18-
const samSchema = await downloadJson<Record<string, unknown>>(GetSamSchemaTask.SAM_SCHEMA_URL);
19-
20-
const resourceSchemas = SamSchemaTransformer.transformSamSchema(samSchema as unknown as SamSchema);
19+
const resourceSchemas = await this.getSamSchemas();
2120

2221
// Convert to SamSchemasType format
2322
const schemas = [...resourceSchemas.entries()].map(([resourceType, schema]) => ({
@@ -42,3 +41,11 @@ export class GetSamSchemaTask extends GetSchemaTask {
4241
}
4342
}
4443
}
44+
45+
export async function getSamSchemas(): Promise<Map<string, CloudFormationResourceSchema>> {
46+
const SAM_SCHEMA_URL =
47+
'https://raw.githubusercontent.com/aws/serverless-application-model/refs/heads/develop/samtranslator/schema/schema.json';
48+
49+
const samSchema = await downloadJson<SamSchema>(SAM_SCHEMA_URL);
50+
return SamSchemaTransformer.transformSamSchema(samSchema);
51+
}

src/schema/GetSchemaTask.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,11 +100,14 @@ export class GetPrivateSchemasTask extends GetSchemaTask {
100100
}
101101
}
102102

103-
export function getRemotePublicSchemas(region: AwsRegion) {
103+
export function getRemotePublicSchemas(region: AwsRegion): Promise<SchemaFileType[]> {
104104
return unZipFile(downloadFile(cfnResourceSchemaLink(region)));
105105
}
106106

107-
export function getRemotePrivateSchemas(awsCredentials: AwsCredentials, cfnService: CfnService) {
107+
export function getRemotePrivateSchemas(
108+
awsCredentials: AwsCredentials,
109+
cfnService: CfnService,
110+
): Promise<DescribeTypeOutput[]> {
108111
if (awsCredentials.credentialsAvailable()) {
109112
return cfnService.getAllPrivateResourceSchemas();
110113
}

src/schema/GetSchemaTaskManager.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { AwsRegion } from '../utils/Region';
77
import { GetSamSchemaTask } from './GetSamSchemaTask';
88
import { GetPrivateSchemasTask, GetPublicSchemaTask } from './GetSchemaTask';
99
import { SchemaFileType } from './RegionalSchemas';
10+
import { CloudFormationResourceSchema } from './SamSchemaTransformer';
1011
import { SchemaStore } from './SchemaStore';
1112

1213
const TenSeconds = 10 * 1000;
@@ -27,12 +28,13 @@ export class GetSchemaTaskManager implements SettingsConfigurable, Closeable {
2728
constructor(
2829
private readonly schemas: SchemaStore,
2930
private readonly getPublicSchemas: (region: AwsRegion) => Promise<SchemaFileType[]>,
30-
private readonly getPrivateResources: () => Promise<DescribeTypeOutput[]>,
31+
getPrivateResources: () => Promise<DescribeTypeOutput[]>,
32+
getSamSchemas: () => Promise<Map<string, CloudFormationResourceSchema>>,
3133
private profile: string = DefaultSettings.profile.profile,
3234
private readonly onSchemaUpdate: (region?: string, profile?: string) => void,
3335
) {
34-
this.privateTask = new GetPrivateSchemasTask(this.getPrivateResources, () => this.profile);
35-
this.samTask = new GetSamSchemaTask();
36+
this.privateTask = new GetPrivateSchemasTask(getPrivateResources, () => this.profile);
37+
this.samTask = new GetSamSchemaTask(getSamSchemas);
3638

3739
this.timeout = setTimeout(() => {
3840
// Wait before trying to call CFN APIs so that credentials have time to update

src/schema/SamSchemaTransformer.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export interface SamSchema {
1111
};
1212
}
1313

14-
interface CloudFormationResourceSchema {
14+
export type CloudFormationResourceSchema = {
1515
typeName: string;
1616
description: string;
1717
documentationUrl?: string;
@@ -24,7 +24,7 @@ interface CloudFormationResourceSchema {
2424
createOnlyProperties?: string[];
2525
primaryIdentifier?: string[];
2626
attributes?: Record<string, unknown>;
27-
}
27+
};
2828

2929
export const SamSchemaTransformer = {
3030
transformSamSchema(samSchema: SamSchema): Map<string, CloudFormationResourceSchema> {

src/schema/SamSchemas.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1+
import { SchemaFileType } from './RegionalSchemas';
12
import { ResourceSchema } from './ResourceSchema';
23

34
export type SamSchemasType = {
45
version: string;
5-
schemas: { name: string; content: string; createdMs: number }[];
6+
schemas: SchemaFileType[];
67
firstCreatedMs: number;
78
lastModifiedMs: number;
89
};

src/schema/SchemaRetriever.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { GetSchemaTaskManager } from './GetSchemaTaskManager';
1212
import { PrivateSchemasType } from './PrivateSchemas';
1313
import { RegionalSchemasType, SchemaFileType } from './RegionalSchemas';
1414
import { SamSchemasType, SamStoreKey } from './SamSchemas';
15+
import { CloudFormationResourceSchema } from './SamSchemaTransformer';
1516
import { SchemaStore } from './SchemaStore';
1617

1718
const StaleDaysThreshold = 7;
@@ -29,12 +30,14 @@ export class SchemaRetriever implements SettingsConfigurable, Closeable {
2930
constructor(
3031
private readonly schemaStore: SchemaStore,
3132
private readonly getPublicSchemas: (region: AwsRegion) => Promise<SchemaFileType[]>,
32-
private readonly getPrivateResources: () => Promise<DescribeTypeOutput[]>,
33+
getPrivateResources: () => Promise<DescribeTypeOutput[]>,
34+
getSamSchemas: () => Promise<Map<string, CloudFormationResourceSchema>>,
3335
) {
3436
this.schemaTaskManager = new GetSchemaTaskManager(
3537
this.schemaStore,
3638
this.getPublicSchemas,
37-
this.getPrivateResources,
39+
getPrivateResources,
40+
getSamSchemas,
3841
this.settings.profile,
3942
(region, profile) => this.rebuildAffectedCombinedSchemas(region, profile),
4043
);

src/server/CfnExternal.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { FeatureFlagProvider } from '../featureFlag/FeatureFlagProvider';
1+
import { FeatureFlagProvider, getFromGitHub } from '../featureFlag/FeatureFlagProvider';
22
import { LspComponents } from '../protocol/LspComponents';
3+
import { getSamSchemas } from '../schema/GetSamSchemaTask';
34
import { getRemotePrivateSchemas, getRemotePublicSchemas } from '../schema/GetSchemaTask';
45
import { SchemaRetriever } from '../schema/SchemaRetriever';
56
import { SchemaStore } from '../schema/SchemaStore';
@@ -46,8 +47,11 @@ export class CfnExternal implements Configurables, Closeable {
4647
this.schemaStore = overrides.schemaStore ?? new SchemaStore(core.dataStoreFactory);
4748
this.schemaRetriever =
4849
overrides.schemaRetriever ??
49-
new SchemaRetriever(this.schemaStore, getRemotePublicSchemas, () =>
50-
getRemotePrivateSchemas(core.awsCredentials, this.cfnService),
50+
new SchemaRetriever(
51+
this.schemaStore,
52+
getRemotePublicSchemas,
53+
() => getRemotePrivateSchemas(core.awsCredentials, this.cfnService),
54+
getSamSchemas,
5155
);
5256

5357
this.cfnLintService =
@@ -58,7 +62,7 @@ export class CfnExternal implements Configurables, Closeable {
5862
new GuardService(core.documentManager, core.diagnosticCoordinator, core.syntaxTreeManager);
5963

6064
this.onlineStatus = overrides.onlineStatus ?? new OnlineStatus(core.clientMessage);
61-
this.featureFlags = overrides.featureFlags ?? new FeatureFlagProvider();
65+
this.featureFlags = overrides.featureFlags ?? new FeatureFlagProvider(getFromGitHub);
6266
}
6367

6468
configurables(): Configurable[] {

tools/telemetry-generator.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,8 @@ import {
4444
createMockLspCommunication,
4545
createMockAuthHandlers,
4646
} from '../tst/utils/MockServerComponents';
47-
import { getTestPrivateSchemas } from '../tst/utils/SchemaUtils';
4847
import { MemoryDataStoreFactoryProvider } from '../src/datastore/DataStore';
4948
import { SchemaStore } from '../src/schema/SchemaStore';
50-
import { GetSchemaTaskManager } from '../src/schema/GetSchemaTaskManager';
51-
import { getRemotePublicSchemas } from '../src/schema/GetSchemaTask';
5249
import { completionHandler } from '../src/handlers/CompletionHandler';
5350
import { hoverHandler } from '../src/handlers/HoverHandler';
5451
import { definitionHandler } from '../src/handlers/DefinitionHandler';
@@ -67,7 +64,6 @@ import { LspResourceHandlers } from '../src/protocol/LspResourceHandlers';
6764
import { LspRelatedResourcesHandlers } from '../src/protocol/LspRelatedResourcesHandlers';
6865
import { LspS3Handlers } from '../src/protocol/LspS3Handlers';
6966
import { ExtendedInitializeParams } from '../src/server/InitParams';
70-
import { FeatureFlagProvider } from '../src/featureFlag/FeatureFlagProvider';
7167
import { RelationshipSchemaService } from '../src/services/RelationshipSchemaService';
7268
import { LspCfnEnvironmentHandlers } from '../src/protocol/LspCfnEnvironmentHandlers';
7369

@@ -200,7 +196,6 @@ function main() {
200196
const schemaStore = new SchemaStore(dataStoreFactory);
201197
const external = new CfnExternal(lsp, core, {
202198
schemaStore,
203-
featureFlags: new FeatureFlagProvider(join(__dirname, '..', 'assets', 'featureFlag', 'alpha.json')),
204199
});
205200

206201
const providers = new CfnLspProviders(core, external, {

0 commit comments

Comments
 (0)