Skip to content

Commit

Permalink
Merge pull request #100 from chsasank/app-store
Browse files Browse the repository at this point in the history
Add app store
  • Loading branch information
chsasank authored Oct 19, 2024
2 parents ed2c16d + 2ed6d51 commit 96a422e
Show file tree
Hide file tree
Showing 48 changed files with 1,324 additions and 0 deletions.
298 changes: 298 additions & 0 deletions src/app-store/app_store/install.py
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()
64 changes: 64 additions & 0 deletions src/app-store/app_store/interpreter.py
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())))
Loading

0 comments on commit 96a422e

Please sign in to comment.