Skip to content

feat(dovecot-charm): add HA support with SSH key exchange and force-sync action#15

Open
alithethird wants to merge 41 commits intomainfrom
pr/4-ha
Open

feat(dovecot-charm): add HA support with SSH key exchange and force-sync action#15
alithethird wants to merge 41 commits intomainfrom
pr/4-ha

Conversation

@alithethird
Copy link
Copy Markdown
Collaborator

Summary

  • Adds HA support via SSH key exchange between primary and secondary units during installation
  • Introduces force-sync action to trigger immediate mail pool synchronisation from primary to secondary on demand
  • Rebased onto updated pr/3-tls branch (resolved conflicts from TLS refactor)

Changes

  • charm.py: adds _setup_ssh_keys, _on_replicas_changed, _ensure_root_ssh_configs, _install_mail_sync_script, _setup_mail_sync_cronjob, _on_force_sync, _secondary_hostname, and _is_primary
  • charmcraft.yaml: adds force-sync action
  • templates/: adds sync-to-secondary.sh.tmpl and sync-to-secondary_cron.tmpl
  • tests/unit/test_charm.py: adds unit tests for HA functionality
  • tests/integration/test_ha.py: adds integration test for HA failover
  • docs/: adds release notes for pr/4-ha

- Updated unit tests for DovecotCharm to use new setup methods for Dovecot and Procmail.
- Replaced calls to _systemctl with _setup_dovecot and _setup_procmail in certificate tests.
- Ensured service reload is properly mocked and verified in tests.
- Added new dependency on charmlibs-interfaces-tls-certificates version 1.8.1 in uv.lock.
When no certificates relation is active the TLS cert files do not exist
yet. Unconditionally setting ssl=required in the dovecot config caused
dovecot to fail to start, putting the unit in error on every upgrade-charm
or install event before a cert is provisioned.

Template now checks tls_enabled (passed from charm, true iff the .pem
file exists in /etc/dovecot/private/) and falls back to ssl=yes when no
cert is present. ssl=required is restored automatically on the next
_reconcile after certificate_available writes the cert to disk.

Also register --use-existing pytest option in tests/conftest.py so it
can be passed on the command line without an 'unrecognised arguments'
error.
- Replace _on_certificate_available with _setup_tls called from _reconcile
- Wire certificate_available event to _reconcile (not separate handler)
- Always ssl=required in dovecot.conf (no conditional)
- Add TLS_CERT_DIR constant, use get_assigned_certificate() API
- Charm blocks with distinct messages for missing relation vs missing cert
- New tests/unit/test_tls.py with 6 tests following SKILL.md principles
- Fix integration tests: copyright, sleeps, stat quoting, ssl assertion
- Update state diagrams for TLS states and events
@alithethird alithethird requested a review from a team as a code owner April 20, 2026 07:11
@alithethird alithethird requested review from yanksyoon and yhaliaw and removed request for a team April 20, 2026 07:11
@alithethird alithethird marked this pull request as draft April 20, 2026 07:13
Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

license-eye has checked 117 files.

Valid Invalid Ignored Fixed
41 1 75 0
Click to see the invalid file list
  • docs/release-notes/artifacts/pr-4-ha.yaml
