Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
d0a884d
fix(postfix): align map filenames with configurator output
alithethird May 14, 2026
0a6ca6c
ci: add integration-tests-configurator-maps job
alithethird May 14, 2026
15a121d
fix(tests): Remove running e2e tests for this branch.
alithethird May 14, 2026
ae019e5
fix(tests): Remove running e2e tests for this branch.
alithethird May 14, 2026
7af8191
fix(tests): Restructure integration test.
alithethird May 14, 2026
5168fa7
ci: add stack-integration environment for cross-charm integration tests
alithethird May 14, 2026
012a6e1
fix: licence and integration test
alithethird May 14, 2026
71ae163
fix: infinite loop
alithethird May 14, 2026
652f8c1
Fix postmap recompilation for externally updated map files
Copilot May 14, 2026
ff760fa
Refine postfix map db path handling
Copilot May 14, 2026
bace917
fix(postfix): recompile maps when source and db mtimes match
Copilot May 14, 2026
aeb6e03
fix(postfix): update sender login mismatch handling and improve tests
alithethird May 15, 2026
755637a
fix(postfix): update fixture names and constants for clarity
alithethird May 15, 2026
cd36284
Apply suggestions from code review
alithethird May 18, 2026
b32fc0c
feat(dovecot): wire internal postfix – LMTP socket, virtual domains, …
alithethird May 19, 2026
2bf8bcc
chore(dovecot): add explanatory comments for postfix/dovecot wiring
alithethird May 19, 2026
8a69fac
test(dovecot): send mail via SMTP/Postfix in integration tests
alithethird May 19, 2026
8a3e30c
Potential fix for pull request finding
alithethird May 20, 2026
ca50c60
fix(postfix): refactor integration test setup and add failure case fo…
alithethird May 20, 2026
212c5b7
fix: add pull-requests write permission to unit-tests job
Copilot May 20, 2026
aea7a89
fix: lint
alithethird May 20, 2026
d5313c0
Apply suggestions from code review
alithethird May 21, 2026
89e66d4
chore: extract helper functions from tests
alithethird May 21, 2026
5138031
Merge branch 'fix-postfix-map-filenames' into feat/dovecot-postfix-wi…
alithethird May 21, 2026
abaab0d
test: add full-stack integration suite
alithethird May 21, 2026
cf0d406
feat: enhance CI workflows with integration tests for multiple charms…
alithethird May 21, 2026
7cf7c60
feat: update promote charm workflow to use charm selection and resolv…
alithethird May 21, 2026
30c26e5
feat: add end-to-end integration test for full mail system and clean …
alithethird May 21, 2026
e443bfb
fix: use stack-integration tox env for global tests
alithethird May 21, 2026
9851f67
chore: remove copyright comments from configuration and test files
alithethird May 21, 2026
220fee0
feat: update tox configuration for charm integration tests and linting
alithethird May 21, 2026
0537841
feat: update integration test module to use end-to-end test
alithethird May 21, 2026
d564d2c
fix: unit test
alithethird May 21, 2026
b188f32
fix: update integration test configuration for stack-integration
alithethird May 21, 2026
8295fcf
fix: update Dovecot charm name in integration tests
alithethird May 21, 2026
83d6291
feat: enhance configurator fixture with SMTP authentication and trans…
alithethird May 21, 2026
f2b5e9f
feat: add LUKS key management for Dovecot deployment
alithethird May 21, 2026
3c914ae
fix: use authenticated sender in e2e test
alithethird May 21, 2026
d6be089
feat(dovecot): add mail-user provisioning action
alithethird May 21, 2026
2726e04
fix(test): align smtp auth user with sender
alithethird May 21, 2026
2465aff
fix(test): allow e2e mailbox sender login mapping
alithethird May 21, 2026
e0a9e78
refactor(tests): improve password handling and clean up integration t…
alithethird May 21, 2026
6867c63
fix(tests): update integration test configurations and change Juju fi…
alithethird May 21, 2026
ff0e999
feat(tests): enhance integration tests and update procmail configuration
alithethird May 21, 2026
9c3a76e
chore: Cleanup by Pi agent
alithethird May 21, 2026
09a6c28
chore: resolving merge conflicts (in progress)
Copilot Jun 9, 2026
538f888
fix: resolve merge conflicts with origin/main
Copilot Jun 9, 2026
31f4550
fix: remove duplicate test path in stack-integration tox env
Copilot Jun 9, 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 .github/workflows/integration_test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ jobs:
canonical/operator-workflows/.github/workflows/integration_test.yaml@main
secrets: inherit
with:
extra-arguments: '-x --log-format="%(asctime)s %(levelname)s %(message)s"'
juju-channel: 3/stable
modules: |
[
Expand All @@ -60,9 +61,28 @@ jobs:
self-hosted-runner-label: "self-hosted-linux-amd64-noble-large"
trivy-fs-enabled: false
with-uv: true
integration-tests-stack:
uses:
canonical/operator-workflows/.github/workflows/integration_test.yaml@main
secrets: inherit
with:
extra-arguments: '-x --log-format="%(asctime)s %(levelname)s %(message)s"'
test-tox-env: stack-integration
provider: lxd
trivy-fs-enabled: false
self-hosted-runner: true
self-hosted-runner-label: "self-hosted-linux-amd64-noble-large"
juju-channel: 3/stable
charmcraft-channel: latest/edge
modules: |
[
"test_e2e.py"
]
with-uv: true
allure-report:
if: ${{ !cancelled() && github.event_name == 'schedule' }}
needs:
- integration-tests-juju-3
- integration-tests-global
- integration-tests-stack
uses: canonical/operator-workflows/.github/workflows/allure_report.yaml@main
44 changes: 34 additions & 10 deletions .github/workflows/promote_charm.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,48 @@ name: Promote charm
on:
workflow_dispatch:
inputs:
origin-channel:
charm:
type: choice
description: 'Origin Channel'
description: 'Charm to promote'
options:
- latest/edge
destination-channel:
type: choice
description: 'Destination Channel'
options:
- latest/stable
- dovecot-charm
- opendkim-operator
- postfix-relay-operator
- postfix-relay-configurator-operator
secrets:
CHARMHUB_TOKEN:
required: true

jobs:
resolve-channels:
runs-on: ubuntu-latest
outputs:
origin-channel: ${{ steps.set-channels.outputs.origin-channel }}
destination-channel: ${{ steps.set-channels.outputs.destination-channel }}
steps:
- name: Set channels
id: set-channels
run: |
case "${{ github.event.inputs.charm }}" in
dovecot-charm)
echo "origin-channel=2.3/edge" >> "$GITHUB_OUTPUT"
echo "destination-channel=2.3/stable" >> "$GITHUB_OUTPUT"
;;
opendkim-operator)
echo "origin-channel=2/edge" >> "$GITHUB_OUTPUT"
echo "destination-channel=2/stable" >> "$GITHUB_OUTPUT"
;;
postfix-relay-operator|postfix-relay-configurator-operator)
echo "origin-channel=3/edge" >> "$GITHUB_OUTPUT"
echo "destination-channel=3/stable" >> "$GITHUB_OUTPUT"
;;
esac

