feat: migrate skills review desk to astro

This commit is contained in:
Marcos Paulo
2026-09-05 16:54:37 +00:00
parent 107e429fb9
commit 71e4775573
66 changed files with 8184 additions and 96 deletions
@@ -0,0 +1,136 @@
---
name: ndo-repro
description: Build an NDO microservice locally with Docker, push it to artifactory, deploy it to a dev env, then reproduce or validate the fix by driving the Business-Operation-Manager (BOM) API and reading live pod logs. Use when debugging or verifying a UNM-* ticket without waiting for CI, when the UI flow is hard to reproduce, when driving the replacement/Map-To/target-insert flow without a browser, or when the user says "repro via API", "drive BOM", "ship to <env>", "deploy my build to dev-2", "validate the fix on the cluster", "run ndo-repro in <env>". Covers env discovery across the saas-rnd-oss and ndo-shared clusters.
---
# NDO build → deploy → repro loop
Full loop on one env, no CI wait: build the service locally, push to artifactory, repoint the k8s deployment, then drive BOM's API and read pod logs to prove the ticket's acceptance criteria.
Two scripts, both env-aware via `-e <alias>`:
- `~/.claude/skills/ndo-repro/ndo-ship.sh` — doctor / test / build / push / deploy / status / rollback
- `~/.claude/skills/ndo-repro/ndo-api.sh` — env registry / auth / BOM API / logs
Run `--help` on either for the full command list.
## Envs
Aliases come from a discovered registry (`envs.tsv`, refreshed with `ndo-api.sh env discover` — it scans every kube context for a namespace running `consolidated-inventory-manager-v1` and reads the `public-gateway` ingress host).
```
ndo-api.sh env ls # alias → context / namespace / gateway
ndo-api.sh -e oss-01/dev-2 env show
```
Alias shape is `<cluster>/<env>` (`oss-01/dev-2`, `oss-03/dev-1`) plus `shared-244` for `ndo-shared-244/ndo`. A bare `dev-2` is accepted **only** if it is unique across clusters; otherwise the script lists the candidates and stops — never guess which cluster the user meant, ask.
Everything needs the corporate VPN. `ndo-dev-1` is decommissioned; do not use it.
## Step 0 — preflight
```
ndo-ship.sh doctor -e <env>
```
Checks docker/OrbStack, buildx, artifactory login, host arch, and kube access for the env. If it reports "NOT logged in": `ndo-ship.sh login` (interactive artifactory password prompt — the user runs it, prefix with `!` in the CLI).
## Step 1 — build (tests first)
```
ndo-ship.sh build <service> [--ticket 231239] [--skip-tests] [--no-cache]
```
- Runs unit tests first — Maven `mvn -B test` for Java services, the dockerfile's `test` stage or `go test ./...` for Go — and aborts the build if they fail. Do not pass `--skip-tests` when the user asked for "build and unit tests successful".
- Java services: runs `mvn -B -DskipTests package` after the tests so `target/*.jar` exists for the `COPY`.
- Builds `--platform linux/amd64`. **Never drop this** — the Mac is arm64, the nodes are amd64, and the mismatch only surfaces as a crashlooping pod after deploy.
- Uses `Dockerfile_local` if present, else `Dockerfile`, and `--target release` when the dockerfile has stages. See `reference/dockerfile-local.md` before writing one.
- Image ref: `[REDACTED REGISTRY]/<[REDACTED USER]>/<service>_unm_<ticket>:<utc-timestamp>`. Ticket is parsed from the git branch (`bugfix/UNM-231239``231239`). The timestamp tag matters: deployments run `imagePullPolicy: IfNotPresent`, so a reused tag silently keeps the old image.
The ref is cached, so `push`/`deploy` need no `--tag`.
## Step 2 — push + deploy
```
ndo-ship.sh push <service>
ndo-ship.sh deploy <service> -e <env> --yes
# or all of it:
ndo-ship.sh ship <service> -e <env> --yes
```
`deploy` records the currently deployed image as a rollback point, `kubectl set image`s the deployment, and waits for `rollout status`. On failure it dumps pod state.
**`deploy`/`ship`/`rollback`/`pullsecret` mutate a shared env.** They refuse to run without `--yes`, and `--yes` is only yours to pass after the user has approved *that* deploy to *that* env. Approval for one env or one ticket does not carry over.
Rollback: `ndo-ship.sh rollback <service> -e <env> --yes`.
If pods go `ImagePullBackOff`, the nodes have no credentials for the `:17009` personal repo:
```
ndo-ship.sh pullsecret <service> -e <env> --yes
```
which creates a `docker-registry` secret from the local docker keychain and patches the deployment's `imagePullSecrets`.
## Step 3 — confirm what is actually running
The single most common cause of "the fix didn't work" is the wrong image.
```
ndo-api.sh -e <env> image <service>
ndo-api.sh -e <env> pods <service>
```
Match the tag to the build you just pushed. Product images look like `…:release_2024.4_<date>`; yours look like `…/<user>/<service>_unm_<ticket>:<timestamp>`.
## Step 4 — drive the BOM API
Auth is automatic and per-env: a keycloak password-grant token (realm `default`, client `frontend`, dev sysadm creds) is minted and refreshed on expiry. Override with `NDO_USER` / `NDO_PASS` / `NDO_REALM` / `NDO_CLIENT`. Tokens live in `~/.cache/ndo-repro/token-<env>.txt`, mode 600 — never echo one into chat or a committed file.
Stateful operation lifecycle (BOM `/business-operation-manager/v1`):
- **initiate**: `POST /operation-request/initiate?key=<opKey>` → returns `operation-request-id` (rid).
- **prepare a sub-operation**: `POST /operation-request/{rid}/prepare?key=<subOpKey>` with `{data, sources, parent-path}` (BOM injects operation-data/inputs from the session).
- **perform a read/action**: `POST /operation-request/{rid}/perform` with `{"method":"GET","url":"/consolidated-inventory-manager/v3/<path>","body":{…}}` — the inner call is wrapped.
Replacement (CIM `/v3/replacement`) endpoints, all via `perform` GET:
- `/report` — impact summary; `resolved-issues` / `unresolved-issues` is the pass/fail metric.
- `/target` — target tree (chassis + slots; does **not** expose ports/interfaces).
- `/target/slots` — slots for a target component.
- `/mapping`, `/mapping/available-target-values` — Map-To candidates (`{impact-type, impacted-entity-mkey, ref-endpoint-mkey, [filter], [only-total]}`); `total:0` = "No available interfaces".
- target insert sub-op key: `nc_op_ci_<as-is|to-be>_hw-component.replacement.target.insert.module`.
Finding ids: `/report` gives source/target mkeys; `/target` gives chassis + slot ids; a DL spec read (`/device-library/v1/restconf/data/hw-component?depth=3&filter=[{op:eq,property:id,value:[<srcId>]}]`) gives `port-interface`/`port-type`.
```
ndo-api.sh -e <env> initiate nc_op_ci_as-is_hw-component.replacement
ndo-api.sh -e <env> report <rid>
ndo-api.sh -e <env> avail <rid> <impactMkey> <refMkey>
ndo-api.sh -e <env> get <rid> /v3/replacement/target
```
## Step 5 — read live logs (ground truth)
```
ndo-api.sh -e <env> logs consolidated-inventory-manager 15m '\[UNM-231239\]'
```
Strips `tenant_id`/`thread`/`traceId`/`spanId`/`request_id` noise. Grep the ticket tag for the dev's INFO traces plus `WARN`/`ERROR`; correlate one call end to end by `request_id=` (drop the sed filter when you need it).
Known noise to ignore: `Unknown token audience: netcracker` — a k8s m2m quirk on the dev envs, not your bug unless the user says otherwise.
## Validating acceptance criteria
When asked to "validate the issue is resolved and acceptance criteria fulfilled", the deliverable is evidence, not an opinion:
1. State the deployed image tag and prove it is your build.
2. For each acceptance criterion, name the API call that exercises it and show the response field that decides pass/fail (e.g. `unresolved-issues: 0`, `total > 0`).
3. Show the log lines that confirm the new code path ran.
4. Report any criterion you could **not** exercise, and why — do not infer a pass from an adjacent one.
## Safety
- Read-mostly on the API side. `prepare`/`perform` writes mutate only the draft stateful session — fine for repro. Do not `/complete` a replacement unless asked.
- Deploying replaces a running service other people may be using. Confirm the env with the user first, keep the rollback point, and roll back when done if they asked you to.
- Never push to `:17099`/`:17003` (product repos) — `:17009` personal only.
- Never open MRs, push branches, or change CI without explicit approval.
- If a stateful session is polluted by earlier inserts, initiate a fresh rid rather than fighting old state.
## Pattern that works
fix in source → `ndo-ship.sh build` (tests gate it) → `push` → confirm env with user → `deploy --yes` → verify image tag → initiate/drive the exact sub-op the UI would → read the report metric → if it still fails, read CIM logs for the real reason → new hypothesis → repeat.
## Media (when QA attaches gifs/videos)
- GIF frames: Python+PIL (`Image.open(g); im.seek(i)`); crop the devtools network panel and upscale to read request names/statuses.
- Video: `ffmpeg -i in.mp4 -vf fps=1/5 out%03d.jpg`, then narrow with `-ss <start> -to <end> -vf fps=1`.
@@ -0,0 +1,3 @@
# Published review fixture — original environment identities and endpoints removed.
# alias context namespace gateway
sample/dev [REDACTED CONTEXT] [REDACTED NAMESPACE] [REDACTED URL]
1 # Published review fixture — original environment identities and endpoints removed.
2 # alias context namespace gateway
3 sample/dev [REDACTED CONTEXT] [REDACTED NAMESPACE] [REDACTED URL]
@@ -0,0 +1,17 @@
#!/usr/bin/env bash
# Published review fixture — original environment discovery and endpoints removed.
_ndo_die() { echo "$*" >&2; exit 2; }
env_list() {
printf '%-16s %-34s %-14s %s\n' ALIAS CONTEXT NAMESPACE GATEWAY
printf '%-16s %-34s %-14s %s\n' sample/dev '[REDACTED CONTEXT]' '[REDACTED NAMESPACE]' '[REDACTED URL]'
}
env_resolve() {
_ndo_die "Environment resolution is disabled in this published, redacted review fixture."
}
env_discover() {
_ndo_die "Environment discovery is disabled in this published, redacted review fixture."
}
@@ -0,0 +1,161 @@
#!/usr/bin/env bash
set -euo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=lib/env.sh
source "$HERE/lib/env.sh"
ENV_ALIAS="${NDO_ENV:-}"
# -e/--env may appear anywhere; strip it before dispatch.
ARGS=()
while [ $# -gt 0 ]; do
case "$1" in
-e|--env) ENV_ALIAS="$2"; shift 2 ;;
*) ARGS+=("$1"); shift ;;
esac
done
set -- "${ARGS[@]:-}"
NDO_REALM="${NDO_REALM:-default}"
NDO_CLIENT="${NDO_CLIENT:-frontend}"
NDO_USER="${NDO_USER:?Set NDO_USER through an approved configuration source before using authenticated API commands}"
NDO_PASS="${NDO_PASS:?Set NDO_PASS through an approved secret source before using authenticated API commands}"
usage() {
cat <<'USAGE'
ndo-api.sh — drive the NDO BOM API for live repro, on any registered env.
Every command needs a target env: -e <alias> (or NDO_ENV=<alias>).
Auth is automatic: a keycloak password-grant token is minted per env and
refreshed on expiry (~15 min). Token cache: ~/.cache/ndo-repro/token-<env>.
Env:
env ls list registered envs
env discover rescan kube contexts, rebuild the registry
env show resolved context / namespace / gateway for -e
API:
login mint a fresh token now
token <jwt> save an externally-supplied bearer token
whoami check auth (200 = ok)
opdef <key> GET operation-definition for an op key
initiate <key> [bodyfile] POST initiate, prints operation-request-id
perform <rid> <innerJsonOrFile> POST /{rid}/perform with a wrapped {method,url,body}
prepare <rid> <key> <bodyfile> POST /{rid}/prepare?key=<key> with body file
get <rid> <cimPath> [innerBodyJson] perform a GET against /consolidated-inventory-manager<cimPath>
report <rid> replacement report (resolved/unresolved)
target <rid> replacement target tree
avail <rid> <impactMkey> <refMkey> [type] available-target-values (type default l2_link)
Cluster:
logs <service> [since] [grep] tail+denoise logs (default since=10m)
image <service> deployed image of <service>-v1
pods <service> pod phase/restarts for <service>-v1
Examples:
ndo-api.sh env ls
ndo-api.sh -e shared-244 whoami
ndo-api.sh -e oss-01/dev-2 report 21dec51b-f9cb-41fe-af94-512c0921036b
ndo-api.sh -e oss-01/dev-2 logs consolidated-inventory-manager 15m '\[UNM-231239\]'
USAGE
}
case "${1:-}" in
""|-h|--help|help) usage; exit 0 ;;
env)
case "${2:-ls}" in
ls|list) env_list; exit 0 ;;
discover) env_discover; exit 0 ;;
show) env_resolve "$ENV_ALIAS"; printf 'alias : %s\ncontext : %s\nns : %s\ngateway : %s\n' \
"$ENV_ALIAS" "$NDO_CTX" "$NDO_NS" "$NDO_GW"; exit 0 ;;
*) echo "env: ls | discover | show" >&2; exit 2 ;;
esac ;;
esac
env_resolve "$ENV_ALIAS"
GW="${NDO_GW_OVERRIDE:-$NDO_GW}"
BOM="$GW/business-operation-manager/v1"
mkdir -p "$NDO_CACHE"
TOKFILE="${NDO_TOKEN_FILE:-$NDO_CACHE/token-$(tr '/' '_' <<<"$ENV_ALIAS").txt}"
mint() {
local out
out=$(curl -sk -X POST "$GW/auth/realms/$NDO_REALM/protocol/openid-connect/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "grant_type=password" --data-urlencode "client_id=$NDO_CLIENT" \
--data-urlencode "username=$NDO_USER" --data-urlencode "password=$NDO_PASS")
printf '%s' "$out" | python3 -c "import sys,json;d=json.load(sys.stdin);open('$TOKFILE','w').write(d['access_token']) if 'access_token' in d else sys.exit('mint failed: '+json.dumps(d)[:200])" || return 1
chmod 600 "$TOKFILE"
}
token_valid() {
[ -s "$TOKFILE" ] || return 1
python3 - "$TOKFILE" <<'PY' 2>/dev/null
import sys,base64,json,time
t=open(sys.argv[1]).read().strip()
p=t.split('.')[1]; p+='='*(-len(p)%4)
exp=json.loads(base64.urlsafe_b64decode(p)).get('exp',0)
sys.exit(0 if exp-time.time()>30 else 1)
PY
}
ensure_token() { token_valid || mint; }
tok() { cat "$TOKFILE"; }
auth() { ensure_token >&2 || { echo "auth failed on $ENV_ALIAS" >&2; exit 1; }; echo "Authorization: Bearer $(tok)"; }
K() { kubectl --context="$NDO_CTX" -n "$NDO_NS" "$@"; }
# Services use either app=<svc>-v1 or name=<svc>-v1 depending on the chart.
selector_for() {
local svc="$1" l
for l in "app=$svc-v1" "name=$svc-v1" "app=$svc" "name=$svc"; do
[ -n "$(K get pod -l "$l" -o name 2>/dev/null)" ] && { echo "$l"; return 0; }
done
echo "no pods for $svc (tried app=/name= selectors) in $NDO_NS" >&2
return 1
}
case "${1:-}" in
token) printf '%s' "$2" > "$TOKFILE"; chmod 600 "$TOKFILE"; echo "saved to $TOKFILE"; ;;
login) mint && echo "minted ($NDO_USER, realm=$NDO_REALM, env=$ENV_ALIAS) → $TOKFILE" ;;
whoami) curl -sk -o /dev/null -w "HTTP %{http_code}\n" -H "$(auth)" "$BOM/operation-definition?key=nc_op_ci_as-is_hw-component.replacement" ;;
opdef) curl -sk -H "$(auth)" "$BOM/operation-definition?key=$2" ;;
initiate)
body="${3:-{} }"; [ -f "${3:-}" ] && body="@$3"
curl -sk -X POST -H "$(auth)" -H 'Content-Type: application/json' "$BOM/operation-request/initiate?key=$2" -d "$body" ;;
perform)
inner="$3"; [ -f "$3" ] && inner="@$3"
curl -sk -X POST -H "$(auth)" -H 'Content-Type: application/json' "$BOM/operation-request/$2/perform" -d "$inner" ;;
prepare)
curl -sk -X POST -H "$(auth)" -H 'Content-Type: application/json' "$BOM/operation-request/$2/prepare?key=$3" -d "@$4" ;;
get)
rid="$2"; path="$3"; innerbody="${4:-}"
if [ -n "$innerbody" ]; then req="{\"method\":\"GET\",\"url\":\"/consolidated-inventory-manager$path\",\"body\":$innerbody}";
else req="{\"method\":\"GET\",\"url\":\"/consolidated-inventory-manager$path\"}"; fi
curl -sk -X POST -H "$(auth)" -H 'Content-Type: application/json' "$BOM/operation-request/$rid/perform" -d "$req" ;;
report)
curl -sk -X POST -H "$(auth)" -H 'Content-Type: application/json' "$BOM/operation-request/$2/perform" \
-d '{"method":"GET","url":"/consolidated-inventory-manager/v3/replacement/report"}' \
| python3 -c "import sys,json;i=json.load(sys.stdin).get('action-report',{}).get('results',{}).get('impact',[]);print(json.dumps(i,indent=1))" ;;
target)
curl -sk -X POST -H "$(auth)" -H 'Content-Type: application/json' "$BOM/operation-request/$2/perform" \
-d '{"method":"GET","url":"/consolidated-inventory-manager/v3/replacement/target"}' ;;
avail)
typ="${5:-l2_link}"
curl -sk -X POST -H "$(auth)" -H 'Content-Type: application/json' "$BOM/operation-request/$2/perform" \
-d "{\"method\":\"GET\",\"url\":\"/consolidated-inventory-manager/v3/replacement/mapping/available-target-values\",\"body\":{\"impact-type\":\"$typ\",\"impacted-entity-mkey\":\"$3\",\"ref-endpoint-mkey\":\"$4\"}}" \
| python3 -c "import sys,json;r=json.load(sys.stdin).get('action-report',{}).get('results',{});print('total',r.get('total'),'values',len(r.get('available-values',[])))" ;;
logs)
svc="$2"; since="${3:-10m}"; pat="${4:-}"
SEL=$(selector_for "$svc") || exit 1
P=$(K get pod -l "$SEL" -o jsonpath='{.items[0].metadata.name}')
K logs "$P" --since="$since" 2>/dev/null \
| sed -E 's/\[(tenant_id|thread|originating_bi_id|traceId|spanId|request_id)=[^]]*\] ?//g' \
| { [ -n "$pat" ] && grep -aE "$pat" || cat; } ;;
image)
K get deploy "$2-v1" -o jsonpath='{.spec.template.spec.containers[0].image}{"\n"}' ;;
pods)
SEL=$(selector_for "$2") || exit 1
K get pod -l "$SEL" -o custom-columns='POD:.metadata.name,PHASE:.status.phase,READY:.status.containerStatuses[0].ready,RESTARTS:.status.containerStatuses[0].restartCount,IMAGE:.status.containerStatuses[0].image' ;;
*) echo "unknown cmd: $1"; usage; exit 1 ;;
esac
@@ -0,0 +1,277 @@
#!/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
@@ -0,0 +1,29 @@
# Reference copy: business-operation-manager Dockerfile_local (verified build 2026-08-12).
# Derived from the stock Dockerfile by dropping the "test" stage (needs ARANGO_DB_HOSTNAME)
# and the shared_resources COPY (CI-injected, absent locally).
# Copy to ~/projects/business-operation-manager/Dockerfile_local to use.
FROM [REDACTED REGISTRY]/product/go-builder:1.26.4 AS base
ENV APP_ROOT=/tmp/project
COPY . ${APP_ROOT}
RUN chmod -R u+x ${APP_ROOT}/scripts && \
chmod -R u+x ${APP_ROOT}/*.sh && \
chgrp -R 0 ${APP_ROOT} && \
chmod -R g=u ${APP_ROOT} /etc/passwd
FROM base AS build
RUN cd ${APP_ROOT} && ${APP_ROOT}/application_build.sh
FROM [REDACTED REGISTRY]/netcracker/qubership-core-base:2.3.7 AS release
COPY --chown=10001:10001 --from=build /tmp/project/scripts/* /bin/
COPY --chown=10001:10001 --from=build /tmp/project/business-operation-manager /bin/app
COPY --chown=10001:10001 --from=build /tmp/project/resources/policies.conf /opt/policies/
COPY --chown=10001:10001 --from=build /tmp/project/resources/business-operation-manager-public-api.json /opt/resources/business-operation-manager-public-api.json
EXPOSE 8080
USER 10001:10001
CMD [ "/bin/app" ]
@@ -0,0 +1,9 @@
# Local Dockerfile note — redacted review fixture
The original operational reference included internal source locations, registries,
and environment details. Those details have been removed from the published review.
For a local Dockerfile guide, keep the general rule: use a project-owned local
override only when the ordinary Dockerfile requires CI-only inputs. Keep runtime
stages, explicit architecture handling, and the application artifact; never copy
credentials, internal endpoints, or personal registry paths into the override.