Skip to content

Commit cd95d67

Browse files
authored
combine compliance results for regions/k8s branches (#949)
* Refactoring: reverse order of arguments * aggregate results for regions in table * Provide details page for subject group (multiple regions or k8s versions) * Links to individual subjects at the top * fix: skip unnecessary upgrade step for empty db Signed-off-by: Matthias Büchse <matthias.buechse@alasca.cloud>
1 parent 633852c commit cd95d67

File tree

5 files changed

+100
-38
lines changed

5 files changed

+100
-38
lines changed

compliance-monitor/bootstrap.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,19 @@ accounts:
3131
public_key_type: "ssh-ed25519"
3232
public_key_name: "primary"
3333
- subject: pco-prod1
34+
group: pco-prod
3435
delegates:
3536
- zuul_ci
3637
- subject: pco-prod2
38+
group: pco-prod
3739
delegates:
3840
- zuul_ci
3941
- subject: pco-prod3
42+
group: pco-prod
4043
delegates:
4144
- zuul_ci
4245
- subject: pco-prod4
46+
group: pco-prod
4347
delegates:
4448
- zuul_ci
4549
- subject: poc-wgcloud
@@ -56,9 +60,11 @@ accounts:
5660
delegates:
5761
- zuul_ci
5862
- subject: syseleven-dus2
63+
group: syseleven
5964
delegates:
6065
- zuul_ci
6166
- subject: syseleven-ham1
67+
group: syseleven
6268
delegates:
6369
- zuul_ci
6470
- subject: wavestack

compliance-monitor/monitor.py

Lines changed: 46 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
db_find_account, db_update_account, db_update_publickey, db_filter_publickeys, db_get_reports,
4242
db_get_keys, db_insert_report, db_get_recent_results2, db_patch_approval2, db_get_report,
4343
db_ensure_schema, db_get_apikeys, db_update_apikey, db_filter_apikeys, db_clear_delegates,
44-
db_find_subjects, db_insert_result2, db_get_relevant_results2, db_add_delegate,
44+
db_find_subjects, db_insert_result2, db_get_relevant_results2, db_add_delegate, db_get_group,
4545
)
4646

4747

@@ -79,6 +79,7 @@ def __init__(self):
7979
self.yaml_path = os.path.abspath("../Tests")
8080

8181

82+
GROUP_PREFIX = 'group-'
8283
ROLES = {'read_any': 1, 'append_any': 2, 'admin': 4, 'approve': 8}
8384
# number of days that expired results will be considered in lieu of more recent, but unapproved ones
8485
GRACE_PERIOD_DAYS = 7
@@ -226,7 +227,8 @@ def import_bootstrap(bootstrap_path, conn):
226227
with conn.cursor() as cur:
227228
for account in accounts:
228229
roles = sum(ROLES[r] for r in account.get('roles', ()))
229-
accountid = db_update_account(cur, {'subject': account['subject'], 'roles': roles})
230+
acc_record = {'subject': account['subject'], 'roles': roles, 'group': account.get('group')}
231+
accountid = db_update_account(cur, acc_record)
230232
db_clear_delegates(cur, accountid)
231233
for delegate in account.get('delegates', ()):
232234
db_add_delegate(cur, accountid, delegate)
@@ -637,6 +639,13 @@ async def get_report_view_full(
637639
)
638640

639641

