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.
Related
I've a problem with forceing NAT mode on network cards in all VirtualBox machines. The only solution I have found is to replace the code in the configuration file located in the C:\Users\USER\VirtualBoxVMs\VirtualMachineName\VirtualMachineName.vbox.
Unfortunately, I cannot simply replace these files because each user has different settings (such as the name of the VM)
I have to replace the text in the
<Adapter>...<Adapter> tag. Doing it on one computer for one user is not a problem. I can't deal with the fact that I have to do it with a script on multiple computers for all users. So far, I've managed to do something like this, and I don't know what to do next.
$InputFiles = Get-Item "C:\Users\*\VirtualBox VMs\*\*.vbox"
$OldString = '<Adapter ...various computer-dependent variables...>
...
...
</Adapter>'
$NewString = '<Adapter ...various computer-dependent variables...>
<NAT/>
</Adapter>'
$InputFiles | ForEach {
(Get-Content -Path $_.FullName).Replace($OldString,$NewString) | Set-Content -Path $_.FullName
}
Another complication is that when saving changes, everything is saved in one file for all users, i.e. all settings from all machines on a given computer as one file.
Unfortunately, VB does not store such values in the register, which makes it very difficult. Maybe there is another, easy way?
EDIT: Sample VBox config file
<?xml version="1.0"?>
<!--
** DO NOT EDIT THIS FILE.
** If you make changes to this file while any VirtualBox related application
** is running, your changes will be overwritten later, without taking effect.
** Use VBoxManage or the VirtualBox Manager GUI to make changes.
-->
<VirtualBox xmlns="http://www.virtualbox.org/" version="1.15-windows">
<Machine uuid="{1d96510c-97ce-4671-a1df-3f08c7d2b2c7}" name="TEST" OSType="Windows7_64" snapshotFolder="Snapshots" lastStateChange="2020-05-04T09:56:34Z">
<MediaRegistry>
<HardDisks>
<HardDisk uuid="{eab23143-54ec-4f7d-822d-a56bf90a58bb}" location="TEST.vdi" format="VDI" type="Normal"/>
</HardDisks>
</MediaRegistry>
<ExtraData>
<ExtraDataItem name="GUI/FirstRun" value="yes"/>
</ExtraData>
<Hardware>
<CPU>
<PAE enabled="false"/>
<LongMode enabled="true"/>
<HardwareVirtExLargePages enabled="true"/>
</CPU>
<Memory RAMSize="2048"/>
<HID Pointing="USBTablet"/>
<Paravirt provider="Default"/>
<Display VRAMSize="18"/>
<VideoCapture fps="25" options="ac_enabled=false"/>
<RemoteDisplay enabled="false"/>
<BIOS>
<IOAPIC enabled="true"/>
</BIOS>
<USB>
<Controllers>
<Controller name="OHCI" type="OHCI"/>
<Controller name="EHCI" type="EHCI"/>
</Controllers>
</USB>
<Network>
<Adapter slot="0" enabled="true" MACAddress="MACAddress" cable="true" type="82540EM">
<NAT/>
</Adapter>
</Network>
<AudioAdapter controller="HDA" driver="DirectSound" enabled="true" enabledIn="false"/>
</Hardware>
<StorageControllers>
<StorageController name="SATA" type="AHCI" PortCount="2" useHostIOCache="false" Bootable="true" IDE0MasterEmulationPort="0" IDE0SlaveEmulationPort="1" IDE1MasterEmulationPort="2" IDE1SlaveEmulationPort="3">
<AttachedDevice type="HardDisk" hotpluggable="false" port="0" device="0">
<Image uuid="{eab23143-54ec-4f7d-822d-a56bf90a58bb}"/>
</AttachedDevice>
<AttachedDevice passthrough="false" type="DVD" hotpluggable="false" port="1" device="0"/>
</StorageController>
</StorageControllers>
</Machine>
</VirtualBox>
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.
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).
I'm building the set up to deploy my SSRS reports through Octopus Deploy, I found out one Octopus Library and I'm working on it, but I've had some issues:
1º ---- Message error: (The path is alright, but it keeps with the same warning)
WARNING: Unable to find datasource SalesDrivers in /Sales Drivers/Data Sources
2º ---- The method doesn't exist
Method invocation failed because [Microsoft.PowerShell.Commands.NewWebserviceProxy.AutogeneratedTypes.WebServiceProxy3er_ReportService2005_asmx_wsdl.ReportingService2005] doesn't contain a method named 'LoadReportDefinition'.
The powershell function from the template\library that is throwing the error can been seen below:
#region Update-ReportParamters()
Function Update-ReportParameters($ReportFile)
{
# declare local variables
$ReportParameters = #();
# necessary so that when attempting to use the report execution service, it doesn't puke on you when it can't find the data source
$ReportData = (Remove-SharedReferences -ReportFile $ReportFile)
# get just the report name
$ReportName = $ReportFile.SubString($ReportFile.LastIndexOf("\") + 1)
$ReportName = $ReportName.SubString(0, $ReportName.IndexOf("."))
# create warnings object
$ReportExecutionWarnings = $null
# load the report definition
Write-Host "*********************************************"
#Write-Host $ReportData
#(Remove-SharedReferences -ReportFile $ReportFile)
#Write-Host $ReportExecutionWarnings
$ExecutionInfo = $ReportExecutionProxy.LoadReportDefinition($ReportData, [ref] $ReportExecutionWarnings);
# loop through the report execution parameters
foreach($Parameter in $ExecutionInfo.Parameters)
{
# create new item parameter object
$ItemParameter = New-Object "$ReportServerProxyNamespace.ItemParameter";
# fill in the properties except valid values, that one needs special processing
Copy-ObjectProperties -SourceObject $Parameter -TargetObject $ItemParameter;
# fill in the valid values
$ItemParameter.ValidValues = Convert-ValidValues -SourceValidValues $Parameter.ValidValues;
# add to list
$ReportParameters += $ItemParameter;
}
# force the parameters to update
Write-Host "Updating report parameters for $ReportFolder/$ReportName"
if ($IsReportService2005) {
$ReportServerProxy.SetReportParameters("$ReportFolder/$ReportName", $ReportParameters);
}
elseif ($IsReportService2010) {
$ReportServerProxy.SetItemParameters("$ReportFolder/$ReportName", $ReportParameters);
}
else { Write-Warning 'Report Service Unknown in Update-ReportParameters method. Use ReportService2005 or ReportService2010.' }
}
Anyone knows how I could sort it out?
I have solved a similar problem but took a slightly different approach. Rather than using powershell and octopus directly I used the useful open source tool RSBuild to deploy the reports. It is pretty easy to bundle up the rsbuild.exe executable (it is tiny) and a deploy.config along with your reports inside the octopus package. Then you can use octopus's substitution feature to rewrite the config file and Powershell function to execute the executable. This also has the advantage that you can deploy easily without octopus, the config for data sources and reports is declarative in XML rather than procedural in Powershell and the smarts of your scripted deployment can live alongside your reports rather than buried in Octopus.
So my config looks a bit like:
<?xml version="1.0" encoding="utf-8" ?>
<Settings>
<Globals>
<Global Name="CollapsedHeight">0.5in</Global>
</Globals>
<ReportServers>
<ReportServer Name="RS1" Protocol="http" Host="${ReportServer}" Path="${ReportServerPath}" Timeout="30" />
</ReportServers>
<DataSources>
<DataSource Name="Source1" Publish="true" Overwrite="true" TargetFolder="Data Sources" ReportServer="RS1">
<ConnectionString>data source=${ReportServer};initial catalog=${DatabaseName}</ConnectionString>
<CredentialRetrieval>Store</CredentialRetrieval>
<WindowsCredentials>False</WindowsCredentials>
<UserName>${RepotrUser}</UserName>
<Password>${ReportsPassword}</Password>
</DataSource>
</DataSources>
<Reports>
<ReportGroup Name="Details" DataSourceName="Source1" TargetFolder="Reports"
ReportServer="RS1" CacheTime="10080">
<Report Name="BusinessReportABC">
<FilePath>reports\BusinessReportABC.rdl</FilePath>
</Report>
<!--More reports here-->
</ReportGroup>
</Reports>
</Settings>
My deployed octopacked artefacts contain RSBuild.Core.dll, RSBuild.exe, deploy.config and the reports files
Then I simply call the executable using powershell:
PS> rsbuild deploy.config
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)
}