Use this command to fix any missing license headers
```bash

docker run -it --rm -v $(pwd):/github/workspace apache/skywalking-eyes header fix

</details>

Comment thread docs/release-notes/artifacts/pr-4-ha.yaml Outdated
- Move all HA setup (SSH keys, authorized_keys, sync script, cronjob)
  into _reconcile so they're re-evaluated on every event, not just install
- Replace os.system calls with subprocess.run and systemd.service_reload
- Replace sed-based sshd_config mutation with pure Python
- Guard _sync_authorized_keys against missing peer relation
- Skip sync script/cronjob when secondary hostname is not yet known
- Remove dead code (sync_smtp_aliases_target)
- Remove _on_replicas_changed handler — folded into _reconcile
- Fix _setup_ssh_keys to handle keygen failure gracefully
- Rewrite unit tests per SKILL.md principles: assert on observable
  state (unit_status, opened_ports), comment every patch
- Add HA patches to storage and TLS tests broken by holistic reconcile
- Fix integration test copyright year and force-sync empty-maildir bug
Comment thread dovecot-charm/templates/sync-to-secondary_cron.tmpl Outdated
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 17 out of 17 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread dovecot-charm/src/charm.py Outdated
Comment thread dovecot-charm/src/ha.py
Comment thread dovecot-charm/templates/sync-to-secondary_cron.tmpl Outdated
Comment thread dovecot-charm/src/ha.py
Comment thread dovecot-charm/tests/integration/test_ha.py Outdated
Comment thread dovecot-charm/charmcraft.yaml
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 17 out of 17 changed files in this pull request and generated 5 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread dovecot-charm/src/ha.py Outdated
Comment thread dovecot-charm/src/charm.py
Comment thread dovecot-charm/src/charm.py
Comment thread dovecot-charm/src/ha.py
Comment thread dovecot-charm/src/ha.py
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 18 out of 18 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread dovecot-charm/src/ha.py
Comment thread dovecot-charm/src/ha.py
Comment thread dovecot-charm/src/ha.py Outdated
Comment thread dovecot-charm/src/charm.py
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 18 out of 18 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread dovecot-charm/src/ha.py Outdated
Comment thread dovecot-charm/src/ha.py
Comment thread dovecot-charm/tests/integration/test_ha.py Outdated
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 20 out of 20 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread dovecot-charm/src/ha.py
Comment thread dovecot-charm/src/dovecot_config.py
Comment thread dovecot-charm/src/ha.py Outdated
Comment thread dovecot-charm/src/constants.py
@alithethird alithethird marked this pull request as ready for review April 24, 2026 12:43
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 20 out of 20 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread dovecot-charm/src/ha.py
Comment on lines +94 to +119
authorized_keys = []
peer_ips: list[str] = []
for unit in relation.units:
pk = relation.data[unit].get("public_key")
if pk:
authorized_keys.append(pk)
ip = relation.data[unit].get("ip_address")
if ip:
peer_ips.append(ip)

our_pk = relation.data[charm.unit].get("public_key")
if our_pk:
authorized_keys.append(our_pk)
our_ip = relation.data[charm.unit].get("ip_address")
if our_ip:
peer_ips.append(our_ip)

if not authorized_keys:
return

auth_file = SSH_DIR / "authorized_keys"
auth_file.write_text("\n".join(authorized_keys) + "\n")
auth_file.chmod(0o600)

ensure_root_ssh_login(peer_ips)

Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

sync_authorized_keys appends this unit’s own ip_address/public_key to the lists that are later used to restrict root SSH (ensure_root_ssh_login(peer_ips)). This means the sshd drop-in will be written even when there are no remote peers yet, and it will allow root SSH from the unit’s own address (which isn’t a peer). Consider passing only remote peer IPs (e.g., from relation.units) to ensure_root_ssh_login, and only enabling the drop-in once at least one remote peer IP is present.

Copilot uses AI. Check for mistakes.
Comment on lines +118 to +127
def _secondary_hostname(self) -> typing.Optional[str]:
"""Return the hostname of the first remote peer unit, or None."""
relation = self.model.get_relation(PEER_RELATION_NAME)
if not relation:
return None
for unit in relation.units:
hostname = relation.data[unit].get("hostname")
if hostname:
return hostname
return None
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

_secondary_hostname reads the peer's hostname field (populated via socket.gethostname() in ha.setup_ssh_keys). That hostname is not guaranteed to be resolvable from the other unit, while you already publish ip_address on the peer relation. Consider switching this property (and the sync script template context) to use the peer’s published ip_address (or another Juju-resolvable address) and keep sync_known_hosts in sync with whatever identifier SSH will actually connect to.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants