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
7 # http://www.apache.org/licenses/LICENSE-2.0
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.
16 # Convert input string to a safe name for K8s resources
17 function safe_name() {
21 sed '/\.\// s|./||' | \
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}"}"
34 # Delete the keys in case they existed already
35 rm -f "${CREDENTIALS_DIR}/${AGE_KEY_NAME}.key" "${CREDENTIALS_DIR}/${AGE_KEY_NAME}.pub"
38 age-keygen -o "${CREDENTIALS_DIR}/${AGE_KEY_NAME}.key"
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"
45 # Helper function to in-place encrypt secrets in manifest
46 function encrypt_secret_inplace() {
53 --encrypted-regex '^(data|stringData)$' \
58 # Helper function to encrypt secrets from stdin
59 function encrypt_secret_from_stdin() {
62 # Save secret manifest to temporary file
63 local TMPFILE=$(mktemp /tmp/secret.XXXXXXXXXX) || exit 1
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"
73 --encrypted-regex '^(data|stringData)$' \
74 --in-place "${TMPFILE}.yaml"
76 # Outputs the result and removes the temporary file
77 cat "${TMPFILE}.yaml" && rm -f "${TMPFILE}.yaml"
81 # Helper function to create secret manifest and encrypt with public key
82 function kubectl_encrypt() {
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}" )
91 encrypt_secret_from_stdin \
96 # Generator function to convert source folder to `ResourceList`
97 function folder2list_generator() {
99 local SUBSTENV="${2:-"false"}"
100 local FILTER="${3:-""}"
102 if [[ "${SUBSTENV,,}" == "true" ]];
104 # Mix input with new generated manifests and replace environment variables
108 kpt fn source "${FOLDER}" | \
109 replace_env_vars "${FILTER}"
112 # Mix input with new generated manifests
116 kpt fn source "${FOLDER}"
123 # Function to convert source folder to `ResourceList` (no generator)
124 function folder2list() {
125 local FOLDER="${1:-}"
127 kpt fn source "${FOLDER}"
131 # Helper function to convert manifest to `ResourceList`
132 function manifest2list() {
133 kustomize cfg cat --wrap-kind ResourceList
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}}"
143 if [[ "${DRY_RUN,,}" == "true" ]];
147 kpt fn sink "${FOLDER}"
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}}"
159 if [[ "${DRY_RUN,,}" == "true" ]];
163 local TMPFOLDER=$(mktemp -d) || exit 1
164 kpt fn sink "${TMPFOLDER}/manifests"
166 # Copy the generated files over the target folder
167 mkdir -p "${FOLDER}/"
168 cp -r "${TMPFOLDER}/manifests/"* "${FOLDER}/"
170 # Delete temporary folder
171 rm -rf "${TMPFOLDER}"
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}}"
183 if [[ "${DRY_RUN,,}" == "true" ]];
187 local TMPFOLDER=$(mktemp -d) || exit 1
188 kpt fn sink "${TMPFOLDER}/manifests"
190 # Copy the generated files over the target folder
191 mkdir -p "${FOLDER}/"
192 rsync -arh --exclude ".git" --exclude ".*" --delete \
193 "${TMPFOLDER}/manifests/" "${FOLDER}/"
195 # Delete temporary folder
196 rm -rf "${TMPFOLDER}"
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() {
204 local TARGET_PROFILE_FOLDER="$2"
205 local MANIFEST_FILENAME="$3"
208 set_filename_to_items \
209 "${MANIFEST_FILENAME}" | \
210 prepend_folder_path \
212 list2folder_cp_over \
213 "${TARGET_PROFILE_FOLDER}"
217 # Set filename to `ResourceList` item
218 function set_filename_to_items() {
221 yq "(.items[]).metadata.annotations.\"config.kubernetes.io/path\" |= \"${FILENAME}\"" | \
222 yq "(.items[]).metadata.annotations.\"internal.config.kubernetes.io/path\" |= \"${FILENAME}\""
226 # Prepend folder path to `ResourceList`
227 function prepend_folder_path() {
230 if [[ (-z "${PREFIX}") || ("${PREFIX}" == ".") ]];
234 yq "(.items[]).metadata.annotations.\"config.kubernetes.io/path\" |= \"${PREFIX}\" + ." | \
235 yq "(.items[]).metadata.annotations.\"internal.config.kubernetes.io/path\" |= \"${PREFIX}\" + ."
240 # Rename file in `ResourceList`
241 function rename_file_in_items() {
242 local SOURCE_NAME="$1"
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}\""
250 # Get value from key in object in `ResourceList`
251 function get_value_from_resourcelist() {
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\")"
257 yq "(.items[]${TARGET_FILTERS})${KEY_PATH}"
261 # Patch "replace" to item in `ResourceList`
262 function patch_replace() {
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\")"
269 yq "(.items[]${TARGET_FILTERS})${KEY_PATH} = \"${VALUE}\""
273 # Add label to item in `ResourceList`
274 function set_label() {
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\")"
281 yq "(.items[]${TARGET_FILTERS}).metadata.labels.${KEY} = \"${VALUE}\""
285 # Patch which "appends" to list existing in item in `ResourceList`
286 function patch_add_to_list() {
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\")"
293 local VALUE_AS_JSON="$(echo "${VALUE}" | yq -o json -I0)"
295 yq "(.items[]${TARGET_FILTERS})${KEY_PATH} += ${VALUE_AS_JSON}"
299 # Patch which removes from list, existing in item in `ResourceList`
300 function patch_delete_from_list() {
302 local TARGET_FILTERS="${2:-}"
304 # local VALUE_AS_JSON="$(echo "${VALUE}" | yq -o json -I0)"
306 yq "del((.items[]${TARGET_FILTERS})${KEY_PATH})"
310 # Check if an element/value is in a given list, existing in item in `ResourceList`
311 function is_element_on_list() {
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\")"
320 yq "(.items[]${TARGET_FILTERS})${KEY_PATH} == \"${VALUE}\"" | grep "true"
323 if [[ "${TEST_RESULT}" != "true" ]]
332 # Patch "replace" to item in `ResourceList` using a JSON as value
333 function patch_replace_inline_json() {
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\")"
340 VALUE_AS_JSON="$(echo "${VALUE}" | yq -o=json)" yq "(.items[]${TARGET_FILTERS})${KEY_PATH} = strenv(VALUE_AS_JSON)"
344 # Delete full object from `ResourceList`
345 function delete_object() {
346 local OBJECT_NAME="$1"
348 local API_VERSION="${3:-""}"
351 if [[ -z "${API_VERSION}" ]]
353 # If `apiVersion` is not specified
354 local TARGET_FILTER="| select(.kind == \"${KIND_NAME}\") | select(.metadata.name == \"${OBJECT_NAME}\")"
356 # Otherwise, it is taken into account
357 local TARGET_FILTER="| select(.kind == \"${KIND_NAME}\") | select(.apiVersion == \"${API_VERSION}\") | select(.metadata.name == \"${OBJECT_NAME}\")"
361 yq "del((.items[]${TARGET_FILTER}))"
365 # Empty transformer function
366 function noop_transformer() {
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"
378 "${FULL_PATCH_CONTENT}" \
379 "| select(.kind == \"Kustomization\") | select(.metadata.name == \"${KUSTOMIZATION_NAME}\")"
383 # Helper function to produce a JSON Patch as specified in RFC 6902
384 function as_json_patch() {
386 local PATCH_PATH="$2"
389 # Convert to JSON dictionary to insert as map instead of string
390 local VALUES_AS_DICT=$(echo "${VALUES}" | yq -o=json)
392 # Generate a patch list
393 cat <<EOF | yq ".[0].value = ${VALUES_AS_DICT}"
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"
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}" )
410 # Accumulates value items into the patch
411 local PATCH_CONTENT=""
412 for VAL in "${VALUES[@]}"
414 local VAL_AS_DICT=$(echo "${VAL}" | yq -o=json)
417 yq --null-input ".op = \"${OPERATION}\", .path = \"${PATCH_PATH}\"" | \
418 yq ".value = ${VAL_AS_DICT}" | \
422 PATCH_CONTENT="$(echo -e "${PATCH_CONTENT}\n${ITEM}")"
425 # Wrap a full patch around, adding target specification
427 yq --null-input ".target.kind = \"${TARGET_KIND}\", .target.name = \"${TARGET_NAME}\"" | \
428 yq ".patch = \"${PATCH_CONTENT}\"" | \
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"
442 # Embed into patch list
443 local FULL_PATCH_CONTENT="$(
446 "${HELMRELEASE_NAME}" \
452 # Path via intermediate Kustomization object
453 add_patch_to_kustomization \
454 "${KUSTOMIZATION_NAME}" \
455 "${FULL_PATCH_CONTENT}"
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"
465 # Embed into patch list
466 local FULL_PATCH_CONTENT="$(
469 "${HELMRELEASE_NAME}" \
475 # Path via intermediate Kustomization object
476 add_patch_to_kustomization \
477 "${KUSTOMIZATION_NAME}" \
478 "${FULL_PATCH_CONTENT}"
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:-""}"
489 local YAML_VALUES_FROM_BOTH=$(cat <<EOF
491 name: "${VALUES_SECRET_NAME}"
493 name: "${VALUES_CM_NAME}"
496 local YAML_VALUES_FROM_SECRET=$(cat <<EOF
498 name: "${VALUES_SECRET_NAME}"
501 local YAML_VALUES_FROM_CM=$(cat <<EOF
503 name: "${VALUES_CM_NAME}"
507 # Chooses the appropriate YAML
509 if [[ ( -n "${VALUES_SECRET_NAME}" ) && ( -n "${VALUES_CM_NAME}" ) ]];
511 VALUES_FROM="${YAML_VALUES_FROM_BOTH}"
512 elif [[ -n "${VALUES_SECRET_NAME}" ]];
514 VALUES_FROM="${YAML_VALUES_FROM_SECRET}"
515 elif [[ -n "${VALUES_CM_NAME}" ]];
517 VALUES_FROM="${YAML_VALUES_FROM_CM}"
519 # If none is set, it must be an error
523 # Calls the low-level function
524 add_referenced_values_to_helmrelease_via_ks \
525 "${KUSTOMIZATION_NAME}" \
526 "${HELMRELEASE_NAME}" \
530 # Substitute environment variables from stdin
531 function replace_env_vars() {
532 # Optional parameter to filter environment variables that can be replaced
535 if [[ -n "${FILTER}" ]];
544 # Join two `ResourceList` **files**
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)
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() {
557 yq eval-all '. as $item ireduce ({}; . *+ $item)' \
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}" )
571 # Mix input with new generated manifests
575 "${SOURCER_FUNCTION}" \
578 set_filename_to_items "${MANIFEST_FILENAME}"
583 function transform_if() {
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}" )
590 # If test result is true (==0), then runs the transformation normally
591 if [[ "${TEST_RESULT}" == "0" ]];
594 # Otherwise, just pass through
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'
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}}"
613 pushd "${FLEET_REPO_DIR}"
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}}"
625 pushd "${FLEET_REPO_DIR}"
628 git commit -m "${COMMIT_MESSAGE}"
629 echo "${COMMIT_MESSAGE}"