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
234 changes: 234 additions & 0 deletions src/scripts/Deploy-Hub.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

<#
.SYNOPSIS
Deploys a FinOps hub instance for local testing.

.DESCRIPTION
Wrapper around Deploy-Toolkit that simplifies FinOps hub deployments by providing scenario-based flags instead of requiring you to remember all the Bicep parameter names.

By default, deploys with Azure Data Explorer (dev SKU). Use -StorageOnly for storage-only or -Fabric for Fabric-based deployments.

All resources use an "{initials}-{name}" naming convention where initials are pulled from git config user.name and name defaults to "adx". Pass a name as the first positional parameter to use a custom value (e.g., "216" for Feb 16).
Comment on lines +4 to +13
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR adds a new helper command. The repo release process states that new/updated functionality must be documented in docs-mslearn/toolkit/changelog.md; please add an entry under the next release section describing the new Deploy-Hub helper (customer-friendly, no internal implementation details).

Copilot uses AI. Check for mistakes.

.EXAMPLE
Deploy-Hub

Deploys a hub with ADX (e.g., RG "aa-adx", ADX "aa-adx").

.EXAMPLE
Deploy-Hub 216

Deploys to a named environment (e.g., RG "aa-216", ADX "aa-216").

.EXAMPLE
Deploy-Hub -StorageOnly

Deploys a storage-only hub (no ADX, no Fabric).

.EXAMPLE
Deploy-Hub -Fabric "https://my-eventhouse.kusto.data.microsoft.com"

Deploys a hub connected to a Microsoft Fabric eventhouse.

.EXAMPLE
Deploy-Hub -Remove 210

Deletes the resource group for the specified name (e.g., "aa-210").

.EXAMPLE
Deploy-Hub -Remove

Lists all resource groups matching the "{initials}-*" naming convention.

.EXAMPLE
Deploy-Hub -Build

Builds the template first, then deploys with ADX.

.EXAMPLE
Deploy-Hub -WhatIf

Validates the deployment without making changes.

.PARAMETER Name
Optional. First positional parameter. Suffix for the "{initials}-{name}" naming convention used for resource group and ADX cluster. Default: "adx".

.PARAMETER HubName
Optional. Name of the hub instance. Default: "hub".

.PARAMETER ADX
Optional. Name of the Azure Data Explorer cluster. Overrides the "{initials}-{name}" convention. Only used when not using -StorageOnly or -Fabric.

.PARAMETER ResourceGroup
Optional. Name of the resource group. Overrides the "{initials}-{name}" convention.

.PARAMETER Fabric
Deploy with Microsoft Fabric. Provide the eventhouse query URI.

.PARAMETER StorageOnly
Deploy a storage-only hub (no Azure Data Explorer or Fabric).

.PARAMETER Remove
Remove test environments. With a name, deletes the target resource group. Alone, lists all resource groups matching "{initials}-*".

.PARAMETER Location
Optional. Azure location. Default: westus.

.PARAMETER Build
Optional. Build the template before deploying.

.PARAMETER WhatIf
Optional. Validate the deployment without making changes.

.LINK
https://github.com/microsoft/finops-toolkit/blob/dev/src/scripts/README.md
#>
param(
[Parameter(Position = 0)]
[string]$Name,
[string]$HubName,
[string]$ADX,
[string]$ResourceGroup,
[string]$Fabric,
[switch]$StorageOnly,
[switch]$Remove,
[string]$Location,
[switch]$Build,
[switch]$WhatIf
)

# Get user initials from git config user.name (first letter of each word, lowercased)
function Get-Initials()
{
$name = (git config user.name 2>$null)
if ($name)
{
$parts = $name.Trim() -split '\s+'
if ($parts.Count -ge 2)
{
return (($parts | ForEach-Object { $_[0] }) -join '').ToLower()
}
return $name.Substring(0, [Math]::Min(2, $name.Length)).ToLower()
}

# Fall back to OS username
$u = ($env:USERNAME ?? $env:USER ?? "xx").ToLower()
$parts = $u -split '[\.\-_\s]'
if ($parts.Count -ge 2)
{
return ($parts | ForEach-Object { $_[0] }) -join ''
}
return $u.Substring(0, [Math]::Min(2, $u.Length))
}

$initials = Get-Initials

# Default name to "adx" when not specified
if (-not $Name)
{
$Name = "adx"
}

# Build the full name: {initials}-{name}
$fullName = "$initials-$Name"

