Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

kubereq API improvements

proposals/2024-09.livemd

kubereq API improvements

Mix.install([
  {:kubereq, github: "mruoss/kubereq", ref: "compile-time-resource-path-lookup"},
  # {:kubereq, path: "/Users/mruoss/src/community/kubereq"},
  {:kino, "~> 0.14.1"}
])

Motivation

Currently, the kubereq library is used as follows:

kubeconfig = Kubereq.Kubeconfig.load(Kubereq.Kubeconfig.Default)
sa_req = Kubereq.new(kubeconfig, "api/v1/namespaces/:namespace/serviceaccounts/:name")

Kubereq.get(sa_req, "default", "default") |> Kino.Tree.new()

I wrote kubereq and kubegen a few months ago but never got around to really use it. Now that I have used kubereq for the K8s runtime in Livebook, I feel like the API needs some minor adaptions.

One part that calls for abstraction is loading the Kubeconfig. Most people probably get by with the default pipeline so while still allowing for explicit override, we can set it as default.

Also, line 2 in the code above is a problem to me. I can never remember this path and always had to look it up or copy it. And I know Kubernetes quite well. For somebody with less deep knowledge of the API behind kubectl, it’s probably not super straight forward to find out what is required.

Proposal 1: Attach function with kubeconfig as option

We can offer Kubereq.attach/2 where the second argument is an options Keyword list. The only option at the moment is :kubeconfig. If it is not defined, the default config Kubereq.Kubeconfig.Default is loaded. Otherwise, the user can pass their own pipeline or an already loaded %Kubereq.Kubeconfig{} struct.

# loads Kubereq.Kubeconfig.Default implicitely:
req1 = Req.new() |> Kubereq.attach()

# pass Kubeconfnig pipeline which will be loaded by `Kubereq.attach/2`:
req2 =
  Req.new()
  |> Kubereq.attach(kubeconfig: {Kubereq.Kubeconfig.File, path: "~/.kube/config"})

# pass loaded %Kubereq.Kubeconfig{} struct to `Kubereq.attach/2`:
kubeconfig =
  Kubereq.Kubeconfig.load({Kubereq.Kubeconfig.File, path: "~/.kube/config"})
req3 =
  Req.new()
  |> Kubereq.attach(kubeconfig: kubeconfig)

Proposal 2: Allow setting resources instead of resource path

People know kubectl and resource manifest YAML files. They know api_version and kind. However, the API expects the plural form of the resource in the REST path, not the kind. Luckily, Kubernetes provides its resource definitions in form of JSON discovery files within their repo. I can download them and “generate” a lookup function which at least works for “core” resources. For CRDs we can still allow to set the path directly or fall back to a resource discovery call to the cluster at runtime like the k8s library does.

We offer Kubereq.set_resource/4 which sets the resource path by looking up the required information from the discovery files:

sa_req =
  Req.new()
  |> Kubereq.attach(kubeconfig: kubeconfig, api_version: "v1", kind: "ServiceAccount")

Req.request(sa_req, operation: :get, path_params: [namespace: "default", name: "default"])

# syntactig sugar:
Kubereq.get(sa_req, "default", "default")

The fourth argument takes a subresource as optional argument:

sa_req =
  Req.new()
  |> Kubereq.attach(kubeconfig: kubeconfig, api_version: "v1", kind: "Namespace")

Req.request(sa_req, operation: :get, path_params: [name: "default"])

# syntactig sugar:
Kubereq.get(sa_req, "default", "default", subresource: "status")

For CRDs, Kubereq needs to discover the resource name from the cluster:

req = Req.new() |> Kubereq.attach(kubeconfig: kubeconfig)

# download and apply cert-manager CRD
crds =
  Req.get!(req,
    url:
      "https://github.com/cert-manager/cert-manager/releases/download/v1.15.3/cert-manager.crds.yaml"
  ).body

crd =
  crds
  |> YamlElixir.read_all_from_string!()
  |> Enum.find(& &1["spec"]["names"]["kind"] == "Certificate")

{:ok, _} =
  Kubereq.apply(req, crd,
    api_version: "apiextensions.k8s.io/v1",
    kind: "CustomResourceDefinition"
  )

# list certificates in all namespaces
{:ok, _} = Kubereq.list(req, api_version: "cert-manager.io/v1", kind: "Certificate")