Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
7100997
chore: fetch dns_record lib and add dns-record relation to charmcraft…
yanksyoon May 7, 2026
0921ba1
feat: add DNSRecordService for publishing A records via dns-record re…
yanksyoon May 7, 2026
0a3d43b
feat: wire up DNSRecordService in HAProxyCharm — publish A records on…
yanksyoon May 7, 2026
4111799
test: add integration tests for dns-record relation against bind-oper…
yanksyoon May 7, 2026
af2c1d1
style: fix ruff import ordering and remove unused imports
yanksyoon May 7, 2026
e25ea6e
test: query bind DNS server directly to verify A record resolution
yanksyoon May 7, 2026
af6e08b
fix: remove charm-libs entry and fix ruff format/lint violations
yanksyoon May 8, 2026
708c909
chore: add missing charm libs
yanksyoon May 8, 2026
7d4ad81
test: add dns record test to integration test suite
yanksyoon May 8, 2026
d8b93bb
test: skip haproxy lib test
yanksyoon May 8, 2026
f30e538
Merge branch 'main' into feat/ISD-3237/dns-record-requirer
yanksyoon May 14, 2026
b7b36ab
Merge branch 'main' into feat/ISD-3237/dns-record-requirer
yanksyoon May 19, 2026
d3ffd80
fix: address Copilot review comments
yanksyoon May 25, 2026
7393fcf
ci: fix CI failures
yanksyoon May 25, 2026
271553d
chore: remove .DS_Store files
yanksyoon May 25, 2026
aa29473
Merge branch 'main' into feat/ISD-3237/dns-record-requirer
yanksyoon May 26, 2026
c11e96f
docs: add changelog entry for dns-record integration
yanksyoon May 27, 2026
21c4401
fix(tests): use hashable CertificateRequestAttributes from charmlibs
yanksyoon May 27, 2026
31fda99
fix: update copyright year and remove flaky lycheeignore entry
yanksyoon May 27, 2026
2896dd8
fix: address review comments
yanksyoon May 27, 2026
94947ed
style: fix ruff formatting in integration test
yanksyoon May 27, 2026
805b0e1
fix: correct mypy error in integration test relations check
yanksyoon May 27, 2026
2b4e661
fix: update copyright year in integration test
yanksyoon May 27, 2026
3fa48e5
fix: update copyright year in unit test
yanksyoon May 27, 2026
2de8d02
refactor: move inline imports to top of file
yanksyoon May 27, 2026
db226f4
refactor: parametrize no-op DNS record unit tests
yanksyoon May 27, 2026
7204454
revert: remove require-check-lib workaround from test.yaml
yanksyoon May 27, 2026
f625a99
fix: use dns-record relation binding for IP resolution
yanksyoon May 27, 2026
36bf2e0
Merge branch 'main' into feat/ISD-3237/dns-record-requirer
yanksyoon May 28, 2026
26b7545
Merge remote-tracking branch 'origin/main' into feat/ISD-3237/dns-rec…
yanksyoon May 29, 2026
f041cc2
Merge branch 'main' into feat/ISD-3237/dns-record-requirer
yanksyoon Jun 1, 2026
623307c
refactor: route dns-record events through reconcile loop
yanksyoon Jun 4, 2026
bd7b0e1
Merge branch 'main' into feat/ISD-3237/dns-record-requirer
yanksyoon Jun 4, 2026
f6b0ec0
fix: update charm-libs entry to dns_integrator namespace
yanksyoon Jun 4, 2026
8fb27ff
Merge branch 'main' into feat/ISD-3237/dns-record-requirer
yanksyoon Jun 9, 2026
8e361ee
Merge branch 'main' into feat/ISD-3237/dns-record-requirer
yanksyoon Jun 17, 2026
de4473e
Merge branch 'main' into feat/ISD-3237/dns-record-requirer
yanksyoon Jun 19, 2026
1382134
Merge branch 'main' into feat/ISD-3237/dns-record-requirer
Thanhphan1147 Jul 6, 2026
bc9c466
Merge branch 'main' into feat/ISD-3237/dns-record-requirer
Thanhphan1147 Jul 7, 2026
94c229e
Merge branch 'main' into feat/ISD-3237/dns-record-requirer
Thanhphan1147 Aug 10, 2026
522e601
Merge branch 'main' into feat/ISD-3237/dns-record-requirer
alithethird Aug 12, 2026
93bafa9
Update haproxy-operator/src/charm.py
alithethird Aug 12, 2026
0eaec4f
fix: sync dns record PR with main and CI
yanksyoon Sep 3, 2026
1bff25c
fix: keep local DNS library out of charmcraft checks
yanksyoon Sep 3, 2026
79832ed
chore: preserve main branch Terraform formatting
yanksyoon Sep 3, 2026
c91a4bf
fix: use standard license header for vendored DNS library
yanksyoon Sep 7, 2026
b0ce0fb
fix: use reachable unit address in DNS integration test
yanksyoon Sep 7, 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
2 changes: 1 addition & 1 deletion .lycheeignore
Original file line number Diff line number Diff line change
@@ -1 +1 @@
https://github.com/canonical/haproxy-operator/deployments/charmhub-stable-promote
https://github.com/canonical/haproxy-operator/deployments/charmhub-stable-promote
4 changes: 4 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ Each revision is versioned by the date of the revision.