#------------------------------------------------------------------------------
# Remove mode
#------------------------------------------------------------------------------

if ($Remove)
{
if ($PSBoundParameters.ContainsKey('Name'))
{
# Delete the specific resource group
$rgName = if ($ResourceGroup) { $ResourceGroup } else { $fullName }
$rg = Get-AzResourceGroup -Name $rgName -ErrorAction SilentlyContinue
if ($null -eq $rg)
{
Write-Host "Resource group '$rgName' not found."
return
}

Write-Host "Deleting resource group '$rgName'..."
Remove-AzResourceGroup -Name $rgName -Force
Write-Host "Deleted '$rgName'."
Comment on lines +154 to +156
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

-WhatIf is accepted by this script but ignored in -Remove mode, so Deploy-Hub -Remove -WhatIf <name> would still delete the resource group. Please honor -WhatIf during removal (for example, pass it through to Remove-AzResourceGroup or short-circuit deletion when -WhatIf is set) to avoid accidental deletions.

Suggested change
Write-Host "Deleting resource group '$rgName'..."
Remove-AzResourceGroup -Name $rgName -Force
Write-Host "Deleted '$rgName'."
if ($WhatIf)
{
# WhatIf mode: do not delete, just report the intended action
Write-Host "WhatIf: would delete resource group '$rgName'."
}
else
{
Write-Host "Deleting resource group '$rgName'..."
Remove-AzResourceGroup -Name $rgName -Force
Write-Host "Deleted '$rgName'."
}

Copilot uses AI. Check for mistakes.
}
else
{
# List all resource groups matching the initials-* pattern
$pattern = "$initials-*"
$groups = Get-AzResourceGroup | Where-Object { $_.ResourceGroupName -like $pattern }
if ($groups.Count -eq 0)
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$groups may be $null (no matches) or a single PSResourceGroup object (1 match), in which case $groups.Count isn't reliable. Use a null/empty check (e.g., if (-not $groups) or @($groups).Count) so the "no resource groups found" path behaves correctly.

Suggested change
if ($groups.Count -eq 0)
if (-not $groups)

Copilot uses AI. Check for mistakes.
{
Write-Host "No resource groups found matching '$pattern'."
}
else
{
Write-Host "Resource groups matching '$pattern':"
$groups | ForEach-Object {
Write-Host " $($_.ResourceGroupName) ($($_.Location))"
}
Write-Host ""
Write-Host "Use -Remove <name> to delete a specific one."
}
}
return
}

#------------------------------------------------------------------------------
# Deploy mode
#------------------------------------------------------------------------------

# Validate mutually exclusive options
if ($StorageOnly -and $Fabric)
{
Write-Error "Cannot specify both -StorageOnly and -Fabric. Please choose one analytics backend."
return
}

# Build parameters
$params = @{}

# Hub name
if ($HubName) { $params.hubName = $HubName }
else { $params.hubName = "hub" }

# Analytics backend
if ($StorageOnly)
{
Write-Host "Scenario: Storage-only (no analytics engine)"
}
elseif ($Fabric)
{
$params.fabricQueryUri = $Fabric
Write-Host "Scenario: Microsoft Fabric ($Fabric)"
}
else
{
# Default: Azure Data Explorer (dev SKU)
if ($ADX) { $params.dataExplorerName = $ADX }
else { $params.dataExplorerName = $fullName }
Write-Host "Scenario: Azure Data Explorer ($($params.dataExplorerName))"
}

Write-Host " Hub: $($params.hubName)"

# Resource group
if (-not $ResourceGroup)
{
$ResourceGroup = $fullName
}

# Forward to Deploy-Toolkit
$deployArgs = @{
Template = "finops-hub"
Parameters = $params
ResourceGroup = $ResourceGroup
}
if ($Location) { $deployArgs.Location = $Location }
$deployArgs.Build = $Build
$deployArgs.WhatIf = $WhatIf

