Created
June 23, 2016 10:34
-
-
Save ahelland/1339dc69791fc9d35658f6a1c31d8567 to your computer and use it in GitHub Desktop.
Azure Function for receiving a Github Webhook, and in turn running a WAWSDeploy of a specific file to an Azure Web App
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#r "Microsoft.Web.Deployment.dll" | |
using System; | |
using System.IO; | |
using System.Net; | |
using System.Security.Cryptography.X509Certificates; | |
using System.Net.Security; | |
using System.Diagnostics; | |
using Microsoft.Web.Deployment; | |
using System.Collections.Generic; | |
using System.Collections.Specialized; | |
using System.Xml; | |
using System.Xml.XPath; | |
using System.Threading; | |
public static async Task<HttpResponseMessage> Run(HttpRequestMessage req, TraceWriter log) | |
{ | |
log.Info($"C# HTTP trigger function processed a request. RequestUri={req.RequestUri}"); | |
// Get request body - not used here | |
dynamic data = await req.Content.ReadAsAsync<object>(); | |
Thread.Sleep(25000); | |
var webDeployHelper = new WebDeployHelper(); | |
DeploymentChangeSummary changeSummary = webDeployHelper.DeployContentToOneSite( | |
"D:\\home\\site\\wwwroot\\GithubWebhookDeployFile\\data\\page.cshtml", //Source Folder | |
"D:\\home\\site\\wwwroot\\GithubWebhookDeployFile\\webapp.publishsettings", //PublishSettings file | |
null, //Password (optional) | |
false, //Allow untrusted certs | |
false, //!Delete existing files | |
TraceLevel.Off, //TraceLevel | |
false, //What-if (will only emulate the action) | |
"themes\\standard\\page.cshtml", //Target Path | |
false //Use Checksum | |
); | |
return req.CreateResponse(HttpStatusCode.OK, "File replacement done."); | |
} | |
public class WebDeployHelper | |
{ | |
public event EventHandler<DeploymentTraceEventArgs> DeploymentTraceEventHandler; | |
/// <summary> | |
/// Deploys the content to one site. | |
/// </summary> | |
/// <param name="sourcePath">The content path.</param> | |
/// <param name="publishSettingsFile">The publish settings file.</param> | |
/// <param name="password">The password.</param> | |
/// <param name="allowUntrusted">Deploy even if destination certificate is untrusted</param> | |
/// <returns>DeploymentChangeSummary.</returns> | |
public DeploymentChangeSummary DeployContentToOneSite(string sourcePath, | |
string publishSettingsFile, | |
string password = null, | |
bool allowUntrusted = false, | |
bool doNotDelete = true, | |
TraceLevel traceLevel = TraceLevel.Off, | |
bool whatIf = false, | |
string targetPath = null, | |
bool useChecksum = false) | |
{ | |
sourcePath = Path.GetFullPath(sourcePath); | |
var sourceBaseOptions = new DeploymentBaseOptions(); | |
DeploymentBaseOptions destBaseOptions; | |
string destinationPath = SetBaseOptions(publishSettingsFile, out destBaseOptions, allowUntrusted); | |
destBaseOptions.TraceLevel = traceLevel; | |
destBaseOptions.Trace += destBaseOptions_Trace; | |
// use the password from the command line args if provided | |
if (!string.IsNullOrEmpty(password)) | |
destBaseOptions.Password = password; | |
var sourceProvider = DeploymentWellKnownProvider.ContentPath; | |
var targetProvider = DeploymentWellKnownProvider.ContentPath; | |
// If a target path was specified, it could be virtual or physical | |
if (!string.IsNullOrEmpty(targetPath)) | |
{ | |
if (Path.IsPathRooted(targetPath)) | |
{ | |
// If it's rooted (e.g. d:\home\site\foo), use DirPath | |
sourceProvider = targetProvider = DeploymentWellKnownProvider.DirPath; | |
destinationPath = targetPath; | |
} | |
else | |
{ | |
// It's virtual, so append it to what we got from the publish profile | |
destinationPath += "/" + targetPath; | |
} | |
} | |
// If the content path is a zip file, use the Package provider | |
if (Path.GetExtension(sourcePath).Equals(".zip", StringComparison.OrdinalIgnoreCase)) | |
{ | |
// For some reason, we can't combine a zip with a physical target path | |
// Maybe there is some way to make it work? | |
if (targetProvider == DeploymentWellKnownProvider.DirPath) | |
{ | |
throw new Exception("A source zip file can't be used with a physical target path"); | |
} | |
sourceProvider = DeploymentWellKnownProvider.Package; | |
} | |
var syncOptions = new DeploymentSyncOptions | |
{ | |
DoNotDelete = doNotDelete, | |
WhatIf = whatIf, | |
UseChecksum = useChecksum | |
}; | |
// Publish the content to the remote site | |
using (var deploymentObject = DeploymentManager.CreateObject(sourceProvider, sourcePath, sourceBaseOptions)) | |
{ | |
// Note: would be nice to have an async flavor of this API... | |
return deploymentObject.SyncTo(targetProvider, destinationPath, destBaseOptions, syncOptions); | |
} | |
} | |
void destBaseOptions_Trace(object sender, DeploymentTraceEventArgs e) | |
{ | |
DeploymentTraceEventHandler.Invoke(sender, e); | |
} | |
private string SetBaseOptions(string publishSettingsPath, out DeploymentBaseOptions deploymentBaseOptions, bool allowUntrusted) | |
{ | |
PublishSettings publishSettings = new PublishSettings(publishSettingsPath); | |
deploymentBaseOptions = new DeploymentBaseOptions | |
{ | |
ComputerName = publishSettings.ComputerName, | |
UserName = publishSettings.Username, | |
Password = publishSettings.Password, | |
AuthenticationType = publishSettings.UseNTLM ? "ntlm" : "basic" | |
}; | |
if (allowUntrusted || publishSettings.AllowUntrusted) | |
{ | |
ServicePointManager.ServerCertificateValidationCallback = AllowCertificateCallback; | |
} | |
return publishSettings.SiteName; | |
} | |
private static bool AllowCertificateCallback(object sender, X509Certificate cert, X509Chain chain, SslPolicyErrors errors) | |
{ | |
return true; | |
} | |
} | |
public class PublishSettings | |
{ | |
private const string ProfileRootNode = "publishData"; | |
private const string ProfileNode = "publishProfile"; | |
private const string PublishMethod = "publishMethod"; | |
private const string MSDeployHandler = "msdeploy.axd"; | |
private const string DefaultPort = ":8172"; | |
private string _publishUrlRaw = string.Empty; | |
private string _computerName = string.Empty; | |
private string _siteName = string.Empty; | |
private string _userName = string.Empty; | |
private string _password = string.Empty; | |
private string _destinationAppUrl = string.Empty; | |
private string _sqlDbConnectionString = string.Empty; | |
private string _mysqlDbConnectionString = string.Empty; | |
private bool _allowUntrusted; | |
private bool? _useNTLM = null; | |
private IDictionary<string, PublishSettingsDatabase> _databases; | |
private PublishSettingsRemoteAgent _agentType = PublishSettingsRemoteAgent.None; | |
private NameValueCollection _otherAttributes; | |
public PublishSettings(string filePath) | |
{ | |
XmlDocument doc = new XmlDocument(); | |
doc.Load(filePath); | |
Load(doc.CreateNavigator()); | |
} | |
/// <summary> | |
/// Used for unit testing | |
/// </summary> | |
internal PublishSettings() | |
{ | |
} | |
internal void Load(XPathNavigator nav) | |
{ | |
Debug.Assert(nav != null, "nav should not be null"); | |
bool foundPublishSettings = false; | |
string name = string.Empty; | |
string value = string.Empty; | |
nav.MoveToFirstChild(); | |
if (nav.Name != ProfileRootNode) | |
{ | |
throw new XmlException(string.Format("Expected root node to be '{0}'", ProfileRootNode)); | |
} | |
bool fContinue = nav.MoveToFirstChild(); | |
if (nav.Name != ProfileNode) | |
{ | |
throw new XmlException(string.Format("Expected first child node to be '{0}'", ProfileNode)); | |
} | |
while (fContinue) | |
{ | |
string publishMethod = nav.GetAttribute(PublishMethod, string.Empty); | |
if (string.Equals(publishMethod, "MSDeploy", StringComparison.OrdinalIgnoreCase)) | |
{ | |
bool hasMoreAttributes = nav.MoveToFirstAttribute(); | |
while (hasMoreAttributes) | |
{ | |
if (string.Equals(nav.Name, "publishUrl", StringComparison.OrdinalIgnoreCase)) | |
{ | |
_publishUrlRaw = nav.Value; | |
} | |
else if (string.Equals(nav.Name, "msdeploySite", StringComparison.OrdinalIgnoreCase)) | |
{ | |
_siteName = nav.Value; | |
} | |
else if (string.Equals(nav.Name, "userName", StringComparison.OrdinalIgnoreCase)) | |
{ | |
_userName = nav.Value; | |
} | |
else if (string.Equals(nav.Name, "userPWD", StringComparison.OrdinalIgnoreCase)) | |
{ | |
_password = nav.Value; | |
} | |
else if (string.Equals(nav.Name, "destinationAppUrl", StringComparison.OrdinalIgnoreCase)) | |
{ | |
_destinationAppUrl = nav.Value; | |
} | |
else if (string.Equals(nav.Name, "agentType", StringComparison.OrdinalIgnoreCase)) | |
{ | |
string agentType = nav.Value; | |
try | |
{ | |
_agentType = (PublishSettingsRemoteAgent)Enum.Parse(typeof(PublishSettingsRemoteAgent), agentType, true); | |
} | |
catch (ArgumentException ex) | |
{ | |
throw new XmlException( | |
string.Format("Invalid agent type. Valid options are '{0}'", | |
string.Join(", ", Enum.GetNames(typeof(PublishSettingsRemoteAgent)))), | |
ex); | |
} | |
} | |
else if (string.Equals(nav.Name, "SQLServerDBConnectionString", StringComparison.OrdinalIgnoreCase)) | |
{ | |
_sqlDbConnectionString = nav.Value; | |
} | |
else if (string.Equals(nav.Name, "mySQLDBConnectionString", StringComparison.OrdinalIgnoreCase)) | |
{ | |
_mysqlDbConnectionString = nav.Value; | |
} | |
else if (string.Equals(nav.Name, "msdeployAllowUntrustedCertificate", StringComparison.OrdinalIgnoreCase)) | |
{ | |
string allowUntrustedValue = nav.Value; | |
_allowUntrusted = string.Equals(allowUntrustedValue, "true", StringComparison.OrdinalIgnoreCase) ? true : false; | |
} | |
else if (string.Equals(nav.Name, "useNTLM", StringComparison.OrdinalIgnoreCase)) | |
{ | |
string useNTLM = nav.Value; | |
if (!string.IsNullOrEmpty(useNTLM)) | |
{ | |
_useNTLM = Convert.ToBoolean(useNTLM); | |
} | |
// User didn't specify a value so we'll automatically figure it out a little later based on agent type. | |
} | |
else if (!string.Equals(nav.Name, PublishMethod, StringComparison.OrdinalIgnoreCase)) | |
{ | |
OtherAttributes.Add(nav.Name, nav.Value); | |
} | |
hasMoreAttributes = nav.MoveToNextAttribute(); | |
} | |
// Move to the publishProfile node | |
nav.MoveToParent(); | |
if (nav.MoveToFirstChild()) | |
{ | |
do | |
{ | |
if (string.Equals(nav.Name, "databases", StringComparison.OrdinalIgnoreCase)) | |
{ | |
AddDatabases(Databases, nav); | |
} | |
} while (nav.MoveToNext()); | |
// Move to publishProfile node | |
nav.MoveToParent(); | |
} | |
foundPublishSettings = true; | |
break; | |
} | |
//move to next publishprofile node | |
fContinue = nav.MoveToNext(); | |
} | |
if (!foundPublishSettings) | |
{ | |
throw new XmlException("Could not find MSDeploy publish settings"); | |
} | |
} | |
internal static void AddDatabases( | |
IDictionary<string, PublishSettingsDatabase> databases, | |
XPathNavigator nav) | |
{ | |
if (nav.MoveToFirstChild()) | |
{ | |
do | |
{ | |
if (string.Equals(nav.Name, "add", StringComparison.OrdinalIgnoreCase)) | |
{ | |
PublishSettingsDatabase database = new PublishSettingsDatabase() | |
{ | |
Name = nav.GetAttribute("name", string.Empty), | |
ConnectionString = nav.GetAttribute("connectionString", string.Empty), | |
ProviderName = nav.GetAttribute("providerName", string.Empty), | |
Type = nav.GetAttribute("type", string.Empty), | |
TargetDatabase = nav.GetAttribute("targetDatabaseEngineType", string.Empty), | |
TargetServerVersion = nav.GetAttribute("targetServerVersion", string.Empty) | |
}; | |
if (string.IsNullOrEmpty(database.Name)) | |
{ | |
throw new XmlException("Database 'add' element must contain a 'Name' attribute"); | |
} | |
databases.Add(database.Name, database); | |
} | |
} | |
while (nav.MoveToNext()); | |
// Move back to the databases node | |
nav.MoveToParent(); | |
} | |
} | |
public string ComputerName | |
{ | |
get | |
{ | |
if (string.IsNullOrEmpty(_computerName) && !string.IsNullOrEmpty(_publishUrlRaw)) | |
{ | |
if (AgentType == PublishSettingsRemoteAgent.WMSvc || | |
AgentType == PublishSettingsRemoteAgent.None) | |
{ | |
_computerName = GetWmsvcUrl(_publishUrlRaw, _siteName); | |
} | |
else | |
{ | |
_computerName = _publishUrlRaw; | |
} | |
} | |
return _computerName; | |
} | |
internal set | |
{ | |
_computerName = value; | |
} | |
} | |
public string PublishUrlRaw | |
{ | |
get | |
{ | |
return _publishUrlRaw; | |
} | |
internal set | |
{ | |
_publishUrlRaw = value; | |
} | |
} | |
public bool AllowUntrusted | |
{ | |
get | |
{ | |
return _allowUntrusted; | |
} | |
internal set | |
{ | |
_allowUntrusted = value; | |
} | |
} | |
public string SiteName | |
{ | |
get | |
{ | |
return _siteName; | |
} | |
internal set | |
{ | |
_siteName = value; | |
} | |
} | |
public string DestinationAppUrl | |
{ | |
get | |
{ | |
return _destinationAppUrl; | |
} | |
internal set | |
{ | |
_destinationAppUrl = value; | |
} | |
} | |
public string Username | |
{ | |
get | |
{ | |
return _userName; | |
} | |
internal set | |
{ | |
_userName = value; | |
} | |
} | |
public string Password | |
{ | |
get | |
{ | |
return _password; | |
} | |
internal set | |
{ | |
_password = value; | |
} | |
} | |
public string MySqlDBConnectionString | |
{ | |
get | |
{ | |
return _mysqlDbConnectionString; | |
} | |
internal set | |
{ | |
_mysqlDbConnectionString = value; | |
} | |
} | |
public string SqlDBConnectionString | |
{ | |
get | |
{ | |
return _sqlDbConnectionString; | |
} | |
internal set | |
{ | |
_sqlDbConnectionString = value; | |
} | |
} | |
public IDictionary<string, PublishSettingsDatabase> Databases | |
{ | |
get | |
{ | |
if (_databases == null) | |
{ | |
_databases = | |
new Dictionary<string, PublishSettingsDatabase>(StringComparer.OrdinalIgnoreCase); | |
} | |
return _databases; | |
} | |
internal set | |
{ | |
_databases = value; | |
} | |
} | |
public PublishSettingsRemoteAgent AgentType | |
{ | |
get | |
{ | |
return _agentType; | |
} | |
internal set | |
{ | |
_agentType = value; | |
} | |
} | |
public bool UseNTLM | |
{ | |
get | |
{ | |
if (!_useNTLM.HasValue) | |
{ | |
if (_agentType == PublishSettingsRemoteAgent.WMSvc || | |
_agentType == PublishSettingsRemoteAgent.None) | |
{ | |
_useNTLM = false; | |
} | |
else | |
{ | |
_useNTLM = true; | |
} | |
} | |
return _useNTLM.Value; | |
} | |
internal set | |
{ | |
_useNTLM = value; | |
} | |
} | |
public NameValueCollection OtherAttributes | |
{ | |
get | |
{ | |
if (_otherAttributes == null) | |
{ | |
_otherAttributes = new NameValueCollection(); | |
} | |
return _otherAttributes; | |
} | |
internal set | |
{ | |
_otherAttributes = value; | |
} | |
} | |
internal static string GetWmsvcUrl(string publishUrl, string siteName) | |
{ | |
string computerName = publishUrl; | |
if (!computerName.StartsWith("http", StringComparison.OrdinalIgnoreCase)) | |
{ | |
// Some examples of what we might expect here: | |
// foo.com:443/MSDeploy/msdeploy.axd | |
// foo.com/MSDeploy/msdeploy.axd | |
// foo.com:443 | |
// foo.com | |
computerName = InsertPortIfNotSpecified(computerName); | |
computerName = AppendHandlerIfNotSpecified(computerName); | |
if (!string.IsNullOrEmpty(siteName)) | |
{ | |
computerName = string.Format("https://{0}?site={1}", computerName, siteName); | |
} | |
else | |
{ | |
computerName = string.Format("https://{0}", computerName); | |
} | |
} | |
return computerName; | |
} | |
internal static string AppendHandlerIfNotSpecified(string publishUrl) | |
{ | |
if (!publishUrl.EndsWith(MSDeployHandler, StringComparison.OrdinalIgnoreCase)) | |
{ | |
if (publishUrl.EndsWith("/")) | |
{ | |
publishUrl = publishUrl + MSDeployHandler; | |
} | |
else | |
{ | |
publishUrl = publishUrl + "/" + MSDeployHandler; | |
} | |
} | |
return publishUrl; | |
} | |
internal static string InsertPortIfNotSpecified(string publishUrl) | |
{ | |
string[] colonParts = publishUrl.Split(new char[] { ':' }); | |
if (colonParts.Length == 1) | |
{ | |
// No port was specified so we need to add it in | |
int slashIndex = publishUrl.IndexOf('/'); | |
if (slashIndex > -1) | |
{ | |
//publishUrl = InsertPortBeforeSlash(publishUrl, slashIndex); | |
publishUrl = publishUrl.Insert(slashIndex, DefaultPort); | |
} | |
else | |
{ | |
publishUrl = publishUrl + DefaultPort; | |
} | |
} | |
if (colonParts.Length > 1) | |
{ | |
// It's possible that a port was specified, but we're not sure. Apps like Monaco do weird | |
// things like put colon characters in the path and who knows what might happen in the future. | |
// We're being extra careful here to make sure that we only look for ports after the hostname. | |
// This means right after a colon, but never following ANY '/' characters. | |
// Examples of colonParts[0] might be | |
// test.com | |
// foo.com/bar | |
int slashIndex = colonParts[0].IndexOf('/'); | |
if (slashIndex > -1) | |
{ | |
// Since a slash was found before the first colon, we know that the first colon was | |
// not used for the port. Therefore we need to inject the default port before the first slash | |
colonParts[0] = colonParts[0].Insert(slashIndex, DefaultPort); | |
publishUrl = string.Join(":", colonParts); | |
} | |
} | |
return publishUrl; | |
} | |
} | |
public enum PublishSettingsRemoteAgent : int | |
{ | |
WMSvc = 0, | |
MSDepSvc, | |
TempAgent, | |
None, | |
} | |
public class PublishSettingsDatabase | |
{ | |
public string Name { get; set; } | |
public string ConnectionString { get; set; } | |
public string ProviderName { get; set; } | |
public string Type { get; set; } | |
public string TargetDatabase { get; set; } | |
public string TargetServerVersion { get; set; } | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment