diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md new file mode 100644 index 0000000..a746451 --- /dev/null +++ b/.claude/CLAUDE.md @@ -0,0 +1,79 @@ +# 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 +- **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. 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 + +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. +- **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. 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/.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/.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 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/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. 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/.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 index ff5b00c..3c3f4e6 100644 --- a/src/AzureChronos.Functions/.gitignore +++ b/src/AzureChronos.Functions/.gitignore @@ -1,264 +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__/ +## 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 index ab2ef15..d95b8e1 100644 --- a/src/AzureChronos.Functions/AzureChronos.Functions.csproj +++ b/src/AzureChronos.Functions/AzureChronos.Functions.csproj @@ -1,27 +1,21 @@ - - - net6.0 - V4 - - - - - - - - - - - - - - - - PreserveNewest - - - PreserveNewest - Never - - - + + + + net10.0 + v4 + Exe + enable + enable + + + + + + + + + + + + + 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/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/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 index c036593..f587f62 100644 --- a/src/AzureChronos.Functions/Properties/launchSettings.json +++ b/src/AzureChronos.Functions/Properties/launchSettings.json @@ -1,9 +1,9 @@ -{ - "profiles": { - "AzureChronos.Functions": { - "commandName": "Project", - "commandLineArgs": "--port 7284", - "launchBrowser": false - } - } +{ + "profiles": { + "src": { + "commandName": "Project", + "commandLineArgs": "--port 7077", + "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 index beb2e40..d4f2d41 100644 --- a/src/AzureChronos.Functions/host.json +++ b/src/AzureChronos.Functions/host.json @@ -1,11 +1,12 @@ -{ - "version": "2.0", - "logging": { - "applicationInsights": { - "samplingSettings": { - "isEnabled": true, - "excludedTypes": "Request" - } - } - } +{ + "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.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/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/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 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 + } +}