Skip to content

Commit

Permalink
feat: pipeline to create releases with patches
Browse files Browse the repository at this point in the history
  • Loading branch information
martabal committed Nov 7, 2024
1 parent 584f707 commit fe7a194
Show file tree
Hide file tree
Showing 5 changed files with 163 additions and 14 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ jobs:
run: poetry install --without dev,test

- name: Create Dockerfile from template
run: poetry run python3 render_templates/main.py --flavor ${{ matrix.image }} --print-dockerfile
run: poetry run python3 render_templates/main.py --flavor ${{ matrix.image }} --print-dockerfile --enable-patches --immich-version ${{ steps.tags.outputs.immich_version }}

- name: Generate docker image tags
id: metadata
Expand Down
4 changes: 4 additions & 0 deletions patches/backup_missing_postgres_port.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"version": "v1.120.1",
"apply": "server"
}
15 changes: 15 additions & 0 deletions patches/backup_missing_postgres_port.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
diff --git a/server/src/services/backup.service.ts b/server/src/services/backup.service.ts
index 1febe7913..3e03687d5 100644
--- a/server/src/services/backup.service.ts
+++ b/server/src/services/backup.service.ts
@@ -85,7 +85,9 @@ export class BackupService extends BaseService {
} = this.configRepository.getEnv();

