Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
84 commits
Select commit Hold shift + click to select a range
7b7a688
Implement request API
Thanhphan1147 Mar 17, 2026
00a3ea7
update model validation before save and add unit tests
Thanhphan1147 Mar 17, 2026
77622dd
Merge branch 'main' into haprox-route-policy-requests-api
Thanhphan1147 Mar 17, 2026
bc11a1f
use environment variables for secret key
Thanhphan1147 Mar 17, 2026
6f1499d
Merge branch 'haprox-route-policy-requests-api' of github.com:canonic…
Thanhphan1147 Mar 17, 2026
6e98d08
ruff format
Thanhphan1147 Mar 17, 2026
f073454
add secret key for testing
Thanhphan1147 Mar 17, 2026
4842e4a
remove port attribute from test
Thanhphan1147 Mar 17, 2026
5b667c0
add requirements.txt for testing
Thanhphan1147 Mar 17, 2026
e719d33
reintroduce port field
Thanhphan1147 Mar 17, 2026
597eb66
Add change artifact
Thanhphan1147 Mar 17, 2026
6f51301
run lint with uv
Thanhphan1147 Mar 17, 2026
7f245ea
add unit testing
Thanhphan1147 Mar 17, 2026
6254485
remove custom test
Thanhphan1147 Mar 17, 2026
ad50aa3
update migration
Thanhphan1147 Mar 17, 2026
3640143
Wrap creation under `transaction.atomic`
Thanhphan1147 Mar 18, 2026
adbf18f
Potential fix for pull request finding
Thanhphan1147 Mar 18, 2026
daef5d3
remove unused code
Thanhphan1147 Mar 18, 2026
78e5dc0
minor fixes to settings
Thanhphan1147 Mar 18, 2026
cd93076
Potential fix for pull request finding
Thanhphan1147 Mar 18, 2026
f93e9cb
use django serializer
Thanhphan1147 Mar 18, 2026
4a92e21
update gitignore
Thanhphan1147 Mar 18, 2026
7b1c9ca
Merge branch 'haprox-route-policy-requests-api' of github.com:canonic…
Thanhphan1147 Mar 18, 2026
22e4aef
update view to use django rest
Thanhphan1147 Mar 19, 2026
266e20e
remove python-version
Thanhphan1147 Mar 19, 2026
d5db748
update gitignore
Thanhphan1147 Mar 19, 2026
34ca372
add missing license headers
Thanhphan1147 Mar 19, 2026
3ea5d0e
Add rules engine
Thanhphan1147 Mar 17, 2026
16833e4
update migration
Thanhphan1147 Mar 17, 2026
efe9beb
update view
Thanhphan1147 Mar 17, 2026
593518e
fix lint
Thanhphan1147 Mar 17, 2026
f23afe1
remove extra tests
Thanhphan1147 Mar 17, 2026
6955b92
add validation and update tests
Thanhphan1147 Mar 18, 2026
ff23fa0
update view
Thanhphan1147 Mar 18, 2026
314a687
remove to_dict
Thanhphan1147 Mar 19, 2026
a10a032
use serializer for get
Thanhphan1147 Mar 19, 2026
c4b297f
use serializer
Thanhphan1147 Mar 19, 2026
4f09fbc
remove unused tests
Thanhphan1147 Mar 19, 2026
ad1c42c
use filter for delete query
Thanhphan1147 Mar 19, 2026
fb9e143
update tests and move validation to serializer class
Thanhphan1147 Mar 19, 2026
b05c12a
Apply suggestion from @github-actions[bot]
Thanhphan1147 Mar 19, 2026
10a2708
Update haproxy-route-policy/policy/migrations/0001_initial.py
Thanhphan1147 Mar 19, 2026
571e469
Revert "Update haproxy-route-policy/policy/migrations/0001_initial.py"
Thanhphan1147 Mar 19, 2026
a22bedd
ignore migration files for license header
Thanhphan1147 Mar 19, 2026
7d3d20b
remove license header from generated files
Thanhphan1147 Mar 19, 2026
e41031c
Merge branch 'haprox-route-policy-requests-api' into haproxy-route-po…
Thanhphan1147 Mar 19, 2026
00c094e
add change artifact
Thanhphan1147 Mar 19, 2026
cd7c49a
add envlist to tox commands
Thanhphan1147 Mar 19, 2026
503a313
update envlist
Thanhphan1147 Mar 19, 2026
fc25b19
Merge branch 'haprox-route-policy-requests-api' into haproxy-route-po…
Thanhphan1147 Mar 19, 2026
a2d1ea2
convert pk to uuid for requests
Thanhphan1147 Mar 19, 2026
a9ceb87
Merge branch 'haprox-route-policy-requests-api' into haproxy-route-po…
Thanhphan1147 Mar 19, 2026
988ba70
Add guard against mal-formed uuid and parameter. Add logging configs,…
Thanhphan1147 Mar 19, 2026
9a246aa
add validators for port and paths
Thanhphan1147 Mar 19, 2026
35a286f
add tests for validators
Thanhphan1147 Mar 19, 2026
a42d4c3
add note for migration
Thanhphan1147 Mar 19, 2026
324114e
Merge remote-tracking branch 'origin/haprox-route-policy-requests-api…
Thanhphan1147 Mar 19, 2026
ee86d7d
ruff format
Thanhphan1147 Mar 19, 2026
9101175
remove unused imports
Thanhphan1147 Mar 19, 2026
69872ec
add static tests
Thanhphan1147 Mar 20, 2026
fee2543
Merge remote-tracking branch 'origin/main' into haproxy-route-policy-…
Thanhphan1147 Mar 20, 2026
b700245
guard rules API against pk
Thanhphan1147 Mar 20, 2026
931b5b1
Merge branch 'main' into haproxy-route-policy-rules-api
alithethird Mar 23, 2026
e3d7215
update view, middle wares and tests
Thanhphan1147 Mar 23, 2026
04f29ca
Merge branch 'haproxy-route-policy-rules-api' of github.com:canonical…
Thanhphan1147 Mar 23, 2026
b4717a4
Merge branch 'main' into haproxy-route-policy-rules-api
Thanhphan1147 Mar 23, 2026
6c1db27
refactor tests by parametrizing
Thanhphan1147 Mar 23, 2026
92fddb4
Merge branch 'haproxy-route-policy-rules-api' of github.com:canonical…
Thanhphan1147 Mar 23, 2026
ea6afe4
group tests by parameterizing
Thanhphan1147 Mar 23, 2026
7ec072c
refactor Rule model to rename attribute from "value" to "parameters"
Thanhphan1147 Mar 23, 2026
9a3d61a
update test name
Thanhphan1147 Mar 23, 2026
6b75a0a
update naming
Thanhphan1147 Mar 23, 2026
5e94907
Add coverage-report as part of unit test suite
Thanhphan1147 Mar 25, 2026
1910db5
update env list
Thanhphan1147 Mar 25, 2026
6a3b4b7
implement rule evaluation
Thanhphan1147 Mar 19, 2026
c564054
add change artifact
Thanhphan1147 Mar 19, 2026
92e6810
update imports
Thanhphan1147 Mar 19, 2026
ed16de9
update naming
Thanhphan1147 Mar 23, 2026
d3b5e6d
update rules matching logic
Thanhphan1147 Mar 25, 2026
ec6d312
update tests
Thanhphan1147 Mar 25, 2026
93ce0f4
save request using serializer with the correct instace
Thanhphan1147 Mar 26, 2026
7172da3
Merge remote-tracking branch 'origin/main' into haproxy-route-policy-…
Thanhphan1147 Mar 26, 2026
3e3a0a9
group tests
Thanhphan1147 Mar 26, 2026
60cc53f
update formatting
Thanhphan1147 Mar 26, 2026
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
20 changes: 20 additions & 0 deletions docs/release-notes/artifacts/pr0401.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
version_schema: 2

