wiki.getshifting.com

--- Sjoerd Hooft's InFormation Technology ---

User Tools

Site Tools


start

This is an old revision of the document!


SHIFT-WIKI - Sjoerd Hooft's InFormation Technology

This WIKI is my personal documentation blog. Please enjoy it and feel free to reach out through blue sky if you have a question, remark, improvement or observation.


Azure DevOps Extension: Send email through Graph

Summary: How to get started with the Azure DevOps Extension for sending email through Microsoft Graph.
Date: 9 December 2025

This is a support page for the Azure DevOps Extension: Send email through Graph.
Marketplace link: https://marketplace.visualstudio.com/items?itemName=GetShifting.GraphEmail
GitHub Repository: https://github.com/getshifting/getshifting/tree/main/adoExtensionGraphEmail

Overview

The marketplace extension page provides a brief overview on how to use the extension, and how to configure the requirements. This page provides more examlples and also some background on how the extension creation is done and tested.

Yaml Examples

The examples below are also provided in the marketplace extension page:

This is an example of a task to send a multiline email:

- task: GetShifting.GraphEmail.graph-email-build-task.GraphEmail@0
  displayName: "Send an email with subject Test mail from $(BUILD.DEFINITIONNAME)"
  inputs:
    To: "sjoerd@getshifting.com"
    BCC: "$(BUILD.REQUESTEDFOREMAIL)"
    From: "sjoerd@getshifting.com"
    Subject: "Test mail from $(BUILD.DEFINITIONNAME)"
    Body: |
      <h1>This is a testmail</h1>
      <p>You can use various variables within the body of the email, for example:</p>
 
      <ul>
      <li> Build ID: $(build.buildid) </li>
      <li> Build Directory: $(agent.builddirectory) </li>
      <li> Build Queued By: $(BUILD.QUEUEDBY) </li>
      <li> Agent Name: $(AGENT.NAME) </li>
      <li> Job Name: $(SYSTEM.JOBDISPLAYNAME) </li>
      <li> Task Name: $(SYSTEM.TASKDISPLAYNAME) </li>
      <li> Build Requested for: $(BUILD.REQUESTEDFOR) </li>
      <li> Commit Message: $(BUILD.SOURCEVERSIONMESSAGE) </li>
      </ul>
 
      Kind regards, <br>
      $(BUILD.REQUESTEDFOR)
    ClientID: "e5e6ce84-d241-4faf-97e0-d71a171f1adf"
    ClientSecret: "$(ClientSecret)"
    ShowClientSecret: false
    TenantDomain: getshifting.com

This is an example of a task to send a single line email:

- task: GetShifting.GraphEmail.graph-email-build-task.GraphEmail@0
  displayName: "Send graph email with subject Testmail from $(BUILD.DEFINITIONNAME)"
  inputs:
    To: "sjoerd@getshifting.com"
    From: "sjoerd@getshifting.com"
    Subject: "Testmail from $(BUILD.DEFINITIONNAME)"
    Body: This is a short testmail
    ClientID: "$(ClientID)"
    ClientSecret: "$(ClientSecret)"
    TenantDomain: getshifting.com

Classic Pipeline Example

Even though classic pipelines are not used that much anymore, when I originally created the extension I still mostly used classic pipelines. Below is an example I used in my pipeline to test the extension:

An example of a task to send a multiline email


The Build and Release Pipeline

Below is the almost full build and release pipeline I use to build, test and release the extension to the marketplace. Some notes have been removed, and some of the guids are changed. The tokenized files that are used in the replacetokens task can be reviewed on the public repository of the extension: https://github.com/getshifting/getshifting/tree/main/adoExtensionGraphEmail:

name: $(Build.DefinitionName)-$(Build.BuildId)
appendCommitMessageToRunName: false

variables:
  - group: ExtensionGraph
  - name: ClientID
    value: "42ff8d60-02e0-43dd-9b57-008cbdd86dd2"
  - name: tfxcli
    value: "v0.9.x"
  - name: extensionid
    value: "graphemail"
  - name: publisherprod
    value: "getshifting"
  - name: publisher
    value: "getshifting-private"
  - name: public
    value: "false"
  - name: currentDate
    value: $[format('{0:yyyyMMdd}', pipeline.startTime)]

