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
9 changes: 6 additions & 3 deletions .github/workflows/integration_test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,19 @@ on:

jobs:
integration-tests:
uses: canonical/operator-workflows/.github/workflows/integration_test.yaml@main
uses:
canonical/operator-workflows/.github/workflows/integration_test.yaml@main
secrets: inherit
with:
provider: lxd
juju-channel: 3/stable
self-hosted-runner: true
charmcraft-channel: latest/edge
modules: '["test_action.py","test_actions.py","test_charm.py","test_config.py","test_cos.py","test_ha.py","test_haproxy_route.py","test_http_interface.py","test_ingress.py", "test_ingress_per_unit.py", "test_haproxy_route_tcp.py"]'
modules: '["test_action.py","test_actions.py","test_charm.py","test_config.py","test_cos.py","test_ha.py","test_haproxy_route.py","test_http_interface.py","test_ingress.py",
"test_ingress_per_unit.py", "test_haproxy_route_tcp.py"]'
with-uv: true
allure-report:
if: always() && !cancelled()
needs:
- integration-tests
- integration-tests
uses: canonical/operator-workflows/.github/workflows/allure_report.yaml@main
6 changes: 4 additions & 2 deletions .github/workflows/load_test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ name: Load tests

on:
schedule:
- cron: "0 12 * * 0"
- cron: "0 12 * * 0"

jobs:
load-tests:
uses: canonical/operator-workflows/.github/workflows/integration_test.yaml@main
uses:
canonical/operator-workflows/.github/workflows/integration_test.yaml@main
with:
load-test-enabled: true
load-test-run-args: "-e LOAD_TEST_HOST=localhost"
with-uv: true
secrets: inherit
1 change: 1 addition & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ jobs:
self-hosted-runner: true
self-hosted-runner-image: "noble"
charmcraft-channel: latest/edge
with-uv: true
1 change: 1 addition & 0 deletions .licenserc.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,5 @@ header:
- 'src/loki_alert_rules/*.rule'
- 'docs/release-notes/**/*.yaml'
- '**/*.md.j2'
- 'uv.lock'
comment: on-failure
44 changes: 22 additions & 22 deletions charmcraft.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,23 @@ platforms:

parts:
charm:
build-packages:
- build-essential
- python3-dev
- pkg-config
- libffi-dev
- libssl-dev
- git
source: .
plugin: uv
build-snaps:
- rustup
override-build: |
rustup default stable
craftctl default

- astral-uv
build-packages:
- git
templates:
plugin: dump
source: .
stage:
- templates
name: haproxy
title: HAProxy charm.
description: |
A [Juju](https://juju.is/) [charm](https://juju.is/docs/olm/charmed-operators)
deploying and managing [HAProxy](https://www.haproxy.org/) on machines.

HAProxy is a TCP/HTTP reverse proxy which is particularly suited for high
availability environments. It features connection persistence through HTTP
cookies, load balancing, header addition, modification, deletion both ways. It
Expand All @@ -38,14 +36,15 @@ description: |
VMs and bare metal.
summary: Fast and reliable load balancing reverse proxy.
links:
documentation: https://discourse.charmhub.io/t/haproxy-documentation-overview/17216
documentation:
https://discourse.charmhub.io/t/haproxy-documentation-overview/17216
issues: https://github.com/canonical/haproxy-operator/issues
source: https://github.com/canonical/haproxy-operator
contact:
- https://launchpad.net/~canonical-is-devops
- https://launchpad.net/~canonical-is-devops

assumes:
- juju >= 3.3
- juju >= 3.3
requires:
certificates:
interface: tls-certificates
Expand Down Expand Up @@ -85,21 +84,22 @@ config:
default: 4096
type: int
description: |
Sets the maximum per-process number of concurrent connections to
<number>. Must be greater than 0.
Sets the maximum per-process number of concurrent connections to
<number>. Must be greater than 0.
vip:
type: string
description: Virtual IP address, used in active-passive ha mode.

actions:
get-certificate:
description: Returns the TLS Certificate. Intended for testing and debugging purposes.
description: Returns the TLS Certificate. Intended for testing and debugging
purposes.
params:
hostname:
type: string
description: Hostname to extract certs from.
required:
- hostname
- hostname
get-proxied-endpoints:
description: |
Return a list of endpoints
Expand All @@ -113,5 +113,5 @@ actions:
If no backend with this name is present, an empty list is returned.

charm-libs:
- lib: traefik_k8s.ingress_per_unit
version: "1"
- lib: traefik_k8s.ingress_per_unit
version: "1"
16 changes: 6 additions & 10 deletions lib/charms/haproxy/v0/haproxy_route_tcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -575,7 +575,7 @@ class TcpRequirerApplicationData(_DatabagModel):
port: int = Field(description="The port exposed on the provider.", gt=0, le=65535)
backend_port: Optional[int] = Field(
description=(
"The port where the backend service is listening. " "Defaults to the provider port."
"The port where the backend service is listening. Defaults to the provider port."
),
default=None,
gt=0,
Expand Down Expand Up @@ -1162,7 +1162,7 @@ def provide_haproxy_route_tcp_requirements(
self.update_relation_data()

# pylint: disable=too-many-arguments,too-many-positional-arguments,too-many-locals
def _generate_application_data( # noqa: C901
def _generate_application_data(
self,
*,
port: Optional[int] = None,
Expand Down Expand Up @@ -1625,10 +1625,8 @@ def configure_bandwidth_limit(
"""
if not upload_bytes_per_second and not download_bytes_per_second:
logger.error(
(
"At least one of `upload_bytes_per_second` "
"or `upload_bytes_per_second` must be set."
)
"At least one of `upload_bytes_per_second` "
"or `upload_bytes_per_second` must be set."
)
return self
self._application_data["bandwidth_limit"] = {
Expand Down Expand Up @@ -1677,10 +1675,8 @@ def configure_timeout(
server_timeout_in_seconds or connect_timeout_in_seconds or queue_timeout_in_seconds
):
logger.error(
(
"At least one of `server_timeout_in_seconds`, `connect_timeout_in_seconds` "
"or `queue_timeout_in_seconds` must be set."
)
"At least one of `server_timeout_in_seconds`, `connect_timeout_in_seconds` "
"or `queue_timeout_in_seconds` must be set."
)
return self
self._application_data["timeout"] = self._generate_timeout_configuration(
Expand Down
6 changes: 3 additions & 3 deletions lib/charms/haproxy/v1/haproxy_route.py
Original file line number Diff line number Diff line change
Expand Up @@ -875,9 +875,9 @@ def publish_proxied_endpoints(self, endpoints: list[str], relation: Relation) ->
endpoints: The list of proxied endpoints to publish.
relation: The relation with the requirer application.
"""
HaproxyRouteProviderAppData(
endpoints=list(map(lambda x: cast(AnyHttpUrl, x), endpoints))
).dump(relation.data[self.charm.app], clear=True)
HaproxyRouteProviderAppData(endpoints=[cast(AnyHttpUrl, e) for e in endpoints]).dump(
relation.data[self.charm.app], clear=True
)


class HaproxyRouteEnpointsReadyEvent(EventBase):
Expand Down
172 changes: 109 additions & 63 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,41 +1,115 @@
[tool.bandit]
exclude_dirs = ["/venv/"]
[tool.bandit.assert_used]
skips = ["*/*test.py", "*/test_*.py", "*tests/*.py"]
# Copyright 2025 Canonical Ltd.
# See LICENSE file for licensing details.

# Testing tools configuration
[tool.coverage.run]
branch = true
omit = [
"src/legacy.py"
[project]
name = "haproxy-operator"
version = "0.0.0"
description = "Fast and reliable load balancing reverse proxy."
readme = "README.md"
requires-python = ">=3.12"
classifiers = [
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
]
dependencies = [
"cosl==1.3.1",
"cryptography==46.0.3",
"interface-hacluster @ git+https://github.com/charmed-kubernetes/charm-interface-hacluster@1.32+ck2",
"jsonschema==4.25.1",
"opentelemetry-api==1.38",
"ops==3.3.1",
"pydantic==2.12.2",
]

# Formatting tools configuration
[tool.black]
[dependency-groups]
fmt = [ "ruff" ]
lint = [
"codespell",
"jinja2==3.1.6",
"jubilant==1.1.1",
"mypy",
"ops[testing]",
"pep8-naming",
"pytest",
"pytest-asyncio",
"pytest-operator",
"requests",
"ruff",
"snowballstemmer<3",
"types-pyyaml",
"types-requests",
]
unit = [ "coverage[toml]", "jinja2==3.1.6", "ops[testing]", "pytest" ]
coverage-report = [ "coverage[toml]", "jinja2==3.1.6", "pytest" ]
static = [ "bandit[toml]", "jinja2==3.1.6" ]
integration = [
"allure-pytest>=2.8.18",
"allure-pytest-collection-report @ git+https://github.com/canonical/data-platform-workflows@v24.0.0#subdirectory=python/pytest_plugins/allure_pytest_collection_report",
"jinja2==3.1.6",
"jubilant==1.1.1",
"juju==3.6.1",
"protobuf==3.20.3",
"pytest",
"pytest-asyncio",
"pytest-operator",
"websockets<14",
]

[tool.uv]
package = false

[tool.ruff]
target-version = "py310"
line-length = 99
target-version = ["py312"]

[tool.coverage.report]
show_missing = true
lint.select = [ "A", "B", "C", "CPY", "D", "E", "F", "I", "N", "RUF", "S", "SIM", "TC", "UP", "W" ]
lint.ignore = [
"B904",
"D107",
"D203",
"D204",
"D205",
"D213",
"D215",
"D400",
"D404",
"D406",
"D407",
"D408",
"D409",
"D413",
"E501",
"S105",
"S603",
"TC002",
"TC006",
"UP006",
"UP007",
"UP035",
"UP045",
]
lint.per-file-ignores."tests/*" = [ "B011", "D100", "D101", "D102", "D103", "D104", "D212", "D415", "D417", "S" ]
lint.flake8-copyright.author = "Canonical Ltd."
lint.flake8-copyright.min-file-size = 1
lint.flake8-copyright.notice-rgx = "Copyright\\s\\d{4}([-,]\\d{4})*\\s+"
lint.mccabe.max-complexity = 10
lint.pydocstyle.convention = "google"

# Linting tools configuration
[tool.flake8]
max-line-length = 99
max-doc-length = 99
max-complexity = 10
exclude = [".git", "__pycache__", ".tox", "build", "dist", "*.egg_info", "venv"]
select = ["E", "W", "F", "C", "N", "R", "D", "H"]
# Ignore W503, E501 because using black creates errors with this
# Ignore D107 Missing docstring in __init__
ignore = ["W503", "E501", "D107"]
# D100, D101, D102, D103: Ignore missing docstrings in tests
per-file-ignores = ["tests/*:D100,D101,D102,D103,D104,D205,D212,D415"]
docstring-convention = "google"
[tool.codespell]
skip = "build,lib,venv,icon.svg,.tox,.git,.mypy_cache,.ruff_cache,.coverage,htmlcov,uv.lock,grafana_dashboards"

[tool.isort]
line_length = 99
profile = "black"
skip = ["src/legacy.py"]
[tool.pytest.ini_options]
minversion = "6.0"
log_cli_level = "INFO"

[tool.coverage.run]
branch = true
omit = [ "src/legacy.py" ]

[tool.coverage.report]
show_missing = true

[tool.mypy]
check_untyped_defs = true
Expand All @@ -48,36 +122,8 @@ namespace_packages = true
disallow_untyped_defs = false
module = "tests.*"

[tool.pylint]
disable = "wrong-import-order"

[tool.pytest.ini_options]
minversion = "6.0"
log_cli_level = "INFO"

# Linting tools configuration
[tool.ruff]
line-length = 99
select = ["E", "W", "F", "C", "N", "D", "I001"]
extend-ignore = [
"D203",
"D204",
"D213",
"D215",
"D400",
"D404",
"D406",
"D407",
"D408",
"D409",
"D413",
]
ignore = ["E501", "D107"]
extend-exclude = ["__pycache__", "*.egg_info"]
per-file-ignores = {"tests/*" = ["D100","D101","D102","D103","D104"]}

[tool.ruff.mccabe]
max-complexity = 10
[tool.bandit]
exclude_dirs = [ "/venv/" ]

[tool.codespell]
skip = "build,lib,venv,icon.svg,.tox,.git,.mypy_cache,.ruff_cache,.coverage"
[tool.bandit.assert_used]
skips = [ "*/*test.py", "*/test_*.py", "*tests/*.py" ]
7 changes: 0 additions & 7 deletions requirements.txt

This file was deleted.

Loading
Loading