From 356ebc392e83691565436abb7f1ebf0466b76f02 Mon Sep 17 00:00:00 2001 From: shenlan Date: Thu, 2 Oct 2025 20:22:32 +0800 Subject: [PATCH] Add GitLab single-node offline installer --- playbooks/roles/charts/gitlab/files/README.md | 19 ++ .../gitlab/files/gitlab-offline.env.example | 6 + .../files/gitlab-values.single-node.yaml | 132 +++++++++++ .../charts/gitlab/files/install-gitlab.sh | 3 + playbooks/roles/charts/gitlab/files/setup.sh | 224 ++++++++++++++++++ scripts/create-gitlab-offline-package.sh | 109 +++++++++ 6 files changed, 493 insertions(+) create mode 100644 playbooks/roles/charts/gitlab/files/README.md create mode 100644 playbooks/roles/charts/gitlab/files/gitlab-offline.env.example create mode 100644 playbooks/roles/charts/gitlab/files/gitlab-values.single-node.yaml create mode 100755 playbooks/roles/charts/gitlab/files/install-gitlab.sh create mode 100755 playbooks/roles/charts/gitlab/files/setup.sh create mode 100755 scripts/create-gitlab-offline-package.sh 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"