From e04660f67e9b6b68410ccd125c9d23172caa1d6d Mon Sep 17 00:00:00 2001 From: Lukas Rottach Date: Fri, 17 Apr 2026 20:56:59 +0200 Subject: [PATCH 1/6] feat: clear legacy project and add CLAUDE.md Remove the abandoned .NET 6 Azure Chronos project (C# sources, Bicep templates, CI workflows, Qodana config) so subsequent features can build the new Azure Functions / Durable Functions engine on .NET 10 from a clean slate. Keep src/, deploy/, and .github/workflows/ as empty placeholders via .gitkeep. Add .claude/CLAUDE.md capturing project intent, target stack (Azure Functions isolated worker, Durable Functions, azd + Terraform), tag-driven scheduling scenarios, git and PR conventions, and the commit-approval rule. --- .claude/CLAUDE.md | 62 ++++ .github/qodana.yaml | 29 -- .github/workflows/.gitkeep | 0 .github/workflows/codeql.yml | 82 ------ .github/workflows/dotnet-desktop.yml | 71 ----- .github/workflows/qodana_code_quality.yml | 19 -- deploy/.gitkeep | 0 deploy/main.bicep | 65 ----- .../Microsoft.Web/functionapp.module.bicep | 131 --------- deploy/param.dev.bicepparam | 11 - deploy/param.prod.bicepparam | 11 - src/.gitignore | 264 ------------------ src/.gitkeep | 0 src/.run/Azurite Compose.run.xml | 12 - src/.run/Azurite.run.xml | 27 -- src/.vscode/extensions.json | 6 - src/.vscode/launch.json | 11 - src/.vscode/settings.json | 8 - src/.vscode/tasks.json | 81 ------ src/AzureChronos.Functions/.gitignore | 264 ------------------ .../AzureChronos.Functions.csproj | 27 -- .../AzureChronosTimerTrigger.cs | 62 ---- .../Common/CronHandler.cs | 45 --- .../Common/StringHandler.cs | 26 -- .../Common/TagHandler.cs | 11 - .../Entities/VirtualMachineEntity.cs | 69 ----- .../Interfaces/IAzureComputeService.cs | 11 - .../Interfaces/IVirtualMachineEntity.cs | 11 - .../Models/EntityInitializePayload.cs | 8 - .../Properties/launchSettings.json | 9 - .../Services/AzureComputeService.cs | 59 ---- src/AzureChronos.Functions/Startup.cs | 16 -- src/AzureChronos.Functions/host.json | 11 - .../AzureChronos.Tests.csproj | 30 -- .../TimeCalculationServicesTests.cs | 80 ------ src/AzureChronos.Tests/Usings.cs | 1 - src/AzureChronos.sln | 23 -- src/docker-compose.yml | 8 - 38 files changed, 62 insertions(+), 1599 deletions(-) create mode 100644 .claude/CLAUDE.md delete mode 100644 .github/qodana.yaml create mode 100644 .github/workflows/.gitkeep delete mode 100644 .github/workflows/codeql.yml delete mode 100644 .github/workflows/dotnet-desktop.yml delete mode 100644 .github/workflows/qodana_code_quality.yml create mode 100644 deploy/.gitkeep delete mode 100644 deploy/main.bicep delete mode 100644 deploy/modules/Microsoft.Web/functionapp.module.bicep delete mode 100644 deploy/param.dev.bicepparam delete mode 100644 deploy/param.prod.bicepparam delete mode 100644 src/.gitignore create mode 100644 src/.gitkeep delete mode 100644 src/.run/Azurite Compose.run.xml delete mode 100644 src/.run/Azurite.run.xml delete mode 100644 src/.vscode/extensions.json delete mode 100644 src/.vscode/launch.json delete mode 100644 src/.vscode/settings.json delete mode 100644 src/.vscode/tasks.json delete mode 100644 src/AzureChronos.Functions/.gitignore delete mode 100644 src/AzureChronos.Functions/AzureChronos.Functions.csproj delete mode 100644 src/AzureChronos.Functions/AzureChronosTimerTrigger.cs delete mode 100644 src/AzureChronos.Functions/Common/CronHandler.cs delete mode 100644 src/AzureChronos.Functions/Common/StringHandler.cs delete mode 100644 src/AzureChronos.Functions/Common/TagHandler.cs delete mode 100644 src/AzureChronos.Functions/Entities/VirtualMachineEntity.cs delete mode 100644 src/AzureChronos.Functions/Interfaces/IAzureComputeService.cs delete mode 100644 src/AzureChronos.Functions/Interfaces/IVirtualMachineEntity.cs delete mode 100644 src/AzureChronos.Functions/Models/EntityInitializePayload.cs delete mode 100644 src/AzureChronos.Functions/Properties/launchSettings.json delete mode 100644 src/AzureChronos.Functions/Services/AzureComputeService.cs delete mode 100644 src/AzureChronos.Functions/Startup.cs delete mode 100644 src/AzureChronos.Functions/host.json delete mode 100644 src/AzureChronos.Tests/AzureChronos.Tests.csproj delete mode 100644 src/AzureChronos.Tests/TimeCalculationServicesTests.cs delete mode 100644 src/AzureChronos.Tests/Usings.cs delete mode 100644 src/AzureChronos.sln delete mode 100644 src/docker-compose.yml diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md new file mode 100644 index 0000000..6c69fde --- /dev/null +++ b/.claude/CLAUDE.md @@ -0,0 +1,62 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project: Chronos for Azure + +Chronos for Azure is a lightweight Azure Functions engine that starts and stops Azure Virtual Machines based on CRON schedules supplied via Azure Resource Tags. Users tag VMs with start and/or stop expressions; Chronos discovers those tags, schedules the corresponding lifecycle events, and executes them — no per-VM configuration or code changes required. The goal is zero-friction, tag-driven cost optimization. + +The repository was originally written on .NET 6 (C# in-process Durable Functions, Bicep) and is undergoing a full rewrite. Assume nothing from the legacy code — patterns, tag names, and architecture are being redesigned. + +## Target stack (authoritative) + +- **Runtime**: .NET 10, Azure Functions isolated worker +- **Orchestration**: Azure Durable Functions (choice between Durable Entities vs. orchestrations is **not yet decided** — must be discussed before scaffolding) +- **IaC**: Terraform (AzureRM provider), under `infra/` +- **Deployment**: Azure Developer CLI (`azd`) headless, orchestrating the Terraform deployment. An `azure.yaml` will be added in a later feature +- **Source layout**: `src/` for Functions code, `infra/` for Terraform, `deploy/` currently held empty via `.gitkeep` pending the infra decision + +None of the above code exists yet. This initialization feature only establishes conventions and clears legacy code. + +## Scheduling scenarios the engine must support + +The exact tag names are **not yet defined**. Refer to these scenarios by behavior: + +1. **Stop-only**: VM has a stop/deallocate schedule but no start schedule → Chronos stops it on schedule and never starts it. +2. **Start-only**: VM has a start schedule but no stop schedule → Chronos starts it on schedule. +3. **Start + stop**: VM has both schedules → Chronos runs both. +4. **Exclusion**: VM has an exclusion tag → Chronos ignores the VM entirely, even if start/stop tags are present. Users must not be forced to remove schedule tags to opt out. +5. **Timezone override**: VM has a timezone tag → Chronos parses it and interprets the CRON expressions in that timezone. Invalid/unparseable timezones must be handled gracefully. + +Do **not** invent concrete tag names. When implementing a feature that needs them, ask the user first. + +## Workflow & conventions + +### Branching +- `main` is protected — **never** commit or push to it directly. +- Every change (code, infra, docs) happens on a feature branch and lands via pull request. + +### Commit message prefixes +- `feat:` — new feature +- `bug:` — hotfix +- `docs:` — documentation +- `refactor:` — refactoring of existing functionality + +### Pull request title prefixes +- `Feature: …` +- `Documentation: …` +- `Refactoring: …` + +(No dedicated PR prefix for hotfixes — use `Feature:` or ask the user.) + +### Commit approval + +**Always ask before creating a commit.** Before running `git commit`, present the proposed commit message to the user and wait for explicit confirmation. Never commit unilaterally, even when changes are obviously ready. This applies to every commit — the user's approval of a prior commit does not roll forward to the next. + +Do **not** add Claude attribution, co-author trailers, or "Generated with Claude Code" footers to commit messages or PR bodies. The harness is configured (`.claude/settings.json` → `attribution`) to suppress these; don't reintroduce them manually. + +## Working-style notes for Claude + +- **Use current Microsoft documentation.** Patterns for Azure Functions have shifted substantially since .NET 6 (isolated worker model, new Durable Functions APIs, DI conventions). Do not replicate legacy in-process patterns. When in doubt, fetch current Microsoft Learn docs for Azure Functions, Durable Functions, .NET 10, and the Terraform AzureRM provider. +- **Ask before deciding architecture.** The user wants tag names, Durable Entities vs. orchestrations, timer cadence, identity model (managed identity scope, RBAC boundaries), and VM discovery strategy (subscription-wide ARG query vs. per-RG enumeration) co-designed — not pre-decided. +- **No speculative scaffolding.** Don't generate full Function apps or Terraform modules without an explicit go-ahead for that feature. diff --git a/.github/qodana.yaml b/.github/qodana.yaml deleted file mode 100644 index 99a40de..0000000 --- a/.github/qodana.yaml +++ /dev/null @@ -1,29 +0,0 @@ -#-------------------------------------------------------------------------------# -# Qodana analysis is configured by qodana.yaml file # -# https://www.jetbrains.com/help/qodana/qodana-yaml.html # -#-------------------------------------------------------------------------------# -version: "1.0" - -#Specify inspection profile for code analysis -profile: - name: qodana.starter - -#Enable inspections -#include: -# - name: - -#Disable inspections -#exclude: -# - name: -# paths: -# - - -#Execute shell command before Qodana execution (Applied in CI/CD pipeline) -#bootstrap: sh ./prepare-qodana.sh - -#Install IDE plugins before Qodana execution (Applied in CI/CD pipeline) -#plugins: -# - id: #(plugin id can be found at https://plugins.jetbrains.com) - -#Specify Qodana linter for analysis (Applied in CI/CD pipeline) -linter: jetbrains/qodana-dotnet:latest diff --git a/.github/workflows/.gitkeep b/.github/workflows/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml deleted file mode 100644 index ef408a1..0000000 --- a/.github/workflows/codeql.yml +++ /dev/null @@ -1,82 +0,0 @@ -# For most projects, this workflow file will not need changing; you simply need -# to commit it to your repository. -# -# You may wish to alter this file to override the set of languages analyzed, -# or to provide custom queries or build logic. -# -# ******** NOTE ******** -# We have attempted to detect the languages in your repository. Please check -# the `language` matrix defined below to confirm you have the correct set of -# supported CodeQL languages. -# -name: "CodeQL" - -on: - push: - branches: [ "main", "*main*" ] - pull_request: - # The branches below must be a subset of the branches above - branches: [ "main" ] - schedule: - - cron: '0 22 * * 1' - -jobs: - analyze: - name: Analyze - # Runner size impacts CodeQL analysis time. To learn more, please see: - # - https://gh.io/recommended-hardware-resources-for-running-codeql - # - https://gh.io/supported-runners-and-hardware-resources - # - https://gh.io/using-larger-runners - # Consider using larger runners for possible analysis time improvements. - runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} - timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }} - permissions: - actions: read - contents: read - security-events: write - - strategy: - fail-fast: false - matrix: - language: [ 'csharp' ] - # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby', 'swift' ] - # Use only 'java' to analyze code written in Java, Kotlin or both - # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both - # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support - - steps: - - name: Checkout repository - uses: actions/checkout@v3 - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v2 - with: - languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - - # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs - # queries: security-extended,security-and-quality - - - # Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v2 - - # ℹ️ Command-line programs to run using the OS shell. - # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun - - # If the Autobuild fails above, remove it and uncomment the following three lines. - # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. - - # - run: | - # echo "Run, Build Application using script" - # ./location_of_script_within_repo/buildscript.sh - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 - with: - category: "/language:${{matrix.language}}" diff --git a/.github/workflows/dotnet-desktop.yml b/.github/workflows/dotnet-desktop.yml deleted file mode 100644 index 21bf22c..0000000 --- a/.github/workflows/dotnet-desktop.yml +++ /dev/null @@ -1,71 +0,0 @@ -# This workflow uses actions that are not certified by GitHub. -# They are provided by a third-party and are governed by -# separate terms of service, privacy policy, and support -# documentation. - -# This workflow will build, test, sign and package a WPF or Windows Forms desktop application -# built on .NET Core. -# To learn how to migrate your existing application to .NET Core, -# refer to https://docs.microsoft.com/en-us/dotnet/desktop-wpf/migration/convert-project-from-net-framework -# -# To configure this workflow: -# -# 1. Configure environment variables -# GitHub sets default environment variables for every workflow run. -# Replace the variables relative to your project in the "env" section below. -# -# 2. Signing -# Generate a signing certificate in the Windows Application -# Packaging Project or add an existing signing certificate to the project. -# Next, use PowerShell to encode the .pfx file using Base64 encoding -# by running the following Powershell script to generate the output string: -# -# $pfx_cert = Get-Content '.\SigningCertificate.pfx' -Encoding Byte -# [System.Convert]::ToBase64String($pfx_cert) | Out-File 'SigningCertificate_Encoded.txt' -# -# Open the output file, SigningCertificate_Encoded.txt, and copy the -# string inside. Then, add the string to the repo as a GitHub secret -# and name it "Base64_Encoded_Pfx." -# For more information on how to configure your signing certificate for -# this workflow, refer to https://github.com/microsoft/github-actions-for-desktop-apps#signing -# -# Finally, add the signing certificate password to the repo as a secret and name it "Pfx_Key". -# See "Build the Windows Application Packaging project" below to see how the secret is used. -# -# For more information on GitHub Actions, refer to https://github.com/features/actions -# For a complete CI/CD sample to get started with GitHub Action workflows for Desktop Applications, -# refer to https://github.com/microsoft/github-actions-for-desktop-apps - -name: .NET Tests - -on: - push: - branches: [ "main" ] - pull_request: - branches: [ "main" ] - -jobs: - - build: - - strategy: - matrix: - configuration: [Debug, Release] - - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - # Install the .NET Core workload - - name: Install .NET Core - uses: actions/setup-dotnet@v3 - with: - dotnet-version: 6.0.x - - # Execute all unit tests in the solution - - name: Execute unit tests - run: dotnet test ./src/AzureChronos.Tests/AzureChronos.Tests.csproj diff --git a/.github/workflows/qodana_code_quality.yml b/.github/workflows/qodana_code_quality.yml deleted file mode 100644 index 04663a3..0000000 --- a/.github/workflows/qodana_code_quality.yml +++ /dev/null @@ -1,19 +0,0 @@ -name: Qodana -on: - workflow_dispatch: - pull_request: - push: - branches: - - main - -jobs: - qodana: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - name: 'Qodana Scan' - uses: JetBrains/qodana-action@v2023.2 - env: - QODANA_TOKEN: ${{ secrets.QODANA_TOKEN }} \ No newline at end of file diff --git a/deploy/.gitkeep b/deploy/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/deploy/main.bicep b/deploy/main.bicep deleted file mode 100644 index aed035a..0000000 --- a/deploy/main.bicep +++ /dev/null @@ -1,65 +0,0 @@ - - -/////////////////////////////////////////////// -// -// Type: Main -// Author: Lukas Rottach -// CreationDate: 15.08.2023 -// Name: Azure Chronos Deployment -// -/////////////////////////////////////////////// - -//////////////////////////////// -// Deployoment Scope -//////////////////////////////// -targetScope = 'subscription' - -//////////////////////////////// -// Parameter Area -//////////////////////////////// - -// Deployment parameter -param deploymentLocation string -param rgName string - -// Function parameter -param functionAppName string - -@description('The language worker runtime to load in the function app.') -@allowed([ - 'dotnet' - 'dotnet-isolated' -]) -param runtime string - -@allowed([ - 'v7.0' - 'v6.0' -]) -param dotnetVersion string - -// Storage parameter -param storageName string - -//////////////////////////////// -// Resource Area -//////////////////////////////// -resource rg 'Microsoft.Resources/resourceGroups@2022-09-01' = { - name: rgName - location: deploymentLocation -} - -//////////////////////////////// -// Module Area -//////////////////////////////// -module func './modules/Microsoft.Web/functionapp.module.bicep' = { - scope: rg - name: 'deploy-${rg.name}' - params: { - deploymentLocation: deploymentLocation - dotnetVersion: dotnetVersion - functionAppName: functionAppName - runtime: runtime - storageName: storageName - } -} diff --git a/deploy/modules/Microsoft.Web/functionapp.module.bicep b/deploy/modules/Microsoft.Web/functionapp.module.bicep deleted file mode 100644 index 45c36fc..0000000 --- a/deploy/modules/Microsoft.Web/functionapp.module.bicep +++ /dev/null @@ -1,131 +0,0 @@ -/////////////////////////////////////////////// -// -// Type: Module -// Author: Lukas Rottach -// CreationDate: 15.08.2023 -// Name: Azure Function App -// Provider: Microsoft.Web -// -/////////////////////////////////////////////// - -//////////////////////////////// -// Deployoment Scope -//////////////////////////////// -targetScope = 'resourceGroup' - -//////////////////////////////// -// Parameter Area -//////////////////////////////// - -// Deployment parameter -param deploymentLocation string - -// Function parameter -param functionAppName string - -@description('The language worker runtime to load in the function app.') -@allowed([ - 'dotnet' - 'dotnet-isolated' -]) -param runtime string - -@allowed([ - 'v7.0' - 'v6.0' -]) -param dotnetVersion string - -// Storage parameter -param storageName string - -//////////////////////////////// -// Resource Area -//////////////////////////////// - -// Hosting Plan -resource asp 'Microsoft.Web/serverfarms@2022-09-01' = { - name: '${functionAppName}-asp1-we' - location: deploymentLocation - sku: { - name: 'Y1' - tier: 'Dynamic' - } - properties: {} -} - -// Storage Account -resource sto 'Microsoft.Storage/storageAccounts@2023-01-01' = { - name: storageName - location: deploymentLocation - sku: { - name: 'Standard_LRS' - } - kind: 'StorageV2' - properties: { - supportsHttpsTrafficOnly: true - defaultToOAuthAuthentication: true - } -} - -// Application Insights -resource appin 'Microsoft.Insights/components@2020-02-02' = { - name: '${functionAppName}-appin1-we' - location: deploymentLocation - kind: 'web' - properties: { - Application_Type: 'web' - Request_Source: 'rest' - } -} - -// Azure Function App -resource func 'Microsoft.Web/sites@2022-09-01' = { - name: functionAppName - location: deploymentLocation - kind: 'functionapp' - identity: { - type: 'SystemAssigned' - } - properties: { - serverFarmId: asp.id - siteConfig: { - appSettings: [ - { - name: 'AzureWebJobsStorage' - value: 'DefaultEndpointsProtocol=https;AccountName=${sto.name};EndpointSuffix=${environment().suffixes.storage};AccountKey=${sto.listKeys().keys[0].value}' - } - { - name: 'WEBSITE_CONTENTAZUREFILECONNECTIONSTRING' - value: 'DefaultEndpointsProtocol=https;AccountName=${sto.name};EndpointSuffix=${environment().suffixes.storage};AccountKey=${sto.listKeys().keys[0].value}' - } - { - name: 'WEBSITE_CONTENTSHARE' - value: toLower(functionAppName) - } - { - name: 'FUNCTIONS_EXTENSION_VERSION' - value: '~4' - } - { - name: 'APPINSIGHTS_INSTRUMENTATIONKEY' - value: appin.properties.InstrumentationKey - } - { - name: 'FUNCTIONS_WORKER_RUNTIME' - value: runtime - } - ] - cors: { - allowedOrigins: [ - 'https://portal.azure.com' - ] - } - netFrameworkVersion: dotnetVersion - use32BitWorkerProcess: true - ftpsState: 'FtpsOnly' - minTlsVersion: '1.2' - } - httpsOnly: true - } -} diff --git a/deploy/param.dev.bicepparam b/deploy/param.dev.bicepparam deleted file mode 100644 index 829d720..0000000 --- a/deploy/param.dev.bicepparam +++ /dev/null @@ -1,11 +0,0 @@ -using 'main.bicep' - -// Deployment Parameter -param deploymentLocation = 'West Europe' -param rgName = 'rg-dev-chronos1-we' - -// Function Parameter -param functionAppName = 'azure-chronos-dev' -param storageName = 'devazchronossto1we' -param runtime = 'dotnet-isolated' -param dotnetVersion = 'v7.0' diff --git a/deploy/param.prod.bicepparam b/deploy/param.prod.bicepparam deleted file mode 100644 index 39d59b1..0000000 --- a/deploy/param.prod.bicepparam +++ /dev/null @@ -1,11 +0,0 @@ -using 'main.bicep' - -// Deployment Parameter -param deploymentLocation = 'West Europe' -param rgName = 'rg-prod-chronos1-we' - -// Function Parameter -param functionAppName = 'azure-chronos' -param storageName = 'prodazchronossto1we' -param runtime = 'dotnet-isolated' -param dotnetVersion = 'v7.0' diff --git a/src/.gitignore b/src/.gitignore deleted file mode 100644 index 3c3f4e6..0000000 --- a/src/.gitignore +++ /dev/null @@ -1,264 +0,0 @@ -## 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/src/.gitkeep b/src/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/.run/Azurite Compose.run.xml b/src/.run/Azurite Compose.run.xml deleted file mode 100644 index 20bd8aa..0000000 --- a/src/.run/Azurite Compose.run.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/src/.run/Azurite.run.xml b/src/.run/Azurite.run.xml deleted file mode 100644 index 3a116c1..0000000 --- a/src/.run/Azurite.run.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/src/.vscode/extensions.json b/src/.vscode/extensions.json deleted file mode 100644 index bb76300..0000000 --- a/src/.vscode/extensions.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "recommendations": [ - "ms-azuretools.vscode-azurefunctions", - "ms-dotnettools.csharp" - ] -} \ No newline at end of file diff --git a/src/.vscode/launch.json b/src/.vscode/launch.json deleted file mode 100644 index 894cbe6..0000000 --- a/src/.vscode/launch.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "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/src/.vscode/settings.json b/src/.vscode/settings.json deleted file mode 100644 index b83eb5f..0000000 --- a/src/.vscode/settings.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "azureFunctions.deploySubpath": "AzureChronos.Functions/bin/Release/net6.0/publish", - "azureFunctions.projectLanguage": "C#", - "azureFunctions.projectRuntime": "~4", - "debug.internalConsoleOptions": "neverOpen", - "azureFunctions.preDeployTask": "publish (functions)", - "dotnet.defaultSolution": "AzureChronos.sln" -} \ No newline at end of file diff --git a/src/.vscode/tasks.json b/src/.vscode/tasks.json deleted file mode 100644 index 03f6af5..0000000 --- a/src/.vscode/tasks.json +++ /dev/null @@ -1,81 +0,0 @@ -{ - "version": "2.0.0", - "tasks": [ - { - "label": "clean (functions)", - "command": "dotnet", - "args": [ - "clean", - "/property:GenerateFullPaths=true", - "/consoleloggerparameters:NoSummary" - ], - "type": "process", - "problemMatcher": "$msCompile", - "options": { - "cwd": "${workspaceFolder}/AzureChronos.Functions" - } - }, - { - "label": "build (functions)", - "command": "dotnet", - "args": [ - "build", - "/property:GenerateFullPaths=true", - "/consoleloggerparameters:NoSummary" - ], - "type": "process", - "dependsOn": "clean (functions)", - "group": { - "kind": "build", - "isDefault": true - }, - "problemMatcher": "$msCompile", - "options": { - "cwd": "${workspaceFolder}/AzureChronos.Functions" - } - }, - { - "label": "clean release (functions)", - "command": "dotnet", - "args": [ - "clean", - "--configuration", - "Release", - "/property:GenerateFullPaths=true", - "/consoleloggerparameters:NoSummary" - ], - "type": "process", - "problemMatcher": "$msCompile", - "options": { - "cwd": "${workspaceFolder}/AzureChronos.Functions" - } - }, - { - "label": "publish (functions)", - "command": "dotnet", - "args": [ - "publish", - "--configuration", - "Release", - "/property:GenerateFullPaths=true", - "/consoleloggerparameters:NoSummary" - ], - "type": "process", - "dependsOn": "clean release (functions)", - "problemMatcher": "$msCompile", - "options": { - "cwd": "${workspaceFolder}/AzureChronos.Functions" - } - }, - { - "type": "func", - "dependsOn": "build (functions)", - "options": { - "cwd": "${workspaceFolder}/AzureChronos.Functions/bin/Debug/net6.0" - }, - "command": "host start", - "isBackground": true, - "problemMatcher": "$func-dotnet-watch" - } - ] -} \ No newline at end of file diff --git a/src/AzureChronos.Functions/.gitignore b/src/AzureChronos.Functions/.gitignore deleted file mode 100644 index ff5b00c..0000000 --- a/src/AzureChronos.Functions/.gitignore +++ /dev/null @@ -1,264 +0,0 @@ -## 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/src/AzureChronos.Functions/AzureChronos.Functions.csproj b/src/AzureChronos.Functions/AzureChronos.Functions.csproj deleted file mode 100644 index ab2ef15..0000000 --- a/src/AzureChronos.Functions/AzureChronos.Functions.csproj +++ /dev/null @@ -1,27 +0,0 @@ - - - net6.0 - V4 - - - - - - - - - - - - - - - - PreserveNewest - - - PreserveNewest - Never - - - diff --git a/src/AzureChronos.Functions/AzureChronosTimerTrigger.cs b/src/AzureChronos.Functions/AzureChronosTimerTrigger.cs deleted file mode 100644 index c935b02..0000000 --- a/src/AzureChronos.Functions/AzureChronosTimerTrigger.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System; -using System.Threading.Tasks; -using AzureChronos.Functions.Common; -using AzureChronos.Functions.Interfaces; -using AzureChronos.Functions.Entities; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; -using Microsoft.Extensions.Logging; - -namespace AzureChronos.Functions; - -public class AzureChronosTimerTrigger -{ - private readonly IAzureComputeService _azureComputeService; - - public AzureChronosTimerTrigger(IAzureComputeService azureComputeService) - { - _azureComputeService = azureComputeService; - } - - [FunctionName("AzureChronosTimerTrigger")] - public async Task Run([TimerTrigger("0 */1 * * * *")]TimerInfo myTimer, - [DurableClient] IDurableClient client, - ILogger log) - { - // Gather required environment variables - var subscriptionId = Environment.GetEnvironmentVariable("AZ_SUBSCRIPTION_ID"); - - log.LogInformation($"[TimerTrigger] C# Timer trigger function executed at: {DateTime.Now}"); - - // Query a list of all virtual machines in the subscription - var virtualMachines = await _azureComputeService.ListAzureVirtualMachinesAsync(subscriptionId); - - log.LogInformation("[TimerTrigger] Found {vmCount} virtual machines in subscription {subscriptionId}", virtualMachines.Count, subscriptionId); - - foreach (var vm in virtualMachines) - { - // Validate if an entity already exists and an event was already scheduled - var entityId = new EntityId(nameof(VirtualMachineEntity), vm.Id.ToString().Replace("/", "")); - var entityState = await client.ReadEntityStateAsync(entityId); - - // If the entity already exists and an event was already scheduled, skip - if (entityState.EntityExists && entityState.EntityState.Scheduled) - { - log.LogWarning( - "[TimerTrigger] Entity for Azure Virtual Machine '{vmId}' already exists and Azure Chronos is already scheduled. Skipping...", - vm.Id); - continue; - } - - // Initialize the entity - log.LogInformation("[TimerTrigger] Initializing entity for Azure Virtual Machine '{vmId}'", vm.Id); - await client.SignalEntityAsync(entityId, proxy => proxy.InitializeEntityAsync( - new Models.EntityInitializePayload - { - SubscriptionId = subscriptionId, - ResourceGroupName = StringHandler.ExtractResourceGroupName(vm.Id.ToString()), - VirtualMachineName = vm.Data.Name - })); - } - } -} \ No newline at end of file diff --git a/src/AzureChronos.Functions/Common/CronHandler.cs b/src/AzureChronos.Functions/Common/CronHandler.cs deleted file mode 100644 index 422bcbf..0000000 --- a/src/AzureChronos.Functions/Common/CronHandler.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System; -using NCrontab; - -namespace AzureChronos.Functions.Common; - -public static class CronHandler -{ - /// - /// Calculates the next occurrence of a cron expression in UTC time zone. - /// - /// The cron expression to evaluate. - /// The next occurrence of the cron expression in UTC time zone, or null if the expression is invalid. - public static DateTime? GetNextOccurrence(string cronExpression) - { - try - { - var expression = CrontabSchedule.Parse(cronExpression); - var nextOccurrence = expression.GetNextOccurrence(DateTime.UtcNow); - return nextOccurrence; - } - catch (Exception) - { - return null; - } - } - - /// - /// Determines whether the specified date and time is within the next specified number of minutes from the current UTC date and time. - /// - /// The date and time to compare. - /// The number of minutes to validate. - /// true if the specified date and time is within the next specified number of minutes from the current UTC date and time; otherwise, false. - public static bool IsDateTimeInRange(DateTime? dateTime, int validationMinutes) - { - if (dateTime == null) - { - return false; - } - - var now = DateTime.UtcNow; - var thirtyMinutesFromNow = now.AddMinutes(validationMinutes); - - return dateTime >= now && dateTime <= thirtyMinutesFromNow; - } -} \ No newline at end of file diff --git a/src/AzureChronos.Functions/Common/StringHandler.cs b/src/AzureChronos.Functions/Common/StringHandler.cs deleted file mode 100644 index 26b7e02..0000000 --- a/src/AzureChronos.Functions/Common/StringHandler.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; -using System.Text.RegularExpressions; - -namespace AzureChronos.Functions.Common; - -/// -/// The StringHandler class is a static class that provides static methods to work with strings. -/// -public static class StringHandler -{ - /// - /// Extracts the resource group name from the given Azure resource ID. - /// - /// The resource ID from which the resource group name is to be extracted. - /// Returns the name of the resource group if the matching is successful, otherwise, returns null. - /// Thrown when the provided resourceId is null or whitespace. - public static string ExtractResourceGroupName(string resourceId) - { - if (string.IsNullOrWhiteSpace(resourceId)) - throw new ArgumentNullException(nameof(resourceId)); - - var match = Regex.Match(resourceId, @"/resourceGroups/(?[^/]+)/", RegexOptions.IgnoreCase); - - return match.Success ? match.Groups["resourceGroupName"].Value : null; - } -} \ No newline at end of file diff --git a/src/AzureChronos.Functions/Common/TagHandler.cs b/src/AzureChronos.Functions/Common/TagHandler.cs deleted file mode 100644 index f687a79..0000000 --- a/src/AzureChronos.Functions/Common/TagHandler.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Azure.ResourceManager.Compute; - -namespace AzureChronos.Functions.Common; - -public static class TagHandler -{ - public static bool ValidateVirtualMachineTag(VirtualMachineResource vm, string tagName) - { - return vm.Data.Tags.ContainsKey(tagName); - } -} \ No newline at end of file diff --git a/src/AzureChronos.Functions/Entities/VirtualMachineEntity.cs b/src/AzureChronos.Functions/Entities/VirtualMachineEntity.cs deleted file mode 100644 index 0a46f66..0000000 --- a/src/AzureChronos.Functions/Entities/VirtualMachineEntity.cs +++ /dev/null @@ -1,69 +0,0 @@ -using System.Threading.Tasks; -using AzureChronos.Functions.Common; -using AzureChronos.Functions.Interfaces; -using AzureChronos.Functions.Models; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; -using Microsoft.Extensions.Logging; -using Newtonsoft.Json; - -namespace AzureChronos.Functions.Entities; - -[JsonObject(MemberSerialization.OptIn)] -public class VirtualMachineEntity : IVirtualMachineEntity -{ - private readonly ILogger _log; - private readonly IAzureComputeService _azureComputeService; - - [JsonProperty("subscriptionId")] - public string SubscriptionId { get; set; } - - [JsonProperty("resourceGroupName")] - public string ResourceGroupName { get; set; } - - [JsonProperty("virtualMachineName")] - public string VirtualMachineName { get; set; } - - [JsonProperty("scheduled")] - public bool Scheduled { get; set; } - - public VirtualMachineEntity(ILogger log, IAzureComputeService azureComputeService) - { - _log = log; - _azureComputeService = azureComputeService; - } - - public async Task InitializeEntityAsync(EntityInitializePayload payload) - { - // Initialize the entity properties - // Set scheduled to false by default - SubscriptionId = payload.SubscriptionId; - ResourceGroupName = payload.ResourceGroupName; - VirtualMachineName = payload.VirtualMachineName; - Scheduled = false; - - if (!await ValidateVirtualMachineEligibilityAsync()) - { - _log.LogInformation($"Virtual machine {VirtualMachineName} is not eligible to be scheduled."); - DeleteEntity(); - } - } - - // Method to validate if a virtual machine is eligible to be scheduled - public async Task ValidateVirtualMachineEligibilityAsync() - { - // Query the Azure API to get the virtual machine resource - var virtualMachine = await _azureComputeService.GetAzureVirtualMachineAsync(SubscriptionId, ResourceGroupName, VirtualMachineName); - return TagHandler.ValidateVirtualMachineTag(virtualMachine, "AzChronos_Startup"); - } - - public void DeleteEntity() - { - Entity.Current.DeleteState(); - } - - [FunctionName(nameof(VirtualMachineEntity))] - public static Task Run([EntityTrigger] IDurableEntityContext ctx, ILogger log) - => ctx.DispatchAsync(log); - -} \ No newline at end of file diff --git a/src/AzureChronos.Functions/Interfaces/IAzureComputeService.cs b/src/AzureChronos.Functions/Interfaces/IAzureComputeService.cs deleted file mode 100644 index 22e89fc..0000000 --- a/src/AzureChronos.Functions/Interfaces/IAzureComputeService.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Collections.Generic; -using System.Threading.Tasks; -using Azure.ResourceManager.Compute; - -namespace AzureChronos.Functions.Interfaces; - -public interface IAzureComputeService -{ - Task> ListAzureVirtualMachinesAsync(string subscriptionId); - Task GetAzureVirtualMachineAsync(string subscriptionId, string resourceGroupName, string vmName); -} \ No newline at end of file diff --git a/src/AzureChronos.Functions/Interfaces/IVirtualMachineEntity.cs b/src/AzureChronos.Functions/Interfaces/IVirtualMachineEntity.cs deleted file mode 100644 index 56e7c1a..0000000 --- a/src/AzureChronos.Functions/Interfaces/IVirtualMachineEntity.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Threading.Tasks; -using AzureChronos.Functions.Models; - -namespace AzureChronos.Functions.Interfaces; - -public interface IVirtualMachineEntity -{ - public Task InitializeEntityAsync(EntityInitializePayload payload); - public Task ValidateVirtualMachineEligibilityAsync(); - public void DeleteEntity(); -} \ No newline at end of file diff --git a/src/AzureChronos.Functions/Models/EntityInitializePayload.cs b/src/AzureChronos.Functions/Models/EntityInitializePayload.cs deleted file mode 100644 index 91e9da6..0000000 --- a/src/AzureChronos.Functions/Models/EntityInitializePayload.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace AzureChronos.Functions.Models; - -public class EntityInitializePayload -{ - public string SubscriptionId { get; set; } - public string ResourceGroupName { get; set; } - public string VirtualMachineName { get; set; } -} \ No newline at end of file diff --git a/src/AzureChronos.Functions/Properties/launchSettings.json b/src/AzureChronos.Functions/Properties/launchSettings.json deleted file mode 100644 index c036593..0000000 --- a/src/AzureChronos.Functions/Properties/launchSettings.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "profiles": { - "AzureChronos.Functions": { - "commandName": "Project", - "commandLineArgs": "--port 7284", - "launchBrowser": false - } - } -} \ No newline at end of file diff --git a/src/AzureChronos.Functions/Services/AzureComputeService.cs b/src/AzureChronos.Functions/Services/AzureComputeService.cs deleted file mode 100644 index 2d20d3d..0000000 --- a/src/AzureChronos.Functions/Services/AzureComputeService.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Azure.Identity; -using Azure.ResourceManager; -using Azure.ResourceManager.Compute; -using Azure.ResourceManager.Compute.Models; -using Azure.ResourceManager.Resources; -using AzureChronos.Functions.Interfaces; - -namespace AzureChronos.Functions.Services; - -/// -/// Class AzureComputeService provides Azure related compute operations. -/// It implements the IAzureComputeService interface. -/// -public class AzureComputeService : IAzureComputeService -{ - /// - /// Property AzureCredential takes care of providing credentials for Azure SDK usage. - /// It uses DefaultAzureCredential mechanism. - /// - private DefaultAzureCredential AzureCredential { get; } = new(); - - /// - /// Asynchronous method ListAzureVirtualMachines retrieves and returns all the virtual machines under the provided Azure subscription. - /// This makes use of Azure ARM client provided by the Azure SDK for .NET Core applications. - /// - /// Azure subscription id used to fetch virtual machines. - /// A list of Azure virtual machine resources. - public async Task> ListAzureVirtualMachinesAsync(string subscriptionId) - { - var client = new ArmClient(AzureCredential); - - var subResourceId = SubscriptionResource.CreateResourceIdentifier(subscriptionId); - var subResource = client.GetSubscriptionResource(subResourceId); - - var virtualMachines = new List(); - - await foreach(var vm in subResource.GetVirtualMachinesAsync()) - { - virtualMachines.Add(vm); - } - - return virtualMachines; - } - - public async Task GetAzureVirtualMachineAsync(string subscriptionId, string resourceGroupName, string vmName) - { - var client = new ArmClient(AzureCredential); - - var resourceId = VirtualMachineResource.CreateResourceIdentifier(subscriptionId, resourceGroupName, vmName); - var virtualMachine = client.GetVirtualMachineResource(resourceId); - - // Invoke the GetAsync() method to retrieve the virtual machine resource - var expand = InstanceViewType.UserData; - return await virtualMachine.GetAsync(expand: expand); - } -} \ No newline at end of file diff --git a/src/AzureChronos.Functions/Startup.cs b/src/AzureChronos.Functions/Startup.cs deleted file mode 100644 index 24694e9..0000000 --- a/src/AzureChronos.Functions/Startup.cs +++ /dev/null @@ -1,16 +0,0 @@ -using AzureChronos.Functions.Interfaces; -using AzureChronos.Functions.Services; -using Microsoft.Azure.Functions.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection; - -[assembly: FunctionsStartup(typeof(AzureChronos.Functions.Startup))] - -namespace AzureChronos.Functions; - -public class Startup : FunctionsStartup -{ - public override void Configure(IFunctionsHostBuilder builder) - { - builder.Services.AddTransient(); - } -} \ No newline at end of file diff --git a/src/AzureChronos.Functions/host.json b/src/AzureChronos.Functions/host.json deleted file mode 100644 index beb2e40..0000000 --- a/src/AzureChronos.Functions/host.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "version": "2.0", - "logging": { - "applicationInsights": { - "samplingSettings": { - "isEnabled": true, - "excludedTypes": "Request" - } - } - } -} \ No newline at end of file diff --git a/src/AzureChronos.Tests/AzureChronos.Tests.csproj b/src/AzureChronos.Tests/AzureChronos.Tests.csproj deleted file mode 100644 index 5e5c91c..0000000 --- a/src/AzureChronos.Tests/AzureChronos.Tests.csproj +++ /dev/null @@ -1,30 +0,0 @@ - - - - net6.0 - enable - enable - - false - true - - - - - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - diff --git a/src/AzureChronos.Tests/TimeCalculationServicesTests.cs b/src/AzureChronos.Tests/TimeCalculationServicesTests.cs deleted file mode 100644 index 0b3fe70..0000000 --- a/src/AzureChronos.Tests/TimeCalculationServicesTests.cs +++ /dev/null @@ -1,80 +0,0 @@ -using AzureChronos.Functions.Common; -using AzureChronos.Functions.Services; -using NUnit.Framework; - -namespace AzureChronos.Tests; - -public class TimeCalculationServicesTests -{ - [SetUp] - public void Setup() { } - - [Test] - public void GetNextOccurrence_ValidCronExpression_ReturnsNextOccurrence() - { - // Arrange - var cronExpression = "0 0 * * *"; // Every day at midnight - var expectedNextOccurrence = DateTime.UtcNow.Date.AddDays(1); - - // Act - var nextOccurrence = CronHandler.GetNextOccurrence(cronExpression); - - // Assert - Assert.AreEqual(expectedNextOccurrence, nextOccurrence?.Date); - } - - [Test] - public void GetNextOccurrence_InvalidCronExpression_ReturnsNull() - { - // Arrange - var cronExpression = "invalid expression"; - - // Act - var nextOccurrence = CronHandler.GetNextOccurrence(cronExpression); - - // Assert - Assert.IsNull(nextOccurrence); - } - - [Test] - public void IsWithinNext30Minutes_DateTimeWithin30Minutes_ReturnsTrue() - { - // Arrange - var dateTime = DateTime.UtcNow.AddMinutes(15); - var validationMinutes = 30; - - // Act - var result = CronHandler.IsDateTimeInRange(dateTime, validationMinutes); - - // Assert - Assert.IsTrue(result); - } - - [Test] - public void IsWithinNext30Minutes_DateTimeOutside30Minutes_ReturnsFalse() - { - // Arrange - var dateTime = DateTime.UtcNow.AddMinutes(45); - var validationMinutes = 30; - - // Act - var result = CronHandler.IsDateTimeInRange(dateTime, validationMinutes); - - // Assert - Assert.IsFalse(result); - } - - [Test] - public void IsWithinNext30Minutes_NullDateTime_ReturnsFalse() - { - // Arrange - DateTime? dateTime = null; - var validationMinutes = 30; - - // Act - var result = CronHandler.IsDateTimeInRange(dateTime, validationMinutes); - - // Assert - Assert.IsFalse(result); - } -} \ No newline at end of file diff --git a/src/AzureChronos.Tests/Usings.cs b/src/AzureChronos.Tests/Usings.cs deleted file mode 100644 index 4ea06d0..0000000 --- a/src/AzureChronos.Tests/Usings.cs +++ /dev/null @@ -1 +0,0 @@ -global using NUnit.Framework; \ No newline at end of file diff --git a/src/AzureChronos.sln b/src/AzureChronos.sln deleted file mode 100644 index b7059b9..0000000 --- a/src/AzureChronos.sln +++ /dev/null @@ -1,23 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AzureChronos.Functions", "AzureChronos.Functions\AzureChronos.Functions.csproj", "{D3E5FBE9-672A-44AB-AB5A-AD3E5941E8F2}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AzureChronos.Tests", "AzureChronos.Tests\AzureChronos.Tests.csproj", "{9BA29E1C-FA5B-4AD6-BF8B-C955C10E34FB}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {D3E5FBE9-672A-44AB-AB5A-AD3E5941E8F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D3E5FBE9-672A-44AB-AB5A-AD3E5941E8F2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D3E5FBE9-672A-44AB-AB5A-AD3E5941E8F2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D3E5FBE9-672A-44AB-AB5A-AD3E5941E8F2}.Release|Any CPU.Build.0 = Release|Any CPU - {9BA29E1C-FA5B-4AD6-BF8B-C955C10E34FB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9BA29E1C-FA5B-4AD6-BF8B-C955C10E34FB}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9BA29E1C-FA5B-4AD6-BF8B-C955C10E34FB}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9BA29E1C-FA5B-4AD6-BF8B-C955C10E34FB}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection -EndGlobal diff --git a/src/docker-compose.yml b/src/docker-compose.yml deleted file mode 100644 index a3404bc..0000000 --- a/src/docker-compose.yml +++ /dev/null @@ -1,8 +0,0 @@ -version: '3.4' -services: - azurite: - image: mcr.microsoft.com/azure-storage/azurite - ports: - - "10000:10000" - - "10001:10001" - - "10002:10002" \ No newline at end of file From 2fb82939e5b01f9c1a5936e393199188b444ee23 Mon Sep 17 00:00:00 2001 From: Lukas Rottach Date: Fri, 17 Apr 2026 20:57:04 +0200 Subject: [PATCH 2/6] feat: configure Claude Code settings and permissions Add .claude/settings.json disabling Claude commit and PR attribution and allowlisting common git commands plus read-only gh wrappers (issue/pr/repo list, view, status, diff, checks). Gitignore .claude/settings.local.json so personal overrides stay out of the repo. --- .claude/settings.json | 54 +++++++++++++++++++++++++++++++++++++++++++ .gitignore | 3 +++ 2 files changed, 57 insertions(+) create mode 100644 .claude/settings.json diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..1915d23 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,54 @@ +{ + "$schema": "https://json.schemastore.org/claude-code-settings.json", + "attribution": { + "commit": "", + "pr": "" + }, + "permissions": { + "allow": [ + "Bash(git status)", + "Bash(git status *)", + "Bash(git log)", + "Bash(git log *)", + "Bash(git diff)", + "Bash(git diff *)", + "Bash(git show)", + "Bash(git show *)", + "Bash(git branch)", + "Bash(git branch *)", + "Bash(git remote -v)", + "Bash(git remote show *)", + "Bash(git blame *)", + "Bash(git reflog)", + "Bash(git reflog *)", + "Bash(git rev-parse *)", + "Bash(git describe *)", + + "Bash(git add *)", + "Bash(git commit *)", + "Bash(git push)", + "Bash(git push *)", + "Bash(git checkout *)", + "Bash(git switch *)", + "Bash(git restore *)", + "Bash(git stash)", + "Bash(git stash *)", + "Bash(git pull)", + "Bash(git pull *)", + "Bash(git fetch)", + "Bash(git fetch *)", + "Bash(git merge *)", + "Bash(git rebase *)", + + "Bash(gh issue list*)", + "Bash(gh issue view*)", + "Bash(gh issue status*)", + "Bash(gh pr list*)", + "Bash(gh pr view*)", + "Bash(gh pr status*)", + "Bash(gh pr diff*)", + "Bash(gh pr checks*)", + "Bash(gh repo view*)" + ] + } +} diff --git a/.gitignore b/.gitignore index dfcfd56..15c3cc1 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,9 @@ ## ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore +# Claude Code personal overrides +.claude/settings.local.json + # User-specific files *.rsuser *.suo From 886ff6f083d8d5fdc29c21a473a6212ddd49549b Mon Sep 17 00:00:00 2001 From: Lukas Rottach Date: Sat, 18 Apr 2026 09:43:50 +0200 Subject: [PATCH 3/6] docs: add target architecture chapter to README Append a new Architecture chapter documenting the target operational model for the Chronos rewrite: Flex Consumption + .NET 10 isolated + Durable Functions on Azure Storage, with a Durable Entity per VM, an eternal per-VM orchestrator, and start/stop activities. Includes the component diagram, tag-mutation flow (edit / removal / partial), the ARG-based discovery model, the minimal-RBAC managed identity, the explicit v1 out-of-scope list, and a record of why the standalone Durable Task SDK was evaluated and rejected. Marked as the target architecture; the legacy content above will be replaced once the new implementation lands. --- README.md | 100 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/README.md b/README.md index 7734db6..bf8920f 100644 --- a/README.md +++ b/README.md @@ -20,3 +20,103 @@ Work in progress. ## Getting started Work in progress. + +--- + +> **Note**: The sections below describe the **target architecture** for the in-progress rewrite of Chronos. The legacy content above will be replaced once the new implementation lands. + +## Architecture + +### Operational model + +Chronos is an Azure Functions app on the **Flex Consumption** plan, **.NET 10 isolated worker**, using **Durable Functions** with the **Azure Storage** backend. The hosting plan gives us scale-to-zero between ticks; the Durable extension gives us per-VM stateful scheduling without a second state store. + +### Components + +``` + ┌──────────────────────────┐ +Timer (5 min)──▶│ DiscoveryTimer │──ARG query──▶ Tagged VMs + └────────┬─────────────────┘ + │ signal per VM + ▼ + ┌───────────────────┐ + │ VmScheduleEntity │ (one Durable Entity per ARM resource ID) + │ - start CRON │ + │ - stop CRON │ + │ - timezone │ + │ - exclusion flag │ + │ - last action │ + └────────┬──────────┘ + │ external event on change + ▼ + ┌───────────────────┐ + │ VmScheduler │ eternal orchestrator, one per VM: + │ (orchestrator) │ read entity → compute next event + │ │ Task.WhenAny(durableTimer, changeEvent) + │ │ on timer : CallActivity(start|stop) + │ │ on event : recompute + │ │ ContinueAsNew + └────────┬──────────┘ + │ + ┌────────▼──────────┐ + │ Start / Stop │ Azure.ResourceManager.Compute + │ activities │ via user-assigned managed identity + └───────────────────┘ +``` + +- **`DiscoveryTimer`** — runs every 5 minutes. Single Azure Resource Graph (ARG) KQL query across all in-scope subscriptions returns VMs carrying Chronos tags. For each VM: signals `VmScheduleEntity.UpdateSchedule(...)`. For VMs that no longer carry Chronos tags: signals `Clear()`. Spawns a `VmScheduler` orchestrator for any actionable entity that doesn't already have one. +- **`VmScheduleEntity`** — a Durable Entity keyed by the VM's ARM resource ID. Holds the materialized schedule (start CRON, stop CRON, timezone, exclusion flag, last executed action). Operations: `UpdateSchedule`, `MarkExecuted`, `Clear`, `Get`. On any state-changing operation, raises a `ScheduleChanged` external event on the running orchestrator. +- **`VmScheduler`** — an eternal orchestrator, one per VM. Reads its entity, computes the next CRON occurrence (with timezone), arms a durable timer to that instant, and races it against the `ScheduleChanged` external event. Whichever fires first wins; the loop continues via `ContinueAsNew`. +- **Start / Stop activities** — thin idempotent wrappers around `Azure.ResourceManager.Compute`; check current power state before acting. Authenticate via the Function App's user-assigned managed identity. Durable handles retry/backoff. + +### Tag mutation flow + +Every kind of tag change reduces to one mechanism: **discovery signals the entity → entity raises `ScheduleChanged` → orchestrator reacts**. + +- **Tag value edited** — discovery sees the new value within 5 minutes, signals `UpdateSchedule`. Entity diffs, persists, raises the change event. The orchestrator's `Task.WhenAny(timer, changeEvent)` returns the change event; the pending durable timer is abandoned. Orchestrator recomputes the next occurrence and `ContinueAsNew`s with the new timer. +- **Tag removed entirely** — discovery signals `Clear()`. Entity wipes state and raises the change event. Orchestrator wakes, sees an empty schedule, and exits cleanly (no `ContinueAsNew`). Discovery only re-spawns the orchestrator when the entity becomes actionable again. +- **Partial removal (e.g. start removed, stop kept)** — same path as "edited"; entity now reflects a stop-only schedule and the orchestrator arms a timer for the next stop event. No special case. + +### Discovery + +A single Azure Resource Graph KQL query enumerates VMs with Chronos tags. Development runs against one subscription; the query and identity model are already shaped for **multi-subscription support** (v-next), which expands by adding additional subscription scopes to the managed identity's role assignments — no code change. + +### Identity and RBAC + +The Function App runs under a **user-assigned managed identity** with the principle of least privilege: + +- `Reader` on the target scope (for ARG queries and VM reads) +- `Microsoft.Compute/virtualMachines/start/action` +- `Microsoft.Compute/virtualMachines/deallocate/action` + +Implemented as a custom role rather than the broad built-in `Virtual Machine Contributor`. + +### Why this shape + +1. **Future-proof.** Manual overrides, additional resource types (VMSS, SQL, AKS node pools), and Event-Grid-driven reconciliation all land on existing seams (entity operations, additional signal sources, new activity types) — not as redesigns. +2. **Clear separation of concerns.** Tags are the *intent*. The entity is the *materialized schedule*. The orchestrator is *control flow*. Each piece has one job. +3. **Operationally cheap.** Flex Consumption scales to zero between ticks; Durable timers avoid polling loops; Azure Storage backend has effectively no fixed cost at this scale. +4. **Failure-tolerant.** Durable retries activities; orchestrator history prevents double-fires or silently-dropped events across Function App restarts. +5. **Idempotent execution.** Start/stop activities check current power state before acting, so re-runs are safe. + +### Out of scope for v1 + +- Event Grid subscription on tag changes (sub-second reconciliation) — additive enhancement, slots in as a second signal source on the same entity. +- Manual override HTTP API (e.g. "skip next start"). +- Resource types other than VMs. +- Durable Task Scheduler backend — unnecessary at this scale. +- Concrete Chronos tag names (still to be decided). + +### Why Durable Functions, not the standalone Durable Task SDK + +The portable [Durable Task SDKs](https://learn.microsoft.com/en-us/azure/durable-task/sdks/durable-task-overview) (GA for .NET, Python, Java) offer the *same* programming model as Durable Functions — orchestrations, entities, activities, timers, external events — but run on any compute (Container Apps, AKS, VMs) and require the **Durable Task Scheduler** (a paid managed Azure resource) as their backend. + +For Chronos's profile they are the wrong trade: + +- **Backend cost.** SDK requires Durable Task Scheduler; Durable Functions can use Azure Storage at near-zero cost. +- **Built-in triggers.** Durable Functions ships `[TimerTrigger]`, HTTP, and Event Grid triggers; with the SDK we'd hand-roll a scheduled job and ingress. +- **Scale to zero.** Flex Consumption gives this for free; SDK on Container Apps/AKS needs KEDA configuration. +- **Operational footprint.** Durable Functions = Function App + Storage account. SDK = container host + DTS resource + custom monitoring + custom HTTP plumbing. +- **Microsoft's own guidance** (["Choose your orchestration framework"](https://learn.microsoft.com/en-us/azure/durable-task/common/choose-orchestration-framework)) says: choose Durable Functions for serverless, scale-to-zero, pay-per-execution, and quick prototyping — all of which fit Chronos. + +The hedge: the programming model is identical, so if Chronos ever needs to leave the Functions host (e.g. mandated AKS deployment), the orchestrator/entity/activity code carries over with mostly cosmetic changes. No design accommodation needed today. From ec83a96b25df65928a25ac61fff0a208d9880c9e Mon Sep 17 00:00:00 2001 From: Lukas Rottach Date: Sat, 18 Apr 2026 09:43:54 +0200 Subject: [PATCH 4/6] docs: align CLAUDE.md with confirmed architecture decisions Replace the "(choice deferred)" hedge in Target stack with the confirmed Durable Entity + eternal orchestrator decision, plus Flex Consumption hosting, Azure Storage backend, and ARG discovery. Add an Architecture (decided) section pointing at the README for the full diagram and rationale. Tighten the working-style notes: architecture is now fixed and shouldn't be relitigated; tag names, manual-override API, and Event Grid integration remain open and require asking the user. --- .claude/CLAUDE.md | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 6c69fde..a746451 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -11,12 +11,28 @@ The repository was originally written on .NET 6 (C# in-process Durable Functions ## Target stack (authoritative) - **Runtime**: .NET 10, Azure Functions isolated worker -- **Orchestration**: Azure Durable Functions (choice between Durable Entities vs. orchestrations is **not yet decided** — must be discussed before scaffolding) +- **Hosting**: Flex Consumption plan (Linux) — required for .NET 10 + Durable on Linux, scales to zero +- **Orchestration**: Azure Durable Functions — **Durable Entity per VM + eternal per-VM orchestrator + activities** (decided; see `README.md` → Architecture) +- **Durable backend**: Azure Storage (default provider) for v1; Durable Task Scheduler is a future migration if scale demands it +- **Discovery**: Azure Resource Graph (single KQL query, multi-subscription-ready) - **IaC**: Terraform (AzureRM provider), under `infra/` - **Deployment**: Azure Developer CLI (`azd`) headless, orchestrating the Terraform deployment. An `azure.yaml` will be added in a later feature - **Source layout**: `src/` for Functions code, `infra/` for Terraform, `deploy/` currently held empty via `.gitkeep` pending the infra decision -None of the above code exists yet. This initialization feature only establishes conventions and clears legacy code. +None of the above code exists yet. Project initialization establishes conventions and architecture; implementation lands in subsequent features. + +## Architecture (decided) + +Four components — full description, diagram, and rationale live in [`README.md`](../README.md) (`## Architecture`). Summary: + +- **`DiscoveryTimer`** (5-min Timer trigger) — ARG query for tagged VMs → signals each `VmScheduleEntity` with the materialized schedule, or `Clear()` for vanished tags. Spawns the orchestrator for actionable entities that don't yet have one. +- **`VmScheduleEntity`** — Durable Entity keyed by ARM resource ID. Holds start CRON, stop CRON, timezone, exclusion flag, last action. Source of truth for per-VM intent. +- **`VmScheduler`** — eternal Durable Orchestrator, one per VM. Reads entity, arms a durable timer to the next CRON occurrence, races it against the `ScheduleChanged` external event. On timer: calls start/stop activity. On event: recomputes. Always `ContinueAsNew`. +- **Start / Stop activities** — idempotent wrappers over `Azure.ResourceManager.Compute` via user-assigned managed identity. + +**Tag mutation mechanism (every case): discovery signals entity → entity raises `ScheduleChanged` → orchestrator reacts.** Edits update the schedule and re-arm the timer; full removal makes the orchestrator exit cleanly. No special cases. + +The standalone Durable Task SDK was evaluated and rejected — see the README section. Do not propose it again without a concrete reason. ## Scheduling scenarios the engine must support @@ -58,5 +74,6 @@ Do **not** add Claude attribution, co-author trailers, or "Generated with Claude ## Working-style notes for Claude - **Use current Microsoft documentation.** Patterns for Azure Functions have shifted substantially since .NET 6 (isolated worker model, new Durable Functions APIs, DI conventions). Do not replicate legacy in-process patterns. When in doubt, fetch current Microsoft Learn docs for Azure Functions, Durable Functions, .NET 10, and the Terraform AzureRM provider. -- **Ask before deciding architecture.** The user wants tag names, Durable Entities vs. orchestrations, timer cadence, identity model (managed identity scope, RBAC boundaries), and VM discovery strategy (subscription-wide ARG query vs. per-RG enumeration) co-designed — not pre-decided. +- **Architecture is fixed; do not relitigate.** The decisions in "Target stack" and "Architecture (decided)" — Durable Entities + eternal per-VM orchestrator on Flex Consumption with Azure Storage backend — were made deliberately and are documented in `README.md`. Do not propose alternatives (pure Timer trigger, ad-hoc orchestrations per event, standalone Durable Task SDK, external state store) without a concrete new reason. +- **Open questions remain — ask before deciding these.** Concrete Chronos tag *names*, the manual-override surface (HTTP API shape), and Event Grid integration timing are all undecided. When implementing a feature that touches these, ask first. - **No speculative scaffolding.** Don't generate full Function apps or Terraform modules without an explicit go-ahead for that feature. From 266f811553f884e51bb7d1f17129f4152070be27 Mon Sep 17 00:00:00 2001 From: Lukas Rottach Date: Sat, 18 Apr 2026 10:03:50 +0200 Subject: [PATCH 5/6] feat: scaffold initial Azure Functions project on .NET 10 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a Timer-triggered Azure Functions project from the official template under src/AzureChronos.Functions/. Targets net10.0, isolated worker (Microsoft.Azure.Functions.Worker 2.51.0), ASP.NET Core integration, Application Insights pre-wired. The sample timer fires every 5 minutes — placeholder for the future DiscoveryTimer. Drop src/.gitkeep now that the folder has real content. Update root .vscode launch/tasks/settings to drive the new project (auto-generated by the Functions extension). --- .vscode/launch.json | 12 +- .vscode/settings.json | 4 +- .vscode/tasks.json | 136 ++++----- src/.gitkeep | 0 src/AzureChronos.Functions/.gitignore | 264 ++++++++++++++++++ src/AzureChronos.Functions/AzureChronos.cs | 26 ++ src/AzureChronos.Functions/Program.cs | 14 + .../Properties/launchSettings.json | 9 + src/AzureChronos.Functions/host.json | 12 + src/AzureChronos.Functions/readme.md | 11 + src/AzureChronos.Functions/src.csproj | 21 ++ 11 files changed, 433 insertions(+), 76 deletions(-) delete mode 100644 src/.gitkeep create mode 100644 src/AzureChronos.Functions/.gitignore create mode 100644 src/AzureChronos.Functions/AzureChronos.cs create mode 100644 src/AzureChronos.Functions/Program.cs create mode 100644 src/AzureChronos.Functions/Properties/launchSettings.json create mode 100644 src/AzureChronos.Functions/host.json create mode 100644 src/AzureChronos.Functions/readme.md create mode 100644 src/AzureChronos.Functions/src.csproj diff --git a/.vscode/launch.json b/.vscode/launch.json index d526e72..ba965d1 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -2,10 +2,10 @@ "version": "0.2.0", "configurations": [ { - "name": "Attach to .NET Functions", - "type": "coreclr", - "request": "attach", - "processId": "${command:azureFunctions.pickProcess}" + "name": "Attach to .NET Functions", + "type": "coreclr", + "request": "attach", + "processId": "${command:azureFunctions.pickProcess}" } - ] -} \ No newline at end of file +] +} diff --git a/.vscode/settings.json b/.vscode/settings.json index 1ecef02..586f227 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,9 +1,9 @@ { - "azureFunctions.deploySubpath": "src/AzureChronos.Functions/bin/Release/net6.0/publish", + "azureFunctions.deploySubpath": "src/bin/Release/net10.0/publish", "azureFunctions.projectLanguage": "C#", "azureFunctions.projectRuntime": "~4", "debug.internalConsoleOptions": "neverOpen", "azureFunctions.projectSubpath": "src/AzureChronos.Functions", "azureFunctions.preDeployTask": "publish (functions)", "dotnet.defaultSolution": "src/AzureChronos.sln" -} \ No newline at end of file +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 75ba153..a6bf81e 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -2,80 +2,80 @@ "version": "2.0.0", "tasks": [ { - "label": "clean (functions)", - "command": "dotnet", - "args": [ - "clean", - "/property:GenerateFullPaths=true", - "/consoleloggerparameters:NoSummary" - ], - "type": "process", - "problemMatcher": "$msCompile", - "options": { - "cwd": "${workspaceFolder}/src/AzureChronos.Functions" - } + "label": "clean (functions)", + "command": "dotnet", + "args": [ + "clean", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "type": "process", + "problemMatcher": "$msCompile", + "options": { + "cwd": "${workspaceFolder}/src/AzureChronos.Functions" + } }, { - "label": "build (functions)", - "command": "dotnet", - "args": [ - "build", - "/property:GenerateFullPaths=true", - "/consoleloggerparameters:NoSummary" - ], - "type": "process", - "dependsOn": "clean (functions)", - "group": { - "kind": "build", - "isDefault": true - }, - "problemMatcher": "$msCompile", - "options": { - "cwd": "${workspaceFolder}/src/AzureChronos.Functions" - } + "label": "build (functions)", + "command": "dotnet", + "args": [ + "build", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "type": "process", + "dependsOn": "clean (functions)", + "group": { + "kind": "build", + "isDefault": true + }, + "problemMatcher": "$msCompile", + "options": { + "cwd": "${workspaceFolder}/src/AzureChronos.Functions" + } }, { - "label": "clean release (functions)", - "command": "dotnet", - "args": [ - "clean", - "--configuration", - "Release", - "/property:GenerateFullPaths=true", - "/consoleloggerparameters:NoSummary" - ], - "type": "process", - "problemMatcher": "$msCompile", - "options": { - "cwd": "${workspaceFolder}/src/AzureChronos.Functions" - } + "label": "clean release (functions)", + "command": "dotnet", + "args": [ + "clean", + "--configuration", + "Release", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "type": "process", + "problemMatcher": "$msCompile", + "options": { + "cwd": "${workspaceFolder}/src/AzureChronos.Functions" + } }, { - "label": "publish (functions)", - "command": "dotnet", - "args": [ - "publish", - "--configuration", - "Release", - "/property:GenerateFullPaths=true", - "/consoleloggerparameters:NoSummary" - ], - "type": "process", - "dependsOn": "clean release (functions)", - "problemMatcher": "$msCompile", - "options": { - "cwd": "${workspaceFolder}/src/AzureChronos.Functions" - } + "label": "publish (functions)", + "command": "dotnet", + "args": [ + "publish", + "--configuration", + "Release", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "type": "process", + "dependsOn": "clean release (functions)", + "problemMatcher": "$msCompile", + "options": { + "cwd": "${workspaceFolder}/src/AzureChronos.Functions" + } }, { - "type": "func", - "dependsOn": "build (functions)", - "options": { - "cwd": "${workspaceFolder}/src/AzureChronos.Functions/bin/Debug/net6.0" - }, - "command": "host start", - "isBackground": true, - "problemMatcher": "$func-dotnet-watch" + "type": "func", + "dependsOn": "build (functions)", + "options": { + "cwd": "${workspaceFolder}/src/bin/Debug/net10.0" + }, + "command": "host start", + "isBackground": true, + "problemMatcher": "$func-dotnet-watch" } - ] -} \ No newline at end of file +] +} diff --git a/src/.gitkeep b/src/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/AzureChronos.Functions/.gitignore b/src/AzureChronos.Functions/.gitignore new file mode 100644 index 0000000..3c3f4e6 --- /dev/null +++ b/src/AzureChronos.Functions/.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/src/AzureChronos.Functions/AzureChronos.cs b/src/AzureChronos.Functions/AzureChronos.cs new file mode 100644 index 0000000..af89c16 --- /dev/null +++ b/src/AzureChronos.Functions/AzureChronos.cs @@ -0,0 +1,26 @@ +using System; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Extensions.Logging; + +namespace AzureChronos.Functions; + +public class AzureChronos +{ + private readonly ILogger _logger; + + public AzureChronos(ILoggerFactory loggerFactory) + { + _logger = loggerFactory.CreateLogger(); + } + + [Function("AzureChronos")] + public void Run([TimerTrigger("0 */5 * * * *")] TimerInfo myTimer) + { + _logger.LogInformation("C# Timer trigger function executed at: {executionTime}", DateTime.Now); + + if (myTimer.ScheduleStatus is not null) + { + _logger.LogInformation("Next timer schedule at: {nextSchedule}", myTimer.ScheduleStatus.Next); + } + } +} \ No newline at end of file diff --git a/src/AzureChronos.Functions/Program.cs b/src/AzureChronos.Functions/Program.cs new file mode 100644 index 0000000..d0b91c8 --- /dev/null +++ b/src/AzureChronos.Functions/Program.cs @@ -0,0 +1,14 @@ +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Builder; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +var builder = FunctionsApplication.CreateBuilder(args); + +builder.ConfigureFunctionsWebApplication(); + +builder.Services + .AddApplicationInsightsTelemetryWorkerService() + .ConfigureFunctionsApplicationInsights(); + +builder.Build().Run(); diff --git a/src/AzureChronos.Functions/Properties/launchSettings.json b/src/AzureChronos.Functions/Properties/launchSettings.json new file mode 100644 index 0000000..f587f62 --- /dev/null +++ b/src/AzureChronos.Functions/Properties/launchSettings.json @@ -0,0 +1,9 @@ +{ + "profiles": { + "src": { + "commandName": "Project", + "commandLineArgs": "--port 7077", + "launchBrowser": false + } + } +} \ No newline at end of file diff --git a/src/AzureChronos.Functions/host.json b/src/AzureChronos.Functions/host.json new file mode 100644 index 0000000..d4f2d41 --- /dev/null +++ b/src/AzureChronos.Functions/host.json @@ -0,0 +1,12 @@ +{ + "version": "2.0", + "logging": { + "applicationInsights": { + "samplingSettings": { + "isEnabled": true, + "excludedTypes": "Request" + }, + "enableLiveMetricsFilters": true + } + } +} \ No newline at end of file diff --git a/src/AzureChronos.Functions/readme.md b/src/AzureChronos.Functions/readme.md new file mode 100644 index 0000000..4a8eb08 --- /dev/null +++ b/src/AzureChronos.Functions/readme.md @@ -0,0 +1,11 @@ +# TimerTrigger - C# + +The `TimerTrigger` makes it incredibly easy to have your functions executed on a schedule. This sample demonstrates a simple use case of calling your function every 5 minutes. + +## How it works + +For a `TimerTrigger` to work, you provide a schedule in the form of a [cron expression](https://en.wikipedia.org/wiki/Cron#CRON_expression)(See the link for full details). A cron expression is a string with 6 separate expressions which represent a given schedule via patterns. The pattern we use to represent every 5 minutes is `0 */5 * * * *`. This, in plain text, means: "When seconds is equal to 0, minutes is divisible by 5, for any hour, day of the month, month, day of the week, or year". + +## Learn more + + Documentation \ No newline at end of file diff --git a/src/AzureChronos.Functions/src.csproj b/src/AzureChronos.Functions/src.csproj new file mode 100644 index 0000000..d95b8e1 --- /dev/null +++ b/src/AzureChronos.Functions/src.csproj @@ -0,0 +1,21 @@ + + + + net10.0 + v4 + Exe + enable + enable + + + + + + + + + + + + + From 716258ee1cb554de6a567d240ad8c26336eab364 Mon Sep 17 00:00:00 2001 From: Lukas Rottach Date: Sat, 18 Apr 2026 10:04:06 +0200 Subject: [PATCH 6/6] refactor: name csproj after the project and add solution + global.json - Rename src.csproj to AzureChronos.Functions.csproj so the assembly output and project identity match the folder name. - Add src/AzureChronos.slnx referencing the Functions project (.NET 10 default .slnx format). - Add src/global.json pinning the SDK to 10.0.105 with rollForward=latestFeature. Build verified: dotnet build src/AzureChronos.slnx succeeds with no warnings. --- .../{src.csproj => AzureChronos.Functions.csproj} | 0 src/AzureChronos.slnx | 3 +++ src/global.json | 7 +++++++ 3 files changed, 10 insertions(+) rename src/AzureChronos.Functions/{src.csproj => AzureChronos.Functions.csproj} (100%) create mode 100644 src/AzureChronos.slnx create mode 100644 src/global.json diff --git a/src/AzureChronos.Functions/src.csproj b/src/AzureChronos.Functions/AzureChronos.Functions.csproj similarity index 100% rename from src/AzureChronos.Functions/src.csproj rename to src/AzureChronos.Functions/AzureChronos.Functions.csproj diff --git a/src/AzureChronos.slnx b/src/AzureChronos.slnx new file mode 100644 index 0000000..58d793d --- /dev/null +++ b/src/AzureChronos.slnx @@ -0,0 +1,3 @@ + + + diff --git a/src/global.json b/src/global.json new file mode 100644 index 0000000..f84dfe2 --- /dev/null +++ b/src/global.json @@ -0,0 +1,7 @@ +{ + "sdk": { + "version": "10.0.105", + "rollForward": "latestFeature", + "allowPrerelease": false + } +}