blob: 867865f8f449e9bbce7e8012720d33a9bf2f47c1 [file] [log] [blame]
garciadeblas70461c52024-07-03 09:17:56 +02001#!/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
17function 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
30function 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
46function 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
59function 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
82function 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`
97function 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)
124function folder2list() {
125 local FOLDER="${1:-}"
126
127 kpt fn source "${FOLDER}"
128}
129
130
131# Helper function to convert manifest to `ResourceList`
132function 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.
139function 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.
155function 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`.
179function 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
202function 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
218function 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`
227function 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`
241function 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`
251function 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`
262function 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`
274function 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`
286function 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`
300function 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`
311function 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
333function 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`
345function 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
366function noop_transformer() {
367 cat
368}
369
370
371# Add patch to `Kustomization` item in `ResourceList`
372function 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
384function 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}
396EOF
397}
398
399
400# Helper function to produce a full patch, with target object + JSON Patch RFC 6902
401function 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}\"" | \
garciadeblas762811c2025-07-18 18:12:40 +0200428 yq eval-all 'select(fileIndex == 0) * select(fileIndex == 1)' - \
429 <(printf "patch: |-\n%s\n" "$(echo "${PATCH_CONTENT}" | sed 's/^/ /')" ) | \
430 yq "[.]"
garciadeblas70461c52024-07-03 09:17:56 +0200431 )
432
433 echo "${PATCH_FULL}"
434}
435
436
437# Add values to `HelmRelease` by patch into `Kustomization` item in `ResourceList`
438function add_values_to_helmrelease_via_ks() {
439 local KUSTOMIZATION_NAME="$1"
440 local HELMRELEASE_NAME="$2"
441 local VALUES="$3"
442
443 # Embed into patch list
444 local FULL_PATCH_CONTENT="$(
445 full_json_patch \
446 "HelmRelease" \
447 "${HELMRELEASE_NAME}" \
448 "add" \
449 "/spec/values" \
450 "${VALUES}"
451 )"
452
453 # Path via intermediate Kustomization object
454 add_patch_to_kustomization \
455 "${KUSTOMIZATION_NAME}" \
456 "${FULL_PATCH_CONTENT}"
457}
458
459
460# Add values from Secret/ConfigMap to `HelmRelease` by patch into `Kustomization` item in `ResourceList`
461function add_referenced_values_to_helmrelease_via_ks() {
462 local KUSTOMIZATION_NAME="$1"
463 local HELMRELEASE_NAME="$2"
464 local VALUES_FROM="$3"
465
466 # Embed into patch list
467 local FULL_PATCH_CONTENT="$(
468 full_json_patch \
469 "HelmRelease" \
470 "${HELMRELEASE_NAME}" \
471 "add" \
472 "/spec/valuesFrom" \
473 "${VALUES_FROM}"
474 )"
475
476 # Path via intermediate Kustomization object
477 add_patch_to_kustomization \
478 "${KUSTOMIZATION_NAME}" \
479 "${FULL_PATCH_CONTENT}"
480}
481
482
483# High level function to add values from Secret, ConfigMap or both to `HelmRelease` by patch into `Kustomization` item in `ResourceList`
484function add_ref_values_to_hr_via_ks() {
485 local KUSTOMIZATION_NAME="$1"
486 local HELMRELEASE_NAME="$2"
487 local VALUES_SECRET_NAME="${3:-""}"
488 local VALUES_CM_NAME="${4:-""}"
489
490 local YAML_VALUES_FROM_BOTH=$(cat <<EOF
491- kind: Secret
492 name: "${VALUES_SECRET_NAME}"
493- kind: ConfigMap
494 name: "${VALUES_CM_NAME}"
495EOF
496 )
497 local YAML_VALUES_FROM_SECRET=$(cat <<EOF
498- kind: Secret
499 name: "${VALUES_SECRET_NAME}"
500EOF
501 )
502 local YAML_VALUES_FROM_CM=$(cat <<EOF
503- kind: ConfigMap
504 name: "${VALUES_CM_NAME}"
505EOF
506 )
507
508 # Chooses the appropriate YAML
509 VALUES_FROM=""
510 if [[ ( -n "${VALUES_SECRET_NAME}" ) && ( -n "${VALUES_CM_NAME}" ) ]];
511 then
512 VALUES_FROM="${YAML_VALUES_FROM_BOTH}"
513 elif [[ -n "${VALUES_SECRET_NAME}" ]];
514 then
515 VALUES_FROM="${YAML_VALUES_FROM_SECRET}"
516 elif [[ -n "${VALUES_CM_NAME}" ]];
517 then
518 VALUES_FROM="${YAML_VALUES_FROM_CM}"
519 else
520 # If none is set, it must be an error
521 return 1
522 fi
523
524 # Calls the low-level function
525 add_referenced_values_to_helmrelease_via_ks \
526 "${KUSTOMIZATION_NAME}" \
527 "${HELMRELEASE_NAME}" \
528 "${VALUES_FROM}"
529}
530
531# Substitute environment variables from stdin
532function replace_env_vars() {
533 # Optional parameter to filter environment variables that can be replaced
534 local FILTER=${1:-}
535
536 if [[ -n "${FILTER}" ]];
537 then
538 envsubst "${FILTER}"
539 else
540 envsubst
541 fi
542}
543
544
545# Join two `ResourceList` **files**
546#
547# Examples of use:
548# $ join_lists list_file1.yaml list_file2.yaml
549# $ join_lists <(manifest2list < manifest_file1.yaml) <(manifest2list < manifest_file2.yaml)
550# $ cat prueba1.yaml | manifest2list | join_lists - <(manifest2list < prueba2.yaml)
551#
552# NOTE: Duplicated keys and arrays may be overwritten by the latest file.
553# See: https://stackoverflow.com/questions/66694238/merging-two-yaml-documents-while-concatenating-arrays
554function join_lists() {
555 local FILE1="$1"
556 local FILE2="$2"
557
558 yq eval-all '. as $item ireduce ({}; . *+ $item)' \
559 "${FILE1}" \
560 "${FILE2}"
561}
562
563
564# Helper function to create a generator from a function that creates manifests
565function make_generator() {
566 local MANIFEST_FILENAME="$1"
567 local SOURCER_FUNCTION="$2"
568 # Gathers all optional parameters for the funcion (if any) and puts them into an array for further use
569 local ALL_PARAMS=( "${@}" )
570 local PARAMS=( "${ALL_PARAMS[@]:2}" )
571
572 # Mix input with new generated manifests
573 join_lists \
574 <(cat) \
575 <(
576 "${SOURCER_FUNCTION}" \
577 "${PARAMS[@]}" | \
578 manifest2list | \
579 set_filename_to_items "${MANIFEST_FILENAME}"
580 )
581}
582
583
584function transform_if() {
585 local TEST_RESULT=$1
586
587 # Gathers all optional parameters for transformer funcion (if any) and puts them into an array for further use
588 local ALL_PARAMS=( "${@}" )
589 local PARAMS=( "${ALL_PARAMS[@]:1}" )
590
591 # If test result is true (==0), then runs the transformation normally
592 if [[ "${TEST_RESULT}" == "0" ]];
593 then
594 "${PARAMS[@]}"
595 # Otherwise, just pass through
596 else
597 cat
598 fi
599}
600
601
602# Helper function to convert multiline input from stdin to comma-separed output
603function multiline2commalist() {
604 mapfile -t TMP_ARRAY < <(cat)
605 printf -v TMP_LIST '%s,' "${TMP_ARRAY[@]}"
606 echo "${TMP_LIST}" | sed 's/,$//g'
607}
608
609
610# Helper function to check pending changes in workdir to `fleet` repo
611function check_fleet_workdir_status() {
612 local FLEET_REPO_DIR="${1:-${FLEET_REPO_DIR}}"
613
614 pushd "${FLEET_REPO_DIR}"
615 git status
616 popd
617}
618
619
620# Helper function to commit changes in workdir to `fleet` repo
621function commit_and_push_to_fleet() {
622 local DEFAULT_COMMIT_MESSAGE="Committing latest changes to fleet repo at $(date +'%Y-%m-%d %H:%M:%S')"
623 local COMMIT_MESSAGE="${1:-${DEFAULT_COMMIT_MESSAGE}}"
624 local FLEET_REPO_DIR="${2:-${FLEET_REPO_DIR}}"
625
626 pushd "${FLEET_REPO_DIR}"
627 git status
628 git add -A
629 git commit -m "${COMMIT_MESSAGE}"
630 echo "${COMMIT_MESSAGE}"
631 git push
632 popd
633}