Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 58 additions & 14 deletions .github/workflows/monthly-capacity-report.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,39 +13,84 @@ permissions:
id-token: write # Required for AWS OIDC authentication

jobs:
generate-report:
export-dashboards:
runs-on: ubuntu-latest
environment: prod
environment: reporting
strategy:
matrix:
env_config:
- name: Prod
dashboard: Demand_And_Capacity_Prod
account_secret: AWS_PROD_ACCOUNT_ID
- name: Preprod
dashboard: Demand_And_Capacity_Preprod
account_secret: AWS_PREPROD_ACCOUNT_ID
- name: Test
dashboard: Demand_And_Capacity_Test
account_secret: AWS_TEST_ACCOUNT_ID
- name: Dev
dashboard: Demand_And_Capacity_Dev
account_secret: AWS_DEV_ACCOUNT_ID

steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v6
uses: actions/setup-python@v5
with:
python-version: "3.11"

- name: "Configure AWS Credentials"
- name: Configure AWS Credentials (${{ matrix.env_config.name }})
uses: aws-actions/configure-aws-credentials@v5
with:
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/service-roles/github-actions-api-deployment-role
role-to-assume: arn:aws:iam::${{ secrets[matrix.env_config.account_secret] }}:role/service-roles/github-actions-api-deployment-role
aws-region: eu-west-2

- name: Generate dashboard report
- name: Export Dashboard (${{ matrix.env_config.name }})
run: |
chmod +x scripts/export_dashboard_image.sh
./scripts/export_dashboard_image.sh Demand_And_Capacity_Prod
./scripts/export_dashboard_image.sh ${{ matrix.env_config.dashboard }} ${{ matrix.env_config.name }}
env:
AWS_REGION: eu-west-2

- name: Upload dashboard export
uses: actions/upload-artifact@v4
with:
name: dashboard-${{ matrix.env_config.name }}
path: dashboard_exports/**/*

generate-report:
runs-on: ubuntu-latest
needs: export-dashboards
environment: reporting

steps:
- name: Checkout code
uses: actions/checkout@v6

- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.11"

- name: Download all dashboard exports
uses: actions/download-artifact@v4
with:
path: dashboard_exports
pattern: dashboard-*
merge-multiple: true

- name: Generate Combined Report
run: python3 scripts/generate_dashboard_report.py --input dashboard_exports

- name: Upload report as artifact
uses: actions/upload-artifact@v5
with:
name: capacity-report
path: |
dashboard_exports/*.html
dashboard_exports/*.png
dashboard_exports/**/*.html
dashboard_exports/**/*.png
retention-days: 90

- name: Send to Slack
Expand All @@ -54,21 +99,20 @@ jobs:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_D_AND_C_WEBHOOK }}
run: |
# Get the latest HTML report
REPORT_FILE=$(ls -t dashboard_exports/dashboard_report_*.html | head -1)
REPORT_FILE=$(find dashboard_exports -name "dashboard_report_*.html" | head -n 1)
REPORT_NAME=$(basename "$REPORT_FILE")

# GitHub Actions URL
GITHUB_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"

