Skip to content

Commit

Permalink
Add ability to specify resource options at the stack level
Browse files Browse the repository at this point in the history
It has been possible to specify Pulumi resource options at the stack
level, but it did not flow through to the actual resources. This PR
makes sure that the inheritance works correctly.

This PR also adds functionality to automatically set the Stack
environment based on the App provider. Because the App creates the
stacks in an async context, we can use provider functions to lookup the
environment and then pass the resolved environment to the stack. This
means that all Stacks have their environment provided by default. This
will cut down on the number of Intrinsics used in the generated
template.

If the user provides a provider to the Stack we are no longer in an
async context which means we can't determine the environment from the
provider and fall back to an environment agnostic stack.

re #61, re #219
  • Loading branch information
corymhall committed Nov 22, 2024
1 parent 399adae commit 08be058
Show file tree
Hide file tree
Showing 15 changed files with 607 additions and 47 deletions.
79 changes: 79 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,85 @@ const app = new pulumicdk.App('app', (scope: pulumicdk.App) => {
});
```

### Stack Level Providers

It is also possible to customize the Providers at the Stack level. This can be
useful in cases where you need to deploy resources to different AWS regions.

```ts
const awsProvider = new aws.Provider('aws-provider');
const awsCCAPIProvider = new ccapi.Provider('ccapi-provider', {
// enable autoNaming
autoNaming: {
autoTrim: true,
randomSuffixMinLength: 7,
}
});

const app = new pulumicdk.App('app', (scope: pulumicdk.App) => {
// inherits the provider from the app
const defaultProviderStack = new pulumicdk.Stack('default-provider-stack');
const bucket = new s3.Bucket(defaultProviderStack, 'Bucket');

// use a different provider for this stack
const east2Stack = new pulumicdk.Stack('east2-stack', {
providers: [
new aws.Provider('east2-provider', { region: 'us-east-2' }),
new ccapi.Provider('east2-ccapi-provider', {
region: 'us-east-2',
autoNaming: {
autoTrim: true,
randomSuffixMinLength: 7,
},
}),
],
});
const bucket2 = new s3.Bucket(east2Stack, 'Bucket');
}, {
providers: [
dockerBuildProvider,
awsProvider,
awsCCAPIProvider,
]
});
```

One thing to note is that when you pass different custom providers to a Stack,
by default the Stack becomes an [environment agnostic stack](https://docs.aws.amazon.com/cdk/v2/guide/configure-env.html#configure-env-examples).
If you want to have the environment specified at the CDK Stack level, then you
also need to provide the environment to the Stack Props.

```ts
const app = new pulumicdk.App('app', (scope: pulumicdk.App) => {
// inherits the provider from the app and has the CDK env auto populated
// based on the default provider
const defaultProviderStack = new pulumicdk.Stack('default-provider-stack');
const bucket = new s3.Bucket(defaultProviderStack, 'Bucket');

// use a different provider for this stack
const east2Stack = new pulumicdk.Stack('east2-stack', {
props: {
env: {
region: 'us-east-2',
account: '12345678912',
},
},
providers: [
new aws.Provider('east2-provider', { region: 'us-east-2' }),
new ccapi.Provider('east2-ccapi-provider', {
region: 'us-east-2',
autoNaming: {
autoTrim: true,
randomSuffixMinLength: 7,
},
}),
],
});
const bucket2 = new s3.Bucket(east2Stack, 'Bucket');
});

