Skip to content

Commit

Permalink
add ci-tooling and config files
Browse files Browse the repository at this point in the history
  • Loading branch information
stavros-k committed Jun 27, 2024
1 parent 01638ca commit ae57e7e
Show file tree
Hide file tree
Showing 9 changed files with 441 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .flake8
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[flake8]
max-line-length = 88
68 changes: 68 additions & 0 deletions .github/scripts/changed_apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
#!/usr/bin/env python3

import pathlib
import json
import sys
import os
import re

APP_REGEX = re.compile(r"^ix-dev\/([-\w\.]+)\/([-\w\.]+)")
TEST_VALUES_DIR = "templates/test_values"


def get_changed_files():
json_files = os.getenv("CHANGED_FILES", "")
if not json_files:
print("Environment variable CHANGED_FILES is empty", file=sys.stderr)
exit(1)

try:
return json.loads(json_files.replace("\\", ""))
except json.JSONDecodeError:
print("Failed to decode JSON from CHANGED_FILES", file=sys.stderr)
exit(1)


def find_test_files(changed_files):
seen = set()
matrix = []
for file in changed_files:
match = APP_REGEX.match(file)
if not match:
continue

full_name = f"{match.group(1)}/{match.group(2)}"
print(f"Detected changed item for {full_name}", file=sys.stderr)

for file in pathlib.Path("ix-dev", full_name, TEST_VALUES_DIR).glob("*.yaml"):
item_tuple = (match.group(1), match.group(2), file.name)
if item_tuple not in seen:
seen.add(item_tuple)
matrix.append(
{
"train": match.group(1),
"app": match.group(2),
"test_file": file.name,
}
)

return matrix


def main():
changed_files = get_changed_files()
matrix = find_test_files(changed_files)
# This should look like:
# {
# "include": [
# { "train": "enterprise", "app": "minio", "test_file": "basic-values.yaml" },
# { "train": "enterprise", "app": "minio", "test_file": "https-values.yaml" },
# ...
# ]
# }

print(json.dumps({"include": matrix}))


if __name__ == "__main__":
main()
245 changes: 245 additions & 0 deletions .github/scripts/ci.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
#!/usr/bin/env python3
import subprocess
import argparse
import secrets
import shutil
import json
import re
import os

CONTAINER_IMAGE = "sonicaj/a_v:latest"
# CONTAINER_IMAGE = "ghcr.io/truenas/apps_validation:latest"


def parse_args():
parser = argparse.ArgumentParser()
parser.add_argument("--app", required=True, help="App name")
parser.add_argument("--train", required=True, help="Train name")
parser.add_argument("--test_file", required=True, help="Test file")
parsed = parser.parse_args()

return {
"app": parsed.app,
"train": parsed.train,
"test_file": parsed.test_file,
"project": secrets.token_hex(16),
}


def print_state():
print("Parameters:")
print(f" - app: [{args['app']}]")
print(f" - train: [{args['train']}]")
print(f" - test_file: [{args['test_file']}]")
print(f" - project: [{args['project']}]")


def command_exists(command):
return shutil.which(command) is not None


def check_required_commands():
required_commands = ["docker", "jq", "openssl"]
for command in required_commands:
if not command_exists(command):
print(f"Error: command [{command}] is not installed")
exit(1)


def get_base_cmd():
rendered_compose = "templates/rendered/docker-compose.yaml"
return " ".join(
[
f"docker compose -p {args['project']} -f",
f"ix-dev/{args['train']}/{args['app']}/{rendered_compose}",
]
)


def pull_app_catalog_container():
print(f"Pulling container image [{CONTAINER_IMAGE}]")
res = subprocess.run(f"docker pull --quiet {CONTAINER_IMAGE}", shell=True)
if res.returncode != 0:
print(f"Failed to pull container image [{CONTAINER_IMAGE}]")
exit(1)
print(f"Done pulling container image [{CONTAINER_IMAGE}]")


def render_compose():
print("Rendering docker-compose file")
test_values_dir = "templates/test_values"
app_dir = f"ix-dev/{args['train']}/{args['app']}"
cmd = " ".join(
[
f"docker run --quiet --rm -v {os.getcwd()}:/workspace {CONTAINER_IMAGE}",
"python3 /app/catalog_templating/scripts/render_compose.py render",
f"--path /workspace/{app_dir}",
f"--values /workspace/{app_dir}/{test_values_dir}/{args['test_file']}",
]
)
print_cmd(cmd)
separator_start()
res = subprocess.run(cmd, shell=True)
separator_start()
if res.returncode != 0:
print("Failed to render docker-compose file")
exit(1)
print("Done rendering docker-compose file")


def print_docker_compose_config():
print("Printing docker compose config (parsed compose)")
cmd = f"{get_base_cmd()} config"
print_cmd(cmd)
separator_start()
res = subprocess.run(cmd, shell=True)
separator_start()
if res.returncode != 0:
print("Failed to print docker compose config")
exit(1)


def separator_start():
print("=" * 40 + "+++++" + "=" * 40)


def separator_end():
print("=" * 40 + "-----" + "=" * 40)


def print_cmd(cmd):
print(f"Running command [{cmd}]")


