Skip to content

Commit e21b6a7

Browse files
authored
Merge pull request #494 from NHSDigital/feature/eja-eli-420-setting-custom-env
Creating a HTML 'Capacity and Demand' report
2 parents 453a998 + 6669b98 commit e21b6a7

File tree

6 files changed

+406
-180
lines changed

6 files changed

+406
-180
lines changed

.github/workflows/monthly-capacity-report.yml

Lines changed: 58 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,39 +13,84 @@ permissions:
1313
id-token: write # Required for AWS OIDC authentication
1414

1515
jobs:
16-
generate-report:
16+
export-dashboards:
1717
runs-on: ubuntu-latest
18-
environment: prod
18+
environment: reporting
19+
strategy:
20+
matrix:
21+
env_config:
22+
- name: Prod
23+
dashboard: Demand_And_Capacity_Prod
24+
account_secret: AWS_PROD_ACCOUNT_ID
25+
- name: Preprod
26+
dashboard: Demand_And_Capacity_Preprod
27+
account_secret: AWS_PREPROD_ACCOUNT_ID
28+
- name: Test
29+
dashboard: Demand_And_Capacity_Test
30+
account_secret: AWS_TEST_ACCOUNT_ID
31+
- name: Dev
32+
dashboard: Demand_And_Capacity_Dev
33+
account_secret: AWS_DEV_ACCOUNT_ID
1934

2035
steps:
2136
- name: Checkout code
22-
uses: actions/checkout@v6
37+
uses: actions/checkout@v4
2338

2439
- name: Set up Python
25-
uses: actions/setup-python@v6
40+
uses: actions/setup-python@v5
2641
with:
2742
python-version: "3.11"
2843

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

35-
- name: Generate dashboard report
50+
- name: Export Dashboard (${{ matrix.env_config.name }})
3651
run: |
3752
chmod +x scripts/export_dashboard_image.sh
38-
./scripts/export_dashboard_image.sh Demand_And_Capacity_Prod
53+
./scripts/export_dashboard_image.sh ${{ matrix.env_config.dashboard }} ${{ matrix.env_config.name }}
3954
env:
4055
AWS_REGION: eu-west-2
4156

57+
- name: Upload dashboard export
58+
uses: actions/upload-artifact@v4
59+
with:
60+
name: dashboard-${{ matrix.env_config.name }}
61+
path: dashboard_exports/**/*
62+
63+
generate-report:
64+
runs-on: ubuntu-latest
65+
needs: export-dashboards
66+
environment: reporting
67+
68+
steps:
69+
- name: Checkout code
70+
uses: actions/checkout@v6
71+
72+
- name: Set up Python
73+
uses: actions/setup-python@v6
74+
with:
75+
python-version: "3.11"
76+
77+
- name: Download all dashboard exports
78+
uses: actions/download-artifact@v4
79+
with:
80+
path: dashboard_exports
81+
pattern: dashboard-*
82+
merge-multiple: true
83+
84+
- name: Generate Combined Report
85+
run: python3 scripts/generate_dashboard_report.py --input dashboard_exports
86+
4287
- name: Upload report as artifact
4388
uses: actions/upload-artifact@v5
4489
with:
4590
name: capacity-report
4691
path: |
47-
dashboard_exports/*.html
48-
dashboard_exports/*.png
92+
dashboard_exports/**/*.html
93+
dashboard_exports/**/*.png
4994
retention-days: 90
5095

5196
- name: Send to Slack
@@ -54,21 +99,20 @@ jobs:
5499
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_D_AND_C_WEBHOOK }}
55100
run: |
56101
# Get the latest HTML report
57-
REPORT_FILE=$(ls -t dashboard_exports/dashboard_report_*.html | head -1)
102+
REPORT_FILE=$(find dashboard_exports -name "dashboard_report_*.html" | head -n 1)
58103
REPORT_NAME=$(basename "$REPORT_FILE")
59104
60105
# GitHub Actions URL
61106
GITHUB_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
62107
63-
# Send Slack notification with simple variables for Workflow Automation
108+
# Send Slack notification
64109
curl -X POST "$SLACK_WEBHOOK_URL" \
65110
-H 'Content-Type: application/json' \
66111
-d @- <<EOF
67112
{
68-
"report_title": "📊 Monthly Demand & Capacity Report - EliD - Prod",
113+
"report_title": "📊 Monthly Demand & Capacity Report - EliD - All Envs",
69114
"report_period": "Last 8 weeks",
70115
"generated_date": "$(date +'%Y-%m-%d %H:%M UTC')",
71-
"widgets_count": "7",
72116
"github_url": "$GITHUB_URL",
73117
"report_name": "$REPORT_NAME"
74118
}

