diff --git a/.github/actions/combine-build/action.yml b/.github/actions/combine-build/action.yml index c20e6521f5..3c17fff60b 100644 --- a/.github/actions/combine-build/action.yml +++ b/.github/actions/combine-build/action.yml @@ -54,9 +54,13 @@ runs: username: ${{ inputs.aws_access_key_id }} password: ${{ inputs.aws_secret_access_key }} + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Build The Combine run: > deploy/scripts/build.py + --arch amd64 arm64 --components ${{ inputs.build_component }} --tag ${{ env.IMAGE_TAG }} --repo ${{ inputs.image_registry }}${{ inputs.image_registry_alias}} diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index 259096015f..9e5892e96b 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -139,6 +139,9 @@ jobs: docker_build: if: ${{ github.event.type }} == "PullRequest" runs-on: ubuntu-22.04 + strategy: + matrix: + arch: ["amd64", "arm64"] steps: # See https://docs.stepsecurity.io/harden-runner/getting-started/ for instructions on # configuring harden-runner and identifying allowed endpoints. @@ -152,10 +155,14 @@ jobs: *.data.mcr.microsoft.com:443 api.nuget.org:443 archive.ubuntu.com:80 + auth.docker.io:443 dc.services.visualstudio.com:443 deb.debian.org:80 github.com:443 mcr.microsoft.com:443 + ports.ubuntu.com:80 + production.cloudflare.docker.com:443 + registry-1.docker.io:443 security.ubuntu.com:80 # For subfolders, currently a full checkout is required. # See: https://github.com/marketplace/actions/build-and-push-docker-images#path-context @@ -163,11 +170,9 @@ jobs: uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: fetch-depth: 0 + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 - name: Build backend run: | - deploy/scripts/build.py --components backend - shell: bash - - name: Image digest - run: | - docker image inspect combine_backend:latest -f '{{json .Id}}' + deploy/scripts/build.py --components backend --arch ${{ matrix.arch }} shell: bash diff --git a/.github/workflows/database.yml b/.github/workflows/database.yml index 834f6c97f8..58c6c8b9c3 100644 --- a/.github/workflows/database.yml +++ b/.github/workflows/database.yml @@ -11,6 +11,9 @@ jobs: docker_build: if: ${{ github.event.type }} == "PullRequest" runs-on: ubuntu-22.04 + strategy: + matrix: + arch: ["amd64", "arm64"] steps: # See https://docs.stepsecurity.io/harden-runner/getting-started/ for instructions on # configuring harden-runner and identifying allowed endpoints. @@ -30,11 +33,9 @@ jobs: uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: fetch-depth: 0 + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 - name: Build database image run: | - deploy/scripts/build.py --components database - shell: bash - - name: Image digest - run: | - docker image inspect combine_database:latest -f '{{json .Id}}' + deploy/scripts/build.py --components database --arch ${{ matrix.arch }} shell: bash diff --git a/.github/workflows/deploy_qa.yml b/.github/workflows/deploy_qa.yml index 08ea29aa69..fc2ae1237a 100644 --- a/.github/workflows/deploy_qa.yml +++ b/.github/workflows/deploy_qa.yml @@ -2,7 +2,7 @@ name: "Deploy Update to QA Server" on: push: - branches: [master] + branches: [arm, master] permissions: contents: read @@ -40,6 +40,7 @@ jobs: files.pythonhosted.org:443 github.com:443 mcr.microsoft.com:443 + ports.ubuntu.com:80 production.cloudflare.docker.com:443 public.ecr.aws:443 pypi.org:443 diff --git a/.github/workflows/deploy_release.yml b/.github/workflows/deploy_release.yml index b89b38f46b..c82d5ab5fd 100644 --- a/.github/workflows/deploy_release.yml +++ b/.github/workflows/deploy_release.yml @@ -39,6 +39,7 @@ jobs: github.com:443 mcr.microsoft.com:443 production.cloudflare.docker.com:443 + ports.ubuntu.com:80 public.ecr.aws:443 pypi.org:443 registry-1.docker.io:443 diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml index 7380c935a9..36f955fa3d 100644 --- a/.github/workflows/frontend.yml +++ b/.github/workflows/frontend.yml @@ -111,6 +111,9 @@ jobs: docker_build: if: ${{ github.event.type }} == "PullRequest" runs-on: ubuntu-22.04 + strategy: + matrix: + arch: ["amd64", "arm64"] steps: # See https://docs.stepsecurity.io/harden-runner/getting-started/ for instructions on # configuring harden-runner and identifying allowed endpoints. @@ -123,6 +126,7 @@ jobs: auth.docker.io:443 files.pythonhosted.org:443 github.com:443 + ports.ubuntu.com:80 production.cloudflare.docker.com:443 pypi.org:443 registry-1.docker.io:443 @@ -131,11 +135,9 @@ jobs: uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: fetch-depth: 0 + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 - name: Build frontend run: | - deploy/scripts/build.py --components frontend - shell: bash - - name: Image digest - run: | - docker image inspect combine_frontend:latest -f '{{json .Id}}' + deploy/scripts/build.py --components frontend --arch ${{ matrix.arch }} shell: bash diff --git a/.github/workflows/maintenance.yml b/.github/workflows/maintenance.yml index 897d5d4ca1..4a38141ef9 100644 --- a/.github/workflows/maintenance.yml +++ b/.github/workflows/maintenance.yml @@ -11,6 +11,9 @@ jobs: docker_build: if: ${{ github.event.type }} == "PullRequest" runs-on: ubuntu-22.04 + strategy: + matrix: + arch: ["amd64", "arm64"] steps: # See https://docs.stepsecurity.io/harden-runner/getting-started/ for instructions on # configuring harden-runner and identifying allowed endpoints. @@ -25,6 +28,7 @@ jobs: auth.docker.io:443 files.pythonhosted.org:443 github.com:443 + ports.ubuntu.com:80 production.cloudflare.docker.com:443 public.ecr.aws:443 pypi.org:443 @@ -36,11 +40,9 @@ jobs: uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: fetch-depth: 0 + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 - name: Build maintenance image run: | - deploy/scripts/build.py --components maintenance - shell: bash - - name: Image digest - run: | - docker image inspect combine_maint:latest -f '{{json .Id}}' + deploy/scripts/build.py --components maintenance --arch ${{ matrix.arch }} shell: bash diff --git a/Backend/Dockerfile b/Backend/Dockerfile index 6833352fcb..8e3b53b29d 100644 --- a/Backend/Dockerfile +++ b/Backend/Dockerfile @@ -7,7 +7,7 @@ ############################################################ # Docker multi-stage build -FROM mcr.microsoft.com/dotnet/sdk:8.0.404-jammy AS builder +FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0.404-jammy AS builder WORKDIR /app # Copy csproj and restore (fetch dependencies) as distinct layers. @@ -33,8 +33,7 @@ ENV APP_FILES=${HOME}/.CombineFiles # Install system dependencies. RUN apt-get update \ - && apt-get install -y \ - ffmpeg \ + && apt-get install -y ffmpeg \ && rm -rf /var/lib/apt/lists/* # Create the home directory for the new app user. @@ -47,7 +46,7 @@ RUN usermod --uid 999 --gid app \ --comment "Docker image user" \ app -## Set up application install directory. +# Set up application install directory. RUN mkdir $APP_HOME && \ mkdir $APP_FILES && \ # Give access to the entire home folder so the backend can create files and folders there. diff --git a/deploy/Dockerfile b/deploy/Dockerfile index d68591591e..652a80f5b6 100644 --- a/deploy/Dockerfile +++ b/deploy/Dockerfile @@ -3,6 +3,7 @@ # # Supported Platforms: # - Intel/AMD 64-bit +# - ARM 64-bit ############################################################ FROM python:3.12.8-slim-bookworm @@ -16,7 +17,8 @@ RUN apt-get update && \ rm -rf /var/lib/apt/lists/* # Install kubectl and helm -RUN curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" && \ +RUN MACH=$(case $(uname -m) in *86*) echo amd64;; *aarch*) echo arm64;; *arm*) echo arm64;; esac) && \ + curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/${MACH}/kubectl" && \ install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl && \ curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3 && \ chmod 700 get_helm.sh && \ diff --git a/deploy/scripts/build.py b/deploy/scripts/build.py index b090535de0..e8630cfe67 100755 --- a/deploy/scripts/build.py +++ b/deploy/scripts/build.py @@ -206,6 +206,13 @@ def parse_args() -> Namespace: description="Build containerd container images for project.", formatter_class=RawFormatter, ) + parser.add_argument( + "--arch", + choices=["amd64", "arm64"], + default=[], + help="Target cpu architecture(s).", + nargs="*", + ) parser.add_argument( "--build-args", nargs="*", help="Build arguments to pass to the docker build." ) @@ -270,10 +277,12 @@ def main() -> None: if args.debug: container_cmd.extend(["-D", "-l", "debug"]) build_cmd = container_cmd + ["buildx", "build"] - push_cmd = container_cmd + ["push"] + build_cmd.append("--load" if args.repo is None else "--push") case _: logging.critical(f"Container CLI '{container_cmd[0]}' is not supported.") sys.exit(1) + if len(args.arch): + build_cmd.extend(["--platform", ",".join([f"linux/{arch}" for arch in args.arch])]) # Setup build options if args.quiet: @@ -306,7 +315,7 @@ def main() -> None: job_set[component] = JobQueue(component, debug=args.debug) logging.debug(f"Adding job {build_cmd + job_opts}") job_set[component].add_job(Job(build_cmd + job_opts, spec.dir)) - if args.repo is not None: + if args.repo is not None and container_cmd[0] == "nerdctl": logging.debug(f"Adding job {push_cmd + [image_name]}") job_set[component].add_job(Job(push_cmd + [image_name], None)) logging.info(f"Building component {component}") diff --git a/deploy/scripts/install-combine.sh b/deploy/scripts/install-combine.sh index 0432f2b427..46b6c5a535 100755 --- a/deploy/scripts/install-combine.sh +++ b/deploy/scripts/install-combine.sh @@ -78,11 +78,16 @@ install-kubernetes () { # Setup Kubernetes environment and WiFi Access Point cd ${DEPLOY_DIR}/ansible + # Set -e/--extra-vars for ansible-playbook + EXTRA_VARS="-e k8s_user=${whoami}" if [ -d "${DEPLOY_DIR}/airgap-images" ] ; then - ansible-playbook playbook_desktop_setup.yml -K -e k8s_user=`whoami` -e install_airgap_images=true $(((DEBUG == 1)) && echo "-vv") - else - ansible-playbook playbook_desktop_setup.yml -K -e k8s_user=`whoami` $(((DEBUG == 1)) && echo "-vv") + EXTRA_VARS="${EXTRA_VARS} -e install_airgap_images=true" + fi + if [ $ARM == 1 ] ; then + EXTRA_VARS="${EXTRA_VARS} -e cpu_arch=arm64" fi + + ansible-playbook playbook_desktop_setup.yml -K ${EXTRA_VARS} $(((DEBUG == 1)) && echo "-vv") } # Set the KUBECONFIG environment variable so that the cluster can @@ -138,7 +143,13 @@ install-the-combine () { cd ${DEPLOY_DIR}/scripts set-combine-env set-k3s-env - ./setup_combine.py --tag ${COMBINE_VERSION} --repo public.ecr.aws/thecombine --target desktop ${SETUP_OPTS} $(((DEBUG == 1)) && echo "--debug") + ./setup_combine.py \ + $(((DEBUG == 1)) && echo "--debug") \ + --repo public.ecr.aws/thecombine \ + $(((ARM == 1)) && echo "--set global.cpuArch=arm64" ) \ + --tag ${COMBINE_VERSION} \ + --target desktop \ + ${SETUP_OPTS} deactivate } @@ -193,6 +204,7 @@ CONFIG_DIR=${HOME}/.config/combine mkdir -p ${CONFIG_DIR} SINGLE_STEP=0 IS_SERVER=0 +ARM=0 DEBUG=0 # See if we need to continue from a previous install @@ -207,6 +219,9 @@ fi while (( "$#" )) ; do OPT=$1 case $OPT in + arm) + ARM=1 + ;; clean) next-state "Pre-reqs" if [ -f ${CONFIG_DIR}/env ] ; then diff --git a/deploy/scripts/package_images.py b/deploy/scripts/package_images.py index 2a0fa5eb50..9bfb0b2a24 100755 --- a/deploy/scripts/package_images.py +++ b/deploy/scripts/package_images.py @@ -7,6 +7,8 @@ helm templates for the middleware used by The Combine and for The Combine itself. The image names are extracted from the templates and then pulled from the repo and stored in ../images as compressed tarballs; zstd compression is used. + +By default, packs images for amd64; use --arch for a different architecture. """ import argparse @@ -39,6 +41,12 @@ def parse_args() -> argparse.Namespace: ) parser.add_argument("output_dir", help="Directory for the collected image files.") # Add Optional arguments + parser.add_argument( + "--arch", + choices=["amd64", "arm64"], + default="amd64", + help="Target cpu architecture.", + ) parser.add_argument( "--config", "-c", @@ -59,28 +67,32 @@ def parse_args() -> argparse.Namespace: return parser.parse_args() -def package_k3s(dest_dir: Path, *, debug: bool = False) -> None: +def package_k3s(dest_dir: Path, *, arch: str = "arm64", debug: bool = False) -> None: logging.info("Packaging k3s images.") ansible_cmd = [ "ansible-playbook", "playbook_k3s_airgapped_files.yml", "--extra-vars", f"package_dir={dest_dir}", + "--extra-vars", + f"cpu_arch={arch}", ] if debug: ansible_cmd.append("-vv") run_cmd(ansible_cmd, cwd=str(ansible_dir), print_cmd=debug, print_output=debug) -def package_images(image_list: List[str], tar_file: Path, *, debug: bool = False) -> None: +def package_images( + image_list: List[str], tar_file: Path, *, arch: str = "amd64", debug: bool = False +) -> None: container_cli_cmd = [os.getenv("CONTAINER_CLI", "docker")] if container_cli_cmd[0] == "nerdctl": container_cli_cmd.extend(["--namespace", "k8s.io"]) # Pull each image + pull_cmd = container_cli_cmd + ["pull", f"--platform=linux/{arch}"] for image in image_list: - pull_cmd = container_cli_cmd + ["pull", image] - run_cmd(pull_cmd, print_cmd=debug, print_output=debug) + run_cmd(pull_cmd + [image], print_cmd=debug, print_output=debug) # Save pulled images into a .tar archive save_cmd = container_cli_cmd + ["save"] + image_list + ["-o", str(tar_file)] @@ -94,7 +106,13 @@ def package_images(image_list: List[str], tar_file: Path, *, debug: bool = False def package_middleware( - config_file: str, *, cluster_type: str, image_dir: Path, chart_dir: Path, debug: bool = False + config_file: str, + *, + cluster_type: str, + image_dir: Path, + chart_dir: Path, + arch: str = "amd64", + debug: bool = False, ) -> None: logging.info("Packaging middleware images.") @@ -149,12 +167,13 @@ def package_middleware( middleware_images.append(match.group(1)) logging.debug(f"Middleware images: {middleware_images}") - package_images( - middleware_images, image_dir / "middleware-airgap-images-amd64.tar", debug=debug - ) + out_file = f"middleware-airgap-images-{arch}.tar" + package_images(middleware_images, image_dir / out_file, arch=arch, debug=debug) -def package_thecombine(tag: str, image_dir: Path, *, debug: bool = False) -> None: +def package_thecombine( + tag: str, image_dir: Path, *, arch: str = "amd64", debug: bool = False +) -> None: logging.info(f"Packaging The Combine version {tag}.") logging.debug("Create helm charts from templates") combine_charts.generate(tag) @@ -186,7 +205,8 @@ def package_thecombine(tag: str, image_dir: Path, *, debug: bool = False) -> Non logging.debug(f"Combine images: {combine_images}") # Logout of AWS to allow pulling the images - package_images(combine_images, image_dir / "combine-airgap-images-amd64.tar", debug=debug) + out_file = f"combine-airgap-images-{arch}.tar" + package_images(combine_images, image_dir / out_file, arch=arch, debug=debug) def main() -> None: @@ -207,15 +227,16 @@ def main() -> None: os.environ["AWS_DEFAULT_REGION"] = "" # Update helm repos - package_k3s(image_dir, debug=args.debug) + package_k3s(image_dir, arch=args.arch, debug=args.debug) package_middleware( args.config, cluster_type="standard", image_dir=image_dir, chart_dir=chart_dir, + arch=args.arch, debug=args.debug, ) - package_thecombine(args.tag, image_dir, debug=args.debug) + package_thecombine(args.tag, image_dir, arch=args.arch, debug=args.debug) if __name__ == "__main__": diff --git a/installer/make-combine-installer.sh b/installer/make-combine-installer.sh index 71eaa578ce..13c2d0cc3f 100755 --- a/installer/make-combine-installer.sh +++ b/installer/make-combine-installer.sh @@ -13,12 +13,16 @@ error () { # cd to the directory where the script is installed SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +ARM=0 DEBUG=0 NET_INSTALL=0 # Parse arguments to customize installation while (( "$#" )) ; do OPT=$1 case $OPT in + --arm) + ARM=1 + ;; --debug) DEBUG=1 ;; @@ -58,7 +62,7 @@ if [[ $NET_INSTALL == 0 ]] ; then # Package The Combine for "offline" installation TEMP_DIR=/tmp/images-$$ pushd scripts - ./package_images.py ${COMBINE_VERSION} ${TEMP_DIR} $((( DEBUG == 1 )) && echo "--debug") + ./package_images.py ${COMBINE_VERSION} ${TEMP_DIR} $((( ARM == 1 )) && echo "--arch arm64") $((( DEBUG == 1 )) && echo "--debug") INSTALLER_NAME="combine-installer.run" popd else @@ -75,7 +79,7 @@ for DIR in venv scripts/__pycache__ ; do done cd ${SCRIPT_DIR} -makeself $((( DEBUG == 0)) && echo "--tar-quietly" ) ../deploy ${INSTALLER_NAME} "Combine Installer" scripts/install-combine.sh ${COMBINE_VERSION} +makeself $((( DEBUG == 0)) && echo "--tar-quietly" ) ../deploy ${INSTALLER_NAME} "Combine Installer" scripts/install-combine.sh ${COMBINE_VERSION} $((( ARM == 1 )) && echo "arm") if [[ $NET_INSTALL == 0 ]] ; then makeself --append ${TEMP_DIR} ${INSTALLER_NAME} rm -rf ${TEMP_DIR}