parameters:
  - name: action
    displayName: Action
    type: string
    default: "Build and Release Privately only"
    values:
      - "Build Only"
      - "Build and Release Privately only"
      - "Build and Release Public"

pool:
  vmImage: windows-latest

trigger: none

resources:
  repositories:
    - repository: self

stages:
  - stage: build
    displayName: "Stage: Build"

    jobs:
      - job: build
        displayName: "Job: Build & Package"
        steps:
          - task: PowerShell@2
            displayName: "Get System Variables"
            condition: eq(variables['System.debug'], true)
            inputs:
              pwsh: true
              targetType: "inline"
              script: |
                Write-Host "`n##[section]Get System Variables`n"
                Get-ChildItem -path env:* | Sort-Object Name

          - task: ms-devlabs.vsts-developer-tools-build-tasks.tfx-installer-build-task.TfxInstaller@5
            displayName: "Use Node CLI for Azure DevOps (tfx-cli): $(tfxcli)"
            inputs:
              version: "$(tfxcli)"

          - task: qetza.replacetokens.replacetokens-task.replacetokens@6
            displayName: 'Replace tokens in vss-extension.json graphEmail\task.json'
            inputs:
              sources: |
                vss-extension.json
                graphEmail\task.json
              tokenPattern: doubleunderscores
              telemetryOptout: true
              root: '$(System.DefaultWorkingDirectory)\public\adoExtensionGraphEmail'
              verbosity: "debug"

          - task: PowerShell@2
            displayName: "Add VstsTaskSdk PowerShell Module to the extension package"
            inputs:
              pwsh: true
              targetType: "inline"
              workingDirectory: '$(System.DefaultWorkingDirectory)\public\adoExtensionGraphEmail'
              script: |
                # Set the verbose flag based on pipeline variable
                if ($env:SYSTEM_DEBUG -eq "True"){
                  Write-Host "##[debug]Verbose logging is enabled"
                  $verboseFlag = $true
                } else {
                  $verboseFlag = $false
                }
 
                Write-Host "##[section]Set Variables"
                $vstsTaskSdkDir = ".\graphEmail\ps_modules\VstsTaskSdk"
                $vstsTempDir = ".\vststemp"
                Write-Host "VstsTaskSdk Directory     : $vstsTaskSdkDir"
                Write-Host "VstsTaskSdk Temp Directory: $vstsTempDir"
 
                if ($verboseFlag) {
                  Write-Host "##[debug]Check current directory structure"
                  Get-Location
                  Get-ChildItem -Recurse
                }
 
                Write-Host "##[section]Save VstsTaskSdk to $vstsTempDir and copy to $vstsTaskSdkDir"
 
                Write-Host "`nCreate required directory structure"
                New-Item -Path $vstsTaskSdkDir -ItemType "Directory" -Verbose:$verboseFlag
                New-Item -Path $vstsTempDir -ItemType "Directory" -Verbose:$verboseFlag
 
                Write-Host "`nSave VstsTaskSdk Module to temporary directory"
                Save-Module -Name VstsTaskSdk -Path $vstsTempDir -Verbose:$verboseFlag
 
                Write-Host "`nCopy all required files to VstsTaskSdk directory"
                Get-ChildItem -Path $vstsTempDir -Recurse -File -Depth 2 | ForEach-Object { Copy-Item -Path $_.FullName -Destination $vstsTaskSdkDir -Verbose:$verboseFlag }
 
                if ($verboseFlag) {
                  Write-Host "##[debug]Check current directory structure"
                  Get-Location
                  Get-ChildItem -Recurse
                }
 
                Write-Host "##[section]Remove temporary directory"
                Remove-Item -Path $vstsTempDir -Recurse -Force -Verbose:$verboseFlag
 
                if ($verboseFlag) {
                  Write-Host "##[debug]Check current directory structure"
                  Get-Location
                  Get-ChildItem -Recurse
                }

          - task: ms-devlabs.vsts-developer-tools-build-tasks.package-extension-build-task.PackageAzureDevOpsExtension@5
            displayName: "Package Extension"
            inputs:
              rootFolder: "$(Build.SourcesDirectory)/public/adoExtensionGraphEmail"
              outputPath: "$(build.artifactstagingdirectory)"
              extensionVersion: "$(currentDate).$(Build.BuildId).$(System.StageAttempt)"

          - task: PublishBuildArtifacts@1
            displayName: "Publish Artifact: extension"
            inputs:
              ArtifactName: extension

  - stage: releasePrivate
    displayName: "Stage: Release Private"
    condition: and(succeeded(),
      or(
      contains('${{ parameters.action }}', 'Build and Release Privately only'),
      contains('${{ parameters.action }}', 'Build and Release Public')))

    jobs:
      - job: releasePrivate
        displayName: "Job: Release Privately"
        steps:
          - task: ms-devlabs.vsts-developer-tools-build-tasks.tfx-installer-build-task.TfxInstaller@5
            displayName: "Use Node CLI for Azure DevOps (tfx-cli): $(tfxcli)"
            inputs:
              version: "$(tfxcli)"

          - task: DownloadPipelineArtifact@2
            inputs:
              artifactName: "extension"
              targetPath: "$(System.DefaultWorkingDirectory)/DevOpsExtensionGraph/extension"

          - task: ms-devlabs.vsts-developer-tools-build-tasks.publish-extension-build-task.PublishAzureDevOpsExtension@5
            displayName: "Publish Extension"
            inputs:
              connectTo: "VsTeam"
              connectedServiceName: "AzureDevOpsMarketPlace"
              fileType: vsix
              vsixFile: "$(System.DefaultWorkingDirectory)/DevOpsExtensionGraph/extension/getshifting-private.GraphEmail-$(currentDate).$(Build.BuildId).$(System.StageAttempt).vsix"
              updateTasksVersion: false
              extensionVisibility: private
              extensionPricing: free

      - job: waitForInstall
        displayName: "Job: Wait for the Private Extension to be installed"
        dependsOn: releasePrivate
        pool: server
        timeoutInMinutes: 1440
        steps:
          - task: ManualValidation@0
            displayName: "Wait for the private extension is installed"
            timeoutInMinutes: 120
            inputs:
              notifyUsers: |
                sjoerd@getshifting.com
              instructions: 'Go to the Marketplace icon in the top right, and select "Manage Extensions". Click on "Send email through Graph by GetShifting-Private" and wait for the extension to be automatically updated to the just released version.'
              onTimeout: "reject"

      - job: testPublic
        displayName: "Job: Test Privately"
        dependsOn: waitForInstall
        steps:
          - task: GetShifting-Private.GraphEmail.graph-email-build-task.GraphEmail@0
            displayName: "Send an email with subject Test mail from $(BUILD.DEFINITIONNAME)"
            inputs:
              To: "sjoerd@getshifting.com"
              BCC: "$(BUILD.REQUESTEDFOREMAIL)"
              From: "sjoerd@getshifting.com"
              Subject: "Test mail from $(BUILD.DEFINITIONNAME)"
              Body: |
                <h1>This is a testmail</h1>
                <p>You can use various variables within the body of the email, for example:</p>
 
                <ul>
                <li> Build ID: $(build.buildid) </li>
                <li> Build Directory: $(agent.builddirectory) </li>
                <li> Build Queued By: $(BUILD.QUEUEDBY) </li>
                <li> Agent Name: $(AGENT.NAME) </li>
                <li> Job Name: $(SYSTEM.JOBDISPLAYNAME) </li>
                <li> Task Name: $(SYSTEM.TASKDISPLAYNAME) </li>
                <li> Build Requested for: $(BUILD.REQUESTEDFOR) </li>
                <li> Commit Message: $(BUILD.SOURCEVERSIONMESSAGE) </li>
                </ul>
 
                Kind regards, <br>
                $(BUILD.REQUESTEDFOR)
              ClientID: "$(ClientID)"
              ClientSecret: "$(ClientSecret)"
              ShowClientSecret: true
              TenantDomain: getshifting.com

  - stage: releasePublic
    displayName: "Stage: Release Public"
    condition: and(succeeded(), eq('${{ parameters.action }}', 'Build and Release Public'))

    jobs:
      - job: releasePublic
        displayName: "Job: Release Publicly"
        steps:
          - task: ms-devlabs.vsts-developer-tools-build-tasks.tfx-installer-build-task.TfxInstaller@5
            displayName: "Use Node CLI for Azure DevOps (tfx-cli): $(tfxcli)"
            inputs:
              version: "$(tfxcli)"

          - task: DownloadPipelineArtifact@2
            inputs:
              artifactName: "extension"
              targetPath: "$(System.DefaultWorkingDirectory)/DevOpsExtensionGraph/extension"

          - task: ms-devlabs.vsts-developer-tools-build-tasks.publish-extension-build-task.PublishAzureDevOpsExtension@5
            displayName: "Publish Extension"
            inputs:
              connectTo: "VsTeam"
              connectedServiceName: "AzureDevOpsMarketPlace"
              fileType: vsix
              vsixFile: "$(System.DefaultWorkingDirectory)/DevOpsExtensionGraph/extension/getshifting-private.GraphEmail-$(currentDate).$(Build.BuildId).$(System.StageAttempt).vsix"
              publisherId: "$(publisherprod)"
              updateTasksVersion: false
              updateTasksId: true
              extensionVisibility: public
              extensionPricing: free

      - job: waitForInstall
        displayName: "Job: Wait for the Public Extension to be installed"
        dependsOn: releasePublic
        pool: server
        timeoutInMinutes: 1440
        steps:
          - task: ManualValidation@0
            displayName: "Wait for the public extension is installed"
            timeoutInMinutes: 120
            inputs:
              notifyUsers: |
                sjoerd@getshifting.com
              instructions: 'Go to the Marketplace icon in the top right, and select "Manage Extensions". Click on "Send email through Graph by GetShifting" and wait for the extension to be automatically updated to the just released version.'
              onTimeout: "reject"

      - job: testPublic
        displayName: "Job: Test Publicly"
        dependsOn: waitForInstall
        steps:
          - task: GetShifting.GraphEmail.graph-email-build-task.GraphEmail@0
            displayName: "Send graph email with subject Testmail from $(BUILD.DEFINITIONNAME)"
            inputs:
              To: "sjoerd@getshifting.com"
              From: "sjoerd@getshifting.com"
              Subject: "Testmail from $(BUILD.DEFINITIONNAME)"
              Body: Test from public
              ClientID: "$(ClientID)"
              ClientSecret: "$(ClientSecret)"
              TenantDomain: getshifting.com

