forked from lab-in-a-box/liab-deployments
344 lines
10 KiB
Python
344 lines
10 KiB
Python
#
|
|
# Deploy Forgejo(-aneksajo) (https://codeberg.org/forgejo-aneksajo/forgejo-aneksajo)
|
|
#
|
|
# Does NOT support multiple deployments under the SAME user account.
|
|
#
|
|
# The deployment has two principle modes of operation:
|
|
# 1) Providing `config_file_asset` allows for deploying or (re)configuring
|
|
# a fully set up forgejo instance; 2) Not providing `config_file_asset` leaves
|
|
# the instance unconfigured, and forgejo will present the initialization wizard
|
|
# that allows for a convenient, basic setup, incl. the creation of an admin account.
|
|
# With (1), no forgejo accounts will be set up, also no admin account. This needs
|
|
# to be achieved with a different method (e.g., using the CLI, or a dedicated
|
|
# user account deployment.
|
|
#
|
|
# Example inventory
|
|
#
|
|
# group = [
|
|
# (
|
|
# '<host-FQDN>',
|
|
# {
|
|
# 'forgejo': {
|
|
# # defaults can provide setting for multiple site deployments
|
|
# 'defaults': {
|
|
# 'container_tag': 'hub.datalad.org/forgejo/forgejo-aneksajo:14-rootless',
|
|
# # repo with customizations to fetch
|
|
# # (should be from other host for robust behavior)
|
|
# # can also be a (url, branch) tuple
|
|
# 'customizations_fetch_url':
|
|
# 'https://<otherhost>/some.git',
|
|
# # credential to fetch the customization, password should be privy-encrypted
|
|
# 'customizations_fetch_auth': ('username', 'password'),
|
|
# # if path to annex-get are given, repo is annex-init'ed too
|
|
# 'customizations_annex_get': '.',
|
|
# 'caddyfile_block_tmpl':
|
|
# """\
|
|
# {serve_address} {{
|
|
# reverse_proxy localhost:{host_port}
|
|
# import cors *
|
|
# }}
|
|
# """,
|
|
# # additional directories to create in the deployment
|
|
# # HOME (as relative dirs). This can be necessary, when
|
|
# # a particular forgejo configuration puts information
|
|
# # into a non-standard location
|
|
# 'ensure_dirs': ['git/repositories', 'gitea/log'],
|
|
# },
|
|
# 'sites': [
|
|
# {
|
|
# 'serve_address': 'hub.example.org',
|
|
# 'user': ('hub', '2000'),
|
|
# 'host_port': 30001,
|
|
# 'config_file_asset': 'assets/hub-example-org-app.ini',
|
|
# },
|
|
# ],
|
|
# }
|
|
# },
|
|
# ),
|
|
# ]
|
|
|
|
|
|
from getpass import getpass
|
|
import os
|
|
import privy
|
|
from pyinfra.api import deploy as _deploy
|
|
from pyinfra import (
|
|
host,
|
|
)
|
|
from pyinfra.operations import (
|
|
files,
|
|
git,
|
|
server,
|
|
)
|
|
from urllib.parse import urlparse
|
|
from liab_deployments.operations import (
|
|
caddy,
|
|
user,
|
|
user_systemd,
|
|
)
|
|
|
|
|
|
service_unit_tmpl = """\
|
|
[Unit]
|
|
Description=Podman container-{name}.service
|
|
Wants=network-online.target
|
|
After=network-online.target
|
|
RequiresMountsFor=%t/containers
|
|
|
|
[Service]
|
|
Environment=PODMAN_SYSTEMD_UNIT=%n
|
|
Restart=always
|
|
TimeoutStopSec=300
|
|
ExecStartPre=/bin/rm \\
|
|
-f %t/%n.ctr-id
|
|
ExecStart=/usr/bin/podman container run \\
|
|
--cidfile=%t/%n.ctr-id \\
|
|
--cgroups=no-conmon \\
|
|
--rm \\
|
|
--sdnotify=conmon \\
|
|
-d \\
|
|
--replace \\
|
|
--pull always \\
|
|
--name {name} \\
|
|
-p {host_port}:3000 \\
|
|
-v {user_home}/gitea:/var/lib/gitea:Z \\
|
|
-v {user_home}/custom:/var/lib/gitea/custom:Z \\
|
|
-v {user_home}/conf:/var/lib/gitea/custom/conf:Z \\
|
|
-v {user_home}/git:/var/lib/gitea/git:Z \\
|
|
--userns keep-id:uid=1000,gid=1000 \\
|
|
{container_tag}
|
|
ExecStop=/usr/bin/podman stop \\
|
|
--ignore -t 10 \\
|
|
--cidfile=%t/%n.ctr-id
|
|
ExecStopPost=/usr/bin/podman rm \\
|
|
-f \\
|
|
--ignore -t 10 \\
|
|
--cidfile=%t/%n.ctr-id
|
|
Type=notify
|
|
NotifyAccess=all
|
|
|
|
[Install]
|
|
WantedBy=default.target
|
|
"""
|
|
|
|
caddyfile_block_tmpl = """\
|
|
{serve_address} {{
|
|
reverse_proxy localhost:{host_port}
|
|
}}
|
|
"""
|
|
|
|
|
|
# used for the systemd service (lower), and the caddy block marker (upper),
|
|
# and in documentation (verbatim capitalization)
|
|
name = 'Forgejo'
|
|
|
|
|
|
@_deploy(f"Deploy {name}")
|
|
def deploy():
|
|
if not hasattr(host.data, name.lower()):
|
|
return
|
|
defaults = host.data.forgejo.get('defaults', {})
|
|
for spec in host.data.forgejo.get('sites', []):
|
|
_deploy_forgejo(
|
|
spec['serve_address'],
|
|
spec['container_tag']
|
|
if 'container_tag' in spec
|
|
else defaults['container_tag'],
|
|
spec['user'],
|
|
spec['host_port'],
|
|
caddyfile_block_tmpl=spec.get(
|
|
'caddyfile_block_tmpl',
|
|
defaults.get(
|
|
'caddyfile_block_tmpl',
|
|
caddyfile_block_tmpl)),
|
|
service_unit_tmpl=spec.get(
|
|
'service_unit_tmpl',
|
|
defaults.get(
|
|
'service_unit_tmpl',
|
|
service_unit_tmpl)),
|
|
config_file_asset=spec['config_file_asset']
|
|
if 'config_file_asset' in spec else None,
|
|
customizations_fetch_url=spec.get(
|
|
'customizations_fetch_url',
|
|
defaults.get('customizations_fetch_url')),
|
|
customizations_fetch_auth=spec.get(
|
|
'customizations_fetch_auth',
|
|
defaults.get('customizations_fetch_auth')),
|
|
customizations_annex_get=spec.get(
|
|
'customizations_annex_get',
|
|
defaults.get('customizations_annex_get')),
|
|
ensure_dirs=spec.get(
|
|
'ensure_dirs',
|
|
defaults.get('ensure_dirs')),
|
|
)
|
|
|
|
|
|
def _deploy_forgejo(
|
|
serve_address: str,
|
|
container_tag: str,
|
|
user_spec: tuple[str, int],
|
|
host_port: int,
|
|
caddyfile_block_tmpl: str,
|
|
service_unit_tmpl: str,
|
|
*,
|
|
config_file_asset: str | None = None,
|
|
customizations_fetch_url: str | tuple[str] | None = None,
|
|
customizations_fetch_auth: tuple[str, str] | None = None,
|
|
customizations_annex_get: str | None = None,
|
|
ensure_dirs: list[str] | None = None,
|
|
):
|
|
user_name, uid = user_spec
|
|
user_home = f'/home/{user_name}'
|
|
|
|
user.systemd_service(
|
|
user_name,
|
|
uid,
|
|
user_home,
|
|
)
|
|
if ensure_dirs is None:
|
|
# this is the set of directories that a "default" config
|
|
# expects, but that is NOT auto-created by forgejo when
|
|
# the installer is NOT running (when a full config is
|
|
# deployed)
|
|
ensure_dirs = ['git/repositories', 'gitea/log']
|
|
else:
|
|
# even if dirs are given, the container setup requires these
|
|
ensure_dirs += ['git', 'gitea']
|
|
|
|
for i in ensure_dirs:
|
|
files.directory(
|
|
path=f'{user_home}/{i}',
|
|
present=True,
|
|
_sudo_user=user_name,
|
|
)
|
|
|
|
user_systemd.service_unit(
|
|
user_name,
|
|
user_home,
|
|
name.lower(),
|
|
service_unit_tmpl.format(
|
|
name=name.lower(),
|
|
user_home=user_home,
|
|
container_tag=container_tag,
|
|
host_port=host_port,
|
|
),
|
|
)
|
|
|
|
if customizations_fetch_url:
|
|
_do_customization(
|
|
user_name,
|
|
f'{user_home}/custom',
|
|
customizations_fetch_url,
|
|
customizations_fetch_auth,
|
|
customizations_annex_get,
|
|
)
|
|
|
|
# needs to be done after a clone of the customization repo
|
|
if config_file_asset:
|
|
files.directory(
|
|
path=f'{user_home}/conf',
|
|
present=True,
|
|
_sudo_user=user_name,
|
|
)
|
|
files.put(
|
|
name=f'{name} config',
|
|
src=config_file_asset,
|
|
dest=f'{user_home}/conf/app.ini',
|
|
_sudo_user=user_name,
|
|
)
|
|
|
|
user_systemd.run_service(
|
|
user_name,
|
|
uid,
|
|
name.lower(),
|
|
)
|
|
|
|
server.wait(
|
|
name=f"Wait for {name} {serve_address!r} to start",
|
|
port=host_port,
|
|
)
|
|
|
|
caddy.caddyfile_block(
|
|
marker=f'{name.upper()} {serve_address}',
|
|
content=caddyfile_block_tmpl.format(
|
|
serve_address=serve_address,
|
|
host_port=host_port,
|
|
),
|
|
)
|
|
|
|
|
|
def _do_customization(
|
|
user_name, dest_dir, fetch_url, fetch_auth, annex_get,
|
|
):
|
|
if isinstance(fetch_url, tuple):
|
|
furl, fbranch = fetch_url
|
|
else:
|
|
furl = fetch_url
|
|
fbranch = None
|
|
url_p = urlparse(furl)
|
|
if fetch_auth:
|
|
_do_git_credential(url_p, user_name, furl, fetch_auth)
|
|
|
|
# we need a git-identity for git-annex-init to function
|
|
git.config(
|
|
name="Ensure Git user name is set",
|
|
key="user.name",
|
|
value=user_name,
|
|
_sudo_user=user_name,
|
|
)
|
|
|
|
git.config(
|
|
name="Ensure Git user email is set",
|
|
key="user.email",
|
|
value=f"{user_name}@localhost",
|
|
_sudo_user=user_name,
|
|
)
|
|
|
|
git.repo(
|
|
name="Clone customizations",
|
|
src=furl,
|
|
branch=fbranch,
|
|
dest=dest_dir,
|
|
pull=True,
|
|
rebase=True,
|
|
_sudo_user=user_name,
|
|
)
|
|
|
|
if annex_get:
|
|
server.shell(
|
|
name='Fetch customizations annex',
|
|
commands=[
|
|
f'cd "{dest_dir}" '
|
|
'&& git -c annex.private=true annex init '
|
|
f'&& git annex get {annex_get}',
|
|
# TODO unused and dropunused
|
|
],
|
|
_sudo_user=user_name,
|
|
)
|
|
|
|
|
|
def _do_git_credential(url_p, user_name, fetch_url, fetch_auth):
|
|
privy_password = os.environ.get('PRIVY_PASSWORD')
|
|
if not privy_password:
|
|
privy_password = getpass('Privy password to decode credential: ')
|
|
if not privy_password:
|
|
raise ValueError
|
|
|
|
gf_user, gf_password = fetch_auth
|
|
gf_password = privy.peek(gf_password, privy_password).decode()
|
|
|
|
git.config(
|
|
name='Enable Git credential caching',
|
|
key='credential.helper',
|
|
value='cache --timeout=3600',
|
|
system=False,
|
|
_sudo_user=user_name,
|
|
)
|
|
|
|
server.shell(
|
|
name='Prime Git credential cache',
|
|
commands=[
|
|
f'echo "protocol={url_p.scheme}\nhost={url_p.netloc}\nusername={gf_user}\npassword={gf_password}" | git credential approve',
|
|
],
|
|
_sudo_user=user_name,
|
|
)
|