diff --git a/.github/workflows/offline-package-argocd-installer.yaml b/.github/workflows/offline-package-argocd-installer.yaml index b1c823b..c47a615 100644 --- a/.github/workflows/offline-package-argocd-installer.yaml +++ b/.github/workflows/offline-package-argocd-installer.yaml @@ -65,15 +65,15 @@ jobs: - name: Prepare directories run: | set -euo pipefail - rm -rf offline-installer - mkdir -p offline-installer/{images,charts,scripts,metadata} + rm -rf argocd-offline-package + mkdir -p argocd-offline-package/{images,charts,scripts,metadata} - name: Stage installer script env: CHART_VERSION: ${{ steps.resolve.outputs.chart_version }} run: | set -euo pipefail - cat <<'SCRIPT' > offline-installer/scripts/install-argocd.sh + cat <<'SCRIPT' > argocd-offline-package/scripts/install-argocd.sh #!/usr/bin/env bash set -euo pipefail @@ -104,8 +104,8 @@ helm upgrade --install "${RELEASE_NAME}" "${CHART_DIR}" \ --create-namespace \ "$@" SCRIPT - chmod +x offline-installer/scripts/install-argocd.sh - cat < offline-installer/metadata/INFO + chmod +x argocd-offline-package/scripts/install-argocd.sh + cat < argocd-offline-package/metadata/INFO chart: argo/argo-cd chart_version: ${CHART_VERSION} created_at: $(date -u +%Y-%m-%dT%H:%M:%SZ) @@ -115,7 +115,7 @@ EOFMETA run: | set -euo pipefail wget https://github.com/containerd/nerdctl/releases/download/v${NERDCTL_VERSION}/nerdctl-${NERDCTL_VERSION}-linux-${{ matrix.arch }}.tar.gz \ - -O offline-installer/nerdctl.tar.gz + -O argocd-offline-package/nerdctl.tar.gz - name: Pull & export required images env: @@ -137,7 +137,7 @@ EOFMETA continue fi safe=$(echo "$img" | tr '/:' '-_') - docker save "$img" -o "offline-installer/images/${safe}.tar" + docker save "$img" -o "argocd-offline-package/images/${safe}.tar" done - name: Download Helm chart @@ -145,19 +145,19 @@ EOFMETA CHART_VERSION: ${{ steps.resolve.outputs.chart_version }} run: | set -euo pipefail - helm pull argo/argo-cd --version "${CHART_VERSION}" --untar --untardir offline-installer/charts + helm pull argo/argo-cd --version "${CHART_VERSION}" --untar --untardir argocd-offline-package/charts - name: Package offline installer run: | set -euo pipefail - tar -czf offline-setup-argocd-${{ matrix.arch }}.tar.gz -C offline-installer . - ls -lh offline-setup-argocd-${{ matrix.arch }}.tar.gz + tar -czf offline-package-argocd-${{ matrix.arch }}.tar.gz -C . argocd-offline-package + ls -lh offline-package-argocd-${{ matrix.arch }}.tar.gz - name: Upload artifact uses: actions/upload-artifact@v4 with: - name: offline-setup-argocd-${{ matrix.arch }} - path: offline-setup-argocd-${{ matrix.arch }}.tar.gz + name: offline-package-argocd-${{ matrix.arch }} + path: offline-package-argocd-${{ matrix.arch }}.tar.gz test-offline-installer: needs: build-offline-installer @@ -169,14 +169,14 @@ EOFMETA - name: Download artifact uses: actions/download-artifact@v4 with: - name: offline-setup-argocd-${{ matrix.arch }} + name: offline-package-argocd-${{ matrix.arch }} path: offline-test - name: Verify offline package integrity run: | set -euo pipefail cd offline-test - tar -tzf offline-setup-argocd-${{ matrix.arch }}.tar.gz > /dev/null + tar -tzf offline-package-argocd-${{ matrix.arch }}.tar.gz > /dev/null publish-release: needs: test-offline-installer @@ -204,13 +204,13 @@ EOFMETA - name: Download amd64 artifact uses: actions/download-artifact@v4 with: - name: offline-setup-argocd-amd64 + name: offline-package-argocd-amd64 path: release-artifacts/amd64 - name: Download arm64 artifact uses: actions/download-artifact@v4 with: - name: offline-setup-argocd-arm64 + name: offline-package-argocd-arm64 path: release-artifacts/arm64 - name: Upload offline installers to GitHub Release @@ -218,8 +218,8 @@ EOFMETA with: tag_name: ${{ env.TAG_NAME }} files: | - release-artifacts/amd64/offline-setup-argocd-amd64.tar.gz - release-artifacts/arm64/offline-setup-argocd-arm64.tar.gz + release-artifacts/amd64/offline-package-argocd-amd64.tar.gz + release-artifacts/arm64/offline-package-argocd-arm64.tar.gz env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -244,8 +244,8 @@ EOFMETA ssh -i ~/.ssh/id_rsa "${RSYNC_SSH_USER}@${VPS_HOST}" "mkdir -p '${REMOTE_DIR}'" echo "Rsync -> ${VPS_HOST}:${REMOTE_DIR}/" rsync -av -e "ssh -i ~/.ssh/id_rsa" \ - release-artifacts/amd64/offline-setup-argocd-amd64.tar.gz \ - release-artifacts/arm64/offline-setup-argocd-arm64.tar.gz \ + release-artifacts/amd64/offline-package-argocd-amd64.tar.gz \ + release-artifacts/arm64/offline-package-argocd-arm64.tar.gz \ "${RSYNC_SSH_USER}@${VPS_HOST}:${REMOTE_DIR}/" retention: diff --git a/.github/workflows/offline-package-gitea-installer.yaml b/.github/workflows/offline-package-gitea-installer.yaml index 3af3948..1ac32e6 100644 --- a/.github/workflows/offline-package-gitea-installer.yaml +++ b/.github/workflows/offline-package-gitea-installer.yaml @@ -67,18 +67,18 @@ jobs: run: | set -euo pipefail rm -rf "${OFFLINE_DIR}" - mkdir -p "${OFFLINE_DIR}"/{images,charts,scripts,metadata} + mkdir -p "${OFFLINE_DIR}"/{images,charts,metadata} - name: Stage installer script env: CHART_VERSION: ${{ steps.resolve.outputs.chart_version }} run: | set -euo pipefail - cat <<'SCRIPT' > "${OFFLINE_DIR}/scripts/install-gitea.sh" + cat <<'SCRIPT' > "${OFFLINE_DIR}/install-gitea.sh" #!/usr/bin/env bash set -euo pipefail -ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" CHART_DIR="${ROOT_DIR}/charts/gitea" IMAGES_DIR="${ROOT_DIR}/images" RELEASE_NAME="${RELEASE_NAME:-gitea}" @@ -187,6 +187,13 @@ EOFMETA cd offline-test tar -tzf offline-package-gitea-${{ matrix.arch }}.tar.gz > /dev/null + - name: Verify install script layout + run: | + set -euo pipefail + cd offline-test + tar -xvpf offline-package-gitea-${{ matrix.arch }}.tar.gz + test -f gitea-offline-package/install-gitea.sh + publish-release: needs: test-offline-installer runs-on: ubuntu-latest diff --git a/playbooks/roles/charts/gitlab/files/README.md b/playbooks/roles/charts/gitlab/files/README.md new file mode 100644 index 0000000..6896491 --- /dev/null +++ b/playbooks/roles/charts/gitlab/files/README.md @@ -0,0 +1,19 @@ +# GitLab Single-Node Offline Installer Assets + +This directory contains helper files used to assemble the GitLab offline package: + +- `setup.sh` – main installer script invoked by `install-gitlab.sh` +- `install-gitlab.sh` – wrapper that forwards to `setup.sh` +- `gitlab-values.single-node.yaml` – values template optimised for single-node installations +- `gitlab-offline.env.example` – sample configuration file consumed by the installer + +The offline package builder (`scripts/create-gitlab-offline-package.sh`) copies these +artifacts into the final archive so that users can extract and run: + +```bash +tar -xvpf offline-package-gitlab-amd64.tar.gz +cd gitlab-offline-package/ +cp gitlab-offline.env.example gitlab-offline.env +# Edit gitlab-offline.env and then execute: +bash install-gitlab.sh --version --domain [--namespace ] +``` diff --git a/playbooks/roles/charts/gitlab/files/gitlab-offline.env.example b/playbooks/roles/charts/gitlab/files/gitlab-offline.env.example new file mode 100644 index 0000000..40f6af2 --- /dev/null +++ b/playbooks/roles/charts/gitlab/files/gitlab-offline.env.example @@ -0,0 +1,6 @@ +# Copy this file to gitlab-offline.env and adjust the values before running install-gitlab.sh +GITLAB_VERSION=7.8.0 +GITLAB_DOMAIN=gitlab.example.com +GITLAB_NAMESPACE=gitlab +# Set to 1 to skip loading bundled images +# SKIP_IMAGE_LOAD=1 diff --git a/playbooks/roles/charts/gitlab/files/gitlab-values.single-node.yaml b/playbooks/roles/charts/gitlab/files/gitlab-values.single-node.yaml new file mode 100644 index 0000000..7eb0d41 --- /dev/null +++ b/playbooks/roles/charts/gitlab/files/gitlab-values.single-node.yaml @@ -0,0 +1,132 @@ +# Rendered by install-gitlab.sh using envsubst. Variables: +# ${GITLAB_DOMAIN} +# ${GITLAB_NAMESPACE} +global: + edition: ce + hosts: + domain: ${GITLAB_DOMAIN} + gitlab: + name: gitlab.${GITLAB_DOMAIN} + registry: + name: registry.${GITLAB_DOMAIN} + minio: + name: minio.${GITLAB_DOMAIN} + smartcard: + name: smartcard.${GITLAB_DOMAIN} + kas: + name: kas.${GITLAB_DOMAIN} + ingress: + class: nginx + configureCertmanager: false + enabled: true + tls: + enabled: false + minio: + enabled: true + gitaly: + persistence: + enabled: true + size: 40Gi + psql: + enabled: false + redis: + password: + enabled: false + appConfig: + enableUsagePing: false + omniauth: + enabled: false + +certmanager: + install: false + installCRDs: false + +nginx-ingress: + enabled: false + +prometheus: + install: false + +gitlab-runner: + install: false + +kas: + enabled: false + +registry: + hpa: + enabled: false + +upgradeCheck: + enabled: false + +gitlab: + gitaly: + persistence: + enabled: true + size: 40Gi + webservice: + minReplicas: 1 + maxReplicas: 1 + ingress: + path: / + hpa: + minReplicas: 1 + maxReplicas: 1 + resources: + limits: + cpu: 1000m + memory: 4Gi + requests: + cpu: 250m + memory: 2Gi + sidekiq: + replicas: + default: 1 + gitlab-shell: + minReplicas: 1 + maxReplicas: 1 + +postgresql: + install: true + global: + postgresql: + auth: + # Adjust these credentials before deploying to production environments. + postgresPassword: changeme + username: gitlab + password: changeme + database: gitlabhq_production + primary: + persistence: + enabled: true + size: 40Gi + resources: + requests: + cpu: 250m + memory: 512Mi + limits: + cpu: 500m + memory: 2Gi + +redis: + install: true + master: + persistence: + enabled: true + size: 10Gi + resources: + requests: + cpu: 100m + memory: 256Mi + limits: + cpu: 200m + memory: 512Mi + +minio: + persistence: + size: 40Gi + +shared-secrets: + self-signed-cert: + generate: true diff --git a/playbooks/roles/charts/gitlab/files/install-gitlab.sh b/playbooks/roles/charts/gitlab/files/install-gitlab.sh new file mode 100755 index 0000000..61a2fe5 --- /dev/null +++ b/playbooks/roles/charts/gitlab/files/install-gitlab.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +exec "${SCRIPT_DIR}/setup.sh" "$@" diff --git a/playbooks/roles/charts/gitlab/files/setup.sh b/playbooks/roles/charts/gitlab/files/setup.sh new file mode 100755 index 0000000..16c5703 --- /dev/null +++ b/playbooks/roles/charts/gitlab/files/setup.sh @@ -0,0 +1,224 @@ +#!/usr/bin/env bash +# Offline installer for GitLab Helm chart tailored for a single-node deployment. +# This script is designed to be embedded in the GitLab offline package and invoked as +# bash install-gitlab.sh --version --domain [--namespace ] +# It renders a single-node friendly values file from the bundled template and installs +# the locally available GitLab Helm chart archive. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +DEFAULT_CONFIG_FILE="${SCRIPT_DIR}/gitlab-offline.env" +DEFAULT_TEMPLATE="${SCRIPT_DIR}/gitlab-values.single-node.yaml" +DEFAULT_CHART_DIR="${SCRIPT_DIR}/charts" + +log() { printf '[\033[32mINFO\033[0m] %s\n' "$*"; } +warn() { printf '[\033[33mWARN\033[0m] %s\n' "$*"; } +err() { printf '[\033[31mERROR\033[0m] %s\n' "$*" >&2; exit 1; } + +usage() { + cat <<'USAGE' +Usage: install-gitlab.sh --version --domain [options] + +Options: + --version, -v GitLab chart version (required) + --domain, -d External domain name for GitLab (required) + --namespace, -n Kubernetes namespace to deploy into (default: gitlab) + --config FILE Configuration file (default: ./gitlab-offline.env if present) + --values FILE Values template to render (default: ./gitlab-values.single-node.yaml) + --chart FILE Explicit GitLab chart archive (*.tgz). Overrides --charts-dir. + --charts-dir DIR Directory that contains the GitLab chart archive (default: ./charts) + --skip-image-load Skip loading container images from ./images + --help, -h Show this help and exit + +Environment variables (overridable via --config or CLI): + GITLAB_VERSION Same as --version + GITLAB_DOMAIN Same as --domain + GITLAB_NAMESPACE Same as --namespace + GITLAB_CHART Same as --chart + GITLAB_CHARTS_DIR Same as --charts-dir + GITLAB_VALUES Same as --values + SKIP_IMAGE_LOAD Same as --skip-image-load +USAGE +} + +load_config_file() { + local file="$1" + [[ -f "$file" ]] || return 0 + # shellcheck disable=SC1090 + source "$file" +} + +parse_args() { + local args=("$@") + local config_file="$DEFAULT_CONFIG_FILE" + + for ((i = 0; i < ${#args[@]}; i++)); do + if [[ "${args[i]}" == "--config" ]]; then + (( i + 1 < ${#args[@]} )) || err "--config requires a value" + config_file="${args[i+1]}" + break + fi + done + + load_config_file "$config_file" + + local i=0 + while [[ $i -lt ${#args[@]} ]]; do + case "${args[i]}" in + --version|-v) + (( i + 1 < ${#args[@]} )) || err "--version requires a value" + GITLAB_VERSION="${args[i+1]}" + ((i+=2)) + ;; + --domain|-d) + (( i + 1 < ${#args[@]} )) || err "--domain requires a value" + GITLAB_DOMAIN="${args[i+1]}" + ((i+=2)) + ;; + --namespace|-n) + (( i + 1 < ${#args[@]} )) || err "--namespace requires a value" + GITLAB_NAMESPACE="${args[i+1]}" + ((i+=2)) + ;; + --config) + ((i+=2)) + ;; + --values) + (( i + 1 < ${#args[@]} )) || err "--values requires a value" + GITLAB_VALUES="${args[i+1]}" + ((i+=2)) + ;; + --chart) + (( i + 1 < ${#args[@]} )) || err "--chart requires a value" + GITLAB_CHART="${args[i+1]}" + ((i+=2)) + ;; + --charts-dir) + (( i + 1 < ${#args[@]} )) || err "--charts-dir requires a value" + GITLAB_CHARTS_DIR="${args[i+1]}" + ((i+=2)) + ;; + --skip-image-load) + SKIP_IMAGE_LOAD="1" + ((i+=1)) + ;; + --help|-h) + usage + exit 0 + ;; + *) + err "Unknown argument: ${args[i]}" + ;; + esac + done +} + +check_prerequisites() { + command -v helm >/dev/null 2>&1 || err "helm is required" + command -v kubectl >/dev/null 2>&1 || err "kubectl is required" + command -v envsubst >/dev/null 2>&1 || err "envsubst (gettext) is required" +} + +pick_loader() { + if command -v docker >/dev/null 2>&1; then + if docker info >/dev/null 2>&1; then + echo "docker" + return + fi + fi + if command -v nerdctl >/dev/null 2>&1; then + if nerdctl info >/dev/null 2>&1; then + echo "nerdctl" + return + fi + fi + if command -v ctr >/dev/null 2>&1; then + echo "ctr" + return + fi + echo "" +} + +load_offline_images() { + local images_dir="$SCRIPT_DIR/images" + [[ "${SKIP_IMAGE_LOAD:-0}" == "1" ]] && { warn "Skipping image import as requested"; return 0; } + [[ -d "$images_dir" ]] || { warn "No images directory found (expected ${images_dir})"; return 0; } + shopt -s nullglob + local archives=("${images_dir}"/*.tar "${images_dir}"/*.tar.gz) + shopt -u nullglob + [[ ${#archives[@]} -gt 0 ]] || { warn "No container image archives found in ${images_dir}"; return 0; } + + local loader; loader="$(pick_loader)" + [[ -n "$loader" ]] || err "Unable to locate docker, nerdctl, or ctr for loading images" + + log "Loading offline container images using ${loader}" + for archive in "${archives[@]}"; do + case "$loader" in + docker) docker load -i "$archive" ;; + nerdctl) nerdctl load -i "$archive" ;; + ctr) ctr -n k8s.io images import "$archive" ;; + esac + done +} + +render_values_file() { + local template="$1" output="$2" + [[ -f "$template" ]] || err "Values template not found: $template" + export GITLAB_DOMAIN GITLAB_NAMESPACE + envsubst '${GITLAB_DOMAIN}${GITLAB_NAMESPACE}' < "$template" > "$output" +} + +select_chart() { + if [[ -n "${GITLAB_CHART:-}" ]]; then + [[ -f "$GITLAB_CHART" ]] || err "Specified chart not found: $GITLAB_CHART" + echo "$GITLAB_CHART" + return + fi + local charts_dir="${GITLAB_CHARTS_DIR:-$DEFAULT_CHART_DIR}" + local chart_archive="${charts_dir}/gitlab-${GITLAB_VERSION}.tgz" + [[ -f "$chart_archive" ]] || err "GitLab chart archive not found: $chart_archive" + echo "$chart_archive" +} + +ensure_namespace() { + if ! kubectl get namespace "$GITLAB_NAMESPACE" >/dev/null 2>&1; then + log "Creating namespace ${GITLAB_NAMESPACE}" + kubectl create namespace "$GITLAB_NAMESPACE" + fi +} + +main() { + parse_args "$@" + check_prerequisites + + [[ -n "${GITLAB_VERSION:-}" ]] || err "GitLab chart version is required (--version)" + [[ -n "${GITLAB_DOMAIN:-}" ]] || err "Domain is required (--domain)" + GITLAB_NAMESPACE="${GITLAB_NAMESPACE:-gitlab}" + + local values_template="${GITLAB_VALUES:-$DEFAULT_TEMPLATE}" + local tmp_values + tmp_values="$(mktemp)" + trap 'rm -f "$tmp_values"' EXIT + + render_values_file "$values_template" "$tmp_values" + load_offline_images + + ensure_namespace + + local chart_archive + chart_archive="$(select_chart)" + + log "Installing GitLab ${GITLAB_VERSION} into namespace ${GITLAB_NAMESPACE}" + helm upgrade --install gitlab "$chart_archive" \ + --namespace "$GITLAB_NAMESPACE" \ + --create-namespace \ + --values "$tmp_values" \ + --timeout 15m \ + --wait \ + --debug + + log "GitLab installation triggered successfully" +} + +main "$@" diff --git a/scripts/create-gitlab-offline-package.sh b/scripts/create-gitlab-offline-package.sh new file mode 100755 index 0000000..7ff72a1 --- /dev/null +++ b/scripts/create-gitlab-offline-package.sh @@ -0,0 +1,109 @@ +#!/usr/bin/env bash +# Build the GitLab offline package archive. +# Usage: GITLAB_VERSION= scripts/create-gitlab-offline-package.sh +# Environment variables: +# GITLAB_VERSION (required) GitLab Helm chart version, e.g. 7.8.0 +# ARCH Target architecture suffix for the archive name (default: amd64) +# INCLUDE_IMAGES If set to 1, pull GitLab images and bundle them into the package (requires docker) +# WORKDIR Working directory name (default: gitlab-offline-package) + +set -euo pipefail + +log() { printf '[\033[32mINFO\033[0m] %s\n' "$*"; } +warn() { printf '[\033[33mWARN\033[0m] %s\n' "$*"; } +err() { printf '[\033[31mERROR\033[0m] %s\n' "$*" >&2; exit 1; } + +command -v helm >/dev/null 2>&1 || err "helm is required to build the offline package" + +GITLAB_VERSION="${GITLAB_VERSION:-}" +[[ -n "$GITLAB_VERSION" ]] || err "GITLAB_VERSION is required" + +ARCH="${ARCH:-amd64}" +WORKDIR="${WORKDIR:-gitlab-offline-package}" +INCLUDE_IMAGES="${INCLUDE_IMAGES:-0}" +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +FILES_DIR="${ROOT_DIR}/playbooks/roles/charts/gitlab/files" +VALUES_TEMPLATE="${FILES_DIR}/gitlab-values.single-node.yaml" +INSTALLER_SCRIPT="${FILES_DIR}/setup.sh" +WRAPPER_SCRIPT="${FILES_DIR}/install-gitlab.sh" +ENV_EXAMPLE="${FILES_DIR}/gitlab-offline.env.example" + +[[ -f "$VALUES_TEMPLATE" ]] || err "Values template missing: $VALUES_TEMPLATE" +[[ -f "$INSTALLER_SCRIPT" ]] || err "Installer script missing: $INSTALLER_SCRIPT" +[[ -f "$WRAPPER_SCRIPT" ]] || err "Wrapper script missing: $WRAPPER_SCRIPT" + +rm -rf "$WORKDIR" +mkdir -p "$WORKDIR/charts" + +log "Pulling GitLab Helm chart ${GITLAB_VERSION}" +helm repo add gitlab https://charts.gitlab.io/ >/dev/null +helm repo update >/dev/null +helm pull gitlab/gitlab --version "$GITLAB_VERSION" --destination "$WORKDIR/charts" + +log "Copying installer assets" +cp "$INSTALLER_SCRIPT" "$WORKDIR/setup.sh" +cp "$WRAPPER_SCRIPT" "$WORKDIR/install-gitlab.sh" +cp "$FILES_DIR/gitlab-values.single-node.yaml" "$WORKDIR/gitlab-values.single-node.yaml" +cp "$ENV_EXAMPLE" "$WORKDIR/gitlab-offline.env.example" +chmod +x "$WORKDIR/setup.sh" "$WORKDIR/install-gitlab.sh" + +cat > "$WORKDIR/README.md" <<'DOC' +# GitLab Offline Package + +## Usage + +``` +tar -xvpf offline-package-gitlab-.tar.gz +cd gitlab-offline-package/ +cp gitlab-offline.env.example gitlab-offline.env +# Adjust gitlab-offline.env then run: +bash install-gitlab.sh --version --domain [--namespace ] +``` + +If container images are bundled, they can be imported automatically. Set +`SKIP_IMAGE_LOAD=1` in `gitlab-offline.env` to skip loading. +DOC + +bundle_images() { + local chart_archive="$WORKDIR/charts/gitlab-${GITLAB_VERSION}.tgz" + local tmp_values tmp_manifest + tmp_values="$(mktemp)" + tmp_manifest="$(mktemp)" + trap 'rm -f "$tmp_values" "$tmp_manifest"' RETURN + + export GITLAB_DOMAIN="gitlab.example.com" GITLAB_NAMESPACE="gitlab" + envsubst '${GITLAB_DOMAIN}${GITLAB_NAMESPACE}' < "$VALUES_TEMPLATE" > "$tmp_values" + helm template gitlab "$chart_archive" -f "$tmp_values" > "$tmp_manifest" + + mapfile -t images < <(awk '/image:/{print $2}' "$tmp_manifest" | sed 's/"//g' | sort -u) + if [[ ${#images[@]} -eq 0 ]]; then + warn "No images detected; skipping image bundle" + return + fi + + command -v docker >/dev/null 2>&1 || err "docker is required to bundle images" + mkdir -p "$WORKDIR/images" + + for image in "${images[@]}"; do + log "Pulling $image" + docker pull --platform "linux/${ARCH}" "$image" + done + + local image_tar="$WORKDIR/images/gitlab-images-${ARCH}.tar" + log "Saving images to ${image_tar}" + docker save -o "$image_tar" "${images[@]}" + printf '%s\n' "${images[@]}" > "$WORKDIR/images/images.txt" +} + +if [[ "$INCLUDE_IMAGES" == "1" ]]; then + log "Bundling container images" + bundle_images +else + warn "Images are not included. Set INCLUDE_IMAGES=1 to bundle them (requires docker)." +fi + +tar_name="offline-package-gitlab-${ARCH}.tar.gz" +rm -f "$tar_name" +log "Creating archive ${tar_name}" +tar -czpf "$tar_name" "$WORKDIR" +log "Done"