Skip to content

Commit cc02f36

Browse files
authored
fix(cni-plugin): fix several race conditions (#477)
This fixes multiple race conditions that occur when new cni config files are created while the install-cni.sh script is executing anywhere here: https://github.com/linkerd/linkerd2-proxy-init/blob/cni-plugin/v1.6.0/cni-plugin/deployment/scripts/install-cni.sh#L323-L348 we have observed this race condition several times in our eks clusters over the past few days where we are chaining cilium to the aws vpc cni. the install-cni.sh script simply fails to patch the cilium cni config sometimes. i.e. cilium and linkerd-cni must be starting up and manipulating /etc/cni/net.d at just about the same time. Signed-off-by: Simon Dickhoven <[email protected]>
1 parent 0f361f3 commit cc02f36

File tree

1 file changed

+76
-64
lines changed

1 file changed

+76
-64
lines changed

cni-plugin/deployment/scripts/install-cni.sh

Lines changed: 76 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -198,14 +198,16 @@ EOF
198198
install_cni_conf() {
199199
local cni_conf_path=$1
200200

201-
local tmp_data=''
202-
local conf_data=''
203-
if [ -e "${cni_conf_path}" ]; then
204-
# Add the linkerd-cni plugin to the existing list
205-
tmp_data=$(cat "${TMP_CONF}")
206-
conf_data=$(jq --argjson CNI_TMP_CONF_DATA "${tmp_data}" -f /linkerd/filter.jq "${cni_conf_path}")
207-
echo "${conf_data}" > ${TMP_CONF}
208-
fi
201+
# Add the linkerd-cni plugin to the existing list.
202+
local tmp_data
203+
local conf_data
204+
tmp_data=$(cat "$TMP_CONF")
205+
conf_data=$(jq --argjson CNI_TMP_CONF_DATA "$tmp_data" -f /linkerd/filter.jq "$cni_conf_path" || true)
206+
207+
# Ensure that CNI config file did not disappear during processing.
208+
[ -n "$conf_data" ] || return 0
209+
210+
echo "$conf_data" > "$TMP_CONF"
209211

210212
# If the old config filename ends with .conf, rename it to .conflist, because it has changed to be a list
211213
filename=${cni_conf_path##*/}
@@ -218,6 +220,23 @@ install_cni_conf() {
218220
cni_conf_path="${cni_conf_path}list"
219221
fi
220222

223+
# Store SHA of each patched file in global `CNI_CONF_SHA` variable.
224+
#
225+
# This must happen in a non-concurrent access context!
226+
#
227+
# The below logic assumes that the `CNI_CONF_SHA` variable is already a
228+
# valid JSON object. So this variable must be initialized with '{}'!
229+
#
230+
# E.g. (pretty-printed; actual variable stores compact JSON object)
231+
#
232+
# {
233+
# "/etc/cni/net.d/05-foo.conflist": "b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c",
234+
# "/etc/cni/net.d/10-bar.conflist": "7d865e959b2466918c9863afca942d0fb89d7c9ac0c99bafc3749504ded97730"
235+
# }
236+
local new_sha
237+
new_sha=$( (sha256sum "$TMP_CONF" || true) | awk '{print $1}' )
238+
CNI_CONF_SHA=$(jq -c --arg f "$cni_conf_path" --arg sha "$new_sha" '. * {$f: $sha}' <<< "$CNI_CONF_SHA")
239+
221240
# Move the temporary CNI config into place.
222241
mv "${TMP_CONF}" "${cni_conf_path}" || exit_with_error 'Failed to mv files.'
223242
[ -n "$old_file_path" ] && rm -f "${old_file_path}" && log "Removing unwanted .conf file"
@@ -226,57 +245,50 @@ install_cni_conf() {
226245
}
227246

228247
# Sync() is responsible for reacting to file system changes. It is used in
229-
# conjunction with inotify events; sync() is called with the name of the file
230-
# that has changed, the event type (which can be either 'CREATE', 'DELETE',
231-
# 'MOVED_TO' or 'MODIFY', and the previously observed SHA of the configuration
232-
# file.
248+
# conjunction with inotify events; sync() is called with the event type (which
249+
# can be either 'CREATE', 'MOVED_TO' or 'MODIFY'), and the name of the file that
250+
# has changed.
233251
#
234-
# Based on the changed file and event type, sync() might re-install the CNI
235-
# plugin's configuration file.
252+
# Based on the changed file, sync() might re-install the CNI configuration file.
236253
sync() {
237-
local filename=$1
238-
local ev=$2
239-
local filepath="${HOST_CNI_NET}/$filename"
240-
241-
local prev_sha=$3
242-
243-
local config_file_count
244-
local new_sha
245-
if [ "$ev" = 'CREATE' ] || [ "$ev" = 'MOVED_TO' ] || [ "$ev" = 'MODIFY' ]; then
246-
# When the event type is 'CREATE', 'MOVED_TO' or 'MODIFY', we check the
247-
# previously observed SHA (updated with each file watch) and compare it
248-
# against the new file's SHA. If they differ, it means something has
249-
# changed.
250-
new_sha=$(sha256sum "${filepath}" | while read -r s _; do echo "$s"; done)
251-
if [ "$new_sha" != "$prev_sha" ]; then
252-
# Create but don't rm old one since we don't know if this will be configured
253-
# to run as _the_ cni plugin.
254-
log "New/changed file [$filename] detected; re-installing"
255-
create_kubeconfig
256-
create_cni_conf
257-
install_cni_conf "$filepath"
258-
else
259-
# If the SHA hasn't changed or we get an unrecognised event, ignore it.
260-
# When the SHA is the same, we can get into infinite loops whereby a file has
261-
# been created and after re-install the watch keeps triggering CREATE events
262-
# that never end.
263-
log "Ignoring event: $ev $filepath; no real changes detected"
264-
fi
254+
local ev=$1
255+
local file=${2//\/\//\/} # replace "//" with "/"
256+
257+
[[ "$file" =~ .*.(conflist|conf)$ ]] || return 0
258+
259+
log "Detected event: $ev $file"
260+
261+
# Retrieve previous SHA of detected file (if any) and compute current SHA.
262+
local previous_sha
263+
local current_sha
264+
previous_sha=$(jq -r --arg f "$file" '.[$f] | select(.)' <<< "$CNI_CONF_SHA")
265+
current_sha=$( (sha256sum "$file" || true) | awk '{print $1}' )
266+
267+
# If the SHA hasn't changed or the detected file has disappeared, ignore it.
268+
# When the SHA is the same, we can get into infinite loops whereby a file
269+
# has been created and after re-install the watch keeps triggering MOVED_TO
270+
# events that never end.
271+
# If the `current_sha` variable is blank then the detected CNI config file has
272+
# disappeared and no further action is required.
273+
# There exists an unhandled (highly improbable) edge case where a CNI plugin
274+
# creates a config file and then _immediately_ removes it again _while_ we are
275+
# in the process of patching it. If this happens, we may create a patched CNI
276+
# config file that should *not* exist.
277+
if [ -n "$current_sha" ] && [ "$current_sha" != "$previous_sha" ]; then
278+
log "New/changed file [$file] detected; re-installing"
279+
create_kubeconfig
280+
create_cni_conf
281+
install_cni_conf "$file"
282+
else
283+
log "Ignoring event: $ev $file; no real changes detected or file disappeared"
265284
fi
266285
}
267286

268287
# monitor_cni_config starts a watch on the host's CNI config directory
269288
monitor_cni_config() {
270289
inotifywait -m "${HOST_CNI_NET}" -e create,moved_to,modify |
271290
while read -r directory action filename; do
272-
if [[ "$filename" =~ .*.(conflist|conf)$ ]]; then
273-
log "Detected change in $directory: $action $filename"
274-
sync "$filename" "$action" "$cni_conf_sha"
275-
# calculate file SHA to use in the next iteration
276-
if [[ -e "$directory/$filename" ]]; then
277-
cni_conf_sha="$(sha256sum "$directory/$filename" | while read -r s _; do echo "$s"; done)"
278-
fi
279-
fi
291+
sync "$action" "$directory/$filename"
280292
done
281293
}
282294

@@ -318,6 +330,14 @@ rm -f "${DEFAULT_CNI_CONF_PATH}"
318330

319331
install_cni_bin
320332

333+
# The CNI config monitor must be set up _before_ we start patching existing CNI
334+
# config files!
335+
# Otherwise, new CNI config files can be created just _after_ the initial round
336+
# of patching and just _before_ we set up the `inotifywait` loop to detect new
337+
# CNI config files.
338+
CNI_CONF_SHA='{}'
339+
monitor_cni_config &
340+
321341
# Append our config to any existing config file (*.conflist or *.conf)
322342
config_files=$(find "${HOST_CNI_NET}" -maxdepth 1 -type f \( -iname '*conflist' -o -iname '*conf' \))
323343
if [ -z "$config_files" ]; then
@@ -329,23 +349,16 @@ else
329349
else
330350
find "${HOST_CNI_NET}" -maxdepth 1 -type f \( -iname '*conflist' -o -iname '*conf' \) -print0 |
331351
while read -r -d $'\0' file; do
332-
log "Installing CNI configuration for $file"
333-
create_kubeconfig
334-
create_cni_conf
335-
install_cni_conf "$file"
352+
log "Trigger CNI config detection for $file"
353+
tmp_file="$(mktemp -u /tmp/linkerd-cni.patch-candidate.XXXXXX)"
354+
cp -fp "$file" "$tmp_file"
355+
# The following will trigger the `sync()` function via filesystem event.
356+
# This requires `monitor_cni_config()` to be up and running!
357+
mv "$tmp_file" "$file" || exit_with_error 'Failed to mv files.'
336358
done
337359
fi
338360
fi
339361

340-
# Compute SHA for first config file found; this will be updated after every iteration.
341-
# First config file is likely to be chosen as the de facto CNI config by the
342-
# host.
343-
conf="$(find "${HOST_CNI_NET}" -maxdepth 1 -type f \( -iname '*conflist' -o -iname '*conf' \) | sort | head -n 1)"
344-
cni_conf_sha=""
345-
if [[ -n "$conf" ]]; then
346-
cni_conf_sha="$(sha256sum "$conf" | while read -r s _; do echo "$s"; done)"
347-
fi
348-
349362
# Watch in bg so we can receive interrupt signals through 'trap'. From 'man
350363
# bash':
351364
# "If bash is waiting for a command to complete and receives a signal
@@ -354,7 +367,6 @@ fi
354367
# builtin, the reception of a signal for which a trap has been set will cause
355368
# the wait builtin to return immediately with an exit status greater than 128,
356369
# immediately after which the trap is executed."
357-
monitor_cni_config &
358370
monitor_service_account_token &
359371
# uses -n so that we exit when the first background job exits (when there's an error)
360372
wait -n

0 commit comments

Comments
 (0)