Skip to content
Open
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
14 changes: 13 additions & 1 deletion build/scripts/NewBcContainer.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ if ("$env:GITHUB_RUN_ID" -eq "") {

Import-Module (Join-Path $PSScriptRoot 'PlatformHelper.psm1') -Force
Import-Module (Join-Path $PSScriptRoot 'EnlistmentHelperFunctions.psm1') -Force
Import-Module (Join-Path $PSScriptRoot 'ResetBcContainerDatabase.psm1') -Force

$platformVersion = (Get-ConfigValue -Key "BCPlatform" -ConfigType Packages).Version
if ($platformVersion) {
Expand All @@ -28,9 +29,20 @@ Set-BcContainerServerConfiguration -containerName $parameters.ContainerName -key
Set-BcContainerServerConfiguration -containerName $parameters.ContainerName -keyName "UsePermissionSetsFromExtensions" -keyValue "true"
Restart-BcContainer -containerName $parameters.ContainerName

# Bulk-reset the application database (drop+recreate) instead of uninstalling every pre-installed app
# one by one. Resolve the license from the artifact manifest; the reset drops the container's license.
$appArtifactPath = Download-Artifacts -artifactUrl $parameters.artifactUrl
$artifactManifest = Get-Content (Join-Path $appArtifactPath 'manifest.json') -Raw | ConvertFrom-Json
$licenseFile = Join-Path $appArtifactPath $artifactManifest.licenseFile
if (-not (Test-Path $licenseFile)) {
throw "Artifact license '$licenseFile' not found; cannot license the reset database."
}
Reset-BcContainerApplicationDatabase -ContainerName $parameters.ContainerName -Credential $parameters.Credential -LicenseFile $licenseFile

$installedApps = Get-BcContainerAppInfo -containerName $parameters.ContainerName -tenantSpecificProperties -sort DependenciesLast

# Clean the container for all apps. Apps will be installed by AL-Go
# The reset leaves only the System Application published. Remove it too so AL-Go publishes the
# repository-built version. This loop now runs over a single app instead of ~150.
foreach($app in $installedApps) {
Write-Host "Removing $($app.Name)"
UnInstall-BcContainerApp -containerName $parameters.ContainerName -name $app.Name -doNotSaveData -doNotSaveSchema -force
Expand Down
177 changes: 177 additions & 0 deletions build/scripts/ResetBcContainerDatabase.psm1
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
<#
.SYNOPSIS
Resets a multitenant Business Central container's application database to an empty state
in a single bulk operation.

.DESCRIPTION
Drops and recreates the application database, which removes every published app at once, and
rebuilds the minimum required to hand the container to AL-Go for publishing the repository apps:
- re-imports the developer license,
- re-publishes the System Application (needed for the SUPER permission set),
- creates a SUPER user,
- restores multitenancy with a fresh, empty 'default' tenant.

This replaces the previous approach of uninstalling ~150 pre-installed apps one by one. That
loop cost ~19 minutes per job because every host-side UnInstall/Unpublish call is a separate
in-container session (~8 s of overhead each), regardless of whether it also cleans schema.
Dropping the application database removes all apps in one operation instead (~4 minutes).

.PARAMETER ContainerName
Name of the multitenant BC container to reset.

.PARAMETER Credential
Credential used to create the SUPER user in the rebuilt database.

.PARAMETER LicenseFile
Path to the developer license (.bclicense) to import into the rebuilt application database.
Recreating the database drops the license that was imported when the container was created.

.NOTES
Only supports multitenant containers. The System Application is re-published from the container's
own currently-published copy, so this does not depend on the System Application being present on
disk (BCApps overrides the platform artifact, which strips it from C:\Applications).
#>
function Reset-BcContainerApplicationDatabase {
param(
[Parameter(Mandatory)] [string] $ContainerName,
[Parameter(Mandatory)] [pscredential] $Credential,
[Parameter(Mandatory)] [string] $LicenseFile
)

$customConfig = Get-BcContainerServerConfiguration -ContainerName $ContainerName
if ($customConfig.Multitenant -ne 'True') {
throw "Reset-BcContainerApplicationDatabase only supports multitenant containers."
}

$sysAppFile = Export-SystemApplicationFromContainer -ContainerName $ContainerName -Credential $Credential

Clear-BcApplicationDatabase -ContainerName $ContainerName -CustomConfig $customConfig

Write-Host "Re-importing license"
Import-BcContainerLicense -containerName $ContainerName -licenseFile $LicenseFile

Write-Host "Publishing System Application"
Publish-BcContainerApp -containerName $ContainerName -appFile $sysAppFile -skipVerification -sync -install

Write-Host "Creating SUPER user"
New-BcContainerBcUser -containerName $ContainerName -Credential $Credential -PermissionSetId SUPER -ChangePasswordAtNextLogOn:$false

Restore-BcMultitenancy -ContainerName $ContainerName -CustomConfig $customConfig
}

<#
.SYNOPSIS
Extracts the container's currently-published System Application to a temporary .app file and
returns its path.
.DESCRIPTION
The System Application provides the SUPER permission set (BCApps runs with
UsePermissionSetsFromExtensions=true), without which no usable admin user can be created. This
must run BEFORE the database is dropped, and pulls from the container's published copy - the only
source that survives BCApps' platform override, which strips it from C:\Applications.
#>
function Export-SystemApplicationFromContainer {
param(
[Parameter(Mandatory)] [string] $ContainerName,
[Parameter(Mandatory)] [pscredential] $Credential
)

$sysAppInfo = Get-BcContainerAppInfo -containerName $ContainerName | Where-Object { $_.Name -eq 'System Application' } | Select-Object -First 1
if (-not $sysAppInfo) { throw "System Application is not published in container '$ContainerName'; cannot reset." }
$sysAppFile = Join-Path ([System.IO.Path]::GetTempPath()) "sysapp_$ContainerName.app"
if (Test-Path $sysAppFile) { Remove-Item $sysAppFile -Force }
Get-BcContainerApp -containerName $ContainerName -appName $sysAppInfo.Name -publisher $sysAppInfo.Publisher -appVersion $sysAppInfo.Version -appFile $sysAppFile -credential $Credential
if (-not (Test-Path $sysAppFile)) { throw "Failed to extract System Application from container '$ContainerName'." }
Write-Host "Extracted System Application to '$sysAppFile'"
return $sysAppFile
}

<#
.SYNOPSIS
Drops the application and tenant databases and recreates an empty single-tenant application
database, removing every published app in one bulk operation.
.DESCRIPTION
Runs as a single in-container session. Restore-BcMultitenancy switches the container back to
multitenant afterwards.
#>
function Clear-BcApplicationDatabase {
param(
[Parameter(Mandatory)] [string] $ContainerName,
[Parameter(Mandatory)] $CustomConfig
)

# -usePwsh $false forces Windows PowerShell 5.1: under the container's pwsh7 the NAV management
# cmdlets (New-NAVApplicationDatabase) and Invoke-Sqlcmd hit .NET assembly-load conflicts
# (e.g. Microsoft.Extensions.Logging.Abstractions), verified on BC 28.
Invoke-ScriptInBcContainer -containerName $ContainerName -useSession $false -usePwsh $false -scriptblock {
Param($databaseName, $databaseServer, $databaseInstance)

$databaseServerInstance = $databaseServer
if ($databaseInstance) { $databaseServerInstance += "\$databaseInstance" }

Write-Host "Stopping service tier"
Set-NavServerInstance -ServerInstance $ServerInstance -stop

# Preserve applicationfamily (used by the platform); the recreated database must keep it.
$dbproperties = Invoke-Sqlcmd -ServerInstance $databaseServerInstance -Query "SELECT [applicationfamily] FROM [$databaseName].[dbo].[`$ndo`$dbproperty]"
if (-not $dbproperties -or -not $dbproperties.applicationfamily) {
throw "Could not read applicationfamily from database '$databaseName'; aborting reset to avoid recreating the database without this platform-required value."
}

Write-Host "Dropping application database '$databaseName' and tenant databases"
Remove-NavDatabase -databasename $databaseName -databaseserver $databaseServer -databaseInstance $databaseInstance

# Rebuild as a single-tenant database named 'tenant'; switch back to multitenant afterwards.
Set-NavServerConfiguration -ServerInstance $ServerInstance -KeyName "Multitenant" -KeyValue "False" -WarningAction SilentlyContinue
Set-NavServerConfiguration -ServerInstance $ServerInstance -KeyName "DatabaseName" -KeyValue "tenant" -WarningAction SilentlyContinue
Remove-NavDatabase -databasename "tenant" -databaseserver $databaseServer -databaseInstance $databaseInstance
Remove-NavDatabase -databasename "default" -databaseserver $databaseServer -databaseInstance $databaseInstance

Write-Host "Creating new empty application database 'tenant'"
New-NAVApplicationDatabase -DatabaseServer $databaseServerInstance -DatabaseName "tenant" | Out-Null
Invoke-Sqlcmd -ServerInstance $databaseServerInstance -Query "UPDATE [tenant].[dbo].[`$ndo`$dbproperty] SET [applicationfamily] = '$($dbproperties.applicationfamily)'"

Write-Host "Starting service tier"
Set-NavServerInstance -ServerInstance $ServerInstance -start
Sync-NavTenant -ServerInstance $ServerInstance -Force

} -argumentList $CustomConfig.DatabaseName, $CustomConfig.DatabaseServer, $CustomConfig.DatabaseInstance
}

<#
.SYNOPSIS
Switches the container back to multitenant: splits the application into its own database and
mounts a fresh, writable 'default' tenant.
#>
function Restore-BcMultitenancy {
param(
[Parameter(Mandatory)] [string] $ContainerName,
[Parameter(Mandatory)] $CustomConfig
)

# -usePwsh $false forces Windows PowerShell 5.1: under the container's pwsh7 the NAV management
# cmdlets (Export-NAVApplication) and Invoke-Sqlcmd hit .NET assembly-load conflicts, verified on
# BC 28. Note Copy-NavDatabase is no longer SMO-based (it copies the SQL files), but the block as
# a whole still requires WinPS.
Invoke-ScriptInBcContainer -containerName $ContainerName -useSession $false -usePwsh $false -scriptblock { Param($databaseName, $databaseServer, $databaseInstance)
$databaseServerInstance = $databaseServer
if ($databaseInstance) { $databaseServerInstance += "\$databaseInstance" }

Write-Host "Switching to multitenancy"
Set-NavServerInstance -ServerInstance $ServerInstance -stop
Set-NavServerConfiguration -ServerInstance $ServerInstance -KeyName "DatabaseName" -KeyValue "$databaseName" -WarningAction SilentlyContinue
Invoke-sqlcmd -serverinstance $databaseServerInstance -Database "tenant" -query 'CREATE USER "NT AUTHORITY\SYSTEM" FOR LOGIN "NT AUTHORITY\SYSTEM";'
Export-NAVApplication -DatabaseServer $databaseServer -DatabaseInstance $databaseInstance -DatabaseName "tenant" -DestinationDatabaseName $databaseName -Force -ServiceAccount 'NT AUTHORITY\SYSTEM' | Out-Null
Write-Host "Removing application from tenant template"
Remove-NAVApplication -DatabaseServer $databaseServer -DatabaseInstance $databaseInstance -DatabaseName "tenant" -Force | Out-Null
Set-NavServerConfiguration -ServerInstance $ServerInstance -KeyName "Multitenant" -KeyValue "True" -WarningAction SilentlyContinue
Set-NavServerInstance -ServerInstance $ServerInstance -start
Write-Host "Copying tenant template to default tenant"
Copy-NavDatabase -SourceDatabaseName "tenant" -DestinationDatabaseName "default"
Write-Host "Mounting default tenant"
# -allowAppDatabaseWrite is required so demo-data generation (DemoTool) can insert records such
# as Media into the application database; without it the tenant mounts read-only against the app DB.
Mount-NavDatabase -ServerInstance $ServerInstance -TenantId "default" -DatabaseName "default" -allowAppDatabaseWrite
} -argumentList $CustomConfig.DatabaseName, $CustomConfig.DatabaseServer, $CustomConfig.DatabaseInstance
}

Export-ModuleMember -Function Reset-BcContainerApplicationDatabase
Loading