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
|
from pathlib import Path
import asyncio
import contextlib
import itertools
import logging.config
import subprocess
import sys
import tempfile
import typing
from hat import aio
from hat import json
import appdirs
import click
from hatter import common
import hatter.backend
import hatter.server
import hatter.ui
user_config_dir: Path = Path(appdirs.user_config_dir('hatter'))
user_data_dir: Path = Path(appdirs.user_data_dir('hatter'))
default_conf_path: Path = user_config_dir / 'server.yaml'
default_db_path: Path = user_data_dir / 'hatter.db'
ssh_key_path: typing.Optional[Path] = None
@click.group()
@click.option('--log-level',
default='INFO',
type=click.Choice(['CRITICAL', 'ERROR', 'WARNING', 'INFO',
'DEBUG', 'NOTSET']),
help="log level")
@click.option('--ssh-key', default=None, metavar='PATH', type=Path,
help="private key used for ssh authentication")
def main(log_level: str,
ssh_key: typing.Optional[Path]):
global ssh_key_path
ssh_key_path = ssh_key
logging.config.dictConfig({
'version': 1,
'formatters': {
'console': {
'format': "[%(asctime)s %(levelname)s %(name)s] %(message)s"}},
'handlers': {
'console': {
'class': 'logging.StreamHandler',
'formatter': 'console',
'level': log_level}},
'root': {
'level': log_level,
'handlers': ['console']},
'disable_existing_loggers': False})
@main.command()
@click.option('--action', default='.hatter.yaml',
help="action file path inside repository")
@click.option('--env', multiple=True,
help="environment variables")
@click.argument('url', required=True)
@click.argument('ref', required=False, default='HEAD')
def execute(action: str,
env: typing.Tuple[str],
url: str,
ref: str):
with contextlib.suppress(Exception):
path = Path(url)
if path.exists():
url = str(path.resolve())
with tempfile.TemporaryDirectory() as repo_dir:
repo_dir = Path(repo_dir)
subprocess.run(['git', 'init', '-q'],
cwd=str(repo_dir),
check=True)
subprocess.run(['git', 'remote', 'add', 'origin', url],
cwd=str(repo_dir),
check=True)
subprocess.run(['git', 'fetch', '-q', '--depth=1', 'origin', ref],
cwd=str(repo_dir),
check=True)
subprocess.run(['git', 'checkout', '-q', 'FETCH_HEAD'],
cwd=str(repo_dir),
check=True)
conf = json.decode_file(repo_dir / action)
common.json_schema_repo.validate('hatter://action.yaml#', conf)
image = conf['image']
command = conf['command']
subprocess.run(['podman', 'run', '-i', '--rm',
'-v', f'{repo_dir}:/hatter',
*itertools.chain.from_iterable(('--env', i)
for i in env),
image, '/bin/sh'],
input=f'set -e\ncd /hatter\n{command}\n',
encoding='utf-8',
check=True)
@main.command()
@click.option('--host', default='0.0.0.0',
help="listening host name (default 0.0.0.0)")
@click.option('--port', default=24000, type=int,
help="listening TCP port (default 24000)")
@click.option('--conf', default=default_conf_path, metavar='PATH', type=Path,
help="configuration defined by hatter://server.yaml# "
"(default $XDG_CONFIG_HOME/hatter/server.yaml)")
@click.option('--db', default=default_db_path, metavar='PATH', type=Path,
help="sqlite database path "
"(default $XDG_CONFIG_HOME/hatter/hatter.db")
def server(host: str,
port: int,
conf: Path,
db: Path):
conf = json.decode_file(conf)
common.json_schema_repo.validate('hatter://server.yaml#', conf)
with contextlib.suppress(asyncio.CancelledError):
aio.run_asyncio(async_server(host, port, conf, db))
async def async_server(host: str,
port: int,
conf: json.Data,
db_path: Path):
async_group = aio.Group()
try:
backend = await hatter.backend.create(db_path)
_bind_resource(async_group, backend)
server = await hatter.server.create(conf, backend)
_bind_resource(async_group, server)
ui = await hatter.ui.create(host, port, server)
_bind_resource(async_group, ui)
await async_group.wait_closing()
finally:
await aio.uncancellable(async_group.async_close())
def _bind_resource(async_group, resource):
async_group.spawn(aio.call_on_cancel, resource.async_close)
async_group.spawn(aio.call_on_done, resource.wait_closing(),
async_group.close)
if __name__ == '__main__':
sys.argv[0] = 'hatter'
main()
|