Skip to content

Commit

Permalink
Validate all CDK resources are mapped to Pulumi resources (#231)
Browse files Browse the repository at this point in the history
Previously we didn't ensure that all CDK resources were actually mapped
to Pulumi resources.
While working on adding support for Custom Resources we noticed that
some resources were not mapped (e.g. resources with ID of 'Default').

This change adds validation to the GraphBuilder in order to ensure that
all resources are mapped.
In order to prevent any unexpected behavior we're failing hard if we do
not detect that all resources are mapped.

Closes #186
  • Loading branch information
flostadler authored Nov 19, 2024
1 parent 2d709d6 commit 890dc36
Show file tree
Hide file tree
Showing 3 changed files with 41 additions and 5 deletions.
2 changes: 1 addition & 1 deletion src/assembly/stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ export class StackManifest {
/**
* Map of resource logicalId to CloudFormation template resource fragment
*/
private readonly resources: { [logicalId: string]: CloudFormationResource };
public readonly resources: { [logicalId: string]: CloudFormationResource };

/**
* The Mappings from the CFN Stack
Expand Down
19 changes: 16 additions & 3 deletions src/graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { debug } from '@pulumi/pulumi/log';
import { debug, warn } from '@pulumi/pulumi/log';
import { CloudFormationResource } from './cfn';
import { parseSub } from './sub';
import { ConstructTree, StackManifest } from './assembly';
Expand Down Expand Up @@ -241,14 +241,27 @@ export class GraphBuilder {
private _build(): Graph {
// passes
// 1. collect all constructs into a map from construct name to DAG node, converting CFN elements to fragments
// 2. hook up dependency edges
// 3. sort the dependency graph
// 2. validate that all CDK resources are mapped to Pulumi resources
// 3. hook up dependency edges
// 4. sort the dependency graph

// Create graph nodes and associate them with constructs and CFN logical IDs.
//
// NOTE: this doesn't handle cross-stack references. We'll likely need to do so, at least for nested stacks.
this.parseTree(this.stack.constructTree);

const unmappedResources: string[] = [];
Object.entries(this.stack.resources).forEach(([logicalId, resource]) => {
if (!this.cfnElementNodes.has(logicalId)) {
warn(`CDK resource ${logicalId} (${resource.Type}) was not mapped to a Pulumi resource.`);
unmappedResources.push(logicalId);
}
});
if (unmappedResources.length > 0) {
const total = Object.keys(this.stack.resources).length;
throw new Error(`${unmappedResources.length} out of ${total} CDK resources failed to map to Pulumi resources.`);
}

// parseTree does not guarantee that the VPC resource will be parsed before the VPCCidrBlock resource
// so we need to process this separately after
if (Object.keys(this._vpcCidrBlockNodes).length) {
Expand Down
25 changes: 24 additions & 1 deletion tests/graph.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
// limitations under the License.

import { GraphBuilder, GraphNode } from '../src/graph';
import { StackManifest, StackManifestProps } from '../src/assembly';
import { ConstructTree, StackManifest, StackManifestProps } from '../src/assembly';
import { createStackManifest } from './utils';
import * as fs from 'fs';
import * as path from 'path';
Expand Down Expand Up @@ -519,6 +519,29 @@ test('parses custom resources', () => {
});
});

test('validates that all resources are mapped', () => {
const stackManifestPath = path.join(__dirname, 'test-data/custom-resource-stack/stack-manifest.json');
const props: StackManifestProps = JSON.parse(fs.readFileSync(stackManifestPath, 'utf-8'));
const metadata = props.metadata;
const resourceToDelete = 's3deployment/WebsiteBucket/Resource';
delete metadata[resourceToDelete];

const deleteResourceFromTree = (tree: ConstructTree, path: string): ConstructTree => {
if (tree.children) {
tree.children = Object.fromEntries(
Object.entries(tree.children)
.filter(([_, value]) => value.path !== path)
.map(([key, value]) => [key, deleteResourceFromTree(value, path)])
);
}
return tree;
}
const constructTree = deleteResourceFromTree(props.tree, resourceToDelete);
const stackManifest = new StackManifest({ ...props, tree: constructTree, metadata });

expect(() => GraphBuilder.build(stackManifest)).toThrow('1 out of 11 CDK resources failed to map to Pulumi resources.');
});

function edgesToArray(edges: Set<GraphNode>): string[] {
return Array.from(edges).flatMap((value) => value.construct.path);
}

0 comments on commit 890dc36

Please sign in to comment.