Useful Resources

This wiki has been made possible by:

2025/12/09 20:26

Copilot Prompt to Get All Unattached Disks in an Azure Subscription

Summary: This wiki page shows how I used copilot to get all unattached disks in an Azure subscription. It's part of the 'copilot' tag series in which I showcase the use of copilot in infrastructure related tasks.
Date: 2 December 2025

I've been using copilot for a while now, and I mostly use it without using a prompt. I let it finish my sentences, or create a small comment and wait for a few seconds to let copilot to give me some hints on how to proceed. But sometimes I also use a prompt, and on this page I show you how I used some prompts to create a script.

The hard facts:

  • IDE: VS Code, 1.106.3
  • Copilot Extension Version: 1.388.0
  • Model: Claude Sonnet 4

The Prompt

Actually, I used two prompts. The first prompt was:

I need an azure cli script that will get all disks from an Azure subscription that have the disk state unattached. In the output I want to see the name, the resourcegroup, the last ownership update time, the location, the time created, the disk size, and the following tags: k8s-azure-created-by, kubernetes.io-created-for-pvc-namespace, kubernetes.io-created-for-pvc-name

This created a script, which with some small modifications worked, except that it didn't return the tags. I found that using the azure cli query option didn't retrieve the tags, so I added another prompt:

The output doesn't display the tags. I think the az disk list can't resolve the tag information. I think you need to retrieve the tags separately for each disk

