Change all Web.*.config files in first stage in release pipeline - azure-devops

I need to change the items inside appsettings on all the Web.*.config files in first stage step. That is I can't do transformation in every step in release pipeline. The reason is that I use Episerver DXC/DXP.
I have 4 stages; "Upload Package", "Integration", "Preproduction", and "Production".
The values is stored i Azure Key Vault.
Is there any smart way to do this?

Did you read the guide on config transforms for DXC? https://world.episerver.com/digital-experience-cloud-service/development-considerations/environment-configurations/

If File transformation is not suitable for your project, what about using powershell script to do the item change?
Sample:
Here is my example web.product.config:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<configSections>
<section name="service.tasks" type="HRNetTaskService.TaskConfigurationSection, TaskService" />
</configSections>
<connectionStrings>
<add name="Production" connectionString="xxxx" providerName="System.Data.SqlClient" />
</connectionStrings>
<appSettings>
<add key="RestServiceUrl" value="https://sample.net" />
</appSettings>
</configuration>
Now I want to update the connectionString of .config file. Add replace.ps1 into repos with below scripts, then call this replace.ps1 file in Powershell task via passing corresponding dynamic value:
Param(
[string]$source,
[string]$connectionstring
)
$webConfig = $source
$doc = (Get-Content $webConfig) -as [Xml]
$root = $doc.get_DocumentElement();
$activeConnection = $root.connectionStrings.SelectNodes("add");
$activeConnection.SetAttribute("connectionString", $connectionstring);
$doc.Save($webConfig)
Here $(ProductValue) is the variable that you configured in Azure key vault. Its call way is same with the pipeline variable. Just you need link the Azure key vault into azure devops, then combine it with Variable group.

What I was trying to do was replace variables in config files from Azure Key Vault before transformation on config files because it can't be done (at this point) during the release pipeline when using Episerver DXC. What I did was replacing them during the build pipeline instead.
Made the variable substitution in Powershell during the build pipeline. Import the Key Vault secrets as separate task before the Powershell task, list all the one I would use as environment variables in the Powershell task.
The environment variables I named the same as the one it should replace in the config files (ex SomeApiKey_Integration). Go through the config files, look for two anything between two double underscores and replace them with value from the environment variable ((Get-ChildItem $variable).Value).
In the config files and environment variable they are named as previous stated, SomeApiKey_Integration. Key Vault name and environment variable value as SomeApiKey-Integration.

Related

How to replace value with more than one attribute in config file in Azure DevOps?

