dependencies
| (this space intentionally left almost blank) | ||||||||||||||||||||||||||||||
(ns clj-k8s.utils (:require [clojure.string :as str])) | |||||||||||||||||||||||||||||||
===================================== Utils | |||||||||||||||||||||||||||||||
(defn find-named [x xs] (some #(when (= (:name %) x) %) xs)) | |||||||||||||||||||||||||||||||
(defmacro not-found->nil [& body] `(try ~@body (catch Throwable t# (if (= 404 (-> t# ex-data :status)) nil (throw t#))))) | |||||||||||||||||||||||||||||||
(defn ->label-selector [labels] (if (string? labels) labels (->> labels (map (fn [[k v]] (format "%s=%s" (name k) v))) (str/join ",")))) | |||||||||||||||||||||||||||||||
Kubernetes Related Predicates | |||||||||||||||||||||||||||||||
(defn- current-condition [{:keys [conditions]}] (->> conditions (filter #(= (:status %) "True")) first)) | |||||||||||||||||||||||||||||||
Returns true if the job is still running | (defn running? [{:keys [status]}] (boolean (and status (nil? (current-condition status))))) | ||||||||||||||||||||||||||||||
Returns true if the job was successful | (defn succeeded? [{:keys [status]}] (= (:type (current-condition status)) "Complete")) | ||||||||||||||||||||||||||||||
Returns true if the job failed | (defn failed? [{:keys [status]}] (or (nil? status) (= (:type (current-condition status)) "Failed"))) | ||||||||||||||||||||||||||||||
(ns clj-k8s.models (:require [schema.core :as s])) | |||||||||||||||||||||||||||||||
========================================== Models | |||||||||||||||||||||||||||||||
(s/defschema KubeClientSpec {:token s/Str :base-url s/Str (s/optional-key :namespace) s/Str}) | |||||||||||||||||||||||||||||||
(s/defschema KubeClient {:base-url s/Str :auths {s/Str s/Str} :namespace s/Str}) | |||||||||||||||||||||||||||||||
(ns clj-k8s.gke (:import (com.google.auth.oauth2 GoogleCredentials) (java.util List))) | |||||||||||||||||||||||||||||||
===================================== Google Kubernetes Engine | |||||||||||||||||||||||||||||||
See: https://developers.google.com/identity/protocols/googlescopes#containerv1 | (defonce gke-scopes ["https://www.googleapis.com/auth/cloud-platform"]) | ||||||||||||||||||||||||||||||
(def ^GoogleCredentials creds (delay (let [app-creds (GoogleCredentials/getApplicationDefault)] (if (.createScopedRequired app-creds) (.createScoped app-creds ^List gke-scopes) app-creds)))) | |||||||||||||||||||||||||||||||
Refresh the token by discarding the cached token and metadata and requesting the new ones if expired | (defn refresh-goog-token! [^GoogleCredentials creds] (.refreshIfExpired creds) creds) | ||||||||||||||||||||||||||||||
Fetches the google access token | (def get-google-access-token (fn [] (some-> @creds refresh-goog-token! .getAccessToken .getTokenValue))) | ||||||||||||||||||||||||||||||
(ns clj-k8s.api (:require [yaml.core :as yaml] [schema.core :as s] [clj-k8s.gke :as gke] [clj-k8s.utils :refer [find-named not-found->nil ->label-selector]] [clj-k8s.models :refer :all] [clojure.java.io :as io] [clojure.string :as str] [kubernetes.core :as k] [kubernetes.api.version :as kv] [kubernetes.api.core-v- :as kc] [kubernetes.api.batch-v- :as kb])) | |||||||||||||||||||||||||||||||
===================================== Public API | |||||||||||||||||||||||||||||||
Constants | |||||||||||||||||||||||||||||||
(defonce ^:private default-ns "default") | |||||||||||||||||||||||||||||||
For application running in Kubernetes context only | (defonce ^:private default-k8s-svc-host (System/getenv "KUBERNETES_SERVICE_HOST")) (defonce ^:private default-k8s-svc-port (System/getenv "KUBERNETES_SERVICE_PORT")) | ||||||||||||||||||||||||||||||
See: https://cloud.google.com/docs/authentication/application-default-credentials?hl=fr | (defonce ^:private default-cluster-service-account-dir (or (System/getenv "GOOGLE_APPLICATION_CREDENTIALS") "/var/run/secrets/kubernetes.io/serviceaccount")) | ||||||||||||||||||||||||||||||
Kubernetes Token | (defonce ^:private token-from-env (str (System/getenv "KUBERNETES_TOKEN"))) | ||||||||||||||||||||||||||||||
Honor | (defonce ^:private default-kube-config-path (or (System/getenv "KUBECONFIG") (str (System/getenv "HOME") "/.kube/config"))) | ||||||||||||||||||||||||||||||
Detect if the application is currently running on k8s platform, or not | (def running-from-kube? (and (.exists (io/file default-cluster-service-account-dir)) (every? some? [default-k8s-svc-host default-k8s-svc-port]))) | ||||||||||||||||||||||||||||||
Detect if the current context is a GKE cluster.
Logic is based on | (defn- is-current-gke? [current-context] (str/starts-with? current-context "gke_")) | ||||||||||||||||||||||||||||||
Authentification | |||||||||||||||||||||||||||||||
Generic client builder There is several ways to instantiate the client:
| (defn mk-client {:added "1.25.8.2"} ([] (if (and default-k8s-svc-host default-k8s-svc-port (not-empty token-from-env)) (s/validate KubeClient {:base-url default-k8s-svc-host :auths {"BearerToken" (str "Bearer " token-from-env)} :namespace default-ns}) (mk-client default-kube-config-path))) ([kube-config] (if (map? kube-config) (let [{:keys [base-url token namespace]} (s/validate KubeClientSpec kube-config)] (s/validate KubeClient {:base-url base-url :auths {"BearerToken" (str "Bearer " token)} :namespace (or namespace default-ns)})) (mk-client kube-config token-from-env))) ([kube-config token] (let [{:keys [clusters contexts current-context]} (yaml/from-file kube-config) context (find-named current-context contexts) cluster (find-named (get-in context [:context :cluster]) clusters) token (if (is-current-gke? current-context) (gke/get-google-access-token) token)] {:base-url (get-in cluster [:cluster :server]) :auths {"BearerToken" (str "Bearer " token)} :namespace (get-in context [:context :namespace] default-ns)}))) | ||||||||||||||||||||||||||||||
Api Utils | |||||||||||||||||||||||||||||||
A helper macro to wrap api-context with default values. | (defmacro with-api-context [api-context & body] `(let [api-context# ~api-context api-context# (-> k/*api-context* (merge api-context#) (assoc :auths (merge (:auths k/*api-context*) (:auths api-context#))))] (binding [k/*api-context* api-context#] ~@body))) | ||||||||||||||||||||||||||||||
(defn active-ns [spec] (with-api-context spec (:namespace k/*api-context*))) | |||||||||||||||||||||||||||||||
Version | |||||||||||||||||||||||||||||||
Retrieve remote cluster build informations | (defn cluster-version {:added "1.25.8.2"} ([spec] (with-api-context spec (kv/get-code-version)))) | ||||||||||||||||||||||||||||||
Namespaces | |||||||||||||||||||||||||||||||
Create a new namespace | (defn create-namespace {:added "1.25.8.2"} ([spec ns-spec] (create-namespace spec ns-spec {})) ([spec ns-spec opts] (let [ns-spec (if (string? ns-spec) {:metadata {:name ns-spec}} ns-spec)] (with-api-context spec (kc/create-core-v1-namespace ns-spec opts))))) | ||||||||||||||||||||||||||||||
Retrieve informations about the selected namespace | (defn get-namespace {:added "1.25.8.2"} ([spec n] (get-namespace spec n {})) ([spec n opts] (with-api-context spec (not-found->nil (kc/read-core-v1-namespace n opts))))) | ||||||||||||||||||||||||||||||
Delete the selected namespace | (defn delete-namespace {:added "1.25.8.2"} ([spec ns] (delete-namespace spec ns {})) ([spec ns opts] (with-api-context spec (not-found->nil (kc/delete-core-v1-namespace ns opts))))) | ||||||||||||||||||||||||||||||
Endpoints | |||||||||||||||||||||||||||||||
Fetches the specified endpoints or returns nil if not found | (defn get-endpoints {:added "1.25.8.2"} ([spec ns] (get-endpoints spec ns {})) ([spec ep-name {:keys [namespace] :or {namespace default-ns} :as opts}] (with-api-context spec (not-found->nil (kc/read-core-v1-namespaced-endpoints ep-name namespace opts))))) | ||||||||||||||||||||||||||||||
Create a new endpoint | (defn create-endpoint {:added "1.25.8.2"} ([spec ep-spec] (create-endpoint spec ep-spec {})) ([spec ep-spec {:keys [namespace] :or {namespace default-ns} :as opts}] (with-api-context spec (not-found->nil (kc/create-core-v1-namespaced-endpoints ep-spec namespace opts))))) | ||||||||||||||||||||||||||||||
Pods | |||||||||||||||||||||||||||||||
List or watch pods | (defn list-pods {:added "1.25.8.2"} ([spec] (list-pods spec {})) ([spec {:keys [namespace all-namespaces] :or {namespace default-ns} :as opts}] (with-api-context spec (let [opts (update opts :label-selector ->label-selector)] (:items (if all-namespaces (kc/list-core-v1-pod-for-all-namespaces opts) (kc/list-core-v1-namespaced-pod namespace opts))))))) | ||||||||||||||||||||||||||||||
Retrieve pod informations | (defn get-pod {:added "1.25.8.2"} ([spec pod-name] (get-pod spec pod-name {})) ([spec pod-name {:keys [namespace] :or {namespace default-ns} :as opts}] (with-api-context spec (not-found->nil (kc/read-core-v1-namespaced-pod pod-name namespace opts))))) | ||||||||||||||||||||||||||||||
Delete a pod by his name | (defn delete-pod {:added "1.25.8.2"} ([spec pod-name] (get-pod spec pod-name {})) ([spec pod-name {:keys [namespace] :or {namespace default-ns} :as opts}] (with-api-context spec (not-found->nil (kc/delete-core-v1-namespaced-pod pod-name namespace opts))))) | ||||||||||||||||||||||||||||||
Reads the logs of a specific pod | (defn pod-logs {:added "1.25.8.2"} ([spec n] (pod-logs spec n {})) ([spec n {:keys [namespace] :or {namespace default-ns} :as opts}] (with-api-context spec (not-found->nil (kc/read-core-v1-namespaced-pod-log n namespace opts))))) | ||||||||||||||||||||||||||||||
Jobs Batch | |||||||||||||||||||||||||||||||
Fetches the specified job or returns nil if not found | (defn get-job {:added "1.25.8.2"} ([spec n] (get-job spec n {})) ([spec n {:keys [namespace] :or {namespace default-ns} :as opts}] (with-api-context spec (not-found->nil (kb/read-batch-v1-namespaced-job n namespace opts))))) | ||||||||||||||||||||||||||||||
List or watch jobs | (defn list-jobs {:added "1.25.8.2"} ([spec] (list-jobs spec {})) ([spec {:keys [namespace all-namespaces] :or {namespace default-ns} :as opts}] (with-api-context spec (let [opts (update opts :label-selector ->label-selector)] (:items (if all-namespaces (kb/list-batch-v1-job-for-all-namespaces opts) (kb/list-batch-v1-namespaced-job namespace opts))))))) | ||||||||||||||||||||||||||||||
Submits a job for execution | (defn submit-job {:added "1.25.8.2"} ([spec job-spec] (submit-job spec job-spec {})) ([spec job-spec opts] (with-api-context spec (let [job-ns (get-in job-spec [:metadata :namespace] default-ns)] (kb/create-batch-v1-namespaced-job job-ns job-spec opts))))) | ||||||||||||||||||||||||||||||
Deletes a job | (defn delete-job {:added "1.25.8.2"} ([spec n] (delete-job spec n {})) ([spec n {:keys [namespace] :or {namespace default-ns} :as opts}] (with-api-context spec (let [opts (merge {:propagation-policy "Foreground"} opts)] (kb/delete-batch-v1-namespaced-job n namespace opts))))) | ||||||||||||||||||||||||||||||
Fetches all of the pods belonging to a specific job | (defn job-pods {:added "1.25.8.2"} ([spec n] (job-pods spec n {})) ([spec n opts] (with-api-context spec (if-let [job (get-job spec n opts)] (->> (get-in job [:spec :selector :matchLabels :controller-uid]) (str "controller-uid=") (assoc opts :label-selector) (kc/list-core-v1-namespaced-pod (get-in job [:metadata :namespace])) :items) [])))) | ||||||||||||||||||||||||||||||