This created a new script, which again needed some small modifications.

The Final Script

Note that this is the script after my modifications. For the modifications, see below. Copilot also provided the possible ways to start the script:

# Display the output for a subscription
.\get-unattached-disks.ps1 -SubscriptionId "your-subscription-id"
# Export to CSV
.\get-unattached-disks.ps1 -SubscriptionId "your-subscription-id" -OutputToCSV -OutputPath "my-disks.csv"
# Get all unattached disks from Azure subscription with detailed information
# This script retrieves disks with state "Unattached" and displays relevant metadata
 
param(
    [string]$SubscriptionId = $null,
    [switch]$OutputToCSV = $false,
    [string]$OutputPath = "unattached-disks.csv"
)
 
# Set subscription if provided
if ($SubscriptionId) {
    Write-Host "Setting subscription to: $SubscriptionId" -ForegroundColor Yellow
    az account set --subscription $SubscriptionId
}
 
# Get current subscription info
$currentSub = az account show --query "{name:name, id:id}" -o json | ConvertFrom-Json
Write-Host "Current subscription: $($currentSub.name) ($($currentSub.id))" -ForegroundColor Green
 
# First get basic disk information for unattached disks
$basicQuery = @"
[?diskState=='Unattached'].{Name: name, ResourceGroup: resourceGroup, LastOwnershipUpdateTime: LastOwnershipUpdateTime, Location: location, TimeCreated: timeCreated, DiskSizeGB: diskSizeGB}
"@
 