I have a file called entlib.config like below:
<loggingConfiguration name="Logging Application Block" tracingEnabled="true"
defaultCategory="General" logWarningsWhenNoCategoriesMatch="true">
<listeners >
<add name="Email TraceListener" toAddress="axx.xxx#xxx.com" fromAddress="axx.xxx#xxx.com" subjectLineStarter="xxxxx " subjectLineEnder="" smtpServer="xxx.com" smtpPort="xxx" formatter="Text Formatter"
Now,in this file needs to replace fromAddress, toAddress and subjectLineStarter attribute has a variables groups for azure pipeline... how to do this change ? can we do have an option to change any config file that has been created as xml to change more than one attribute ?
To replace the vaules in **.config file with the variables in Variable group, you can try the following methods:
1.You can use the XML variable substitution option in the Azure App service deploy task or IIS deploy task.
For more detailed steps, refer to this doc: XML variable substitution
2.You can use PowerShell script to modify attributes in the config file.
Here is the PowerShell script sample:
$entlibConfig = 'path\entlib.config'
Function updateConfig($config)
{
$doc = (Get-Content $config) -as [Xml]
$root = $doc.get_DocumentElement();
$activeConnection = $root.loggingConfiguration.listeners.SelectNodes("add"); #You need to define the correct location of the parameter
$activeConnection.SetAttribute("fromAddress", "$(fromAddress)");
$activeConnection.SetAttribute("toAddress ", "$(toAddress )“);
$activeConnection.SetAttribute("subjectLineStarter ", "$(subjectLineStarter )“);
$doc.Save($config)
}
updateConfig($entlibConfig)
3.When you add the mark: #{..}# in entlib.config file, you can try to use the Replace Token task from Replace Tokens Extension.
Refer to the example in this ticket:How to perform XML Element substitution in web.config using Replace Tokens?

Update Build number in App config xml file on build pipeline

I have a build pipeline in Azure DevOps, I need to update the build number in my apconfig exe file that will be $(Build.BuildNumber).
I just tried this way:
Adding a variable name = BuildNumber value = $(Build.BuildNumber).
And in my apconfig.exe file have a key same like <add key="BuildNumber" value="1812201901" />.
Why I have tried like this way: thinking like it will update in the config file if variable name match with the key.
But it is not working. can anyone please help? I have just started in CI/CD.
Update Build number in App config xml file on build pipeline
Just like the Shayki said, using the Replace Tokens extension should be the directly way to resolve this issue.
But since you need to request to get this extension, as workaround, you could also use power shell scripts to resolve this issue, you can check below my test powershell scripts:
$currentDirectory = [IO.Path]::GetDirectoryName($MyInvocation.MyCommand.Path)
$appConfigFile = [IO.Path]::Combine($currentDirectory, 'App.config')
$appConfig = New-Object XML
$appConfig.Load($appConfigFile)
foreach($BuildNumber in $appConfig.configuration.add)
{
'name: ' + $BuildNumber.name
'BuildNumber: ' + $BuildNumber.value
$BuildNumber.value = '123456789'
}
$appConfig.Save($appConfigFile)
As result, the app.config:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
</startup>
<add key="BuildNumber" value="123456789" />
</configuration>
Note: Set the powershell scripts at the same folder of the app.config file.
Hope this helps.
You can use the Replace Tokens extension and in the apconfig.exe file put this:
<add key="BuildNumber" value="__BuildNumber__" />
Configure the task to search variables with __ prefix and suffix:
Now the value will be replaced with the value of the BuildNumber variable you configured (equal to Build.BuildNumber).

Azure DevOps Release Pipeline Web.Config Edit

I know that when creating a release pipeline in Azure DevOps you can have the web.config of an app updated with variables from the pipeline and that works great for all the appSettings values.
But, during the release pipeline I'd like to update a different section of the web.config, specifically the sessionState provider node. I've tried a few plugins for the release pipeline like Config Transform by Magic Chunks but the problem is it needs you to specify the path of the configuration file to edit but by the time it gets to the release pipeline the source files are in a zip archive. Somehow the normal transformations of the appSettings are able to work off the unzipped version but I can't get other transformations to happen after the file is unzipped.
I know you can make changes in the build pipeline but there are reasons we want to do it in the release pipeline.
Anyone know a way to make changes to the web.config outside of the appSettings grouping in a release pipeline for an Azure App Service?
You can use PowerShell to do the transformation within the zip file.
For example, I have this node in the web.config:
<configuration>
<sessionstate
mode="__mode__"
cookieless="false"
timeout="20"
sqlconnectionstring="data source=127.0.0.1;user id=<user id>;password=<password>"
server="127.0.0.1"
port="42424"
/>
</configuration>
I use this script:
# cd to the agent artifcats direcory (where the zip file exist)
cd $env:Agent_ReleaseDirectory
$fileToEdit = "web.config"
[Reflection.Assembly]::LoadWithPartialName("System.IO.Compression.FileSystem");
# Open zip and find the particular file (assumes only one inside the Zip file)
$zipfileName = dir -filter '*.zip'
$zip = [System.IO.Compression.ZipFile]::Open($zipfileName.FullName,"Update")
$configFile = $zip.Entries.Where({$_.name -like $fileToEdit})
# Read the contents of the file
$desiredFile = [System.IO.StreamReader]($configFile).Open()
$text = $desiredFile.ReadToEnd()
$desiredFile.Close()
$desiredFile.Dispose()
$text = $text -replace '__mode__',"stateserver"
#update file with new content
$desiredFile = [System.IO.StreamWriter]($configFile).Open()
$desiredFile.BaseStream.SetLength(0)
# Insert the $text to the file and close
$desiredFile.Write($text)
$desiredFile.Flush()
$desiredFile.Close()
# Write the changes and close the zip file
$zip.Dispose()
Before:
After (inside the zip file, without unzip and re-zip):
I was looking to do something similar, but found that there is a built-in task called File Transform [https://learn.microsoft.com/en-us/azure/devops/pipelines/tasks/utility/file-transform?view=azure-devops] by Microsoft. With it, all you have to do is define a variable with the key in web.config if it is a simple substitute. If you need more involved transformation you can specify that too.
Since I had edge case, where I got 405 status on PUT and DELETE as seen here:
WebAPI Delete not working - 405 Method Not Allowed
which required me to change web.config file that is created only when project is released. So I needed to insert couple of lines of code in web.config like:
<modules>
<remove name="WebDAVModule" />
</modules>
and few more.
My answer is based on #Shayki Abramczyk one, I think it offers another, updated, take on this issue.
As his answer did not work fully for me, and for someone who is not professional in field of DevOps, rather programmer that wanted to automate the CI-CD stuff.
Issue I think is present nowadays is that line:
cd $env:Agent_ReleaseDirectory
is not navigating to proper folder. You still need to navigate to the folder and drop where your zip file is like so: cd _Your.Project-CI\drop
So start by adding another PowerShell component in your release pipeline like so:
And add following code to it:
# cd to the agent artifacts directory (where the zip file exist)
cd $env:Agent_ReleaseDirectory
cd _Your.Project-CI\drop
$fileToEdit = "web.config"
[Reflection.Assembly]::LoadWithPartialName("System.IO.Compression.FileSystem");
# Open zip and find the particular file (assumes only one inside the Zip file)
$zipfileName = dir -filter '*.zip'
$zip = [System.IO.Compression.ZipFile]::Open($zipfileName.FullName,"Update")
$configFile = $zip.Entries.Where({$_.name -like $fileToEdit})
# Read the contents of the file
$desiredFile = [System.IO.StreamReader]($configFile).Open()
$text = $desiredFile.ReadToEnd()
$desiredFile.Close()
$desiredFile.Dispose()
$contentToAdd1 = #'
<system.webServer>
<modules>
<remove name="WebDAVModule" />
</modules>
'#
#$text[3] = $text[3] -replace '<system.webServer>',$contentToAdd1
$text = $text -replace '<system.webServer>',$contentToAdd1
$contentToAdd2 = #'
<handlers>
<remove name="WebDAV" />
<remove name="ExtensionlessUrlHandler-Integrated-4.0" />
<add name="ExtensionlessUrlHandler-Integrated-4.0" path="*." verb="GET,HEAD,POST,PUT,DELETE,DEBUG" type="System.Web.Handlers.TransferRequestHandler" resourceType="Unspecified" requireAccess="Script" preCondition="integratedMode,runtimeVersionv4.0" responseBufferLimit="0" />
'#
# $text[4] = $text[4] -replace '<handlers>',$contentToAdd2
$text = $text -replace '<handlers>',$contentToAdd2
#update file with new content
$desiredFile = [System.IO.StreamWriter]($configFile).Open()
$desiredFile.BaseStream.SetLength(0)
# Insert the $text to the file and close
$desiredFile.Write($text)
$desiredFile.Flush()
$desiredFile.Close()
# Write the changes and close the zip file
$zip.Dispose()
Only thing that is left to do is to replace: cd _Your.Project-CI\drop with your project name e.g. cd _Weather.Front-CI\drop.

Securely Signing ClickOnce Applications in Azure DevOps Pipeline

I'm trying to do CI/CD in Azure DevOps with a ClickOnce application. How can I securely make my code signing certificate available during the build when using a hosted agent?
Note I'm aware you can use a script as suggested at Visual studio team services deploymen/buildt certificate error. However this approach is not secure. The certificate would be loaded into the certificate store of the account the hosted agent is running under. This would allow the agent, and hence other Azure DevOps accounts, to potentially access and use the certificate.
The solution to the issue is to override the built in task SignFile. Interestingly enough the task SignFile uses a built in function in Microsoft.Build.Tasks.Deployment.ManifestUtilities.SecurityUtilities.SignFile which has two overloads, one that takes a thumbprint, and one that takes a file and password.
The solution is then to create a new Task that can reference the other overload. Since we cannot change the calling SignFile we need to maintain the same signature, and place the appropriate variables in the environment variables. In this case "CertificateFile" and "CertificatePassword".
Then reference those two in the overwritten SignFile. What I did was to create a new targets file (filesign.targets) and place the code there. Checked that in to my repository and referenced it from the main project file(s).
<Import Project="filesign.targets" />
This way we can also hold our key files in an Azure Key Vault, load them at built and give them a unique password just for that build.
The targets file holds the new FileSign task:
<?xml version="1.0" encoding="Windows-1252"?>
<!--
***********************************************************************************************
Microsoft.VisualStudio.Tools.Office.targets
WARNING: DO NOT MODIFY this file unless you are knowledgeable about MSBuild and have
created a backup copy. Incorrect changes to this file will make it
impossible to load or build your projects from the command-line or the IDE.
This file defines the steps in the standard build process specific for Visual Studio Tools for
Office projects.
Copyright (C) Microsoft Corporation. All rights reserved.
***********************************************************************************************
-->
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<UsingTask TaskName="SignFile" TaskFactory="CodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.Core.dll">
<ParameterGroup>
<SigningTarget Required="true" ParameterType="Microsoft.Build.Framework.ITaskItem" />
<CertificateThumbprint ParameterType="System.String" />
<TargetFrameworkVersion ParameterType="System.String" />
<TimestampUrl ParameterType="System.String" />
<CertificateFile ParameterType="System.String" />
<CertificatePassword ParameterType="System.String" />
</ParameterGroup>
<Task>
<Reference Include="mscorlib" />
<Reference Include="Microsoft.Build.Tasks.Core" />
<Using Namespace="System" />
<Code Type="Fragment" Language="cs">
<![CDATA[
var EnvCertFile = System.Environment.GetEnvironmentVariable("CertificateFile");
Log.LogMessage("CertFile:!!" + EnvCertFile);
if (string.IsNullOrWhiteSpace(CertificateFile) && string.IsNullOrWhiteSpace(EnvCertFile)) {
var signFile = new Microsoft.Build.Tasks.SignFile();
signFile.CertificateThumbprint = CertificateThumbprint;
signFile.SigningTarget = SigningTarget;
signFile.TargetFrameworkVersion = TargetFrameworkVersion;
signFile.TimestampUrl = TimestampUrl;
return signFile.Execute();
} else {
var certificate = string.IsNullOrWhiteSpace(CertificateFile) ? EnvCertFile : CertificateFile;
var EnvCertPassword = System.Environment.GetEnvironmentVariable("CertificatePassword");
var certificatePassword = string.IsNullOrWhiteSpace(CertificatePassword) ? EnvCertPassword : CertificatePassword;
var testString = new System.Security.SecureString();
// Use the AppendChar method to add each char value to the secure string.
if (!string.IsNullOrWhiteSpace(certificatePassword))
foreach (char ch in certificatePassword)
testString.AppendChar(ch);
Microsoft.Build.Tasks.Deployment.ManifestUtilities.SecurityUtilities.SignFile(certificate, testString,
TimestampUrl == null ? null : new Uri(TimestampUrl),
SigningTarget.ItemSpec);
return true;
}
]]>
</Code>
</Task>
</UsingTask>
</Project>
Code based on:
https://gist.github.com/KirillOsenkov/4cd32c40bffd3045f77e
References:
https://github.com/Microsoft/msbuild/blob/fc10ea8ce260b764bb9fa5033b327af9fefcaabe/src/Tasks/ManifestUtil/SecurityUtil.cs
https://github.com/Microsoft/msbuild/blob/master/src/Tasks/SignFile.cs

Can NuGet edit a config file or only add to it?

I've been working on a NuGet package for my company and one of the requirements is being able to update some of our config files.
I know it's possible to add to a config file, but is it possible to edit one?
Example:
<add name="conn" connectionString="Data Source=.\;Initial Catalog=DB;Integrated Security=True" />
changes to below
<add name="conn" connectionString="Data Source=.\;Initial Catalog=DB;User ID=ex;Password=example" />
NuGet transforms can't edit existing values. But NuGet lets you run Powershell scripts on package install, so you can edit the config file that way.
Create an Install.ps1 file and use this code:
# Install.ps1
param($installPath, $toolsPath, $package, $project)
$xml = New-Object xml
# find the Web.config file
$config = $project.ProjectItems | where {$_.Name -eq "Web.config"}
# find its path on the file system
$localPath = $config.Properties | where {$_.Name -eq "LocalPath"}
# load Web.config as XML
$xml.Load($localPath.Value)
# select the node
$node = $xml.SelectSingleNode("configuration/connectionStrings/add[#name='gveconn']")
# change the connectionString value
$node.SetAttribute("connectionString", "Data Source=.\;Initial Catalog=GVE;User ID=ex;Password=example")
# save the Web.config file
$xml.Save($localPath.Value)
As of NuGet 2.6 and above, you can actually transform Web.config files using the XDT syntax that is used for Web.config transforms in Visual studio.
See http://docs.nuget.org/docs/creating-packages/configuration-file-and-source-code-transformations:
Support for XML-Document-Transform (XDT)
Starting with NuGet 2.6, XDT is supported to transform XML files inside a project. The XDT syntax can be utilized in the .install.xdt and .uninstall.xdt file(s) under the package's Content folder, which will be applied during package installation and uninstallation time, respectively.
For example, to add MyNuModule to web.config file like what's illustrated above, the following section can be used in the web.config.install.xdt file:
<?xml version="1.0"?>
<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
<system.webServer>
<modules>
<add name="MyNuModule" type="Sample.MyNuModule" xdt:Transform="Insert" />
</modules>
</system.webServer>
</configuration>
On the other hand, to remove only the MyNuModule element during package uninstall, the following section can be used in the web.config.uninstall.xdt file:
<?xml version="1.0"?>
<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
<system.webServer>
<modules>
<add name="MyNuModule" xdt:Transform="Remove" xdt:Locator="Match(name)" />
</modules>
</system.webServer>
</configuration>
EDIT: The answer is now YES as of NUGET 2.6 and above.
The answer is NO. From the nuget site I found the following answer:
"When NuGet merges a transform file into a project's configuration file, it only adds elements or adds attributes to existing elements in the configuration file; it does not change existing elements or attributes in any other way."
http://docs.nuget.org/docs/creating-packages/configuration-file-and-source-code-transformations
Yes, it's possible, but you have to include install.ps1 file into tools folder. And then when you will get your package from nuget server, visual studio run Powershell scripts.
I use this script
# fileName can be App.Config Or Web.Config or something else
$fileName = "App.Config"
$file=$project.ProjectItems.Item($fileName)
if($file.Properties){
# Get localpath
$localPath = $file.Properties.Item("LocalPath")
if($localPath){
$localPath = $localPath.Value
}
}
if ($localPath -eq $null) {
Exit
}
#Load our config file as XML file
[xml]$file = Get-Content $localPath
if($file){
# Create node
$childNode = $file.CreateElement("add")
$childNode.SetAttribute("connectionString", "DataSource=.\;InitialCatalog=GVE;User ID=ex;Password=example")
#Get parent node
$node = $file.SelectSingleNode("configuration/connectionStrings")
#Insert our node into parent
$node.AppendChild($childNode)
$file.Save($localPath)
}