#!/usr/bin/python3
# Copyright (C) 2023 Sofus Albert Høgsbro Rose
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
import os
import sys
if not all([
sys.version_info.major == 3,
sys.version_info.minor in [9, 10, 11, 12, 13],
]):
sys.exit(1)
from pathlib import Path
import platform
import shutil
import subprocess
import contextlib
####################
# - Constants
####################
SCRIPT_PATH = Path(__file__).resolve().parent
CMD_DEPENDENCIES = [
'podman',
'git',
'pre-commit'
]
IMAGE_NAME = "site-support"
IMAGE_VERSION = ("0", "1", "0")
REGISTRY_HOST = "git.sofus.io"
REGISTRY_USER = "so-rose"
REGISTRY_PASSWORD = lambda: subprocess.check_output(
['pass', 'services/home/git.sofus.io/container-registry-token']
).decode('ascii').strip()
####################
# - Help Text
####################
def action_help() -> None:
print(f"""This script provides one-click development, CI, and deployment,
to support the use of the {IMAGE_NAME} project.
Usage:
echo -e "./run.py [OPTION] [EXTRAS]"
Program Information:
Version
=> {".".join(IMAGE_VERSION)}
OCI Container
=> {REGISTRY_HOST}/{REGISTRY_USER}/{IMAGE_NAME}:{IMAGE_VERSION[0]
The following commands must be available:
podman
=> This project is developed and run in podman containers.
=> https://podman.io/
git
=> This project uses git for versioning, and collaboration.
=> https://git-scm.com/
pre-commit
=> Enforces that certain checks pass at each commit.
=> https://pre-commit.com/
Options:
./run.py
=> Equivilant to run.py help.
./run.py -h|h|help
=> Shows this text.
Options, Run Locally:
./run.py app
=> Will run the app live on port 8787.
Options, Check:
./run.py check
=> Will run all checks, including static analysis and tests.
./run.py analyze-quality [OPTIONS]
=> Not yet implemented...
=> Potentially, could run ruff on Python snippets in the book.
./run.py analyze-types [OPTIONS]
=> Not yet implemented...
=> Potentially, could run mypy on Python snippets in the book.
./run.py analyze-security
=> Will run the static security analysis suite.
./run.py analyze-format [--overwrite] [OPTIONS]
=> Not yet implemented...
=> Potentially, could format Python snippets in the book.
./run.py test
=> Not yet impelmented...
=> Potentially, would test distro commands in an appropriate container.
=> Potentially, would turn Python snippets into pytest units.
- See https://github.com/modal-labs/pytest-markdown-docs
=> Will run all Markdown Python snippets as tests.
Options, Build & Deploy:
./run.py build
=> Will build a docker image, and tag it :dev.
Options, Housekeeping:
./run.py clean
=> Not yet implemented...
=> Will delete all data caused by this project's presence on your system.
""")
####################
# - Utilities
####################
@contextlib.contextmanager
def cd_script_dir() -> None:
cwd_orig = Path.cwd()
os.chdir(SCRIPT_PATH)
try:
yield
finally:
os.chdir(cwd_orig)
def get_git_revision_hash(short = True) -> str:
commit_id = subprocess.check_output(
['git', 'rev-parse', 'HEAD']
).decode('ascii').strip()
return commit_id[:16] if short else commit_id
def cmd_exists(cmd: str) -> bool:
"""Checks if a command exists. Supports Linux, Mac, Windows.
"""
return shutil.which(cmd) is not None
####################
# - Actions - Build
####################
def action_build(release = False) -> None:
tag = get_git_revision_hash() if release else "dev"
subprocess.run([
"podman", "build",
".",
# : - Tag Commit ID
"--tag",
f"{IMAGE_NAME}:{tag}",
])
def action_publish() -> None:
action_build(release = True)
# Login to Registry
subprocess.run([
"podman", "login", REGISTRY_HOST,
"--username", REGISTRY_USER,
"--password", REGISTRY_PASSWORD(),
])
# Tag & Publish Image @ :
subprocess.run([
"podman", "image", "push",
f"{IMAGE_NAME}:{get_git_revision_hash()}",
f"{REGISTRY_HOST}/{REGISTRY_USER}/{IMAGE_NAME}:{get_git_revision_hash()}",
])
# Publish Image @ :M, :M.m, :M.m.p
for image_tag in [
f"{IMAGE_NAME}:{'.'.join(IMAGE_VERSION)}",
f"{IMAGE_NAME}:{'.'.join(IMAGE_VERSION[:2])}",
f"{IMAGE_NAME}:{IMAGE_VERSION[0]}",
]:
# Tag Image
subprocess.run([
"podman", "tag",
f"{IMAGE_NAME}:{get_git_revision_hash()}",
image_tag
])
# Publish Image
subprocess.run([
"podman", "image", "push",
image_tag,
f"{REGISTRY_HOST}/{REGISTRY_USER}/{image_tag}",
])
####################
# - Actions - Run
####################
def action_app() -> None:
action_build()
subprocess.run([
"podman", "run", "--rm", "-it",
"--publish", "8787:8787",
f"{IMAGE_NAME}:{get_git_revision_hash()}",
])
####################
# - Actions - Analyze
####################
def action_analyze_security() -> None:
Path('.cache-trivy').mkdir(parents=True, exist_ok=True)
subprocess.run([
"podman", "run", "--rm", "-it",
"--workdir", "/src",
"--volume", ".:/src",
"--volume", "./.cache-trivy:/root/.cache",
"ghcr.io/aquasecurity/trivy:latest",
"fs", "--quiet", "--scanners", "vuln,secret,config,license",
"--exit-code", "1", "./"
])
####################
# - Script
####################
if __name__ == "__main__":
# Check Available Commands
for cmd in CMD_DEPENDENCIES:
if not cmd_exists(cmd) :
print(f"{cmd} is not installed. Please see --help for instructions.")
sys.exit(1)
with cd_script_dir():
if len(sys.argv) > 1:
{
"build": action_build,
"publish": action_publish,
"app": action_app,
"dev": lambda: print("TBD"),
"analyze-security": action_analyze_security,
"help": action_help,
"-h": action_help,
"--help": action_help,
}[sys.argv[1]]()
else:
action_help()