feat: migrate skills review desk to astro
This commit is contained in:
@@ -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]
|
||||
|
@@ -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
|
||||
+277
@@ -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
|
||||
+29
@@ -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" ]
|
||||
+9
@@ -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.
|
||||
Reference in New Issue
Block a user