Write-Host "Retrieving basic disk information..." -ForegroundColor Yellow
$basicDisks = az disk list --query $basicQuery -o json | ConvertFrom-Json
 
if ($basicDisks.Count -eq 0) {
    Write-Host "No unattached disks found in the subscription." -ForegroundColor Green
    exit 0
}
 
Write-Host "Found $($basicDisks.Count) unattached disk(s). Retrieving tag information..." -ForegroundColor Yellow
 
# Now retrieve tags for each disk individually
$disks = @()
$counter = 0
foreach ($disk in $basicDisks) {
    $counter++
    Write-Progress -Activity "Retrieving disk tags" -Status "Processing disk $counter of $($basicDisks.Count): $($disk.Name)" -PercentComplete (($counter / $basicDisks.Count) * 100)
 
    # Get tags for this specific disk
    $tags = az disk show --name $disk.Name --resource-group $disk.ResourceGroup --query "tags" -o json | ConvertFrom-Json
 
    # Create enriched disk object
    $enrichedDisk = [PSCustomObject]@{
        Name = $disk.Name
        ResourceGroup = $disk.ResourceGroup
        LastOwnershipUpdateTime = $disk.LastOwnershipUpdateTime
        Location = $disk.Location
        TimeCreated = $disk.TimeCreated
        DiskSizeGB = $disk.DiskSizeGB
        CreatedBy = $tags.'k8s-azure-created-by'
        PVCNamespace = $tags.'kubernetes.io-created-for-pvc-namespace'
        PVCName = $tags.'kubernetes.io-created-for-pvc-name'
        #AllTags = $tags
    }
 
    $disks += $enrichedDisk
}
 
Write-Progress -Activity "Retrieving disk tags" -Completed
Write-Host "Completed retrieving tag information for all disks." -ForegroundColor Green
Write-Host ""
 
if ($OutputToCSV) {
    # Output to CSV
    $disks | Export-Csv -Path $OutputPath -NoTypeInformation -Encoding UTF8
    Write-Host "Results exported to: $OutputPath" -ForegroundColor Green
} else {
    # Display in table format
    $disks | Format-Table -AutoSize -Property @(
        'Name',
        'ResourceGroup',
        @{Name='LastOwnershipUpdate'; Expression={$_.LastOwnershipUpdateTime}; Width=20},
        'Location',
        @{Name='TimeCreated'; Expression={$_.TimeCreated}; Width=20},
        @{Name='SizeGB'; Expression={$_.DiskSizeGB}; Width=8},
        @{Name='CreatedBy'; Expression={$_.CreatedBy}; Width=20},
        @{Name='PVCNamespace'; Expression={$_.PVCNamespace}; Width=15},
        @{Name='PVCName'; Expression={$_.PVCName}; Width=15}
    )
}
 