642+
def _resolve_group(cur, subject, prefix=GROUP_PREFIX):
643+
group = subject.removeprefix(prefix)
644+
if subject != group:
645+
return group, db_get_group(cur, group)
646+
return None, [subject]
647+
648+
640649
@app.get("/{view_type}/detail/{subject}/{scopeuuid}")
641650
async def get_detail(
642651
request: Request,
@@ -646,14 +655,18 @@ async def get_detail(
646655
scopeuuid: str,
647656
):
648657
with conn.cursor() as cur:
649-
rows2 = db_get_relevant_results2(cur, subject, scopeuuid, approved_only=True)
658+
group, subjects = _resolve_group(cur, subject)
659+
rows2 = []
660+
for subj in subjects:
661+
rows2.extend(db_get_relevant_results2(cur, subj, scopeuuid, approved_only=True))
650662
results2 = convert_result_rows_to_dict2(
651663
rows2, get_scopes(), include_report=True, grace_period_days=GRACE_PERIOD_DAYS,
652664
subjects=(subject, ), scopes=(scopeuuid, ),
653665
)
666+
title = f'Details for group {group}' if group else f'Details for subject {subject}'
654667
return render_view(
655668
VIEW_DETAIL, view_type, results=results2, base_url=settings.base_url,
656-
title=f'{subject} compliance',
669+
title=title,
657670
)
658671

659672

@@ -666,13 +679,17 @@ async def get_detail_full(
666679
scopeuuid: str,
667680
):
668681
with conn.cursor() as cur:
669-
rows2 = db_get_relevant_results2(cur, subject, scopeuuid, approved_only=False)
682+
group, subjects = _resolve_group(cur, subject)
683+
rows2 = []
684+
for subject in subjects:
685+
rows2.extend(db_get_relevant_results2(cur, subject, scopeuuid, approved_only=False))
670686
results2 = convert_result_rows_to_dict2(
671687
rows2, get_scopes(), include_report=True, subjects=(subject, ), scopes=(scopeuuid, ),
672688
)
689+
title = f'Details for group {group}' if group else f'Details for subject {subject}'
673690
return render_view(
674691
VIEW_DETAIL, view_type, results=results2, base_url=settings.base_url,
675-
title=f'{subject} compliance (incl. unverified results)',
692+
title=f'{title} (incl. unverified results)',
676693
)
677694

678695

@@ -785,13 +802,34 @@ async def get_healthz(request: Request):
785802
return Response() # empty response with status 200
786803

787804

788-
def pick_filter(results, subject, scope):
805+
def pick_filter(results, scope, *subjects):
789806
"""Jinja filter to pick scope results from `results` for given `subject` and `scope`"""
790-
return results.get(subject, {}).get(scope, {})
807+
# simple case (backwards compatible): precisely one subject
808+
if len(subjects) == 1:
809+
return results.get(subjects[0], {}).get(scope, {})
810+
# generalized case: multiple subjects
811+
# in this case, drop None
812+
rs = [results.get(subject, {}).get(scope, {}) for subject in subjects]
813+
return [r for r in rs if r is not None]
814+
815+
816+
STATUS_ORDERING = {
817+
'effective': 10,
818+
'warn': 5,
819+
'deprecated': 1,
820+
}
791821

792822

793823
def summary_filter(scope_results):
794824
"""Jinja filter to construct summary from `scope_results`"""
825+
if not isinstance(scope_results, dict):
826+
# new generalized case: "aggregate" results for multiple subjects
827+
# simplified computation: just select the worst subject to represent the group
828+
scope_results = min(
829+
scope_results,
830+
default={},
831+
key=lambda sr: STATUS_ORDERING.get(sr.get('best_passed'), -1),
832+
)
795833
passed_str = scope_results.get('passed_str', '') or '–'
796834
best_passed = scope_results.get('best_passed')
797835
# avoid simple 🟢🔴 (hard to distinguish for color-blind folks)

compliance-monitor/sql.py

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33

44
# list schema versions in ascending order
55
SCHEMA_VERSION_KEY = 'version'
6-
SCHEMA_VERSIONS = ['v1', 'v2', 'v3']
6+
SCHEMA_VERSIONS = ['v1', 'v2', 'v3', 'v4']
77
# use ... (Ellipsis) here to indicate that no default value exists (will lead to error if no value is given)
8-
ACCOUNT_DEFAULTS = {'subject': ..., 'api_key': ..., 'roles': ...}
8+
ACCOUNT_DEFAULTS = {'subject': ..., 'api_key': ..., 'roles': ..., 'group': None}
99
PUBLIC_KEY_DEFAULTS = {'public_key': ..., 'public_key_type': ..., 'public_key_name': ...}
1010

1111

@@ -135,6 +135,14 @@ def db_ensure_schema_v3(cur: cursor):
135135
''')
136136

137137

138+
def db_ensure_schema_v4(cur: cursor):
139+
# start from v3, do small alteration
140+
db_ensure_schema_v2(cur)
141+
cur.execute('''
142+
ALTER TABLE account ADD COLUMN IF NOT EXISTS "group" text;
143+
''')
144+
145+
138146
def db_upgrade_data_v1_v2(cur):
139147
# we are going to drop table result, but use delete anyway to have the transaction safety
140148
cur.execute('''
@@ -191,8 +199,8 @@ def db_upgrade_schema(conn: connection, cur: cursor):
191199
if current is None:
192200
# this is an empty db, but it also used to be the case with v1
193201
# I (mbuechse) made sure manually that the value v1 is set on running installations
194-
db_ensure_schema_v3(cur)
195-
db_set_schema_version(cur, 'v3')
202+
db_ensure_schema_v4(cur)
203+
db_set_schema_version(cur, 'v4')
196204
conn.commit()
197205
elif current == 'v1':
198206
db_ensure_schema_v2(cur)
@@ -207,6 +215,10 @@ def db_upgrade_schema(conn: connection, cur: cursor):
207215
db_ensure_schema_v3(cur)
208216
db_set_schema_version(cur, 'v3')
209217
conn.commit()
218+
elif current == 'v3':
219+
db_ensure_schema_v4(cur)
220+
db_set_schema_version(cur, 'v4')
221+
conn.commit()
210222

211223

212224
def db_ensure_schema(conn: connection):
@@ -229,11 +241,12 @@ def db_ensure_schema(conn: connection):
229241
def db_update_account(cur: cursor, record: dict):
230242
sanitized = sanitize_record(record, ACCOUNT_DEFAULTS)
231243
cur.execute('''
232-
INSERT INTO account (subject, roles)
233-
VALUES (%(subject)s, %(roles)s)
244+
INSERT INTO account (subject, roles, "group")
245+
VALUES (%(subject)s, %(roles)s, %(group)s)
234246
ON CONFLICT (subject)
235247
DO UPDATE
236-
SET roles = EXCLUDED.roles
248+
SET roles = EXCLUDED.roles,
249+
"group" = EXCLUDED."group"
237250
RETURNING accountid;''', sanitized)
238251
accountid, = cur.fetchone()
239252
return accountid
@@ -262,6 +275,11 @@ def db_find_subjects(cur: cursor, delegate):
262275
return [row[0] for row in cur.fetchall()]
263276

264277

278+
def db_get_group(cur: cursor, group):
279+
cur.execute('''SELECT subject FROM account WHERE "group" = %s;''', (group, ))
280+
return [row[0] for row in cur.fetchall()]
281+
282+
265283
def db_update_apikey(cur: cursor, accountid, apikey_hash):
266284
sanitized = dict(accountid=accountid, apikey_hash=apikey_hash)
267285
cur.execute('''

compliance-monitor/templates/details.md.j2

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
1+
{% if results | length > 1 %}
2+
Jump to
3+
4+
{% for subject in results -%}
5+
- [{{subject}}](#{{subject}})
6+
{% endfor %}
7+
{% endif %}
18
{% for subject, subject_result in results.items() -%}
29
{# omit h1 title here because we can only have one of those,
310
and the html wrapper template will add one anyway -#}
411
{% for scopeuuid, scope_result in subject_result.items() -%}
5-
## {{ scope_result.name }}
12+
<a name="{{ subject }}"></a>
13+
## {{ subject }}: {{ scope_result.name }}
614

715
- [spec overview]({{ scope_url(scopeuuid) }})
816

compliance-monitor/templates/overview.md.j2

Lines changed: 14 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -11,40 +11,32 @@ Version numbers are suffixed by a symbol depending on state: * for _draft_, †
1111
| Name | Description | Operator | [SCS-compatible IaaS](https://docs.scs.community/standards/scs-compatible-iaas/) | HealthMon |
1212
|-------|--------------|-----------|----------------------|:----------:|
1313
| [scs2](https://docs.scs.community/community/cloud-resources/plusserver-gx-scs) | Dev/Test/Demo environment (2nd gen) provided for SCS & GAIA-X context | plusserver GmbH |
14-
{#- #} [{{ results | pick('scs2', iaas) | summary }}]({{ detail_url('scs2', iaas) }}) {# -#}
14+
{#- #} [{{ results | pick(iaas, 'scs2') | summary }}]({{ detail_url('scs2', iaas) }}) {# -#}
1515
| [HM](https://health.prod1.plusserver.sovereignit.cloud:3000/d/9ltTEmlnk/openstack-health-monitor2?orgId=1&refresh=5m&var-mycmd=All&var-mymethod=All&var-mywait=All&var-mybench=All&var-mycloud=gx-scs2) |
1616
| [aov.cloud](https://www.aov.de/) | Community cloud for customers | aov IT.Services GmbH |
17-
{#- #} [{{ results | pick('aov-cloud', iaas) | summary }}]({{ detail_url('aov-cloud', iaas) }}) {# -#}
17+
{#- #} [{{ results | pick(iaas, 'aov-cloud') | summary }}]({{ detail_url('aov-cloud', iaas) }}) {# -#}
1818
| [HM](https://health.aov.cloud/) |
1919
| [CC@RRZE](https://www.rrze.fau.de/) | Private Compute Cloud (CC) for [FAU](https://www.fau.de/) | Regionales Rechenzentrum Erlangen |
20-
{#- #} [{{ results | pick('cc-rrze', iaas) | summary }}]({{ detail_url('cc-rrze', iaas) }}) {# -#}
20+
{#- #} [{{ results | pick(iaas, 'cc-rrze') | summary }}]({{ detail_url('cc-rrze', iaas) }}) {# -#}
2121
| (soon) |
2222
| [CNDS](https://cnds.io/) | Public cloud for customers | artcodix GmbH |
23-
{#- #} [{{ results | pick('artcodix', iaas) | summary }}]({{ detail_url('artcodix', iaas) }}) {# -#}
23+
{#- #} [{{ results | pick(iaas, 'artcodix') | summary }}]({{ detail_url('artcodix', iaas) }}) {# -#}
2424
| [HM](https://ohm.muc.cloud.cnds.io/) |
25-
| [pluscloud open](https://www.plusserver.com/en/products/pluscloud-open)<br />(4 regions) | Public cloud for customers | plusserver GmbH | {# #}
26-
{#- #}prod1: [{{ results | pick('pco-prod1', iaas) | summary }}]({{ detail_url('pco-prod1', iaas) }}){# -#}
27-
<br />
28-
{#- #}prod2: [{{ results | pick('pco-prod2', iaas) | summary }}]({{ detail_url('pco-prod2', iaas) }}){# -#}
29-
<br />
30-
{#- #}prod3: [{{ results | pick('pco-prod3', iaas) | summary }}]({{ detail_url('pco-prod3', iaas) }}){# -#}
31-
<br />
32-
{#- #}prod4: [{{ results | pick('pco-prod4', iaas) | summary }}]({{ detail_url('pco-prod4', iaas) }}) {# -#}
33-
| [HM1](https://health.prod1.plusserver.sovereignit.cloud:3000/d/9ltTEmlnk/openstack-health-monitor2?orgId=1&var-mycloud=plus-pco)<br />[HM2](https://health.prod1.plusserver.sovereignit.cloud:3000/d/9ltTEmlnk/openstack-health-monitor2?orgId=1&var-mycloud=plus-prod2)<br />[HM3](https://health.prod1.plusserver.sovereignit.cloud:3000/d/9ltTEmlnk/openstack-health-monitor2?orgId=1&var-mycloud=plus-prod3)<br />[HM4](https://health.prod1.plusserver.sovereignit.cloud:3000/d/9ltTEmlnk/openstack-health-monitor2?orgId=1&var-mycloud=plus-prod4) |
25+
| [pluscloud open](https://www.plusserver.com/en/products/pluscloud-open) | Public cloud for customers (4 regions) | plusserver GmbH | {# #}
26+
{#- #}[{{ results | pick(iaas, 'pco-prod1', 'pco-prod2', 'pco-prod3', 'pco-prod4') | summary }}]({{ detail_url('group-pco-prod', iaas) }}) {# -#}
27+
| [HM1](https://health.prod1.plusserver.sovereignit.cloud:3000/d/9ltTEmlnk/openstack-health-monitor2?orgId=1&var-mycloud=plus-pco) [HM2](https://health.prod1.plusserver.sovereignit.cloud:3000/d/9ltTEmlnk/openstack-health-monitor2?orgId=1&var-mycloud=plus-prod2) [HM3](https://health.prod1.plusserver.sovereignit.cloud:3000/d/9ltTEmlnk/openstack-health-monitor2?orgId=1&var-mycloud=plus-prod3) [HM4](https://health.prod1.plusserver.sovereignit.cloud:3000/d/9ltTEmlnk/openstack-health-monitor2?orgId=1&var-mycloud=plus-prod4) |
3428
| PoC WG-Cloud OSBA | Cloud PoC for FITKO | Cloud&amp;Heat Technologies GmbH |
35-
{#- #} [{{ results | pick('poc-wgcloud', iaas) | summary }}]({{ detail_url('poc-wgcloud', iaas) }}) {# -#}
29+
{#- #} [{{ results | pick(iaas, 'poc-wgcloud') | summary }}]({{ detail_url('poc-wgcloud', iaas) }}) {# -#}
3630
| [HM](https://health.poc-wgcloud.osba.sovereignit.cloud:3000/d/9ltTEmlnk/openstack-health-monitor2?var-mycloud=poc-wgcloud&orgId=1) |
3731
| [REGIO.cloud](https://regio.digital) | Public cloud for customers | OSISM GmbH |
38-
{#- #} [{{ results | pick('regio-a', iaas) | summary }}]({{ detail_url('regio-a', iaas) }}) {# -#}
32+
{#- #} [{{ results | pick(iaas, 'regio-a') | summary }}]({{ detail_url('regio-a', iaas) }}) {# -#}
3933
| [HM](https://apimon.services.regio.digital/public-dashboards/17cf094a47404398a5b8e35a4a3968d4?orgId=1&refresh=5m) |
4034
| [ScaleUp Open Cloud](https://www.scaleuptech.com/cloud-hosting/) | Public cloud for customers | ScaleUp Technologies GmbH & Co. KG |
41-
{#- #} [{{ results | pick('scaleup-occ2', iaas) | summary }}]({{ detail_url('scaleup-occ2', iaas) }}) {# -#}
35+
{#- #} [{{ results | pick(iaas, 'scaleup-occ2') | summary }}]({{ detail_url('scaleup-occ2', iaas) }}) {# -#}
4236
| [HM](https://health.occ2.scaleup.sovereignit.cloud) |
43-
| [syseleven](https://www.syseleven.de/en/products-services/openstack-cloud/)<br />(2 SCS regions) | Public OpenStack Cloud | SysEleven GmbH | {# #}
44-
{#- #}dus2: [{{ results | pick('syseleven-dus2', iaas) | summary }}]({{ detail_url('syseleven-dus2', iaas) }}){# -#}
45-
<br />
46-
{#- #}ham1: [{{ results | pick('syseleven-ham1', iaas) | summary }}]({{ detail_url('syseleven-ham1', iaas) }}) {# -#}
47-
| (soon)<br />(soon) |
37+
| [syseleven](https://www.syseleven.de/en/products-services/openstack-cloud/) | Public OpenStack Cloud (2 SCS regions) | SysEleven GmbH | {# #}
38+
{#- #} [{{ results | pick(iaas, 'syseleven-dus2', 'syseleven-ham1') | summary }}]({{ detail_url('group-syseleven', iaas) }}) {# -#}
39+
| (soon) |
4840
| [Wavestack](https://www.noris.de/wavestack-cloud/) | Public cloud for customers | noris network AG/Wavecon GmbH |
49-
{#- #} [{{ results | pick('wavestack', iaas) | summary }}]({{ detail_url('wavestack', iaas) }}) {# -#}
41+
{#- #} [{{ results | pick(iaas, 'wavestack') | summary }}]({{ detail_url('wavestack', iaas) }}) {# -#}
5042
| [HM](https://health.wavestack1.sovereignit.cloud:3000/) |

0 commit comments

Comments
 (0)