Skip to content

fix: make k3d local dev cluster commands work on Windows - #887

Open
cordeirops wants to merge 3 commits into
kubeflow:mainfrom
cordeirops:fix/k3d-windows-compat
Open

fix: make k3d local dev cluster commands work on Windows#887
cordeirops wants to merge 3 commits into
kubeflow:mainfrom
cordeirops:fix/k3d-windows-compat

Conversation

@cordeirops

Copy link
Copy Markdown
Contributor

Hi all, could someone take a look at this? I ran into a few issues getting the k3d local dev cluster working on Windows and put together fixes for them.

Summary

Follow-up to #753. The kfp-dev-* Make targets (and the dev/jupyter
targets they sit alongside) only worked on Linux/macOS — running them on a
native Windows shell (cmd.exe, no WSL/git-bash) failed at multiple points.
This fixes that while keeping Linux/macOS behavior unchanged, and documents
the k3d workflow in CONTRIBUTING.md (it was never documented in #753).

Changes

  • scripts/kfp-dev-setup.shscripts/kfp-dev-setup.py: ported the
    cluster setup/start/stop/delete/status/upgrade script to Python. The bash
    script relied on kill, pkill, background jobs (&), and
    set -euo pipefail, none of which are available on native Windows without
    WSL or git-bash.

    • Fixes a host.docker.internal kubeconfig entry that k3d writes but that
      kubectl can't resolve/reach from native Windows, rewriting it to
      127.0.0.1 after cluster creation/context switch.
    • Process kill falls back to taskkill /F on Windows when os.kill isn't
      enough.
  • Makefile:

    • kfp-dev-* targets now call python scripts/kfp-dev-setup.py instead of
      bash scripts/kfp-dev-setup.sh.
    • UV now falls back to %USERPROFILE%\.local\bin\uv.exe on Windows when
      uv isn't found on PATH — a freshly-installed uv doesn't show up in
      an already-open terminal's PATH until the terminal is restarted.
    • check-uv and dev gained Windows branches for the lines that used
      bash-only syntax (printf, inline VAR=val cmd, command -v, { }
      grouping) — cmd.exe doesn't understand any of it.
    • dev's labextension link step creates a directory junction (mklink /J)
      directly on Windows instead of calling jupyter labextension develop --overwrite ., which fails there with OSError: Cannot call rmtree on a symbolic link (Python's shutil.rmtree refuses to remove a Windows
      directory symlink/junction).
  • CONTRIBUTING.md / docs/source/contributing.md: document the k3d
    local dev cluster workflow (make kfp-dev-setupkfp-dev-start
    dev/jupyter), which had no documentation since feat(dev): add lightweight KFP local dev cluster via k3d #753.

No change to Linux/macOS code paths — every Windows branch is gated behind
$(OS) == Windows_NT / sys.platform == 'win32'.

Test plan

  • make kfp-dev-setup — cluster create, KFP deploy, port-forward — on
    Windows (cmd.exe)
  • make dev — uv sync, labextension build + link, pre-commit — on
    Windows (cmd.exe)
  • Linux/macOS regression pass (unchanged code paths, but worth a sanity
    run before merge)

- Port scripts/kfp-dev-setup.sh to Python (kfp-dev-setup.py); bash-only
  syntax (kill, pkill, background jobs) doesn't run under Windows cmd.exe
  without WSL/git-bash.
- Fix kubeconfig host.docker.internal -> 127.0.0.1 rewrite so kubectl on
  Windows can reach the k3d API server.
- Add Windows branches to check-uv/dev Makefile targets (uv install,
  PATH fallback, labextension junction instead of symlink) so the full
  `make dev && make jupyter` flow works cross-platform.

Signed-off-by: Pedro Sbaraini Cordeiro <pedro.sbarainicordeiro@gmail.com>
@cordeirops cordeirops changed the title fix(dev): make k3d local dev cluster commands work on Windows fix: make k3d local dev cluster commands work on Windows Jul 19, 2026

@ederign ederign left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See my comments iunline. We mostly need to guard the kubeconfig fix behind sys.platform == "win32" and scope it to the specific cluster context.

Comment thread scripts/kfp-dev-setup.py
Comment thread Makefile Outdated
… Windows only

Address review feedback on PR kubeflow#887 from @ederign:

- fix_kubeconfig_host_docker_internal() now guards on
  sys.platform == "win32" (the DNS resolution issue it works around is
  Windows-only) and rewrites only the k3d-<cluster_name> cluster entry
  via `kubectl config set-cluster`, instead of blindly replacing every
  occurrence of host.docker.internal anywhere in the kubeconfig file.
  Verified this no longer touches unrelated contexts (e.g.
  docker-desktop) that happen to also use host.docker.internal.

- Makefile: replaced the hardcoded `.venv` path in the labextension
  junction step with a `VENV_DIR ?= .venv` variable.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Pedro Sbaraini Cordeiro <pedro.sbarainicordeiro@gmail.com>
@google-oss-prow

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
Once this PR has been reviewed and has the lgtm label, please ask for approval from ederign. For more information see the Kubernetes Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@ederign

ederign commented Jul 25, 2026

Copy link
Copy Markdown
Member

@cordeirops shell script is so much fun! :)

