diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 00000000..02eb140d --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,10 @@ +# Code style + +- Do not add jsdoc comments (/** ... */); TypeScript typing is sufficient. +- Use C block syntax (/* ... */) for method-level comments. +- Use C++ style comments (// ...) for inline comments (within method body). +- Nullable or boolean arguments are code smells; prefer configuration objects as arguments. +- Avoid casts and check if a cast is really necessary; do not use unnecessary casts. +- Always remove legacy and unreachable code. +- Avoid creating overly complex or large methods/modules; split into smaller, manageable functions with clear naming. +- Comment unclear code sections with C block comments explaining the reason for the code and, when applicable, the input and output produced. diff --git a/.github/workflows/code_review_infra_exp.yaml b/.github/workflows/code_review_infra_exp.yaml deleted file mode 100644 index 49a9aa04..00000000 --- a/.github/workflows/code_review_infra_exp.yaml +++ /dev/null @@ -1,264 +0,0 @@ -name: Infra Code Review - -on: - workflow_dispatch: - pull_request: - types: - - opened - - synchronize - - reopened - - ready_for_review - paths: - # Trigger the workflow when source files that generate terraform are modified - - "**/opex.ts" - - "**/openapi*.yaml" - - "**/openapi/**/*.yaml" - # Trigger the workflow when the involved workflows are modified - - ".github/workflows/code_review_infra_exp.yaml" - -permissions: - contents: read - pull-requests: write - id-token: write - -jobs: - setup_and_detect: - runs-on: ubuntu-latest - outputs: - environments: ${{ steps.detect.outputs.environments }} - env: - TURBO_CACHE_DIR: .turbo-cache - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup yarn - shell: bash - run: corepack enable - - - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 - with: - node-version-file: ".node-version" - cache: yarn - cache-dependency-path: "yarn.lock" - - - name: Setup turbo cache - if: ${{ env.TURBO_CACHE_DIR != '' }} - uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 - with: - path: ./${{ env.TURBO_CACHE_DIR }} - key: ${{ runner.os }}-turbo-${{ github.job }}-${{ github.sha }} - restore-keys: | - ${{ runner.os }}-turbo-${{ github.job }}- - - - name: Install dependencies - run: yarn install --immutable - - - name: Generate Terraform code - run: | - yarn turbo run infra:generate - - - name: Verify generated files - run: | - echo "Generated Terraform files:" - git ls-files --others '*.tf.json' || echo "No .tf.json files found" - - - name: Upload generated terraform files - uses: actions/upload-artifact@v4 - with: - name: terraform-files - path: | - **/*.tf.json - retention-days: 1 - - - name: Detect environments - id: detect - run: | - # Fetch main branch for comparison - git fetch origin main:main 2>/dev/null || git fetch origin main 2>/dev/null || true - - # Get changed files compared to main branch (same logic for both PR and manual dispatch) - if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then - changed_files=$(git diff --name-only main...HEAD 2>/dev/null || git diff --name-only HEAD^ 2>/dev/null || echo "") - else - changed_files=$(git diff --name-only main...HEAD 2>/dev/null || gh pr diff ${{ github.event.number }} --name-only 2>/dev/null || echo "") - fi - - if [ -z "$changed_files" ]; then - environments="[]" - else - # Find directories that meet the criteria - environments=$(echo "$changed_files" | while read file; do - if [ -n "$file" ]; then - dir=$(dirname "$file") - - # Check if the file is a .tf file that was modified - if [[ "$file" =~ \.tf$ ]]; then - echo "$dir" - # Check if the directory contains cdk.tf.json and has any modified files - elif [ -f "$dir/cdk.tf.json" ]; then - echo "$dir" - fi - fi - done | \ - sort -u | \ - while read dir; do - # Filter out directories that start with underscore or are contained in underscore directories - if [[ ! "$(basename "$dir")" =~ ^_ ]] && [[ ! "$dir" =~ /_[^/]*/ ]] && [[ ! "$dir" =~ /\._/ ]]; then - echo "$dir" - fi - done | \ - sort -u | \ - jq -R -s -c 'split("\n")[:-1] | map(select(length > 0))') - fi - - echo "environments=$environments" >> $GITHUB_OUTPUT - echo "Found environments with changes: $environments" - env: - GH_TOKEN: ${{ github.token }} - - code_review: - needs: [setup_and_detect] - if: needs.setup_and_detect.outputs.environments != '[]' - strategy: - matrix: - environment: ${{ fromJson(needs.setup_and_detect.outputs.environments) }} - fail-fast: false - name: Code Review Infra Plan (${{ matrix.environment }}) - runs-on: ubuntu-latest - environment: infra-dev-ci - concurrency: - group: ${{ github.workflow }}-${{ matrix.environment }}-infra/resources-ci - cancel-in-progress: false - permissions: - id-token: write - contents: read - pull-requests: write - env: - ARM_SUBSCRIPTION_ID: ${{ secrets.ARM_SUBSCRIPTION_ID }} - ARM_TENANT_ID: ${{ secrets.ARM_TENANT_ID }} - ARM_USE_OIDC: true - ARM_USE_AZUREAD: true - ARM_STORAGE_USE_AZUREAD: true - ARM_CLIENT_ID: ${{ secrets.ARM_CLIENT_ID }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - steps: - - name: Set directory - id: directory - env: - ENVIRONMENT: ${{ matrix.environment }} - run: | - set -euo pipefail - if [ -z "$ENVIRONMENT" ]; then - echo "Environment must be provided." - exit 1 - else - printf "dir=%q" "$ENVIRONMENT" >> "$GITHUB_OUTPUT" - fi - - - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Checkout - - - name: Download generated terraform files - uses: actions/download-artifact@v4 - with: - name: terraform-files - path: infra/ - - - name: Azure Login - uses: pagopa/dx/.github/actions/azure-login@main - - - name: Terraform Setup - id: set-terraform-version - uses: pagopa/dx/.github/actions/terraform-setup@main - - - name: Terraform Init - working-directory: ${{ steps.directory.outputs.dir }} - run: | - terraform init - - - name: Terraform Plan - id: plan - working-directory: ${{ steps.directory.outputs.dir }} - run: | - terraform plan -lock-timeout=3000s -no-color -out=plan.out 2>&1 | grep -v "hidden-link:" | tee tf_plan_stdout.txt - terraform show -no-color plan.out > full_plan.txt - - # Extracts only the diff section from the Plan by skipping everything before the resource changes, - # and filters out non-essential log lines like state refreshes and reads. - if [ -s full_plan.txt ]; then - sed -n '/^ #/,$p' full_plan.txt | grep -Ev "Refreshing state|state lock|Reading|Read" > filtered_plan.txt || echo "No changes detected." > filtered_plan.txt - else - echo "No plan output available." > filtered_plan.txt - fi - - # The summary with number of resources to be added, changed, or destroyed (will be used in case the plan output is too long) - SUMMARY_LINE=$(grep -E "^Plan: [0-9]+ to add" tf_plan_stdout.txt || echo "No changes.") - - echo "$SUMMARY_LINE" > plan_summary.txt - - # If the filtered plan is too long use the summary line, otherwise use the full filtered plan - if [ "$(wc -c < filtered_plan.txt)" -gt 60000 ]; then - echo "${SUMMARY_LINE}" > plan_output_multiline.txt - echo "" >> plan_output_multiline.txt - echo "Full plan output was too long and was omitted. Check the workflow logs for full details." >> plan_output_multiline.txt - else - cat filtered_plan.txt > plan_output_multiline.txt - fi - - # Error detection based on tf_plan_stdout.txt content - if grep -q "::error::Terraform exited with code" tf_plan_stdout.txt; then - echo "failed" - exit 1 - fi - - - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 - name: Post Plan on PR - id: comment - if: always() && github.event_name == 'pull_request' - env: - OUTPUT_DIR: ${{ steps.directory.outputs.dir }} - PLAN_STATUS: ${{ steps.plan.outcome }} - with: - script: | - const fs = require('fs'); - const outputDir = process.env.OUTPUT_DIR; - const output = fs.readFileSync(`${outputDir}/plan_output_multiline.txt`, 'utf8'); - const status = process.env.PLAN_STATUS; - const { data: comments } = await github.rest.issues.listComments({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number - }) - const botComment = comments.find(comment => { - return comment.user.type === 'Bot' && comment.body.includes(`Terraform Plan (${outputDir})`) - }) - const commentBody = `#### 📖 Terraform Plan (${outputDir}) - ${status} -
- Terraform Plan - - \`\`\`hcl - ${output} - \`\`\` - -
- `; - if (botComment) { - await github.rest.issues.deleteComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: botComment.id - }) - } - - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - body: commentBody, - issue_number: context.issue.number - }) - - - name: Check Terraform Plan Result - if: always() && steps.plan.outcome != 'success' - run: | - exit 1 diff --git a/.github/workflows/code_review_infra_opex.yaml b/.github/workflows/code_review_infra_opex.yaml new file mode 100644 index 00000000..69b5572a --- /dev/null +++ b/.github/workflows/code_review_infra_opex.yaml @@ -0,0 +1,67 @@ +name: Infra OPEX Code Review + +on: + pull_request: + types: + - opened + - synchronize + - reopened + - ready_for_review + paths: + # Trigger the workflow when source files that generate terraform are modified + - "test-opex-api/**/opex.ts" + - "test-opex-api/**/openapi/*.yaml" + # - "**/openapi*.yaml" + # - "**/openapi/**/*.yaml" + # Trigger the workflow when the involved workflows are modified + - ".github/workflows/code_review_infra_opex.yaml" + +permissions: + contents: read + pull-requests: write + id-token: write + +jobs: + generate_infra: + runs-on: ubuntu-latest + name: Generate Infra OPEX + steps: + - name: Checkout repository + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Corepack Enable + run: corepack enable + + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + node-version: "20" + cache: "yarn" + + - name: Install dependencies + run: yarn install + + - name: Build dependencies + run: yarn build + + - name: Generate infra for test-opex-api + run: yarn run generate:infra --workspace=test-opex-api + + - name: Upload generated infra artifacts + uses: actions/upload-artifact@v4 + with: + name: generated-infra-opex + path: apps/test-opex-api/stacks/ + + code_review_dev: + needs: generate_infra + uses: pagopa/dx-playground/.github/workflows/infra_plan_opex.yaml@chores/opex-refactor-hex + name: Code Review Infra OPEX Plan + secrets: inherit + with: + environment: opex-dashboard + override_github_environment: "infra-dev" + base_path: test-opex-api/stacks + artifact_name: generated-infra-opex diff --git a/.github/workflows/infra_plan_opex.yaml b/.github/workflows/infra_plan_opex.yaml new file mode 100644 index 00000000..1657bbc2 --- /dev/null +++ b/.github/workflows/infra_plan_opex.yaml @@ -0,0 +1,273 @@ +on: + workflow_call: + inputs: + environment: + description: Environment where the resources will be deployed. + type: string + required: true + base_path: + description: The base path on which the script will look for Terraform projects + type: string + required: true + artifact_name: + description: Name of the artifact containing generated infra files to download + type: string + required: false + env_vars: + description: List of environment variables to set up, given in env=value format. + type: string + required: false + use_private_agent: + description: Use a private agent to run the Terraform plan. + type: boolean + required: false + default: false + override_github_environment: + description: Set a value if GitHub Environment name is different than the TF environment folder + type: string + required: false + default: "" + use_labels: + description: Use labels to start the right environment's GitHub runner. If use_labels is true, also use_private_agent must be set to true + type: boolean + required: false + default: false + override_labels: + description: Needed for special cases where the environment alone is not sufficient as a distinguishing label + type: string + required: false + default: "" + +env: + ARM_SUBSCRIPTION_ID: ${{ secrets.ARM_SUBSCRIPTION_ID }} + ARM_TENANT_ID: ${{ secrets.ARM_TENANT_ID }} + ARM_USE_OIDC: true + ARM_USE_AZUREAD: true + ARM_STORAGE_USE_AZUREAD: true + TF_IN_AUTOMATION: true + REFRESH_STATE_EVERY_SECONDS: 600 # 10 minutes + +jobs: + get-terraform-version: + name: Get Terraform Version + runs-on: ubuntu-latest + outputs: + terraform_version: ${{ steps.get-version.outputs.terraform_version }} + steps: + - name: Checkout + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.7 + + - name: Get terraform version from .terraform-version file + id: get-version + uses: pagopa/dx/.github/actions/get-terraform-version@main + with: + default_version: "1.10.4" + + tf-modules-check: + uses: pagopa/dx/.github/workflows/static_analysis.yaml@main + name: Check terraform registry modules hashes + needs: [get-terraform-version] + secrets: inherit + with: + terraform_version: ${{ needs.get-terraform-version.outputs.terraform_version }} + check_to_run: lock_modules + folder: ${{ inputs.base_path }}/${{ inputs.environment }}/ + verbose: true + + tf_plan: + name: "Terraform Plan" + needs: [get-terraform-version, tf-modules-check] + # Use inputs.override_labels if set; otherwise, fall back to inputs.environment. + # When inputs.use_labels and inputs.use_private_agent are true, apply the selected labels. + # Default to 'self-hosted' if inputs.use_private_agent is true, or 'ubuntu-latest' otherwise. + runs-on: ${{ inputs.use_labels && inputs.use_private_agent && (inputs.override_labels != '' && inputs.override_labels || inputs.environment) || inputs.use_private_agent && 'self-hosted' || 'ubuntu-latest' }} + environment: ${{ inputs.override_github_environment == '' && inputs.environment || inputs.override_github_environment}}-ci + concurrency: + group: ${{ github.workflow }}-${{ inputs.environment }}-${{ inputs.base_path }}-ci + cancel-in-progress: false + permissions: + id-token: write + contents: read + pull-requests: write + env: + ARM_CLIENT_ID: ${{ secrets.ARM_CLIENT_ID }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + steps: + # Set the directory where the Terraform files are located + # The directory the value is then available in ${{ steps.directory.outputs.dir }} + - name: Set directory + id: directory + env: + ENVIRONMENT: ${{ inputs.environment }} + BASE_PATH: ${{ inputs.base_path }} + run: | + set -euo pipefail + + if [ -z "$ENVIRONMENT" ]; then + echo "Environment must be provided." + exit 1 + else + # The directory is expected to be in the format + # infra/resources/$ENVIRONMENT + # Example: infra/resources/prod + printf "dir=%q/%q" "$BASE_PATH" "$ENVIRONMENT" >> "$GITHUB_OUTPUT" + fi + + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + name: Checkout + + - name: Download generated infra artifacts + if: ${{ inputs.artifact_name }} + uses: actions/download-artifact@v4 + with: + name: ${{ inputs.artifact_name }} + path: ${{ inputs.base_path }} + + - name: Set Environment Variables + if: ${{ inputs.env_vars }} + env: + ENV_VARS: ${{ inputs.env_vars }} + run: | + set -euo pipefail + + for i in "$ENV_VARS[@]" + do + printf "%q\n" "$i" >> "$GITHUB_ENV" + done + + - name: Azure Login + uses: pagopa/dx/.github/actions/azure-login@main + + - name: Terraform Setup + id: set-terraform-version + uses: pagopa/dx/.github/actions/terraform-setup@main + with: + terraform_version: ${{ needs.get-terraform-version.outputs.terraform_version }} + + - name: Terraform Init + working-directory: ${{ steps.directory.outputs.dir }} + run: | + terraform init -input=false + + - name: Generate Cache Key + if: github.event_name == 'pull_request' + id: cache-key + env: + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + # Get current epoch time rounded to chosen window + TIME_WINDOW=$(( $(date +%s) / $REFRESH_STATE_EVERY_SECONDS )) + echo "cache-key=tfplan-pr${PR_NUMBER}-${TIME_WINDOW}s" >> $GITHUB_OUTPUT + + # Restore PR TFPlan Cache (miss = first run or >10 minutes, hit = within 10 minutes) + - name: Check for Cache + if: github.event_name == 'pull_request' + id: tfplan-pr-cache + uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 + with: + path: ${{ steps.directory.outputs.dir }}/.pr_plan_marker + key: ${{ steps.cache-key.outputs.cache-key }} + + - name: Set Terraform Refresh Flag + if: github.event_name == 'pull_request' + working-directory: ${{ steps.directory.outputs.dir }} + env: + CACHE_HIT: ${{ steps.tfplan-pr-cache.outputs.cache-hit }} + run: | + if [ "$CACHE_HIT" == "true" ]; then + echo "TF_PLAN_REFRESH_FLAG=-refresh=false -lock=false" >> $GITHUB_ENV + echo "::notice::State will NOT be refreshed" + else + echo "TF_PLAN_REFRESH_FLAG=" >> $GITHUB_ENV + echo "::notice::State will be refreshed" + + echo "$(date)" > ./.pr_plan_marker + fi + + # Run Terraform Plan + # The plan output is saved in a file and then processed to remove unnecessary lines + # The step never fails but the result is checked in the next step + # This is because we want to post the plan output in the PR even if the plan fails + - name: Terraform Plan + id: plan + working-directory: ${{ steps.directory.outputs.dir }} + run: | + terraform plan \ + -lock-timeout=120s \ + -input=false \ + -no-color \ + $TF_PLAN_REFRESH_FLAG \ + -out=plan.out 2>&1 | \ + grep -v "hidden-link:" | \ + tee tf_plan_stdout.txt + + terraform show -no-color plan.out > full_plan.txt + + # Extracts only the diff section from the Plan by skipping everything before the resource changes, + # and filters out non-essential log lines like state refreshes and reads. + if [ -s full_plan.txt ]; then + sed -n '/^ #/,$p' full_plan.txt | grep -Ev "hidden-link:|Refreshing state|state lock|Reading|Read" > filtered_plan.txt || echo "No changes detected." > filtered_plan.txt + else + echo "No plan output available." > filtered_plan.txt + fi + + # The summary with number of resources to be added, changed, or destroyed (will be used in case the plan output is too long) + SUMMARY_LINE=$(grep -E "^Plan: [0-9]+ to (add|change|destroy|import)" tf_plan_stdout.txt || echo "No changes.") + + # If the filtered plan is too long use the summary line, otherwise use the full filtered plan + if [ "$(wc -c < filtered_plan.txt)" -gt 60000 ]; then + echo "${SUMMARY_LINE}" > plan_output.txt + echo "COMPLETE_PLAN=false" >> $GITHUB_OUTPUT + else + cat filtered_plan.txt > plan_output.txt + echo "COMPLETE_PLAN=true" >> $GITHUB_OUTPUT + fi + + # Error detection based on tf_plan_stdout.txt content + if grep -q "::error::Terraform exited with code" tf_plan_stdout.txt; then + echo "failed" + exit 1 + fi + + - name: Set Plan Output + id: set-plan-output + env: + WORKFLOW_URL: "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" + OUTPUT_DIR: ${{ steps.directory.outputs.dir }} + PLAN_OUTCOME: ${{ steps.plan.outcome }} + COMPLETE_PLAN: ${{ steps.plan.outputs.COMPLETE_PLAN }} + working-directory: ${{ steps.directory.outputs.dir }} + run: | + echo "### 📖 Terraform Plan ($OUTPUT_DIR) - $PLAN_OUTCOME" > message_body.txt + echo "
" >> message_body.txt + echo "Show Plan" >> message_body.txt + echo "" >> message_body.txt + + echo "\`\`\`hcl" >> message_body.txt + cat plan_output.txt >> message_body.txt + echo "\`\`\`" >> message_body.txt + + if [ $COMPLETE_PLAN == 'false' ]; then + echo "Full plan output was too long and was omitted. Check the [workflow logs]($WORKFLOW_URL) for full details." >> message_body.txt + fi + + echo "" >> message_body.txt + echo "
" >> message_body.txt + + # Post the plan output in the PR + - name: Post Plan on PR + id: comment + if: always() && github.event_name == 'pull_request' + uses: pagopa/dx/actions/pr-comment@main + env: + COMMENT_BODY_FILE: "${{ steps.directory.outputs.dir }}/message_body.txt" + with: + comment-body-file: ${{ env.COMMENT_BODY_FILE }} + search-pattern: "Terraform Plan (${{ steps.directory.outputs.dir }})" + + # Fail the workflow if the Terraform plan failed + - name: Check Terraform Plan Result + if: always() && steps.plan.outcome != 'success' + run: | + exit 1 diff --git a/.gitignore b/.gitignore index 6eca8e55..07cc7ad2 100644 --- a/.gitignore +++ b/.gitignore @@ -294,3 +294,5 @@ changeset-status.json cdktf.out/ cdk.tf.json manifest.json + +stacks diff --git a/.vscode/settings.json b/.vscode/settings.json index 99282936..fc16e396 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -6,5 +6,8 @@ "editor.formatOnSave": true, "editor.formatOnSaveMode": "file", "files.autoSave": "onFocusChange", - "vs-code-prettier-eslint.prettierLast": false + "vs-code-prettier-eslint.prettierLast": false, + "[typescript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + } } diff --git a/apps/test-opex-api/package.json b/apps/test-opex-api/package.json index adf55354..0d0c9a17 100644 --- a/apps/test-opex-api/package.json +++ b/apps/test-opex-api/package.json @@ -11,13 +11,12 @@ "typecheck": "tsc --noemit", "lint": "eslint src --fix", "lint:check": "eslint src", - "infra:generate": "yarn -q dlx tsx src/opex.ts" + "generate:infra": "yarn -q dlx tsx src/opex.ts" }, "dependencies": { + "@pagopa/opex-dashboard-ts": "workspace:^", "cdktf": "^0.21.0", - "cdktf-monitoring-stack": "workspace:*", - "constructs": "^10.4.2", - "opex-common": "workspace:*" + "constructs": "^10.4.2" }, "devDependencies": { "@pagopa/eslint-config": "^5.0.0", diff --git a/apps/test-opex-api/src/opex.ts b/apps/test-opex-api/src/opex.ts index 6a0e4220..b8cc4799 100644 --- a/apps/test-opex-api/src/opex.ts +++ b/apps/test-opex-api/src/opex.ts @@ -1,31 +1,81 @@ -import { App, AzurermBackend } from "cdktf"; -import { MonitoringStack } from "cdktf-monitoring-stack"; -import { backendConfig, opexConfig } from "opex-common"; +/* eslint-disable no-console */ +import { addOpexStack, DashboardConfig } from "@pagopa/opex-dashboard-ts"; +import { App, AzurermBackend, AzurermBackendConfig } from "cdktf"; -const app = new App(); +const opexConfig: DashboardConfig = { + action_groups: [ + "/subscriptions/uuid/resourceGroups/my-rg/providers/microsoft.insights/actionGroups/my-action-group-email", + "/subscriptions/uuid/resourceGroups/my-rg/providers/microsoft.insights/actionGroups/my-action-group-slack", + ], + data_source: + "/subscriptions/uuid/resourceGroups/my-rg/providers/Microsoft.Network/applicationGateways/my-gtw", + name: "My spec", + oa3_spec: + "https://raw.githubusercontent.com/pagopa/opex-dashboard/refs/heads/main/test/data/io_backend_light.yaml", + overrides: { + endpoints: { + "/api/v1/services/{service_id}": { + availability_evaluation_frequency: 30, // Default: 10 + availability_evaluation_time_window: 50, // Default: 20 + availability_event_occurrences: 3, // Default: 1 + availability_threshold: 0.95, // Default: 99% + response_time_evaluation_frequency: 35, // Default: 10 + response_time_evaluation_time_window: 55, // Default: 20 + response_time_event_occurrences: 5, // Default: 1 + response_time_threshold: 2, // Default: 1 + }, + }, + hosts: [ + // Use these hosts instead of those inside the OpenApi spec + "https://example.com", + "https://example2.com", + ], + }, + resource_group_name: "dashboards", + resource_type: "app-gateway", + tags: { + Environment: "TEST", + Owner: "team-opex", + }, + timespan: "6m", // Default, a number or a timespan https://docs.microsoft.com/en-us/azure/data-explorer/kusto/query/scalar-data-types/timespan +}; -const stack1 = new MonitoringStack(app, "test-opex-api-v1", { - config: opexConfig, - openApiFilePaths: ["docs/openapi-v2.yaml"], -}); - -new AzurermBackend(stack1, { - ...backendConfig, +const backendConfig: AzurermBackendConfig = { + containerName: "tfstate", key: "dx.test-opex-api1.tfstate", -}); + resourceGroupName: "my-rg", + storageAccountName: "mystorageaccount", +}; + +const app = new App({ hclOutput: true, outdir: "." }); -/////// +(async function () { + try { + const { opexStack: s1 } = await addOpexStack({ + app, + config: opexConfig, + }); -const stack2 = new MonitoringStack(app, "test-opex-api-v2", { - config: opexConfig, - openApiFilePaths: ["docs/openapi-v3.yaml"], -}); + new AzurermBackend(s1, { + ...backendConfig, + key: "dx.test-opex-api1.tfstate", + }); -new AzurermBackend(stack2, { - ...backendConfig, - key: "dx.test-opex-api2.tfstate", -}); + // Add more stacks if needed + // const { opexStack: s2 } = await addAzureDashboard({ + // app, + // config: opexConfig, + // }); + // new AzurermBackend(s2, { + // ...backendConfig, + // key: "dx.test-opex-api2.tfstate", + // }); -/////// + app.synth(); -app.synth(); + console.log("Terraform CDKTF code generated successfully"); + } catch (error) { + console.error("Error generating Terraform CDKTF code:", error); + process.exit(1); + } +})(); diff --git a/apps/to-do-api/package.json b/apps/to-do-api/package.json index ca242f0d..fa771936 100644 --- a/apps/to-do-api/package.json +++ b/apps/to-do-api/package.json @@ -30,11 +30,9 @@ "@to-do/azure-adapters": "workspace:^", "@to-do/domain": "workspace:^", "cdktf": "^0.21.0", - "cdktf-monitoring-stack": "workspace:*", "constructs": "^10.4.2", "fp-ts": "^2.16.10", "io-ts": "^2.2.22", - "opex-common": "workspace:*", "ulid": "^3.0.0" }, "devDependencies": { diff --git a/apps/to-do-api/src/opex.ts b/apps/to-do-api/src/opex.ts index d2438d4e..328d94fe 100644 --- a/apps/to-do-api/src/opex.ts +++ b/apps/to-do-api/src/opex.ts @@ -1,24 +1,24 @@ -import { App, AzurermBackend } from "cdktf"; -import { MonitoringStack } from "cdktf-monitoring-stack"; -import { backendConfig, opexConfig } from "opex-common"; +// import { App, AzurermBackend } from "cdktf"; +// import { MonitoringStack } from "cdktf-monitoring-stack"; +// import { backendConfig, opexConfig } from "opex-common"; -const app = new App(); +// const app = new App(); -// Use the following lines if you need to resolve paths dynamically -// -// import * as path from "path"; -// import { fileURLToPath } from "url"; -// const __filename = fileURLToPath(import.meta.url); -// const __dirname = path.dirname(__filename); -// -const stack = new MonitoringStack(app, "to-do-api", { - config: opexConfig, - openApiFilePaths: ["docs/openapi.yaml"], -}); +// // Use the following lines if you need to resolve paths dynamically +// // +// // import * as path from "path"; +// // import { fileURLToPath } from "url"; +// // const __filename = fileURLToPath(import.meta.url); +// // const __dirname = path.dirname(__filename); +// // +// const stack = new MonitoringStack(app, "to-do-api", { +// config: opexConfig, +// openApiFilePaths: ["docs/openapi.yaml"], +// }); -new AzurermBackend(stack, { - ...backendConfig, - key: "dx.opex.to-do-api.tfstate", -}); +// new AzurermBackend(stack, { +// ...backendConfig, +// key: "dx.opex.to-do-api.tfstate", +// }); -app.synth(); +// app.synth(); diff --git a/apps/to-do-webapp/package.json b/apps/to-do-webapp/package.json index 334dc582..3dbfaa4d 100644 --- a/apps/to-do-webapp/package.json +++ b/apps/to-do-webapp/package.json @@ -6,10 +6,10 @@ "scripts": { "clean": "shx rm -rf out/ dist/ tmp_dir/", "dev": "next dev --turbopack", - "build": "yarn generate && next build && yarn standalone:copy && yarn standalone:clean && yarn standalone:rename", + "build": "next build && yarn standalone:copy && yarn standalone:clean && yarn standalone:rename", "start": "next start", "lint": "eslint src --fix", - "standalone:copy": "shx mkdir tmp_dir && shx cp -r .next/standalone/apps/to-do-webapp/.next/* tmp_dir && shx cp -r .next/static tmp_dir && shx mkdir dist && shx cp .next/standalone/apps/to-do-webapp/server.js dist", + "standalone:copy": "shx mkdir -p tmp_dir && shx cp -r .next/standalone/apps/to-do-webapp/.next/* tmp_dir && shx cp -r .next/static tmp_dir && shx mkdir dist && shx cp .next/standalone/apps/to-do-webapp/server.js dist", "standalone:clean": "shx rm -rf .next", "standalone:rename": "shx mv tmp_dir .next", "generate": "gen-api-models --api-spec https://raw.githubusercontent.com/pagopa/dx-playground/4cb60e3994eb14a76e6b3d1c49075c76f2f3cfbc/apps/to-do-api/docs/openapi.yaml --out-dir ./src/lib/client --no-strict --request-types --response-decoders --client" diff --git a/infra/bootstrapper/dev/.terraform.lock.hcl b/infra/bootstrapper/dev/.terraform.lock.hcl index 8dce70bf..71f2b405 100644 --- a/infra/bootstrapper/dev/.terraform.lock.hcl +++ b/infra/bootstrapper/dev/.terraform.lock.hcl @@ -9,6 +9,7 @@ provider "registry.terraform.io/hashicorp/azuread" { "h1:2rk36pu4YyhBVz/Mf4swYCQxaB31iPaXOiWNlqZMXbM=", "h1:7ZNdNGnUB6N6Z6St3COzRXFaghMEyYkZt7WyOCRKOqo=", "h1:EZNO8sEtUABuRxujQrDrW1z1QsG0dq6iLbzWtnG7Om4=", + "h1:GS/WN8VS6Wp9hvs46lgDsR4ERV8o3Sr+zatF/z2XohU=", "zh:162916b037e5133f49298b0ffa3e7dcef7d76530a8ca738e7293373980f73c68", "zh:1c3e89cf19118fc07d7b04257251fc9897e722c16e0a0df7b07fcd261f8c12e7", "zh:492931cea4f30887ab5bca36a8556dfcb897288eddd44619c0217fc5da2d57e7", @@ -28,6 +29,7 @@ provider "registry.terraform.io/hashicorp/azurerm" { version = "4.35.0" constraints = ">= 3.0.0, >= 3.110.0, ~> 4.0, < 5.0.0" hashes = [ + "h1:/4tb6lwsWlSu3XCqFS7XPItU3dkvMSCOIW9/nhF2+IA=", "h1:3zJsyLWItriZDhtx6kBkoy9UPcA9l5G4PKi4ZYxhsnA=", "h1:WnSak7fbscDv2LIBl9o/2zoQj4fnEq1JmUelFw2EMrw=", "h1:k2PriDCNaoY/osyZzfje7oyZipzbCHcjtQcEmNtKYHU=", @@ -55,6 +57,7 @@ provider "registry.terraform.io/integrations/github" { "h1:GV6m1zdKNV0ECNhvzj7UoBXQz1Cv2UQa0UZ7Wt4RgTw=", "h1:P4SRG4605PvPKASeDu1lW49TTz1cCGsjQ7qbOBgNd6I=", "h1:Yq0DZYKVFwPdc+v5NnXYcgTYWKInSkjv5WjyWMODj9U=", + "h1:jPtUxZC/fwDeA+CPJoJHAhoDy/KhZdE7viycsWuXvgM=", "zh:0b1b5342db6a17de7c71386704e101be7d6761569e03fb3ff1f3d4c02c32d998", "zh:2fb663467fff76852126b58315d9a1a457e3b04bec51f04bf1c0ddc9dfbb3517", "zh:4183e557a1dfd413dae90ca4bac37dbbe499eae5e923567371f768053f977800", @@ -78,6 +81,7 @@ provider "registry.terraform.io/pagopa-dx/azure" { constraints = ">= 0.0.6, >= 0.0.7, < 1.0.0" hashes = [ "h1:+1dGui62P1/cvAnQYiQLwW50cTrHg3hsPo4GblmIYaE=", + "h1:fCV5KUBAZPEG/KHos32DeYkipT4adxzDFQVdNsHBr4U=", "h1:h7qyvjqn7mGB56DvVhCb2laGWNRxSD+mnj0VC0HhGE4=", "h1:i5I234l00EsX7wTJfU2t521pRXlr0T/tH729aF7Fa8w=", "h1:w8bOpDG6/f+Wgdgnlc66gFXRbEL+6fNt8z/ht3R0ufs=", diff --git a/infra/resources/_modules/api/.terraform.lock.hcl b/infra/resources/_modules/api/.terraform.lock.hcl index 119f8bd1..50584c88 100644 --- a/infra/resources/_modules/api/.terraform.lock.hcl +++ b/infra/resources/_modules/api/.terraform.lock.hcl @@ -4,6 +4,7 @@ provider "registry.terraform.io/hashicorp/azurerm" { version = "4.14.0" hashes = [ + "h1:FYWhn/x1jSjnxUsKkV4+sXIfOc+H3Sq8ja/ZBB2IAaU=", "h1:cGkb9Ps5A1FwXO7BaZ9T7Ufe79gsNzk6lfaNfcWn0+s=", "zh:05aaea16fc5f27b14d9fbad81654edf0638949ed3585576b2219c76a2bee095a", "zh:065ce6ed16ba3fa7efcf77888ea582aead54e6a28f184c6701b73d71edd64bb0", diff --git a/infra/resources/_modules/application_insights/.terraform.lock.hcl b/infra/resources/_modules/application_insights/.terraform.lock.hcl index cf9cb179..3292709c 100644 --- a/infra/resources/_modules/application_insights/.terraform.lock.hcl +++ b/infra/resources/_modules/application_insights/.terraform.lock.hcl @@ -5,6 +5,7 @@ provider "registry.terraform.io/hashicorp/azurerm" { version = "4.17.0" hashes = [ "h1:VgnUh7PiRa/76P+0NFk8vmrmfLnPT6+tOZ/AP6h4TeQ=", + "h1:aTtjvuCBO0X0VbcorVVdBmxJBcTiLx43om+UpmNBxHc=", "zh:163b81a3bf29c8f161a1c100a48164b1bd1af434cd564b44596cb71a6c33f03d", "zh:2996b107d3c05a9db14458b32b6f22f8cde0adb96263196d82d3dc302907a257", "zh:361abd84b6e73016ebebb9ef9cd14c237d8b1e4500ea75f73243ff0534e5e4fb", @@ -25,6 +26,7 @@ provider "registry.terraform.io/pagopa-dx/azure" { constraints = "~> 0.0" hashes = [ "h1:I/tTgCuaapwwzUuoaxIg+x8/HNr8pYdYSGeiNpobKOw=", + "h1:OahH6LwxRUBmh0GqOlX6gM68dpb24wm6svQl/hCrZeA=", "zh:2301715691aabde18a23834654b236f972aecf4448df62750c5235e4c219b9ff", "zh:42c235c4bd422fdf97c84dd91cb5b8db00293b9f8785c86377358f9d72d66f92", "zh:49ece07513d55aa3c1fffd374ab51c3097aa7518196e76eb5d8dff066d5c1a53", diff --git a/infra/resources/_modules/virtual_machine/.terraform.lock.hcl b/infra/resources/_modules/virtual_machine/.terraform.lock.hcl index 28d7f2ee..4708f07a 100644 --- a/infra/resources/_modules/virtual_machine/.terraform.lock.hcl +++ b/infra/resources/_modules/virtual_machine/.terraform.lock.hcl @@ -5,6 +5,7 @@ provider "registry.terraform.io/hashicorp/azurerm" { version = "4.13.0" hashes = [ "h1:IAy+6S1EY78ZyipSZDqjMLFLMMn9UBaz9tZE2i4aKEI=", + "h1:IzEHsQIYeSaD1gwSyP474QYELBAezcvhPWCQIkjfbgs=", "zh:23e04573f50cec091cb32113e3e78033b1ba00ddbc9b7aece0d6397ae60b9b5e", "zh:53d07a697e5aa36561a4b11e22a68c7cb582d46ed42cd4a61c08796d38f18bc9", "zh:56064e9fbd5330ba734af24aca23ed0c93b12117474ae08d8180bca0dbf3ac06", diff --git a/infra/resources/dev/tfmodules.lock.json b/infra/resources/dev/tfmodules.lock.json index 949dbfdb..ec3313b3 100644 --- a/infra/resources/dev/tfmodules.lock.json +++ b/infra/resources/dev/tfmodules.lock.json @@ -1,15 +1,15 @@ { "apim": { - "hash": "7c4ede49c7e6ebd45b656abca088b846d0e417d3e8e9b700464c7acaefbe5d04", - "version": "1.2.2", + "hash": "853ca35ce07874a2bf1891ef9e9fc8f687c330a8cfa79c31ff1c76ddfa375b06", + "version": "2.0.1", "name": "azure_api_management", - "source": "https://registry.terraform.io/modules/pagopa-dx/azure-api-management/azurerm/1.2.2" + "source": "https://registry.terraform.io/modules/pagopa-dx/azure-api-management/azurerm/2.0.1" }, "apim_roles": { - "hash": "433fe44b2863b8af8d4f4206ee2590599b0cb67d78b372e65d68317d3199aaa1", - "version": "1.1.2", + "hash": "9d429fd0aba8a29c1a0dbb6005e7dd72a79d9d8dda5076220eb9f0c8bef438ae", + "version": "1.2.1", "name": "azure_role_assignments", - "source": "https://registry.terraform.io/modules/pagopa-dx/azure-role-assignments/azurerm/1.1.2" + "source": "https://registry.terraform.io/modules/pagopa-dx/azure-role-assignments/azurerm/1.2.1" }, "app_service": { "hash": "1578c3d974a6e23784264e334d4a5fc51fc11bb42e73d650d7f6ff335346184c", @@ -18,34 +18,34 @@ "source": "https://registry.terraform.io/modules/pagopa-dx/azure-app-service/azurerm/1.0.1" }, "app_service_roles": { - "hash": "433fe44b2863b8af8d4f4206ee2590599b0cb67d78b372e65d68317d3199aaa1", - "version": "1.1.2", + "hash": "9d429fd0aba8a29c1a0dbb6005e7dd72a79d9d8dda5076220eb9f0c8bef438ae", + "version": "1.2.1", "name": "azure_role_assignments", - "source": "https://registry.terraform.io/modules/pagopa-dx/azure-role-assignments/azurerm/1.1.2" + "source": "https://registry.terraform.io/modules/pagopa-dx/azure-role-assignments/azurerm/1.2.1" }, "azure_function_v3_function_app": { - "hash": "20d1bf568efdf78dc97cf5dac105c7f30caf710e7a49a3edf7acab1ea78a4699", - "version": "1.0.1", + "hash": "a38c42efbd1577c3971123cc4f335a6827e0ead44695b9d014789e0598c2672d", + "version": "3.0.0", "name": "azure_function_app", - "source": "https://registry.terraform.io/modules/pagopa-dx/azure-function-app/azurerm/1.0.1" + "source": "https://registry.terraform.io/modules/pagopa-dx/azure-function-app/azurerm/3.0.0" }, "cosmos": { - "hash": "398ffd2c1fb90f2228a5d42e7f90e6601162c2e90e23daeee726e1f1f02aa322", - "version": "0.1.0", + "hash": "1e6e66245084310310d36106509c99ee0eed25e98640680c4acc8cff426ff321", + "version": "0.2.0", "name": "azure_cosmos_account", - "source": "https://registry.terraform.io/modules/pagopa-dx/azure-cosmos-account/azurerm/0.1.0" + "source": "https://registry.terraform.io/modules/pagopa-dx/azure-cosmos-account/azurerm/0.2.0" }, "func_api_role": { - "hash": "433fe44b2863b8af8d4f4206ee2590599b0cb67d78b372e65d68317d3199aaa1", - "version": "1.1.2", + "hash": "9d429fd0aba8a29c1a0dbb6005e7dd72a79d9d8dda5076220eb9f0c8bef438ae", + "version": "1.2.1", "name": "azure_role_assignments", - "source": "https://registry.terraform.io/modules/pagopa-dx/azure-role-assignments/azurerm/1.1.2" + "source": "https://registry.terraform.io/modules/pagopa-dx/azure-role-assignments/azurerm/1.2.1" }, "function_app": { - "hash": "20d1bf568efdf78dc97cf5dac105c7f30caf710e7a49a3edf7acab1ea78a4699", - "version": "1.0.1", + "hash": "a38c42efbd1577c3971123cc4f335a6827e0ead44695b9d014789e0598c2672d", + "version": "3.0.0", "name": "azure_function_app", - "source": "https://registry.terraform.io/modules/pagopa-dx/azure-function-app/azurerm/1.0.1" + "source": "https://registry.terraform.io/modules/pagopa-dx/azure-function-app/azurerm/3.0.0" }, "function_test_durable": { "hash": "367548c5df2a42f5273583c0de4181c865bd419a4d0aca3cffa2ba19a90c210a", @@ -54,10 +54,10 @@ "source": "https://registry.terraform.io/modules/pagopa-dx/azure-function-app/azurerm/0.3.1" }, "function_v3_api_role": { - "hash": "433fe44b2863b8af8d4f4206ee2590599b0cb67d78b372e65d68317d3199aaa1", - "version": "1.1.2", + "hash": "9d429fd0aba8a29c1a0dbb6005e7dd72a79d9d8dda5076220eb9f0c8bef438ae", + "version": "1.2.1", "name": "azure_role_assignments", - "source": "https://registry.terraform.io/modules/pagopa-dx/azure-role-assignments/azurerm/1.1.2" + "source": "https://registry.terraform.io/modules/pagopa-dx/azure-role-assignments/azurerm/1.2.1" }, "naming_convention": { "hash": "d7973237e601af346ca3cc8797623f4edff8bdb94b661453d4242609f8d49118", diff --git a/packages/cdktf-monitoring-stack/package.json b/packages/cdktf-monitoring-stack/package.json deleted file mode 100644 index e69aba2f..00000000 --- a/packages/cdktf-monitoring-stack/package.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "name": "cdktf-monitoring-stack", - "version": "1.0.0", - "private": true, - "main": "dist/index.js", - "types": "dist/index.d.ts", - "type": "module", - "scripts": { - "build": "tsc", - "typecheck": "tsc --noemit", - "lint": "eslint src --fix", - "lint:check": "eslint src" - }, - "dependencies": { - "@cdktf/provider-azurerm": "^14.2.0", - "cdktf": "^0.21.0", - "constructs": "^10.4.2", - "js-yaml": "^4.1.0" - }, - "devDependencies": { - "@pagopa/eslint-config": "^5.0.0", - "@pagopa/typescript-config-node": "workspace:^", - "@types/js-yaml": "^4.0.9", - "eslint": "^9.30.1", - "prettier": "^3.6.2", - "typescript": "^5.8.3" - } -} diff --git a/packages/cdktf-monitoring-stack/src/dashboard-generator.ts b/packages/cdktf-monitoring-stack/src/dashboard-generator.ts deleted file mode 100644 index 5b013506..00000000 --- a/packages/cdktf-monitoring-stack/src/dashboard-generator.ts +++ /dev/null @@ -1,156 +0,0 @@ -import type { EndpointWithProperties } from "./openapi-processor.js"; - -import { MonitoringConfig } from "./monitoring-stack.js"; -import { - getApimAvailabilityQuery, - getApimResponseCodesQuery, - getApimResponseTimeQuery, -} from "./query-builder.js"; -import { uriToRegex } from "./uri-utils.js"; - -/** - * Dashboard chart configuration types - */ -export interface ChartOptions { - logAnalyticsWorkspaceId: string; - position: { colSpan: number; rowSpan: number; x: number; y: number }; - query: string; - specificChart: "Line" | "Pie" | "StackedArea"; - splitBy?: { name: string; type: string }[]; - subtitle: string; - title: string; - yAxis: { name: string; type: string }[]; -} - -/** - * Creates a chart part configuration for the dashboard - */ -export function createChartPart( - options: ChartOptions, -): Record { - return { - metadata: { - inputs: [ - { - name: "Scope", - value: { resourceIds: [options.logAnalyticsWorkspaceId] }, - }, - { name: "Query", value: options.query }, - { name: "Version", value: "2.0" }, - { name: "TimeRange", value: "PT4H" }, - { name: "PartTitle", value: options.title }, - { name: "PartSubTitle", value: options.subtitle }, - ], - settings: { - content: { - Dimensions: { - aggregation: "Sum", - splitBy: options.splitBy || [], - xAxis: { name: "TimeGenerated", type: "datetime" }, - yAxis: options.yAxis, - }, - LegendOptions: { isEnabled: true, position: "Bottom" }, - PartSubTitle: options.subtitle, - PartTitle: options.title, - Query: options.query, - SpecificChart: options.specificChart, - }, - }, - type: "Extension/Microsoft_OperationsManagementSuite_Workspace/PartType/LogsDashboardPart", - }, - position: options.position, - }; -} - -/** - * Generates dashboard properties JSON string for the Azure Portal Dashboard - */ -export function generateDashboardProperties( - endpoints: EndpointWithProperties[], - config: MonitoringConfig, -): string { - const parts: Record> = {}; - - endpoints.forEach((endpoint, index) => { - const yPos = index * 4; - const hostPath = endpoint.host - ? `${endpoint.host}${endpoint.path}` - : endpoint.path; - const subtitle = `${endpoint.method} ${hostPath}`; - const endpointPath = uriToRegex(endpoint.path); - - // Create three parts for each endpoint: Availability, Response Codes, - // and Response Time - - // Availability Chart - parts[index * 3 + 0] = createChartPart({ - logAnalyticsWorkspaceId: config.logAnalyticsWorkspaceId, - position: { colSpan: 6, rowSpan: 4, x: 0, y: yPos }, - query: getApimAvailabilityQuery({ - endpointPath, - isAlarm: false, - threshold: endpoint.availabilityThreshold, - timeSpan: endpoint.availabilityTimeSpan, - }), - specificChart: "Line", - subtitle, - title: "Availability (PT5M)", - yAxis: [ - { name: "availability", type: "real" }, - { name: "watermark", type: "real" }, - ], - }); - - // Response Codes Chart - parts[index * 3 + 1] = createChartPart({ - logAnalyticsWorkspaceId: config.logAnalyticsWorkspaceId, - position: { colSpan: 6, rowSpan: 4, x: 6, y: yPos }, - query: getApimResponseCodesQuery({ - endpointPath, - isAlarm: false, - threshold: endpoint.responseCodeThreshold, - timeSpan: endpoint.responseCodeTimeSpan, - }), - specificChart: "StackedArea", - splitBy: [{ name: "HTTPStatus", type: "string" }], - subtitle, - title: "Response Codes (PT5M)", - yAxis: [{ name: "count_", type: "long" }], - }); - - // Response Time Chart - parts[index * 3 + 2] = createChartPart({ - logAnalyticsWorkspaceId: config.logAnalyticsWorkspaceId, - position: { colSpan: 6, rowSpan: 4, x: 12, y: yPos }, - query: getApimResponseTimeQuery({ - endpointPath, - isAlarm: false, - threshold: endpoint.responseTimeThreshold, - timeSpan: endpoint.responseTimeTimeSpan, - }), - specificChart: "Line", - subtitle, - title: "95th Percentile Response Time (ms)", - yAxis: [ - { name: "duration_percentile_95", type: "real" }, - { name: "watermark", type: "long" }, - ], - }); - }); - - const dashboardStructure = { - properties: { - lenses: { "0": { order: 0, parts: parts } }, - metadata: { - model: { - timeRange: { - type: "MsPortalFx.Composition.Configuration.ValueTypes.TimeRange", - value: { relative: { duration: 4, timeUnit: 2 } }, - }, - }, - }, - }, - }; - - return JSON.stringify(dashboardStructure.properties); -} diff --git a/packages/cdktf-monitoring-stack/src/index.ts b/packages/cdktf-monitoring-stack/src/index.ts deleted file mode 100644 index bc08b07e..00000000 --- a/packages/cdktf-monitoring-stack/src/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export { - MonitoringConfig, - MonitoringStack, - MonitoringStackProps, - Tags, -} from "./monitoring-stack.js"; diff --git a/packages/cdktf-monitoring-stack/src/monitoring-stack.ts b/packages/cdktf-monitoring-stack/src/monitoring-stack.ts deleted file mode 100644 index 030bf28e..00000000 --- a/packages/cdktf-monitoring-stack/src/monitoring-stack.ts +++ /dev/null @@ -1,190 +0,0 @@ -import { MonitorScheduledQueryRulesAlert } from "@cdktf/provider-azurerm/lib/monitor-scheduled-query-rules-alert"; -import { PortalDashboard } from "@cdktf/provider-azurerm/lib/portal-dashboard"; -import { AzurermProvider } from "@cdktf/provider-azurerm/lib/provider"; -import { ResourceGroup } from "@cdktf/provider-azurerm/lib/resource-group"; -import { TerraformStack } from "cdktf"; -import { Construct } from "constructs"; - -import { generateDashboardProperties } from "./dashboard-generator.js"; -import { - endpointsWithDefaultProperties, - processOpenApiFiles, -} from "./openapi-processor.js"; -import { - getApimAvailabilityQuery, - getApimResponseTimeQuery, -} from "./query-builder.js"; -import { uriToRegex } from "./uri-utils.js"; - -/** - * Core configuration types for the monitoring stack - */ - -export interface MonitoringConfig { - actionGroupId: string; - apimServiceName: string; - location: string; - logAnalyticsWorkspaceId: string; - resourceGroupName: string; - tags?: Tags; -} - -export interface MonitoringStackProps { - config: MonitoringConfig; - openApiFilePaths: string[]; -} - -export interface Tags { - // Allow additional unknown properties - [key: string]: string; - BusinessUnit: string; - CostCenter: string; - CreatedBy: string; - Environment: string; - ManagementTeam: string; - Scope: string; - Source: string; -} - -/** - * Azure monitoring stack for API Management services - * Creates alerts and dashboards based on OpenAPI specifications - */ -export class MonitoringStack extends TerraformStack { - constructor(scope: Construct, id: string, props: MonitoringStackProps) { - super(scope, id); - - const { config, openApiFilePaths } = props; - - // Configure Azure provider - new AzurermProvider(this, "azurerm", { - features: [{}], - storageUseAzuread: true, - }); - - // Create resource group - const resourceGroup = new ResourceGroup(this, "rg", { - location: config.location, - name: config.resourceGroupName, - }); - - // Process OpenAPI specs to extract endpoints - const uniqueEndpoints = processOpenApiFiles(openApiFilePaths); - const endpointsWithProps = endpointsWithDefaultProperties(uniqueEndpoints); - - // Create monitoring alerts for each endpoint - this.createMonitoringAlerts(id, endpointsWithProps, config, resourceGroup); - - // Create monitoring dashboard - this.createMonitoringDashboard( - id, - endpointsWithProps, - config, - resourceGroup, - ); - } - - /** - * Creates availability and response time alerts for all endpoints - */ - private createMonitoringAlerts( - id: string, - endpoints: ReturnType, - config: MonitoringStackProps["config"], - resourceGroup: ResourceGroup, - ): void { - endpoints.forEach((endpoint) => { - const sanitizedName = this.createSanitizedEndpointName(endpoint); - const baseAlertName = `${id}-${sanitizedName}`; - const endpointPath = uriToRegex(endpoint.path); - - // Create availability alert - new MonitorScheduledQueryRulesAlert( - this, - `avail-alert-${sanitizedName}`, - { - action: { actionGroup: [config.actionGroupId] }, - dataSourceId: config.logAnalyticsWorkspaceId, - frequency: 5, - location: config.location, - name: `Alert-Avail-${baseAlertName}`, - query: getApimAvailabilityQuery({ - endpointPath, - isAlarm: true, - threshold: endpoint.availabilityThreshold, - timeSpan: endpoint.availabilityTimeSpan, - }), - resourceGroupName: resourceGroup.name, - severity: 2, - tags: config.tags, - timeWindow: 10, - trigger: { operator: "GreaterThan", threshold: 0 }, - }, - ); - - // Create response time alert - new MonitorScheduledQueryRulesAlert( - this, - `resptime-alert-${sanitizedName}`, - { - action: { actionGroup: [config.actionGroupId] }, - dataSourceId: config.logAnalyticsWorkspaceId, - frequency: 5, - location: config.location, - name: `Alert-RespTime-${baseAlertName}`, - query: getApimResponseTimeQuery({ - endpointPath, - isAlarm: true, - threshold: endpoint.responseTimeThreshold, - timeSpan: endpoint.responseTimeTimeSpan, - }), - resourceGroupName: resourceGroup.name, - severity: 2, - tags: config.tags, - timeWindow: 10, - trigger: { operator: "GreaterThan", threshold: 0 }, - }, - ); - }); - } - - /** - * Creates a monitoring dashboard with charts for all endpoints - */ - private createMonitoringDashboard( - id: string, - endpoints: ReturnType, - config: MonitoringStackProps["config"], - resourceGroup: ResourceGroup, - ): void { - const dashboardName = `Dashboard-${id}`; - - new PortalDashboard(this, "api-dashboard", { - dashboardProperties: generateDashboardProperties(endpoints, config), - location: config.location, - name: dashboardName, - resourceGroupName: resourceGroup.name, - tags: { "hidden-title": dashboardName, ...config.tags }, - }); - } - - /** - * Creates a sanitized name for an endpoint that can be used in Azure resource names - */ - private createSanitizedEndpointName(endpoint: { - host?: string; - method: string; - path: string; - }): string { - const hostPrefix = endpoint.host - ? endpoint.host.replace(/[./]/g, "-") + "-" - : ""; - const methodPrefix = endpoint.method.toLowerCase() + "-"; - - return ( - hostPrefix + - methodPrefix + - endpoint.path.replace(/[/{}]/g, "-").replace(/^-|-$/g, "") - ); - } -} diff --git a/packages/cdktf-monitoring-stack/src/openapi-processor.ts b/packages/cdktf-monitoring-stack/src/openapi-processor.ts deleted file mode 100644 index 4ec5f493..00000000 --- a/packages/cdktf-monitoring-stack/src/openapi-processor.ts +++ /dev/null @@ -1,215 +0,0 @@ -import * as fs from "fs"; -import * as yaml from "js-yaml"; - -export interface EndpointDetails { - host?: string; - method: string; - path: string; -} - -export interface EndpointWithProperties { - availabilityThreshold: number; - availabilityTimeSpan: string; - host?: string; - method: string; - path: string; - responseCodeThreshold: number; - responseCodeTimeSpan: string; - responseTimeThreshold: number; - responseTimeTimeSpan: string; -} - -/** - * OpenAPI specification types and related data structures - */ -export interface OpenApiSpec { - basePath?: string; // OpenAPI 2.0 base path field - host?: string; // OpenAPI 2.0 legacy field - paths: Record>; - servers?: { description?: string; url: string }[]; // OpenAPI 3.x field -} - -/** - * Extract base paths from OpenAPI spec. - * For OpenAPI 3.x: extracts path portion from server URLs - * For OpenAPI 2.0: extracts the basePath field - * Returns an array of base paths. - */ -export function extractServerBasePaths(openApiSpec: OpenApiSpec): string[] { - const basePaths: string[] = []; - - // Handle OpenAPI 3.x servers field - if (openApiSpec.servers && openApiSpec.servers.length > 0) { - openApiSpec.servers.forEach((server) => { - try { - const url = new URL(server.url); - const pathname = url.pathname.endsWith("/") - ? url.pathname.slice(0, -1) - : url.pathname; - basePaths.push(pathname || ""); - } catch { - // If URL is relative or invalid, treat as path-only - const pathname = server.url.startsWith("/") - ? server.url - : `/${server.url}`; - basePaths.push( - pathname.endsWith("/") ? pathname.slice(0, -1) : pathname, - ); - } - }); - } else { - // Handle OpenAPI 2.0 basePath field - const basePath = openApiSpec.basePath || ""; - basePaths.push(basePath); - } - - return basePaths; -} - -/** - * Extract server URLs from OpenAPI spec. - * Handles both OpenAPI 3.x servers field and OpenAPI 2.0 host + basePath fields. - * Returns an array of host URLs (without path components for consistency). - */ -export function extractServerUrls(openApiSpec: OpenApiSpec): string[] { - const serverUrls: string[] = []; - - // Handle OpenAPI 3.x servers field - if (openApiSpec.servers && openApiSpec.servers.length > 0) { - openApiSpec.servers.forEach((server) => { - try { - const url = new URL(server.url); - serverUrls.push(url.host); - } catch { - // If URL is relative or invalid, skip it silently - } - }); - } - - // Fallback to OpenAPI 2.0 host field if no servers found - if (serverUrls.length === 0 && openApiSpec.host) { - serverUrls.push(openApiSpec.host); - } - - return serverUrls; -} - -const HTTP_METHODS = [ - "delete", - "get", - "head", - "options", - "patch", - "post", - "put", - "trace", -]; - -/** - * Convert endpoint details to endpoints with monitoring properties - */ -export function endpointsWithDefaultProperties( - endpoints: EndpointDetails[], -): EndpointWithProperties[] { - return endpoints.map((endpoint) => ({ - availabilityThreshold: 99.0, - availabilityTimeSpan: "5m", - host: endpoint.host, - method: endpoint.method, - path: endpoint.path, - responseCodeThreshold: 1000, - responseCodeTimeSpan: "5m", - responseTimeThreshold: 1000, - responseTimeTimeSpan: "5m", - })); -} - -/** - * Process all OpenAPI specs and collect all endpoints with their hosts and methods - */ -export function processOpenApiFiles( - openApiFilePaths: string[], -): EndpointDetails[] { - const allEndpointsWithDetails: EndpointDetails[] = []; - - openApiFilePaths.forEach((openApiFilePath) => { - const openApiSpec = yaml.load( - fs.readFileSync(openApiFilePath, "utf8"), - ) as OpenApiSpec; - - const endpoints = extractEndpointsFromSpec(openApiSpec); - allEndpointsWithDetails.push(...endpoints); - }); - - return removeDuplicateEndpoints(allEndpointsWithDetails); -} - -/** - * Extract endpoints from a single OpenAPI specification - */ -function extractEndpointsFromSpec(openApiSpec: OpenApiSpec): EndpointDetails[] { - const endpoints: EndpointDetails[] = []; - - // Extract server URLs and base paths from the spec - const serverUrls = extractServerUrls(openApiSpec); - const basePaths = extractServerBasePaths(openApiSpec); - - const paths = Object.keys(openApiSpec.paths); - - paths.forEach((path) => { - const pathItem = openApiSpec.paths[path]; - if (pathItem) { - const methods = Object.keys(pathItem).filter((key) => - HTTP_METHODS.includes(key.toLowerCase()), - ); - - methods.forEach((method) => { - // If we have server URLs, create an endpoint for each server - if (serverUrls.length > 0 && basePaths.length > 0) { - // For OpenAPI 3.x with servers, or OpenAPI 2.0 with host + basePath - serverUrls.forEach((serverUrl, index) => { - const basePath = basePaths[index] || basePaths[0] || ""; - const fullPath = basePath ? `${basePath}${path}` : path; - endpoints.push({ - host: serverUrl, - method: method.toUpperCase(), - path: fullPath, - }); - }); - } else { - // Fallback to legacy host field or no host - // For OpenAPI 2.0, we need to combine basePath with the path - const fullPath = openApiSpec.basePath - ? `${openApiSpec.basePath}${path}` - : path; - - endpoints.push({ - host: openApiSpec.host, - method: method.toUpperCase(), - path: fullPath, - }); - } - }); - } - }); - - return endpoints; -} - -/** - * Remove duplicates based on path, method, and host combination - */ -function removeDuplicateEndpoints( - endpoints: EndpointDetails[], -): EndpointDetails[] { - return endpoints.filter( - (endpoint, index, self) => - index === - self.findIndex( - (e) => - e.path === endpoint.path && - e.method === endpoint.method && - e.host === endpoint.host, - ), - ); -} diff --git a/packages/cdktf-monitoring-stack/src/query-builder.ts b/packages/cdktf-monitoring-stack/src/query-builder.ts deleted file mode 100644 index d98e15ac..00000000 --- a/packages/cdktf-monitoring-stack/src/query-builder.ts +++ /dev/null @@ -1,72 +0,0 @@ -/** - * Query parameter types and KQL query generation functions - */ - -export interface QueryParams { - endpointPath: string; - isAlarm: boolean; - threshold: number; - timeSpan: string; -} - -/** - * Generate KQL query for APIM availability monitoring - */ -export function getApimAvailabilityQuery(params: QueryParams): string { - const { endpointPath, isAlarm, threshold, timeSpan } = params; - return ` - let threshold = ${threshold / 100}; - AzureDiagnostics - | where ResourceProvider == "MICROSOFT.APIMANAGEMENT" - and url_s matches regex "${endpointPath}" - | summarize - Total=count(), - Success=count(responseCode_d < 500) by bin(TimeGenerated, ${timeSpan}) - | extend availability=toreal(Success) / Total - ${ - isAlarm - ? "| where availability < threshold" - : `| project TimeGenerated, availability, watermark=threshold - | render timechart with (xtitle = "time", ytitle= "availability(%)")` - } -`; -} - -/** - * Generate KQL query for APIM response codes monitoring - */ -export function getApimResponseCodesQuery(params: QueryParams): string { - const { endpointPath, timeSpan } = params; - return ` - AzureDiagnostics - | where url_s matches regex "${endpointPath}" - | extend HTTPStatus = case( - responseCode_d between (100 .. 199), "1XX", - responseCode_d between (200 .. 299), "2XX", - responseCode_d between (300 .. 399), "3XX", - responseCode_d between (400 .. 499), "4XX", - "5XX") - | summarize count() by HTTPStatus, bin(TimeGenerated, ${timeSpan}) - | render areachart with (xtitle = "time", ytitle= "count") - `; -} - -/** - * Generate KQL query for APIM response time monitoring - */ -export function getApimResponseTimeQuery(params: QueryParams): string { - const { endpointPath, isAlarm, threshold, timeSpan } = params; - return ` - let threshold = ${threshold}; - AzureDiagnostics - | where url_s matches regex "${endpointPath}" - | summarize - watermark=threshold, - duration_percentile_95=percentiles(todouble(DurationMs)/1000, 95) by bin(TimeGenerated, ${timeSpan}) - ${ - isAlarm - ? `| where duration_percentile_95 > threshold` - : `| render timechart with (xtitle = "time", ytitle= "response time(s)")` - } - `; -} diff --git a/packages/cdktf-monitoring-stack/src/uri-utils.ts b/packages/cdktf-monitoring-stack/src/uri-utils.ts deleted file mode 100644 index 10f6353f..00000000 --- a/packages/cdktf-monitoring-stack/src/uri-utils.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * URI transformation utilities - */ - -/** - * Translate path parameters of a URI to a generic version thanks to regex - */ -export function uriToRegex(value: string): string { - return String(value).replace(/{[^}]+}/g, "[^/]+") + "$"; -} diff --git a/packages/cdktf-monitoring-stack/tsconfig.json b/packages/cdktf-monitoring-stack/tsconfig.json deleted file mode 100644 index 4c35d6d7..00000000 --- a/packages/cdktf-monitoring-stack/tsconfig.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "extends": "@pagopa/typescript-config-node/tsconfig.json", - "compilerOptions": { - "outDir": "./dist", - "rootDir": "./src", - "sourceMap": true, - "declaration": true, - "moduleResolution": "bundler", - "module": "ESNext" - } -} \ No newline at end of file diff --git a/packages/opex-common/eslint.config.js b/packages/opex-common/eslint.config.js deleted file mode 100644 index 9011b4d7..00000000 --- a/packages/opex-common/eslint.config.js +++ /dev/null @@ -1,8 +0,0 @@ -import lintRules from "@pagopa/eslint-config"; - -export default [ - ...lintRules, - { - ignores: ["dist/*"], - }, -]; diff --git a/packages/opex-common/package.json b/packages/opex-common/package.json deleted file mode 100644 index 4084c88a..00000000 --- a/packages/opex-common/package.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "name": "opex-common", - "version": "1.0.0", - "private": true, - "main": "dist/index.js", - "types": "dist/index.d.ts", - "type": "module", - "scripts": { - "build": "tsc", - "typecheck": "tsc --noemit", - "lint": "eslint src --fix", - "lint:check": "eslint src" - }, - "dependencies": { - "cdktf": "^0.21.0", - "cdktf-monitoring-stack": "workspace:*" - }, - "devDependencies": { - "@pagopa/eslint-config": "^5.0.0", - "@pagopa/typescript-config-node": "workspace:^", - "eslint": "^9.30.1", - "prettier": "^3.6.2", - "typescript": "^5.8.3" - } -} diff --git a/packages/opex-common/src/index.ts b/packages/opex-common/src/index.ts deleted file mode 100644 index 6337d265..00000000 --- a/packages/opex-common/src/index.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { AzurermBackendConfig } from "cdktf"; -import { MonitoringConfig } from "cdktf-monitoring-stack"; - -export const opexConfig: MonitoringConfig = { - actionGroupId: - "/subscriptions/d7de83e0-0571-40ad-b63a-64c942385eae/resourceGroups/dev/providers/microsoft.insights/actionGroups/dx-d-itn-opex-rg-01", - apimServiceName: "dx-d-itn-playground-pg-apim-01", - location: "Italy North", - logAnalyticsWorkspaceId: - "/subscriptions/d7de83e0-0571-40ad-b63a-64c942385eae/resourceGroups/dx-d-itn-test-rg-01/providers/Microsoft.OperationalInsights/workspaces/dx-d-itn-playground-azure-function-v3-log-01", - resourceGroupName: "dx-d-itn-opex-rg-01", - tags: { - BusinessUnit: "DevEx", - CostCenter: "TS000 - Tecnologia e Servizi", - CreatedBy: "Terraform", - Environment: "Dev", - ManagementTeam: "Developer Experience", - Scope: "Opex PoC", - Source: - "https://github.com/pagopa/dx-playground/tree/main/infra/repository", - }, -} as const; - -export const backendConfig: AzurermBackendConfig = { - containerName: "terraform-state", - key: "dx.opex.tfstate", - resourceGroupName: "terraform-state-rg", - storageAccountName: "tfdevdx", -} as const; diff --git a/packages/opex-common/tsconfig.json b/packages/opex-common/tsconfig.json deleted file mode 100644 index 4c35d6d7..00000000 --- a/packages/opex-common/tsconfig.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "extends": "@pagopa/typescript-config-node/tsconfig.json", - "compilerOptions": { - "outDir": "./dist", - "rootDir": "./src", - "sourceMap": true, - "declaration": true, - "moduleResolution": "bundler", - "module": "ESNext" - } -} \ No newline at end of file diff --git a/packages/opex-dashboard-ts/CONTRIBUTING.md b/packages/opex-dashboard-ts/CONTRIBUTING.md new file mode 100644 index 00000000..37462fd1 --- /dev/null +++ b/packages/opex-dashboard-ts/CONTRIBUTING.md @@ -0,0 +1,310 @@ +# Contributing to Opex Dashboard TS + +Thank you for your interest in contributing to the Opex Dashboard TypeScript package! +This document outlines the guidelines and best practices for contributing to this project. + +## Installation + +```bash +# Clone the repository +git clone +cd packages/opex-dashboard-ts + +# Install dependencies +yarn install + +# Build the project +yarn build +``` + +## Architecture + +This project follows **Hexagonal Architecture** (Ports & Adapters) principles, +separating business logic from infrastructure concerns for better testability, +maintainability, and extensibility. + +### Core Principles + +- **Domain Layer**: Pure business logic, independent of frameworks +- **Application Layer**: Use cases orchestrating domain services +- **Infrastructure Layer**: Adapters for external systems (CLI, files, OpenAPI, + Terraform) + +### Layer Structure + +1. **Domain** (`src/domain/`) + - **Entities**: Core business objects (`Endpoint`, `DashboardConfig`) + - **Services**: Business logic (`EndpointParserService`, `KustoQueryService`) + - **Ports**: Interfaces for external dependencies + +2. **Application** (`src/application/`) + - **Use Cases**: Orchestrate domain services (`GenerateDashboardUseCase`) + +3. **Infrastructure** (`src/infrastructure/`) + - **Adapters**: Concrete implementations of ports + - CLI Adapter: Commander.js integration + - OpenAPI Adapter: SwaggerParser integration + - Terraform Adapter: CDKTF integration + - File Adapter: File system operations + - Config Adapter: Zod validation + +### Key Components + +#### Domain Services + +- **EndpointParserService**: Parses OpenAPI specs into endpoint configurations +- **KustoQueryService**: Generates Azure Log Analytics queries + +#### Application Use Cases + +- **GenerateDashboardUseCase**: Main workflow for dashboard generation + +#### Infrastructure Adapters + +- **OpenAPISpecResolverAdapter**: Resolves OpenAPI specifications +- **TerraformGeneratorAdapter**: Generates CDKTF code +- **ConfigValidatorAdapter**: Validates configuration with Zod +- **FileReaderAdapter**: Reads YAML configuration files + +### Benefits + +- **Testability**: Domain logic tested in isolation with mock adapters +- **Extensibility**: Easy to add new adapters (e.g., AWS, different CLI + frameworks) +- **Maintainability**: Clear separation of concerns +- **Framework Independence**: Domain layer free from CDKTF/Commander + dependencies + +## API Documentation + +### Domain Layer + +#### Entities + +```typescript +interface Endpoint { + path: string; + availabilityThreshold?: number; + responseTimeThreshold?: number; + // ... other properties +} + +interface DashboardConfig { + oa3_spec: string; + name: string; + location: string; + data_source: string; + // ... other properties +} +``` + +#### Domain Services + +```typescript +class EndpointParserService { + parseEndpoints(spec: OpenAPISpec, config: DashboardConfig): Endpoint[]; +} + +class KustoQueryService { + buildAvailabilityQuery(endpoint: Endpoint, config: DashboardConfig): string; + buildResponseTimeQuery(endpoint: Endpoint, config: DashboardConfig): string; + buildResponseCodesQuery(endpoint: Endpoint, config: DashboardConfig): string; +} +``` + +### Application Layer + +#### Use Cases + +```typescript +class GenerateDashboardUseCase { + constructor( + fileReader: IFileReader, + configValidator: IConfigValidator, + openAPISpecResolver: IOpenAPISpecResolver, + endpointParser: IEndpointParser, + kustoQueryGenerator: IKustoQueryGenerator, + terraformGenerator: ITerraformGenerator, + ) {} + + async execute(configFilePath: string): Promise; +} +``` + +### Infrastructure Layer + +#### Adapters + +```typescript +class OpenAPISpecResolverAdapter implements IOpenAPISpecResolver { + async resolve(specPath: string): Promise; +} + +class TerraformGeneratorAdapter implements ITerraformGenerator { + async generate(config: DashboardConfig): Promise; +} + +class ConfigValidatorAdapter implements IConfigValidator { + validateConfig(rawConfig: unknown): DashboardConfig; +} + +class FileReaderAdapter implements IFileReader { + async readYamlFile(filePath: string): Promise; +} +``` + +## Example: Programmatic Usage + +```typescript +import { GenerateDashboardUseCase } from "./src/application/index.js"; +import { FileReaderAdapter } from "./src/infrastructure/file/file-reader-adapter.js"; +import { ConfigValidatorAdapter } from "./src/infrastructure/config/config-validator-adapter.js"; +import { OpenAPISpecResolverAdapter } from "./src/infrastructure/openapi/openapi-spec-resolver-adapter.js"; +import { EndpointParserService } from "./src/domain/services/endpoint-parser-service.js"; +import { KustoQueryService } from "./src/domain/services/kusto-query-service.js"; +import { TerraformGeneratorAdapter } from "./src/infrastructure/terraform/terraform-generator-adapter.js"; + +async function generateDashboard() { + // Create adapters + const fileReader = new FileReaderAdapter(); + const configValidator = new ConfigValidatorAdapter(); + const openAPISpecResolver = new OpenAPISpecResolverAdapter(); + const endpointParser = new EndpointParserService(); + const kustoQueryGenerator = new KustoQueryService(); + const terraformGenerator = new TerraformGeneratorAdapter(); + + // Create use case + const useCase = new GenerateDashboardUseCase( + fileReader, + configValidator, + openAPISpecResolver, + endpointParser, + kustoQueryGenerator, + terraformGenerator, + ); + + // Execute + await useCase.execute("./config.yaml"); +} +``` + +## Testing + +### Running Tests + +```bash +# Run all tests +yarn test + +# Run with coverage +yarn test --coverage + +# Run specific test file +yarn test resolver.test.ts + +# Watch mode +yarn test --watch +``` + +### Test Structure + +``` +test/ +├── unit/ +│ ├── resolver.test.ts # OpenAPI resolver tests +│ ├── endpoint-parser.test.ts # Endpoint parsing tests +│ ├── kusto-queries.test.ts # Query generation tests +│ └── cli.test.ts # CLI tests +└── integration/ # Integration tests (future) +``` + +### Test Coverage + +Current test coverage includes: + +- ✅ OpenAPI specification parsing +- ✅ Endpoint extraction and configuration +- ✅ Kusto query generation for both resource types +- ✅ CLI command structure and options +- ✅ Error handling and edge cases + +## Development + +### Prerequisites + +- Node.js 18+ +- Yarn 1.22+ +- Azure CLI (for deployment) + +### Development Workflow + +```bash +# Install dependencies +yarn install + +# Build in watch mode +yarn watch + +# Run tests in watch mode +yarn test --watch + +# Lint code +yarn lint + +# Format code +yarn format +``` + +### Project Structure + +``` +packages/opex-dashboard-ts/ +├── src/ +│ ├── domain/ # Business logic layer +│ │ ├── entities/ # Core business objects +│ │ │ ├── endpoint.ts # Endpoint entity +│ │ │ └── dashboard-config.ts # Configuration entity +│ │ ├── services/ # Domain services +│ │ │ ├── endpoint-parser-service.ts +│ │ │ └── kusto-query-service.ts +│ │ ├── ports/ # Interface definitions +│ │ │ └── index.ts +│ │ └── index.ts # Domain exports +│ ├── application/ # Application layer +│ │ ├── use-cases/ # Use case implementations +│ │ │ └── generate-dashboard-use-case.ts +│ │ └── index.ts # Application exports +│ ├── infrastructure/ # Infrastructure layer +│ │ ├── cli/ # CLI adapter +│ │ │ ├── index.ts +│ │ │ └── generate.ts +│ │ ├── openapi/ # OpenAPI adapter +│ │ │ └── openapi-spec-resolver-adapter.ts +│ │ ├── terraform/ # Terraform adapter +│ │ │ ├── azure-dashboard.ts +│ │ │ ├── azure-alerts.ts +│ │ │ ├── dashboard-properties.ts +│ │ │ └── terraform-generator-adapter.ts +│ │ ├── file/ # File adapter +│ │ │ └── file-reader-adapter.ts +│ │ ├── config/ # Config adapter +│ │ │ └── config-validator-adapter.ts +│ │ └── index.ts # Infrastructure exports +│ ├── shared/ # Shared utilities +│ │ └── openapi.ts # OpenAPI type guards +│ └── index.ts # Main exports +├── test/ # Test files +│ └── unit/ # Unit tests +├── examples/ # Example configurations +├── package.json +├── tsconfig.json +└── README.md +``` + +## Debug Mode + +Enable debug logging: + +```bash +DEBUG=cdktf:* yarn tsx src/cli/index.ts generate --config-file config.yaml +``` diff --git a/packages/opex-dashboard-ts/README.md b/packages/opex-dashboard-ts/README.md new file mode 100644 index 00000000..79955706 --- /dev/null +++ b/packages/opex-dashboard-ts/README.md @@ -0,0 +1,181 @@ +# OpEx Dashboard TypeScript + +**Generate standardized PagoPA's Operational Excellence dashboards from OpenAPI +specs using TypeScript and CDKTF.** + +## Overview + +This is a TypeScript port of the Python +[opex-dashboard](https://github.com/pagopa/opex-dashboard) project. It generates +Azure dashboards and alerts from OpenAPI specifications using CDK for Terraform +(CDKTF), maintaining exact compatibility with the Python version. + +## Features + +- ** Scheduled Alerts**: Generates Azure Monitor alerts for availability and + response time +- **📋 OpenAPI Support**: Parses Swagger and OpenAPI 3.x specifications +- **🏗️ CDKTF Integration**: Uses CDK for Terraform for infrastructure as code +- **🔒 Type Safety**: Full TypeScript support with comprehensive type checking +- **⚡ Exact Replication**: Maintains 100% compatibility with Python version + output + +## Quick Start + +1. **Create a configuration file** (`config.yaml`): + +```yaml +oa3_spec: ./examples/petstore.yaml +name: PetStore Dashboard +resource_group_name: dashboards # the dashboard and alerts will be created in this RG +# Location is inherited from the Resource Group (no need to specify it) +data_source: /subscriptions/xxx/resourceGroups/xxx/providers/Microsoft.Network/applicationGateways/xxx +resource_type: app-gateway +action_groups: + - /subscriptions/xxx/resourceGroups/xxx/providers/Microsoft.Insights/actionGroups/xxx +tags: + BusinessUnit: PAGOPA + Environment: DEV +``` + +2. **Generate Terraform (CDKTF) code**: + +```bash +yarn tsx src/cli/index.ts generate --config-file config.yaml +``` + +## Configuration + +The configuration format is identical to the Python version: + +### Basic Configuration + +```yaml +# Required fields +oa3_spec: ./path/to/openapi.yaml # Path to OpenAPI spec file +name: My API Dashboard # Dashboard name +resource_group_name: dashboards # Resource Group to host the dashboard and alerts +data_source: /subscriptions/.../applicationGateways/my-gtw # Resource ID used in queries + +# Optional fields +resource_type: app-gateway # 'app-gateway' or 'api-management' (default: app-gateway) +timespan: 5m # Dashboard timespan (default: 5m) +action_groups: # Action groups for alerts + - /subscriptions/.../actionGroups/my-action-group +tags: # Tags applied to dashboard and alerts + CostCenter: CC123 + Environment: DEV +``` + +### Advanced Configuration + +For each host and endpoint, you can override default settings: + +```yaml +# Override defaults +overrides: + hosts: + - api.example.com + - staging.api.example.com + endpoints: + /api/users: + availability_threshold: 0.95 + response_time_threshold: 2.0 + /api/orders: + enabled: false # Disable monitoring for this endpoint +``` + +### Configuration Reference + +| Field | Type | Required | Default | Description | +| --------------------- | --------------------- | -------- | ------------- | -------------------------------------------------------------------------- | +| `oa3_spec` | string | ✅ | - | Path/URL to OpenAPI specification | +| `name` | string | ✅ | - | Dashboard name | +| `resource_group_name` | string | ✅ | `dashboards` | Resource Group where dashboard and alerts will be created | +| `data_source` | string | ✅ | - | Azure resource ID used by KQL queries (e.g., Application Gateway resource) | +| `resource_type` | string | ❌ | `app-gateway` | Resource type (`app-gateway` or `api-management`) | +| `timespan` | string | ❌ | `5m` | Dashboard timespan | +| `action_groups` | string[] | ❌ | - | Action groups for alerts | +| `tags` | Record | ❌ | - | Tags applied to dashboard and alerts | +| `overrides` | object | ❌ | - | Override default settings for hosts/endpoints | + +Notes: + +- The Azure location is inferred from the specified `resource_group_name` via data source lookup; do not set `location` in config. + +## Migration + +This project now inherits the Azure location from the specified Resource Group and supports applying tags to both the dashboard and alerts. If you're upgrading from an earlier version where `location` was set explicitly in the configuration, follow the steps below. + +### What changed + +- Location is no longer read from the configuration file. It is resolved at synth time from the Resource Group via a data source. The `location` key, if present, is ignored. +- A new optional `tags` block can be specified and will be applied to the `azurerm_portal_dashboard` and all `azurerm_monitor_scheduled_query_rules_alert` resources. + +### How to update your configuration + +1. Ensure `resource_group_name` points to the target Azure Resource Group. The resources will inherit the location of this group. +2. Remove the `location` field from your YAML (it will be ignored if left in place). +3. Optionally add a `tags` map to propagate tags to the dashboard and alerts. + +Before (old config): + +```yaml +oa3_spec: ./examples/petstore.yaml +name: PetStore Dashboard +location: westeurope +data_source: /subscriptions/xxx/resourceGroups/xxx/providers/Microsoft.Network/applicationGateways/xxx +resource_type: app-gateway +action_groups: + - /subscriptions/xxx/resourceGroups/xxx/providers/Microsoft.Insights/actionGroups/xxx +``` + +After (new config): + +```yaml +oa3_spec: ./examples/petstore.yaml +name: PetStore Dashboard +resource_group_name: dashboards # location is inherited from this RG +data_source: /subscriptions/xxx/resourceGroups/xxx/providers/Microsoft.Network/applicationGateways/xxx +resource_type: app-gateway +action_groups: + - /subscriptions/xxx/resourceGroups/xxx/providers/Microsoft.Insights/actionGroups/xxx +tags: + BusinessUnit: PAGOPA + Environment: DEV +``` + +You can omit `resource_group_name` which defaults to `dashboards`. + +### Impact on Terraform plans + +- If your previous `location` matched the Resource Group's location, you should see no resource recreation due to location. The dashboard and alerts will continue to be deployed in the same region. +- If your previous `location` differed from the Resource Group's location, Terraform may propose to replace resources to align with the RG location. Align the Resource Group or accept the plan as appropriate. +- Adding `tags` will result in in-place updates where supported. + +### Programmatic usage (TypeScript) + +- The `DashboardConfig` type now treats `location` as optional and it is not used. Set `resource_group_name` to control the deployment location. You may also set the new optional `tags: Record` property. + +### FAQ + +- Can I still specify `location` in my config? Yes, but it is ignored. The only source of truth for location is the Resource Group. +- Do I need to migrate immediately? No. Leaving `location` in your YAML won't break generation, but removing it is recommended to avoid confusion. + +## Examples + +### Example 1: Basic Dashboard Generation + +```bash +# Generate Terraform CDK code +yarn tsx src/cli/index.ts generate \ + --config-file examples/basic-config.yaml +``` + +## Related Projects + +- [opex-dashboard (Python)](https://github.com/pagopa/opex-dashboard) - Original + Python implementation +- [CDK for Terraform](https://www.terraform.io/cdktf) - CDKTF documentation +- [Azure Monitor](https://docs.microsoft.com/en-us/azure/azure-monitor/) - Azure + monitoring documentation diff --git a/packages/cdktf-monitoring-stack/eslint.config.js b/packages/opex-dashboard-ts/eslint.config.js similarity index 63% rename from packages/cdktf-monitoring-stack/eslint.config.js rename to packages/opex-dashboard-ts/eslint.config.js index 9011b4d7..ebf6e6e3 100644 --- a/packages/cdktf-monitoring-stack/eslint.config.js +++ b/packages/opex-dashboard-ts/eslint.config.js @@ -1,8 +1,8 @@ import lintRules from "@pagopa/eslint-config"; export default [ - ...lintRules, { - ignores: ["dist/*"], + ignores: ["test/**/*", "**/*local*", "examples"], }, + ...lintRules, ]; diff --git a/packages/opex-dashboard-ts/package.json b/packages/opex-dashboard-ts/package.json new file mode 100644 index 00000000..9b0a8533 --- /dev/null +++ b/packages/opex-dashboard-ts/package.json @@ -0,0 +1,67 @@ +{ + "name": "@pagopa/opex-dashboard-ts", + "version": "0.0.1", + "description": "Generate standardized Operational Excellence dashboards from OpenAPI specs using TypeScript and CDKTF", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "bin": "dist/infrastructure/cli/index.js", + "type": "module", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "require": "./dist/index.cjs" + }, + "./cli": { + "types": "./dist/infrastructure/cli/index.d.ts", + "import": "./dist/infrastructure/cli/index.js", + "require": "./dist/infrastructure/cli/index.cjs" + } + }, + "scripts": { + "build": "tsup", + "lint": "eslint --fix src", + "lint:check": "eslint src", + "format": "prettier --write .", + "format:check": "prettier --check .", + "typecheck": "tsc --noEmit", + "test": "vitest run", + "test:coverage": "vitest --coverage", + "watch": "tsc --watch", + "cdktf:deploy": "cdktf deploy", + "clean": "rm -rf dist cdktf.out" + }, + "keywords": [ + "opex", + "dashboard", + "azure", + "terraform", + "cdkt", + "openapi" + ], + "author": "PagoPA", + "license": "MIT", + "dependencies": { + "@apidevtools/swagger-parser": "^10.1.0", + "@cdktf/provider-azurerm": "^14.12.0", + "cdktf": "^0.21.0", + "commander": "^12.0.0", + "constructs": "^10.3.0", + "js-yaml": "^4.1.0", + "openapi-types": "^12.1.3", + "zod": "^4.1.5" + }, + "devDependencies": { + "@pagopa/eslint-config": "^5.0.0", + "@tsconfig/node22": "^22.0.2", + "@types/js-yaml": "^4.0.5", + "@types/node": "^20.0.0", + "@typescript-eslint/eslint-plugin": "^6.0.0", + "@typescript-eslint/parser": "^6.0.0", + "eslint": "^8.0.0", + "prettier": "^3.6.2", + "tsup": "^8.5.0", + "typescript": "^5.0.0", + "vitest": "^3.2.4" + } +} diff --git a/packages/opex-dashboard-ts/src/application/index.ts b/packages/opex-dashboard-ts/src/application/index.ts new file mode 100644 index 00000000..9b67ff34 --- /dev/null +++ b/packages/opex-dashboard-ts/src/application/index.ts @@ -0,0 +1,2 @@ +export * from "./use-cases/create-dashboard-stack-use-case.js"; +export * from "./use-cases/generate-dashboard-use-case.js"; diff --git a/packages/opex-dashboard-ts/src/application/use-cases/create-dashboard-stack-use-case.ts b/packages/opex-dashboard-ts/src/application/use-cases/create-dashboard-stack-use-case.ts new file mode 100644 index 00000000..46b53e2b --- /dev/null +++ b/packages/opex-dashboard-ts/src/application/use-cases/create-dashboard-stack-use-case.ts @@ -0,0 +1,42 @@ +import { App } from "cdktf"; + +import { + Endpoint, + IConfigValidator, + IEndpointParser, + IOpenAPISpecResolver, + ITerraformStackGenerator, +} from "../../domain/index.js"; +import { AzureOpexStack } from "../../infrastructure/terraform/azure-dashboard.js"; + +export class CreateDashboardStackUseCase { + constructor( + private readonly configValidator: IConfigValidator, + private readonly openAPISpecResolver: IOpenAPISpecResolver, + private readonly endpointParser: IEndpointParser, + private readonly terraformGenerator: ITerraformStackGenerator, + ) {} + + async execute(config: unknown, app: App): Promise { + // Validate configuration + const validatedConfig = this.configValidator.validateConfig(config); + + // Resolve OpenAPI spec and extract hosts + const { hosts: extractedHosts, spec } = + await this.openAPISpecResolver.resolveWithHosts(validatedConfig.oa3_spec); + + // Parse endpoints + const endpoints: Endpoint[] = this.endpointParser.parseEndpoints( + spec, + validatedConfig, + ); + + // Update config with parsed data + validatedConfig.endpoints = endpoints; + validatedConfig.hosts = validatedConfig.overrides?.hosts || extractedHosts; + validatedConfig.resourceIds = [validatedConfig.data_source]; + + // Generate and return the stack + return await this.terraformGenerator.generate(validatedConfig, app); + } +} diff --git a/packages/opex-dashboard-ts/src/application/use-cases/generate-dashboard-use-case.ts b/packages/opex-dashboard-ts/src/application/use-cases/generate-dashboard-use-case.ts new file mode 100644 index 00000000..02a26e38 --- /dev/null +++ b/packages/opex-dashboard-ts/src/application/use-cases/generate-dashboard-use-case.ts @@ -0,0 +1,44 @@ +import { + Endpoint, + IConfigValidator, + IEndpointParser, + IFileReader, + IOpenAPISpecResolver, + ITerraformFileGenerator, +} from "../../domain/index.js"; + +export class GenerateDashboardUseCase { + constructor( + private readonly fileReader: IFileReader, + private readonly configValidator: IConfigValidator, + private readonly openAPISpecResolver: IOpenAPISpecResolver, + private readonly endpointParser: IEndpointParser, + private readonly terraformGenerator: ITerraformFileGenerator, + ) {} + + async execute(configFilePath: string): Promise { + // Load and parse configuration + const rawConfig = await this.fileReader.readYamlFile(configFilePath); + + // Validate configuration + const validatedConfig = this.configValidator.validateConfig(rawConfig); + + // Resolve OpenAPI spec and extract hosts + const { hosts: extractedHosts, spec } = + await this.openAPISpecResolver.resolveWithHosts(validatedConfig.oa3_spec); + + // Parse endpoints + const endpoints: Endpoint[] = this.endpointParser.parseEndpoints( + spec, + validatedConfig, + ); + + // Update config with parsed data + validatedConfig.endpoints = endpoints; + validatedConfig.hosts = validatedConfig.overrides?.hosts || extractedHosts; + validatedConfig.resourceIds = [validatedConfig.data_source]; + + // Generate Terraform code + await this.terraformGenerator.generate(validatedConfig); + } +} diff --git a/packages/opex-dashboard-ts/src/core/resolver.ts b/packages/opex-dashboard-ts/src/core/resolver.ts new file mode 100644 index 00000000..9a620dce --- /dev/null +++ b/packages/opex-dashboard-ts/src/core/resolver.ts @@ -0,0 +1,24 @@ +import SwaggerParser from "@apidevtools/swagger-parser"; + +import { OpenAPISpec } from "../shared/openapi.js"; + +export class OA3Resolver { + async resolve(specPath: string): Promise { + try { + const spec = await SwaggerParser.parse(specPath); + return spec as OpenAPISpec; + } catch (error: unknown) { + if (error instanceof Error) { + throw new ParseError(`OA3 parsing error: ${error.message}`); + } + throw new ParseError(`OA3 parsing error: ${String(error)}`); + } + } +} + +export class ParseError extends Error { + constructor(message: string) { + super(message); + this.name = "ParseError"; + } +} diff --git a/packages/opex-dashboard-ts/src/domain/entities/dashboard-config.ts b/packages/opex-dashboard-ts/src/domain/entities/dashboard-config.ts new file mode 100644 index 00000000..d6a5f835 --- /dev/null +++ b/packages/opex-dashboard-ts/src/domain/entities/dashboard-config.ts @@ -0,0 +1,37 @@ +import { z } from "zod"; + +import { EndpointSchema } from "./endpoint.js"; + +// Zod schema for DashboardConfig +export const DashboardConfigSchema = z.object({ + action_groups: z.array(z.string()).optional(), + data_source: z.string(), + endpoints: z.array(EndpointSchema).optional(), + evaluation_frequency: z.number().default(10), + evaluation_time_window: z.number().default(20), + event_occurrences: z.number().default(1), + // Computed properties (optional in input) + hosts: z.array(z.string()).optional(), + location: z.string().optional(), + name: z.string(), + oa3_spec: z.string(), + overrides: z + .object({ + endpoints: z.record(z.string(), EndpointSchema.partial()).optional(), + hosts: z.array(z.string()).optional(), + }) + .optional(), + resource_group_name: z.string().default("dashboards"), + resource_type: z + .enum(["app-gateway", "api-management"]) + .default("app-gateway"), + resourceIds: z.array(z.string()).optional(), + tags: z.record(z.string(), z.string()).optional(), + timespan: z.string().default("5m"), +}); + +// Fields with defaults applied can be omitted in input +export type DashboardConfig = z.input; + +// Inferred types from Zod schemas +export type ValidDashboardConfig = z.infer; diff --git a/packages/opex-dashboard-ts/src/domain/entities/endpoint.ts b/packages/opex-dashboard-ts/src/domain/entities/endpoint.ts new file mode 100644 index 00000000..b7e622ff --- /dev/null +++ b/packages/opex-dashboard-ts/src/domain/entities/endpoint.ts @@ -0,0 +1,17 @@ +import { z } from "zod"; + +// Zod schema for Endpoint +export const EndpointSchema = z.object({ + availability_evaluation_frequency: z.number().default(10), + availability_evaluation_time_window: z.number().default(20), + availability_event_occurrences: z.number().default(1), + availability_threshold: z.number().default(0.99), + path: z.string(), + response_time_evaluation_frequency: z.number().default(10), + response_time_evaluation_time_window: z.number().default(20), + response_time_event_occurrences: z.number().default(1), + response_time_threshold: z.number().default(1), +}); + +// Inferred types from Zod schemas +export type Endpoint = z.infer; diff --git a/packages/opex-dashboard-ts/src/domain/entities/panel.ts b/packages/opex-dashboard-ts/src/domain/entities/panel.ts new file mode 100644 index 00000000..ae2f41d8 --- /dev/null +++ b/packages/opex-dashboard-ts/src/domain/entities/panel.ts @@ -0,0 +1,21 @@ +export interface Panel { + chart: { + inputSpecificChart: string; + settingsSpecificChart: string; + }; + dimensions: { + aggregation?: string; + splitBy?: { name: string; type: string }[]; + xAxis: { name: string; type: string }; + yAxis: { name: string; type: string }[]; + }; + id: number; + kind: PanelKind; + path: string; + position: { colSpan: number; rowSpan: number; x: number; y: number }; + query: string; + subtitle: string; + title: string; +} + +export type PanelKind = "availability" | "response-codes" | "response-time"; diff --git a/packages/opex-dashboard-ts/src/domain/index.ts b/packages/opex-dashboard-ts/src/domain/index.ts new file mode 100644 index 00000000..ca3bb7a7 --- /dev/null +++ b/packages/opex-dashboard-ts/src/domain/index.ts @@ -0,0 +1,8 @@ +export * from "../shared/openapi.js"; +export * from "./entities/dashboard-config.js"; +export * from "./entities/endpoint.js"; +export * from "./entities/panel.js"; +export * from "./ports/index.js"; +export * from "./services/endpoint-parser-service.js"; +export * from "./services/kusto-query-service.js"; +export * from "./services/panel-factory.js"; diff --git a/packages/opex-dashboard-ts/src/domain/ports/index.ts b/packages/opex-dashboard-ts/src/domain/ports/index.ts new file mode 100644 index 00000000..6a593b7d --- /dev/null +++ b/packages/opex-dashboard-ts/src/domain/ports/index.ts @@ -0,0 +1,49 @@ +export interface IConfigValidator { + validateConfig(rawConfig: unknown): ValidDashboardConfig; +} + +export interface IEndpointParser { + parseEndpoints(spec: OpenAPISpec, config: ValidDashboardConfig): Endpoint[]; +} + +export interface IFileReader { + readYamlFile(filePath: string): Promise; +} + +export interface IKustoQueryGenerator { + buildAvailabilityQuery( + endpoint: Endpoint, + config: ValidDashboardConfig, + ): string; + buildResponseCodesQuery( + endpoint: Endpoint, + config: ValidDashboardConfig, + ): string; + buildResponseTimeQuery( + endpoint: Endpoint, + config: ValidDashboardConfig, + ): string; +} + +export interface IOpenAPISpecResolver { + resolve(specPath: string): Promise; + resolveWithHosts( + specPath: string, + ): Promise<{ hosts: string[]; spec: OpenAPISpec }>; +} + +export interface ITerraformFileGenerator { + generate(config: ValidDashboardConfig): Promise; +} + +export interface ITerraformStackGenerator { + generate(config: ValidDashboardConfig, app: App): Promise; +} + +import type { App } from "cdktf"; + +import type { AzureOpexStack } from "../../infrastructure/terraform/azure-dashboard.js"; +// Import types +import type { OpenAPISpec } from "../../shared/openapi.js"; +import type { ValidDashboardConfig } from "../entities/dashboard-config.js"; +import type { Endpoint } from "../entities/endpoint.js"; diff --git a/packages/opex-dashboard-ts/src/domain/services/endpoint-parser-service.ts b/packages/opex-dashboard-ts/src/domain/services/endpoint-parser-service.ts new file mode 100644 index 00000000..eef65a74 --- /dev/null +++ b/packages/opex-dashboard-ts/src/domain/services/endpoint-parser-service.ts @@ -0,0 +1,66 @@ +import { isOpenAPIV2, isOpenAPIV3, OpenAPISpec } from "../../shared/openapi.js"; +import { ValidDashboardConfig } from "../entities/dashboard-config.js"; +import { Endpoint, EndpointSchema } from "../entities/endpoint.js"; + +export class EndpointParserService { + parseEndpoints(spec: OpenAPISpec, config: ValidDashboardConfig): Endpoint[] { + const endpoints: Endpoint[] = []; + const hosts = this.extractHosts(spec); + const paths = Object.keys(spec.paths); + + for (const host of hosts) { + for (const path of paths) { + const endpointPath = this.buildEndpointPath(host, path, spec); + const endpoint = EndpointSchema.parse({ + path: endpointPath, + ...this.getEndpointOverrides(endpointPath, config.overrides), + }); + endpoints.push(endpoint); + } + } + + return endpoints; + } + + private buildEndpointPath( + host: string, + path: string, + spec: OpenAPISpec, + ): string { + if (host.startsWith("http")) { + const url = new URL(host); + return `${url.pathname}${path}`.replace(/\/+/g, "/"); + } else { + // For OpenAPI 2.x, use basePath if available + const basePath = isOpenAPIV2(spec) ? spec.basePath || "" : ""; + return `${basePath}${path}`.replace(/\/+/g, "/"); + } + } + + private extractHosts(spec: OpenAPISpec): string[] { + if (isOpenAPIV3(spec)) { + // OpenAPI 3.x uses servers array + if (spec.servers && spec.servers.length > 0) { + return spec.servers.map((server) => server.url); + } + } else if (isOpenAPIV2(spec)) { + // OpenAPI 2.x uses host and basePath + if (spec.host && spec.basePath) { + return [`${spec.host}${spec.basePath}`]; + } else if (spec.host) { + return [spec.host]; + } + } + return []; + } + + private getEndpointOverrides( + endpointPath: string, + overrides?: ValidDashboardConfig["overrides"], + ): Partial { + if (!overrides?.endpoints) { + return {}; + } + return overrides.endpoints[endpointPath] || {}; + } +} diff --git a/packages/opex-dashboard-ts/src/domain/services/kusto-query-service.ts b/packages/opex-dashboard-ts/src/domain/services/kusto-query-service.ts new file mode 100644 index 00000000..39f0d33c --- /dev/null +++ b/packages/opex-dashboard-ts/src/domain/services/kusto-query-service.ts @@ -0,0 +1,137 @@ +import { ValidDashboardConfig } from "../entities/dashboard-config.js"; +import { Endpoint } from "../entities/endpoint.js"; + +/* + KustoQueryService now supports two contexts: + - "dashboard": return full time-series with watermark and render clause (no threshold filtering) + - "alert": return filtered series (apply threshold predicate) suitable for scheduled query alerts + Default remains "alert" to preserve existing alert generation behaviour. +*/ + +export type QueryContext = "alert" | "dashboard"; + +export class KustoQueryService { + buildAvailabilityQuery( + endpoint: Endpoint, + config: ValidDashboardConfig, + context: QueryContext = "alert", + ): string { + const threshold = endpoint.availability_threshold ?? 0.99; + const regex = this.createGenericRegex(endpoint.path); + + const base = + config.resource_type === "api-management" + ? `AzureDiagnostics +| where url_s matches regex "${regex}" +| summarize Total=count(), Success=count(responseCode_d < 500) by bin(TimeGenerated, ${config.timespan}) +| extend availability=toreal(Success) / Total` + : `${this.createHostsDataTable(config.hosts || [])} +AzureDiagnostics +| where originalHost_s in (api_hosts) +| where requestUri_s matches regex "${regex}" +| summarize + Total=count(), + Success=count(httpStatus_d < 500) by bin(TimeGenerated, ${config.timespan}) +| extend availability=toreal(Success) / Total`; + + if (context === "dashboard") { + // Keep full series with watermark line for visualization + return `let threshold = ${threshold}; +${base} +| extend watermark=threshold +| project TimeGenerated, availability, watermark +| render timechart with (xtitle = "time", ytitle= "availability(%)")`; + } + // alert context: filtered series to fire on breaches + return `let threshold = ${threshold}; +${base} +| where availability < threshold`; + } + + buildResponseCodesQuery( + endpoint: Endpoint, + config: ValidDashboardConfig, + ): string { + /* + Aggregate HTTP status into families (1XX .. 5XX) to reduce noise and align with reference dashboard. + Anything outside the standard ranges will be bucketed as "Other". + */ + const regex = this.createGenericRegex(endpoint.path); + const timeBin = config.timespan; + const codeColumn = + config.resource_type === "api-management" + ? "responseCode_d" + : "httpStatus_d"; + const sourceFilter = + config.resource_type === "api-management" + ? `AzureDiagnostics\n| where url_s matches regex "${regex}"` + : `${this.createHostsDataTable(config.hosts || [])}\nAzureDiagnostics\n| where originalHost_s in (api_hosts)\n| where requestUri_s matches regex "${regex}"`; + + return `${sourceFilter} +| extend HTTPStatus = case(${codeColumn} between (100 .. 199), "1XX", ${codeColumn} between (200 .. 299), "2XX", ${codeColumn} between (300 .. 399), "3XX", ${codeColumn} between (400 .. 499), "4XX", ${codeColumn} between (500 .. 599), "5XX", "Other") +| summarize count_ = count() by bin(TimeGenerated, ${timeBin}), HTTPStatus +| render timechart with (xtitle = "time", ytitle = "count")`; + } + + buildResponseTimeQuery( + endpoint: Endpoint, + config: ValidDashboardConfig, + context: QueryContext = "alert", + ): string { + const threshold = endpoint.response_time_threshold ?? 1; + const regex = this.createGenericRegex(endpoint.path); + + if (config.resource_type === "api-management") { + if (context === "dashboard") { + return `let threshold = ${threshold}; +AzureDiagnostics +| where url_s matches regex "${regex}" +| summarize duration_percentile_95_ms = percentile(DurationMs, 95) by bin(TimeGenerated, ${config.timespan}) +| extend duration_percentile_95 = duration_percentile_95_ms / 1000.0, watermark = threshold +| render timechart with (xtitle = "time", ytitle = "response time (s)")`; + } + return `let threshold = ${threshold}; +AzureDiagnostics +| where url_s matches regex "${regex}" +| summarize duration_percentile_95_ms = percentile(DurationMs, 95) by bin(TimeGenerated, ${config.timespan}) +| extend duration_percentile_95 = duration_percentile_95_ms / 1000.0 +| where duration_percentile_95 > threshold`; + } + + const hosts = config.hosts || []; + const hostsDataTable = this.createHostsDataTable(hosts); + if (context === "dashboard") { + return `let threshold = ${threshold}; +${hostsDataTable} +AzureDiagnostics +| where originalHost_s in (api_hosts) +| where requestUri_s matches regex "${regex}" +| summarize duration_percentile_95=percentiles(timeTaken_d, 95) by bin(TimeGenerated, ${config.timespan}) +| extend watermark=threshold +| render timechart with (xtitle = "time", ytitle = "response time (s)")`; + } + return `let threshold = ${threshold}; +${hostsDataTable} +AzureDiagnostics +| where originalHost_s in (api_hosts) +| where requestUri_s matches regex "${regex}" +| summarize duration_percentile_95=percentiles(timeTaken_d, 95) by bin(TimeGenerated, ${config.timespan}) +| extend watermark=threshold +| where duration_percentile_95 > threshold`; + } + + private createGenericRegex(path: string): string { + // Convert path like "/api/v1/services/{service_id}" to "/api/v1/services/[^/]+$" + return ( + path + .replace(/\{[^}]+\}/g, "[^/]+") // Replace {param} with [^/]+ + .replace(/[.*?${}()|\\]/g, "\\$&") + // Escape regex special chars (but not [ ] ^ +) + "$" + ); // Add end anchor + } + + private createHostsDataTable(hosts: string[]): string { + const hostsArray = hosts.map((host) => `"${host}"`).join(", "); + return `let api_hosts = datatable (name: string) [${hostsArray}];`; + } +} diff --git a/packages/opex-dashboard-ts/src/domain/services/panel-factory.ts b/packages/opex-dashboard-ts/src/domain/services/panel-factory.ts new file mode 100644 index 00000000..cd1c0b97 --- /dev/null +++ b/packages/opex-dashboard-ts/src/domain/services/panel-factory.ts @@ -0,0 +1,90 @@ +import { ValidDashboardConfig } from "../entities/dashboard-config.js"; +import { Panel } from "../entities/panel.js"; +import { KustoQueryService } from "./kusto-query-service.js"; +import { normalizePathPlaceholders } from "./path-utils.js"; + +/* + PanelFactory converts Endpoints into a list of logical Panel models (domain) decoupled + from Terraform/JSON serialization. This makes layout rules and future panel kinds testable + without touching infrastructure adapters. +*/ +export class PanelFactory { + private readonly queryService = new KustoQueryService(); + + buildPanels(config: ValidDashboardConfig): Panel[] { + if (!config.endpoints) return []; + + const panels: Panel[] = []; + + config.endpoints.forEach((endpoint, idx) => { + const rowY = idx * 4; + // Availability + panels.push({ + chart: { inputSpecificChart: "Line", settingsSpecificChart: "Line" }, + dimensions: { + xAxis: { name: "TimeGenerated", type: "datetime" }, + yAxis: [ + { name: "availability", type: "real" }, + { name: "watermark", type: "real" }, + ], + }, + id: idx * 3, + kind: "availability", + path: endpoint.path, + position: { colSpan: 6, rowSpan: 4, x: 0, y: rowY }, + query: this.queryService.buildAvailabilityQuery( + endpoint, + config, + "dashboard", + ), + subtitle: normalizePathPlaceholders(endpoint.path), + title: `Availability (${config.timespan})`, + }); + // Response Codes + panels.push({ + chart: { + inputSpecificChart: "Pie", + settingsSpecificChart: "StackedArea", + }, + dimensions: { + xAxis: { name: "TimeGenerated", type: "datetime" }, + yAxis: [{ name: "count_", type: "long" }], + }, + id: idx * 3 + 1, + kind: "response-codes", + path: endpoint.path, + position: { colSpan: 6, rowSpan: 4, x: 6, y: rowY }, + query: this.queryService.buildResponseCodesQuery(endpoint, config), + subtitle: normalizePathPlaceholders(endpoint.path), + title: `Response Codes (${config.timespan})`, + }); + // Response Time + panels.push({ + chart: { + inputSpecificChart: "StackedColumn", + settingsSpecificChart: "Line", + }, + dimensions: { + xAxis: { name: "TimeGenerated", type: "datetime" }, + yAxis: [ + { name: "duration_percentile_95", type: "real" }, + { name: "watermark", type: "long" }, + ], + }, + id: idx * 3 + 2, + kind: "response-time", + path: endpoint.path, + position: { colSpan: 6, rowSpan: 4, x: 12, y: rowY }, + query: this.queryService.buildResponseTimeQuery( + endpoint, + config, + "dashboard", + ), + subtitle: normalizePathPlaceholders(endpoint.path), + title: `Percentile Response Time (${config.timespan})`, + }); + }); + + return panels; + } +} diff --git a/packages/opex-dashboard-ts/src/domain/services/path-utils.ts b/packages/opex-dashboard-ts/src/domain/services/path-utils.ts new file mode 100644 index 00000000..7b3e2b8e --- /dev/null +++ b/packages/opex-dashboard-ts/src/domain/services/path-utils.ts @@ -0,0 +1,13 @@ +/* Utility functions for path placeholder normalization */ + +/* Convert placeholders like {serviceId} or {ServiceID} to {service_id} for display */ +export function normalizePathPlaceholders(path: string): string { + return path.replace(/\{([^}]+)\}/g, (_, name) => `{${toSnakeCase(name)}}`); +} + +function toSnakeCase(input: string): string { + return input + .replace(/([a-z0-9])([A-Z])/g, "$1_$2") + .replace(/[-\s]+/g, "_") + .toLowerCase(); +} diff --git a/packages/opex-dashboard-ts/src/index.ts b/packages/opex-dashboard-ts/src/index.ts new file mode 100644 index 00000000..4ddc260d --- /dev/null +++ b/packages/opex-dashboard-ts/src/index.ts @@ -0,0 +1,3 @@ +export { GenerateDashboardUseCase } from "./application/index.js"; +export * from "./domain/index.js"; +export * from "./infrastructure/index.js"; diff --git a/packages/opex-dashboard-ts/src/infrastructure/cli/generate.ts b/packages/opex-dashboard-ts/src/infrastructure/cli/generate.ts new file mode 100644 index 00000000..22b20d53 --- /dev/null +++ b/packages/opex-dashboard-ts/src/infrastructure/cli/generate.ts @@ -0,0 +1,50 @@ +/* eslint-disable no-console */ +import { Command } from "commander"; + +import { GenerateDashboardUseCase } from "../../application/index.js"; +import { EndpointParserService } from "../../domain/services/endpoint-parser-service.js"; +import { ConfigValidatorAdapter } from "../config/config-validator-adapter.js"; +import { FileReaderAdapter } from "../file/file-reader-adapter.js"; +import { OpenAPISpecResolverAdapter } from "../openapi/openapi-spec-resolver-adapter.js"; +import { TerraformFileGeneratorAdapter } from "../terraform/terraform-generator-adapter.js"; + +export const generateCommand = new Command() + .name("generate") + .description("Generate dashboard definition") + .requiredOption("-c, --config-file ", "YAML config file") + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .action(async (options: any) => { + try { + // See https://github.com/hashicorp/terraform-cdk/pull/3876 + process.env.SYNTH_HCL_OUTPUT = "true"; + + // Create adapters + const fileReader = new FileReaderAdapter(); + const configValidator = new ConfigValidatorAdapter(); + const openAPISpecResolver = new OpenAPISpecResolverAdapter(); + const endpointParser = new EndpointParserService(); + const terraformGenerator = new TerraformFileGeneratorAdapter(); + + // Create use case + const generateDashboardUseCase = new GenerateDashboardUseCase( + fileReader, + configValidator, + openAPISpecResolver, + endpointParser, + terraformGenerator, + ); + + // Execute use case + await generateDashboardUseCase.execute(options.configFile); + + // Output result + console.log("Terraform CDKTF code generated successfully"); + } catch (error: unknown) { + console.error( + "Error:", + error instanceof Error ? error.message : "Unknown error", + { cause: error }, + ); + process.exit(1); + } + }); diff --git a/packages/opex-dashboard-ts/src/infrastructure/cli/index.ts b/packages/opex-dashboard-ts/src/infrastructure/cli/index.ts new file mode 100644 index 00000000..8fe22308 --- /dev/null +++ b/packages/opex-dashboard-ts/src/infrastructure/cli/index.ts @@ -0,0 +1,18 @@ +#!/usr/bin/env node + +import { Command } from "commander"; + +import { generateCommand } from "./generate.js"; + +const program = new Command(); + +program + .name("opex-dashboard-ts") + .description( + "Generate standardized PagoPA Operational Excellence dashboards from OpenAPI specs", + ) + .version("1.0.0"); + +program.addCommand(generateCommand); + +program.parse(); diff --git a/packages/opex-dashboard-ts/src/infrastructure/config/config-validator-adapter.ts b/packages/opex-dashboard-ts/src/infrastructure/config/config-validator-adapter.ts new file mode 100644 index 00000000..711e0fc5 --- /dev/null +++ b/packages/opex-dashboard-ts/src/infrastructure/config/config-validator-adapter.ts @@ -0,0 +1,23 @@ +import { + DashboardConfigSchema, + IConfigValidator, + ValidDashboardConfig, +} from "../../domain/index.js"; + +export class ConfigValidatorAdapter implements IConfigValidator { + validateConfig(rawConfig: unknown): ValidDashboardConfig { + // Parse and validate with zod using safeParse + const result = DashboardConfigSchema.safeParse(rawConfig); + + if (!result.success) { + // Format validation errors + const errorMessage = result.error.issues + .map((err) => `• ${err.path.join(".")}: ${err.message}`) + .join("\n"); + throw new Error(`Configuration validation failed:\n${errorMessage}`); + } + + // Apply defaults + return result.data; + } +} diff --git a/packages/opex-dashboard-ts/src/infrastructure/exports/add-opex-stack.ts b/packages/opex-dashboard-ts/src/infrastructure/exports/add-opex-stack.ts new file mode 100644 index 00000000..50c397cc --- /dev/null +++ b/packages/opex-dashboard-ts/src/infrastructure/exports/add-opex-stack.ts @@ -0,0 +1,49 @@ +import { App } from "cdktf"; + +import { CreateDashboardStackUseCase } from "../../application/use-cases/create-dashboard-stack-use-case.js"; +import { DashboardConfig } from "../../domain/index.js"; +import { EndpointParserService } from "../../domain/services/endpoint-parser-service.js"; +import { ConfigValidatorAdapter } from "../config/config-validator-adapter.js"; +import { OpenAPISpecResolverAdapter } from "../openapi/openapi-spec-resolver-adapter.js"; +import { AzureOpexStack } from "../terraform/azure-dashboard.js"; +import { TerraformStackGeneratorAdapter } from "../terraform/terraform-stack-generator-adapter.js"; + +/** + * Generates Azure dashboard and alerts from configuration object. + */ +export async function addOpexStack({ + app, + config, +}: { + app: App; + config: DashboardConfig; +}): Promise<{ opexStack: AzureOpexStack }> { + try { + // See https://github.com/hashicorp/terraform-cdk/pull/3876 + process.env.SYNTH_HCL_OUTPUT = "true"; + + // Create adapters + const configValidator = new ConfigValidatorAdapter(); + const openAPISpecResolver = new OpenAPISpecResolverAdapter(); + const endpointParser = new EndpointParserService(); + const terraformGenerator = new TerraformStackGeneratorAdapter(); + + // Create use case + const createDashboardStackUseCase = new CreateDashboardStackUseCase( + configValidator, + openAPISpecResolver, + endpointParser, + terraformGenerator, + ); + + // Execute use case with config object and app + const opexStack = await createDashboardStackUseCase.execute(config, app); + + return { opexStack }; + } catch (error: unknown) { + throw new Error( + `Error generating dashboard: ${error instanceof Error ? error.message : "Unknown error"}`, + { cause: error }, + ); + } +} diff --git a/packages/opex-dashboard-ts/src/infrastructure/file/file-reader-adapter.ts b/packages/opex-dashboard-ts/src/infrastructure/file/file-reader-adapter.ts new file mode 100644 index 00000000..25b55036 --- /dev/null +++ b/packages/opex-dashboard-ts/src/infrastructure/file/file-reader-adapter.ts @@ -0,0 +1,11 @@ +import * as fs from "fs"; +import * as yaml from "js-yaml"; + +import { IFileReader } from "../../domain/index.js"; + +export class FileReaderAdapter implements IFileReader { + async readYamlFile(filePath: string): Promise { + const fileContent = fs.readFileSync(filePath, "utf8"); + return yaml.load(fileContent); + } +} diff --git a/packages/opex-dashboard-ts/src/infrastructure/index.ts b/packages/opex-dashboard-ts/src/infrastructure/index.ts new file mode 100644 index 00000000..baa49709 --- /dev/null +++ b/packages/opex-dashboard-ts/src/infrastructure/index.ts @@ -0,0 +1,7 @@ +export * from "./cli/generate.js"; +export * from "./config/config-validator-adapter.js"; +export * from "./exports/add-opex-stack.js"; +export * from "./file/file-reader-adapter.js"; +export * from "./openapi/openapi-spec-resolver-adapter.js"; +export * from "./terraform/terraform-generator-adapter.js"; +export * from "./terraform/terraform-stack-generator-adapter.js"; diff --git a/packages/opex-dashboard-ts/src/infrastructure/openapi/openapi-spec-resolver-adapter.ts b/packages/opex-dashboard-ts/src/infrastructure/openapi/openapi-spec-resolver-adapter.ts new file mode 100644 index 00000000..36e1934f --- /dev/null +++ b/packages/opex-dashboard-ts/src/infrastructure/openapi/openapi-spec-resolver-adapter.ts @@ -0,0 +1,55 @@ +import SwaggerParser from "@apidevtools/swagger-parser"; + +import { IOpenAPISpecResolver, OpenAPISpec } from "../../domain/index.js"; +import { isOpenAPIV2, isOpenAPIV3 } from "../../shared/openapi.js"; + +export class OpenAPISpecResolverAdapter implements IOpenAPISpecResolver { + async resolve(specPath: string): Promise { + try { + const spec = await SwaggerParser.parse(specPath); + return spec as OpenAPISpec; + } catch (error: unknown) { + if (error instanceof Error) { + throw new ParseError(`OA3 parsing error: ${error.message}`); + } + throw new ParseError(`OA3 parsing error: ${String(error)}`); + } + } + + async resolveWithHosts( + specPath: string, + ): Promise<{ hosts: string[]; spec: OpenAPISpec }> { + const spec = await this.resolve(specPath); + const hosts = this.extractHostsFromSpec(spec); + return { hosts, spec }; + } + + private extractHostsFromSpec(spec: OpenAPISpec): string[] { + if (isOpenAPIV3(spec)) { + // OpenAPI 3.x uses servers array + if (spec.servers && spec.servers.length > 0) { + return spec.servers.map((server) => { + try { + const url = new URL(server.url); + return url.hostname; + } catch { + return server.url.replace(/^https?:\/\//, "").split("/")[0]; + } + }); + } + } else if (isOpenAPIV2(spec)) { + // OpenAPI 2.x uses host field + if (spec.host) { + return [spec.host]; + } + } + return []; + } +} + +export class ParseError extends Error { + constructor(message: string) { + super(message); + this.name = "ParseError"; + } +} diff --git a/packages/opex-dashboard-ts/src/infrastructure/terraform/azure-alerts.ts b/packages/opex-dashboard-ts/src/infrastructure/terraform/azure-alerts.ts new file mode 100644 index 00000000..e5848809 --- /dev/null +++ b/packages/opex-dashboard-ts/src/infrastructure/terraform/azure-alerts.ts @@ -0,0 +1,196 @@ +import { + dataAzurermClientConfig, + monitorScheduledQueryRulesAlert, + portalDashboard, +} from "@cdktf/provider-azurerm"; +import { Construct } from "constructs"; + +import { Endpoint, ValidDashboardConfig } from "../../domain/index.js"; +import { KustoQueryService } from "../../domain/services/kusto-query-service.js"; + +export class AzureAlertsConstruct { + private readonly kustoQueryService = new KustoQueryService(); + + constructor( + scope: Construct, + config: ValidDashboardConfig, + dashboard: portalDashboard.PortalDashboard, + clientConfig: dataAzurermClientConfig.DataAzurermClientConfig, + resolvedLocation: string, + ) { + if (!config.endpoints) return; + + config.endpoints.forEach((endpoint, index) => { + this.createAvailabilityAlert( + scope, + config, + endpoint, + index, + dashboard, + clientConfig, + resolvedLocation, + ); + this.createResponseTimeAlert( + scope, + config, + endpoint, + index, + dashboard, + clientConfig, + resolvedLocation, + ); + }); + } + + private buildAlertDescription( + endpointPath: string, + alertType: "availability" | "responsetime", + thresholdValue: number, + dashboardId: string, + tenantId: string, + ): string { + const url = `https://portal.azure.com/#@${tenantId}/dashboard/arm${dashboardId}`; + if (alertType === "availability") { + const pct = (thresholdValue * 100).toFixed(0) + "%"; + return `Availability for ${endpointPath} is below ${pct} - ${url}`; + } + return `Response time (p95) for ${endpointPath} is above ${thresholdValue}s - ${url}`; + } + + private buildAlertName( + dashboardName: string, + alertType: "availability" | "responsetime", + endpointPath: string, + ): string { + return `${this.slug(dashboardName)}-${alertType}_${this.slug(endpointPath)}`; + } + + private createAvailabilityAlert( + scope: Construct, + config: ValidDashboardConfig, + endpoint: Endpoint, + index: number, + dashboard: portalDashboard.PortalDashboard, + clientConfig: dataAzurermClientConfig.DataAzurermClientConfig, + resolvedLocation: string, + ) { + const alertName = this.buildAlertName( + config.name, + "availability", + endpoint.path, + ); + const availabilityThreshold = endpoint.availability_threshold ?? 0.99; + new monitorScheduledQueryRulesAlert.MonitorScheduledQueryRulesAlert( + scope, + `alarm_availability_${index}`, // Changed from availability-alert-{index} + { + action: { + actionGroup: config.action_groups || [], + }, + autoMitigationEnabled: false, + dataSourceId: config.data_source, + description: this.buildAlertDescription( + endpoint.path, + "availability", + availabilityThreshold, + dashboard.id, + clientConfig.tenantId, + ), + enabled: true, + frequency: endpoint.availability_evaluation_frequency ?? 10, + location: resolvedLocation, + name: alertName, + query: this.kustoQueryService.buildAvailabilityQuery( + endpoint, + config, + "alert", + ), + resourceGroupName: config.resource_group_name, + severity: 1, + tags: config.tags, + timeWindow: endpoint.availability_evaluation_time_window ?? 20, + trigger: { + operator: "GreaterThanOrEqual", + threshold: endpoint.availability_event_occurrences ?? 1, + }, + }, + ); + } + + private createResponseTimeAlert( + scope: Construct, + config: ValidDashboardConfig, + endpoint: Endpoint, + index: number, + dashboard: portalDashboard.PortalDashboard, + clientConfig: dataAzurermClientConfig.DataAzurermClientConfig, + resolvedLocation: string, + ) { + const alertName = this.buildAlertName( + config.name, + "responsetime", + endpoint.path, + ); + const responseThreshold = endpoint.response_time_threshold ?? 1; + + new monitorScheduledQueryRulesAlert.MonitorScheduledQueryRulesAlert( + scope, + `alarm_time_${index}`, // Changed from response-time-alert-{index} + { + action: { + actionGroup: config.action_groups || [], + }, + autoMitigationEnabled: false, + dataSourceId: config.data_source, + description: this.buildAlertDescription( + endpoint.path, + "responsetime", + responseThreshold, + dashboard.id, + clientConfig.tenantId, + ), + enabled: true, + frequency: endpoint.response_time_evaluation_frequency ?? 10, + location: resolvedLocation, + name: alertName, + query: this.kustoQueryService.buildResponseTimeQuery( + endpoint, + config, + "alert", + ), + resourceGroupName: config.resource_group_name, + severity: 1, + tags: config.tags, + timeWindow: endpoint.response_time_evaluation_time_window ?? 20, + trigger: { + operator: "GreaterThanOrEqual", + threshold: endpoint.response_time_event_occurrences ?? 1, + }, + }, + ); + } + + /* Build a safe slug for embedding in Azure resource names */ + private slug(input: string): string { + return ( + input + .trim() + // remove leading slashes to avoid leading underscores later + .replace(/^\/+/, "") + // normalize spaces and unsupported chars + .replace(/\s+/g, "-") + .replace(/[{}]/g, "") + .replace(/[^a-zA-Z0-9-_/]/g, "-") + // drop trailing slashes first + .replace(/\/+$/g, "") + // turn path separators into underscores + .replace(/\//g, "_") + // collapse duplicates + .replace(/-+/g, "-") + .replace(/_+/g, "_") + // finally, trim leading/trailing separators + .replace(/^[-_]+/, "") + .replace(/[-_]+$/, "") + ); + } +} diff --git a/packages/opex-dashboard-ts/src/infrastructure/terraform/azure-dashboard.ts b/packages/opex-dashboard-ts/src/infrastructure/terraform/azure-dashboard.ts new file mode 100644 index 00000000..460a0fd5 --- /dev/null +++ b/packages/opex-dashboard-ts/src/infrastructure/terraform/azure-dashboard.ts @@ -0,0 +1,57 @@ +import { + dataAzurermClientConfig, + dataAzurermResourceGroup, + portalDashboard, + provider, +} from "@cdktf/provider-azurerm"; +import { TerraformStack } from "cdktf"; +import { Construct } from "constructs"; + +import { ValidDashboardConfig } from "../../domain/index.js"; +import { AzureAlertsConstruct } from "./azure-alerts.js"; +import { buildDashboardPropertiesTemplate } from "./dashboard-properties.js"; + +export class AzureOpexStack extends TerraformStack { + constructor(scope: Construct, id: string, config: ValidDashboardConfig) { + super(scope, id); + + // Configure Azure provider + new provider.AzurermProvider(this, "azure", { + features: [{}], + storageUseAzuread: true, + }); + + // Get current Azure client configuration for tenant info + const clientConfig = new dataAzurermClientConfig.DataAzurermClientConfig( + this, + "current", + {}, + ); + + // Lookup Resource Group to inherit location + const rg = new dataAzurermResourceGroup.DataAzurermResourceGroup( + this, + "rg", + { name: config.resource_group_name }, + ); + const resolvedLocation = rg.location; + + // Create the dashboard using CDKTF PortalDashboard + const dashboard = new portalDashboard.PortalDashboard(this, "dashboard", { + dashboardProperties: buildDashboardPropertiesTemplate(config), + location: resolvedLocation, + name: config.name.replace(/\s+/g, "_"), + resourceGroupName: config.resource_group_name, + tags: config.tags, + }); + + // Create alerts within the same stack, passing dashboard reference and tenant + new AzureAlertsConstruct( + this, + config, + dashboard, + clientConfig, + resolvedLocation, + ); + } +} diff --git a/packages/opex-dashboard-ts/src/infrastructure/terraform/dashboard-properties.ts b/packages/opex-dashboard-ts/src/infrastructure/terraform/dashboard-properties.ts new file mode 100644 index 00000000..028c6228 --- /dev/null +++ b/packages/opex-dashboard-ts/src/infrastructure/terraform/dashboard-properties.ts @@ -0,0 +1,155 @@ +import { Panel } from "../../domain/entities/panel.js"; +import { ValidDashboardConfig } from "../../domain/index.js"; +import { PanelFactory } from "../../domain/services/panel-factory.js"; + +export function buildDashboardPropertiesTemplate( + config: ValidDashboardConfig, +): string { + const factory = new PanelFactory(); + const panels = factory.buildPanels(config); + const parts = panels + .map((panel) => `"${panel.id}": ${serializePanel(panel, config)}`) + .join(","); + + return `{ + "lenses": { + "0": { + "order": 0, + "parts": { + ${parts} + } + } + }, + "metadata": { + "model": { + "timeRange": { + "value": { + "relative": { + "duration": 24, + "timeUnit": 1 + } + }, + "type": "MsPortalFx.Composition.Configuration.ValueTypes.TimeRange" + }, + "filterLocale": { + "value": "en-us" + }, + "filters": { + "value": { + "MsPortalFx_TimeRange": { + "model": { + "format": "local", + "granularity": "auto", + "relative": "48h" + }, + "displayCache": { + "name": "Local Time", + "value": "Past 48 hours" + }, + "filteredPartIds": [ + "StartboardPart-LogsDashboardPart-9badbd78-7607-4131-8fa1-8b85191432ed", + "StartboardPart-LogsDashboardPart-9badbd78-7607-4131-8fa1-8b85191432ef", + "StartboardPart-LogsDashboardPart-9badbd78-7607-4131-8fa1-8b85191432f1", + "StartboardPart-LogsDashboardPart-9badbd78-7607-4131-8fa1-8b85191432f3", + "StartboardPart-LogsDashboardPart-9badbd78-7607-4131-8fa1-8b85191432f5", + "StartboardPart-LogsDashboardPart-9badbd78-7607-4131-8fa1-8b85191432f7", + "StartboardPart-LogsDashboardPart-9badbd78-7607-4131-8fa1-8b85191432f9", + "StartboardPart-LogsDashboardPart-9badbd78-7607-4131-8fa1-8b85191432fb", + "StartboardPart-LogsDashboardPart-9badbd78-7607-4131-8fa1-8b85191432fd" + ] + } + } + } + } + } + }`; +} + +function buildInputDimensions(panel: Panel): string { + if (panel.kind === "availability") { + return JSON.stringify({ + aggregation: "Sum", + splitBy: [], + xAxis: { name: "TimeGenerated", type: "datetime" }, + yAxis: [ + { name: "availability", type: "real" }, + { name: "watermark", type: "real" }, + ], + }); + } + if (panel.kind === "response-codes") { + return JSON.stringify({ + aggregation: "Sum", + splitBy: [{ name: "HTTPStatus", type: "string" }], + xAxis: { name: "TimeGenerated", type: "datetime" }, + yAxis: [{ name: "count_", type: "long" }], + }); + } + if (panel.kind === "response-time") { + return JSON.stringify({ + aggregation: "Sum", + splitBy: [], + xAxis: { name: "TimeGenerated", type: "datetime" }, + yAxis: [ + { name: "duration_percentile_95", type: "real" }, + { name: "watermark", type: "long" }, + ], + }); + } + return "{}"; +} + +function buildSettingsDimensions(panel: Panel): string | undefined { + if (panel.kind === "response-codes") { + return JSON.stringify({ + aggregation: "Sum", + splitBy: [{ name: "HTTPStatus", type: "string" }], + xAxis: { name: "TimeGenerated", type: "datetime" }, + yAxis: [{ name: "count_", type: "long" }], + }); + } + if (panel.kind === "response-time") { + return JSON.stringify({ + aggregation: "Sum", + splitBy: [], + xAxis: { name: "TimeGenerated", type: "datetime" }, + yAxis: [ + { name: "duration_percentile_95", type: "real" }, + { name: "watermark", type: "long" }, + ], + }); + } + return undefined; +} + +function serializePanel(panel: Panel, config: ValidDashboardConfig): string { + const resourceIds = JSON.stringify(config.resourceIds || []); + const inputsChart = panel.chart.inputSpecificChart; + const dimensions = buildInputDimensions(panel); + const settingsDimensions = buildSettingsDimensions(panel); + return `{ + "position": ${JSON.stringify(panel.position)}, + "metadata": { + "inputs": [ + { "name": "resourceTypeMode", "isOptional": true }, + { "name": "ComponentId", "isOptional": true }, + { "name": "Scope", "value": { "resourceIds": ${resourceIds} }, "isOptional": true }, + { "name": "PartId", "isOptional": true }, + { "name": "Version", "value": "2.0", "isOptional": true }, + { "name": "TimeRange", "value": "PT4H", "isOptional": true }, + { "name": "DashboardId", "isOptional": true }, + { "name": "DraftRequestParameters", "value": { "scope": "hierarchy" }, "isOptional": true }, + { "name": "Query", "value": ${JSON.stringify(panel.query)}, "isOptional": true }, + { "name": "ControlType", "value": "FrameControlChart", "isOptional": true }, + { "name": "SpecificChart", "value": "${inputsChart}", "isOptional": true }, + { "name": "PartTitle", "value": "${panel.title}", "isOptional": true }, + { "name": "PartSubTitle", "value": "${panel.subtitle}", "isOptional": true }, + { "name": "Dimensions", "value": ${dimensions}, "isOptional": true }, + { "name": "LegendOptions", "value": { "isEnabled": true, "position": "Bottom" }, "isOptional": true }, + { "name": "IsQueryContainTimeRange", "value": false, "isOptional": true } + ], + "type": "Extension/Microsoft_OperationsManagementSuite_Workspace/PartType/LogsDashboardPart", + "settings": { "content": { "Query": ${JSON.stringify(panel.query)}, ${panel.kind === "response-codes" ? '"SpecificChart": "StackedArea",' : panel.kind === "response-time" ? '"SpecificChart": "Line",' : ""} "PartTitle": "${panel.title}"${settingsDimensions ? `, "Dimensions": ${settingsDimensions}` : ""} } } + } + }`; +} diff --git a/packages/opex-dashboard-ts/src/infrastructure/terraform/terraform-generator-adapter.ts b/packages/opex-dashboard-ts/src/infrastructure/terraform/terraform-generator-adapter.ts new file mode 100644 index 00000000..f43de552 --- /dev/null +++ b/packages/opex-dashboard-ts/src/infrastructure/terraform/terraform-generator-adapter.ts @@ -0,0 +1,20 @@ +import { App } from "cdktf"; + +import { + ITerraformFileGenerator, + ValidDashboardConfig, +} from "../../domain/index.js"; +import { AzureOpexStack } from "./azure-dashboard.js"; + +export class TerraformFileGeneratorAdapter implements ITerraformFileGenerator { + async generate(config: ValidDashboardConfig): Promise { + // Create CDKTF app + const app = new App({ hclOutput: true, outdir: "opex" }); + + // Create the main stack + new AzureOpexStack(app, "opex-dashboard", config); + + // Generate Terraform code + app.synth(); + } +} diff --git a/packages/opex-dashboard-ts/src/infrastructure/terraform/terraform-stack-generator-adapter.ts b/packages/opex-dashboard-ts/src/infrastructure/terraform/terraform-stack-generator-adapter.ts new file mode 100644 index 00000000..b2b964ff --- /dev/null +++ b/packages/opex-dashboard-ts/src/infrastructure/terraform/terraform-stack-generator-adapter.ts @@ -0,0 +1,20 @@ +import { App } from "cdktf"; + +import { + ITerraformStackGenerator, + ValidDashboardConfig, +} from "../../domain/index.js"; +import { AzureOpexStack } from "./azure-dashboard.js"; + +export class TerraformStackGeneratorAdapter + implements ITerraformStackGenerator +{ + async generate( + config: ValidDashboardConfig, + app: App, + ): Promise { + // Create and return the stack using the provided app + const opexStack = new AzureOpexStack(app, "opex-dashboard", config); + return opexStack; + } +} diff --git a/packages/opex-dashboard-ts/src/shared/openapi.ts b/packages/opex-dashboard-ts/src/shared/openapi.ts new file mode 100644 index 00000000..3e8f107a --- /dev/null +++ b/packages/opex-dashboard-ts/src/shared/openapi.ts @@ -0,0 +1,12 @@ +import { OpenAPIV2, OpenAPIV3 } from "openapi-types"; + +export type OpenAPISpec = OpenAPIV2.Document | OpenAPIV3.Document; + +// Type guards to check OpenAPI version +export function isOpenAPIV2(spec: OpenAPISpec): spec is OpenAPIV2.Document { + return "swagger" in spec; +} + +export function isOpenAPIV3(spec: OpenAPISpec): spec is OpenAPIV3.Document { + return "openapi" in spec; +} diff --git a/packages/opex-dashboard-ts/test/fixtures/azure_dashboard_config.yaml b/packages/opex-dashboard-ts/test/fixtures/azure_dashboard_config.yaml new file mode 100644 index 00000000..4331ec25 --- /dev/null +++ b/packages/opex-dashboard-ts/test/fixtures/azure_dashboard_config.yaml @@ -0,0 +1,12 @@ +oa3_spec: https://raw.githubusercontent.com/pagopa/opex-dashboard/refs/heads/main/test/data/io_backend_light.yaml +name: My Dashboard +timespan: 5m +data_source: /subscriptions/uuid/resourceGroups/my-rg/providers/Microsoft.Network/applicationGateways/my-gtw +resource_type: app-gateway +resource_group_name: dashboards +action_groups: + - /subscriptions/uuid/resourceGroups/my-rg/providers/microsoft.insights/actionGroups/my-action-group-email + - /subscriptions/uuid/resourceGroups/my-rg/providers/microsoft.insights/actionGroups/my-action-group-slack +tags: + Environment: TEST + CostCenter: CC123 diff --git a/packages/opex-dashboard-ts/test/fixtures/azure_dashboard_overrides_config.yaml b/packages/opex-dashboard-ts/test/fixtures/azure_dashboard_overrides_config.yaml new file mode 100644 index 00000000..f6491cd8 --- /dev/null +++ b/packages/opex-dashboard-ts/test/fixtures/azure_dashboard_overrides_config.yaml @@ -0,0 +1,26 @@ +oa3_spec: https://raw.githubusercontent.com/pagopa/opex-dashboard/refs/heads/main/test/data/io_backend_light.yaml +name: My spec +timespan: 6m # Default, a number or a timespan https://docs.microsoft.com/en-us/azure/data-explorer/kusto/query/scalar-data-types/timespan +data_source: /subscriptions/uuid/resourceGroups/my-rg/providers/Microsoft.Network/applicationGateways/my-gtw +resource_type: app-gateway +resource_group_name: dashboards +action_groups: + - /subscriptions/uuid/resourceGroups/my-rg/providers/microsoft.insights/actionGroups/my-action-group-email + - /subscriptions/uuid/resourceGroups/my-rg/providers/microsoft.insights/actionGroups/my-action-group-slack +tags: + Environment: TEST + Owner: team-opex +overrides: + hosts: # Use these hosts instead of those inside the OpenApi spec + - https://example.com + - https://example2.com + endpoints: + /api/v1/services/{service_id}: + availability_threshold: 0.95 # Default: 99% + availability_evaluation_frequency: 30 # Default: 10 + availability_evaluation_time_window: 50 # Default: 20 + availability_event_occurrences: 3 # Default: 1 + response_time_threshold: 2 # Default: 1 + response_time_evaluation_frequency: 35 # Default: 10 + response_time_evaluation_time_window: 55 # Default: 20 + response_time_event_occurrences: 5 # Default: 1 diff --git a/packages/opex-dashboard-ts/test/fixtures/io_backend_light.yaml b/packages/opex-dashboard-ts/test/fixtures/io_backend_light.yaml new file mode 100644 index 00000000..3e0297ce --- /dev/null +++ b/packages/opex-dashboard-ts/test/fixtures/io_backend_light.yaml @@ -0,0 +1,124 @@ +swagger: "2.0" +info: + version: 1.0.0 + title: Proxy API + description: Mobile and web proxy API gateway. +host: app-backend.io.italia.it +basePath: /api/v1 +schemes: + - https +security: + - Bearer: [] +paths: + "/services/{service_id}": + x-swagger-router-controller: ServicesController + parameters: + - name: service_id + in: path + type: string + required: true + description: The ID of an existing Service. + get: + operationId: getService + summary: Get Service + description: A previously created service with the provided service ID is returned. + responses: + "200": + description: Service found. + schema: + "$ref": "#/definitions/ServicePublic" + examples: + application/json: + department_name: "IO" + organization_fiscal_code: "00000000000" + organization_name: "IO" + service_id: "5a563817fcc896087002ea46c49a" + service_name: "App IO" + version: 1 + "400": + description: Bad request + schema: + $ref: "#/definitions/ProblemJson" + "401": + description: Bearer token null or expired. + "404": + description: No service found for the provided ID. + schema: + $ref: "#/definitions/ProblemJson" + "429": + description: Too many requests + schema: + $ref: "#/definitions/ProblemJson" + "500": + description: There was an error in retrieving the service. + schema: + $ref: "#/definitions/ProblemJson" + parameters: [] + "/services": + x-swagger-router-controller: ServicesController + get: + operationId: getVisibleServices + summary: Get all visible services + description: |- + Returns the description of all visible services. + responses: + "200": + description: Found. + schema: + $ref: "#/definitions/PaginatedServiceTupleCollection" + examples: + application/json: + items: + - service_id: "AzureDeployc49a" + version: 1 + - service_id: "5a25abf4fcc89605c082f042c49a" + version: 0 + page_size: 1 + "401": + description: Bearer token null or expired. + "429": + description: Too many requests + schema: + $ref: "#/definitions/ProblemJson" + "500": + description: There was an error in retrieving the services. + schema: + $ref: "#/definitions/ProblemJson" + parameters: + - $ref: "#/parameters/PaginationRequest" +definitions: + ProblemJson: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/ProblemJson" + ServiceId: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/ServiceId" + ServiceName: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/ServiceName" + ServicePublic: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/ServicePublic" + PaginatedServiceTupleCollection: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/PaginatedServiceTupleCollection" +responses: {} +parameters: + PageSize: + name: page_size + type: integer + in: query + minimum: 1 + maximum: 100 + required: false + description: How many items a page should include. + PaginationRequest: + type: string + name: cursor + in: query + minimum: 1 + description: An opaque identifier that points to the next item in the collection. +consumes: + - application/json +produces: + - application/json +securityDefinitions: + Bearer: + type: apiKey + name: Authorization + in: header diff --git a/packages/opex-dashboard-ts/test/fixtures/iobackend_light_no_overrides.txt b/packages/opex-dashboard-ts/test/fixtures/iobackend_light_no_overrides.txt new file mode 100644 index 00000000..7c7ee448 --- /dev/null +++ b/packages/opex-dashboard-ts/test/fixtures/iobackend_light_no_overrides.txt @@ -0,0 +1,1027 @@ + +locals { + name = "${var.prefix}-${var.env_short}-PROD-IO/IO_App_Availability" + dashboard_base_addr = "https://portal.azure.com/#@pagopait.onmicrosoft.com/dashboard/arm" +} + +data "azurerm_resource_group" "this" { + name = "dashboards" +} + +resource "azurerm_portal_dashboard" "this" { + name = local.name + resource_group_name = data.azurerm_resource_group.this.name + location = data.azurerm_resource_group.this.location + + dashboard_properties = <<-PROPS + { + "lenses": { + "0": { + "order": 0, + "parts": { + "0": { + "position": { + "x": 0, + "y": 0, + "colSpan": 6, + "rowSpan": 4 + }, + "metadata": { + "inputs": [ + { + "name": "resourceTypeMode", + "isOptional": true + }, + { + "name": "ComponentId", + "isOptional": true + }, + { + "name": "Scope", + "value": { + "resourceIds": [ + "/subscriptions/uuid/resourceGroups/io-p-rg-external/providers/Microsoft.Network/applicationGateways/io-p-appgateway" + ] + }, + "isOptional": true + }, + { + "name": "PartId", + "isOptional": true + }, + { + "name": "Version", + "value": "2.0", + "isOptional": true + }, + { + "name": "TimeRange", + "value": "PT4H", + "isOptional": true + }, + { + "name": "DashboardId", + "isOptional": true + }, + { + "name": "DraftRequestParameters", + "value": { + "scope": "hierarchy" + }, + "isOptional": true + }, + { + "name": "Query", + "value": "\nlet api_hosts = datatable (name: string) [\"app-backend.io.italia.it\"];\nlet threshold = 0.99;\nAzureDiagnostics\n| where originalHost_s in (api_hosts)\n| where requestUri_s matches regex \"/api/v1/services/[^/]+$\"\n| summarize\n Total=count(),\n Success=count(httpStatus_d < 500) by bin(TimeGenerated, 5m)\n| extend availability=toreal(Success) / Total\n| project TimeGenerated, availability, watermark=threshold\n| render timechart with (xtitle = \"time\", ytitle= \"availability(%)\")\n", + "isOptional": true + }, + { + "name": "ControlType", + "value": "FrameControlChart", + "isOptional": true + }, + { + "name": "SpecificChart", + "value": "Line", + "isOptional": true + }, + { + "name": "PartTitle", + "value": "Availability (5m)", + "isOptional": true + }, + { + "name": "PartSubTitle", + "value": "/api/v1/services/{service_id}", + "isOptional": true + }, + { + "name": "Dimensions", + "value": { + "xAxis": { + "name": "TimeGenerated", + "type": "datetime" + }, + "yAxis": [ + { + "name": "availability", + "type": "real" + }, + { + "name": "watermark", + "type": "real" + } + ], + "splitBy": [], + "aggregation": "Sum" + }, + "isOptional": true + }, + { + "name": "LegendOptions", + "value": { + "isEnabled": true, + "position": "Bottom" + }, + "isOptional": true + }, + { + "name": "IsQueryContainTimeRange", + "value": false, + "isOptional": true + } + ], + "type": "Extension/Microsoft_OperationsManagementSuite_Workspace/PartType/LogsDashboardPart", + "settings": { + "content": { + "Query": "\nlet api_hosts = datatable (name: string) [\"app-backend.io.italia.it\"];\nlet threshold = 0.99;\nAzureDiagnostics\n| where originalHost_s in (api_hosts)\n| where requestUri_s matches regex \"/api/v1/services/[^/]+$\"\n| summarize\n Total=count(),\n Success=count(httpStatus_d < 500) by bin(TimeGenerated, 5m)\n| extend availability=toreal(Success) / Total\n| project TimeGenerated, availability, watermark=threshold\n| render timechart with (xtitle = \"time\", ytitle= \"availability(%)\")\n", + "PartTitle": "Availability (5m)" + } + } + } + }, + "1": { + "position": { + "x": 6, + "y": 0, + "colSpan": 6, + "rowSpan": 4 + }, + "metadata": { + "inputs": [ + { + "name": "resourceTypeMode", + "isOptional": true + }, + { + "name": "ComponentId", + "isOptional": true + }, + { + "name": "Scope", + "value": { + "resourceIds": [ + "/subscriptions/uuid/resourceGroups/io-p-rg-external/providers/Microsoft.Network/applicationGateways/io-p-appgateway" + ] + }, + "isOptional": true + }, + { + "name": "PartId", + "isOptional": true + }, + { + "name": "Version", + "value": "2.0", + "isOptional": true + }, + { + "name": "TimeRange", + "value": "PT4H", + "isOptional": true + }, + { + "name": "DashboardId", + "isOptional": true + }, + { + "name": "DraftRequestParameters", + "value": { + "scope": "hierarchy" + }, + "isOptional": true + }, + { + "name": "Query", + "value": "\nlet api_url = \"/api/v1/services/[^/]+$\";\nlet api_hosts = datatable (name: string) [\"app-backend.io.italia.it\"];\nAzureDiagnostics\n| where originalHost_s in (api_hosts)\n| where requestUri_s matches regex api_url\n| extend HTTPStatus = case(\n httpStatus_d between (100 .. 199), \"1XX\",\n httpStatus_d between (200 .. 299), \"2XX\",\n httpStatus_d between (300 .. 399), \"3XX\",\n httpStatus_d between (400 .. 499), \"4XX\",\n \"5XX\")\n| summarize count() by HTTPStatus, bin(TimeGenerated, 5m)\n| render areachart with (xtitle = \"time\", ytitle= \"count\")\n", + "isOptional": true + }, + { + "name": "ControlType", + "value": "FrameControlChart", + "isOptional": true + }, + { + "name": "SpecificChart", + "value": "Pie", + "isOptional": true + }, + { + "name": "PartTitle", + "value": "Response Codes (5m)", + "isOptional": true + }, + { + "name": "PartSubTitle", + "value": "/api/v1/services/{service_id}", + "isOptional": true + }, + { + "name": "Dimensions", + "value": { + "xAxis": { + "name": "httpStatus_d", + "type": "string" + }, + "yAxis": [ + { + "name": "count_", + "type": "long" + } + ], + "splitBy": [], + "aggregation": "Sum" + }, + "isOptional": true + }, + { + "name": "LegendOptions", + "value": { + "isEnabled": true, + "position": "Bottom" + }, + "isOptional": true + }, + { + "name": "IsQueryContainTimeRange", + "value": false, + "isOptional": true + } + ], + "type": "Extension/Microsoft_OperationsManagementSuite_Workspace/PartType/LogsDashboardPart", + "settings": { + "content": { + "Query": "\nlet api_url = \"/api/v1/services/[^/]+$\";\nlet api_hosts = datatable (name: string) [\"app-backend.io.italia.it\"];\nAzureDiagnostics\n| where originalHost_s in (api_hosts)\n| where requestUri_s matches regex api_url\n| extend HTTPStatus = case(\n httpStatus_d between (100 .. 199), \"1XX\",\n httpStatus_d between (200 .. 299), \"2XX\",\n httpStatus_d between (300 .. 399), \"3XX\",\n httpStatus_d between (400 .. 499), \"4XX\",\n \"5XX\")\n| summarize count() by HTTPStatus, bin(TimeGenerated, 5m)\n| render areachart with (xtitle = \"time\", ytitle= \"count\")\n", + "SpecificChart": "StackedArea", + "PartTitle": "Response Codes (5m)", + "Dimensions": { + "xAxis": { + "name": "TimeGenerated", + "type": "datetime" + }, + "yAxis": [ + { + "name": "count_", + "type": "long" + } + ], + "splitBy": [ + { + "name": "HTTPStatus", + "type": "string" + } + ], + "aggregation": "Sum" + } + } + } + } + }, + "2": { + "position": { + "x": 12, + "y": 0, + "colSpan": 6, + "rowSpan": 4 + }, + "metadata": { + "inputs": [ + { + "name": "resourceTypeMode", + "isOptional": true + }, + { + "name": "ComponentId", + "isOptional": true + }, + { + "name": "Scope", + "value": { + "resourceIds": [ + "/subscriptions/uuid/resourceGroups/io-p-rg-external/providers/Microsoft.Network/applicationGateways/io-p-appgateway" + ] + }, + "isOptional": true + }, + { + "name": "PartId", + "isOptional": true + }, + { + "name": "Version", + "value": "2.0", + "isOptional": true + }, + { + "name": "TimeRange", + "value": "PT4H", + "isOptional": true + }, + { + "name": "DashboardId", + "isOptional": true + }, + { + "name": "DraftRequestParameters", + "value": { + "scope": "hierarchy" + }, + "isOptional": true + }, + { + "name": "Query", + "value": "\nlet api_hosts = datatable (name: string) [\"app-backend.io.italia.it\"];\nlet threshold = 1;\nAzureDiagnostics\n| where originalHost_s in (api_hosts)\n| where requestUri_s matches regex \"/api/v1/services/[^/]+$\"\n| summarize\n watermark=threshold,\n duration_percentile_95=percentiles(timeTaken_d, 95) by bin(TimeGenerated, 5m)\n| render timechart with (xtitle = \"time\", ytitle= \"response time(s)\")\n", + "isOptional": true + }, + { + "name": "ControlType", + "value": "FrameControlChart", + "isOptional": true + }, + { + "name": "SpecificChart", + "value": "StackedColumn", + "isOptional": true + }, + { + "name": "PartTitle", + "value": "Percentile Response Time (5m)", + "isOptional": true + }, + { + "name": "PartSubTitle", + "value": "/api/v1/services/{service_id}", + "isOptional": true + }, + { + "name": "Dimensions", + "value": { + "xAxis": { + "name": "TimeGenerated", + "type": "datetime" + }, + "yAxis": [ + { + "name": "duration_percentile_95", + "type": "real" + } + ], + "splitBy": [], + "aggregation": "Sum" + }, + "isOptional": true + }, + { + "name": "LegendOptions", + "value": { + "isEnabled": true, + "position": "Bottom" + }, + "isOptional": true + }, + { + "name": "IsQueryContainTimeRange", + "value": false, + "isOptional": true + } + ], + "type": "Extension/Microsoft_OperationsManagementSuite_Workspace/PartType/LogsDashboardPart", + "settings": { + "content": { + "Query": "\nlet api_hosts = datatable (name: string) [\"app-backend.io.italia.it\"];\nlet threshold = 1;\nAzureDiagnostics\n| where originalHost_s in (api_hosts)\n| where requestUri_s matches regex \"/api/v1/services/[^/]+$\"\n| summarize\n watermark=threshold,\n duration_percentile_95=percentiles(timeTaken_d, 95) by bin(TimeGenerated, 5m)\n| render timechart with (xtitle = \"time\", ytitle= \"response time(s)\")\n", + "SpecificChart": "Line", + "PartTitle": "Percentile Response Time (5m)", + "Dimensions": { + "xAxis": { + "name": "TimeGenerated", + "type": "datetime" + }, + "yAxis": [ + { + "name": "watermark", + "type": "long" + }, + { + "name": "duration_percentile_95", + "type": "real" + } + ], + "splitBy": [], + "aggregation": "Sum" + } + } + } + } + }, + "3": { + "position": { + "x": 0, + "y": 4, + "colSpan": 6, + "rowSpan": 4 + }, + "metadata": { + "inputs": [ + { + "name": "resourceTypeMode", + "isOptional": true + }, + { + "name": "ComponentId", + "isOptional": true + }, + { + "name": "Scope", + "value": { + "resourceIds": [ + "/subscriptions/uuid/resourceGroups/io-p-rg-external/providers/Microsoft.Network/applicationGateways/io-p-appgateway" + ] + }, + "isOptional": true + }, + { + "name": "PartId", + "isOptional": true + }, + { + "name": "Version", + "value": "2.0", + "isOptional": true + }, + { + "name": "TimeRange", + "value": "PT4H", + "isOptional": true + }, + { + "name": "DashboardId", + "isOptional": true + }, + { + "name": "DraftRequestParameters", + "value": { + "scope": "hierarchy" + }, + "isOptional": true + }, + { + "name": "Query", + "value": "\nlet api_hosts = datatable (name: string) [\"app-backend.io.italia.it\"];\nlet threshold = 0.99;\nAzureDiagnostics\n| where originalHost_s in (api_hosts)\n| where requestUri_s matches regex \"/api/v1/services$\"\n| summarize\n Total=count(),\n Success=count(httpStatus_d < 500) by bin(TimeGenerated, 5m)\n| extend availability=toreal(Success) / Total\n| project TimeGenerated, availability, watermark=threshold\n| render timechart with (xtitle = \"time\", ytitle= \"availability(%)\")\n", + "isOptional": true + }, + { + "name": "ControlType", + "value": "FrameControlChart", + "isOptional": true + }, + { + "name": "SpecificChart", + "value": "Line", + "isOptional": true + }, + { + "name": "PartTitle", + "value": "Availability (5m)", + "isOptional": true + }, + { + "name": "PartSubTitle", + "value": "/api/v1/services", + "isOptional": true + }, + { + "name": "Dimensions", + "value": { + "xAxis": { + "name": "TimeGenerated", + "type": "datetime" + }, + "yAxis": [ + { + "name": "availability", + "type": "real" + }, + { + "name": "watermark", + "type": "real" + } + ], + "splitBy": [], + "aggregation": "Sum" + }, + "isOptional": true + }, + { + "name": "LegendOptions", + "value": { + "isEnabled": true, + "position": "Bottom" + }, + "isOptional": true + }, + { + "name": "IsQueryContainTimeRange", + "value": false, + "isOptional": true + } + ], + "type": "Extension/Microsoft_OperationsManagementSuite_Workspace/PartType/LogsDashboardPart", + "settings": { + "content": { + "Query": "\nlet api_hosts = datatable (name: string) [\"app-backend.io.italia.it\"];\nlet threshold = 0.99;\nAzureDiagnostics\n| where originalHost_s in (api_hosts)\n| where requestUri_s matches regex \"/api/v1/services$\"\n| summarize\n Total=count(),\n Success=count(httpStatus_d < 500) by bin(TimeGenerated, 5m)\n| extend availability=toreal(Success) / Total\n| project TimeGenerated, availability, watermark=threshold\n| render timechart with (xtitle = \"time\", ytitle= \"availability(%)\")\n", + "PartTitle": "Availability (5m)" + } + } + } + }, + "4": { + "position": { + "x": 6, + "y": 4, + "colSpan": 6, + "rowSpan": 4 + }, + "metadata": { + "inputs": [ + { + "name": "resourceTypeMode", + "isOptional": true + }, + { + "name": "ComponentId", + "isOptional": true + }, + { + "name": "Scope", + "value": { + "resourceIds": [ + "/subscriptions/uuid/resourceGroups/io-p-rg-external/providers/Microsoft.Network/applicationGateways/io-p-appgateway" + ] + }, + "isOptional": true + }, + { + "name": "PartId", + "isOptional": true + }, + { + "name": "Version", + "value": "2.0", + "isOptional": true + }, + { + "name": "TimeRange", + "value": "PT4H", + "isOptional": true + }, + { + "name": "DashboardId", + "isOptional": true + }, + { + "name": "DraftRequestParameters", + "value": { + "scope": "hierarchy" + }, + "isOptional": true + }, + { + "name": "Query", + "value": "\nlet api_url = \"/api/v1/services$\";\nlet api_hosts = datatable (name: string) [\"app-backend.io.italia.it\"];\nAzureDiagnostics\n| where originalHost_s in (api_hosts)\n| where requestUri_s matches regex api_url\n| extend HTTPStatus = case(\n httpStatus_d between (100 .. 199), \"1XX\",\n httpStatus_d between (200 .. 299), \"2XX\",\n httpStatus_d between (300 .. 399), \"3XX\",\n httpStatus_d between (400 .. 499), \"4XX\",\n \"5XX\")\n| summarize count() by HTTPStatus, bin(TimeGenerated, 5m)\n| render areachart with (xtitle = \"time\", ytitle= \"count\")\n", + "isOptional": true + }, + { + "name": "ControlType", + "value": "FrameControlChart", + "isOptional": true + }, + { + "name": "SpecificChart", + "value": "Pie", + "isOptional": true + }, + { + "name": "PartTitle", + "value": "Response Codes (5m)", + "isOptional": true + }, + { + "name": "PartSubTitle", + "value": "/api/v1/services", + "isOptional": true + }, + { + "name": "Dimensions", + "value": { + "xAxis": { + "name": "httpStatus_d", + "type": "string" + }, + "yAxis": [ + { + "name": "count_", + "type": "long" + } + ], + "splitBy": [], + "aggregation": "Sum" + }, + "isOptional": true + }, + { + "name": "LegendOptions", + "value": { + "isEnabled": true, + "position": "Bottom" + }, + "isOptional": true + }, + { + "name": "IsQueryContainTimeRange", + "value": false, + "isOptional": true + } + ], + "type": "Extension/Microsoft_OperationsManagementSuite_Workspace/PartType/LogsDashboardPart", + "settings": { + "content": { + "Query": "\nlet api_url = \"/api/v1/services$\";\nlet api_hosts = datatable (name: string) [\"app-backend.io.italia.it\"];\nAzureDiagnostics\n| where originalHost_s in (api_hosts)\n| where requestUri_s matches regex api_url\n| extend HTTPStatus = case(\n httpStatus_d between (100 .. 199), \"1XX\",\n httpStatus_d between (200 .. 299), \"2XX\",\n httpStatus_d between (300 .. 399), \"3XX\",\n httpStatus_d between (400 .. 499), \"4XX\",\n \"5XX\")\n| summarize count() by HTTPStatus, bin(TimeGenerated, 5m)\n| render areachart with (xtitle = \"time\", ytitle= \"count\")\n", + "SpecificChart": "StackedArea", + "PartTitle": "Response Codes (5m)", + "Dimensions": { + "xAxis": { + "name": "TimeGenerated", + "type": "datetime" + }, + "yAxis": [ + { + "name": "count_", + "type": "long" + } + ], + "splitBy": [ + { + "name": "HTTPStatus", + "type": "string" + } + ], + "aggregation": "Sum" + } + } + } + } + }, + "5": { + "position": { + "x": 12, + "y": 4, + "colSpan": 6, + "rowSpan": 4 + }, + "metadata": { + "inputs": [ + { + "name": "resourceTypeMode", + "isOptional": true + }, + { + "name": "ComponentId", + "isOptional": true + }, + { + "name": "Scope", + "value": { + "resourceIds": [ + "/subscriptions/uuid/resourceGroups/io-p-rg-external/providers/Microsoft.Network/applicationGateways/io-p-appgateway" + ] + }, + "isOptional": true + }, + { + "name": "PartId", + "isOptional": true + }, + { + "name": "Version", + "value": "2.0", + "isOptional": true + }, + { + "name": "TimeRange", + "value": "PT4H", + "isOptional": true + }, + { + "name": "DashboardId", + "isOptional": true + }, + { + "name": "DraftRequestParameters", + "value": { + "scope": "hierarchy" + }, + "isOptional": true + }, + { + "name": "Query", + "value": "\nlet api_hosts = datatable (name: string) [\"app-backend.io.italia.it\"];\nlet threshold = 1;\nAzureDiagnostics\n| where originalHost_s in (api_hosts)\n| where requestUri_s matches regex \"/api/v1/services$\"\n| summarize\n watermark=threshold,\n duration_percentile_95=percentiles(timeTaken_d, 95) by bin(TimeGenerated, 5m)\n| render timechart with (xtitle = \"time\", ytitle= \"response time(s)\")\n", + "isOptional": true + }, + { + "name": "ControlType", + "value": "FrameControlChart", + "isOptional": true + }, + { + "name": "SpecificChart", + "value": "StackedColumn", + "isOptional": true + }, + { + "name": "PartTitle", + "value": "Percentile Response Time (5m)", + "isOptional": true + }, + { + "name": "PartSubTitle", + "value": "/api/v1/services", + "isOptional": true + }, + { + "name": "Dimensions", + "value": { + "xAxis": { + "name": "TimeGenerated", + "type": "datetime" + }, + "yAxis": [ + { + "name": "duration_percentile_95", + "type": "real" + } + ], + "splitBy": [], + "aggregation": "Sum" + }, + "isOptional": true + }, + { + "name": "LegendOptions", + "value": { + "isEnabled": true, + "position": "Bottom" + }, + "isOptional": true + }, + { + "name": "IsQueryContainTimeRange", + "value": false, + "isOptional": true + } + ], + "type": "Extension/Microsoft_OperationsManagementSuite_Workspace/PartType/LogsDashboardPart", + "settings": { + "content": { + "Query": "\nlet api_hosts = datatable (name: string) [\"app-backend.io.italia.it\"];\nlet threshold = 1;\nAzureDiagnostics\n| where originalHost_s in (api_hosts)\n| where requestUri_s matches regex \"/api/v1/services$\"\n| summarize\n watermark=threshold,\n duration_percentile_95=percentiles(timeTaken_d, 95) by bin(TimeGenerated, 5m)\n| render timechart with (xtitle = \"time\", ytitle= \"response time(s)\")\n", + "SpecificChart": "Line", + "PartTitle": "Percentile Response Time (5m)", + "Dimensions": { + "xAxis": { + "name": "TimeGenerated", + "type": "datetime" + }, + "yAxis": [ + { + "name": "watermark", + "type": "long" + }, + { + "name": "duration_percentile_95", + "type": "real" + } + ], + "splitBy": [], + "aggregation": "Sum" + } + } + } + } + } + } + } + }, + "metadata": { + "model": { + "timeRange": { + "value": { + "relative": { + "duration": 24, + "timeUnit": 1 + } + }, + "type": "MsPortalFx.Composition.Configuration.ValueTypes.TimeRange" + }, + "filterLocale": { + "value": "en-us" + }, + "filters": { + "value": { + "MsPortalFx_TimeRange": { + "model": { + "format": "local", + "granularity": "auto", + "relative": "48h" + }, + "displayCache": { + "name": "Local Time", + "value": "Past 48 hours" + }, + "filteredPartIds": [ + "StartboardPart-LogsDashboardPart-9badbd78-7607-4131-8fa1-8b85191432ed", + "StartboardPart-LogsDashboardPart-9badbd78-7607-4131-8fa1-8b85191432ef", + "StartboardPart-LogsDashboardPart-9badbd78-7607-4131-8fa1-8b85191432f1", + "StartboardPart-LogsDashboardPart-9badbd78-7607-4131-8fa1-8b85191432f3", + "StartboardPart-LogsDashboardPart-9badbd78-7607-4131-8fa1-8b85191432f5", + "StartboardPart-LogsDashboardPart-9badbd78-7607-4131-8fa1-8b85191432f7", + "StartboardPart-LogsDashboardPart-9badbd78-7607-4131-8fa1-8b85191432f9", + "StartboardPart-LogsDashboardPart-9badbd78-7607-4131-8fa1-8b85191432fb", + "StartboardPart-LogsDashboardPart-9badbd78-7607-4131-8fa1-8b85191432fd" + ] + } + } + } + } + } +} + PROPS + + tags = var.tags +} + + +resource "azurerm_monitor_scheduled_query_rules_alert" "alarm_availability_0" { + name = replace(join("_",split("/", "${local.name}-availability @ /api/v1/services/{service_id}")), "/\\{|\\}/", "") + resource_group_name = data.azurerm_resource_group.this.name + location = data.azurerm_resource_group.this.location + + action { + action_group = ["/subscriptions/uuid/resourceGroups/my-rg/providers/microsoft.insights/actionGroups/my-action-group-email", "/subscriptions/uuid/resourceGroups/my-rg/providers/microsoft.insights/actionGroups/my-action-group-slack"] + } + + data_source_id = "data_source_id" + description = "Availability for /api/v1/services/{service_id} is less than or equal to 99% - ${local.dashboard_base_addr}${azurerm_portal_dashboard.this.id}" + enabled = true + auto_mitigation_enabled = false + + query = <<-QUERY + + +let api_hosts = datatable (name: string) ["app-backend.io.italia.it"]; +let threshold = 0.99; +AzureDiagnostics +| where originalHost_s in (api_hosts) +| where requestUri_s matches regex "/api/v1/services/[^/]+$" +| summarize + Total=count(), + Success=count(httpStatus_d < 500) by bin(TimeGenerated, 5m) +| extend availability=toreal(Success) / Total +| where availability < threshold + + + QUERY + + severity = 1 + frequency = 10 + time_window = 20 + trigger { + operator = "GreaterThanOrEqual" + threshold = 2 + } + + tags = var.tags +} + +resource "azurerm_monitor_scheduled_query_rules_alert" "alarm_time_0" { + name = replace(join("_",split("/", "${local.name}-responsetime @ /api/v1/services/{service_id}")), "/\\{|\\}/", "") + resource_group_name = data.azurerm_resource_group.this.name + location = data.azurerm_resource_group.this.location + + action { + action_group = ["/subscriptions/uuid/resourceGroups/my-rg/providers/microsoft.insights/actionGroups/my-action-group-email", "/subscriptions/uuid/resourceGroups/my-rg/providers/microsoft.insights/actionGroups/my-action-group-slack"] + } + + data_source_id = "data_source_id" + description = "Response time for /api/v1/services/{service_id} is less than or equal to 1s - ${local.dashboard_base_addr}${azurerm_portal_dashboard.this.id}" + enabled = true + auto_mitigation_enabled = false + + query = <<-QUERY + + +let api_hosts = datatable (name: string) ["app-backend.io.italia.it"]; +let threshold = 1; +AzureDiagnostics +| where originalHost_s in (api_hosts) +| where requestUri_s matches regex "/api/v1/services/[^/]+$" +| summarize + watermark=threshold, + duration_percentile_95=percentiles(timeTaken_d, 95) by bin(TimeGenerated, 5m) +| where duration_percentile_95 > threshold + + + QUERY + + severity = 1 + frequency = 10 + time_window = 20 + trigger { + operator = "GreaterThanOrEqual" + threshold = 2 + } + + tags = var.tags +} + +resource "azurerm_monitor_scheduled_query_rules_alert" "alarm_availability_1" { + name = replace(join("_",split("/", "${local.name}-availability @ /api/v1/services")), "/\\{|\\}/", "") + resource_group_name = data.azurerm_resource_group.this.name + location = data.azurerm_resource_group.this.location + + action { + action_group = ["/subscriptions/uuid/resourceGroups/my-rg/providers/microsoft.insights/actionGroups/my-action-group-email", "/subscriptions/uuid/resourceGroups/my-rg/providers/microsoft.insights/actionGroups/my-action-group-slack"] + } + + data_source_id = "data_source_id" + description = "Availability for /api/v1/services is less than or equal to 99% - ${local.dashboard_base_addr}${azurerm_portal_dashboard.this.id}" + enabled = true + auto_mitigation_enabled = false + + query = <<-QUERY + + +let api_hosts = datatable (name: string) ["app-backend.io.italia.it"]; +let threshold = 0.99; +AzureDiagnostics +| where originalHost_s in (api_hosts) +| where requestUri_s matches regex "/api/v1/services$" +| summarize + Total=count(), + Success=count(httpStatus_d < 500) by bin(TimeGenerated, 5m) +| extend availability=toreal(Success) / Total +| where availability < threshold + + + QUERY + + severity = 1 + frequency = 10 + time_window = 20 + trigger { + operator = "GreaterThanOrEqual" + threshold = 2 + } + + tags = var.tags +} + +resource "azurerm_monitor_scheduled_query_rules_alert" "alarm_time_1" { + name = replace(join("_",split("/", "${local.name}-responsetime @ /api/v1/services")), "/\\{|\\}/", "") + resource_group_name = data.azurerm_resource_group.this.name + location = data.azurerm_resource_group.this.location + + action { + action_group = ["/subscriptions/uuid/resourceGroups/my-rg/providers/microsoft.insights/actionGroups/my-action-group-email", "/subscriptions/uuid/resourceGroups/my-rg/providers/microsoft.insights/actionGroups/my-action-group-slack"] + } + + data_source_id = "data_source_id" + description = "Response time for /api/v1/services is less than or equal to 1s - ${local.dashboard_base_addr}${azurerm_portal_dashboard.this.id}" + enabled = true + auto_mitigation_enabled = false + + query = <<-QUERY + + +let api_hosts = datatable (name: string) ["app-backend.io.italia.it"]; +let threshold = 1; +AzureDiagnostics +| where originalHost_s in (api_hosts) +| where requestUri_s matches regex "/api/v1/services$" +| summarize + watermark=threshold, + duration_percentile_95=percentiles(timeTaken_d, 95) by bin(TimeGenerated, 5m) +| where duration_percentile_95 > threshold + + + QUERY + + severity = 1 + frequency = 10 + time_window = 20 + trigger { + operator = "GreaterThanOrEqual" + threshold = 2 + } + + tags = var.tags +} + diff --git a/packages/opex-dashboard-ts/test/fixtures/legacy.tf.txt b/packages/opex-dashboard-ts/test/fixtures/legacy.tf.txt new file mode 100644 index 00000000..856358a1 --- /dev/null +++ b/packages/opex-dashboard-ts/test/fixtures/legacy.tf.txt @@ -0,0 +1,1028 @@ + +locals { + name = "${var.prefix}-${var.env_short}-My_Dashboard" + dashboard_base_addr = "https://portal.azure.com/#@pagopait.onmicrosoft.com/dashboard/arm" +} + +data "azurerm_resource_group" "this" { + name = "dashboards" +} + +resource "azurerm_portal_dashboard" "this" { + name = local.name + resource_group_name = data.azurerm_resource_group.this.name + location = data.azurerm_resource_group.this.location + + dashboard_properties = <<-PROPS + { + "lenses": { + "0": { + "order": 0, + "parts": { + "0": { + "position": { + "x": 0, + "y": 0, + "colSpan": 6, + "rowSpan": 4 + }, + "metadata": { + "inputs": [ + { + "name": "resourceTypeMode", + "isOptional": true + }, + { + "name": "ComponentId", + "isOptional": true + }, + { + "name": "Scope", + "value": { + "resourceIds": [ + "/subscriptions/uuid/resourceGroups/my-rg/providers/Microsoft.Network/applicationGateways/my-gtw" + ] + }, + "isOptional": true + }, + { + "name": "PartId", + "isOptional": true + }, + { + "name": "Version", + "value": "2.0", + "isOptional": true + }, + { + "name": "TimeRange", + "value": "PT4H", + "isOptional": true + }, + { + "name": "DashboardId", + "isOptional": true + }, + { + "name": "DraftRequestParameters", + "value": { + "scope": "hierarchy" + }, + "isOptional": true + }, + { + "name": "Query", + "value": "\nlet api_hosts = datatable (name: string) [\"app-backend.io.italia.it\"];\nlet threshold = 0.99;\nAzureDiagnostics\n| where originalHost_s in (api_hosts)\n| where requestUri_s matches regex \"/api/v1/services/[^/]+$\"\n| summarize\n Total=count(),\n Success=count(httpStatus_d < 500) by bin(TimeGenerated, 5m)\n| extend availability=toreal(Success) / Total\n| project TimeGenerated, availability, watermark=threshold\n| render timechart with (xtitle = \"time\", ytitle= \"availability(%)\")\n", + "isOptional": true + }, + { + "name": "ControlType", + "value": "FrameControlChart", + "isOptional": true + }, + { + "name": "SpecificChart", + "value": "Line", + "isOptional": true + }, + { + "name": "PartTitle", + "value": "Availability (5m)", + "isOptional": true + }, + { + "name": "PartSubTitle", + "value": "/api/v1/services/{service_id}", + "isOptional": true + }, + { + "name": "Dimensions", + "value": { + "xAxis": { + "name": "TimeGenerated", + "type": "datetime" + }, + "yAxis": [ + { + "name": "availability", + "type": "real" + }, + { + "name": "watermark", + "type": "real" + } + ], + "splitBy": [], + "aggregation": "Sum" + }, + "isOptional": true + }, + { + "name": "LegendOptions", + "value": { + "isEnabled": true, + "position": "Bottom" + }, + "isOptional": true + }, + { + "name": "IsQueryContainTimeRange", + "value": false, + "isOptional": true + } + ], + "type": "Extension/Microsoft_OperationsManagementSuite_Workspace/PartType/LogsDashboardPart", + "settings": { + "content": { + "Query": "\nlet api_hosts = datatable (name: string) [\"app-backend.io.italia.it\"];\nlet threshold = 0.99;\nAzureDiagnostics\n| where originalHost_s in (api_hosts)\n| where requestUri_s matches regex \"/api/v1/services/[^/]+$\"\n| summarize\n Total=count(),\n Success=count(httpStatus_d < 500) by bin(TimeGenerated, 5m)\n| extend availability=toreal(Success) / Total\n| project TimeGenerated, availability, watermark=threshold\n| render timechart with (xtitle = \"time\", ytitle= \"availability(%)\")\n", + "PartTitle": "Availability (5m)" + } + } + } + }, + "1": { + "position": { + "x": 6, + "y": 0, + "colSpan": 6, + "rowSpan": 4 + }, + "metadata": { + "inputs": [ + { + "name": "resourceTypeMode", + "isOptional": true + }, + { + "name": "ComponentId", + "isOptional": true + }, + { + "name": "Scope", + "value": { + "resourceIds": [ + "/subscriptions/uuid/resourceGroups/my-rg/providers/Microsoft.Network/applicationGateways/my-gtw" + ] + }, + "isOptional": true + }, + { + "name": "PartId", + "isOptional": true + }, + { + "name": "Version", + "value": "2.0", + "isOptional": true + }, + { + "name": "TimeRange", + "value": "PT4H", + "isOptional": true + }, + { + "name": "DashboardId", + "isOptional": true + }, + { + "name": "DraftRequestParameters", + "value": { + "scope": "hierarchy" + }, + "isOptional": true + }, + { + "name": "Query", + "value": "\nlet api_url = \"/api/v1/services/[^/]+$\";\nlet api_hosts = datatable (name: string) [\"app-backend.io.italia.it\"];\nAzureDiagnostics\n| where originalHost_s in (api_hosts)\n| where requestUri_s matches regex api_url\n| extend HTTPStatus = case(\n httpStatus_d between (100 .. 199), \"1XX\",\n httpStatus_d between (200 .. 299), \"2XX\",\n httpStatus_d between (300 .. 399), \"3XX\",\n httpStatus_d between (400 .. 499), \"4XX\",\n \"5XX\")\n| summarize count() by HTTPStatus, bin(TimeGenerated, 5m)\n| render areachart with (xtitle = \"time\", ytitle= \"count\")\n", + "isOptional": true + }, + { + "name": "ControlType", + "value": "FrameControlChart", + "isOptional": true + }, + { + "name": "SpecificChart", + "value": "Pie", + "isOptional": true + }, + { + "name": "PartTitle", + "value": "Response Codes (5m)", + "isOptional": true + }, + { + "name": "PartSubTitle", + "value": "/api/v1/services/{service_id}", + "isOptional": true + }, + { + "name": "Dimensions", + "value": { + "xAxis": { + "name": "httpStatus_d", + "type": "string" + }, + "yAxis": [ + { + "name": "count_", + "type": "long" + } + ], + "splitBy": [], + "aggregation": "Sum" + }, + "isOptional": true + }, + { + "name": "LegendOptions", + "value": { + "isEnabled": true, + "position": "Bottom" + }, + "isOptional": true + }, + { + "name": "IsQueryContainTimeRange", + "value": false, + "isOptional": true + } + ], + "type": "Extension/Microsoft_OperationsManagementSuite_Workspace/PartType/LogsDashboardPart", + "settings": { + "content": { + "Query": "\nlet api_url = \"/api/v1/services/[^/]+$\";\nlet api_hosts = datatable (name: string) [\"app-backend.io.italia.it\"];\nAzureDiagnostics\n| where originalHost_s in (api_hosts)\n| where requestUri_s matches regex api_url\n| extend HTTPStatus = case(\n httpStatus_d between (100 .. 199), \"1XX\",\n httpStatus_d between (200 .. 299), \"2XX\",\n httpStatus_d between (300 .. 399), \"3XX\",\n httpStatus_d between (400 .. 499), \"4XX\",\n \"5XX\")\n| summarize count() by HTTPStatus, bin(TimeGenerated, 5m)\n| render areachart with (xtitle = \"time\", ytitle= \"count\")\n", + "SpecificChart": "StackedArea", + "PartTitle": "Response Codes (5m)", + "Dimensions": { + "xAxis": { + "name": "TimeGenerated", + "type": "datetime" + }, + "yAxis": [ + { + "name": "count_", + "type": "long" + } + ], + "splitBy": [ + { + "name": "HTTPStatus", + "type": "string" + } + ], + "aggregation": "Sum" + } + } + } + } + }, + "2": { + "position": { + "x": 12, + "y": 0, + "colSpan": 6, + "rowSpan": 4 + }, + "metadata": { + "inputs": [ + { + "name": "resourceTypeMode", + "isOptional": true + }, + { + "name": "ComponentId", + "isOptional": true + }, + { + "name": "Scope", + "value": { + "resourceIds": [ + "/subscriptions/uuid/resourceGroups/my-rg/providers/Microsoft.Network/applicationGateways/my-gtw" + ] + }, + "isOptional": true + }, + { + "name": "PartId", + "isOptional": true + }, + { + "name": "Version", + "value": "2.0", + "isOptional": true + }, + { + "name": "TimeRange", + "value": "PT4H", + "isOptional": true + }, + { + "name": "DashboardId", + "isOptional": true + }, + { + "name": "DraftRequestParameters", + "value": { + "scope": "hierarchy" + }, + "isOptional": true + }, + { + "name": "Query", + "value": "\nlet api_hosts = datatable (name: string) [\"app-backend.io.italia.it\"];\nlet threshold = 1;\nAzureDiagnostics\n| where originalHost_s in (api_hosts)\n| where requestUri_s matches regex \"/api/v1/services/[^/]+$\"\n| summarize\n watermark=threshold,\n duration_percentile_95=percentiles(timeTaken_d, 95) by bin(TimeGenerated, 5m)\n| render timechart with (xtitle = \"time\", ytitle= \"response time(s)\")\n", + "isOptional": true + }, + { + "name": "ControlType", + "value": "FrameControlChart", + "isOptional": true + }, + { + "name": "SpecificChart", + "value": "StackedColumn", + "isOptional": true + }, + { + "name": "PartTitle", + "value": "Percentile Response Time (5m)", + "isOptional": true + }, + { + "name": "PartSubTitle", + "value": "/api/v1/services/{service_id}", + "isOptional": true + }, + { + "name": "Dimensions", + "value": { + "xAxis": { + "name": "TimeGenerated", + "type": "datetime" + }, + "yAxis": [ + { + "name": "duration_percentile_95", + "type": "real" + } + ], + "splitBy": [], + "aggregation": "Sum" + }, + "isOptional": true + }, + { + "name": "LegendOptions", + "value": { + "isEnabled": true, + "position": "Bottom" + }, + "isOptional": true + }, + { + "name": "IsQueryContainTimeRange", + "value": false, + "isOptional": true + } + ], + "type": "Extension/Microsoft_OperationsManagementSuite_Workspace/PartType/LogsDashboardPart", + "settings": { + "content": { + "Query": "\nlet api_hosts = datatable (name: string) [\"app-backend.io.italia.it\"];\nlet threshold = 1;\nAzureDiagnostics\n| where originalHost_s in (api_hosts)\n| where requestUri_s matches regex \"/api/v1/services/[^/]+$\"\n| summarize\n watermark=threshold,\n duration_percentile_95=percentiles(timeTaken_d, 95) by bin(TimeGenerated, 5m)\n| render timechart with (xtitle = \"time\", ytitle= \"response time(s)\")\n", + "SpecificChart": "Line", + "PartTitle": "Percentile Response Time (5m)", + "Dimensions": { + "xAxis": { + "name": "TimeGenerated", + "type": "datetime" + }, + "yAxis": [ + { + "name": "watermark", + "type": "long" + }, + { + "name": "duration_percentile_95", + "type": "real" + } + ], + "splitBy": [], + "aggregation": "Sum" + } + } + } + } + }, + "3": { + "position": { + "x": 0, + "y": 4, + "colSpan": 6, + "rowSpan": 4 + }, + "metadata": { + "inputs": [ + { + "name": "resourceTypeMode", + "isOptional": true + }, + { + "name": "ComponentId", + "isOptional": true + }, + { + "name": "Scope", + "value": { + "resourceIds": [ + "/subscriptions/uuid/resourceGroups/my-rg/providers/Microsoft.Network/applicationGateways/my-gtw" + ] + }, + "isOptional": true + }, + { + "name": "PartId", + "isOptional": true + }, + { + "name": "Version", + "value": "2.0", + "isOptional": true + }, + { + "name": "TimeRange", + "value": "PT4H", + "isOptional": true + }, + { + "name": "DashboardId", + "isOptional": true + }, + { + "name": "DraftRequestParameters", + "value": { + "scope": "hierarchy" + }, + "isOptional": true + }, + { + "name": "Query", + "value": "\nlet api_hosts = datatable (name: string) [\"app-backend.io.italia.it\"];\nlet threshold = 0.99;\nAzureDiagnostics\n| where originalHost_s in (api_hosts)\n| where requestUri_s matches regex \"/api/v1/services$\"\n| summarize\n Total=count(),\n Success=count(httpStatus_d < 500) by bin(TimeGenerated, 5m)\n| extend availability=toreal(Success) / Total\n| project TimeGenerated, availability, watermark=threshold\n| render timechart with (xtitle = \"time\", ytitle= \"availability(%)\")\n", + "isOptional": true + }, + { + "name": "ControlType", + "value": "FrameControlChart", + "isOptional": true + }, + { + "name": "SpecificChart", + "value": "Line", + "isOptional": true + }, + { + "name": "PartTitle", + "value": "Availability (5m)", + "isOptional": true + }, + { + "name": "PartSubTitle", + "value": "/api/v1/services", + "isOptional": true + }, + { + "name": "Dimensions", + "value": { + "xAxis": { + "name": "TimeGenerated", + "type": "datetime" + }, + "yAxis": [ + { + "name": "availability", + "type": "real" + }, + { + "name": "watermark", + "type": "real" + } + ], + "splitBy": [], + "aggregation": "Sum" + }, + "isOptional": true + }, + { + "name": "LegendOptions", + "value": { + "isEnabled": true, + "position": "Bottom" + }, + "isOptional": true + }, + { + "name": "IsQueryContainTimeRange", + "value": false, + "isOptional": true + } + ], + "type": "Extension/Microsoft_OperationsManagementSuite_Workspace/PartType/LogsDashboardPart", + "settings": { + "content": { + "Query": "\nlet api_hosts = datatable (name: string) [\"app-backend.io.italia.it\"];\nlet threshold = 0.99;\nAzureDiagnostics\n| where originalHost_s in (api_hosts)\n| where requestUri_s matches regex \"/api/v1/services$\"\n| summarize\n Total=count(),\n Success=count(httpStatus_d < 500) by bin(TimeGenerated, 5m)\n| extend availability=toreal(Success) / Total\n| project TimeGenerated, availability, watermark=threshold\n| render timechart with (xtitle = \"time\", ytitle= \"availability(%)\")\n", + "PartTitle": "Availability (5m)" + } + } + } + }, + "4": { + "position": { + "x": 6, + "y": 4, + "colSpan": 6, + "rowSpan": 4 + }, + "metadata": { + "inputs": [ + { + "name": "resourceTypeMode", + "isOptional": true + }, + { + "name": "ComponentId", + "isOptional": true + }, + { + "name": "Scope", + "value": { + "resourceIds": [ + "/subscriptions/uuid/resourceGroups/my-rg/providers/Microsoft.Network/applicationGateways/my-gtw" + ] + }, + "isOptional": true + }, + { + "name": "PartId", + "isOptional": true + }, + { + "name": "Version", + "value": "2.0", + "isOptional": true + }, + { + "name": "TimeRange", + "value": "PT4H", + "isOptional": true + }, + { + "name": "DashboardId", + "isOptional": true + }, + { + "name": "DraftRequestParameters", + "value": { + "scope": "hierarchy" + }, + "isOptional": true + }, + { + "name": "Query", + "value": "\nlet api_url = \"/api/v1/services$\";\nlet api_hosts = datatable (name: string) [\"app-backend.io.italia.it\"];\nAzureDiagnostics\n| where originalHost_s in (api_hosts)\n| where requestUri_s matches regex api_url\n| extend HTTPStatus = case(\n httpStatus_d between (100 .. 199), \"1XX\",\n httpStatus_d between (200 .. 299), \"2XX\",\n httpStatus_d between (300 .. 399), \"3XX\",\n httpStatus_d between (400 .. 499), \"4XX\",\n \"5XX\")\n| summarize count() by HTTPStatus, bin(TimeGenerated, 5m)\n| render areachart with (xtitle = \"time\", ytitle= \"count\")\n", + "isOptional": true + }, + { + "name": "ControlType", + "value": "FrameControlChart", + "isOptional": true + }, + { + "name": "SpecificChart", + "value": "Pie", + "isOptional": true + }, + { + "name": "PartTitle", + "value": "Response Codes (5m)", + "isOptional": true + }, + { + "name": "PartSubTitle", + "value": "/api/v1/services", + "isOptional": true + }, + { + "name": "Dimensions", + "value": { + "xAxis": { + "name": "httpStatus_d", + "type": "string" + }, + "yAxis": [ + { + "name": "count_", + "type": "long" + } + ], + "splitBy": [], + "aggregation": "Sum" + }, + "isOptional": true + }, + { + "name": "LegendOptions", + "value": { + "isEnabled": true, + "position": "Bottom" + }, + "isOptional": true + }, + { + "name": "IsQueryContainTimeRange", + "value": false, + "isOptional": true + } + ], + "type": "Extension/Microsoft_OperationsManagementSuite_Workspace/PartType/LogsDashboardPart", + "settings": { + "content": { + "Query": "\nlet api_url = \"/api/v1/services$\";\nlet api_hosts = datatable (name: string) [\"app-backend.io.italia.it\"];\nAzureDiagnostics\n| where originalHost_s in (api_hosts)\n| where requestUri_s matches regex api_url\n| extend HTTPStatus = case(\n httpStatus_d between (100 .. 199), \"1XX\",\n httpStatus_d between (200 .. 299), \"2XX\",\n httpStatus_d between (300 .. 399), \"3XX\",\n httpStatus_d between (400 .. 499), \"4XX\",\n \"5XX\")\n| summarize count() by HTTPStatus, bin(TimeGenerated, 5m)\n| render areachart with (xtitle = \"time\", ytitle= \"count\")\n", + "SpecificChart": "StackedArea", + "PartTitle": "Response Codes (5m)", + "Dimensions": { + "xAxis": { + "name": "TimeGenerated", + "type": "datetime" + }, + "yAxis": [ + { + "name": "count_", + "type": "long" + } + ], + "splitBy": [ + { + "name": "HTTPStatus", + "type": "string" + } + ], + "aggregation": "Sum" + } + } + } + } + }, + "5": { + "position": { + "x": 12, + "y": 4, + "colSpan": 6, + "rowSpan": 4 + }, + "metadata": { + "inputs": [ + { + "name": "resourceTypeMode", + "isOptional": true + }, + { + "name": "ComponentId", + "isOptional": true + }, + { + "name": "Scope", + "value": { + "resourceIds": [ + "/subscriptions/uuid/resourceGroups/my-rg/providers/Microsoft.Network/applicationGateways/my-gtw" + ] + }, + "isOptional": true + }, + { + "name": "PartId", + "isOptional": true + }, + { + "name": "Version", + "value": "2.0", + "isOptional": true + }, + { + "name": "TimeRange", + "value": "PT4H", + "isOptional": true + }, + { + "name": "DashboardId", + "isOptional": true + }, + { + "name": "DraftRequestParameters", + "value": { + "scope": "hierarchy" + }, + "isOptional": true + }, + { + "name": "Query", + "value": "\nlet api_hosts = datatable (name: string) [\"app-backend.io.italia.it\"];\nlet threshold = 1;\nAzureDiagnostics\n| where originalHost_s in (api_hosts)\n| where requestUri_s matches regex \"/api/v1/services$\"\n| summarize\n watermark=threshold,\n duration_percentile_95=percentiles(timeTaken_d, 95) by bin(TimeGenerated, 5m)\n| render timechart with (xtitle = \"time\", ytitle= \"response time(s)\")\n", + "isOptional": true + }, + { + "name": "ControlType", + "value": "FrameControlChart", + "isOptional": true + }, + { + "name": "SpecificChart", + "value": "StackedColumn", + "isOptional": true + }, + { + "name": "PartTitle", + "value": "Percentile Response Time (5m)", + "isOptional": true + }, + { + "name": "PartSubTitle", + "value": "/api/v1/services", + "isOptional": true + }, + { + "name": "Dimensions", + "value": { + "xAxis": { + "name": "TimeGenerated", + "type": "datetime" + }, + "yAxis": [ + { + "name": "duration_percentile_95", + "type": "real" + } + ], + "splitBy": [], + "aggregation": "Sum" + }, + "isOptional": true + }, + { + "name": "LegendOptions", + "value": { + "isEnabled": true, + "position": "Bottom" + }, + "isOptional": true + }, + { + "name": "IsQueryContainTimeRange", + "value": false, + "isOptional": true + } + ], + "type": "Extension/Microsoft_OperationsManagementSuite_Workspace/PartType/LogsDashboardPart", + "settings": { + "content": { + "Query": "\nlet api_hosts = datatable (name: string) [\"app-backend.io.italia.it\"];\nlet threshold = 1;\nAzureDiagnostics\n| where originalHost_s in (api_hosts)\n| where requestUri_s matches regex \"/api/v1/services$\"\n| summarize\n watermark=threshold,\n duration_percentile_95=percentiles(timeTaken_d, 95) by bin(TimeGenerated, 5m)\n| render timechart with (xtitle = \"time\", ytitle= \"response time(s)\")\n", + "SpecificChart": "Line", + "PartTitle": "Percentile Response Time (5m)", + "Dimensions": { + "xAxis": { + "name": "TimeGenerated", + "type": "datetime" + }, + "yAxis": [ + { + "name": "watermark", + "type": "long" + }, + { + "name": "duration_percentile_95", + "type": "real" + } + ], + "splitBy": [], + "aggregation": "Sum" + } + } + } + } + } + } + } + }, + "metadata": { + "model": { + "timeRange": { + "value": { + "relative": { + "duration": 24, + "timeUnit": 1 + } + }, + "type": "MsPortalFx.Composition.Configuration.ValueTypes.TimeRange" + }, + "filterLocale": { + "value": "en-us" + }, + "filters": { + "value": { + "MsPortalFx_TimeRange": { + "model": { + "format": "local", + "granularity": "auto", + "relative": "48h" + }, + "displayCache": { + "name": "Local Time", + "value": "Past 48 hours" + }, + "filteredPartIds": [ + "StartboardPart-LogsDashboardPart-9badbd78-7607-4131-8fa1-8b85191432ed", + "StartboardPart-LogsDashboardPart-9badbd78-7607-4131-8fa1-8b85191432ef", + "StartboardPart-LogsDashboardPart-9badbd78-7607-4131-8fa1-8b85191432f1", + "StartboardPart-LogsDashboardPart-9badbd78-7607-4131-8fa1-8b85191432f3", + "StartboardPart-LogsDashboardPart-9badbd78-7607-4131-8fa1-8b85191432f5", + "StartboardPart-LogsDashboardPart-9badbd78-7607-4131-8fa1-8b85191432f7", + "StartboardPart-LogsDashboardPart-9badbd78-7607-4131-8fa1-8b85191432f9", + "StartboardPart-LogsDashboardPart-9badbd78-7607-4131-8fa1-8b85191432fb", + "StartboardPart-LogsDashboardPart-9badbd78-7607-4131-8fa1-8b85191432fd" + ] + } + } + } + } + } +} + PROPS + + tags = var.tags +} + + +resource "azurerm_monitor_scheduled_query_rules_alert" "alarm_availability_0" { + name = replace(join("_", split("/", "${local.name}-availability @ /api/v1/services/{service_id}")), "/\\{|\\}/", "") + resource_group_name = data.azurerm_resource_group.this.name + location = data.azurerm_resource_group.this.location + + action { + action_group = ["/subscriptions/uuid/resourceGroups/my-rg/providers/microsoft.insights/actionGroups/my-action-group-email", "/subscriptions/uuid/resourceGroups/my-rg/providers/microsoft.insights/actionGroups/my-action-group-slack"] + } + + data_source_id = "/subscriptions/uuid/resourceGroups/my-rg/providers/Microsoft.Network/applicationGateways/my-gtw" + description = "Availability for /api/v1/services/{service_id} is less than or equal to 99% - ${local.dashboard_base_addr}${azurerm_portal_dashboard.this.id}" + enabled = true + auto_mitigation_enabled = false + + query = <<-QUERY + + +let api_hosts = datatable (name: string) ["app-backend.io.italia.it"]; +let threshold = 0.99; +AzureDiagnostics +| where originalHost_s in (api_hosts) +| where requestUri_s matches regex "/api/v1/services/[^/]+$" +| summarize + Total=count(), + Success=count(httpStatus_d < 500) by bin(TimeGenerated, 5m) +| extend availability=toreal(Success) / Total +| where availability < threshold + + + QUERY + + severity = 1 + frequency = 10 + time_window = 20 + trigger { + operator = "GreaterThanOrEqual" + threshold = 1 + } + + tags = var.tags +} + +resource "azurerm_monitor_scheduled_query_rules_alert" "alarm_time_0" { + name = replace(join("_", split("/", "${local.name}-responsetime @ /api/v1/services/{service_id}")), "/\\{|\\}/", "") + resource_group_name = data.azurerm_resource_group.this.name + location = data.azurerm_resource_group.this.location + + action { + action_group = ["/subscriptions/uuid/resourceGroups/my-rg/providers/microsoft.insights/actionGroups/my-action-group-email", "/subscriptions/uuid/resourceGroups/my-rg/providers/microsoft.insights/actionGroups/my-action-group-slack"] + } + + data_source_id = "/subscriptions/uuid/resourceGroups/my-rg/providers/Microsoft.Network/applicationGateways/my-gtw" + description = "Response time for /api/v1/services/{service_id} is less than or equal to 1s - ${local.dashboard_base_addr}${azurerm_portal_dashboard.this.id}" + enabled = true + auto_mitigation_enabled = false + + query = <<-QUERY + + +let api_hosts = datatable (name: string) ["app-backend.io.italia.it"]; +let threshold = 1; +AzureDiagnostics +| where originalHost_s in (api_hosts) +| where requestUri_s matches regex "/api/v1/services/[^/]+$" +| summarize + watermark=threshold, + duration_percentile_95=percentiles(timeTaken_d, 95) by bin(TimeGenerated, 5m) +| where duration_percentile_95 > threshold + + + QUERY + + severity = 1 + frequency = 10 + time_window = 20 + trigger { + operator = "GreaterThanOrEqual" + threshold = 1 + } + + tags = var.tags +} + +resource "azurerm_monitor_scheduled_query_rules_alert" "alarm_availability_1" { + name = replace(join("_", split("/", "${local.name}-availability @ /api/v1/services")), "/\\{|\\}/", "") + resource_group_name = data.azurerm_resource_group.this.name + location = data.azurerm_resource_group.this.location + + action { + action_group = ["/subscriptions/uuid/resourceGroups/my-rg/providers/microsoft.insights/actionGroups/my-action-group-email", "/subscriptions/uuid/resourceGroups/my-rg/providers/microsoft.insights/actionGroups/my-action-group-slack"] + } + + data_source_id = "/subscriptions/uuid/resourceGroups/my-rg/providers/Microsoft.Network/applicationGateways/my-gtw" + description = "Availability for /api/v1/services is less than or equal to 99% - ${local.dashboard_base_addr}${azurerm_portal_dashboard.this.id}" + enabled = true + auto_mitigation_enabled = false + + query = <<-QUERY + + +let api_hosts = datatable (name: string) ["app-backend.io.italia.it"]; +let threshold = 0.99; +AzureDiagnostics +| where originalHost_s in (api_hosts) +| where requestUri_s matches regex "/api/v1/services$" +| summarize + Total=count(), + Success=count(httpStatus_d < 500) by bin(TimeGenerated, 5m) +| extend availability=toreal(Success) / Total +| where availability < threshold + + + QUERY + + severity = 1 + frequency = 10 + time_window = 20 + trigger { + operator = "GreaterThanOrEqual" + threshold = 1 + } + + tags = var.tags +} + +resource "azurerm_monitor_scheduled_query_rules_alert" "alarm_time_1" { + name = replace(join("_", split("/", "${local.name}-responsetime @ /api/v1/services")), "/\\{|\\}/", "") + resource_group_name = data.azurerm_resource_group.this.name + location = data.azurerm_resource_group.this.location + + action { + action_group = ["/subscriptions/uuid/resourceGroups/my-rg/providers/microsoft.insights/actionGroups/my-action-group-email", "/subscriptions/uuid/resourceGroups/my-rg/providers/microsoft.insights/actionGroups/my-action-group-slack"] + } + + data_source_id = "/subscriptions/uuid/resourceGroups/my-rg/providers/Microsoft.Network/applicationGateways/my-gtw" + description = "Response time for /api/v1/services is less than or equal to 1s - ${local.dashboard_base_addr}${azurerm_portal_dashboard.this.id}" + enabled = true + auto_mitigation_enabled = false + + query = <<-QUERY + + +let api_hosts = datatable (name: string) ["app-backend.io.italia.it"]; +let threshold = 1; +AzureDiagnostics +| where originalHost_s in (api_hosts) +| where requestUri_s matches regex "/api/v1/services$" +| summarize + watermark=threshold, + duration_percentile_95=percentiles(timeTaken_d, 95) by bin(TimeGenerated, 5m) +| where duration_percentile_95 > threshold + + + QUERY + + severity = 1 + frequency = 10 + time_window = 20 + trigger { + operator = "GreaterThanOrEqual" + threshold = 1 + } + + tags = var.tags +} + + diff --git a/packages/opex-dashboard-ts/test/fixtures/test_openapi.yaml b/packages/opex-dashboard-ts/test/fixtures/test_openapi.yaml new file mode 100644 index 00000000..1ebae78f --- /dev/null +++ b/packages/opex-dashboard-ts/test/fixtures/test_openapi.yaml @@ -0,0 +1,1533 @@ +swagger: "2.0" +info: + version: 1.0.0 + title: Proxy API + description: Mobile and web proxy API gateway. +host: app-backend.io.italia.it +basePath: /api/v1 +schemes: + - https +security: + - Bearer: [] +paths: + "/services/{service_id}": + x-swagger-router-controller: ServicesController + parameters: + - name: service_id + in: path + type: string + required: true + description: The ID of an existing Service. + get: + operationId: getService + summary: Get Service + description: A previously created service with the provided service ID is returned. + responses: + "200": + description: Service found. + schema: + "$ref": "#/definitions/ServicePublic" + examples: + application/json: + department_name: "IO" + organization_fiscal_code: "00000000000" + organization_name: "IO" + service_id: "5a563817fcc896087002ea46c49a" + service_name: "App IO" + version: 1 + "400": + description: Bad request + schema: + $ref: "#/definitions/ProblemJson" + "401": + description: Bearer token null or expired. + "404": + description: No service found for the provided ID. + schema: + $ref: "#/definitions/ProblemJson" + "429": + description: Too many requests + schema: + $ref: "#/definitions/ProblemJson" + "500": + description: There was an error in retrieving the service. + schema: + $ref: "#/definitions/ProblemJson" + parameters: [] + "/services/{service_id}/preferences": + post: + operationId: upsertServicePreferences + summary: UpsertServicePreferences + parameters: + - name: service_id + in: path + type: string + required: true + description: The ID of an existing Service. + - in: body + name: body + schema: + $ref: "#/definitions/UpsertServicePreference" + responses: + "200": + description: Service Preference found. + schema: + "$ref": "#/definitions/ServicePreference" + examples: + application/json: + can_access_message_read_status: true + is_inbox_enabled: true + is_email_enabled: false + is_webhook_enabled: true + settings_version: 1 + "400": + description: Bad request + schema: + $ref: "#/definitions/ProblemJson" + "401": + description: Unauthorized + "404": + description: No service found for the provided ID. + "409": + description: |- + Conflict. Either the provided preference setting version is not consistent with the current version stored in the Profile + or the Profile is not in the correct preference mode. + "429": + description: Too many requests + "500": + description: Internal Server Error + get: + operationId: getServicePreferences + summary: GetServicePreferences + parameters: + - name: service_id + in: path + type: string + required: true + description: The ID of an existing Service. + responses: + "200": + description: Service Preference found. + schema: + "$ref": "#/definitions/ServicePreference" + examples: + application/json: + can_access_message_read_status: true + is_inbox_enabled: true + is_email_enabled: false + is_webhook_enabled: true + settings_version: 1 + "400": + description: Bad request + schema: + $ref: "#/definitions/ProblemJson" + "401": + description: Unauthorized + "404": + description: No service found for the provided ID. + "409": + description: Conflict. The Profile is not in the correct preference mode. + "429": + description: Too many requests + "500": + description: Internal Server Error + "/services": + x-swagger-router-controller: ServicesController + get: + operationId: getVisibleServices + summary: Get all visible services + description: |- + Returns the description of all visible services. + responses: + "200": + description: Found. + schema: + $ref: "#/definitions/PaginatedServiceTupleCollection" + examples: + application/json: + items: + - service_id: "AzureDeployc49a" + version: 1 + - service_id: "5a25abf4fcc89605c082f042c49a" + version: 0 + page_size: 1 + "401": + description: Bearer token null or expired. + "429": + description: Too many requests + schema: + $ref: "#/definitions/ProblemJson" + "500": + description: There was an error in retrieving the services. + schema: + $ref: "#/definitions/ProblemJson" + parameters: + - $ref: "#/parameters/PaginationRequest" + "/messages": + x-swagger-router-controller: MessagesController + parameters: + - $ref: "#/parameters/PageSize" + - $ref: "#/parameters/EnrichResultData" + - $ref: "#/parameters/GetArchivedMessages" + - $ref: "#/parameters/MaximumId" + - $ref: "#/parameters/MinimumId" + get: + operationId: getUserMessages + summary: Get user's messages + description: |- + Returns the messages for the user identified by the provided fiscal code. + Messages will be returned in inverse acceptance order (from last to first). + The "next" field, when present, contains an URL pointing to the next page of results. + responses: + "200": + description: Found. + schema: + $ref: "#/definitions/PaginatedPublicMessagesCollection" + examples: + application/json: + items: + - created_at: "2018-05-21T07:36:41.209Z" + fiscal_code: "LSSLCU79B24L219P" + id: "01CE0T1Z18T3NT9ECK5NJ09YR3" + sender_service_id: "5a563817fcc896087002ea46c49a" + time_to_live: 3600 + - created_at: "2018-05-21T07:41:01.361Z" + fiscal_code: "LSSLCU79B24L219P" + id: "01CE0T9X1HT595GEF8FH9NRSW7" + sender_service_id: "5a563817fcc896087002ea46c49a" + time_to_live: 3600 + next: 01CE0T9X1HT595GEF8FH9NRSW7 + "400": + description: Bad request + schema: + $ref: "#/definitions/ProblemJson" + "401": + description: Bearer token null or expired. + "404": + description: No message found. + schema: + $ref: "#/definitions/ProblemJson" + "429": + description: Too many requests + schema: + $ref: "#/definitions/ProblemJson" + "500": + description: There was an error in retrieving the messages. + schema: + $ref: "#/definitions/ProblemJson" + "/messages/{id}": + x-swagger-router-controller: MessagesController + parameters: + - name: id + in: path + type: string + required: true + description: The ID of the message. + - $ref: "#/parameters/PublicMessage" + get: + operationId: getUserMessage + summary: Get message + description: |- + Returns the message with the provided message ID. + responses: + "200": + description: Found. + schema: + $ref: "#/definitions/CreatedMessageWithContentAndAttachments" + examples: + application/json: | + content: { + markdown: "hey hey !! some content here ..... this is a link with a style applied, some other content", + subject: "my subject ............", + attachments: [{name:"attachment", content:"aBase64Encoding", mime_type: "image/png"}] + }, + created_at: "2018-06-06T12:22:24.523Z", + fiscal_code: "LSSLCU79B24L219P", + id: "01CFAGRMGB9XCA8B2CQ4QA7K76", + sender_service_id: "5a25abf4fcc89605c082f042c49a", + time_to_live: 3600 + "400": + description: Bad request + schema: + $ref: "#/definitions/ProblemJson" + "401": + description: Bearer token null or expired. + "404": + description: No message found for the provided ID. + schema: + $ref: "#/definitions/ProblemJson" + "429": + description: Too many requests + schema: + $ref: "#/definitions/ProblemJson" + "500": + description: There was an error in retrieving the message. + schema: + $ref: "#/definitions/ProblemJson" + "/messages/{id}/message-status": + x-swagger-router-controller: MessagesController + put: + operationId: upsertMessageStatusAttributes + summary: UpsertMessageStatusAttributes + description: Updates the status of a message with attributes + parameters: + - name: id + in: path + type: string + required: true + description: The ID of the message. + - name: body + in: body + schema: + $ref: "#/definitions/MessageStatusChange" + required: true + x-examples: + application/json: | + change_type: "bulk", + is_archived: true, + is_read: true + responses: + "200": + description: Success. + schema: + $ref: "#/definitions/MessageStatusAttributes" + examples: + application/json: + is_read: true, + is_archived: false + "400": + description: Bad request + schema: + $ref: "#/definitions/ProblemJson" + "401": + description: Bearer token null or expired. + "403": + description: Operation forbidden. + "404": + description: No message found for the provided ID. + schema: + $ref: "#/definitions/ProblemJson" + "429": + description: Too many requests + schema: + $ref: "#/definitions/ProblemJson" + "500": + description: There was an error in upserting message's status attributes. + schema: + $ref: "#/definitions/ProblemJson" + "/legal-messages/{id}": + x-swagger-router-controller: MessagesController + parameters: + - name: id + in: path + type: string + required: true + description: The ID of the message. + get: + operationId: getUserLegalMessage + summary: Get legal message + description: |- + Returns the legal message with the provided message ID. + responses: + "200": + description: Found. + schema: + $ref: "#/definitions/LegalMessageWithContent" + examples: + application/json: | + content: { + markdown: "hey hey !! some content here ..... this is a link with a style applied, some other content", + subject: "my subject ............", + attachments: [{name:"attachment", content:"aBase64Encoding", mime_type: "image/png"}] + }, + created_at: "2018-06-06T12:22:24.523Z", + fiscal_code: "LSSLCU79B24L219P", + id: "01CFAGRMGB9XCA8B2CQ4QA7K76", + sender_service_id: "5a25abf4fcc89605c082f042c49a", + time_to_live: 3600 + "400": + description: Bad request + schema: + $ref: "#/definitions/ProblemJson" + "401": + description: Bearer token null or expired. + "404": + description: No message found for the provided ID. + schema: + $ref: "#/definitions/ProblemJson" + "429": + description: Too many requests + schema: + $ref: "#/definitions/ProblemJson" + "500": + description: There was an error in retrieving the message. + schema: + $ref: "#/definitions/ProblemJson" + "/legal-messages/{id}/attachments/{attachment_id}": + x-swagger-router-controller: MessagesController + get: + operationId: getLegalMessageAttachment + summary: Retrieve an attachment of a legal message + produces: + - application/octet-stream + parameters: + - name: id + in: path + type: string + required: true + description: The ID of the message. + - in: path + name: attachment_id + required: true + type: string + responses: + "200": + description: Success + schema: + format: binary + type: string + "400": + description: Bad Request + "401": + description: Unauthorized + "403": + description: Forbidden + "429": + description: Too Many Requests + "500": + description: Internal Server Error + "/third-party-messages/{id}": + x-swagger-router-controller: MessagesController + get: + operationId: getThirdPartyMessage + summary: Retrieve Third Party message + description: |- + Returns the Third Party message with the provided message ID. + parameters: + - name: id + in: path + type: string + minLength: 1 + required: true + description: ID of the IO message. + responses: + "200": + description: Found. + schema: + $ref: "#/definitions/ThirdPartyMessageWithContent" + examples: + text/json: | + content: { + markdown: "hey hey !! some content here ..... this is a link with a style applied, some other content", + subject: "my subject ............", + third_party_data: [{id: "aThirdPartyMessageId"}] + }, + third_party_message: { + attachments: [], + custom_property: "a custom property" + }, + created_at: "2018-06-06T12:22:24.523Z", + fiscal_code: "LSSLCU79B24L219P", + id: "01CFAGRMGB9XCA8B2CQ4QA7K76", + sender_service_id: "5a25abf4fcc89605c082f042c49a", + time_to_live: 3600 + "400": + description: Bad request + schema: + $ref: "#/definitions/ProblemJson" + "401": + description: Bearer token null or expired. + "403": + description: Forbidden + "404": + description: No message found for the provided ID. + schema: + $ref: "#/definitions/ProblemJson" + "410": + description: Third Party Service no longer available + "429": + description: Too many requests + schema: + $ref: "#/definitions/ProblemJson" + "500": + description: There was an error in retrieving the message. + schema: + $ref: "#/definitions/ProblemJson" + "501": + description: Not Implemented + "504": + description: Gateway Timeout + "/third-party-messages/{id}/attachments/{attachment_url}": + x-swagger-router-controller: MessagesController + get: + operationId: getThirdPartyMessageAttachment + summary: Retrieve an attachment of a Thrid Party message + produces: + - application/octet-stream + parameters: + - name: id + in: path + type: string + minLength: 1 + required: true + description: ID of the IO message. + - in: path + name: attachment_url + type: string + minLength: 1 + required: true + responses: + "200": + description: Success + schema: + format: binary + type: string + "400": + description: Bad Request + "401": + description: Unauthorized + "403": + description: Forbidden + "404": + description: No message found for the provided ID. + schema: + $ref: "#/definitions/ProblemJson" + "410": + description: Third Party Service no longer available + "429": + description: Too Many Requests + "500": + description: Internal Server Error + "501": + description: Not Implemented + "504": + description: Gateway Timeout + "/profile": + x-swagger-router-controller: ProfileController + get: + operationId: getUserProfile + summary: Get user's profile + description: Returns the profile for the user identified by the provided fiscal code. + responses: + "200": + description: Found. + schema: + $ref: "#/definitions/InitializedProfile" + examples: + application/json: + email: "email@example.com" + family_name: "Rossi" + fiscal_code: "TMMEXQ60A10Y526X" + has_profile: true + is_email_set: true + is_inbox_enabled: true + is_webhook_enabled: true + name: "Mario" + spid_email: "preferred@example.com" + service_preferences_settings: + - mode: LEGACY + version: 1 + "400": + description: Bad request + schema: + $ref: "#/definitions/ProblemJson" + "401": + description: Bearer token null or expired. + "429": + description: Too many requests + schema: + $ref: "#/definitions/ProblemJson" + "500": + description: There was an error in retrieving the user profile. + schema: + $ref: "#/definitions/ProblemJson" + post: + operationId: updateProfile + summary: Update the User's profile + description: Update the profile for the user identified by the provided fiscal code. + parameters: + - in: body + name: body + schema: + $ref: "#/definitions/Profile" + required: true + x-examples: + application/json: + email: foobar@example.com + preferred_languages: [it_IT] + is_inbox_enabled: true + is_webhook_enabled: false + version: 1 + responses: + "200": + description: Profile updated. + schema: + $ref: "#/definitions/InitializedProfile" + examples: + application/json: + email: "email@example.com" + family_name: "Rossi" + fiscal_code: "TMMEXQ60A10Y526X" + has_profile: true + is_email_set: true + is_inbox_enabled: true + is_webhook_enabled: true + name: "Mario" + spid_email: "preferred@example.com" + version: 0 + "400": + description: Invalid payload. + schema: + $ref: "#/definitions/ProblemJson" + "401": + description: Bearer token null or expired. + "404": + description: User not found + schema: + $ref: "#/definitions/ProblemJson" + "409": + description: Conflict. + schema: + $ref: "#/definitions/ProblemJson" + "429": + description: Too many requests + schema: + $ref: "#/definitions/ProblemJson" + "500": + description: Profile cannot be updated. + schema: + $ref: "#/definitions/ProblemJson" + "/api-profile": + x-swagger-router-controller: ProfileController + get: + operationId: getApiUserProfile + summary: Get user's profile stored into the API + description: Returns the profile for the user identified by the provided fiscal code. + responses: + "200": + description: Found. + schema: + $ref: "#/definitions/ExtendedProfile" + examples: + application/json: + email: "email@example.com" + preferred_languages: ["it_IT"] + is_inbox_enabled: true + accepted_tos_version: 1 + is_webhook_enabled: true + is_email_enabled: true + version: 1 + sender_allowed: true + "400": + description: Bad request + schema: + $ref: "#/definitions/ProblemJson" + "401": + description: Bearer token null or expired. + "404": + description: Profile not found + schema: + $ref: "#/definitions/ProblemJson" + "429": + description: Too many requests + schema: + $ref: "#/definitions/ProblemJson" + "500": + description: There was an error in retrieving the user profile. + schema: + $ref: "#/definitions/ProblemJson" + "/email-validation-process": + x-swagger-router-controller: ProfileController + post: + operationId: startEmailValidationProcess + summary: Start the Email Validation Process + description: |- + Start the email validation process that create the validation token + and send the validation email + responses: + "202": + description: Accepted + "400": + description: Bad request + schema: + $ref: "#/definitions/ProblemJson" + "401": + description: Bearer token null or expired. + "404": + description: Profile not found + schema: + $ref: "#/definitions/ProblemJson" + "429": + description: Too many requests + schema: + $ref: "#/definitions/ProblemJson" + "500": + description: There was an error starting email validation process + schema: + $ref: "#/definitions/ProblemJson" + "/user-metadata": + x-swagger-router-controller: userMetadataController + get: + operationId: getUserMetadata + summary: Get user's metadata + description: Returns metadata for the current authenticated user. + responses: + "200": + description: Found. + schema: + $ref: "#/definitions/UserMetadata" + "204": + description: No Content. + "401": + description: Bearer token null or expired. + "500": + description: There was an error in retrieving the user metadata. + schema: + $ref: "#/definitions/ProblemJson" + post: + operationId: upsertUserMetadata + summary: Set User's metadata + description: Create or update metadata for the current authenticated user. + parameters: + - in: body + name: body + schema: + $ref: "#/definitions/UserMetadata" + required: true + responses: + "200": + description: User Metadata updated. + schema: + $ref: "#/definitions/UserMetadata" + "400": + description: Invalid payload. + schema: + $ref: "#/definitions/ProblemJson" + "401": + description: Bearer token null or expired. + "409": + description: Conflict. + schema: + $ref: "#/definitions/ProblemJson" + "500": + description: Profile cannot be updated. + schema: + $ref: "#/definitions/ProblemJson" + "/installations/{installationID}": + x-swagger-router-controller: NotificationController + parameters: + - name: installationID + in: path + required: true + description: The ID of the message. + type: string + put: + operationId: createOrUpdateInstallation + summary: Create or update an Installation + description: Create or update an Installation to the Azure Notification hub. + parameters: + - name: body + in: body + schema: + $ref: "#/definitions/Installation" + required: true + x-examples: + application/json: + platform: "gcm" + pushChannel: "fLKP3EATnBI:APA91bEy4go681jeSEpLkNqhtIrdPnEKu6Dfi-STtUiEnQn8RwMfBiPGYaqdWrmzJyXIh5Yms4017MYRS9O1LGPZwA4sOLCNIoKl4Fwg7cSeOkliAAtlQ0rVg71Kr5QmQiLlDJyxcq3p" + responses: + "200": + description: Success. + schema: + $ref: "#/definitions/SuccessResponse" + examples: + application/json: + "message": "ok" + "401": + description: Bearer token null or expired. + "500": + description: There was an error in registering the device to the Notification Hub. + schema: + $ref: "#/definitions/ProblemJson" + "/session": + x-swagger-router-controller: AuthenticationController + get: + operationId: getSessionState + summary: Get the user current session + description: Return the session state for the current authenticated user. + responses: + "200": + description: Found. + schema: + $ref: "#/definitions/PublicSession" + examples: + application/json: + spidLevel: "https://www.spid.gov.it/SpidL2" + walletToken: "c77de47586c841adbd1a1caeb90dce25dcecebed620488a4f932a6280b10ee99a77b6c494a8a6e6884ccbeb6d3fe736b" + "400": + description: Bad request + schema: + $ref: "#/definitions/ProblemJson" + "401": + description: Bearer token null or expired. + "500": + description: Internal server error + schema: + $ref: "#/definitions/ProblemJson" + "/sessions": + x-swagger-router-controller: AuthenticationController + get: + operationId: listUserSessions + summary: List sessions of a User + description: Return all the active sessions for an authenticated User. + responses: + "200": + description: Found. + schema: + $ref: "#/definitions/SessionsList" + "400": + description: Bad request + schema: + $ref: "#/definitions/ProblemJson" + "401": + description: Bearer token null or expired. + "500": + description: Unavailable service + schema: + $ref: "#/definitions/ProblemJson" + "/token/support": + x-swagger-router-controller: SupportController + get: + operationId: getSupportToken + summary: Get a JWT Support Token + description: Return a JWT Support Token for the authenticated user. + responses: + "200": + description: Created. + schema: + $ref: "#/definitions/SupportToken" + "400": + description: Bad request + schema: + $ref: "#/definitions/ProblemJson" + "401": + description: Bearer token null or expired. + "500": + description: Unavailable service + schema: + $ref: "#/definitions/ProblemJson" + + "/payment-requests/{rptId}": + x-swagger-router-controller: PagoPAProxyController + parameters: + - name: rptId + in: path + required: true + description: Unique identifier for payments. + type: string + - name: test + in: query + description: Use test environment of PagoPAClient + type: boolean + required: false + get: + operationId: getPaymentInfo + summary: Get Payment Info + description: Retrieve information about a payment + responses: + "200": + description: Payment information retrieved + schema: + "$ref": "#/definitions/PaymentRequestsGetResponse" + examples: + application/json: + importoSingoloVersamento: 200, + codiceContestoPagamento: "ABC123" + "400": + description: Bad request + schema: + $ref: "#/definitions/ProblemJson" + "401": + description: Bearer token null or expired. + "500": + description: PagoPA services are not available or request is rejected + schema: + $ref: "#/definitions/PaymentProblemJson" + "504": + description: gateway timeout. + "/payment-activations": + x-swagger-router-controller: PagoPAProxyController + parameters: + - name: test + in: query + description: Use test environment of PagoPAClient + type: boolean + required: false + post: + operationId: activatePayment + summary: Activate Payment + description: Require a lock (activation) for a payment + parameters: + - in: body + name: body + schema: + $ref: "#/definitions/PaymentActivationsPostRequest" + required: true + x-examples: + application/json: + rptId: "12345678901012123456789012345" + importoSingoloVersamento: 200 + codiceContestoPagamento: "ABC123" + responses: + "200": + description: Payment activation process started + schema: + "$ref": "#/definitions/PaymentActivationsPostResponse" + examples: + application/json: + importoSingoloVersamento: 200 + "400": + description: Bad request + schema: + $ref: "#/definitions/ProblemJson" + "401": + description: Bearer token null or expired. + "500": + description: PagoPA services are not available or request is rejected + schema: + $ref: "#/definitions/PaymentProblemJson" + "504": + description: gateway timeout. + "/payment-activations/{codiceContestoPagamento}": + x-swagger-router-controller: PagoPAProxyController + parameters: + - name: codiceContestoPagamento + in: path + required: true + description: Transaction Id used to identify the communication flow. + type: string + - name: test + in: query + description: Use test environment of PagoPAClient + type: boolean + required: false + get: + operationId: getActivationStatus + summary: Get Activation status + description: Check the activation status to retrieve the paymentId + responses: + "200": + description: Payment information + schema: + $ref: "#/definitions/PaymentActivationsGetResponse" + examples: + application/json: + idPagamento: "123455" + "400": + description: Invalid input + schema: + $ref: "#/definitions/ProblemJson" + "401": + description: Bearer token null or expired. + "404": + description: Activation status not found + schema: + $ref: "#/definitions/ProblemJson" + "500": + description: Unavailable service + schema: + $ref: "#/definitions/ProblemJson" + "504": + description: gateway timeout. + "/user-data-processing": + x-swagger-router-controller: UserDataProcessingController + post: + operationId: upsertUserDataProcessing + summary: Set User's data processing choices + description: Let the authenticated user express his will to retrieve or delete his stored data. + parameters: + - in: body + name: body + schema: + $ref: "#/definitions/UserDataProcessingChoiceRequest" + required: true + responses: + "200": + description: User Data processing created / updated. + schema: + $ref: "#/definitions/UserDataProcessing" + "400": + description: Invalid payload. + schema: + $ref: "#/definitions/ProblemJson" + "401": + description: Bearer token null or expired. + "409": + description: Conflict. + schema: + $ref: "#/definitions/ProblemJson" + "429": + description: Too may requests + "500": + description: User Data processing choice cannot be taken in charge. + schema: + $ref: "#/definitions/ProblemJson" + "/user-data-processing/{choice}": + x-swagger-router-controller: UserDataProcessingController + get: + operationId: getUserDataProcessing + summary: Get User's data processing + description: Get the user's request to delete or download his stored data by providing a kind of choice. + parameters: + - $ref: "#/parameters/UserDataProcessingChoiceParam" + responses: + "200": + description: User data processing retrieved + schema: + $ref: "#/definitions/UserDataProcessing" + "401": + description: Bearer token null or expired. + "404": + description: Not found. + schema: + $ref: "#/definitions/ProblemJson" + "429": + description: Too many requests + delete: + operationId: abortUserDataProcessing + summary: Abort User's revious data processing request + description: |- + Ask for a request to abort, if present + tags: + - restricted + parameters: + - $ref: "#/parameters/UserDataProcessingChoiceParam" + responses: + "202": + description: The abort request has been recorded + "400": + description: Invalid request. + schema: + $ref: "#/definitions/ProblemJson" + "401": + description: Unauthorized + "404": + description: Not Found + "409": + description: Conflict + schema: + $ref: "#/definitions/ProblemJson" + "429": + description: Too many requests + "500": + description: Server Error + schema: + $ref: "#/definitions/ProblemJson" +definitions: + # Definitions from the digital citizenship APIs + AcceptedTosVersion: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/AcceptedTosVersion" + AppVersion: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/AppVersion" + BlockedInboxOrChannels: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/BlockedInboxOrChannels" + DepartmentName: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/DepartmentName" + EmailAddress: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/EmailAddress" + PreferredLanguage: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/PreferredLanguage" + PreferredLanguages: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/PreferredLanguages" + Profile: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/Profile" + ExtendedProfile: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/ExtendedProfile" + FiscalCode: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/FiscalCode" + IsEmailEnabled: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/IsEmailEnabled" + IsInboxEnabled: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/IsInboxEnabled" + IsEmailValidated: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/IsEmailValidated" + IsTestProfile: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/IsTestProfile" + IsWebhookEnabled: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/IsWebhookEnabled" + LimitedProfile: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/LimitedProfile" + MessageBodyMarkdown: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/MessageBodyMarkdown" + MessageContent: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/MessageContent" + MessageResponseNotificationStatus: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/MessageResponseNotificationStatus" + NotificationChannelStatusValue: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/NotificationChannelStatusValue" + NotificationChannel: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/NotificationChannel" + MessageSubject: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/MessageSubject" + MessageContentBase: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/MessageContentBase" + EUCovidCert: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/EUCovidCert" + OrganizationFiscalCode: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/OrganizationFiscalCode" + NewMessageContent: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/NewMessageContent" + Payee: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/Payee" + PaymentDataBase: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/PaymentDataBase" + PaymentDataWithRequiredPayee: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/PaymentDataWithRequiredPayee" + OrganizationName: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/OrganizationName" + PaginationResponse: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/PaginationResponse" + PrescriptionData: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/PrescriptionData" + ProblemJson: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/ProblemJson" + ServiceId: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/ServiceId" + ServiceName: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/ServiceName" + ServicePublic: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/ServicePublic" + ServiceMetadata: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/ServiceMetadata" + CommonServiceMetadata: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/CommonServiceMetadata" + StandardServiceMetadata: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/StandardServiceMetadata" + SpecialServiceMetadata: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/SpecialServiceMetadata" + ServiceTuple: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/ServiceTuple" + ServiceScope: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/ServiceScope" + ServiceCategory: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/ServiceCategory" + SpecialServiceCategory: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/SpecialServiceCategory" + StandardServiceCategory: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/StandardServiceCategory" + PaginatedServiceTupleCollection: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/PaginatedServiceTupleCollection" + Timestamp: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/Timestamp" + PaymentNoticeNumber: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/PaymentNoticeNumber" + PaymentAmount: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/PaymentAmount" + PaymentData: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/PaymentData" + TimeToLiveSeconds: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/TimeToLiveSeconds" + CreatedMessageWithContent: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/CreatedMessageWithContent" + CreatedMessageWithoutContent: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/CreatedMessageWithoutContent" + CreatedMessageWithoutContentCollection: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/CreatedMessageWithoutContentCollection" + PaginatedCreatedMessageWithoutContentCollection: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/PaginatedCreatedMessageWithoutContentCollection" + UserDataProcessingStatus: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/UserDataProcessingStatus" + UserDataProcessingChoice: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/UserDataProcessingChoice" + UserDataProcessingChoiceRequest: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/UserDataProcessingChoiceRequest" + UserDataProcessing: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/UserDataProcessing" + MessageResponseWithContent: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/MessageResponseWithContent" + ServicePreferencesSettings: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/ServicePreferencesSettings" + ServicesPreferencesMode: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/ServicesPreferencesMode" + BasicServicePreference: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/BasicServicePreference" + ServicePreference: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/ServicePreference" + UpsertServicePreference: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/UpsertServicePreference" + EnrichedMessage: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/EnrichedMessage" + PublicMessage: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/PublicMessage" + PublicMessagesCollection: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/PublicMessagesCollection" + PaginatedPublicMessagesCollection: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/PaginatedPublicMessagesCollection" + MessageCategory: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/MessageCategory" + MessageCategoryBase: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/MessageCategoryBase" + MessageCategoryPayment: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/MessageCategoryPayment" + LegalMessageWithContent: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/LegalMessageWithContent" + MessageCategoryPN: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/MessageCategoryPN" + LegalMessage: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/LegalMessage" + + ThirdPartyData: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/ThirdPartyData" + ThirdPartyMessageWithContent: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/ThirdPartyMessageWithContent" + ThirdPartyMessage: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/ThirdPartyMessage" + ThirdPartyAttachment: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/ThirdPartyAttachment" + + LegalMessageEml: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/LegalMessageEml" + LegalMessageCertData: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/LegalMessageCertData" + CertData: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/CertData" + CertDataHeader: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/CertDataHeader" + Attachment: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/Attachment" + LegalData: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/LegalData" + MessageStatusAttributes: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/MessageStatusAttributes" + MessageStatusReadingChange: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/MessageStatusReadingChange" + MessageStatusArchivingChange: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/MessageStatusArchivingChange" + MessageStatusBulkChange: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/MessageStatusBulkChange" + MessageStatusChange: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/MessageStatusChange" + CreatedMessageWithContentResponse: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/CreatedMessageWithContentResponse" + CreatedMessageWithContentAndEnrichedData: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/CreatedMessageWithContentAndEnrichedData" + # Definitions from pagopa-proxy + PaymentProblemJson: + $ref: "https://raw.githubusercontent.com/pagopa/io-pagopa-proxy/v0.20.0/api_pagopa.yaml#/definitions/PaymentProblemJson" + CodiceContestoPagamento: + $ref: "https://raw.githubusercontent.com/pagopa/io-pagopa-proxy/v0.20.0/api_pagopa.yaml#/definitions/CodiceContestoPagamento" + EnteBeneficiario: + $ref: "https://raw.githubusercontent.com/pagopa/io-pagopa-proxy/v0.20.0/api_pagopa.yaml#/definitions/EnteBeneficiario" + Iban: + $ref: "https://raw.githubusercontent.com/pagopa/io-pagopa-proxy/v0.20.0/api_pagopa.yaml#/definitions/Iban" + ImportoEuroCents: + $ref: "https://raw.githubusercontent.com/pagopa/io-pagopa-proxy/v0.20.0/api_pagopa.yaml#/definitions/ImportoEuroCents" + PaymentActivationsGetResponse: + $ref: "https://raw.githubusercontent.com/pagopa/io-pagopa-proxy/v0.20.0/api_pagopa.yaml#/definitions/PaymentActivationsGetResponse" + PaymentActivationsPostRequest: + $ref: "https://raw.githubusercontent.com/pagopa/io-pagopa-proxy/v0.20.0/api_pagopa.yaml#/definitions/PaymentActivationsPostRequest" + PaymentActivationsPostResponse: + $ref: "https://raw.githubusercontent.com/pagopa/io-pagopa-proxy/v0.20.0/api_pagopa.yaml#/definitions/PaymentActivationsPostResponse" + PaymentRequestsGetResponse: + $ref: "https://raw.githubusercontent.com/pagopa/io-pagopa-proxy/v0.20.0/api_pagopa.yaml#/definitions/PaymentRequestsGetResponse" + RptId: + $ref: "https://raw.githubusercontent.com/pagopa/io-pagopa-proxy/v0.20.0/api_pagopa.yaml#/definitions/RptId" + SpezzoneStrutturatoCausaleVersamento: + $ref: "https://raw.githubusercontent.com/pagopa/io-pagopa-proxy/v0.20.0/api_pagopa.yaml#/definitions/SpezzoneStrutturatoCausaleVersamento" + SpezzoniCausaleVersamento: + $ref: "https://raw.githubusercontent.com/pagopa/io-pagopa-proxy/v0.20.0/api_pagopa.yaml#/definitions/SpezzoniCausaleVersamento" + SpezzoniCausaleVersamentoItem: + $ref: "https://raw.githubusercontent.com/pagopa/io-pagopa-proxy/v0.20.0/api_pagopa.yaml#/definitions/SpezzoniCausaleVersamentoItem" + MessageContentWithAttachments: + allOf: + - type: object + properties: + attachments: + type: array + items: + $ref: "#/definitions/MessageAttachment" + - $ref: "#/definitions/NewMessageContent" + MessageAttachment: + type: object + title: MessageAttachment + description: Describes a message's attachment + properties: + name: + type: string + content: + type: string + mime_type: + type: string + required: + - name + - content + - mime_type + CreatedMessageWithContentAndAttachments: + allOf: + - type: object + properties: + content: + $ref: "#/definitions/MessageContentWithAttachments" + required: + - content + - $ref: "#/definitions/CreatedMessageWithContentResponse" + Installation: + type: object + title: Installation + description: Describes an app installation. + properties: + platform: + $ref: "#/definitions/Platform" + pushChannel: + $ref: "#/definitions/PushChannel" + required: + - platform + - pushChannel + InitializedProfile: + type: object + title: Initialized profile + description: Describes the user's profile after it has been stored in the Profile API. + properties: + accepted_tos_version: + $ref: "#/definitions/AcceptedTosVersion" + email: + $ref: "#/definitions/EmailAddress" + blocked_inbox_or_channels: + $ref: "#/definitions/BlockedInboxOrChannels" + preferred_languages: + $ref: "#/definitions/PreferredLanguages" + is_inbox_enabled: + $ref: "#/definitions/IsInboxEnabled" + is_email_validated: + $ref: "#/definitions/IsEmailValidated" + is_email_enabled: + $ref: "#/definitions/IsEmailEnabled" + is_webhook_enabled: + $ref: "#/definitions/IsWebhookEnabled" + family_name: + type: string + fiscal_code: + $ref: "#/definitions/FiscalCode" + has_profile: + $ref: "#/definitions/HasProfile" + last_app_version: + $ref: "#/definitions/AppVersion" + name: + type: string + spid_email: + $ref: "#/definitions/EmailAddress" + date_of_birth: + type: string + format: date + service_preferences_settings: + $ref: "#/definitions/ServicePreferencesSettings" + version: + $ref: "#/definitions/Version" + required: + - family_name + - fiscal_code + - has_profile + - is_inbox_enabled + - is_email_enabled + - is_webhook_enabled + - name + - service_preferences_settings + - version + UserMetadata: + type: object + title: User Metadata information + properties: + version: + type: number + metadata: + type: string + required: + - version + - metadata + PublicSession: + type: object + title: User session data + description: Describe the current session of an authenticated user. + properties: + spidLevel: + $ref: "#/definitions/SpidLevel" + walletToken: + type: string + myPortalToken: + type: string + bpdToken: + type: string + zendeskToken: + type: string + fimsToken: + type: string + required: + - spidLevel + - walletToken + - myPortalToken + - bpdToken + - zendeskToken + - fimsToken + SessionInfo: + type: object + title: Session info of a user + description: Decribe a session of an authenticated user. + properties: + createdAt: + $ref: "#/definitions/Timestamp" + sessionToken: + type: string + required: + - createdAt + - sessionToken + SessionsList: + description: Contains all active sessions for an authenticated user. + type: object + properties: + sessions: + type: array + items: + $ref: "#/definitions/SessionInfo" + required: + - sessions + InstallationID: + type: string + description: |- + The sixteen octets of an Installation ID are represented as 32 hexadecimal (base 16) digits, displayed in five groups + separated by hyphens, in the form 8-4-4-4-12 for a total of 36 characters (32 alphanumeric characters and four + hyphens). + See https://en.wikipedia.org/wiki/Universally_unique_identifier + minLength: 1 + HasProfile: + type: boolean + default: false + description: True if the user has a remote profile. + IsEmailSet: + type: boolean + default: false + description: True if the user has presonalized the email. + Version: + type: integer + description: The entity version. + Platform: + type: string + description: The platform type where the installation happened. + x-extensible-enum: + - apns + - gcm + PushChannel: + type: string + description: |- + The Push Notification Service handle for this Installation. + See https://docs.microsoft.com/en-us/azure/notification-hubs/notification-hubs-push-notification-registration-management + SpidLevel: + type: string + description: A SPID level. + x-extensible-enum: + - https://www.spid.gov.it/SpidL1 + - https://www.spid.gov.it/SpidL2 + - https://www.spid.gov.it/SpidL3 + SuccessResponse: + type: object + properties: + message: + type: string + LimitedFederatedUser: + title: Federated user + description: User data needed by federated applications. + type: object + properties: + fiscal_code: + $ref: "#/definitions/FiscalCode" + required: + - fiscal_code + FederatedUser: + title: Federated user + description: User data needed by federated applications. + allOf: + - type: object + properties: + name: + type: string + family_name: + type: string + required: + - name + - family_name + - $ref: "#/definitions/LimitedFederatedUser" + SupportToken: + title: Support token + description: A Support Token response + type: object + properties: + access_token: + type: string + expires_in: + type: number + required: + - access_token + - expires_in +responses: {} +parameters: + PublicMessage: + name: public_message + in: query + type: boolean + description: Discriminate when to return public message shape. Default to false. + EnrichResultData: + name: enrich_result_data + type: boolean + in: query + required: false + description: Indicates whether result data should be enriched or not. + GetArchivedMessages: + name: archived + type: boolean + in: query + required: false + description: Indicates whether retrieve archived/not archived messages. Default is false + PageSize: + name: page_size + type: integer + in: query + minimum: 1 + maximum: 100 + required: false + description: How many items a page should include. + MaximumId: + name: maximum_id + type: string + in: query + required: false + description: >- + The maximum id to get messages until to. + MinimumId: + name: minimum_id + type: string + in: query + required: false + description: >- + The minimum id to get messages from. + PaginationRequest: + type: string + name: cursor + in: query + minimum: 1 + description: An opaque identifier that points to the next item in the collection. + UserDataProcessingChoiceParam: + name: choice + in: path + type: string + enum: [DOWNLOAD, DELETE] + description: A representation of a user data processing choice + required: true + x-example: DOWNLOAD +consumes: + - application/json +produces: + - application/json +securityDefinitions: + Bearer: + type: apiKey + name: Authorization + in: header diff --git a/packages/opex-dashboard-ts/test/unit/cli.test.ts b/packages/opex-dashboard-ts/test/unit/cli.test.ts new file mode 100644 index 00000000..94aa5eb2 --- /dev/null +++ b/packages/opex-dashboard-ts/test/unit/cli.test.ts @@ -0,0 +1,125 @@ +import { generateCommand } from "../../src/infrastructure/cli/generate.js"; +import { ConfigValidatorAdapter } from "../../src/infrastructure/config/config-validator-adapter.js"; +import { describe, it, expect } from "vitest"; + +describe("CLI Commands", () => { + describe("generateCommand", () => { + it("should be a commander command instance", () => { + expect(generateCommand).toBeDefined(); + expect(generateCommand.name()).toBe("generate"); + }); + + it("should have correct description", () => { + expect(generateCommand.description()).toContain( + "Generate dashboard definition", + ); + }); + + it("should have required config-file option", () => { + const options = generateCommand.options; + const configOption = options.find((opt) => + opt.flags.includes("--config-file"), + ); + + expect(configOption).toBeDefined(); + expect(configOption?.flags).toContain("-c"); + expect(configOption?.flags).toContain("--config-file"); + expect(configOption?.required).toBe(true); + }); + + it("should not have template-name option", () => { + const options = generateCommand.options; + const templateOption = options.find((opt) => + opt.flags.includes("--template-name"), + ); + + expect(templateOption).toBeUndefined(); + }); + + it("should have an action configured", () => { + // Since we can't access private properties, we verify the command has the expected structure + expect(generateCommand).toHaveProperty("options"); + expect(Array.isArray(generateCommand.options)).toBe(true); + }); + }); + + describe("config validation", () => { + const configValidator = new ConfigValidatorAdapter(); + + it("should validate a valid config", () => { + const validConfig = { + oa3_spec: "https://example.com/spec.yaml", + name: "Test Dashboard", + location: "West Europe", + data_source: + "/subscriptions/uuid/resourceGroups/my-rg/providers/Microsoft.Network/applicationGateways/my-gtw", + resource_type: "app-gateway" as const, + timespan: "5m", + action_groups: [ + "/subscriptions/uuid/resourceGroups/my-rg/providers/microsoft.insights/actionGroups/my-action-group", + ], + }; + + expect(() => configValidator.validateConfig(validConfig)).not.toThrow(); + const result = configValidator.validateConfig(validConfig); + expect(result.oa3_spec).toBe("https://example.com/spec.yaml"); + expect(result.name).toBe("Test Dashboard"); + }); + + it("should throw error for missing required fields", () => { + const invalidConfig = { + name: "Test Dashboard", + location: "West Europe", + // missing oa3_spec and data_source + }; + + expect(() => configValidator.validateConfig(invalidConfig)).toThrow( + "Configuration validation failed:", + ); + expect(() => configValidator.validateConfig(invalidConfig)).toThrow( + "oa3_spec: Invalid input: expected string, received undefined", + ); + expect(() => configValidator.validateConfig(invalidConfig)).toThrow( + "data_source: Invalid input: expected string, received undefined", + ); + }); + + it("should apply defaults for optional fields", () => { + const configWithDefaults = { + oa3_spec: "https://example.com/spec.yaml", + name: "Test Dashboard", + location: "West Europe", + data_source: + "/subscriptions/uuid/resourceGroups/my-rg/providers/Microsoft.Network/applicationGateways/my-gtw", + }; + + const result = configValidator.validateConfig(configWithDefaults); + expect(result.resource_type).toBe("app-gateway"); // default value + expect(result.timespan).toBe("5m"); // default value + expect(result.resource_group_name).toBe("dashboards"); // default value + }); + }); + + describe("command validation", () => { + it("should accept valid template names", () => { + const validTemplates = ["azure-dashboard"]; + + validTemplates.forEach((template) => { + expect(() => { + // This would normally validate the template name in the action handler + // For testing purposes, we just check the command structure + }).not.toThrow(); + }); + }); + + it("should require config file to exist", () => { + // This test validates the conceptual requirement + // The actual file existence check happens in the action handler + expect( + generateCommand.options.some((opt) => + opt.flags.includes("--config-file"), + ), + ).toBe(true); + }); + }); +}); diff --git a/packages/opex-dashboard-ts/test/unit/dashboard-properties.test.ts b/packages/opex-dashboard-ts/test/unit/dashboard-properties.test.ts new file mode 100644 index 00000000..264576f9 --- /dev/null +++ b/packages/opex-dashboard-ts/test/unit/dashboard-properties.test.ts @@ -0,0 +1,50 @@ +import { describe, it, expect } from "vitest"; +import { buildDashboardPropertiesTemplate } from "../../src/infrastructure/terraform/dashboard-properties.js"; +import { DashboardConfig, Endpoint } from "../../src/domain/index.js"; + +/* + Snapshot-style test to ensure dashboard queries (availability & response time) contain + full time-series (render clause + watermark) and do NOT include alert-only filters. +*/ + +describe("dashboard properties template", () => { + const endpoint: Endpoint = { + path: "/api/v1/services/{serviceId}", + availability_threshold: 0.99, + response_time_threshold: 1, + } as Endpoint; + + const config: DashboardConfig = { + oa3_spec: "spec.yaml", + name: "Demo Dashboard", + location: "westeurope", + data_source: "workspace-id", + resource_type: "app-gateway", + timespan: "5m", + resource_group_name: "rg-demo", + resourceIds: [ + "/subscriptions/xxxx/resourceGroups/rg-demo/providers/Microsoft.Network/applicationGateways/gtw-demo", + ], + hosts: ["app-backend.io.italia.it"], + endpoints: [endpoint], + } as unknown as DashboardConfig; + + it("generates dashboard JSON with full-series availability & response time queries", () => { + const json = buildDashboardPropertiesTemplate(config); + + // Availability: should have render + watermark projection; no threshold filter line + expect(json).toContain("availability(%)"); + expect(json).toContain("watermark"); + expect(json).not.toContain("where availability < threshold"); + + // Response time: dashboard variant renders chart, not filtered by threshold + expect(json).toContain("response time (s)"); + expect(json).not.toContain("where duration_percentile_95 > threshold"); + + // Regex expansion check + expect(json).toContain("/api/v1/services/[^/]+$"); + // Subtitle normalization: camelCase placeholder should be snake_case + expect(json).toContain("{service_id}"); + expect(json).not.toContain("{serviceId}"); + }); +}); diff --git a/packages/opex-dashboard-ts/test/unit/endpoint-parser.test.ts b/packages/opex-dashboard-ts/test/unit/endpoint-parser.test.ts new file mode 100644 index 00000000..d3088191 --- /dev/null +++ b/packages/opex-dashboard-ts/test/unit/endpoint-parser.test.ts @@ -0,0 +1,82 @@ +import { EndpointParserService } from "../../src/domain/services/endpoint-parser-service.js"; +import { OpenAPISpec } from "../../src/shared/openapi.js"; +import { DashboardConfig } from "../../src/domain/entities/dashboard-config.js"; +import { describe, it, expect } from "vitest"; + +describe("parseEndpoints", () => { + const endpointParser = new EndpointParserService(); + const mockConfig: DashboardConfig = { + oa3_spec: "/path/to/spec.yaml", + name: "Test Dashboard", + location: "eastus", + data_source: "test-workspace", + endpoints: [], + }; + + describe("with simple OpenAPI 3.0 spec", () => { + const mockSpec: OpenAPISpec = { + openapi: "3.0.0", + info: { title: "Test API", version: "1.0.0" }, + servers: [{ url: "https://api.example.com" }], + paths: { + "/users": { get: {} } as any, + "/users/{id}": { get: {}, put: {}, delete: {} } as any, + "/posts/{postId}/comments": { get: {}, post: {} } as any, + }, + } as OpenAPISpec; + + it("should parse endpoints with server URL", () => { + const endpoints = endpointParser.parseEndpoints(mockSpec, mockConfig); + + expect(endpoints.length).toBe(3); + expect(endpoints.map((e) => e.path)).toEqual([ + "/users", + "/users/{id}", + "/posts/{postId}/comments", + ]); + }); + + it("should apply default configuration to endpoints", () => { + const endpoints = endpointParser.parseEndpoints(mockSpec, mockConfig); + + endpoints.forEach((endpoint) => { + expect(endpoint).toHaveProperty("path"); + expect(endpoint).toHaveProperty("availability_threshold", 0.99); // from defaults + expect(endpoint).toHaveProperty("response_time_threshold", 1); // from defaults + }); + }); + }); + + describe("with OpenAPI 2.0 spec without servers", () => { + const mockSpec: OpenAPISpec = { + swagger: "2.0", + info: { title: "Test API", version: "1.0.0" }, + host: "api.example.com", + basePath: "/v1", + paths: { + "/users": { get: {} } as any, + }, + } as OpenAPISpec; + + it("should use host and basePath", () => { + const endpoints = endpointParser.parseEndpoints(mockSpec, mockConfig); + + expect(endpoints.length).toBe(1); + expect(endpoints[0].path).toBe("/v1/users"); + }); + }); + + describe("with empty paths", () => { + const mockSpec: OpenAPISpec = { + openapi: "3.0.0", + info: { title: "Test API", version: "1.0.0" }, + servers: [{ url: "https://api.example.com" }], + paths: {}, + } as OpenAPISpec; + + it("should return empty array", () => { + const endpoints = endpointParser.parseEndpoints(mockSpec, mockConfig); + expect(endpoints).toEqual([]); + }); + }); +}); diff --git a/packages/opex-dashboard-ts/test/unit/kusto-queries.test.ts b/packages/opex-dashboard-ts/test/unit/kusto-queries.test.ts new file mode 100644 index 00000000..8fa3147d --- /dev/null +++ b/packages/opex-dashboard-ts/test/unit/kusto-queries.test.ts @@ -0,0 +1,207 @@ +import { describe, expect, it } from "vitest"; + +import { DashboardConfig } from "../../src/domain/entities/dashboard-config.js"; +import { Endpoint } from "../../src/domain/entities/endpoint.js"; +import { KustoQueryService } from "../../src/domain/services/kusto-query-service.js"; + +describe("Kusto Query Generation", () => { + const kustoQueryService = new KustoQueryService(); + const mockEndpoint: Endpoint = { + path: "/api/users", + availability_threshold: 0.99, + availability_evaluation_frequency: 10, + availability_evaluation_time_window: 20, + availability_event_occurrences: 1, + response_time_threshold: 1, + response_time_evaluation_frequency: 10, + response_time_evaluation_time_window: 20, + response_time_event_occurrences: 1, + }; + + const mockConfig: DashboardConfig = { + oa3_spec: "/path/to/spec.yaml", + name: "Test Dashboard", + location: "eastus", + data_source: "test-workspace", + resource_type: "app-gateway", + timespan: "5m", + hosts: ["api.example.com"], + endpoints: [], + }; + + describe("buildAvailabilityQuery", () => { + it("should generate correct availability query for app-gateway (alert)", () => { + const query = kustoQueryService.buildAvailabilityQuery( + mockEndpoint, + mockConfig, + "alert", + ); + + expect(query).toContain("AzureDiagnostics"); + expect(query).toContain("originalHost_s in (api_hosts)"); + expect(query).toContain( + 'let api_hosts = datatable (name: string) ["api.example.com"]', + ); + expect(query).toContain("requestUri_s matches regex"); + expect(query).toContain("httpStatus_d < 500"); + expect(query).toContain("availability=toreal(Success) / Total"); + expect(query).toContain("where availability < threshold"); + expect(query).toContain("let threshold = 0.99"); + }); + + it("should generate correct availability query for api-management (alert)", () => { + const apiConfig = { + ...mockConfig, + resource_type: "api-management" as const, + }; + const query = kustoQueryService.buildAvailabilityQuery( + mockEndpoint, + apiConfig, + "alert", + ); + + expect(query).toContain("url_s matches regex"); + expect(query).toContain("responseCode_d < 500"); + expect(query).not.toContain("originalHost_s"); + expect(query).not.toContain("api_hosts"); // No datatable for api-management + }); + + it("should include time window in query (alert)", () => { + const query = kustoQueryService.buildAvailabilityQuery( + mockEndpoint, + mockConfig, + "alert", + ); + expect(query).toContain("bin(TimeGenerated, 5m)"); + }); + + it("should include api_hosts datatable for app-gateway (alert)", () => { + const query = kustoQueryService.buildAvailabilityQuery( + mockEndpoint, + mockConfig, + "alert", + ); + + expect(query).toContain("let api_hosts = datatable (name: string)"); + expect(query).toContain("originalHost_s in (api_hosts)"); + }); + it("should NOT filter by threshold in dashboard availability query", () => { + const query = kustoQueryService.buildAvailabilityQuery( + mockEndpoint, + mockConfig, + "dashboard", + ); + expect(query).toContain("render timechart"); + expect(query).not.toContain("where availability < threshold"); + expect(query).toContain("watermark=threshold"); + }); + }); + + describe("buildResponseTimeQuery", () => { + it("should generate correct response time query for app-gateway (alert)", () => { + const query = kustoQueryService.buildResponseTimeQuery( + mockEndpoint, + mockConfig, + "alert", + ); + + expect(query).toContain("AzureDiagnostics"); + expect(query).toContain("originalHost_s in (api_hosts)"); + expect(query).toContain("let api_hosts = datatable (name: string)"); + expect(query).toContain("requestUri_s matches regex"); + expect(query).toContain("timeTaken_d"); + expect(query).toContain("percentiles(timeTaken_d, 95)"); + expect(query).toContain("watermark=threshold"); + expect(query).toContain("where duration_percentile_95 > threshold"); + }); + + it("should generate correct response time query for api-management (alert)", () => { + const apiConfig = { + ...mockConfig, + resource_type: "api-management" as const, + }; + const query = kustoQueryService.buildResponseTimeQuery( + mockEndpoint, + apiConfig, + "alert", + ); + + expect(query).toContain("url_s matches regex"); + expect(query).toContain("DurationMs"); + expect(query).toContain("percentile(DurationMs, 95)"); + expect(query).not.toContain("originalHost_s"); + expect(query).not.toContain("api_hosts"); // No datatable for api-management + }); + + it("should use correct response time threshold (alert)", () => { + const customEndpoint = { ...mockEndpoint, response_time_threshold: 2 }; + const query = kustoQueryService.buildResponseTimeQuery( + customEndpoint, + mockConfig, + "alert", + ); + + expect(query).toContain("let threshold = 2"); + }); + it("should NOT filter by threshold in dashboard response time query", () => { + const query = kustoQueryService.buildResponseTimeQuery( + mockEndpoint, + mockConfig, + "dashboard", + ); + expect(query).toContain("render timechart"); + expect(query).not.toContain("where duration_percentile_95 > threshold"); + }); + }); + + describe("query validation", () => { + it("should generate valid Kusto syntax", () => { + const availabilityQuery = kustoQueryService.buildAvailabilityQuery( + mockEndpoint, + mockConfig, + "alert", + ); + const responseTimeQuery = kustoQueryService.buildResponseTimeQuery( + mockEndpoint, + mockConfig, + "alert", + ); + + // Basic syntax checks + expect(availabilityQuery).toMatch(/^[A-Za-z]/); // Starts with letter + expect(responseTimeQuery).toMatch(/^[A-Za-z]/); // Starts with letter + }); + + it("should use generic regex pattern for parameterized paths", () => { + const endpointWithParam = { + ...mockEndpoint, + path: "/api/v1/services/{service_id}", + }; + const query = kustoQueryService.buildAvailabilityQuery( + endpointWithParam, + mockConfig, + "alert", + ); + + expect(query).toContain("requestUri_s matches regex"); + expect(query).toContain("/api/v1/services/[^/]+$"); + expect(query).not.toContain("{service_id}"); + }); + + it("should handle multiple parameters in path", () => { + const endpointWithMultipleParams = { + ...mockEndpoint, + path: "/api/v1/users/{user_id}/posts/{post_id}", + }; + const query = kustoQueryService.buildAvailabilityQuery( + endpointWithMultipleParams, + mockConfig, + "alert", + ); + + expect(query).toContain("/api/v1/users/[^/]+/posts/[^/]+$"); + expect(query).not.toContain("{user_id}"); + expect(query).not.toContain("{post_id}"); + }); + }); +}); diff --git a/packages/opex-dashboard-ts/test/unit/resolver.test.ts b/packages/opex-dashboard-ts/test/unit/resolver.test.ts new file mode 100644 index 00000000..afb72bf4 --- /dev/null +++ b/packages/opex-dashboard-ts/test/unit/resolver.test.ts @@ -0,0 +1,43 @@ +import { + OpenAPISpecResolverAdapter, + ParseError, +} from "../../src/infrastructure/openapi/openapi-spec-resolver-adapter.js"; +import { describe, it, expect, beforeEach } from "vitest"; + +describe("OA3Resolver", () => { + let resolver: OpenAPISpecResolverAdapter; + + beforeEach(() => { + resolver = new OpenAPISpecResolverAdapter(); + }); + + describe("ParseError", () => { + it("should be a custom error class", () => { + const error = new ParseError("Test error"); + expect(error).toBeInstanceOf(Error); + expect(error.name).toBe("ParseError"); + expect(error.message).toBe("Test error"); + }); + + it("should have proper inheritance", () => { + const error = new ParseError("Test error"); + expect(error instanceof Error).toBe(true); + expect(error instanceof ParseError).toBe(true); + }); + }); + + describe("OA3Resolver class", () => { + it("should be instantiable", () => { + expect(resolver).toBeInstanceOf(OpenAPISpecResolverAdapter); + }); + + it("should have a resolve method", () => { + expect(typeof resolver.resolve).toBe("function"); + }); + + it("should have resolve method that returns a Promise", () => { + const result = resolver.resolve("./test/fixtures/test_openapi.yaml"); + expect(result).toBeInstanceOf(Promise); + }); + }); +}); diff --git a/packages/opex-dashboard-ts/test/unit/synth-tags.test.ts b/packages/opex-dashboard-ts/test/unit/synth-tags.test.ts new file mode 100644 index 00000000..f376f90e --- /dev/null +++ b/packages/opex-dashboard-ts/test/unit/synth-tags.test.ts @@ -0,0 +1,87 @@ +import { describe, it, expect } from "vitest"; +import { App } from "cdktf"; +import { promises as fs } from "fs"; +import * as os from "os"; +import * as path from "path"; + +import { AzureOpexStack } from "../../src/infrastructure/terraform/azure-dashboard.js"; +import { DashboardConfig, Endpoint } from "../../src/domain/index.js"; + +describe("synth: tags are applied to dashboard and alerts", () => { + it("emits tags in cdk.tf for portal dashboard and alerts", async () => { + const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "opex-tags-")); + + const endpoint: Endpoint = { + path: "/api/v1/services", + availability_threshold: 0.99, + response_time_threshold: 1, + } as Endpoint; + + const dataSource = + "/subscriptions/uuid/resourceGroups/my-rg/providers/Microsoft.Network/applicationGateways/my-gtw"; + + const config: DashboardConfig = { + oa3_spec: "spec.yaml", + name: "Tags Dashboard", + data_source: dataSource, + resource_type: "app-gateway", + timespan: "5m", + resource_group_name: "dashboards", + resourceIds: [dataSource], + hosts: ["app-backend.io.italia.it"], + endpoints: [endpoint], + tags: { Environment: "TEST", CostCenter: "CC123" }, + } as unknown as DashboardConfig; + + const app = new App({ outdir: tmp, hclOutput: false }); + // eslint-disable-next-line no-new + new AzureOpexStack(app, "opex-dashboard", config); + app.synth(); + + // Prefer JSON output from CDKTF; fall back to legacy paths if needed + const candidates = [ + path.join(tmp, "stacks", "opex-dashboard", "cdk.tf.json"), + path.join(tmp, "stacks", "opex-dashboard", "cdk.tf"), + path.join(tmp, "cdk.tf.json"), + path.join(tmp, "cdk.tf"), + ]; + + let tf: string | undefined; + let tfPath: string | undefined; + for (const p of candidates) { + try { + tf = await fs.readFile(p, "utf8"); + tfPath = p; + break; + } catch { + // try next candidate + } + } + + if (!tf || !tfPath) { + throw new Error( + `Could not find synthesized Terraform output. Tried: ${candidates.join(", ")}`, + ); + } + const json = JSON.parse(tf); + + // Dashboard has tags + const dashboard = json.resource.azurerm_portal_dashboard.dashboard; + expect(dashboard).toBeDefined(); + expect(dashboard.tags).toMatchObject({ + Environment: "TEST", + CostCenter: "CC123", + }); + + // Alerts have tags + const alerts = json.resource.azurerm_monitor_scheduled_query_rules_alert; + expect(alerts.alarm_availability_0.tags).toMatchObject({ + Environment: "TEST", + CostCenter: "CC123", + }); + expect(alerts.alarm_time_0.tags).toMatchObject({ + Environment: "TEST", + CostCenter: "CC123", + }); + }); +}); diff --git a/packages/opex-dashboard-ts/tsconfig.json b/packages/opex-dashboard-ts/tsconfig.json new file mode 100644 index 00000000..bb7596b2 --- /dev/null +++ b/packages/opex-dashboard-ts/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "@tsconfig/node22/tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "moduleResolution": "NodeNext", + "module": "NodeNext", + "target": "ES2022" + }, + "include": ["src"] +} diff --git a/packages/opex-dashboard-ts/tsup.config.ts b/packages/opex-dashboard-ts/tsup.config.ts new file mode 100644 index 00000000..47747936 --- /dev/null +++ b/packages/opex-dashboard-ts/tsup.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + clean: true, + dts: true, + entry: { + index: "src/index.ts", + cli: "src/infrastructure/cli/index.ts", + }, + format: ["esm", "cjs"], + outDir: "dist", + target: "esnext", + tsconfig: "./tsconfig.json", +}); diff --git a/turbo.json b/turbo.json index d2a57c9d..d7084d6a 100644 --- a/turbo.json +++ b/turbo.json @@ -18,6 +18,7 @@ "dependsOn": [ "^clean", "^generate", + "generate", "^build" ] }, @@ -70,7 +71,7 @@ "infra:generate": { "dependsOn": [ "^build" - ], + ], "inputs": [ "src/**/opex.ts", "**/openapi*.yaml", diff --git a/vitest.workspace.ts b/vitest.workspace.ts index 66b7b777..4c5796ee 100644 --- a/vitest.workspace.ts +++ b/vitest.workspace.ts @@ -1,3 +1,4 @@ export default [ - "apps/*" + "apps/*", + "packages/*" ] diff --git a/yarn.lock b/yarn.lock index f8de8716..13f7ba1b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -82,7 +82,7 @@ __metadata: languageName: node linkType: hard -"@apidevtools/swagger-parser@npm:^10.0.1": +"@apidevtools/swagger-parser@npm:^10.0.1, @apidevtools/swagger-parser@npm:^10.1.0": version: 10.1.1 resolution: "@apidevtools/swagger-parser@npm:10.1.1" dependencies: @@ -551,13 +551,13 @@ __metadata: languageName: node linkType: hard -"@cdktf/provider-azurerm@npm:^14.2.0": - version: 14.2.0 - resolution: "@cdktf/provider-azurerm@npm:14.2.0" +"@cdktf/provider-azurerm@npm:^14.12.0": + version: 14.12.0 + resolution: "@cdktf/provider-azurerm@npm:14.12.0" peerDependencies: cdktf: ^0.21.0 constructs: ^10.4.2 - checksum: 10c0/17ec170de0c2b4e3b23110361cdcb32a6ad2602290c75e5eed94a004e6961acad892c1a70ef6f82876e41b5db4d1964c67a8e11e51a084bc366c0b5d83cfd753 + checksum: 10c0/c1fe62fa6bc846b6e07bd5a5ed5f540b11ac276b2d0da59b36176bda8cc4c777074a4e9ab49df17992ab859e12bd653970efd9618c55135253859b40b2117601 languageName: node linkType: hard @@ -1337,7 +1337,18 @@ __metadata: languageName: node linkType: hard -"@eslint-community/regexpp@npm:^4.10.0, @eslint-community/regexpp@npm:^4.12.1, @eslint-community/regexpp@npm:^4.6.1": +"@eslint-community/eslint-utils@npm:^4.4.0": + version: 4.9.0 + resolution: "@eslint-community/eslint-utils@npm:4.9.0" + dependencies: + eslint-visitor-keys: "npm:^3.4.3" + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + checksum: 10c0/8881e22d519326e7dba85ea915ac7a143367c805e6ba1374c987aa2fbdd09195cc51183d2da72c0e2ff388f84363e1b220fd0d19bef10c272c63455162176817 + languageName: node + linkType: hard + +"@eslint-community/regexpp@npm:^4.10.0, @eslint-community/regexpp@npm:^4.12.1, @eslint-community/regexpp@npm:^4.5.1, @eslint-community/regexpp@npm:^4.6.1": version: 4.12.1 resolution: "@eslint-community/regexpp@npm:4.12.1" checksum: 10c0/a03d98c246bcb9109aec2c08e4d10c8d010256538dcb3f56610191607214523d4fb1b00aa81df830b6dffb74c5fa0be03642513a289c567949d3e550ca11cdf6 @@ -1723,24 +1734,6 @@ __metadata: languageName: node linkType: hard -"@infra/github-runner@workspace:infra/github-runner": - version: 0.0.0-use.local - resolution: "@infra/github-runner@workspace:infra/github-runner" - languageName: unknown - linkType: soft - -"@infra/identity@workspace:infra/identity": - version: 0.0.0-use.local - resolution: "@infra/identity@workspace:infra/identity" - languageName: unknown - linkType: soft - -"@infra/repository@workspace:infra/repository": - version: 0.0.0-use.local - resolution: "@infra/repository@workspace:infra/repository" - languageName: unknown - linkType: soft - "@infra/resources@workspace:infra/resources": version: 0.0.0-use.local resolution: "@infra/resources@workspace:infra/resources" @@ -3086,6 +3079,34 @@ __metadata: languageName: node linkType: hard +"@pagopa/opex-dashboard-ts@workspace:^, @pagopa/opex-dashboard-ts@workspace:packages/opex-dashboard-ts": + version: 0.0.0-use.local + resolution: "@pagopa/opex-dashboard-ts@workspace:packages/opex-dashboard-ts" + dependencies: + "@apidevtools/swagger-parser": "npm:^10.1.0" + "@cdktf/provider-azurerm": "npm:^14.12.0" + "@pagopa/eslint-config": "npm:^5.0.0" + "@tsconfig/node22": "npm:^22.0.2" + "@types/js-yaml": "npm:^4.0.5" + "@types/node": "npm:^20.0.0" + "@typescript-eslint/eslint-plugin": "npm:^6.0.0" + "@typescript-eslint/parser": "npm:^6.0.0" + cdktf: "npm:^0.21.0" + commander: "npm:^12.0.0" + constructs: "npm:^10.3.0" + eslint: "npm:^8.0.0" + js-yaml: "npm:^4.1.0" + openapi-types: "npm:^12.1.3" + prettier: "npm:^3.6.2" + tsup: "npm:^8.5.0" + typescript: "npm:^5.0.0" + vitest: "npm:^3.2.4" + zod: "npm:^4.1.5" + bin: + opex-dashboard-ts: dist/infrastructure/cli/index.js + languageName: unknown + linkType: soft + "@pagopa/ts-commons@npm:^10.15.0": version: 10.15.0 resolution: "@pagopa/ts-commons@npm:10.15.0" @@ -3238,6 +3259,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-android-arm-eabi@npm:4.50.1": + version: 4.50.1 + resolution: "@rollup/rollup-android-arm-eabi@npm:4.50.1" + conditions: os=android & cpu=arm + languageName: node + linkType: hard + "@rollup/rollup-android-arm64@npm:4.40.2": version: 4.40.2 resolution: "@rollup/rollup-android-arm64@npm:4.40.2" @@ -3245,6 +3273,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-android-arm64@npm:4.50.1": + version: 4.50.1 + resolution: "@rollup/rollup-android-arm64@npm:4.50.1" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + "@rollup/rollup-darwin-arm64@npm:4.40.2": version: 4.40.2 resolution: "@rollup/rollup-darwin-arm64@npm:4.40.2" @@ -3252,6 +3287,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-darwin-arm64@npm:4.50.1": + version: 4.50.1 + resolution: "@rollup/rollup-darwin-arm64@npm:4.50.1" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + "@rollup/rollup-darwin-x64@npm:4.40.2": version: 4.40.2 resolution: "@rollup/rollup-darwin-x64@npm:4.40.2" @@ -3259,6 +3301,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-darwin-x64@npm:4.50.1": + version: 4.50.1 + resolution: "@rollup/rollup-darwin-x64@npm:4.50.1" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + "@rollup/rollup-freebsd-arm64@npm:4.40.2": version: 4.40.2 resolution: "@rollup/rollup-freebsd-arm64@npm:4.40.2" @@ -3266,6 +3315,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-freebsd-arm64@npm:4.50.1": + version: 4.50.1 + resolution: "@rollup/rollup-freebsd-arm64@npm:4.50.1" + conditions: os=freebsd & cpu=arm64 + languageName: node + linkType: hard + "@rollup/rollup-freebsd-x64@npm:4.40.2": version: 4.40.2 resolution: "@rollup/rollup-freebsd-x64@npm:4.40.2" @@ -3273,6 +3329,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-freebsd-x64@npm:4.50.1": + version: 4.50.1 + resolution: "@rollup/rollup-freebsd-x64@npm:4.50.1" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + "@rollup/rollup-linux-arm-gnueabihf@npm:4.40.2": version: 4.40.2 resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.40.2" @@ -3280,6 +3343,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-arm-gnueabihf@npm:4.50.1": + version: 4.50.1 + resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.50.1" + conditions: os=linux & cpu=arm & libc=glibc + languageName: node + linkType: hard + "@rollup/rollup-linux-arm-musleabihf@npm:4.40.2": version: 4.40.2 resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.40.2" @@ -3287,6 +3357,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-arm-musleabihf@npm:4.50.1": + version: 4.50.1 + resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.50.1" + conditions: os=linux & cpu=arm & libc=musl + languageName: node + linkType: hard + "@rollup/rollup-linux-arm64-gnu@npm:4.40.2": version: 4.40.2 resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.40.2" @@ -3294,6 +3371,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-arm64-gnu@npm:4.50.1": + version: 4.50.1 + resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.50.1" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + "@rollup/rollup-linux-arm64-musl@npm:4.40.2": version: 4.40.2 resolution: "@rollup/rollup-linux-arm64-musl@npm:4.40.2" @@ -3301,6 +3385,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-arm64-musl@npm:4.50.1": + version: 4.50.1 + resolution: "@rollup/rollup-linux-arm64-musl@npm:4.50.1" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + "@rollup/rollup-linux-loongarch64-gnu@npm:4.40.2": version: 4.40.2 resolution: "@rollup/rollup-linux-loongarch64-gnu@npm:4.40.2" @@ -3308,6 +3399,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-loongarch64-gnu@npm:4.50.1": + version: 4.50.1 + resolution: "@rollup/rollup-linux-loongarch64-gnu@npm:4.50.1" + conditions: os=linux & cpu=loong64 & libc=glibc + languageName: node + linkType: hard + "@rollup/rollup-linux-powerpc64le-gnu@npm:4.40.2": version: 4.40.2 resolution: "@rollup/rollup-linux-powerpc64le-gnu@npm:4.40.2" @@ -3315,6 +3413,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-ppc64-gnu@npm:4.50.1": + version: 4.50.1 + resolution: "@rollup/rollup-linux-ppc64-gnu@npm:4.50.1" + conditions: os=linux & cpu=ppc64 & libc=glibc + languageName: node + linkType: hard + "@rollup/rollup-linux-riscv64-gnu@npm:4.40.2": version: 4.40.2 resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.40.2" @@ -3322,6 +3427,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-riscv64-gnu@npm:4.50.1": + version: 4.50.1 + resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.50.1" + conditions: os=linux & cpu=riscv64 & libc=glibc + languageName: node + linkType: hard + "@rollup/rollup-linux-riscv64-musl@npm:4.40.2": version: 4.40.2 resolution: "@rollup/rollup-linux-riscv64-musl@npm:4.40.2" @@ -3329,6 +3441,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-riscv64-musl@npm:4.50.1": + version: 4.50.1 + resolution: "@rollup/rollup-linux-riscv64-musl@npm:4.50.1" + conditions: os=linux & cpu=riscv64 & libc=musl + languageName: node + linkType: hard + "@rollup/rollup-linux-s390x-gnu@npm:4.40.2": version: 4.40.2 resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.40.2" @@ -3336,6 +3455,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-s390x-gnu@npm:4.50.1": + version: 4.50.1 + resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.50.1" + conditions: os=linux & cpu=s390x & libc=glibc + languageName: node + linkType: hard + "@rollup/rollup-linux-x64-gnu@npm:4.40.2": version: 4.40.2 resolution: "@rollup/rollup-linux-x64-gnu@npm:4.40.2" @@ -3343,6 +3469,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-x64-gnu@npm:4.50.1": + version: 4.50.1 + resolution: "@rollup/rollup-linux-x64-gnu@npm:4.50.1" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + "@rollup/rollup-linux-x64-musl@npm:4.40.2": version: 4.40.2 resolution: "@rollup/rollup-linux-x64-musl@npm:4.40.2" @@ -3350,6 +3483,20 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-x64-musl@npm:4.50.1": + version: 4.50.1 + resolution: "@rollup/rollup-linux-x64-musl@npm:4.50.1" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"@rollup/rollup-openharmony-arm64@npm:4.50.1": + version: 4.50.1 + resolution: "@rollup/rollup-openharmony-arm64@npm:4.50.1" + conditions: os=openharmony & cpu=arm64 + languageName: node + linkType: hard + "@rollup/rollup-win32-arm64-msvc@npm:4.40.2": version: 4.40.2 resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.40.2" @@ -3357,6 +3504,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-win32-arm64-msvc@npm:4.50.1": + version: 4.50.1 + resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.50.1" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + "@rollup/rollup-win32-ia32-msvc@npm:4.40.2": version: 4.40.2 resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.40.2" @@ -3364,6 +3518,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-win32-ia32-msvc@npm:4.50.1": + version: 4.50.1 + resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.50.1" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + "@rollup/rollup-win32-x64-msvc@npm:4.40.2": version: 4.40.2 resolution: "@rollup/rollup-win32-x64-msvc@npm:4.40.2" @@ -3371,6 +3532,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-win32-x64-msvc@npm:4.50.1": + version: 4.50.1 + resolution: "@rollup/rollup-win32-x64-msvc@npm:4.50.1" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@rtsao/scc@npm:^1.1.0": version: 1.1.0 resolution: "@rtsao/scc@npm:1.1.0" @@ -3475,6 +3643,13 @@ __metadata: languageName: node linkType: hard +"@tsconfig/node22@npm:^22.0.2": + version: 22.0.2 + resolution: "@tsconfig/node22@npm:22.0.2" + checksum: 10c0/c75e6b9ea86ec2a384adefac0e2f16a6c08202ae5cf6c8c745944396a6931ffb38e742809491c1882d1868c2af1c33744193701779674bfb1e05f6a130045a18 + languageName: node + linkType: hard + "@tybys/wasm-util@npm:^0.9.0": version: 0.9.0 resolution: "@tybys/wasm-util@npm:0.9.0" @@ -3503,6 +3678,15 @@ __metadata: languageName: node linkType: hard +"@types/chai@npm:^5.2.2": + version: 5.2.2 + resolution: "@types/chai@npm:5.2.2" + dependencies: + "@types/deep-eql": "npm:*" + checksum: 10c0/49282bf0e8246800ebb36f17256f97bd3a8c4fb31f92ad3c0eaa7623518d7e87f1eaad4ad206960fcaf7175854bdff4cb167e4fe96811e0081b4ada83dd533ec + languageName: node + linkType: hard + "@types/connect@npm:*": version: 3.4.38 resolution: "@types/connect@npm:3.4.38" @@ -3512,6 +3696,13 @@ __metadata: languageName: node linkType: hard +"@types/deep-eql@npm:*": + version: 4.0.2 + resolution: "@types/deep-eql@npm:4.0.2" + checksum: 10c0/bf3f811843117900d7084b9d0c852da9a044d12eb40e6de73b552598a6843c21291a8a381b0532644574beecd5e3491c5ff3a0365ab86b15d59862c025384844 + languageName: node + linkType: hard + "@types/estree@npm:1.0.7, @types/estree@npm:^1.0.0": version: 1.0.7 resolution: "@types/estree@npm:1.0.7" @@ -3519,7 +3710,7 @@ __metadata: languageName: node linkType: hard -"@types/estree@npm:^1.0.6": +"@types/estree@npm:1.0.8, @types/estree@npm:^1.0.6": version: 1.0.8 resolution: "@types/estree@npm:1.0.8" checksum: 10c0/39d34d1afaa338ab9763f37ad6066e3f349444f9052b9676a7cc0252ef9485a41c6d81c9c4e0d26e9077993354edf25efc853f3224dd4b447175ef62bdcc86a5 @@ -3564,7 +3755,7 @@ __metadata: languageName: node linkType: hard -"@types/js-yaml@npm:^4.0.9": +"@types/js-yaml@npm:^4.0.5": version: 4.0.9 resolution: "@types/js-yaml@npm:4.0.9" checksum: 10c0/24de857aa8d61526bbfbbaa383aa538283ad17363fcd5bb5148e2c7f604547db36646440e739d78241ed008702a8920665d1add5618687b6743858fae00da211 @@ -3578,7 +3769,7 @@ __metadata: languageName: node linkType: hard -"@types/json-schema@npm:^7.0.15, @types/json-schema@npm:^7.0.6": +"@types/json-schema@npm:^7.0.12, @types/json-schema@npm:^7.0.15, @types/json-schema@npm:^7.0.6": version: 7.0.15 resolution: "@types/json-schema@npm:7.0.15" checksum: 10c0/a996a745e6c5d60292f36731dd41341339d4eeed8180bb09226e5c8d23759067692b1d88e5d91d72ee83dfc00d3aca8e7bd43ea120516c17922cbcb7c3e252db @@ -3624,6 +3815,15 @@ __metadata: languageName: node linkType: hard +"@types/node@npm:^20.0.0": + version: 20.19.13 + resolution: "@types/node@npm:20.19.13" + dependencies: + undici-types: "npm:~6.21.0" + checksum: 10c0/25fba13d50c4f18ac56a50d4491502e7d095f09e44c17c0c6887aa5e4e6122e961e92fc3b682ed8c053ab87a05ce10f501345d23d96fdfba8002ecce6c8ced51 + languageName: node + linkType: hard + "@types/node@npm:^20.17.43": version: 20.17.47 resolution: "@types/node@npm:20.17.47" @@ -3728,6 +3928,13 @@ __metadata: languageName: node linkType: hard +"@types/semver@npm:^7.5.0": + version: 7.7.1 + resolution: "@types/semver@npm:7.7.1" + checksum: 10c0/c938aef3bf79a73f0f3f6037c16e2e759ff40c54122ddf0b2583703393d8d3127130823facb880e694caa324eb6845628186aac1997ee8b31dc2d18fafe26268 + languageName: node + linkType: hard + "@types/send@npm:*": version: 0.17.4 resolution: "@types/send@npm:0.17.4" @@ -3814,6 +4021,31 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/eslint-plugin@npm:^6.0.0": + version: 6.21.0 + resolution: "@typescript-eslint/eslint-plugin@npm:6.21.0" + dependencies: + "@eslint-community/regexpp": "npm:^4.5.1" + "@typescript-eslint/scope-manager": "npm:6.21.0" + "@typescript-eslint/type-utils": "npm:6.21.0" + "@typescript-eslint/utils": "npm:6.21.0" + "@typescript-eslint/visitor-keys": "npm:6.21.0" + debug: "npm:^4.3.4" + graphemer: "npm:^1.4.0" + ignore: "npm:^5.2.4" + natural-compare: "npm:^1.4.0" + semver: "npm:^7.5.4" + ts-api-utils: "npm:^1.0.1" + peerDependencies: + "@typescript-eslint/parser": ^6.0.0 || ^6.0.0-alpha + eslint: ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + typescript: + optional: true + checksum: 10c0/f911a79ee64d642f814a3b6cdb0d324b5f45d9ef955c5033e78903f626b7239b4aa773e464a38c3e667519066169d983538f2bf8e5d00228af587c9d438fb344 + languageName: node + linkType: hard + "@typescript-eslint/parser@npm:8.35.1": version: 8.35.1 resolution: "@typescript-eslint/parser@npm:8.35.1" @@ -3863,7 +4095,7 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/parser@npm:^6.21.0": +"@typescript-eslint/parser@npm:^6.0.0, @typescript-eslint/parser@npm:^6.21.0": version: 6.21.0 resolution: "@typescript-eslint/parser@npm:6.21.0" dependencies: @@ -3943,6 +4175,23 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/type-utils@npm:6.21.0": + version: 6.21.0 + resolution: "@typescript-eslint/type-utils@npm:6.21.0" + dependencies: + "@typescript-eslint/typescript-estree": "npm:6.21.0" + "@typescript-eslint/utils": "npm:6.21.0" + debug: "npm:^4.3.4" + ts-api-utils: "npm:^1.0.1" + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + typescript: + optional: true + checksum: 10c0/7409c97d1c4a4386b488962739c4f1b5b04dc60cf51f8cd88e6b12541f84d84c6b8b67e491a147a2c95f9ec486539bf4519fb9d418411aef6537b9c156468117 + languageName: node + linkType: hard + "@typescript-eslint/type-utils@npm:8.32.1": version: 8.32.1 resolution: "@typescript-eslint/type-utils@npm:8.32.1" @@ -4076,6 +4325,23 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/utils@npm:6.21.0": + version: 6.21.0 + resolution: "@typescript-eslint/utils@npm:6.21.0" + dependencies: + "@eslint-community/eslint-utils": "npm:^4.4.0" + "@types/json-schema": "npm:^7.0.12" + "@types/semver": "npm:^7.5.0" + "@typescript-eslint/scope-manager": "npm:6.21.0" + "@typescript-eslint/types": "npm:6.21.0" + "@typescript-eslint/typescript-estree": "npm:6.21.0" + semver: "npm:^7.5.4" + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + checksum: 10c0/ab2df3833b2582d4e5467a484d08942b4f2f7208f8e09d67de510008eb8001a9b7460f2f9ba11c12086fd3cdcac0c626761c7995c2c6b5657d5fa6b82030a32d + languageName: node + linkType: hard + "@typescript-eslint/utils@npm:8.32.1": version: 8.32.1 resolution: "@typescript-eslint/utils@npm:8.32.1" @@ -4341,6 +4607,19 @@ __metadata: languageName: node linkType: hard +"@vitest/expect@npm:3.2.4": + version: 3.2.4 + resolution: "@vitest/expect@npm:3.2.4" + dependencies: + "@types/chai": "npm:^5.2.2" + "@vitest/spy": "npm:3.2.4" + "@vitest/utils": "npm:3.2.4" + chai: "npm:^5.2.0" + tinyrainbow: "npm:^2.0.0" + checksum: 10c0/7586104e3fd31dbe1e6ecaafb9a70131e4197dce2940f727b6a84131eee3decac7b10f9c7c72fa5edbdb68b6f854353bd4c0fa84779e274207fb7379563b10db + languageName: node + linkType: hard + "@vitest/mocker@npm:3.1.4": version: 3.1.4 resolution: "@vitest/mocker@npm:3.1.4" @@ -4360,6 +4639,25 @@ __metadata: languageName: node linkType: hard +"@vitest/mocker@npm:3.2.4": + version: 3.2.4 + resolution: "@vitest/mocker@npm:3.2.4" + dependencies: + "@vitest/spy": "npm:3.2.4" + estree-walker: "npm:^3.0.3" + magic-string: "npm:^0.30.17" + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + checksum: 10c0/f7a4aea19bbbf8f15905847ee9143b6298b2c110f8b64789224cb0ffdc2e96f9802876aa2ca83f1ec1b6e1ff45e822abb34f0054c24d57b29ab18add06536ccd + languageName: node + linkType: hard + "@vitest/pretty-format@npm:3.1.4, @vitest/pretty-format@npm:^3.1.4": version: 3.1.4 resolution: "@vitest/pretty-format@npm:3.1.4" @@ -4369,6 +4667,15 @@ __metadata: languageName: node linkType: hard +"@vitest/pretty-format@npm:3.2.4, @vitest/pretty-format@npm:^3.2.4": + version: 3.2.4 + resolution: "@vitest/pretty-format@npm:3.2.4" + dependencies: + tinyrainbow: "npm:^2.0.0" + checksum: 10c0/5ad7d4278e067390d7d633e307fee8103958806a419ca380aec0e33fae71b44a64415f7a9b4bc11635d3c13d4a9186111c581d3cef9c65cc317e68f077456887 + languageName: node + linkType: hard + "@vitest/runner@npm:3.1.4": version: 3.1.4 resolution: "@vitest/runner@npm:3.1.4" @@ -4379,6 +4686,17 @@ __metadata: languageName: node linkType: hard +"@vitest/runner@npm:3.2.4": + version: 3.2.4 + resolution: "@vitest/runner@npm:3.2.4" + dependencies: + "@vitest/utils": "npm:3.2.4" + pathe: "npm:^2.0.3" + strip-literal: "npm:^3.0.0" + checksum: 10c0/e8be51666c72b3668ae3ea348b0196656a4a5adb836cb5e270720885d9517421815b0d6c98bfdf1795ed02b994b7bfb2b21566ee356a40021f5bf4f6ed4e418a + languageName: node + linkType: hard + "@vitest/snapshot@npm:3.1.4": version: 3.1.4 resolution: "@vitest/snapshot@npm:3.1.4" @@ -4390,6 +4708,17 @@ __metadata: languageName: node linkType: hard +"@vitest/snapshot@npm:3.2.4": + version: 3.2.4 + resolution: "@vitest/snapshot@npm:3.2.4" + dependencies: + "@vitest/pretty-format": "npm:3.2.4" + magic-string: "npm:^0.30.17" + pathe: "npm:^2.0.3" + checksum: 10c0/f8301a3d7d1559fd3d59ed51176dd52e1ed5c2d23aa6d8d6aa18787ef46e295056bc726a021698d8454c16ed825ecba163362f42fa90258bb4a98cfd2c9424fc + languageName: node + linkType: hard + "@vitest/spy@npm:3.1.4": version: 3.1.4 resolution: "@vitest/spy@npm:3.1.4" @@ -4399,6 +4728,15 @@ __metadata: languageName: node linkType: hard +"@vitest/spy@npm:3.2.4": + version: 3.2.4 + resolution: "@vitest/spy@npm:3.2.4" + dependencies: + tinyspy: "npm:^4.0.3" + checksum: 10c0/6ebf0b4697dc238476d6b6a60c76ba9eb1dd8167a307e30f08f64149612fd50227682b876420e4c2e09a76334e73f72e3ebf0e350714dc22474258292e202024 + languageName: node + linkType: hard + "@vitest/utils@npm:3.1.4": version: 3.1.4 resolution: "@vitest/utils@npm:3.1.4" @@ -4410,6 +4748,17 @@ __metadata: languageName: node linkType: hard +"@vitest/utils@npm:3.2.4": + version: 3.2.4 + resolution: "@vitest/utils@npm:3.2.4" + dependencies: + "@vitest/pretty-format": "npm:3.2.4" + loupe: "npm:^3.1.4" + tinyrainbow: "npm:^2.0.0" + checksum: 10c0/024a9b8c8bcc12cf40183c246c244b52ecff861c6deb3477cbf487ac8781ad44c68a9c5fd69f8c1361878e55b97c10d99d511f2597f1f7244b5e5101d028ba64 + languageName: node + linkType: hard + "a-sync-waterfall@npm:^1.0.0": version: 1.0.1 resolution: "a-sync-waterfall@npm:1.0.1" @@ -5126,6 +5475,17 @@ __metadata: languageName: node linkType: hard +"bundle-require@npm:^5.1.0": + version: 5.1.0 + resolution: "bundle-require@npm:5.1.0" + dependencies: + load-tsconfig: "npm:^0.2.3" + peerDependencies: + esbuild: ">=0.18" + checksum: 10c0/8bff9df68eb686f05af952003c78e70ffed2817968f92aebb2af620cc0b7428c8154df761d28f1b38508532204278950624ef86ce63644013dc57660a9d1810f + languageName: node + linkType: hard + "busboy@npm:1.6.0": version: 1.6.0 resolution: "busboy@npm:1.6.0" @@ -5229,23 +5589,6 @@ __metadata: languageName: node linkType: hard -"cdktf-monitoring-stack@workspace:*, cdktf-monitoring-stack@workspace:packages/cdktf-monitoring-stack": - version: 0.0.0-use.local - resolution: "cdktf-monitoring-stack@workspace:packages/cdktf-monitoring-stack" - dependencies: - "@cdktf/provider-azurerm": "npm:^14.2.0" - "@pagopa/eslint-config": "npm:^5.0.0" - "@pagopa/typescript-config-node": "workspace:^" - "@types/js-yaml": "npm:^4.0.9" - cdktf: "npm:^0.21.0" - constructs: "npm:^10.4.2" - eslint: "npm:^9.30.1" - js-yaml: "npm:^4.1.0" - prettier: "npm:^3.6.2" - typescript: "npm:^5.8.3" - languageName: unknown - linkType: soft - "cdktf@npm:^0.21.0": version: 0.21.0 resolution: "cdktf@npm:0.21.0" @@ -5338,6 +5681,15 @@ __metadata: languageName: node linkType: hard +"chokidar@npm:^4.0.3": + version: 4.0.3 + resolution: "chokidar@npm:4.0.3" + dependencies: + readdirp: "npm:^4.0.1" + checksum: 10c0/a58b9df05bb452f7d105d9e7229ac82fa873741c0c40ddcc7bb82f8a909fbe3f7814c9ebe9bc9a2bef9b737c0ec6e2d699d179048ef06ad3ec46315df0ebe6ad + languageName: node + linkType: hard + "chownr@npm:^3.0.0": version: 3.0.0 resolution: "chownr@npm:3.0.0" @@ -5472,6 +5824,13 @@ __metadata: languageName: node linkType: hard +"commander@npm:^12.0.0": + version: 12.1.0 + resolution: "commander@npm:12.1.0" + checksum: 10c0/6e1996680c083b3b897bfc1cfe1c58dfbcd9842fd43e1aaf8a795fbc237f65efcc860a3ef457b318e73f29a4f4a28f6403c3d653d021d960e4632dd45bde54a9 + languageName: node + linkType: hard + "commander@npm:^4.0.0": version: 4.1.1 resolution: "commander@npm:4.1.1" @@ -5520,14 +5879,28 @@ __metadata: languageName: node linkType: hard -"constructs@npm:^10.4.2": - version: 10.4.2 - resolution: "constructs@npm:10.4.2" - checksum: 10c0/dcd5edd631c7313964f89fffb7365e1eebaede16cbc9ae69eab5337710353913684b860ccc4b2a3dfaf147656f48f0ae7853ca94cb51833e152b46047ac7a4ff +"confbox@npm:^0.1.8": + version: 0.1.8 + resolution: "confbox@npm:0.1.8" + checksum: 10c0/fc2c68d97cb54d885b10b63e45bd8da83a8a71459d3ecf1825143dd4c7f9f1b696b3283e07d9d12a144c1301c2ebc7842380bdf0014e55acc4ae1c9550102418 languageName: node linkType: hard -"content-disposition@npm:0.5.4": +"consola@npm:^3.4.0": + version: 3.4.2 + resolution: "consola@npm:3.4.2" + checksum: 10c0/7cebe57ecf646ba74b300bcce23bff43034ed6fbec9f7e39c27cee1dc00df8a21cd336b466ad32e304ea70fba04ec9e890c200270de9a526ce021ba8a7e4c11a + languageName: node + linkType: hard + +"constructs@npm:^10.3.0, constructs@npm:^10.4.2": + version: 10.4.2 + resolution: "constructs@npm:10.4.2" + checksum: 10c0/dcd5edd631c7313964f89fffb7365e1eebaede16cbc9ae69eab5337710353913684b860ccc4b2a3dfaf147656f48f0ae7853ca94cb51833e152b46047ac7a4ff + languageName: node + linkType: hard + +"content-disposition@npm:0.5.4": version: 0.5.4 resolution: "content-disposition@npm:0.5.4" dependencies: @@ -5707,7 +6080,7 @@ __metadata: languageName: node linkType: hard -"debug@npm:4, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4, debug@npm:^4.3.5, debug@npm:^4.4.0": +"debug@npm:4, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4, debug@npm:^4.3.5, debug@npm:^4.4.0, debug@npm:^4.4.1": version: 4.4.1 resolution: "debug@npm:4.4.1" dependencies: @@ -6708,7 +7081,7 @@ __metadata: languageName: node linkType: hard -"eslint@npm:^8.57.1": +"eslint@npm:^8.0.0, eslint@npm:^8.57.1": version: 8.57.1 resolution: "eslint@npm:8.57.1" dependencies: @@ -7079,6 +7452,18 @@ __metadata: languageName: node linkType: hard +"fdir@npm:^6.5.0": + version: 6.5.0 + resolution: "fdir@npm:6.5.0" + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + checksum: 10c0/e345083c4306b3aed6cb8ec551e26c36bab5c511e99ea4576a16750ddc8d3240e63826cc624f5ae17ad4dc82e68a253213b60d556c11bfad064b7607847ed07f + languageName: node + linkType: hard + "fecha@npm:^4.2.0": version: 4.2.3 resolution: "fecha@npm:4.2.3" @@ -7155,6 +7540,17 @@ __metadata: languageName: node linkType: hard +"fix-dts-default-cjs-exports@npm:^1.0.0": + version: 1.0.1 + resolution: "fix-dts-default-cjs-exports@npm:1.0.1" + dependencies: + magic-string: "npm:^0.30.17" + mlly: "npm:^1.7.4" + rollup: "npm:^4.34.8" + checksum: 10c0/61a3cbe32b6c29df495ef3aded78199fe9dbb52e2801c899fe76d9ca413d3c8c51f79986bac83f8b4b2094ebde883ddcfe47b68ce469806ba13ca6ed4e7cd362 + languageName: node + linkType: hard + "flat-cache@npm:^3.0.4": version: 3.2.0 resolution: "flat-cache@npm:3.2.0" @@ -7745,7 +8141,7 @@ __metadata: languageName: node linkType: hard -"ignore@npm:^5.2.0": +"ignore@npm:^5.2.0, ignore@npm:^5.2.4": version: 5.3.2 resolution: "ignore@npm:5.3.2" checksum: 10c0/f9f652c957983634ded1e7f02da3b559a0d4cc210fca3792cb67f1b153623c9c42efdc1c4121af171e295444459fc4a9201101fb041b1104a3c000bccb188337 @@ -8301,7 +8697,7 @@ __metadata: languageName: node linkType: hard -"joycon@npm:^3.0.1": +"joycon@npm:^3.0.1, joycon@npm:^3.1.1": version: 3.1.1 resolution: "joycon@npm:3.1.1" checksum: 10c0/131fb1e98c9065d067fd49b6e685487ac4ad4d254191d7aa2c9e3b90f4e9ca70430c43cad001602bdbdabcf58717d3b5c5b7461c1bd8e39478c8de706b3fe6ae @@ -8315,6 +8711,13 @@ __metadata: languageName: node linkType: hard +"js-tokens@npm:^9.0.1": + version: 9.0.1 + resolution: "js-tokens@npm:9.0.1" + checksum: 10c0/68dcab8f233dde211a6b5fd98079783cbcd04b53617c1250e3553ee16ab3e6134f5e65478e41d82f6d351a052a63d71024553933808570f04dbf828d7921e80e + languageName: node + linkType: hard + "js-yaml@npm:^3.13.1, js-yaml@npm:^3.14.0, js-yaml@npm:^3.6.1": version: 3.14.1 resolution: "js-yaml@npm:3.14.1" @@ -8561,6 +8964,13 @@ __metadata: languageName: node linkType: hard +"lilconfig@npm:^3.1.1": + version: 3.1.3 + resolution: "lilconfig@npm:3.1.3" + checksum: 10c0/f5604e7240c5c275743561442fbc5abf2a84ad94da0f5adc71d25e31fa8483048de3dcedcb7a44112a942fed305fd75841cdf6c9681c7f640c63f1049e9a5dcc + languageName: node + linkType: hard + "lines-and-columns@npm:^1.1.6": version: 1.2.4 resolution: "lines-and-columns@npm:1.2.4" @@ -8754,6 +9164,13 @@ __metadata: languageName: node linkType: hard +"loupe@npm:^3.1.4": + version: 3.2.1 + resolution: "loupe@npm:3.2.1" + checksum: 10c0/910c872cba291309664c2d094368d31a68907b6f5913e989d301b5c25f30e97d76d77f23ab3bf3b46d0f601ff0b6af8810c10c31b91d2c6b2f132809ca2cc705 + languageName: node + linkType: hard + "lru-cache@npm:^10.0.1, lru-cache@npm:^10.2.0": version: 10.4.3 resolution: "lru-cache@npm:10.4.3" @@ -9037,6 +9454,18 @@ __metadata: languageName: node linkType: hard +"mlly@npm:^1.7.4": + version: 1.8.0 + resolution: "mlly@npm:1.8.0" + dependencies: + acorn: "npm:^8.15.0" + pathe: "npm:^2.0.3" + pkg-types: "npm:^1.3.1" + ufo: "npm:^1.6.1" + checksum: 10c0/f174b844ae066c71e9b128046677868e2e28694f0bbeeffbe760b2a9d8ff24de0748d0fde6fabe706700c1d2e11d3c0d7a53071b5ea99671592fac03364604ab + languageName: node + linkType: hard + "module-details-from-path@npm:^1.0.3": version: 1.0.4 resolution: "module-details-from-path@npm:1.0.4" @@ -9090,7 +9519,7 @@ __metadata: languageName: node linkType: hard -"nanoid@npm:^3.3.6, nanoid@npm:^3.3.8": +"nanoid@npm:^3.3.11, nanoid@npm:^3.3.6, nanoid@npm:^3.3.8": version: 3.3.11 resolution: "nanoid@npm:3.3.11" bin: @@ -9427,19 +9856,12 @@ __metadata: languageName: node linkType: hard -"opex-common@workspace:*, opex-common@workspace:packages/opex-common": - version: 0.0.0-use.local - resolution: "opex-common@workspace:packages/opex-common" - dependencies: - "@pagopa/eslint-config": "npm:^5.0.0" - "@pagopa/typescript-config-node": "workspace:^" - cdktf: "npm:^0.21.0" - cdktf-monitoring-stack: "workspace:*" - eslint: "npm:^9.30.1" - prettier: "npm:^3.6.2" - typescript: "npm:^5.8.3" - languageName: unknown - linkType: soft +"openapi-types@npm:^12.1.3": + version: 12.1.3 + resolution: "openapi-types@npm:12.1.3" + checksum: 10c0/4ad4eb91ea834c237edfa6ab31394e87e00c888fc2918009763389c00d02342345195d6f302d61c3fd807f17723cd48df29b47b538b68375b3827b3758cd520f + languageName: node + linkType: hard "optionator@npm:^0.9.3": version: 0.9.4 @@ -9670,7 +10092,7 @@ __metadata: languageName: node linkType: hard -"pathe@npm:^2.0.3": +"pathe@npm:^2.0.1, pathe@npm:^2.0.3": version: 2.0.3 resolution: "pathe@npm:2.0.3" checksum: 10c0/c118dc5a8b5c4166011b2b70608762e260085180bb9e33e80a50dcdb1e78c010b1624f4280c492c92b05fc276715a4c357d1f9edc570f8f1b3d90b6839ebaca1 @@ -9761,6 +10183,13 @@ __metadata: languageName: node linkType: hard +"picomatch@npm:^4.0.3": + version: 4.0.3 + resolution: "picomatch@npm:4.0.3" + checksum: 10c0/9582c951e95eebee5434f59e426cddd228a7b97a0161a375aed4be244bd3fe8e3a31b846808ea14ef2c8a2527a6eeab7b3946a67d5979e81694654f939473ae2 + languageName: node + linkType: hard + "pify@npm:^4.0.1": version: 4.0.1 resolution: "pify@npm:4.0.1" @@ -9775,6 +10204,17 @@ __metadata: languageName: node linkType: hard +"pkg-types@npm:^1.3.1": + version: 1.3.1 + resolution: "pkg-types@npm:1.3.1" + dependencies: + confbox: "npm:^0.1.8" + mlly: "npm:^1.7.4" + pathe: "npm:^2.0.1" + checksum: 10c0/19e6cb8b66dcc66c89f2344aecfa47f2431c988cfa3366bdfdcfb1dd6695f87dcce37fbd90fe9d1605e2f4440b77f391e83c23255347c35cf84e7fd774d7fcea + languageName: node + linkType: hard + "possible-typed-array-names@npm:^1.0.0": version: 1.1.0 resolution: "possible-typed-array-names@npm:1.1.0" @@ -9800,6 +10240,29 @@ __metadata: languageName: node linkType: hard +"postcss-load-config@npm:^6.0.1": + version: 6.0.1 + resolution: "postcss-load-config@npm:6.0.1" + dependencies: + lilconfig: "npm:^3.1.1" + peerDependencies: + jiti: ">=1.21.0" + postcss: ">=8.0.9" + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + jiti: + optional: true + postcss: + optional: true + tsx: + optional: true + yaml: + optional: true + checksum: 10c0/74173a58816dac84e44853f7afbd283f4ef13ca0b6baeba27701214beec33f9e309b128f8102e2b173e8d45ecba45d279a9be94b46bf48d219626aa9b5730848 + languageName: node + linkType: hard + "postcss@npm:8.4.31": version: 8.4.31 resolution: "postcss@npm:8.4.31" @@ -9822,6 +10285,17 @@ __metadata: languageName: node linkType: hard +"postcss@npm:^8.5.6": + version: 8.5.6 + resolution: "postcss@npm:8.5.6" + dependencies: + nanoid: "npm:^3.3.11" + picocolors: "npm:^1.1.1" + source-map-js: "npm:^1.2.1" + checksum: 10c0/5127cc7c91ed7a133a1b7318012d8bfa112da9ef092dddf369ae699a1f10ebbd89b1b9f25f3228795b84585c72aabd5ced5fc11f2ba467eedf7b081a66fad024 + languageName: node + linkType: hard + "postgres-array@npm:~2.0.0": version: 2.0.0 resolution: "postgres-array@npm:2.0.0" @@ -10246,6 +10720,13 @@ __metadata: languageName: node linkType: hard +"readdirp@npm:^4.0.1": + version: 4.1.2 + resolution: "readdirp@npm:4.1.2" + checksum: 10c0/60a14f7619dec48c9c850255cd523e2717001b0e179dc7037cfa0895da7b9e9ab07532d324bfb118d73a710887d1e35f79c495fa91582784493e085d18c72c62 + languageName: node + linkType: hard + "readdirp@npm:~3.6.0": version: 3.6.0 resolution: "readdirp@npm:3.6.0" @@ -10456,6 +10937,84 @@ __metadata: languageName: node linkType: hard +"rollup@npm:^4.34.8, rollup@npm:^4.43.0": + version: 4.50.1 + resolution: "rollup@npm:4.50.1" + dependencies: + "@rollup/rollup-android-arm-eabi": "npm:4.50.1" + "@rollup/rollup-android-arm64": "npm:4.50.1" + "@rollup/rollup-darwin-arm64": "npm:4.50.1" + "@rollup/rollup-darwin-x64": "npm:4.50.1" + "@rollup/rollup-freebsd-arm64": "npm:4.50.1" + "@rollup/rollup-freebsd-x64": "npm:4.50.1" + "@rollup/rollup-linux-arm-gnueabihf": "npm:4.50.1" + "@rollup/rollup-linux-arm-musleabihf": "npm:4.50.1" + "@rollup/rollup-linux-arm64-gnu": "npm:4.50.1" + "@rollup/rollup-linux-arm64-musl": "npm:4.50.1" + "@rollup/rollup-linux-loongarch64-gnu": "npm:4.50.1" + "@rollup/rollup-linux-ppc64-gnu": "npm:4.50.1" + "@rollup/rollup-linux-riscv64-gnu": "npm:4.50.1" + "@rollup/rollup-linux-riscv64-musl": "npm:4.50.1" + "@rollup/rollup-linux-s390x-gnu": "npm:4.50.1" + "@rollup/rollup-linux-x64-gnu": "npm:4.50.1" + "@rollup/rollup-linux-x64-musl": "npm:4.50.1" + "@rollup/rollup-openharmony-arm64": "npm:4.50.1" + "@rollup/rollup-win32-arm64-msvc": "npm:4.50.1" + "@rollup/rollup-win32-ia32-msvc": "npm:4.50.1" + "@rollup/rollup-win32-x64-msvc": "npm:4.50.1" + "@types/estree": "npm:1.0.8" + fsevents: "npm:~2.3.2" + dependenciesMeta: + "@rollup/rollup-android-arm-eabi": + optional: true + "@rollup/rollup-android-arm64": + optional: true + "@rollup/rollup-darwin-arm64": + optional: true + "@rollup/rollup-darwin-x64": + optional: true + "@rollup/rollup-freebsd-arm64": + optional: true + "@rollup/rollup-freebsd-x64": + optional: true + "@rollup/rollup-linux-arm-gnueabihf": + optional: true + "@rollup/rollup-linux-arm-musleabihf": + optional: true + "@rollup/rollup-linux-arm64-gnu": + optional: true + "@rollup/rollup-linux-arm64-musl": + optional: true + "@rollup/rollup-linux-loongarch64-gnu": + optional: true + "@rollup/rollup-linux-ppc64-gnu": + optional: true + "@rollup/rollup-linux-riscv64-gnu": + optional: true + "@rollup/rollup-linux-riscv64-musl": + optional: true + "@rollup/rollup-linux-s390x-gnu": + optional: true + "@rollup/rollup-linux-x64-gnu": + optional: true + "@rollup/rollup-linux-x64-musl": + optional: true + "@rollup/rollup-openharmony-arm64": + optional: true + "@rollup/rollup-win32-arm64-msvc": + optional: true + "@rollup/rollup-win32-ia32-msvc": + optional: true + "@rollup/rollup-win32-x64-msvc": + optional: true + fsevents: + optional: true + bin: + rollup: dist/bin/rollup + checksum: 10c0/2029282826d5fb4e308be261b2c28329a4d2bd34304cc3960da69fd21d5acccd0267d6770b1656ffc8f166203ef7e865b4583d5f842a519c8ef059ac71854205 + languageName: node + linkType: hard + "rollup@npm:^4.34.9": version: 4.40.2 resolution: "rollup@npm:4.40.2" @@ -11278,6 +11837,15 @@ __metadata: languageName: node linkType: hard +"strip-literal@npm:^3.0.0": + version: 3.0.0 + resolution: "strip-literal@npm:3.0.0" + dependencies: + js-tokens: "npm:^9.0.1" + checksum: 10c0/d81657f84aba42d4bbaf2a677f7e7f34c1f3de5a6726db8bc1797f9c0b303ba54d4660383a74bde43df401cf37cce1dff2c842c55b077a4ceee11f9e31fba828 + languageName: node + linkType: hard + "styled-jsx@npm:5.1.6": version: 5.1.6 resolution: "styled-jsx@npm:5.1.6" @@ -11301,7 +11869,7 @@ __metadata: languageName: node linkType: hard -"sucrase@npm:^3.20.3": +"sucrase@npm:^3.20.3, sucrase@npm:^3.35.0": version: 3.35.0 resolution: "sucrase@npm:3.35.0" dependencies: @@ -11444,13 +12012,12 @@ __metadata: resolution: "test-opex-api@workspace:apps/test-opex-api" dependencies: "@pagopa/eslint-config": "npm:^5.0.0" + "@pagopa/opex-dashboard-ts": "workspace:^" "@pagopa/typescript-config-node": "workspace:^" "@types/node": "npm:^22.15.14" cdktf: "npm:^0.21.0" - cdktf-monitoring-stack: "workspace:*" constructs: "npm:^10.4.2" eslint: "npm:9.30.1" - opex-common: "workspace:*" prettier: "npm:^3.6.2" typescript: "npm:^5.8.3" languageName: unknown @@ -11504,6 +12071,16 @@ __metadata: languageName: node linkType: hard +"tinyglobby@npm:^0.2.11, tinyglobby@npm:^0.2.14, tinyglobby@npm:^0.2.15": + version: 0.2.15 + resolution: "tinyglobby@npm:0.2.15" + dependencies: + fdir: "npm:^6.5.0" + picomatch: "npm:^4.0.3" + checksum: 10c0/869c31490d0d88eedb8305d178d4c75e7463e820df5a9b9d388291daf93e8b1eb5de1dad1c1e139767e4269fe75f3b10d5009b2cc14db96ff98986920a186844 + languageName: node + linkType: hard + "tinyglobby@npm:^0.2.12, tinyglobby@npm:^0.2.13": version: 0.2.13 resolution: "tinyglobby@npm:0.2.13" @@ -11531,6 +12108,13 @@ __metadata: languageName: node linkType: hard +"tinypool@npm:^1.1.1": + version: 1.1.1 + resolution: "tinypool@npm:1.1.1" + checksum: 10c0/bf26727d01443061b04fa863f571016950888ea994ba0cd8cba3a1c51e2458d84574341ab8dbc3664f1c3ab20885c8cf9ff1cc4b18201f04c2cde7d317fff69b + languageName: node + linkType: hard + "tinyrainbow@npm:^2.0.0": version: 2.0.0 resolution: "tinyrainbow@npm:2.0.0" @@ -11545,6 +12129,13 @@ __metadata: languageName: node linkType: hard +"tinyspy@npm:^4.0.3": + version: 4.0.3 + resolution: "tinyspy@npm:4.0.3" + checksum: 10c0/0a92a18b5350945cc8a1da3a22c9ad9f4e2945df80aaa0c43e1b3a3cfb64d8501e607ebf0305e048e3c3d3e0e7f8eb10cea27dc17c21effb73e66c4a3be36373 + languageName: node + linkType: hard + "tmp@npm:^0.0.33": version: 0.0.33 resolution: "tmp@npm:0.0.33" @@ -11572,13 +12163,11 @@ __metadata: "@vitest/coverage-v8": "npm:^3.1.4" azure-functions-core-tools: "npm:^4.0.7317" cdktf: "npm:^0.21.0" - cdktf-monitoring-stack: "workspace:*" constructs: "npm:^10.4.2" esbuild: "npm:^0.25.4" eslint: "npm:9.30.1" fp-ts: "npm:^2.16.10" io-ts: "npm:^2.2.22" - opex-common: "workspace:*" prettier: "npm:^3.6.2" shx: "npm:^0.4.0" swagger-cli: "npm:^4.0.4" @@ -11794,6 +12383,48 @@ __metadata: languageName: node linkType: hard +"tsup@npm:^8.5.0": + version: 8.5.0 + resolution: "tsup@npm:8.5.0" + dependencies: + bundle-require: "npm:^5.1.0" + cac: "npm:^6.7.14" + chokidar: "npm:^4.0.3" + consola: "npm:^3.4.0" + debug: "npm:^4.4.0" + esbuild: "npm:^0.25.0" + fix-dts-default-cjs-exports: "npm:^1.0.0" + joycon: "npm:^3.1.1" + picocolors: "npm:^1.1.1" + postcss-load-config: "npm:^6.0.1" + resolve-from: "npm:^5.0.0" + rollup: "npm:^4.34.8" + source-map: "npm:0.8.0-beta.0" + sucrase: "npm:^3.35.0" + tinyexec: "npm:^0.3.2" + tinyglobby: "npm:^0.2.11" + tree-kill: "npm:^1.2.2" + peerDependencies: + "@microsoft/api-extractor": ^7.36.0 + "@swc/core": ^1 + postcss: ^8.4.12 + typescript: ">=4.5.0" + peerDependenciesMeta: + "@microsoft/api-extractor": + optional: true + "@swc/core": + optional: true + postcss: + optional: true + typescript: + optional: true + bin: + tsup: dist/cli-default.js + tsup-node: dist/cli-node.js + checksum: 10c0/2eddc1138ad992a2e67d826e92e0b0c4f650367355866c77df8368ade9489e0a8bf2b52b352e97fec83dc690af05881c29c489af27acb86ac2cef38b0d029087 + languageName: node + linkType: hard + "tsutils@npm:^3.21.0": version: 3.21.0 resolution: "tsutils@npm:3.21.0" @@ -11995,6 +12626,16 @@ __metadata: languageName: node linkType: hard +"typescript@npm:^5.0.0": + version: 5.9.2 + resolution: "typescript@npm:5.9.2" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 10c0/cd635d50f02d6cf98ed42de2f76289701c1ec587a363369255f01ed15aaf22be0813226bff3c53e99d971f9b540e0b3cc7583dbe05faded49b1b0bed2f638a18 + languageName: node + linkType: hard + "typescript@npm:^5.8.3, typescript@npm:~5.8.3": version: 5.8.3 resolution: "typescript@npm:5.8.3" @@ -12015,6 +12656,16 @@ __metadata: languageName: node linkType: hard +"typescript@patch:typescript@npm%3A^5.0.0#optional!builtin": + version: 5.9.2 + resolution: "typescript@patch:typescript@npm%3A5.9.2#optional!builtin::version=5.9.2&hash=5786d5" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 10c0/34d2a8e23eb8e0d1875072064d5e1d9c102e0bdce56a10a25c0b917b8aa9001a9cf5c225df12497e99da107dc379360bc138163c66b55b95f5b105b50578067e + languageName: node + linkType: hard + "typescript@patch:typescript@npm%3A^5.8.3#optional!builtin, typescript@patch:typescript@npm%3A~5.8.3#optional!builtin": version: 5.8.3 resolution: "typescript@patch:typescript@npm%3A5.8.3#optional!builtin::version=5.8.3&hash=5786d5" @@ -12025,6 +12676,13 @@ __metadata: languageName: node linkType: hard +"ufo@npm:^1.6.1": + version: 1.6.1 + resolution: "ufo@npm:1.6.1" + checksum: 10c0/5a9f041e5945fba7c189d5410508cbcbefef80b253ed29aa2e1f8a2b86f4bd51af44ee18d4485e6d3468c92be9bf4a42e3a2b72dcaf27ce39ce947ec994f1e6b + languageName: node + linkType: hard + "ulid@npm:^2.3.0": version: 2.4.0 resolution: "ulid@npm:2.4.0" @@ -12248,6 +12906,21 @@ __metadata: languageName: node linkType: hard +"vite-node@npm:3.2.4": + version: 3.2.4 + resolution: "vite-node@npm:3.2.4" + dependencies: + cac: "npm:^6.7.14" + debug: "npm:^4.4.1" + es-module-lexer: "npm:^1.7.0" + pathe: "npm:^2.0.3" + vite: "npm:^5.0.0 || ^6.0.0 || ^7.0.0-0" + bin: + vite-node: vite-node.mjs + checksum: 10c0/6ceca67c002f8ef6397d58b9539f80f2b5d79e103a18367288b3f00a8ab55affa3d711d86d9112fce5a7fa658a212a087a005a045eb8f4758947dd99af2a6c6b + languageName: node + linkType: hard + "vite@npm:^5.0.0 || ^6.0.0": version: 6.3.5 resolution: "vite@npm:6.3.5" @@ -12303,6 +12976,61 @@ __metadata: languageName: node linkType: hard +"vite@npm:^5.0.0 || ^6.0.0 || ^7.0.0-0": + version: 7.1.5 + resolution: "vite@npm:7.1.5" + dependencies: + esbuild: "npm:^0.25.0" + fdir: "npm:^6.5.0" + fsevents: "npm:~2.3.3" + picomatch: "npm:^4.0.3" + postcss: "npm:^8.5.6" + rollup: "npm:^4.43.0" + tinyglobby: "npm:^0.2.15" + peerDependencies: + "@types/node": ^20.19.0 || >=22.12.0 + jiti: ">=1.21.0" + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: ">=0.54.8" + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + dependenciesMeta: + fsevents: + optional: true + peerDependenciesMeta: + "@types/node": + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + bin: + vite: bin/vite.js + checksum: 10c0/782d2f20c25541b26d1fb39bef5f194149caff39dc25b7836e25f049ca919f2e2ce186bddb21f3f20f6195354b3579ec637a8ca08d65b117f8b6f81e3e730a9c + languageName: node + linkType: hard + "vitest-mock-extended@npm:^3.1.0": version: 3.1.0 resolution: "vitest-mock-extended@npm:3.1.0" @@ -12369,6 +13097,62 @@ __metadata: languageName: node linkType: hard +"vitest@npm:^3.2.4": + version: 3.2.4 + resolution: "vitest@npm:3.2.4" + dependencies: + "@types/chai": "npm:^5.2.2" + "@vitest/expect": "npm:3.2.4" + "@vitest/mocker": "npm:3.2.4" + "@vitest/pretty-format": "npm:^3.2.4" + "@vitest/runner": "npm:3.2.4" + "@vitest/snapshot": "npm:3.2.4" + "@vitest/spy": "npm:3.2.4" + "@vitest/utils": "npm:3.2.4" + chai: "npm:^5.2.0" + debug: "npm:^4.4.1" + expect-type: "npm:^1.2.1" + magic-string: "npm:^0.30.17" + pathe: "npm:^2.0.3" + picomatch: "npm:^4.0.2" + std-env: "npm:^3.9.0" + tinybench: "npm:^2.9.0" + tinyexec: "npm:^0.3.2" + tinyglobby: "npm:^0.2.14" + tinypool: "npm:^1.1.1" + tinyrainbow: "npm:^2.0.0" + vite: "npm:^5.0.0 || ^6.0.0 || ^7.0.0-0" + vite-node: "npm:3.2.4" + why-is-node-running: "npm:^2.3.0" + peerDependencies: + "@edge-runtime/vm": "*" + "@types/debug": ^4.1.12 + "@types/node": ^18.0.0 || ^20.0.0 || >=22.0.0 + "@vitest/browser": 3.2.4 + "@vitest/ui": 3.2.4 + happy-dom: "*" + jsdom: "*" + peerDependenciesMeta: + "@edge-runtime/vm": + optional: true + "@types/debug": + optional: true + "@types/node": + optional: true + "@vitest/browser": + optional: true + "@vitest/ui": + optional: true + happy-dom: + optional: true + jsdom: + optional: true + bin: + vitest: vitest.mjs + checksum: 10c0/5bf53ede3ae6a0e08956d72dab279ae90503f6b5a05298a6a5e6ef47d2fd1ab386aaf48fafa61ed07a0ebfe9e371772f1ccbe5c258dd765206a8218bf2eb79eb + languageName: node + linkType: hard + "vue-eslint-parser@npm:^9.4.3": version: 9.4.3 resolution: "vue-eslint-parser@npm:9.4.3" @@ -12772,3 +13556,10 @@ __metadata: checksum: 10c0/53cc090b949ed1776e241fb882d5de0f66096727aabbfb53ff9c64c875b575c9d3cbd75556bc816bcdd09aff0a26608e1c40d79dd863ab5c31a96331f1e7a4b9 languageName: node linkType: hard + +"zod@npm:^4.1.5": + version: 4.1.5 + resolution: "zod@npm:4.1.5" + checksum: 10c0/7826fb931bc71d4d0fff2fbb72f1a1cf30a6672cf9dbe6933a216bbb60242ef1c3bdfbcd3c5b27e806235a35efaad7a4a9897ff4d3621452f9ea278bce6fd42a + languageName: node + linkType: hard