diff --git a/packages/@aws-cdk/aws-elasticache-alpha/grants.json b/packages/@aws-cdk/aws-elasticache-alpha/grants.json new file mode 100644 index 0000000000000..5a6741893975b --- /dev/null +++ b/packages/@aws-cdk/aws-elasticache-alpha/grants.json @@ -0,0 +1,14 @@ +{ + "resources": { + "ServerlessCache": { + "grants": { + "connect": { + "actions": [ + "elasticache:Connect", "elasticache:DescribeServerlessCaches" + ], + "docSummary": "Grant connect permissions to the cache" + } + } + } + } +} diff --git a/packages/@aws-cdk/aws-elasticache-alpha/lib/index.ts b/packages/@aws-cdk/aws-elasticache-alpha/lib/index.ts index 8a2038b8a56ad..df16354df052d 100644 --- a/packages/@aws-cdk/aws-elasticache-alpha/lib/index.ts +++ b/packages/@aws-cdk/aws-elasticache-alpha/lib/index.ts @@ -6,3 +6,4 @@ export * from './password-user'; export * from './no-password-user'; export * from './serverless-cache-base'; export * from './serverless-cache'; +export * from './elasticache-grants.generated'; diff --git a/packages/@aws-cdk/aws-elasticache-alpha/lib/serverless-cache-base.ts b/packages/@aws-cdk/aws-elasticache-alpha/lib/serverless-cache-base.ts index 01dd5b863a435..d9d7488e05448 100644 --- a/packages/@aws-cdk/aws-elasticache-alpha/lib/serverless-cache-base.ts +++ b/packages/@aws-cdk/aws-elasticache-alpha/lib/serverless-cache-base.ts @@ -4,6 +4,11 @@ import * as ec2 from 'aws-cdk-lib/aws-ec2'; import * as iam from 'aws-cdk-lib/aws-iam'; import * as kms from 'aws-cdk-lib/aws-kms'; import { IResource, Resource, Duration } from 'aws-cdk-lib/core'; +import { + IServerlessCacheRef, + ServerlessCacheReference, +} from 'aws-cdk-lib/interfaces/generated/aws-elasticache-interfaces.generated'; +import { ServerlessCacheGrants } from './elasticache-grants.generated'; /** * Supported cache engines together with available versions. @@ -44,7 +49,7 @@ export enum CacheEngine { /** * Represents a Serverless ElastiCache cache */ -export interface IServerlessCache extends IResource, ec2.IConnectable { +export interface IServerlessCache extends IResource, ec2.IConnectable, IServerlessCacheRef { /** * The cache engine used by this cache */ @@ -161,13 +166,24 @@ export abstract class ServerlessCacheBase extends Resource implements IServerles */ public abstract readonly connections: ec2.Connections; + /** + * Collection of grant methods for this cache + */ + public readonly grants = ServerlessCacheGrants.fromServerlessCache(this); + + public get serverlessCacheRef(): ServerlessCacheReference { + return { + serverlessCacheName: this.serverlessCacheName, + }; + } + /** * Grant connect permissions to the cache * * @param grantee The principal to grant permissions to */ public grantConnect(grantee: iam.IGrantable): iam.Grant { - return this.grant(grantee, 'elasticache:Connect', 'elasticache:DescribeServerlessCaches'); + return this.grants.connect(grantee); } /** * Grant the given identity custom permissions diff --git a/packages/@aws-cdk/aws-elasticache-alpha/test/serverless-cache-base.test.ts b/packages/@aws-cdk/aws-elasticache-alpha/test/serverless-cache-base.test.ts index edeec9a03afd1..58cdcd9be904f 100644 --- a/packages/@aws-cdk/aws-elasticache-alpha/test/serverless-cache-base.test.ts +++ b/packages/@aws-cdk/aws-elasticache-alpha/test/serverless-cache-base.test.ts @@ -143,7 +143,26 @@ describe('serverless cache base', () => { 'elasticache:Connect', 'elasticache:DescribeServerlessCaches', ], - Resource: { 'Fn::GetAtt': ['Cache18F6EE16', 'ARN'] }, + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':elasticache:', + { + Ref: 'AWS::Region', + }, + ':', + { + Ref: 'AWS::AccountId', + }, + ':serverlesscache:Cache', + ], + ], + }, }, ]), }, diff --git a/packages/aws-cdk-lib/scripts/gen.ts b/packages/aws-cdk-lib/scripts/gen.ts index bb9fefa12b370..bace7d80a7673 100644 --- a/packages/aws-cdk-lib/scripts/gen.ts +++ b/packages/aws-cdk-lib/scripts/gen.ts @@ -5,6 +5,7 @@ import generateServiceSubmoduleFiles from './submodules'; import writeCloudFormationIncludeMapping from './submodules/cloudformation-include'; const awsCdkLibDir = path.join(__dirname, '..'); +const atAwsCdkDir = path.join(__dirname, '../../@aws-cdk'); const pkgJsonPath = path.join(awsCdkLibDir, 'package.json'); const topLevelIndexFilePath = path.join(awsCdkLibDir, 'index.ts'); const scopeMapPath = path.join(__dirname, 'scope-map.json'); @@ -20,7 +21,7 @@ main().catch(e => { async function main() { // Generate all L1s based on config in scope-map.json - const generated = (await generateAll(awsCdkLibDir, { + const generated = (await generateAll(awsCdkLibDir, atAwsCdkDir, { skippedServices: [], scopeMapPath, })); diff --git a/tools/@aws-cdk/spec2cdk/lib/cdk/aws-cdk-lib.ts b/tools/@aws-cdk/spec2cdk/lib/cdk/aws-cdk-lib.ts index 01db553e365a5..05c3e8c18c600 100644 --- a/tools/@aws-cdk/spec2cdk/lib/cdk/aws-cdk-lib.ts +++ b/tools/@aws-cdk/spec2cdk/lib/cdk/aws-cdk-lib.ts @@ -48,6 +48,19 @@ class AwsCdkLibServiceSubmodule extends BaseServiceSubmodule { } } +export interface GrantsProps { + /** + * The JSON string to configure the grants for the service + */ + config: string; + + /** + * Whether the generated grants should be considered as stable or experimental. + * This has implications on where the generated file is placed. + */ + stable: boolean; +} + export interface AwsCdkLibFilePatterns { /** * The pattern used to name resource files. @@ -133,14 +146,14 @@ export class AwsCdkLibBuilder extends LibraryBuilder }); } - protected createServiceSubmodule(service: Service, submoduleName: string, grantsConfig?: string): AwsCdkLibServiceSubmodule { + protected createServiceSubmodule(service: Service, submoduleName: string, grantsProps?: GrantsProps): AwsCdkLibServiceSubmodule { const resourcesMod = this.rememberModule(this.createResourceModule(submoduleName, service)); const augmentations = this.rememberModule(this.createAugmentationsModule(submoduleName, service)); const cannedMetrics = this.rememberModule(this.createCannedMetricsModule(submoduleName, service)); const [interfaces, didCreateInterfaceModule] = this.obtainInterfaceModule(service); - const grants = grantsConfig != null - ? this.rememberModule(this.createGrantsModule(submoduleName, service, grantsConfig)) + const grants = grantsProps != null + ? this.rememberModule(this.createGrantsModule(submoduleName, service, grantsProps)) : undefined; const createdSubmod: AwsCdkLibServiceSubmodule = new AwsCdkLibServiceSubmodule({ @@ -157,10 +170,10 @@ export class AwsCdkLibBuilder extends LibraryBuilder return createdSubmod; } - private createGrantsModule(moduleName: string, service: Service, grantsConfig: string): LocatedModule { + private createGrantsModule(moduleName: string, service: Service, grantsProps: GrantsProps): LocatedModule { const filePath = this.pathsFor(moduleName, service).grants; return { - module: new GrantsModule(service, this.db, JSON.parse(grantsConfig)), + module: new GrantsModule(service, this.db, JSON.parse(grantsProps.config), grantsProps.stable), filePath, }; } diff --git a/tools/@aws-cdk/spec2cdk/lib/cdk/grants-module.ts b/tools/@aws-cdk/spec2cdk/lib/cdk/grants-module.ts index 38eea162df014..da0f83dcad70d 100644 --- a/tools/@aws-cdk/spec2cdk/lib/cdk/grants-module.ts +++ b/tools/@aws-cdk/spec2cdk/lib/cdk/grants-module.ts @@ -25,7 +25,11 @@ const $this = $E(expr.this_()); * repository. */ export class GrantsModule extends Module { - public constructor(private readonly service: Service, private readonly db: SpecDatabase, private readonly schema: GrantsFileSchema) { + public constructor( + private readonly service: Service, + private readonly db: SpecDatabase, + private readonly schema: GrantsFileSchema, + public readonly stable: boolean) { super(`${service.shortName}.grants`); } @@ -259,8 +263,12 @@ export class GrantsModule extends Module { } if (hasContent) { - new ExternalModule(`aws-cdk-lib/aws-${this.service.shortName}`) - .import(this, this.service.shortName, { fromLocation: `./${this.service.shortName}.generated` }); + if (this.stable) { + new ExternalModule(`aws-cdk-lib/aws-${this.service.shortName}`) + .import(this, this.service.shortName, { fromLocation: `./${this.service.shortName}.generated` }); + } else { + new ExternalModule(`aws-cdk-lib/aws-${this.service.shortName}`).import(this, this.service.shortName); + } new ExternalModule('aws-cdk-lib/aws-iam').import(this, 'iam'); } } diff --git a/tools/@aws-cdk/spec2cdk/lib/cdk/library-builder.ts b/tools/@aws-cdk/spec2cdk/lib/cdk/library-builder.ts index 64e563d12e0eb..c89891da251b5 100644 --- a/tools/@aws-cdk/spec2cdk/lib/cdk/library-builder.ts +++ b/tools/@aws-cdk/spec2cdk/lib/cdk/library-builder.ts @@ -1,8 +1,9 @@ /* eslint-disable @cdklabs/no-throw-default-error */ import * as path from 'path'; -import { SpecDatabase, Resource, Service } from '@aws-cdk/service-spec-types'; +import { Resource, Service, SpecDatabase } from '@aws-cdk/service-spec-types'; import { Module } from '@cdklabs/typewriter'; import { IWriter, substituteFilePattern } from '../util'; +import { GrantsProps } from './aws-cdk-lib'; import { BaseServiceSubmodule, LocatedModule, relativeImportPath } from './service-submodule'; export interface AddServiceProps { @@ -27,11 +28,9 @@ export interface AddServiceProps { readonly destinationSubmodule?: string; /** - * The JSON string to configure the grants for the service - * - * @default No grants module is generated + * Properties used to create the grants module for the service */ - readonly grantsConfig?: string; + readonly grantsProps?: GrantsProps; } export interface LibraryBuilderProps { @@ -70,7 +69,7 @@ export abstract class LibraryBuilder e.entity); - const submod = this.obtainServiceSubmodule(service, props?.destinationSubmodule, props?.grantsConfig); + const submod = this.obtainServiceSubmodule(service, props?.destinationSubmodule, props?.grantsProps); for (const resource of resources) { this.addResourceToSubmodule(submod, resource, props); @@ -119,7 +118,7 @@ export abstract class LibraryBuilder { const db = await loadAwsServiceSpec(); @@ -81,6 +83,7 @@ export async function generateAll( moduleGenerationRequests, { outputPath: outPath, + alphaOutputPath: alphaRootDir, clearOutput: false, builderProps: { inCdkLib: true, diff --git a/tools/@aws-cdk/spec2cdk/lib/generate.ts b/tools/@aws-cdk/spec2cdk/lib/generate.ts index 74a86b5b104ad..74d6cf7bf72d7 100644 --- a/tools/@aws-cdk/spec2cdk/lib/generate.ts +++ b/tools/@aws-cdk/spec2cdk/lib/generate.ts @@ -4,7 +4,8 @@ import { DatabaseBuilder } from '@aws-cdk/service-spec-importers'; import { Service, SpecDatabase } from '@aws-cdk/service-spec-types'; import { TypeScriptRenderer } from '@cdklabs/typewriter'; import * as fs from 'fs-extra'; -import { AwsCdkLibBuilder } from './cdk/aws-cdk-lib'; +import { AwsCdkLibBuilder, GrantsProps } from './cdk/aws-cdk-lib'; +import { GrantsModule } from './cdk/grants-module'; import { LibraryBuilder } from './cdk/library-builder'; import { queryDb, log, TsFileWriter } from './util'; @@ -56,6 +57,11 @@ export interface GenerateOptions{ */ readonly outputPath: string; + /** + * Base path for generated alpha files + */ + readonly alphaOutputPath?: string; + /** * Should the location be deleted before generating new files * @default false @@ -202,7 +208,7 @@ async function generator( destinationSubmodule: moduleName, nameSuffix: req.suffix, deprecated: req.deprecated, - grantsConfig: readGrantsConfig(moduleName, options.outputPath), + grantsProps: grantsPropsForModule(moduleName, outputPath, options.alphaOutputPath), }); servicesPerModule[moduleName] ??= []; @@ -218,8 +224,20 @@ async function generator( const moduleOutputFiles = ast.filesBySubmodule(); - const writer = new TsFileWriter(outputPath, renderer); - ast.writeAll(writer); + const writer = new TsFileWriter(renderer); + + // Write all modules into their respective files + for (const [fileName, module] of ast.modules.entries()) { + if (!module.isEmpty()) { + // Decide the full path based on whether this is a stable or alpha module + // Stable modules go into `outputPath`, alpha modules into `alphaOutputPath` (with the "-alpha" suffix) + // At the moment, only GrantsModules can be alpha + const isStable = module instanceof GrantsModule ? module.stable : true; + const rootDir = isStable ? outputPath : options.alphaOutputPath ?? outputPath; + const fullPath = path.join(rootDir, isStable ? fileName : toAlphaPackage(fileName)); + writer.write(module, fullPath); + } + } const allResources = Object.values(resourcesPerModule).flat().reduce(mergeObjects, {}); log.info('Summary:'); @@ -248,10 +266,24 @@ function mergeObjects(all: T, res: T) { }; } -function readGrantsConfig(moduleName: string, rootDir: string): string | undefined { - const filename = `${moduleName}/grants.json`; +function grantsPropsForModule(moduleName: string, stablePath: string, alphaPath?: string): GrantsProps | undefined { + let stable = true; + let grantsFileLocation = path.join(stablePath, moduleName); + if (alphaPath != null) { + const alpha = path.join(alphaPath, `${moduleName}-alpha`); + if (fs.existsSync(path.join(alpha, 'grants.json'))) { + grantsFileLocation = alpha; + stable = false; + } + } + + const config = readGrantsConfig(grantsFileLocation); + return config == null ? undefined : { stable, config }; +} + +function readGrantsConfig(dir: string): string | undefined { try { - return fs.readFileSync(path.join(rootDir, filename), 'utf-8'); + return fs.readFileSync(path.join(dir, 'grants.json'), 'utf-8'); } catch (e: any) { if (e.code === 'ENOENT') { return undefined; @@ -259,3 +291,7 @@ function readGrantsConfig(moduleName: string, rootDir: string): string | undefin throw e; } } + +function toAlphaPackage(s: string) { + return s.replace(/^(aws-[^/]+)/, '$1-alpha'); +} diff --git a/tools/@aws-cdk/spec2cdk/lib/util/ts-file-writer.ts b/tools/@aws-cdk/spec2cdk/lib/util/ts-file-writer.ts index 009b2e2fe5279..ba64f34478df6 100644 --- a/tools/@aws-cdk/spec2cdk/lib/util/ts-file-writer.ts +++ b/tools/@aws-cdk/spec2cdk/lib/util/ts-file-writer.ts @@ -1,4 +1,3 @@ -import * as path from 'node:path'; import { Module, TypeScriptRenderer } from '@cdklabs/typewriter'; import * as fs from 'fs-extra'; import * as log from './log'; @@ -10,14 +9,10 @@ export interface IWriter { export class TsFileWriter implements IWriter { public outputFiles = new Array(); - constructor( - private readonly rootDir: string, - private readonly renderer: TypeScriptRenderer, - ) {} + constructor(private readonly renderer: TypeScriptRenderer) {} - public write(module: Module, filePath: string): string { - log.debug(module.name, filePath, 'render'); - const fullPath = path.join(this.rootDir, filePath); + public write(module: Module, fullPath: string): string { + log.debug(module.name, fullPath, 'render'); fs.outputFileSync(fullPath, this.renderer.render(module)); this.outputFiles.push(fullPath); return fullPath; diff --git a/tools/@aws-cdk/spec2cdk/test/grants.test.ts b/tools/@aws-cdk/spec2cdk/test/grants.test.ts index 78b161de10308..f70562be0ec4c 100644 --- a/tools/@aws-cdk/spec2cdk/test/grants.test.ts +++ b/tools/@aws-cdk/spec2cdk/test/grants.test.ts @@ -29,7 +29,7 @@ test('generates grants for methods with and without key actions', async () => { }, }; const service = db.lookup('service', 'name', 'equals', 'aws-sns').only(); - const module = new GrantsModule(service, db, config); + const module = new GrantsModule(service, db, config, true); const scope = new Module('@aws-cdk/aws-sns'); const refInterface = new InterfaceType(scope, {