Skip to content
Open
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
14 changes: 14 additions & 0 deletions packages/@aws-cdk/aws-elasticache-alpha/grants.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"resources": {
"ServerlessCache": {
"grants": {
"connect": {
"actions": [
"elasticache:Connect", "elasticache:DescribeServerlessCaches"
],
"docSummary": "Grant connect permissions to the cache"
}
}
}
}
}
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-elasticache-alpha/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
*/
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,26 @@ describe('serverless cache base', () => {
'elasticache:Connect',
'elasticache:DescribeServerlessCaches',
],
Resource: { 'Fn::GetAtt': ['Cache18F6EE16', 'ARN'] },
Resource: {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've confirmed that this is the correct ARN format for a serverless cache.

'Fn::Join': [
'',
[
'arn:',
{
Ref: 'AWS::Partition',
},
':elasticache:',
{
Ref: 'AWS::Region',
},
':',
{
Ref: 'AWS::AccountId',
},
':serverlesscache:Cache',
],
],
},
},
]),
},
Expand Down
3 changes: 2 additions & 1 deletion packages/aws-cdk-lib/scripts/gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this tool, which lives in aws-cdk-lib, going to search outside its own directory for things to generate?

I don't like that very much.

I think we can't get around making the generator a standalone tool, perhaps move all this code into tools/@aws-cdk/spec2cdk, or perhaps one next to it... and then it gets invoked as part of yarn gen in each package separately and confines its search and generation to that package.

const pkgJsonPath = path.join(awsCdkLibDir, 'package.json');
const topLevelIndexFilePath = path.join(awsCdkLibDir, 'index.ts');
const scopeMapPath = path.join(__dirname, 'scope-map.json');
Expand All @@ -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,
}));
Expand Down
23 changes: 18 additions & 5 deletions tools/@aws-cdk/spec2cdk/lib/cdk/aws-cdk-lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -133,14 +146,14 @@ export class AwsCdkLibBuilder extends LibraryBuilder<AwsCdkLibServiceSubmodule>
});
}

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({
Expand All @@ -157,10 +170,10 @@ export class AwsCdkLibBuilder extends LibraryBuilder<AwsCdkLibServiceSubmodule>
return createdSubmod;
}

private createGrantsModule(moduleName: string, service: Service, grantsConfig: string): LocatedModule<GrantsModule> {
private createGrantsModule(moduleName: string, service: Service, grantsProps: GrantsProps): LocatedModule<GrantsModule> {
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,
};
}
Expand Down
14 changes: 11 additions & 3 deletions tools/@aws-cdk/spec2cdk/lib/cdk/grants-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`);
}

Expand Down Expand Up @@ -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');
}
}
Expand Down
19 changes: 9 additions & 10 deletions tools/@aws-cdk/spec2cdk/lib/cdk/library-builder.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -70,7 +69,7 @@ export abstract class LibraryBuilder<ServiceSubmodule extends BaseServiceSubmodu
*/
public addService(service: Service, props?: AddServiceProps) {
const resources = this.db.follow('hasResource', service).map(e => 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);
Expand Down Expand Up @@ -119,7 +118,7 @@ export abstract class LibraryBuilder<ServiceSubmodule extends BaseServiceSubmodu
continue;
}

// Group by the first path component component
// Group by the first path component
const parts = fileName.split(path.posix.sep);
if (parts.length === 1) {
continue;
Expand Down Expand Up @@ -154,7 +153,7 @@ export abstract class LibraryBuilder<ServiceSubmodule extends BaseServiceSubmodu
}
}

private obtainServiceSubmodule(service: Service, targetSubmodule?: string, grantsConfig?: string): ServiceSubmodule {
private obtainServiceSubmodule(service: Service, targetSubmodule?: string, grantsProps?: GrantsProps): ServiceSubmodule {
const submoduleName = targetSubmodule ?? service.name;
const key = `${submoduleName}/${service.name}`;

Expand All @@ -163,15 +162,15 @@ export abstract class LibraryBuilder<ServiceSubmodule extends BaseServiceSubmodu
return existingSubmod;
}

const createdSubmod = this.createServiceSubmodule(service, submoduleName, grantsConfig);
const createdSubmod = this.createServiceSubmodule(service, submoduleName, grantsProps);
this.serviceSubmodules.set(key, createdSubmod);
return createdSubmod;
}

/**
* Implement this to create an instance of a service module.
*/
protected abstract createServiceSubmodule(service: Service, submoduleName: string, grantsConfig?: string): ServiceSubmodule;
protected abstract createServiceSubmodule(service: Service, submoduleName: string, grantsProps?: GrantsProps): ServiceSubmodule;

public module(key: string) {
const ret = this.modules.get(key);
Expand Down
3 changes: 3 additions & 0 deletions tools/@aws-cdk/spec2cdk/lib/cfn2ts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,13 @@ export interface GenerateAllOptions {
* Code-generates all L1s, and writes the necessary index files.
*
* @param outPath The root directory to generate L1s in
* @param alphaRootDir The root directory to generate grant modules for grants configured in alpha packages
* @param param1 Options
* @returns A ModuleMap containing the ModuleDefinition and CFN scopes for each generated module.
*/
export async function generateAll(
outPath: string,
alphaRootDir: string,
{ scopeMapPath, skippedServices }: GenerateAllOptions,
): Promise<ModuleMap> {
const db = await loadAwsServiceSpec();
Expand Down Expand Up @@ -81,6 +83,7 @@ export async function generateAll(
moduleGenerationRequests,
{
outputPath: outPath,
alphaOutputPath: alphaRootDir,
clearOutput: false,
builderProps: {
inCdkLib: true,
Expand Down
50 changes: 43 additions & 7 deletions tools/@aws-cdk/spec2cdk/lib/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -56,6 +57,11 @@ export interface GenerateOptions<Builder extends LBC>{
*/
readonly outputPath: string;

/**
* Base path for generated alpha files
*/
readonly alphaOutputPath?: string;

/**
* Should the location be deleted before generating new files
* @default false
Expand Down Expand Up @@ -202,7 +208,7 @@ async function generator<Builder extends LBC = typeof AwsCdkLibBuilder>(
destinationSubmodule: moduleName,
nameSuffix: req.suffix,
deprecated: req.deprecated,
grantsConfig: readGrantsConfig(moduleName, options.outputPath),
grantsProps: grantsPropsForModule(moduleName, outputPath, options.alphaOutputPath),
});

servicesPerModule[moduleName] ??= [];
Expand All @@ -218,8 +224,20 @@ async function generator<Builder extends LBC = typeof AwsCdkLibBuilder>(

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:');
Expand Down Expand Up @@ -248,14 +266,32 @@ function mergeObjects<T>(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;
}
throw e;
}
}

function toAlphaPackage(s: string) {
return s.replace(/^(aws-[^/]+)/, '$1-alpha');
}
11 changes: 3 additions & 8 deletions tools/@aws-cdk/spec2cdk/lib/util/ts-file-writer.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -10,14 +9,10 @@ export interface IWriter {
export class TsFileWriter implements IWriter {
public outputFiles = new Array<string>();

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 {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The behavior here has changed: the second parameter is an absolute path, instead of relative to some root directory passed to the constructor.

log.debug(module.name, fullPath, 'render');
fs.outputFileSync(fullPath, this.renderer.render(module));
this.outputFiles.push(fullPath);
return fullPath;
Expand Down
Loading
Loading