# Summary
Write-Host ""
Write-Host "=== SUMMARY ===" -ForegroundColor Cyan
Write-Host "Total unattached disks: $($disks.Count)" -ForegroundColor White
$totalSizeGB = ($disks | Measure-Object -Property DiskSizeGB -Sum).Sum
Write-Host "Total size: $totalSizeGB GB" -ForegroundColor White
 
# Group by resource group
$byResourceGroup = $disks | Group-Object -Property ResourceGroup | Sort-Object Count -Descending
Write-Host ""
Write-Host "By Resource Group:" -ForegroundColor Cyan
foreach ($group in $byResourceGroup) {
    $groupSizeGB = ($group.Group | Measure-Object -Property DiskSizeGB -Sum).Sum
    Write-Host "  $($group.Name): $($group.Count) disks ($groupSizeGB GB)" -ForegroundColor White
}
 
# Show Kubernetes-related disks
$k8sDisks = $disks | Where-Object { $_.CreatedBy -or $_.PVCNamespace -or $_.PVCName }
if ($k8sDisks.Count -gt 0) {
    Write-Host ""
    Write-Host "Kubernetes-related disks: $($k8sDisks.Count)" -ForegroundColor Magenta
 
    # Show detailed Kubernetes disk information
    Write-Host ""
    Write-Host "Kubernetes Disk Details:" -ForegroundColor Magenta
    $k8sDisks | Format-Table -AutoSize -Property @(
        'Name',
        @{Name='CreatedBy'; Expression={$_.CreatedBy}; Width=20},
        @{Name='PVCNamespace'; Expression={$_.PVCNamespace}; Width=15},
        @{Name='PVCName'; Expression={$_.PVCName}; Width=20},
        @{Name='SizeGB'; Expression={$_.DiskSizeGB}; Width=8}
    )
}

Modifications Made

Tags

In the first version of the script, the query also tried to retrive the tags. This was not allowed by the query syntax, so I had to remove that from the query.

Basic query

Originally, copilot had a very easy to read query, with every field in a separate line. However, that gave the following error:

ERROR: argument --query: invalid jmespath_type value: "[?diskState=='Unattached'].{"
To learn more about --query, please visit: 'https://learn.microsoft.com/cli/azure/query-azure-cli'

I updated the query to be just on one line, which solved the issue.

Basic query fields

Two of the fields the query would ask for did not work. I had to check the actual field names in the output of `az disk list`. The fields `LastOwnershipUpdateTime` and `diskSizeGB` had wrong casing. I updated them to the correct casing.

All Tags

Even though I didin't ask for it, copilot also added the field `AllTags`, which contained all tags as a hashtable. It worked, but it made the csv output messy, so I commented the field out.

This wiki has been made possible by:

2025/12/02 11:08

Thoughts on Upgrading Terraform Providers

Summary: On this wiki page I will cover my experience in upgrading the AzureRM Terraform provider from version 3.112 to 4.51.
Date: 8 November 2025

Recently I had to upgrade the AzureRM terraform provider. We hadn't upgraded it in a while due to other priorities, but once these were addressed, I set about the task of upgrading the provider. On this page I will try to give some background on the process and the provider itself, as well as some of the issues I encountered.

Background

The AzureRM provider is the official Terraform provider for managing Microsoft Azure resources. It is maintained by HashiCorp and Microsoft, and it allows users to define and manage Azure infrastructure using Terraform's declarative configuration language. The provider is regularly updated to add new features, fix bugs, and improve performance.

The latest version can be seen on the terraform registry. It allows you to check available resources and the available options for these resources.

Upgrade Process

Hashicorp, the company behind terraform, provides a general guide on upgrading providers. The process basically comes down to:

  • Get your current deployment stable and working
    • Make sure you can do a `terraform init`, `terraform plan` and `terraform apply` without any issues
    • Create a backup of your terraform state file
    • Also install the latest version of the terraform CLI. Note that if you're running terraform from a pipeline on which you cannot control the terraform CLI version, you should install the same version as installed on the pipeline to avoid any discrepancies.
  • Update provider configuration
    • Update the version in your `provider` block to the desired version
  • Create a plan
    • Run `terraform init` to download the new provider version
      • Use the `-upgrade` flag to ensure that the latest version is downloaded
    • Run `terraform plan` to see what changes will be made
  • Identify and document errors, warnings, or actions
    • Tackle one issue at a time. Start with errors, and only proceed to warnings once all errors are resolved. I had several occurences that after removing certain errors, also warnings dissapeared.
    • Refactor your configuration as needed
  • Apply your plan
    • Run `terraform apply` to implement the changes

