conlink 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dockerignore +5 -0
- package/Dockerfile +34 -0
- package/LICENSE +373 -0
- package/README.md +485 -0
- package/TODO +34 -0
- package/conlink +11 -0
- package/conlink-start.sh +172 -0
- package/examples/dot.js +36 -0
- package/examples/index.html +11 -0
- package/examples/net2dot.yaml +21 -0
- package/examples/test1-compose.yaml +60 -0
- package/examples/test2-compose.yaml +31 -0
- package/examples/test2-network.yaml +5 -0
- package/examples/test3-network.yaml +5 -0
- package/examples/test4-multiple/all-compose.yaml +5 -0
- package/examples/test4-multiple/base-compose.yaml +25 -0
- package/examples/test4-multiple/node1-compose.yaml +17 -0
- package/examples/test4-multiple/nodes2-compose.yaml +20 -0
- package/examples/test4-multiple/web-network.yaml +2 -0
- package/examples/test5-geneve-compose.yaml +31 -0
- package/examples/test6-cfn.yaml +184 -0
- package/examples/test7-compose.yaml +31 -0
- package/examples/test8-compose.yaml +35 -0
- package/host-build.yaml +1 -0
- package/inspect.json +210 -0
- package/link-add.sh +197 -0
- package/link-del.sh +60 -0
- package/net2dot +11 -0
- package/notes.txt +82 -0
- package/old/Dockerfile.bak +26 -0
- package/old/add-link.sh +82 -0
- package/old/conlink +12 -0
- package/old/conlink.cljs +131 -0
- package/old/dot_gitignore +1 -0
- package/old/examples/test2-compose.yaml +32 -0
- package/old/examples/test2-network.yaml +42 -0
- package/old/move-link.sh +108 -0
- package/old/net2dot.py +122 -0
- package/old/notes-old.txt +97 -0
- package/old/package.json +16 -0
- package/old/schema.yaml +138 -0
- package/old/schema.yaml.bak +76 -0
- package/old/test2b-compose.yaml +18 -0
- package/old/veth-link.sh +96 -0
- package/package.json +15 -0
- package/schema-ish.yaml +29 -0
- package/schema.yaml +71 -0
- package/shadow-cljs.edn +33 -0
- package/src/conlink/addrs.cljc +63 -0
- package/src/conlink/core.cljs +772 -0
- package/src/conlink/net2dot.cljs +158 -0
- package/src/conlink/util.cljs +140 -0
- package/tests/invalid-schema-1.yaml +6 -0
- package/tests/invalid-schema-2.yaml +6 -0
- package/tests/invalid-schema-3.yaml +17 -0
- package/tests/invalid-schema-4.yaml +14 -0
- package/tests/invalid-schema-5.yaml +12 -0
- package/tests/invalid-schema-6.yaml +12 -0
- package/tmp/conlink/.env +1 -0
|
@@ -0,0 +1,772 @@
|
|
|
1
|
+
#!/usr/bin/env nbb
|
|
2
|
+
|
|
3
|
+
(ns conlink.core
|
|
4
|
+
(:require [clojure.string :as S]
|
|
5
|
+
[clojure.pprint :refer [pprint]]
|
|
6
|
+
[promesa.core :as P]
|
|
7
|
+
[cljs-bean.core :refer [->clj ->js]]
|
|
8
|
+
[conlink.util :refer [parse-opts Eprintln fatal
|
|
9
|
+
trim indent interpolate-walk deep-merge
|
|
10
|
+
spawn read-file load-config]]
|
|
11
|
+
[conlink.addrs :as addrs]
|
|
12
|
+
#_["ajv$default" :as Ajv]
|
|
13
|
+
#_["dockerode$default" :as Docker]))
|
|
14
|
+
|
|
15
|
+
;; TODO: use require syntax when shadow-cljs works with "*$default"
|
|
16
|
+
(def Ajv (js/require "ajv"))
|
|
17
|
+
(def Docker (js/require "dockerode"))
|
|
18
|
+
|
|
19
|
+
(def usage "
|
|
20
|
+
conlink: advanced container layer 2/3 linking/networking.
|
|
21
|
+
|
|
22
|
+
Usage:
|
|
23
|
+
conlink [options]
|
|
24
|
+
|
|
25
|
+
General Options:
|
|
26
|
+
-v, --verbose Show verbose output (stderr)
|
|
27
|
+
[env: VERBOSE]
|
|
28
|
+
--bridge-mode BRIDGE-MODE Bridge mode (ovs or linux) to use for
|
|
29
|
+
bridge/switch connections
|
|
30
|
+
[default: ovs]
|
|
31
|
+
--network-file NETWORK-FILE... Network config file
|
|
32
|
+
--compose-file COMPOSE-FILE... Docker compose file with network config
|
|
33
|
+
--compose-project NAME Docker compose project name for resolving
|
|
34
|
+
link :service keys (if conlink is a
|
|
35
|
+
compose service then this defaults to
|
|
36
|
+
the current compose project name)
|
|
37
|
+
--config-schema SCHEMA-FILE JSON schema file for validating network config
|
|
38
|
+
[default: schema.yaml]
|
|
39
|
+
--docker-socket PATH Docker socket to listen to
|
|
40
|
+
[default: /var/run/docker.sock]
|
|
41
|
+
--podman-socket PATH Podman socket to listen to
|
|
42
|
+
[default: /var/run/podman/podman.sock]
|
|
43
|
+
")
|
|
44
|
+
|
|
45
|
+
;; TODO: :service should require either command line option or
|
|
46
|
+
;; detection of running in a compose project (but not both).
|
|
47
|
+
|
|
48
|
+
(def OVS-START-CMD (str "/usr/share/openvswitch/scripts/ovs-ctl start"
|
|
49
|
+
" --system-id=random --no-mlockall --delete-bridges"))
|
|
50
|
+
|
|
51
|
+
(def VLAN-TYPES #{:vlan :macvlan :macvtap :ipvlan :ipvtap})
|
|
52
|
+
(def LINK-ADD-OPTS [:ip :mac :route :mtu :nat :netem :mode :vlanid :remote :vni])
|
|
53
|
+
(def INTF-MAX-LEN 15)
|
|
54
|
+
|
|
55
|
+
(def ctx (atom {:error #(apply Eprintln "ERROR:" %&)
|
|
56
|
+
:warn #(apply Eprintln "WARNING:" %&)
|
|
57
|
+
:log Eprintln
|
|
58
|
+
:info list}))
|
|
59
|
+
|
|
60
|
+
;; Simple utility functions
|
|
61
|
+
(defn json-str [obj]
|
|
62
|
+
(js/JSON.stringify (->js obj)))
|
|
63
|
+
|
|
64
|
+
(def conjv (fnil conj []))
|
|
65
|
+
|
|
66
|
+
(defn indent-pprint-str [o pre]
|
|
67
|
+
(indent (trim (with-out-str (pprint o))) pre))
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
(defn load-configs
|
|
71
|
+
"Load network configs from a list of compose file paths and a list
|
|
72
|
+
of network config file paths. Network configs in compose files are
|
|
73
|
+
under the 'x-network' top-level key or as an 'x-network' property of
|
|
74
|
+
services. The network configs are merged together into a single
|
|
75
|
+
network configuration that is returned."
|
|
76
|
+
[comp-cfgs net-cfgs]
|
|
77
|
+
(P/let [comp-cfgs (P/all (map load-config comp-cfgs))
|
|
78
|
+
xnet-cfgs (mapcat #(into [(:x-network %)]
|
|
79
|
+
(for [[s sd] (:services %)
|
|
80
|
+
:let [cfg (:x-network sd)]]
|
|
81
|
+
;; current service is default
|
|
82
|
+
(assoc cfg :links
|
|
83
|
+
(for [l (:links cfg)]
|
|
84
|
+
(merge {:service (name s)} l)))))
|
|
85
|
+
comp-cfgs)
|
|
86
|
+
net-cfgs (P/all (map load-config net-cfgs))
|
|
87
|
+
net-cfg (reduce deep-merge {} (concat xnet-cfgs net-cfgs))]
|
|
88
|
+
net-cfg))
|
|
89
|
+
|
|
90
|
+
(defn enrich-link
|
|
91
|
+
"Add default values to a link:
|
|
92
|
+
- type: veth
|
|
93
|
+
- dev: eth0
|
|
94
|
+
- mtu: 9000 (for non *vlan type)
|
|
95
|
+
- base: :conlink for veth type, :host for *vlan types, :local otherwise"
|
|
96
|
+
[{:as link :keys [type base bridge ip vlanid]}]
|
|
97
|
+
(let [type (keyword (or type "veth"))
|
|
98
|
+
base-default (cond (= :veth type) :conlink
|
|
99
|
+
(VLAN-TYPES type) :host
|
|
100
|
+
:else :local)
|
|
101
|
+
base (get link :base base-default)
|
|
102
|
+
link (merge
|
|
103
|
+
link
|
|
104
|
+
{:type type
|
|
105
|
+
:dev (get link :dev "eth0")
|
|
106
|
+
:base base}
|
|
107
|
+
(when (not (VLAN-TYPES type))
|
|
108
|
+
{:mtu (get link :mtu 9000)}))]
|
|
109
|
+
link))
|
|
110
|
+
|
|
111
|
+
(defn enrich-network-config
|
|
112
|
+
"Validate and update each link (enrich-link) and add
|
|
113
|
+
:containers and :services maps with restructured link and command
|
|
114
|
+
configuration to provide a more efficient structure for looking up
|
|
115
|
+
configuration later."
|
|
116
|
+
[{:as cfg :keys [links commands]}]
|
|
117
|
+
(let [links (vec (map enrich-link links))
|
|
118
|
+
cfg (merge cfg {:links links :containers {} :services {}})
|
|
119
|
+
rfn (fn [kind cfg {:as x :keys [container service]}]
|
|
120
|
+
(cond-> cfg
|
|
121
|
+
container (update-in [:containers container kind] conjv x)
|
|
122
|
+
service (update-in [:services service kind] conjv x)))
|
|
123
|
+
cfg (reduce (partial rfn :links) cfg links)
|
|
124
|
+
cfg (reduce (partial rfn :commands) cfg commands)]
|
|
125
|
+
cfg))
|
|
126
|
+
|
|
127
|
+
(defn ajv-error-to-str [error]
|
|
128
|
+
(let [path (:instancePath error)
|
|
129
|
+
params (dissoc (:params error) :type :pattern :missingProperty)]
|
|
130
|
+
(str " " (if (not (empty? path)) path "/")
|
|
131
|
+
" " (:message error)
|
|
132
|
+
(if (not (empty? params)) (str " " params) ""))))
|
|
133
|
+
|
|
134
|
+
(defn check-schema [data schema verbose]
|
|
135
|
+
(let [{:keys [info warn]} @ctx
|
|
136
|
+
ajv (Ajv. #js {:allErrors true})
|
|
137
|
+
validator (.compile ajv (->js schema))
|
|
138
|
+
valid (validator (->js data))]
|
|
139
|
+
(if valid
|
|
140
|
+
data
|
|
141
|
+
(let [errors (-> validator .-errors ->clj)
|
|
142
|
+
msg (if verbose
|
|
143
|
+
(indent-pprint-str errors " ")
|
|
144
|
+
(S/join "\n" (map ajv-error-to-str errors)))]
|
|
145
|
+
(fatal 1 (str "\nError during schema validation:\n"
|
|
146
|
+
(when verbose
|
|
147
|
+
"\nUser config:\n" (indent-pprint-str data " "))
|
|
148
|
+
"\nValidation errors:\n" msg))))))
|
|
149
|
+
|
|
150
|
+
(defn gen-network-state
|
|
151
|
+
"Generate network state/context from network configuration. Adds
|
|
152
|
+
empty :devices map and :bridges map containing nil status for
|
|
153
|
+
each bridge mentioned in the network config :links and :tunnels."
|
|
154
|
+
[{:keys [links tunnels]}]
|
|
155
|
+
(reduce (fn [state bridge]
|
|
156
|
+
(assoc-in state [:bridges bridge :status] nil))
|
|
157
|
+
{:devices {} :bridges {}}
|
|
158
|
+
(keep :bridge (concat links tunnels))))
|
|
159
|
+
|
|
160
|
+
(defn link-outer-dev
|
|
161
|
+
"outer-dev format:
|
|
162
|
+
- standalone: container '-' dev
|
|
163
|
+
- compose: service '_' index '-' dev
|
|
164
|
+
- len > 15: 'c' cid[0:8] '-' dev[0:5]"
|
|
165
|
+
[{:as link :keys [container service dev]} cid index]
|
|
166
|
+
(let [oif (str (if service (str service "_" index) container) "-" dev)
|
|
167
|
+
oif (if (<= (count oif) INTF-MAX-LEN)
|
|
168
|
+
oif
|
|
169
|
+
(str "c" (.substring cid 0 8) "-" (.substring dev 0 5)))]
|
|
170
|
+
oif))
|
|
171
|
+
|
|
172
|
+
(defn link-add-offset
|
|
173
|
+
"Add offset value to ip and mac keys in a link definition to account
|
|
174
|
+
for multiple instances of that link i.e. a compose service with
|
|
175
|
+
multiple replicas (scale >= 2)."
|
|
176
|
+
[{:as link :keys [ip mac]} offset]
|
|
177
|
+
;; TODO: add vlanid
|
|
178
|
+
(let [mac (when mac
|
|
179
|
+
(addrs/int->mac
|
|
180
|
+
(+ offset (addrs/mac->int mac))))
|
|
181
|
+
ip (when ip
|
|
182
|
+
(let [[ip prefix] (S/split ip #"/")]
|
|
183
|
+
(str (addrs/int->ip
|
|
184
|
+
(+ offset (addrs/ip->int ip)))
|
|
185
|
+
"/" prefix)))]
|
|
186
|
+
(merge link (when mac {:mac mac}) (when ip {:ip ip}))))
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
(defn link-instance-enrich
|
|
190
|
+
"Add/update properties of a specific runtime link instance using the
|
|
191
|
+
container properties from an event and the current pid of the
|
|
192
|
+
network container. Updates iterable properties of the link
|
|
193
|
+
(via link-add-offset) and adds the following keys:
|
|
194
|
+
- :container - the container properties (passed in)
|
|
195
|
+
- :outer-pid - PID of the network namespace (passed in)
|
|
196
|
+
- :pid - PID of this container
|
|
197
|
+
- :dev-id - container name + container interface name
|
|
198
|
+
- :outer-dev - outer interface name for veth and *vlan link types"
|
|
199
|
+
[link container self-pid]
|
|
200
|
+
(let [{:keys [id pid index name]} container
|
|
201
|
+
dev-id (str name ":" (:dev link))
|
|
202
|
+
outer-pid (condp = (:base link)
|
|
203
|
+
:conlink self-pid
|
|
204
|
+
:host 1
|
|
205
|
+
:local nil)
|
|
206
|
+
link (link-add-offset link (dec index))
|
|
207
|
+
link (if (and outer-pid (not (:outer-dev link)))
|
|
208
|
+
(assoc link :outer-dev (link-outer-dev link id index))
|
|
209
|
+
link)
|
|
210
|
+
link (merge link {:container container
|
|
211
|
+
:dev-id dev-id
|
|
212
|
+
:pid pid
|
|
213
|
+
:outer-pid outer-pid})]
|
|
214
|
+
link))
|
|
215
|
+
|
|
216
|
+
(defn tunnel-instance-enrich
|
|
217
|
+
[tunnel self-pid]
|
|
218
|
+
(let [dev (str (:type tunnel) "-" (:vni tunnel))]
|
|
219
|
+
(merge tunnel {:dev dev
|
|
220
|
+
:outer-dev dev
|
|
221
|
+
:dev-id dev
|
|
222
|
+
:pid self-pid})))
|
|
223
|
+
|
|
224
|
+
;;; General commands
|
|
225
|
+
|
|
226
|
+
(defn run
|
|
227
|
+
"Run/spawn a shell command with result logging. If :quiet is not set
|
|
228
|
+
then print indented results (success to stdout, failure to stderr).
|
|
229
|
+
If :id is set then it will be included in the results. Returns
|
|
230
|
+
command result (whether failure or success)."
|
|
231
|
+
[cmd & [{:as opts :keys [quiet id]}]]
|
|
232
|
+
(P/let [{:keys [info warn]} @ctx
|
|
233
|
+
id (if id (str " (" id ")") "")
|
|
234
|
+
_ (when (not quiet) (info (str "Running" id ": " cmd)))
|
|
235
|
+
res (P/catch (spawn cmd) #(identity %))]
|
|
236
|
+
(P/do
|
|
237
|
+
(when (not quiet)
|
|
238
|
+
(if (= 0 (:code res))
|
|
239
|
+
(when (not (empty? (:stdout res)))
|
|
240
|
+
(info (str "Result" id ":\n"
|
|
241
|
+
(indent (:stdout res) " "))))
|
|
242
|
+
(warn (str "[code: " (:code res) "]" id ":\n"
|
|
243
|
+
(indent (:stdout res) " ") "\n"
|
|
244
|
+
(indent (:stderr res) " ")))))
|
|
245
|
+
res)))
|
|
246
|
+
|
|
247
|
+
(defn run*
|
|
248
|
+
"Like run but runs each cmd in cmds. Returns final cmd result or if
|
|
249
|
+
a cmd fails then returns that cmd's result."
|
|
250
|
+
[cmds opts]
|
|
251
|
+
(P/loop [cmds cmds]
|
|
252
|
+
(P/let [[cmd & cmds] cmds
|
|
253
|
+
res (run cmd opts)]
|
|
254
|
+
(if (and (= 0 (:code res)) (seq cmds))
|
|
255
|
+
(P/recur cmds)
|
|
256
|
+
res))))
|
|
257
|
+
|
|
258
|
+
(defn rename-docker-eth0
|
|
259
|
+
"If eth0 exists, then rename it to DOCKER-ETH0 to prevent 'RTNETLINK
|
|
260
|
+
answers: File exists' errors during creation of links that use
|
|
261
|
+
'eth0' device name. This is necessary because even if the netns is
|
|
262
|
+
specified with the same link create command, the creation and move
|
|
263
|
+
does not appear to be idempotent and results in the conflict."
|
|
264
|
+
[]
|
|
265
|
+
(P/let [{:keys [log]} @ctx
|
|
266
|
+
res (run "[ -d /sys/class/net/eth0 ]" {:quiet true})]
|
|
267
|
+
(if (not= 0 (:code res))
|
|
268
|
+
(log "No eth0 docker network interface detected")
|
|
269
|
+
(P/let [_ (log "Renaming eth0 to DOCKER-ETH0")
|
|
270
|
+
res (run* [(str "ip route save dev eth0 > /tmp/routesave")
|
|
271
|
+
(str "ip link set eth0 down")
|
|
272
|
+
(str "ip link set eth0 name DOCKER-ETH0")
|
|
273
|
+
(str "ip link set DOCKER-ETH0 up")
|
|
274
|
+
(str "ip route restore < /tmp/routesave")]
|
|
275
|
+
{:id "rename"})]
|
|
276
|
+
(when (not= 0 (:code res))
|
|
277
|
+
(fatal 1 "Could not rename docker eth0 interface"))))))
|
|
278
|
+
|
|
279
|
+
(defn start-ovs
|
|
280
|
+
"Start and initialize the openvswitch daemons. Exit with error if it
|
|
281
|
+
can't be started."
|
|
282
|
+
[]
|
|
283
|
+
(P/let [res (run OVS-START-CMD)]
|
|
284
|
+
(if (not= 0 (:code res))
|
|
285
|
+
(fatal 1 (str "Failed starting OVS: " (:stderr res)))
|
|
286
|
+
res)))
|
|
287
|
+
|
|
288
|
+
(defn kmod-loaded?
|
|
289
|
+
"Return whether kernel module 'kmod' is loaded."
|
|
290
|
+
[kmod]
|
|
291
|
+
(P/let [cmd (str "grep -o '^" kmod "\\>' /proc/modules")
|
|
292
|
+
res (run cmd {:quiet true})]
|
|
293
|
+
(and (= 0 (:code res)) (= kmod (trim (:stdout res))))))
|
|
294
|
+
|
|
295
|
+
;;; Link and bridge commands
|
|
296
|
+
|
|
297
|
+
(defn check-no-bridge
|
|
298
|
+
"Check that no bridge named 'bridge' is currently configured.
|
|
299
|
+
Bridge type is dependent on bridge-mode (:ovs or :linux). Exit with
|
|
300
|
+
error if the bridge already exists."
|
|
301
|
+
[bridge]
|
|
302
|
+
(P/let [{:keys [info bridge-mode]} @ctx
|
|
303
|
+
cmd (get {:ovs (str "ovs-vsctl list-ifaces " bridge)
|
|
304
|
+
:linux (str "ip link show type bridge " bridge)}
|
|
305
|
+
bridge-mode)
|
|
306
|
+
res (run cmd {:quiet true})]
|
|
307
|
+
(if (= 0 (:code res))
|
|
308
|
+
;; TODO: maybe mark as :exists and use without cleanup
|
|
309
|
+
(fatal 1 (str "Bridge " bridge " already exists"))
|
|
310
|
+
(if (re-seq #"(does not exist|no bridge named)" (:stderr res))
|
|
311
|
+
true
|
|
312
|
+
(fatal 1 (str "Unable to run '" cmd "': " (:stderr res)))))))
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
(defn bridge-create
|
|
316
|
+
"Create a bridge named 'bridge'.
|
|
317
|
+
Bridge type is dependent on bridge-mode (:ovs or :linux)."
|
|
318
|
+
[bridge]
|
|
319
|
+
(P/let [{:keys [info error bridge-mode]} @ctx
|
|
320
|
+
_ (info "Creating bridge/switch" bridge)
|
|
321
|
+
cmd (get {:ovs (str "ovs-vsctl add-br " bridge)
|
|
322
|
+
:linux (str "ip link add " bridge " up type bridge")}
|
|
323
|
+
bridge-mode)
|
|
324
|
+
res (run cmd)]
|
|
325
|
+
(if (not= 0 (:code res))
|
|
326
|
+
(error (str "Unable to create bridge/switch " bridge))
|
|
327
|
+
(swap! ctx assoc-in [:network-state :bridges bridge :status] :created))
|
|
328
|
+
res))
|
|
329
|
+
|
|
330
|
+
(defn bridge-del
|
|
331
|
+
"Delete the bridge named 'bridge'.
|
|
332
|
+
Bridge type is dependent on bridge-mode (:ovs or :linux)."
|
|
333
|
+
[bridge]
|
|
334
|
+
(P/let [{:keys [info error bridge-mode]} @ctx
|
|
335
|
+
_ (info "Deleting bridge/switch" bridge)
|
|
336
|
+
cmd (get {:ovs (str "ovs-vsctl del-br " bridge)
|
|
337
|
+
:linux (str "ip link del " bridge)} bridge-mode)
|
|
338
|
+
res (run cmd)]
|
|
339
|
+
(if (not= 0 (:code res))
|
|
340
|
+
(error (str "Unable to delete bridge " bridge))
|
|
341
|
+
(swap! ctx assoc-in [:network-state :bridges bridge :status] nil))
|
|
342
|
+
res))
|
|
343
|
+
|
|
344
|
+
(defn bridge-add-link
|
|
345
|
+
"Add the link/interface 'dev' to the bridge 'bridge'.
|
|
346
|
+
Bridge type is dependent on bridge-mode (:ovs or :linux)."
|
|
347
|
+
[bridge dev]
|
|
348
|
+
(P/let [{:keys [error bridge-mode]} @ctx
|
|
349
|
+
cmd (get {:ovs (str "ovs-vsctl add-port " bridge " " dev)
|
|
350
|
+
:linux (str "ip link set dev " dev " master " bridge)}
|
|
351
|
+
bridge-mode)
|
|
352
|
+
res (run cmd)]
|
|
353
|
+
(when (not= 0 (:code res))
|
|
354
|
+
(error (str "Unable to add link " dev " into " bridge)))
|
|
355
|
+
res))
|
|
356
|
+
|
|
357
|
+
(defn bridge-drop-link
|
|
358
|
+
"Remove the link/interface 'dev' from the bridge 'bridge'.
|
|
359
|
+
Bridge type is dependent on bridge-mode (:ovs or :linux)."
|
|
360
|
+
[bridge dev]
|
|
361
|
+
(P/let [{:keys [error bridge-mode]} @ctx
|
|
362
|
+
cmd (get {:ovs (str "ovs-vsctl del-port " bridge " " dev)
|
|
363
|
+
:linux (str "ip link set dev " dev " nomaster")}
|
|
364
|
+
bridge-mode)
|
|
365
|
+
res (run cmd)]
|
|
366
|
+
(when (not= 0 (:code res))
|
|
367
|
+
(error (str "Unable to drop link " dev " from " bridge)))
|
|
368
|
+
res))
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
(defn link-add
|
|
372
|
+
"Create a link/interface defined by 'link' in a container by calling
|
|
373
|
+
the 'link-add.sh' script. This function just marshalls the command
|
|
374
|
+
line arguments from the 'link' definition and reports the results."
|
|
375
|
+
[link]
|
|
376
|
+
(P/let [{:keys [error]} @ctx
|
|
377
|
+
{:keys [type dev outer-dev pid outer-pid container dev-id]} link
|
|
378
|
+
cmd (str "link-add.sh"
|
|
379
|
+
" '" (name type) "' '" pid "' '" dev "'"
|
|
380
|
+
(when outer-pid (str " --pid1 " outer-pid))
|
|
381
|
+
(when outer-dev (str " --intf1 " outer-dev))
|
|
382
|
+
(S/join ""
|
|
383
|
+
(for [o LINK-ADD-OPTS]
|
|
384
|
+
(when-let [v (get link o)]
|
|
385
|
+
(str " --" (name o) " '" v "'")))))
|
|
386
|
+
res (run cmd {:id dev-id})]
|
|
387
|
+
(when (not= 0 (:code res))
|
|
388
|
+
(error (str "Unable to add " (name type) " " dev-id)))
|
|
389
|
+
res))
|
|
390
|
+
|
|
391
|
+
(defn link-del
|
|
392
|
+
"Delete a link/interface defined by 'link' in a container by calling
|
|
393
|
+
the 'link-del.sh' script. This function just marshalls the command
|
|
394
|
+
line arguments from the 'link' definition and reports the results."
|
|
395
|
+
[link]
|
|
396
|
+
(P/let [{:keys [warn error]} @ctx
|
|
397
|
+
{:keys [dev pid dev-id]} link
|
|
398
|
+
cmd (str "link-del.sh " pid " " dev)
|
|
399
|
+
res (run cmd {:id dev-id :quiet true})]
|
|
400
|
+
(when (not= 0 (:code res))
|
|
401
|
+
(if (re-seq #"is no longer running" (:stderr res))
|
|
402
|
+
(warn (str "Skipping delete of " dev-id " (container gone)"))
|
|
403
|
+
(error (str "Unable to delete " dev-id ": " (:stderr res)))))
|
|
404
|
+
res))
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
;;; docker/docker-compose utilities
|
|
408
|
+
|
|
409
|
+
(defn get-container-id
|
|
410
|
+
"Determine and return our docker or podman container ID. Returns nil
|
|
411
|
+
if no container ID can be determined (e.g. we are probably not
|
|
412
|
+
running in a container)"
|
|
413
|
+
[]
|
|
414
|
+
(P/let [[cgroup mountinfo]
|
|
415
|
+
, (P/all [(read-file "/proc/self/cgroup" "utf8")
|
|
416
|
+
(read-file "/proc/self/mountinfo" "utf8")])
|
|
417
|
+
;; docker
|
|
418
|
+
d-cgroups (map second (re-seq #"/docker/([^/\n]*)" cgroup))
|
|
419
|
+
;; podman (root)
|
|
420
|
+
p-cgroups (map second (re-seq #"libpod-([^/.\n]*)" cgroup))
|
|
421
|
+
;; general fallback
|
|
422
|
+
o-mounts (map second (re-seq #"containers/([^/]{64})/.*/etc/hosts" mountinfo))]
|
|
423
|
+
(first (concat d-cgroups p-cgroups o-mounts))))
|
|
424
|
+
|
|
425
|
+
(defn list-containers
|
|
426
|
+
"Return a sequence of container objects optionally limited to those
|
|
427
|
+
matching filters in 'filters'."
|
|
428
|
+
[client & [filters]]
|
|
429
|
+
(P/let [opts (if filters {:filters (json-str filters)} {})]
|
|
430
|
+
^obj (.listContainers client (->js opts))))
|
|
431
|
+
|
|
432
|
+
(defn get-container
|
|
433
|
+
"Return a dockerode container object with container ID 'cid'."
|
|
434
|
+
[client cid]
|
|
435
|
+
^obj (.getContainer client cid))
|
|
436
|
+
|
|
437
|
+
(defn inspect-container
|
|
438
|
+
"Return a map of inspected container properties for the 'container'
|
|
439
|
+
container object."
|
|
440
|
+
[container]
|
|
441
|
+
(P/-> ^obj (.inspect container) ->clj))
|
|
442
|
+
|
|
443
|
+
(defn get-compose-labels
|
|
444
|
+
"Return a map of compose related container labels with the
|
|
445
|
+
'com.docker.compose.' prefix stripped off and converted to
|
|
446
|
+
keywords."
|
|
447
|
+
[container]
|
|
448
|
+
(into {}
|
|
449
|
+
(for [[k v] (get-in container [:Config :Labels])
|
|
450
|
+
:let [n (name k)]
|
|
451
|
+
:when (S/starts-with? n "com.docker.compose.")]
|
|
452
|
+
[(keyword (-> n
|
|
453
|
+
(S/replace #"^com\.docker\.compose\." "")
|
|
454
|
+
(S/replace #"\." "-")))
|
|
455
|
+
v])))
|
|
456
|
+
|
|
457
|
+
;;;
|
|
458
|
+
|
|
459
|
+
(defn docker-client
|
|
460
|
+
"Return a docker/dockerode client object for the docker/podman
|
|
461
|
+
server listening at 'path'."
|
|
462
|
+
[path]
|
|
463
|
+
(P/let [{:keys [error log]} @ctx]
|
|
464
|
+
(P/catch
|
|
465
|
+
(P/let
|
|
466
|
+
[client (Docker. #js {:socketPath path})
|
|
467
|
+
;; client is lazy so trigger it now
|
|
468
|
+
containers (list-containers client)]
|
|
469
|
+
(log (str "Listening on " path))
|
|
470
|
+
client)
|
|
471
|
+
#(error "Could not start docker client on '" path "': " %))))
|
|
472
|
+
|
|
473
|
+
(defn docker-listen
|
|
474
|
+
"Listen for docker events from 'client' that match filter 'filters'.
|
|
475
|
+
Calls 'event-callback' function with each decoded event map."
|
|
476
|
+
[client filters event-callback]
|
|
477
|
+
(P/let [{:keys [error log]} @ctx]
|
|
478
|
+
(P/catch
|
|
479
|
+
(P/let
|
|
480
|
+
[ev-stream ^obj (.getEvents client #js {:filters (json-str filters)})
|
|
481
|
+
_ ^obj (.on ev-stream "data"
|
|
482
|
+
#(event-callback client (->clj (js/JSON.parse %))))]
|
|
483
|
+
ev-stream)
|
|
484
|
+
#(error "Could not start docker listener"))))
|
|
485
|
+
|
|
486
|
+
(defn link-repr [{:keys [type dev remote outer-dev bridge ip dev-id]}]
|
|
487
|
+
(str dev-id
|
|
488
|
+
(if remote
|
|
489
|
+
(str " (bridge " bridge ", remote " remote ")")
|
|
490
|
+
(str (when ip (str " (IP " ip ")"))
|
|
491
|
+
(when outer-dev (str " <-> " outer-dev))
|
|
492
|
+
(when bridge (str " (bridge " bridge ")"))))))
|
|
493
|
+
|
|
494
|
+
(defn modify-link
|
|
495
|
+
"Depending on 'action' create ('start') or delete ('die') a link
|
|
496
|
+
defined by 'link'. veth type links and tunnel interfaces will also
|
|
497
|
+
be added to the local bridge defined in the link. The network-state
|
|
498
|
+
for this link will be updated to either :creating or :deleting
|
|
499
|
+
before any action is taken. Once the async commands complete, the
|
|
500
|
+
state will be updated to either :created or nil."
|
|
501
|
+
[link action]
|
|
502
|
+
(P/let
|
|
503
|
+
[{:keys [error log]} @ctx
|
|
504
|
+
{:keys [type outer-dev bridge dev-id]} link
|
|
505
|
+
status-path [:network-state :devices dev-id :status]
|
|
506
|
+
link-status (get-in @ctx status-path)]
|
|
507
|
+
(log (str (get {"start" "Creating" "die" "Deleting"} action)
|
|
508
|
+
" " (name type) " link " (link-repr link)))
|
|
509
|
+
(condp = action
|
|
510
|
+
"start"
|
|
511
|
+
(if link-status
|
|
512
|
+
(error (str "Link " dev-id " already exists"))
|
|
513
|
+
(P/do
|
|
514
|
+
(swap! ctx assoc-in status-path :creating)
|
|
515
|
+
(link-add link)
|
|
516
|
+
(when bridge (bridge-add-link bridge outer-dev))
|
|
517
|
+
(swap! ctx assoc-in status-path :created)))
|
|
518
|
+
|
|
519
|
+
"die"
|
|
520
|
+
(if (not link-status)
|
|
521
|
+
(error (str "Link " dev-id " does not exist"))
|
|
522
|
+
(P/do
|
|
523
|
+
(swap! ctx assoc-in status-path :deleting)
|
|
524
|
+
(when bridge (bridge-drop-link bridge outer-dev))
|
|
525
|
+
(link-del link)
|
|
526
|
+
(swap! ctx assoc-in status-path nil))))))
|
|
527
|
+
|
|
528
|
+
(defn exec-command
|
|
529
|
+
"Exec a command 'command' in the container named 'cname' using the
|
|
530
|
+
'container' object. If 'command' is a string then call the command
|
|
531
|
+
using 'sh -c'. Every line of output from the command is prefixed
|
|
532
|
+
with 'cname' and printed to stdout."
|
|
533
|
+
[cname container command]
|
|
534
|
+
(P/let [{:keys [error log]} @ctx
|
|
535
|
+
cmd (if (string? command)
|
|
536
|
+
["sh", "-c", command]
|
|
537
|
+
command)
|
|
538
|
+
_ (log (str "Exec command in " cname ": " cmd))
|
|
539
|
+
ex (.exec container (->js {:Cmd cmd
|
|
540
|
+
:AttachStdout true
|
|
541
|
+
:AttachStderr true}))
|
|
542
|
+
stream (.start ex)]
|
|
543
|
+
^obj (.on stream "data"
|
|
544
|
+
(fn [b] (log (str " " cname ": " (trim (.toString b "utf8"))))))
|
|
545
|
+
ex))
|
|
546
|
+
|
|
547
|
+
(defn all-connected-check
|
|
548
|
+
"Check if all containers/services have been connected (at least one
|
|
549
|
+
link for each service) and if so, output the ending network state (if
|
|
550
|
+
verbose) and the message 'All links connected'. This will only fire
|
|
551
|
+
once. Caveat: can fire early depending on the service replica/scale
|
|
552
|
+
counts."
|
|
553
|
+
[]
|
|
554
|
+
(let [{:keys [network-config network-state log info]} @ctx
|
|
555
|
+
{:keys [links tunnels]} network-config
|
|
556
|
+
{:keys [devices all-connected]} network-state]
|
|
557
|
+
(when (and (not all-connected)
|
|
558
|
+
(every? #(= :created (:status %)) (vals devices))
|
|
559
|
+
(>= (count devices) (+ (count links) (count tunnels))))
|
|
560
|
+
;; Save all-connected to prevent scale/stop-start from
|
|
561
|
+
;; showing this message multiple times.
|
|
562
|
+
(swap! ctx assoc-in [:network-state :all-connected] true)
|
|
563
|
+
(info (str "Ending network state:\n"
|
|
564
|
+
(indent-pprint-str network-state " ")))
|
|
565
|
+
(log "All links connected"))))
|
|
566
|
+
|
|
567
|
+
(defn handle-event
|
|
568
|
+
"Handle a docker/podman container event. Match the event to
|
|
569
|
+
a container or service network definition (if any), then create all
|
|
570
|
+
the links for that container and then run any commmands defined for
|
|
571
|
+
the container. Finally call all-connected-check to check and notify
|
|
572
|
+
if all containers/services are connected."
|
|
573
|
+
[client {:keys [status id]}]
|
|
574
|
+
(P/let
|
|
575
|
+
[{:keys [log info network-config compose-opts self-pid]} @ctx
|
|
576
|
+
container-obj (get-container client id)
|
|
577
|
+
container (inspect-container container-obj)
|
|
578
|
+
cname (->> container :Name (re-seq #"(.*/)?(.*)") first last)
|
|
579
|
+
pid (-> container :State :Pid)
|
|
580
|
+
|
|
581
|
+
clabels (get-compose-labels container)
|
|
582
|
+
svc-name (:service clabels)
|
|
583
|
+
svc-num (:container-number clabels)
|
|
584
|
+
cindex (if svc-num (js/parseInt svc-num) 1)
|
|
585
|
+
container-info {:id id
|
|
586
|
+
:name cname
|
|
587
|
+
:index cindex
|
|
588
|
+
:service svc-name
|
|
589
|
+
:pid pid
|
|
590
|
+
:labels clabels}
|
|
591
|
+
|
|
592
|
+
svc-match? (and (let [p (:project compose-opts)]
|
|
593
|
+
(or (not p) (= p (:project clabels))))
|
|
594
|
+
(let [d (:project-working_dir compose-opts)]
|
|
595
|
+
(or (not d) (= d (:project-working_dir clabels)))))
|
|
596
|
+
containers (get-in network-config [:containers cname])
|
|
597
|
+
services (when svc-match? (get-in network-config [:services svc-name]))
|
|
598
|
+
links (concat (:links containers) (:links services))
|
|
599
|
+
commands (concat (:commands containers) (:commands services))]
|
|
600
|
+
(if (and (not (seq links)) (not (seq commands)))
|
|
601
|
+
(info (str "Event: no matching config for " cname ", ignoring"))
|
|
602
|
+
(P/do
|
|
603
|
+
(info "Event:" status cname id)
|
|
604
|
+
(P/all (for [link links
|
|
605
|
+
:let [link (link-instance-enrich
|
|
606
|
+
link container-info self-pid)]]
|
|
607
|
+
(modify-link link status)))
|
|
608
|
+
(when (= "start" status)
|
|
609
|
+
(P/all (for [{:keys [command]} commands]
|
|
610
|
+
(exec-command cname container-obj command))))
|
|
611
|
+
|
|
612
|
+
(all-connected-check)))))
|
|
613
|
+
|
|
614
|
+
(defn exit-handler
|
|
615
|
+
"When the process is exiting, delete all links and bridges that are
|
|
616
|
+
currently configured (have :created status)."
|
|
617
|
+
[err origin]
|
|
618
|
+
(let [{:keys [log info network-state]} @ctx
|
|
619
|
+
{:keys [devices bridges]} network-state
|
|
620
|
+
;; filter for :created status (ignore :exists)
|
|
621
|
+
devices (filter #(= :created (-> % val :status)) devices)
|
|
622
|
+
bridges (filter #(= :created (-> % val :status)) bridges)]
|
|
623
|
+
(info (str "Got " origin ":") err)
|
|
624
|
+
(P/do
|
|
625
|
+
(when (seq devices)
|
|
626
|
+
(P/do
|
|
627
|
+
(log (str "Removing devices: " (S/join ", " (keys devices))))
|
|
628
|
+
(P/all (map link-del (vals devices)))))
|
|
629
|
+
(when (seq bridges)
|
|
630
|
+
(P/do
|
|
631
|
+
(log (str "Removing bridges: " (S/join ", " (keys bridges))))
|
|
632
|
+
(P/all (map bridge-del (keys bridges)))))
|
|
633
|
+
(js/process.exit 127))))
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
;;;
|
|
637
|
+
|
|
638
|
+
(defn arg-checks
|
|
639
|
+
"Check command line arguments. Exit with error if arguments are
|
|
640
|
+
invalid."
|
|
641
|
+
[{:keys [network-file compose-file]}]
|
|
642
|
+
(when (and (empty? network-file) (empty? compose-file))
|
|
643
|
+
(fatal 2 "either --network-file or --compose-file is required")))
|
|
644
|
+
|
|
645
|
+
(defn startup-checks
|
|
646
|
+
"Check startup state and exit if openvswitch kernel module is not
|
|
647
|
+
loaded or if no docker or podman connection could be established."
|
|
648
|
+
[bridge-mode docker podman]
|
|
649
|
+
(P/let
|
|
650
|
+
[kmod-okay? (if (= :ovs bridge-mode)
|
|
651
|
+
(kmod-loaded? "openvswitch")
|
|
652
|
+
true)]
|
|
653
|
+
(when (not kmod-okay?)
|
|
654
|
+
(fatal 1 "bridge-mode is 'ovs', but no 'openvswitch' module loaded"))
|
|
655
|
+
(when (and (not docker) (not podman))
|
|
656
|
+
(fatal 1 "Failed to start either docker or podman client/listener"))))
|
|
657
|
+
|
|
658
|
+
(defn server
|
|
659
|
+
"Process:
|
|
660
|
+
- parse/validate command line options
|
|
661
|
+
- load/combine/mangle/validate network configuration
|
|
662
|
+
- connect to docker and/or podman server
|
|
663
|
+
- determine our own container ID and compose properties (if any)
|
|
664
|
+
- generate runtime network state and other process context/state
|
|
665
|
+
- install exit/cleanup handlers
|
|
666
|
+
- start/init openvswitch daemons/config (if :ovs bridge-mode)
|
|
667
|
+
- check that any defined bridges do not already exist
|
|
668
|
+
- create any bridges defined in network config links
|
|
669
|
+
- start listening/handling docker/podman container events
|
|
670
|
+
- list/handle any already running containers that match the config
|
|
671
|
+
"
|
|
672
|
+
[& args]
|
|
673
|
+
(P/let
|
|
674
|
+
[{:as opts :keys [verbose]} (parse-opts usage args)
|
|
675
|
+
{:keys [log info]} (swap! ctx merge (when verbose {:info Eprintln}))
|
|
676
|
+
opts (merge
|
|
677
|
+
opts
|
|
678
|
+
{:bridge-mode (keyword (:bridge-mode opts))
|
|
679
|
+
:network-file (mapcat #(S/split % #":") (:network-file opts))
|
|
680
|
+
:compose-file (mapcat #(S/split % #":") (:compose-file opts))})
|
|
681
|
+
_ (arg-checks opts)
|
|
682
|
+
_ (info (str "User options:\n" (indent-pprint-str opts " ")))
|
|
683
|
+
|
|
684
|
+
{:keys [network-file compose-file compose-project bridge-mode]} opts
|
|
685
|
+
env (js->clj (js/Object.assign #js {} js/process.env))
|
|
686
|
+
self-pid js/process.pid
|
|
687
|
+
schema (load-config (:config-schema opts))
|
|
688
|
+
network-config (P/-> (load-configs compose-file network-file)
|
|
689
|
+
(interpolate-walk env)
|
|
690
|
+
(check-schema schema verbose)
|
|
691
|
+
(enrich-network-config))
|
|
692
|
+
|
|
693
|
+
docker (docker-client (:docker-socket opts))
|
|
694
|
+
podman (docker-client (:podman-socket opts))
|
|
695
|
+
_ (startup-checks bridge-mode docker podman)
|
|
696
|
+
self-cid (get-container-id)
|
|
697
|
+
self-container-obj (when self-cid
|
|
698
|
+
(get-container (or docker podman) self-cid))
|
|
699
|
+
self-container (inspect-container self-container-obj)
|
|
700
|
+
compose-opts (if compose-project
|
|
701
|
+
{:project compose-project}
|
|
702
|
+
(get-compose-labels self-container))
|
|
703
|
+
network-state (gen-network-state network-config)
|
|
704
|
+
ctx-data {:bridge-mode bridge-mode
|
|
705
|
+
:network-config network-config
|
|
706
|
+
:network-state network-state
|
|
707
|
+
:compose-opts compose-opts
|
|
708
|
+
:docker docker
|
|
709
|
+
:podman podman
|
|
710
|
+
:self-pid self-pid
|
|
711
|
+
:self-cid self-cid}]
|
|
712
|
+
|
|
713
|
+
|
|
714
|
+
(swap! ctx merge ctx-data)
|
|
715
|
+
|
|
716
|
+
(js/process.on "SIGINT" #(exit-handler % "signal"))
|
|
717
|
+
(js/process.on "SIGTERM" #(exit-handler % "signal"))
|
|
718
|
+
(js/process.on "uncaughtException" #(exit-handler %1 %2))
|
|
719
|
+
|
|
720
|
+
(log "Bridge mode:" (name bridge-mode))
|
|
721
|
+
(info (str "Starting network config\n"
|
|
722
|
+
(indent-pprint-str network-config " ")))
|
|
723
|
+
(info (str "Starting network state:\n"
|
|
724
|
+
(indent-pprint-str network-state " ")))
|
|
725
|
+
(when self-cid
|
|
726
|
+
(info "Detected enclosing container:" self-cid))
|
|
727
|
+
(when compose-project
|
|
728
|
+
(info "Detected compose context:" compose-project))
|
|
729
|
+
|
|
730
|
+
(P/do
|
|
731
|
+
(when self-cid
|
|
732
|
+
(rename-docker-eth0))
|
|
733
|
+
|
|
734
|
+
(when (= :ovs bridge-mode)
|
|
735
|
+
(start-ovs))
|
|
736
|
+
|
|
737
|
+
;; Check that bridges/switches do not already exist
|
|
738
|
+
(P/all (for [bridge (-> network-state :bridges keys)]
|
|
739
|
+
(check-no-bridge bridge)))
|
|
740
|
+
;; Create bridges/switch configs
|
|
741
|
+
;; TODO: should be done on-demand
|
|
742
|
+
(P/all (for [bridge (-> network-state :bridges keys)]
|
|
743
|
+
(bridge-create bridge)))
|
|
744
|
+
|
|
745
|
+
;; Create tunnels configs
|
|
746
|
+
(P/all (for [tunnel (:tunnels network-config)
|
|
747
|
+
:let [tunnel (tunnel-instance-enrich tunnel self-pid)]]
|
|
748
|
+
(modify-link tunnel "start")))
|
|
749
|
+
|
|
750
|
+
(P/all (for [client [docker podman] :when client]
|
|
751
|
+
(P/let
|
|
752
|
+
[event-filter {"event" ["start" "die"]}
|
|
753
|
+
;; Listen for docker and/or podman events
|
|
754
|
+
_ (docker-listen client event-filter handle-event)
|
|
755
|
+
containers ^obj (list-containers client)]
|
|
756
|
+
;; Generate fake events for existing containers
|
|
757
|
+
(P/all (for [container containers
|
|
758
|
+
:let [ev {:status "start"
|
|
759
|
+
:from "pre-existing"
|
|
760
|
+
:id (.-Id ^obj container)}]]
|
|
761
|
+
(handle-event client ev))))))
|
|
762
|
+
nil)))
|
|
763
|
+
|
|
764
|
+
|
|
765
|
+
(defn main
|
|
766
|
+
[& args]
|
|
767
|
+
;; nbb implicitly does this wrapping but shadow-cljs does not
|
|
768
|
+
;; (exceptions result in successful exit code).
|
|
769
|
+
(P/catch
|
|
770
|
+
(apply server args)
|
|
771
|
+
(fn [err]
|
|
772
|
+
(fatal 1 "Error during conlink server startup:" err))))
|