# Send Slack notification with simple variables for Workflow Automation
# Send Slack notification
curl -X POST "$SLACK_WEBHOOK_URL" \
-H 'Content-Type: application/json' \
-d @- <<EOF
{
"report_title": "📊 Monthly Demand & Capacity Report - EliD - Prod",
"report_title": "📊 Monthly Demand & Capacity Report - EliD - All Envs",
"report_period": "Last 8 weeks",
"generated_date": "$(date +'%Y-%m-%d %H:%M UTC')",
"widgets_count": "7",
"github_url": "$GITHUB_URL",
"report_name": "$REPORT_NAME"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -639,13 +639,23 @@ resource "aws_iam_policy" "firehose_readonly" {
}

resource "aws_iam_policy" "cloudwatch_management" {
#checkov:skip=CKV_AWS_355: GetMetricWidgetImage requires wildcard resource
#checkov:skip=CKV_AWS_290: GetMetricWidgetImage requires wildcard resource
name = "cloudwatch-management"
description = "Allow GitHub Actions to manage CloudWatch logs, alarms, and SNS topics"
path = "/service-policies/"

policy = jsonencode({
Version = "2012-10-17",
Statement = [
{
Effect = "Allow",
Action = [
# GetMetricWidgetImage does not support resource-level permissions
"cloudwatch:GetMetricWidgetImage"
],
Resource = "*"
},
{
Effect = "Allow",
Action = [
Expand All @@ -663,6 +673,7 @@ resource "aws_iam_policy" "cloudwatch_management" {
"cloudwatch:ListTagsForResource",
"cloudwatch:TagResource",
"cloudwatch:UntagResource",
"cloudwatch:GetDashboard",

"sns:CreateTopic",
"sns:DeleteTopic",
Expand All @@ -683,6 +694,7 @@ resource "aws_iam_policy" "cloudwatch_management" {
"arn:aws:logs:${var.default_aws_region}:${data.aws_caller_identity.current.account_id}:log-group:aws-wafv2-logs-*",
"arn:aws:logs:${var.default_aws_region}:${data.aws_caller_identity.current.account_id}:log-group:aws-waf-logs-*",
"arn:aws:cloudwatch:${var.default_aws_region}:${data.aws_caller_identity.current.account_id}:alarm:*",
"arn:aws:cloudwatch::${data.aws_caller_identity.current.account_id}:dashboard/Demand_And_Capacity_*",
"arn:aws:sns:${var.default_aws_region}:${data.aws_caller_identity.current.account_id}:cloudwatch-security-alarms*",
"arn:aws:logs:${var.default_aws_region}:${data.aws_caller_identity.current.account_id}:log-group:/aws/apigateway/default-eligibility-signposting-api*",
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ data "aws_iam_policy_document" "permissions_boundary" {
"cloudwatch:ListTagsForResource",
"cloudwatch:TagResource",
"cloudwatch:UntagResource",
"cloudwatch:GetDashboard",
"cloudwatch:GetMetricWidgetImage",

# DynamoDB - table management
"dynamodb:DescribeTimeToLive",
Expand Down
158 changes: 158 additions & 0 deletions scripts/dashboard_report.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
/* NHS Dashboard Report Styles */
:root {
--nhs-blue: #005EB8;
--nhs-white: #FFFFFF;
--nhs-black: #231f20;
--nhs-dark-grey: #425563;
--nhs-mid-grey: #768692;
--nhs-pale-grey: #E8EDEE;
--nhs-warm-yellow: #FFB81C;
}

* {
margin: 0;
padding: 0;
box-sizing: border-box;
}

body {
font-family: "Frutiger W01", Arial, sans-serif;
background: var(--nhs-pale-grey);
color: var(--nhs-black);
line-height: 1.5;
}

.nhs-header {
background-color: var(--nhs-blue);
color: var(--nhs-white);
padding: 24px 0;
margin-bottom: 32px;
}

.nhs-container {
max-width: 960px;
margin: 0 auto;
padding: 0 16px;
}

.nhs-logo {
font-weight: 700;
font-size: 24px;
letter-spacing: -0.5px;
display: inline-block;
margin-right: 16px;
padding-right: 16px;
border-right: 1px solid rgba(255, 255, 255, 0.3);
}

.report-title {
font-size: 24px;
font-weight: 600;
display: inline-block;
}

.report-meta {
margin-top: 8px;
font-size: 14px;
opacity: 0.9;
}

.content {
padding-bottom: 48px;
}

.section-header {
background: var(--nhs-white);
padding: 16px 24px;
margin-bottom: 24px;
border-left: 8px solid var(--nhs-blue);
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}

.section-header h2 {
font-size: 24px;
color: var(--nhs-blue);
margin: 0;
}

.env-section {
margin-bottom: 48px;
}

.env-title {
font-size: 20px;
color: var(--nhs-dark-grey);
margin-bottom: 20px;
padding-bottom: 8px;
border-bottom: 2px solid #d8dde0;
}

.widget-card {
background: var(--nhs-white);
border: 1px solid #d8dde0;
border-bottom: 4px solid var(--nhs-blue);
margin-bottom: 32px;
padding: 24px;
page-break-inside: avoid;
}

.widget-header {
margin-bottom: 16px;
border-bottom: 1px solid var(--nhs-pale-grey);
padding-bottom: 16px;
}

.widget-title {
font-size: 19px;
font-weight: 600;
color: var(--nhs-black);
margin-bottom: 8px;
}

.widget-description {
font-size: 16px;
color: var(--nhs-dark-grey);
background: #f0f4f5;
padding: 12px;
border-left: 4px solid var(--nhs-mid-grey);
}

.widget-image-container {
margin-top: 20px;
text-align: center;
}

.widget-image {
max-width: 100%;
height: auto;
border: 1px solid var(--nhs-pale-grey);
}

.footer {
text-align: center;
padding: 32px 0;
color: var(--nhs-mid-grey);
font-size: 14px;
border-top: 1px solid #d8dde0;
margin-top: 48px;
}

@media print {
body {
background: white;
}
.nhs-header {
background: white;
color: black;
border-bottom: 2px solid var(--nhs-blue);
}
.widget-card {
border: none;
border-bottom: 1px solid #ccc;
}
.section-header {
border: none;
padding: 0;
margin-bottom: 16px;
}
}
15 changes: 6 additions & 9 deletions scripts/export_dashboard_image.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
set -e

DASHBOARD_NAME="${1:-Demand_And_Capacity_Prod}"
OUTPUT_DIR="dashboard_exports"
ENVIRONMENT="${2:-Prod}"
OUTPUT_BASE="dashboard_exports"
OUTPUT_DIR="${OUTPUT_BASE}/${ENVIRONMENT}"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
REGION="${AWS_REGION:-eu-west-2}"

Expand All @@ -15,6 +17,7 @@ echo "========================================="
echo "CloudWatch Dashboard Image Export"
echo "========================================="
echo "Dashboard: $DASHBOARD_NAME"
echo "Environment: $ENVIRONMENT"
echo "Region: $REGION"
echo "Output: $OUTPUT_DIR"
echo ""
Expand Down Expand Up @@ -49,7 +52,7 @@ import sys
import os

# Read dashboard definition
with open(f"dashboard_exports/dashboard_definition_{os.environ['TIMESTAMP']}.json") as f:
with open(f"{os.environ['OUTPUT_DIR']}/dashboard_definition_{os.environ['TIMESTAMP']}.json") as f:
dashboard_data = json.load(f)

# Parse the dashboard body
Expand Down Expand Up @@ -133,10 +136,4 @@ print("========================================")
PYTHON_SCRIPT

echo ""
echo "Generating HTML report..."
python3 scripts/generate_dashboard_report.py --input "${OUTPUT_DIR}"

echo ""
echo "✓ Complete! Check the ${OUTPUT_DIR}/ directory for:"
echo " - Individual widget images (PNG files)"
echo " - Combined HTML report (dashboard_report_*.html)"
echo "✓ Export for $ENVIRONMENT complete!"
Loading
Loading