And an additional note, if you're working with a pipeline to deploy your terraform code, try to be able to perform `terraform plan` locally first. This will save you a lot of time.

Update the Provider Version

This was our old provider block:

terraform {
  required_version = ">= 0.14.9"
 
  required_providers {
    azurerm = {
      source = "hashicorp/azurerm"
      version = "3.112"
    }
  }
}

And this is our new provider block. As you can see, only the version number has changed:

terraform {
  required_version = ">= 0.14.9"
 
  required_providers {
    azurerm = {
      source = "hashicorp/azurerm"
      version = "4.51.0"
    }
  }
}

Run the Plan Locally

The following commands were run in a PowerShell terminal on my local machine:

az login
az account set --subscription 30b3c71d-a123-a123-a123-abcd12345678
cd "C:\Repos\tf\applications\main"
terraform version
# Do a normal terraform init. It will tell you to use -upgrade to get the latest provider version
terraform init `
  -backend-config=storage_account_name="saeuwdevtf" `
  -backend-config=container_name="terraform" `
  -backend-config=key="dev.tfstate" `
  -backend-config=resource_group_name="rg-euw-dev-tf" `
  -backend-config=subscription_id="30b3c71d-a123-a123-a123-abcd12345678"
# So do a terraform init with -upgrade
terraform init `
  -backend-config=storage_account_name="saeuwdevtf" `
  -backend-config=container_name="terraform" `
  -backend-config=key="dev.tfstate" `
  -backend-config=resource_group_name="rg-euw-dev-tf" `
  -backend-config=subscription_id="30b3c71d-a123-a123-a123-abcd12345678" `
  -upgrade
# Then do a plan, and make sure to provide your tfvars file
terraform plan `
  -var-file="env/dev.tfvars" `
  -input="false"

Issues

When running the `terraform plan` command, several issues are to be expected. I'll address some of the issues I encountered below.

Deprecated Attributes

We had some resources with deprecated attributes. Especially the Kubernetes cluster resource had many attributes that were no longer supported. The way to tackle them is like this:

  • Lookup the resource in the terraform registry
  • Try to find the deprecated attribute. Sometimes it's only renamed, but then the description stayed the same. Other times it is completely removed. If that's the case it's isually mentioned in the old version of the documentation.
    • To find the old version of the documentation, you can use the version selector (it's almost at the top) on the terraform registry page of the resource.
  • Rename or remove the attribute in your terraform code

New Default Values

Both on the kubernetes resource as well as on storage account we had some attributes that had new default values. This can happen because the new provider suddenly has support for an attribute and sets a default value different from the Azure default value. These settings are usually new, so you need to check them thoroughly. For example, on the storage account the options to allow for cross tenant copy got disabled. And for the kubernetes cluster the node upgrade channel got changed. This actually caused an node image upgrade during the night because I missed that during the plan.

Provider Changes

We also had some errors and warning for the provider itself. It suddenly required a subscription id and also some of the attributes of the provider got renamed.

This was our old provider configuration:

provider "azurerm" {
  skip_provider_registration = true
  features {
    virtual_machine {
      skip_shutdown_and_force_delete = true
      delete_os_disk_on_deletion     = true
    }
  }
}

And this is our new provider configuration:

provider "azurerm" {
  subscription_id                 = var.env_subscription_id
  resource_provider_registrations = "none"
  features {
    virtual_machine {
      skip_shutdown_and_force_delete = true
      delete_os_disk_on_deletion     = true
    }
  }
}
Note that adding the subscription id solved a lot of decoding errors:
│ Warning: Failed to decode resource from state
│
│ Error decoding "module.restore_storage_account.module.diagnostic_settings.azurerm_monitor_diagnostic_setting.diagnostic_setting[0]"
│ from prior state: unsupported attribute "log"

Conclusion

Upgrading terraform providers can be a tedious task, especially when there are many breaking changes. However, by following a systematic approach and addressing issues one at a time, the process can be managed effectively. Always ensure to back up your state file before making any changes, and test thoroughly after the upgrade to ensure everything is functioning as expected. The whole process took me two working days with time for other issues and tasks as well in between.

This wiki has been made possible by:

2025/11/08 16:23

How to Add Text to Each File in a Directory in a Repository

Summary: This wiki page shows how I added google ads to each wiki page in a Repository using powershell, and use a VS Code extension to make sure new files get the same treatment.
Date: 25 October 2025

Background

When using Google Auto Adsense on my wiki, I get ads like everywhere, which makes the pages look cluttered and hard to read. My solution is to add an ad at the bottom of each page. However, I don not want to do that manually.

Using a VS Code extension to run a script on file save

I originally wanted to use git hooks to run a script. However, git hooks have a notourious bad reputation when running them on Windows. So instead I opted to use a VS Code extension that runs a script when saving a file.

I previously used this Run on Save extension for work, which worked very well, so I decided to use it here again.

After installing the extension, you need to add some configuration to your settings.json file. I've used the vscode settings extensively before, so I just needed to add the following settings:

    "emeraldwalk.runonsave": {
        // Messages to show before & after all commands
        "message": "*** All Start ***",
        "messageAfter": "*** All Complete ***",
        // Show elappsed time for all commands
        "showElapsed": true,
        "commands": [
            {
                "match": "\\.txt$",
                "notMatch": "\\drafts/.*$",
                "cmd": "pwsh.exe -ExecutionPolicy Bypass -File ${workspaceFolder}\\ads\\Add-AdsToWikiPages.ps1"
            }
        ]
    }

This will run the powershell script located in the ads folder every time a .txt file is saved in the dokuwiki/pages folder.

The Powershell script

This is the powershell script me and copilot created together and I tested very thoroughly before using it on my actual wiki pages:

  • It starts with defining the function that can later be called with parameters.
  • It has an option to exclude certain files from being modified.
  • It checks if the string to be added is already present in the file, and only adds it if it's not there yet.
  • It appends the string at the end of the file, ensuring proper formatting with new lines.
function Add-StringToFiles {
  param(
    [Parameter(Mandatory)]
    [string]$FolderPath,
    [Parameter(Mandatory)]
    [string]$StringToAdd,
    [string[]]$ExcludeList = @()
  )
 
  Get-ChildItem -Path $FolderPath -File -Filter "*.txt" | ForEach-Object {
    $filePath = $_.FullName
    $fileName = $_.Name
 
    # Skip if file is in exclude list
    if ($ExcludeList -contains $fileName) {
      Write-Host "Skipping excluded file: $fileName"
      return
    }
 
    $content = Get-Content -Path $filePath -Raw
 
    if ($content -notmatch [regex]::Escape("//This wiki has been made possible by://")) {
      $newContent = "$content`r`n$StringToAdd`r`n"
      Set-Content -Path $filePath -Value $newContent
    }
  }
}
 
$folder = "C:\Repos\GetShifting\knowledge\dokuwiki\pages"
$excludeFiles = @("overview.txt", "alltags.txt", "sidebar.txt")
$adString = '//This wiki has been made possible by://
 
<HTML>
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-8613096447910897"
     crossorigin="anonymous"></script>
<!-- Wiki End of Page -->
<ins class="adsbygoogle"
     style="display:block"
     data-ad-client="ca-pub-8613096447910897"
     data-ad-slot="6221699236"
     data-ad-format="auto"
     data-full-width-responsive="true"></ins>
<script>
     (adsbygoogle = window.adsbygoogle || []).push({});
</script>
</HTML>
'
 
Add-StringToFiles -FolderPath $folder -StringToAdd $adString -ExcludeList $excludeFiles

This wiki has been made possible by:

2025/10/25 18:06
start.1748779162.txt.gz · Last modified: by 127.0.0.1