Start KinD (Kubernetes in Docker) single-node cluster, install the ODH components that concern the IDE team, and run some tests.
Specifically, the components deployed are:
- (ODH) Dashboard
- (ODH) Notebook Controller
- Datascience Pipelines Controller
The conspicuously missing component is of course the ODH Platform Operator.
The deployed Kubernetes manifests are taken directly from individual components' repositories, and the images in the cluster tend to be :latest-tagged builds from OpenShift CI.
See .github/workflows/rhoai-in-kind.yaml
There are various Kubernetes environments where the components could be installed and test be then executed.
| OpenShift feature | OCP Local | OpenShift HCP (Hypershift) | Microshift | OKD | Vanila Kubernetes (KinD) |
|---|---|---|---|---|---|
| Enable/disable integrated image registry | OK | may not be reenabled once disabled | not present | OK | not present |
| Integrated OAuth for oauth-proxy | OK | OK | not present | requires rh subscription | not present |
| SCC, RunAsNonRoot | OK | OK | OK | OK | not present |
| ca-service operator | OK | OK | ? | OK | not present |
| Istio and Kserve operators | OK | OK | ? | requires rh subscription | not present |
| Project | OK | OK | OK | OK | not present |
| Route | OK | OK | different | OK | not present |
This repository focuses on KinD, which is a vanilla Kubernetes. Therefore, what's missing from OpenShift and is required needs to be poly-filled in.
-
Integrated image registry Our notebook-controller handles missing internal image registry; operating without internal image registry is an OpenShift AI feature.
-
oauth-proxy On okd it's necessary to use unauthenticated image, it can be substituted with kustomize
On kubernetes it is necessary to also fake /.well-known/oauth endpoint and the users.user.openshift.io resource
Dashboard uses the special (openshift-only) ~ user instead of the standard SelfSubjectAccessReview in one place. Additionally, Dashboard inspects OpenShift Groups to determine if the user is an admin or not.
It is possible to implement and deploy a kubernetes API extension server and implement the user.openshift.io/v1/users/~ endpoint, already did that
-
RunAsNonRoot prevents running openshift workloads using openshift deployment manifests on kubernetes, because openshift automatically runs some workloads as nonroot while kubernetes requires specifying uid explicitly
This concerns the ca-service operator, that can be run on Kubernetes, but tweaks are necessary.
Either change user in extra layer in Dockerfile (I did that) or use Kyverno to either remove RunAsNonRoot, or set UID in the deployment the operator spawns.
-
Service mesh and Kserve operators are not necessary for Dashboard+Pipelines+Workbenches, so I did not investigate
In brief, the problem is that these are available in redhat-operators OLM catalog which is only for paying customers
To expose services to the outside world, I am using a minimal Istio install with the Gateway API. Any OpenShift route that actually needs to be exposed requires manually creating a Gateway API equivalent next to it to be actually exposed.
-
Project and Route (and ProjectRequest and User)
These are required for the ODH Dashboard to function. Namespace alone will not suffice, a Project must be present.
I tried spoofing stuff with the API extension server, but so far did not make it work. I can make the Dashboard usable enough for running the Elyra integration test already, but I can't run the (selected, workbench focused) Dashboard e2e tests ok Kubernetes yet.
CRDs that only need to be present (CRUD operations on them) but need not be functional, can be implemented with a fake CRD.
Might get out-of-date, see .github/workflows/rhoai-in-kind.yaml for authoritative steps.
For rhoai-2.25, use notebooks tag v1.36.0.
This is what went into the corresponding ODH release
# On Apple Silicon, enable Rosetta so the VM can run the amd64-only images (see note below).
# Rosetta is set via containers.conf (there is no `podman machine` CLI flag); it is the
# default on recent Podman — check with `podman machine inspect --format '{{.Rosetta}}'`.
# Enable it once if needed, before creating the machine:
mkdir -p ~/.config/containers && printf '[machine]\nrosetta = true\n' >> ~/.config/containers/containers.conf
podman machine init --rootful --memory $((16 * 1024)) --cpus 4
podman machine start
kind create cluster --config components/00-kind-cluster.yaml --image docker.io/kindest/node:v1.34.11@sha256:44e222ee2132dab25ff87301682f89eb82c7880ea3a1bf543bfe9708fd08d67d
python3 components/deploy.py --workbench-branch=v1.36.0Apple Silicon (arm64) note: several images deployed here are published for
amd64only (e.g.api-extension, the data-science-pipelines-operator). Under plain QEMU emulation their Go binaries crash at startup (lfstack.push invalid packing/SIGSEGVinasm_amd64.s). The fix is to enable Rosetta 2 in the Podman machine, which translates amd64 correctly:
- Rosetta is enabled via
[machine] rosetta = truein~/.config/containers/containers.conf(there is nopodman machineCLI flag for it). On recent Podman this is the default — check withpodman machine inspect --format '{{.Rosetta}}', so hand-editing the config is often unnecessary.- Applying it to an existing machine only needs a restart, not a recreate:
podman machine stop && podman machine start(thepodman machine rmis not required — a restart re-provisions the Rosetta share and preserves your kind cluster).- Verify inside the VM:
podman machine ssh "cat /proc/sys/fs/binfmt_misc/rosetta"should reportenabled.With Rosetta on, the amd64 images run as-is. As an alternative you can build
api-extensionnatively for arm64:podman build --platform linux/arm64 -t quay.io/jdanek/api-extension:latest -f components/api-extension/Dockerfile components/api-extension/thenkind load docker-image quay.io/jdanek/api-extension:latest.
What does it do? This, among other things, in order to setup argocd access
PASSWORD=$(kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d)
export ARGOCD_OPTS='--insecure --port-forward --port-forward-namespace argocd'
argocd login --core
argocd cluster add kind-kind --yes
kubectl apply -f components/03-kf-pipelines.yaml
argocd app sync kf-pipelinesSetting up the environment using this repo requires fast internet connection, otherwise things tend to timeout.
If you're getting timeouts from kubectl apply, increase the timeout encoded in the URL.
If you're getting timeouts while syncing ArgoCD applications, try increasing the git clone timeout in components/01-argocd/gittimeoutconfig.yaml.
We will have the ability to do shallow clones in ArgoCD 3.0.
Services are exposed under sslip.io wildcard hostnames such as
minio.apps.127.0.0.1.sslip.io, which are supposed to resolve to 127.0.0.1.
If curl -k https://minio.apps.127.0.0.1.sslip.io hangs or deploy.py fails in
create_buckets() with NameResolutionError / EndpointConnectionError — while
the cluster gateway is actually up — the problem is host DNS resolution, not the
cluster.
Two common macOS causes:
- A per-domain resolver in
/etc/resolver/points at a local DNS that isn't running. A file like/etc/resolver/127.0.0.1.sslip.iocontainingnameserver 127.0.0.1routes every*.127.0.0.1.sslip.iolookup to127.0.0.1:53. If you don't have a local resolver (dnsmasq/CoreDNS) listening there, resolution fails. - DNS-rebinding protection in Tailscale MagicDNS or some corporate/VPN resolvers, which drops answers pointing to a loopback address.
Diagnostic commands (macOS):
# What the system resolver (curl/python/boto3) actually returns — empty means it refuses the name
dscacheutil -q host -a name minio.apps.127.0.0.1.sslip.io
# Direct query, bypassing /etc/resolver overrides (sslip.io should answer 127.0.0.1)
nslookup minio.apps.127.0.0.1.sslip.io
# Inspect resolver scopes and per-domain nameservers
scutil --dns
ls -la /etc/resolver/ && cat /etc/resolver/*
# Prove the gateway works by bypassing DNS entirely (403 from MinIO = reachable)
curl -k --resolve minio.apps.127.0.0.1.sslip.io:443:127.0.0.1 https://minio.apps.127.0.0.1.sslip.io/
# Is a local resolver actually listening on :53?
sudo lsof -nP -iUDP:53 -iTCP:53Fixes:
-
If you rely on a local sslip.io resolver, make sure it is running.
-
Otherwise remove the stale override so normal DNS (which resolves sslip.io to
127.0.0.1) is used, and flush the cache:sudo rm /etc/resolver/127.0.0.1.sslip.io sudo dscacheutil -flushcache && sudo killall -HUP mDNSResponder -
If Tailscale is intercepting DNS:
tailscale set --accept-dns=false.
The most robust setup is a local dnsmasq that maps the whole wildcard to loopback — it works offline, is fast, and sidesteps rebinding filters. This is the setup this repo was developed with:
brew install dnsmasq
# Homebrew's prefix differs by arch (/opt/homebrew on Apple Silicon, /usr/local on Intel).
BREW_PREFIX="$(brew --prefix)"
# 1. Map every *.127.0.0.1.sslip.io host to 127.0.0.1.
# `brew services` starts dnsmasq with `-7 "$BREW_PREFIX/etc/dnsmasq.d",*.conf`,
# so any *.conf here is loaded automatically (no conf-dir edit needed).
echo 'address=/.127.0.0.1.sslip.io/127.0.0.1' > "$BREW_PREFIX/etc/dnsmasq.d/sslip.conf"
# 2. Route *.127.0.0.1.sslip.io lookups to the local resolver
echo 'nameserver 127.0.0.1' | sudo tee /etc/resolver/127.0.0.1.sslip.io
# 3. Start dnsmasq (needs sudo to bind port 53)
sudo brew services start dnsmasqVerify:
dscacheutil -q host -a name minio.apps.127.0.0.1.sslip.io # should print 127.0.0.1The usual failure mode is simply that the dnsmasq service isn't running (e.g. after
a reboot if it wasn't enabled): sudo brew services start dnsmasq fixes it.