Skip to content

Commit

Permalink
AB#98 refactor: extract different modes from ApprovalService
Browse files Browse the repository at this point in the history
  • Loading branch information
giovannibaratta committed Dec 7, 2023
1 parent 4dec128 commit 9ce27a8
Show file tree
Hide file tree
Showing 5 changed files with 143 additions and 100 deletions.
118 changes: 20 additions & 98 deletions core/libs/service/src/approval/approval.service.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,19 @@
import {Configuration} from "@libs/domain/configuration/configuration"
import {
Action,
DiffType,
TerraformDiff,
TerraformDiffMap,
mapDiffTypeToActions
} from "@libs/domain/terraform/diffs"
import {
TerraformEntity,
isDiffActionIncludedInEntityDecorator,
printShortTerraformEntity
} from "@libs/domain/terraform/resource"
import {TerraformDiff, TerraformDiffMap} from "@libs/domain/terraform/diffs"
import {TerraformEntity} from "@libs/domain/terraform/resource"
import {Injectable, Logger} from "@nestjs/common"
import {BootstrappingService} from "../bootstrapping/bootstrapping.service"
import {getSafeToApplyActionsFromDecorator} from "@libs/domain/terraform/approval"
import {RequireApprovalModeUseCase} from "./require-approval-mode.use-case"
import {SafeToApplyModeUseCase} from "./safe-to-apply-mode.use-case"

@Injectable()
export class ApprovalService {
constructor(private readonly bootstrappingService: BootstrappingService) {}
constructor(
private readonly bootstrappingService: BootstrappingService,
private readonly requireApprovalModeUseCase: RequireApprovalModeUseCase,
private readonly safeToApplyModeUseCase: SafeToApplyModeUseCase
) {}

async isApprovalRequired(params: IsApprovalRequiredParams): Promise<boolean> {
if (params.mode === "require_approval") return this.requireApprovalMode()
if (params.mode === "safe_to_apply") return this.safeToApplyMode()

throw new Error("Mode not supported")
}

private async requireApprovalMode(): Promise<boolean> {
const {terraformEntities, terraformDiffMap, configuration} =
await this.bootstrappingService.bootstrap()

Expand All @@ -35,42 +22,19 @@ export class ApprovalService {
terraformEntities
)

// From all the diffs, keep only the ones that requires approval
const resourcesThatRequiredApproval = diffsEntityPairs.filter(
pair =>
// Verify first if one of the action to achieve the diffType is in the list of actions that
// always require approval. If this is the case the resource requires approval.
this.doesContainActionThatAlwaysRequireApproval(
configuration,
pair[0].diffType
) ||
// If no match is found check is there is a specific decorator associated to the resource.
(pair[1].decorator.type === "manual_approval" &&
isDiffActionIncludedInEntityDecorator(pair[1].decorator, pair[0]))
)
if (params.mode === "require_approval")
return this.requireApprovalModeUseCase.isApprovalRequired({
configuration,
diffsEntityPairs
})

Logger.log(
`Found ${resourcesThatRequiredApproval.length} resource(s) that require approval:`
)
resourcesThatRequiredApproval.forEach(it =>
Logger.log(`- ${printShortTerraformEntity(it[1])}`)
)
if (params.mode === "safe_to_apply")
return this.safeToApplyModeUseCase.isApprovalRequired({
configuration,
diffsEntityPairs
})

return resourcesThatRequiredApproval.length > 0
}

private doesContainActionThatAlwaysRequireApproval(
configuration: Configuration,
diffType: DiffType
): boolean {
const actionaThatAlwaysRequireApproval =
configuration.global.requireApprovalActions
const actions = mapDiffTypeToActions(diffType)

return (
actionaThatAlwaysRequireApproval !== undefined &&
actions.some(it => actionaThatAlwaysRequireApproval.includes(it))
)
throw new Error("Mode not supported")
}

private generateDiffEntityPairs(
Expand Down Expand Up @@ -103,41 +67,6 @@ export class ApprovalService {
}, [])
}

