diff --git a/.changeset/early-parks-judge.md b/.changeset/early-parks-judge.md new file mode 100644 index 000000000..56bf1b488 --- /dev/null +++ b/.changeset/early-parks-judge.md @@ -0,0 +1,5 @@ +--- +'@gradientedge/cdk-utils-azure': patch +--- + +Add `createManagementLockByScopeDeleteLock` to create a `CanNotDelete` management lock for a Pulumi resource. diff --git a/packages/azure/src/services/authorisation/main.ts b/packages/azure/src/services/authorisation/main.ts index 5a27c9416..6a03569ed 100644 --- a/packages/azure/src/services/authorisation/main.ts +++ b/packages/azure/src/services/authorisation/main.ts @@ -1,10 +1,15 @@ -import { PrincipalType, RoleAssignment } from '@pulumi/azure-native/authorization/index.js' -import { Input, ResourceOptions } from '@pulumi/pulumi' +import { + LockLevel, + ManagementLockByScope, + PrincipalType, + RoleAssignment, +} from '@pulumi/azure-native/authorization/index.js' +import { CustomResource, Input, mergeOptions, ResourceOptions } from '@pulumi/pulumi' import { CommonAzureConstruct } from '../../common/index.js' import { RoleDefinitionId } from './constants.js' -import { RoleAssignmentProps } from './types.js' +import { ManagementLockByScopeProps, RoleAssignmentProps } from './types.js' /** * Provides operations on Azure Authorisation using Pulumi @@ -25,6 +30,52 @@ import { RoleAssignmentProps } from './types.js' * @category Service */ export class AzureAuthorisationManager { + /** + * @summary Method to create a management lock at a resource scope + * @param id scoped id of the resource + * @param scope scope in which this resource is defined + * @param props Management lock properties + * @param resourceOptions Optional settings to control resource behaviour + * @see [Pulumi Azure Native Management Lock By Scope]{@link https://www.pulumi.com/registry/packages/azure-native/api-docs/authorization/managementlockbyscope/} + */ + public createManagementLockByScope( + id: string, + scope: CommonAzureConstruct, + props: ManagementLockByScopeProps, + resourceOptions?: ResourceOptions + ) { + if (!props) throw new Error(`Props undefined for ${id}`) + + return new ManagementLockByScope(`${id}`, props, { parent: scope, ...resourceOptions }) + } + + /** + * @summary Method to create a CanNotDelete management lock at a resource scope + * @param id scoped id of the resource + * @param scope scope in which this resource is defined + * @param resource Pulumi resource to protect from deletion + * @param resourceOptions Optional settings to control resource behaviour + */ + public createManagementLockByScopeDeleteLock( + id: string, + scope: CommonAzureConstruct, + resource: CustomResource, + resourceOptions?: ResourceOptions + ) { + if (!resource) throw new Error(`Resource undefined for ${id}`) + + return this.createManagementLockByScope( + `${id}-delete-lock`, + scope, + { + lockName: `${id}-delete-lock`, + level: LockLevel.CanNotDelete, + scope: resource.id, + }, + mergeOptions(resourceOptions, { dependsOn: [resource] }) + ) + } + /** * @summary Method to create a new role assignment * @param id scoped id of the resource diff --git a/packages/azure/src/services/authorisation/types.ts b/packages/azure/src/services/authorisation/types.ts index 6531a96d0..aca96c7a7 100644 --- a/packages/azure/src/services/authorisation/types.ts +++ b/packages/azure/src/services/authorisation/types.ts @@ -1,4 +1,11 @@ -import { RoleAssignmentArgs } from '@pulumi/azure-native/authorization/index.js' +import { ManagementLockByScopeArgs, RoleAssignmentArgs } from '@pulumi/azure-native/authorization/index.js' + +/** + * Properties for creating an Azure management lock by scope + * @see [Pulumi Azure Native Management Lock By Scope]{@link https://www.pulumi.com/registry/packages/azure-native/api-docs/authorization/managementlockbyscope/} + * @category Interface + */ +export interface ManagementLockByScopeProps extends ManagementLockByScopeArgs {} /** * Properties for creating an Azure role assignment diff --git a/packages/azure/test/services/authorisation-manager.test.ts b/packages/azure/test/services/authorisation-manager.test.ts index 891238499..b7bd87459 100644 --- a/packages/azure/test/services/authorisation-manager.test.ts +++ b/packages/azure/test/services/authorisation-manager.test.ts @@ -1,4 +1,9 @@ -import { PrincipalType, RoleAssignment } from '@pulumi/azure-native/authorization/index.js' +import { + LockLevel, + ManagementLockByScope, + PrincipalType, + RoleAssignment, +} from '@pulumi/azure-native/authorization/index.js' import * as pulumi from '@pulumi/pulumi' import { CommonAzureConstruct, CommonAzureStack, CommonAzureStackProps, RoleDefinitionId } from '../../src/index.js' import { outputToPromise } from '../helpers.js' @@ -20,9 +25,13 @@ const testStackProps: any = { subscriptionId: 'test-subscription-id', } +const registeredResourceInputs: Record> = {} + class TestCommonConstruct extends CommonAzureConstruct { declare props: TestAzureStackProps roleAssignment: RoleAssignment + managementLock: ManagementLockByScope + managementDeleteLock: ManagementLockByScope storageTableRole: RoleAssignment storageAccountRole: RoleAssignment appConfigRole: RoleAssignment @@ -64,6 +73,17 @@ class TestCommonConstruct extends CommonAzureConstruct { PrincipalType.ServicePrincipal, this.authorisationManager.resolveRoleDefinitionId(this, RoleDefinitionId.APP_CONFIGURATION_DATA_READER) ) + + this.managementLock = this.authorisationManager.createManagementLockByScope('test-delete-lock', this, { + lockName: 'test-delete-lock', + level: LockLevel.CanNotDelete, + scope: '/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.Storage/storageAccounts/testsa', + }) + this.managementDeleteLock = this.authorisationManager.createManagementLockByScopeDeleteLock( + 'test-delete-lock-helper', + this, + this.storageAccountRole + ) } } @@ -85,6 +105,7 @@ pulumi.runtime.setAllConfig({ pulumi.runtime.setMocks({ newResource: (args: pulumi.runtime.MockResourceArgs) => { + registeredResourceInputs[args.name] = args.inputs return { id: `${args.name}-id`, state: { ...args.inputs, name: args.name }, @@ -105,6 +126,8 @@ describe('TestAzureAuthorisationConstruct', () => { expect(stack.construct.storageTableRole).toBeDefined() expect(stack.construct.storageAccountRole).toBeDefined() expect(stack.construct.appConfigRole).toBeDefined() + expect(stack.construct.managementLock).toBeDefined() + expect(stack.construct.managementDeleteLock).toBeDefined() }) }) @@ -140,6 +163,31 @@ describe('TestAzureAuthorisationConstruct', () => { }) ) }) + + test('provisions management lock by scope as expected', async () => { + await outputToPromise( + pulumi.all([stack.construct.managementLock.id, stack.construct.managementLock.level]).apply(([id, level]) => { + expect(id).toBeDefined() + expect(level).toBe(LockLevel.CanNotDelete) + }) + ) + }) + + test('provisions a CanNotDelete management lock by scope', async () => { + await outputToPromise( + pulumi + .all([ + stack.construct.managementDeleteLock.id, + stack.construct.managementDeleteLock.level, + stack.construct.storageAccountRole.id, + ]) + .apply(([id, level, resourceId]) => { + expect(id).toBeDefined() + expect(level).toBe(LockLevel.CanNotDelete) + expect(registeredResourceInputs['test-delete-lock-helper-delete-lock']?.scope).toBe(resourceId) + }) + ) + }) }) describe('TestAzureAuthorisationConstruct - Error Handling', () => { @@ -148,4 +196,24 @@ describe('TestAzureAuthorisationConstruct - Error Handling', () => { stack.construct.authorisationManager.createRoleAssignment('test-role-err', stack.construct, undefined as any) }).toThrow('Props undefined for test-role-err') }) + + test('createManagementLockByScope throws when props are undefined', () => { + expect(() => { + stack.construct.authorisationManager.createManagementLockByScope( + 'test-lock-err', + stack.construct, + undefined as any + ) + }).toThrow('Props undefined for test-lock-err') + }) + + test('createManagementLockByScopeDeleteLock throws when resource is undefined', () => { + expect(() => { + stack.construct.authorisationManager.createManagementLockByScopeDeleteLock( + 'test-delete-lock-err', + stack.construct, + undefined as any + ) + }).toThrow('Resource undefined for test-delete-lock-err') + }) })