From 17563dab4d71a765f53ae948e284e9df1476da2d Mon Sep 17 00:00:00 2001 From: Mark Quickel <112185610+mark-quickel@users.noreply.github.com> Date: Mon, 10 Jun 2024 10:11:28 -0400 Subject: [PATCH 1/2] Added Azure HTTP function with a QS param to handle allocation / deallocation --- fn/.gitignore | 264 +++++++++++++++++++++ fn/.vscode/extensions.json | 6 + fn/.vscode/launch.json | 11 + fn/.vscode/settings.json | 7 + fn/.vscode/tasks.json | 69 ++++++ fn/Azure/AzureServiceBase.cs | 28 +++ fn/Azure/DeallocatableResource.cs | 6 + fn/Azure/DeallocatableResourceResponse.cs | 7 + fn/Azure/DeallocatableServiceManager.cs | 24 ++ fn/Azure/IDeallocatableService.cs | 5 + fn/Azure/ManagedContainerClusterService.cs | 22 ++ fn/Azure/ResourceService.cs | 34 +++ fn/Azure/SpringAppsService.cs | 22 ++ fn/Azure/SynapseSQLPoolService.cs | 24 ++ fn/Azure/VirtualMachineService.cs | 22 ++ fn/DeallocatorFunction.cs | 72 ++++++ fn/Program.cs | 13 + fn/Properties/launchSettings.json | 9 + fn/fn.csproj | 38 +++ fn/fn.sln | 25 ++ fn/host.json | 12 + 21 files changed, 720 insertions(+) create mode 100644 fn/.gitignore create mode 100644 fn/.vscode/extensions.json create mode 100644 fn/.vscode/launch.json create mode 100644 fn/.vscode/settings.json create mode 100644 fn/.vscode/tasks.json create mode 100644 fn/Azure/AzureServiceBase.cs create mode 100644 fn/Azure/DeallocatableResource.cs create mode 100644 fn/Azure/DeallocatableResourceResponse.cs create mode 100644 fn/Azure/DeallocatableServiceManager.cs create mode 100644 fn/Azure/IDeallocatableService.cs create mode 100644 fn/Azure/ManagedContainerClusterService.cs create mode 100644 fn/Azure/ResourceService.cs create mode 100644 fn/Azure/SpringAppsService.cs create mode 100644 fn/Azure/SynapseSQLPoolService.cs create mode 100644 fn/Azure/VirtualMachineService.cs create mode 100644 fn/DeallocatorFunction.cs create mode 100644 fn/Program.cs create mode 100644 fn/Properties/launchSettings.json create mode 100644 fn/fn.csproj create mode 100644 fn/fn.sln create mode 100644 fn/host.json diff --git a/fn/.gitignore b/fn/.gitignore new file mode 100644 index 0000000..ff5b00c --- /dev/null +++ b/fn/.gitignore @@ -0,0 +1,264 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# Azure Functions localsettings file +local.settings.json + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +project.fragment.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +#*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc \ No newline at end of file diff --git a/fn/.vscode/extensions.json b/fn/.vscode/extensions.json new file mode 100644 index 0000000..bb76300 --- /dev/null +++ b/fn/.vscode/extensions.json @@ -0,0 +1,6 @@ +{ + "recommendations": [ + "ms-azuretools.vscode-azurefunctions", + "ms-dotnettools.csharp" + ] +} \ No newline at end of file diff --git a/fn/.vscode/launch.json b/fn/.vscode/launch.json new file mode 100644 index 0000000..894cbe6 --- /dev/null +++ b/fn/.vscode/launch.json @@ -0,0 +1,11 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Attach to .NET Functions", + "type": "coreclr", + "request": "attach", + "processId": "${command:azureFunctions.pickProcess}" + } + ] +} \ No newline at end of file diff --git a/fn/.vscode/settings.json b/fn/.vscode/settings.json new file mode 100644 index 0000000..eed4725 --- /dev/null +++ b/fn/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "azureFunctions.deploySubpath": "bin/Release/net8.0/publish", + "azureFunctions.projectLanguage": "C#", + "azureFunctions.projectRuntime": "~4", + "debug.internalConsoleOptions": "neverOpen", + "azureFunctions.preDeployTask": "publish (functions)" +} \ No newline at end of file diff --git a/fn/.vscode/tasks.json b/fn/.vscode/tasks.json new file mode 100644 index 0000000..5199259 --- /dev/null +++ b/fn/.vscode/tasks.json @@ -0,0 +1,69 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "clean (functions)", + "command": "dotnet", + "args": [ + "clean", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "type": "process", + "problemMatcher": "$msCompile" + }, + { + "label": "build (functions)", + "command": "dotnet", + "args": [ + "build", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "type": "process", + "dependsOn": "clean (functions)", + "group": { + "kind": "build", + "isDefault": true + }, + "problemMatcher": "$msCompile" + }, + { + "label": "clean release (functions)", + "command": "dotnet", + "args": [ + "clean", + "--configuration", + "Release", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "type": "process", + "problemMatcher": "$msCompile" + }, + { + "label": "publish (functions)", + "command": "dotnet", + "args": [ + "publish", + "--configuration", + "Release", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "type": "process", + "dependsOn": "clean release (functions)", + "problemMatcher": "$msCompile" + }, + { + "type": "func", + "dependsOn": "build (functions)", + "options": { + "cwd": "${workspaceFolder}/bin/Debug/net8.0" + }, + "command": "host start", + "isBackground": true, + "problemMatcher": "$func-dotnet-watch" + } + ] +} \ No newline at end of file diff --git a/fn/Azure/AzureServiceBase.cs b/fn/Azure/AzureServiceBase.cs new file mode 100644 index 0000000..acef9c7 --- /dev/null +++ b/fn/Azure/AzureServiceBase.cs @@ -0,0 +1,28 @@ +using System.ComponentModel.DataAnnotations; +using Azure.Core; +using Azure.Identity; +using Azure.ResourceManager; +using Azure.ResourceManager.Resources; + +namespace Microsoft.Education +{ + public class AzureServiceBase + { + private ArmClient _client; + private ResourceGroupResource? _resourceGroup; + private SubscriptionResource? _subscription; + + public ResourceGroupResource? GetResourceGroup() + { + return _resourceGroup; + } + + public AzureServiceBase(string subscription, string resourceGroup) + { + _client = new ArmClient(new DefaultAzureCredential()); + _subscription = _client.GetSubscriptions().GetAsync(subscription).Result; + _resourceGroup = _subscription.GetResourceGroupAsync(resourceGroup).Result; + } + + } +} \ No newline at end of file diff --git a/fn/Azure/DeallocatableResource.cs b/fn/Azure/DeallocatableResource.cs new file mode 100644 index 0000000..1ff6617 --- /dev/null +++ b/fn/Azure/DeallocatableResource.cs @@ -0,0 +1,6 @@ +public class DeallocatableResource +{ + public string? Name { get; set; } + public string? Type { get; set; } + public string? Error { get; set; } +} \ No newline at end of file diff --git a/fn/Azure/DeallocatableResourceResponse.cs b/fn/Azure/DeallocatableResourceResponse.cs new file mode 100644 index 0000000..e6e5a10 --- /dev/null +++ b/fn/Azure/DeallocatableResourceResponse.cs @@ -0,0 +1,7 @@ +public class DeallocatableResourceResponse +{ + public string? Action { get; set; } + public int ResourceCount { get; set; } + public List? Resources { get; set; } + public string? Error { get; set; } +} \ No newline at end of file diff --git a/fn/Azure/DeallocatableServiceManager.cs b/fn/Azure/DeallocatableServiceManager.cs new file mode 100644 index 0000000..3bc5c68 --- /dev/null +++ b/fn/Azure/DeallocatableServiceManager.cs @@ -0,0 +1,24 @@ +namespace Microsoft.Education +{ + public class DeallocatableServiceManager(string subscription, string resourceGroup) + { + private ManagedContainerClusterService _containerService = new ManagedContainerClusterService(subscription, resourceGroup); + private SpringAppsService _springAppsService = new SpringAppsService(subscription, resourceGroup); + private SynapseSQLPoolService _synapseService = new SynapseSQLPoolService(subscription, resourceGroup); + private VirtualMachineService _vmService = new VirtualMachineService(subscription, resourceGroup); + + public IDeallocatableService Get(string service) + { + #pragma warning disable CS8603 // Possible null reference return. + return service switch + { + "Microsoft.ContainerService/managedClusters" => _containerService, + "Microsoft.AppPlatform/Spring" => _springAppsService, + "Microsoft.Synapse/workspaces" => _synapseService, + "Microsoft.Compute/virtualMachines" => _vmService, + _ => null, + }; + #pragma warning restore CS8603 // Possible null reference return. + } + } +} \ No newline at end of file diff --git a/fn/Azure/IDeallocatableService.cs b/fn/Azure/IDeallocatableService.cs new file mode 100644 index 0000000..f86bb5f --- /dev/null +++ b/fn/Azure/IDeallocatableService.cs @@ -0,0 +1,5 @@ +public interface IDeallocatableService +{ + Task Down(string name); + Task Up(string name); +} \ No newline at end of file diff --git a/fn/Azure/ManagedContainerClusterService.cs b/fn/Azure/ManagedContainerClusterService.cs new file mode 100644 index 0000000..91a9c75 --- /dev/null +++ b/fn/Azure/ManagedContainerClusterService.cs @@ -0,0 +1,22 @@ +using Microsoft.Azure; +using Azure; +using Azure.ResourceManager; +using Azure.ResourceManager.ContainerService; + +namespace Microsoft.Education +{ + public class ManagedContainerClusterService(string subscription, string resourceGroup) : AzureServiceBase(subscription, resourceGroup), IDeallocatableService + { + public async Task Down(string name) + { + var cluster = base.GetResourceGroup().GetContainerServiceManagedCluster(name); + await cluster.Value.StopAsync(WaitUntil.Started); + } + + public async Task Up(string name) + { + var cluster = base.GetResourceGroup().GetContainerServiceManagedCluster(name); + await cluster.Value.StartAsync(WaitUntil.Started); + } + } +} \ No newline at end of file diff --git a/fn/Azure/ResourceService.cs b/fn/Azure/ResourceService.cs new file mode 100644 index 0000000..7bb97e9 --- /dev/null +++ b/fn/Azure/ResourceService.cs @@ -0,0 +1,34 @@ +using Microsoft.AspNetCore.Components.Web; +using Microsoft.Azure; +using System.Collections.Generic; +using System.Dynamic; +using System.Linq; +using System.Security.Cryptography.X509Certificates; + +namespace Microsoft.Education +{ + public class ResourceService(string resourceGroup, string subscription) : AzureServiceBase(resourceGroup, subscription) + { + public async Task> GetResources(string filter = "") + { + var resourceGroup = base.GetResourceGroup() ?? throw new Exception("Resource service returned no resource groups."); + + var resources = String.IsNullOrWhiteSpace(filter) ? + resourceGroup.GetGenericResourcesAsync().AsPages() : + resourceGroup.GetGenericResourcesAsync(filter).AsPages(); + + var deallocatables = new List(); + + await resources.ForEachAsync(page => + { + foreach (var resource in page.Values) + { + deallocatables.Add(new DeallocatableResource(){ Name = resource.Data.Name, Type = resource.Data.ResourceType.ToString() }); + } + }); + + return deallocatables; + } + + } +} \ No newline at end of file diff --git a/fn/Azure/SpringAppsService.cs b/fn/Azure/SpringAppsService.cs new file mode 100644 index 0000000..156ac4b --- /dev/null +++ b/fn/Azure/SpringAppsService.cs @@ -0,0 +1,22 @@ +using Microsoft.Azure; +using Azure; +using Azure.ResourceManager; +using Azure.ResourceManager.AppPlatform; + +namespace Microsoft.Education +{ + public class SpringAppsService(string resourceGroup, string subscription) : AzureServiceBase(resourceGroup, subscription), IDeallocatableService + { + public async Task Down(string name) + { + var app = await base.GetResourceGroup().GetAppPlatformServiceAsync(name); + await app.Value.StopAsync(WaitUntil.Started); + } + + public async Task Up(string name) + { + var app = await base.GetResourceGroup().GetAppPlatformServiceAsync(name); + await app.Value.StartAsync(WaitUntil.Started); + } + } +} \ No newline at end of file diff --git a/fn/Azure/SynapseSQLPoolService.cs b/fn/Azure/SynapseSQLPoolService.cs new file mode 100644 index 0000000..3377c18 --- /dev/null +++ b/fn/Azure/SynapseSQLPoolService.cs @@ -0,0 +1,24 @@ +using Microsoft.Azure; +using Azure; +using Azure.ResourceManager; +using Azure.ResourceManager.Synapse; + +namespace Microsoft.Education +{ + public class SynapseSQLPoolService(string resourceGroup, string subscription) : AzureServiceBase(resourceGroup, subscription), IDeallocatableService + { + public async Task Down(string name) + { + var workspace = await base.GetResourceGroup().GetSynapseWorkspaceAsync(name); + var pools = workspace.Value.GetSynapseSqlPools(); + await pools.ForEachAsync(async pool => await pool.PauseAsync(WaitUntil.Started)); + } + + public async Task Up(string name) + { + var workspace = await base.GetResourceGroup().GetSynapseWorkspaceAsync(name); + var pools = workspace.Value.GetSynapseSqlPools(); + await pools.ForEachAsync(async pool => await pool.ResumeAsync(WaitUntil.Started)); + } + } +} \ No newline at end of file diff --git a/fn/Azure/VirtualMachineService.cs b/fn/Azure/VirtualMachineService.cs new file mode 100644 index 0000000..c57dc6b --- /dev/null +++ b/fn/Azure/VirtualMachineService.cs @@ -0,0 +1,22 @@ +using Microsoft.Azure; +using Azure; +using Azure.ResourceManager; +using Azure.ResourceManager.Compute; + +namespace Microsoft.Education +{ + public class VirtualMachineService(string resourceGroup, string subscription) : AzureServiceBase(resourceGroup, subscription), IDeallocatableService + { + public async Task Down(string name) + { + var vm = await base.GetResourceGroup().GetVirtualMachineAsync(name); + await vm.Value.DeallocateAsync(WaitUntil.Started); + } + + public async Task Up(string name) + { + var vm = await base.GetResourceGroup().GetVirtualMachineAsync(name); + await vm.Value.PowerOnAsync(WaitUntil.Started); + } + } +} \ No newline at end of file diff --git a/fn/DeallocatorFunction.cs b/fn/DeallocatorFunction.cs new file mode 100644 index 0000000..d513af9 --- /dev/null +++ b/fn/DeallocatorFunction.cs @@ -0,0 +1,72 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Extensions.Logging; +using System.Dynamic; + +namespace Microsoft.Education +{ + public class DeallocatorFunction + { + private readonly ILogger _logger; + private string? subscription = Environment.GetEnvironmentVariable("AZURE_SUBSCRIPTION_ID"); + private string? resourceGroup = Environment.GetEnvironmentVariable("AZURE_RESOURCE_GROUP"); + private ResourceService _resourceService; + private DeallocatableServiceManager _serviceManager; + + public DeallocatorFunction(ILogger logger) + { + _logger = logger; + if (string.IsNullOrEmpty(subscription) || string.IsNullOrEmpty(resourceGroup)) + { + _logger.LogError("Subscription or resourceGroup is null"); + throw new ArgumentNullException("Subscription or resourceGroup is null"); + } + _resourceService = new ResourceService(subscription, resourceGroup); + _serviceManager = new DeallocatableServiceManager(subscription, resourceGroup); + } + + [Function("DeallocatorFunction")] + public IActionResult Run([HttpTrigger(AuthorizationLevel.Function, "get", "post")] HttpRequest req) + { + _logger.LogInformation("C# HTTP trigger function processed a request."); + + var query = req.Query.ContainsKey("filter") ? req.Query["filter"].ToString() : ""; + var action = req.Query.ContainsKey("action") ? req.Query["action"].ToString().ToLowerInvariant() : ""; + + var resources = new List(); + try { resources = _resourceService.GetResources(query).Result; } + catch (Exception ex) { return new OkObjectResult(new DeallocatableResourceResponse() { Action = action, ResourceCount = resources.Count, Resources = resources, Error = ex.Message.ToString() }); } + + if (resources.Count == 0) { return new OkObjectResult(new DeallocatableResourceResponse() { Resources = resources, Action = action, ResourceCount = resources.Count}); } + if (string.IsNullOrWhiteSpace(action)) { return new OkObjectResult(new DeallocatableResourceResponse() { Resources = resources, Action = action, ResourceCount = resources.Count, Error = "Action must be 'up' or 'down'."}); } + + Execute(resources, action); + + return new OkObjectResult(new DeallocatableResourceResponse() { Action = action, ResourceCount = resources.Count, Resources = resources}); + + } + + private void Execute(List resources, string action) + { + foreach (var resource in resources) + { + if (resource == null || resource.Type == null || resource.Name == null) continue; + var service = _serviceManager.Get(resource.Type); + if (service == null) { resource.Error = "Not a dellocatable service."; continue; } + try { if (action == "up") { service.Up(resource.Name).Wait(); } else {service.Down(resource.Name).Wait(); }} + catch (Exception ex) { resource.Error = ex.Message.ToString(); continue; } + } + } + + } +} + +/* +Supports standard Azure RM filters: +resourceType eq 'Microsoft.Compute/virtualMachines' OR resourceType eq 'Microsoft.ContainerService/managedClusters' OR resourceType eq 'Microsoft.Synapse/workspaces/sqlPools' +http://localhost:7071/api/DeallocatorFunction?filter=resourceType%20eq%20%27Microsoft.Compute/virtualMachines%27%20OR%20resourceType%20eq%20%27Microsoft.ContainerService/managedClusters%27%20OR%20resourceType%20eq%20%27Microsoft.Synapse/workspaces/sqlPools%27 +tagName eq 'shutdown' AND tagValue eq 'true' +http://localhost:7071/api/DeallocatorFunction?filter=tagName%20eq%20%27shutdown%27%20AND%20tagValue%20eq%20%27true%27 +Read more: https://learn.microsoft.com/en-us/rest/api/resources/resources/list?view=rest-resources-2021-04-01 +*/ \ No newline at end of file diff --git a/fn/Program.cs b/fn/Program.cs new file mode 100644 index 0000000..9389455 --- /dev/null +++ b/fn/Program.cs @@ -0,0 +1,13 @@ +using Microsoft.Azure.Functions.Worker; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.DependencyInjection; + +var host = new HostBuilder() + .ConfigureFunctionsWebApplication() + .ConfigureServices(services => { + services.AddApplicationInsightsTelemetryWorkerService(); + services.ConfigureFunctionsApplicationInsights(); + }) + .Build(); + +host.Run(); diff --git a/fn/Properties/launchSettings.json b/fn/Properties/launchSettings.json new file mode 100644 index 0000000..b3e8070 --- /dev/null +++ b/fn/Properties/launchSettings.json @@ -0,0 +1,9 @@ +{ + "profiles": { + "fn": { + "commandName": "Project", + "commandLineArgs": "--port 7068", + "launchBrowser": false + } + } +} \ No newline at end of file diff --git a/fn/fn.csproj b/fn/fn.csproj new file mode 100644 index 0000000..0b14220 --- /dev/null +++ b/fn/fn.csproj @@ -0,0 +1,38 @@ + + + net8.0 + v4 + Exe + enable + enable + + + + + + + + + + + + + + + + + + + + + PreserveNewest + + + PreserveNewest + Never + + + + + + \ No newline at end of file diff --git a/fn/fn.sln b/fn/fn.sln new file mode 100644 index 0000000..0797166 --- /dev/null +++ b/fn/fn.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.002.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "fn", "fn.csproj", "{D3C98A7E-FE03-40FF-9ED4-72572F97A035}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {D3C98A7E-FE03-40FF-9ED4-72572F97A035}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D3C98A7E-FE03-40FF-9ED4-72572F97A035}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D3C98A7E-FE03-40FF-9ED4-72572F97A035}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D3C98A7E-FE03-40FF-9ED4-72572F97A035}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {B1A07C81-248B-4151-BBDB-48615E33FFD4} + EndGlobalSection +EndGlobal diff --git a/fn/host.json b/fn/host.json new file mode 100644 index 0000000..ee5cf5f --- /dev/null +++ b/fn/host.json @@ -0,0 +1,12 @@ +{ + "version": "2.0", + "logging": { + "applicationInsights": { + "samplingSettings": { + "isEnabled": true, + "excludedTypes": "Request" + }, + "enableLiveMetricsFilters": true + } + } +} \ No newline at end of file From 86f8493422a89f0a25ca51182025a1a480534c9c Mon Sep 17 00:00:00 2001 From: Mark Quickel <112185610+mark-quickel@users.noreply.github.com> Date: Tue, 9 Jul 2024 16:28:47 -0400 Subject: [PATCH 2/2] updated services scope to the subscription level instead of resource group. specific rgs can be passed as filter arguments --- fn/Azure/AzureServiceBase.cs | 8 +++----- fn/Azure/DeallocatableResource.cs | 1 + fn/Azure/DeallocatableServiceManager.cs | 10 +++++----- fn/Azure/IDeallocatableService.cs | 4 ++-- fn/Azure/ManagedContainerClusterService.cs | 14 +++++++++----- fn/Azure/ResourceService.cs | 14 +++++++------- fn/Azure/SpringAppsService.cs | 14 +++++++++----- fn/Azure/SynapseSQLPoolService.cs | 14 +++++++++----- fn/Azure/VirtualMachineService.cs | 14 +++++++++----- fn/DeallocatorFunction.cs | 10 ++++++---- 10 files changed, 60 insertions(+), 43 deletions(-) diff --git a/fn/Azure/AzureServiceBase.cs b/fn/Azure/AzureServiceBase.cs index acef9c7..00b8146 100644 --- a/fn/Azure/AzureServiceBase.cs +++ b/fn/Azure/AzureServiceBase.cs @@ -9,19 +9,17 @@ namespace Microsoft.Education public class AzureServiceBase { private ArmClient _client; - private ResourceGroupResource? _resourceGroup; private SubscriptionResource? _subscription; - public ResourceGroupResource? GetResourceGroup() + public SubscriptionResource? GetSubscription() { - return _resourceGroup; + return _subscription; } - public AzureServiceBase(string subscription, string resourceGroup) + public AzureServiceBase(string subscription) { _client = new ArmClient(new DefaultAzureCredential()); _subscription = _client.GetSubscriptions().GetAsync(subscription).Result; - _resourceGroup = _subscription.GetResourceGroupAsync(resourceGroup).Result; } } diff --git a/fn/Azure/DeallocatableResource.cs b/fn/Azure/DeallocatableResource.cs index 1ff6617..d9e2b73 100644 --- a/fn/Azure/DeallocatableResource.cs +++ b/fn/Azure/DeallocatableResource.cs @@ -1,6 +1,7 @@ public class DeallocatableResource { public string? Name { get; set; } + public string? ResourceGroup { get; set; } public string? Type { get; set; } public string? Error { get; set; } } \ No newline at end of file diff --git a/fn/Azure/DeallocatableServiceManager.cs b/fn/Azure/DeallocatableServiceManager.cs index 3bc5c68..89cfee1 100644 --- a/fn/Azure/DeallocatableServiceManager.cs +++ b/fn/Azure/DeallocatableServiceManager.cs @@ -1,11 +1,11 @@ namespace Microsoft.Education { - public class DeallocatableServiceManager(string subscription, string resourceGroup) + public class DeallocatableServiceManager(string subscription) { - private ManagedContainerClusterService _containerService = new ManagedContainerClusterService(subscription, resourceGroup); - private SpringAppsService _springAppsService = new SpringAppsService(subscription, resourceGroup); - private SynapseSQLPoolService _synapseService = new SynapseSQLPoolService(subscription, resourceGroup); - private VirtualMachineService _vmService = new VirtualMachineService(subscription, resourceGroup); + private ManagedContainerClusterService _containerService = new(subscription); + private SpringAppsService _springAppsService = new(subscription); + private SynapseSQLPoolService _synapseService = new(subscription); + private VirtualMachineService _vmService = new(subscription); public IDeallocatableService Get(string service) { diff --git a/fn/Azure/IDeallocatableService.cs b/fn/Azure/IDeallocatableService.cs index f86bb5f..034fef6 100644 --- a/fn/Azure/IDeallocatableService.cs +++ b/fn/Azure/IDeallocatableService.cs @@ -1,5 +1,5 @@ public interface IDeallocatableService { - Task Down(string name); - Task Up(string name); + Task Down(string name, string resourceGroup); + Task Up(string name, string resourceGroup); } \ No newline at end of file diff --git a/fn/Azure/ManagedContainerClusterService.cs b/fn/Azure/ManagedContainerClusterService.cs index 91a9c75..f1f6a1e 100644 --- a/fn/Azure/ManagedContainerClusterService.cs +++ b/fn/Azure/ManagedContainerClusterService.cs @@ -5,17 +5,21 @@ namespace Microsoft.Education { - public class ManagedContainerClusterService(string subscription, string resourceGroup) : AzureServiceBase(subscription, resourceGroup), IDeallocatableService + public class ManagedContainerClusterService(string subscription) : AzureServiceBase(subscription), IDeallocatableService { - public async Task Down(string name) + public async Task Down(string name, string resourceGroup) { - var cluster = base.GetResourceGroup().GetContainerServiceManagedCluster(name); + var subscription = base.GetSubscription(); + if (subscription == null) { return; } + var cluster = subscription.GetResourceGroup(resourceGroup).Value.GetContainerServiceManagedCluster(name); await cluster.Value.StopAsync(WaitUntil.Started); } - public async Task Up(string name) + public async Task Up(string name, string resourceGroup) { - var cluster = base.GetResourceGroup().GetContainerServiceManagedCluster(name); + var subscription = base.GetSubscription(); + if (subscription == null) { return; } + var cluster = subscription.GetResourceGroup(resourceGroup).Value.GetContainerServiceManagedCluster(name); await cluster.Value.StartAsync(WaitUntil.Started); } } diff --git a/fn/Azure/ResourceService.cs b/fn/Azure/ResourceService.cs index 7bb97e9..37c5ec9 100644 --- a/fn/Azure/ResourceService.cs +++ b/fn/Azure/ResourceService.cs @@ -7,23 +7,23 @@ namespace Microsoft.Education { - public class ResourceService(string resourceGroup, string subscription) : AzureServiceBase(resourceGroup, subscription) + public class ResourceService(string subscription) : AzureServiceBase(subscription) { public async Task> GetResources(string filter = "") { - var resourceGroup = base.GetResourceGroup() ?? throw new Exception("Resource service returned no resource groups."); - + var subscription = base.GetSubscription() ?? throw new Exception("Service base returned no subscriptions."); + var resources = String.IsNullOrWhiteSpace(filter) ? - resourceGroup.GetGenericResourcesAsync().AsPages() : - resourceGroup.GetGenericResourcesAsync(filter).AsPages(); + subscription.GetGenericResourcesAsync().AsPages() : + subscription.GetGenericResourcesAsync(filter).AsPages(); - var deallocatables = new List(); + var deallocatables = new List(); await resources.ForEachAsync(page => { foreach (var resource in page.Values) { - deallocatables.Add(new DeallocatableResource(){ Name = resource.Data.Name, Type = resource.Data.ResourceType.ToString() }); + deallocatables.Add(new DeallocatableResource(){ Name = resource.Data.Name, Type = resource.Data.ResourceType.ToString(), ResourceGroup = resource.Data.Id.ResourceGroupName }); } }); diff --git a/fn/Azure/SpringAppsService.cs b/fn/Azure/SpringAppsService.cs index 156ac4b..14551f2 100644 --- a/fn/Azure/SpringAppsService.cs +++ b/fn/Azure/SpringAppsService.cs @@ -5,17 +5,21 @@ namespace Microsoft.Education { - public class SpringAppsService(string resourceGroup, string subscription) : AzureServiceBase(resourceGroup, subscription), IDeallocatableService + public class SpringAppsService(string subscription) : AzureServiceBase(subscription), IDeallocatableService { - public async Task Down(string name) + public async Task Down(string name, string resourceGroup) { - var app = await base.GetResourceGroup().GetAppPlatformServiceAsync(name); + var subscription = base.GetSubscription(); + if (subscription == null) { return; } + var app = await subscription.GetResourceGroup(resourceGroup).Value.GetAppPlatformServiceAsync(name); await app.Value.StopAsync(WaitUntil.Started); } - public async Task Up(string name) + public async Task Up(string name, string resourceGroup) { - var app = await base.GetResourceGroup().GetAppPlatformServiceAsync(name); + var subscription = base.GetSubscription(); + if (subscription == null) { return; } + var app = await subscription.GetResourceGroup(resourceGroup).Value.GetAppPlatformServiceAsync(name); await app.Value.StartAsync(WaitUntil.Started); } } diff --git a/fn/Azure/SynapseSQLPoolService.cs b/fn/Azure/SynapseSQLPoolService.cs index 3377c18..d789ecf 100644 --- a/fn/Azure/SynapseSQLPoolService.cs +++ b/fn/Azure/SynapseSQLPoolService.cs @@ -5,18 +5,22 @@ namespace Microsoft.Education { - public class SynapseSQLPoolService(string resourceGroup, string subscription) : AzureServiceBase(resourceGroup, subscription), IDeallocatableService + public class SynapseSQLPoolService(string subscription) : AzureServiceBase(subscription), IDeallocatableService { - public async Task Down(string name) + public async Task Down(string name, string resourceGroup) { - var workspace = await base.GetResourceGroup().GetSynapseWorkspaceAsync(name); + var subscription = base.GetSubscription(); + if (subscription == null) { return; } + var workspace = await subscription.GetResourceGroup(resourceGroup).Value.GetSynapseWorkspaceAsync(name); var pools = workspace.Value.GetSynapseSqlPools(); await pools.ForEachAsync(async pool => await pool.PauseAsync(WaitUntil.Started)); } - public async Task Up(string name) + public async Task Up(string name, string resourceGroup) { - var workspace = await base.GetResourceGroup().GetSynapseWorkspaceAsync(name); + var subscription = base.GetSubscription(); + if (subscription == null) { return; } + var workspace = await subscription.GetResourceGroup(resourceGroup).Value.GetSynapseWorkspaceAsync(name); var pools = workspace.Value.GetSynapseSqlPools(); await pools.ForEachAsync(async pool => await pool.ResumeAsync(WaitUntil.Started)); } diff --git a/fn/Azure/VirtualMachineService.cs b/fn/Azure/VirtualMachineService.cs index c57dc6b..c5bc2dd 100644 --- a/fn/Azure/VirtualMachineService.cs +++ b/fn/Azure/VirtualMachineService.cs @@ -5,17 +5,21 @@ namespace Microsoft.Education { - public class VirtualMachineService(string resourceGroup, string subscription) : AzureServiceBase(resourceGroup, subscription), IDeallocatableService + public class VirtualMachineService(string subscription) : AzureServiceBase(subscription), IDeallocatableService { - public async Task Down(string name) + public async Task Down(string name, string resourceGroup) { - var vm = await base.GetResourceGroup().GetVirtualMachineAsync(name); + var subscription = base.GetSubscription(); + if (subscription == null) { return; } + var vm = await subscription.GetResourceGroup(resourceGroup).Value.GetVirtualMachineAsync(name); await vm.Value.DeallocateAsync(WaitUntil.Started); } - public async Task Up(string name) + public async Task Up(string name, string resourceGroup) { - var vm = await base.GetResourceGroup().GetVirtualMachineAsync(name); + var subscription = base.GetSubscription(); + if (subscription == null) { return; } + var vm = await subscription.GetResourceGroup(resourceGroup).Value.GetVirtualMachineAsync(name); await vm.Value.PowerOnAsync(WaitUntil.Started); } } diff --git a/fn/DeallocatorFunction.cs b/fn/DeallocatorFunction.cs index d513af9..d894f89 100644 --- a/fn/DeallocatorFunction.cs +++ b/fn/DeallocatorFunction.cs @@ -22,8 +22,8 @@ public DeallocatorFunction(ILogger logger) _logger.LogError("Subscription or resourceGroup is null"); throw new ArgumentNullException("Subscription or resourceGroup is null"); } - _resourceService = new ResourceService(subscription, resourceGroup); - _serviceManager = new DeallocatableServiceManager(subscription, resourceGroup); + _resourceService = new ResourceService(subscription); + _serviceManager = new DeallocatableServiceManager(subscription); } [Function("DeallocatorFunction")] @@ -51,10 +51,10 @@ private void Execute(List resources, string action) { foreach (var resource in resources) { - if (resource == null || resource.Type == null || resource.Name == null) continue; + if (resource == null || resource.Type == null || resource.Name == null || resource.ResourceGroup == null) continue; var service = _serviceManager.Get(resource.Type); if (service == null) { resource.Error = "Not a dellocatable service."; continue; } - try { if (action == "up") { service.Up(resource.Name).Wait(); } else {service.Down(resource.Name).Wait(); }} + try { if (action == "up") { service.Up(resource.Name, resource.ResourceGroup).Wait(); } else {service.Down(resource.Name, resource.ResourceGroup).Wait(); }} catch (Exception ex) { resource.Error = ex.Message.ToString(); continue; } } } @@ -66,6 +66,8 @@ private void Execute(List resources, string action) Supports standard Azure RM filters: resourceType eq 'Microsoft.Compute/virtualMachines' OR resourceType eq 'Microsoft.ContainerService/managedClusters' OR resourceType eq 'Microsoft.Synapse/workspaces/sqlPools' http://localhost:7071/api/DeallocatorFunction?filter=resourceType%20eq%20%27Microsoft.Compute/virtualMachines%27%20OR%20resourceType%20eq%20%27Microsoft.ContainerService/managedClusters%27%20OR%20resourceType%20eq%20%27Microsoft.Synapse/workspaces/sqlPools%27 +resourceGroup eq 'autogen' AND resourceType eq 'Microsoft.Compute/virtualMachines' +http://localhost:7071/api/DeallocatorFunction?filter=resourceGroup%20eq%20%27autogen%27%20AND%20resourceType%20eq%20%27Microsoft.Compute/virtualMachines%27 tagName eq 'shutdown' AND tagValue eq 'true' http://localhost:7071/api/DeallocatorFunction?filter=tagName%20eq%20%27shutdown%27%20AND%20tagValue%20eq%20%27true%27 Read more: https://learn.microsoft.com/en-us/rest/api/resources/resources/list?view=rest-resources-2021-04-01