promote-charm:
needs: resolve-channels
uses: canonical/operator-workflows/.github/workflows/promote_charm.yaml@main
with:
origin-channel: ${{ github.event.inputs.origin-channel }}
destination-channel: ${{ github.event.inputs.destination-channel }}
origin-channel: ${{ needs.resolve-channels.outputs.origin-channel }}
destination-channel: ${{ needs.resolve-channels.outputs.destination-channel }}
working-directory: ${{ github.event.inputs.charm }}
secrets: inherit
24 changes: 23 additions & 1 deletion .github/workflows/publish_charm.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,29 @@ jobs:
publish-to-edge:
strategy:
matrix:
configuration: [{working-directory: "./dovecot-charm", channel: "2.3/edge", tag-prefix: "dovecot"}]
configuration:
[
{
working-directory: "./dovecot-charm",
channel: "2.3/edge",
tag-prefix: "dovecot",
},
{
working-directory: "./opendkim-operator",
channel: "2/edge",
tag-prefix: "opendkim-operator",
},
{
working-directory: "./postfix-relay-operator",
channel: "3/edge",
tag-prefix: "postfix-relay",
},
{
working-directory: "./postfix-relay-configurator-operator",
channel: "3/edge",
tag-prefix: "postfix-relay-configurator",
},
]
permissions:
actions: read
contents: write
Expand Down
56 changes: 56 additions & 0 deletions .github/workflows/publish_snap.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Copyright 2026 Canonical Ltd.
# See LICENSE file for licensing details.