- Added relation endpoints reference page.

## 2026-05-27

- Added optional `dns-record` relation to publish DNS A records for HAProxy hostnames.

## 2026-05-26

- Added `port_mapping` attribute to `haproxy-route-tcp` relation to allow mapping a range of frontend ports to a range of backend ports.
Expand Down
18 changes: 16 additions & 2 deletions haproxy-operator/charmcraft.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,9 @@ requires:
interface: haproxy-route-policy
optional: true
limit: 1

dns-record:
interface: dns_record
optional: true

provides:
ingress:
Expand Down Expand Up @@ -162,9 +164,21 @@ actions:
Client IP hash salts are redacted. Intended for debugging purposes.

charm-libs:
- lib: traefik_k8s.ingress_per_unit
- lib: certificate_transfer_interface.certificate_transfer
version: "1"
- lib: grafana_agent.cos_agent
version: "0"
- lib: haproxy.ddos_protection
version: "0"
- lib: haproxy_route_policy.haproxy_route_policy
version: "0"
- lib: operator_libs_linux.apt
version: "0"
- lib: operator_libs_linux.systemd
version: "1"
- lib: tls_certificates_interface.tls_certificates
version: "4"
- lib: traefik_k8s.ingress
version: "2"
- lib: traefik_k8s.ingress_per_unit
version: "1"
Comment thread
yanksyoon marked this conversation as resolved.
1 change: 1 addition & 0 deletions haproxy-operator/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ lint.ignore = [
"UP045",
]
lint.per-file-ignores."tests/*" = [ "B011", "D100", "D101", "D102", "D103", "D104", "D212", "D415", "D417", "S" ]
lint.per-file-ignores."src/charms/dns_integrator/v0/dns_record.py" = [ "ALL" ]
lint.per-file-ignores."tests/integration/grpc_server/*_pb2.py" = [ "ALL" ]
lint.per-file-ignores."tests/integration/grpc_server/*_pb2_grpc.py" = [ "ALL" ]
lint.flake8-copyright.author = "Canonical Ltd."
Expand Down
60 changes: 59 additions & 1 deletion haproxy-operator/src/charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@
from ops.charm import ActionEvent
from ops.model import Port, SecretNotFoundError