const isUrlConnection = config.connectionType === 'url';
- const databaseParams = isUrlConnection ? ['-d', config.url] : ['-U', config.username, '-h', config.host];
+ const databaseParams = isUrlConnection
+ ? ['-d', config.url]
+ : ['-U', config.username, '-h', config.host, '-p', config.port.toString()];
const backupFilePath = path.join(
StorageCore.getBaseFolder(StorageFolder.BACKUPS),
`immich-db-backup-${Date.now()}.sql.gz.tmp`,
138 changes: 125 additions & 13 deletions render_templates/main.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
#!/usr/bin/env python3

import argparse
from enum import Enum
import sys
from jinja2 import Environment, FileSystemLoader
import json
import os
from shutil import copytree, rmtree
import shutil
from typing import List, TypedDict, Optional


Expand All @@ -13,6 +15,27 @@ class Flavor(TypedDict):
machine_learning_provider: Optional[str]


class Patches(TypedDict):
cli: List[str]
ml: List[str]
server: List[str]
web: List[str]


class ApplyPatches(TypedDict):
cli: bool
ml: bool
server: bool
web: bool


class PatchType(Enum):
WEB = "web"
SERVER = "server"
ML = "ml"
CLI = "cli"


flavors: List[Flavor] = [
{
"name": "armnn",
Expand All @@ -37,6 +60,8 @@ class Flavor(TypedDict):
]
dockerfile_template_name = "Dockerfile.j2"
output_dir = os.getcwd()
patches_dir = os.path.join(output_dir, "patches")
patches_build_dir = "patches"
templates_dir = os.path.join(output_dir, "templates")
build_folder_prefix = "build-"
s6_path = "etc/s6-overlay/s6-rc.d"
Expand All @@ -53,10 +78,37 @@ class Flavor(TypedDict):
]


def get_matching_patches(folder_path, current_version) -> Patches:
patch_paths: Patches = {"cli": [], "ml": [], "server": [], "web": []}
current_version = current_version.lstrip("v")
for filename in os.listdir(folder_path):
if filename.endswith(".json"):
json_file_path = os.path.join(folder_path, filename)
patch_file_path = json_file_path.replace(".json", ".patch")

if os.path.exists(patch_file_path):
with open(json_file_path, "r") as json_file:
data = json.load(json_file)
version_range = data.get("version").lstrip("v")
if version_range == version_range:
apply_to_project: Optional[PatchType] = data.get("apply")
if apply_to_project in patch_paths:
patch_paths[apply_to_project].append(patch_file_path)

all_patch_files = [
os.path.basename(file)
for patch_list in patch_paths.values()
for file in patch_list
]
print(f"Patches applied: {all_patch_files}\n")
return patch_paths


def generate_all(
build_path: Optional[str] = None,
print_content: Optional[bool] = None,
no_generate: Optional[bool] = None,
patch_path: Optional[Patches] = None,
) -> None:
if no_generate is False:
print("generating Dockerfiles and context for all flavors: ")
Expand All @@ -69,10 +121,7 @@ def generate_all(
build_folder = os.path.join(output_dir, build_folder_name)

if no_generate is False:
generate_template(
build_folder,
flavor,
)
generate_template(build_folder, flavor, patch_path)
if print_content is True:
dockerfile_path = os.path.join(build_folder, "Dockerfile")
print_dockerfile_content(dockerfile_path)
Expand All @@ -81,31 +130,74 @@ def generate_all(
def generate_template(
build_folder: str,
flavor: Flavor,
patch_path: Optional[Patches] = None,
) -> None:
flavor_name = flavor["name"]
machine_learning_provider = flavor["machine_learning_provider"]
root_folder_path = os.path.join(build_folder, "root")
dockerfile_path = os.path.join(build_folder, "Dockerfile")

project_patches: ApplyPatches = {
PatchType.CLI.value: len(patch_path[PatchType.CLI.value]) > 0
if patch_path
else False,
PatchType.ML.value: (
len(patch_path[PatchType.ML.value]) > 0
if machine_learning_provider
else False
)
if patch_path
else False,
PatchType.SERVER.value: len(patch_path[PatchType.SERVER.value]) > 0
if patch_path
else False,
PatchType.WEB.value: len(patch_path[PatchType.WEB.value]) > 0
if patch_path
else False,
}

patches_path = os.path.join(build_folder, patches_build_dir)
patches = (
project_patches["web"]
or project_patches["server"]
or project_patches["cli"]
or project_patches["ml"]
)

if not os.path.exists(build_folder):
os.makedirs(build_folder)

if patches and os.path.exists(patches_path):
shutil.rmtree(patches_path)

if os.path.exists(root_folder_path):
rmtree(root_folder_path)
shutil.rmtree(root_folder_path)

if os.path.exists(dockerfile_path):
os.remove(dockerfile_path)

variables = {
"machine_learning_provider": machine_learning_provider,
"patches": patches,
}

dockerfile_rendered_template = dockerfile_template.render(variables)

with open(dockerfile_path, "w") as dockerfile:
dockerfile.write(dockerfile_rendered_template)

copytree(os.path.join(output_dir, "root"), root_folder_path)
shutil.copytree(os.path.join(output_dir, "root"), root_folder_path)

if patches:
projects = [patch_type.value for patch_type in PatchType]
for project in projects:
if len(patch_path[project]) > 0:
pathes_build_path = os.path.join(build_folder, patches_build_dir)
project_build_dir = os.path.join(pathes_build_path, project)
os.makedirs(project_build_dir, exist_ok=True)
for path in patch_path[project]:
dest_path = os.path.join(project_build_dir, os.path.basename(path))
shutil.copyfile(path, dest_path)

for subfolder in subfolders_templates_run:
folder_name = os.path.basename(subfolder)
Expand All @@ -128,7 +220,7 @@ def generate_template(
os.chmod(run_path, st.st_mode | 0o111)

if machine_learning_provider is None and "machine-learning" in folder_name:
rmtree(os.path.join(root_folder_path, machine_learning_svc_path))
shutil.rmtree(os.path.join(root_folder_path, machine_learning_svc_path))
os.remove(
os.path.join(
root_folder_path,
Expand Down Expand Up @@ -187,10 +279,33 @@ def init(argv: Optional[List[str]] = None):
required=False,
)

parser.add_argument(
"--enable-patches",
help="Use patches (require --immich-version)",
action="store_true",
required=False,
)

parser.add_argument(
"--immich-version",
help="Immich version",
type=str,
required=False,
)

args = parser.parse_args(argv)
if args.print_dockerfile is False and args.no_generate is True:
parser.error("You can't have --no-generate without --print-dockerfile")

if args.enable_patches is False and args.immich_version is None:
print("Immich version not passed")

patches = (
get_matching_patches(patches_dir, args.immich_version)
if args.enable_patches is True and args.immich_version is not None
else None
)

if args.flavor is not None:
flavor = get_flavor_by_name(args.flavor)
flavor_name = flavor["name"]
Expand All @@ -206,16 +321,13 @@ def init(argv: Optional[List[str]] = None):
print(f"generating Dockerfiles and context for {flavor_name}: ")

if args.no_generate is False:
generate_template(
build_folder,
flavor,
)
generate_template(build_folder, flavor, patches)
if args.print_dockerfile is True:
dockerfile_path = os.path.join(build_folder, "Dockerfile")
print_dockerfile_content(dockerfile_path)

else:
generate_all(args.build_path, args.print_dockerfile, args.no_generate)
generate_all(args.build_path, args.print_dockerfile, args.no_generate, patches)


if __name__ == "__main__":
Expand Down
18 changes: 18 additions & 0 deletions templates/Dockerfile.j2
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ ARG NODEJS_VERSION
LABEL build_version="Build-date:- ${BUILD_DATE}"
LABEL maintainer="martabal"

{% if patches -%}
# patches
COPY patches/ /tmp/patches
{% endif -%}

# environment settings
ENV \
IMMICH_ENV="production" \
Expand Down Expand Up @@ -101,6 +106,9 @@ RUN \
echo "**** install build packages ****" && \
apt-get update && \
apt-get install --no-install-recommends -y \
{% if patches -%}
patch \
{% endif -%}
build-essential && \
echo "**** install runtime packages ****" && \
apt-get install --no-install-recommends -y \
Expand Down Expand Up @@ -136,6 +144,13 @@ RUN \
cp -r \
scripts \
/app/immich/server && \
{% if patches -%}
echo "**** apply patches ****" && \
cd /tmp/immich && \
for patch in /tmp/patches/*/*.patch; do \
patch -p1 < "$patch"; \
done && \
{% endif -%}
echo "**** build server ****" && \
cd /tmp/immich/server && \
npm ci --no-fund --no-update-notifier && \
Expand Down Expand Up @@ -226,6 +241,9 @@ RUN \
curl -sSL https://install.python-poetry.org | python3 - --uninstall && \
{% endif -%}
apt-get remove -y --purge \
{% if patches -%}
patch \
{% endif -%}
build-essential \
{% if machine_learning_provider -%}
{% if machine_learning_provider == "openvino" -%}
Expand Down

0 comments on commit fe7a194

Please sign in to comment.