Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/early-parks-judge.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@gradientedge/cdk-utils-azure': patch
---

Add `createManagementLockByScopeDeleteLock` to create a `CanNotDelete` management lock for a Pulumi resource.
57 changes: 54 additions & 3 deletions packages/azure/src/services/authorisation/main.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down
9 changes: 8 additions & 1 deletion packages/azure/src/services/authorisation/types.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down
70 changes: 69 additions & 1 deletion packages/azure/test/services/authorisation-manager.test.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -20,9 +25,13 @@ const testStackProps: any = {
subscriptionId: 'test-subscription-id',
}

const registeredResourceInputs: Record<string, Record<string, unknown>> = {}

class TestCommonConstruct extends CommonAzureConstruct {
declare props: TestAzureStackProps
roleAssignment: RoleAssignment
managementLock: ManagementLockByScope
managementDeleteLock: ManagementLockByScope
storageTableRole: RoleAssignment
storageAccountRole: RoleAssignment
appConfigRole: RoleAssignment
Expand Down Expand Up @@ -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
)
}
}

Expand All @@ -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 },
Expand All @@ -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()
})
})

Expand Down Expand Up @@ -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', () => {
Expand All @@ -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')
})
})
Loading