src/core.bend

bend-kube-runtime@0.2.0.0 · 408 lines · 81 definitions

1import Base
2import bend-kube-runtime@0.2.0.0/src/json.bend as J
3import bend-kube-runtime@0.2.0.0/src/http.bend as H
4
5# Core
6# ====
7#
8# kube-core in Bend: what a resource is, the requests every verb makes, and
9# the shapes the API answers with. Pure: nothing here touches the network.
10# Objects are dynamic JSON (kube-core's DynamicObject); typed views read
11# their fields with Obj.*.
12
13# A resource kind, as discovery would describe it (kube-core ApiResource).
14type Res is Data:
15  Res{group: String, version: String, kind: String, plural: String, namespaced: Bool}
16
17# Where an Api points: every namespace, one namespace, or cluster scope.
18type Scope is Data:
19  AllNs{}
20  InNs{ns: String}
21  Cluster{}
22
23type Api is Data:
24  Api{res: Res, scope: Scope}
25
26type ListParams is Data:
27  ListParams{label: String, field: String, limit: U32, cont: String, rv: String}
28
29type WatchParams is Data:
30  WatchParams{label: String, field: String, timeout: U32, bookmarks: Bool}
31
32# How a patch body merges (kube-core Patch).
33type PatchKind is Data:
34  PMerge{}
35  PJson{}
36  PStrategic{}
37  PApply{manager: String, force: Bool}
38
39# Well-known resources
40# --------------------
41
42def Res.core(kind: String, plural: String, ns: Bool) -> Res:
43  Res{"", "v1", kind, plural, ns}
44
45def Res.pods() -> Res:
46  Res.core("Pod", "pods", True{})
47
48def Res.config_maps() -> Res:
49  Res.core("ConfigMap", "configmaps", True{})
50
51def Res.secrets() -> Res:
52  Res.core("Secret", "secrets", True{})
53
54def Res.services() -> Res:
55  Res.core("Service", "services", True{})
56
57def Res.namespaces() -> Res:
58  Res.core("Namespace", "namespaces", False{})
59
60def Res.events() -> Res:
61  Res{"events.k8s.io", "v1", "Event", "events", True{}}
62
63def Res.deployments() -> Res:
64  Res{"apps", "v1", "Deployment", "deployments", True{}}
65
66def Res.leases() -> Res:
67  Res{"coordination.k8s.io", "v1", "Lease", "leases", True{}}
68
69def Res.crds() -> Res:
70  Res{"apiextensions.k8s.io", "v1", "CustomResourceDefinition", "customresourcedefinitions", False{}}
71
72def Res.api_version.go(g: String, v: String, core: Bool) -> String:
73  match core:
74    case True{}:
75      v
76    case False{}:
77      g ++ "/" ++ v
78
79def Res.api_version(r: Res) -> String:
80  match r:
81    case Res{+g, v, k, p, n}:
82      Res.api_version.go(g, v, String.is_empty(g))
83
84# Paths
85# -----
86
87def Api.prefix(g: String, v: String, core: Bool) -> String:
88  match core:
89    case True{}:
90      "/api/" ++ v
91    case False{}:
92      "/apis/" ++ g ++ "/" ++ v
93
94def Api.ns_part(s: Scope, namespaced: Bool) -> String:
95  match s namespaced:
96    case InNs{ns} True{}:
97      "/namespaces/" ++ H.Http.encode(ns)
98    case _ _:
99      ""
100
101# The collection's path, as kube-core's url_path: /api/v1/namespaces/x/pods.
102def Api.path(a: Api) -> String:
103  match a:
104    case Api{Res{+g, v, k, p, n}, s}:
105      Api.prefix(g, v, String.is_empty(g)) ++ Api.ns_part(s, n) ++ "/" ++ p
106
107def Api.item(a: Api, name: String) -> String:
108  Api.path(a) ++ "/" ++ H.Http.encode(name)
109
110# Queries
111# -------
112
113type Q is Data:
114  Q{key: String, val: String}
115
116def Query.go.put(k: String, v: String, rest: List<&2, String>, empty: Bool) -> List<&2, String>:
117  match empty:
118    case True{}:
119      rest
120    case False{}:
121      (H.Http.encode(k) ++ "=" ++ H.Http.encode(v)) <> rest
122
123# Drops pairs with an empty value, then joins "?k=v&k=v".
124def Query.go(qs: List<&2, Q>) -> List<&2, String>:
125  match qs:
126    case Nil{}:
127      Nil{}
128    case Con{Q{k, +v}, t}:
129      Query.go.put(k, v, Query.go(t), String.is_empty(v))
130
131def Query.render.go(ps: List<&2, String>) -> String:
132  match ps:
133    case Nil{}:
134      ""
135    case +xs:
136      "?" ++ String.join(xs, "&")
137
138def Query.render(qs: List<&2, Q>) -> String:
139  Query.render.go(Query.go(qs))
140
141def Query.u32.go(+n: U32, zero: Bool) -> String:
142  match zero:
143    case True{}:
144      ""
145    case False{}:
146      U32.show(n)
147
148def Query.u32(+n: U32) -> String:
149  Query.u32.go(n, U32.is_eq(n, 0))
150
151def Query.bool(b: Bool) -> String:
152  match b:
153    case True{}:
154      "true"
155    case False{}:
156      ""
157
158def ListParams.default() -> ListParams:
159  ListParams{"", "", 500, "", ""}
160
161def WatchParams.default() -> WatchParams:
162  WatchParams{"", "", 290, True{}}
163
164def ListParams.query(lp: ListParams) -> List<&2, Q>:
165  match lp:
166    case ListParams{l, f, n, c, rv}:
167      [Q{"labelSelector", l}, Q{"fieldSelector", f}, Q{"limit", Query.u32(n)},
168        Q{"continue", c}, Q{"resourceVersion", rv}]
169
170def WatchParams.query(wp: WatchParams, rv: String) -> List<&2, Q>:
171  match wp:
172    case WatchParams{l, f, t, b}:
173      [Q{"watch", "true"}, Q{"labelSelector", l}, Q{"fieldSelector", f},
174        Q{"timeoutSeconds", Query.u32(t)}, Q{"allowWatchBookmarks", Query.bool(b)},
175        Q{"resourceVersion", rv}]
176
177# Requests
178# --------
179
180def Req.json_headers() -> List<&2, H.Header>:
181  [H.Header{"Accept", "application/json"}]
182
183def Req.body_headers(ct: String) -> List<&2, H.Header>:
184  [H.Header{"Accept", "application/json"}, H.Header{"Content-Type", ct}]
185
186def Req.get(path: String) -> H.Req:
187  H.Req{"GET", path, Req.json_headers(), ""}
188
189def Req.list(a: Api, lp: ListParams) -> H.Req:
190  Req.get(Api.path(a) ++ Query.render(ListParams.query(lp)))
191
192def Req.watch(a: Api, wp: WatchParams, rv: String) -> H.Req:
193  Req.get(Api.path(a) ++ Query.render(WatchParams.query(wp, rv)))
194
195def Req.read(a: Api, name: String) -> H.Req:
196  Req.get(Api.item(a, name))
197
198def Req.create(a: Api, obj: J.Json) -> H.Req:
199  H.Req{"POST", Api.path(a), Req.body_headers("application/json"), J.Json.show(obj)}
200
201def Req.replace(a: Api, name: String, obj: J.Json) -> H.Req:
202  H.Req{"PUT", Api.item(a, name), Req.body_headers("application/json"), J.Json.show(obj)}
203
204def Req.delete(a: Api, name: String) -> H.Req:
205  H.Req{"DELETE", Api.item(a, name), Req.json_headers(), ""}
206
207def PatchKind.content_type(p: PatchKind) -> String:
208  match p:
209    case PMerge{}:
210      "application/merge-patch+json"
211    case PJson{}:
212      "application/json-patch+json"
213    case PStrategic{}:
214      "application/strategic-merge-patch+json"
215    case PApply{m, f}:
216      "application/apply-patch+yaml"
217
218def PatchKind.query(p: PatchKind) -> List<&2, Q>:
219  match p:
220    case PApply{m, f}:
221      [Q{"fieldManager", m}, Q{"force", Query.bool(f)}]
222    case _:
223      Nil{}
224
225def Req.sub.go(s: String, empty: Bool) -> String:
226  match empty:
227    case True{}:
228      ""
229    case False{}:
230      "/" ++ s
231
232def Req.sub(+s: String) -> String:
233  Req.sub.go(s, String.is_empty(s))
234
235# A patch of the object, or of a subresource ("status", "scale") when sub
236# is not empty. Server-side apply (PApply) sends JSON, which is YAML.
237def Req.patch(a: Api, name: String, +sub: String, +p: PatchKind, body: J.Json) -> H.Req:
238  H.Req{"PATCH", Api.item(a, name) ++ Req.sub(sub) ++ Query.render(PatchKind.query(p)),
239    Req.body_headers(PatchKind.content_type(p)), J.Json.show(body)}
240
241def Req.replace_status(a: Api, name: String, obj: J.Json) -> H.Req:
242  H.Req{"PUT", Api.item(a, name) ++ "/status", Req.body_headers("application/json"),
243    J.Json.show(obj)}
244
245# Objects
246# -------
247
248def Obj.meta(o: J.Json, field: String) -> J.Json:
249  J.Json.get(J.Json.get(o, "metadata"), field)
250
251def Obj.name(o: J.Json) -> String:
252  J.Json.str_or(Obj.meta(o, "name"), "")
253
254def Obj.namespace(o: J.Json) -> String:
255  J.Json.str_or(Obj.meta(o, "namespace"), "")
256
257def Obj.uid(o: J.Json) -> String:
258  J.Json.str_or(Obj.meta(o, "uid"), "")
259
260def Obj.rv(o: J.Json) -> String:
261  J.Json.str_or(Obj.meta(o, "resourceVersion"), "")
262
263def Obj.num_text(j: J.Json) -> String:
264  match j:
265    case J.JNum{s}:
266      s
267    case _:
268      ""
269
270def Obj.generation(o: J.Json) -> String:
271  Obj.num_text(Obj.meta(o, "generation"))
272
273def Obj.is_deleting(o: J.Json) -> Bool:
274  Bool.not(J.Json.is_null(Obj.meta(o, "deletionTimestamp")))
275
276def Obj.strs(xs: List<&2, J.Json>) -> List<&2, String>:
277  match xs:
278    case Nil{}:
279      Nil{}
280    case Con{J.JStr{s}, t}:
281      s <> Obj.strs(t)
282    case Con{x, t}:
283      Obj.strs(t)
284
285def Obj.finalizers(o: J.Json) -> List<&2, String>:
286  Obj.strs(J.Json.items(Obj.meta(o, "finalizers")))
287
288def Obj.label(o: J.Json, key: String) -> Maybe<&2, String>:
289  J.Json.as_str(J.Json.get(Obj.meta(o, "labels"), key))
290
291def Obj.key.go(ns: String, name: String, cluster: Bool) -> String:
292  match cluster:
293    case True{}:
294      name
295    case False{}:
296      ns ++ "/" ++ name
297
298def Obj.key.of(+ns: String, name: String) -> String:
299  Obj.key.go(ns, name, String.is_empty(ns))
300
301# The store key, as client-go's MetaNamespaceKeyFunc: "ns/name", or "name"
302# for cluster-scoped objects.
303def Obj.key(+o: J.Json) -> String:
304  Obj.key.of(Obj.namespace(o), Obj.name(o))
305
306# An object reference: what a reconcile is keyed by (kube-runtime ObjectRef).
307type ObjRef is Data:
308  ObjRef{ns: String, name: String}
309
310def ObjRef.key(r: ObjRef) -> String:
311  match r:
312    case ObjRef{ns, name}:
313      Obj.key.of(ns, name)
314
315def ObjRef.of(+o: J.Json) -> ObjRef:
316  ObjRef{Obj.namespace(o), Obj.name(o)}
317
318def Obj.owners.put(ns: String, name: String, hit: Bool, rest: List<&2, ObjRef>) ->
319  List<&2, ObjRef>:
320  match hit:
321    case True{}:
322      ObjRef{ns, name} <> rest
323    case False{}:
324      rest
325
326def Obj.owners.go(xs: List<&2, J.Json>, +ns: String, +kind: String) -> List<&2, ObjRef>:
327  match xs:
328    case Nil{}:
329      Nil{}
330    case Con{+r, t}:
331      Obj.owners.put(ns, J.Json.str_or(J.Json.get(r, "name"), ""),
332        String.eq(J.Json.str_or(J.Json.get(r, "kind"), ""), kind), Obj.owners.go(t, ns, kind))
333
334# The owners an object names of a kind (kube-runtime's trigger_owners): each
335# is a reference in the object's namespace.
336def Obj.owners(+o: J.Json, +kind: String) -> List<&2, ObjRef>:
337  Obj.owners.go(J.Json.items(Obj.meta(o, "ownerReferences")), Obj.namespace(o), kind)
338
339def Res.kind(r: Res) -> String:
340  match r:
341    case Res{g, v, k, p, n}:
342      k
343
344# An owner reference to o, for objects a controller creates.
345def Obj.owner_ref(+o: J.Json, +res: Res) -> J.Json:
346  J.Json.obj([J.KV{"apiVersion", J.JStr{Res.api_version(res)}}, J.KV{"kind", J.JStr{Res.kind(res)}},
347    J.KV{"name", J.JStr{Obj.name(o)}}, J.KV{"uid", J.JStr{Obj.uid(o)}},
348    J.KV{"controller", J.JBool{True{}}}, J.KV{"blockOwnerDeletion", J.JBool{True{}}}])
349
350# Lists and watch events
351# ----------------------
352
353type Page is Data:
354  Page{items: List<&2, J.Json>, rv: String, cont: String}
355
356def Page.of(+j: J.Json) -> Page:
357  Page{J.Json.items(J.Json.get(j, "items")),
358    J.Json.str_or(J.Json.at(j, ["metadata", "resourceVersion"]), ""),
359    J.Json.str_or(J.Json.at(j, ["metadata", "continue"]), "")}
360
361# A metav1.Status: the code, reason and message an error answers with.
362type Status is Data:
363  Status{code: U32, reason: String, message: String}
364
365def Status.of(+j: J.Json) -> Status:
366  Status{Maybe.default(&2, U32, J.Json.as_u32(J.Json.get(j, "code")), 0),
367    J.Json.str_or(J.Json.get(j, "reason"), ""), J.Json.str_or(J.Json.get(j, "message"), "")}
368
369def Status.show(s: Status) -> String:
370  match s:
371    case Status{c, r, m}:
372      U32.show(c) ++ " " ++ r ++ ": " ++ m
373
374type WEvent is Data:
375  WAdded{obj: J.Json}
376  WModified{obj: J.Json}
377  WDeleted{obj: J.Json}
378  WBookmark{rv: String}
379  WError{status: Status}
380  WBad{msg: String}
381
382def WEvent.of.go(t: String, +o: J.Json) -> WEvent:
383  match t:
384    case "ADDED":
385      WAdded{o}
386    case "MODIFIED":
387      WModified{o}
388    case "DELETED":
389      WDeleted{o}
390    case "BOOKMARK":
391      WBookmark{Obj.rv(o)}
392    case "ERROR":
393      WError{Status.of(o)}
394    case _:
395      WBad{"unknown watch event type"}
396
397def WEvent.of(+j: J.Json) -> WEvent:
398  WEvent.of.go(J.Json.str_or(J.Json.get(j, "type"), ""), J.Json.get(j, "object"))
399
400def WEvent.parse.go(r: Result<&2, &2, String, J.Json>) -> WEvent:
401  match r:
402    case Done{j}:
403      WEvent.of(j)
404    case Fail{e}:
405      WBad{e}
406
407def WEvent.parse(line: String) -> WEvent:
408  WEvent.parse.go(J.Json.parse(line))