Skip to content

Commit

Permalink
feat: detect circular dependencies
Browse files Browse the repository at this point in the history
  • Loading branch information
ChloeMouret committed Apr 5, 2024
1 parent a5df518 commit 65c0d5f
Show file tree
Hide file tree
Showing 6 changed files with 155 additions and 11 deletions.
3 changes: 2 additions & 1 deletion packages/graph-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@
},
"dependencies": {
"@sls-mentor/arn": "workspace:^",
"@sls-mentor/aws-api": "workspace:^"
"@sls-mentor/aws-api": "workspace:^",
"graph-cycles": "^1.2.1"
},
"devDependencies": {
"@types/node": "^20.6.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { analyzeGraph } from 'graph-cycles';

import { CloudformationStackARN } from '@sls-mentor/arn';
import { findStacksToStacksImports } from '@sls-mentor/aws-api';

import { CircularDependenciesWarnings, CloudFormationWarnings } from 'types';

type LinkBetweenStacks = {
exportingStack?: CloudformationStackARN;
importingStack?: CloudformationStackARN;
}[];

type Graph = {
[key: string]: string[];
};

const detectCircularDependencies = (
stacksImports: LinkBetweenStacks,
): string[][] | null => {
const graph: Graph = stacksImports.reduce(
(acc: Graph, { exportingStack, importingStack }) => {
if (exportingStack !== undefined && importingStack !== undefined) {
const exportingStackName = exportingStack.toString();
const importingStackName = importingStack.toString();

if (acc[exportingStackName] === undefined) {
acc[exportingStackName] = [];
}

acc[exportingStackName]?.push(importingStackName);
}

return acc;
},
{},
);

const { cycles } = analyzeGraph(Object.entries(graph));

return cycles;
};

type StacksToStacksImportsWithWarnings = {
exportingStack?: CloudformationStackARN;
importingStack?: CloudformationStackARN;
warnings: CircularDependenciesWarnings[];
}[];

export const findStacksToStacksImportsWithCircularDependencies =
async (): Promise<StacksToStacksImportsWithWarnings> => {
const stacksToStacksImports = await findStacksToStacksImports();
const circularDependencies = detectCircularDependencies(
stacksToStacksImports,
);

const dictOfCircularDependencies: Record<string, boolean> = {};

if (circularDependencies !== null) {
circularDependencies.forEach(([from, to]) => {
if (to === undefined) {
dictOfCircularDependencies[`${from ?? ''}-${from ?? ''}`] = true;
} else if (from === undefined) {
throw new Error('This should not happen');
} else {
dictOfCircularDependencies[`${from}-${to}`] = true;
dictOfCircularDependencies[`${to}-${from}`] = true;
}
});
}

return stacksToStacksImports.map(({ exportingStack, importingStack }) => {
if (
dictOfCircularDependencies[
`${exportingStack?.toString() ?? ''}-${
importingStack?.toString() ?? ''
}`
] === true
) {
return {
exportingStack,
importingStack,
warnings: [CloudFormationWarnings.CircularDependencies],
};
}

return {
exportingStack,
importingStack,
warnings: [],
};
});
};
1 change: 1 addition & 0 deletions packages/graph-core/src/edges/cloudFormation/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './detectCircularDependencies';
20 changes: 11 additions & 9 deletions packages/graph-core/src/edges/getEdges.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import {
fetchAllLambdaConfigurations,
fetchAllQueuesAttributes,
fetchAllStepFunctionConfigurations,
findStacksToStacksImports,
getAllRulesOfEventBus,
getAllTargetsOfEventBridgeRule,
} from '@sls-mentor/aws-api';
Expand All @@ -20,6 +19,7 @@ import { Edge } from 'types';

import { getHttpApiEdges, getRestApiEdges } from './apiGateway';
import { findArnsFromDataSources } from './appSync';
import { findStacksToStacksImportsWithCircularDependencies } from './cloudFormation';
import { findLambdasInDefinition } from './stepFunction/findLambdasInDefinition';

export const getEdges = async (
Expand Down Expand Up @@ -67,7 +67,7 @@ export const getEdges = async (
fetchAllQueuesAttributes(arns),
fetchAllStepFunctionConfigurations(arns),
fetchAllGraphqlApiResources(arns),
findStacksToStacksImports(),
findStacksToStacksImportsWithCircularDependencies(),
]);

const lambdaFunctionsAndRoleArn = lambdaFunctions.map(l => ({
Expand All @@ -79,13 +79,15 @@ export const getEdges = async (
}));

const rawEdges: Edge[] = [
...stacksToStacksImports.map(({ exportingStack, importingStack }) => {
return {
from: exportingStack?.toString() ?? '*',
to: importingStack?.toString() ?? '*',
warnings: [],
};
}),
...stacksToStacksImports.map(
({ exportingStack, importingStack, warnings }) => {
return {
from: exportingStack?.toString() ?? '*',
to: importingStack?.toString() ?? '*',
warnings,
};
},
),
...graphqlApiResources
.map(({ arn, dataSources }) => {
const dataSourcesArns = findArnsFromDataSources(dataSources);
Expand Down
12 changes: 11 additions & 1 deletion packages/graph-core/src/types/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,17 @@ import { BaseEdge } from 'edges/types';
import { DynamoDBTableStats, LambdaFunctionStats } from '../nodes';
import { NodeBase, SerializedNodeBase } from './helpers';

type Warnings = RestApiWarnings | HttpApiWarnings;
export const CloudFormationWarnings = {
CircularDependencies: 'CircularDependencies',
} as const;

export type CircularDependenciesWarnings =
(typeof CloudFormationWarnings)[keyof typeof CloudFormationWarnings];

type Warnings =
| RestApiWarnings
| HttpApiWarnings
| CircularDependenciesWarnings;

export type Edge = BaseEdge & {
warnings: Warnings[];
Expand Down
38 changes: 38 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 65c0d5f

Please sign in to comment.