-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #100 from chsasank/app-store
Add app store
- Loading branch information
Showing
48 changed files
with
1,324 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,298 @@ | ||
import os | ||
import argparse | ||
import glob | ||
import socket | ||
import tinydb | ||
import shutil | ||
from parser import ( | ||
parse_sexp, | ||
generate_systemd, | ||
lookup_sexp, | ||
parse_env_file, | ||
generate_env_file, | ||
) | ||
from systemd import ( | ||
reload_units, | ||
status_units, | ||
start_units, | ||
stop_units, | ||
restart_units, | ||
log_units, | ||
podman_pull, | ||
podman_build, | ||
) | ||
from interpreter import config_lisp | ||
|
||
definitions_path = os.path.join( | ||
os.path.dirname(os.path.realpath(__file__)), "../definitions/*/*.lisp" | ||
) | ||
definitions = { | ||
os.path.basename(f)[: -len(".lisp")]: f for f in glob.glob(definitions_path) | ||
} | ||
|
||
app_dir = os.path.join(os.path.expanduser("~"), ".johnny") | ||
os.makedirs(app_dir, exist_ok=True) | ||
app_db = tinydb.TinyDB(os.path.join(app_dir, "app_db.json")) | ||
|
||
systemd_dir = os.path.join(os.path.expanduser("~"), ".config/containers/systemd/") | ||
os.makedirs(systemd_dir, exist_ok=True) | ||
|
||
|
||
def get_random_free_port(): | ||
sock = socket.socket() | ||
sock.bind(("", 0)) | ||
return sock.getsockname()[1] | ||
|
||
|
||
def gen_pod(app_name, ports): | ||
App = tinydb.Query() | ||
app_data = app_db.search(App.name == app_name) | ||
|
||
port_mapping = {str(p): str(get_random_free_port()) for p in ports} | ||
if app_data: | ||
# if app_data exists, overwrite with old ports | ||
old_ports = app_data[0]["ports"] | ||
port_mapping = {k: old_ports.get(k, v) for k, v in port_mapping.items()} | ||
app_db.update({"ports": port_mapping}, App.name == app_name) | ||
else: | ||
app_db.insert({"name": app_name, "ports": port_mapping}) | ||
|
||
pod_service_data = [ | ||
[ | ||
"Pod", | ||
[ | ||
["PublishPort", f"{p_host}:{p_container}"] | ||
for p_container, p_host in port_mapping.items() | ||
], | ||
], | ||
["Install", [["WantedBy", "default.target"]]], | ||
] | ||
return generate_systemd(pod_service_data) | ||
|
||
|
||
def gen_container(app_name, container): | ||
definitions_dir = os.path.dirname(definitions[app_name]) | ||
try: | ||
image = lookup_sexp(container, "image")[0] | ||
podman_pull(image) | ||
except KeyError: | ||
image = lookup_sexp(container, "build")[0] | ||
podman_build(image, definitions_dir) | ||
|
||
try: | ||
volumes = lookup_sexp(container, "volumes") | ||
except KeyError: | ||
volumes = [] | ||
container_name = lookup_sexp(container, "name")[0] | ||
|
||
# volumes | ||
volume_mapping = {} | ||
app_data_dir = os.path.join(app_dir, app_name) | ||
for name, container_path in volumes: | ||
# check if name is found in definitions_path | ||
# if so copy it to app_data_dir | ||
defns_path = os.path.join(definitions_dir, name) | ||
host_path = os.path.join(app_data_dir, name) | ||
if os.path.exists(host_path): | ||
# do nothing | ||
pass | ||
elif os.path.isfile(defns_path): | ||
shutil.copy(defns_path, host_path) | ||
elif os.path.isdir(defns_path): | ||
shutil.copytree(defns_path, host_path) | ||
else: | ||
# create a new dir if nothing exists | ||
os.makedirs(host_path) | ||
|
||
volume_mapping[host_path] = container_path | ||
|
||
# environment | ||
try: | ||
env = lookup_sexp(container, "environment") | ||
except KeyError: | ||
env = [] | ||
env = {k: v for k, v in env} | ||
env_file_name = os.path.join(app_data_dir, f"{container_name}.env") | ||
if os.path.isfile(env_file_name): | ||
with open(env_file_name) as f: | ||
env_existing = parse_env_file(f.read()) | ||
|
||
# keep existing values | ||
env.update(env_existing) | ||
|
||
with open(env_file_name, "w") as f: | ||
f.write(generate_env_file(env)) | ||
|
||
# additional flags | ||
try: | ||
additional_flags = lookup_sexp(container, "additional-flags") | ||
except KeyError: | ||
additional_flags = [] | ||
|
||
additional_flags = " ".join(additional_flags) | ||
|
||
# command | ||
try: | ||
command = lookup_sexp(container, "command")[0] | ||
except (KeyError, IndexError): | ||
command = "" | ||
|
||
# entrypoint | ||
try: | ||
entrypoint = lookup_sexp(container, "entrypoint")[0] | ||
except (KeyError, IndexError): | ||
entrypoint = "" | ||
|
||
container_options = [ | ||
["Image", image], | ||
["Pod", f"{app_name}.pod"], | ||
["EnvironmentFile", env_file_name], | ||
["PodmanArgs", additional_flags], | ||
["Exec", command], | ||
] | ||
for host_dir, container_dir in volume_mapping.items(): | ||
container_options.append(["Volume", f"{host_dir}:{container_dir}"]) | ||
|
||
if entrypoint: | ||
container_options.append(["Entrypoint", entrypoint]) | ||
|
||
container_service_data = [ | ||
["Container", container_options], | ||
[ | ||
"Service", | ||
[["Restart", "always"]], | ||
], | ||
["Install", [["WantedBy", "default.target"]]], | ||
] | ||
return generate_systemd(container_service_data) | ||
|
||
|
||
def install(app_name): | ||
with open(definitions[app_name]) as f: | ||
app_defn = config_lisp(parse_sexp(f.read())) | ||
|
||
# TODO: write a validator, possibly based on a schema/grammar | ||
ports = lookup_sexp(app_defn, "ports") | ||
|
||
pod_fname = os.path.join(systemd_dir, f"{app_name}.pod") | ||
pod_service = gen_pod(app_name, ports) | ||
with open(pod_fname, "w") as f: | ||
f.write(pod_service) | ||
print(f"==> installed {pod_fname} ✅") | ||
|
||
containers = lookup_sexp(app_defn, "containers") | ||
for container in containers: | ||
container_name = lookup_sexp(container, "name")[0] | ||
container_fname = os.path.join( | ||
systemd_dir, f"{app_name}-{container_name}.container" | ||
) | ||
container_service = gen_container(app_name, container) | ||
with open(container_fname, "w") as f: | ||
f.write(container_service) | ||
print(f"==> installed {container_fname} ✅") | ||
|
||
reload_units() | ||
restart_units(app_name) | ||
show_ports(app_name) | ||
status_units(app_name) | ||
|
||
|
||
def uninstall(app_name): | ||
stop_units(app_name) | ||
app_systemd_files = glob.glob(os.path.join(systemd_dir, f"{app_name}*")) | ||
for fname in app_systemd_files: | ||
os.remove(fname) | ||
print(f"==> removed {fname} ✅") | ||
|
||
reload_units() | ||
|
||
|
||
def show_ports(app_name): | ||
App = tinydb.Query() | ||
app_data = app_db.search(App.name == app_name) | ||
port_mapping = app_data[0]["ports"] | ||
for p_container, p_host in port_mapping.items(): | ||
print(f"==> host port 🌐 {p_host} was mapped to app port 📦 {p_container}") | ||
|
||
|
||
def printenv(app_name): | ||
app_data_dir = os.path.join(app_dir, app_name) | ||
env_files = glob.glob(os.path.join(app_data_dir, "*.env")) | ||
for fname in env_files: | ||
print(f"# {fname}:") | ||
with open(fname) as f: | ||
print(f.read()) | ||
|
||
|
||
def main(): | ||
examples_text = """ | ||
Examples: | ||
--------- | ||
Install an app called thelounge | ||
johnny install thelounge | ||
Check status of the app once installed | ||
johnny status thelounge | ||
Find open ports | ||
johnny ports thelounge | ||
Print environment used | ||
johnny printenv thelounge | ||
Stop the app | ||
johnny stop thelounge | ||
Restart it | ||
johnny restart thelounge | ||
""" | ||
|
||
parser = argparse.ArgumentParser( | ||
description="JOHNAIC package manager", | ||
prog="johnny", | ||
epilog=examples_text, | ||
formatter_class=argparse.RawTextHelpFormatter, | ||
) | ||
parser.add_argument( | ||
"action", | ||
type=str, | ||
help="One of install|uninstall|start|stop|restart|ports|printenv|status", | ||
) | ||
parser.add_argument("app_name", type=str, help="Name of the app") | ||
args = parser.parse_args() | ||
|
||
action = args.action | ||
app_name = args.app_name | ||
|
||
if action == "install": | ||
install(app_name) | ||
elif action == "uninstall": | ||
uninstall(app_name) | ||
elif action == "start": | ||
start_units(app_name) | ||
elif action == "stop": | ||
stop_units(app_name) | ||
elif action == "restart": | ||
restart_units(app_name) | ||
elif action == "status": | ||
status_units(app_name) | ||
elif action == "ports": | ||
show_ports(app_name) | ||
elif action == "printenv": | ||
printenv(app_name) | ||
elif action == "logs": | ||
log_units(app_name) | ||
else: | ||
raise RuntimeError(f"Unknown action {action}") | ||
|
||
|
||
if __name__ == "__main__": | ||
main() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
import secrets | ||
import bcrypt | ||
|
||
|
||
def _is_list(sexp): | ||
return isinstance(sexp, list) | ||
|
||
|
||
def interactive_input(prompt, docs=""): | ||
print("==> Entering interactive input") | ||
print(docs) | ||
inp = input(f"==> {prompt}: ").strip() | ||
print(f"==> recieved {inp} for {prompt}") | ||
return inp | ||
|
||
|
||
def hash_password(password): | ||
# https://www.geeksforgeeks.org/hashing-passwords-in-python-with-bcrypt/ | ||
bytes = password.encode() | ||
salt = bcrypt.gensalt() | ||
hash = bcrypt.hashpw(bytes, salt) | ||
return hash.decode() | ||
|
||
|
||
standard_lib = { | ||
"gen-password": lambda: secrets.token_urlsafe(16), | ||
"hash-password": hash_password, | ||
"interactive-input": interactive_input, | ||
} | ||
|
||
|
||
def config_lisp(sexp, env=standard_lib): | ||
if _is_list(sexp): | ||
if len(sexp) > 0 and sexp[0] == "let": | ||
assert len(sexp) == 3 | ||
varialbes = sexp[1] | ||
body = sexp[2] | ||
for var_name, var_value in varialbes: | ||
assert isinstance(var_name, str) | ||
env[var_name] = config_lisp(var_value, env) | ||
|
||
return config_lisp(body, env) | ||
elif len(sexp) > 0 and sexp[0] == "unquote": | ||
quoted_sexp = sexp[1] | ||
if _is_list(quoted_sexp): | ||
# function | ||
fn_name = quoted_sexp[0] | ||
args = config_lisp(quoted_sexp[1:], env) | ||
return env[fn_name](*args) | ||
else: | ||
# variable | ||
return env[quoted_sexp] | ||
else: | ||
return [config_lisp(x, env) for x in sexp] | ||
else: | ||
# atom | ||
return sexp | ||
|
||
|
||
if __name__ == "__main__": | ||
import sys | ||
from parser import parse_sexp | ||
|
||
print(config_lisp(parse_sexp(open(sys.argv[-1]).read()))) |
Oops, something went wrong.