Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
1 change: 1 addition & 0 deletions docs-mslearn/toolkit/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ The following section lists features and enhancements that are currently in deve
- Replaced redundant `tolower()` comparisons in hub KQL with case-insensitive operators (`has`, `=~`, `!~`) so the engine can use the term index instead of scanning every row ([#2213](https://github.com/microsoft/finops-toolkit/issues/2213)).
- Replaced whole-term `contains` matches with `has` across hub KQL and the query catalog (resource ID paths, licensing phrases, SKU description terms) and added a per-row operator-equivalence regression harness with unit test coverage ([#2220](https://github.com/microsoft/finops-toolkit/pull/2220)).
- **Fixed**
- Fixed ADF schedule trigger deployment failures (`InvalidWorkflowTriggerRecurrence`) in regions not covered by the region-to-time-zone map (for example, Sweden Central), where the trigger's `startTime` was missing the required `Z` suffix for the `UTC` fallback time zone ([#2157](https://github.com/microsoft/finops-toolkit/issues/2157), [#2291](https://github.com/microsoft/finops-toolkit/pull/2291)).
Comment thread
flanakin marked this conversation as resolved.
- Fixed private-network deployments that Azure Policy blocked when `defaultOutboundAccess` was omitted. Private mode subnets now set it to `false`, while an Azure Files private endpoint supports deployment-script storage and the NAT Gateway provides required container egress ([#2258](https://github.com/microsoft/finops-toolkit/issues/2258), [#2259](https://github.com/microsoft/finops-toolkit/pull/2259)).
- Fixed the `ContractedCost` recompute guard to compare with a null-safe tolerance instead of exact float equality, eliminating millions of no-op rewrites that polluted the `x_SourceValues` audit trail while preserving the null-cost backfill and no longer overwriting an existing cost when the unit price is missing ([#2216](https://github.com/microsoft/finops-toolkit/issues/2216)).
- Fixed the SQL VMs without Azure Hybrid Benefit recommendation query to join on the SQL VM `virtualMachineResourceId` instead of a case-sensitive VM name match that skipped VMs with uppercase names and dropped duplicate names, and made all Azure Resource Graph join kinds explicit so no query relies on the `innerunique` default ([#2225](https://github.com/microsoft/finops-toolkit/pull/2225)).
Expand Down
6 changes: 3 additions & 3 deletions docs/deploy/finops-hub-14.0.json
Original file line number Diff line number Diff line change
Expand Up @@ -14033,7 +14033,7 @@
"recurrence": {
"frequency": "Hour",
"interval": 24,
"startTime": "2023-01-01T01:01:00",
"startTime": "[if(equals(reference('timeZones').outputs.Timezone.value, 'UTC'), '2023-01-01T01:01:00Z', '2023-01-01T01:01:00')]",
Comment thread
flanakin marked this conversation as resolved.
"timeZone": "[reference('timeZones').outputs.Timezone.value]"
}
}
Expand Down Expand Up @@ -14064,7 +14064,7 @@
"recurrence": {
"frequency": "Month",
"interval": 1,
"startTime": "2023-01-05T01:11:00",
"startTime": "[if(equals(reference('timeZones').outputs.Timezone.value, 'UTC'), '2023-01-05T01:11:00Z', '2023-01-05T01:11:00')]",
"timeZone": "[reference('timeZones').outputs.Timezone.value]",
"schedule": {
"monthDays": [
Expand Down Expand Up @@ -23925,7 +23925,7 @@
"recurrence": {
"frequency": "Hour",
"interval": 24,
"startTime": "2023-01-01T01:01:00",
"startTime": "[if(equals(reference('timeZones').outputs.Timezone.value, 'UTC'), '2023-01-01T01:01:00Z', '2023-01-01T01:01:00')]",
"timeZone": "[reference('timeZones').outputs.Timezone.value]"
}
}
Expand Down
60 changes: 60 additions & 0 deletions src/powershell/Tests/Unit/HubsAdfTriggerTimeZones.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,16 @@
(docs/deploy/finops-hub-latest.json and finops-hub-preview.json), because those are regenerated at
release time and can lag a src fix (see the #2236 review discussion). Older versioned templates are
historical artifacts and intentionally not covered.

Separately, regression coverage for the ADF trigger startTime Z-suffix fix (issue #2157, PR #2291):
Data Factory requires a trailing 'Z' on a trigger's startTime whenever timeZone resolves to the 'UTC'
fallback; without it, activation fails with the same InvalidWorkflowTriggerRecurrence error. The three
affected triggers in ManagedExports/app.bicep and IngestionQueries/app.bicep make the 'Z' conditional
on the resolved timeZone, and finops-hub-14.0.json carries the same fix compiled to an ARM if(equals(...))
expression. These tests pin that pairing so it cannot silently regress. finops-hub-latest.json and
finops-hub-preview.json are intentionally not covered here (see the #2291 review discussion) - they
still ship the pre-fix, unconditional startTime and are expected to pick up the fix when regenerated
at the next release.
#>

Describe 'HubsAdfTriggerTimeZones' {
Expand Down Expand Up @@ -70,6 +80,38 @@ Describe 'HubsAdfTriggerTimeZones' {
}
}
}

# Extract every trigger startTime from the two src files fixed by #2291, keyed by the raw line
# so a failure points straight at the offending statement instead of just a file name.
$startTimeSrcFiles = @(
'src/templates/finops-hub/modules/Microsoft.CostManagement/ManagedExports/app.bicep'
'src/templates/finops-hub/modules/Microsoft.FinOpsHubs/IngestionQueries/app.bicep'
)
$startTimeSrcLines = @()
foreach ($relativePath in $startTimeSrcFiles)
{
foreach ($line in (Get-Content -Path (Join-Path $repoRoot $relativePath)))
{
if ($line -match '^\s*startTime:')
{
$startTimeSrcLines += @{ File = $relativePath; Line = $line.Trim() }
}
}
}

# Same pairing, compiled to the ARM if(equals(...)) form, in the one shipped deploy artifact
# patched alongside the src fix (finops-hub-14.0.json). finops-hub-latest.json and
# finops-hub-preview.json are excluded - they still ship the pre-fix startTime and pick up the
# fix at the next release regen.
$startTimeDeployFile = 'finops-hub-14.0.json'
$startTimeDeployLines = @()
foreach ($line in (Get-Content -Path (Join-Path $repoRoot "docs/deploy/$startTimeDeployFile")))
{
if ($line -match '"startTime":')
{
$startTimeDeployLines += @{ File = $startTimeDeployFile; Line = $line.Trim() }
}
}
}

BeforeAll {
Expand Down Expand Up @@ -142,4 +184,22 @@ Describe 'HubsAdfTriggerTimeZones' {
$bicepContent | Should -Match "\?\?\s*'UTC'"
Comment thread
flanakin marked this conversation as resolved.
}
}

Context 'startTime UTC suffix' {
Comment thread
flanakin marked this conversation as resolved.
It 'Pairs a Z-suffixed and bare startTime around the UTC fallback in <File>: <Line>' -TestCases $startTimeSrcLines {
# Data Factory requires the trailing 'Z' only when timeZone resolves to the 'UTC' fallback;
# a bare 'Z' or an unconditional literal would silently reintroduce #2157 or change scheduling
# behavior for every mapped, non-UTC region.
$matched = $Line -match "^startTime:\s*timeZones\.outputs\.Timezone == 'UTC' \? '([0-9T:-]+)Z' : '([0-9T:-]+)'$"
$matched | Should -BeTrue -Because "startTime should be conditional on the UTC fallback, got: $Line"
$Matches[1] | Should -BeExactly $Matches[2] -Because 'the UTC and non-UTC branches must use the same timestamp, differing only by the trailing Z'
}

It 'Pairs a Z-suffixed and bare startTime around the UTC fallback in <File>: <Line>' -TestCases $startTimeDeployLines {
# Same invariant, compiled to the ARM if(equals(...)) form shipped in the patched deploy artifact.
$matched = $Line -match "^`"startTime`":\s*`"\[if\(equals\(reference\('timeZones'\)\.outputs\.Timezone\.value, 'UTC'\), '([0-9T:-]+)Z', '([0-9T:-]+)'\)\]`",?$"
$matched | Should -BeTrue -Because "startTime should be conditional on the UTC fallback, got: $Line"
$Matches[1] | Should -BeExactly $Matches[2] -Because 'the UTC and non-UTC branches must use the same timestamp, differing only by the trailing Z'
}
}
}
Comment thread
flanakin marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,11 @@ resource dataFactory 'Microsoft.DataFactory/factories@2018-06-01' existing = {
recurrence: {
frequency: 'Hour'
interval: 24
startTime: '2023-01-01T01:01:00'
// Data Factory requires a trailing 'Z' on startTime when timeZone resolves to 'UTC' (the
// fallback for regions not in timeZones.bicep's map); a missing 'Z' fails trigger activation
// with InvalidWorkflowTriggerRecurrence. Mapped, non-UTC regions must keep the no-'Z' format
// so they continue scheduling on local wall-clock time.
startTime: timeZones.outputs.Timezone == 'UTC' ? '2023-01-01T01:01:00Z' : '2023-01-01T01:01:00'
Comment thread
flanakin marked this conversation as resolved.
Comment thread
flanakin marked this conversation as resolved.
timeZone: timeZones.outputs.Timezone
}
}
Expand All @@ -138,7 +142,8 @@ resource dataFactory 'Microsoft.DataFactory/factories@2018-06-01' existing = {
recurrence: {
frequency: 'Month'
interval: 1
startTime: '2023-01-05T01:11:00'
// See trigger_DailySchedule above for why the 'Z' suffix is conditional on the UTC fallback.
startTime: timeZones.outputs.Timezone == 'UTC' ? '2023-01-05T01:11:00Z' : '2023-01-05T01:11:00'
timeZone: timeZones.outputs.Timezone
schedule: {
monthDays: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,11 @@ resource trigger_DailySchedule 'Microsoft.DataFactory/factories/triggers@2018-06
recurrence: {
frequency: 'Hour'
interval: 24
startTime: '2023-01-01T01:01:00'
// Data Factory requires a trailing 'Z' on startTime when timeZone resolves to 'UTC' (the
// fallback for regions not in timeZones.bicep's map); a missing 'Z' fails trigger activation
// with InvalidWorkflowTriggerRecurrence. Mapped, non-UTC regions must keep the no-'Z' format
// so they continue scheduling on local wall-clock time.
startTime: timeZones.outputs.Timezone == 'UTC' ? '2023-01-01T01:01:00Z' : '2023-01-01T01:01:00'
timeZone: timeZones.outputs.Timezone
}
}
Expand Down