#!/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:-artifactorycn.netcracker.com:17009}" 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 print the image ref that would be built test run unit tests only (maven, or docker --target test) build build the image (runs unit tests first unless --skip-tests) push push the last built (or --tag'd) image deploy -e ENV point -v1 at the image + wait for rollout [needs --yes] ship -e ENV test → build → push → deploy → rollout wait [needs --yes] status -e ENV deployed image, replicas, pod state rollback -e ENV restore the image recorded before the last deploy [needs --yes] pullsecret -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/) -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//_unm_: 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