private async safeToApplyMode(): Promise<boolean> {
const {terraformEntities, terraformDiffMap, configuration} =
await this.bootstrappingService.bootstrap()

const diffsEntityPairs = this.generateDiffEntityPairs(
terraformDiffMap,
terraformEntities
)

// From all the diffs, remove all the ones that are safe to apply
const resourcesThatAreNotSafeToApply = diffsEntityPairs.filter(pair => {
// Merge the safe to apply actions defined at the global level and the ones defined at the resource level
const safeActionsForResource: Action[] = [
...(configuration.global.safeToApplyActions ?? []),
...getSafeToApplyActionsFromDecorator(pair[1].decorator)
]

// It all the actions that will be perfomed to apply the plan are not included in the safe-list,
// it means that there is a potential unsafe action for the resource and we need to ask for approval.
return !areAllItemsIncluded(
safeActionsForResource,
mapDiffTypeToActions(pair[0].diffType)
)
})

Logger.log(
`Found ${resourcesThatAreNotSafeToApply.length} resource(s) that are not safe to apply:`
)
resourcesThatAreNotSafeToApply.forEach(it =>
Logger.log(`- ${printShortTerraformEntity(it[1])}`)
)

return resourcesThatAreNotSafeToApply.length > 0
}

private findDiffCounterpartInEntities(
diff: TerraformDiff,
entities: TerraformEntity[]
Expand All @@ -162,13 +91,6 @@ export class ApprovalService {
}
}

function areAllItemsIncluded<T>(
items: ReadonlyArray<T>,
itemsToCheck: ReadonlyArray<T>
): boolean {
return itemsToCheck.every(it => items.includes(it))
}

export interface IsApprovalRequiredParams {
mode: Mode
}
Expand Down
59 changes: 59 additions & 0 deletions core/libs/service/src/approval/require-approval-mode.use-case.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import {Configuration} from "@libs/domain/configuration/configuration"
import {
DiffType,
TerraformDiff,
mapDiffTypeToActions
} from "@libs/domain/terraform/diffs"
import {
TerraformEntity,
isDiffActionIncludedInEntityDecorator,
printShortTerraformEntity
} from "@libs/domain/terraform/resource"
import {Injectable, Logger} from "@nestjs/common"

@Injectable()
export class RequireApprovalModeUseCase {
isApprovalRequired(data: {
configuration: Configuration
diffsEntityPairs: [TerraformDiff, TerraformEntity][]
}): boolean {
const {diffsEntityPairs, configuration} = data

// From all the diffs, keep only the ones that requires approval
const resourcesThatRequiredApproval = diffsEntityPairs.filter(
pair =>
// Verify first if one of the action to achieve the diffType is in the list of actions that
// always require approval. If this is the case the resource requires approval.
this.doesContainActionThatAlwaysRequireApproval(
configuration,
pair[0].diffType
) ||
// If no match is found check is there is a specific decorator associated to the resource.
(pair[1].decorator.type === "manual_approval" &&
isDiffActionIncludedInEntityDecorator(pair[1].decorator, pair[0]))
)

Logger.log(
`Found ${resourcesThatRequiredApproval.length} resource(s) that require approval:`
)
resourcesThatRequiredApproval.forEach(it =>
Logger.log(`- ${printShortTerraformEntity(it[1])}`)
)

return resourcesThatRequiredApproval.length > 0
}

private doesContainActionThatAlwaysRequireApproval(
configuration: Configuration,
diffType: DiffType
): boolean {
const actionaThatAlwaysRequireApproval =
configuration.global.requireApprovalActions
const actions = mapDiffTypeToActions(diffType)

return (
actionaThatAlwaysRequireApproval !== undefined &&
actions.some(it => actionaThatAlwaysRequireApproval.includes(it))
)
}
}
54 changes: 54 additions & 0 deletions core/libs/service/src/approval/safe-to-apply-mode.use-case.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import {Configuration} from "@libs/domain/configuration/configuration"
import {getSafeToApplyActionsFromDecorator} from "@libs/domain/terraform/approval"
import {
Action,
TerraformDiff,
mapDiffTypeToActions
} from "@libs/domain/terraform/diffs"
import {
TerraformEntity,
printShortTerraformEntity
} from "@libs/domain/terraform/resource"
import {Injectable, Logger} from "@nestjs/common"

