Build Once, Deploy Everywhere with Web Deploy and .NET Web.configs - deployment

I am working on setting up a continuous build and deploy system that will manage builds and deployments of our .NET applications to multiple environments. We want to do this so that we build, deploy that build to our development environment, and at a later time have the option to deploy that same build to our test environment with different config file settings. Currently our developers are used to using web.config transforms to manage the config values for each environment and they would prefer to continue doing it that way. Finally, we want to do our deployments with MS Web Deploy 3.6 and its package deploy option.
After doing some research we have found and considered the following options:
Use the Web Deploy parameterization feature to change the config files at deploy time. This would replace the web.config transformations which we would like to avoid.
Run MSBuild once per project configuration/web.config transform to produce a package containing a transformed web.config for each environment. This has the downside of increasing build times and storage requirements for our packages.
Use both Web Deploy parameterization and web.config transformations. This allows developers to continue to use web.configs to debug other environments as they have been and avoids creating multiple packages, but requires us to maintain config settings in multiple places.
At build time, use the web.config transformations to generate multiple config files but only one package and at deploy time use a script to insert the proper config into the correct location of in the package. This seems easier said than done since it's not the way Web Deploy was designed to work and our on initial evaluation appears complicated to implement.
Are there any other options that we haven't considered? Is there a way to do this that allows us to keep using web.configs as we have been but only generate a single Web Deploy package?

In .NET 4.7.1, another option is possible: using ConfigurationBuilder.
The idea is that a custom class has the opportunity to manipulate the values contained in the web.config before they are passed to the application. This allows to plug in other configuration systems.
For example: Using a similar approach to configuration as ASP.NET Core, the NuGet packages it includes can be used independently on .NET Framework as well to load json and override json files. Then an environment variable (or any other value like IIS app pool ID, machine name etc.) can be used to determine which override json file to use.
For example: If there was an appsettings.json file like
{
"appSettings": { "Foo": "FooValue", "Bar": "BarValue" }
}
and an appsettings.Production.json file containing
{
"appSettings": { "Foo": "ProductionFooValue" }
}
One could write a config builder like
public class AppSettingsConfigurationBuilder : ConfigurationBuilder
{
public override ConfigurationSection ProcessConfigurationSection(ConfigurationSection configSection)
{
if(configSection is AppSettingsSection appSettingsSection)
{
var appSettings = appSettingsSection.Settings;
var environmentName = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production";
var appConfig = new ConfigurationBuilder()
.AddJsonFile("appsettings.json", optional: false)
.AddJsonFile($"appsettings.{environmentName}.json", optional: true)
.Build();
appSettings.Add("Foo", appConfig["appSettings:Foo"]);
appSettings.Add("Bar", appConfig["appSettings:Bar"]);
}
return configSection;
}
}
and then wire up the config builder in Web.config:
<configSections>
<section name="configBuilders" type="System.Configuration.ConfigurationBuildersSection, System.Configuration, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" restartOnExternalChanges="false" requirePermission="false"/>
</configSections>
<configBuilders>
<builders>
<add name="AppSettingsConfigurationBuilder" type="My.Project.AppSettingsConfigurationBuilder, My.Project"/>
</builders>
</configBuilders>
<appSettings configBuilders="AppSettingsConfigurationBuilder" />
If you then set the ASPNETCORE_ENVIRONMENT (name only chosen so it ASP.NET Core Apps on the same server would use the same be default) environment variable to Development on dev machines, ConfigurationManager.AppSettings["Foo"] would see FooValue instead of FooProductionValue.
You could also use the APP_POOL_ID to hardcode environment names or use the IIS 10 feature to set environment variables on app pools. This way you can truly build once and copy the same output to different servers or even to multiple directories on the same server and still use a different config for different server.