from charms.dns_integrator.v0.dns_record import DNSRecordRequires
from dns_record import DNS_RECORD_RELATION, DNSRecordService
from haproxy import HAPROXY_CONFIG, HAPROXY_SERVICE, HAProxyService, file_exists, read_file
from http_interface import (
HTTPBackendAvailableEvent,
Expand All @@ -59,7 +61,12 @@
from state.charm_state import CharmState, ProxyMode
from state.ddos_protection import DDosProtection
from state.exception import CharmStateValidationBaseError
from state.ha import HACLUSTER_INTEGRATION, HAPROXY_PEER_INTEGRATION, HAInformation
from state.ha import (
HACLUSTER_INTEGRATION,
HAPROXY_PEER_INTEGRATION,
HAInformation,
HAInformationValidationError,
)
from state.haproxy_route import (
HAPROXY_ROUTE_RELATION,
HAProxyRouteBackend,
Expand Down Expand Up @@ -172,6 +179,8 @@ def __init__(self, *args: typing.Any):
)

self._tls = TLSRelationService(self.model, self.certificates, self.recv_ca_certs)
self.dns_record_requirer = DNSRecordRequires(self)
self._dns_record_service = DNSRecordService(self.model, self.dns_record_requirer)
self.website_requirer = HTTPProvider(self, WEBSITE_RELATION)

self._grafana_agent = COSAgentProvider(
Expand Down Expand Up @@ -211,6 +220,18 @@ def __init__(self, *args: typing.Any):
self._ingress_per_unit_provider.on.data_removed, self._on_ingress_data_removed
)
self.framework.observe(self.hacluster.on.ha_ready, self._on_config_changed)
self.framework.observe(
self.on[HACLUSTER_INTEGRATION].relation_changed, self._on_config_changed
)
self.framework.observe(
self.on[HACLUSTER_INTEGRATION].relation_broken, self._on_config_changed
)
self.framework.observe(
self.on[DNS_RECORD_RELATION].relation_created, self._on_config_changed
)
self.framework.observe(
self.on[DNS_RECORD_RELATION].relation_joined, self._on_config_changed
)
self.framework.observe(
self.recv_ca_certs.on.certificate_set_updated, self._on_ca_certificates_updated
)
Expand Down Expand Up @@ -346,6 +367,8 @@ def _reconcile(self) -> None:

self.unit.set_ports(80)
self.haproxy_service.reconcile_default(charm_state)
if self.unit.is_leader():
self._update_dns_records()
self.unit.status = ops.ActiveStatus(status_message)

def _configure_ingress(
Expand Down Expand Up @@ -583,6 +606,41 @@ def _on_ca_certificates_removed(self, _: CertificatesRemovedEvent) -> None:
"""Handle the CA certificates removed event."""
self._reconcile()

def _update_dns_records(self) -> None:
"""Publish A records for all managed hostnames to the dns-record relation.

Uses the VIP when an HA relation is active, otherwise uses the ingress
binding address of this unit.

Called at the end of _reconcile(), so config/TLS validation has already passed.
"""
if not self.unit.is_leader():
return

Comment thread
yanksyoon marked this conversation as resolved.
hostnames = [req.common_name for req in self._get_certificate_requests()]
if not hostnames:
return

try:
ha_information = HAInformation.from_charm(self)
except HAInformationValidationError:
ha_information = None
Comment thread
yanksyoon marked this conversation as resolved.

if ha_information and ha_information.ha_integration_ready and ha_information.vip:
ip = str(ha_information.vip)
else:
network_binding = self.model.get_binding(DNS_RECORD_RELATION)
if network_binding is None:
logger.warning("Cannot resolve network binding for DNS records.")
return
ingress_addresses = network_binding.network.ingress_addresses
if not ingress_addresses:
logger.warning("No ingress addresses found; skipping DNS record update.")
return
ip = str(ingress_addresses[0])
Comment on lines +636 to +640

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.

Suggested change
ingress_addresses = network_binding.network.ingress_addresses
if not ingress_addresses:
logger.warning("No ingress addresses found; skipping DNS record update.")
return
ip = str(ingress_addresses[0])
if not (ip := str(network_binding.network.ingress_address)):
logger.warning("No ingress addresses found; skipping DNS record update.")
return

Very minuscule nitpick.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I kept the existing empty-list guard instead of using network.ingress_address: it avoids indexing an empty address list, and str(None) would produce the truthy string "None". The current code therefore skips the update when no ingress address is available.


self._dns_record_service.update_dns_records(hostnames, ip)
Comment thread
yanksyoon marked this conversation as resolved.

@validate_config_and_tls(defer=False)
def _on_ingress_per_unit_data_provided(self, _: IngressDataReadyEvent) -> None:
"""Handle the data-provided event for ingress-per-unit."""
Expand Down
Loading
Loading