diff --git a/.github/workflows/azure-deploy-all.yml b/.github/workflows/azure-deploy-all.yml
new file mode 100644
index 0000000..8436780
--- /dev/null
+++ b/.github/workflows/azure-deploy-all.yml
@@ -0,0 +1,16 @@
+name: Deploy everything to Azure
+
+on: workflow_dispatch
+
+jobs:
+ build:
+
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v2
+
+ - name: Deploy everything to Azure
+ run: |
+ cd src/Grinder.Farmer
+ dotnet run --all
diff --git a/src/Grinder.Farmer/.farmer/farmer-deploy.json b/src/Grinder.Farmer/.farmer/farmer-deploy.json
new file mode 100644
index 0000000..897f3ab
--- /dev/null
+++ b/src/Grinder.Farmer/.farmer/farmer-deploy.json
@@ -0,0 +1,118 @@
+{
+ "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
+ "contentVersion": "1.0.0.0",
+ "outputs": {},
+ "parameters": {
+ "docker-password-for-vahterregistry": {
+ "type": "securestring"
+ }
+ },
+ "resources": [
+ {
+ "apiVersion": "2020-03-01-preview",
+ "location": "northeurope",
+ "name": "vahter-log",
+ "properties": {
+ "publicNetworkAccessForIngestion": "Enabled",
+ "publicNetworkAccessForQuery": "Enabled",
+ "retentionInDays": 30,
+ "sku": {
+ "name": "PerGB2018"
+ }
+ },
+ "tags": {},
+ "type": "Microsoft.OperationalInsights/workspaces"
+ },
+ {
+ "apiVersion": "2016-08-01",
+ "dependsOn": [
+ "vahter-app-farm",
+ "vahter-log"
+ ],
+ "identity": {
+ "type": "None"
+ },
+ "kind": "app,linux,container",
+ "location": "northeurope",
+ "name": "vahter-app",
+ "properties": {
+ "httpsOnly": false,
+ "serverFarmId": "vahter-app-farm",
+ "siteConfig": {
+ "alwaysOn": true,
+ "appCommandLine": "",
+ "appSettings": [
+ {
+ "name": "DOCKER_ENABLE_CI",
+ "value": "true"
+ },
+ {
+ "name": "DOCKER_REGISTRY_SERVER_PASSWORD",
+ "value": "[parameters('docker-password-for-vahterregistry')]"
+ },
+ {
+ "name": "DOCKER_REGISTRY_SERVER_URL",
+ "value": "https://vahterregistry.azurecr.io"
+ },
+ {
+ "name": "DOCKER_REGISTRY_SERVER_USERNAME",
+ "value": "vahterregistry"
+ },
+ {
+ "name": "VAHTER_CONFIG",
+ "value": "{\r\n \"Bot\": {\r\n \"Token\": \"800514531:AAH12BMMpXJvuvtdj_6p4UHWszVs6101I5I\",\r\n \"ChannelId\": -1001492656505,\r\n \"AdminUserId\": 67509832,\r\n \"ChatsToMonitor\": [\r\n \"@fsharp_flood\",\r\n \"@dotnetby\",\r\n \"@cilchat\",\r\n \"@comput_math\",\r\n \"@higher_math\",\r\n \"@elasticsearch_ru\",\r\n \"@Avalonia\",\r\n \"@fsharp_jobs\",\r\n \"@pro_latex\",\r\n \"@fsharp_chat\",\r\n \"@pro_net\",\r\n \"@DotNetRuChat\",\r\n \"@dotnettalks\",\r\n \"@DotNetRuJobs\",\r\n \"@DotNetChat\",\r\n \"@powershell_pro\"\r\n ],\r\n \"AllowedUsers\": [\r\n \"Liminiens\",\r\n \"fvnever\",\r\n \"aensidhe\",\r\n \"etkee\",\r\n \"EgorBo\",\r\n \"neftedollar\",\r\n \"zawodskoj\",\r\n \"kekekeks\",\r\n \"ahydrax\",\r\n \"hacklex\",\r\n \"striped\",\r\n \"XaveScor\",\r\n \"omgszer\",\r\n \"grishace\",\r\n \"AnutaU\"\r\n ]\r\n }\r\n}"
+ }
+ ],
+ "connectionStrings": [],
+ "linuxFxVersion": "DOCKER|vahterregistry.azurecr.io/vahter/grinder:latest",
+ "metadata": []
+ }
+ },
+ "tags": {},
+ "type": "Microsoft.Web/sites"
+ },
+ {
+ "apiVersion": "2018-02-01",
+ "kind": "linux",
+ "location": "northeurope",
+ "name": "vahter-app-farm",
+ "properties": {
+ "name": "vahter-app-farm",
+ "perSiteScaling": false,
+ "reserved": true
+ },
+ "sku": {
+ "capacity": 1,
+ "name": "B1",
+ "size": "0",
+ "tier": "Basic"
+ },
+ "tags": {},
+ "type": "Microsoft.Web/serverfarms"
+ },
+ {
+ "type": "Microsoft.Web/sites/providers/diagnosticSettings",
+ "apiVersion": "2017-05-01-preview",
+ "name": "[concat('vahter-app', '/microsoft.insights/', 'vahter-log')]",
+ "dependsOn": [],
+ "properties": {
+ "workspaceId": "[resourceId('Microsoft.OperationalInsights/workspaces', 'vahter-log')]",
+ "metrics": [],
+ "logs": [
+ {
+ "category": "AppServiceConsoleLogs",
+ "enabled": true
+ },
+ {
+ "category": "AppServiceAppLogs",
+ "enabled": true
+ },
+ {
+ "category": "AppServiceHTTPLogs",
+ "enabled": true
+ }
+ ]
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/src/Grinder.Farmer/Grinder.Farmer.fsproj b/src/Grinder.Farmer/Grinder.Farmer.fsproj
new file mode 100644
index 0000000..0015ab9
--- /dev/null
+++ b/src/Grinder.Farmer/Grinder.Farmer.fsproj
@@ -0,0 +1,22 @@
+
+
+
+ Exe
+ net5.0
+
+
+
+
+
+ PreserveNewest
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Grinder.Farmer/Program.fs b/src/Grinder.Farmer/Program.fs
new file mode 100644
index 0000000..1b6d5f0
--- /dev/null
+++ b/src/Grinder.Farmer/Program.fs
@@ -0,0 +1,185 @@
+open System
+open System.IO
+open Farmer
+open Farmer.Builders
+open Medallion.Shell
+
+let resourceGroup = "vahter-rg"
+let acrName = "vahterregistry"
+let logName = "vahter-log"
+let appName = "vahter-app"
+
+let getEnv name =
+ match Environment.GetEnvironmentVariable name with
+ | null ->
+ failwith $"Provide required ENV variable: {name}"
+// Console.WriteLine $"Please provide ENV variable: {name}"
+// Console.ReadLine()
+ | value -> value
+
+let appId = getEnv "VAHTER_DEPLOY_APPID"
+let pwd = getEnv "VAHTER_DEPLOY_PWD"
+let tenant = getEnv "VAHTER_DEPLOY_TENANT"
+
+type Result.ResultBuilder with
+ member _.Bind(cmd: Command, next: unit -> Result<'a, string>): Result<'a, string> =
+ cmd.StandardOutput.PipeToAsync(Console.Out, true) |> ignore
+ cmd.StandardError.PipeToAsync(Console.Error, true) |> ignore
+ if cmd.Result.Success then
+ next()
+ else
+ Error cmd.Result.StandardError
+
+let botSettings =
+ let vahterConfig = Environment.GetEnvironmentVariable "VAHTER_CONFIG"
+ if File.Exists "./settings.json" then
+ File.ReadAllText "settings.json"
+ else if vahterConfig <> null then
+ vahterConfig
+ else
+ failwith "Please put bot settings either 1) in settings.json 2) or in env VAHTER_CONFIG"
+
+let logs = logAnalytics {
+ name logName
+
+ retention_period 30
+ enable_query
+ enable_ingestion
+}
+
+let registry = containerRegistry {
+ name acrName
+
+ sku ContainerRegistry.Basic
+ enable_admin_user
+}
+
+let botApp = webApp {
+ name appName
+
+ app_insights_off
+ always_on
+ operating_system Linux
+ sku WebApp.Sku.B1
+
+ setting "VAHTER_CONFIG" botSettings
+
+ docker_ci
+ docker_use_azure_registry acrName
+ docker_image "vahter/grinder:latest" ""
+
+ depends_on logs.Name
+}
+
+let registryDeployment = arm {
+ location Location.NorthEurope
+ add_resource registry
+ output "host" registry.LoginServer
+ output "pwd" $"[listCredentials(resourceId('Microsoft.ContainerRegistry/registries','{acrName}'),'2017-10-01').passwords[0].value]"
+ output "login" $"['{acrName}']"
+}
+
+let appDeployment = arm {
+ location Location.NorthEurope
+ add_resources [
+ logs
+ botApp
+ ]
+ add_resource (Resource.ofJson $"""
+{{
+ "type": "Microsoft.Web/sites/providers/diagnosticSettings",
+ "apiVersion": "2017-05-01-preview",
+ "name": "[concat('{appName}', '/microsoft.insights/', '{logName}')]",
+ "dependsOn": [],
+ "properties": {{
+ "workspaceId": "[resourceId('Microsoft.OperationalInsights/workspaces', '{logName}')]",
+ "metrics": [],
+ "logs": [
+ {{
+ "category": "AppServiceConsoleLogs",
+ "enabled": true
+ }},
+ {{
+ "category": "AppServiceAppLogs",
+ "enabled": true
+ }},
+ {{
+ "category": "AppServiceHTTPLogs",
+ "enabled": true
+ }}
+ ]
+ }}
+}}
+""")
+}
+
+let getAcrCreds() = result {
+ let azShell = Shell(fun opts ->
+ opts.ThrowOnError(false)
+ |> ignore
+ )
+ do! azShell.Run("az", "login", "--service-principal", "-u", appId, "-p", pwd, "--tenant", tenant)
+ let pwd = azShell.Run("az", "acr", "credential", "show", "--name", acrName, "--query", "passwords[0].value")
+ let pwdStringRaw = pwd.Result.StandardOutput
+ let pwdString = pwdStringRaw.Substring(1, pwdStringRaw.Length - 3) // truncating first and last chars
+ return $"{acrName}.azurecr.io", acrName, pwdString
+}
+
+let pushDockerImage (host, user, pwd) = result {
+ let dockerShell = Shell(fun opts ->
+ opts.ThrowOnError(false)
+ .WorkingDirectory("../..")
+ |> ignore
+ )
+
+ do! dockerShell.Run("docker", "login", "-u", user, "-p", pwd, host)
+ do! dockerShell.Run("docker", "build", ".", "-t", "grinder")
+ do! dockerShell.Run("docker", "tag", "grinder", $"{host}/vahter/grinder")
+ do! dockerShell.Run("docker", "push", $"{host}/vahter/grinder")
+ return ()
+}
+
+let deployAll() = result {
+ // authenticate into Azure
+ let! authResult = Deploy.authenticate appId pwd tenant
+ printfn "%A" authResult
+
+ // deploying container registry
+ let! registryDeploymentResult =
+ Deploy.tryExecute
+ resourceGroup
+ Deploy.NoParameters
+ registryDeployment
+
+ let registryPwd = registryDeploymentResult.["pwd"]
+ let registryLogin = registryDeploymentResult.["login"]
+ let registryHost = registryDeploymentResult.["host"]
+
+ // build&push image to registry
+ do! pushDockerImage(registryHost, registryLogin, registryPwd)
+
+ // deploy webapp with bot
+ let! deploymentResult =
+ Deploy.tryExecute
+ resourceGroup
+ [ botApp.DockerAcrCredentials.Value.Password.Value, registryPwd ]
+ appDeployment
+ return printfn "%A" deploymentResult
+}
+
+[]
+let main argv =
+ match argv with
+ | null | [||] ->
+ // deploy only image
+ result {
+ let! host, usr, pwd = getAcrCreds()
+ do! pushDockerImage(host, usr, pwd)
+ }
+ | x when Array.contains "--all" x ->
+ // deploy everything = resources + image
+ deployAll()
+ | x ->
+ failwithf "Unknown arguments %A" x
+ |> Result.get
+ 0
\ No newline at end of file
diff --git a/src/Grinder.Farmer/README.MD b/src/Grinder.Farmer/README.MD
new file mode 100644
index 0000000..4e4a321
--- /dev/null
+++ b/src/Grinder.Farmer/README.MD
@@ -0,0 +1,28 @@
+#Description
+This project will deploy Grinder bot to Azure
+There are two scenarios
+- Deploy Azure resources together with docker image
+- Build and push docker image only into predeployed container registry
+
+To deploy everything pass `--all` argument
+
+To deploy just an image don't pass any argument
+
+#Prerequisites
+- AZ CLI
+- Docker
+- Net5 SDK
+
+- Azure credentials (put them in env variables)
+ - `VAHTER_DEPLOY_APPID`
+ - `VAHTER_DEPLOY_PWD`
+ - `VAHTER_DEPLOY_TENANT`
+
+- Bot settings (pick one)
+ - `settings.json` file
+ - `VAHTER_CONFIG` variable
+
+#Run
+`dotnet run [args]` (from project folder)
+
+or just run from IDE