mirror of
https://github.com/chatmail/relay.git
synced 2026-06-09 13:11:08 +00:00
Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a77f5c5f42 | |||
| 84b970f1ea | |||
| 1f4ba7bf43 | |||
| 5de5ba46ad | |||
| 337e045284 | |||
| b60bc1862a | |||
| 579e7c767d | |||
| a735543c80 | |||
| 948efff70d | |||
| 9945402d4c | |||
| b39c27c5cc | |||
| 38ba28be8a | |||
| 1aa4896260 | |||
| 823bf88c74 | |||
| 78b326dbd1 | |||
| e708027edb | |||
| 27b72039c8 | |||
| 32534251bf | |||
| 1a03d56c80 | |||
| e043c1baff | |||
| a9d3709663 | |||
| 741948682f | |||
| 68b78bf6d2 | |||
| 0e756c653d | |||
| 69de709b0f | |||
| d11163629b | |||
| e92c8766f6 | |||
| 334e468889 | |||
| 33e6807ca4 | |||
| 2029acc5a9 | |||
| d1788b7c65 | |||
| 84ab4bb6b8 | |||
| 04451ad537 | |||
| d09a118e54 | |||
| 5b982a3b0f | |||
| 71636b8250 | |||
| e7df1a43a3 | |||
| cfc94a37b3 | |||
| 22b77168ed | |||
| ffcd657a88 | |||
| 39fd04473c | |||
| 49613f7e71 |
+66
-1
@@ -1,4 +1,6 @@
|
||||
This diagram shows components of the chatmail server; this is a draft
|
||||
## Chatmail Relay
|
||||
|
||||
This diagram shows components of the chatmail relay; this is a draft
|
||||
overview as of mid-August 2025:
|
||||
|
||||
```mermaid
|
||||
@@ -48,3 +50,66 @@ graph LR;
|
||||
The edges in this graph should not be taken too literally; they
|
||||
reflect some sort of communication path or dependency relationship
|
||||
between components of the chatmail server.
|
||||
|
||||
## cmdeploy
|
||||
|
||||
cmdeploy is a Python program that uses the pyinfra library to deploy
|
||||
chatmail servers, with all the necessary software, configuration, and
|
||||
services. The deployment process performs three primary types of operation:
|
||||
|
||||
1. Installation of software, universal across all deployments.
|
||||
2. Configuration of software, with deploy-specific variations.
|
||||
3. Activation of services.
|
||||
|
||||
The process is implemented through a family of "deployer" objects
|
||||
which all derive from a common `Deployer` base class, defined in
|
||||
[deployer.py](cmdeploy/src/cmdeploy/deployer.py). Each object
|
||||
provides implementation methods for the three stages -- install,
|
||||
configure, and activate. The top-level procedure in
|
||||
`deploy_chatmail()` calls these methods for all the deployer objects,
|
||||
first calling all the install methods, then the configure methods,
|
||||
then the activate methods.
|
||||
|
||||
The base class also implements support for a CMDEPLOY_STAGES
|
||||
environment variable, which allows limiting the process to specific
|
||||
stages. Note that some deployers are stateful between the stages
|
||||
(this is one reason why they are implemented as objects), and that
|
||||
state will not get propagated between stages when run in separate
|
||||
invocations of cmdeploy. This environment variable is intended for
|
||||
use in future revisions to support building Docker images with
|
||||
software pre-installed, and configuration of containers at run time
|
||||
from environmnet variables.
|
||||
|
||||
The `install_impl()` method for the deployer classes is static, to
|
||||
ensure that it does not rely on any object state, in particular, the
|
||||
configuration details of the deployment. This helps ensure that all
|
||||
install methods are suitable for running as part of a container image
|
||||
build.
|
||||
|
||||
Operations that start services for systemd-based deployments should
|
||||
only be called from the `activate_impl()` methods. These methods will
|
||||
not be called in non-systemd container environments.
|
||||
|
||||
### Deployer objects
|
||||
|
||||
One might ask why the deployers are implemented as object classes, as
|
||||
opposed to callable functions or the like. There are various reasons
|
||||
why objects are a good fit for the deployment process.
|
||||
|
||||
1. Objects provide a way to organize the install, configure, and
|
||||
deploy operations for each component that is installed, supporting a
|
||||
"driver" type of pattern. This could be implemented in other ways
|
||||
without objects, such as function jump tables, but objects provide a
|
||||
clean and formalized way to do essentially the same thing.
|
||||
|
||||
2. Class inheritance provides a natural way to define
|
||||
component-specific operations for the various stages of deployment, by
|
||||
overriding the no-op stub methods in the base class. The base class
|
||||
handles policy decisions about which stages are to be executed,
|
||||
ensuring consistent handling of the stages in a central location.
|
||||
|
||||
3. Some of the components track state between stages, basing decisions
|
||||
like whether to restart a service on whether the software or
|
||||
configuration of that service was changed in an earlier stage.
|
||||
Keeping track of state between method calls is an ideal use case for
|
||||
objects.
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
|
||||
## untagged
|
||||
|
||||
- Organized cmdeploy into install, configure, and activate stages
|
||||
([#695](https://github.com/chatmail/relay/pull/695))
|
||||
|
||||
- don't deploy the website if there are merge conflicts in the www folder
|
||||
([#714](https://github.com/chatmail/relay/pull/714))
|
||||
|
||||
|
||||
+589
-390
File diff suppressed because it is too large
Load Diff
@@ -2,66 +2,79 @@ import importlib.resources
|
||||
|
||||
from pyinfra.operations import apt, files, server, systemd
|
||||
|
||||
from ..deployer import Deployer
|
||||
|
||||
def deploy_acmetool(email="", domains=[]):
|
||||
"""Deploy acmetool."""
|
||||
apt.packages(
|
||||
name="Install acmetool",
|
||||
packages=["acmetool"],
|
||||
)
|
||||
|
||||
files.put(
|
||||
src=importlib.resources.files(__package__).joinpath("acmetool.cron").open("rb"),
|
||||
dest="/etc/cron.d/acmetool",
|
||||
user="root",
|
||||
group="root",
|
||||
mode="644",
|
||||
)
|
||||
class AcmetoolDeployer(Deployer):
|
||||
def __init__(self, *, email, domains, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.domains = domains
|
||||
self.email = email
|
||||
self.need_restart = False
|
||||
|
||||
files.put(
|
||||
src=importlib.resources.files(__package__).joinpath("acmetool.hook").open("rb"),
|
||||
dest="/usr/lib/acme/hooks/nginx",
|
||||
user="root",
|
||||
group="root",
|
||||
mode="744",
|
||||
)
|
||||
@staticmethod
|
||||
def install_impl():
|
||||
apt.packages(
|
||||
name="Install acmetool",
|
||||
packages=["acmetool"],
|
||||
)
|
||||
|
||||
files.template(
|
||||
src=importlib.resources.files(__package__).joinpath("response-file.yaml.j2"),
|
||||
dest="/var/lib/acme/conf/responses",
|
||||
user="root",
|
||||
group="root",
|
||||
mode="644",
|
||||
email=email,
|
||||
)
|
||||
def configure_impl(self):
|
||||
files.put(
|
||||
src=importlib.resources.files(__package__).joinpath("acmetool.cron").open("rb"),
|
||||
dest="/etc/cron.d/acmetool",
|
||||
user="root",
|
||||
group="root",
|
||||
mode="644",
|
||||
)
|
||||
|
||||
files.template(
|
||||
src=importlib.resources.files(__package__).joinpath("target.yaml.j2"),
|
||||
dest="/var/lib/acme/conf/target",
|
||||
user="root",
|
||||
group="root",
|
||||
mode="644",
|
||||
)
|
||||
files.put(
|
||||
src=importlib.resources.files(__package__).joinpath("acmetool.hook").open("rb"),
|
||||
dest="/usr/lib/acme/hooks/nginx",
|
||||
user="root",
|
||||
group="root",
|
||||
mode="744",
|
||||
)
|
||||
|
||||
service_file = files.put(
|
||||
src=importlib.resources.files(__package__).joinpath(
|
||||
"acmetool-redirector.service"
|
||||
),
|
||||
dest="/etc/systemd/system/acmetool-redirector.service",
|
||||
user="root",
|
||||
group="root",
|
||||
mode="644",
|
||||
)
|
||||
files.template(
|
||||
src=importlib.resources.files(__package__).joinpath("response-file.yaml.j2"),
|
||||
dest="/var/lib/acme/conf/responses",
|
||||
user="root",
|
||||
group="root",
|
||||
mode="644",
|
||||
email=self.email,
|
||||
)
|
||||
|
||||
systemd.service(
|
||||
name="Setup acmetool-redirector service",
|
||||
service="acmetool-redirector.service",
|
||||
running=True,
|
||||
enabled=True,
|
||||
restarted=service_file.changed,
|
||||
)
|
||||
files.template(
|
||||
src=importlib.resources.files(__package__).joinpath("target.yaml.j2"),
|
||||
dest="/var/lib/acme/conf/target",
|
||||
user="root",
|
||||
group="root",
|
||||
mode="644",
|
||||
)
|
||||
|
||||
server.shell(
|
||||
name=f"Request certificate for: {', '.join(domains)}",
|
||||
commands=[f"acmetool want --xlog.severity=debug {' '.join(domains)}"],
|
||||
)
|
||||
service_file = files.put(
|
||||
src=importlib.resources.files(__package__).joinpath(
|
||||
"acmetool-redirector.service"
|
||||
),
|
||||
dest="/etc/systemd/system/acmetool-redirector.service",
|
||||
user="root",
|
||||
group="root",
|
||||
mode="644",
|
||||
)
|
||||
self.need_restart = service_file.changed
|
||||
|
||||
def activate_impl(self):
|
||||
systemd.service(
|
||||
name="Setup acmetool-redirector service",
|
||||
service="acmetool-redirector.service",
|
||||
running=True,
|
||||
enabled=True,
|
||||
restarted=self.need_restart,
|
||||
)
|
||||
self.need_restart = False
|
||||
|
||||
server.shell(
|
||||
name=f"Request certificate for: {', '.join(self.domains)}",
|
||||
commands=[f"acmetool want --xlog.severity=debug {' '.join(self.domains)}"],
|
||||
)
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import os
|
||||
|
||||
from pyinfra.operations import server
|
||||
|
||||
|
||||
class Deployment:
|
||||
def install(self, deployer):
|
||||
# optional 'required_users' contains a list of (user, group, secondary-group-list) tuples.
|
||||
# If the group is None, no group is created corresponding to that user.
|
||||
# If the secondary group list is not None, all listed groups are created as well.
|
||||
required_users = getattr(deployer, "required_users", [])
|
||||
for user, group, groups in required_users:
|
||||
if group is not None:
|
||||
server.group(
|
||||
name="Create {} group".format(group), group=group, system=True
|
||||
)
|
||||
if groups is not None:
|
||||
for group2 in groups:
|
||||
server.group(
|
||||
name="Create {} group".format(group2), group=group2, system=True
|
||||
)
|
||||
server.user(
|
||||
name="Create {} user".format(user),
|
||||
user=user,
|
||||
group=group,
|
||||
groups=groups,
|
||||
system=True,
|
||||
)
|
||||
|
||||
ret = bool(deployer.install())
|
||||
if ret:
|
||||
deployer.need_restart = True
|
||||
|
||||
def configure(self, deployer):
|
||||
deployer.configure()
|
||||
|
||||
def activate(self, deployer):
|
||||
deployer.activate()
|
||||
|
||||
def perform_stages(self, deployers):
|
||||
default_stages = "install,configure,activate"
|
||||
stages = os.getenv("CMDEPLOY_STAGES", default_stages).split(",")
|
||||
|
||||
for stage in stages:
|
||||
for deployer in deployers:
|
||||
getattr(self, stage)(deployer)
|
||||
|
||||
|
||||
class Deployer:
|
||||
need_restart = False
|
||||
|
||||
def install(self):
|
||||
pass
|
||||
|
||||
def configure(self):
|
||||
pass
|
||||
|
||||
def activate(self):
|
||||
pass
|
||||
Binary file not shown.
Executable
+3
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
echo "All runlevel operations denied by policy" >&2
|
||||
exit 101
|
||||
Reference in New Issue
Block a user