Compare commits
13
Commits
57af30fdd2
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c727244df | ||
|
|
fa125ed616 | ||
|
|
b65730c45f | ||
|
|
45f7663258 | ||
|
|
fc4cb86f24 | ||
|
|
782a130bb9 | ||
|
|
f7bcad0b38 | ||
|
|
4417645639 | ||
|
|
803f05a60b | ||
|
|
faabf0b82b | ||
|
|
5b4bbce5c8 | ||
|
|
bc5ffaf1d2 | ||
|
|
bd196ec9e0 |
@@ -41,3 +41,6 @@ nb-configuration.xml
|
||||
|
||||
# Plugin directory
|
||||
/.quarkus/cli/plugins/
|
||||
|
||||
# deploy-services.sh summary logs
|
||||
deploy-*.log
|
||||
|
||||
@@ -7,16 +7,46 @@ If you want to learn more about Quarkus, please visit its website: https://quark
|
||||
## Building and deploying natively
|
||||
|
||||
Build the native image with the Mandrel-based multi-stage Dockerfile and push it
|
||||
directly to a remote Docker host via [unregistry](https://github.com/psviderski/unregistry)
|
||||
(no external registry required; only missing layers are transferred over SSH):
|
||||
to a remote Docker host over an SSH tunnel. The script starts a temporary
|
||||
`registry:2` container on the remote (localhost only), forwards it to the build
|
||||
machine, pushes the image through that tunnel, then pulls and retags it on the
|
||||
remote Docker daemon. No external registry is required.
|
||||
|
||||
```shell script
|
||||
./scripts/build-and-push.sh --remote deploy@prod.example.com
|
||||
```
|
||||
|
||||
The script reads `artifactId` and `version` from `pom.xml` for the image name
|
||||
and tag, builds for `linux/amd64`, and verifies the image is present on the
|
||||
remote after pushing. See `./scripts/build-and-push.sh --help` for all flags.
|
||||
To push every service in one project, pass that project's list file to
|
||||
`./scripts/deploy-services.sh`. It runs `build-and-push.sh` once per path and
|
||||
reports any failures at the end. See `scripts/README.md` for the list format.
|
||||
|
||||
The script uses the last directory of the service location, lowercased, as the image name
|
||||
and reads `version` from `pom.xml` for the local build tag (`1.0.0-SNAPSHOT`
|
||||
if there is no pom). It builds for `linux/amd64` and verifies the deploy on
|
||||
the remote.
|
||||
On the remote, durable tags are only:
|
||||
|
||||
- `<name>:latest` — always the most recent successful push
|
||||
- `<name>:vYYYY.MM.DD-HH.MM` — UTC timestamp of that push (for rollback)
|
||||
|
||||
Older releases stay tagged by date only: the script keeps the current image (with
|
||||
`latest` and its date tag) and the previous date tag, and removes other tags and
|
||||
images (including old `1.0.x`, `sha256-*`, and third copies). Run
|
||||
`--cleanup` on the remote to apply the same pruning without a new push.
|
||||
|
||||
To roll back on the remote, run a previous `v…` tag from `docker images` instead of `:latest`.
|
||||
|
||||
The temporary registry container and SSH tunnel are removed when the script
|
||||
finishes. See `./scripts/build-and-push.sh --help` for all flags.
|
||||
|
||||
To remove leftover temporary registry containers and `localhost:<port>/...` image
|
||||
tags on the remote (for example after an interrupted push or manual tests):
|
||||
|
||||
```shell script
|
||||
./scripts/build-and-push.sh --remote deploy@prod.example.com --cleanup
|
||||
```
|
||||
|
||||
This does not rebuild or push; it only needs SSH access to the remote host.
|
||||
|
||||
It can be invoked from any directory: by default it resolves the project root
|
||||
from the script's own location. Override with `--project-dir PATH` or the
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>com.example</groupId>
|
||||
<artifactId>test-quarkus</artifactId>
|
||||
<version>1.0.0-SNAPSHOT</version>
|
||||
<version>1.0.4</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>Test Quarkus</name>
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
# Scripts de despliegue
|
||||
|
||||
Guía de uso de los scripts de este directorio.
|
||||
|
||||
## `build-and-push.sh`
|
||||
|
||||
Construye la imagen nativa de Quarkus y la envía al daemon Docker de un host remoto por SSH, sin usar un registry externo ([unregistry](https://github.com/psviderski/unregistry) / `docker pussh`). Solo se transfieren las capas que el remoto aún no tiene.
|
||||
|
||||
### Qué hace el flujo
|
||||
|
||||
1. Toma el nombre de la imagen de la última carpeta de la ubicación del servicio, en minúsculas, y lee `version` de [`pom.xml`](../pom.xml) para el tag, salvo que uses `--name` o `--tag`. Si no hay `pom.xml`, el tag es `1.0.0-SNAPSHOT`. `--name` también se pasa a minúsculas.
|
||||
2. Ejecuta `docker build` para la plataforma fija `linux/amd64` (útil si construyes desde un Mac arm64). Con `--type java` (el valor por defecto) usa [`src/main/docker/Dockerfile`](../src/main/docker/Dockerfile). Con `--type web` usa `Dockerfile` en la raíz del servicio.
|
||||
3. Ejecuta `docker pussh` hacia el destino SSH indicado.
|
||||
4. Comprueba por SSH que la imagen existe en el remoto (salvo `--no-verify`).
|
||||
5. Muestra un comando `docker run` sugerido para arrancar el contenedor en el remoto.
|
||||
|
||||
### Requisitos
|
||||
|
||||
| Requisito | Descripción |
|
||||
|-----------|-------------|
|
||||
| Docker local | Daemon en marcha (`docker info` debe responder). |
|
||||
| SSH | Acceso al host remoto; en el remoto debe estar instalado y usable Docker. |
|
||||
| Plugin `docker pussh` | CLI plugin de unregistry. |
|
||||
|
||||
Si el plugin no está instalado, el script indica cómo añadirlo:
|
||||
|
||||
```shell
|
||||
mkdir -p ~/.docker/cli-plugins
|
||||
curl -fsSL https://raw.githubusercontent.com/psviderski/unregistry/main/docker-pussh \
|
||||
-o ~/.docker/cli-plugins/docker-pussh
|
||||
chmod +x ~/.docker/cli-plugins/docker-pussh
|
||||
```
|
||||
|
||||
`xmllint` es opcional: si no está en el PATH y existe `pom.xml`, el script obtiene `version` del POM con `grep`/`sed`.
|
||||
|
||||
### Invocación
|
||||
|
||||
Puedes ejecutar el script desde cualquier directorio. Por defecto usa como raíz del proyecto el directorio padre de `scripts/` (donde está `pom.xml`).
|
||||
|
||||
```shell
|
||||
./scripts/build-and-push.sh --remote USER@HOST
|
||||
```
|
||||
|
||||
Alternativas para la raíz del proyecto:
|
||||
|
||||
- `--project-dir /ruta/al/proyecto`
|
||||
- variable de entorno `PROJECT_DIR`
|
||||
|
||||
Ayuda integrada (en inglés):
|
||||
|
||||
```shell
|
||||
./scripts/build-and-push.sh --help
|
||||
```
|
||||
|
||||
### Opciones
|
||||
|
||||
**Destino (obligatorio)**
|
||||
|
||||
| Opción | Descripción |
|
||||
|--------|-------------|
|
||||
| `--remote TARGET` | Destino SSH: `usuario@host[:puerto]` o un alias definido en `~/.ssh/config` (sin `@`). |
|
||||
|
||||
**Identidad de la imagen** (el nombre es la última carpeta del servicio, en minúsculas; el tag sale de `pom.xml` si existe)
|
||||
|
||||
| Opción | Descripción |
|
||||
|--------|-------------|
|
||||
| `--name NAME` | Nombre del repositorio de la imagen. Se guarda en minúsculas. |
|
||||
| `--tag TAG` | Tag de la imagen. |
|
||||
|
||||
**Directorio de trabajo**
|
||||
|
||||
| Opción | Descripción |
|
||||
|--------|-------------|
|
||||
| `--project-dir PATH` | Raíz del servicio. El nombre de la imagen es su última carpeta, en minúsculas. |
|
||||
| `PROJECT_DIR` | Misma función vía entorno. |
|
||||
|
||||
**Build**
|
||||
|
||||
| Opción | Descripción |
|
||||
|--------|-------------|
|
||||
| `--type` | Dónde está el Dockerfile. `java` (por defecto): `src/main/docker/Dockerfile`. `web`: `Dockerfile` en la raíz del servicio. |
|
||||
| `--skip-build` | No construye; solo hace `pussh` de una imagen que ya exista en local. La plataforma de build sigue siendo `linux/amd64` cuando no se omite el build. |
|
||||
|
||||
**SSH** (se reenvían a `docker pussh` y, en la verificación, a `ssh`)
|
||||
|
||||
| Opción | Descripción |
|
||||
|--------|-------------|
|
||||
| `--ssh-key PATH` | Clave privada; tiene prioridad sobre `IdentityFile` del config SSH. |
|
||||
| `--ssh-config PATH` | Archivo de configuración SSH (por defecto `~/.ssh/config`). |
|
||||
|
||||
**Post-push**
|
||||
|
||||
| Opción | Descripción |
|
||||
|--------|-------------|
|
||||
| `--no-verify` | Omite la comprobación `docker images` en el remoto. |
|
||||
|
||||
**Otros**
|
||||
|
||||
| Opción | Descripción |
|
||||
|--------|-------------|
|
||||
| `-h`, `--help` | Muestra la ayuda del script. |
|
||||
|
||||
### Alias en `~/.ssh/config`
|
||||
|
||||
Si `--remote` es un alias sin `@`, el script resuelve `HostName`, `User`, `Port` e `IdentityFile` del bloque `Host` correspondiente. `--ssh-key` siempre anula `IdentityFile`.
|
||||
|
||||
Ejemplo de configuración:
|
||||
|
||||
```text
|
||||
# ~/.ssh/config
|
||||
Host prod
|
||||
HostName prod.example.com
|
||||
User deploy
|
||||
Port 22
|
||||
IdentityFile ~/.ssh/deploy_ed25519
|
||||
```
|
||||
|
||||
Uso:
|
||||
|
||||
```shell
|
||||
./scripts/build-and-push.sh --remote prod
|
||||
# Equivale a deploy@prod.example.com:22 con la clave del bloque Host prod
|
||||
```
|
||||
|
||||
### Ejemplos
|
||||
|
||||
Destino explícito:
|
||||
|
||||
```shell
|
||||
./scripts/build-and-push.sh --remote deploy@prod.example.com
|
||||
```
|
||||
|
||||
Alias SSH:
|
||||
|
||||
```shell
|
||||
./scripts/build-and-push.sh --remote prod
|
||||
```
|
||||
|
||||
Clave explícita:
|
||||
|
||||
```shell
|
||||
./scripts/build-and-push.sh --remote ubuntu@1.2.3.4 --ssh-key ~/.ssh/deploy_ed25519
|
||||
```
|
||||
|
||||
Solo push (imagen ya construida) con otro tag:
|
||||
|
||||
```shell
|
||||
./scripts/build-and-push.sh --remote deploy@prod --skip-build --tag 2.0.0
|
||||
```
|
||||
|
||||
Ruta absoluta al script:
|
||||
|
||||
```shell
|
||||
/absolute/path/to/test-quarkus/scripts/build-and-push.sh --remote deploy@prod
|
||||
```
|
||||
|
||||
Proyecto en otra ruta:
|
||||
|
||||
```shell
|
||||
PROJECT_DIR=/srv/proj ./scripts/build-and-push.sh --remote deploy@prod --skip-build
|
||||
```
|
||||
|
||||
### Después del push
|
||||
|
||||
Al terminar con éxito, el script sugiere algo equivalente a:
|
||||
|
||||
```shell
|
||||
ssh deploy@prod.example.com \
|
||||
"docker run -d --name <carpeta> --restart unless-stopped -p 8080:8080 <carpeta>:<version>"
|
||||
```
|
||||
|
||||
Sustituye `<carpeta>` por la última carpeta de la ubicación del servicio en minúsculas (o el valor de `--name`, también en minúsculas) y `<version>` por el tag del build (el de `pom.xml`, `1.0.0-SNAPSHOT` si no hay POM, o el de `--tag`).
|
||||
|
||||
### Solución de problemas
|
||||
|
||||
#### `invalid reference format` con saltos de línea en el tag
|
||||
|
||||
Si `docker build` falla con un tag parecido a `test-quarkus\n1.0.0-SNAPSHOT:test-quarkus\n1.0.0-SNAPSHOT`, el checkout del script es anterior al commit `57af30f`. En hosts sin `xmllint` (por ejemplo vm-compilador), el fallback antiguo de `read_pom` mezclaba `artifactId` y `version` en una sola cadena.
|
||||
|
||||
En la máquina donde compilas (`~/repo` u otra ruta del proyecto):
|
||||
|
||||
```shell
|
||||
git rev-parse --short HEAD
|
||||
grep -n 'head -2' scripts/build-and-push.sh || echo "script actualizado"
|
||||
git pull
|
||||
git rev-parse --short HEAD # debe ser 57af30f o posterior
|
||||
./scripts/build-and-push.sh --remote TU_DESTINO
|
||||
```
|
||||
|
||||
Comprueba que la primera línea del log sea `Image: test-quarkus:1.0.0-SNAPSHOT` (sin saltos de línea).
|
||||
|
||||
**Atajo** si aún no puedes actualizar el repo: fija nombre y tag explícitos (no usa el fallback roto):
|
||||
|
||||
```shell
|
||||
./scripts/build-and-push.sh --remote TU_DESTINO \
|
||||
--name test-quarkus --tag 1.0.0-SNAPSHOT
|
||||
```
|
||||
|
||||
Opcional: instala `xmllint` para que el script lea el POM con XPath (`libxml2-utils` en Debian/Ubuntu).
|
||||
|
||||
## `deploy-services.sh`
|
||||
|
||||
Script independiente, en el mismo directorio que `build-and-push.sh`. No lo modifica: lo ejecuta una vez por cada servicio de un archivo de lista. Si un servicio falla, sigue con el resto y al terminar indica cuáles fallaron. El código de salida es `1` si alguno falló.
|
||||
|
||||
Al terminar el lote, ese resumen queda también en `deploy-YYYYMMDD-HHMMSS.log` en el directorio desde el que se lanzó el script (hora local de inicio). Si ese nombre ya existe, se añade el PID. Si no se puede escribir el archivo, el script avisa y conserva el código de salida del lote.
|
||||
|
||||
No hay lista por defecto. `--services` es obligatorio, así cada proyecto puede tener su propio archivo.
|
||||
|
||||
```shell
|
||||
./scripts/deploy-services.sh --remote prod --services /srv/proyecto-a/services.txt
|
||||
./scripts/deploy-services.sh --remote prod --services /srv/proyecto-b/services.txt --skip-build
|
||||
```
|
||||
|
||||
Formato del archivo: una ruta por línea y, si hace falta, un segundo campo `java` o `web`. Si el tipo falta, es `java`. Se ignoran las líneas vacías y las que empiezan por `#`. Una ruta relativa se resuelve desde el directorio del archivo de lista, no desde el directorio donde lanzas el script.
|
||||
|
||||
```text
|
||||
# /srv/proyecto-a/services.txt
|
||||
./api
|
||||
./portal web
|
||||
```
|
||||
|
||||
`java` usa `src/main/docker/Dockerfile`. `web` usa `Dockerfile` en la raíz del servicio. El nombre de la imagen es la última carpeta de esa ruta, en minúsculas. El tag sale de `pom.xml` si existe; si no, es `1.0.0-SNAPSHOT`.
|
||||
|
||||
Se reenvían a cada servicio: `--ssh-key`, `--ssh-config`, `--skip-build`, `--no-verify` y `--cleanup`, más el `--type` de esa línea. No se aceptan `--name`, `--tag`, `--project-dir` ni `--type`.
|
||||
|
||||
Ctrl+C interrumpe el lote. Ayuda: `./scripts/deploy-services.sh --help`.
|
||||
|
||||
## `update-repos.sh`
|
||||
|
||||
Actualiza un repositorio git por cada ruta de un archivo de lista. Ejecuta `git pull` en cada uno. Si alguno falla, sigue con el resto y al terminar indica cuáles fallaron. El código de salida es `1` si alguno falló.
|
||||
|
||||
No hay lista por defecto. `--repos` es obligatorio.
|
||||
|
||||
```shell
|
||||
./scripts/update-repos.sh --repos /srv/proyecto/repos.txt
|
||||
```
|
||||
|
||||
Al arrancar pide `Usuario:` y `Contraseña:` en la terminal, una sola vez para todo el lote. Esas credenciales se usan en los remotos HTTPS. No se leen desde la línea de comandos ni desde el archivo, y no se guardan. Un remoto SSH se actualiza con su acceso habitual (clave), sin esa contraseña.
|
||||
|
||||
Formato del archivo: una ruta por línea. Se ignoran las líneas vacías y las que empiezan por `#`. Una ruta relativa se resuelve desde el directorio del archivo de lista, no desde el directorio donde lanzas el script. Cada ruta debe ser la raíz de un repositorio git.
|
||||
|
||||
```text
|
||||
# /srv/proyecto/repos.txt
|
||||
/srv/repos/api
|
||||
./portal
|
||||
```
|
||||
|
||||
Ctrl+C interrumpe el lote. Ayuda: `./scripts/update-repos.sh --help`.
|
||||
+310
-61
@@ -1,20 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Build the native Quarkus image and push it to a remote Docker host
|
||||
# via docker-pussh (unregistry): https://github.com/psviderski/unregistry
|
||||
# via a temporary registry:2 on the remote, reached through an SSH tunnel.
|
||||
#
|
||||
# - Image name/tag default to pom.xml properties (quarkus.application.*)
|
||||
# - Image name defaults to the last directory of the service location
|
||||
# - Image tag defaults to the pom.xml version (1.0.0-SNAPSHOT if there is no pom)
|
||||
# - Builds for linux/amd64 by default (cross-compile from arm64 hosts)
|
||||
# - Transfers only missing layers over SSH (no external registry required)
|
||||
# - Transfers layers over the tunnel (no external registry required)
|
||||
# - On the remote, durable tags are :latest and vYYYY.MM.DD-HH.MM (UTC) only
|
||||
|
||||
set -euo pipefail
|
||||
IFS=$'\n\t'
|
||||
|
||||
readonly DOCKERFILE="src/main/docker/Dockerfile"
|
||||
readonly DEFAULT_PLATFORM="linux/amd64"
|
||||
readonly UNREGISTRY_PLUGIN="https://raw.githubusercontent.com/psviderski/unregistry/main/docker-pussh"
|
||||
readonly PLUGIN_DIR="${HOME}/.docker/cli-plugins"
|
||||
readonly PLUGIN_PATH="${PLUGIN_DIR}/docker-pussh"
|
||||
readonly TEMP_REGISTRY_IMAGE="registry:2"
|
||||
readonly SCRIPT_DIR="$(cd "$(dirname "$(readlink -f "$0")")" && pwd)"
|
||||
|
||||
REMOTE=""
|
||||
@@ -23,8 +22,11 @@ IMAGE_TAG=""
|
||||
SSH_KEY=""
|
||||
SSH_CONFIG=""
|
||||
PROJECT_DIR="${PROJECT_DIR:-${SCRIPT_DIR}/..}"
|
||||
APP_TYPE="java"
|
||||
DOCKERFILE=""
|
||||
SKIP_BUILD=0
|
||||
SKIP_VERIFY=0
|
||||
CLEANUP=0
|
||||
|
||||
# ---------- logging ----------
|
||||
if [[ -t 1 ]]; then
|
||||
@@ -56,32 +58,43 @@ usage() {
|
||||
Usage: $(basename "$0") --remote USER@HOST[:PORT] [options]
|
||||
|
||||
Build the native Quarkus image and push it to a remote Docker host using
|
||||
unregistry (docker-pussh).
|
||||
a temporary Docker registry on the remote, reached through an SSH tunnel.
|
||||
|
||||
Required:
|
||||
--remote TARGET SSH destination. Either 'user@host[:port]'
|
||||
or a bare alias resolved from ~/.ssh/config
|
||||
(e.g. 'prod'). See SSH config aliases below.
|
||||
|
||||
Image identity (defaults read from pom.xml):
|
||||
--name NAME Image repository name
|
||||
--tag TAG Image tag
|
||||
Image identity:
|
||||
--name NAME Image repository name (default: last directory of
|
||||
the service location, lowercased)
|
||||
--tag TAG Local build tag (pom.xml version by default, or
|
||||
1.0.0-SNAPSHOT if there is no pom.xml). On the
|
||||
remote only :latest and vYYYY.MM.DD-HH.MM (UTC).
|
||||
|
||||
Working directory (defaults to the parent of the script's directory):
|
||||
--project-dir PATH Project root containing pom.xml
|
||||
(override with env PROJECT_DIR)
|
||||
--project-dir PATH Service root. The image name is its last directory,
|
||||
lowercased (override with env PROJECT_DIR)
|
||||
|
||||
Build options:
|
||||
--skip-build Skip docker build, only pussh an existing image
|
||||
--type java|web Dockerfile location. java (default):
|
||||
src/main/docker/Dockerfile
|
||||
web: Dockerfile at the service root
|
||||
--skip-build Skip docker build, only push an existing image
|
||||
(platform is fixed to ${DEFAULT_PLATFORM})
|
||||
|
||||
SSH options forwarded to docker pussh:
|
||||
SSH options:
|
||||
--ssh-key PATH Private key file (overrides SSH config)
|
||||
--ssh-config PATH SSH config file (default: ~/.ssh/config)
|
||||
|
||||
Post-push:
|
||||
--no-verify Skip the ssh/docker images verification step
|
||||
|
||||
Maintenance:
|
||||
--cleanup Remove temporary registry leftovers on the remote,
|
||||
drop stale tags (version, sha256-*, etc.), and keep
|
||||
at most two app images (current + previous release)
|
||||
|
||||
Other:
|
||||
-h, --help Show this help
|
||||
|
||||
@@ -96,6 +109,8 @@ Examples:
|
||||
$(basename "$0") --remote prod # uses 'Host prod' in ~/.ssh/config
|
||||
$(basename "$0") --remote ubuntu@1.2.3.4 --ssh-key ~/.ssh/deploy_ed25519
|
||||
$(basename "$0") --remote deploy@prod --skip-build --tag 2.0.0
|
||||
$(basename "$0") --remote prod --type web
|
||||
$(basename "$0") --remote prod --cleanup
|
||||
/absolute/path/to/test-quarkus/scripts/$(basename "$0") --remote deploy@prod
|
||||
PROJECT_DIR=/srv/proj $(basename "$0") --remote deploy@prod --skip-build
|
||||
EOF
|
||||
@@ -104,16 +119,44 @@ EOF
|
||||
read_pom() {
|
||||
local xpath="$1" fallback="$2" tag="${3:-}"
|
||||
local value=""
|
||||
if [[ ! -f pom.xml ]]; then
|
||||
printf '%s' "$fallback"
|
||||
return 0
|
||||
fi
|
||||
if command -v xmllint >/dev/null 2>&1; then
|
||||
value=$(xmllint --xpath "string($xpath)" pom.xml 2>/dev/null || true)
|
||||
fi
|
||||
if [[ -z "$value" && -n "$tag" ]]; then
|
||||
value=$(grep -E "<${tag}>" pom.xml | head -1 \
|
||||
| sed -E "s:.*<${tag}>(.*)</${tag}>.*:\1:")
|
||||
| sed -E "s:.*<${tag}>(.*)</${tag}>.*:\1:" || true)
|
||||
fi
|
||||
printf '%s' "${value:-$fallback}"
|
||||
}
|
||||
|
||||
# java: src/main/docker/Dockerfile; web: Dockerfile at the service root.
|
||||
resolve_dockerfile() {
|
||||
case "$APP_TYPE" in
|
||||
java) DOCKERFILE="src/main/docker/Dockerfile" ;;
|
||||
web) DOCKERFILE="Dockerfile" ;;
|
||||
*) die "Unknown --type '$APP_TYPE' (use java or web)." ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Docker repository names must be lowercase. ${var,,} needs bash 4+.
|
||||
lowercase() {
|
||||
printf '%s' "$1" | tr '[:upper:]' '[:lower:]'
|
||||
}
|
||||
|
||||
# Last directory of the service location (cwd must already be that directory).
|
||||
image_name_from_location() {
|
||||
local dir
|
||||
dir="$(pwd)"
|
||||
dir="${dir%/}"
|
||||
[[ -n "$dir" && "$dir" != "/" ]] \
|
||||
|| die "Cannot derive image name from project directory."
|
||||
lowercase "${dir##*/}"
|
||||
}
|
||||
|
||||
require() {
|
||||
command -v "$1" >/dev/null 2>&1 || die "Required command '$1' not found in PATH."
|
||||
}
|
||||
@@ -178,34 +221,214 @@ resolve_ssh_alias() {
|
||||
printf '%s@%s:%s|%s\n' "$user" "$hostname" "$port" "$identity"
|
||||
}
|
||||
|
||||
random_port() {
|
||||
echo $((55000 + RANDOM % 10536))
|
||||
}
|
||||
|
||||
# REMOTE_TARGET is user@host:port; OpenSSH needs -p and user@host separately.
|
||||
parse_ssh_target() {
|
||||
if [[ "$REMOTE_TARGET" == *:* ]]; then
|
||||
SSH_PORT="${REMOTE_TARGET##*:}"
|
||||
SSH_HOST_USER="${REMOTE_TARGET%:*}"
|
||||
else
|
||||
SSH_PORT="22"
|
||||
SSH_HOST_USER="$REMOTE_TARGET"
|
||||
fi
|
||||
}
|
||||
|
||||
# Populates global ssh_args for ssh(1).
|
||||
build_ssh_args() {
|
||||
ssh_args=()
|
||||
[[ -n "$SSH_KEY" ]] && ssh_args+=(-i "$SSH_KEY")
|
||||
[[ -n "$SSH_CONFIG" ]] && ssh_args+=(-F "$SSH_CONFIG")
|
||||
[[ "$SSH_PORT" != "22" ]] && ssh_args+=(-p "$SSH_PORT")
|
||||
ssh_args+=("$SSH_HOST_USER")
|
||||
}
|
||||
|
||||
run_remote() {
|
||||
ssh "${ssh_args[@]}" "$@"
|
||||
}
|
||||
|
||||
# Sets REMOTE_DOCKER to "docker" or "sudo -n docker".
|
||||
detect_remote_docker() {
|
||||
if run_remote "docker info" >/dev/null 2>&1; then
|
||||
REMOTE_DOCKER="docker"
|
||||
return 0
|
||||
fi
|
||||
if run_remote "sudo -n docker info" >/dev/null 2>&1; then
|
||||
REMOTE_DOCKER="sudo -n docker"
|
||||
return 0
|
||||
fi
|
||||
die "Cannot run docker on ${REMOTE_TARGET}. Ensure Docker is installed and the SSH user may run docker (or passwordless sudo docker)."
|
||||
}
|
||||
|
||||
cleanup_remote_registry() {
|
||||
log "Cleaning temporary registry leftovers on ${REMOTE_TARGET}…"
|
||||
run_remote "${REMOTE_DOCKER} ps -aq --filter name=reg-push- | xargs -r ${REMOTE_DOCKER} rm -f" \
|
||||
>/dev/null 2>&1 || true
|
||||
run_remote "${REMOTE_DOCKER} rm -f regtest" >/dev/null 2>&1 || true
|
||||
run_remote "${REMOTE_DOCKER} images --format '{{.Repository}}:{{.Tag}}' \
|
||||
| grep -E '^localhost:[0-9]+/' | xargs -r ${REMOTE_DOCKER} rmi" \
|
||||
>/dev/null 2>&1 || true
|
||||
ok "Remote cleanup complete."
|
||||
}
|
||||
|
||||
# Keep :latest plus the two newest vYYYY.MM.DD-HH.MM releases; drop other tags/IDs.
|
||||
prune_remote_image_history() {
|
||||
local repo="${1:-$IMAGE_NAME}"
|
||||
[[ -n "$repo" ]] || return 0
|
||||
log "Pruning ${repo} on remote (keep latest and previous release)…"
|
||||
run_remote "bash -s" <<EOS
|
||||
set -euo pipefail
|
||||
RD='${REMOTE_DOCKER}'
|
||||
REPO='${repo}'
|
||||
release_re='^v[0-9]{4}\\.[0-9]{2}\\.[0-9]{2}-[0-9]{2}\\.[0-9]{2}\$'
|
||||
|
||||
rmi_ref() {
|
||||
\${RD} rmi "\$1" 2>/dev/null || true
|
||||
}
|
||||
|
||||
while IFS= read -r ref; do
|
||||
tag="\${ref#*:}"
|
||||
[[ "\$tag" == latest ]] && continue
|
||||
if [[ "\$tag" =~ \${release_re} ]]; then continue; fi
|
||||
rmi_ref "\$ref"
|
||||
done < <(\${RD} images --format '{{.Repository}}:{{.Tag}}' | grep -E "^\${REPO}:" || true)
|
||||
|
||||
mapfile -t release_refs < <(\${RD} images --format '{{.Repository}}:{{.Tag}}' | grep -E "^\${REPO}:v[0-9]{4}\\." | sort -t: -k2)
|
||||
declare -A keep_ids=()
|
||||
n=\${#release_refs[@]}
|
||||
if (( n == 0 )); then exit 0; fi
|
||||
for (( i=n-1; i>=0 && i>=n-2; i-- )); do
|
||||
id=\$(\${RD} images --format '{{.ID}}' "\${release_refs[i]}" | head -1)
|
||||
[[ -n "\$id" ]] && keep_ids["\$id"]=1
|
||||
done
|
||||
|
||||
while IFS= read -r ref; do
|
||||
id=\$(\${RD} images --format '{{.ID}}' "\$ref" | head -1)
|
||||
[[ -n "\${keep_ids[\$id]:-}" ]] && continue
|
||||
rmi_ref "\$ref"
|
||||
done < <(\${RD} images --format '{{.Repository}}:{{.Tag}}' | grep -E "^\${REPO}:" || true)
|
||||
EOS
|
||||
ok "Remote ${repo} pruned."
|
||||
}
|
||||
|
||||
REG_PUSH_CONTAINER=""
|
||||
REG_REMOTE_PORT=""
|
||||
REG_LOCAL_PORT=""
|
||||
TUNNEL_PID=""
|
||||
LOCAL_REGISTRY_REF=""
|
||||
RELEASE_REF=""
|
||||
|
||||
cleanup_push_resources() {
|
||||
[[ -n "${LOCAL_REGISTRY_REF:-}" ]] \
|
||||
&& docker rmi "$LOCAL_REGISTRY_REF" >/dev/null 2>&1 || true
|
||||
if [[ -n "${TUNNEL_PID:-}" ]]; then
|
||||
kill "$TUNNEL_PID" >/dev/null 2>&1 || true
|
||||
wait "$TUNNEL_PID" 2>/dev/null || true
|
||||
fi
|
||||
if [[ -n "${REG_PUSH_CONTAINER:-}" && -n "${REMOTE_DOCKER:-}" && -n "${REMOTE_TARGET:-}" ]]; then
|
||||
parse_ssh_target
|
||||
build_ssh_args
|
||||
run_remote "${REMOTE_DOCKER} rm -f ${REG_PUSH_CONTAINER}" >/dev/null 2>&1 || true
|
||||
fi
|
||||
}
|
||||
|
||||
start_remote_registry() {
|
||||
local attempt output
|
||||
run_remote "${REMOTE_DOCKER} image inspect ${TEMP_REGISTRY_IMAGE}" >/dev/null 2>&1 \
|
||||
|| run_remote "${REMOTE_DOCKER} pull ${TEMP_REGISTRY_IMAGE}"
|
||||
|
||||
for attempt in $(seq 1 10); do
|
||||
REG_REMOTE_PORT=$(random_port)
|
||||
REG_PUSH_CONTAINER="reg-push-$$-${REG_REMOTE_PORT}"
|
||||
if output=$(run_remote "${REMOTE_DOCKER} run -d \
|
||||
--name ${REG_PUSH_CONTAINER} \
|
||||
-p 127.0.0.1:${REG_REMOTE_PORT}:5000 \
|
||||
${TEMP_REGISTRY_IMAGE}" 2>&1); then
|
||||
return 0
|
||||
fi
|
||||
run_remote "${REMOTE_DOCKER} rm -f ${REG_PUSH_CONTAINER}" >/dev/null 2>&1 || true
|
||||
if ! echo "$output" | grep -qi bind; then
|
||||
die "Failed to start temporary registry on remote:\n${output}"
|
||||
fi
|
||||
done
|
||||
die "Failed to start temporary registry on remote (port binding)."
|
||||
}
|
||||
|
||||
start_ssh_tunnel() {
|
||||
local attempt
|
||||
for attempt in $(seq 1 10); do
|
||||
REG_LOCAL_PORT=$(random_port)
|
||||
if command -v nc >/dev/null 2>&1 && nc -z 127.0.0.1 "$REG_LOCAL_PORT" 2>/dev/null; then
|
||||
continue
|
||||
fi
|
||||
ssh "${ssh_args[@]}" \
|
||||
-o ExitOnForwardFailure=yes \
|
||||
-N \
|
||||
-L "127.0.0.1:${REG_LOCAL_PORT}:127.0.0.1:${REG_REMOTE_PORT}" &
|
||||
TUNNEL_PID=$!
|
||||
sleep 1
|
||||
if kill -0 "$TUNNEL_PID" 2>/dev/null; then
|
||||
return 0
|
||||
fi
|
||||
TUNNEL_PID=""
|
||||
done
|
||||
die "Failed to establish SSH tunnel to remote registry."
|
||||
}
|
||||
|
||||
build_release_tag() {
|
||||
printf 'v%s' "$(TZ=UTC date +%Y.%m.%d-%H.%M)"
|
||||
}
|
||||
|
||||
push_via_registry_tunnel() {
|
||||
LOCAL_REGISTRY_REF="localhost:${REG_LOCAL_PORT}/${IMAGE_NAME}:${IMAGE_TAG}"
|
||||
local remote_registry_ref="localhost:${REG_REMOTE_PORT}/${IMAGE_NAME}:${IMAGE_TAG}"
|
||||
local latest_ref="${IMAGE_NAME}:latest"
|
||||
local release_tag
|
||||
|
||||
release_tag=$(build_release_tag)
|
||||
RELEASE_REF="${IMAGE_NAME}:${release_tag}"
|
||||
|
||||
log "Tagging ${IMAGE_REF} as ${LOCAL_REGISTRY_REF}…"
|
||||
docker tag "$IMAGE_REF" "$LOCAL_REGISTRY_REF"
|
||||
|
||||
log "Pushing ${LOCAL_REGISTRY_REF} through SSH tunnel…"
|
||||
docker push "$LOCAL_REGISTRY_REF"
|
||||
|
||||
log "Tagging local image as ${latest_ref} and ${RELEASE_REF}…"
|
||||
docker tag "$IMAGE_REF" "$latest_ref"
|
||||
docker tag "$IMAGE_REF" "$RELEASE_REF"
|
||||
|
||||
log "Pulling image on remote and tagging ${latest_ref}, ${RELEASE_REF}…"
|
||||
run_remote "${REMOTE_DOCKER} pull ${remote_registry_ref}"
|
||||
run_remote "${REMOTE_DOCKER} tag ${remote_registry_ref} ${latest_ref}"
|
||||
run_remote "${REMOTE_DOCKER} tag ${remote_registry_ref} ${RELEASE_REF}"
|
||||
run_remote "${REMOTE_DOCKER} rmi ${remote_registry_ref}" >/dev/null || true
|
||||
prune_remote_image_history "${IMAGE_NAME}"
|
||||
ok "Remote tags: ${latest_ref}, ${RELEASE_REF}."
|
||||
}
|
||||
|
||||
# ---------- arg parsing ----------
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--remote) REMOTE="$2"; shift 2 ;;
|
||||
--name) IMAGE_NAME="$2"; shift 2 ;;
|
||||
--name) IMAGE_NAME="$(lowercase "$2")"; shift 2 ;;
|
||||
--tag) IMAGE_TAG="$2"; shift 2 ;;
|
||||
--ssh-key) SSH_KEY="$2"; shift 2 ;;
|
||||
--ssh-config) SSH_CONFIG="$2"; shift 2 ;;
|
||||
--project-dir) PROJECT_DIR="$2"; shift 2 ;;
|
||||
--type) APP_TYPE="$2"; shift 2 ;;
|
||||
--skip-build) SKIP_BUILD=1; shift ;;
|
||||
--no-verify) SKIP_VERIFY=1; shift ;;
|
||||
--cleanup) CLEANUP=1; shift ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) die "Unknown argument: $1 (use --help)" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# ---------- resolve project root ----------
|
||||
if ! cd "${PROJECT_DIR}" 2>/dev/null; then
|
||||
die "Invalid --project-dir: ${PROJECT_DIR}"
|
||||
fi
|
||||
PROJECT_DIR="$(pwd)"
|
||||
|
||||
# ---------- defaults ----------
|
||||
[[ -z "$IMAGE_NAME" ]] && IMAGE_NAME=$(read_pom "/*[local-name()='project']/*[local-name()='artifactId']" "test-quarkus" "artifactId")
|
||||
[[ -z "$IMAGE_TAG" ]] && IMAGE_TAG=$(read_pom "/*[local-name()='project']/*[local-name()='version']" "1.0.0-SNAPSHOT" "version")
|
||||
[[ -z "$REMOTE" ]] && die "Missing --remote USER@HOST[:PORT] (use --help)."
|
||||
|
||||
IMAGE_REF="${IMAGE_NAME}:${IMAGE_TAG}"
|
||||
resolve_dockerfile
|
||||
|
||||
# ---------- resolve SSH target ----------
|
||||
resolved=$(resolve_ssh_alias "$REMOTE")
|
||||
@@ -219,6 +442,34 @@ if [[ -z "$SSH_KEY" && -n "$REMOTE_IDENTITY" ]]; then
|
||||
log "Using IdentityFile from SSH config: ${SSH_KEY}"
|
||||
fi
|
||||
|
||||
# ---------- remote cleanup only ----------
|
||||
if [[ "$CLEANUP" -eq 1 ]]; then
|
||||
log "Remote: ${REMOTE_TARGET}"
|
||||
require ssh
|
||||
if ! cd "${PROJECT_DIR}" 2>/dev/null; then
|
||||
die "Invalid --project-dir: ${PROJECT_DIR}"
|
||||
fi
|
||||
[[ -z "$IMAGE_NAME" ]] && IMAGE_NAME="$(image_name_from_location)"
|
||||
parse_ssh_target
|
||||
build_ssh_args
|
||||
detect_remote_docker
|
||||
cleanup_remote_registry
|
||||
prune_remote_image_history "${IMAGE_NAME}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ---------- resolve project root ----------
|
||||
if ! cd "${PROJECT_DIR}" 2>/dev/null; then
|
||||
die "Invalid --project-dir: ${PROJECT_DIR}"
|
||||
fi
|
||||
PROJECT_DIR="$(pwd)"
|
||||
|
||||
# ---------- defaults ----------
|
||||
[[ -z "$IMAGE_NAME" ]] && IMAGE_NAME="$(image_name_from_location)"
|
||||
[[ -z "$IMAGE_TAG" ]] && IMAGE_TAG=$(read_pom "/*[local-name()='project']/*[local-name()='version']" "1.0.0-SNAPSHOT" "version")
|
||||
|
||||
IMAGE_REF="${IMAGE_NAME}:${IMAGE_TAG}"
|
||||
|
||||
# ---------- pre-flight ----------
|
||||
log "Image: ${IMAGE_REF}"
|
||||
log "Platform: ${DEFAULT_PLATFORM}"
|
||||
@@ -226,25 +477,15 @@ log "Dockerfile: ${DOCKERFILE}"
|
||||
log "Remote: ${REMOTE_TARGET}"
|
||||
|
||||
require docker
|
||||
require ssh
|
||||
docker info >/dev/null 2>&1 \
|
||||
|| die "Docker daemon is not reachable. Start Docker Desktop (or your daemon) and retry."
|
||||
|
||||
if ! docker pussh --help >/dev/null 2>&1; then
|
||||
cat >&2 <<EOF
|
||||
|
||||
'docker pussh' plugin not found. Install it with:
|
||||
|
||||
mkdir -p ${PLUGIN_DIR}
|
||||
curl -fsSL ${UNREGISTRY_PLUGIN} -o ${PLUGIN_PATH}
|
||||
chmod +x ${PLUGIN_PATH}
|
||||
|
||||
Then rerun this script.
|
||||
EOF
|
||||
exit 1
|
||||
fi
|
||||
|
||||
[[ -f "$DOCKERFILE" ]] || die "Dockerfile not found at ${DOCKERFILE}."
|
||||
[[ -f pom.xml ]] || die "pom.xml not found in current directory."
|
||||
|
||||
parse_ssh_target
|
||||
build_ssh_args
|
||||
detect_remote_docker
|
||||
|
||||
# ---------- build ----------
|
||||
if [[ "$SKIP_BUILD" -eq 1 ]]; then
|
||||
@@ -261,32 +502,40 @@ else
|
||||
ok "Build finished."
|
||||
fi
|
||||
|
||||
# ---------- pussh ----------
|
||||
log "Pusshing ${IMAGE_REF} to ${REMOTE_TARGET} via unregistry…"
|
||||
# ---------- push via temporary registry ----------
|
||||
trap cleanup_push_resources EXIT
|
||||
|
||||
pussh_args=( "$REMOTE_TARGET" )
|
||||
[[ -n "$SSH_KEY" ]] && pussh_args+=( -i "$SSH_KEY" )
|
||||
[[ -n "$SSH_CONFIG" ]] && pussh_args+=( -F "$SSH_CONFIG" )
|
||||
log "Pushing ${IMAGE_REF} to ${REMOTE_TARGET} via temporary registry…"
|
||||
|
||||
docker pussh "${IMAGE_REF}" "${pussh_args[@]}"
|
||||
ok "Pussh complete."
|
||||
start_remote_registry
|
||||
ok "Temporary registry listening on remote 127.0.0.1:${REG_REMOTE_PORT}."
|
||||
|
||||
start_ssh_tunnel
|
||||
ok "SSH tunnel localhost:${REG_LOCAL_PORT} → remote:${REG_REMOTE_PORT}."
|
||||
|
||||
push_via_registry_tunnel
|
||||
ok "Push complete."
|
||||
|
||||
# ---------- verify ----------
|
||||
if [[ "$SKIP_VERIFY" -eq 1 ]]; then
|
||||
warn "Skipping post-push verification (--no-verify)."
|
||||
else
|
||||
log "Verifying ${IMAGE_REF} on ${REMOTE_TARGET}…"
|
||||
ssh_args=( "$REMOTE_TARGET" )
|
||||
[[ -n "$SSH_KEY" ]] && ssh_args=( -i "$SSH_KEY" "${ssh_args[@]}" )
|
||||
[[ -n "$SSH_CONFIG" ]] && ssh_args=( -F "$SSH_CONFIG" "${ssh_args[@]}" )
|
||||
|
||||
if ! ssh "${ssh_args[@]}" "docker images --format '{{.Repository}}:{{.Tag}} {{.Size}}' | grep -E '^${IMAGE_NAME}:${IMAGE_TAG} '"; then
|
||||
die "Image ${IMAGE_REF} not found on ${REMOTE_TARGET} after pussh."
|
||||
log "Verifying tags on ${REMOTE_TARGET}…"
|
||||
remote_images=$(run_remote "${REMOTE_DOCKER} images --format '{{.Repository}}:{{.Tag}}'")
|
||||
missing=0
|
||||
for ref in "${IMAGE_NAME}:latest" "${RELEASE_REF}"; do
|
||||
if ! printf '%s\n' "$remote_images" | grep -Fxq "$ref"; then
|
||||
err "Missing on remote: ${ref}"
|
||||
missing=1
|
||||
fi
|
||||
ok "Image present on remote."
|
||||
done
|
||||
[[ "$missing" -eq 0 ]] || die "Expected tags were not found on ${REMOTE_TARGET} after push."
|
||||
ok "Remote has ${IMAGE_NAME}:latest and ${RELEASE_REF}."
|
||||
fi
|
||||
|
||||
# ---------- next step hint ----------
|
||||
printf '\n%s→ To run on the remote:%s\n' "$C_OK" "$C_RESET"
|
||||
printf ' ssh %s "docker run -d --name %s --restart unless-stopped -p 8080:8080 %s"\n\n' \
|
||||
"$REMOTE_TARGET" "$IMAGE_NAME" "$IMAGE_REF"
|
||||
printf '\n%s→ To run the latest image on the remote:%s\n' "$C_OK" "$C_RESET"
|
||||
printf ' ssh %s "docker run -d --name %s --restart unless-stopped -p 8080:8080 %s:latest"\n' \
|
||||
"$REMOTE_TARGET" "$IMAGE_NAME" "$IMAGE_NAME"
|
||||
printf '%s→ Rollback: list tags with docker images %s, then run a previous vYYYY.MM.DD-HH.MM tag.%s\n\n' \
|
||||
"$C_OK" "$IMAGE_NAME" "$C_RESET"
|
||||
|
||||
Executable
+334
@@ -0,0 +1,334 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Run scripts/build-and-push.sh once per service listed in a project file.
|
||||
# Continues after a failure and prints a summary when the batch finishes.
|
||||
# That summary is also written to deploy-YYYYMMDD-HHMMSS.log in the directory
|
||||
# from which the script was launched.
|
||||
#
|
||||
# The list path is required (--services). Relative service paths are resolved
|
||||
# against the directory that contains that file, not the caller's cwd.
|
||||
# An optional second field, java or web, selects the Dockerfile location.
|
||||
|
||||
set -euo pipefail
|
||||
IFS=$'\n\t'
|
||||
|
||||
readonly SCRIPT_DIR="$(cd "$(dirname "$(readlink -f "$0")")" && pwd)"
|
||||
readonly BUILD_SCRIPT="${SCRIPT_DIR}/build-and-push.sh"
|
||||
|
||||
SERVICES_FILE=""
|
||||
REMOTE=""
|
||||
forward_args=()
|
||||
|
||||
# ---------- logging ----------
|
||||
if [[ -t 1 ]]; then
|
||||
readonly C_INFO=$'\033[0;36m'
|
||||
readonly C_OK=$'\033[0;32m'
|
||||
readonly C_WARN=$'\033[0;33m'
|
||||
readonly C_ERR=$'\033[0;31m'
|
||||
readonly C_RESET=$'\033[0m'
|
||||
else
|
||||
readonly C_INFO="" C_OK="" C_WARN="" C_ERR="" C_RESET=""
|
||||
fi
|
||||
|
||||
log() { printf '%s[INFO]%s %s\n' "$C_INFO" "$C_RESET" "$*"; }
|
||||
ok() { printf '%s[OK]%s %s\n' "$C_OK" "$C_RESET" "$*"; }
|
||||
warn() { printf '%s[WARN]%s %s\n' "$C_WARN" "$C_RESET" "$*" >&2; }
|
||||
err() { printf '%s[ERROR]%s %s\n' "$C_ERR" "$C_RESET" "$*" >&2; }
|
||||
die() { err "$*"; exit 1; }
|
||||
|
||||
on_error() {
|
||||
local exit_code=$?
|
||||
err "Script aborted on line ${BASH_LINENO[0]} (exit ${exit_code})."
|
||||
exit "$exit_code"
|
||||
}
|
||||
trap on_error ERR
|
||||
|
||||
# ---------- helpers ----------
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $(basename "$0") --remote TARGET --services FILE [options]
|
||||
|
||||
Run build-and-push.sh once for every service listed in FILE. One list file
|
||||
per project: pass a different --services path to deploy another project.
|
||||
A failure does not stop the batch. The exit code is 1 if any service failed.
|
||||
When the batch finishes, that summary is written to deploy-YYYYMMDD-HHMMSS.log
|
||||
in the current directory (local start time). If that name already exists, the
|
||||
process id is appended.
|
||||
|
||||
Required:
|
||||
--remote TARGET SSH destination forwarded to build-and-push.sh
|
||||
('user@host[:port]' or a ~/.ssh/config alias)
|
||||
--services FILE Service list for one project. One path per line,
|
||||
with an optional second field: java or web
|
||||
(default java). Blank lines and lines starting
|
||||
with '#' are ignored. Relative paths resolve
|
||||
against the directory that contains FILE.
|
||||
|
||||
Forwarded to each service (build-and-push.sh):
|
||||
--ssh-key PATH Private key file
|
||||
--ssh-config PATH SSH config file
|
||||
--skip-build Push an existing local image
|
||||
--no-verify Skip the remote image check
|
||||
--cleanup Prune the remote without building or pushing
|
||||
|
||||
Not accepted (each service has its own directory; --type comes from the
|
||||
list, and --project-dir is set from that path):
|
||||
--name, --tag, --project-dir, --type
|
||||
|
||||
Other:
|
||||
-h, --help Show this help
|
||||
|
||||
Examples:
|
||||
$(basename "$0") --remote prod --services /srv/proyecto-a/services.txt
|
||||
$(basename "$0") --remote prod --services /srv/proyecto-b/services.txt --skip-build
|
||||
EOF
|
||||
}
|
||||
|
||||
trim() {
|
||||
local value="$1"
|
||||
value="${value#"${value%%[![:space:]]*}"}"
|
||||
value="${value%"${value##*[![:space:]]}"}"
|
||||
printf '%s' "$value"
|
||||
}
|
||||
|
||||
require_value() {
|
||||
[[ $# -ge 2 && -n "${2:-}" && "$2" != -* ]] \
|
||||
|| die "Missing value for $1"
|
||||
}
|
||||
|
||||
# ---------- arg parsing ----------
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--services)
|
||||
require_value "$@"
|
||||
SERVICES_FILE="$2"
|
||||
shift 2
|
||||
;;
|
||||
--remote)
|
||||
require_value "$@"
|
||||
REMOTE="$2"
|
||||
shift 2
|
||||
;;
|
||||
--ssh-key|--ssh-config)
|
||||
require_value "$@"
|
||||
forward_args+=("$1" "$2")
|
||||
shift 2
|
||||
;;
|
||||
--skip-build|--no-verify|--cleanup)
|
||||
forward_args+=("$1")
|
||||
shift
|
||||
;;
|
||||
--name|--tag|--project-dir|--type)
|
||||
die "$1 is not accepted. Each service uses its own directory; --type comes from the services file."
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
die "Unknown argument: $1 (use --help)"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
[[ -n "$REMOTE" ]] || die "Missing --remote TARGET (use --help)."
|
||||
[[ -n "$SERVICES_FILE" ]] || die "Missing --services FILE (use --help)."
|
||||
[[ -f "$BUILD_SCRIPT" ]] || die "build-and-push.sh not found next to this script: ${BUILD_SCRIPT}"
|
||||
[[ -f "$SERVICES_FILE" ]] || die "Services file not found: ${SERVICES_FILE}"
|
||||
|
||||
SERVICES_FILE="$(readlink -f "$SERVICES_FILE")"
|
||||
SERVICES_DIR="$(cd "$(dirname "$SERVICES_FILE")" && pwd)"
|
||||
|
||||
service_paths=()
|
||||
service_types=()
|
||||
while IFS= read -r raw_line || [[ -n "$raw_line" ]]; do
|
||||
line="$(trim "$raw_line")"
|
||||
[[ -z "$line" || "$line" == \#* ]] && continue
|
||||
svc_path=""
|
||||
svc_type=""
|
||||
svc_extra=""
|
||||
IFS=$' \t' read -r svc_path svc_type svc_extra <<< "$line"
|
||||
[[ -n "$svc_extra" ]] && die "Service line has an extra field: ${line}"
|
||||
[[ -n "$svc_path" ]] || die "Service line is missing a path: ${line}"
|
||||
svc_path="${svc_path/#\~/$HOME}"
|
||||
[[ -z "$svc_type" ]] && svc_type="java"
|
||||
case "$svc_type" in
|
||||
java|web) ;;
|
||||
*) die "Unknown service type '${svc_type}' in ${SERVICES_FILE} (use java or web)." ;;
|
||||
esac
|
||||
service_paths+=("$svc_path")
|
||||
service_types+=("$svc_type")
|
||||
done < "$SERVICES_FILE"
|
||||
|
||||
[[ ${#service_paths[@]} -gt 0 ]] || die "Services file is empty: ${SERVICES_FILE}"
|
||||
|
||||
run_build() {
|
||||
local project_dir="$1"
|
||||
local app_type="$2"
|
||||
if [[ ${#forward_args[@]} -eq 0 ]]; then
|
||||
"$BUILD_SCRIPT" --project-dir "$project_dir" --remote "$REMOTE" --type "$app_type"
|
||||
else
|
||||
"$BUILD_SCRIPT" --project-dir "$project_dir" --remote "$REMOTE" --type "$app_type" "${forward_args[@]}"
|
||||
fi
|
||||
}
|
||||
|
||||
resolve_service() {
|
||||
local entry="$1"
|
||||
if [[ "$entry" == /* ]]; then
|
||||
printf '%s' "$entry"
|
||||
else
|
||||
printf '%s' "${SERVICES_DIR}/${entry}"
|
||||
fi
|
||||
}
|
||||
|
||||
# Collapse "." and ".." without requiring the path to exist.
|
||||
normalize_path() {
|
||||
local path="$1"
|
||||
local part joined=""
|
||||
local -a stack=()
|
||||
local old_ifs="$IFS"
|
||||
set -f
|
||||
IFS=/
|
||||
# shellcheck disable=SC2206
|
||||
local -a parts=($path)
|
||||
IFS="$old_ifs"
|
||||
set +f
|
||||
for part in "${parts[@]}"; do
|
||||
case "$part" in
|
||||
""|.) ;;
|
||||
..)
|
||||
if [[ ${#stack[@]} -gt 0 ]]; then
|
||||
unset 'stack[${#stack[@]}-1]'
|
||||
fi
|
||||
;;
|
||||
*) stack+=("$part") ;;
|
||||
esac
|
||||
done
|
||||
if [[ ${#stack[@]} -eq 0 ]]; then
|
||||
printf '/'
|
||||
return
|
||||
fi
|
||||
for part in "${stack[@]}"; do
|
||||
joined+="/${part}"
|
||||
done
|
||||
printf '%s' "$joined"
|
||||
}
|
||||
|
||||
# Plain-text summary in the directory from which the batch was launched.
|
||||
# A write failure is reported and does not change the batch exit code.
|
||||
write_summary_log() {
|
||||
local finished="" summary_index=0 note="" path="" status=""
|
||||
local -a lines=()
|
||||
|
||||
finished="$(date '+%Y-%m-%d %H:%M:%S %z')" || {
|
||||
warn "Could not write log file: ${log_path}"
|
||||
return 0
|
||||
}
|
||||
|
||||
lines+=("started: ${run_started}")
|
||||
lines+=("finished: ${finished}")
|
||||
lines+=("remote: ${REMOTE}")
|
||||
lines+=("services_file: ${SERVICES_FILE}")
|
||||
lines+=("")
|
||||
|
||||
while [[ "$summary_index" -lt "$total" ]]; do
|
||||
note="${result_notes[$summary_index]}"
|
||||
path="${result_paths[$summary_index]}"
|
||||
if [[ "$note" == "0" ]]; then
|
||||
status="ok"
|
||||
else
|
||||
status="$note"
|
||||
fi
|
||||
lines+=("${path} ${status}")
|
||||
summary_index=$((summary_index + 1))
|
||||
done
|
||||
|
||||
lines+=("")
|
||||
lines+=("succeeded: ${ok_count}")
|
||||
lines+=("failed: ${fail_count}")
|
||||
|
||||
if ! printf '%s\n' "${lines[@]}" >"$log_path"; then
|
||||
warn "Could not write log file: ${log_path}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log "Log file: ${log_path}"
|
||||
}
|
||||
|
||||
total="${#service_paths[@]}"
|
||||
index=0
|
||||
ok_count=0
|
||||
fail_count=0
|
||||
result_paths=()
|
||||
result_notes=()
|
||||
launch_dir="$(pwd)"
|
||||
run_started="$(date '+%Y-%m-%d %H:%M:%S %z')"
|
||||
log_stamp="$(date '+%Y%m%d-%H%M%S')"
|
||||
log_path="${launch_dir}/deploy-${log_stamp}.log"
|
||||
if [[ -e "$log_path" ]]; then
|
||||
log_path="${launch_dir}/deploy-${log_stamp}-$$.log"
|
||||
fi
|
||||
|
||||
log "Services file: ${SERVICES_FILE}"
|
||||
log "Remote: ${REMOTE}"
|
||||
log "Services: ${total}"
|
||||
|
||||
while [[ "$index" -lt "$total" ]]; do
|
||||
entry="${service_paths[$index]}"
|
||||
app_type="${service_types[$index]}"
|
||||
index=$((index + 1))
|
||||
resolved="$(normalize_path "$(resolve_service "$entry")")"
|
||||
|
||||
if [[ -d "$resolved" ]]; then
|
||||
resolved="$(cd "$resolved" && pwd)"
|
||||
fi
|
||||
|
||||
printf '\n%s========== [%s/%s] %s ==========%s\n' \
|
||||
"$C_INFO" "$index" "$total" "$resolved" "$C_RESET"
|
||||
|
||||
if [[ ! -d "$resolved" ]]; then
|
||||
err "Service directory not found: ${resolved}"
|
||||
result_paths+=("$resolved")
|
||||
result_notes+=("not found")
|
||||
fail_count=$((fail_count + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
exit_code=0
|
||||
if run_build "$resolved" "$app_type"; then
|
||||
ok "Finished ${resolved}"
|
||||
result_paths+=("$resolved")
|
||||
result_notes+=("0")
|
||||
ok_count=$((ok_count + 1))
|
||||
else
|
||||
exit_code=$?
|
||||
err "Failed ${resolved} (exit ${exit_code})"
|
||||
result_paths+=("$resolved")
|
||||
result_notes+=("exit ${exit_code}")
|
||||
fail_count=$((fail_count + 1))
|
||||
fi
|
||||
done
|
||||
|
||||
printf '\n%s========== Summary ==========%s\n' "$C_INFO" "$C_RESET"
|
||||
summary_index=0
|
||||
while [[ "$summary_index" -lt "$total" ]]; do
|
||||
note="${result_notes[$summary_index]}"
|
||||
path="${result_paths[$summary_index]}"
|
||||
if [[ "$note" == "0" ]]; then
|
||||
ok "${path}"
|
||||
else
|
||||
err "${path} (${note})"
|
||||
fi
|
||||
summary_index=$((summary_index + 1))
|
||||
done
|
||||
|
||||
batch_exit=0
|
||||
if [[ "$fail_count" -gt 0 ]]; then
|
||||
err "${ok_count} succeeded, ${fail_count} failed."
|
||||
batch_exit=1
|
||||
else
|
||||
ok "${ok_count} succeeded, 0 failed."
|
||||
fi
|
||||
|
||||
write_summary_log
|
||||
exit "$batch_exit"
|
||||
Executable
+290
@@ -0,0 +1,290 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Update one git repository per line in a list file.
|
||||
# Prompts once for a username and password and uses them for HTTPS remotes.
|
||||
# Continues after a failure and prints a summary when the batch finishes.
|
||||
#
|
||||
# The list path is required (--repos). Relative paths are resolved against
|
||||
# the directory that contains that file, not the caller's cwd.
|
||||
|
||||
set -euo pipefail
|
||||
IFS=$'\n\t'
|
||||
|
||||
REPOS_FILE=""
|
||||
GIT_USER=""
|
||||
GIT_PASS=""
|
||||
ASKPASS_FILE=""
|
||||
|
||||
# ---------- logging ----------
|
||||
if [[ -t 1 ]]; then
|
||||
readonly C_INFO=$'\033[0;36m'
|
||||
readonly C_OK=$'\033[0;32m'
|
||||
readonly C_WARN=$'\033[0;33m'
|
||||
readonly C_ERR=$'\033[0;31m'
|
||||
readonly C_RESET=$'\033[0m'
|
||||
else
|
||||
readonly C_INFO="" C_OK="" C_WARN="" C_ERR="" C_RESET=""
|
||||
fi
|
||||
|
||||
log() { printf '%s[INFO]%s %s\n' "$C_INFO" "$C_RESET" "$*"; }
|
||||
ok() { printf '%s[OK]%s %s\n' "$C_OK" "$C_RESET" "$*"; }
|
||||
warn() { printf '%s[WARN]%s %s\n' "$C_WARN" "$C_RESET" "$*" >&2; }
|
||||
err() { printf '%s[ERROR]%s %s\n' "$C_ERR" "$C_RESET" "$*" >&2; }
|
||||
die() { err "$*"; exit 1; }
|
||||
|
||||
cleanup() {
|
||||
GIT_USER=""
|
||||
GIT_PASS=""
|
||||
unset GIT_USER GIT_PASS
|
||||
if [[ -n "${ASKPASS_FILE:-}" && -f "$ASKPASS_FILE" ]]; then
|
||||
rm -f "$ASKPASS_FILE"
|
||||
fi
|
||||
ASKPASS_FILE=""
|
||||
}
|
||||
|
||||
on_error() {
|
||||
local exit_code=$?
|
||||
err "Script aborted on line ${BASH_LINENO[0]} (exit ${exit_code})."
|
||||
exit "$exit_code"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
trap on_error ERR
|
||||
|
||||
# ---------- helpers ----------
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $(basename "$0") --repos FILE
|
||||
|
||||
Run git pull once for every repository listed in FILE. A failure does not
|
||||
stop the batch. The exit code is 1 if any repository failed.
|
||||
|
||||
The script asks for a username and password on the terminal and uses them
|
||||
for HTTPS remotes. They are not read from the command line or the list file.
|
||||
|
||||
Required:
|
||||
--repos FILE Repository list. One path per line. Blank lines
|
||||
and lines starting with '#' are ignored. Relative
|
||||
paths resolve against the directory that contains
|
||||
FILE.
|
||||
|
||||
Other:
|
||||
-h, --help Show this help
|
||||
|
||||
Examples:
|
||||
$(basename "$0") --repos /srv/proyecto/repos.txt
|
||||
$(basename "$0") --repos ./repos.txt
|
||||
EOF
|
||||
}
|
||||
|
||||
trim() {
|
||||
local value="$1"
|
||||
value="${value#"${value%%[![:space:]]*}"}"
|
||||
value="${value%"${value##*[![:space:]]}"}"
|
||||
value="${value%$'\r'}"
|
||||
printf '%s' "$value"
|
||||
}
|
||||
|
||||
require_value() {
|
||||
[[ $# -ge 2 && -n "${2:-}" && "$2" != -* ]] \
|
||||
|| die "Missing value for $1"
|
||||
}
|
||||
|
||||
resolve_repo() {
|
||||
local entry="$1"
|
||||
if [[ "$entry" == /* ]]; then
|
||||
printf '%s' "$entry"
|
||||
else
|
||||
printf '%s' "${REPOS_DIR}/${entry}"
|
||||
fi
|
||||
}
|
||||
|
||||
# Collapse "." and ".." without requiring the path to exist.
|
||||
normalize_path() {
|
||||
local path="$1"
|
||||
local part joined=""
|
||||
local -a stack=()
|
||||
local old_ifs="$IFS"
|
||||
set -f
|
||||
IFS=/
|
||||
# shellcheck disable=SC2206
|
||||
local -a parts=($path)
|
||||
IFS="$old_ifs"
|
||||
set +f
|
||||
for part in "${parts[@]}"; do
|
||||
case "$part" in
|
||||
""|.) ;;
|
||||
..)
|
||||
if [[ ${#stack[@]} -gt 0 ]]; then
|
||||
unset 'stack[${#stack[@]}-1]'
|
||||
fi
|
||||
;;
|
||||
*) stack+=("$part") ;;
|
||||
esac
|
||||
done
|
||||
if [[ ${#stack[@]} -eq 0 ]]; then
|
||||
printf '/'
|
||||
return
|
||||
fi
|
||||
for part in "${stack[@]}"; do
|
||||
joined+="/${part}"
|
||||
done
|
||||
printf '%s' "$joined"
|
||||
}
|
||||
|
||||
prompt_credentials() {
|
||||
local user="" pass=""
|
||||
[[ -r /dev/tty && -w /dev/tty ]] \
|
||||
|| die "A terminal is required to enter the username and password."
|
||||
read -r -p "Usuario: " user </dev/tty
|
||||
read -r -s -p "Contraseña: " pass </dev/tty
|
||||
printf '\n' >/dev/tty
|
||||
[[ -n "$user" && -n "$pass" ]] || die "Username and password are required."
|
||||
GIT_USER="$user"
|
||||
GIT_PASS="$pass"
|
||||
}
|
||||
|
||||
write_askpass() {
|
||||
ASKPASS_FILE="$(mktemp "${TMPDIR:-/tmp}/git-update-askpass.XXXXXX")"
|
||||
cat >"$ASKPASS_FILE" <<'EOF'
|
||||
#!/bin/sh
|
||||
prompt=$(printf '%s' "${1:-}" | tr '[:upper:]' '[:lower:]')
|
||||
case "$prompt" in
|
||||
*username*) printf '%s\n' "${GIT_UPDATE_USER:-}" ;;
|
||||
*password*) printf '%s\n' "${GIT_UPDATE_PASS:-}" ;;
|
||||
*) exit 1 ;;
|
||||
esac
|
||||
EOF
|
||||
chmod 700 "$ASKPASS_FILE"
|
||||
}
|
||||
|
||||
remote_uses_http() {
|
||||
local repo="$1"
|
||||
local upstream="" remote="" url=""
|
||||
upstream="$(git -C "$repo" rev-parse --abbrev-ref --symbolic-full-name '@{upstream}' 2>/dev/null || true)"
|
||||
[[ -n "$upstream" && "$upstream" == */* ]] || return 1
|
||||
remote="${upstream%%/*}"
|
||||
url="$(git -C "$repo" remote get-url "$remote" 2>/dev/null || true)"
|
||||
[[ "$url" == http://* || "$url" == https://* ]]
|
||||
}
|
||||
|
||||
pull_repo() {
|
||||
local repo="$1"
|
||||
if remote_uses_http "$repo"; then
|
||||
GIT_ASKPASS="$ASKPASS_FILE" \
|
||||
GIT_TERMINAL_PROMPT=0 \
|
||||
GIT_UPDATE_USER="$GIT_USER" \
|
||||
GIT_UPDATE_PASS="$GIT_PASS" \
|
||||
git -C "$repo" -c credential.helper= pull
|
||||
else
|
||||
git -C "$repo" pull
|
||||
fi
|
||||
}
|
||||
|
||||
# ---------- arg parsing ----------
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--repos)
|
||||
require_value "$@"
|
||||
REPOS_FILE="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
die "Unknown argument: $1 (use --help)"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
[[ -n "$REPOS_FILE" ]] || die "Missing --repos FILE (use --help)."
|
||||
[[ -f "$REPOS_FILE" ]] || die "Repository list not found: ${REPOS_FILE}"
|
||||
|
||||
REPOS_FILE="$(readlink -f "$REPOS_FILE")"
|
||||
REPOS_DIR="$(cd "$(dirname "$REPOS_FILE")" && pwd)"
|
||||
|
||||
repos=()
|
||||
while IFS= read -r raw_line || [[ -n "$raw_line" ]]; do
|
||||
line="$(trim "$raw_line")"
|
||||
[[ -z "$line" || "$line" == \#* ]] && continue
|
||||
line="${line/#\~/$HOME}"
|
||||
repos+=("$line")
|
||||
done < "$REPOS_FILE"
|
||||
|
||||
[[ ${#repos[@]} -gt 0 ]] || die "Repository list is empty: ${REPOS_FILE}"
|
||||
|
||||
prompt_credentials
|
||||
write_askpass
|
||||
|
||||
total="${#repos[@]}"
|
||||
index=0
|
||||
ok_count=0
|
||||
fail_count=0
|
||||
result_paths=()
|
||||
result_notes=()
|
||||
|
||||
log "Repository list: ${REPOS_FILE}"
|
||||
log "Repositories: ${total}"
|
||||
|
||||
for entry in "${repos[@]}"; do
|
||||
index=$((index + 1))
|
||||
resolved="$(normalize_path "$(resolve_repo "$entry")")"
|
||||
if [[ -d "$resolved" ]]; then
|
||||
resolved="$(cd "$resolved" && pwd)"
|
||||
fi
|
||||
|
||||
printf '\n%s========== [%s/%s] %s ==========%s\n' \
|
||||
"$C_INFO" "$index" "$total" "$resolved" "$C_RESET"
|
||||
|
||||
if [[ ! -d "$resolved" ]]; then
|
||||
err "Directory not found: ${resolved}"
|
||||
result_paths+=("$resolved")
|
||||
result_notes+=("not found")
|
||||
fail_count=$((fail_count + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
toplevel="$(git -C "$resolved" rev-parse --show-toplevel 2>/dev/null || true)"
|
||||
if [[ -z "$toplevel" || "$(cd "$resolved" && pwd -P)" != "$(cd "$toplevel" && pwd -P)" ]]; then
|
||||
err "Not a git work tree: ${resolved}"
|
||||
result_paths+=("$resolved")
|
||||
result_notes+=("not a git work tree")
|
||||
fail_count=$((fail_count + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
exit_code=0
|
||||
if pull_repo "$resolved"; then
|
||||
ok "Updated ${resolved}"
|
||||
result_paths+=("$resolved")
|
||||
result_notes+=("0")
|
||||
ok_count=$((ok_count + 1))
|
||||
else
|
||||
exit_code=$?
|
||||
err "Failed ${resolved} (exit ${exit_code})"
|
||||
result_paths+=("$resolved")
|
||||
result_notes+=("exit ${exit_code}")
|
||||
fail_count=$((fail_count + 1))
|
||||
fi
|
||||
done
|
||||
|
||||
printf '\n%s========== Summary ==========%s\n' "$C_INFO" "$C_RESET"
|
||||
summary_index=0
|
||||
while [[ "$summary_index" -lt "$total" ]]; do
|
||||
note="${result_notes[$summary_index]}"
|
||||
path="${result_paths[$summary_index]}"
|
||||
if [[ "$note" == "0" ]]; then
|
||||
ok "${path}"
|
||||
else
|
||||
err "${path} (${note})"
|
||||
fi
|
||||
summary_index=$((summary_index + 1))
|
||||
done
|
||||
|
||||
if [[ "$fail_count" -gt 0 ]]; then
|
||||
err "${ok_count} updated, ${fail_count} failed."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ok "${ok_count} updated, 0 failed."
|
||||
@@ -1,5 +1,5 @@
|
||||
## Stage 1 : build with maven builder image with native capabilities
|
||||
FROM quay.io/quarkus/ubi-quarkus-mandrel-builder-image:jdk-21 AS build
|
||||
FROM quay.io/quarkus/ubi-quarkus-mandrel-builder-image:jdk-25 AS build
|
||||
COPY --chown=quarkus:quarkus mvnw /code/mvnw
|
||||
COPY --chown=quarkus:quarkus .mvn /code/.mvn
|
||||
COPY --chown=quarkus:quarkus pom.xml /code/
|
||||
|
||||
Reference in New Issue
Block a user