diff --git a/Fake.sln b/Fake.sln index 6aa47b67fd6..481f8d4355c 100644 --- a/Fake.sln +++ b/Fake.sln @@ -76,6 +76,14 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{CCAC5CAB-0 EndProject Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "Fake.Core.UnitTests", "src/test/Fake.Core.UnitTests/Fake.Core.UnitTests.fsproj", "{31A5759B-B562-43C0-A845-14EFA4091543}" EndProject +Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "Fake.Azure.CloudServices", "src/app/Fake.Azure.CloudServices/Fake.Azure.CloudServices.fsproj", "{D8850C67-0542-427A-ABCB-92174EA42C95}" +EndProject +Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "Fake.Azure.Emulators", "src/app/Fake.Azure.Emulators/Fake.Azure.Emulators.fsproj", "{8D72BED1-BC02-4B23-A631-4849BD0FD3E1}" +EndProject +Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "Fake.Azure.Kudu", "src/app/Fake.Azure.Kudu/Fake.Azure.Kudu.fsproj", "{A1CAA84D-3C99-4218-AFB6-55EE2288800E}" +EndProject +Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "Fake.Azure.WebJobs", "src/app/Fake.Azure.WebJobs/Fake.Azure.WebJobs.fsproj", "{F15967FF-E905-4CAD-9545-E59E0F47AD8E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -506,6 +514,54 @@ Global {31A5759B-B562-43C0-A845-14EFA4091543}.Release|x64.Build.0 = Release|Any CPU {31A5759B-B562-43C0-A845-14EFA4091543}.Release|x86.ActiveCfg = Release|Any CPU {31A5759B-B562-43C0-A845-14EFA4091543}.Release|x86.Build.0 = Release|Any CPU + {D8850C67-0542-427A-ABCB-92174EA42C95}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D8850C67-0542-427A-ABCB-92174EA42C95}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D8850C67-0542-427A-ABCB-92174EA42C95}.Debug|x64.ActiveCfg = Debug|Any CPU + {D8850C67-0542-427A-ABCB-92174EA42C95}.Debug|x64.Build.0 = Debug|Any CPU + {D8850C67-0542-427A-ABCB-92174EA42C95}.Debug|x86.ActiveCfg = Debug|Any CPU + {D8850C67-0542-427A-ABCB-92174EA42C95}.Debug|x86.Build.0 = Debug|Any CPU + {D8850C67-0542-427A-ABCB-92174EA42C95}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D8850C67-0542-427A-ABCB-92174EA42C95}.Release|Any CPU.Build.0 = Release|Any CPU + {D8850C67-0542-427A-ABCB-92174EA42C95}.Release|x64.ActiveCfg = Release|Any CPU + {D8850C67-0542-427A-ABCB-92174EA42C95}.Release|x64.Build.0 = Release|Any CPU + {D8850C67-0542-427A-ABCB-92174EA42C95}.Release|x86.ActiveCfg = Release|Any CPU + {D8850C67-0542-427A-ABCB-92174EA42C95}.Release|x86.Build.0 = Release|Any CPU + {8D72BED1-BC02-4B23-A631-4849BD0FD3E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8D72BED1-BC02-4B23-A631-4849BD0FD3E1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8D72BED1-BC02-4B23-A631-4849BD0FD3E1}.Debug|x64.ActiveCfg = Debug|Any CPU + {8D72BED1-BC02-4B23-A631-4849BD0FD3E1}.Debug|x64.Build.0 = Debug|Any CPU + {8D72BED1-BC02-4B23-A631-4849BD0FD3E1}.Debug|x86.ActiveCfg = Debug|Any CPU + {8D72BED1-BC02-4B23-A631-4849BD0FD3E1}.Debug|x86.Build.0 = Debug|Any CPU + {8D72BED1-BC02-4B23-A631-4849BD0FD3E1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8D72BED1-BC02-4B23-A631-4849BD0FD3E1}.Release|Any CPU.Build.0 = Release|Any CPU + {8D72BED1-BC02-4B23-A631-4849BD0FD3E1}.Release|x64.ActiveCfg = Release|Any CPU + {8D72BED1-BC02-4B23-A631-4849BD0FD3E1}.Release|x64.Build.0 = Release|Any CPU + {8D72BED1-BC02-4B23-A631-4849BD0FD3E1}.Release|x86.ActiveCfg = Release|Any CPU + {8D72BED1-BC02-4B23-A631-4849BD0FD3E1}.Release|x86.Build.0 = Release|Any CPU + {A1CAA84D-3C99-4218-AFB6-55EE2288800E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A1CAA84D-3C99-4218-AFB6-55EE2288800E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A1CAA84D-3C99-4218-AFB6-55EE2288800E}.Debug|x64.ActiveCfg = Debug|Any CPU + {A1CAA84D-3C99-4218-AFB6-55EE2288800E}.Debug|x64.Build.0 = Debug|Any CPU + {A1CAA84D-3C99-4218-AFB6-55EE2288800E}.Debug|x86.ActiveCfg = Debug|Any CPU + {A1CAA84D-3C99-4218-AFB6-55EE2288800E}.Debug|x86.Build.0 = Debug|Any CPU + {A1CAA84D-3C99-4218-AFB6-55EE2288800E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A1CAA84D-3C99-4218-AFB6-55EE2288800E}.Release|Any CPU.Build.0 = Release|Any CPU + {A1CAA84D-3C99-4218-AFB6-55EE2288800E}.Release|x64.ActiveCfg = Release|Any CPU + {A1CAA84D-3C99-4218-AFB6-55EE2288800E}.Release|x64.Build.0 = Release|Any CPU + {A1CAA84D-3C99-4218-AFB6-55EE2288800E}.Release|x86.ActiveCfg = Release|Any CPU + {A1CAA84D-3C99-4218-AFB6-55EE2288800E}.Release|x86.Build.0 = Release|Any CPU + {F15967FF-E905-4CAD-9545-E59E0F47AD8E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F15967FF-E905-4CAD-9545-E59E0F47AD8E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F15967FF-E905-4CAD-9545-E59E0F47AD8E}.Debug|x64.ActiveCfg = Debug|Any CPU + {F15967FF-E905-4CAD-9545-E59E0F47AD8E}.Debug|x64.Build.0 = Debug|Any CPU + {F15967FF-E905-4CAD-9545-E59E0F47AD8E}.Debug|x86.ActiveCfg = Debug|Any CPU + {F15967FF-E905-4CAD-9545-E59E0F47AD8E}.Debug|x86.Build.0 = Debug|Any CPU + {F15967FF-E905-4CAD-9545-E59E0F47AD8E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F15967FF-E905-4CAD-9545-E59E0F47AD8E}.Release|Any CPU.Build.0 = Release|Any CPU + {F15967FF-E905-4CAD-9545-E59E0F47AD8E}.Release|x64.ActiveCfg = Release|Any CPU + {F15967FF-E905-4CAD-9545-E59E0F47AD8E}.Release|x64.Build.0 = Release|Any CPU + {F15967FF-E905-4CAD-9545-E59E0F47AD8E}.Release|x86.ActiveCfg = Release|Any CPU + {F15967FF-E905-4CAD-9545-E59E0F47AD8E}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -546,6 +602,10 @@ Global {D24CEE35-B6C0-4C92-AE18-E80F90B69974} = {7BFFAE76-DEE9-417A-A79B-6A6644C4553A} {DB27F0BB-D546-42B2-85DA-52870B4424FD} = {7BFFAE76-DEE9-417A-A79B-6A6644C4553A} {31A5759B-B562-43C0-A845-14EFA4091543} = {CCAC5CAB-03C8-4C11-ADBE-A0D05F6A4F18} + {D8850C67-0542-427A-ABCB-92174EA42C95} = {7BFFAE76-DEE9-417A-A79B-6A6644C4553A} + {8D72BED1-BC02-4B23-A631-4849BD0FD3E1} = {7BFFAE76-DEE9-417A-A79B-6A6644C4553A} + {A1CAA84D-3C99-4218-AFB6-55EE2288800E} = {7BFFAE76-DEE9-417A-A79B-6A6644C4553A} + {F15967FF-E905-4CAD-9545-E59E0F47AD8E} = {7BFFAE76-DEE9-417A-A79B-6A6644C4553A} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {058A0C5E-2216-4306-8AFB-0AE28320C26A} diff --git a/build.fsx b/build.fsx index e051ebeaa6b..b683e83fde3 100644 --- a/build.fsx +++ b/build.fsx @@ -220,6 +220,10 @@ let dotnetAssemblyInfos = [ "dotnet-fake", "Fake dotnet-cli command line tool" "Fake.Api.Slack", "Slack Integration Support" "Fake.Api.GitHub", "GitHub Client API Support via Octokit" + "Fake.Azure.CloudServices", "FAKE - F# Make Azure Cloud Services Support" + "Fake.Azure.Emulators", "FAKE - F# Make Azure Emulators Support" + "Fake.Azure.Kudu", "FAKE - F# Make Azure Kudu Support" + "Fake.Azure.WebJobs", "FAKE - F# Make Azure Web Jobs Support" "Fake.Core.Context", "Core Context Infrastructure" "Fake.Core.Environment", "Environment Detection" "Fake.Core.Process", "Starting and managing Processes" diff --git a/src/app/Fake.Azure.CloudServices/AssemblyInfo.fs b/src/app/Fake.Azure.CloudServices/AssemblyInfo.fs new file mode 100644 index 00000000000..b0d6f10078b --- /dev/null +++ b/src/app/Fake.Azure.CloudServices/AssemblyInfo.fs @@ -0,0 +1,17 @@ +// Auto-Generated by FAKE; do not edit +namespace System +open System.Reflection + +[] +[] +[] +[] +[] +do () + +module internal AssemblyVersionInformation = + let [] AssemblyTitle = "FAKE - F# Make Azure Cloud Services Support" + let [] AssemblyProduct = "FAKE - F# Make" + let [] AssemblyVersion = "5.0.0" + let [] AssemblyInformationalVersion = "5.0.0" + let [] AssemblyFileVersion = "5.0.0" diff --git a/src/app/Fake.Azure.CloudServices/CloudServices.fs b/src/app/Fake.Azure.CloudServices/CloudServices.fs new file mode 100644 index 00000000000..07d8b393366 --- /dev/null +++ b/src/app/Fake.Azure.CloudServices/CloudServices.fs @@ -0,0 +1,97 @@ +/// Contains tasks to package Azure Cloud Services. +module Fake.Azure.CloudServices + +open System.IO +open Fake.Core +open Fake.IO +open Fake.IO.Globbing.Operators + +/// Configuration details for packaging cloud services. +[] +type PackageCloudServiceParams = + { /// The name of the Cloud Service. + CloudService : string + /// The name of the role in the service. + WorkerRole : string + /// The SDK version to use e.g. 2.2. If None, the latest available version is used. + SdkVersion : float option + /// The output path for the .cspkg. + OutputPath : string option } + +let DefaultCloudServiceParams = { CloudService = ""; WorkerRole = ""; SdkVersion = None; OutputPath = None } + +module VmSizes = + type VmSize = | VmSize of size:string + let ExtraSmall = VmSize "ExtraSmall" + let Small = VmSize "Small" + let Medium = VmSize "Medium" + let Large = VmSize "Large" + let ExtraLarge = VmSize "ExtraLarge" + let A5 = VmSize "A5" + let A6 = VmSize "A6" + let A7 = VmSize "A7" + let A8 = VmSize "A8" + let A9 = VmSize "A9" + +/// Modifies the size of the Worker Role in the csdef. +let ModifyVMSize (VmSizes.VmSize vmSize) cloudService = + let csdefPath = sprintf @"%s\ServiceDefinition.csdef" cloudService + csdefPath + |> File.ReadAllText + |> Xml.Doc + |> Xml.XPathReplaceNS + "/svchost:ServiceDefinition/svchost:WorkerRole/@vmsize" + vmSize + [ "svchost", "http://schemas.microsoft.com/ServiceHosting/2008/10/ServiceDefinition" ] + |> fun doc -> + use fileStream = new FileStream(csdefPath, FileMode.Create) + doc.Save fileStream + +/// Packages a cloud service role into a .cspkg, ready for deployment. +let PackageRole packageCloudServiceParams = + let csPack = + let sdkRoots = + [ @"C:\Program Files\Microsoft SDKs\Windows Azure\.NET SDK\" + @"C:\Program Files\Microsoft SDKs\Azure\.NET SDK\" ] + + let availableCsPacks = + sdkRoots + |> Seq.collect(fun sdkRoot -> + !! (sdkRoot + @"**\cspack.exe") + |> Seq.filter(fun path -> path.Substring(sdkRoot.Length).StartsWith "v") + |> Seq.map(fun path -> sdkRoot, path)) + |> Seq.map(fun (sdkRoot, cspackPath) -> + let version = + cspackPath.Substring(sdkRoot.Length).Split '\\' + |> Seq.head + |> fun version -> version.Substring 1 + |> float + version, sdkRoot, cspackPath) + |> Seq.cache + + match packageCloudServiceParams.SdkVersion with + | Some version -> + availableCsPacks + |> Seq.tryFind(fun (csPackVersion,_,_) -> csPackVersion = version) + |> Option.map(fun (_,_,csPackFileInfo) -> csPackFileInfo) + | None -> + availableCsPacks + |> Seq.sortBy(fun (v,_,_) -> -v) + |> Seq.map(fun (_,_,csPackFileInfo) -> csPackFileInfo) + |> Seq.tryFind(fun _ -> true) + + csPack + |> Option.map(fun csPack -> + packageCloudServiceParams.OutputPath |> Option.iter(DirectoryInfo.ensure << DirectoryInfo.ofPath) + let outputFileArg = + packageCloudServiceParams.OutputPath + |> Option.map(fun path -> Path.Combine(path, (packageCloudServiceParams.CloudService + ".cspkg"))) + |> Option.map(sprintf "/out:%s") + |> defaultArg + <| "" + + Process.shellExec + { ExecParams.Empty with + Program = csPack + CommandLine = sprintf @"%s\ServiceDefinition.csdef /role:%s;%s\bin\release;%s.dll %s" packageCloudServiceParams.CloudService packageCloudServiceParams.WorkerRole packageCloudServiceParams.WorkerRole packageCloudServiceParams.WorkerRole outputFileArg + Args = [] }) diff --git a/src/app/Fake.Azure.CloudServices/Fake.Azure.CloudServices.fsproj b/src/app/Fake.Azure.CloudServices/Fake.Azure.CloudServices.fsproj new file mode 100644 index 00000000000..2ba7b7f4a8e --- /dev/null +++ b/src/app/Fake.Azure.CloudServices/Fake.Azure.CloudServices.fsproj @@ -0,0 +1,23 @@ + + + net46;netstandard1.6;netstandard2.0 + Fake.Azure.CloudServices + Library + + + $(DefineConstants);RELEASE + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/app/Fake.Azure.CloudServices/paket.references b/src/app/Fake.Azure.CloudServices/paket.references new file mode 100644 index 00000000000..88a62a0db97 --- /dev/null +++ b/src/app/Fake.Azure.CloudServices/paket.references @@ -0,0 +1,12 @@ +group netcore + +FSharp.Core +NETStandard.Library +System.Diagnostics.FileVersionInfo +System.Diagnostics.Process +System.IO.FileSystem.Watcher +System.Xml.XDocument +System.Xml.XPath +System.Xml.XPath.XDocument +System.Xml.XPath.XmlDocument +System.Xml.ReaderWriter \ No newline at end of file diff --git a/src/app/Fake.Azure.Emulators/AssemblyInfo.fs b/src/app/Fake.Azure.Emulators/AssemblyInfo.fs new file mode 100644 index 00000000000..dd6ce7a5a29 --- /dev/null +++ b/src/app/Fake.Azure.Emulators/AssemblyInfo.fs @@ -0,0 +1,17 @@ +// Auto-Generated by FAKE; do not edit +namespace System +open System.Reflection + +[] +[] +[] +[] +[] +do () + +module internal AssemblyVersionInformation = + let [] AssemblyTitle = "FAKE - F# Make Azure Emulators Support" + let [] AssemblyProduct = "FAKE - F# Make" + let [] AssemblyVersion = "5.0.0" + let [] AssemblyInformationalVersion = "5.0.0" + let [] AssemblyFileVersion = "5.0.0" diff --git a/src/app/Fake.Azure.Emulators/Emulators.fs b/src/app/Fake.Azure.Emulators/Emulators.fs new file mode 100644 index 00000000000..49b2c8abcd8 --- /dev/null +++ b/src/app/Fake.Azure.Emulators/Emulators.fs @@ -0,0 +1,85 @@ +/// Contains tasks to control the local Azure Emulator +module Fake.Azure.Emulators + +open System +open Fake.Core +open Fake.IO +open Fake.IO.FileSystemOperators + +/// A type for the controlling parameter +[] +type private AzureEmulatorParams = { + StorageEmulatorToolPath:Lazy + CSRunToolPath:string + TimeOut:TimeSpan + } + +/// Base path for getting tools from Microsoft SDKs +let msSdkBasePath = Environment.ProgramFilesX86 @@ "Microsoft SDKs" + +/// The default parameters for Azure emulators +let private AzureEmulatorDefaults = { + StorageEmulatorToolPath = + lazy + let path = msSdkBasePath @@ @"\Azure\Storage Emulator\AzureStorageEmulator.exe" + if File.exists path then path + else failwith (sprintf "Unable to locate Azure Storage Emulator at %s" path) + CSRunToolPath = "\"C:\Program Files\Microsoft SDKs\Windows Azure\Emulator\csrun.exe\"" + TimeOut = TimeSpan.FromMinutes 5. + } + +let private (|StorageAlreadyStarted|StorageAlreadyStopped|Ok|OtherError|) = function + | 0 -> Ok + | -5 -> StorageAlreadyStarted + | -6 -> StorageAlreadyStopped + | _ -> OtherError + +/// Stops the storage emulator +let StopStorageEmulator = (fun _ -> + match Process.Exec (fun info -> + { info with + FileName = AzureEmulatorDefaults.StorageEmulatorToolPath.Value + Arguments = "stop" }) AzureEmulatorDefaults.TimeOut with + | Ok | StorageAlreadyStopped -> () + | _ -> failwithf "Azure Emulator Failure on stop Storage Emulator" +) + +/// Starts the storage emulator +let StartStorageEmulator = (fun _ -> + match Process.Exec (fun info -> + { info with + FileName = AzureEmulatorDefaults.StorageEmulatorToolPath.Value + Arguments = "start" }) AzureEmulatorDefaults.TimeOut with + | Ok | StorageAlreadyStarted -> () + | _ -> failwithf "Azure Emulator Failure on start Storage Emulator" +) + +/// Stops the compute emulator +let StopComputeEmulator = (fun _ -> + if 0 <> Process.Exec (fun info -> + { info with + FileName = AzureEmulatorDefaults.CSRunToolPath + Arguments = "/devfabric:shutdown" }) AzureEmulatorDefaults.TimeOut + then + failwithf "Azure Emulator Failure on stop Fabric Emulator" +) + +/// Starts the compute emulator +let StartComputeEmulator = (fun _ -> + if 0 <> Process.Exec (fun info -> + { info with + FileName = AzureEmulatorDefaults.CSRunToolPath + Arguments = "/devfabric:start" }) AzureEmulatorDefaults.TimeOut + then + failwithf "Azure Emulator Failure on start Fabric Emulator" +) + +/// Resets the devstore (BLOB, Queues and Tables) +let ResetDevStorage = (fun _ -> + if 0 <> Process.Exec (fun info -> + { info with + FileName = AzureEmulatorDefaults.StorageEmulatorToolPath.Value + Arguments = "clear all" }) AzureEmulatorDefaults.TimeOut + then + failwithf "Azure Emulator Failure on reset Dev Storage" +) diff --git a/src/app/Fake.Azure.Emulators/Fake.Azure.Emulators.fsproj b/src/app/Fake.Azure.Emulators/Fake.Azure.Emulators.fsproj new file mode 100644 index 00000000000..39cabeb6303 --- /dev/null +++ b/src/app/Fake.Azure.Emulators/Fake.Azure.Emulators.fsproj @@ -0,0 +1,23 @@ + + + net46;netstandard1.6;netstandard2.0 + Fake.Azure.Emulators + Library + + + $(DefineConstants);RELEASE + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/app/Fake.Azure.Emulators/paket.references b/src/app/Fake.Azure.Emulators/paket.references new file mode 100644 index 00000000000..b888890f25f --- /dev/null +++ b/src/app/Fake.Azure.Emulators/paket.references @@ -0,0 +1,7 @@ +group netcore + +FSharp.Core +NETStandard.Library +System.Diagnostics.FileVersionInfo +System.Diagnostics.Process +System.IO.FileSystem.Watcher \ No newline at end of file diff --git a/src/app/Fake.Azure.Kudu/AssemblyInfo.fs b/src/app/Fake.Azure.Kudu/AssemblyInfo.fs new file mode 100644 index 00000000000..90091e8f2b4 --- /dev/null +++ b/src/app/Fake.Azure.Kudu/AssemblyInfo.fs @@ -0,0 +1,17 @@ +// Auto-Generated by FAKE; do not edit +namespace System +open System.Reflection + +[] +[] +[] +[] +[] +do () + +module internal AssemblyVersionInformation = + let [] AssemblyTitle = "FAKE - F# Make Azure Kudu Support" + let [] AssemblyProduct = "FAKE - F# Make" + let [] AssemblyVersion = "5.0.0" + let [] AssemblyInformationalVersion = "5.0.0" + let [] AssemblyFileVersion = "5.0.0" diff --git a/src/app/Fake.Azure.Kudu/Fake.Azure.Kudu.fsproj b/src/app/Fake.Azure.Kudu/Fake.Azure.Kudu.fsproj new file mode 100644 index 00000000000..3c8bcc23fa4 --- /dev/null +++ b/src/app/Fake.Azure.Kudu/Fake.Azure.Kudu.fsproj @@ -0,0 +1,26 @@ + + + net46;netstandard1.6;netstandard2.0 + Fake.Azure.Kudu + Library + + + $(DefineConstants);NETSTANDARD;USE_HTTPCLIENT + + + $(DefineConstants);RELEASE + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/app/Fake.Azure.Kudu/Kudu.fs b/src/app/Fake.Azure.Kudu/Kudu.fs new file mode 100644 index 00000000000..600c7f60a6e --- /dev/null +++ b/src/app/Fake.Azure.Kudu/Kudu.fs @@ -0,0 +1,121 @@ +/// Contains tasks to stage and deploy Azure website and webjobs using source code deployment with Kudu Sync. +module Fake.Azure.Kudu + +open System +open System.IO +#if NETSTANDARD +open System.Net.Http +#else +open System.Net +#endif +open Fake.Core +open Fake.Core.Environment +open Fake.IO + +/// Location where staged outputs should go before synced up to the site. +let deploymentTemp = environVarOrDefault "DEPLOYMENT_TEMP" (Path.GetTempPath() + "kudutemp") +/// Location where synced outputs should be deployed to. +let deploymentTarget = environVarOrDefault "DEPLOYMENT_TARGET" (Path.GetTempPath() + "kudutarget") +/// Used by KuduSync for tracking and diffing deployments. +let nextManifestPath = environVarOrDefault "NEXT_MANIFEST_PATH" String.Empty +/// Used by KuduSync for tracking and diffing deployments. +let previousManifestPath = environVarOrDefault "PREVIOUS_MANIFEST_PATH" String.Empty +/// The path to the KuduSync application. +let kuduPath = (environVarOrDefault "GO_WEB_CONFIG_TEMPLATE" ".") |> DirectoryInfo.ofPath + +/// The different types of web jobs. +type WebJobType = Scheduled | Continuous + +// Some initial cleanup / prep +do + Directory.ensure deploymentTemp |> ignore + Directory.ensure deploymentTarget |> ignore + Shell.CleanDir deploymentTemp + +/// +/// Stages a folder and all subdirectories into the temp deployment area, ready for deployment into the website. +/// +/// The source folder to copy. +/// A predicate which includes files from the folder. If the entire directory should be copied, this predicate should always return true. +let stageFolder source shouldInclude = + Shell.CopyRecursive source deploymentTemp true + |> Seq.filter (not << shouldInclude) + |> Seq.iter File.Delete + +/// Gets the path for deploying a web job to. +let getWebJobPath webJobType webJobName = + let webJobType = match webJobType with Scheduled -> "triggered" | Continuous -> "continuous" + sprintf @"%s\app_data\jobs\%s\%s\" deploymentTemp webJobType webJobName + +/// Stages a set of files into a WebJob folder in the temp deployment area, ready for deployment into the website as a webjob. +let stageWebJob webJobType webJobName files = + let webJobPath = getWebJobPath webJobType webJobName + Directory.ensure webJobPath |> ignore + files |> Shell.CopyFiles webJobPath + +/// Synchronises all staged files from the temporary deployment to the actual deployment, removing +/// any obsolete files, updating changed files and adding new files. +let kuduSync() = + let succeeded, output = + Process.ExecRedirected(fun psi -> + { psi with + FileName = Path.Combine(kuduPath.FullName, "kudusync.cmd") + Arguments = sprintf """-v 50 -f "%s" -t "%s" -n "%s" -p "%s" -i ".git;.hg;.deployment;deploy.cmd""" deploymentTemp deploymentTarget nextManifestPath previousManifestPath }) + (TimeSpan.FromMinutes 5.) + output |> Seq.iter (fun cm -> printfn "%O: %s" cm.Timestamp cm.Message) + if not succeeded then failwith "Error occurred during Kudu Sync deployment." + +/// Kudu ZipDeploy parameters +type ZipDeployParams = + { /// The url of the website, usually in the format of https://.scm.azurewebsites.net + Url : Uri + /// The WebDeploy or Git username, usually the $username from the site's publish profile + UserName : string + /// The WebDeploy or Git Password + Password : string + /// The path to the zip archive to upload + PackageLocation: string } + +/// Synchronizes contents of the zip package with the target web app using Kudu ZipDeploy. +/// See https://blogs.msdn.microsoft.com/appserviceteam/2017/10/16/zip-push-deployment-for-web-apps-functions-and-webjobs/ +let zipDeploy { Url = uri; UserName = username; Password = password; PackageLocation = zipFile } = + let authToken = Convert.ToBase64String(Text.Encoding.ASCII.GetBytes(username + ":" + password)) + + let statusCode = +#if NETSTANDARD + use client = new HttpClient(Timeout = TimeSpan.FromMilliseconds 300000.) + client.DefaultRequestHeaders.Authorization <- Headers.AuthenticationHeaderValue("Basic", authToken) + + use fileStream = new FileStream(zipFile, FileMode.Open) + use content = new StreamContent(fileStream) + content.Headers.ContentType <- Headers.MediaTypeHeaderValue("multipart/form-data") + + let response = client.PostAsync(uri.AbsoluteUri + "api/zipdeploy", content).Result + response.StatusCode +#else + // Create the web request. + let request = + HttpWebRequest.Create(uri.AbsoluteUri + "api/zipdeploy", + Method = "POST", + ContentType = "multipart/form-data", + Timeout = 300000) :?> HttpWebRequest + + // Set the authorization header. + request.Headers.Add("Authorization", "Basic " + authToken) + + // Write the zip file to the request stream, then flush and close it to send. + do use fileStream = new FileStream(zipFile, FileMode.Open) + use inFile = request.GetRequestStream() + fileStream.CopyTo(inFile) + inFile.Flush() + inFile.Close() + + // Get the response. If 200 OK, then the deploy succeeded. Otherwise, the deploy failed. + use response = request.GetResponse() :?> Net.HttpWebResponse + response.StatusCode +#endif + + if statusCode = Net.HttpStatusCode.OK then + Trace.tracefn "Deployed %s" uri.AbsoluteUri + else + failwithf "Failed to deploy package with status code %A" statusCode diff --git a/src/app/Fake.Azure.Kudu/paket.references b/src/app/Fake.Azure.Kudu/paket.references new file mode 100644 index 00000000000..82f798ceea3 --- /dev/null +++ b/src/app/Fake.Azure.Kudu/paket.references @@ -0,0 +1,8 @@ +group netcore + +FSharp.Core +NETStandard.Library +System.Diagnostics.FileVersionInfo +System.Diagnostics.Process +System.IO.FileSystem.Watcher +System.Net.Http diff --git a/src/app/Fake.Azure.WebJobs/AssemblyInfo.fs b/src/app/Fake.Azure.WebJobs/AssemblyInfo.fs new file mode 100644 index 00000000000..787b94f5571 --- /dev/null +++ b/src/app/Fake.Azure.WebJobs/AssemblyInfo.fs @@ -0,0 +1,17 @@ +// Auto-Generated by FAKE; do not edit +namespace System +open System.Reflection + +[] +[] +[] +[] +[] +do () + +module internal AssemblyVersionInformation = + let [] AssemblyTitle = "FAKE - F# Make Azure Web Jobs Support" + let [] AssemblyProduct = "FAKE - F# Make" + let [] AssemblyVersion = "5.0.0" + let [] AssemblyInformationalVersion = "5.0.0" + let [] AssemblyFileVersion = "5.0.0" diff --git a/src/app/Fake.Azure.WebJobs/Fake.Azure.WebJobs.fsproj b/src/app/Fake.Azure.WebJobs/Fake.Azure.WebJobs.fsproj new file mode 100644 index 00000000000..706af3f2f03 --- /dev/null +++ b/src/app/Fake.Azure.WebJobs/Fake.Azure.WebJobs.fsproj @@ -0,0 +1,24 @@ + + + net46;netstandard1.6;netstandard2.0 + Fake.Azure.WebJobs + Library + + + $(DefineConstants);NETSTANDARD;USE_HTTPCLIENT + + + $(DefineConstants);RELEASE + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/app/Fake.Azure.WebJobs/WebJobs.fs b/src/app/Fake.Azure.WebJobs/WebJobs.fs new file mode 100644 index 00000000000..cca964d5928 --- /dev/null +++ b/src/app/Fake.Azure.WebJobs/WebJobs.fs @@ -0,0 +1,115 @@ +/// Contains tasks to package and deploy [Azure Web Jobs](http://azure.microsoft.com/en-gb/documentation/articles/web-sites-create-web-jobs/) via the [Kudu](https://github.com/projectkudu/kudu) Zip controller +module Fake.Azure.WebJobs + +open System +open System.IO +#if NETSTANDARD +open System.Net.Http +#else +open System.Net +#endif +open System.Text +open Fake.Core.Trace +open Fake.IO + +/// The running modes of webjobs +[] +type WebJobType = + | Continuous + | Triggered + +#if NETSTANDARD +#else +type WebClientWithTimeout() = + inherit WebClient() + member val Timeout = 600000 with get, set + + override x.GetWebRequest uri = + let r = base.GetWebRequest(uri) + r.Timeout <- x.Timeout + r +#endif + +/// WebJob type +type WebJob = + { + /// The name of the web job, this will also be the name out of zip file. + Name : string + /// Specifies what type of webjob this is. Note that this also determines it's deployment location on Azure + JobType : WebJobType + /// The project to be zipped and deployed as a webjob + Project : string + /// The directory path of the webjob to zip + DirectoryToPackage : string + /// The package path to once zipped + PackageLocation: string } + +/// The website that webjobs are deployed to +type WebSite = + { + /// The url of the website, usually in the format of https://.scm.azurewebsites.net + Url : Uri + /// The FTP username, usually the $username from the site's publish profile + UserName : string + /// The FTP Password + Password : string + /// The webjobs to deploy to this web site + WebJobs : WebJob list } + +let private jobTypePath webJobType = + match webJobType with + | WebJobType.Continuous -> "continuous" + | WebJobType.Triggered -> "triggered" + +let private zipWebJob webSite webJob = + let packageFile = FileInfo.ofPath webJob.PackageLocation + DirectoryInfo.ensure packageFile.Directory + let zipName = webJob.PackageLocation + let filesToZip = Directory.GetFiles(webJob.DirectoryToPackage, "*.*", SearchOption.AllDirectories) + tracefn "Zipping %s webjob to %s" webJob.Project webJob.PackageLocation + Zip.CreateZip webJob.DirectoryToPackage zipName "" 0 false filesToZip + +/// This task to can be used create a zip for each webjob to deploy to a website +/// The output structure is: `outputpath/{websitename}/webjobs/{continuous/triggered}/{webjobname}.zip` +/// ## Parameters +/// +/// - `webSites` - The websites and webjobs to build zips from. +let PackageWebJobs webSites = + webSites |> List.iter (fun webSite -> webSite.WebJobs |> List.iter (zipWebJob webSite)) + +let private deployWebJobToWebSite webSite webJob = + let uploadUri = Uri(webSite.Url, sprintf "api/%swebjobs/%s" (jobTypePath webJob.JobType) webJob.Name) + let filePath = webJob.PackageLocation + tracefn "Deploying %s webjob to %O" filePath uploadUri +#if NETSTANDARD + use client = new HttpClient(Timeout = TimeSpan.FromMilliseconds 600000.) + let authToken = Convert.ToBase64String(Text.Encoding.ASCII.GetBytes(webSite.UserName + ":" + webSite.Password)) + client.DefaultRequestHeaders.Authorization <- Headers.AuthenticationHeaderValue("Basic", authToken) + + use fileStream = new FileStream(filePath, FileMode.Open) + use content = new StreamContent(fileStream) + content.Headers.ContentDisposition <- Headers.ContentDispositionHeaderValue("attachment; filename=" + (Path.GetFileName webJob.PackageLocation)) + content.Headers.ContentType <- Headers.MediaTypeHeaderValue("application/zip") + + let response = client.PutAsync(uploadUri, content).Result + let result = response.Content.ReadAsStringAsync().Result + tracefn "Response from webjob upload: %s" result +#else + use client = new WebClientWithTimeout(Credentials = NetworkCredential(webSite.UserName, webSite.Password)) + + client.Headers.Add(HttpRequestHeader.ContentType, "application/zip") + client.Headers.Add("Content-Disposition", "attachment; filename=" + (Path.GetFileName webJob.PackageLocation)) + + let response = client.UploadFile(uploadUri, "PUT", filePath) + tracefn "Response from webjob upload: %s" (Encoding.ASCII.GetString response) +#endif + +let private deployWebJobsToWebSite webSite = + webSite.WebJobs |> List.iter (deployWebJobToWebSite webSite) + +/// This task to can be used deploy a prebuilt webjob zip to a website +/// ## Parameters +/// +/// - `webSites` - The websites and webjobs to deploy. +let DeployWebJobs webSites = + webSites |> List.iter deployWebJobsToWebSite diff --git a/src/app/Fake.Azure.WebJobs/paket.references b/src/app/Fake.Azure.WebJobs/paket.references new file mode 100644 index 00000000000..2fa27199f4c --- /dev/null +++ b/src/app/Fake.Azure.WebJobs/paket.references @@ -0,0 +1,9 @@ +group netcore + +FSharp.Core +NETStandard.Library +System.Diagnostics.FileVersionInfo +System.IO.Compression +System.IO.Compression.ZipFile +System.IO.FileSystem.Watcher +System.Net.Http \ No newline at end of file diff --git a/src/legacy/FakeLib/AzureKudu.fs b/src/legacy/FakeLib/AzureKudu.fs index 46120da60b0..9c79b0cfe41 100644 --- a/src/legacy/FakeLib/AzureKudu.fs +++ b/src/legacy/FakeLib/AzureKudu.fs @@ -56,3 +56,43 @@ let kuduSync() = (TimeSpan.FromMinutes 5.) output |> Seq.iter (fun cm -> printfn "%O: %s" cm.Timestamp cm.Message) if not succeeded then failwith "Error occurred during Kudu Sync deployment." + +/// Kudu ZipDeploy parameters +type ZipDeployParams = + { /// The url of the website, usually in the format of https://.scm.azurewebsites.net + Url : Uri + /// The WebDeploy or Git username, usually the $username from the site's publish profile + UserName : string + /// The WebDeploy or Git Password + Password : string + /// The path to the zip archive to upload + PackageLocation: string } + +/// Synchronizes contents of the zip package with the target web app using Kudu ZipDeploy. +/// See https://blogs.msdn.microsoft.com/appserviceteam/2017/10/16/zip-push-deployment-for-web-apps-functions-and-webjobs/ +let zipDeploy { Url = uri; UserName = username; Password = password; PackageLocation = zipFile } = + // Create the web request. + let request = + Net.HttpWebRequest.Create(uri.AbsoluteUri + "api/zipdeploy", + Method = "POST", + ContentType = "multipart/form-data", + Timeout = 300000) :?> Net.HttpWebRequest + + // Set the authorization header. + let authToken = + Convert.ToBase64String(Text.Encoding.ASCII.GetBytes(sprintf "%s:%s" username password)) + request.Headers.Add("Authorization", sprintf "Basic %s" authToken) + + // Write the zip file to the request stream, then flush and close it to send. + do use fileStream = new FileStream(zipFile, FileMode.Open) + use inFile = request.GetRequestStream() + fileStream.CopyTo(inFile) + inFile.Flush() + inFile.Close() + + // Get the response. If 200 OK, then the deploy succeeded. Otherwise, the deploy failed. + use response = request.GetResponse() :?> Net.HttpWebResponse + if response.StatusCode = Net.HttpStatusCode.OK then + tracefn "Deployed %s" uri.AbsoluteUri + else + failwithf "Failed to deploy package with status code %A" response.StatusCode