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
2 changes: 2 additions & 0 deletions docs-mslearn/toolkit/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ The following section lists features and enhancements that are currently in deve

### [FinOps hubs](hubs/finops-hubs-overview.md) v14

- **Changed**
- Added typed metadata contracts between hub apps to formalize dependency management and enable compile-time verification of inter-app interfaces.
- **Fixed**
- Fixed Init-DataFactory deployment script failing when an Event Grid subscription is already provisioning by checking subscription status before attempting subscribe/unsubscribe and polling separately for completion ([#1996](https://github.com/microsoft/finops-toolkit/issues/1996)).

Expand Down
18 changes: 18 additions & 0 deletions src/scripts/Build-Toolkit.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,24 @@ $templates | ForEach-Object {
}
}

# Replace $$ftkver$$ in all Bicep app.bicep files (for metadata version and URLs)
Write-Verbose " Replacing version placeholders in app.bicep files..."
$hubAppFiles = Get-ChildItem "$destDir" -Include 'app.bicep' -Recurse -Force
$replacedCount = 0
$hubAppFiles | ForEach-Object {
$content = Get-Content $_.FullName -Raw
if ($content -match '\$\$ftkver\$\$')
{
Write-Verbose " Replacing version in: $($_.FullName.Replace($destDir, ''))"
$content -replace '\$\$ftkver\$\$', $ver | Out-File $_.FullName -NoNewline
$replacedCount++
}
}
if ($replacedCount -gt 0)
{
Write-Verbose " Replaced version placeholder in $replacedCount file(s)"
}

# Build main.bicep, if applicable
if (Test-Path "$srcDir/main.bicep")
{
Expand Down
29 changes: 26 additions & 3 deletions src/scripts/Package-Toolkit.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -112,9 +112,12 @@ function Copy-TemplateFiles()
}
}

$zip = if ($unversionedZip) {
$zip = if ($unversionedZip)
{
Join-Path (Get-Item $relDir) "$templateName.zip"
} else {
}
else
{
Join-Path (Get-Item $relDir) "$templateName-$tag.zip"
}

Expand Down Expand Up @@ -152,7 +155,27 @@ function Copy-TemplateFiles()
& "$PSScriptRoot/New-Directory" $targetDir

# Copy files and directories
$packageManifest.deployment.Files | ForEach-Object { Copy-Item "$srcPath/$($_.source)" "$targetDir/$($_.destination)" -Force }
$packageManifest.deployment.Files | ForEach-Object {
$destPath = $_.destination
$srcFolder = "$($srcPath.FullName)/$($_.sourceFolder)/".Replace("//", "/")
if (-not (Test-Path $srcFolder))
{
throw "Package manifest references source folder '$($_.sourceFolder)' that does not exist: $srcFolder"
}
Get-ChildItem $srcFolder -Include $_.source -Recurse:$_.recurse | ForEach-Object {
if ($destPath -eq '*')
{
$relativeDest = "$targetDir/$($_.FullName.Replace($srcFolder, ''))"
$destDir = Split-Path $relativeDest -Parent
if ($destDir) { & "$PSScriptRoot/New-Directory" $destDir }
Copy-Item $_ $relativeDest -Force
}
else
{
Copy-Item $_ "$targetDir/$destPath" -Force
}
}
}
$packageManifest.deployment.Directories | ForEach-Object {
& "$PSScriptRoot/New-Directory" "$targetDir/$($_.destination)"
Get-ChildItem "$srcPath/$($_.source)" | Copy-Item -Destination "$targetDir/$($_.destination)" -Recurse -Force
Expand Down
1 change: 1 addition & 0 deletions src/templates/finops-hub/bicepconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
}
},
"experimentalFeaturesEnabled": {
"userDefinedConstraints": true,
"userDefinedFunctions": true
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,18 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import { finOpsToolkitVersion, HubAppProperties } from '../../fx/hub-types.bicep'
import { finOpsToolkitVersion, HubAppProperties, isSupportedVersion } from '../../fx/hub-types.bicep'
import { AppMetadata as CoreMetadata } from '../../Microsoft.FinOpsHubs/Core/metadata.bicep'
import { AppMetadata as ExportsMetadata } from './metadata.bicep'

metadata hubApp = {
id: 'Microsoft.CostManagement.Exports'
version: '$$ftkver$$'
dependencies: [
'Microsoft.FinOpsHubs.Core'
]
metadata: 'https://microsoft.github.io/finops-toolkit/deploy/$$ftkver$$/Microsoft.CostManagement/Exports/metadata.bicep'
}


//==============================================================================
Expand All @@ -11,18 +22,17 @@ import { finOpsToolkitVersion, HubAppProperties } from '../../fx/hub-types.bicep
@description('Required. FinOps hub app getting deployed.')
param app HubAppProperties

@description('Required. Metadata describing shared resources from the Core app. Must be v13 or higher.')
@validate(x => isSupportedVersion(x.version, '13.0', ''), 'Cost Management Exports requires FinOps hubs version 13.0 or higher.')
param core CoreMetadata


//==============================================================================
// Variables
//==============================================================================

var CONFIG = 'config'
var INGESTION = 'ingestion'
var MSEXPORTS = 'msexports'

// Separator used to separate ingestion ID from file name for ingested files
var ingestionIdFileNameSeparator = '__'


//==============================================================================
// Resources
Expand Down Expand Up @@ -53,7 +63,7 @@ module schemaFiles '../../fx/hub-storage.bicep' = {
]
params: {
app: app
container: 'config'
container: core.containers.config
files: {
// cSpell:ignore actualcost, amortizedcost, focuscost, pricesheet, reservationdetails, reservationrecommendations, reservationtransactions
'schemas/actualcost_c360-2025-04.json': loadTextContent('./schemas/actualcost_c360-2025-04.json')
Expand Down Expand Up @@ -102,23 +112,23 @@ resource dataFactory 'Microsoft.DataFactory/factories@2018-06-01' existing = {
}

resource dataset_config 'datasets' existing = {
name: CONFIG
name: core.datasets.config
}

resource dataset_ingestion 'datasets' existing = {
name: INGESTION
name: core.datasets.ingestion
}

resource dataset_ingestion_files 'datasets' existing = {
name: '${INGESTION}_files'
name: core.datasets.ingestionFiles
}

resource dataset_ingestion_manifest 'datasets' existing = {
name: 'ingestion_manifest'
name: core.datasets.ingestionManifest
}

resource dataset_msexports_manifest 'datasets' = {
name: 'msexports_manifest'
name: '${MSEXPORTS}_manifest'
properties: {
parameters: {
fileName: {
Expand Down Expand Up @@ -152,7 +162,7 @@ resource dataFactory 'Microsoft.DataFactory/factories@2018-06-01' existing = {
}

resource dataset_msexports 'datasets' = {
name: replace('${MSEXPORTS}', '-', '_')
name: replace(MSEXPORTS, '-', '_')
properties: {
parameters: {
blobPath: {
Expand Down Expand Up @@ -1003,7 +1013,7 @@ resource dataFactory 'Microsoft.DataFactory/factories@2018-06-01' existing = {
parameters: {
fileName: 'manifest.json'
folderPath: {
value: '@concat(\'${INGESTION}/\', variables(\'destinationFolder\'))'
value: '@concat(\'${core.containers.ingestion}/\', variables(\'destinationFolder\'))'
type: 'Expression'
}
}
Expand Down Expand Up @@ -1059,7 +1069,7 @@ resource dataFactory 'Microsoft.DataFactory/factories@2018-06-01' existing = {
// Triggered by msexports_ExecuteETL
//---------------------------------------------------------------------------
resource pipeline_ToIngestion 'pipelines' = {
name: '${MSEXPORTS}_ETL_${INGESTION}'
name: '${MSEXPORTS}_ETL_${core.containers.ingestion}'
properties: {
activities: [
{ // Get Existing Parquet Files
Expand Down Expand Up @@ -1115,7 +1125,7 @@ resource dataFactory 'Microsoft.DataFactory/factories@2018-06-01' existing = {
}
condition: {
// cSpell:ignore endswith
value: '@and(endswith(item().name, \'.parquet\'), not(startswith(item().name, concat(pipeline().parameters.ingestionId, \'${ingestionIdFileNameSeparator}\'))))'
value: '@and(endswith(item().name, \'.parquet\'), not(startswith(item().name, concat(pipeline().parameters.ingestionId, \'${core.ingestionIdFileNameSeparator}\'))))'
type: 'Expression'
}
}
Expand Down Expand Up @@ -1153,7 +1163,7 @@ resource dataFactory 'Microsoft.DataFactory/factories@2018-06-01' existing = {
value: '@toLower(pipeline().parameters.schemaFile)'
type: 'Expression'
}
folderPath: '${CONFIG}/schemas'
folderPath: '${core.containers.config}/schemas'
}
}
}
Expand Down Expand Up @@ -1274,14 +1284,14 @@ resource dataFactory 'Microsoft.DataFactory/factories@2018-06-01' existing = {
typeProperties: {
variableName: 'destinationPath'
value: {
value: '@concat(pipeline().parameters.destinationFolder, \'/\', pipeline().parameters.ingestionId, \'${ingestionIdFileNameSeparator}\', pipeline().parameters.destinationFile)'
value: '@concat(pipeline().parameters.destinationFolder, \'/\', pipeline().parameters.ingestionId, \'${core.ingestionIdFileNameSeparator}\', pipeline().parameters.destinationFile)'
type: 'Expression'
}
}
}
{ // Convert to Parquet
name: 'Convert to Parquet'
description: 'Convert CSV to parquet and move the file to the ${INGESTION} container.'
description: 'Convert CSV to parquet and move the file to the ${core.containers.ingestion} container.'
type: 'Switch'
dependsOn: [
{
Expand Down Expand Up @@ -1582,7 +1592,7 @@ resource dataFactory 'Microsoft.DataFactory/factories@2018-06-01' existing = {
type: 'DatasetReference'
parameters: {
fileName: 'settings.json'
folderPath: CONFIG
folderPath: core.containers.config
}
}
}
Expand Down Expand Up @@ -1711,8 +1721,20 @@ module trigger_ExportManifestAdded '../../fx/hub-eventTrigger.bicep' = {
@description('Properties of the hub app.')
output app HubAppProperties = app

@description('Name of the container used for Cost Management exports.')
output exportContainer string = exportContainer.outputs.containerName

@description('Number of schema files uploaded.')
output schemaFilesUploaded int = schemaFiles.outputs.filesUploaded

@description('Metadata describing resources created by the Cost Management Exports app.')
output metadata ExportsMetadata = {
id: 'Microsoft.CostManagement.Exports'
version: finOpsToolkitVersion
containers: {
msexports: exportContainer.outputs.containerName
}
datasets: {
msexportsManifest: dataFactory::dataset_msexports_manifest.name
msexports: dataFactory::dataset_msexports.name
msexportsGzip: dataFactory::dataset_msexports_gzip.name
msexportsParquet: dataFactory::dataset_msexports_parquet.name
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

//==============================================================================
// App metadata definition
//==============================================================================

@export()
@description('Metadata for resources created by the Cost Management Exports app.')
type AppMetadata = {
@description('Fully-qualified app identifier.')
id: string
@description('App version.')
version: string
@description('Storage container names.')
containers: {
@description('Container for raw Cost Management export files.')
msexports: string
}
@description('Data Factory dataset names.')
datasets: {
@description('JSON dataset for export manifest files.')
msexportsManifest: string
@description('CSV dataset for raw export files.')
msexports: string
@description('Gzip dataset for compressed export files.')
msexportsGzip: string
@description('Parquet dataset for converted export files.')
msexportsParquet: string
}
}
Loading