```

## CDK Lookups

CDK [lookups](https://docs.aws.amazon.com/cdk/v2/guide/context.html#context_methods) are currently disabled by default.
Expand Down
39 changes: 32 additions & 7 deletions api-docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,14 +74,14 @@ export const bucket = app.outputs['bucket'];

###### Defined in

[stack.ts:96](https://github.com/pulumi/pulumi-cdk/blob/main/src/stack.ts#L96)
[stack.ts:105](https://github.com/pulumi/pulumi-cdk/blob/main/src/stack.ts#L105)

#### Properties

| Property | Modifier | Type | Default value | Description | Defined in |
| ------ | ------ | ------ | ------ | ------ | ------ |
| `name` | `readonly` | `string` | `undefined` | The name of the component | [stack.ts:55](https://github.com/pulumi/pulumi-cdk/blob/main/src/stack.ts#L55) |
| `outputs` | `public` | `object` | `{}` | The collection of outputs from the AWS CDK Stack represented as Pulumi Outputs. Each CfnOutput defined in the AWS CDK Stack will populate a value in the outputs. | [stack.ts:61](https://github.com/pulumi/pulumi-cdk/blob/main/src/stack.ts#L61) |
| `name` | `readonly` | `string` | `undefined` | The name of the component | [stack.ts:57](https://github.com/pulumi/pulumi-cdk/blob/main/src/stack.ts#L57) |
| `outputs` | `public` | `object` | `{}` | The collection of outputs from the AWS CDK Stack represented as Pulumi Outputs. Each CfnOutput defined in the AWS CDK Stack will populate a value in the outputs. | [stack.ts:63](https://github.com/pulumi/pulumi-cdk/blob/main/src/stack.ts#L63) |

***

Expand Down Expand Up @@ -121,7 +121,7 @@ Create and register an AWS CDK stack deployed with Pulumi.

###### Defined in

[stack.ts:264](https://github.com/pulumi/pulumi-cdk/blob/main/src/stack.ts#L264)
[stack.ts:330](https://github.com/pulumi/pulumi-cdk/blob/main/src/stack.ts#L330)

#### Methods

Expand Down Expand Up @@ -151,7 +151,7 @@ A Pulumi Output value.

###### Defined in

[stack.ts:277](https://github.com/pulumi/pulumi-cdk/blob/main/src/stack.ts#L277)
[stack.ts:432](https://github.com/pulumi/pulumi-cdk/blob/main/src/stack.ts#L432)

## Interfaces

Expand Down Expand Up @@ -233,6 +233,31 @@ Options specific to the Pulumi CDK App component.

Options for creating a Pulumi CDK Stack

Any Pulumi resource options provided at the Stack level will override those configured
at the App level

#### Example

```ts
new App('testapp', (scope: App) => {
// This stack will inherit the options from the App
new Stack(scope, 'teststack1');

// Override the options for this stack
new Stack(scope, 'teststack', {
providers: [
new native.Provider('custom-provider', { region: 'us-east-1' }),
],
props: { env: { region: 'us-east-1' } },
})
}, {
providers: [
new native.Provider('app-provider', { region: 'us-west-2' }),
]

})
```

#### Extends

- `ComponentResourceOptions`
Expand All @@ -241,7 +266,7 @@ Options for creating a Pulumi CDK Stack

| Property | Type | Description | Defined in |
| ------ | ------ | ------ | ------ |
| `props?` | `StackProps` | The CDK Stack props | [stack.ts:230](https://github.com/pulumi/pulumi-cdk/blob/main/src/stack.ts#L230) |
| `props?` | `StackProps` | The CDK Stack props | [stack.ts:289](https://github.com/pulumi/pulumi-cdk/blob/main/src/stack.ts#L289) |

## Type Aliases

Expand All @@ -255,7 +280,7 @@ Options for creating a Pulumi CDK Stack

#### Defined in

[stack.ts:24](https://github.com/pulumi/pulumi-cdk/blob/main/src/stack.ts#L24)
[stack.ts:26](https://github.com/pulumi/pulumi-cdk/blob/main/src/stack.ts#L26)

## Functions

Expand Down
65 changes: 65 additions & 0 deletions examples/examples_nodejs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,71 @@ func TestScalableWebhook(t *testing.T) {
integration.ProgramTest(t, &test)
}

func TestStackProvider(t *testing.T) {
// App will use default provider and one stack will use explicit provider
// with region=us-east-1
t.Run("With default env", func(t *testing.T) {
test := getJSBaseOptions(t).
With(integration.ProgramTestOptions{
Dir: filepath.Join(getCwd(t), "stack-provider"),
ExtraRuntimeValidation: func(t *testing.T, stack integration.RuntimeValidationStackInfo) {
east1LogsRegion := stack.Outputs["east1LogsRegion"].(string)
defaultLogsRegion := stack.Outputs["defaultLogsRegion"].(string)
east1StackRegion := stack.Outputs["east1StackRegion"].(string)
defaultStackRegion := stack.Outputs["defaultStackRegion"].(string)
assert.Equalf(t, "us-east-1", east1LogsRegion, "Expected east1LogsRegion to be us-east-1, got %s", east1LogsRegion)
assert.Equalf(t, "us-east-2", defaultLogsRegion, "Expected defaultLogsRegion to be us-east-2, got %s", defaultLogsRegion)
assert.Equalf(t, "us-east-1", east1StackRegion, "Expected east1StackRegion to be us-east-1, got %s", east1StackRegion)
assert.Equalf(t, "us-east-2", defaultStackRegion, "Expected defaultStackRegion to be us-east-2, got %s", defaultStackRegion)
},
})

integration.ProgramTest(t, &test)
})

// App will use a custom explicit provider and one stack will use explicit provider
// with region=us-east-1
t.Run("With different env", func(t *testing.T) {
test := getJSBaseOptions(t).
With(integration.ProgramTestOptions{
Dir: filepath.Join(getCwd(t), "stack-provider"),
Config: map[string]string{
"default-region": "us-west-2",
},
ExtraRuntimeValidation: func(t *testing.T, stack integration.RuntimeValidationStackInfo) {
east1LogsRegion := stack.Outputs["east1LogsRegion"].(string)
defaultLogsRegion := stack.Outputs["defaultLogsRegion"].(string)
east1StackRegion := stack.Outputs["east1StackRegion"].(string)
defaultStackRegion := stack.Outputs["defaultStackRegion"].(string)
assert.Equalf(t, "us-east-1", east1LogsRegion, "Expected east1LogsRegion to be us-east-1, got %s", east1LogsRegion)
assert.Equalf(t, "us-west-2", defaultLogsRegion, "Expected defaultLogsRegion to be us-west-2, got %s", defaultLogsRegion)
assert.Equalf(t, "us-east-1", east1StackRegion, "Expected east1StackRegion to be us-east-1, got %s", east1StackRegion)
assert.Equalf(t, "us-west-2", defaultStackRegion, "Expected defaultStackRegion to be us-west-2, got %s", defaultStackRegion)
},
})

integration.ProgramTest(t, &test)
})

t.Run("Fails with different cdk env", func(t *testing.T) {
var output bytes.Buffer
test := getJSBaseOptions(t).
With(integration.ProgramTestOptions{
Dir: filepath.Join(getCwd(t), "stack-provider"),
Stderr: &output,
ExpectFailure: true,
Config: map[string]string{
"cdk-region": "us-east-2",
},
ExtraRuntimeValidation: func(t *testing.T, stack integration.RuntimeValidationStackInfo) {
assert.Contains(t, output.String(), "The stack 'teststack' has conflicting regions between the native provider (us-east-1) and the stack environment (us-east-2)")
},
})

integration.ProgramTest(t, &test)
})
}

func TestTheBigFan(t *testing.T) {
test := getJSBaseOptions(t).
With(integration.ProgramTestOptions{
Expand Down
3 changes: 3 additions & 0 deletions examples/stack-provider/Pulumi.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
name: pulumi-stack-provider
runtime: nodejs
description: A minimal TypeScript Pulumi program
70 changes: 70 additions & 0 deletions examples/stack-provider/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import * as logs from 'aws-cdk-lib/aws-logs';
import * as pulumi from '@pulumi/pulumi';
import * as pulumicdk from '@pulumi/cdk';
import * as native from '@pulumi/aws-native';
import { RemovalPolicy } from 'aws-cdk-lib';

const config = new pulumi.Config();
const cdkRegion = config.get('cdk-region');
const cdkAccount = config.get('cdk-account');
const defaultRegion = config.get('default-region');

export class StackProviderStack extends pulumicdk.Stack {
public readonly logsRegion: pulumi.Output<string>;
constructor(app: pulumicdk.App, id: string, providers?: pulumi.ProviderResource[]) {
super(app, id, {
providers,
props:
cdkRegion || cdkAccount
? {
env: {
region: cdkRegion,
account: cdkAccount,
},
}
: undefined,
});

const group = new logs.LogGroup(this, 'group', {
retention: logs.RetentionDays.ONE_DAY,
removalPolicy: RemovalPolicy.DESTROY,
});

this.logsRegion = this.asOutput(group.logGroupArn).apply((arn) => arn.split(':')[3]);
}
}

const app = new pulumicdk.App(
'app',
(scope: pulumicdk.App) => {
const stack = new StackProviderStack(scope, 'teststack', [
new native.Provider('ccapi-provider', {
region: 'us-east-1', // a different region from the app provider
}),
]);
const defaultStack = new StackProviderStack(scope, 'default-stack');
return {
east1LogsRegion: stack.logsRegion,
east1StackRegion: stack.asOutput(stack.region),
defaultLogsRegion: defaultStack.logsRegion,
defaultStackRegion: defaultStack.asOutput(defaultStack.region),
};
},
{
providers: defaultRegion
? [
new native.Provider('app-provider', {
region: defaultRegion as native.Region, // a different region from the default env
}),
]
: undefined,
},
);

// You can (we check for this though) configure a different region on the provider
// that the stack uses vs the region in the CDK StackProps. This tests checks that both the
// stack region and the region the resources are deployed to are the same.
export const east1LogsRegion = app.outputs['east1LogsRegion'];
export const defaultLogsRegion = app.outputs['defaultLogsRegion'];
export const east1StackRegion = app.outputs['east1StackRegion'];
export const defaultStackRegion = app.outputs['defaultStackRegion'];
13 changes: 13 additions & 0 deletions examples/stack-provider/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"name": "pulumi-aws-cdk",
"devDependencies": {
"@types/node": "^10.0.0"
},
"dependencies": {
"@pulumi/aws": "^6.0.0",
"@pulumi/aws-native": "^1.9.0",
"@pulumi/cdk": "^0.5.0",
"aws-cdk-lib": "2.156.0",
"constructs": "10.3.0"
}
}
18 changes: 18 additions & 0 deletions examples/stack-provider/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"compilerOptions": {
"strict": true,
"outDir": "bin",
"target": "es2016",
"module": "commonjs",
"moduleResolution": "node",
"sourceMap": true,
"experimentalDecorators": true,
"pretty": true,
"noFallthroughCasesInSwitch": true,
"noImplicitReturns": true,
"forceConsistentCasingInFileNames": true
},
"files": [
"index.ts"
]
}
11 changes: 7 additions & 4 deletions src/converters/app-converter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ export class StackConverter extends ArtifactConverter implements intrinsics.Intr
readonly resources = new Map<string, Mapping<pulumi.Resource>>();
readonly constructs = new Map<ConstructInfo, pulumi.Resource>();
private readonly cdkStack: cdk.Stack;
private readonly stackOptions?: pulumi.ComponentResourceOptions;

private _stackResource?: CdkConstruct;
private readonly graph: Graph;
Expand All @@ -110,6 +111,7 @@ export class StackConverter extends ArtifactConverter implements intrinsics.Intr
constructor(host: AppComponent, readonly stack: StackManifest) {
super(host);
this.cdkStack = host.stacks[stack.id];
this.stackOptions = host.stackOptions[stack.id];
this.graph = GraphBuilder.build(this.stack);
}

Expand All @@ -122,6 +124,7 @@ export class StackConverter extends ArtifactConverter implements intrinsics.Intr
for (const n of this.graph.nodes) {
if (n.construct.id === this.stack.id) {
this._stackResource = new CdkConstruct(`${this.app.name}/${n.construct.path}`, n.construct.id, {
...this.stackOptions,
parent: this.app.component,
// NOTE: Currently we make the stack depend on all the assets and then all resources
// have the parent as the stack. This means we deploy all assets before we deploy any resources
Expand Down Expand Up @@ -564,15 +567,15 @@ export class StackConverter extends ArtifactConverter implements intrinsics.Intr

switch (target) {
case 'AWS::AccountId':
return getAccountId({ parent: this.app.component }).then((r) => r.accountId);
return getAccountId({ parent: this.stackResource }).then((r) => r.accountId);
case 'AWS::NoValue':
return undefined;
case 'AWS::Partition':
return getPartition({ parent: this.app.component }).then((p) => p.partition);
return getPartition({ parent: this.stackResource }).then((p) => p.partition);
case 'AWS::Region':
return getRegion({ parent: this.app.component }).then((r) => r.region);
return getRegion({ parent: this.stackResource }).then((r) => r.region);
case 'AWS::URLSuffix':
return getUrlSuffix({ parent: this.app.component }).then((r) => r.urlSuffix);
return getUrlSuffix({ parent: this.stackResource }).then((r) => r.urlSuffix);
case 'AWS::NotificationARNs':
case 'AWS::StackId':
case 'AWS::StackName':
Expand Down
Loading

0 comments on commit 08be058

Please sign in to comment.