-
Notifications
You must be signed in to change notification settings - Fork 0
/
api.clj
298 lines (242 loc) · 9.83 KB
/
api.clj
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
(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 `KUBECONFIG` before generating fallback one from
;;; the user `HOME`
(defonce ^:private default-kube-config-path
(or (System/getenv "KUBECONFIG")
(str (System/getenv "HOME") "/.kube/config")))
(def running-from-kube?
"Detect if the application is currently running on k8s
platform, or not"
(and (.exists (io/file default-cluster-service-account-dir))
(every? some? [default-k8s-svc-host default-k8s-svc-port])))
(defn- is-current-gke?
"Detect if the current context is a GKE cluster.
Logic is based on `gke_*` naming scheme"
[current-context]
(str/starts-with? current-context "gke_"))
;;; Authentification
(defn mk-client
"Generic client builder
There is several ways to instantiate the client:
1. Without any arguments, the function will fetch several env variables
to build the client. This method is useful for applications that run directly
in a Kubernetes Pod, or for executions context where env variables are used for
configurations, required env variables are:
- `KUBERNETES_SERVICE_HOST` - the Kubernetes Service Endpoint
- `KUBERNETES_SERVICE_HOST` - the Kubernetes Service Endpoint Port
- `KUBERNETES_TOKEN` - the actual Kubernetes token to be used
See: https://docs.selectel.com/cloud/managed-kubernetes/instructions/service-account-token
2. The more straightforward, and easy to setup is from a map spec directly, every
elements are explicitly used for client building (probably this will be fetched
from your component management system like Integrant.
3. The last one is from, configuration file directly. The builder will auto-detect
if the current context is a GKE cluster. If so, you'll need to be sure that the
`GOOGLE_APPLICATION_CREDENTIALS` variable is set and corretly pointing to your SA
for fetching token from API.
WARN: If not running on GKE, you'll need to explicitly setup your token variable to use
with your current context.
Note: This methods is used by default if the builder isn't call with arguments
AND all variables are not satisfied."
{: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
(defmacro with-api-context
"A helper macro to wrap *api-context*
with default values."
[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
(defn cluster-version
"Retrieve remote cluster build informations"
{:added "1.25.8.2"}
([spec]
(with-api-context spec
(kv/get-code-version))))
;;; Namespaces
(defn create-namespace
"Create a new 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)))))
(defn get-namespace
"Retrieve informations about the selected 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)))))
(defn delete-namespace
"Delete the selected 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
(defn get-endpoints
"Fetches the specified endpoints or
returns nil if not found"
{: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)))))
(defn create-endpoint
"Create a new 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
(defn list-pods
"List or watch 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)))))))
(defn get-pod
"Retrieve pod informations"
{: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)))))
(defn delete-pod
"Delete a pod by his name"
{: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)))))
(defn pod-logs
"Reads the logs of a specific pod"
{: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
(defn get-job
"Fetches the specified job or returns nil if not found"
{: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)))))
(defn list-jobs
"List or watch 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)))))))
(defn submit-job
"Submits a job for execution"
{: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)))))
(defn delete-job
"Deletes a 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)))))
(defn job-pods
"Fetches all of the pods belonging to a specific job"
{: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)
[]))))