The two things I asked for are both correctly addressed. fix_kubeconfig_host_docker_internal is properly guarded and now scoped via kubectl config set-cluster instead of rewriting the file, which is better than what I'd suggested, and VENV_DIR is in.

I checked the branch out locally and hit two new problems, both on the Linux/macOS side. That matches the one unchecked box in your test plan. CI is green but doesn't cover either, so there was no way for you to have seen them from Windows.

1. make dev silently stops installing the pre-commit hook (Makefile:79)

The comma in "To enable, run: git config..." is a top-level comma inside $(if ...), so GNU make reads it as the argument separator. The Windows message gets truncated and everything after the comma becomes the Unix branch. Result:

$ make -n dev | grep 'pre-commit install'
run: git config --unset core.hooksPath, uv run pre-commit install 2>/dev/null || { printf ... }

The command name is now run:. uv run pre-commit install is just an argument — it never runs. It's invisible because the 2>/dev/null you had for pre-commit now swallows sh: run: not found, and || returns 0, so make reports success.

Visible in your own CI run 29958441793:

Note: pre-commit hooks not installed (core.hooksPath is set globally).

versus the same step on main:

pre-commit installed at .git/hooks/pre-commit

core.hooksPath isn't set on those runners, so that message is a false positive from the fallback branch.

Fix — drop the comma:

-		$(UV) run pre-commit install 2>nul || echo Note: ... To enable, run: git config --unset core.hooksPath, \
+		$(UV) run pre-commit install 2>nul || echo Note: ... To enable run: git config --unset core.hooksPath, \

Worth a general note: any $(if ...) branch here must avoid top-level commas, or wrap them in $(comma).

2. All six kfp-dev-* targets fail on macOS/Debian (Makefile:190-205)

They call bare python, which doesn't exist on stock macOS (Apple removed it in 12.3) or Debian/Ubuntu without python-is-python3:

$ make kfp-dev-setup
/bin/sh: python: command not found
make: *** [kfp-dev-setup] Error 127

It can't just become python3, since on Windows that's the Microsoft Store app-execution alias. Suggest the same pattern you already used for UV:

PYTHON := python3
ifeq ($(OS),Windows_NT)
    PYTHON := python
endif