name: Release snap to edge and promote

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

on:
workflow_dispatch:
inputs:
promote-to:
description: Channel to promote the snap to (e.g. latest/candidate)
required: false
type: string
push:
branches:
- main
paths:
- opendkim-snap/**

jobs:
build:
name: Build snap
uses: canonical/data-platform-workflows/.github/workflows/build_snap.yaml@v49
with:
path-to-snap-project-directory: ./opendkim-snap

release-edge:
name: Release snap to edge
needs: build
uses: canonical/data-platform-workflows/.github/workflows/release_snap.yaml@v49
with:
channel: latest/edge
artifact-prefix: ${{ needs.build.outputs.artifact-prefix }}
path-to-snap-project-directory: ./opendkim-snap
secrets:
snap-store-token: ${{ secrets.SNAP_STORE_TOKEN }}
permissions:
contents: write

promote:
name: Promote snap
if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.promote-to != '' }}
needs: [build, release-edge]
uses: canonical/data-platform-workflows/.github/workflows/release_snap.yaml@v49
with:
channel: ${{ github.event.inputs.promote-to }}
artifact-prefix: ${{ needs.build.outputs.artifact-prefix }}
path-to-snap-project-directory: ./opendkim-snap
create-git-tags: false
secrets:
snap-store-token: ${{ secrets.SNAP_STORE_TOKEN }}
permissions:
contents: write
27 changes: 26 additions & 1 deletion .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,17 @@ on:

jobs:
unit-tests:
strategy:
matrix:
charm:
- name: dovecot
working-directory: ./dovecot-charm
- name: opendkim-operator
working-directory: ./opendkim-operator
- name: postfix-relay-operator
working-directory: ./postfix-relay-operator
- name: postfix-relay-configurator-operator
working-directory: ./postfix-relay-configurator-operator
uses: canonical/operator-workflows/.github/workflows/test.yaml@main
secrets: inherit
permissions:
Expand All @@ -13,6 +24,20 @@ jobs:
with:
self-hosted-runner: false
with-uv: true
working-directory: dovecot-charm
working-directory: ${{ matrix.charm.working-directory }}
runs-on-base: ubuntu-24.04
python-version: 3.12

snap-tests:
name: Snap spread tests
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Set up LXD
uses: canonical/setup-lxd@main
- name: Install snapcraft
run: sudo snap install snapcraft --classic
- name: Run spread tests
run: CI=1 snapcraft test
working-directory: opendkim-snap
2 changes: 2 additions & 0 deletions .licenserc.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ header:
- 'docs/release-notes/template/*.yaml'
- 'uv.lock'
- 'opendkim-snap/snap/local/opendkim.conf'
- 'opendkim-operator/templates/opendkim.conf.j2'
- 'opendkim-operator/tests/unit/files/**'
- '**/templates/**'
- '**/lib/**'
comment: on-failure
14 changes: 14 additions & 0 deletions dovecot-charm/charmcraft.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,20 @@ config:
type: string

actions:
create-mail-user:
description: Create or update local mail users used for SMTP and mailbox login.
params:
username:
type: string
description: Primary system username to create/update.
password:
type: string
description: Plaintext password to set for created users.
mailbox-user:
type: string
description: Optional mailbox username (for example user@example.com).
default: ""
required: [username, password]
clear-queue:
description: Forcibly remove messages from the Postfix mail queue.
params:
Expand Down
74 changes: 74 additions & 0 deletions dovecot-charm/tests/integration/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -316,3 +316,77 @@ def wait_for_sync_trigger(
"Timed out waiting for sync trigger on "
f"{unit}; previous mtime={previous_mtime}, previous timer count={previous_timer_count}"
)


def get_last_sync_mtime(juju: jubilant.Juju, unit: str) -> int | None:
"""Return /srv/mail/.last-dsync mtime epoch on unit, or None if missing."""
output = juju.exec(
"stat -c %Y /srv/mail/.last-dsync 2>/dev/null || true", unit=unit
).stdout.strip()
return int(output) if output.isdigit() else None


def get_sync_timer_run_count(juju: jubilant.Juju, unit: str) -> int:
"""Return count of sync-to-secondary service invocations from the journal."""
output = juju.exec(
"journalctl -u sync-to-secondary.service --no-pager -q 2>/dev/null | wc -l || true",
unit=unit,
).stdout.strip()
return int(output) if output.isdigit() else 0


def get_sync_log_content(juju: jubilant.Juju, unit: str, lines: int = 20) -> str:
"""Return last N lines from the sync-to-secondary service journal for debugging."""
output = juju.exec(
f"journalctl -u sync-to-secondary.service --no-pager -n {lines} 2>/dev/null || echo 'No journal entries for sync-to-secondary'",
unit=unit,
).stdout
return output


def get_timer_status(juju: jubilant.Juju, unit: str) -> str | None:
"""Return systemctl show output for the sync-to-secondary timer, or None if absent."""
result = juju.exec(
"systemctl show sync-to-secondary.timer --property=ActiveState,LastTriggerUSec 2>/dev/null || true",
unit=unit,
).stdout.strip()
return result if result else None


def wait_for_sync_trigger(
juju: jubilant.Juju,
unit: str,
previous_mtime: int | None,
previous_timer_count: int,
timeout: int = 4 * 60,
poll_interval: int = 5,
) -> int:
"""Wait until /srv/mail/.last-dsync mtime advances, indicating a completed sync.

The sync script touches .last-dsync only at the very end, so this is a
reliable end-of-sync marker. Journal timer count is checked only to log
that the timer appears to have fired while we continue waiting for
.last-dsync to be updated.
"""
deadline = time.time() + timeout
timer_fired = False
while time.time() < deadline:
current_mtime = get_last_sync_mtime(juju, unit)
if current_mtime is not None and (
previous_mtime is None or current_mtime > previous_mtime
):
return current_mtime

current_timer_count = get_sync_timer_run_count(juju, unit)
if current_timer_count > previous_timer_count and not timer_fired:
logging.info(
"Timer fired (journal count increased); waiting for .last-dsync to update..."
)
timer_fired = True

time.sleep(poll_interval)

raise AssertionError(
"Timed out waiting for sync trigger on "
f"{unit}; previous mtime={previous_mtime}, previous timer count={previous_timer_count}"
)
12 changes: 3 additions & 9 deletions dovecot-charm/tests/integration/test_mail.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Copyright 2026 Canonical Ltd.
# See LICENSE file for licensing details.

"""Integration tests for end-to-end mail delivery via Postfix LMTP Dovecot."""
"""Integration tests for end-to-end mail delivery via Postfix -> LMTP -> Dovecot."""

import logging
from secrets import token_hex
Expand All @@ -14,7 +14,7 @@


def test_mail_workflow(juju: jubilant.Juju, dovecot_charm: str):
"""Test end-to-end mail delivery via Postfix LMTP Dovecot and IMAP retrieval.
"""Test end-to-end mail delivery via Postfix LMTP -> Dovecot and IMAP retrieval.

Mail is submitted over SMTP on port 25. Postfix matches the recipient domain
against virtual_mailbox_domains and forwards it to Dovecot via the LMTP Unix
Expand All @@ -31,19 +31,13 @@ def test_mail_workflow(juju: jubilant.Juju, dovecot_charm: str):
password = token_hex(8)
logging.info("Configuring user 'ubuntu'...")
setup_mail_user(juju, primary=unit_name, secondary=None, user="ubuntu", password=password)

result = juju.run(
unit_name, "create-mail-user", params={"username": "ubuntu", "password": password}
)
assert result.status == "completed"
assert result.results["status"] == "success"

logging.info("Sending test email...")
subject = "Mail Verification"
cmd = f"echo 'This is the body' | mail -s '{subject}' ubuntu@localhost"
juju.exec(cmd, unit=unit_name)

logging.info("Verifying via IMAP...")
# Resolve the unit IP before sending so we can reuse it for the IMAP check.
status = juju.status()
unit_ip = status.apps[dovecot_charm].units[unit_name].public_address

Expand Down
Loading
Loading