-
Notifications
You must be signed in to change notification settings - Fork 441
Description
Description
This issue is intended to track the efforts for alinging the CARML modules to the Azure Verified Module (AVM) specs, ultimately enabling us to publish the CARML modules to the Public Bicep Registry.
Bulk edits
These can ideally be updated 'on scale', with the already defined interfaces & conventions (e.g., folder structure), that must be updated in all modules (while taking individual characterists into account).
Note: Some of these changes may require changes to the CI environment.
Checklist
- Align in-module folder structure to AVM #4057
- Align RG test file names to AVM #4131
- Align to Diagnostic Settings specs #4151
- Align to Role Assignments AVM specs #4123
- Align to Resource Locks specs #4110
- Align to Tags specs #4157
- Align to Managed Identities specs #4052
- Align to Private Endpoints specs #4108
- Align to Customer Managed Keys specs #4172
- Once
Set-ModuleReadMescript is updated, remove & regenerate all ReadMes - Update the UDTs to the latest AVM Specs (e.g., remove rundant
nullvalues) #4207 - Add idemptency to tests (- optionally supported by the attached script) #4209
Per-module edits
Side-by-side / following the alignment to the extension interface, the following list should be used to track the 'full' alignment of the modules (e.g., PSRule compliance, etc.)
Checklist
- Align
aad/domain-serviceto AVM specs - Align
analysis-services/serverto AVM specs #4398 - Align
api-management/serviceto AVM specs #4355 - Align
app/container-appto AVM specs #4489 - Align
app/jobsto AVM specs - Align
app/managed-environmentto AVM specs #4442 - Align
app-configuration/configuration-storeto AVM specs #4504 - Align
automation/automation-accountto AVM specs #4311 - Align
batch/batch-accountto AVM specs #4056 - Align
cache/redisto AVM specs #4399 - Align
cache/redis-enterpriseto AVM specs - Align
cdn/profileto AVM specs #4510 - Align
cognitive-services/accountto AVM specs #4055 - Align
compute/availability-setto AVM specs #4411 - Align
compute/diskto AVM specs #4406 - Align
compute/disk-encryption-setto AVM specs #4425 - Align
compute/galleryto AVM specs #4422 - Align
compute/imageto AVM specs #4400 - Align
compute/proximity-placement-groupto AVM specs #4426 - Align
compute/ssh-public-keyto AVM specs #4075 - Align
compute/virtual-machineto AVM specs #4107 - Align
compute/virtual-machine-scale-setto AVM specs #4502 - Align
consumption/budgetto AVM specs #4407 - Align
container-instance/container-groupto AVM specs #4481 - Align
container-registry/registryto AVM specs #4456 - Align
container-service/managed-clusterto AVM specs #4194 - Align
data-factory/factoryto AVM specs #4376 - Align
data-protection/backup-vaultto AVM specs #4405 - Align
databricks/workspaceto AVM specs #4402 - Align
db-for-my-sql/flexible-serverto AVM specs #4401 - Align
db-for-postgre-sql/flexible-serverto AVM specs #4305 - Align
desktop-virtualization/application-groupto AVM specs #4473 - Align
desktop-virtualization/host-poolto AVM specs #4475 - Align
desktop-virtualization/scaling-planto AVM specs #4474 - Align
desktop-virtualization/workspaceto AVM specs #4476 - Align
dev-test-lab/labto AVM specs #4412 - Align
digital-twins/digital-twins-instanceto AVM specs #4141 - Align
document-db/database-accountto AVM specs #4321 - Align
event-grid/domainto AVM specs #4384 - Align
event-grid/system-topicto AVM specs #4148 - Align
event-grid/topicto AVM specs #4385 - Align
event-hub/namespaceto AVM specs #4479 - Align
health-bot/health-botto AVM specs #4404 - Align
healthcare-apis/workspaceto AVM specs #4531 - Align
insights-actiongroupto AVM specs #4074 - Align
insights/activity-log-alertto AVM specs #4344 - Align
insights/componentto AVM specs #4268 - Align
insights/data-collection-endpointto AVM specs #4332 - Align
insights/data-collection-ruleto AVM specs #4333 - Align
insights/diagnostic-settingto AVM specs #4249 - Align
insights/metric-alertto AVM specs #4343 - Align
insights/private-link-scopeto AVM specs #4413 - Align
insights/scheduled-query-ruleto AVM specs #4345 - Align
insights/webtestto AVM specs #4377 - Align
key-vault/vaultto AVM specs #4063 - Align
kubernetes-configuration/extensionto AVM specs #4054 - Align
kubernetes-configuration/flux-configurationto AVM specs #4053 - Align
logic/workflowto AVM specs #4180 - Align
machine-learning-services/workspaceto AVM specs #4458 - Align
maintenance/maintenance-configurationto AVM specs #4378 - Align
managed-identity/user-assigned-identityto AVM specs #4149 - Align
managed-services/registration-definitionto AVM specs - Align
management/management-groupto AVM specs #4496 - Align
net-app/net-app-accountto AVM specs #4403 - Align
network/application-gatewayto AVM specs - Align
network/application-gateway-web-application-firewall-policyto AVM specs #4532 - Align
network/application-security-groupto AVM specs #4490 - Align
network/azure-firewallto AVM specs #4507 - Align
network/bastion-hostto AVM specs #4324 - Align
network/connectionto AVM specs #4389 - Align
network/ddos-protection-planto AVM specs #4408 - Align
network/dns-forwarding-rulesetto AVM specs #4139 - Align
network/dns-resolverto AVM specs #4101 - Align
network/dns-zoneto AVM specs #4163 - Align
network/express-route-circuitto AVM specs #4264 - Align
network/express-route-gatewayto AVM specs #4265 - Align
network/firewall-policyto AVM specs #4432 - Align
network/front-doorto AVM specs #4433 - Align
network/front-door-web-application-firewall-policyto AVM specs #4434 - Align
network/ip-groupto AVM specs #4414 - Align
network/load-balancerto AVM specs #4044 - Align
network/local-network-gatewayto AVM specs #4383 - Align
network/nat-gatewayto AVM specs #4382 - Align
network/network-interfaceto AVM specs #4062 - Align
network/network-managerto AVM specs #4415 - Align
network/network-security-groupto AVM specs #4443 - Align
network/private-dns-zoneto AVM specs #4140 - Align
network/private-endpointto AVM specs #4064 - Align
network/private-link-serviceto AVM specs #4416 - Align
network/public-ip-addressto AVM specs #4043 - Align
network/public-ip-prefixto AVM specs #4323 - Align
network/route-tableto AVM specs #4444 - Align
network/service-endpoint-policyto AVM specs - [Feature Request]: Align
network/trafficmanagerprofileto AVM specs #4314 - Align
network/virtual-hubto AVM specs - Align
network/virtual-networkto AVM specs #4061 - Align
network/virtual-network-gatewayto AVM specs #4386 - Align
network/virtual-wanto AVM specs - Align
network/vpn-gatewayto AVM specs #4387 - Align
network/vpn-siteto AVM specs #4390 - Align
operational-insights/workspaceto AVM specs #4060 - Align
operations-management/solutionto AVM specs #4059 - Align
power-bi-dedicated/capacityto AVM specs #4337 - Align
purview/accountto AVM specs #4460 - Align
recovery-services/vaultto AVM specs #4494 - Align
relay/namespaceto AVM specs #4528 - Align
resource-graph/queryto AVM specs #4445 - Align
resources/deployment-scriptto AVM specs #4198 - Align
resources/resource-groupto AVM specs #4430 - Align
search/search-serviceto AVM specs #4266 - (pattern) Align
security/azure-security-centerto AVM specs - Align
service-bus/namespaceto AVM specs #4179 - Align
service-fabric/clusterto AVM specs - Align
signal-r-service/signal-rto AVM specs #4511 - Align
signal-r-service/web-pub-subto AVM specs #4514 - Align
sql/managed-instanceto AVM specs - Align
sql/serverto AVM specs #4270 - Align
storage/storage-accountto AVM specs #4058 - Align
synapse/private-link-hubto AVM specs #4480 - Align
synapse/workspaceto AVM specs #4467 - Align
virtual-machine-images/image-templateto AVM specs #4417 - Align
web/connectionto AVM specs #4529 - Align
web/hosting-environmentto AVM specs - Align
web/serverfarmto AVM specs #4423 - Align
web/siteto AVM specs #4438 - Align
web/static-siteto AVM specs #4446
Migration Guide
This section provides a checklist of things to look out for per module to ensure they're AVM compliant, both as per module specifications & the Contribution Guide.
Checklist
-
Tests (ref)
- Rename test folder and add nested
e2efolder (ref) - Rename
minfolder todefaults - Rename
commonfolder tomax - Add
waf-alignedfolder (e.g., based oncommon). This test should not fail PSRule & show the module being deployed with best-practices - For each folder,
- Update the
serviceShortparameter to align with the new naming (e.g.,wafforwaf-aligned). For now, we should continue usingminfordefaultsto align with PSRule. - Update the
namePrefixinput parameter value from[[namePrefix]]to#_namePrefix_#(the reason being that Bicep has a compilation issue because of the prefix & suffix in another location) - Update the
../../main.bicepmodule template reference to../../../main.bicep - If a resource group is deployed, update the RG parameter name to the new format that also uses the
namePrefix. For example:@description('Optional. The name of the resource group to deploy for testing purposes.') @maxLength(90) param resourceGroupName string = 'dep-${namePrefix}-network.privateendpoints-${serviceShort}-rg'
- (Optionally) add a block like the following below the target scope to render a more meaningful example in the ReadMe
metadata name = 'Using only defaults' metadata description = 'This instance deploys the module with the minimum set of required parameters.'
- You should also try and test idempotency if possible. You can do this by updating the test invocation to
@batchSize(1) module testDeployment '../../../main.bicep' = [for iteration in [ 'init', 'idem' ]: { scope: resourceGroup name: '${uniqueString(deployment().name, location)}-test-${serviceShort}-${iteration}' (...) }]
- Rename test folder and add nested
-
For each module that supports
Diagnostic Settings
Reference to AVM specs
- Add the
diagnosticSettingTypedescribed in the above reference to a// Definitionsblock at the bottom of the template file - Remove any of the current diagnosticSetting parameters & variables
- Add the the new
diagnosticSettingsparameter as per the specs to the template - Updated the deployment block as per the specs to enable it to work with the new object type
- Check if any of the tests must be updated. The new block may look like
diagnosticSettings: [ { name: 'customSetting' eventHubName: diagnosticDependencies.outputs.eventHubNamespaceEventHubName eventHubAuthorizationRuleResourceId: diagnosticDependencies.outputs.eventHubAuthorizationRuleId storageAccountResourceId: diagnosticDependencies.outputs.storageAccountResourceId workspaceResourceId: diagnosticDependencies.outputs.logAnalyticsWorkspaceResourceId } ]
NOTE:
⚠️ Make sure that if the module does not support e.g. metrics, that you update the logic accordinglyRole Assignments
Reference to AVM specs
- Add the
roleAssignmentTypedescribed in the above reference to a// Definitionsblock at the bottom of the template file - Update the current
roleAssignmentsparameter as per the specs (- should now reference the User-defined-type) - Take the current list of
builtInRoleNamesfrom thenested_roleAssignments.bicepfile and add them to the variables block of the main template. The new schema does not require the nested template. Also, reduce the list of specified roles to only those that make sense for this resource (ref)/ For, for example, Cognitive Services, we should only provide the important ones as Owner, Contributor, etc. + all service specific roles such as 'Cognitive Services User'. - Replace the original module deployment block with the new resource deployment block
- Check if any of the tests must be updated. The new block may look like
roleAssignments: [ { roleDefinitionIdOrName: 'Reader' principalId: nestedDependencies.outputs.managedIdentityPrincipalId principalType: 'ServicePrincipal' } ]
Resource Locks
Reference to AVM specs
- Add the
lockTypedescribed in the above reference to a// Definitionsblock at the bottom of the template file - Update the current
lockparameter as per the specs (- should now reference the User-defined-type) - Updated the deployment block as per the specs to enable it to work with the new object type
- Check if any of the tests must be updated. The new block may look like
lock: { kind: 'CanNotDelete' name: 'myCustomLockName' }
Managed Identities
Reference to AVM specs
- Add the
managedIdentitiesTypedescribed in the above reference to a// Definitionsblock at the bottom of the template file - Remove any of the current identity parameters & variables
- Add the the new
managedIdentitiesparameter as per the specs to the template - Updated the deployment block as per the specs to enable it to work with the new object type
- Check if any of the tests must be updated. The new block may look like
managedIdentities: { systemAssigned: true userAssignedResourcesIds: [ nestedDependencies.outputs.managedIdentityResourceId ] }
NOTE:
⚠️ Make sure that if the module does not support e.g. user-assigned-identities, that you update the logic accordinglyPrivate Endpoints
Reference to AVM specs
- Add the
privateEndpointTypedescribed in the above reference to a// Definitionsblock at the bottom of the template file - Update the current
privateEndpointsparameter as per the specs (- should now reference the User-defined-type) - Updated the deployment block as per the specs to enable it to work with the new object type.
Note: For any resource that only supports one service/groupID (e.g.
'vault'for KeyVault, but NOT'blob'for StorageAccount) we can provide a default value for that property (hence there are 2 variants in the spec). - Check if any of the tests must be updated. The new block may look like
privateEndpoints: [ { privateDnsZoneResourceIds: [ nestedDependencies.outputs.privateDNSZoneResourceId ] subnetResourceId: nestedDependencies.outputs.subnetResourceId tags: { 'hidden-title': 'This is visible in the resource name' Environment: 'Non-Prod' Role: 'DeploymentValidation' } } ]
Customer Managed Keys
Reference to AVM specs
- Add the
customerManagedKeyTypedescribed in the above reference to a// Definitionsblock at the bottom of the template file - Remove any of the current customer-managed-key parameters & variables
- Add the the new
customerManagedKeyparameter as per the specs to the template - Update the
existingresource references as per the specs - Updated the deployment block as per the specs to enable it to work with the new object type
-⚠️ BEWARE module-specific differences
- Note also that the new schema SHOULD support system-assigned-identities. As this cannot be done in a single deployment, you can find a reference how this would look like here - Check if any of the tests must be updated. The new block may look like
customerManagedKey: { keyName: nestedDependencies.outputs.keyVaultKeyName keyVaultResourceId: nestedDependencies.outputs.keyVaultResourceId userAssignedIdentityResourceId: nestedDependencies.outputs.managedIdentityResourceId }
NOTE:
⚠️ Make sure that if the module does not support e.g. metrics, that you update the logic accordingly - Add the
-
Other
- Set version in
version.jsonback to0.1 - (Optional) Introduce the new
nullablefeature for parameters where-ever it makes sense to you (and ensure to test it). This enables us to simplify logic like in the following example// Old param attributesExp int = -1 resource secret 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = { (...) properties: { attributes: { exp: attributesExp != -1 ? attributesExp : null } } } // New param attributesExp int? resource secret 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = { (...) properties: { attributes: { exp: attributesExp } } }
- Set version in
-
ReadMe
- Regenerate all module ReadMe's & compile all module Bicep templates from the ground up. Ideally, remove the ReadMEs and regenerate them completely. Take note of any extra content (e.g., 'considerations') that were added manually and add them to the module's description metadata in the respective
main.bicepfile
- Regenerate all module ReadMe's & compile all module Bicep templates from the ground up. Ideally, remove the ReadMEs and regenerate them completely. Take note of any extra content (e.g., 'considerations') that were added manually and add them to the module's description metadata in the respective
Helper script (work in progress)
Snippet
#region helper functions
function Convert-Folders {
[CmdletBinding()]
param (
[Parameter(Mandatory = $true)]
[string] $FolderPath
)
# .bicep
(Get-ChildItem -Path $FolderPath -Directory -Recurse -Filter '.bicep').FullName | Where-Object { $_ } | ForEach-Object {
if (-not (Test-Path (Join-Path (Split-Path $_) 'modules'))) {
$null = Rename-Item -Path $_ -NewName 'modules' -Force
}
}
# .test
# Add [e2e] folder
(Get-ChildItem -Path $FolderPath -Directory -Recurse -Filter '.test').FullName | ForEach-Object {
$newPath = Join-Path $_ 'e2e'
if (-not (Test-Path $newPath)) {
$null = New-Item -ItemType 'Directory' -Path $newPath
}
}
# Move tests to new sub folder
$testTopFolders = (Get-ChildItem -Path $FolderPath -Directory -Recurse -Filter '.test').FullName
foreach ($testfolderPath in $testTopFolders) {
$testCaseFolderPaths = (Get-ChildItem $testfolderPath -Directory -Exclude 'e2e').FullName
foreach ($testFolderPath in $testCaseFolderPaths) {
$expectedFolderName = Split-Path $testfolderPath -Leaf
$newTestFolderPath = Join-Path (Split-Path $testFolderPath -Parent) 'e2e' $expectedFolderName
if (-not (Test-Path $newTestFolderPath)) {
$null = New-Item -ItemType 'Directory' -Path $newTestFolderPath
}
$originalTestFilePaths = Get-ChildItem $testfolderPath -File -Recurse
foreach ($originalTestFilePath in $originalTestFilePaths) {
$null = Move-Item -Path $originalTestFilePath -Destination $newTestFolderPath -Force
}
# If default folder [min/common], rename to new names [defaults/max]
switch ($expectedFolderName) {
'min' {
if (-not (Test-Path (Join-Path (Split-Path $newTestFolderPath) 'defaults'))) {
$null = Rename-Item -Path $newTestFolderPath -NewName 'defaults' -Force
}
}
'common' {
if (-not (Test-Path (Join-Path (Split-Path $newTestFolderPath) 'max'))) {
$null = Rename-Item -Path $newTestFolderPath -NewName 'max' -Force
}
}
Default {}
}
# Delete original folder
$null = Remove-Item -Path $testFolderPath -Force
}
}
# Rename test folder
(Get-ChildItem -Path $FolderPath -Directory -Recurse -Filter '.test').FullName | Rename-Item -NewName 'tests'
# Generate WAF folder
$wafFolderPath = Join-Path $FolderPath 'tests' 'e2e' 'waf-aligned'
if (-not (Test-Path $wafFolderPath)) {
# Duplicate 'max' test folder
$null = New-Item -Path $wafFolderPath -ItemType 'Directory' -Force
$maxTestPath = Join-Path $FolderPath 'tests' 'e2e' 'max'
$null = Copy-Item -Path "$maxTestPath/*" -Destination $wafFolderPath -Recurse
}
}
function Set-ReadMe {
[CmdletBinding()]
param (
[Parameter(Mandatory = $true)]
[string] $FolderPath
)
$readMePaths = (Get-ChildItem -Path $FolderPath -File -Recurse -Filter 'readme.md').FullName
# Remove original ReadMes
$readMePaths | ForEach-Object { $null = Remove-Item -Path $_ -Force }
# Regenerate new ReadMes
# Set-AVMModule -ModuleFolderPath $FolderPath -Recurse
}
function Set-VersionFile {
[CmdletBinding()]
param (
[Parameter(Mandatory = $true)]
[string] $FolderPath
)
$versionPaths = (Get-ChildItem -Path $FolderPath -File -Recurse -Filter 'version.json').FullName
foreach ($path in $versionPaths) {
$originalContent = Get-Content -Path $path -Raw | ConvertFrom-Json
$originalContent.version = '0.1'
$originalContent | ConvertTo-Json -Depth 100 | Set-Content -Path $path -Force
}
}
function Set-TestFiles {
[CmdletBinding()]
param (
[Parameter(Mandatory = $true)]
[string] $FolderPath
)
$testFilePaths = (Get-ChildItem -Path $FolderPath -File -Recurse -Filter 'main.test.bicep').FullName
$testFolderPaths = $testFilePaths | ForEach-Object { Split-Path $_ }
# [1] Update service shorts
foreach ($testFolderPath in $testFolderPaths) {
$testFolderName = Split-Path $testFolderPath -Leaf
# If default folder [min/common], rename to new names [defaults/max]
switch ($testFolderName) {
'defaults' {
$testContent = Get-Content (Join-Path $testFolderPath 'main.test.bicep') -Raw
# Should remain `min` for now to work with PSRule config
# $testContent = $testContent -replace "(param serviceShort string = '[a-zA-Z]+)min'", ("`$1{0}'" -f 'min')
$null = Set-Content (Join-Path $testFolderPath 'main.test.bicep') -Value $testContent -Force
}
'max' {
$testContent = Get-Content (Join-Path $testFolderPath 'main.test.bicep') -Raw
$testContent = $testContent -replace "(param serviceShort string = '[a-zA-Z]+)com'", ("`$1{0}'" -f 'max')
$null = Set-Content (Join-Path $testFolderPath 'main.test.bicep') -Value $testContent -Force
}
'waf-aligned' {
$testContent = Get-Content (Join-Path $testFolderPath 'main.test.bicep') -Raw
$testContent = $testContent -replace "(param serviceShort string = '[a-zA-Z]+)com'", ("`$1{0}'" -f 'waf')
$null = Set-Content (Join-Path $testFolderPath 'main.test.bicep') -Value $testContent -Force
}
Default {}
}
}
foreach ($testFilePath in $testFilePaths) {
$testContent = Get-Content $testFilePath -Raw
# [2] Remove telemetry
$testContent = $testContent -replace "@description\('.+\(GUID\)\.'\)(\r\n|\r|\n){1}param enableDefaultTelemetry.+(\r\n|\r|\n){2}", ''
$testContent = $testContent -replace '.*enableDefaultTelemetry: enableDefaultTelemetry.*(\r\n|\r|\n){1}', ''
# [3] Update RG name
$testContent = $testContent -replace "(param resourceGroupName string = ')ms.(.+)", '$1dep-${namePrefix}-$2'
# [4] Idempotency & main.bicep reference
$testContent = $testContent -replace "module testDeployment '\.\.\/\.\.\/main\.bicep' = {", "@batchSize(1)`r`nmodule testDeployment '../../../main.bicep' = [for iteration in [ 'init', 'idem' ]: {"
$testContent = $testContent -replace "name: '\`${uniqueString\(deployment\(\)\.name, location\)}-test-\`${serviceShort}'", "name: '`${uniqueString(deployment().name, location)}-test-`${serviceShort}-`${iteration}'"
# [5] diagnosticsTemplateReference
$testContent = $testContent -replace '.+diagnostic.dependencies.bicep', "module diagnosticDependencies '../../../../../../utilities/e2e-template-assets/templates/diagnostic.dependencies.bicep"
# [6] Replace namePrefix token with new format
$testContent = $testContent -replace '\[\[namePrefix\]\]', '#_namePrefix_#'
$null = Set-Content -Path $testFilePath -Value $testContent -Force
}
}
#endregion
function Convert-CarmlToAvm {
[CmdletBinding()]
param (
[Parameter(Mandatory = $true)]
[string] $FolderPath,
[Parameter(Mandatory = $false)]
[string] $RepoRoot = 'C:\dev\ip\Azure-ResourceModules\ResourceModules'
)
# Load external functions
# . (Join-Path $RepoRoot 'utilities' 'tools' 'Set-AVMModule.ps1')
# [1] Convert folders
Convert-Folders -FolderPath $FolderPath
# [2] Set version.json back to 0.1
Set-VersionFile -FolderPath $FolderPath
# [3] Test file changes
Set-TestFiles -FolderPath $FolderPath
# [4] Regenerate ReadMe
Set-ReadMe -FolderPath $FolderPath
}
Convert-CarmlToAvm -FolderPath 'C:\dev\ip\Azure-ResourceModules\ResourceModules\modules\key-vault\vault'Final steps: Migration
For the final AVM contribution a few more changes will be necessary as described in the following
- Update the reference to the diagnostic dependencies file in each test file that uses it to
avm/utilities/e2e-template-assets/templates/diagnostic.dependencies.bicep - Update the telemetry implementation as per it's new schema (ref). If the module references any external module, make sure to pass the
enableTelemetryflag through likeenableTelemetry: enableTelemetryto enable users to enable/disable for the entire deployment. Child resources should remain with the telemetry switched off for now. - Remove telemetry from test cases
- Remove
version.jsonfiles from child modules (as can't publish them yet) - Align app/managed-environment to AVM specs #4409
This module has already been migrated to AVM. Only the AVM version is expected to receive updates / new features. Please do not work on improving this module in CARML.
- If the migrated module will be orphaned, add a
ORPHANED.mdfile with the following content to the module in AVM onlyThis module is currently orphaned. Only security and bug fixes are being handled by the AVM core team at present. If interested in becoming a module owner (must be Microsoft FTE) for this orphaned module please comment on the issue here
These changes should be done after creating a fork of the public bicep registory respository and the module is added in the /avm/res/ folder. From there you can not only test the module using the AVM CI, but also open the Pull Request to the Upstream repository.
Metadata
Metadata
Assignees
Labels
Type
Projects
Status