changes:
- title: Added rule matching engine and request evaluation on creation
author: tphan025
type: minor
description: >
Added a rule matching engine that evaluates backend requests against rules
ordered by descending priority. Within the same priority group, deny rules
take precedence over allow rules. Integrated the engine into the bulk create
endpoint so that each new request is evaluated immediately and its status is
set to accepted, rejected, or pending accordingly. Included unit tests for the
matching logic and integration tests for rule evaluation during request creation.
urls:
pr:
- https://github.com/canonical/haproxy-operator/pull/401
related_doc:
related_issue:
visibility: public
highlight: false
2 changes: 1 addition & 1 deletion haproxy-route-policy/policy/db_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ class BackendRequest(models.Model):
hostname_acls: models.JSONField = models.JSONField(
default=list, validators=[validate_hostname_acls], blank=True
)
backend_name: models.TextField = models.TextField()
backend_name: models.TextField = models.TextField(unique=True)
paths: models.JSONField = models.JSONField(
default=list, validators=[validate_paths], blank=True
)
Expand Down
16 changes: 13 additions & 3 deletions haproxy-route-policy/policy/migrations/0001_initial.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,19 @@ class Migration(migrations.Migration):
validators=[policy.db_models.validate_hostname_acls],
),
),
("backend_name", models.TextField()),
("paths", models.JSONField(blank=True, default=list)),
("port", models.IntegerField()),
("backend_name", models.TextField(unique=True)),
(
"paths",
models.JSONField(
blank=True,
default=list,
validators=[policy.db_models.validate_paths],
),
),
(
"port",
models.IntegerField(validators=[policy.db_models.validate_port]),
),
(
"status",
models.TextField(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
# Generated by Django 6.0.3 on 2026-03-23 21:53
# Generated by Django 6.0.3 on 2026-03-24 14:42

import policy.db_models
import uuid
from django.db import migrations, models

Expand Down Expand Up @@ -40,16 +39,4 @@ class Migration(migrations.Migration):
("updated_at", models.DateTimeField(auto_now=True)),
],
),
migrations.AlterField(
model_name="backendrequest",
name="paths",
field=models.JSONField(
blank=True, default=list, validators=[policy.db_models.validate_paths]
),
),
migrations.AlterField(
model_name="backendrequest",
name="port",
field=models.IntegerField(validators=[policy.db_models.validate_port]),
),
]
122 changes: 122 additions & 0 deletions haproxy-route-policy/policy/rule_engine.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# Copyright 2026 Canonical Ltd.
# See LICENSE file for licensing details.