& "$PSScriptRoot/Deploy-Toolkit" @deployArgs
78 changes: 74 additions & 4 deletions src/scripts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ On this page:
- [🆕 Init-Repo](#-init-repo)
- [🌐 Build-OpenData](#-build-opendata)
- [📦 Build-Toolkit](#-build-toolkit)
- [🚀 Deploy-Hub](#-deploy-hub)
- [🚀 Deploy-Toolkit](#-deploy-toolkit)
- [🧪 Test-PowerShell](#-test-powershell)
- [🏷️ Get-Version](#️-get-version)
Expand Down Expand Up @@ -139,6 +140,73 @@ Build-Toolkit runs the following scripts internally:

<br>

## 🚀 Deploy-Hub

[Deploy-Hub.ps1](./Deploy-Hub.ps1) is a wrapper around Deploy-Toolkit that simplifies FinOps hub deployments by providing scenario-based flags instead of requiring you to remember all the Bicep parameter names.

By default, deploys with Azure Data Explorer (dev SKU). Use `-StorageOnly` for storage-only or `-Fabric` for Fabric-based deployments.

All resources use an `{initials}-{name}` naming convention where initials are pulled from `git config user.name` and name defaults to `adx`. Pass a name as the first positional parameter to use a custom value (e.g., `216` for Feb 16).

| Parameter | Description |
| ---------------- | ---------------------------------------------------------------------------------------------------------- |
| `‑Name` | Optional. First positional parameter. Suffix for `{initials}-{name}` convention. Default: `adx`. |
| `‑HubName` | Optional. Name of the hub instance. Default: `hub`. |
| `‑ADX` | Optional. Name of the Azure Data Explorer cluster. Overrides the `{initials}-{name}` convention. |
| `‑ResourceGroup` | Optional. Name of the resource group. Overrides the `{initials}-{name}` convention. |
| `‑Fabric` | Optional. Deploy with Microsoft Fabric. Provide the eventhouse query URI. |
| `‑StorageOnly` | Optional. Deploy a storage-only hub (no Azure Data Explorer or Fabric). |
| `‑Remove` | Optional. Remove test environments. With a name, deletes the target RG. Alone, lists all `{initials}-*`. |
| `‑Location` | Optional. Azure location. Default: `westus`. |
| `‑Build` | Optional. Build the template before deploying. |
| `‑WhatIf` | Optional. Validate the deployment without making changes. |

Examples:

- Deploy a hub with ADX (e.g., RG `aa-adx`, ADX `aa-adx`):

```powershell
./Deploy-Hub
```

- Deploy to a named environment (e.g., RG `aa-216`, ADX `aa-216`):

```powershell
./Deploy-Hub 216
```

- Deploy a storage-only hub:

```powershell
./Deploy-Hub -StorageOnly
```

- Deploy with Microsoft Fabric:

```powershell
./Deploy-Hub -Fabric "https://my-eventhouse.kusto.data.microsoft.com"
```

- Build the template first, then deploy:

```powershell
./Deploy-Hub -Build
```

- Clean up a specific test environment (e.g., `aa-210`):

```powershell
./Deploy-Hub -Remove 210
```

- List all test environments:

```powershell
./Deploy-Hub -Remove
```

<br>

## 🚀 Deploy-Toolkit

[Deploy-Toolkit.ps1](./Deploy-Toolkit.ps1) deploys toolkit templates for local testing purposes.
Expand Down Expand Up @@ -302,7 +370,7 @@ Examples:

| Parameter | Description |
| ----------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `‑Template` | Name of the template or module to publish. Default = * (all templates). |
| `‑Template` | Name of the template or module to publish. Default = \* (all templates). |
| `‑QuickstartRepo` | Optional. Name of the folder where the Azure Quickstart Templates repo is cloned. Default = azure-quickstart-templates. |
| `‑RegistryRepo` | Optional. Name of the folder where the Bicep Registry repo is cloned. Default = bicep-registry-modules. |
| `‑Build` | Optional. Indicates whether the the Build-Toolkit command should be executed first. Default = false. |
Expand All @@ -311,19 +379,19 @@ Examples:
Examples:

- Builds and publishes the FinOps hub template to the Azure Quickstart Templates repo, commits changes, and pushes to the fork to prepare for a PR.

```powershell
./Publish-Toolkit "finops-hub" -Build -Commit
```

- Builds and publishes the resource group scheduled action module to the Bicep Registry repo locally but does not commit.

```powershell
./Publish-Toolkit "resourcegroup-scheduled-action" -Build
```

- Publishes documentation to the Microsoft Learn repo locally but does not commit.

```powershell
./Publish-Toolkit "docs"
```
Expand Down Expand Up @@ -354,6 +422,8 @@ Examples:
```powershell
./Package-Toolkit -Build

```

- Builds the latest version of a specific template and updates the deployment files for the website.

```powershell
Expand Down