Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"

Expand Down
61 changes: 61 additions & 0 deletions flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

106 changes: 106 additions & 0 deletions flake.nix
Original file line number Diff line number Diff line change
@@ -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"
'';
};
});
}
11 changes: 11 additions & 0 deletions shell.nix
Original file line number Diff line number Diff line change
@@ -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
15 changes: 10 additions & 5 deletions src/helpers.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down
Loading