├── .gitignore ├── .vscode └── launch.json ├── LICENSE ├── Modules └── PBIDevOps │ └── PBIDevOps.psm1 ├── README.md ├── SampleProject ├── DataSets │ └── WWI - Sales.pbix ├── PaginatedReports │ └── PaginatedReport.rdl └── Reports │ ├── Customer.pbix │ ├── Purchases.pbix │ └── Sales.pbix ├── config-dev.json ├── config-prd.json ├── deploy-run-DEV.cmd ├── deploy-run-PRD.cmd ├── deploy.ps1 └── tool.FixReportConnections.ps1 /.gitignore: -------------------------------------------------------------------------------- 1 | /_temp 2 | 3 | config.credentials.json 4 | 5 | shareddatasets.json 6 | /config.credentials - DEMO.json 7 | /ReportWithInvalidDataset.pbix 8 | 9 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [] 7 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Rui Romano 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Modules/PBIDevOps/PBIDevOps.psm1: -------------------------------------------------------------------------------- 1 | #Requires -Modules @{ ModuleName="MicrosoftPowerBIMgmt"; ModuleVersion="1.2.1026" } -Assembly System.IO.Compression 2 | 3 | function Get-EnvironmentMetadata 4 | { 5 | [CmdletBinding()] 6 | param 7 | ( 8 | [string]$configPath 9 | ) 10 | 11 | if (!(Test-Path -Path $configPath)) 12 | { 13 | throw "Cannot find metadata file '$configPath'" 14 | } 15 | 16 | Write-Host "##[command]Reading metadata from file: '$configPath'" 17 | 18 | $metadata = Get-Content -Path $configPath | ConvertFrom-Json 19 | 20 | return $metadata 21 | } 22 | 23 | function Get-WorkspaceMetadata 24 | { 25 | [CmdletBinding()] 26 | param 27 | ( 28 | $environmentMetadata, 29 | $name 30 | ) 31 | 32 | $metadata = $environmentMetadata.Workspaces.psobject.Properties |? { $_.Name -eq $name } | Select -First 1 33 | 34 | if (!$metadata) 35 | { 36 | throw "Cannot find configuration for workspace '$name'" 37 | } 38 | 39 | if ([string]::IsNullOrEmpty($metadata.Value.WorkspaceId)) 40 | { 41 | $pbiWorkspaceName = $metadata.Value.WorkspaceName 42 | 43 | if ([string]::IsNullOrEmpty($pbiWorkspaceName)) 44 | { 45 | $pbiWorkspaceName = $metadata.Name 46 | } 47 | 48 | #$pbiWorkspace = Get-PBIWorkspace -authToken $authToken -name $pbiWorkspaceName 49 | $pbiWorkspace = Get-PowerBIWorkspace -Name $pbiWorkspaceName 50 | 51 | if ($pbiWorkspace) 52 | { 53 | $metadata.Value | Add-Member -NotePropertyName WorkspaceId -NotePropertyValue $pbiWorkspace.id 54 | $metadata.Value | Add-Member -NotePropertyName CapacityId -NotePropertyValue $pbiWorkspace.capacityId 55 | } 56 | else 57 | { 58 | throw "Cannot find Power BI Workspace with name '$pbiWorkspaceName'" 59 | } 60 | } 61 | 62 | 63 | Write-Output $metadata.Value 64 | } 65 | 66 | function Get-DataSetMetadata 67 | { 68 | [CmdletBinding()] 69 | param 70 | ( 71 | $environmentMetadata, 72 | $name, 73 | [switch]$getWorkspaceName 74 | ) 75 | 76 | $metadata = @($environmentMetadata.DataSets.psobject.Properties |? { $_.Name -eq $name } |% { $_.Value }) 77 | 78 | if (!$metadata) 79 | { 80 | $metadata = $environmentMetadata.DataSets.Default 81 | 82 | if (!$metadata) 83 | { 84 | Write-Host "##[error] Cannot find configuration for dataset '$name'" 85 | 86 | return 87 | } 88 | } 89 | 90 | # Solve other metadata fields if needed 91 | $metadata |% { 92 | 93 | $item = $_ 94 | 95 | if ([string]::IsNullOrEmpty($item.WorkspaceId)) 96 | { 97 | if (![string]::IsNullOrEmpty($item.Workspace)) 98 | { 99 | $workspaceMetadata = Get-WorkspaceMetadata -environmentMetadata $environmentMetadata -name $item.Workspace 100 | 101 | $workspaceId = $workspaceMetadata.WorkspaceId 102 | } 103 | elseif (![string]::IsNullOrEmpty($item.WorkspaceName)) 104 | { 105 | #$pbiWorkspace = Get-PBIWorkspace -authToken $authToken -name $item.WorkspaceName 106 | $pbiWorkspace = Get-PowerBIWorkspace -name $item.WorkspaceName 107 | 108 | if (!$pbiWorkspace) 109 | { 110 | throw "Cannot find Power BI Workspace with name '$($item.WorkspaceName)'" 111 | } 112 | 113 | $workspaceId = $pbiWorkspace.id 114 | } 115 | 116 | $item | Add-Member -NotePropertyName WorkspaceId -NotePropertyValue $workspaceId 117 | } 118 | 119 | if ($getWorkspaceName -and [string]::IsNullOrEmpty($item.WorkspaceName)) 120 | { 121 | #$pbiWorkspace = Get-PBIWorkspace -authToken $authToken -id $item.WorkspaceId 122 | $pbiWorkspace = Get-PowerBIWorkspace -id $item.WorkspaceId 123 | 124 | $item | Add-Member -NotePropertyName WorkspaceName -NotePropertyValue $pbiWorkspace.name 125 | } 126 | 127 | $pbiDataSetName = $item.DataSetName 128 | 129 | if ([string]::IsNullOrEmpty($pbiDataSetName)) 130 | { 131 | $pbiDataSetName = [System.IO.Path]::GetFileNameWithoutExtension($name) 132 | 133 | $item | Add-Member -NotePropertyName DataSetName -NotePropertyValue $pbiDataSetName 134 | } 135 | 136 | if ([string]::IsNullOrEmpty($item.DataSetId) -and [string]::IsNullOrEmpty($item.Server)) 137 | { 138 | #$pbiDataSet = Get-PBIDataSet -authToken $authToken -groupId $item.WorkspaceId -name $pbiDataSetName 139 | $pbiDataSet = Get-PowerBIDataset -WorkspaceId $item.WorkspaceId -name $pbiDataSetName 140 | 141 | if ($pbiDataSet) 142 | { 143 | $item | Add-Member -NotePropertyName DataSetId -NotePropertyValue $pbiDataSet.id 144 | } 145 | } 146 | 147 | Write-Output $item 148 | } 149 | 150 | } 151 | 152 | function Get-ReportMetadata 153 | { 154 | [CmdletBinding()] 155 | param 156 | ( 157 | $environmentMetadata, 158 | $filePath 159 | ) 160 | 161 | $metadata = @($environmentMetadata.Reports.psobject.Properties |? { $filePath -like "*$($_.Name)*" } |% { $_.Value }) 162 | 163 | if (!$metadata) 164 | { 165 | Write-Host "##[error] Cannot find configuration for report '$filePath'" 166 | 167 | return 168 | } 169 | 170 | # Solve other metadata fields if needed 171 | 172 | $metadata |% { 173 | 174 | $item = $_ 175 | 176 | $reportName = $item.ReportName 177 | 178 | if (!$reportName) 179 | { 180 | $reportName = [System.IO.Path]::GetFileNameWithoutExtension($filePath) 181 | } 182 | 183 | $item | Add-Member -NotePropertyName ReportName -NotePropertyValue $reportName -Force 184 | 185 | $reportType = "PowerBI" 186 | 187 | if ([System.IO.Path]::GetExtension($filePath) -ieq ".rdl") 188 | { 189 | $reportType = "PaginatedReport" 190 | } 191 | 192 | $item | Add-Member -NotePropertyName ReportType -NotePropertyValue $reportType -Force 193 | 194 | if ([string]::IsNullOrEmpty($item.WorkspaceId)) 195 | { 196 | if (![string]::IsNullOrEmpty($item.Workspace)) 197 | { 198 | $workspaceMetadata = Get-WorkspaceMetadata -environmentMetadata $environmentMetadata -name $item.Workspace 199 | 200 | $workspaceId = $workspaceMetadata.WorkspaceId 201 | } 202 | elseif (![string]::IsNullOrEmpty($item.WorkspaceName)) 203 | { 204 | $pbiWorkspace = Get-PowerBIWorkspace -name $item.WorkspaceName 205 | 206 | if (!$pbiWorkspace) 207 | { 208 | throw "Cannot find Power BI Workspace with name '$($item.WorkspaceName)'" 209 | } 210 | 211 | $workspaceId = $pbiWorkspace.id 212 | } 213 | 214 | $item | Add-Member -NotePropertyName WorkspaceId -NotePropertyValue $workspaceId 215 | 216 | $item | Add-Member -NotePropertyName WorkspaceMetadata -NotePropertyValue $workspaceMetadata 217 | } 218 | 219 | if ([string]::IsNullOrEmpty($item.DataSetId)) 220 | { 221 | $dataSetMetadata = Get-DataSetMetadata -environmentMetadata $environmentMetadata -name $item.DataSet 222 | 223 | if (!$dataSetMetadata) 224 | { 225 | throw "Cannot find DataSet configuration '$($item.DataSet)'" 226 | } 227 | 228 | $item | Add-Member -NotePropertyName DataSetId -NotePropertyValue $dataSetMetadata.DataSetId 229 | } 230 | 231 | Write-Output $item 232 | } 233 | } 234 | 235 | function Publish-PBIDataSets 236 | { 237 | [CmdletBinding()] 238 | param( 239 | $path 240 | , 241 | $configPath = "" 242 | , 243 | $deleteDataSetReport = $true 244 | , 245 | $filter = @() 246 | ) 247 | 248 | $rootPath = $PSScriptRoot 249 | 250 | if ([string]::IsNullOrEmpty($path)) 251 | { 252 | $path = "$rootPath\DataSets" 253 | } 254 | 255 | if ([string]::IsNullOrEmpty($configPath)) 256 | { 257 | $configPath = "$rootPath\config.json" 258 | } 259 | 260 | Write-Host "##[debug] Publish-PBIDataSets" 261 | 262 | $paramtersStr = ($MyInvocation.MyCommand.Parameters.GetEnumerator() |% {$_.Key + "='$((Get-Variable -Name $_.Key -EA SilentlyContinue).Value)'"}) -join ";" 263 | 264 | Write-Host "##[debug]Parameters: $paramtersStr" 265 | 266 | $datasets = Get-ChildItem -File -Path "$path\*.pbix" -Recurse -ErrorAction SilentlyContinue 267 | 268 | if ($filter -and $filter.Count -gt 0) 269 | { 270 | $datasets = $datasets |? { $filter -contains $_.Name } 271 | } 272 | 273 | if ($datasets.Count -eq 0) 274 | { 275 | Write-Host "##[warning] No models to deploy on path '$path'" 276 | return 277 | } 278 | 279 | $environmentMetadata = Get-EnvironmentMetadata $configPath 280 | 281 | $datasets |% { 282 | 283 | $modelFile = $_ 284 | 285 | $filePath = $modelFile.FullName 286 | $fileName = $modelFile.Name 287 | $defaultDatasetName = [System.IO.Path]::GetFileNameWithoutExtension($fileName) 288 | 289 | $datasetMetadata = @(Get-DataSetMetadata -environmentMetadata $environmentMetadata -name $fileName) 290 | 291 | if (!$datasetMetadata) 292 | { 293 | throw "Cannot find DataSet configuration '$fileName'" 294 | } 295 | 296 | Write-Host "##[command] Deploying to $($datasetMetadata.Count) locations" 297 | 298 | $datasetMetadata |% { 299 | 300 | $targetServer = $_ 301 | 302 | $workspaceId = $_.WorkspaceId 303 | 304 | if (![string]::IsNullOrEmpty($targetServer.DataSetName)) 305 | { 306 | $datasetName = $targetServer.DataSetName 307 | } 308 | else 309 | { 310 | $datasetName = $defaultDatasetName 311 | } 312 | 313 | Write-Host "##[group]Deploying Model: '$filePath' to Workspace '$workspaceId'" 314 | 315 | Write-Host "##[debug]Publishing '$fileName' to '$datasetName'" 316 | 317 | #$import = Import-PBIFile -authToken $authToken -file $filePath -dataSetName $datasetName -nameConflict CreateOrOverwrite -groupId $workspaceId -wait 318 | 319 | $newDataSet = New-PowerBIReport -Path $filePath -Name $datasetName -ConflictAction CreateOrOverwrite -WorkspaceId $workspaceId 320 | 321 | #$newDataSet = Get-PBIDataSet -authToken $authToken -name $datasetName -groupId $workspaceId 322 | $newDataSet = Get-PowerBIDataset -WorkspaceId $workspaceId -Name $datasetName 323 | 324 | if (!$newDataSet) 325 | { 326 | throw "Error publishing dataset" 327 | } 328 | 329 | # Delete the published report of the dataset 330 | 331 | if ($deleteDataSetReport) 332 | { 333 | #$datasetReports = @(Get-PBIReport -authToken $authToken -groupId $workspaceId |? { $_.datasetId -eq $newDataSet.id -and $_.name -eq $datasetName}) 334 | $datasetReports = @(Get-PowerBIReport -WorkspaceId $workspaceId |? { $_.datasetId -eq $newDataSet.id -and $_.name -eq $datasetName}) 335 | 336 | if ($datasetReports.Count -gt 1) 337 | { 338 | Write-Host "##[warning]There is more than 1 report using the published dataset" 339 | } 340 | elseif ($datasetReports.Count -eq 1) 341 | { 342 | Write-Host "##[command]Deleting the PBIX Report that comes with the PBIX" 343 | 344 | #$datasetReports | Remove-PBIReport -authToken $authToken 345 | 346 | Invoke-PowerBIRestMethod -Url "groups/$workspaceId/reports/$($datasetReports[0].id)" -Method Delete | Out-Null 347 | } 348 | } 349 | 350 | if ($targetServer.Parameters) 351 | { 352 | $dsParams = $targetServer.Parameters.psobject.properties | foreach -begin {$h=@{}} -process {$h."$($_.Name)" = $_.Value } -end {$h} 353 | 354 | if ($dsParams.Count -ne 0) 355 | { 356 | Write-Host "##[debug]Take Ownership of DataSet" 357 | 358 | #Invoke-PBIRequest -authToken $authToken -resource "datasets/$($newDataSet.id)/Default.TakeOver" -groupId $workspaceId -method Post 359 | 360 | Invoke-PowerBIRestMethod -Url "groups/$workspaceId/datasets/$($newDataSet.id)/Default.TakeOver" -Method Post | Out-Null 361 | 362 | Write-Host "##[debug]Setting DataSet parameters" 363 | 364 | #$newDataSet | Set-PBIDatasetParameters -authToken $authToken -groupId $workspaceId -parameters $dsParams 365 | 366 | $updateDetails = @() 367 | 368 | @($dsParams.Keys) |% { 369 | $updateDetails += @{ 370 | "name" = $_ 371 | ; 372 | "newValue" = $dsParams[$_] 373 | } 374 | } 375 | 376 | $bodyObj = @{updateDetails=$updateDetails} 377 | 378 | $bodyStr = $bodyObj | ConvertTo-Json 379 | 380 | Invoke-PowerBIRestMethod -Url "groups/$workspaceId/datasets/$($newDataSet.id)/UpdateParameters" -Body $bodyStr -Method Post | Out-Null 381 | } 382 | } 383 | 384 | Write-Host "##[endgroup]" 385 | } 386 | } 387 | } 388 | 389 | function Publish-PBIReports 390 | { 391 | [CmdletBinding()] 392 | param( 393 | $path 394 | , 395 | $configPath = "" 396 | , 397 | $filter = @() 398 | ) 399 | 400 | $rootPath = $PSScriptRoot 401 | 402 | if ([string]::IsNullOrEmpty($path)) 403 | { 404 | $path = "$rootPath\Reports" 405 | } 406 | 407 | if ([string]::IsNullOrEmpty($configPath)) 408 | { 409 | $configPath = "$rootPath\config.json" 410 | } 411 | 412 | $tempPath = Join-Path ([System.IO.Path]::GetTempPath()) "PublishPBIReports_$((New-Guid).ToString("N"))" 413 | 414 | New-Item -ItemType Directory -Path $tempPath -Force -ErrorAction SilentlyContinue | Out-Null 415 | 416 | Write-Host "##[debug] Publish-PBIReports" 417 | 418 | $paramtersStr = ($MyInvocation.MyCommand.Parameters.GetEnumerator() |% {$_.Key + "='$((Get-Variable -Name $_.Key -EA SilentlyContinue).Value)'"}) -join ";" 419 | 420 | Write-Host "##[debug]Parameters: $paramtersStr" 421 | 422 | $reports = Get-ChildItem -File -Path $path -Include @("*.pbix", "*.rdl") -Recurse -ErrorAction SilentlyContinue 423 | 424 | if ($filter -and $filter.Count -gt 0) 425 | { 426 | $reports = $reports |? { $filter -contains $_.Name } 427 | } 428 | 429 | if ($reports.Count -eq 0) 430 | { 431 | Write-Host "##[warning] No reports to deploy on path '$path'" 432 | return 433 | } 434 | 435 | $environmentMetadata = Get-EnvironmentMetadata $configPath 436 | 437 | Write-Host "##[command]Deploying '$($reports.Count)' reports" 438 | 439 | $reports |% { 440 | 441 | $pbixFile = $_ 442 | 443 | Write-Host "##[group]Deploying report: '$($pbixFile.Name)'" 444 | 445 | $filePath = $pbixFile.FullName 446 | 447 | $reportsMetadata = @(Get-ReportMetadata -environmentMetadata $environmentMetadata -filePath $filePath) 448 | 449 | if (!$reportsMetadata) 450 | { 451 | throw "Cannot find Report configuration '$filePath'" 452 | } 453 | 454 | try 455 | { 456 | foreach($reportMetadata in $reportsMetadata) 457 | { 458 | $workspaceId = $reportMetadata.WorkspaceId 459 | $targetDatasetId = $reportMetadata.DataSetId 460 | $reportName = $reportMetadata.ReportName 461 | $reportType = $reportMetadata.ReportType 462 | 463 | if ([string]::IsNullOrEmpty($targetDatasetId)) 464 | { 465 | throw "Cannot solve target dataset id, make sure its deployed" 466 | } 467 | 468 | if ($reportType -ieq "PaginatedReport" -and $reportMetadata.WorkspaceMetadata.CapacityId -eq $null) 469 | { 470 | throw "Cannot deploy Paginated Reports to Non Premium Workspaces" 471 | } 472 | 473 | $reportNameForUpload = $reportName 474 | 475 | # PaginatedReport upload requires the 'datasetDisplayName' to end with "*.rdl" 476 | 477 | if ($reportType -ieq "PaginatedReport") 478 | { 479 | $reportNameForUpload += ".rdl" 480 | } 481 | 482 | if ($reportType -ieq "PaginatedReport") 483 | { 484 | Write-Host "##[command] Rebinding Paginated Report to dataset '$targetDatasetId' by changing the connectionstring on RDL file" 485 | 486 | $rdlXml = [xml](Get-Content $filePath) 487 | 488 | foreach($rdlDatasource in $rdlXml.Report.DataSources.DataSource) 489 | { 490 | if ($rdlDatasource.ConnectionProperties.DataProvider -ieq "PBIDATASET") 491 | { 492 | $connStringBuilder = New-Object System.Data.Common.DbConnectionStringBuilder 493 | #$connStringBuilder.ConnectionString = $rdlDatasource.ConnectionProperties.ConnectString 494 | $connStringBuilder.PSObject.Properties['ConnectionString'].Value = $rdlDatasource.ConnectionProperties.ConnectString 495 | 496 | $catalog = $connStringBuilder["Initial Catalog"] 497 | 498 | $newCatalog = "sobe_wowvirtualserver-$targetDatasetId" 499 | 500 | Write-Host "Rebinding datasource: '$($rdlDatasource.DataSourceID)' from '$catalog' to '$newCatalog'" 501 | 502 | $connStringBuilder["Initial Catalog"] = $newCatalog 503 | 504 | $rdlDatasource.ConnectionProperties.ConnectString = $connStringBuilder.ConnectionString 505 | } 506 | } 507 | 508 | $tempRDLFilePath = Join-Path $tempPath $pbixFile.Name 509 | 510 | $rdlXml.Save($tempRDLFilePath); 511 | 512 | $filePath = $tempRDLFilePath 513 | } 514 | 515 | Write-Host "##[command] Uploading report '$reportName' into workspace '$workspaceId' and binding to dataset '$targetDatasetId'" 516 | 517 | $targetReport = @(Get-PowerBIReport -WorkspaceId $workspaceId -Name $reportName) 518 | 519 | if ($targetReport.Count -eq 0) 520 | { 521 | Write-Host "##[command] Uploading new report to workspace '$workspaceId'" 522 | 523 | $importResult = New-PowerBIReport -Path $filePath -WorkspaceId $workspaceId -Name $reportNameForUpload -ConflictAction Abort 524 | 525 | $targetReportId = $importResult.Id 526 | } 527 | else 528 | { 529 | if ($targetReport.Count -gt 1) 530 | { 531 | throw "More than one report with name '$reportName'" 532 | } 533 | 534 | Write-Host "##[command] Report already exists on workspace '$workspaceId', uploading to temp report & updatereportcontent" 535 | 536 | $targetReport = $targetReport[0] 537 | 538 | $targetReportId = $targetReport.id 539 | 540 | if ($reportType -ieq "PaginatedReport") 541 | { 542 | Write-Host "##[command] Overwrite paginated report" 543 | 544 | $importResult = New-PowerBIReport -Path $filePath -WorkspaceId $workspaceId -Name $reportNameForUpload -ConflictAction Overwrite 545 | } 546 | else 547 | { 548 | # Upload a temp report and update the report content of the target report 549 | 550 | # README - This is required because of a "bug" of IMport API that always duplicate the report if the dataset is different (may be solved in the future) 551 | 552 | $tempReportName = "Temp_$([System.Guid]::NewGuid().ToString("N"))" 553 | 554 | Write-Host "##[command] Uploadind as a temp report '$tempReportName'" 555 | 556 | $importResult = New-PowerBIReport -Path $filePath -WorkspaceId $workspaceId -Name $tempReportName -ConflictAction Abort 557 | 558 | $tempReportId = $importResult.Id 559 | 560 | Write-Host "##[command] Updating report content" 561 | 562 | $updateContentResult = Invoke-PowerBIRestMethod -method Post -Url "groups/$workspaceId/reports/$targetReportId/UpdateReportContent" -Body (@{ 563 | sourceType = "ExistingReport" 564 | sourceReport = @{ 565 | sourceReportId = $tempReportId 566 | sourceWorkspaceId = $workspaceId 567 | } 568 | } | ConvertTo-Json) 569 | 570 | # Delete the temp report 571 | 572 | Write-Host "##[command] Deleting temp report '$tempReportId'" 573 | 574 | Invoke-PowerBIRestMethod -Method Delete -Url "groups/$workspaceId/reports/$tempReportId" | Out-Null 575 | } 576 | } 577 | 578 | if ($reportType -ieq "PaginatedReport") 579 | { 580 | # Rebinding the RDL locally, this way works even when the local rdl is targeting an invalid datasetid 581 | 582 | # Write-Host "##[command] Rebinding Paginated Report to dataset '$targetDatasetId'" 583 | 584 | # $paginatedReportDataSources = @(Invoke-PowerBIRestMethod -url "groups/$workspaceId/reports/$targetReportId/datasources" -Method Get | ConvertFrom-Json | Select -ExpandProperty value) 585 | 586 | # foreach($datasource in $paginatedReportDataSources) 587 | # { 588 | # if ($datasource.datasourceType -eq "AnalysisServices" -and $datasource.connectionDetails.server -ilike "pbiazure://*") 589 | # { 590 | # Write-Host "##[command] Changing RDL Datasource '$($datasource.name)'" 591 | 592 | # $bodyObj = @{ 593 | # updateDetails=@( 594 | # @{ 595 | # "datasourceName" = $datasource.name 596 | # ; 597 | # "connectionDetails" = @{ 598 | # "server" = $datasource.connectionDetails.server 599 | # ; 600 | # "database" = "sobe_wowvirtualserver-$targetDatasetId" 601 | # } 602 | # } 603 | # ) 604 | # } 605 | 606 | # $bodyStr = $bodyObj | ConvertTo-Json -Depth 5 607 | 608 | # Invoke-PowerBIRestMethod -url "groups/$workspaceId/reports/$targetReportId/Default.UpdateDatasources" -Method Post -Body $bodyStr 609 | # } 610 | 611 | # } 612 | } 613 | else 614 | { 615 | if ($targetReportId) 616 | { 617 | Write-Host "##[command] Rebinding to dataset '$targetDatasetId'" 618 | 619 | Invoke-PowerBIRestMethod -Method Post -Url "groups/$workspaceId/reports/$targetReportId/Rebind" -Body "{datasetId: '$targetDatasetId'}" | Out-Null 620 | } 621 | } 622 | } 623 | 624 | } 625 | catch 626 | { 627 | $ex = $_.Exception 628 | 629 | if ($_.ErrorDetails.Message -and $_.ErrorDetails.Message.Contains("PowerBIModelNotFoundException")) 630 | { 631 | Write-Error -Exception $ex -Message "PBIX is connecting to a nonexistent DataSet or user dont have permission" 632 | } 633 | else 634 | { 635 | throw 636 | } 637 | } 638 | 639 | Write-Host "##[endgroup]" 640 | } 641 | } 642 | 643 | 644 | function Publish-PBIWorkspaces 645 | { 646 | [CmdletBinding()] 647 | param( 648 | $configPath = "" 649 | , 650 | $defaultWorkspaceConfigPath = "" 651 | , 652 | $filter = @() 653 | ) 654 | 655 | $rootPath = $PSScriptRoot 656 | 657 | if ([string]::IsNullOrEmpty($configPath)) 658 | { 659 | $configPath = "$rootPath\config.json" 660 | } 661 | 662 | Write-Host "##[debug]Publish-PBIWorkspaces" 663 | 664 | $paramtersStr = ($MyInvocation.MyCommand.Parameters.GetEnumerator() |% {$_.Key + "='$((Get-Variable -Name $_.Key -EA SilentlyContinue).Value)'"}) -join ";" 665 | 666 | Write-Host "##[debug]Parameters: $paramtersStr" 667 | 668 | $environmentMetadata = Get-EnvironmentMetadata -configPath $configPath 669 | 670 | if (!$environmentMetadata) 671 | { 672 | Write-Host "##[warning]No configuration found" 673 | return 674 | } 675 | 676 | if (!$environmentMetadata.Workspaces) 677 | { 678 | Write-Host "##[debug]No workspaces configured" 679 | return 680 | } 681 | 682 | Write-Host "##[command]Getting Power BI Workspaces" 683 | 684 | $pbiWorkspaces = Get-PowerBIWorkspace -All 685 | 686 | $defaultWorkspaceMetadata = $environmentMetadata.Workspaces.Default 687 | 688 | $workspacesMetadata = @($environmentMetadata.Workspaces.psobject.Properties |? Name -ne "Default") 689 | 690 | $defaultWorkspaceConfig = $null 691 | 692 | if ($defaultWorkspaceConfigPath) 693 | { 694 | If (Test-Path $defaultWorkspaceConfigPath) 695 | { 696 | $defaultWorkspaceConfig = Get-Content -Path $defaultWorkspaceConfigPath | ConvertFrom-Json 697 | } 698 | else 699 | { 700 | Write-Host "##[warning]Cannot find default workspace configuration: '$defaultWorkspaceConfigPath'" 701 | } 702 | } 703 | 704 | $workspacesMetadata |% { 705 | 706 | $workspacePermissions = $_.Value.Permissions 707 | $capacityId = $_.Value.DedicatedCapacityId 708 | $capacityName = $_.Value.DedicatedCapacityName 709 | $workspaceName = $_.Value.WorkspaceName 710 | $workspaceDeployOptions = $_.Value.DeployOptions 711 | 712 | if ([string]::IsNullOrEmpty($workspaceName)) 713 | { 714 | $workspaceName = $_.Name 715 | } 716 | 717 | Write-Host "##[group]Configuring workspace: '$workspaceName'" 718 | 719 | $workspace = $pbiWorkspaces |? name -eq $workspaceName | Select -First 1 720 | 721 | if (!$workspace) 722 | { 723 | $workspace = New-PowerBIWorkspace -name $workspaceName 724 | } 725 | else 726 | { 727 | Write-Host "##[debug]Workspace '$workspaceName' already exists" 728 | } 729 | 730 | $workspaceUsers = Invoke-PowerBIRestMethod -Url "groups/$($workspace.id)/users" -Method Get | ConvertFrom-Json | Select -ExpandProperty value 731 | 732 | if (!$workspaceDeployOptions -or !$workspaceDeployOptions.IgnoreDefaultPermissions) 733 | { 734 | if ($defaultWorkspaceMetadata -and $defaultWorkspaceMetadata.Permissions) 735 | { 736 | $workspacePermissions += $defaultWorkspaceMetadata.Permissions 737 | } 738 | 739 | if ($defaultWorkspaceConfig -and $defaultWorkspaceConfig.Permissions) 740 | { 741 | $workspacePermissions += @($defaultWorkspaceConfig.Permissions) 742 | } 743 | } 744 | 745 | # remove duplicate identifiers 746 | 747 | $workspacePermissions = $workspacePermissions |% {$_} | Group-Object identifier |% { $_.Group[0] } 748 | 749 | # Set new/Update permissions 750 | 751 | $workspacePermissions |% { 752 | 753 | $configPermission = $_ 754 | 755 | # FInd if the identifier is present on the workspace permissions 756 | 757 | $pbiPermission = $workspaceUsers |? { 758 | ($_.identifier -and $_.identifier -eq $configPermission.identifier) 759 | } 760 | 761 | $body = $configPermission | select identifier, groupUserAccessRight, principalType | ConvertTo-Json 762 | 763 | if (!$pbiPermission) 764 | { 765 | Write-Host "##[debug]Adding new permission for principal '$($configPermission.identifier)' | '$($configPermission.principalType)' | '$($configPermission.groupUserAccessRight)'" 766 | 767 | Invoke-PowerBIRestMethod -Url "groups/$($workspace.id)/users" -Method Post -Body $body | Out-Null 768 | 769 | } 770 | else 771 | { 772 | if ($configPermission.groupUserAccessRight -ne $pbiPermission.groupUserAccessRight) 773 | { 774 | Write-Host "##[debug]Updating permission for principal '$($configPermission.identifier)' | '$($configPermission.principalType)' | '$($configPermission.groupUserAccessRight)'" 775 | 776 | #Invoke-PBIRequest -authToken $authToken -groupId $workspace.id -resource "users" -method Put -body $body 777 | 778 | Invoke-PowerBIRestMethod -Url "groups/$($workspace.id)/users" -Method Put -Body $body | Out-Null 779 | } 780 | } 781 | } 782 | 783 | # Clean all the permissions not existent on the config files: default & project 784 | 785 | if ($workspaceDeployOptions -and $workspaceDeployOptions.CleanPermissions -eq $true) 786 | { 787 | $workspacePermissionsToDelete = @($workspaceUsers |? { 788 | ($_.identifier -and $_.identifier -notin $workspacePermissions.identifier) 789 | }) 790 | 791 | if ($workspacePermissionsToDelete.Count -ne 0) 792 | { 793 | Write-Host "##[debug]Cleaning permissions: $($workspacePermissionsToDelete.Count)" 794 | } 795 | 796 | foreach ($permission in $workspacePermissionsToDelete) 797 | { 798 | Write-Host "##[debug]Deleting permission for principal '$($permission.identifier)'" 799 | 800 | Invoke-PowerBIRestMethod -Url "groups/$($workspace.id)/users/$($permission.identifier)" -Method Delete | Out-Null 801 | } 802 | } 803 | 804 | if (!$workspace.isOnDedicatedCapacity) 805 | { 806 | if ([string]::IsNullOrEmpty($capacityId)) 807 | { 808 | if ($defaultWorkspaceMetadata -and ![string]::IsNullOrEmpty($defaultWorkspaceMetadata.DedicatedCapacityId)) 809 | { 810 | $capacityId = $defaultWorkspaceMetadata.DedicatedCapacityId 811 | } 812 | elseif ($defaultWorkspaceConfig -and ![string]::IsNullOrEmpty($defaultWorkspaceConfig.DedicatedCapacityId)) 813 | { 814 | $capacityId = $defaultWorkspaceConfig.DedicatedCapacityId 815 | } 816 | } 817 | 818 | if ([string]::IsNullOrEmpty($capacityName)) 819 | { 820 | if ($defaultWorkspaceMetadata -and ![string]::IsNullOrEmpty($defaultWorkspaceMetadata.DedicatedCapacityName)) 821 | { 822 | $capacityName = $defaultWorkspaceMetadata.DedicatedCapacityName 823 | } 824 | elseif ($defaultWorkspaceConfig -and ![string]::IsNullOrEmpty($defaultWorkspaceConfig.DedicatedCapacityName)) 825 | { 826 | $capacityName = $defaultWorkspaceConfig.DedicatedCapacityName 827 | } 828 | } 829 | 830 | if ([string]::IsNullOrEmpty($capacityId) -and ![string]::IsNullOrEmpty($capacityName)) 831 | { 832 | #$capacities = Invoke-PBIRequest -authToken $authToken -resource "capacities" -method Get 833 | $capacities = Invoke-PowerBIRestMethod -Url "capacities" -Method Get | ConvertFrom-Json | Select -ExpandProperty value 834 | 835 | $capacity = $capacities |? {$_.displayName -eq $capacityName} | select -First 1 836 | 837 | if ($capacity -and $capacity.id) 838 | { 839 | $capacityId= $capacity.id 840 | } 841 | else 842 | { 843 | throw "Cannot find capacity with name '$capacityName'" 844 | } 845 | } 846 | 847 | 848 | # Assign premium if asked 849 | 850 | if (![string]::IsNullOrEmpty($capacityId)) 851 | { 852 | Write-Host "##[debug]Assigning Premium Capacity: '$capacityId'" 853 | 854 | #Invoke-PBIRequest -authToken $authToken -groupId $workspace.id -resource "AssignToCapacity" -method Post -body "{'capacityId':'$capacityId'}" 855 | 856 | Invoke-PowerBIRestMethod -Url "groups/AssignToCapacity" -Method Post -Body "{'capacityId':'$capacityId'}" | Out-Null 857 | 858 | } 859 | } 860 | 861 | Write-Host "##[endgroup]" 862 | } 863 | } 864 | 865 | # Unsuported, uses an internal API that currently is no longer supported to be called by the Microsoft PowerShell module 866 | function Set-PBIReportConnections_Auto 867 | { 868 | [CmdletBinding()] 869 | param( 870 | $path 871 | , 872 | $configPath 873 | , 874 | $internalAPIURL = "https://wabi-west-europe-redirect.analysis.windows.net" 875 | , 876 | $workingDir 877 | , 878 | $backupDir 879 | , 880 | $filter = @() 881 | ) 882 | 883 | $rootPath = $PSScriptRoot 884 | 885 | if ([string]::IsNullOrEmpty($path)) 886 | { 887 | $path = "$rootPath\Reports" 888 | } 889 | 890 | if ([string]::IsNullOrEmpty($backupDir)) 891 | { 892 | $backupDir = "$rootPath\_Backup" 893 | } 894 | 895 | New-Item -ItemType Directory -Path $backupDir -Force -ErrorAction SilentlyContinue | Out-Null 896 | 897 | if ([string]::IsNullOrEmpty($workingDir)) 898 | { 899 | $workingDir = Join-Path ([System.IO.Path]::GetTempPath()) "PBIFixConnections_$((New-Guid).ToString("N"))" 900 | } 901 | 902 | New-Item -ItemType Directory -Path $workingDir -Force -ErrorAction SilentlyContinue | Out-Null 903 | 904 | if ([string]::IsNullOrEmpty($configPath)) 905 | { 906 | $configPath = "$rootPath\config.json" 907 | } 908 | 909 | $reports = Get-ChildItem -File -Path "$path\*.pbix" -Recurse -ErrorAction SilentlyContinue 910 | 911 | if ($filter -and $filter.Count -gt 0) 912 | { 913 | $reports = $reports |? { $filter -contains $_.Name } 914 | } 915 | 916 | if ($reports.Count -eq 0) 917 | { 918 | Write-Host "##[warning] No reports on path '$path'" 919 | return 920 | } 921 | 922 | if (!(Test-Path -Path $configPath)) 923 | { 924 | throw "Cannot find metadata file '$configPath'" 925 | } 926 | 927 | $environmentMetadata = Get-EnvironmentMetadata $configPath 928 | 929 | Write-Host "##[command]Deploying '$($reports.Count)' reports" 930 | 931 | $bearerToken = Get-PowerBIAccessToken -AsString 932 | # Get all datasets the user has access to get the modelid for the rebind 933 | 934 | $sharedDataSetsStr = Invoke-RestMethod -Method Get -Headers @{'Authorization' = $bearerToken} -Uri "$internalAPIURL/metadata/gallery/SharedDatasets" 935 | 936 | if (!$sharedDataSetsStr) 937 | { 938 | throw "Cannot get SharedDatasets, make sure you are using the internal api for the Power BI tenant" 939 | } 940 | 941 | #ConverFrom-Json doesnt like properties with same name 942 | $sharedDataSetsStr = $sharedDataSetsStr.Replace("nextRefreshTime","nextRefreshTime_").Replace("lastRefreshTime","lastRefreshTime_") 943 | $sharedDataSets = $sharedDataSetsStr | ConvertFrom-Json 944 | 945 | $reports |% { 946 | 947 | $pbixFile = $_ 948 | 949 | Write-Host "##[group]Fixing connection of report: '$($pbixFile.Name)'" 950 | 951 | $filePath = $pbixFile.FullName 952 | 953 | $fileName = [System.IO.Path]::GetFileName($pbixFile.FullName) 954 | 955 | $fileNameWithoutExt = [System.IO.Path]::GetFileNameWithoutExtension($fileName) 956 | 957 | # Select the first target workspace to discover the datasetid 958 | 959 | $reportMetadata = Get-ReportMetadata -environmentMetadata $environmentMetadata -filePath $filePath | Select -First 1 960 | 961 | if (!$reportMetadata) 962 | { 963 | throw "Cannot find Report configuration '$filePath'" 964 | } 965 | 966 | $dataSetId = $reportMetadata.DataSetId 967 | 968 | Write-Host "##[debug]Finding dataset model id of dataset '$dataSetId'" 969 | 970 | # Find model for the dataset 971 | 972 | $model = $sharedDataSets |? { $_.model.dbName -eq $dataSetId } 973 | 974 | if ($model) 975 | { 976 | $modelId = $model.modelId 977 | 978 | Write-Host "##[debug] Found Power BI model '$modelId' for '$dataSetId'" 979 | 980 | Write-Host "##[debug] Backup '$fileName' into '$backupDir'" 981 | 982 | Copy-Item -Path $filePath -Destination "$backupDir\$fileNameWithoutExt.$(Get-Date -Format "yyyyMMddHHmmss").pbix" -Force 983 | 984 | $zipFile = "$workingDir\$fileName.zip" 985 | 986 | $zipFolder = "$workingDir\$fileNameWithoutExt" 987 | 988 | Write-Host "##[command] Unziping '$fileName' into $zipFolder" 989 | 990 | Copy-Item -Path $filePath -Destination $zipFile -Force 991 | 992 | Expand-Archive -Path $zipFile -DestinationPath $zipFolder -Force | Out-Null 993 | 994 | $connectionsJson = Get-Content "$zipFolder\Connections" | ConvertFrom-Json 995 | 996 | $connection = $connectionsJson.Connections[0] 997 | 998 | if ($connection.PbiModelDatabaseName -eq $dataSetId) 999 | { 1000 | Write-Host "##[warning] PBIX '$fileName' already connects to dataset '$dataSetId' skipping the rebind" 1001 | return 1002 | } 1003 | 1004 | $connection.PbiServiceModelId = $modelId 1005 | $connection.ConnectionString = $connection.ConnectionString.Replace($connection.PbiModelDatabaseName, $dataSetId) 1006 | $connection.PbiModelDatabaseName = $dataSetId 1007 | 1008 | if($connectionsJson.RemoteArtifacts -and $connectionsJson.RemoteArtifacts.Count -ne 0) 1009 | { 1010 | $connectionsJson.RemoteArtifacts[0].DatasetId = $dataSetId 1011 | } 1012 | 1013 | $connectionsJson | ConvertTo-Json -Compress | Out-File "$zipFolder\Connections" -Encoding ASCII 1014 | 1015 | # Update the connections on zip file 1016 | 1017 | Write-Host "##[debug] Updating connections file on zip file" 1018 | 1019 | Compress-Archive -Path "$zipFolder\Connections" -CompressionLevel Optimal -DestinationPath $zipFile -Update 1020 | 1021 | # Remove SecurityBindings 1022 | 1023 | Write-Host "##[debug] Removing SecurityBindings" 1024 | 1025 | try{ 1026 | $stream = new-object IO.FileStream($zipfile, [IO.FileMode]::Open) 1027 | $zipArchive = new-object IO.Compression.ZipArchive($stream, [IO.Compression.ZipArchiveMode]::Update) 1028 | $securityBindingsFile = $zipArchive.Entries |? Name -eq "SecurityBindings" | Select -First 1 1029 | 1030 | if ($securityBindingsFile) 1031 | { 1032 | $securityBindingsFile.Delete() 1033 | } 1034 | else 1035 | { 1036 | Write-Host "##[warning] Cannot find SecurityBindings on zip" 1037 | } 1038 | 1039 | } 1040 | finally{ 1041 | if ($zipArchive) { $zipArchive.Dispose() } 1042 | if ($stream) { $stream.Dispose() } 1043 | } 1044 | 1045 | Write-Host "##[debug] Overwriting original pbix" 1046 | 1047 | Copy-Item -Path $zipfile -Destination $filePath -Force 1048 | 1049 | } 1050 | else 1051 | { 1052 | Write-Host "##[warning] Cannot find a Power BI model for dataset '$dataSetId'" 1053 | } 1054 | 1055 | Write-Host "##[endgroup]" 1056 | } 1057 | } 1058 | 1059 | function Set-PBIReportConnections 1060 | { 1061 | [CmdletBinding()] 1062 | param( 1063 | $path 1064 | , 1065 | $configPath 1066 | , 1067 | # Go to app.powerbi.com and network trace the call to 'https://*.analysis.windows.net/metadata/gallery/SharedDatasets" and save the file locally 1068 | $sharedDatasetsPath 1069 | , 1070 | $workingDir 1071 | , 1072 | $backupDir 1073 | , 1074 | $filter = @() 1075 | ) 1076 | 1077 | $rootPath = $PSScriptRoot 1078 | 1079 | if (!(Test-Path $sharedDatasetsPath)) 1080 | { 1081 | throw "Cannot find shareddatasets file '$sharedDatasetsPath'. Login to app.powerbi.com and networktrace the 'sharedatasets' file" 1082 | } 1083 | 1084 | if ([string]::IsNullOrEmpty($path)) 1085 | { 1086 | $path = "$rootPath\Reports" 1087 | } 1088 | 1089 | if ([string]::IsNullOrEmpty($backupDir)) 1090 | { 1091 | $backupDir = "$rootPath\_Backup" 1092 | } 1093 | 1094 | New-Item -ItemType Directory -Path $backupDir -Force -ErrorAction SilentlyContinue | Out-Null 1095 | 1096 | if ([string]::IsNullOrEmpty($workingDir)) 1097 | { 1098 | $workingDir = Join-Path ([System.IO.Path]::GetTempPath()) "PBIFixConnections_$((New-Guid).ToString("N"))" 1099 | } 1100 | 1101 | New-Item -ItemType Directory -Path $workingDir -Force -ErrorAction SilentlyContinue | Out-Null 1102 | 1103 | if ([string]::IsNullOrEmpty($configPath)) 1104 | { 1105 | $configPath = "$rootPath\config.json" 1106 | } 1107 | 1108 | $reports = Get-ChildItem -File -Path "$path\*.pbix" -Recurse -ErrorAction SilentlyContinue 1109 | 1110 | if ($filter -and $filter.Count -gt 0) 1111 | { 1112 | $reports = $reports |? { $filter -contains $_.Name } 1113 | } 1114 | 1115 | if ($reports.Count -eq 0) 1116 | { 1117 | Write-Host "##[warning] No reports on path '$path'" 1118 | return 1119 | } 1120 | 1121 | if (!(Test-Path -Path $configPath)) 1122 | { 1123 | throw "Cannot find metadata file '$configPath'" 1124 | } 1125 | 1126 | $environmentMetadata = Get-EnvironmentMetadata $configPath 1127 | 1128 | Write-Host "##[command]Deploying '$($reports.Count)' reports" 1129 | 1130 | $sharedDataSetsStr = Get-Content $sharedDatasetsPath 1131 | 1132 | #ConverFrom-Json doesnt like properties with same name 1133 | $sharedDataSetsStr = $sharedDataSetsStr.Replace("nextRefreshTime","nextRefreshTime_").Replace("lastRefreshTime","lastRefreshTime_") 1134 | $sharedDataSets = $sharedDataSetsStr | ConvertFrom-Json 1135 | 1136 | $reports |% { 1137 | 1138 | $pbixFile = $_ 1139 | 1140 | Write-Host "##[group]Fixing connection of report: '$($pbixFile.Name)'" 1141 | 1142 | $filePath = $pbixFile.FullName 1143 | 1144 | $fileName = [System.IO.Path]::GetFileName($pbixFile.FullName) 1145 | 1146 | $fileNameWithoutExt = [System.IO.Path]::GetFileNameWithoutExtension($fileName) 1147 | 1148 | # Select the first target workspace to discover the datasetid 1149 | 1150 | $reportMetadata = Get-ReportMetadata -environmentMetadata $environmentMetadata -filePath $filePath | Select -First 1 1151 | 1152 | if (!$reportMetadata) 1153 | { 1154 | throw "Cannot find Report configuration '$filePath'" 1155 | } 1156 | 1157 | $dataSetId = $reportMetadata.DataSetId 1158 | 1159 | Write-Host "##[debug]Finding dataset model id of dataset '$dataSetId'" 1160 | 1161 | # Find model for the dataset 1162 | 1163 | $model = $sharedDataSets |? { $_.model.dbName -eq $dataSetId } 1164 | 1165 | if ($model) 1166 | { 1167 | $modelId = $model.modelId 1168 | 1169 | Write-Host "##[debug] Found Power BI model '$modelId' for '$dataSetId'" 1170 | 1171 | Write-Host "##[debug] Backup '$fileName' into '$backupDir'" 1172 | 1173 | Copy-Item -Path $filePath -Destination "$backupDir\$fileNameWithoutExt.$(Get-Date -Format "yyyyMMddHHmmss").pbix" -Force 1174 | 1175 | $zipFile = "$workingDir\$fileName.zip" 1176 | 1177 | $zipFolder = "$workingDir\$fileNameWithoutExt" 1178 | 1179 | Write-Host "##[command] Unziping '$fileName' into $zipFolder" 1180 | 1181 | Copy-Item -Path $filePath -Destination $zipFile -Force 1182 | 1183 | Expand-Archive -Path $zipFile -DestinationPath $zipFolder -Force | Out-Null 1184 | 1185 | $connectionsJson = Get-Content "$zipFolder\Connections" | ConvertFrom-Json 1186 | 1187 | $connection = $connectionsJson.Connections[0] 1188 | 1189 | if ($connection.PbiModelDatabaseName -eq $dataSetId) 1190 | { 1191 | Write-Host "##[warning] PBIX '$fileName' already connects to dataset '$dataSetId' skipping the rebind" 1192 | return 1193 | } 1194 | 1195 | $connection.PbiServiceModelId = $modelId 1196 | $connection.ConnectionString = $connection.ConnectionString.Replace($connection.PbiModelDatabaseName, $dataSetId) 1197 | $connection.PbiModelDatabaseName = $dataSetId 1198 | 1199 | if($connectionsJson.RemoteArtifacts -and $connectionsJson.RemoteArtifacts.Count -ne 0) 1200 | { 1201 | $connectionsJson.RemoteArtifacts[0].DatasetId = $dataSetId 1202 | } 1203 | 1204 | $connectionsJson | ConvertTo-Json -Compress | Out-File "$zipFolder\Connections" -Encoding ASCII 1205 | 1206 | # Update the connections on zip file 1207 | 1208 | Write-Host "##[debug] Updating connections file on zip file" 1209 | 1210 | Compress-Archive -Path "$zipFolder\Connections" -CompressionLevel Optimal -DestinationPath $zipFile -Update 1211 | 1212 | # Remove SecurityBindings 1213 | 1214 | Write-Host "##[debug] Removing SecurityBindings" 1215 | 1216 | try{ 1217 | $stream = new-object IO.FileStream($zipfile, [IO.FileMode]::Open) 1218 | $zipArchive = new-object IO.Compression.ZipArchive($stream, [IO.Compression.ZipArchiveMode]::Update) 1219 | $securityBindingsFile = $zipArchive.Entries |? Name -eq "SecurityBindings" | Select -First 1 1220 | 1221 | if ($securityBindingsFile) 1222 | { 1223 | $securityBindingsFile.Delete() 1224 | } 1225 | else 1226 | { 1227 | Write-Host "##[warning] Cannot find SecurityBindings on zip" 1228 | } 1229 | 1230 | } 1231 | finally{ 1232 | if ($zipArchive) { $zipArchive.Dispose() } 1233 | if ($stream) { $stream.Dispose() } 1234 | } 1235 | 1236 | Write-Host "##[debug] Overwriting original pbix" 1237 | 1238 | Copy-Item -Path $zipfile -Destination $filePath -Force 1239 | 1240 | } 1241 | else 1242 | { 1243 | Write-Host "##[warning] Cannot find a Power BI model for dataset '$dataSetId'" 1244 | } 1245 | 1246 | Write-Host "##[endgroup]" 1247 | } 1248 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Install Required PowerShell Modules (as Administrator) 2 | ``` 3 | Install-Module -Name MicrosoftPowerBIMgmt -RequiredVersion 1.2.1026 4 | ``` 5 | 6 | # How to Run? 7 | 8 | Setup a folder with the Datasets & Reports 9 | 10 | Change the [Config File](./config-prd.json) with your deployment scenario 11 | 12 | Run the [Deploy](./deploy.ps1) powershell script: 13 | 14 | ``` 15 | powershell deploy.ps1 -path .\SampleProject -configPath .\config.json 16 | ``` 17 | 18 | # Sample Project 19 | 20 | This repo includes a sample project with datasets & reports, if you try to deploy this sample project before running you need to do the following: 21 | 22 | - Deploy the dataset to a workspace (you can use the deploy.ps1 script) 23 | - Run the script [tool.FixReportConnections.ps1](./tool.FixReportConnections.ps1) to ensure local PBIX files target an existent powerbi.com dataset, otherwise you will get an error on report deploy 24 | 25 | # Multiple Config Files 26 | 27 | The main advantage of declaring your deployment environment is that you can easily have multiple deployment configurations (multiple [config](./config.json) files) and call the deploy.ps1 using the sample local development files but different deployment config. 28 | 29 | # Multiple Config Files 30 | 31 | Its possible to setup permissions for the workspaces, use the following json 32 | 33 | ## User 34 | ``` 35 | { 36 | "identifier": "user1@company.com", 37 | "groupUserAccessRight": "Member", 38 | "principalType": "User" 39 | } 40 | ``` 41 | 42 | ## Group 43 | ``` 44 | { 45 | "identifier": "[AZURE ID OBJECT ID OF THE GROUP]", 46 | "groupUserAccessRight": "Admin", 47 | "principalType": "Group" 48 | } 49 | ``` 50 | 51 | ## Service Principal / APP 52 | ``` 53 | { 54 | "identifier": "[AZURE ID OBJECT ID OF THE SERVICE PRINCIPAL]", 55 | "groupUserAccessRight": "Member", 56 | "principalType": "App" 57 | } 58 | ``` -------------------------------------------------------------------------------- /SampleProject/DataSets/WWI - Sales.pbix: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RuiRomano/pbidevops/e369dca8440b376d9cf45c3cc015f530c92d2be4/SampleProject/DataSets/WWI - Sales.pbix -------------------------------------------------------------------------------- /SampleProject/PaginatedReports/PaginatedReport.rdl: -------------------------------------------------------------------------------- 1 |  2 | 3 | Mm 4 | 22cc6470-8117-475d-b242-a491e13cdb2f 5 | Segoe UI 6 | 0 7 | 8 | 9 | None 10 | 11 | PBIDATASET 12 | Data Source=pbiazure://api.powerbi.com/;Identity Provider="https://login.microsoftonline.com/common, https://analysis.windows.net/powerbi/api, f0b72488-7082-488a-a7e8-eada97bd842d";Initial Catalog=sobe_wowvirtualserver-846c0ddb-aae5-4ac9-a014-1238490ff5dd;Integrated Security=ClaimsToken 13 | 14 | 64b866a6-41ce-420c-a700-71ef5327b480 15 | Demo - PBIDevOps {DEV} 16 | WWI - Sales 17 | 18 | 19 | 20 | 21 | 22 | 23 | DemoPBIDevOpsDEV_WWISales 24 | 25 | 26 | DAX 27 | 28 | 70 | false 71 | Model 72 | 73 | 74 | 75 | 76 | 77 | 78 | true 79 | 80 | 81 | 82 | EVALUATE SUMMARIZECOLUMNS('Calendar'[Year], 'Stock Item'[Category], "Sales Amount", [Sales Amount]) 83 | 84 | 85 | 86 | 87 | EVALUATE SUMMARIZECOLUMNS('Calendar'[Year], 'Stock Item'[Category], "Sales Amount", [Sales Amount]) 88 | 89 | 90 | 91 | System.String 92 | Calendar[Year] 93 | 94 | 95 | System.String 96 | Stock Item[Category] 97 | 98 | 99 | System.Int32 100 | [Sales Amount] 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | Title 111 | ReportTitle 112 | true 113 | true 114 | 115 | 116 | 117 | 118 | Test RDL - XPTO 119 | 123 | 124 | 125 | 134 | 135 | 2pt 136 | 2pt 137 | 2pt 138 | 2pt 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | Textbox4 149 | true 150 | true 151 | 152 | 153 | 154 | 155 | Year 156 | 166 | 167 | 2pt 168 | 2pt 169 | 2pt 170 | 2pt 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 65.14747mm 182 | 183 | 184 | 185 | 186 | 11.81806mm 187 | 188 | 189 | 190 | 191 | Sales_Amount 192 | true 193 | true 194 | 195 | 196 | 197 | 198 | =Sum(Fields!Sales_Amount.Value) 199 | 209 | 210 | 2pt 211 | 2pt 212 | 2pt 213 | 2pt 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | =Fields!Category.Value 228 | 229 | 230 | 231 | 232 | =Fields!Category.Value 233 | 234 | 235 | 236 | 11.81806mm 237 | 238 | 239 | Category 240 | true 241 | true 242 | 243 | 244 | 245 | 246 | =Fields!Category.Value 247 | 257 | 258 | 2pt 259 | 2pt 260 | 2pt 261 | 2pt 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | =Fields!Year.Value 275 | 276 | 277 | 278 | 279 | =Fields!Year.Value 280 | 281 | 282 | 283 | 65.14747mm 284 | 285 | 286 | Year 287 | true 288 | true 289 | 290 | 291 | 292 | 293 | =Fields!Year.Value 294 | 304 | 305 | 2pt 306 | 2pt 307 | 2pt 308 | 2pt 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | DataSet1 317 | 21.3995mm 318 | 9.40506mm 319 | 23.63612mm 320 | 130.29494mm 321 | 1 322 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | =Fields!Year.Value 335 | 336 | 337 | 338 | 339 | =Fields!Year.Value 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | =Sum(Fields!Sales_Amount.Value) 360 | 361 | 362 | 398 | 399 | 8pt 400 | #5c5c5c 401 | 402 | 403 | 404 | 408 | 409 | 410 | False 411 | 416 | 417 | 418 | 422 | 423 | 424 | 425 | 426 | 430 | 431 | 432 | 433 | 434 | 439 | 0.5 440 | 441 | NaN 442 | NaN 443 | NaN 444 | 445 | 453 | 454 | 8pt 455 | #5c5c5c 456 | 457 | 458 | 459 | 463 | 464 | 465 | False 466 | 471 | 472 | 473 | 477 | 478 | 479 | 480 | 481 | 485 | 486 | 487 | 488 | 489 | 494 | 0.5 495 | 496 | NaN 497 | Opposite 498 | NaN 499 | NaN 500 | 501 | 511 | 512 | 8pt 513 | #5c5c5c 514 | 515 | 516 | 517 | 521 | 522 | 523 | 528 | 529 | 530 | 534 | 535 | 536 | 537 | 538 | 542 | 543 | 544 | 545 | 546 | 551 | 0.5 552 | 553 | NaN 554 | NaN 555 | NaN 556 | 557 | 565 | 566 | 8pt 567 | #5c5c5c 568 | 569 | 570 | 571 | 575 | 576 | 577 | 582 | 583 | 584 | 588 | 589 | 590 | 591 | 592 | 596 | 597 | 598 | 599 | 600 | 605 | 0.5 606 | 607 | NaN 608 | Opposite 609 | NaN 610 | NaN 611 | 612 | 620 | 621 | 622 | 623 | 624 | 628 | TopLeft 629 | 630 | 631 | 636 | 637 | Black 638 | Black 639 | 640 | 641 | 642 | 643 | Chart Title 644 | 652 | TopLeft 653 | 654 | 655 | Pacific 656 | 657 | 662 | 663 | 664 | No Data Available 665 | 671 | 672 | DataSet1 673 | 56.67728mm 674 | 13.462mm 675 | 54.50417mm 676 | 116.59306mm 677 | 2 678 | 682 | 683 | White 684 | None 685 | 686 | 687 | 688 | 125.76528mm 689 | 692 | 693 | 694 | 695 | 152.4mm 696 | 697 | 698 | 11.43mm 699 | true 700 | true 701 | 702 | 703 | ExecutionTime 704 | true 705 | true 706 | 707 | 708 | 709 | 710 | =Globals!ExecutionTime 711 | 717 | 718 | 719 | 5.08mm 720 | 101.6mm 721 | 6.35mm 722 | 50.8mm 723 | 726 | 727 | 2pt 728 | 2pt 729 | 2pt 730 | 2pt 731 | 732 | 733 | 734 | 737 | 738 | 739 | 740 | 29.7cm 741 | 21cm 742 | 2cm 743 | 2cm 744 | 2cm 745 | 2cm 746 | 0.13cm 747 |