-
Notifications
You must be signed in to change notification settings - Fork 250
/
Copy pathPublish-NavContainerApp.ps1
415 lines (392 loc) · 24.1 KB
/
Publish-NavContainerApp.ps1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
<#
.Synopsis
Publish App to a NAV/BC Container
.Description
Copies the appFile to the container if necessary
Creates a session to the container and runs the CmdLet Publish-NavApp in the container
.Parameter containerName
Name of the container in which you want to publish an app
.Parameter appFile
Path of the app you want to publish
.Parameter skipVerification
Include this parameter if the app you want to publish is not signed
.Parameter ignoreIfAppExists
Include this parameter if you want to ignore the error if the app already is published/installed
.Parameter sync
Include this parameter if you want to synchronize the app after publishing
.Parameter syncMode
Specify Add, Clean or Development based on how you want to synchronize the database schema. Default is Add
.Parameter install
Include this parameter if you want to install the app after publishing
.Parameter upgrade
Include this parameter if you want to upgrade the app after publishing. if no upgrade is necessary then its just installed instead.
.Parameter tenant
If you specify the install switch, then you can specify the tenant in which you want to install the app
.Parameter packageType
Specify Extension or SymbolsOnly based on which package you want to publish
.Parameter scope
Specify Global or Tenant based on how you want to publish the package. Default is Global
.Parameter useDevEndpoint
Specify the useDevEndpoint switch if you want to publish using the Dev Endpoint (like VS Code). This allows VS Code to re-publish.
.Parameter credential
Specify the credentials for the admin user if you use DevEndpoint and authentication is set to UserPassword
.Parameter language
Specify language version that is used for installing the app. The value must be a valid culture name for a language in Business Central, such as en-US or da-DK. If the specified language does not exist on the Business Central Server instance, then en-US is used.
.Parameter includeOnlyAppIds
Array of AppIds. If specified, then include Only Apps in the specified AppFile array or archive which is contained in this Array and their dependencies
.Parameter excludeRuntimePackages
If specified, then runtime packages will be excluded
.Parameter copyInstalledAppsToFolder
If specified, the installed apps will be copied to this folder in addition to being installed in the container
.Parameter replaceDependencies
With this parameter, you can specify a hashtable, describring that the specified dependencies in the apps being published should be replaced
.Parameter internalsVisibleTo
An Array of hashtable, containing id, name and publisher of an app, which should be added to internals Visible to
.Parameter showMyCode
With this parameter you can change or check ShowMyCode in the app file. Check will throw an error if ShowMyCode is False.
.Parameter PublisherAzureActiveDirectoryTenantId
AAD Tenant of the publisher to ensure access to keyvault (unless publisher check is disables in server config)
.Parameter bcAuthContext
Authorization Context created by New-BcAuthContext. By specifying BcAuthContext and environment, the function will publish the app to the online Business Central Environment specified
.Parameter environment
Environment to use for publishing
.Example
Publish-BcContainerApp -appFile c:\temp\myapp.app
.Example
Publish-BcContainerApp -containerName test2 -appFile c:\temp\myapp.app -skipVerification
.Example
Publish-BcContainerApp -containerName test2 -appFile c:\temp\myapp.app -install -sync
.Example
Publish-BcContainerApp -containerName test2 -appFile c:\temp\myapp.app -skipVerification -install -sync -tenant mytenant
.Example
Publish-BcContainerApp -containerName test2 -appFile c:\temp\myapp.app -install -sync -replaceDependencies @{ "437dbf0e-84ff-417a-965d-ed2bb9650972" = @{ "id" = "88b7902e-1655-4e7b-812e-ee9f0667b01b"; "name" = "MyBaseApp"; "publisher" = "Freddy Kristiansen"; "minversion" = "1.0.0.0" }}
#>
function Publish-BcContainerApp {
Param (
[string] $containerName = "",
[Parameter(Mandatory=$true)]
$appFile,
[switch] $skipVerification,
[switch] $ignoreIfAppExists,
[switch] $sync,
[Parameter(Mandatory=$false)]
[ValidateSet('Add','Clean','Development','ForceSync')]
[string] $syncMode,
[switch] $install,
[switch] $upgrade,
[Parameter(Mandatory=$false)]
[string] $tenant = "default",
[ValidateSet('Extension','SymbolsOnly')]
[string] $packageType = 'Extension',
[Parameter(Mandatory=$false)]
[ValidateSet('Global','Tenant')]
[string] $scope,
[switch] $useDevEndpoint,
[pscredential] $credential,
[string] $language = "",
[string[]] $includeOnlyAppIds = @(),
[string] $copyInstalledAppsToFolder = "",
[hashtable] $replaceDependencies = $null,
[hashtable[]] $internalsVisibleTo = $null,
[ValidateSet('Ignore','True','False','Check')]
[string] $ShowMyCode = "Ignore",
[switch] $replacePackageId,
[string] $PublisherAzureActiveDirectoryTenantId,
[Hashtable] $bcAuthContext,
[string] $environment,
[switch] $checkAlreadyInstalled,
[ValidateSet('default','ignore','strict')]
[string] $dependencyPublishingOption = "default",
[switch] $excludeRuntimePackages
)
$telemetryScope = InitTelemetryScope -name $MyInvocation.InvocationName -parameterValues $PSBoundParameters -includeParameters @()
try {
Add-Type -AssemblyName System.Net.Http
if ($containerName -eq "" -and (!($bcAuthContext -and $environment))) {
$containerName = $bcContainerHelperConfig.defaultContainerName
}
$installedApps = @()
if ($containerName) {
$customconfig = Get-BcContainerServerConfiguration -ContainerName $containerName
$appFolder = Join-Path $bcContainerHelperConfig.hostHelperFolder "Extensions\$containerName\$([guid]::NewGuid().ToString())"
if ($appFile -is [string] -and $appFile.Startswith(':')) {
New-Item $appFolder -ItemType Directory | Out-Null
$destFile = Join-Path $appFolder ([System.IO.Path]::GetFileName($appFile.SubString(1)).Replace('*','').Replace('?',''))
Invoke-ScriptInBcContainer -containerName $containerName -scriptblock { Param($appFile, $destFile)
Copy-Item -Path $appFile -Destination $destFile -Force
} -argumentList (Get-BcContainerPath -containerName $containerName -path $appFile), (Get-BcContainerPath -containerName $containerName -path $destFile) | Out-Null
$appFiles = @($destFile)
}
else {
$appFiles = CopyAppFilesToFolder -appFiles $appFile -folder $appFolder
}
$navversion = Get-BcContainerNavversion -containerOrImageName $containerName
$version = [System.Version]($navversion.split('-')[0])
$force = ($version.Major -ge 14)
if ($checkAlreadyInstalled) {
# Get Installed apps (if UseDevEndpoint is specified, only get global apps)
$installedApps = Get-BcContainerAppInfo -containerName $containerName -installedOnly | Where-Object { (-not $useDevEndpoint.IsPresent) -or ($_.Scope -eq 'Global') } | ForEach-Object {
@{ "id" = "$($_.appId)"; "publisher" = $_.publisher; "name" = $_.name; "version" = $_.Version }
}
}
}
else {
$appFolder = Join-Path ([System.IO.Path]::GetTempPath()) ([guid]::NewGuid().ToString())
$appFiles = CopyAppFilesToFolder -appFiles $appFile -folder $appFolder
$force = $true
if ($checkAlreadyInstalled) {
# Get Installed apps (if UseDevEndpoint is specified, only get global apps or PTEs)
# PublishedAs is either "Global", " PTE" or " Dev" (with leading space)
$installedApps = Get-BcInstalledExtensions -bcAuthContext $bcAuthContext -environment $environment
Write-Host "InstalledApps:"
$installedApps | ForEach-Object { Write-Host "- $($_.id) $($_.displayName) $($_.VersionMajor).$($_.VersionMinor).$($_.VersionBuild).$($_.VersionRevision) '$($_.PublishedAs)' $($_.IsInstalled) $($_.publisher)" }
$installedApps = $installedApps | Where-Object { $_.IsInstalled -and ((-not $useDevEndpoint.IsPresent) -or ($_.PublishedAs -ne ' Dev')) } | ForEach-Object {
@{ "id" = $_.id; "publisher" = $_.publisher; "name" = $_.displayName; "version" = [System.Version]::new($_.VersionMajor,$_.VersionMinor,$_.VersionBuild,$_.VersionRevision) }
}
}
}
try {
$appFiles = @(Sort-AppFilesByDependencies -containerName $containerName -appFiles $appFiles -includeOnlyAppIds $includeOnlyAppIds -excludeInstalledApps $installedApps -excludeRuntimePackages:$excludeRuntimePackages -WarningAction SilentlyContinue)
$appFiles | Where-Object { $_ } | ForEach-Object {
$appFile = $_
if ($ShowMyCode -ne "Ignore" -or $replaceDependencies -or $replacePackageId -or $internalsVisibleTo) {
Write-Host "Checking dependencies in $appFile"
Replace-DependenciesInAppFile -Path $appFile -replaceDependencies $replaceDependencies -internalsVisibleTo $internalsVisibleTo -ShowMyCode $ShowMyCode -replacePackageId:$replacePackageId
}
if ($copyInstalledAppsToFolder) {
if (!(Test-Path -Path $copyInstalledAppsToFolder)) {
New-Item -Path $copyInstalledAppsToFolder -ItemType Directory | Out-Null
}
Write-Host "Copy $appFile to $copyInstalledAppsToFolder"
Copy-Item -Path $appFile -Destination $copyInstalledAppsToFolder -force
}
if ($bcAuthContext -and $environment) {
$useDevEndpoint = $true
}
elseif ($customconfig.ServerInstance -eq "") {
throw "You cannot publish an app to a filesOnly container. Specify bcAuthContext and environemnt to publish to an online tenant"
}
if ($useDevEndpoint) {
if ($scope -eq "Global") {
throw "You cannot publish to global scope using the dev. endpoint"
}
$sslVerificationDisabled = $false
if ($bcAuthContext -and $environment) {
$bcAuthContext = Renew-BcAuthContext -bcAuthContext $bcAuthContext
$devServerUrl = "$($bcContainerHelperConfig.apiBaseUrl.TrimEnd('/'))/v2.0/$environment"
$tenant = ""
$handler = New-Object System.Net.Http.HttpClientHandler
$HttpClient = [System.Net.Http.HttpClient]::new($handler)
$HttpClient.DefaultRequestHeaders.Authorization = New-Object System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", $bcAuthContext.AccessToken)
$HttpClient.Timeout = [System.Threading.Timeout]::InfiniteTimeSpan
$HttpClient.DefaultRequestHeaders.ExpectContinue = $false
}
else {
$handler = New-Object System.Net.Http.HttpClientHandler
if ($customConfig.DeveloperServicesSSLEnabled -eq "true") {
$protocol = "https://"
}
else {
$protocol = "http://"
}
$sslVerificationDisabled = ($protocol -eq "https://")
if ($sslVerificationDisabled) {
Write-Host "Disabling SSL Verification on HttpClient"
[SslVerification]::DisableSsl($handler)
}
if ($customConfig.ClientServicesCredentialType -eq "Windows") {
$handler.UseDefaultCredentials = $true
}
$HttpClient = [System.Net.Http.HttpClient]::new($handler)
if ($customConfig.ClientServicesCredentialType -eq "NavUserPassword") {
if (!($credential)) {
throw "You need to specify credentials when you are not using Windows Authentication"
}
$pair = ("$($Credential.UserName):"+[System.Runtime.InteropServices.Marshal]::PtrToStringBSTR([System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($credential.Password)))
$bytes = [System.Text.Encoding]::ASCII.GetBytes($pair)
$base64 = [System.Convert]::ToBase64String($bytes)
$HttpClient.DefaultRequestHeaders.Authorization = New-Object System.Net.Http.Headers.AuthenticationHeaderValue("Basic", $base64);
}
$HttpClient.Timeout = [System.Threading.Timeout]::InfiniteTimeSpan
$HttpClient.DefaultRequestHeaders.ExpectContinue = $false
$ip = Get-BcContainerIpAddress -containerName $containerName
if ($ip) {
$devServerUrl = "$($protocol)$($ip):$($customConfig.DeveloperServicesPort)/$($customConfig.ServerInstance)"
}
else {
$devServerUrl = "$($protocol)$($containerName):$($customConfig.DeveloperServicesPort)/$($customConfig.ServerInstance)"
}
}
$schemaUpdateMode = "synchronize"
if ($syncMode -eq "Clean") {
$schemaUpdateMode = "recreate";
}
elseif ($syncMode -eq "ForceSync") {
$schemaUpdateMode = "forcesync"
}
$url = "$devServerUrl/dev/apps?SchemaUpdateMode=$schemaUpdateMode"
if ($PSBoundParameters.ContainsKey('dependencyPublishingOption')) {
$url += "&DependencyPublishingOption=$dependencyPublishingOption"
}
if ($tenant) {
$url += "&tenant=$tenant"
}
$appName = [System.IO.Path]::GetFileName($appFile)
$multipartContent = [System.Net.Http.MultipartFormDataContent]::new()
$FileStream = [System.IO.FileStream]::new($appFile, [System.IO.FileMode]::Open)
try {
$fileHeader = [System.Net.Http.Headers.ContentDispositionHeaderValue]::new("form-data")
$fileHeader.Name = "$AppName"
$fileHeader.FileName = "$appName"
$fileHeader.FileNameStar = "$appName"
$fileContent = [System.Net.Http.StreamContent]::new($FileStream)
$fileContent.Headers.ContentDisposition = $fileHeader
$multipartContent.Add($fileContent)
Write-Host "Publishing $appName to $url"
$result = $HttpClient.PostAsync($url, $multipartContent).GetAwaiter().GetResult()
if (!$result.IsSuccessStatusCode) {
$message = "Status Code $($result.StatusCode) : $($result.ReasonPhrase)"
try {
$resultMsg = $result.Content.ReadAsStringAsync().Result
try {
$json = $resultMsg | ConvertFrom-Json
$message += "`n$($json.Message)"
}
catch {
$message += "`n$resultMsg"
}
}
catch {}
throw $message
}
}
catch {
GetExtendedErrorMessage -errorRecord $_ | Out-Host
throw
}
finally {
$FileStream.Close()
}
if ($bcContainerHelperConfig.NoOfSecondsToSleepAfterPublishBcContainerApp -gt 0) {
# Avoid race condition
Start-Sleep -Seconds $bcContainerHelperConfig.NoOfSecondsToSleepAfterPublishBcContainerApp
}
}
else {
[ScriptBlock] $scriptblock = { Param($appFile, $skipVerification, $sync, $install, $upgrade, $tenant, $syncMode, $packageType, $scope, $language, $PublisherAzureActiveDirectoryTenantId, $force, $ignoreIfAppExists, $version)
$prevPreference = $ProgressPreference; $ProgressPreference = "SilentlyContinue"
$publishArgs = @{ "packageType" = $packageType }
if ($scope) {
$publishArgs += @{ "Scope" = $scope }
if ($scope -eq "Tenant") {
$publishArgs += @{ "Tenant" = $tenant }
}
}
if ($PublisherAzureActiveDirectoryTenantId) {
$publishArgs += @{ "PublisherAzureActiveDirectoryTenantId" = $PublisherAzureActiveDirectoryTenantId }
}
if ($force) {
$publishArgs += @{ "Force" = $true }
}
$publishIt = $true
if ($ignoreIfAppExists) {
$navAppInfo = Get-NAVAppInfo -Path $appFile
$addArg = @{
"tenantSpecificProperties" = $true
"tenant" = $tenant
}
if ($packageType -eq "SymbolsOnly") {
$addArg = @{ "SymbolsOnly" = $true }
}
$appInfo = (Get-NAVAppInfo -ServerInstance $serverInstance -Name $navAppInfo.Name -Publisher $navAppInfo.Publisher @addArg) | Where-Object { $_.Version -ge $navAppInfo.Version } | Sort-Object { $_.Version } | Select-Object -Last 1
if ($appInfo) {
$publishIt = $false
Write-Host "$($navAppInfo.Name) version $($appInfo.Version) is already published"
if ($appInfo.IsInstalled) {
$sync = ($appinfo.SyncState -ne "Synced")
$install = $false
$upgrade = $false
Write-Host "$($navAppInfo.Name) version $($appInfo.Version) is already installed"
}
}
}
if ($publishIt) {
Write-Host "Publishing $appFile"
$publishArgs += @{"serverInstance" = $serverInstance; "Path" = $appFile; "SkipVerification" = $skipVerification.IsPresent}
if ($PSVersionTable.PSVersion.Major -lt 7 -and $version.Major -ge 24) {
# GIANT HACK AHEAD...
# To cope with this issue https://github.com/microsoft/navcontainerhelper/issues/3591 where the PS5 bridge to PS7 hangs when calling Publish-NavApp directly
# and due to https://github.com/microsoft/navcontainerhelper/issues/3575 and https://github.com/PowerShell/PowerShell/issues/23982 we cannot use -usepwsh:$true
$command = '$serviceTierFolder = (Get-Item ''C:\Program Files\Microsoft Dynamics NAV\*\Service'').FullName;Import-Module (Join-Path $serviceTierFolder ''Admin\Microsoft.Dynamics.Nav.Management.psm1'');Import-Module (Join-Path $serviceTierFolder ''Admin\Microsoft.BusinessCentral.Management.psd1'');Import-Module (Join-Path $serviceTierFolder ''Admin\Microsoft.BusinessCentral.Apps.Management.dll'');'
$command += "Publish-NavApp $(($publishArgs.Keys | ForEach-Object { $v=$publishArgs."$_"; if($v -is [boolean]){"-$($_):`$$($publishArgs."$_")"}elseif($v -is [string]){"-$($_):'$($publishArgs."$_")'"}else{"-$($_):$($publishArgs."$_")"} }) -join ' ')"
pwsh -command "$($command.Replace('"','""'))" | Out-Host
if ($LASTEXITCODE -ne 0) {
throw "Publish-NavApp failed"
}
}
else {
Publish-NavApp @publishArgs
}
}
if ($sync -or $install -or $upgrade) {
$navAppInfo = Get-NAVAppInfo -Path $appFile
$appPublisher = $navAppInfo.Publisher
$appName = $navAppInfo.Name
$appVersion = $navAppInfo.Version
$syncArgs = @{}
if ($syncMode) {
$syncArgs += @{ "Mode" = $syncMode }
}
if ($sync) {
Write-Host "Synchronizing $appName on tenant $tenant"
Sync-NavTenant -ServerInstance $ServerInstance -Tenant $tenant -Force
Sync-NavApp -ServerInstance $ServerInstance -Publisher $appPublisher -Name $appName -Version $appVersion -Tenant $tenant @syncArgs -force -WarningAction Ignore
}
if($upgrade -and $install){
$navAppInfoFromDb = Get-NAVAppInfo -ServerInstance $ServerInstance -Publisher $appPublisher -Name $appName -Version $appVersion -Tenant $tenant -TenantSpecificProperties
if($null -eq $navAppInfoFromDb.ExtensionDataVersion -or $navAppInfoFromDb.ExtensionDataVersion -eq $navAppInfoFromDb.Version){
$upgrade = $false
} else {
$install = $false
}
}
$installArgs = @{}
if ($language) {
$installArgs += @{ "Language" = $language }
}
if ($force) {
$installArgs += @{ "Force" = $true }
}
if ($install) {
Write-Host "Installing $appName on tenant $tenant"
Install-NavApp -ServerInstance $ServerInstance -Publisher $appPublisher -Name $appName -Version $appVersion -Tenant $tenant @installArgs
}
if ($upgrade) {
Write-Host "Upgrading $appName on tenant $tenant"
Start-NavAppDataUpgrade -ServerInstance $ServerInstance -Publisher $appPublisher -Name $appName -Version $appVersion -Tenant $tenant @installArgs
}
}
$ProgressPreference = $prevPreference
}
Invoke-ScriptInBcContainer `
-containerName $containerName `
-ScriptBlock $scriptblock `
-ArgumentList (Get-BcContainerPath -containerName $containerName -path $appFile), $skipVerification, $sync, $install, $upgrade, $tenant, $syncMode, $packageType, $scope, $language, $PublisherAzureActiveDirectoryTenantId, $force, $ignoreIfAppExists, $version
}
Write-Host -ForegroundColor Green "App $([System.IO.Path]::GetFileName($appFile)) successfully published"
}
}
finally {
Remove-Item $appFolder -Recurse -Force -ErrorAction SilentlyContinue
}
}
catch {
TrackException -telemetryScope $telemetryScope -errorRecord $_
throw
}
finally {
TrackTrace -telemetryScope $telemetryScope
}
}
Set-Alias -Name Publish-NavContainerApp -Value Publish-BcContainerApp
Export-ModuleMember -Function Publish-BcContainerApp -Alias Publish-NavContainerApp