then $(PYTHON) scripts/kfp-dev-setup.py in all six. ($(UV) run python would also work and matches Makefile:267, but adds a uv dependency to targets that don't need one.)

Please verify on Windows

I've only confirmed the Unix side plus a simulated make OS=Windows_NT -n dev, which is not the real thing:

  • make dev prints the complete "To enable run: git config --unset core.hooksPath" hint (cmd.exe's echo treats the comma as a plain character, so removing it shouldn't matter, but confirm the message isn't mangled).
  • make dev actually reaches pre-commit install and reports either success or a genuine failure.
  • make kfp-dev-setup / -start / -status still resolve python correctly.

Two smaller things while you're in here

  • stop_port_forward (kfp-dev-setup.py:320): the Windows filter is Name='kubectl.exe' and CommandLine like '%port-forward%ml-pipeline-ui%' with no port, while the Unix pkill includes .*{local_port}. So it force-kills every ml-pipeline-ui port-forward, including one against a real cluster. Same scoping issue as the kubeconfig one.
  • status_cluster (:471): except Exception: warn("Cluster not running or unreachable") is unreachable — subprocess.run without check=True doesn't raise on non-zero. An unreachable cluster now dumps kubectl's raw error instead of the friendly message the old Makefile printed. Same pattern for the uncaught CalledProcessError in create_cluster / apply_kfp_manifests, which give a traceback where bash set -e gave a clean message.

Neither is a blocker. Fix 1 and 2 and I'm happy to /lgtm.

Address remaining feedback from @ederign on kfp-dev-* Windows compat:

- Makefile: the pre-commit fallback message had a literal comma inside
  a $(if ...) call, which GNU make parses as the true/false-branch
  separator. This silently truncated the Windows branch and merged
  the Unix branch's leading text into it, turning `run:` into the
  command name and swallowing `uv run pre-commit install` entirely
  (masked by the existing 2>nul). Verified with `make -n dev` that both
  branches now resolve to the intended single command.

- Makefile: kfp-dev-* targets called bare `python`, which doesn't exist
  on stock macOS (removed in 12.3) or Debian/Ubuntu without
  python-is-python3. Added a PYTHON variable (python3 by default,
  python on Windows, since python3 there resolves to the Microsoft
  Store app-execution alias) and used it in all six targets. Verified
  with `make -n kfp-dev-setup OS=` that the Unix branch now resolves to
  `python3`.

- kfp-dev-setup.py: the Windows port-forward cleanup filter matched
  any kubectl port-forward for ml-pipeline-ui, unlike the Unix pkill
  pattern which scopes by port. Added the port to the WMI filter so it
  only kills the port-forward this script owns.

- kfp-dev-setup.py: status_cluster's `except Exception` after a
  subprocess.run() call without check=True was unreachable dead code
  (subprocess.run doesn't raise on non-zero exit by default), so an
  unreachable cluster fell through to kubectl's raw error instead of
  the friendly message. Added check=True so the except actually fires.

- kfp-dev-setup.py: wrapped the main() entry point in a single
  try/except CalledProcessError so failed kubectl/k3d commands
  (e.g. in create_cluster/apply_kfp_manifests) print a clean error via
  die() instead of a raw Python traceback, matching the old bash
  script's `set -e` behavior.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Pedro Sbaraini Cordeiro <pedro.sbarainicordeiro@gmail.com>
@cordeirops

Copy link
Copy Markdown
Contributor Author

Hi @ederign, thanks for the thorough second pass! All four items fixed in 6729330:

  1. Pre-commit comma bug: the comma inside $(if ...) was indeed getting parsed as the branch separator, silently truncating the Windows message and swallowing uv run pre-commit install entirely. Removed the comma. Verified with make -n dev that the full command now resolves correctly.

  2. python to PYTHON variable: added PYTHON := python3, overridden to python only on Windows_NT, and swapped it into all six kfp-dev-* targets. Verified with make -n kfp-dev-setup OS= that it resolves to python3 on the non-Windows branch.

  3. Port-forward filter scoping: added {local_port} to the Windows WMI filter so it only kills the port-forward this script started, matching the Unix pkill pattern.

  4. Dead exception handler and raw tracebacks: status_cluster now passes check=True so the "Cluster not running or unreachable" message actually fires instead of being dead code, and main() is wrapped in a single try/except CalledProcessError so any kubectl/k3d failure prints a clean die() message instead of a Python traceback.

I do not have a real Linux/macOS box to test on, so I have only confirmed the Windows side directly and the Unix branch via make -n ... OS= (text only, confirms the Makefile picks the right branch, not that it actually runs there). If you are able to sanity check make dev and make kfp-dev-setup on macOS or Linux, that would close the loop. Otherwise happy to have someone else confirm before merge.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants