From 53572d31f31916b05e7b776c0e8c2ef4afcae8e6 Mon Sep 17 00:00:00 2001 From: tan Date: Thu, 2 Jul 2026 13:05:12 +0530 Subject: [PATCH 1/3] Add Nix dev shell for running tests against a local kind cluster Provides a reproducible environment (kubectl, kind) plus helper scripts (kuber-cluster-up/proxy/test/cluster-down) to run the integration suite against an isolated, pinned kind cluster. KUBECONFIG is isolated so it never reads or clobbers a system cluster (e.g. k3s) config. Julia is not pinned by Nix; it is taken from the host PATH (juliaup), since nix develop / nix-shell are impure by default. flake.nix is the primary entry point (nix develop); shell.nix is a flake-compat shim for non-flake nix-shell users. Verified end-to-end: suite passes 69/69 against the pinned kindest/node:v1.35.0 using juliaup's julia. --- flake.lock | 61 ++++++++++++++++++++++++++++++ flake.nix | 106 +++++++++++++++++++++++++++++++++++++++++++++++++++++ shell.nix | 11 ++++++ 3 files changed, 178 insertions(+) create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 shell.nix diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..118c857 --- /dev/null +++ b/flake.lock @@ -0,0 +1,61 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1782888527, + "narHash": "sha256-ry+llIN/SGgvPrvHaGaXb/tOHfTWtkmspDu28pgsitc=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "f76e4c7b1840704deda511ab34f37b829f6b5636", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..96ef7a8 --- /dev/null +++ b/flake.nix @@ -0,0 +1,106 @@ +{ + description = "Dev shell for testing Kuber.jl against a local kind cluster"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = { self, nixpkgs, flake-utils }: + flake-utils.lib.eachDefaultSystem (system: + let + pkgs = nixpkgs.legacyPackages.${system}; + + # Cluster/proxy settings shared by the helper scripts below. + clusterName = "kuber-test"; + proxyPort = "8001"; # test/runtests.jl hardcodes http://localhost:8001 + # Pinned kind node image. Kuber.jl's bundled OpenAPI client is generated + # against k8s ~1.24 (see gen/spec/k8s_1_24_openapi_v3.json), but the test + # suite also passes against modern servers: API-group discovery logs and + # skips groups the client doesn't know rather than erroring. v1.35.0 is + # the image shipped with the pinned kind (0.31.0), so it pulls by digest + # with no version-skew warning. Change this to e.g. kindest/node:v1.24.17 + # to test against the exact version the client was generated from. + nodeImage = "kindest/node:v1.35.0"; + + # Isolated kubeconfig so kind/kubectl never touch (or depend on) an + # existing cluster's config. Without this, on a host whose KUBECONFIG + # points at a read-only file (e.g. a system k3s at + # /etc/rancher/k3s/k3s.yaml), `kind create cluster` fails to write its + # kubeconfig AND `kubectl proxy` silently falls back to the current + # context - so the suite would run against the wrong cluster. + kubeconfig = "$HOME/.kube/kind-${clusterName}.config"; + + cluster-up = pkgs.writeShellScriptBin "kuber-cluster-up" '' + set -euo pipefail + export KUBECONFIG="${kubeconfig}" + if kind get clusters 2>/dev/null | grep -qx "${clusterName}"; then + echo "kind cluster '${clusterName}' already exists." + else + echo "Creating kind cluster '${clusterName}' (${nodeImage})..." + kind create cluster --name "${clusterName}" --image "${nodeImage}" + fi + kubectl cluster-info --context "kind-${clusterName}" + echo + echo "Next: run 'kuber-proxy' in this shell, then 'kuber-test' in another." + ''; + + cluster-down = pkgs.writeShellScriptBin "kuber-cluster-down" '' + set -euo pipefail + export KUBECONFIG="${kubeconfig}" + kind delete cluster --name "${clusterName}" + ''; + + proxy = pkgs.writeShellScriptBin "kuber-proxy" '' + set -euo pipefail + export KUBECONFIG="${kubeconfig}" + kubectl config use-context "kind-${clusterName}" + echo "Serving Kubernetes API at http://localhost:${proxyPort} (Ctrl-C to stop)..." + exec kubectl proxy --port=${proxyPort} + ''; + + run-tests = pkgs.writeShellScriptBin "kuber-test" '' + set -euo pipefail + cd "''${KUBER_ROOT:-$PWD}" + exec julia --project -e 'using Pkg; Pkg.instantiate(); Pkg.test()' + ''; + in { + devShells.default = pkgs.mkShell { + # Julia is intentionally NOT provided here - use your own juliaup-managed + # `julia` from the host PATH (nix develop / nix-shell are impure by + # default, so it stays available). This keeps the toolchain under + # juliaup's control rather than pinning a nixpkgs Julia. + packages = [ + pkgs.kubectl + pkgs.kind + cluster-up + cluster-down + proxy + run-tests + ]; + + shellHook = '' + export KUBER_ROOT="$PWD" + # Point all kubectl/kind at an isolated kubeconfig for this project so + # we never read or clobber a system cluster (e.g. k3s) config. + export KUBECONFIG="${kubeconfig}" + echo "Kuber.jl test shell" + echo " KUBECONFIG=${kubeconfig} (isolated - kind cluster only)" + echo " Requires a running Docker daemon on the host (kind shells out to 'docker')." + if ! docker info >/dev/null 2>&1; then + echo " WARNING: 'docker info' failed - start Docker before 'kuber-cluster-up'." + fi + if command -v julia >/dev/null 2>&1; then + echo " julia: $(command -v julia) (provided externally, e.g. juliaup)" + else + echo " WARNING: no 'julia' on PATH - install it via juliaup before 'kuber-test'." + fi + echo + echo " kuber-cluster-up create the kind cluster (k8s pinned to ${nodeImage})" + echo " kuber-proxy serve the API at http://localhost:${proxyPort} (run in its own terminal)" + echo " kuber-test run the Kuber.jl test suite" + echo " kuber-cluster-down tear the cluster down" + ''; + }; + }); +} diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..fc08064 --- /dev/null +++ b/shell.nix @@ -0,0 +1,11 @@ +# Compatibility shim for `nix-shell` users who don't have flakes enabled. +# Delegates to the devShell defined in flake.nix (pinned via flake.lock) so both +# entry points give the identical environment. Prefer `nix develop` if you can. +(import ( + fetchTarball { + url = "https://github.com/edolstra/flake-compat/archive/refs/tags/v1.1.0.tar.gz"; + sha256 = "19d2z6xsvpxm184m41qrpi1bplilwipgnzv9jy17fgw421785q1m"; + } +) { + src = ./.; +}).shellNix.default From e6a3db58fd659e247560cfcfd6ce03542a0580ab Mon Sep 17 00:00:00 2001 From: tan Date: Thu, 2 Jul 2026 13:29:17 +0530 Subject: [PATCH 2/3] Support JSON.jl 1.x JSON 1.x parses objects to JSON.Object rather than the concrete Dict{String,Any} that OpenAPI's from_json and Kuber's own helpers dispatch on. Force dicttype=Dict{String,Any} at Kuber's parse sites (a no-op on 0.21, the fix on 1.x) and widen the compat bound. Verified against a kind cluster (k8s v1.35.0): 64/64 tests pass on both JSON 1.6.1 and 0.21.4. --- Project.toml | 2 +- src/helpers.jl | 15 ++++++++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/Project.toml b/Project.toml index 1271299..1ff33a7 100644 --- a/Project.toml +++ b/Project.toml @@ -17,7 +17,7 @@ TimeZones = "f269a46b-ccf7-5d73-abea-4c690281aa53" [compat] Downloads = "1" OpenAPI = "0.1,0.2" -JSON = "0.21" +JSON = "0.21, 1" TimeZones = "1" julia = "1" diff --git a/src/helpers.jl b/src/helpers.jl index eff6e63..d7eb2da 100644 --- a/src/helpers.jl +++ b/src/helpers.jl @@ -190,9 +190,14 @@ function with_timeout(fn, ctx::Union{KuberContext,KuberWatchContext}, timeout::I end end +# JSON 1.x parses objects to JSON.Object; both OpenAPI's from_json and Kuber's own +# helpers dispatch on the concrete Dict{String,Any}. dicttype forces that on 1.x and +# is a no-op on 0.21 (already the default), so one path serves both. +_parse_json(x) = JSON.parse(x; dicttype=Dict{String,Any}) + convert(::Type{Vector{UInt8}}, s::T) where {T<:AbstractString} = collect(codeunits(s)) -convert(::Type{T}, json::String) where {T<:OpenAPI.APIModel} = convert(T, JSON.parse(json)) -convert(::Type{Dict{String,Any}}, model::T) where {T<:OpenAPI.APIModel} = JSON.parse(JSON.json(model)) +convert(::Type{T}, json::String) where {T<:OpenAPI.APIModel} = convert(T, _parse_json(json)) +convert(::Type{Dict{String,Any}}, model::T) where {T<:OpenAPI.APIModel} = _parse_json(JSON.json(model)) is_json_mime(mime::T) where {T <: AbstractString} = ("*/*" == mime) || occursin(r"(?i)application/json(;.*)?", mime) || occursin(r"(?i)application/(.*)-patch\+json(;.*)?", mime) @@ -203,11 +208,11 @@ function kind_to_type(ctx::KuberContext, kind::Symbol, version::Union{String,Not end kuber_type(ctx::KuberContext, d) = kuber_type(ctx, Any, d) -kuber_type(ctx::KuberContext, T, data::String) = kuber_type(ctx, T, JSON.parse(data)) +kuber_type(ctx::KuberContext, T, data::String) = kuber_type(ctx, T, _parse_json(data)) function kuber_type(ctx::KuberContext, return_types::Dict{Regex,Type}, response_code::Union{Nothing,Integer}, response_data::String) default_type = OpenAPI.Clients.get_api_return_type(return_types, response_code, response_data) try - json_resp = JSON.parse(response_data) + json_resp = _parse_json(response_data) return kuber_type(ctx, default_type, json_resp) catch return default_type @@ -239,7 +244,7 @@ end # OpenAPI conversions insist that JSONs objects are always `Dict{String,Any}`. # To ensure that for a user supplied Dict, we serialize that to string and parse it back as json. kuber_obj(ctx::KuberContext, j::Dict{String,Any}) = kuber_obj(ctx, JSON.json(j)) -kuber_obj(ctx::KuberContext, data::String) = _kuber_obj(ctx, JSON.parse(data)) +kuber_obj(ctx::KuberContext, data::String) = _kuber_obj(ctx, _parse_json(data)) _kuber_obj(ctx::KuberContext, j::Dict{String,Any}) = convert(kind_to_type(ctx, j["kind"], get(j, "apiVersion", nothing)), j) show(io::IO, ctx::KuberContext) = print(io, "Kubernetes namespace ", ctx.namespace, " at ", ctx.client.root) From b87b589014e299c1b17b80b52d27d78819908fbb Mon Sep 17 00:00:00 2001 From: tan Date: Thu, 2 Jul 2026 13:29:53 +0530 Subject: [PATCH 3/3] bump patch version for tagging --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 1ff33a7..d0c229b 100644 --- a/Project.toml +++ b/Project.toml @@ -4,7 +4,7 @@ authors = ["JuliaHub Inc."] keywords = ["kubernetes", "client"] license = "MIT" desc = "Julia Kubernetes Client" -version = "0.7.8" +version = "0.7.9" [deps] Dates = "ade2ca70-3891-5945-98fb-dc099432e06a"