The 'Wait for the build Job' k8s_info until-conditional crashed ('dict object has
no attribute resources') when a poll hit a transient cluster-API timeout. Guard
with resources | default([]) so a blip is treated as not-done-yet and retried
instead of failing the build. Applied to build.yml + build-capture.yml.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|---|---|---|
| a2c | ||
| aap | ||
| app | ||
| apps | ||
| containerfiles | ||
| docs | ||
| tasks | ||
| templates | ||
| .gitignore | ||
| build.sh | ||
| discover.yml | ||
| README.md | ||
simple/ — migrate a Java app to OpenShift in one helm install
This is the dumb-proof path. No LLM, no fact schema, no operators to babysit. The idea fits in three sentences:
- The app is built from source by a generic, per-runtime Containerfile.
build.shpicks the right one fromcontainerfiles/(springboot / tomcat9 / wildfly) based on the app's detected runtime, builds the source (Maven → small runtime stage), and pushes the image — you write no Containerfile. The cluster just pulls it. Plain, readable, portable — no S2I builder images. (Build it locally withbuild.sh, or from the AAPsimple: build appJT — which runs the same recipe as an in-clusterbuildahJob, no laptop podman.) - The database travels with the app. A
postgres:16sidecar runs in the same pod onlocalhost:5432, seeded from yourseed.sql.hostAliasespoint every database hostname your app hard-codes at the sidecar — so your app runs unmodified, with the exact config it had on the VM. (It is a dev/demo DB: ephemeralemptyDir,trustauth — see Known limitations.) - One small Helm chart (
app/) turns a short values file into all of that: Deployment (app + db sidecar), Service, Route. It pulls the prebuilt image — it does not build.
simple/
app/ the ONE Helm chart (read app/templates/*)
apps/<name>/
values.yaml describes one app (image, port, health, DB hostAliases)
seed.sql roles + database + the app's schema/data
containerfiles/ 3 generic per-runtime Containerfiles (springboot/tomcat9/wildfly)
build.sh build + push an app's image (picks the Containerfile by runtime)
discover.yml optional: thin entry that scans a VM and writes a values.yaml
tasks/discover.yml the actual detection logic discover.yml includes
templates/values.yaml.j2 the values.yaml discover writes
aap/ run build + deploy (and discover) from AAP — see aap/README.md
README.md this file
You do not write a Containerfile: the three in containerfiles/ are generic per
runtime (not per app), and build.sh selects the right one from the app's
runtime:. They build from the app's source — point build.sh at a checkout.
Documentation
This README is the quickstart. For the full picture:
- docs/walkthrough.md — start here: the whole solution step by step, with diagrams, for someone new to it.
- docs/architecture.md — the design (Containerfile →
registry → helm-pull + the DB-as-sidecar/
hostAliasesmodel) and why. - docs/end-to-end.md — the full VM → OpenShift walkthrough (discover → containerize → deploy) with real commands and troubleshooting.
- docs/reference.md — file-by-file map + the per-app
values.yaml/seed.sqlcontract + the Containerfile shapes. - docs/limitations.md — honest maturity: what's automated vs by-hand, known constraints, and the roadmap.
- aap/README.md — running build + deploy (and discover) from AAP.
Prerequisites
- The app's image built and pushed to a registry. Two ways:
./build.sh <name> <path-to-app-source>locally (needspodman+ a registry login), or the AAPsimple: build appJT, which runs the same recipe as an in-clusterbuildahJob (no laptop podman). Both pick the generic Containerfile by the app's runtime and push the image. See aap/README.md. - A namespace (
simple-apps) with a registry pull Secret namedoci-arsalan-pull. helmandocon your laptop, logged in.
export KUBECONFIG=~/.kube/anaeem-kubeconfig # the anaeem SNO target
oc new-project simple-apps 2>/dev/null || oc project simple-apps
Deploy an app
Two equivalent ways — by hand, or from AAP.
By hand (helm upgrade --install):
cd simple
helm upgrade --install pricing-svc ./app -n simple-apps \
-f apps/pricing-svc/values.yaml \
--set-file database.seedSql=apps/pricing-svc/seed.sql
From AAP: launch the simple: deploy app Job Template and pick the app.
It runs aap/deploy.yml, which is exactly the same helm upgrade --install
(with --wait). See aap/README.md for the AAP setup.
That is the whole command (the image was already built + pushed — by build.sh
or the AAP simple: build app JT). What happens next:
- The Deployment pulls the prebuilt image and starts the app container.
- The
dbsidecar starts alongside it and seeds itself fromseed.sql. - The Route serves the app.
Watch it:
oc get pods -n simple-apps -w # app pod -> 2/2 Running
oc get route pricing-svc -n simple-apps # the URL
Verify (pricing-svc):
URL=https://$(oc get route pricing-svc -n simple-apps -o jsonpath='{.spec.host}')
curl -sk $URL/actuator/health # {"status":"UP",...,"db":{"status":"UP"}}
curl -sk "$URL/api/price?sku=ELEC-LAPT-01&basePrice=1000" # computed price from seeded rules
The four sample apps
| App | Runtime (runtime:) |
What it exercises |
|---|---|---|
pricing-svc |
springboot |
the simplest case: REST CRUD, /actuator/health, datasource on localhost |
classic-shop |
tomcat9 |
two WARs on one Tomcat, JNDI datasource baked into context.xml, file uploads |
report-factory |
tomcat9 |
three datasources in one sidecar, a Quartz schema seeded before boot |
ledger-ejb |
wildfly |
Jakarta EE 10: EJB/JPA + a JMS MDB; WildFly is provisioned inside the build |
Each is the same helm install with a different apps/<name>/. They build with
the generic per-runtime Containerfiles in containerfiles/ (multi-stage):
- pricing-svc —
maven:…-17build →eclipse-temurin:17-jre, runs the fat jar. - classic-shop / report-factory —
maven:…-11build →tomcat:9-jdk11-temurin, drops the WAR(s) into Tomcat (with achgrp 0so Tomcat's work/temp dirs are writable under OpenShift's arbitrary UID). These two compile on Java 11. - ledger-ejb —
maven:…-17build where thewildfly-maven-pluginprovisions a trimmed WildFly undertarget/server(with the LedgerDS datasource + LedgerEvents queue baked in) →eclipse-temurin:17-jreruns that server. The one app that carries app-server-specific build config.
What "runs unmodified" did and didn't mean
The app source runs as-is — its baked-in datasource config, hostnames, and
credentials are untouched (the sidecar + hostAliases make them resolve). The
only adjustments are in the Containerfiles/poms above (build-tooling fits) and two
env overrides for pricing-svc (a writable log path and open-in-view for one list
endpoint) in apps/pricing-svc/values.yaml — the realistic edges of a migration.
How a new app gets a values.yaml
Either hand-write apps/<name>/values.yaml (copy an existing one — they are
short), or run discover.yml against the VM the app runs on and it will write
one for you:
ansible-playbook discover.yml -i <vm-host>, -e target_vm=<vm-host> -e app_name=<name>
discover.yml is a thin entry point (assert the args, then include_tasks: tasks/discover.yml). tasks/discover.yml runs read-only over SSH with
become (the app-server dirs are 0700) — it reads the running java
command line and the jdbc:postgresql://… URLs, and (for Tomcat) tars conf/ +
the driver jars into an overlay that the build copies into the image — then
derives:
- runtime —
wildflyif the process is JBoss/WildFly,springbootfor an executable.jarthat isn't Catalina, otherwisetomcat9. hostAliases— the unique non-localhost DB hostnames pulled from those JDBC URLs (these become thehostAliasesthat pin the app's DB hostnames at the sidecar).- health path —
/actuator/healthfor Spring Boot, else a guessed/<app_name>/health.
It renders templates/values.yaml.j2 into apps/<name>/values.yaml: a
deploy-ready file with a flat image: ref (a placeholder
oci.arsalan.io/migrated-apps/<app_name>:latest), plus runtime, port,
health, route, database.hostAliases, and (when found) javaOpts +
packages. You still add the app's apps/<name>/seed.sql and fix anything the
heuristics got wrong (see below).
Known limitations / what you still do by hand
This is a demo/tutorial path. discover.yml gets you a starter values.yaml,
not a finished migration. Be honest about the edges:
- The health path is a guess. For non-Spring-Boot apps it emits
/<app_name>/health, which is wrong whenever the WAR context path differs from the app name (e.g.classic-shopis served at/storefront, so its real health path is/storefront/health). Eyeball and correcthealth.pathafter discover. - Discover does not find the source repo or trigger the build. It writes a
placeholder flat
image:ref and stops. Building is a separate step — run./build.sh <app> <source-dir>locally (podman) or launch the AAPsimple: build appJT (in-cluster buildah); either uses the generic per-runtime Containerfile and pushesoci.arsalan.io/migrated-apps/<app>. You still tell it where the source is, and pointimage:at what you push. - The postgres sidecar is a dev/demo datastore, not production. It uses an
ephemeral
emptyDir(data is lost when the pod restarts) andPOSTGRES_HOST_AUTH_METHOD=trust(no password). For production you would pointhostAliasesat a real external database instead of enabling the sidecar. - PostgreSQL only. Discover only greps for
jdbc:postgresql://URLs and the sidecar ispostgres:16. Other databases are not detected or carried. - Only three runtimes are detected:
springboot,tomcat9,wildfly. Anything else falls through totomcat9and will likely be wrong.
So: discover is a good first pass on a real VM, but treat the values.yaml it writes as a draft to review — don't expect to point it at any VM and deploy the output untouched.
Why this is "simple"
It deletes the things that made the original collection hard to read: the Containerfile renderer, the LLM repair loop, the large generated chart, the fact-schema engine, and all the imperative DB provisioning. What's left is one chart you can read in ten minutes, a values file per app you can read in one, and a thin discover playbook.
The full migration.discovery collection (the separate
ansible-collection-discovery repo) remains for the deep, do-everything case.