-
Notifications
You must be signed in to change notification settings - Fork 115
/
fabfile.py
193 lines (146 loc) · 6.11 KB
/
fabfile.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
# TODO: Refactor or replace this file. It's kinda overengineered and fabric
# is terribly documented
import json
import os
import posixpath
from fabric import Config, Connection, task
from invoke import Collection
from invoke.config import merge_dicts
ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
CONFIG_DIR = os.path.join(ROOT_DIR, "config", "deploy")
class ConfigFiles:
SHARED = os.path.join(CONFIG_DIR, "shared.json")
GITHUB_ACTIONS = os.path.join(CONFIG_DIR, "github_actions.json")
PROD = os.path.join(CONFIG_DIR, "prod.json")
class DeployConfig(Config):
@staticmethod
def global_defaults():
hkn_shared = json.load(open(ConfigFiles.SHARED))
return merge_dicts(Config.global_defaults(), hkn_shared)
def setup(c: Connection, revision=None, release=None):
print("== Setup ==")
# Returns the server date-time, encoded as YYYYMMSS_HHMMSS.
timestamp = c.run("date +%Y%m%d_%H%M%S").stdout.strip()
# Point the connection to the correct git config
c.release = release if release else timestamp
c.revision = revision if revision else c.deploy.branch
print(f"release: {c.release}")
print(f"revision: {c.revision}")
# Setup paths
c.deploy_path = posixpath.join(c.deploy.path.root, c.deploy.name)
c.repo_path = posixpath.join(c.deploy_path, c.deploy.path.repo)
c.releases_path = posixpath.join(c.deploy_path, c.deploy.path.releases)
c.current_path = posixpath.join(c.deploy_path, c.deploy.path.current)
c.shared_path = posixpath.join(c.deploy_path, c.deploy.path.shared)
c.release_path = posixpath.join(c.releases_path, c.release)
# Create dirs
dirs = (
c.repo_path,
c.deploy_path,
c.releases_path,
c.shared_path,
c.release_path,
)
for d in dirs:
c.run(f"mkdir -p {d}")
def update(c: Connection):
print("== Update ==")
if c.deploy.use_local_repo:
c.deploy.repo_url = (
c.run("git config --get remote.origin.url").stdout.strip() + ".git"
)
c.revision = c.run("git rev-parse HEAD").stdout.strip()
c.run(f"git clone --bare {c.deploy.repo_url} {c.repo_path}", echo=True)
file_exists = lambda p: c.run(f"[[ -f {p} ]]", warn=True).ok
repo_exists = file_exists(f"{c.repo_path}/HEAD")
if repo_exists: # fetch
with c.cd(c.repo_path):
c.run(f"git remote set-url origin {c.deploy.repo_url}", echo=True)
c.run("git remote update", echo=True)
c.run(f"git fetch origin {c.revision}:{c.revision} --force", echo=True)
else: # clone
c.run(f"git clone --bare {c.deploy.repo_url} {c.repo_path}", echo=True)
with c.cd(c.repo_path):
print("-- Creating git archive for release")
revision = c.revision
revision_number = c.run(
f"git rev-list --max-count=1 {revision} --", echo=True
).stdout.strip()
c.revision = revision_number
c.run(
f"git archive {c.revision} | tar -x -f - -C '{c.release_path}'", echo=True
)
with c.cd(c.release_path):
print("-- Symlinking shared files")
c.run(f"ln -s {c.shared_path}/media ./media", echo=True)
if c.deploy.run_blackbox_postdeploy:
print("-- Decrypting secrets")
c.run("blackbox_postdeploy", echo=True)
else:
print("-- Skipping decrypting secrets")
print("-- Updating dependencies")
c.run("poetry install --with prod")
# Can't figure out how to properly set an env var with fabric
# so leaving them at the starts of the commands for now
print("-- Running migrations")
c.run(f"HKNWEB_MODE={hknweb_mode} poetry run python manage.py migrate")
if c.deploy.run_collectstatic:
print("-- Collecting static files")
c.run(
f"HKNWEB_MODE={hknweb_mode} poetry run python manage.py collectstatic --noinput"
)
else:
print("-- Skipping collecting static files")
def publish(c: Connection) -> None:
print("== Publish ==")
print("-- Symlinking current@ to release")
c.run(f"ln -sfn {c.release_path} {c.current_path}", echo=True)
c.run(f"chmod +x {c.current_path}/run", echo=True)
print("-- Restarting systemd unit")
c.run("systemctl --user restart hknweb.service", echo=True)
"""
For the following @task functions, "target" is an ignored parameter. It is a workaround to
allow for use in using command line arguments for selecting a "target" in ns.configure.
Otherwise, fabfile will claim to not recognize it.
"""
@task
def deploy(c, target=None, revision=None):
with Connection(c.deploy.host, user=c.deploy.user, config=c.config) as c:
setup(c, revision=revision)
update(c)
publish(c)
@task
def rollback(c, target=None, release=None):
with Connection(c.deploy.host, user=c.deploy.user, config=c.config) as c:
setup(c, release=release)
update(c)
publish(c)
@task
def deploy_github_actions(c, target=None):
with Connection(c.deploy.host, user=c.deploy.user, config=c.config) as c:
c.run = c.local
setup(c)
update(c)
"""
The local publish flow is slightly different than the default deploy publish flow.
We don't use systemctl to restart a service; instead, we use a custom run script
"""
print("== Publish ==")
print("-- Symlinking current@ to release")
c.run(f"ln -sfn {c.release_path} {c.current_path}", echo=True)
print("-- Skipping restarting systemd unit")
with c.cd(c.current_path):
print("-- Starting server")
c.run(f"bash ./scripts/run_github_actions.sh {c.current_path}")
hknweb_mode = os.getenv("HKNWEB_MODE", "dev").lower()
if hknweb_mode == "dev":
config_file = ConfigFiles.GITHUB_ACTIONS
deploy = deploy_github_actions
elif hknweb_mode == "prod":
config_file = ConfigFiles.PROD
else:
raise ValueError(f"HKNWEB_MODE {hknweb_mode!r} is not a valid value")
config_dict = json.load(open(config_file))
config = DeployConfig(overrides=config_dict)
ns = Collection(deploy=deploy, rollback=rollback)
ns.configure(config)