I don't know if it's less complicated than option 4 above, but the solution we are going with is to run a PowerShell script immediately prior to running MSBuild which parses the web.config transforms and either generates or augments the parameters.xml file. This gives us the flexibility to use parameterization and it's ability to modify config files other than the web.config while preserving 100% of the current functionality of the web.config transformations. Here is the script we are currently using for the benefit of future seekers:
function Convert-XmlElementToString
{
[CmdletBinding()]
param([Parameter(Mandatory=$true)] $xml, [String[]] $attributesToExclude)
$attributesToRemove = #()
foreach($attr in $xml.Attributes) {
if($attr.Name.Contains('xdt') -or $attr.Name.Contains('xmlns') -or $attributesToExclude -contains $attr.Name) {
$attributesToRemove += $attr
}
}
foreach($attr in $attributesToRemove) { $removedAttr = $xml.Attributes.Remove($attr) }
$sw = New-Object System.IO.StringWriter
$xmlSettings = New-Object System.Xml.XmlWriterSettings
$xmlSettings.ConformanceLevel = [System.Xml.ConformanceLevel]::Fragment
$xmlSettings.Indent = $true
$xw = [System.Xml.XmlWriter]::Create($sw, $xmlSettings)
$xml.WriteTo($xw)
$xw.Close()
return $sw.ToString()
}
function BuildParameterXml ($name, $match, $env, $value, $parameterXmlDocument)
{
$existingNode = $parameterXmlDocument.selectNodes("//parameter[#name='$name']")
$value = $value.Replace("'","&apos;") #Need to make sure any single quotes in the value don't break XPath
if($existingNode.Count -eq 0){
#no existing parameter for this transformation
$newParamter = [xml]("<parameter name=`"" + $name + "`">" +
"<parameterEntry kind=`"XmlFile`" scope=`"\\web.config$`" match=`"" + $match + "`" />" +
"<parameterValue env=`"" + $env + "`" value=`"`" />" +
"</parameter>")
$newParamter.selectNodes('//parameter/parameterValue').ItemOf(0).SetAttribute('value', $value)
$imported=$parameterXmlDocument.ImportNode($newParamter.DocumentElement, $true)
$appendedNode = $parameterXmlDocument.selectNodes('//parameters').ItemOf(0).AppendChild($imported)
} else {
#parameter exists but entry is different from an existing entry
$entryXPath = "//parameter[#name=`"$name`"]/parameterEntry[#kind=`"XmlFile`" and #scope=`"\\web.config$`" and #match=`"$match`"]"
$existingEntry = $parameterXmlDocument.selectNodes($entryXPath)
if($existingEntry.Count -eq 0) { throw "There is web.config transformation ($name) that conflicts with an existing parameters.xml entry" }
#parameter exists but environment value is different from an existing environment value
$envValueXPath = "//parameter[#name='$name']/parameterValue[#env='$env' and #value='$value']"
$existingEnvValue = $parameterXmlDocument.selectNodes($envValueXPath)
$existingEnv = $parameterXmlDocument.selectNodes("//parameter[#name=`"$name`"]/parameterValue[#env=`"$env`"]")
if($existingEnvValue.Count -eq 0 -and $existingEnv.Count -gt 0) {
throw "There is web.config transformation ($name) for this environment ($env) that conflicts with an existing parameters.xml value"
} elseif ($existingEnvValue.Count -eq 0 -and $existingEnv.Count -eq 0) {
$newParamter = [xml]("<parameterValue env=`"" + $env + "`" value=`"`" />")
$newParamter.selectNodes('//parameterValue').ItemOf(0).SetAttribute('value', $value)
$imported=$parameterXmlDocument.ImportNode($newParamter.DocumentElement, $true)
$appendedNode = $existingNode.ItemOf(0).AppendChild($imported)
}
}
}
function UpdateSetParams ($node, $originalXml, $path, $env, $parametersXml)
{
foreach ($childNode in $node.ChildNodes)
{
$xdtValue = ""
$name = ""
$match = ($path + $childNode.toString())
if($childNode.Attributes -and $childNode.Attributes.GetNamedItem('xdt:Locator').Value) {
$hasMatch = $childNode.Attributes.GetNamedItem('xdt:Locator').Value -match ".?\((.*?)\).*"
$name = $childNode.Attributes.GetNamedItem($matches[1]).Value
$match = $match + "[#" + $matches[1] + "=`'" + $name + "`']"
}
if($childNode.Attributes -and $childNode.Attributes.GetNamedItem('xdt:Transform')) {
$xdtValue = $childNode.Attributes.GetNamedItem('xdt:Transform').Value
}
if($xdtValue -eq 'Replace') {
if($childNode.Attributes.GetNamedItem('xdt:Locator').Value) {
$hasMatch = $childNode.Attributes.GetNamedItem('xdt:Locator').Value -match ".?\((.*?)\).*"
$name = $childNode.Attributes.GetNamedItem($matches[1]).Value
} else {
$name = $childNode.toString()
}
$nodeString = Convert-XmlElementToString $childNode.PsObject.Copy()
BuildParameterXml $name $match $env $nodeString $parametersXml
} elseif ($xdtValue.Contains('RemoveAttributes')) {
if($originalXml.selectNodes($match).Count -gt 0) {
$hasMatch = $xdtValue -match ".?\((.*?)\).*"
$nodeString = Convert-XmlElementToString $originalXml.selectNodes($match).ItemOf(0).PsObject.Copy() $matches[1].Split(',')
$newParamter = BuildParameterXml $childNode.toString() $match $env $nodeString $parametersXml
$newParamters += $newParamter
}
} elseif ($xdtValue.Contains('SetAttributes')) {
if($originalXml.selectNodes($match).Count -gt 0) {
$nodeCopy = $originalXml.selectNodes($match).ItemOf(0).PsObject.Copy()
$hasMatch = $xdtValue -match ".?\((.*?)\).*"
foreach($attr in $matches[1].Split(',')){
$nodeCopy.SetAttribute($attr, $childNode.Attributes.GetNamedItem($attr).Value)
}
$nodeString = Convert-XmlElementToString $nodeCopy
BuildParameterXml $childNode.toString() "($match)[1]" $env $nodeString $parametersXml
}
} elseif ($xdtValue) {
throw "Yikes! the script doesn't know how to handle this transformation!"
}
#Recurse into this node to check if it has transformations on its children
if($childNode) {
UpdateSetParams $childNode $originalXml ($match + "/") $env $parametersXml
}
}
}
function TransformConfigsIntoParamters ($webConfigPath, $webConfigTransformPath, $parametersXml)
{
#Parse out the environment names
$hasMatch = $webConfigTransformPath -match ".?web\.(.*?)\.config.*"
[xml]$transformXml = Get-Content $webConfigTransformPath
[xml]$webConfigXml = Get-Content $webConfigPath
UpdateSetParams $transformXml $webConfigXml '//' $matches[1] $parametersXml
}
$applicationRoot = $ENV:WORKSPACE
if(Test-Path ($applicationRoot + '\parameters.xml')) {
[xml]$parametersXml = Get-Content ($applicationRoot + '\parameters.xml')
$parametersNode = $parametersXml.selectNodes('//parameters').ItemOf(0)
} else {
[System.XML.XMLDocument]$parametersXml=New-Object System.XML.XMLDocument
[System.XML.XMLElement]$parametersNode=$parametersXml.CreateElement("parameters")
$appendedNode = $parametersXml.appendChild($parametersNode)
}
TransformConfigsIntoParamters ($applicationRoot + '\web.config') ($applicationRoot + '\web.Development.config') $parametersXml
TransformConfigsIntoParamters ($applicationRoot + '\web.config') ($applicationRoot + '\web.SystemTest.config') $parametersXml
TransformConfigsIntoParamters ($applicationRoot + '\web.config') ($applicationRoot + '\web.Production.config') $parametersXml
$parametersXml.Save($applicationRoot + '\parameters.xml')

Related

Is it possible to create specific constant elements of a PowerShell array?

Let's say I have array Grid that was initialized with
$Grid = #(#(1..3), #(4..6), #(7..9))
and I want to change Grid[0][0] to value "Test" but I want to make it unchangeable, is there a way I could to that?
So far I've tried playing around with classes that allow read-only or constant declarations through the class as opposed to using New-Variable/Set-Variable but it doesn't affect the index itself but the individual element as in
$Grid[0][0] = [Array]::AsReadOnly(#(1,2,3))
$Grid[0][0] # 1 \n 2 \n 3
$Grid[0][0].IsReadOnly # True
$Grid[0][0] = "test"
$Grid[0][0] # test
I assume this is due to $Grid[0][0] being read-only as opposed to constant and the behaviour I experienced supported that:
$test = [Array]::AsReadOnly(#(1,2,3,4))
$test[0]=1 # Errors
$test = "test"
$test # test
$Grid = #(#(1..3), #(4..6), #(7..9))
$Grid[0][0] = [Array]::AsReadOnly(#(1,2,3))
$Grid[0][0][0] = 1 # Errors
$Grid[0][0] = "test"
$Grid[0][0] # test
I'm not sure what to try next and I know that this is very simple with classes but I am not looking for that as a solution.
You'll have to make both dimensions of your nested array read-only to prevent anyone from overwriting $grid[0]:
$grid =
[array]::AsReadOnly(#(
,[array]::AsReadOnly(#(1,2,3))
,[array]::AsReadOnly(#(3,2,1))
))
(the unary , is not a typo, it prevents PowerShell from "flattening" the resulting read-only collection)
Now $grid should behave as you expect:
$grid[0] # 1,2,3
$grid[0][0] # 1
$grid[0][0] = 4 # error
$grid[0] = 4 # error
If you want to be able to prevent writing to individual "cells", you'll have to define a custom type:
using namespace System.Collections
class Grid {
hidden [int[,]] $data
hidden [bool[,]] $mask
Grid([int]$width,[int]$height){
$this.mask = [bool[,]]::new($width, $height)
$this.data = [int[,]]::new($width, $height)
}
[int]
Get([int]$x,[int]$y)
{
if(-not $this.CheckBounds($x,$y)){
throw [System.ArgumentOutOfRangeException]::new()
}
return $this.data[$x,$y]
}
Set([int]$x,[int]$y,[int]$value)
{
if(-not $this.CheckBounds($x,$y)){
throw [System.ArgumentOutOfRangeException]::new()
}
if(-not $this.mask[$x,$y])
{
$this.data[$x,$y] = $value
}
else
{
throw [System.InvalidOperationException]::new("Cell [$x,$y] is currently frozen")
}
}
Freeze([int]$x,[int]$y)
{
if(-not $this.CheckBounds($x,$y)){
throw [System.ArgumentOutOfRangeException]::new()
}
$this.mask[$x,$y] = $true
}
Unfreeze([int]$x,$y)
{
if(-not $this.CheckBounds($x,$y)){
throw [System.ArgumentOutOfRangeException]::new()
}
$this.mask[$x,$y] = $false
}
hidden [bool]
CheckBounds([int]$x,[int]$y)
{
return (
$x -ge $this.data.GetLowerBound(0) -and
$x -le $this.data.GetUpperBound(0) -and
$y -ge $this.data.GetLowerBound(1) -and
$y -le $this.data.GetUpperBound(1)
)
}
}
Now you can do:
$grid = [Grid]::new(5,5)
$grid.Set(0, 0, 1) # Set cell value
$grid.Get(0, 0) # Get cell value
$grid.Freeze(0, 0) # Now freeze cell 0,0
$grid.Set(0, 0, 2) # ... and this will now throw an exception
$grid.Set(0, 1, 1) # Setting any other cell still works
If you want native support for index expressions (ie. $grid[0,0]), the Grid class will need to implement System.Collections.IList

Get XML node children using a variable

Given the following file:
<environments>
<dev>
<property1>23</property1>
<property2>blue</property2>
<property3>apple</property3>
</dev>
<prod>
<property1>27</property1>
<property2>red</property2>
<property3>orange</property3>
</prod>
</environments>
And the following code:
$environmentsFile = [System.Xml.XmlDocument](Get-Content "environments.xml");
$envs = $environmentsFile.environments;
$envName = "prod"
How would I access the "prod" children and iterate through them? I'm already iterating through the environments using ForEach ($env in $envs.ChildNodes) {} and I know I oculd do $envs.prod
I tried this:
$test = $envs.SelectNodes("//$envName")
ForEach ($ele in $test) {
Write-Host $ele.value
}
But it didn't output anything. Inspecting reveals that it looks like $test has a single node that contains all the nodes I want, so I tried $test[0] and that didn't work either.
I had done a couple things wrong. This functions as desired:
$test = $envs.SelectNodes("//$envName")[0]
ForEach ($ele in $test.ChildNodes) {
Write-Host "$($ele.Name): $($ele.InnerText)"
}

Test Connection and loop if true or false (change colors...) [duplicate]

Something is really weird with this language. I'm trying to execute a function and use its result value as condition. This is my code:
function Get-Platform()
{
# Determine current Windows architecture (32/64 bit)
if ([System.Environment]::GetEnvironmentVariable("ProgramFiles(x86)") -ne $null)
{
echo "x64"
return "x64"
}
else
{
echo "x86"
return "x86"
}
}
if (Get-Platform -eq "x64")
{
echo "64 bit platform"
}
if (Get-Platform -eq "x86")
{
echo "32 bit platform"
}
The expected output is this:
x64
64 bit platform
But the actual output is this:
64 bit platform
32 bit platform
What's going on here? How can this be fixed? I couldn't find any examples on the web that use functions inside an ifcondition. Is that possible at all in Powershell? I'm on Windows 7 with no special setup, so I have whatever PS version comes with it.
If you want to compare the return value of a function in a conditional, you must group the function call (i.e. put it in parentheses) or (as #FlorianGerhardt suggested) assign the return value of the function to a variable and use that variable in the conditional. Otherwise the comparison operator and the other operand would be passed as arguments to the function (where in your case they're silently discarded). Your function then returns a result that is neither "" nor 0 nor $null, so it evaluates to $true, causing both messages to be displayed.
This should do what you want:
...
if ( (Get-Platform) -eq 'x64' ) {
echo "64 bit platform"
}
...
BTW, you should avoid using separate if statements for conditions that are mutually exclusive. For a platform check an if..then..elseif
$platform = Get-Platform
if ($platform -eq "x64") {
...
} elseif ($platform -eq "x86") {
...
}
or a switch statement
Switch (Get-Platform) {
"x86" { ... }
"x64" { ... }
}
would be more appropriate.
I'd also avoid echoing inside the function. Just return the value and do any echoing that might be required in with the returned value. Anything echoed inside the function will also be returned to the caller.
One last note: personally I'd rather not rely on the existence of a particular folder or environment variable for determining the operating system architecture. Using WMI for this task deems me a lot more reliable:
function Get-Platform {
return (gwmi Win32_OperatingSystem).OSArchitecture
}
This function will return a string "32-Bit" or "64-Bit", depending on the operating system architecture.
I think you are comparing a function and not the function result. Also somehow the echo does not work as expected in a function. I usually use Write-Host.
Here is my solution to your problem:
function Get-Platform()
{
# Determine current Windows architecture (32/64 bit)
if ([System.Environment]::GetEnvironmentVariable("ProgramFiles(x86)") -ne $null)
{
Write-Host("x64")
return "x64"
}
else
{
Write-Host("x86")
return "x86"
}
}
$platform = Get-Platform
if ($platform -eq 'x64')
{
echo "64 bit platform"
}
if ($platform -eq 'x86')
{
echo "32 bit platform"
}

Tidying up a powershell script

So I need help tidying up a script that I have. The purpose of this script is to make 18 different sql files based on the data below 18 different column headers. What my script does now is make 1 sql file based on which column I choose to input via "Read-Host". This is my current script
function get-header
{Read-Host "Type the Column header betwen B-Z for which sql files needs to be created"
}
function get-column
{
Read-Host "Type the Column number"
}
do
{
$val = get-header
}
while(!($val))
do
{$col = get-column
}
while(!($col))
switch ($val)
{
"B"{$column = "1"}
"C"{$column = "2"}
"D"{$column = "3"}
"E"{$column = "4"}
"F"{$column = "5"}
"G"{$column = "6"}
"H"{$column = "7"}
"I"{$column = "8"}
"J"{$column = "9"}
"K"{$column = "10"}
"L"{$column = "11"}
"M"{$column = "12"}
"N"{$column = "13"}
"O"{$column = "14"}
"P"{$column = "15"}
"Q"{$column = "16"}
"R"{$column = "17"}
"S"{$column = "18"}
"T"{$column = "19"}
"U"{$column = "20"}
"V"{$column = "21"}
"W"{$column = "22"}
"X"{$column = "23"}
"Y"{$column = "24"}
"Z"{$column = "25"}
default { $column = 'Unknown' }
}
if ($column -eq 'Unknown')
{
Write-Warning "Not a valid input"
return
}
$csv = Import-Csv "Indices Updates - September 2018.csv" -Header 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26
$date = (Get-Date -Format "MM/dd/yyyy").Replace("-","/")
$sql = #("INSERT INTO benchmark_values(created_at,benchmark_id,date,amount,created_by_username)
foreach($data in $csv)
{
$secondcolumn = [int]$column + 1
$sql += "('$date',$col,'$($data.1)',$($data.$secondcolumn),'BPylla'),"
}
$sql | Out-File "sqldata.sql"
Now I want to get rid of read-host entirely because I dont want to input any values. I also will give an example of what the csv file looks like and what the sql file should look like.
So the goal is to produce different sql files from each column of information using the the sql format posted. I already have the template for that in my script now, I just need the script to create all the sql files based on headers and still input the data below the headers in the new sql files. Any help would be greatly appreciated! Thanks!

Error in moxiemanager with PHP7

After updating the version from PHP5 to PHP7, an error appears when trying to insert images from the moxiemanager plugin of the tinymce that I have integrated into the project.
just tell me:
Error:
Array to string conversion
After a few hours, I could find the error
Specifically in: /home/user/website/admin/js/vendor/tinymce/plugins/moxiemanager/classes/Util/EventDispatcher.php:118
In the method:
public function dispatch($sender, $name, $args) {
$name = strtolower($name);
if (isset($this->observers[$name])) {
$observers = $this->observers[$name];
$args->setSender($sender);
for ($i = 0, $l = count($observers); $i < $l; $i++) {
$value = $observers[$i][1]->$observers[$i][0]($args);
// Is stopped then break the loop
if ($value === false || $args->isStopped()) {
return $args;
}
}
}
return $args;
}
you must replace the following line:
$value = $observers[$i][1]->$observers[$i][0]($args);
For this:
$value = $observers[$i][1]->{$observers[$i][0]}($args);
PHP7 uses an abstract syntactic tree when analyzing the source files. Indirect access to variables, properties and methods will now be strictly evaluated from left to right.