"""Rule matching engine for evaluating backend requests against rules.

Rules are evaluated following these principles:
P1: Rules are grouped by priority and evaluated starting from the highest
priority group.
P2: Within the same priority group, "deny" rules take precedence over
"allow" rules.

If no rules match a request, its status remains "pending".
"""

import logging
from itertools import groupby
from policy.db_models import (
BackendRequest,
Rule,
RULE_ACTION_ALLOW,
RULE_ACTION_DENY,
RULE_KIND_HOSTNAME_AND_PATH_MATCH,
REQUEST_STATUS_ACCEPTED,
REQUEST_STATUS_REJECTED,
REQUEST_STATUS_PENDING,
)

logger = logging.getLogger(__name__)


def _hostname_and_path_match(rule: Rule, request: BackendRequest) -> bool:
"""Check if a hostname_and_path_match rule matches a backend request.

A rule matches if:
1. Any of the rule's `hostnames` appear in the request's `hostname_acls`
if `hostnames` is not empty.
2. Any of the rule's `paths` appear in the request's `paths`
if `paths` is not empty..

Args:
rule: The rule to check.
request: The backend request to evaluate.

Returns:
True if the rule matches the request, False otherwise.
"""
rule_hostnames: list = rule.parameters.get("hostnames", [])
rule_paths: list = rule.parameters.get("paths", [])

# A rule with no hostnames can never match.
if not rule_hostnames:
return False

# At least one rule hostname must appear in the request's hostname_acls.
hostname_matched = bool(set(rule_hostnames).intersection(request.hostname_acls))
if not hostname_matched:
return False

# Empty rule paths means "match all paths" (wildcard).
if not rule_paths:
return True

# At least one rule path must appear in the request's paths.
return bool(set(rule_paths).intersection(request.paths))


def evaluate_request(request: BackendRequest) -> str:
"""Evaluate a backend request against all rules and return the resulting status.

Rules are fetched from the database, ordered by descending priority.
They are grouped by priority level and evaluated from highest to lowest.

Within the same priority group:
- If any "deny" rule matches, the request is rejected.
- If any "allow" rule matches (and no deny matched), the request is accepted.
- If no rules match at this priority level, move to the next group.

If no rules match at any priority level, the request stays "pending".

Args:
request: The backend request to evaluate.

Returns:
The resulting status string: "accepted", "rejected", or "pending".
"""
rules = Rule.objects.all().order_by("-priority")

for _priority, group in groupby(rules, key=lambda rule: rule.priority):
allow_matched = False
deny_matched = False

for rule in group:
if not _matches(rule, request):
continue

if rule.action == RULE_ACTION_DENY:
deny_matched = True
elif rule.action == RULE_ACTION_ALLOW:
allow_matched = True

# P2: deny rules have priority over allow rules within the same priority level
if deny_matched:
return REQUEST_STATUS_REJECTED
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we discussed it in the spec, but I wonder if we should not store the information of which rule has been matched somewhere to ease troubleshooting?

if allow_matched:
return REQUEST_STATUS_ACCEPTED

return REQUEST_STATUS_PENDING


def _matches(rule: Rule, request: BackendRequest) -> bool:
"""Dispatch matching logic based on the rule kind.

Args:
rule: The rule to evaluate.
request: The backend request to evaluate against.

Returns:
True if the rule matches the request.
"""
if rule.kind == RULE_KIND_HOSTNAME_AND_PATH_MATCH:
return _hostname_and_path_match(rule, request)
return False
Loading
Loading