def docker_cleanup():
cmd = f"{get_base_cmd()} down --remove-orphans --volumes"
print_cmd(cmd)
separator_start()
subprocess.run(cmd, shell=True)
separator_end()

cmd = f"{get_base_cmd()} rm --force --stop --volumes"
print_cmd(cmd)
separator_start()
subprocess.run(cmd, shell=True)
separator_end()


def print_logs():
cmd = f"{get_base_cmd()} logs"
print_cmd(cmd)
separator_start()
subprocess.run(cmd, shell=True)
separator_end()


def print_docker_processes():
cmd = f"{get_base_cmd()} ps --all"
print_cmd(cmd)
separator_start()
subprocess.run(cmd, shell=True)
separator_end()


def get_failed_containers():
cmd = f"{get_base_cmd()} ps --status exited --all --format json"
print_cmd(cmd)
failed = subprocess.run(cmd, shell=True, capture_output=True)
failed = failed.stdout.decode("utf-8")
# if failed starts with { put it inside []
if failed.startswith("{"):
failed = f"[{failed}]"

return json.loads(failed)


def print_inspect_data(container):
cmd = f"docker container inspect {container['ID']}"
print_cmd(cmd)
res = subprocess.run(cmd, shell=True, capture_output=True)
data = json.loads(res.stdout.decode("utf-8"))
separator_start()
print(json.dumps(data, indent=4))
separator_end()


def run_app():
cmd = f"{get_base_cmd()} up --detach --quiet-pull --wait --wait-timeout 600"
print_cmd(cmd)
res = subprocess.run(cmd, shell=True)

print_logs()
print_docker_processes()

if res.returncode != 0:
print("Failed to start container(s)")
for container in get_failed_containers():
print(f"Container [{container['ID']}] exited. Printing Inspect Data")
print_inspect_data(container)
return res.returncode

print("Containers started successfully")


def check_app_dir_exists():
if not os.path.exists(f"ix-dev/{args['train']}/{args['app']}"):
print(f"App directory [ix-dev/{args['train']}/{args['app']}] does not exist")
exit(1)


def get_latest_lib_version():
libs = [
lib
for lib in os.listdir("library")
if os.path.isdir(os.path.join("library", lib))
]

def version_key(version):
return [int(part) for part in re.split(r"\.", version)]

sorted_libs = sorted(libs, key=version_key)
return sorted_libs[-1] if sorted_libs else None


def copy_lib():
# get latest lib version
lib_version = get_latest_lib_version()
if not lib_version:
print("Failed to get latest lib version")
exit(1)
print(f"Copying lib version [{lib_version}]")
lib = f"base_v{lib_version.replace('.', '_')}"
if os.path.exists(f"ix-dev/{args['train']}/{args['app']}/templates/library/{lib}"):
shutil.rmtree(f"ix-dev/{args['train']}/{args['app']}/templates/library/{lib}")
os.makedirs(
f"ix-dev/{args['train']}/{args['app']}/templates/library/{lib}", exist_ok=True
)
try:
shutil.copytree(
f"library/{lib_version}",
f"ix-dev/{args['train']}/{args['app']}/templates/library/{lib}",
dirs_exist_ok=True,
)
except shutil.Error:
print(f"Failed to copy lib [{lib_version}]")
exit(1)


def main():
print_state()
check_app_dir_exists()
copy_lib()
check_required_commands()
pull_app_catalog_container()
render_compose()
print_docker_compose_config()
res = run_app()
docker_cleanup()

exit(res)


args = parse_args()

if __name__ == "__main__":
main()
61 changes: 61 additions & 0 deletions .github/workflows/app-test-suite.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
name: Apps Test Suite

on:
pull_request: {}

jobs:
changed-files:
name: Generate matrix
runs-on: ubuntu-latest
outputs:
changed-apps: ${{ steps.changed-apps.outputs.changed-apps }}
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Get changed files
id: changed-files-json
uses: tj-actions/changed-files@v44
with:
json: true

- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: "3.11"

- name: Matrix Output
id: changed-apps
env:
CHANGED_FILES: ${{ steps.changed-files-json.outputs.all_changed_files }}
run: |
out=$(python3 .github/scripts/changed_apps.py)
echo "changed-apps=${out}" >> $GITHUB_OUTPUT
run-apps:
name: Run Docker Compose Render/Install
needs: changed-files
runs-on: ubuntu-latest
strategy:
matrix: ${{ fromJson(needs.changed-files.outputs.changed-apps) }}
fail-fast: false
max-parallel: 10
steps:
- name: Environment Information
run: |
echo "====== Docker Info ======"
docker info
echo "========================="
- name: Checkout
uses: actions/checkout@v4

- name: Test
shell: bash
run: |
echo "Testing [${{matrix.train}}/${{matrix.app}}/templates/test_values/${{matrix.test_file}}]"
# FIXME: remove
sudo mkdir -p /mnt/test/{data1,data2,data3,data4,postgres}
sudo chown -R 568:568 /mnt/test/{data1,data2,data3,data4}
sudo chown -R 999:999 /mnt/test/postgres
python3 ./.github/scripts/ci.py --train ${{matrix.train}} --app ${{matrix.app}} --test_file ${{matrix.test_file}}
Loading

0 comments on commit ae57e7e

Please sign in to comment.