Feature 11019: Workflow for cloud-native operations in OSM following Gitops model
[osm/devops.git] / docker / osm-krm-functions / scripts / library / helper-functions.rc
1 #!/bin/bash
2 #
3 #   Licensed under the Apache License, Version 2.0 (the "License");
4 #   you may not use this file except in compliance with the License.
5 #   You may obtain a copy of the License at
6 #
7 #       http://www.apache.org/licenses/LICENSE-2.0
8 #
9 #   Unless required by applicable law or agreed to in writing, software
10 #   distributed under the License is distributed on an "AS IS" BASIS,
11 #   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 #   See the License for the specific language governing permissions and
13 #   limitations under the License.
14 #
15
16 # Convert input string to a safe name for K8s resources
17 function safe_name() {
18   local INPUT="$1"
19
20   echo "${INPUT,,}" | \
21     sed '/\.\// s|./||' | \
22     sed 's|\.|-|g' | \
23     sed 's|/|-|g' | \
24     sed 's|_|-|g' | \
25     sed 's| |-|g'
26 }
27
28
29 # Helper function to create a new age key pair
30 function create_age_keypair() {
31   local AGE_KEY_NAME="$1"
32   local CREDENTIALS_DIR="${2:-"${CREDENTIALS_DIR}"}"
33
34   # Delete the keys in case they existed already
35   rm -f "${CREDENTIALS_DIR}/${AGE_KEY_NAME}.key" "${CREDENTIALS_DIR}/${AGE_KEY_NAME}.pub"
36
37   # Private key
38   age-keygen -o "${CREDENTIALS_DIR}/${AGE_KEY_NAME}.key"
39
40   # Public key (extracted from comment at private key)
41   age-keygen -y "${CREDENTIALS_DIR}/${AGE_KEY_NAME}.key" > "${CREDENTIALS_DIR}/${AGE_KEY_NAME}.pub"
42 }
43
44
45 # Helper function to in-place encrypt secrets in manifest
46 function encrypt_secret_inplace() {
47   local FILE="$1"
48   local PUBLIC_KEY="$2"
49
50   sops \
51     --age=${PUBLIC_KEY} \
52     --encrypt \
53     --encrypted-regex '^(data|stringData)$' \
54     --in-place "${FILE}"
55 }
56
57
58 # Helper function to encrypt secrets from stdin
59 function encrypt_secret_from_stdin() {
60   local PUBLIC_KEY="$1"
61
62   # Save secret manifest to temporary file
63   local TMPFILE=$(mktemp /tmp/secret.XXXXXXXXXX) || exit 1
64   cat > "${TMPFILE}"
65   # NOTE: Required workaround for busybox's version of `mktemp`, which is quite limited and does not support temporary files with extensions.
66   #       `.yaml` is required for proper `sops` behaviour.
67   mv "${TMPFILE}" "${TMPFILE}.yaml"
68
69   # Encrypt
70   sops \
71     --age=${PUBLIC_KEY} \
72     --encrypt \
73     --encrypted-regex '^(data|stringData)$' \
74     --in-place "${TMPFILE}.yaml"
75
76   # Outputs the result and removes the temporary file
77   cat "${TMPFILE}.yaml" && rm -f "${TMPFILE}.yaml"
78 }
79
80
81 # Helper function to create secret manifest and encrypt with public key
82 function kubectl_encrypt() {
83   local PUBLIC_KEY="$1"
84
85   # Gathers all optional parameters for transformer funcion (if any) and puts them into an array for further use
86   local ALL_PARAMS=( "${@}" )
87   local PARAMS=( "${ALL_PARAMS[@]:1}" )
88
89   kubectl \
90     "${PARAMS[@]}" | \
91   encrypt_secret_from_stdin \
92     "${PUBLIC_KEY}"
93 }
94
95
96 # Generator function to convert source folder to `ResourceList`
97 function folder2list_generator() {
98   local FOLDER="${1:-}"
99   local SUBSTENV="${2:-"false"}"
100   local FILTER="${3:-""}"
101
102   if [[ "${SUBSTENV,,}" == "true" ]];
103   then
104     # Mix input with new generated manifests and replace environment variables
105     join_lists \
106       <(cat) \
107       <(
108         kpt fn source "${FOLDER}" | \
109         replace_env_vars "${FILTER}"
110       )
111   else
112     # Mix input with new generated manifests
113     join_lists \
114       <(cat) \
115       <(
116         kpt fn source "${FOLDER}"
117       )
118   fi
119
120 }
121
122
123 # Function to convert source folder to `ResourceList` (no generator)
124 function folder2list() {
125   local FOLDER="${1:-}"
126
127   kpt fn source "${FOLDER}"
128 }
129
130
131 # Helper function to convert manifest to `ResourceList`
132 function manifest2list() {
133   kustomize cfg cat --wrap-kind ResourceList
134 }
135
136
137 # Helper function to convert `ResourceList` to manifests in folder structure.
138 # - New folder must be created to render the manifests.
139 function list2folder() {
140   local FOLDER="${1:-}"
141   local DRY_RUN="${2:-${DRY_RUN:-false}}"
142
143   if [[ "${DRY_RUN,,}" == "true" ]];
144   then
145     cat
146   else
147     kpt fn sink "${FOLDER}"
148   fi
149 }
150
151
152 # Helper function to convert `ResourceList` to manifests in folder structure.
153 # - It copies (cp) the generated files/subfolders over the target folder.
154 # - Pre-existing files and subfolder structure in target folder is preserved.
155 function list2folder_cp_over() {
156   local FOLDER="${1:-}"
157   local DRY_RUN="${2:-${DRY_RUN:-false}}"
158
159   if [[ "${DRY_RUN,,}" == "true" ]];
160   then
161     cat
162   else
163     local TMPFOLDER=$(mktemp -d) || exit 1
164     kpt fn sink "${TMPFOLDER}/manifests"
165
166     # Copy the generated files over the target folder
167     mkdir -p "${FOLDER}/"
168     cp -r "${TMPFOLDER}/manifests/"* "${FOLDER}/"
169
170     # Delete temporary folder
171     rm -rf "${TMPFOLDER}"
172   fi
173 }
174
175
176 # Helper function to convert `ResourceList` to manifests in folder structure.
177 # - It syncs the generated files/subfolders over the target folder.
178 # - Pre-existing files and subfolder structure in target folder is deleted if not present in `ResourceList`.
179 function list2folder_sync_replace() {
180   local FOLDER="${1:-}"
181   local DRY_RUN="${2:-${DRY_RUN:-false}}"
182
183   if [[ "${DRY_RUN,,}" == "true" ]];
184   then
185     cat
186   else
187     local TMPFOLDER=$(mktemp -d) || exit 1
188     kpt fn sink "${TMPFOLDER}/manifests"
189
190     # Copy the generated files over the target folder
191     mkdir -p "${FOLDER}/"
192     rsync -arh --exclude ".git" --exclude ".*" --delete \
193       "${TMPFOLDER}/manifests/" "${FOLDER}/"
194
195     # Delete temporary folder
196     rm -rf "${TMPFOLDER}"
197   fi
198 }
199
200
201 # Helper function to render **SAFELY** a single manifest coming from stdin into a profile, with a proper KSU subfolder
202 function render_manifest_over_ksu() {
203   local KSU_NAME="$1"
204   local TARGET_PROFILE_FOLDER="$2"
205   local MANIFEST_FILENAME="$3"
206
207   manifest2list | \
208   set_filename_to_items \
209     "${MANIFEST_FILENAME}" | \
210   prepend_folder_path \
211     "${KSU_NAME}/" | \
212   list2folder_cp_over \
213     "${TARGET_PROFILE_FOLDER}"
214 }
215
216
217 # Set filename to `ResourceList` item
218 function set_filename_to_items() {
219   local FILENAME="$1"
220
221   yq "(.items[]).metadata.annotations.\"config.kubernetes.io/path\" |= \"${FILENAME}\"" | \
222   yq "(.items[]).metadata.annotations.\"internal.config.kubernetes.io/path\" |= \"${FILENAME}\""
223 }
224
225
226 # Prepend folder path to `ResourceList`
227 function prepend_folder_path() {
228   local PREFIX="$1"
229
230   if [[ (-z "${PREFIX}") || ("${PREFIX}" == ".") ]];
231   then
232     cat
233   else
234     yq "(.items[]).metadata.annotations.\"config.kubernetes.io/path\" |= \"${PREFIX}\" + ." | \
235     yq "(.items[]).metadata.annotations.\"internal.config.kubernetes.io/path\" |= \"${PREFIX}\" + ."
236   fi
237 }
238
239
240 # Rename file in `ResourceList`
241 function rename_file_in_items() {
242   local SOURCE_NAME="$1"
243   local DEST_NAME="$2"
244
245   yq "(.items[].metadata.annotations | select (.\"config.kubernetes.io/path\" == \"${SOURCE_NAME}\")).\"config.kubernetes.io/path\" = \"${DEST_NAME}\"" | \
246   yq "(.items[].metadata.annotations | select (.\"internal.config.kubernetes.io/path\" == \"${SOURCE_NAME}\")).\"internal.config.kubernetes.io/path\" = \"${DEST_NAME}\""
247 }
248
249
250 # Get value from key in object in `ResourceList`
251 function get_value_from_resourcelist() {
252   local KEY_PATH="$1"
253   local TARGET_FILTERS="${2:-}"
254   # Example: To get a specific kind ("ProviderConfig") with a specific name ("default"). (TIP: Note the escaped double quotes).
255   # TARGET_FILTERS="| select(.kind == \"ProviderConfig\") | select(.metadata.name == \"default\")"
256
257   yq "(.items[]${TARGET_FILTERS})${KEY_PATH}"
258 }
259
260
261 # Patch "replace" to item in `ResourceList`
262 function patch_replace() {
263   local KEY_PATH="$1"
264   local VALUE="$2"
265   local TARGET_FILTERS="${3:-}"
266   # Example: To only patch a specific kind ("ProviderConfig") with a specific name ("default"). (TIP: Note the escaped double quotes).
267   # TARGET_FILTERS="| select(.kind == \"ProviderConfig\") | select(.metadata.name == \"default\")"
268
269   yq "(.items[]${TARGET_FILTERS})${KEY_PATH} = \"${VALUE}\""
270 }
271
272
273 # Add label to item in `ResourceList`
274 function set_label() {
275   local KEY="$1"
276   local VALUE="$2"
277   local TARGET_FILTERS="${3:-}"
278   # Example: To only patch a specific kind ("ProviderConfig") with a specific name ("default"). (TIP: Note the escaped double quotes).
279   # TARGET_FILTERS="| select(.kind == \"ProviderConfig\") | select(.metadata.name == \"default\")"
280
281   yq "(.items[]${TARGET_FILTERS}).metadata.labels.${KEY} = \"${VALUE}\""
282 }
283
284
285 # Patch which "appends" to list existing in item in `ResourceList`
286 function patch_add_to_list() {
287   local KEY_PATH="$1"
288   local VALUE="$2"
289   local TARGET_FILTERS="${3:-}"
290   # Example: To only patch a specific kind ("ProviderConfig") with a specific name ("default"). (TIP: Note the escaped double quotes).
291   # TARGET_FILTERS="| select(.kind == \"ProviderConfig\") | select(.metadata.name == \"default\")"
292
293   local VALUE_AS_JSON="$(echo "${VALUE}" | yq -o json -I0)"
294
295   yq "(.items[]${TARGET_FILTERS})${KEY_PATH} += ${VALUE_AS_JSON}"
296 }
297
298
299 # Patch which removes from list, existing in item in `ResourceList`
300 function patch_delete_from_list() {
301   local KEY_PATH="$1"
302   local TARGET_FILTERS="${2:-}"
303
304   # local VALUE_AS_JSON="$(echo "${VALUE}" | yq -o json -I0)"
305
306   yq "del((.items[]${TARGET_FILTERS})${KEY_PATH})"
307 }
308
309
310 # Check if an element/value is in a given list, existing in item in `ResourceList`
311 function is_element_on_list() {
312   local KEY_PATH="$1"
313   local VALUE="$2"
314   local TARGET_FILTERS="${3:-}"
315   # Example: To only patch a specific kind ("ProviderConfig") with a specific name ("default"). (TIP: Note the escaped double quotes).
316   # TARGET_FILTERS="| select(.kind == \"ProviderConfig\") | select(.metadata.name == \"default\")"
317
318   TEST_RESULT=$(
319     cat | \
320     yq "(.items[]${TARGET_FILTERS})${KEY_PATH} == \"${VALUE}\"" | grep "true"
321   )
322
323   if [[ "${TEST_RESULT}" != "true" ]]
324   then
325     echo "false"
326   else
327     echo "true"
328   fi
329 }
330
331
332 # Patch "replace" to item in `ResourceList` using a JSON as value
333 function patch_replace_inline_json() {
334   local KEY_PATH="$1"
335   local VALUE="$2"
336   local TARGET_FILTERS="${3:-}"
337   # Example: To only patch a specific kind ("ProviderConfig") with a specific name ("default"). (TIP: Note the escaped double quotes).
338   # TARGET_FILTERS="| select(.kind == \"ProviderConfig\") | select(.metadata.name == \"default\")"
339
340   VALUE_AS_JSON="$(echo "${VALUE}" | yq -o=json)" yq "(.items[]${TARGET_FILTERS})${KEY_PATH} = strenv(VALUE_AS_JSON)"
341 }
342
343
344 # Delete full object from `ResourceList`
345 function delete_object() {
346   local OBJECT_NAME="$1"
347   local KIND_NAME="$2"
348   local API_VERSION="${3:-""}"
349
350   # Calculated inputs
351   if [[ -z "${API_VERSION}" ]]
352   then
353     # If `apiVersion` is not specified
354     local TARGET_FILTER="| select(.kind == \"${KIND_NAME}\") | select(.metadata.name == \"${OBJECT_NAME}\")"
355   else
356     # Otherwise, it is taken into account
357     local TARGET_FILTER="| select(.kind == \"${KIND_NAME}\") | select(.apiVersion == \"${API_VERSION}\") | select(.metadata.name == \"${OBJECT_NAME}\")"
358   fi
359
360   # Delete object
361   yq "del((.items[]${TARGET_FILTER}))"
362 }
363
364
365 # Empty transformer function
366 function noop_transformer() {
367   cat
368 }
369
370
371 # Add patch to `Kustomization` item in `ResourceList`
372 function add_patch_to_kustomization() {
373   local KUSTOMIZATION_NAME="$1"
374   local FULL_PATCH_CONTENT="$2"
375
376   patch_add_to_list \
377     ".spec.patches" \
378     "${FULL_PATCH_CONTENT}" \
379     "| select(.kind == \"Kustomization\") | select(.metadata.name == \"${KUSTOMIZATION_NAME}\")"
380 }
381
382
383 # Helper function to produce a JSON Patch as specified in RFC 6902
384 function as_json_patch() {
385   local OPERATION="$1"
386   local PATCH_PATH="$2"
387   local VALUES="$3"
388
389   # Convert to JSON dictionary to insert as map instead of string
390   local VALUES_AS_DICT=$(echo "${VALUES}" | yq -o=json)
391
392   # Generate a patch list
393   cat <<EOF | yq ".[0].value = ${VALUES_AS_DICT}"
394 - op: ${OPERATION}
395   path: ${PATCH_PATH}
396 EOF
397 }
398
399
400 # Helper function to produce a full patch, with target object + JSON Patch RFC 6902
401 function full_json_patch() {
402   local TARGET_KIND="$1"
403   local TARGET_NAME="$2"
404   local OPERATION="$3"
405   local PATCH_PATH="$4"
406   # Gathers all optional parameters for transformer funcion (if any) and puts them into an array for further use
407   local ALL_PARAMS=( "${@}" )
408   local VALUES=( "${ALL_PARAMS[@]:4}" )
409
410   # Accumulates value items into the patch
411   local PATCH_CONTENT=""
412   for VAL in "${VALUES[@]}"
413   do
414     local VAL_AS_DICT=$(echo "${VAL}" | yq -o=json)
415
416     ITEM=$(
417       yq --null-input ".op = \"${OPERATION}\", .path = \"${PATCH_PATH}\"" | \
418       yq ".value = ${VAL_AS_DICT}" | \
419       yq "[ . ]"
420     )
421
422     PATCH_CONTENT="$(echo -e "${PATCH_CONTENT}\n${ITEM}")"
423   done
424
425   # Wrap a full patch around, adding target specification
426   local PATCH_FULL=$(
427     yq --null-input ".target.kind = \"${TARGET_KIND}\", .target.name = \"${TARGET_NAME}\"" | \
428     yq ".patch = \"${PATCH_CONTENT}\"" | \
429     yq "[ . ]"
430   )
431
432   echo "${PATCH_FULL}"
433 }
434
435
436 # Add values to `HelmRelease` by patch into `Kustomization` item in `ResourceList`
437 function add_values_to_helmrelease_via_ks() {
438   local KUSTOMIZATION_NAME="$1"
439   local HELMRELEASE_NAME="$2"
440   local VALUES="$3"
441
442   # Embed into patch list
443   local FULL_PATCH_CONTENT="$(
444     full_json_patch \
445       "HelmRelease" \
446       "${HELMRELEASE_NAME}" \
447       "add" \
448       "/spec/values" \
449       "${VALUES}"
450   )"
451
452   # Path via intermediate Kustomization object
453   add_patch_to_kustomization \
454     "${KUSTOMIZATION_NAME}" \
455     "${FULL_PATCH_CONTENT}"
456 }
457
458
459 # Add values from Secret/ConfigMap to `HelmRelease` by patch into `Kustomization` item in `ResourceList`
460 function add_referenced_values_to_helmrelease_via_ks() {
461   local KUSTOMIZATION_NAME="$1"
462   local HELMRELEASE_NAME="$2"
463   local VALUES_FROM="$3"
464
465   # Embed into patch list
466   local FULL_PATCH_CONTENT="$(
467     full_json_patch \
468       "HelmRelease" \
469       "${HELMRELEASE_NAME}" \
470       "add" \
471       "/spec/valuesFrom" \
472       "${VALUES_FROM}"
473   )"
474
475   # Path via intermediate Kustomization object
476   add_patch_to_kustomization \
477     "${KUSTOMIZATION_NAME}" \
478     "${FULL_PATCH_CONTENT}"
479 }
480
481
482 # High level function to add values from Secret, ConfigMap or both to `HelmRelease` by patch into `Kustomization` item in `ResourceList`
483 function add_ref_values_to_hr_via_ks() {
484   local KUSTOMIZATION_NAME="$1"
485   local HELMRELEASE_NAME="$2"
486   local VALUES_SECRET_NAME="${3:-""}"
487   local VALUES_CM_NAME="${4:-""}"
488
489   local YAML_VALUES_FROM_BOTH=$(cat <<EOF
490 - kind: Secret
491   name: "${VALUES_SECRET_NAME}"
492 - kind: ConfigMap
493   name: "${VALUES_CM_NAME}"
494 EOF
495   )
496   local YAML_VALUES_FROM_SECRET=$(cat <<EOF
497 - kind: Secret
498   name: "${VALUES_SECRET_NAME}"
499 EOF
500   )
501   local YAML_VALUES_FROM_CM=$(cat <<EOF
502 - kind: ConfigMap
503   name: "${VALUES_CM_NAME}"
504 EOF
505   )
506
507   # Chooses the appropriate YAML
508   VALUES_FROM=""
509   if [[ ( -n "${VALUES_SECRET_NAME}" ) && ( -n "${VALUES_CM_NAME}" ) ]];
510   then
511     VALUES_FROM="${YAML_VALUES_FROM_BOTH}"
512   elif [[ -n "${VALUES_SECRET_NAME}" ]];
513   then
514     VALUES_FROM="${YAML_VALUES_FROM_SECRET}"
515   elif [[ -n "${VALUES_CM_NAME}" ]];
516   then
517     VALUES_FROM="${YAML_VALUES_FROM_CM}"
518   else
519     # If none is set, it must be an error
520     return 1
521   fi
522
523   # Calls the low-level function
524   add_referenced_values_to_helmrelease_via_ks \
525     "${KUSTOMIZATION_NAME}" \
526     "${HELMRELEASE_NAME}" \
527     "${VALUES_FROM}"
528 }
529
530 # Substitute environment variables from stdin
531 function replace_env_vars() {
532   # Optional parameter to filter environment variables that can be replaced
533   local FILTER=${1:-}
534
535   if [[ -n "${FILTER}" ]];
536   then
537     envsubst "${FILTER}"
538   else
539     envsubst
540   fi
541 }
542
543
544 # Join two `ResourceList` **files**
545 #
546 # Examples of use:
547 # $ join_lists list_file1.yaml list_file2.yaml
548 # $ join_lists <(manifest2list < manifest_file1.yaml) <(manifest2list < manifest_file2.yaml)
549 # $ cat prueba1.yaml | manifest2list | join_lists - <(manifest2list < prueba2.yaml)
550 #
551 # NOTE: Duplicated keys and arrays may be overwritten by the latest file.
552 # See: https://stackoverflow.com/questions/66694238/merging-two-yaml-documents-while-concatenating-arrays
553 function join_lists() {
554   local FILE1="$1"
555   local FILE2="$2"
556
557   yq eval-all '. as $item ireduce ({}; . *+ $item)' \
558     "${FILE1}" \
559     "${FILE2}"
560 }
561
562
563 # Helper function to create a generator from a function that creates manifests
564 function make_generator() {
565   local MANIFEST_FILENAME="$1"
566   local SOURCER_FUNCTION="$2"
567   # Gathers all optional parameters for the funcion (if any) and puts them into an array for further use
568   local ALL_PARAMS=( "${@}" )
569   local PARAMS=( "${ALL_PARAMS[@]:2}" )
570
571   # Mix input with new generated manifests
572   join_lists \
573     <(cat) \
574     <(
575       "${SOURCER_FUNCTION}" \
576         "${PARAMS[@]}" | \
577       manifest2list | \
578       set_filename_to_items "${MANIFEST_FILENAME}"
579     )
580 }
581
582
583 function transform_if() {
584   local TEST_RESULT=$1
585
586   # Gathers all optional parameters for transformer funcion (if any) and puts them into an array for further use
587   local ALL_PARAMS=( "${@}" )
588   local PARAMS=( "${ALL_PARAMS[@]:1}" )
589
590   # If test result is true (==0), then runs the transformation normally
591   if [[ "${TEST_RESULT}" == "0" ]];
592   then
593     "${PARAMS[@]}"
594   # Otherwise, just pass through
595   else
596     cat
597   fi
598 }
599
600
601 # Helper function to convert multiline input from stdin to comma-separed output
602 function multiline2commalist() {
603   mapfile -t TMP_ARRAY < <(cat)
604   printf -v TMP_LIST '%s,' "${TMP_ARRAY[@]}"
605   echo "${TMP_LIST}" | sed 's/,$//g'
606 }
607
608
609 # Helper function to check pending changes in workdir to `fleet` repo
610 function check_fleet_workdir_status() {
611   local FLEET_REPO_DIR="${1:-${FLEET_REPO_DIR}}"
612
613   pushd "${FLEET_REPO_DIR}"
614   git status
615   popd
616 }
617
618
619 # Helper function to commit changes in workdir to `fleet` repo
620 function commit_and_push_to_fleet() {
621   local DEFAULT_COMMIT_MESSAGE="Committing latest changes to fleet repo at $(date +'%Y-%m-%d %H:%M:%S')"
622   local COMMIT_MESSAGE="${1:-${DEFAULT_COMMIT_MESSAGE}}"
623   local FLEET_REPO_DIR="${2:-${FLEET_REPO_DIR}}"
624
625   pushd "${FLEET_REPO_DIR}"
626   git status
627   git add -A
628   git commit -m "${COMMIT_MESSAGE}"
629   echo "${COMMIT_MESSAGE}"
630   git push
631   popd
632 }