278 lines
11 KiB
Bash
278 lines
11 KiB
Bash
#!/usr/bin/env bash
|
|
# Build a NDO service locally with Docker, push to artifactory, point a k8s deployment at it.
|
|
set -euo pipefail
|
|
|
|
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
# shellcheck source=lib/env.sh
|
|
source "$HERE/lib/env.sh"
|
|
|
|
REG="${NDO_REGISTRY:-[REDACTED REGISTRY]}"
|
|
ART_USER="${NDO_ARTIFACTORY_USER:-$USER}"
|
|
PLATFORM="${NDO_PLATFORM:-linux/amd64}"
|
|
PROJECTS="${NDO_PROJECTS:-$HOME/projects}"
|
|
|
|
ENV_ALIAS="${NDO_ENV:-}"
|
|
SVC=""; DIR=""; TAG=""; TICKET=""; DFILE=""; TARGET="release"
|
|
YES=0; NOCACHE=0; SKIP_TESTS=0; TIMEOUT="10m"
|
|
|
|
die() { echo "ERROR: $*" >&2; exit 1; }
|
|
say() { echo "==> $*" >&2; }
|
|
|
|
usage() {
|
|
cat <<'USAGE'
|
|
ndo-ship.sh — local build → artifactory → k8s deploy for NDO services.
|
|
|
|
Commands:
|
|
doctor check docker/buildx/registry-login/kubectl
|
|
login docker login to artifactory (interactive)
|
|
tag <service> print the image ref that would be built
|
|
test <service> run unit tests only (maven, or docker --target test)
|
|
build <service> build the image (runs unit tests first unless --skip-tests)
|
|
push <service> push the last built (or --tag'd) image
|
|
deploy <service> -e ENV point <service>-v1 at the image + wait for rollout [needs --yes]
|
|
ship <service> -e ENV test → build → push → deploy → rollout wait [needs --yes]
|
|
status <service> -e ENV deployed image, replicas, pod state
|
|
rollback <service> -e ENV restore the image recorded before the last deploy [needs --yes]
|
|
pullsecret <service> -e ENV attach local docker creds as an imagePullSecret (ImagePullBackOff fix) [needs --yes]
|
|
|
|
Options:
|
|
-e, --env ALIAS target env (see: ndo-api.sh env ls). Ambiguous short names are rejected.
|
|
-t, --tag TAG image tag (default: UTC timestamp, always unique)
|
|
--ticket N UNM number for the repo name (default: parsed from git branch)
|
|
-d, --dir PATH service repo (default: $NDO_PROJECTS/<service>)
|
|
-f, --file FILE dockerfile (default: Dockerfile_local, falls back to Dockerfile)
|
|
--target STAGE build target (default: release; ignored if the dockerfile has no stages)
|
|
--platform P default linux/amd64 — do NOT drop this on an arm64 Mac
|
|
--skip-tests skip unit tests in build/ship
|
|
--no-cache docker build --no-cache
|
|
--timeout D rollout wait (default 10m)
|
|
-y, --yes confirm a cluster-mutating command (deploy/ship/rollback/pullsecret)
|
|
|
|
Image ref: $REG/<[REDACTED USER]>/<service>_unm_<ticket>:<tag>
|
|
Env overrides: NDO_REGISTRY NDO_ARTIFACTORY_USER NDO_PLATFORM NDO_PROJECTS NDO_ENV
|
|
USAGE
|
|
}
|
|
|
|
parse_opts() {
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
-e|--env) ENV_ALIAS="$2"; shift 2 ;;
|
|
-t|--tag) TAG="$2"; shift 2 ;;
|
|
--ticket) TICKET="$2"; shift 2 ;;
|
|
-d|--dir) DIR="$2"; shift 2 ;;
|
|
-f|--file) DFILE="$2"; shift 2 ;;
|
|
--target) TARGET="$2"; shift 2 ;;
|
|
--platform) PLATFORM="$2"; shift 2 ;;
|
|
--timeout) TIMEOUT="$2"; shift 2 ;;
|
|
--skip-tests) SKIP_TESTS=1; shift ;;
|
|
--no-cache) NOCACHE=1; shift ;;
|
|
-y|--yes) YES=1; shift ;;
|
|
-*) die "unknown option $1" ;;
|
|
*) [ -z "$SVC" ] && SVC="$1" || die "unexpected arg $1"; shift ;;
|
|
esac
|
|
done
|
|
}
|
|
|
|
need_svc() { [ -n "$SVC" ] || die "no service given"; }
|
|
|
|
svc_dir() {
|
|
need_svc
|
|
[ -n "$DIR" ] || DIR="$PROJECTS/$SVC"
|
|
[ -d "$DIR" ] || die "service repo not found: $DIR (use --dir)"
|
|
echo "$DIR"
|
|
}
|
|
|
|
dockerfile() {
|
|
local d; d="$(svc_dir)"
|
|
if [ -n "$DFILE" ]; then [ -f "$d/$DFILE" ] || [ -f "$DFILE" ] || die "dockerfile not found: $DFILE"; echo "$DFILE"; return; fi
|
|
if [ -f "$d/Dockerfile_local" ]; then echo "Dockerfile_local"; return; fi
|
|
echo "Dockerfile"
|
|
echo "no Dockerfile_local in $d — using Dockerfile. If the build pulls shared/external artifacts, create Dockerfile_local (see reference/dockerfile-local.md)." >&2
|
|
}
|
|
|
|
ticket() {
|
|
[ -n "$TICKET" ] && { echo "$TICKET"; return; }
|
|
local d b; d="$(svc_dir)"
|
|
b=$(git -C "$d" branch --show-current 2>/dev/null || true)
|
|
if [[ "$b" =~ [Uu][Nn][Mm][-_]?([0-9]+) ]]; then echo "${BASH_REMATCH[1]}"; else echo "local"; fi
|
|
}
|
|
|
|
image_ref() {
|
|
need_svc
|
|
local t; t="${TAG:-$(date -u +%Y%m%d-%H%M%S)}"
|
|
echo "$REG/$ART_USER/${SVC}_unm_$(ticket):$t"
|
|
}
|
|
|
|
last_image_file() { mkdir -p "$NDO_CACHE/last-image"; echo "$NDO_CACHE/last-image/$SVC"; }
|
|
|
|
resolve_image() {
|
|
if [ -n "$TAG" ]; then image_ref; return; fi
|
|
local f; f="$(last_image_file)"
|
|
[ -s "$f" ] || die "no image built yet for $SVC — run 'build' first or pass --tag"
|
|
cat "$f"
|
|
}
|
|
|
|
confirm() {
|
|
[ "$YES" -eq 1 ] || die "'$1' mutates shared env '$ENV_ALIAS' (context $NDO_CTX, ns $NDO_NS). Re-run with --yes once the user has approved."
|
|
}
|
|
|
|
container_name() {
|
|
local names first
|
|
names=$(kubectl --context="$NDO_CTX" -n "$NDO_NS" get deploy "$SVC-v1" \
|
|
-o jsonpath='{range .spec.template.spec.containers[*]}{.name}{"\n"}{end}')
|
|
if grep -qx "$SVC" <<<"$names"; then echo "$SVC"; else first=$(head -1 <<<"$names"); [ -n "$first" ] || die "no containers in $SVC-v1"; echo "$first"; fi
|
|
}
|
|
|
|
current_image() {
|
|
kubectl --context="$NDO_CTX" -n "$NDO_NS" get deploy "$SVC-v1" \
|
|
-o jsonpath='{.spec.template.spec.containers[0].image}'
|
|
}
|
|
|
|
rollback_file() { mkdir -p "$NDO_CACHE/rollback"; echo "$NDO_CACHE/rollback/$(tr '/' '_' <<<"$ENV_ALIAS")__$SVC"; }
|
|
|
|
is_maven() { [ -f "$(svc_dir)/pom.xml" ]; }
|
|
is_go() { [ -f "$(svc_dir)/go.mod" ]; }
|
|
has_stages() { grep -qiE '^[[:space:]]*FROM .* AS ' "$(svc_dir)/$(dockerfile)"; }
|
|
copies_target() { grep -qE 'COPY .*target/' "$(svc_dir)/$(dockerfile)"; }
|
|
|
|
mvn_env() {
|
|
export JAVA_HOME="${JAVA_HOME:-/Library/Java/JavaVirtualMachines/jdk-25.0.2.jdk/Contents/Home}"
|
|
export PATH="$JAVA_HOME/bin:$PATH"
|
|
}
|
|
|
|
run_tests() {
|
|
local d; d="$(svc_dir)"
|
|
if is_maven; then
|
|
say "maven unit tests ($SVC)"
|
|
( mvn_env; cd "$d" && mvn -B test )
|
|
elif is_go && grep -qiE '^[[:space:]]*FROM .* AS test' "$d/$(dockerfile)"; then
|
|
say "docker test stage ($SVC)"
|
|
docker build --platform "$PLATFORM" -f "$d/$(dockerfile)" --target test -t "$SVC-test:local" "$d"
|
|
elif is_go; then
|
|
say "go test ($SVC)"
|
|
( cd "$d" && go test ./... )
|
|
else
|
|
say "no unit-test runner detected for $SVC — skipping"
|
|
fi
|
|
}
|
|
|
|
do_build() {
|
|
local d df img args=()
|
|
d="$(svc_dir)"; df="$(dockerfile)"; img="$(image_ref)"
|
|
[ "$SKIP_TESTS" -eq 1 ] || run_tests
|
|
# Java services copy target/*.jar into the image — package first.
|
|
if is_maven && copies_target; then
|
|
say "mvn package -DskipTests (jar for the image layer)"
|
|
( mvn_env; cd "$d" && mvn -B -DskipTests package )
|
|
fi
|
|
args=(build --platform "$PLATFORM" -f "$d/$df" -t "$img")
|
|
has_stages && grep -qiE "^[[:space:]]*FROM .* AS $TARGET\$" "$d/$df" && args+=(--target "$TARGET")
|
|
[ "$NOCACHE" -eq 1 ] && args+=(--no-cache)
|
|
args+=("$d")
|
|
say "docker ${args[*]}"
|
|
docker "${args[@]}"
|
|
echo "$img" > "$(last_image_file)"
|
|
echo "$img"
|
|
}
|
|
|
|
do_push() {
|
|
local img; img="$(resolve_image)"
|
|
say "docker push $img"
|
|
docker push "$img"
|
|
echo "$img"
|
|
}
|
|
|
|
do_deploy() {
|
|
local img c prev
|
|
env_resolve "$ENV_ALIAS"
|
|
confirm deploy
|
|
img="$(resolve_image)"
|
|
c="$(container_name)"
|
|
prev="$(current_image)"
|
|
echo "$prev" > "$(rollback_file)"
|
|
say "rollback point saved: $prev"
|
|
say "set image $SVC-v1/$c=$img (ctx=$NDO_CTX ns=$NDO_NS)"
|
|
kubectl --context="$NDO_CTX" -n "$NDO_NS" set image "deploy/$SVC-v1" "$c=$img"
|
|
kubectl --context="$NDO_CTX" -n "$NDO_NS" rollout status "deploy/$SVC-v1" --timeout="$TIMEOUT" || {
|
|
echo "--- rollout failed; pod events ---" >&2
|
|
kubectl --context="$NDO_CTX" -n "$NDO_NS" get pod -l "app=$SVC-v1" \
|
|
-o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.status.phase}{"\t"}{range .status.containerStatuses[*]}{.state}{end}{"\n"}{end}' >&2
|
|
echo "ImagePullBackOff => node has no creds for $REG. Fix: ndo-ship.sh pullsecret $SVC -e $ENV_ALIAS --yes" >&2
|
|
return 1
|
|
}
|
|
do_status
|
|
}
|
|
|
|
do_status() {
|
|
env_resolve "$ENV_ALIAS"
|
|
need_svc
|
|
echo "env : $ENV_ALIAS (ctx=$NDO_CTX ns=$NDO_NS)"
|
|
echo "image : $(current_image)"
|
|
kubectl --context="$NDO_CTX" -n "$NDO_NS" get deploy "$SVC-v1" \
|
|
-o custom-columns='READY:.status.readyReplicas,DESIRED:.spec.replicas,UPDATED:.status.updatedReplicas'
|
|
kubectl --context="$NDO_CTX" -n "$NDO_NS" get pod -l "app=$SVC-v1" \
|
|
-o custom-columns='POD:.metadata.name,PHASE:.status.phase,RESTARTS:.status.containerStatuses[0].restartCount,AGE:.metadata.creationTimestamp'
|
|
}
|
|
|
|
do_rollback() {
|
|
local f prev c
|
|
env_resolve "$ENV_ALIAS"
|
|
confirm rollback
|
|
f="$(rollback_file)"
|
|
[ -s "$f" ] || die "no rollback point recorded for $SVC on $ENV_ALIAS"
|
|
prev="$(cat "$f")"; c="$(container_name)"
|
|
say "restoring $prev"
|
|
kubectl --context="$NDO_CTX" -n "$NDO_NS" set image "deploy/$SVC-v1" "$c=$prev"
|
|
kubectl --context="$NDO_CTX" -n "$NDO_NS" rollout status "deploy/$SVC-v1" --timeout="$TIMEOUT"
|
|
}
|
|
|
|
do_pullsecret() {
|
|
env_resolve "$ENV_ALIAS"
|
|
confirm pullsecret
|
|
local sec=ndo-repro-artifactory pw
|
|
pw=$(printf '%s' "$REG" | docker-credential-osxkeychain get 2>/dev/null \
|
|
| python3 -c 'import sys,json;print(json.load(sys.stdin)["Secret"])') || die "no local docker creds for $REG — run: ndo-ship.sh login"
|
|
kubectl --context="$NDO_CTX" -n "$NDO_NS" create secret docker-registry "$sec" \
|
|
--docker-server="$REG" --docker-username="$ART_USER" --docker-password="$pw" \
|
|
--dry-run=client -o yaml | kubectl --context="$NDO_CTX" -n "$NDO_NS" apply -f -
|
|
unset pw
|
|
kubectl --context="$NDO_CTX" -n "$NDO_NS" patch deploy "$SVC-v1" \
|
|
-p "{\"spec\":{\"template\":{\"spec\":{\"imagePullSecrets\":[{\"name\":\"$sec\"}]}}}}"
|
|
kubectl --context="$NDO_CTX" -n "$NDO_NS" rollout status "deploy/$SVC-v1" --timeout="$TIMEOUT"
|
|
}
|
|
|
|
do_doctor() {
|
|
printf 'docker : %s\n' "$(docker version --format '{{.Server.Version}}' 2>&1 | head -1)"
|
|
printf 'context : %s\n' "$(docker context show 2>/dev/null)"
|
|
printf 'buildx : %s\n' "$(docker buildx version 2>&1 | head -1)"
|
|
printf 'host arch : %s (build platform %s)\n' "$(uname -m)" "$PLATFORM"
|
|
if printf '%s' "$REG" | docker-credential-osxkeychain get >/dev/null 2>&1; then
|
|
printf 'registry : logged in to %s as %s\n' "$REG" "$ART_USER"
|
|
else
|
|
printf 'registry : NOT logged in to %s — run: ndo-ship.sh login\n' "$REG"
|
|
fi
|
|
printf 'envs : %s\n' "$(awk -F'\t' '!/^#/&&NF>=4' "$(env_file)" | wc -l | tr -d ' ') registered"
|
|
[ -n "$ENV_ALIAS" ] && { env_resolve "$ENV_ALIAS"; printf 'env %-10s: ctx=%s ns=%s\n gw=%s\n' "$ENV_ALIAS" "$NDO_CTX" "$NDO_NS" "$NDO_GW"; \
|
|
kubectl --context="$NDO_CTX" -n "$NDO_NS" get deploy -o name >/dev/null 2>&1 \
|
|
&& echo 'kube access : ok' || echo 'kube access : FAILED (VPN down or creds expired)'; }
|
|
return 0
|
|
}
|
|
|
|
CMD="${1:-}"; shift || true
|
|
case "$CMD" in
|
|
doctor) parse_opts "$@"; do_doctor ;;
|
|
login) docker login "$REG" ;;
|
|
tag) parse_opts "$@"; image_ref ;;
|
|
test) parse_opts "$@"; run_tests ;;
|
|
build) parse_opts "$@"; do_build ;;
|
|
push) parse_opts "$@"; do_push ;;
|
|
deploy) parse_opts "$@"; do_deploy ;;
|
|
status) parse_opts "$@"; do_status ;;
|
|
rollback) parse_opts "$@"; do_rollback ;;
|
|
pullsecret) parse_opts "$@"; do_pullsecret ;;
|
|
ship) parse_opts "$@"; env_resolve "$ENV_ALIAS"; confirm ship
|
|
do_build >/dev/null; TAG=""; do_push >/dev/null; do_deploy ;;
|
|
""|-h|--help|help) usage ;;
|
|
*) die "unknown command: $CMD (see --help)" ;;
|
|
esac
|