@Injectable()
export class SafeToApplyModeUseCase {
isApprovalRequired(data: {
configuration: Configuration
diffsEntityPairs: [TerraformDiff, TerraformEntity][]
}): boolean {
const {diffsEntityPairs, configuration} = data

// From all the diffs, remove all the ones that are safe to apply
const resourcesThatAreNotSafeToApply = diffsEntityPairs.filter(pair => {
// Merge the safe to apply actions defined at the global level and the ones defined at the resource level
const safeActionsForResource: Action[] = [
...(configuration.global.safeToApplyActions ?? []),
...getSafeToApplyActionsFromDecorator(pair[1].decorator)
]

// It all the actions that will be perfomed to apply the plan are not included in the safe-list,
// it means that there is a potential unsafe action for the resource and we need to ask for approval.
return !areAllItemsIncluded(
safeActionsForResource,
mapDiffTypeToActions(pair[0].diffType)
)
})

Logger.log(
`Found ${resourcesThatAreNotSafeToApply.length} resource(s) that are not safe to apply:`
)
resourcesThatAreNotSafeToApply.forEach(it =>
Logger.log(`- ${printShortTerraformEntity(it[1])}`)
)

return resourcesThatAreNotSafeToApply.length > 0
}
}

function areAllItemsIncluded<T>(
items: ReadonlyArray<T>,
itemsToCheck: ReadonlyArray<T>
): boolean {
return itemsToCheck.every(it => items.includes(it))
}
6 changes: 5 additions & 1 deletion core/libs/service/src/service.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import {BootstrappingService} from "./bootstrapping/bootstrapping.service"
import {CodebaseReaderService} from "./codebase-reader/codebase-reader.service"
import {ConfigurationService} from "./configuration/configuration.service"
import {PlanReaderService} from "./plan-reader/plan-reader.service"
import {RequireApprovalModeUseCase} from "./approval/require-approval-mode.use-case"
import {SafeToApplyModeUseCase} from "./approval/safe-to-apply-mode.use-case"

@Module({
imports: [ExternalModule],
Expand All @@ -13,7 +15,9 @@ import {PlanReaderService} from "./plan-reader/plan-reader.service"
CodebaseReaderService,
PlanReaderService,
ConfigurationService,
BootstrappingService
BootstrappingService,
RequireApprovalModeUseCase,
SafeToApplyModeUseCase
],
exports: [ApprovalService, BootstrappingService]
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import {BootstrappingService} from "@libs/service/bootstrapping/bootstrapping.se
import {Test, TestingModule} from "@nestjs/testing"
import {BootstrappingServiceMock} from "../mocks/bootstrapping.service.mock"
import {mockConfiguration} from "@libs/testing/mocks/configuration.mock"
import {RequireApprovalModeUseCase} from "@libs/service/approval/require-approval-mode.use-case"
import {SafeToApplyModeUseCase} from "@libs/service/approval/safe-to-apply-mode.use-case"

describe("ApprovalService", () => {
let approvalService: ApprovalService
Expand All @@ -17,7 +19,9 @@ describe("ApprovalService", () => {
{
provide: BootstrappingService,
useClass: BootstrappingServiceMock
}
},
RequireApprovalModeUseCase,
SafeToApplyModeUseCase
]
}).compile()

Expand Down

0 comments on commit 9ce27a8

Please sign in to comment.