infrastructure/stacks/iams-developer-roles/github_actions_policies.tf

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -639,13 +639,23 @@ resource "aws_iam_policy" "firehose_readonly" {
639639
}
640640

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

646648
policy = jsonencode({
647649
Version = "2012-10-17",
648650
Statement = [
651+
{
652+
Effect = "Allow",
653+
Action = [
654+
# GetMetricWidgetImage does not support resource-level permissions
655+
"cloudwatch:GetMetricWidgetImage"
656+
],
657+
Resource = "*"
658+
},
649659
{
650660
Effect = "Allow",
651661
Action = [
@@ -663,6 +673,7 @@ resource "aws_iam_policy" "cloudwatch_management" {
663673
"cloudwatch:ListTagsForResource",
664674
"cloudwatch:TagResource",
665675
"cloudwatch:UntagResource",
676+
"cloudwatch:GetDashboard",
666677

667678
"sns:CreateTopic",
668679
"sns:DeleteTopic",
@@ -683,6 +694,7 @@ resource "aws_iam_policy" "cloudwatch_management" {
683694
"arn:aws:logs:${var.default_aws_region}:${data.aws_caller_identity.current.account_id}:log-group:aws-wafv2-logs-*",
684695
"arn:aws:logs:${var.default_aws_region}:${data.aws_caller_identity.current.account_id}:log-group:aws-waf-logs-*",
685696
"arn:aws:cloudwatch:${var.default_aws_region}:${data.aws_caller_identity.current.account_id}:alarm:*",
697+
"arn:aws:cloudwatch::${data.aws_caller_identity.current.account_id}:dashboard/Demand_And_Capacity_*",
686698
"arn:aws:sns:${var.default_aws_region}:${data.aws_caller_identity.current.account_id}:cloudwatch-security-alarms*",
687699
"arn:aws:logs:${var.default_aws_region}:${data.aws_caller_identity.current.account_id}:log-group:/aws/apigateway/default-eligibility-signposting-api*",
688700
]

infrastructure/stacks/iams-developer-roles/iams_permissions_boundary.tf

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ data "aws_iam_policy_document" "permissions_boundary" {
2626
"cloudwatch:ListTagsForResource",
2727
"cloudwatch:TagResource",
2828
"cloudwatch:UntagResource",
29+
"cloudwatch:GetDashboard",
30+
"cloudwatch:GetMetricWidgetImage",
2931

3032
# DynamoDB - table management
3133
"dynamodb:DescribeTimeToLive",

scripts/dashboard_report.css

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
/* NHS Dashboard Report Styles */
2+
:root {
3+
--nhs-blue: #005EB8;
4+
--nhs-white: #FFFFFF;
5+
--nhs-black: #231f20;
6+
--nhs-dark-grey: #425563;
7+
--nhs-mid-grey: #768692;
8+
--nhs-pale-grey: #E8EDEE;
9+
--nhs-warm-yellow: #FFB81C;
10+
}
11+
12+
* {
13+
margin: 0;
14+
padding: 0;
15+
box-sizing: border-box;
16+
}
17+
18+
body {
19+
font-family: "Frutiger W01", Arial, sans-serif;
20+
background: var(--nhs-pale-grey);
21+
color: var(--nhs-black);
22+
line-height: 1.5;
23+
}
24+
25+
.nhs-header {
26+
background-color: var(--nhs-blue);
27+
color: var(--nhs-white);
28+
padding: 24px 0;
29+
margin-bottom: 32px;
30+
}
31+
32+
.nhs-container {
33+
max-width: 960px;
34+
margin: 0 auto;
35+
padding: 0 16px;
36+
}
37+
38+
.nhs-logo {
39+
font-weight: 700;
40+
font-size: 24px;
41+
letter-spacing: -0.5px;
42+
display: inline-block;
43+
margin-right: 16px;
44+
padding-right: 16px;
45+
border-right: 1px solid rgba(255, 255, 255, 0.3);
46+
}
47+
48+
.report-title {
49+
font-size: 24px;
50+
font-weight: 600;
51+
display: inline-block;
52+
}
53+
54+
.report-meta {
55+
margin-top: 8px;
56+
font-size: 14px;
57+
opacity: 0.9;
58+
}
59+
60+
.content {
61+
padding-bottom: 48px;
62+
}
63+
64+
.section-header {
65+
background: var(--nhs-white);
66+
padding: 16px 24px;
67+
margin-bottom: 24px;
68+
border-left: 8px solid var(--nhs-blue);
69+
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
70+
}
71+
72+
.section-header h2 {
73+
font-size: 24px;
74+
color: var(--nhs-blue);
75+
margin: 0;
76+
}
77+
78+
.env-section {
79+
margin-bottom: 48px;
80+
}
81+
82+
.env-title {
83+
font-size: 20px;
84+
color: var(--nhs-dark-grey);
85+
margin-bottom: 20px;
86+
padding-bottom: 8px;
87+
border-bottom: 2px solid #d8dde0;
88+
}
89+
90+
.widget-card {
91+
background: var(--nhs-white);
92+
border: 1px solid #d8dde0;
93+
border-bottom: 4px solid var(--nhs-blue);
94+
margin-bottom: 32px;
95+
padding: 24px;
96+
page-break-inside: avoid;
97+
}
98+
99+
.widget-header {
100+
margin-bottom: 16px;
101+
border-bottom: 1px solid var(--nhs-pale-grey);
102+
padding-bottom: 16px;
103+
}
104+
105+
.widget-title {
106+
font-size: 19px;
107+
font-weight: 600;
108+
color: var(--nhs-black);
109+
margin-bottom: 8px;
110+
}
111+
112+
.widget-description {
113+
font-size: 16px;
114+
color: var(--nhs-dark-grey);
115+
background: #f0f4f5;
116+
padding: 12px;
117+
border-left: 4px solid var(--nhs-mid-grey);
118+
}
119+
120+
.widget-image-container {
121+
margin-top: 20px;
122+
text-align: center;
123+
}
124+
125+
.widget-image {
126+
max-width: 100%;
127+
height: auto;
128+
border: 1px solid var(--nhs-pale-grey);
129+
}
130+
131+
.footer {
132+
text-align: center;
133+
padding: 32px 0;
134+
color: var(--nhs-mid-grey);
135+
font-size: 14px;
136+
border-top: 1px solid #d8dde0;
137+
margin-top: 48px;
138+
}
139+
140+
@media print {
141+
body {
142+
background: white;
143+
}
144+
.nhs-header {
145+
background: white;
146+
color: black;
147+
border-bottom: 2px solid var(--nhs-blue);
148+
}
149+
.widget-card {
150+
border: none;
151+
border-bottom: 1px solid #ccc;
152+
}
153+
.section-header {
154+
border: none;
155+
padding: 0;
156+
margin-bottom: 16px;
157+
}
158+
}

scripts/export_dashboard_image.sh

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
set -e
55

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

@@ -15,6 +17,7 @@ echo "========================================="
1517
echo "CloudWatch Dashboard Image Export"
1618
echo "========================================="
1719
echo "Dashboard: $DASHBOARD_NAME"
20+
echo "Environment: $ENVIRONMENT"
1821
echo "Region: $REGION"
1922
echo "Output: $OUTPUT_DIR"
2023
echo ""
@@ -49,7 +52,7 @@ import sys
4952
import os
5053
5154
# Read dashboard definition
52-
with open(f"dashboard_exports/dashboard_definition_{os.environ['TIMESTAMP']}.json") as f:
55+
with open(f"{os.environ['OUTPUT_DIR']}/dashboard_definition_{os.environ['TIMESTAMP']}.json") as f:
5356
dashboard_data = json.load(f)
5457
5558
# Parse the dashboard body
@@ -133,10 +136,4 @@ print("========================================")
133136
PYTHON_SCRIPT
134137

135138
echo ""
136-
echo "Generating HTML report..."
137-
python3 scripts/generate_dashboard_report.py --input "${OUTPUT_DIR}"
138-
139-
echo ""
140-
echo "✓ Complete! Check the ${OUTPUT_DIR}/ directory for:"
141-
echo " - Individual widget images (PNG files)"
142-
echo " - Combined HTML report (dashboard_report_*.html)"
139+
echo "✓ Export for $ENVIRONMENT complete!"

0 commit comments

Comments
 (0)