From 83775ba1fd86a4d7dcf31a4b20859b29fe5f961f Mon Sep 17 00:00:00 2001 From: garciadeblas Date: Wed, 23 Jul 2025 18:35:24 +0200 Subject: [PATCH] Feature 11074: Enhanced OSM declarative modelling for applications. OSM's SDK for intent manipulation Change-Id: I6d03faa143eafcf30380b3b854c54f177dcf8f25 Signed-off-by: garciadeblas --- docker/osm-nushell-krm-functions/Dockerfile | 80 + .../krm/concatenate.nu | 105 + .../osm-nushell-krm-functions/krm/convert.nu | 147 ++ .../krm/generator.nu | 252 +++ .../krm/jsonpatch.nu | 107 + .../osm-nushell-krm-functions/krm/keypair.nu | 105 + docker/osm-nushell-krm-functions/krm/mod.nu | 47 + .../krm/overlaypatch.nu | 436 ++++ docker/osm-nushell-krm-functions/krm/patch.nu | 453 ++++ .../krm/strategicmergepatch.nu | 61 + .../configmap/templates/configmap.yaml | 24 + .../krm/tests/artifacts/empty/.gitkeep | 0 .../jenkins/manifests/bitnamicharts-repo.yaml | 27 + .../jenkins/manifests/jenkins-hr.yaml | 37 + .../jenkins/templates/jenkins-ks.yaml | 31 + .../jenkins/templates/jenkins-ns.yaml | 24 + .../namespace/templates/namespace.yaml | 22 + .../artifacts/secret/templates/secret.yaml | 24 + .../krm/tests/concatenate.nu | 251 +++ .../krm/tests/convert.nu | 448 ++++ .../krm/tests/generator.nu | 640 ++++++ .../krm/tests/jsonpatch.nu | 250 +++ .../krm/tests/keypair.nu | 214 ++ .../krm/tests/mod.nu | 67 + .../krm/tests/overlaypatch.nu | 1992 +++++++++++++++++ .../krm/tests/patch.nu | 1438 ++++++++++++ .../krm/tests/strategicmergepatch.nu | 144 ++ .../operations/app.nu | 211 ++ .../operations/brick.nu | 398 ++++ .../operations/custom/mod.nu | 109 + .../operations/ksu.nu | 233 ++ .../operations/location.nu | 173 ++ .../operations/mod.nu | 30 + .../operations/pattern.nu | 83 + .../operations/replace.nu | 58 + .../operations/tests/app.nu | 261 +++ .../example1/app-instance-from-model.yaml | 169 ++ .../apps/example1/expected_result.yaml | 218 ++ .../database-manifests/hrset-database.yaml | 58 + .../repo-zalando-postgres.yaml | 29 + .../main-brick-manifests/configmap.yaml | 23 + .../templates/main-pattern/ks-database.yaml | 36 + .../templates/main-pattern/ks-main.yaml | 35 + .../example2/app-instance-from-model.yaml | 270 +++ .../apps/example2/expected_result.yaml | 320 +++ .../database-manifests/hrset-database.yaml | 58 + .../repo-zalando-postgres.yaml | 29 + .../main-brick-manifests/configmap.yaml | 23 + .../templates/main-pattern/ks-database.yaml | 36 + .../templates/main-pattern/ks-main.yaml | 37 + .../operations/tests/brick.nu | 18 + .../operations/tests/ksu.nu | 19 + .../operations/tests/location.nu | 232 ++ .../operations/tests/mod.nu | 55 + .../operations/tests/pattern.nu | 18 + .../scripts/entrypoint-config.nu | 119 + .../scripts/entrypoint.sh | 40 + 57 files changed, 10824 insertions(+) create mode 100644 docker/osm-nushell-krm-functions/Dockerfile create mode 100644 docker/osm-nushell-krm-functions/krm/concatenate.nu create mode 100644 docker/osm-nushell-krm-functions/krm/convert.nu create mode 100644 docker/osm-nushell-krm-functions/krm/generator.nu create mode 100644 docker/osm-nushell-krm-functions/krm/jsonpatch.nu create mode 100644 docker/osm-nushell-krm-functions/krm/keypair.nu create mode 100644 docker/osm-nushell-krm-functions/krm/mod.nu create mode 100644 docker/osm-nushell-krm-functions/krm/overlaypatch.nu create mode 100644 docker/osm-nushell-krm-functions/krm/patch.nu create mode 100644 docker/osm-nushell-krm-functions/krm/strategicmergepatch.nu create mode 100644 docker/osm-nushell-krm-functions/krm/tests/artifacts/configmap/templates/configmap.yaml create mode 100644 docker/osm-nushell-krm-functions/krm/tests/artifacts/empty/.gitkeep create mode 100644 docker/osm-nushell-krm-functions/krm/tests/artifacts/jenkins/manifests/bitnamicharts-repo.yaml create mode 100644 docker/osm-nushell-krm-functions/krm/tests/artifacts/jenkins/manifests/jenkins-hr.yaml create mode 100644 docker/osm-nushell-krm-functions/krm/tests/artifacts/jenkins/templates/jenkins-ks.yaml create mode 100644 docker/osm-nushell-krm-functions/krm/tests/artifacts/jenkins/templates/jenkins-ns.yaml create mode 100644 docker/osm-nushell-krm-functions/krm/tests/artifacts/namespace/templates/namespace.yaml create mode 100644 docker/osm-nushell-krm-functions/krm/tests/artifacts/secret/templates/secret.yaml create mode 100644 docker/osm-nushell-krm-functions/krm/tests/concatenate.nu create mode 100644 docker/osm-nushell-krm-functions/krm/tests/convert.nu create mode 100644 docker/osm-nushell-krm-functions/krm/tests/generator.nu create mode 100644 docker/osm-nushell-krm-functions/krm/tests/jsonpatch.nu create mode 100644 docker/osm-nushell-krm-functions/krm/tests/keypair.nu create mode 100644 docker/osm-nushell-krm-functions/krm/tests/mod.nu create mode 100644 docker/osm-nushell-krm-functions/krm/tests/overlaypatch.nu create mode 100644 docker/osm-nushell-krm-functions/krm/tests/patch.nu create mode 100644 docker/osm-nushell-krm-functions/krm/tests/strategicmergepatch.nu create mode 100644 docker/osm-nushell-krm-functions/operations/app.nu create mode 100644 docker/osm-nushell-krm-functions/operations/brick.nu create mode 100644 docker/osm-nushell-krm-functions/operations/custom/mod.nu create mode 100644 docker/osm-nushell-krm-functions/operations/ksu.nu create mode 100644 docker/osm-nushell-krm-functions/operations/location.nu create mode 100644 docker/osm-nushell-krm-functions/operations/mod.nu create mode 100644 docker/osm-nushell-krm-functions/operations/pattern.nu create mode 100644 docker/osm-nushell-krm-functions/operations/replace.nu create mode 100644 docker/osm-nushell-krm-functions/operations/tests/app.nu create mode 100644 docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example1/app-instance-from-model.yaml create mode 100644 docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example1/expected_result.yaml create mode 100644 docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example1/manifests/main-pattern/database-manifests/hrset-database.yaml create mode 100644 docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example1/manifests/main-pattern/database-manifests/repo-zalando-postgres.yaml create mode 100644 docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example1/manifests/main-pattern/main-brick-manifests/configmap.yaml create mode 100644 docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example1/templates/main-pattern/ks-database.yaml create mode 100644 docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example1/templates/main-pattern/ks-main.yaml create mode 100644 docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example2/app-instance-from-model.yaml create mode 100644 docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example2/expected_result.yaml create mode 100644 docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example2/manifests/main-pattern/database-manifests/hrset-database.yaml create mode 100644 docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example2/manifests/main-pattern/database-manifests/repo-zalando-postgres.yaml create mode 100644 docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example2/manifests/main-pattern/main-brick-manifests/configmap.yaml create mode 100644 docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example2/templates/main-pattern/ks-database.yaml create mode 100644 docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example2/templates/main-pattern/ks-main.yaml create mode 100644 docker/osm-nushell-krm-functions/operations/tests/brick.nu create mode 100644 docker/osm-nushell-krm-functions/operations/tests/ksu.nu create mode 100644 docker/osm-nushell-krm-functions/operations/tests/location.nu create mode 100644 docker/osm-nushell-krm-functions/operations/tests/mod.nu create mode 100644 docker/osm-nushell-krm-functions/operations/tests/pattern.nu create mode 100644 docker/osm-nushell-krm-functions/scripts/entrypoint-config.nu create mode 100755 docker/osm-nushell-krm-functions/scripts/entrypoint.sh diff --git a/docker/osm-nushell-krm-functions/Dockerfile b/docker/osm-nushell-krm-functions/Dockerfile new file mode 100644 index 00000000..e3f1db3e --- /dev/null +++ b/docker/osm-nushell-krm-functions/Dockerfile @@ -0,0 +1,80 @@ +####################################################################################### +# Copyright ETSI Contributors and Others. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +####################################################################################### + +FROM alpine:3.21.3 +# FROM ghcr.io/nushell/nushell:0.104.0-alpine + +ARG NUSHELL_VERSION=0.104.1 + + +# Install Nushell for Alpine +#----------------------------------------------------- +# +RUN echo '/usr/bin/nu' >> /etc/shells \ + && adduser -D -s /usr/bin/nu nushell \ + && mkdir -p /home/nushell/.config/nushell/ \ + && cd /tmp \ + && wget "https://github.com/nushell/nushell/releases/download/${NUSHELL_VERSION}/nu-${NUSHELL_VERSION}-x86_64-unknown-linux-musl.tar.gz" \ + && mkdir nu-${NUSHELL_VERSION} && tar xvf nu-*.tar.gz --directory=nu-${NUSHELL_VERSION} \ + && cp -aR nu-${NUSHELL_VERSION}/**/* /usr/bin/ \ + # Setup default config file for nushell + && cd /home/nushell/.config/nushell \ + && chmod +x /usr/bin/nu \ + && chown -R nushell:nushell /home/nushell/.config/nushell \ + # Reset Nushell config to default + && su -c 'config reset -w' nushell \ + && ls /usr/bin/nu_plugin* \ + | xargs -I{} su -c 'plugin add {}' nushell \ + && rm -rf /tmp/* +# +#----------------------------------------------------- + +# Install dependencies +RUN apk add --no-cache \ + age \ + envsubst \ + git \ + kubectl \ + sops && \ + # Install dependencies unavailable at Alpine repos + wget https://github.com/kptdev/kpt/releases/download/v1.0.0-beta.57/kpt_linux_amd64 -O kpt && \ + chmod +x kpt && \ + mv kpt /usr/local/bin/ && \ + # Create default folders and files + mkdir -p /repos && \ + chmod 755 /repos && \ + mkdir -p /model/parameters/clear && \ + mkdir -p /model/parameters/secret && \ + chmod -R 755 /model && \ + echo '{}' > /model/app_instance_model.yaml && \ + echo '{}' > /model/parameters/clear/environment.yaml && \ + echo '{}' > /model/parameters/secret/environment.yaml + + +# Become the standard user +USER nushell +WORKDIR /app + +# Add libraries and helper scripts +COPY --chown=nushell:nushell scripts /app/scripts +COPY --chown=nushell:nushell krm /app/osm/krm +COPY --chown=nushell:nushell operations /app/osm/operations + +# Add the entrypoint and default command +ENTRYPOINT ["/bin/sh", "/app/scripts/entrypoint.sh"] + +CMD ["nu"] diff --git a/docker/osm-nushell-krm-functions/krm/concatenate.nu b/docker/osm-nushell-krm-functions/krm/concatenate.nu new file mode 100644 index 00000000..dde50959 --- /dev/null +++ b/docker/osm-nushell-krm-functions/krm/concatenate.nu @@ -0,0 +1,105 @@ +####################################################################################### +# Copyright ETSI Contributors and Others. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +####################################################################################### + +# Module with utility functions to concatenate descriptions of Kubernetes resources, either embedded in ResourceLists or from plain multi-resource manifests (i.e. lists of records). + + +# Join two ResourceLists, one from stdin and another from the first argument. +# Empty records at stdin or at the parameter are valid and should be treated as empty ResourceLists. +export def resourcelists [ + resourcelist2: record # 2nd `ResourceList` to concatenate +]: [ + record -> record + nothing -> record +] { + # Gather the input and convert to record if empty + let list1: record = if $in == null { {} } else { $in } + let list2: record = if $resourcelist2 == null { {} } else { $resourcelist2 } + + # If both are empty, returns an empty ResourceList + if $list1 == {} and $list2 == {} { + { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [] + } + } else if $list2 == {} { + # If the second ResourceList is empty, returns just the one from stdin + $list1 + } else { + # Merge both resource lists strategically + { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: ($list1.items? | append $list2.items?) + } + # ALTERNATIVELY: $in_list | merge deep --strategy "append" $source_list + ## Strategy is "append", so that item lists are appended + } +} + +export alias resourcelist = resourcelists +export alias rl = resourcelists + + +# Join two ResourceList files +# NOT EXPORTED +def "resourcelists from files" [file1: path, file2: path] { + let list1 = (open $file1) + let list2 = (open $file2) + $list1 | merge $list2 +} + +alias join_lists = resourcelists from files + + +# Join two manifests, one from stdin and another from the first argument +# Empty manifests at stdin or at the parameter are valid and should be treated as empty manifests. +export def manifests [ + mnfst2: any # 2nd manifest to concatenate +]: [ + any -> list +] { + + # Gather the input and convert to list + # let manifest1: list = if $in == null { [] } else { $in } + # let manifest2: list = if $mnfst2 == null { [] } else { $mnfst2 } + let manifest1: list = (if $in == null { [] } + else if ($in | describe | str starts-with "record") { [ $in ] } + else if ($in | describe | str starts-with "list") or ($in | describe | str starts-with "table") { $in } + else { error make {msg: $"Error: Expected a record or a list of records, but received ($in | describe)."}}) + + let manifest2: list = (if $mnfst2 == null { [] } + else if ($mnfst2 | describe | str starts-with "record") { [ $mnfst2 ] } + else if ($mnfst2 | describe | str starts-with "list") or ($mnfst2 | describe | str starts-with "table") { $mnfst2 } + else { error make {msg: $"Error: Expected a record or a list of records, but received ($mnfst2 | describe)."}}) + + # Return the concatenation + [ + $manifest1 + $manifest2 + ] | flatten + + # How to convert to YAML manifests again: + # + # let merged_manifests = ($manifest1 | manifests $manifest2) + # $merged_manifests + # | each { |obj| $obj | to yaml } + # | str join "---\n" +} + +export alias manifest = manifests diff --git a/docker/osm-nushell-krm-functions/krm/convert.nu b/docker/osm-nushell-krm-functions/krm/convert.nu new file mode 100644 index 00000000..07578a0a --- /dev/null +++ b/docker/osm-nushell-krm-functions/krm/convert.nu @@ -0,0 +1,147 @@ +####################################################################################### +# Copyright ETSI Contributors and Others. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +####################################################################################### + +# Module with functions to convert between the different alternative representations of a set of Kubernetes resources: folders with manifests, lists of records (concatenated manifests), or ResourceLists. + + +use ./concatenate.nu + + + +# Substitute environment variables in text from stdin. +# +# Environment variables can be listed either as a string in the format accepted by the `envsubst` command (e.g. `$VAR1,$VAR2`) or as a list of strings with the environment variable names that will be formatted by the function. In case the parameter is empty, it should invoke `envsubst` with no parameters, so that it replaces all the environment variables that are found. +export def "replace environment variables" [vars_to_replace: any = ""]: [ + string -> string +] { + # Gather reference to stdin before it is lost + let text: string = $in + + # Adapt the input as needed + let filter: string = (if ($vars_to_replace | describe) == "string" { + # If it is a string, it can be used directly as filter for envsubst + $vars_to_replace + } else if ($vars_to_replace | describe) == "list" { + # If it is a list of strings, we can concatenate them in a single string with the right format for envsubst + ($vars_to_replace | each {|var| $"${($var)}" } | str join ',') + } else { + # Handle unexpected type for $vars_to_replace + error make {msg: $"Error: Expected a string or list of strings, but received ($vars_to_replace | describe)"} + }) + + # Proceed with the substitution + if ($filter | is-empty) { + $text | ^envsubst + } else { + $text | ^envsubst $filter + } +} + +alias replace_env_vars = replace environment variables + + +# Convert manifests in a source folder to a ResourceList +export def "folder to resourcelist" [ + --subst-env, # Set if environment variables should be replaced + folder: path, + env_filter?: any = "" +]: [ + record -> record + nothing -> record +] { + # Gather the input and convert to record if empty + let in_list: record = if $in == null { {} } else { $in } + + # Create a ResourceList from the source folder and substitute environment variables if needed + let source_list: record = ( + kpt fn source $folder + | if $subst_env { + $in | replace environment variables $env_filter + } else { + $in + } + | from yaml + ) + + # Merge both resource lists carefully + $in_list | concatenate resourcelists $source_list +} + +export alias "folder to rl" = folder to resourcelist +export alias folder2list_generator = folder to resourcelist + + +# Convert a manifest from stdin to a ResourceList +## NOTE: It is an equivalent with type-checks to: +## kustomize cfg cat --wrap-kind ResourceList +export def "manifest to resourcelist" []: [ + any -> record +] { + # Gather the input and convert to list + let manifest_in: list = (if $in == null { [] } + else if ($in | describe | str starts-with "record") { [ $in ] } + else if ($in | describe | str starts-with "list") or ($in | describe | str starts-with "table") { $in } + else { error make {msg: $"Error: Expected a record or a list of records, but received ($in | describe)."}}) + + { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: $manifest_in + } +} + +export alias manifest2list = manifest to resourcelist + + +# Convert a ResourceList to file manifests in a target folder +export def "resourcelist to folder" [ + --sync, # If sync is true, replaces all contents in the folder, otherwise just copies over. + folder: path, + dry_run?: bool = false # If true, just prints the ResourceList but does not render any file +]: [ + record -> any +] { + + # Preserves the input value + let list_in: record = $in + + # As optional parameter, defaults to $env.DRY_RUN when not set + let is_dry_run: bool = if $dry_run == null { $env.DRY_RUN | into bool } else { $dry_run } + + # If it is a dry-run, just prints the input ResourceList and exits + if $is_dry_run { + return ($list_in | to yaml) + } + + # First, render the manifests to a temporary folder + let tmp_folder: string = (mktemp -t -d) + let tmp_target: string = ($tmp_folder | path join manifests) + $list_in | to yaml | ^kpt fn sink $tmp_target + + # Writes the contents to the target folder + if $sync { + # Sync actually removes any previous contents in the folder + rm -rf $folder + } + mkdir $folder + ls $tmp_target | get name | cp -r ...$in $folder + + # Removes the temporary folder + rm -rf $tmp_folder +} + +export alias list2folder = resourcelist to folder diff --git a/docker/osm-nushell-krm-functions/krm/generator.nu b/docker/osm-nushell-krm-functions/krm/generator.nu new file mode 100644 index 00000000..98d52b5c --- /dev/null +++ b/docker/osm-nushell-krm-functions/krm/generator.nu @@ -0,0 +1,252 @@ +####################################################################################### +# Copyright ETSI Contributors and Others. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +####################################################################################### + +# Module with KRM generator functions for various kinds of Kubernetes resources. + + +use ./convert.nu +use ./concatenate.nu +use ./patch.nu +use ./keypair.nu + + +# KRM generator function with input from a ResourceList. +# Works as generic generator function to insert any resource from a ResourceList, passed as parameter, into another ResourceList, passed from stdin. +export def "from resourcelist" [ + rl: record +]: [ + record -> record +] { + # Regularizes ResourceList from stdin + let list1: record = if $in == null { {} } else { $in } + + # Regularizes the second ResourceList + let list2: record = if $rl == null { {} } else { $rl } + + # Checks that both ResourceLists are actual ResourceLists + {stdin: $list1, "input parameter": $list2} | items { |name, rl| + if ( + $rl != {} + and ( + ($rl | get -i kind) != "ResourceList" + or ($rl | get -i apiVersion) != "config.kubernetes.io/v1" + ) + ) { + error make {msg: $"Error: Expected a ResourceList, but received ($rl) from ($name)."} + } + } + + # Merges both ResourceLists + $list1 + | concatenate resourcelists $list2 +} + + +# KRM generator function with input from a manifest +# +# Example of use: Generator from an encrypted secret: +# +# use ./keypair.nu +# +# let secret_value: string = "my_secret_value" +# let secret_name: string = "mysecret" +# let secret_key: string = "mykey" +# let public_key: string = "age1s236gmpr7myjjyqfrl6hwz0npqjgxa9t6tjj46yq28j2c4nk653saqreav" +# +# {} +# | generator from manifest ( +# $secret_value +# | (^kubectl create secret generic ($secret_name) +# --from-file=($secret_key)=/dev/stdin +# --dry-run=client +# -o yaml) +# | keypair encrypt_secret_from_stdin $public_key +# | from yaml +# ) +# | to yaml +# +# RESULT: +# +# apiVersion: config.kubernetes.io/v1 +# kind: ResourceList +# items: +# - apiVersion: v1 +# data: +# mykey: ENC[AES256_GCM,data:XKTW8X5ZI6c3yWYtyOPUP/UskKc=,iv:ZOkqLmSgXNCNCQrsMUq7iDL05rklDBuTaVS6E5Bgyl8=,tag:/2rLYqnh+RJWWH4OmEHJBA==,type:str] +# kind: Secret +# metadata: +# creationTimestamp: null +# name: mysecret +# sops: +# kms: [] +# gcp_kms: [] +# azure_kv: [] +# hc_vault: [] +# age: +# - recipient: age1s236gmpr7myjjyqfrl6hwz0npqjgxa9t6tjj46yq28j2c4nk653saqreav +# enc: | +# -----BEGIN AGE ENCRYPTED FILE----- +# YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSByNk9KWnhBa2xiMFpyT1Fj +# QWw3aGxRZnhHbUNOcXUvc05zZDZIckdPWWtNCm91VzUwU2l5NnVSajJyQkhBMldK +# ZkJYWXFTd1J5Q2Z1cTJ6MExkeVBWVXcKLS0tIHpKQ0EvdmpzNS9nZFFHK0JoV0Rx +# NXRyMXROK2p3bkpnOXowQ1RYdFk2blkKzsJiw31EA7hZbcRaHe0RkjsrSs7GQjXc +# YNAtoPquu0xaocX3pEUV/aojG/WejNY7peDXVDI43yfv8eJlO072Sw== +# -----END AGE ENCRYPTED FILE----- +# lastmodified: 2025-03-10T17:47:08Z +# mac: ENC[AES256_GCM,data:JZttY7AvtRmVaJpCIdJc4Tve7EykKpR7SETQoR7fSiFOVfm4EX+ZcwYoxQYiMsNWXnx/K/IAo8VKoT1+x/lsyFTFucP3YsZ35cfXtAPt43d+gi+IEYS9hfjDQL4BmLAlIiwmij0QGOzcWFFSDhatD717zIBzEDbs2qNGHTqc68E=,iv:Dtiwbvb7LPTyShw2DrnpM/EAWdLyxSDimh7Kk15Jox4=,tag:1VBGnQbotN5KDSmznvNPdg==,type:str] +# pgp: [] +# encrypted_regex: ^(data|stringData)$ +# version: 3.8.1 +export def "from manifest" [ + manifest: any +]: [ + record -> record + nothing -> record +] { + # Keeps prior ResourceList, with regularization if needed + let in_rl: record = if $in == null { {} } else { $in } + + # Regularizes the manifest in the parameter so that is is a list + let manifest1: list = (if $manifest == null { [] } + else if ($manifest | describe | str starts-with "record") { [ $manifest ] } + else if ($manifest | describe | str starts-with "list") or ($manifest | describe | str starts-with "table") { $manifest } + else { error make {msg: $"Error: Expected a record or a list of records, but received ($manifest | describe)."}}) + + # Creates a ResourceList from the manifest and merges with ResourceList from stdin + $in_rl + | concatenate resourcelists ($manifest1 | convert manifest to resourcelist) +} + + +# KRM generator function for a ConfigMap +export def "configmap" [ + --filename: string, # File name to keep the manifest + --index: int, # Number of the index in the file, for multi-resource manifests + key_pairs: record, # Key-value pairs to add to the ConfigMap + name: string, + namespace?: string = "default" +]: [ + record -> record + nothing -> record +] { + # Regularizes ResourceList from stdin + let in_rl: record = if $in == null { {} } else { $in } + + $in_rl + | ( from manifest + # ConfigMap manifest structure + { + apiVersion: v1, + kind: ConfigMap, + metadata: { + name: $name, + namespace: $namespace, + }, + data: $key_pairs + } + ) + # Add file name if required + | if ($filename | is-empty) { + $in + } else { + $in + | (patch resource filename set + --index $index + $filename + "v1" + "ConfigMap" + $name + $namespace + ) + } +} + + +# KRM generator function for a Secret +export def "secret" [ + --filename: string, # File name to keep the manifest + --index: int, # Number of the index in the file, for multi-resource manifests + --public-age-key: string # Age key to encrypt the contents of the Secret manifest + --type: string # Type of Kubernetes secret. Built-in types: `Opaque` (default), `kubernetes.io/service-account-token`, `kubernetes.io/dockercfg`, `kubernetes.io/dockerconfigjson`, `kubernetes.io/basic-auth`, `kubernetes.io/ssh-auth`, `kubernetes.io/tls`, `bootstrap.kubernetes.io/token` + key_pairs: record, # Key-value pairs to add to the Secret + name: string, + namespace?: string = "default" +]: [ + record -> record + nothing -> record +] { + # Regularizes ResourceList from stdin + let in_rl: record = if $in == null { {} } else { $in } + + # Encode the values with base64 + let encoded_key_pairs: record = ( + ($key_pairs | columns) + | zip ( + $key_pairs + | values + | each {$in | encode base64} + ) + | reduce -f {} {|it, acc| $acc | upsert $it.0 $it.1 } + ) + + # Generate the secret + $in_rl + | ( from manifest + # ConfigMap manifest structure + ( + { + apiVersion: v1, + kind: Secret, + metadata: { + name: $name, + namespace: $namespace, + }, + data: $encoded_key_pairs + } + # Add Secret type if specified + | if ($type | is-empty) { + $in + } else { + $in + | insert type $type + } + # Encode if an age key was supplied + | if ($public_age_key | is-empty) { + $in + } else { + $in + | to yaml + | keypair encrypt_secret_from_stdin $public_age_key + | from yaml + } + ) + ) + # Add file name if required + | if ($filename | is-empty) { + $in + } else { + $in + | (patch resource filename set + --index $index + $filename + "v1" + "Secret" + $name + $namespace + ) + } +} diff --git a/docker/osm-nushell-krm-functions/krm/jsonpatch.nu b/docker/osm-nushell-krm-functions/krm/jsonpatch.nu new file mode 100644 index 00000000..1718b9b4 --- /dev/null +++ b/docker/osm-nushell-krm-functions/krm/jsonpatch.nu @@ -0,0 +1,107 @@ +####################################################################################### +# Copyright ETSI Contributors and Others. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +####################################################################################### + +# Module with helper functions to create JSON patches for Kubernetes resources as per RFC6902 (patchJson6902). + + +# Helper function to create an operation for a JSON patch (patchJson6902) as per RFC6902 +# +# Example: +# $ jsonpatch create operation add /spec/template/spec/securityContext {runAsUser: 10000, fsGroup: 1337} | to yaml +# +# op: add +# path: /spec/template/spec/securityContext +# value: +# runAsUser: 10000 +# fsGroup: 1337 +export def "create operation" [ + op: string, # Operation type: "add", "remove", "replace", "move", "copy", or "test" + path: string, # JSON pointer path at the target key location in format "/a/b/c" + value?: any # Value to be added, replaced, or removed + from?: string, # JSON pointer path (format "/a/b/c") at the TARGET RESOURCE to take as source in "copy" or "move" operations. +]: [ + nothing -> record +] { + if $op in ["add", "replace"] { + if not ($value | is-empty) { + { + op: $op, + path: $path, + value: $value + } + } else { + error make { msg: "Value is required for 'add' and 'replace' operations." } + } + } else if $op in ["remove"] { + { + op: $op, + path: $path + } + } else if $op in ["move", "copy"] { + if not ($from | is-empty) { + { + op: $op, + from: $from, + path: $path + } + } else { + error make { msg: "Source path is required for 'move' and 'copy' operations." } + } + } else { + error make { msg: "Invalid operation type. Supported values are 'add', 'remove', 'replace', 'move', 'copy'. See RFC6902 for details." } + } +} + + +# Helper to create a full JSON patch (patchJson6902), including the target object specification and a list of operations +# +# Example 1: Using records directly +# $ jsonpatch create {kind: Deployment, name: podinfo} {op: add, target: /spec/template/spec/securityContext, value: {runAsUser: 10000, fsGroup: 1337}} | to yaml +# +# target: +# kind: Deployment +# name: podinfo +# patch: | +# - op: add +# path: /spec/template/spec/securityContext +# value: +# runAsUser: 10000 +# fsGroup: 1337 +# +# Example 2: Leveraging the operation helper function +# $ jsonpatch create {kind: Deployment, name: podinfo} (jsonpatch create operation add /spec/template/spec/securityContext {runAsUser: 10000, fsGroup: 1337}) | to yaml +# +# target: +# kind: Deployment +# name: podinfo +# patch: | +# - op: add +# path: /spec/template/spec/securityContext +# value: +# runAsUser: 10000 +# fsGroup: 1337 +export def "create" [ + target: record, # Target resource specification as per + ...operations: record # List of patch operations as per RFC6902 +]: [ + nothing -> record +] { + { + target: $target, + patch: ($operations | to yaml) + } +} diff --git a/docker/osm-nushell-krm-functions/krm/keypair.nu b/docker/osm-nushell-krm-functions/krm/keypair.nu new file mode 100644 index 00000000..83689e38 --- /dev/null +++ b/docker/osm-nushell-krm-functions/krm/keypair.nu @@ -0,0 +1,105 @@ +####################################################################################### +# Copyright ETSI Contributors and Others. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +####################################################################################### + +# Module with custom commands to create and manage age key pairs for SOPS encryption/decryption of Kubernetes secrets. + + +# Create a new age key pair +export def "create age" [ + age_key_name: string, + credentials_dir?: path # Optional, defaults to $env.CREDENTIALS_DIR +] { + let dir: path = if $credentials_dir == null { $env.CREDENTIALS_DIR } else { $credentials_dir } + let key_path: path = ({ parent: $dir, stem: $age_key_name, extension: "key"} | path join) + let pub_path: path = ({ parent: $dir, stem: $age_key_name, extension: "pub"} | path join) + + # Delete existing keys + rm -f $key_path $pub_path + + # Generate private key + ^age-keygen -o $key_path + + # Extract public key + ^age-keygen -y $key_path | save $pub_path +} + +export alias create_age_keypair = create age + + +# In-place encrypt secrets in manifest +# -- NOT EXPORTED -- +def "encrypt secret inplace" [ + file: path, + public_key: string +]: [ + nothing -> nothing +] { + ^sops --age $public_key --encrypt --encrypted-regex '^(data|stringData)$' --in-place $file +} + +export alias encrypt_secret_inplace = encrypt secret inplace + + +# Encrypt with SOPS a manifest of Kubernetes secret received from stdin +export def "encrypt secret manifest" [public_key: string]: [ + string -> string +] { + # Saves the input to preserve it from multiple invokes + let manifest: string = $in + + # If the input empty, just returns an empty string + if $manifest == "" { + return "" + } + + let tmp_file = (mktemp -t --suffix .yaml) + $manifest | save -f $tmp_file + + ^sops --age $public_key --encrypt --encrypted-regex '^(data|stringData)$' --in-place $tmp_file + + let content: string = (open $tmp_file | to yaml) + rm -f $tmp_file + $content +} + +export alias encrypt_secret_from_stdin = encrypt secret manifest + + +# Decrypt with SOPS a manifest of a Kubernetes secret received from stdin +export def "decrypt secret manifest" [private_key: string]: [ + string -> string +] { + # Saves the input to preserve it from multiple invokes + let encrypted_manifest: string = $in + + # If the input empty, just returns an empty string + if $encrypted_manifest == "" { + return "" + } + + # Decrypt using temporary file + let tmp_encrypted_file = (mktemp -t --suffix .yaml) + $encrypted_manifest | save -f $tmp_encrypted_file + let decrypted_manifest: string = ( + $private_key + | SOPS_AGE_KEY_FILE="/dev/stdin" sops --decrypt $tmp_encrypted_file + ) + rm $tmp_encrypted_file # Clean up temporary key file + + # Returns the decrypted secret + $decrypted_manifest +} diff --git a/docker/osm-nushell-krm-functions/krm/mod.nu b/docker/osm-nushell-krm-functions/krm/mod.nu new file mode 100644 index 00000000..57b45074 --- /dev/null +++ b/docker/osm-nushell-krm-functions/krm/mod.nu @@ -0,0 +1,47 @@ +####################################################################################### +# Copyright ETSI Contributors and Others. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +####################################################################################### + +# Meta-module of helper functions to manage, transform and generate specifications of Kubernetes resources. +# This meta-module comprises all the modules of the OSM's SDK for App Modelling. + + +# Import submodules +export module ./keypair.nu +export module ./concatenate.nu +export module ./convert.nu +export module ./generator.nu +export module ./patch.nu +export module ./jsonpatch.nu +export module ./strategicmergepatch.nu +export module ./overlaypatch.nu + + +# Convert input string to a safe name for Kubernetes resources +export def "safe resource name" [input: string]: [ + nothing -> string +] { + $input + | str downcase + | str replace -a './' '' + | str replace -a '.' '-' + | str replace -a '/' '-' + | str replace -a '_' '-' + | str replace -a ' ' '-' + | str replace -a ':' '-' +} + +export alias safe_name = safe resource name diff --git a/docker/osm-nushell-krm-functions/krm/overlaypatch.nu b/docker/osm-nushell-krm-functions/krm/overlaypatch.nu new file mode 100644 index 00000000..1fcaf0dd --- /dev/null +++ b/docker/osm-nushell-krm-functions/krm/overlaypatch.nu @@ -0,0 +1,436 @@ +####################################################################################### +# Copyright ETSI Contributors and Others. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +####################################################################################### + +# Module with custom commands to generate `overlay patches`, i.e., patches to a Kustomization that references the resources that we intend to patch at runtime. + + +use ./patch.nu +use ./jsonpatch.nu +use ./strategicmergepatch.nu +use ./generator.nu + + +# Add overlay patch to Kustomization item (in a ResourceList) to modify a key in a referenced resource, using the JSON patch (patchJson6902) format +export def "add patch" [ + --ks-namespace: string, # Namespace of the Kustomization + kustomization_name: string, # Kustomization to add the patch to + target: record, # Target resource for the patch, as per + patch_value: record # Patch content as record type. It can be a JSON patch (patchJson6902) or a Strategic Merge Patch +]: [ + record -> record +] { + let in_resourcelist: record = $in + + let patch_content: record = ( + { + target: $target, + patch: ($patch_value | to yaml) + } + ) + + $in_resourcelist + | (patch list append item + $.spec.patches + $patch_content + "kustomize.toolkit.fluxcd.io/v1" + "Kustomization" + $kustomization_name + $ks_namespace + ) +} + + +# Add an overlay patch to a Kustomization item (in a ResourceList) to modify a key in a referenced resource, using the JSON patch (patchJson6902) format +# This command provides a user-friendly interface to create a JSON patch with exactly ONE operation +export def "add jsonpatch" [ + --ks-namespace: string, # Namespace of the Kustomization + --operation: string = "add", # Operation types: "add", "remove", "replace", "move", "copy", or "test", as per RFC6902 + kustomization_name: string, # Kustomization to add the patch to + target: record, # Target resource for the patch, as per + path: string, # JSON pointer path (format "/a/b/c") at the TARGET RESOURCE to be patched. + value?: any # Value to set in the target path (required for "add" and "replace" operations) + from?: string, # JSON pointer path (format "/a/b/c") at the TARGET RESOURCE to take as source in "copy" or "move" operations. +]: [ + record -> record +] { + let in_resourcelist: record = $in + + let operation_spec: record = ( + if $operation in ["add", "replace"] { + { + op: $operation, + path: $path, + value: $value + } + } else if $operation in ["remove"] { + { + op: $operation, + path: $path + } + } else if $operation in ["move", "copy"] { + { + op: $operation, + from: $from, + path: $path + } + } else { + error make { msg: "Invalid operation type. Supported values are 'add', 'remove', 'replace', 'move', 'copy'. See RFC6902 for details" } + } + ) + + let patch_content: record = ( + jsonpatch create + $target + $operation_spec + ) + + $in_resourcelist + | (patch list append item + $.spec.patches + $patch_content + "kustomize.toolkit.fluxcd.io/v1" + "Kustomization" + $kustomization_name + $ks_namespace + ) +} + + +# Add a StrategicMergePatch to a Kustomization item (in a ResourceList) to modify a key in a referenced resource +# This command provides a user-friendly interface to create a patch +export def "add strategicmergepatch" [ + --ks-namespace: string, # Namespace of the Kustomization + kustomization_name: string, # Kustomization to add the patch to + target: record, # Target resource for the patch, as per + patch: record, # Contents of the strategic patch in the format of a record +]: [ + record -> record +] { + let in_resourcelist: record = $in + + let patch_content: record = ( + strategicmergepatch create + $target + $patch + ) + + $in_resourcelist + | (patch list append item + $.spec.patches + $patch_content + "kustomize.toolkit.fluxcd.io/v1" + "Kustomization" + $kustomization_name + $ks_namespace + ) +} + + +# Modify a referenced HelmRelease to add inline values via an overlay patch in a Kustomization (in a ResourceList) +export def "helmrelease add inline values" [ + --ks-namespace: string, # Namespace of the Kustomization + --hr-namespace: string, # Namespace of the HelmRelease + --operation: string = "add", # Allowed operation types: "add", "replace". Default is "add" + kustomization_name: string, # Kustomization to add the patch to + helmrelease_name: string, # HelmRelease to add the values to + values: record # Helm values to include inline in the HelmRelease spec +]: [ + record -> record +] { + let in_resourcelist: record = $in + + # Exit if the operation is not supported + if $operation not-in ["add", "replace"] { + error make { msg: "Invalid operation type. Supported values are 'add', 'replace'. See RFC6902 for details" } + } + + $in_resourcelist + | (add jsonpatch + --ks-namespace $ks_namespace + --operation $operation + $kustomization_name + ( + if ($hr_namespace | is-empty) { + { kind: "HelmRelease", name: $helmrelease_name } + } else { + { kind: "HelmRelease", name: $helmrelease_name, namespace: $hr_namespace } + } + ) + "/spec/values" + $values + ) + +} + + +# Modify a referenced HelmRelease to add values from a ConfigMap via an overlay patch in a Kustomization (in a ResourceList) +export def "helmrelease add values from configmap" [ + --ks-namespace: string, # Namespace of the Kustomization + --hr-namespace: string, # Namespace of the HelmRelease + --target-path: string, # Optional `targetPath` to merge the values to (optional) + --optional, # Optional flag to indicate if the values reference is optional + kustomization_name: string, # Kustomization to add the patch to + helmrelease_name: string, # HelmRelease to add the values to + cm_name: string # ConfigMap to read the values from + cm_key?: string = "values.yaml" # ConfigMap key to read the values from +]: [ + record -> record +] { + let in_resourcelist: record = $in + + # Record to reference the values in the ConfigMap and, optionally, specify on how to merge them + let full_reference: record = { + kind: "ConfigMap", + name: $cm_name, + valuesKey: $cm_key + } + | ( + if ($target_path | is-empty) { + $in + } else { + $in | insert targetPath $target_path + } + ) | ( + if $optional { + $in | insert optional true + } else { + $in + } + ) + + $in_resourcelist + | ( + add strategicmergepatch + --ks-namespace $ks_namespace + $kustomization_name + ( + if ($hr_namespace | is-empty) { + { kind: "HelmRelease", name: $helmrelease_name } + } else { + { kind: "HelmRelease", name: $helmrelease_name, namespace: $hr_namespace } + } + ) + { + apiVersion: "helm.toolkit.fluxcd.io/v2", + kind: "HelmRelease", + metadata: ( + if ($hr_namespace | is-empty) { + { name: $helmrelease_name } + } else { + { name: $helmrelease_name, namespace: $hr_namespace } + } + ), + spec: { + valuesFrom: [ + $full_reference + ] + } + } + ) +} + +alias "helmrelease add values from cm" = helmrelease add values from configmap + + +# Modify a referenced HelmRelease to add values from a Secret via an overlay patch in a Kustomization (in a ResourceList) +export def "helmrelease add values from secret" [ + --ks-namespace: string, # Namespace of the Kustomization + --hr-namespace: string, # Namespace of the HelmRelease + --target-path: string, # Optional `targetPath` to merge the values to (optional) + --optional, # Optional flag to indicate if the values reference is optional + kustomization_name: string, # Kustomization to add the patch to + helmrelease_name: string, # HelmRelease to add the values to + secret_name: string # Secret to read the values from + secret_key?: string = "values.yaml" # Secret key to read the values from +]: [ + record -> record +] { + let in_resourcelist: record = $in + + # Record to reference the values in the Secret and, optionally, specify on how to merge them + let full_reference: record = { + kind: "Secret", + name: $secret_name, + valuesKey: $secret_key + } + | ( + if ($target_path | is-empty) { + $in + } else { + $in | insert targetPath $target_path + } + ) | ( + if $optional { + $in | insert optional true + } else { + $in + } + ) + + $in_resourcelist + | ( + add strategicmergepatch + --ks-namespace $ks_namespace + $kustomization_name + ( + if ($hr_namespace | is-empty) { + { kind: "HelmRelease", name: $helmrelease_name } + } else { + { kind: "HelmRelease", name: $helmrelease_name, namespace: $hr_namespace } + } + ) + { + apiVersion: "helm.toolkit.fluxcd.io/v2", + kind: "HelmRelease", + metadata: ( + if ($hr_namespace | is-empty) { + { name: $helmrelease_name } + } else { + { name: $helmrelease_name, namespace: $hr_namespace } + } + ), + spec: { + valuesFrom: [ + $full_reference + ] + } + } + ) +} + + +# Umbrella command to add values to a HelmRelease via an overlay patch to a Kustomization, using either inline values, a reference to a ConfigMap and/or a reference to a Secret. +# Parameters representing values (`inline_values`, `cm_name` or `secret_name`) that are empty will be skipped; only non-empty parameters will be used and add an overlay patch. +export def "helmrelease set values" [ + --ks-namespace: string, # Namespace of the Kustomization + --hr-namespace: string, # Namespace of the HelmRelease (optional) + --operation: string = "add", # Allowed operation types: "add", "replace". Default is "add" + --cm-key: string = "values.yaml", # ConfigMap key to reference values from (default: "values.yaml") + --cm-target-path: string, # Optional targetPath for ConfigMap values + --cm-optional, # Flag to mark ConfigMap values as optional (optional) + --create-cm-with-values: record, # Record with values to include in a new generated ConfigMap (default: empty, i.e., does not create a new ConfigMap). + --secret-key: string = "values.yaml", # Secret key to reference values from (default: "values.yaml") + --secret-target-path: string, # Optional targetPath for Secret values + --secret-optional, # Flag to mark Secret values as optional (optional) + --create-secret-with-values: record, # Record with values to include in a new generated Secret (default: empty, i.e., does not create a new Secret). + --public-age-key: string # Age key to encrypt the contents of the new Secret (if applicable) + kustomization_name: string, # Kustomization to add the patch to + helmrelease_name: string, # HelmRelease to modify + inline_values?: record, # Inline values to add to the HelmRelease spec (optional) + cm_name?: string, # ConfigMap name to reference values from (optional) + secret_name?: string # Secret name to reference values from (optional) +]: [ + record -> record +] { + let in_resourcelist: record = $in + + # Validate operation type + if $operation not-in ["add", "replace"] { + error make { msg: "Invalid operation type. Supported values are 'add', 'replace'. See RFC6902 for details" } + } + + # === Transformations === + $in_resourcelist + # Add inline values if provided and not empty + | if ($inline_values | is-empty) { + $in + } else { + $in + | ( + helmrelease add inline values + --ks-namespace $ks_namespace + --hr-namespace $hr_namespace + --operation $operation + $kustomization_name + $helmrelease_name + $inline_values + ) + } + # Add reference to ConfigMap-based values if cm_name is provided and not empty + | if ($cm_name | is-empty) { + $in + } else { + $in + | ( + helmrelease add values from configmap + --ks-namespace $ks_namespace + --hr-namespace $hr_namespace + --target-path $cm_target_path + --optional=$cm_optional + $kustomization_name + $helmrelease_name + $cm_name + $cm_key + ) + } + # Add reference to Secret-based values if secret_name is provided and not empty + | if ($secret_name | is-empty) { + $in + } else { + $in + | ( + helmrelease add values from secret + --ks-namespace $ks_namespace + --hr-namespace $hr_namespace + --target-path $secret_target_path + --optional=$secret_optional + $kustomization_name + $helmrelease_name + $secret_name + $secret_key + ) + } + # Generate a ConfigMap if required + | if ($create_cm_with_values | is-empty) or ($cm_name | is-empty) { + $in + } else { + $in + | ( + generator configmap + --filename $"($cm_name).yaml" + { $cm_key: ($create_cm_with_values | to yaml | str trim)} + $cm_name + ($hr_namespace | default "default") + ) + } + # Generate a Secret if required + | if ($create_secret_with_values | is-empty) or ($secret_name | is-empty) { + $in + } else { + # If there is an age key, it is used to encrypt the secret manifest; otherwise, it is kept clear + if ($public_age_key | is-empty) { + $in + | ( + generator secret + --filename $"($secret_name).yaml" + { $secret_key: ($create_secret_with_values | to yaml | str trim)} + $secret_name + ($hr_namespace | default "default") + ) + } else { + $in + | ( + generator secret + --filename $"($secret_name).yaml" + --public-age-key $public_age_key + { $secret_key: ($create_secret_with_values | to yaml | str trim)} + $secret_name + ($hr_namespace | default "default") + ) + } + } +} diff --git a/docker/osm-nushell-krm-functions/krm/patch.nu b/docker/osm-nushell-krm-functions/krm/patch.nu new file mode 100644 index 00000000..91403d10 --- /dev/null +++ b/docker/osm-nushell-krm-functions/krm/patch.nu @@ -0,0 +1,453 @@ +####################################################################################### +# Copyright ETSI Contributors and Others. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +####################################################################################### + +# Module with utility functions to patch or amend Kubernetes resources (items) enumerated in a ResourceList. + + +# Checks that the ResourceList is an actual ResourceList +# -- NOT EXPORTED -- +def "check if resourcelist" [ + name?: string +]: [ + record -> nothing +] { + + $in + | if ( + $in != {} + and ( + ($in | get -i kind) != "ResourceList" + or ($in | get -i apiVersion) != "config.kubernetes.io/v1" + ) + ) { + if ($name | is-empty) { + error make {msg: $"Error: Expected a ResourceList, but received ($in)."} + } else { + error make {msg: $"Error: Expected a ResourceList, but received ($in) from ($name)."} + } + } +} + + +# Keep item in ResourceList +export def "resource keep" [ + apiVersion?: string + kind?: string, + name?: string, + namespace?: string, +]: [ + record -> record +] { + let in_rl: record = $in + + # If not a valid ResourceList, throws an error; otherwise, continues + $in_rl | check if resourcelist + + $in_rl + | update items {|items| + $items.items | filter {|it| ( + (($apiVersion | is-empty) or $it.apiVersion == $apiVersion) + and (($kind | is-empty) or $it.kind == $kind) + and (($name | is-empty) or $it.metadata.name == $name) + and (($namespace | is-empty) or $it.metadata.namespace == $namespace) + ) + } + } +} + + +# Delete item in ResourceList +export def "resource delete" [ + apiVersion?: string + kind?: string, + name?: string, + namespace?: string, +]: [ + record -> record +] { + let in_rl: record = $in + + # If not a valid ResourceList, throws an error; otherwise, continues + $in_rl | check if resourcelist + + $in_rl + | update items {|items| + $items.items | filter {|it| ( + (not ($name | is-empty) and $it.metadata.name != $name) + or (not ($namespace | is-empty) and $it.metadata.namespace != $namespace) + or (not ($kind | is-empty) and $it.kind != $kind) + or (not ($apiVersion | is-empty) and $it.apiVersion != $apiVersion) + ) + } + } +} + + +# Patch item in ResourceList with a custom closure +export def "resource custom function" [ + custom_function: closure, # Custom function to apply to the keypath + key_path: cell-path, + value: any, + apiVersion?: string + kind?: string, + name?: string, + namespace?: string, +]: [ + record -> record +] { + let in_rl: record = $in + + # If not a valid ResourceList, throws an error; otherwise, continues + $in_rl | check if resourcelist + + $in_rl + | update items {|items| + $items.items | each {|item| + if ((($name | is-empty) or $item.metadata.name == $name) and + (($namespace | is-empty) or ($item | get -i metadata.namespace) == $namespace) and + (($kind | is-empty) or ($item | get -i kind) == $kind) and + (($apiVersion | is-empty) or ($item | get -i apiVersion) == $apiVersion)) { + $item | do $custom_function $key_path $value + } else { + $item + } + } + } +} + + +# Patch item in ResourceList with an insert. Fails if key already exists. +export def "resource insert key" [ + key_path: cell-path, + value: any, + apiVersion?: string + kind?: string, + name?: string, + namespace?: string, +]: [ + record -> record +] { + let in_rl: record = $in + + # If not a valid ResourceList, throws an error; otherwise, continues + $in_rl | check if resourcelist + + $in_rl + | (resource custom function + { |k, v| ($in | insert $k $v) } + $key_path + $value + $apiVersion + $kind + $name + $namespace + ) +} + + +# Patch item in ResourceList with an upsert (update if exists, otherwise insert) +export def "resource upsert key" [ + key_path: cell-path, + value: any, + apiVersion?: string + kind?: string, + name?: string, + namespace?: string, +]: [ + record -> record +] { + let in_rl: record = $in + + # If not a valid ResourceList, throws an error; otherwise, continues + $in_rl | check if resourcelist + + $in_rl + | (resource custom function + { |k, v| ($in | upsert $k $v) } + $key_path + $value + $apiVersion + $kind + $name + $namespace + ) +} + +export alias patch_replace = resource upsert key + + +# Patch item in ResourceList with an update. Fails if key does not exist. +export def "resource update key" [ + key_path: cell-path, + value: any, + apiVersion?: string + kind?: string, + name?: string, + namespace?: string, +]: [ + record -> record +] { + let in_rl: record = $in + + # If not a valid ResourceList, throws an error; otherwise, continues + $in_rl | check if resourcelist + + $in_rl + | (resource custom function + { |k, v| ($in | update $k $v) } + $key_path + $value + $apiVersion + $kind + $name + $namespace + ) +} + + +# Patch item in ResourceList by deleting a key. +export def "resource reject key" [ + key_path: cell-path, + apiVersion?: string + kind?: string, + name?: string, + namespace?: string, +]: [ + record -> record +] { + let in_rl: record = $in + + # If not a valid ResourceList, throws an error; otherwise, continues + $in_rl | check if resourcelist + + $in_rl + | (resource custom function + { |k, v| ($in | reject $k) } + $key_path + "" + $apiVersion + $kind + $name + $namespace + ) +} + +export alias "resource delete key" = resource reject key + + +# Patch item in ResourceList to add a file name (and, optionally, order in the file) for an eventual conversion to a folder of manifests +export def "resource filename set" [ + --index: int, # Number of the index in the file, for multi-resource manifests + filename: string, + apiVersion?: string + kind?: string, + name?: string, + namespace?: string, +]: [ + record -> record +] { + let in_rl: record = $in + + # If not a valid ResourceList, throws an error; otherwise, continues + $in_rl | check if resourcelist + + # Adds index in file if specified; otherwise, keeps the input + let input_rl: record = if ($index | is-empty) { + $in_rl + } else { + $in_rl + | (resource upsert key + $.metadata.annotations."config.kubernetes.io/index" + ($index | into string) + $apiVersion + $kind + $name + $namespace + ) + | (resource upsert key + $.metadata.annotations."internal.config.kubernetes.io/index" + ($index | into string) + $apiVersion + $kind + $name + $namespace + ) + } + + # Finally, adds file name to the items in the ResourceList + $input_rl + | (resource upsert key + $.metadata.annotations."config.kubernetes.io/path" + $filename + $apiVersion + $kind + $name + $namespace + ) + | (resource upsert key + $.metadata.annotations."internal.config.kubernetes.io/path" + $filename + $apiVersion + $kind + $name + $namespace + ) +} + +export alias set_filename_to_items = resource filename set + + +# Patch item in ResourceList to append/upsert element to a list at a given key. +# +# The expected behaviour should be as follows: +# +# 1. If the key already exists, the value should be a list, and the item should be appended to the list. +# 2. If the key does not exist, the value should be created as a list with the new item. +# 3. If the key already exists but the value is not a list, it should throw an error. +# +export def "list append item" [ + key_path: cell-path, + value: any, + apiVersion?: string + kind?: string, + name?: string, + namespace?: string, +]: [ + record -> record +] { + let in_rl: record = $in + + # If not a valid ResourceList, throws an error; otherwise, continues + $in_rl | check if resourcelist + + # Checks if all the preexisting values at the matching keys are lists; otherwise, throws an error + let non_conformant: list = ( + $in_rl + # Only the resources that match the input criteria + | (resource keep + $apiVersion + $kind + $name + $namespace + ) + # Only keeps the resources in a regular list + | get -i items + # Removes the resources where the key does not exist + | filter { |it| ($it | get -i $key_path) != null } + # Keeps only the resources where the key is not a list + | filter { |it| not ( + $it + | get -i $key_path + | describe + | ($in | str starts-with "list") or ($in | str starts-with "table") + ) + } + ) + + if not ($non_conformant | is-empty) { + error make { msg: $"Error: Some matching keys are not lists. Non conformant:\n($non_conformant | to yaml)"} + } + + # Actual processing + $in_rl + | (resource custom function + { |k, v| ($in | upsert $k { + |row| + let existing = ($row | get $k -i) + if $existing == null { + [$v] + } else { + $existing | append $value + } + } + ) + } + $key_path + $value + $apiVersion + $kind + $name + $namespace + ) +} + +export alias patch_add_to_list = list append item +export alias "list upsert item" = list append item + + +# Patch item in ResourceList to drop/delete element from a list at a given key with a given value. +export def "list drop item" [ + --keep-empty-list, + key_path: cell-path, + value: any, + apiVersion?: string + kind?: string, + name?: string, + namespace?: string, +]: [ + record -> record +] { + let in_rl: record = $in + + # If not a valid ResourceList, throws an error; otherwise, continues + $in_rl | check if resourcelist + + # Checks if all the preexisting values at the matching keys are lists; otherwise, throws an error + let non_conformant: list = ( + $in_rl + # Only the resources that match the input criteria + | (resource keep + $apiVersion + $kind + $name + $namespace + ) + # Only keeps the resources in a regular list + | get -i items + # Removes the resources where the key does not exist + | filter { |it| ($it | get -i $key_path) != null } + # Keeps only the resources where the key is not a list + | filter { |it| not ($it | get -i $key_path | describe | str starts-with "list") } + ) + + if not ($non_conformant | is-empty) { + error make { msg: $"Error: Some matching keys are not lists. Non conformant:\n($non_conformant | to yaml)"} + } + + # Actual processing + $in_rl + | (resource custom function + { |k, v| ($in + | update $k {|row| + $row | get $k -i | filter {|value| ($value != $v)} + } + # Delete the key in case the list at the key is now empty and the flag is disabled + | if (not $keep_empty_list) and ($in | get $k -i | is-empty) { + $in | reject $k + } else { $in } + ) + } + $key_path + $value + $apiVersion + $kind + $name + $namespace + ) +} + +export alias patch_delete_from_list = list drop item diff --git a/docker/osm-nushell-krm-functions/krm/strategicmergepatch.nu b/docker/osm-nushell-krm-functions/krm/strategicmergepatch.nu new file mode 100644 index 00000000..895acfcf --- /dev/null +++ b/docker/osm-nushell-krm-functions/krm/strategicmergepatch.nu @@ -0,0 +1,61 @@ +####################################################################################### +# Copyright ETSI Contributors and Others. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +####################################################################################### + +# Module with helper functions to create strategic merge patches for Kubernetes resources (patchStrategicMerge). + + +# Helper to create a full strategic merge patch, including the target object specification and partial resource manifest. +# +# Example: +# $ strategicmergepatch create {kind: Deployment, name: podinfo} ( +# 'apiVersion: apps/v1 +# kind: Deployment +# metadata: +# name: not-used +# spec: +# template: +# metadata: +# annotations: +# cluster-autoscaler.kubernetes.io/safe-to-evict: "true"' | from yaml +# ) | to yaml +# +# target: +# kind: Deployment +# name: podinfo +# patch: | +# apiVersion: apps/v1 +# kind: Deployment +# metadata: +# name: not-used +# spec: +# template: +# metadata: +# annotations: +# cluster-autoscaler.kubernetes.io/safe-to-evict: 'true' +# +# Further information about advanced syntax for strategic merge patch (e.g. '$patch' directives) can be found at +export def "create" [ + target: record, # Target resource specification as per + patch: record # Partial resource manifest +]: [ + nothing -> record +] { + { + target: $target, + patch: ($patch | to yaml) + } +} diff --git a/docker/osm-nushell-krm-functions/krm/tests/artifacts/configmap/templates/configmap.yaml b/docker/osm-nushell-krm-functions/krm/tests/artifacts/configmap/templates/configmap.yaml new file mode 100644 index 00000000..a758a7b9 --- /dev/null +++ b/docker/osm-nushell-krm-functions/krm/tests/artifacts/configmap/templates/configmap.yaml @@ -0,0 +1,24 @@ +####################################################################################### +# Copyright ETSI Contributors and Others. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +####################################################################################### + +apiVersion: v1 +data: + mykey: myvalue +kind: ConfigMap +metadata: + creationTimestamp: null + name: my-cm diff --git a/docker/osm-nushell-krm-functions/krm/tests/artifacts/empty/.gitkeep b/docker/osm-nushell-krm-functions/krm/tests/artifacts/empty/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/docker/osm-nushell-krm-functions/krm/tests/artifacts/jenkins/manifests/bitnamicharts-repo.yaml b/docker/osm-nushell-krm-functions/krm/tests/artifacts/jenkins/manifests/bitnamicharts-repo.yaml new file mode 100644 index 00000000..354b8372 --- /dev/null +++ b/docker/osm-nushell-krm-functions/krm/tests/artifacts/jenkins/manifests/bitnamicharts-repo.yaml @@ -0,0 +1,27 @@ +####################################################################################### +# Copyright ETSI Contributors and Others. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +####################################################################################### + +--- +apiVersion: source.toolkit.fluxcd.io/v1beta2 +kind: HelmRepository +metadata: + name: bitnamicharts + namespace: jenkins +spec: + interval: 10m0s + type: oci + url: oci://registry-1.docker.io/bitnamicharts diff --git a/docker/osm-nushell-krm-functions/krm/tests/artifacts/jenkins/manifests/jenkins-hr.yaml b/docker/osm-nushell-krm-functions/krm/tests/artifacts/jenkins/manifests/jenkins-hr.yaml new file mode 100644 index 00000000..c87a95e9 --- /dev/null +++ b/docker/osm-nushell-krm-functions/krm/tests/artifacts/jenkins/manifests/jenkins-hr.yaml @@ -0,0 +1,37 @@ +####################################################################################### +# Copyright ETSI Contributors and Others. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +####################################################################################### + +--- +apiVersion: helm.toolkit.fluxcd.io/v2beta1 +kind: HelmRelease +metadata: + name: jenkins + namespace: jenkins +spec: + chart: + spec: + chart: jenkins + reconcileStrategy: ChartVersion + sourceRef: + kind: HelmRepository + name: bitnamicharts + namespace: jenkins + install: + createNamespace: true + interval: 3m0s + targetNamespace: jenkins + values: {} diff --git a/docker/osm-nushell-krm-functions/krm/tests/artifacts/jenkins/templates/jenkins-ks.yaml b/docker/osm-nushell-krm-functions/krm/tests/artifacts/jenkins/templates/jenkins-ks.yaml new file mode 100644 index 00000000..bbf4d7ba --- /dev/null +++ b/docker/osm-nushell-krm-functions/krm/tests/artifacts/jenkins/templates/jenkins-ks.yaml @@ -0,0 +1,31 @@ +####################################################################################### +# Copyright ETSI Contributors and Others. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +####################################################################################### + +--- +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: jenkins + namespace: jenkins +spec: + interval: 1h0m0s + path: ./apps/jenkins/manifests + prune: true + sourceRef: + kind: GitRepository + name: sw-catalogs + namespace: flux-system diff --git a/docker/osm-nushell-krm-functions/krm/tests/artifacts/jenkins/templates/jenkins-ns.yaml b/docker/osm-nushell-krm-functions/krm/tests/artifacts/jenkins/templates/jenkins-ns.yaml new file mode 100644 index 00000000..ca2fff85 --- /dev/null +++ b/docker/osm-nushell-krm-functions/krm/tests/artifacts/jenkins/templates/jenkins-ns.yaml @@ -0,0 +1,24 @@ +####################################################################################### +# Copyright ETSI Contributors and Others. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +####################################################################################### + +apiVersion: v1 +kind: Namespace +metadata: + creationTimestamp: null + name: jenkins +spec: {} +status: {} diff --git a/docker/osm-nushell-krm-functions/krm/tests/artifacts/namespace/templates/namespace.yaml b/docker/osm-nushell-krm-functions/krm/tests/artifacts/namespace/templates/namespace.yaml new file mode 100644 index 00000000..d4c03868 --- /dev/null +++ b/docker/osm-nushell-krm-functions/krm/tests/artifacts/namespace/templates/namespace.yaml @@ -0,0 +1,22 @@ +####################################################################################### +# Copyright ETSI Contributors and Others. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +####################################################################################### + +apiVersion: v1 +kind: Namespace +metadata: + name: ${TARGET_NS} + diff --git a/docker/osm-nushell-krm-functions/krm/tests/artifacts/secret/templates/secret.yaml b/docker/osm-nushell-krm-functions/krm/tests/artifacts/secret/templates/secret.yaml new file mode 100644 index 00000000..54346c5b --- /dev/null +++ b/docker/osm-nushell-krm-functions/krm/tests/artifacts/secret/templates/secret.yaml @@ -0,0 +1,24 @@ +####################################################################################### +# Copyright ETSI Contributors and Others. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +####################################################################################### + +apiVersion: v1 +data: + mykey: bXl2YWx1ZQ== +kind: Secret +metadata: + creationTimestamp: null + name: my-secret diff --git a/docker/osm-nushell-krm-functions/krm/tests/concatenate.nu b/docker/osm-nushell-krm-functions/krm/tests/concatenate.nu new file mode 100644 index 00000000..ae674b86 --- /dev/null +++ b/docker/osm-nushell-krm-functions/krm/tests/concatenate.nu @@ -0,0 +1,251 @@ +#!/usr/bin/env -S nu --stdin +####################################################################################### +# Copyright ETSI Contributors and Others. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +####################################################################################### + + +use std assert +use ../../krm/concatenate.nu * + + +# --- resourcelists tests --- + +export def "test concatenate resourcelists empty inputs" []: [ + nothing -> nothing +] { + let input: record = {} + let resourcelist2: record = {} + + let actual: record = resourcelists $resourcelist2 + let expected: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [] + } + + assert equal $actual $expected +} + + +export def "test concatenate resourcelists empty stdin" []: [ + nothing -> nothing +] { + let input: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: ["item1", "item2"] + } + let resourcelist2: record = {} + + # Simulate empty stdin by passing input as an argument + let actual: record = resourcelists $input + let expected: record = $input + + assert equal $actual $expected +} + + +export def "test concatenate resourcelists empty second list" []: [ + nothing -> nothing +] { + let input: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: ["item1", "item2"] + } + let resourcelist2: record = {} + + # Simulate empty stdin by passing input as an argument + let actual: record = resourcelists $resourcelist2 + let expected: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [] + } + + # Since we're testing with stdin, we need to simulate it + let actual_with_stdin: record = ( + echo $input | resourcelists $resourcelist2 + ) + let expected_with_stdin: record = $input + + assert equal $actual_with_stdin $expected_with_stdin +} + + +export def "test concatenate resourcelists merge lists" []: [ + nothing -> nothing +] { + let input: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: ["item1", "item2"] + } + let resourcelist2: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: ["item3", "item4"] + } + + let actual: record = ( + echo $input | resourcelists $resourcelist2 + ) + let expected: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: ["item1", "item2", "item3", "item4"] + } + + assert equal $actual $expected +} + + +export def "test concatenate resourcelists non-empty inputs with no items" []: [ + nothing -> nothing +] { + let input: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + } + let resourcelist2: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + } + + let actual: record = ( + echo $input | resourcelists $resourcelist2 + ) + let expected: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [] + } + + assert equal $actual $expected +} + + + +# --- manifests tests --- + +export def "test concatenate manifests empty inputs" []: [ + nothing -> nothing +] { + let mnfst2: any = null + + let actual: list = manifests $mnfst2 + let expected: list = [] + + assert equal $actual $expected +} + + +export def "test concatenate manifests empty stdin" []: [ + nothing -> nothing +] { + let mnfst2: record = { + name: "example" + kind: "Deployment" + } + + let actual: list = manifests $mnfst2 + let expected: list = [ $mnfst2 ] + + assert equal $actual $expected +} + + +export def "test concatenate manifests empty second manifest" []: [ + nothing -> nothing +] { + let mnfst1: record = { + name: "example1" + kind: "Deployment" + } + + let mnfst2: any = null + + let actual: list = ( + echo $mnfst1 | manifests $mnfst2 + ) + let expected: list = [ $mnfst1 ] + + assert equal $actual $expected +} + + +export def "test concatenate manifests single records" []: [ + nothing -> nothing +] { + let mnfst1: record = { + name: "example1" + kind: "Deployment" + } + let mnfst2: record = { + name: "example2" + kind: "Service" + } + + let actual: list = ( + echo $mnfst1 | manifests $mnfst2 + ) + let expected: list = [ $mnfst1, $mnfst2 ] + + assert equal $actual $expected +} + + +export def "test concatenate manifests lists of records" []: [ + nothing -> nothing +] { + let mnfst1: list = [ + { name: "example1", kind: "Deployment" } + { name: "example2", kind: "Service" } + ] + let mnfst2: list = [ + { name: "example3", kind: "Pod" } + { name: "example4", kind: "ConfigMap" } + ] + + let actual: list = ( + echo $mnfst1 | manifests $mnfst2 + ) + let expected: list = [ + { name: "example1", kind: "Deployment" } + { name: "example2", kind: "Service" } + { name: "example3", kind: "Pod" } + { name: "example4", kind: "ConfigMap" } + ] + + assert equal $actual $expected +} + + +export def "test concatenate manifests invalid input" []: [ + nothing -> nothing +] { + let mnfst2: string = "Invalid manifest" + + let actual_error: error = ( + try { + manifests $mnfst2 + } catch { + |err| $err.msg + } + ) + + assert equal $actual_error "Error: Expected a record or a list of records, but received string." +} diff --git a/docker/osm-nushell-krm-functions/krm/tests/convert.nu b/docker/osm-nushell-krm-functions/krm/tests/convert.nu new file mode 100644 index 00000000..09710ebd --- /dev/null +++ b/docker/osm-nushell-krm-functions/krm/tests/convert.nu @@ -0,0 +1,448 @@ +#!/usr/bin/env -S nu --stdin +####################################################################################### +# Copyright ETSI Contributors and Others. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +####################################################################################### + + +use std assert +use std null-device +use ../../krm/convert.nu * + + +# --- replace environment variables tests --- + +export def "test convert replace environment variables no vars" []: [ + nothing -> nothing +] { + let text: string = "Hello, $USER!" + + $env.USER = "test_user" + let actual: string = ( + echo $text | replace environment variables + ) + let expected: string = "Hello, test_user!" + + assert equal $actual $expected +} + + +export def "test convert replace environment variables string vars" []: [ + nothing -> nothing +] { + let text: string = "Hello, $USER! Your HOME is $HOME." + let vars_to_replace: string = "$USER,$HOME" + + load-env { + USER: "test_user" + HOME: "/home/test_user" + } + + let actual: string = ( + echo $text | replace environment variables $vars_to_replace + ) + let expected: string = "Hello, test_user! Your HOME is /home/test_user." + + assert equal $actual $expected +} + + +export def "test convert replace environment variables list vars" []: [ + nothing -> nothing +] { + let text: string = "Hello, $USER! Your HOME is $HOME." + let vars_to_replace: list = ["USER", "HOME"] + + load-env { + USER: "test_user" + HOME: "/home/test_user" + } + + let actual: string = ( + echo $text | replace environment variables $vars_to_replace + ) + let expected: string = "Hello, test_user! Your HOME is /home/test_user." + + assert equal $actual $expected +} + + +export def "test convert replace environment variables invalid input" []: [ + nothing -> nothing +] { + let text: string = "Hello, $USER!" + let vars_to_replace: int = 123 + + let error_occurred: error = try { + echo $text | replace environment variables $vars_to_replace + + } catch { + |err| $err.msg + } + + assert equal $error_occurred "Error: Expected a string or list of strings, but received int" +} + + +export def "test convert replace environment variables no replacement" []: [ + nothing -> nothing +] { + let text: string = "Hello, $NON_EXISTENT_VAR!" + let actual: string = ( + echo $text | replace environment variables + ) + + let expected: string = "Hello, !" + + assert equal $actual $expected +} + + +# --- folder to resourcelist tests --- + +export def "test convert folder to resourcelist empty input" []: [ + nothing -> nothing +] { + let folder: path = "./artifacts/empty" + let expected: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [] + } + + let actual: record = (folder to resourcelist $folder) + assert equal $actual $expected +} + + +export def "test convert folder to resourcelist no substitution" []: [ + nothing -> nothing +] { + let folder: path = "./artifacts/jenkins/templates" + let input_list: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: ["item1", "item2"] + } + + let actual: record = ( + echo $input_list | folder to resourcelist $folder + ) + let expected_items: list = ["item1", "item2"] | append ( + kpt fn source $folder + | from yaml + | get items + ) + + assert equal $actual.apiVersion "config.kubernetes.io/v1" + assert equal $actual.kind "ResourceList" + assert equal $actual.items $expected_items +} + + +export def "test convert folder to resourcelist with substitution" []: [ + nothing -> nothing +] { + let folder: path = "./artifacts/namespace/templates" + let input_list: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: ["item1", "item2"] + } + $env.TARGET_NS = "target-namespace" + + let actual: record = ( + echo $input_list | folder to resourcelist --subst-env $folder + ) + let expected_items: list = ["item1", "item2"] | append ( + kpt fn source $folder + | replace environment variables + | from yaml + | get items + ) + + assert equal $actual.apiVersion "config.kubernetes.io/v1" + assert equal $actual.kind "ResourceList" + assert equal $actual.items $expected_items + assert equal $actual.items.2.metadata.name $env.TARGET_NS +} + + +export def "test convert folder to resourcelist with filter" []: [ + nothing -> nothing +] { + let folder: path = "./artifacts/namespace/templates" + let input_list: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: ["item1", "item2"] + } + $env.TARGET_NS = "target-namespace" + let env_filter = "$TARGET_NS" + + let actual: record = ( + echo $input_list | folder to resourcelist --subst-env $folder $env_filter + ) + let expected_items: list = ["item1", "item2"] | append ( + kpt fn source $folder + | replace environment variables $env_filter + | from yaml + | get items + ) + + assert equal $actual.apiVersion "config.kubernetes.io/v1" + assert equal $actual.kind "ResourceList" + assert equal $actual.items $expected_items + assert equal $actual.items.2.metadata.name $env.TARGET_NS +} + + +export def "test convert folder to resourcelist invalid input" []: [ + nothing -> nothing +] { + let folder: path = "./non-existent-folder" + let input_list: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: ["item1", "item2"] + } + + let error_occurred: bool = try { + echo $input_list | folder to resourcelist $folder err> (null-device) + } catch { + |err| $err.msg + } + + assert equal $error_occurred "Can't convert to record." +} + + + +# --- manifest to resourcelist tests --- + +export def "test convert manifest to resourcelist empty input" []: [ + nothing -> nothing +] { + let actual: record = manifest to resourcelist + let expected: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [] + } + + assert equal $actual $expected +} + + +export def "test convert manifest to resourcelist single record" []: [ + nothing -> nothing +] { + let manifest: record = { + name: "example" + kind: "Deployment" + } + + let actual: record = ( + $manifest | manifest to resourcelist + ) + let expected: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ $manifest ] + } + + assert equal $actual $expected +} + + +export def "test convert manifest to resourcelist list of records" []: [ + nothing -> nothing +] { + let manifests: list = [ + { name: "example1", kind: "Deployment" } + { name: "example2", kind: "Service" } + ] + + let actual: record = ( + $manifests | manifest to resourcelist + ) + let expected: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: $manifests + } + + assert equal $actual $expected +} + + +export def "test convert manifest to resourcelist invalid input" []: [ + nothing -> nothing +] { + let invalid_manifest: string = "Invalid manifest" + + let error_occurred: bool = try { + $invalid_manifest | manifest to resourcelist + } catch { + |err| $err.msg + } + + assert equal $error_occurred "Error: Expected a record or a list of records, but received string." +} + + + +# --- resourcelist to folder tests --- + +export def "test convert resourcelist to folder dry run" []: [ + nothing -> nothing +] { + let rl: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: ["item1", "item2"] + } + + let output: record = ( + $rl | resourcelist to folder "no-folder" true | from yaml + ) + + assert equal $output $rl +} + + +export def "test convert resourcelist to folder no sync" []: [ + nothing -> nothing +] { + let source_folder: path = "./artifacts/jenkins/templates/" + let rl: record = ( + convert folder to resourcelist $source_folder + ) + let target_folder: string = (mktemp -t -d) + + # Run the command + $rl | resourcelist to folder $target_folder + + # Check if the contents were copied correctly + let actual_contents: list = ( + ls --short-names $target_folder + | get name + | sort + ) + + # Cleanup + rm -rf $target_folder + + # Expected + let expected_contents: list = ( + ls --short-names $source_folder + | get name + | sort + ) + + assert equal $actual_contents $expected_contents +} + + +export def "test convert resourcelist to folder sync" []: [ + nothing -> nothing +] { + let source_folder: path = "./artifacts/jenkins/templates/" + let rl: record = ( + convert folder to resourcelist $source_folder + ) + let target_folder: string = (mktemp -t -d) + + # Add an extra file to the target folder (it should be removed by the synchronization) + ^touch ($target_folder | path join "extra_file.txt") + + # Run the command + $rl | resourcelist to folder --sync $target_folder + + # Check if the contents were copied correctly + let actual_contents: list = ( + ls --short-names $target_folder + | get name + | sort + ) + + # Cleanup + rm -rf $target_folder + + # Expected + let expected_contents: list = ( + ls --short-names $source_folder + | get name + | sort + ) + + assert equal $actual_contents $expected_contents +} + + +# export def "test convert resourcelist to folder invalid input" []: [ +# nothing -> nothing +# ] { +# let invalid_input: record = { "Invalid input": "invalid value" } +# let target_folder: string = (mktemp -t -d) + +# let error_occurred: any = try { +# $invalid_input | resourcelist to folder $target_folder +# } catch { +# |err| $err.msg +# } + +# # Cleanup +# print $target_folder +# # rm -rf $target_folder + +# assert equal $error_occurred "Can't convert to boolean." +# } + + +export def "test convert resourcelist to folder non-existent folder" []: [ + nothing -> nothing +] { + let source_folder: path = "./artifacts/jenkins/templates/" + let rl: record = ( + convert folder to resourcelist $source_folder + ) + + let temp_folder: string = (mktemp -t -d) + let target_folder: string = ($temp_folder | path join "new-folder") + mkdir $target_folder + + # Run the command + $rl | resourcelist to folder $target_folder + + # Check if the contents were copied correctly + let actual_contents: list = ( + ls --short-names $target_folder + | get name + | sort + ) + + # Cleanup + rm -rf $temp_folder + + # Expected + let expected_contents: list = ( + ls --short-names $source_folder + | get name + | sort + ) + + assert equal $actual_contents $expected_contents +} diff --git a/docker/osm-nushell-krm-functions/krm/tests/generator.nu b/docker/osm-nushell-krm-functions/krm/tests/generator.nu new file mode 100644 index 00000000..5f133a0a --- /dev/null +++ b/docker/osm-nushell-krm-functions/krm/tests/generator.nu @@ -0,0 +1,640 @@ +#!/usr/bin/env -S nu --stdin +####################################################################################### +# Copyright ETSI Contributors and Others. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +####################################################################################### + + +use std assert +use ../../krm/generator.nu * + + +# --- from resourcelist tests --- + +export def "test generator from resourcelist empty inputs" []: [ + nothing -> nothing +] { + let in_rl: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: ["item1", "item2"] + } + let rl: record = {} + + let actual: record = ($in_rl | from resourcelist $rl) + let expected: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: ["item1", "item2"] + } + + assert equal $actual $expected +} + + +export def "test generator from resourcelist empty stdin" []: [ + nothing -> nothing +] { + let in_rl: record = {} + let rl: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: ["item1", "item2"] + } + + let actual: record = ($in_rl | from resourcelist $rl) + let expected: record = $rl + + assert equal $actual $expected +} + + +export def "test generator from resourcelist merge lists" []: [ + nothing -> nothing +] { + let in_rl: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: ["item1", "item2"] + } + let rl: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: ["item3", "item4"] + } + + let actual: record = ( + echo $in_rl | from resourcelist $rl + ) + let expected: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: ["item1", "item2", "item3", "item4"] + } + + assert equal $actual $expected +} + + +export def "test generator from resourcelist non-empty inputs with no items" []: [ + nothing -> nothing +] { + let in_rl: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + } + let rl: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + } + + let actual: record = ( + echo $in_rl | from resourcelist $rl + ) + let expected: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [] + } + + assert equal $actual $expected +} + + +export def "test generator from resourcelist invalid input parameter" []: [ + nothing -> nothing +] { + let in_rl: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: ["item1", "item2"] + } + let rl: record = { "Invalid input": "Invalid value" } + + let error_occurred: any = try { + $in_rl | from resourcelist $rl + } catch { + |err| $err.json | from json | get inner.msg.0 + } + + assert ($error_occurred | str starts-with "Error: Expected a ResourceList, but received") +} + + +export def "test generator from resourcelist invalid input from stdin" []: [ + nothing -> nothing +] { + let in_rl: record = { "Invalid input": "Invalid value" } + let rl: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: ["item1", "item2"] + } + + let error_occurred: any = try { + $in_rl | from resourcelist $rl + } catch { + |err| $err.json | from json | get inner.msg.0 + } + + assert ($error_occurred | str starts-with "Error: Expected a ResourceList, but received") +} + + +# --- from manifest tests --- + +export def "test generator from manifest empty inputs" []: [ + nothing -> nothing +] { + let manifest: any = null + + let actual: record = from manifest $manifest + let expected: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [] + } + + assert equal $actual $expected +} + + +export def "test generator from manifest empty stdin" []: [ + nothing -> nothing +] { + let manifest: record = { + name: "example" + kind: "Deployment" + } + + let actual: record = from manifest $manifest + let expected: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ $manifest ] + } + + assert equal $actual $expected +} + + +export def "test generator from manifest merge lists" []: [ + nothing -> nothing +] { + let in_rl: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: ["item1", "item2"] + } + let manifest: record = { + name: "example" + kind: "Deployment" + } + + let actual: record = ( + echo $in_rl | from manifest $manifest + ) + let expected: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: ["item1", "item2", $manifest] + } + + assert equal $actual.apiVersion $expected.apiVersion + assert equal $actual.kind $expected.kind + assert equal $actual.items $expected.items +} + + +export def "test generator from manifest list of manifests" []: [ + nothing -> nothing +] { + let in_rl: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: ["item1", "item2"] + } + let manifest: list = [ + { name: "example1", kind: "Deployment" } + { name: "example2", kind: "Service" } + ] + + let actual: record = ( + echo $in_rl | from manifest $manifest + ) + let expected: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: ["item1", "item2", { name: "example1", kind: "Deployment" }, { name: "example2", kind: "Service" }] + } + + assert equal $actual.apiVersion $expected.apiVersion + assert equal $actual.kind $expected.kind + assert equal $actual.items $expected.items +} + + +export def "test generator from manifest invalid input" []: [ + nothing -> nothing +] { + let manifest: string = "Invalid manifest" + + let error_occurred: error = try { + from manifest $manifest + } catch { + |err| $err.msg + } + + assert equal $error_occurred "Error: Expected a record or a list of records, but received string." +} + + + +# --- configmap tests --- + +export def "test generator configmap basic" []: [ + nothing -> nothing +] { + let key_pairs: record = { + key1: "value1", + key2: "value2" + } + let name: string = "example-configmap" + + let actual: record = configmap $key_pairs $name + let expected: record = { + apiVersion: "config.kubernetes.io/v1", + kind: ResourceList, + items: [ + { + apiVersion: "v1" + kind: "ConfigMap" + metadata: { name: $name, namespace: "default" } + data: $key_pairs + } + ] + } + + assert equal $actual $expected +} + + +export def "test generator configmap with namespace" []: [ + nothing -> nothing +] { + let key_pairs: record = { + key1: "value1", + key2: "value2" + } + let name: string = "example-configmap" + let namespace: string = "custom-namespace" + + let actual: record = configmap $key_pairs $name $namespace + let expected: record = { + apiVersion: "config.kubernetes.io/v1", + kind: ResourceList, + items: [ + { + apiVersion: "v1" + kind: "ConfigMap" + metadata: { name: $name, namespace: $namespace } + data: $key_pairs + } + ] + } + + assert equal $actual $expected +} + + +export def "test generator configmap with filename" []: [ + nothing -> nothing +] { + let key_pairs: record = { + key1: "value1", + key2: "value2" + } + let name: string = "example-configmap" + let filename: string = "example-configmap.yaml" + + let actual: record = configmap --filename $filename $key_pairs $name + let expected: record = { + apiVersion: "config.kubernetes.io/v1", + kind: ResourceList, + items: [ + { + apiVersion: "v1" + kind: "ConfigMap" + metadata: { + name: $name, + namespace: "default", + annotations: { + "config.kubernetes.io/path": $filename, + "internal.config.kubernetes.io/path": $filename + } + } + data: $key_pairs + } + ] + } + + assert equal $actual $expected +} + + +export def "test generator configmap with filename and index" []: [ + nothing -> nothing +] { + let key_pairs: record = { + key1: "value1", + key2: "value2" + } + let name: string = "example-configmap" + let filename: string = "example-configmap.yaml" + let index: int = 0 + + let actual: record = configmap --filename $filename --index $index $key_pairs $name + let expected: record = { + apiVersion: "config.kubernetes.io/v1", + kind: ResourceList, + items: [ + { + apiVersion: "v1" + kind: "ConfigMap" + metadata: { + name: $name, + namespace: "default", + annotations: { + "config.kubernetes.io/path": $filename, + "internal.config.kubernetes.io/path": $filename, + "config.kubernetes.io/index": "0", + "internal.config.kubernetes.io/index": "0" + } + } + data: $key_pairs + } + ] + } + + assert equal $actual $expected +} + + + +# TODO: + +# --- secret tests --- + +export def "test generator secret basic" []: [ + nothing -> nothing +] { + let key_pairs: record = { + key1: "value1", + key2: "value2" + } + let name: string = "example-secret" + + let actual: record = secret $key_pairs $name + let expected_encoded_values: record = { + key1: ("value1" | encode base64), + key2: ("value2" | encode base64) + } + let expected: record = { + apiVersion: "config.kubernetes.io/v1", + kind: ResourceList, + items: [ + { + apiVersion: "v1" + kind: "Secret" + metadata: { name: $name, namespace: "default" } + data: $expected_encoded_values + } + ] + } + + assert equal $actual $expected +} + + +export def "test generator secret with namespace" []: [ + nothing -> nothing +] { + let key_pairs: record = { + key1: "value1", + key2: "value2" + } + let name: string = "example-secret" + let namespace: string = "custom-namespace" + + let actual: record = secret $key_pairs $name $namespace + let expected_encoded_values: record = { + key1: ("value1" | encode base64), + key2: ("value2" | encode base64) + } + let expected: record = { + apiVersion: "config.kubernetes.io/v1", + kind: ResourceList, + items: [ + { + apiVersion: "v1" + kind: "Secret" + metadata: { name: $name, namespace: $namespace } + data: $expected_encoded_values + } + ] + } + + assert equal $actual $expected +} + + +export def "test generator secret with filename" []: [ + nothing -> nothing +] { + let key_pairs: record = { + key1: "value1", + key2: "value2" + } + let name: string = "example-secret" + let filename: string = "example-secret.yaml" + + let actual: record = ( + secret + --filename $filename + $key_pairs + $name + ) + let expected_encoded_values: record = { + key1: ("value1" | encode base64), + key2: ("value2" | encode base64) + } + let expected: record = { + apiVersion: "config.kubernetes.io/v1", + kind: ResourceList, + items: [ + { + apiVersion: "v1" + kind: "Secret" + metadata: { + name: $name, + namespace: "default", + annotations: { + "config.kubernetes.io/path": $filename, + "internal.config.kubernetes.io/path": $filename + } + } + data: $expected_encoded_values + } + ] + } + + assert equal $actual $expected +} + + +export def "test generator secret with filename and index" []: [ + nothing -> nothing +] { + let key_pairs: record = { + key1: "value1", + key2: "value2" + } + let name: string = "example-secret" + let filename: string = "example-secret.yaml" + let index: int = 0 + + let actual: record = ( + secret + --filename $filename + --index $index + $key_pairs + $name + ) + let expected_encoded_values: record = { + key1: ("value1" | encode base64), + key2: ("value2" | encode base64) + } + let expected: record = { + apiVersion: "config.kubernetes.io/v1", + kind: ResourceList, + items: [ + { + apiVersion: "v1" + kind: "Secret" + metadata: { + name: $name, + namespace: "default", + annotations: { + "config.kubernetes.io/path": $filename, + "internal.config.kubernetes.io/path": $filename, + "config.kubernetes.io/index": "0", + "internal.config.kubernetes.io/index": "0" + } + } + data: $expected_encoded_values + } + ] + } + + assert equal $actual $expected +} + + +export def "test generator secret with type" []: [ + nothing -> nothing +] { + let key_pairs: record = { + key1: "value1", + key2: "value2" + } + let name: string = "example-secret" + let type: string = "Opaque" + + let actual: record = secret --type $type $key_pairs $name + let expected_encoded_values: record = { + key1: ("value1" | encode base64), + key2: ("value2" | encode base64) + } + let expected: record = { + apiVersion: "config.kubernetes.io/v1", + kind: ResourceList, + items: [ + { + apiVersion: "v1" + kind: "Secret" + metadata: { name: $name, namespace: "default" } + type: $type + data: $expected_encoded_values + } + ] + } + + assert equal $actual $expected +} + + +export def "test generator secret with age encryption" []: [ + nothing -> nothing +] { + let test_public_key: string = "age1hsrtxphk7exrdc0kt8dgr8a8r3hx88v3xpsw0ezaxvefsy9asegqknppc0" + let test_private_key: string = "AGE-SECRET-KEY-12CC3A4LEDYF4S26UV6Z2MEG7ZQL9PTU5NHH6N3FN6FLJ5HACW9LQX0UWP2" + + let key_pairs: record = { + key1: "value1", + key2: "value2" + } + let name: string = "example-secret" + let filename: string = "example-secret.yaml" + + # Here we extract the encrypted manifest only + # File name and index are also removed, since they were not taken into account for age encryptio + let result: record = ( + secret + --filename $filename + --public-age-key $test_public_key + $key_pairs + $name + ) + | get items.0 + | reject $.metadata.annotations + + # Verify decryption + let tmp_encrypted_file = (mktemp -t --suffix .yaml) + $result | save -f $tmp_encrypted_file + let actual: record = ( + $test_private_key + | SOPS_AGE_KEY_FILE="/dev/stdin" sops --decrypt $tmp_encrypted_file + | from yaml + ) + rm $tmp_encrypted_file # Clean up temporary key file + + + let expected_encoded_values: record = { + key1: ("value1" | encode base64), + key2: ("value2" | encode base64) + } + + let expected: record = { + apiVersion: "v1" + kind: "Secret" + metadata: { + name: $name, + namespace: "default" + } + data: $expected_encoded_values + } + + assert equal $actual $expected +} diff --git a/docker/osm-nushell-krm-functions/krm/tests/jsonpatch.nu b/docker/osm-nushell-krm-functions/krm/tests/jsonpatch.nu new file mode 100644 index 00000000..77cbcd44 --- /dev/null +++ b/docker/osm-nushell-krm-functions/krm/tests/jsonpatch.nu @@ -0,0 +1,250 @@ +#!/usr/bin/env -S nu --stdin +####################################################################################### +# Copyright ETSI Contributors and Others. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +####################################################################################### + + +use std assert +use ../../krm/jsonpatch.nu * + + +# --- create operation tests --- + +export def "test jsonpatch create operation add" []: [ + nothing -> nothing +] { + let op: string = "add" + let path: string = "/spec/template/spec/securityContext" + let value: record = { + runAsUser: 10000 + fsGroup: 1337 + } + + let actual: record = create operation $op $path $value + let expected: record = { + op: $op, + path: $path, + value: $value + } + + assert equal $actual $expected +} + + +export def "test jsonpatch create operation remove" []: [ + nothing -> nothing +] { + let op: string = "remove" + let path: string = "/spec/template/spec/securityContext" + + let actual: record = create operation $op $path + let expected: record = { + op: $op, + path: $path + } + + assert equal $actual $expected +} + + +export def "test jsonpatch create operation replace" []: [ + nothing -> nothing +] { + let op: string = "replace" + let path: string = "/spec/template/spec/securityContext" + let value: record = { + runAsUser: 10000 + fsGroup: 1337 + } + + let actual: record = create operation $op $path $value + let expected: record = { + op: $op, + path: $path, + value: $value + } + + assert equal $actual $expected +} + + +export def "test jsonpatch create operation move" []: [ + nothing -> nothing +] { + let op: string = "move" + let from: string = "/spec/template/spec/securityContext" + let path: string = "/spec/template/spec/newSecurityContext" + + let actual: record = create operation $op $path '' $from + let expected: record = { + op: $op, + from: $from, + path: $path + } + + assert equal $actual $expected +} + + +export def "test jsonpatch create operation copy" []: [ + nothing -> nothing +] { + let op: string = "copy" + let from: string = "/spec/template/spec/securityContext" + let path: string = "/spec/template/spec/newSecurityContext" + + let actual: record = create operation $op $path '' $from + let expected: record = { + op: $op, + from: $from, + path: $path + } + + assert equal $actual $expected +} + + +export def "test jsonpatch create operation invalid op" []: [ + nothing -> nothing +] { + let op: string = "invalid" + let path: string = "/spec/template/spec/securityContext" + + let error_occurred: error = try { + create operation $op $path + } catch { + |err| $err.msg + } + + assert equal $error_occurred "Invalid operation type. Supported values are 'add', 'remove', 'replace', 'move', 'copy'. See RFC6902 for details." +} + + +export def "test jsonpatch create operation missing value for add/replace" []: [ + nothing -> nothing +] { + let op: string = "add" + let path: string = "/spec/template/spec/securityContext" + + let error_occurred: error = try { + create operation $op $path + } catch { + |err| $err.msg + } + + assert equal $error_occurred "Value is required for 'add' and 'replace' operations." +} + + +export def "test jsonpatch create operation missing from for move/copy" []: [ + nothing -> nothing +] { + let op: string = "move" + let path: string = "/spec/template/spec/newSecurityContext" + + let error_occurred: error = try { + create operation $op $path + } catch { + |err| $err.msg + } + + assert equal $error_occurred "Source path is required for 'move' and 'copy' operations." +} + + +# --- create JSON patch tests --- + +export def "test jsonpatch create JSON patch basic" []: [ + nothing -> nothing +] { + let target: record = { + kind: "Deployment" + name: "podinfo" + } + let operation: record = { + op: "add" + path: "/spec/template/spec/securityContext" + value: { + runAsUser: 10000 + fsGroup: 1337 + } + } + + let actual: record = create $target $operation + let expected: record = { + target: $target, + patch: ([$operation] | to yaml) + } + + assert equal $actual.target $expected.target + assert equal $actual.patch $expected.patch +} + + +export def "test jsonpatch create JSON patch multiple operations" []: [ + nothing -> nothing +] { + let target: record = { + kind: "Deployment" + name: "podinfo" + } + let operation1: record = { + op: "add" + path: "/spec/template/spec/securityContext" + value: { + runAsUser: 10000 + fsGroup: 1337 + } + } + let operation2: record = { + op: "replace" + path: "/spec/replicas" + value: 3 + } + + let actual: record = create $target $operation1 $operation2 + let expected: record = { + target: $target, + patch: ( + [$operation1, $operation2] | to yaml + ) + } + + assert equal $actual.target $expected.target + assert equal $actual.patch $expected.patch +} + + +export def "test jsonpatch create JSON patch using operation helper" []: [ + nothing -> nothing +] { + let target: record = { + kind: "Deployment" + name: "podinfo" + } + let operation: record = ( + create operation add "/spec/template/spec/securityContext" {runAsUser: 10000, fsGroup: 1337} + ) + + let actual: record = create $target $operation + let expected: record = { + target: $target, + patch: ([$operation] | to yaml) + } + + assert equal $actual.target $expected.target + assert equal $actual.patch $expected.patch +} diff --git a/docker/osm-nushell-krm-functions/krm/tests/keypair.nu b/docker/osm-nushell-krm-functions/krm/tests/keypair.nu new file mode 100644 index 00000000..e3a1ab7c --- /dev/null +++ b/docker/osm-nushell-krm-functions/krm/tests/keypair.nu @@ -0,0 +1,214 @@ +#!/usr/bin/env -S nu --stdin +####################################################################################### +# Copyright ETSI Contributors and Others. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +####################################################################################### + + +use std assert +use std null-device +use ../../krm/keypair.nu * + + +# --- create age tests --- + +export def "test keypair create age basic functionality" [] { + # Setup + let test_dir = (mktemp -t -d) + let key_name = "test_key" + + # Execute + create age $key_name $test_dir err> (null-device) + + # Assert + assert ([$test_dir $"($key_name).key"] | path join | path exists) + assert ([$test_dir $"($key_name).pub"] | path join | path exists) + + # Cleanup + rm -rf $test_dir +} + + +export def "test keypair create age overwrites existing keys" [] { + # Setup + let test_dir = (mktemp -t -d) + let key_name = "test_key" + touch ([$test_dir $"($key_name).key"] | path join) + touch ([$test_dir $"($key_name).pub"] | path join) + + # Execute + create age $key_name $test_dir err> (null-device) + + # Assert + let key_path = [$test_dir $"($key_name).key"] | path join + let pub_path = [$test_dir $"($key_name).pub"] | path join + assert ($key_path | path exists) + assert ($pub_path | path exists) + assert greater (open $key_path | str length) 0 + assert greater (open $pub_path | str length) 0 + + # Cleanup + rm -rf $test_dir +} + + +export def "test keypair create age uses default directory" [] { + # Setup + let original_credentials_dir = $env.CREDENTIALS_DIR? + let test_dir = (mktemp -t -d) + $env.CREDENTIALS_DIR = $test_dir + let key_name = "test_key" + + # Execute + create age $key_name err> (null-device) + + # Assert + assert ([$test_dir $"($key_name).key"] | path join | path exists) + assert ([$test_dir $"($key_name).pub"] | path join | path exists) + + # Cleanup + rm -rf $test_dir + $env.CREDENTIALS_DIR = $original_credentials_dir +} + + +export def "test keypair create age generates valid keys" [] { + # Setup + let test_dir = (mktemp -t -d) + let key_name = "test_key" + + # Execute + create age $key_name $test_dir err> (null-device) + + # Assert + let pub_path = [$test_dir $"($key_name).pub"] | path join + let pub_key = (open $pub_path) + assert ($pub_key | str starts-with "age1") + assert equal ($pub_key | str length) 63 # Standard length for age public keys + + # Cleanup + rm -rf $test_dir +} + + +# --- encrypt secret manifest tests --- + +export def "test keypair encrypt secret manifest basic functionality" [] { + # Setup + let test_public_key: string = "age1hsrtxphk7exrdc0kt8dgr8a8r3hx88v3xpsw0ezaxvefsy9asegqknppc0" + let test_private_key: string = "AGE-SECRET-KEY-12CC3A4LEDYF4S26UV6Z2MEG7ZQL9PTU5NHH6N3FN6FLJ5HACW9LQX0UWP2" + let input_yaml: string = "apiVersion: v1\nkind: Secret\nmetadata:\n name: test-secret\ndata:\n username: dXNlcm5hbWU=\n password: cGFzc3dvcmQ=" + + # Execute + let result = ($input_yaml | encrypt secret manifest $test_public_key) + + # Assert + assert ($result | str contains "sops:") + assert ($result | str contains "encrypted_regex: ^(data|stringData)$") + assert ($result | str contains "ENC[AES256_GCM,data:") + + # Verify decryption + let tmp_encrypted_file = (mktemp -t --suffix .yaml) + $result | save -f $tmp_encrypted_file + + let decrypted: string = ($test_private_key + | SOPS_AGE_KEY_FILE="/dev/stdin" sops --decrypt $tmp_encrypted_file + ) + rm $tmp_encrypted_file # Clean up temporary key file + + assert str contains $decrypted "username: dXNlcm5hbWU=" + assert str contains $decrypted "password: cGFzc3dvcmQ=" +} + + +export def "test keypair encrypt secret manifest handles empty input" [] { + # Setup + let test_public_key = "age1hsrtxphk7exrdc0kt8dgr8a8r3hx88v3xpsw0ezaxvefsy9asegqknppc0" + + # Execute and Assert + let result: string = (try { "" + | encrypt secret manifest $test_public_key + } catch { $in | to yaml }) + + # assert str contains $result "Error" + assert (not ($result | str contains "Error")) $"ERROR: Got ($result)" +} + + +export def "test keypair encrypt secret manifest encrypts correct fields" [] { + # Setup + let test_public_key: string = "age1hsrtxphk7exrdc0kt8dgr8a8r3hx88v3xpsw0ezaxvefsy9asegqknppc0" + let test_private_key: string = "AGE-SECRET-KEY-12CC3A4LEDYF4S26UV6Z2MEG7ZQL9PTU5NHH6N3FN6FLJ5HACW9LQX0UWP2" + let input_yaml: string = "apiVersion: v1\nkind: Secret\nmetadata:\n name: test-secret\ndata:\n username: dXNlcm5hbWU=\n password: cGFzc3dvcmQ=\nstringData:\n api_key: my-api-key" + + # Execute + let result: string = ($input_yaml | encrypt secret manifest $test_public_key) + + # Assert + assert str contains $result "ENC[AES256_GCM,data:" + assert str contains $result "username:" + assert str contains $result "password:" + assert str contains $result "api_key:" + assert (not ($result | str contains "dXNlcm5hbWU=")) + assert (not ($result | str contains "cGFzc3dvcmQ=")) + assert (not ($result | str contains "my-api-key")) + assert str contains $result "metadata:\n name: test-secret" + + # Verify decryption + let tmp_encrypted_file = (mktemp -t --suffix .yaml) + $result | save -f $tmp_encrypted_file + let decrypted: string = ($test_private_key + | SOPS_AGE_KEY_FILE="/dev/stdin" sops --decrypt $tmp_encrypted_file + ) + rm $tmp_encrypted_file # Clean up temporary key file + assert str contains $decrypted "username: dXNlcm5hbWU=" + assert str contains $decrypted "password: cGFzc3dvcmQ=" + assert str contains $decrypted "api_key: my-api-key" +} + + +export def "test keypair decrypt secret manifest" [] { + # Setup + let test_public_key: string = "age1hsrtxphk7exrdc0kt8dgr8a8r3hx88v3xpsw0ezaxvefsy9asegqknppc0" + let test_private_key: string = "AGE-SECRET-KEY-12CC3A4LEDYF4S26UV6Z2MEG7ZQL9PTU5NHH6N3FN6FLJ5HACW9LQX0UWP2" + let input_record: record = { + apiVersion: v1, + kind: Secret, + metadata: { name: test-secret } + data: { + username: ('myusername' | encode base64) + password: ('mypassword' | encode base64) + } + } + + # Encrypt + let encrypted_record: record = ( + $input_record + | to yaml + | encrypt secret manifest $test_public_key + | from yaml + ) + + # Decrypt + let decrypted_record: record = ( + $encrypted_record + | to yaml + | keypair decrypt secret manifest $test_private_key + | from yaml + ) + + # Test + assert equal $input_record $decrypted_record +} diff --git a/docker/osm-nushell-krm-functions/krm/tests/mod.nu b/docker/osm-nushell-krm-functions/krm/tests/mod.nu new file mode 100644 index 00000000..d2065630 --- /dev/null +++ b/docker/osm-nushell-krm-functions/krm/tests/mod.nu @@ -0,0 +1,67 @@ +#!/usr/bin/env -S nu --stdin +####################################################################################### +# Copyright ETSI Contributors and Others. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +####################################################################################### + +use std assert +use ../../krm * +use ./keypair.nu * +use ./concatenate.nu * +use ./convert.nu * +use ./strategicmergepatch.nu * +use ./jsonpatch.nu * +use ./generator.nu * +use ./patch.nu * +use ./overlaypatch.nu * + + +# Test launcher +def main [] { + print "Running tests..." + + let test_commands: list = ( + scope commands + | where ($it.type == "custom") + and ($it.name | str starts-with "test ") + and not ($it.description | str starts-with "ignore") + | get name + ) + + let count_test_commands: int = ($test_commands | length) + let test_commands_together: string = ( + $test_commands + | enumerate + | each { |test| + [$"print '--> [($test.index + 1)/($count_test_commands)] ($test.item)'", $test.item] + } + | flatten + | str join ";" + ) + + nu --commands $"source `($env.CURRENT_FILE)`; ($test_commands_together)" + print $"\n✅ ALL TESTS COMPLETED SUCCESSFULLY" +} + + +# --- safe_name tests --- + +export def "test safe resource name" []: [ + nothing -> nothing +] { + let actual: string = safe resource name "This is a_test w/special:characters." + let expected: string = "this-is-a-test-w-special-characters-" + assert equal $actual $expected +} diff --git a/docker/osm-nushell-krm-functions/krm/tests/overlaypatch.nu b/docker/osm-nushell-krm-functions/krm/tests/overlaypatch.nu new file mode 100644 index 00000000..54730938 --- /dev/null +++ b/docker/osm-nushell-krm-functions/krm/tests/overlaypatch.nu @@ -0,0 +1,1992 @@ +#!/usr/bin/env -S nu --stdin +####################################################################################### +# Copyright ETSI Contributors and Others. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +####################################################################################### + + +use std assert +use ../../krm/patch.nu * +use ../../krm/overlaypatch.nu * + + + +# --- add patch tests --- + +export def "test overlaypatch add patch to kustomization" []: [ + nothing -> nothing +] { + let resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { + apiVersion: "kustomize.toolkit.fluxcd.io/v1" + kind: "Kustomization" + metadata: { name: "example-kustomization", namespace: "default" } + # spec: { patches: [] } + spec: {} + } + ] + } + + let kustomization_name: string = "example-kustomization" + let ks_namespace: string = "default" + let target: record = { + kind: "Deployment" + name: "example-deployment" + } + let patch_value: record = { + op: "replace" + path: "/spec/replicas" + value: 3 + } + + let actual: record = $resourcelist | add patch --ks-namespace $ks_namespace $kustomization_name $target $patch_value + let expected_patch_content: record = { + target: $target + patch: ($patch_value | to yaml) + } + let expected_resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { + apiVersion: "kustomize.toolkit.fluxcd.io/v1" + kind: "Kustomization" + metadata: { name: $kustomization_name, namespace: $ks_namespace } + spec: { patches: [$expected_patch_content] } + } + ] + } + + assert equal $actual $expected_resourcelist +} + + +export def "test overlaypatch add patch to kustomization with existing patches" []: [ + nothing -> nothing +] { + let resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { + apiVersion: "kustomize.toolkit.fluxcd.io/v1" + kind: "Kustomization" + metadata: { name: "example-kustomization", namespace: "default" } + spec: { patches: [{ target: { kind: "Service", name: "example-service" }, patch: "op:\n replace\npath:\n /spec/type\nvalue:\n NodePort" }] } + } + ] + } + + let kustomization_name: string = "example-kustomization" + let ks_namespace: string = "default" + let target: record = { + kind: "Deployment" + name: "example-deployment" + } + let patch_value: record = { + op: "replace" + path: "/spec/replicas" + value: 3 + } + + let actual: record = $resourcelist | add patch --ks-namespace $ks_namespace $kustomization_name $target $patch_value + let expected_patch_content_1st_patch: record = { + target: { kind: "Service", name: "example-service" } + patch: "op:\n replace\npath:\n /spec/type\nvalue:\n NodePort" + } + let expected_patch_content_2nd_patch: record = { + target: $target + patch: ($patch_value | to yaml) + } + let expected_resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { + apiVersion: "kustomize.toolkit.fluxcd.io/v1" + kind: "Kustomization" + metadata: { name: $kustomization_name, namespace: $ks_namespace } + spec: { patches: [$expected_patch_content_1st_patch, $expected_patch_content_2nd_patch] } + } + ] + } + + assert equal $actual $expected_resourcelist +} + + +# --- add jsonpatch tests --- + +export def "test overlaypatch add jsonpatch add operation" []: [ + nothing -> nothing +] { + let resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { + apiVersion: "kustomize.toolkit.fluxcd.io/v1" + kind: "Kustomization" + metadata: { name: "example-kustomization", namespace: "default" } + # spec: { patches: [] } + spec: {} + } + ] + } + + let kustomization_name: string = "example-kustomization" + let ks_namespace: string = "default" + let target: record = { + kind: "Deployment" + name: "example-deployment" + } + let path: string = "/spec/replicas" + let value: any = 3 + + let actual: record = $resourcelist | add jsonpatch --ks-namespace $ks_namespace $kustomization_name $target $path $value + let expected_patch_content: record = { + target: $target + patch: ( + [{ op: "add", path: $path, value: $value }] | to yaml + ) + } + let expected_resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { + apiVersion: "kustomize.toolkit.fluxcd.io/v1" + kind: "Kustomization" + metadata: { name: $kustomization_name, namespace: $ks_namespace } + spec: { patches: [$expected_patch_content] } + } + ] + } + + assert equal $actual $expected_resourcelist +} + + +export def "test overlaypatch add jsonpatch replace operation" []: [ + nothing -> nothing +] { + let resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { + apiVersion: "kustomize.toolkit.fluxcd.io/v1" + kind: "Kustomization" + metadata: { name: "example-kustomization", namespace: "default" } + # spec: { patches: [] } + spec: {} + } + ] + } + + let kustomization_name: string = "example-kustomization" + let ks_namespace: string = "default" + let target: record = { + kind: "Deployment" + name: "example-deployment" + } + let path: string = "/spec/replicas" + let value: any = 3 + + let actual: record = $resourcelist | ( + add jsonpatch + --ks-namespace $ks_namespace + --operation "replace" + $kustomization_name + $target + $path + $value + ) + let expected_patch_content: record = { + target: $target + patch: ( + [{ op: "replace", path: $path, value: $value }] + | to yaml + ) + } + let expected_resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { + apiVersion: "kustomize.toolkit.fluxcd.io/v1" + kind: "Kustomization" + metadata: { name: $kustomization_name, namespace: $ks_namespace } + spec: { patches: [$expected_patch_content] } + } + ] + } + + assert equal $actual $expected_resourcelist +} + + +export def "test overlaypatch add jsonpatch remove operation" []: [ + nothing -> nothing +] { + let resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { + apiVersion: "kustomize.toolkit.fluxcd.io/v1" + kind: "Kustomization" + metadata: { name: "example-kustomization", namespace: "default" } + spec: { patches: [] } + } + ] + } + + let kustomization_name: string = "example-kustomization" + let ks_namespace: string = "default" + let target: record = { + kind: "Deployment" + name: "example-deployment" + } + let path: string = "/spec/replicas" + + let actual: record = $resourcelist | ( + add jsonpatch + --ks-namespace $ks_namespace + --operation "remove" + $kustomization_name + $target + $path + ) + let expected_patch_content: record = { + target: $target + patch: ( + [{ op: "remove", path: $path}] + | to yaml + ) + } + let expected_resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { + apiVersion: "kustomize.toolkit.fluxcd.io/v1" + kind: "Kustomization" + metadata: { name: $kustomization_name, namespace: $ks_namespace } + spec: { patches: [$expected_patch_content] } + } + ] + } + + assert equal $actual $expected_resourcelist +} + + +export def "test overlaypatch add jsonpatch move operation" []: [ + nothing -> nothing +] { + let resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { + apiVersion: "kustomize.toolkit.fluxcd.io/v1" + kind: "Kustomization" + metadata: { name: "example-kustomization", namespace: "default" } + # spec: { patches: [] } + spec: {} + } + ] + } + + let kustomization_name: string = "example-kustomization" + let ks_namespace: string = "default" + let target: record = { + kind: "Deployment" + name: "example-deployment" + } + let path: string = "/spec/new-replicas" + let from: string = "/spec/replicas" + + let actual: record = ( + $resourcelist + | add jsonpatch + --ks-namespace $ks_namespace + --operation "move" + $kustomization_name + $target + $path + '' + $from + ) + let expected_patch_content: record = { + target: $target + patch: ( + [{ op: "move", from: $from, path: $path }] + | to yaml + ) + } + let expected_resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { + apiVersion: "kustomize.toolkit.fluxcd.io/v1" + kind: "Kustomization" + metadata: { name: $kustomization_name, namespace: $ks_namespace } + spec: { patches: [$expected_patch_content] } + } + ] + } + + assert equal $actual $expected_resourcelist +} + + +export def "test overlaypatch add jsonpatch copy operation" []: [ + nothing -> nothing +] { + let resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { + apiVersion: "kustomize.toolkit.fluxcd.io/v1" + kind: "Kustomization" + metadata: { name: "example-kustomization", namespace: "default" } + spec: { patches: [] } + } + ] + } + + let kustomization_name: string = "example-kustomization" + let ks_namespace: string = "default" + let target: record = { + kind: "Deployment" + name: "example-deployment" + } + let path: string = "/spec/new-replicas" + let from: string = "/spec/replicas" + + let actual: record = ( + $resourcelist + | add jsonpatch + --ks-namespace $ks_namespace + --operation "copy" + $kustomization_name + $target + $path + '' + $from + ) + let expected_patch_content: record = { + target: $target + patch: ( + [{ op: "copy", from: $from, path: $path }] + | to yaml + ) + } + let expected_resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { + apiVersion: "kustomize.toolkit.fluxcd.io/v1" + kind: "Kustomization" + metadata: { name: $kustomization_name, namespace: $ks_namespace } + spec: { patches: [$expected_patch_content] } + } + ] + } + + assert equal $actual $expected_resourcelist +} + + +export def "test overlaypatch add jsonpatch invalid operation" []: [ + nothing -> nothing +] { + let resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { + apiVersion: "kustomize.toolkit.fluxcd.io/v1" + kind: "Kustomization" + metadata: { name: "example-kustomization", namespace: "default" } + # spec: { patches: [] } + spec: {} + } + ] + } + + let kustomization_name: string = "example-kustomization" + let ks_namespace: string = "default" + let target: record = { + kind: "Deployment" + name: "example-deployment" + } + let path: string = "/spec/replicas" + + let error_occurred: any = try { + $resourcelist | ( + add jsonpatch + --ks-namespace $ks_namespace + --operation "invalid" + $kustomization_name + $target + $path + ) + } catch { + |err| $err.msg + } + + assert equal $error_occurred "Invalid operation type. Supported values are 'add', 'remove', 'replace', 'move', 'copy'. See RFC6902 for details" +} + + +# --- helmrelease add inline values tests --- + +export def "test overlaypatch helmrelease add inline values with add operation" []: [ + nothing -> nothing +] { + let resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { + apiVersion: "kustomize.toolkit.fluxcd.io/v1" + kind: "Kustomization" + metadata: { name: "example-kustomization", namespace: "default" } + # spec: { patches: [] } + spec: {} + } + ] + } + + let ks_namespace: string = "default" + let hr_namespace: string = "" + let kustomization_name: string = "example-kustomization" + let helmrelease_name: string = "example-helmrelease" + let values: record = { key1: "value1", key2: "value2" } + + let actual: record = ( + $resourcelist + | ( + helmrelease add inline values + --ks-namespace $ks_namespace + $kustomization_name + $helmrelease_name + $values + ) + ) + let expected_patch_content: record = { + target: { kind: "HelmRelease", name: $helmrelease_name } + patch: ( + [{ op: "add", path: "/spec/values", value: $values }] + | to yaml + ) + } + let expected_resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { + apiVersion: "kustomize.toolkit.fluxcd.io/v1" + kind: "Kustomization" + metadata: { name: $kustomization_name, namespace: $ks_namespace } + spec: { patches: [$expected_patch_content] } + } + ] + } + + assert equal $actual $expected_resourcelist +} + + +export def "test overlaypatch helmrelease add inline values with replace operation" []: [ + nothing -> nothing +] { + let resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { + apiVersion: "kustomize.toolkit.fluxcd.io/v1" + kind: "Kustomization" + metadata: { name: "example-kustomization", namespace: "default" } + # spec: { patches: [] } + spec: {} + } + ] + } + + let ks_namespace: string = "default" + let hr_namespace: string = "" + let kustomization_name: string = "example-kustomization" + let helmrelease_name: string = "example-helmrelease" + let values: record = { key1: "value1", key2: "value2" } + + let actual: record = ( + $resourcelist + | ( + helmrelease add inline values + --ks-namespace $ks_namespace + --operation replace + $kustomization_name + $helmrelease_name + $values + ) + ) + let expected_patch_content: record = { + target: { kind: "HelmRelease", name: $helmrelease_name } + patch: ( + [{ op: "replace", path: "/spec/values", value: $values }] + | to yaml + ) + } + let expected_resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { + apiVersion: "kustomize.toolkit.fluxcd.io/v1" + kind: "Kustomization" + metadata: { name: $kustomization_name, namespace: $ks_namespace } + spec: { patches: [$expected_patch_content] } + } + ] + } + + assert equal $actual $expected_resourcelist +} + + +export def "test overlaypatch helmrelease add inline values with existing patches" []: [ + nothing -> nothing +] { + let resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { + apiVersion: "kustomize.toolkit.fluxcd.io/v1" + kind: "Kustomization" + metadata: { name: "example-kustomization", namespace: "default" } + spec: { patches: [ + { + target: { kind: "HelmRelease", name: "existing-helmrelease" } + patch: ( + [{ op: "replace", path: "/spec/values/replicaCount", value: 2 }] + | to yaml + ) + } + ] } + } + ] + } + + let ks_namespace: string = "default" + let hr_namespace: string = "" + let kustomization_name: string = "example-kustomization" + let helmrelease_name: string = "example-helmrelease" + let values: record = { key1: "value1", key2: "value2" } + + let actual: record = $resourcelist | helmrelease add inline values --ks-namespace $ks_namespace $kustomization_name $helmrelease_name $values + let expected_patch_content_new: record = { + target: { kind: "HelmRelease", name: $helmrelease_name } + patch: ( + [{ op: "add", path: "/spec/values", value: $values }] + | to yaml + ) + } + let expected_resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { + apiVersion: "kustomize.toolkit.fluxcd.io/v1" + kind: "Kustomization" + metadata: { name: $kustomization_name, namespace: $ks_namespace } + spec: { patches: [ + { + target: { kind: "HelmRelease", name: "existing-helmrelease" } + patch: ( + [{ op: "replace", path: "/spec/values/replicaCount", value: 2 }] + | to yaml + ) + }, + $expected_patch_content_new + ] } + } + ] + } + + assert equal $actual $expected_resourcelist +} + + +# --- helmrelease add values from configmap tests --- + +export def "test overlaypatch helmrelease add values from configmap basic" []: [ + nothing -> nothing +] { + let resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { + apiVersion: "kustomize.toolkit.fluxcd.io/v1" + kind: "Kustomization" + metadata: { name: "example-kustomization", namespace: "default" } + # spec: { patches: [] } + spec: {} + } + ] + } + + let ks_namespace: string = "default" + let hr_namespace: string = "" + let kustomization_name: string = "example-kustomization" + let helmrelease_name: string = "example-helmrelease" + let cm_name: string = "example-configmap" + let cm_key: string = "values.yaml" + + let actual: record = ( + $resourcelist + | ( + helmrelease add values from configmap + --ks-namespace $ks_namespace + $kustomization_name + $helmrelease_name + $cm_name + $cm_key + ) + ) + let expected_patch_content: record = { + target: { kind: "HelmRelease", name: $helmrelease_name } + patch: ( + [{ op: "add", path: "/spec/valuesFrom/-", value: { kind: "ConfigMap", name: $cm_name, key: $cm_key } }] + | to yaml + ) + } + let expected_resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { + apiVersion: "kustomize.toolkit.fluxcd.io/v1" + kind: "Kustomization" + metadata: { name: $kustomization_name, namespace: $ks_namespace } + spec: { patches: [$expected_patch_content] } + } + ] + } + + assert equal $actual $expected_resourcelist +} + + +export def "test overlaypatch helmrelease add values from configmap with target path" []: [ + nothing -> nothing +] { + let resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { + apiVersion: "kustomize.toolkit.fluxcd.io/v1" + kind: "Kustomization" + metadata: { name: "example-kustomization", namespace: "default" } + # spec: { patches: [] } + spec: {} + } + ] + } + + let ks_namespace: string = "default" + let hr_namespace: string = "" + let kustomization_name: string = "example-kustomization" + let helmrelease_name: string = "example-helmrelease" + let cm_name: string = "example-configmap" + let cm_key: string = "values.yaml" + let target_path: string = "/custom/path" + + let actual: record = ( + $resourcelist + | ( + helmrelease add values from configmap + --ks-namespace $ks_namespace + --target-path $target_path + $kustomization_name + $helmrelease_name + $cm_name + $cm_key + ) + ) + let expected_patch_content: record = { + target: { kind: "HelmRelease", name: $helmrelease_name } + patch: ( + [{ op: "add", path: "/spec/valuesFrom/-", value: { kind: "ConfigMap", name: $cm_name, key: $cm_key, targetPath: $target_path } }] + | to yaml + ) + } + let expected_resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { + apiVersion: "kustomize.toolkit.fluxcd.io/v1" + kind: "Kustomization" + metadata: { name: $kustomization_name, namespace: $ks_namespace } + spec: { patches: [$expected_patch_content] } + } + ] + } + + assert equal $actual $expected_resourcelist +} + + +export def "test overlaypatch helmrelease add values from configmap with existing patches" []: [ + nothing -> nothing +] { + let resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { + apiVersion: "kustomize.toolkit.fluxcd.io/v1" + kind: "Kustomization" + metadata: { name: "example-kustomization", namespace: "default" } + spec: { patches: [ + { + target: { kind: "HelmRelease", name: "existing-helmrelease" } + patch: ( + [{ op: "add", path: "/spec/valuesFrom/-", value: { kind: "ConfigMap", name: "existing-configmap", key: "existing-values.yaml" } }] + | to yaml + ) + } + ] } + } + ] + } + + let ks_namespace: string = "default" + let hr_namespace: string = "" + let kustomization_name: string = "example-kustomization" + let helmrelease_name: string = "example-helmrelease" + let cm_name: string = "example-configmap" + let cm_key: string = "values.yaml" + + let actual: record = ( + $resourcelist + | ( + helmrelease add values from configmap + --ks-namespace $ks_namespace + $kustomization_name + $helmrelease_name + $cm_name + $cm_key + ) + ) + let expected_patch_content_existing: record = { + target: { kind: "HelmRelease", name: "existing-helmrelease" } + patch: ( + [{ op: "add", path: "/spec/valuesFrom/-", value: { kind: "ConfigMap", name: "existing-configmap", key: "existing-values.yaml" } }] + | to yaml + ) + } + let expected_patch_content_new: record = { + target: { kind: "HelmRelease", name: $helmrelease_name } + patch: ( + [{ op: "add", path: "/spec/valuesFrom/-", value: { kind: "ConfigMap", name: $cm_name, key: $cm_key } }] + | to yaml + ) + } + let expected_resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { + apiVersion: "kustomize.toolkit.fluxcd.io/v1" + kind: "Kustomization" + metadata: { name: $kustomization_name, namespace: $ks_namespace } + spec: { patches: [$expected_patch_content_existing, $expected_patch_content_new] } + } + ] + } + + assert equal $actual $expected_resourcelist +} + + + +# --- helmrelease add values from secret tests --- + +export def "test overlaypatch helmrelease add values from secret basic" []: [ + nothing -> nothing +] { + let resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { + apiVersion: "kustomize.toolkit.fluxcd.io/v1" + kind: "Kustomization" + metadata: { name: "example-kustomization", namespace: "default" } + # spec: { patches: [] } + spec: {} + } + ] + } + + let ks_namespace: string = "default" + let hr_namespace: string = "" + let kustomization_name: string = "example-kustomization" + let helmrelease_name: string = "example-helmrelease" + let secret_name: string = "example-secret" + let secret_key: string = "values.yaml" + + let actual: record = ( + $resourcelist + | ( + helmrelease add values from secret + --ks-namespace $ks_namespace + $kustomization_name + $helmrelease_name + $secret_name + $secret_key + ) + ) + let expected_patch_content: record = { + target: { kind: "HelmRelease", name: $helmrelease_name } + patch: ( + [{ op: "add", path: "/spec/valuesFrom/-", value: { kind: "Secret", name: $secret_name, key: $secret_key } }] + | to yaml + ) + } + let expected_resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { + apiVersion: "kustomize.toolkit.fluxcd.io/v1" + kind: "Kustomization" + metadata: { name: $kustomization_name, namespace: $ks_namespace } + spec: { patches: [$expected_patch_content] } + } + ] + } + + assert equal $actual $expected_resourcelist +} + + +export def "test overlaypatch helmrelease add values from secret with target path" []: [ + nothing -> nothing +] { + let resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { + apiVersion: "kustomize.toolkit.fluxcd.io/v1" + kind: "Kustomization" + metadata: { name: "example-kustomization", namespace: "default" } + # spec: { patches: [] } + spec: {} + } + ] + } + + let ks_namespace: string = "default" + let hr_namespace: string = "" + let kustomization_name: string = "example-kustomization" + let helmrelease_name: string = "example-helmrelease" + let secret_name: string = "example-secret" + let secret_key: string = "values.yaml" + let target_path: string = "/custom/path" + + let actual: record = ( + $resourcelist + | ( + helmrelease add values from secret + --ks-namespace $ks_namespace + --target-path $target_path + $kustomization_name + $helmrelease_name + $secret_name + $secret_key + ) + ) + let expected_patch_content: record = { + target: { kind: "HelmRelease", name: $helmrelease_name } + patch: ( + [{ op: "add", path: "/spec/valuesFrom/-", value: { kind: "Secret", name: $secret_name, key: $secret_key, targetPath: $target_path } }] + | to yaml + ) + } + let expected_resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { + apiVersion: "kustomize.toolkit.fluxcd.io/v1" + kind: "Kustomization" + metadata: { name: $kustomization_name, namespace: $ks_namespace } + spec: { patches: [$expected_patch_content] } + } + ] + } + + assert equal $actual $expected_resourcelist +} + + +export def "test overlaypatch helmrelease add values from secret with optional flag" []: [ + nothing -> nothing +] { + let resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { + apiVersion: "kustomize.toolkit.fluxcd.io/v1" + kind: "Kustomization" + metadata: { name: "example-kustomization", namespace: "default" } + # spec: { patches: [] } + spec: {} + } + ] + } + + let ks_namespace: string = "default" + let hr_namespace: string = "" + let kustomization_name: string = "example-kustomization" + let helmrelease_name: string = "example-helmrelease" + let secret_name: string = "example-secret" + let secret_key: string = "values.yaml" + + let actual: record = ( + $resourcelist + | ( + helmrelease add values from secret + --ks-namespace $ks_namespace + --optional $kustomization_name + $helmrelease_name + $secret_name + $secret_key + ) + ) + let expected_patch_content: record = { + target: { kind: "HelmRelease", name: $helmrelease_name } + patch: ( + [{ op: "add", path: "/spec/valuesFrom/-", value: { kind: "Secret", name: $secret_name, key: $secret_key, optional: true } }] + | to yaml + ) + } + let expected_resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { + apiVersion: "kustomize.toolkit.fluxcd.io/v1" + kind: "Kustomization" + metadata: { name: $kustomization_name, namespace: $ks_namespace } + spec: { patches: [$expected_patch_content] } + } + ] + } + + assert equal $actual $expected_resourcelist +} + + +export def "test overlaypatch helmrelease add values from secret with hr namespace" []: [ + nothing -> nothing +] { + let resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { + apiVersion: "kustomize.toolkit.fluxcd.io/v1" + kind: "Kustomization" + metadata: { name: "example-kustomization", namespace: "default" } + # spec: { patches: [] } + spec: {} + } + ] + } + + let ks_namespace: string = "default" + let hr_namespace: string = "example-namespace" + let kustomization_name: string = "example-kustomization" + let helmrelease_name: string = "example-helmrelease" + let secret_name: string = "example-secret" + let secret_key: string = "values.yaml" + + let actual: record = ( + $resourcelist + | ( + helmrelease add values from secret + --ks-namespace $ks_namespace + --hr-namespace $hr_namespace + $kustomization_name + $helmrelease_name + $secret_name + $secret_key + ) + ) + let expected_patch_content: record = { + target: { kind: "HelmRelease", name: $helmrelease_name, namespace: $hr_namespace } + patch: ( + [{ op: "add", path: "/spec/valuesFrom/-", value: { kind: "Secret", name: $secret_name, key: $secret_key } }] + | to yaml + ) + } + let expected_resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { + apiVersion: "kustomize.toolkit.fluxcd.io/v1" + kind: "Kustomization" + metadata: { name: $kustomization_name, namespace: $ks_namespace } + spec: { patches: [$expected_patch_content] } + } + ] + } + + assert equal $actual $expected_resourcelist +} + + +export def "test overlaypatch helmrelease add values from secret with existing patches" []: [ + nothing -> nothing +] { + let resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { + apiVersion: "kustomize.toolkit.fluxcd.io/v1" + kind: "Kustomization" + metadata: { name: "example-kustomization", namespace: "default" } + spec: { patches: [ + { + target: { kind: "HelmRelease", name: "existing-helmrelease" } + patch: ( + [{ op: "add", path: "/spec/valuesFrom/-", value: { kind: "Secret", name: "existing-secret", key: "existing-values.yaml" } }] + | to yaml + ) + } + ] } + } + ] + } + + let ks_namespace: string = "default" + let hr_namespace: string = "" + let kustomization_name: string = "example-kustomization" + let helmrelease_name: string = "example-helmrelease" + let secret_name: string = "example-secret" + let secret_key: string = "values.yaml" + + let actual: record = ( + $resourcelist + | ( + helmrelease add values from secret + --ks-namespace $ks_namespace + $kustomization_name + $helmrelease_name + $secret_name + $secret_key + ) + ) + let expected_patch_content_existing: record = { + target: { kind: "HelmRelease", name: "existing-helmrelease" } + patch: ( + [{ op: "add", path: "/spec/valuesFrom/-", value: { kind: "Secret", name: "existing-secret", key: "existing-values.yaml" } }] + | to yaml + ) + } + let expected_patch_content_new: record = { + target: { kind: "HelmRelease", name: $helmrelease_name } + patch: ( + [{ op: "add", path: "/spec/valuesFrom/-", value: { kind: "Secret", name: $secret_name, key: $secret_key } }] + | to yaml + ) + } + let expected_resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { + apiVersion: "kustomize.toolkit.fluxcd.io/v1" + kind: "Kustomization" + metadata: { name: $kustomization_name, namespace: $ks_namespace } + spec: { patches: [$expected_patch_content_existing, $expected_patch_content_new] } + } + ] + } + + assert equal $actual $expected_resourcelist +} + + +# TODO: + +# --- helmrelease set values --- + +## Inline values only +export def "test overlaypatch helmrelease set values with inline values only" []: [ + nothing -> nothing +] { + let resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { + apiVersion: "kustomize.toolkit.fluxcd.io/v1" + kind: "Kustomization" + metadata: { name: "example-kustomization", namespace: "default" } + # spec: { patches: [] } + spec: {} + } + ] + } + + let ks_namespace: string = "default" + let kustomization_name: string = "example-kustomization" + let helmrelease_name: string = "example-helmrelease" + let inline_values: record = { key1: "value1", key2: "value2" } + + let actual: record = ( + $resourcelist + | ( + helmrelease set values + --ks-namespace $ks_namespace + $kustomization_name + $helmrelease_name + $inline_values + ) + ) + let expected_patch_content: record = { + target: { kind: "HelmRelease", name: $helmrelease_name } + patch: ( + [{ op: "add", path: "/spec/values", value: $inline_values }] + | to yaml + ) + } + let expected_resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { + apiVersion: "kustomize.toolkit.fluxcd.io/v1" + kind: "Kustomization" + metadata: { name: $kustomization_name, namespace: $ks_namespace } + spec: { patches: [$expected_patch_content] } + } + ] + } + + assert equal $actual $expected_resourcelist +} + + +## Values from ConfigMap only +export def "test overlaypatch helmrelease set values with configmap only" []: [ + nothing -> nothing +] { + let resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { + apiVersion: "kustomize.toolkit.fluxcd.io/v1" + kind: "Kustomization" + metadata: { name: "example-kustomization", namespace: "default" } + # spec: { patches: [] } + spec: {} + } + ] + } + + let ks_namespace: string = "default" + let kustomization_name: string = "example-kustomization" + let helmrelease_name: string = "example-helmrelease" + let cm_name: string = "example-configmap" + + let actual: record = ( + $resourcelist + | ( + helmrelease set values + --ks-namespace $ks_namespace + $kustomization_name + $helmrelease_name + {} + $cm_name + ) + ) + let expected_patch_content: record = { + target: { kind: "HelmRelease", name: $helmrelease_name } + patch: ( + [{ op: "add", path: "/spec/valuesFrom/-", value: { kind: "ConfigMap", name: $cm_name, key: "values.yaml" } }] + | to yaml + ) + } + let expected_resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { + apiVersion: "kustomize.toolkit.fluxcd.io/v1" + kind: "Kustomization" + metadata: { name: $kustomization_name, namespace: $ks_namespace } + spec: { patches: [$expected_patch_content] } + } + ] + } + + assert equal $actual $expected_resourcelist +} + + +## Values from Secret only +export def "test overlaypatch helmrelease set values with secret only" []: [ + nothing -> nothing +] { + let resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { + apiVersion: "kustomize.toolkit.fluxcd.io/v1" + kind: "Kustomization" + metadata: { name: "example-kustomization", namespace: "default" } + # spec: { patches: [] } + spec: {} + } + ] + } + + let ks_namespace: string = "default" + let kustomization_name: string = "example-kustomization" + let helmrelease_name: string = "example-helmrelease" + let secret_name: string = "example-secret" + + let actual: record = ( + $resourcelist + | ( + helmrelease set values + --ks-namespace $ks_namespace + $kustomization_name + $helmrelease_name + {} + '' + $secret_name + ) + ) + let expected_patch_content: record = { + target: { kind: "HelmRelease", name: $helmrelease_name } + patch: ( + [{ op: "add", path: "/spec/valuesFrom/-", value: { kind: "Secret", name: $secret_name, key: "values.yaml" } }] + | to yaml + ) + } + let expected_resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { + apiVersion: "kustomize.toolkit.fluxcd.io/v1" + kind: "Kustomization" + metadata: { name: $kustomization_name, namespace: $ks_namespace } + spec: { patches: [$expected_patch_content] } + } + ] + } + + assert equal $actual $expected_resourcelist +} + + +## Inline values and values from ConfigMap +export def "test overlaypatch helmrelease set values with inline and configmap" []: [ + nothing -> nothing +] { + let resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { + apiVersion: "kustomize.toolkit.fluxcd.io/v1" + kind: "Kustomization" + metadata: { name: "example-kustomization", namespace: "default" } + # spec: { patches: [] } + spec: {} + } + ] + } + + let ks_namespace: string = "default" + let kustomization_name: string = "example-kustomization" + let helmrelease_name: string = "example-helmrelease" + let inline_values: record = { key1: "value1", key2: "value2" } + let cm_name: string = "example-configmap" + + let actual: record = ( + $resourcelist + | ( + helmrelease set values + --ks-namespace $ks_namespace + $kustomization_name + $helmrelease_name + $inline_values + $cm_name + ) + ) + let expected_patch_content_inline_values: record = { + target: { kind: "HelmRelease", name: $helmrelease_name } + patch: ( + [{ op: "add", path: "/spec/values", value: $inline_values }] + | to yaml + ) + } + let expected_patch_content_cm_values: record = { + target: { kind: "HelmRelease", name: $helmrelease_name } + patch: ( + [{ op: "add", path: "/spec/valuesFrom/-", value: { kind: "ConfigMap", name: $cm_name, key: "values.yaml" } }] + | to yaml + ) + } + let expected_resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { + apiVersion: "kustomize.toolkit.fluxcd.io/v1" + kind: "Kustomization" + metadata: { name: $kustomization_name, namespace: $ks_namespace } + spec: { patches: [$expected_patch_content_inline_values, $expected_patch_content_cm_values] } + } + ] + } + + assert equal $actual $expected_resourcelist +} + + +## Inline values and values from secret +export def "test overlaypatch helmrelease set values with inline and secret" []: [ + nothing -> nothing +] { + let resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { + apiVersion: "kustomize.toolkit.fluxcd.io/v1" + kind: "Kustomization" + metadata: { name: "example-kustomization", namespace: "default" } + # spec: { patches: [] } + spec: {} + } + ] + } + + let ks_namespace: string = "default" + let kustomization_name: string = "example-kustomization" + let helmrelease_name: string = "example-helmrelease" + let inline_values: record = { key1: "value1", key2: "value2" } + let secret_name: string = "example-secret" + + let actual: record = ( + $resourcelist + | ( + helmrelease set values + --ks-namespace $ks_namespace + $kustomization_name + $helmrelease_name + $inline_values + '' + $secret_name + ) + ) + let expected_patch_content_inline_values: record = { + target: { kind: "HelmRelease", name: $helmrelease_name } + patch: ( + [{ op: "add", path: "/spec/values", value: $inline_values }] + | to yaml + ) + } + let expected_patch_content_secret_values: record = { + target: { kind: "HelmRelease", name: $helmrelease_name } + patch: ( + [{ op: "add", path: "/spec/valuesFrom/-", value: { kind: "Secret", name: $secret_name, key: "values.yaml" } }] + | to yaml + ) + } + let expected_resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { + apiVersion: "kustomize.toolkit.fluxcd.io/v1" + kind: "Kustomization" + metadata: { name: $kustomization_name, namespace: $ks_namespace } + spec: { patches: [$expected_patch_content_inline_values, $expected_patch_content_secret_values] } + } + ] + } + + assert equal $actual $expected_resourcelist +} + + +## Values from cm and values from secret +export def "test overlaypatch helmrelease set values with configmap and secret" []: [ + nothing -> nothing +] { + let resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { + apiVersion: "kustomize.toolkit.fluxcd.io/v1" + kind: "Kustomization" + metadata: { name: "example-kustomization", namespace: "default" } + # spec: { patches: [] } + spec: {} + } + ] + } + + let ks_namespace: string = "default" + let kustomization_name: string = "example-kustomization" + let helmrelease_name: string = "example-helmrelease" + let cm_name: string = "example-configmap" + let secret_name: string = "example-secret" + + let actual: record = ( + $resourcelist + | ( + helmrelease set values + --ks-namespace $ks_namespace + $kustomization_name + $helmrelease_name + {} + $cm_name + $secret_name + ) + ) + let expected_patch_content_cm_values: record = { + target: { kind: "HelmRelease", name: $helmrelease_name } + patch: ( + [{ op: "add", path: "/spec/valuesFrom/-", value: { kind: "ConfigMap", name: $cm_name, key: "values.yaml" } }] + | to yaml + ) + } + let expected_patch_content_secret_values: record = { + target: { kind: "HelmRelease", name: $helmrelease_name } + patch: ( + [{ op: "add", path: "/spec/valuesFrom/-", value: { kind: "Secret", name: $secret_name, key: "values.yaml" } }] + | to yaml + ) + } + let expected_resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { + apiVersion: "kustomize.toolkit.fluxcd.io/v1" + kind: "Kustomization" + metadata: { name: $kustomization_name, namespace: $ks_namespace } + spec: { patches: [$expected_patch_content_cm_values, $expected_patch_content_secret_values] } + } + ] + } + + assert equal $actual $expected_resourcelist +} + + +## Inline values, values from cm and values from secret +export def "test overlaypatch helmrelease set values with inline, configmap, and secret" []: [ + nothing -> nothing +] { + let resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { + apiVersion: "kustomize.toolkit.fluxcd.io/v1" + kind: "Kustomization" + metadata: { name: "example-kustomization", namespace: "default" } + # spec: { patches: [] } + spec: {} + } + ] + } + + let ks_namespace: string = "default" + let kustomization_name: string = "example-kustomization" + let helmrelease_name: string = "example-helmrelease" + let inline_values: record = { key1: "value1", key2: "value2" } + let cm_name: string = "example-configmap" + let secret_name: string = "example-secret" + + let actual_resourcelist: record = ( + $resourcelist + | ( + helmrelease set values + --ks-namespace $ks_namespace + $kustomization_name + $helmrelease_name + $inline_values + $cm_name + $secret_name + ) + ) + + # Expected patches for inline values, ConfigMap, and Secret + let expected_patch_inline_values: record = { + target: { kind: "HelmRelease", name: $helmrelease_name } + patch: ( + [{ op: "add", path: "/spec/values", value: $inline_values }] + | to yaml + ) + } + let expected_patch_cm_values: record = { + target: { kind: "HelmRelease", name: $helmrelease_name } + patch: ( + [{ op: "add", path: "/spec/valuesFrom/-", value: { kind: "ConfigMap", name: $cm_name, key: "values.yaml" } }] + | to yaml + ) + } + let expected_patch_secret_values: record = { + target: { kind: "HelmRelease", name: $helmrelease_name } + patch: ( + [{ op: "add", path: "/spec/valuesFrom/-", value: { kind: "Secret", name: $secret_name, key: "values.yaml" } }] + | to yaml + ) + } + let expected_resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { + apiVersion: "kustomize.toolkit.fluxcd.io/v1" + kind: "Kustomization" + metadata: { name: $kustomization_name, namespace: $ks_namespace } + spec: { patches: [$expected_patch_inline_values, $expected_patch_cm_values, $expected_patch_secret_values] } + } + ] + } + + assert equal $actual_resourcelist $expected_resourcelist +} + + +export def "test helmrelease set values with create configmap" []: [ + nothing -> nothing +] { + let resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { + apiVersion: "kustomize.toolkit.fluxcd.io/v1" + kind: "Kustomization" + metadata: { name: "example-kustomization", namespace: "default" } + # spec: { patches: [] } + spec: {} + } + ] + } + + let ks_namespace: string = "default" + let kustomization_name: string = "example-kustomization" + let helmrelease_name: string = "example-helmrelease" + let cm_name: string = "example-configmap" + let create_cm_with_values: record = { key1: "value1", key2: "value2" } + + let actual: record = ( + $resourcelist | + ( + helmrelease set values + --ks-namespace $ks_namespace + --create-cm-with-values $create_cm_with_values + $kustomization_name + $helmrelease_name + {} + $cm_name + ) + ) + let expected_cm_manifest: record = { + apiVersion: "v1" + kind: "ConfigMap" + metadata: { + name: $cm_name, + namespace: "default" + annotations: { + # "config.kubernetes.io/path": "example-kustomization-example-helmrelease-example-configmap.yaml", + # "internal.config.kubernetes.io/path": "example-kustomization-example-helmrelease-example-configmap.yaml" + "config.kubernetes.io/path": "example-configmap.yaml", + "internal.config.kubernetes.io/path": "example-configmap.yaml" + } + } + data: { + "values.yaml": ($create_cm_with_values | to yaml | str trim) + } + } + let expected_patch_content: record = { + target: { kind: "HelmRelease", name: $helmrelease_name } + patch: ( + [ + { + op: "add", + path: "/spec/valuesFrom/-", + value: { kind: "ConfigMap", name: $cm_name, key: "values.yaml" } + } + ] | to yaml + ) + } + let expected_resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { + apiVersion: "kustomize.toolkit.fluxcd.io/v1" + kind: "Kustomization" + metadata: { name: $kustomization_name, namespace: $ks_namespace } + spec: { patches: [$expected_patch_content] } + }, + $expected_cm_manifest + ] + } + + assert equal $actual $expected_resourcelist +} + + +export def "test helmrelease set values with create secret" []: [ + nothing -> nothing +] { + let resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { + apiVersion: "kustomize.toolkit.fluxcd.io/v1" + kind: "Kustomization" + metadata: { name: "example-kustomization", namespace: "default" } + # spec: { patches: [] } + spec: {} + } + ] + } + + let ks_namespace: string = "default" + let kustomization_name: string = "example-kustomization" + let helmrelease_name: string = "example-helmrelease" + let secret_name: string = "example-secret" + let create_secret_with_values: record = {key1: "value1", key2: "value2"} + + let actual: record = $resourcelist | ( + helmrelease set values + --ks-namespace $ks_namespace + --create-secret-with-values $create_secret_with_values + $kustomization_name + $helmrelease_name + {} + '' + $secret_name + ) + let expected_secret_manifest: record = { + apiVersion: "v1" + kind: "Secret" + metadata: { + name: $secret_name, + namespace: "default" + annotations: { + # "config.kubernetes.io/path": "example-kustomization-example-helmrelease-example-secret.yaml", + # "internal.config.kubernetes.io/path": "example-kustomization-example-helmrelease-example-secret.yaml" + "config.kubernetes.io/path": "example-secret.yaml", + "internal.config.kubernetes.io/path": "example-secret.yaml" + } + } + data: { + "values.yaml": ($create_secret_with_values | to yaml | str trim | encode base64) + } + } + let expected_patch_content: record = { + target: { kind: "HelmRelease", name: $helmrelease_name } + patch: ( + [ + { + op: "add", + path: "/spec/valuesFrom/-", + value: { kind: "Secret", name: $secret_name, key: "values.yaml" } + } + ] | to yaml + ) + } + let expected_resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { + apiVersion: "kustomize.toolkit.fluxcd.io/v1" + kind: "Kustomization" + metadata: { name: $kustomization_name, namespace: $ks_namespace } + spec: { patches: [$expected_patch_content] } + }, + $expected_secret_manifest + ] + } + + assert equal $actual $expected_resourcelist +} + + +export def "test helmrelease set values with create configmap and secret" []: [ + nothing -> nothing +] { + let resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { + apiVersion: "kustomize.toolkit.fluxcd.io/v1" + kind: "Kustomization" + metadata: { name: "example-kustomization", namespace: "default" } + # spec: { patches: [] } + spec: {} + } + ] + } + + let ks_namespace: string = "default" + let kustomization_name: string = "example-kustomization" + let helmrelease_name: string = "example-helmrelease" + let cm_name: string = "example-configmap" + let secret_name: string = "example-secret" + let create_cm_with_values: record = { key1: "value1", key2: "value2" } + let create_secret_with_values: record = { key3: "value3", key4: "value4" } + + let actual: record = $resourcelist | ( + helmrelease set values + --ks-namespace $ks_namespace + --create-cm-with-values $create_cm_with_values + --create-secret-with-values $create_secret_with_values + $kustomization_name + $helmrelease_name + {} + $cm_name + $secret_name + ) + let expected_cm_manifest: record = { + apiVersion: "v1" + kind: "ConfigMap" + metadata: { + name: $cm_name, + namespace: "default" + annotations: { + # "config.kubernetes.io/path": "example-kustomization-example-helmrelease-example-configmap.yaml", + # "internal.config.kubernetes.io/path": "example-kustomization-example-helmrelease-example-configmap.yaml" + "config.kubernetes.io/path": "example-configmap.yaml", + "internal.config.kubernetes.io/path": "example-configmap.yaml" + } + } + data: { + "values.yaml": ($create_cm_with_values | to yaml | str trim) + } + } + let expected_secret_manifest: record = { + apiVersion: "v1" + kind: "Secret" + metadata: { + name: $secret_name, + namespace: "default", + annotations: { + # "config.kubernetes.io/path": "example-kustomization-example-helmrelease-example-secret.yaml", + # "internal.config.kubernetes.io/path": "example-kustomization-example-helmrelease-example-secret.yaml" + "config.kubernetes.io/path": "example-secret.yaml", + "internal.config.kubernetes.io/path": "example-secret.yaml" + } + } + data: { + "values.yaml": ($create_secret_with_values | to yaml | str trim | encode base64) + } + } + let expected_patch_content_cm: record = { + target: { kind: "HelmRelease", name: $helmrelease_name } + patch: ( + [ + { + op: "add", + path: "/spec/valuesFrom/-", + value: { kind: "ConfigMap", name: $cm_name, key: "values.yaml" } + } + ] | to yaml + ) + } + let expected_patch_content_secret: record = { + target: { kind: "HelmRelease", name: $helmrelease_name } + patch: ( + [ + { + op: "add", + path: "/spec/valuesFrom/-", + value: { kind: "Secret", name: $secret_name, key: "values.yaml" } + } + ] | to yaml + ) + } + let expected_resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { + apiVersion: "kustomize.toolkit.fluxcd.io/v1" + kind: "Kustomization" + metadata: { name: $kustomization_name, namespace: $ks_namespace } + spec: { patches: [$expected_patch_content_cm, $expected_patch_content_secret] } + }, + $expected_cm_manifest, + $expected_secret_manifest + ] + } + + assert equal $actual $expected_resourcelist +} + + +export def "test helmrelease set values with create secret and age encryption" []: [ + nothing -> nothing +] { + let resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { + apiVersion: "kustomize.toolkit.fluxcd.io/v1" + kind: "Kustomization" + metadata: { name: "example-kustomization", namespace: "default" } + # spec: { patches: [] } + spec: {} + } + ] + } + + let ks_namespace: string = "default" + let kustomization_name: string = "example-kustomization" + let helmrelease_name: string = "example-helmrelease" + let secret_name: string = "example-secret" + let create_secret_with_values: record = { + key1: "value1", + key2: "value2" + } + let test_public_key: string = "age1hsrtxphk7exrdc0kt8dgr8a8r3hx88v3xpsw0ezaxvefsy9asegqknppc0" + let test_private_key: string = "AGE-SECRET-KEY-12CC3A4LEDYF4S26UV6Z2MEG7ZQL9PTU5NHH6N3FN6FLJ5HACW9LQX0UWP2" + + let actual: record = $resourcelist | ( + helmrelease set values + --ks-namespace $ks_namespace + --create-secret-with-values $create_secret_with_values + --public-age-key $test_public_key + $kustomization_name + $helmrelease_name + {} + '' + $secret_name + ) + + let expected_secret_manifest: record = { + apiVersion: "v1" + kind: "Secret" + metadata: { + name: $secret_name, + namespace: "default" + annotations: { + # "config.kubernetes.io/path": "example-kustomization-example-helmrelease-example-secret.yaml", + # "internal.config.kubernetes.io/path": "example-kustomization-example-helmrelease-example-secret.yaml" + "config.kubernetes.io/path": "example-secret.yaml", + "internal.config.kubernetes.io/path": "example-secret.yaml" + } + } + data: { + "values.yaml": ($create_secret_with_values | to yaml | str trim | encode base64) + } + } + + # NOTE: Here the secret is kept decrypted intentionally, since the same secret encrypted twice is never equal and we will need to decrypt them anyway to check they are equal + let expected_patch_content: record = { + target: { kind: "HelmRelease", name: $helmrelease_name } + patch: ( + [ + { + op: "add", + path: "/spec/valuesFrom/-", + value: { kind: "Secret", name: $secret_name, key: "values.yaml" } + } + ] | to yaml + ) + } + let expected_resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + $expected_secret_manifest, + { + apiVersion: "kustomize.toolkit.fluxcd.io/v1" + kind: "Kustomization" + metadata: { name: $kustomization_name, namespace: $ks_namespace } + spec: { patches: [$expected_patch_content] } + } + ] + } + + # Check that everything except the encrypted secret is equal + (assert equal + ($actual | patch resource delete '' 'Secret') + ($expected_resourcelist | patch resource delete '' 'Secret') + ) + + # Check that both secrets, once decrypted, are equal + let actual_secret_manifest: record = ( + # First, extracts the manifest of the encrypted Secret + $actual + | patch resource keep '' 'Secret' + | get items.0 + # Removes the filename annotations, since they are excluded from encryption + | reject $.metadata.annotations + # Then, decrypts the manifest using the private key + | to yaml + | keypair decrypt secret manifest $test_private_key + | from yaml + ) + (assert equal + $actual_secret_manifest + ($expected_secret_manifest | reject $.metadata.annotations) + ) +} diff --git a/docker/osm-nushell-krm-functions/krm/tests/patch.nu b/docker/osm-nushell-krm-functions/krm/tests/patch.nu new file mode 100644 index 00000000..ec2ae70d --- /dev/null +++ b/docker/osm-nushell-krm-functions/krm/tests/patch.nu @@ -0,0 +1,1438 @@ +#!/usr/bin/env -S nu --stdin +####################################################################################### +# Copyright ETSI Contributors and Others. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +####################################################################################### + + +use std assert +use ../../krm/patch.nu * + + +# --- resource keep tests --- + +export def "test patch resource keep no filters" []: [ + nothing -> nothing +] { + let resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example1", namespace: "default" } } + { apiVersion: "v1", kind: "Pod", metadata: { name: "example2", namespace: "default" } } + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example3", namespace: "other" } } + ] + } + + let actual: record = $resourcelist | resource keep + let expected: record = $resourcelist + + assert equal $actual $expected +} + + +export def "test patch resource keep by apiVersion" []: [ + nothing -> nothing +] { + let resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example1", namespace: "default" } } + { apiVersion: "v1", kind: "Pod", metadata: { name: "example2", namespace: "default" } } + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example3", namespace: "other" } } + ] + } + + let actual: record = $resourcelist | resource keep "apps/v1" + let expected: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example1", namespace: "default" } } + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example3", namespace: "other" } } + ] + } + + assert equal $actual $expected +} + + +export def "test patch resource keep by kind" []: [ + nothing -> nothing +] { + let resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example1", namespace: "default" } } + { apiVersion: "v1", kind: "Pod", metadata: { name: "example2", namespace: "default" } } + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example3", namespace: "other" } } + ] + } + + let actual: record = $resourcelist | resource keep '' "Deployment" + let expected: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example1", namespace: "default" } } + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example3", namespace: "other" } } + ] + } + + assert equal $actual $expected +} + + +export def "test patch resource keep by name" []: [ + nothing -> nothing +] { + let resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example1", namespace: "default" } } + { apiVersion: "v1", kind: "Pod", metadata: { name: "example2", namespace: "default" } } + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example3", namespace: "other" } } + ] + } + + let actual: record = $resourcelist | resource keep '' '' "example1" + let expected: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example1", namespace: "default" } } + ] + } + + assert equal $actual $expected +} + + +export def "test patch resource keep by namespace" []: [ + nothing -> nothing +] { + let resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example1", namespace: "default" } } + { apiVersion: "v1", kind: "Pod", metadata: { name: "example2", namespace: "default" } } + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example3", namespace: "other" } } + ] + } + + let actual: record = $resourcelist | resource keep '' '' '' "default" + let expected: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example1", namespace: "default" } } + { apiVersion: "v1", kind: "Pod", metadata: { name: "example2", namespace: "default" } } + ] + } + + assert equal $actual $expected +} + + +export def "test patch resource keep multiple filters" []: [ + nothing -> nothing +] { + let resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example1", namespace: "default" } } + { apiVersion: "v1", kind: "Pod", metadata: { name: "example2", namespace: "default" } } + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example3", namespace: "other" } } + ] + } + + let actual: record = $resourcelist | resource keep "apps/v1" "Deployment" '' "default" + let expected: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example1", namespace: "default" } } + ] + } + + assert equal $actual $expected +} + + +export def "test patch resource keep invalid input" []: [ + nothing -> nothing +] { + let invalid_input: record = {kind: "Invalid kind"} + + let error_occurred: any = try { + $invalid_input | resource keep + } catch { + |err| $err.msg + } + + assert ($error_occurred | str starts-with "Error: Expected a ResourceList, but received") +} + + + +# --- resource delete tests --- + +export def "test patch resource delete no filters" []: [ + nothing -> nothing +] { + let resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example1", namespace: "default" } } + { apiVersion: "v1", kind: "Pod", metadata: { name: "example2", namespace: "default" } } + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example3", namespace: "other" } } + ] + } + + let actual: record = $resourcelist | resource delete + let expected: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [] + } + + assert equal $actual $expected +} + + +export def "test patch resource delete by apiVersion" []: [ + nothing -> nothing +] { + let resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example1", namespace: "default" } } + { apiVersion: "v1", kind: "Pod", metadata: { name: "example2", namespace: "default" } } + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example3", namespace: "other" } } + ] + } + + let actual: record = $resourcelist | resource delete "apps/v1" + let expected: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { apiVersion: "v1", kind: "Pod", metadata: { name: "example2", namespace: "default" } } + ] + } + + assert equal $actual $expected +} + + +export def "test patch resource delete by kind" []: [ + nothing -> nothing +] { + let resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example1", namespace: "default" } } + { apiVersion: "v1", kind: "Pod", metadata: { name: "example2", namespace: "default" } } + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example3", namespace: "other" } } + ] + } + + let actual: record = $resourcelist | resource delete '' "Deployment" + let expected: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { apiVersion: "v1", kind: "Pod", metadata: { name: "example2", namespace: "default" } } + ] + } + + assert equal $actual $expected +} + + +export def "test patch resource delete by name" []: [ + nothing -> nothing +] { + let resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example1", namespace: "default" } } + { apiVersion: "v1", kind: "Pod", metadata: { name: "example2", namespace: "default" } } + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example3", namespace: "other" } } + ] + } + + let actual: record = $resourcelist | resource delete '' '' "example1" + let expected: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { apiVersion: "v1", kind: "Pod", metadata: { name: "example2", namespace: "default" } } + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example3", namespace: "other" } } + ] + } + + assert equal $actual $expected +} + + +export def "test patch resource delete by namespace" []: [ + nothing -> nothing +] { + let resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example1", namespace: "default" } } + { apiVersion: "v1", kind: "Pod", metadata: { name: "example2", namespace: "default" } } + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example3", namespace: "other" } } + ] + } + + let actual: record = $resourcelist | resource delete '' '' '' "default" + let expected: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example3", namespace: "other" } } + ] + } + + assert equal $actual $expected +} + + +export def "test patch resource delete multiple filters" []: [ + nothing -> nothing +] { + let resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example1", namespace: "default" } } + { apiVersion: "v1", kind: "Pod", metadata: { name: "example2", namespace: "default" } } + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example3", namespace: "other" } } + ] + } + + let actual: record = $resourcelist | resource delete "apps/v1" "Deployment" '' "default" + let expected: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { apiVersion: "v1", kind: "Pod", metadata: { name: "example2", namespace: "default" } } + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example3", namespace: "other" } } + ] + } + + assert equal $actual $expected +} + + +export def "test patch resource delete invalid input" []: [ + nothing -> nothing +] { + let invalid_input: record = {kind: "Invalid kind"} + + let error_occurred: any = try { + echo $invalid_input | resource delete + } catch { + |err| $err.msg + } + + assert ($error_occurred | str starts-with "Error: Expected a ResourceList, but received") + +} + + + +# --- resource custom function tests --- + +export def "test patch resource custom function no filters" []: [ + nothing -> nothing +] { + let resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example1", namespace: "default" } } + { apiVersion: "v1", kind: "Pod", metadata: { name: "example2", namespace: "default" } } + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example3", namespace: "other" } } + ] + } + + let custom_function: closure = { |k: cell-path, v: any| ($in | upsert $k $v) } + let key_path: cell-path = $.metadata.labels + let value: any = { app: "example" } + + let actual: record = $resourcelist | resource custom function $custom_function $key_path $value + let expected: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example1", namespace: "default", labels: { app: "example" } } } + { apiVersion: "v1", kind: "Pod", metadata: { name: "example2", namespace: "default", labels: { app: "example" } } } + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example3", namespace: "other", labels: { app: "example" } } } + ] + } + + assert equal $actual $expected +} + + +export def "test patch resource custom function by apiVersion" []: [ + nothing -> nothing +] { + let resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example1", namespace: "default" } } + { apiVersion: "v1", kind: "Pod", metadata: { name: "example2", namespace: "default" } } + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example3", namespace: "other" } } + ] + } + + let custom_function: closure = { |k: cell-path, v: any| ($in | upsert $k $v) } + let key_path: cell-path = $.metadata.labels + let value: any = { app: "example" } + + let actual: record = $resourcelist | resource custom function $custom_function $key_path $value "apps/v1" + let expected: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example1", namespace: "default", labels: { app: "example" } } } + { apiVersion: "v1", kind: "Pod", metadata: { name: "example2", namespace: "default" } } + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example3", namespace: "other", labels: { app: "example" } } } + ] + } + + assert equal $actual $expected +} + + +export def "test patch resource custom function by kind" []: [ + nothing -> nothing +] { + let resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example1", namespace: "default" } } + { apiVersion: "v1", kind: "Pod", metadata: { name: "example2", namespace: "default" } } + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example3", namespace: "other" } } + ] + } + + let custom_function: closure = { |k: cell-path, v: any| ($in | upsert $k $v) } + let key_path: cell-path = $.metadata.labels + let value: any = { app: "example" } + + let actual: record = $resourcelist | resource custom function $custom_function $key_path $value '' "Deployment" + let expected: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example1", namespace: "default", labels: { app: "example" } } } + { apiVersion: "v1", kind: "Pod", metadata: { name: "example2", namespace: "default" } } + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example3", namespace: "other", labels: { app: "example" } } } + ] + } + + assert equal $actual $expected +} + + +export def "test patch resource custom function by name" []: [ + nothing -> nothing +] { + let resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example1", namespace: "default" } } + { apiVersion: "v1", kind: "Pod", metadata: { name: "example2", namespace: "default" } } + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example3", namespace: "other" } } + ] + } + + let custom_function: closure = { |k: cell-path, v: any| ($in | upsert $k $v) } + let key_path: cell-path = $.metadata.labels + let value: any = { app: "example" } + + let actual: record = $resourcelist | resource custom function $custom_function $key_path $value '' '' "example1" + let expected: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example1", namespace: "default", labels: { app: "example" } } } + { apiVersion: "v1", kind: "Pod", metadata: { name: "example2", namespace: "default" } } + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example3", namespace: "other" } } + ] + } + + assert equal $actual $expected +} + + +export def "test patch resource custom function by namespace" []: [ + nothing -> nothing +] { + let resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example1", namespace: "default" } } + { apiVersion: "v1", kind: "Pod", metadata: { name: "example2", namespace: "default" } } + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example3", namespace: "other" } } + ] + } + + let custom_function: closure = { |k: cell-path, v: any| ($in | upsert $k $v) } + let key_path: cell-path = $.metadata.labels + let value: any = { app: "example" } + + let actual: record = $resourcelist | resource custom function $custom_function $key_path $value '' '' '' "default" + let expected: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example1", namespace: "default", labels: { app: "example" } } } + { apiVersion: "v1", kind: "Pod", metadata: { name: "example2", namespace: "default", labels: { app: "example" } } } + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example3", namespace: "other" } } + ] + } + + assert equal $actual $expected +} + + +export def "test patch resource custom function multiple filters" []: [ + nothing -> nothing +] { + let resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example1", namespace: "default" } } + { apiVersion: "v1", kind: "Pod", metadata: { name: "example2", namespace: "default" } } + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example3", namespace: "other" } } + ] + } + + let custom_function: closure = { |k: cell-path, v: any| ($in | upsert $k $v) } + let key_path: cell-path = $.metadata.labels + let value: any = { app: "example" } + + let actual: record = $resourcelist | resource custom function $custom_function $key_path $value "apps/v1" "Deployment" '' "default" + let expected: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example1", namespace: "default", labels: { app: "example" } } } + { apiVersion: "v1", kind: "Pod", metadata: { name: "example2", namespace: "default" } } + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example3", namespace: "other" } } + ] + } + + assert equal $actual $expected +} + + +export def "test patch resource custom function invalid input" []: [ + nothing -> nothing +] { + let invalid_input: record = {kind: "Invalid kind"} + + let error_occurred: any = try { + $invalid_input | resource custom function {|item, key_path, value| $item | update $key_path $value } $.metadata.labels { app: "example" } + } catch { + |err| $err.msg + } + + assert ($error_occurred | str starts-with "Error: Expected a ResourceList, but received") +} + + + +# --- resource upsert key tests --- + +export def "test patch resource upsert key no filters" []: [ + nothing -> nothing +] { + let resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example1", namespace: "default" } } + { apiVersion: "v1", kind: "Pod", metadata: { name: "example2", namespace: "default" } } + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example3", namespace: "other" } } + ] + } + + let key_path: cell-path = $.metadata.labels + let value: any = { app: "example" } + + let actual: record = $resourcelist | resource upsert key $key_path $value + let expected: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example1", namespace: "default", labels: { app: "example" } } } + { apiVersion: "v1", kind: "Pod", metadata: { name: "example2", namespace: "default", labels: { app: "example" } } } + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example3", namespace: "other", labels: { app: "example" } } } + ] + } + + assert equal $actual $expected +} + + +export def "test patch resource upsert key by apiVersion" []: [ + nothing -> nothing +] { + let resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example1", namespace: "default" } } + { apiVersion: "v1", kind: "Pod", metadata: { name: "example2", namespace: "default" } } + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example3", namespace: "other" } } + ] + } + + let key_path: cell-path = $.metadata.labels + let value: any = { app: "example" } + + let actual: record = $resourcelist | resource upsert key $key_path $value "apps/v1" + let expected: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example1", namespace: "default", labels: { app: "example" } } } + { apiVersion: "v1", kind: "Pod", metadata: { name: "example2", namespace: "default" } } + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example3", namespace: "other", labels: { app: "example" } } } + ] + } + + assert equal $actual $expected +} + + +export def "test patch resource upsert key by kind" []: [ + nothing -> nothing +] { + let resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example1", namespace: "default" } } + { apiVersion: "v1", kind: "Pod", metadata: { name: "example2", namespace: "default" } } + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example3", namespace: "other" } } + ] + } + + let key_path: cell-path = $.metadata.labels + let value: any = { app: "example" } + + let actual: record = $resourcelist | resource upsert key $key_path $value '' "Deployment" + let expected: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example1", namespace: "default", labels: { app: "example" } } } + { apiVersion: "v1", kind: "Pod", metadata: { name: "example2", namespace: "default" } } + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example3", namespace: "other", labels: { app: "example" } } } + ] + } + + assert equal $actual $expected +} + + +export def "test patch resource upsert key by name" []: [ + nothing -> nothing +] { + let resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example1", namespace: "default" } } + { apiVersion: "v1", kind: "Pod", metadata: { name: "example2", namespace: "default" } } + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example3", namespace: "other" } } + ] + } + + let key_path: cell-path = $.metadata.labels + let value: any = { app: "example" } + + let actual: record = $resourcelist | resource upsert key $key_path $value '' '' "example1" + let expected: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example1", namespace: "default", labels: { app: "example" } } } + { apiVersion: "v1", kind: "Pod", metadata: { name: "example2", namespace: "default" } } + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example3", namespace: "other" } } + ] + } + + assert equal $actual $expected +} + + +export def "test patch resource upsert key by namespace" []: [ + nothing -> nothing +] { + let resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example1", namespace: "default" } } + { apiVersion: "v1", kind: "Pod", metadata: { name: "example2", namespace: "default" } } + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example3", namespace: "other" } } + ] + } + + let key_path: cell-path = $.metadata.labels + let value: any = { app: "example" } + + let actual: record = $resourcelist | resource upsert key $key_path $value '' '' '' "default" + let expected: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example1", namespace: "default", labels: { app: "example" } } } + { apiVersion: "v1", kind: "Pod", metadata: { name: "example2", namespace: "default", labels: { app: "example" } } } + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example3", namespace: "other" } } + ] + } + + assert equal $actual $expected +} + + +export def "test patch resource upsert key multiple filters" []: [ + nothing -> nothing +] { + let resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example1", namespace: "default" } } + { apiVersion: "v1", kind: "Pod", metadata: { name: "example2", namespace: "default" } } + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example3", namespace: "other" } } + ] + } + + let key_path: cell-path = $.metadata.labels + let value: any = { app: "example" } + + let actual: record = $resourcelist | resource upsert key $key_path $value "apps/v1" "Deployment" '' "default" + let expected: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example1", namespace: "default", labels: { app: "example" } } } + { apiVersion: "v1", kind: "Pod", metadata: { name: "example2", namespace: "default" } } + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example3", namespace: "other" } } + ] + } + + assert equal $actual $expected +} + + +export def "test patch resource upsert key invalid input" []: [ + nothing -> nothing +] { + let invalid_input: record = {kind: "Invalid kind"} + + let error_occurred: any = try { + $invalid_input | resource upsert key $.metadata.labels { app: "example" } + } catch { + |err| $err.msg + } + + assert ($error_occurred | str starts-with "Error: Expected a ResourceList, but received") +} + + + +# --- resource filename set tests --- + +export def "test patch resource filename set no index" []: [ + nothing -> nothing +] { + let resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example1", namespace: "default" } } + { apiVersion: "v1", kind: "Pod", metadata: { name: "example2", namespace: "default" } } + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example3", namespace: "other" } } + ] + } + + let filename: string = "example.yaml" + let actual: record = $resourcelist | resource filename set $filename + let expected: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example1", namespace: "default", annotations: { "config.kubernetes.io/path": $filename, "internal.config.kubernetes.io/path": $filename } } } + { apiVersion: "v1", kind: "Pod", metadata: { name: "example2", namespace: "default", annotations: { "config.kubernetes.io/path": $filename, "internal.config.kubernetes.io/path": $filename } } } + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example3", namespace: "other", annotations: { "config.kubernetes.io/path": $filename, "internal.config.kubernetes.io/path": $filename } } } + ] + } + + assert equal $actual $expected +} + + +export def "test patch resource filename set with index" []: [ + nothing -> nothing +] { + let resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example1", namespace: "default" } } + { apiVersion: "v1", kind: "Pod", metadata: { name: "example2", namespace: "default" } } + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example3", namespace: "other" } } + ] + } + + let filename: string = "example.yaml" + let index: int = 0 + let actual: record = $resourcelist | resource filename set --index $index $filename + let expected: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example1", namespace: "default", annotations: { "config.kubernetes.io/path": $filename, "internal.config.kubernetes.io/path": $filename, "config.kubernetes.io/index": "0", "internal.config.kubernetes.io/index": "0" } } } + { apiVersion: "v1", kind: "Pod", metadata: { name: "example2", namespace: "default", annotations: { "config.kubernetes.io/path": $filename, "internal.config.kubernetes.io/path": $filename, "config.kubernetes.io/index": "0", "internal.config.kubernetes.io/index": "0" } } } + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example3", namespace: "other", annotations: { "config.kubernetes.io/path": $filename, "internal.config.kubernetes.io/path": $filename, "config.kubernetes.io/index": "0", "internal.config.kubernetes.io/index": "0" } } } + ] + } + + assert equal $actual $expected +} + + +export def "test patch resource filename set by apiVersion" []: [ + nothing -> nothing +] { + let resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example1", namespace: "default" } } + { apiVersion: "v1", kind: "Pod", metadata: { name: "example2", namespace: "default" } } + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example3", namespace: "other" } } + ] + } + + let filename: string = "example.yaml" + let actual: record = $resourcelist | resource filename set $filename "apps/v1" + let expected: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example1", namespace: "default", annotations: { "config.kubernetes.io/path": $filename, "internal.config.kubernetes.io/path": $filename } } } + { apiVersion: "v1", kind: "Pod", metadata: { name: "example2", namespace: "default" } } + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example3", namespace: "other", annotations: { "config.kubernetes.io/path": $filename, "internal.config.kubernetes.io/path": $filename } } } + ] + } + + assert equal $actual $expected +} + + +export def "test patch resource filename set by kind" []: [ + nothing -> nothing +] { + let resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example1", namespace: "default" } } + { apiVersion: "v1", kind: "Pod", metadata: { name: "example2", namespace: "default" } } + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example3", namespace: "other" } } + ] + } + + let filename: string = "example.yaml" + let actual: record = $resourcelist | resource filename set $filename '' "Deployment" + let expected: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example1", namespace: "default", annotations: { "config.kubernetes.io/path": $filename, "internal.config.kubernetes.io/path": $filename } } } + { apiVersion: "v1", kind: "Pod", metadata: { name: "example2", namespace: "default" } } + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example3", namespace: "other", annotations: { "config.kubernetes.io/path": $filename, "internal.config.kubernetes.io/path": $filename } } } + ] + } + + assert equal $actual $expected +} + + +export def "test patch resource filename set by name" []: [ + nothing -> nothing +] { + let resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example1", namespace: "default" } } + { apiVersion: "v1", kind: "Pod", metadata: { name: "example2", namespace: "default" } } + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example3", namespace: "other" } } + ] + } + + let filename: string = "example.yaml" + let actual: record = $resourcelist | resource filename set $filename '' '' "example1" + let expected: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example1", namespace: "default", annotations: { "config.kubernetes.io/path": $filename, "internal.config.kubernetes.io/path": $filename } } } + { apiVersion: "v1", kind: "Pod", metadata: { name: "example2", namespace: "default" } } + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example3", namespace: "other" } } + ] + } + + assert equal $actual $expected +} + + +export def "test patch resource filename set by namespace" []: [ + nothing -> nothing +] { + let resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example1", namespace: "default" } } + { apiVersion: "v1", kind: "Pod", metadata: { name: "example2", namespace: "default" } } + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example3", namespace: "other" } } + ] + } + + let filename: string = "example.yaml" + let actual: record = $resourcelist | resource filename set $filename '' '' '' "default" + let expected: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example1", namespace: "default", annotations: { "config.kubernetes.io/path": $filename, "internal.config.kubernetes.io/path": $filename } } } + { apiVersion: "v1", kind: "Pod", metadata: { name: "example2", namespace: "default", annotations: { "config.kubernetes.io/path": $filename, "internal.config.kubernetes.io/path": $filename } } } + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example3", namespace: "other" } } + ] + } + + assert equal $actual $expected +} + + +export def "test patch resource filename set multiple filters" []: [ + nothing -> nothing +] { + let resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example1", namespace: "default" } } + { apiVersion: "v1", kind: "Pod", metadata: { name: "example2", namespace: "default" } } + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example3", namespace: "other" } } + ] + } + + let filename: string = "example.yaml" + let actual: record = $resourcelist | resource filename set $filename "apps/v1" "Deployment" '' "default" + let expected: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example1", namespace: "default", annotations: { "config.kubernetes.io/path": $filename, "internal.config.kubernetes.io/path": $filename } } } + { apiVersion: "v1", kind: "Pod", metadata: { name: "example2", namespace: "default" } } + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example3", namespace: "other" } } + ] + } + + assert equal $actual $expected +} + + + +# TODO: + +# --- list append item tests --- + +export def "test patch list append item no filters" []: [ + nothing -> nothing +] { + let resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example1", namespace: "default" } } + { apiVersion: "v1", kind: "Pod", metadata: { name: "example2", namespace: "default" } } + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example3", namespace: "other" } } + ] + } + + let key_path: cell-path = $.metadata.annotations.example + let value: any = "example-value" + let actual: record = $resourcelist | list append item $key_path $value + let expected: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example1", namespace: "default", annotations: { example: ["example-value"] } } } + { apiVersion: "v1", kind: "Pod", metadata: { name: "example2", namespace: "default", annotations: { example: ["example-value"] } } } + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example3", namespace: "other", annotations: { example: ["example-value"] } } } + ] + } + + assert equal $actual $expected +} + + +export def "test patch list append item existing list" []: [ + nothing -> nothing +] { + let resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example1", namespace: "default", annotations: { example: ["initial-value"] } } } + { apiVersion: "v1", kind: "Pod", metadata: { name: "example2", namespace: "default", annotations: { example: ["initial-value"] } } } + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example3", namespace: "other", annotations: { example: ["initial-value"] } } } + ] + } + + let key_path: cell-path = $.metadata.annotations.example + let value: any = "example-value" + let actual: record = $resourcelist | list append item $key_path $value + let expected: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example1", namespace: "default", annotations: { example: ["initial-value", "example-value"] } } } + { apiVersion: "v1", kind: "Pod", metadata: { name: "example2", namespace: "default", annotations: { example: ["initial-value", "example-value"] } } } + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example3", namespace: "other", annotations: { example: ["initial-value", "example-value"] } } } + ] + } + + assert equal $actual $expected +} + + +export def "test patch list append item existing non-list value" []: [ + nothing -> nothing +] { + let resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example1", namespace: "default", annotations: { example: "initial-value" } } } + { apiVersion: "v1", kind: "Pod", metadata: { name: "example2", namespace: "default", annotations: { example: "initial-value" } } } + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example3", namespace: "other", annotations: { example: "initial-value" } } } + ] + } + + let key_path: cell-path = $.metadata.annotations.example + let value: any = "example-value" + + let error_occurred: any = try { + $resourcelist | list append item $key_path $value + } catch { + |err| $err.msg + } + + assert ($error_occurred | str starts-with "Error: Some matching keys are not lists. Non conformant:") +} + + +export def "test patch list append item by apiVersion" []: [ + nothing -> nothing +] { + let resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example1", namespace: "default" } } + { apiVersion: "v1", kind: "Pod", metadata: { name: "example2", namespace: "default" } } + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example3", namespace: "other" } } + ] + } + + let key_path: cell-path = $.metadata.annotations.example + let value: any = "example-value" + let actual: record = $resourcelist | list append item $key_path $value "apps/v1" + let expected: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example1", namespace: "default", annotations: { example: ["example-value"] } } } + { apiVersion: "v1", kind: "Pod", metadata: { name: "example2", namespace: "default" } } + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example3", namespace: "other", annotations: { example: ["example-value"] } } } + ] + } + + assert equal $actual $expected +} + + +export def "test patch list append item by kind" []: [ + nothing -> nothing +] { + let resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example1", namespace: "default" } } + { apiVersion: "v1", kind: "Pod", metadata: { name: "example2", namespace: "default" } } + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example3", namespace: "other" } } + ] + } + + let key_path: cell-path = $.metadata.annotations.example + let value: any = "example-value" + let actual: record = $resourcelist | list append item $key_path $value '' "Deployment" + let expected: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example1", namespace: "default", annotations: { example: ["example-value"] } } } + { apiVersion: "v1", kind: "Pod", metadata: { name: "example2", namespace: "default" } } + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example3", namespace: "other", annotations: { example: ["example-value"] } } } + ] + } + + assert equal $actual $expected +} + + +export def "test patch list append item by name" []: [ + nothing -> nothing +] { + let resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example1", namespace: "default" } } + { apiVersion: "v1", kind: "Pod", metadata: { name: "example2", namespace: "default" } } + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example3", namespace: "other" } } + ] + } + + let key_path: cell-path = $.metadata.annotations.example + let value: any = "example-value" + let actual: record = $resourcelist | list append item $key_path $value '' '' "example1" + let expected: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example1", namespace: "default", annotations: { example: ["example-value"] } } } + { apiVersion: "v1", kind: "Pod", metadata: { name: "example2", namespace: "default" } } + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example3", namespace: "other" } } + ] + } + + assert equal $actual $expected +} + + +export def "test patch list append item by namespace" []: [ + nothing -> nothing +] { + let resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example1", namespace: "default" } } + { apiVersion: "v1", kind: "Pod", metadata: { name: "example2", namespace: "default" } } + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example3", namespace: "other" } } + ] + } + + let key_path: cell-path = $.metadata.annotations.example + let value: any = "example-value" + let actual: record = $resourcelist | list append item $key_path $value '' '' '' "default" + let expected: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example1", namespace: "default", annotations: { example: ["example-value"] } } } + { apiVersion: "v1", kind: "Pod", metadata: { name: "example2", namespace: "default", annotations: { example: ["example-value"] } } } + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example3", namespace: "other" } } + ] + } + + assert equal $actual $expected +} + + +export def "test patch list append item multiple filters" []: [ + nothing -> nothing +] { + let resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example1", namespace: "default" } } + { apiVersion: "v1", kind: "Pod", metadata: { name: "example2", namespace: "default" } } + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example3", namespace: "other" } } + ] + } + + let key_path: cell-path = $.metadata.annotations.example + let value: any = "example-value" + let actual: record = $resourcelist | list append item $key_path $value "apps/v1" 'Deployment' '' "default" + let expected: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example1", namespace: "default", annotations: { example: ["example-value"] } } } + { apiVersion: "v1", kind: "Pod", metadata: { name: "example2", namespace: "default" } } + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example3", namespace: "other" } } + ] + } + + assert equal $actual $expected +} + + +# TODO: + +# --- list drop item tests --- + +export def "test patch list drop item no filters" []: [ + nothing -> nothing +] { + let resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example1", namespace: "default", annotations: { example: ["value1", "value2"] } } } + { apiVersion: "v1", kind: "Pod", metadata: { name: "example2", namespace: "default", annotations: { example: ["value1", "value2"] } } } + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example3", namespace: "other", annotations: { example: ["value1", "value2"] } } } + ] + } + + let key_path: cell-path = $.metadata.annotations.example + let value: any = "value1" + let actual: record = $resourcelist | list drop item $key_path $value + let expected: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example1", namespace: "default", annotations: { example: ["value2"] } } } + { apiVersion: "v1", kind: "Pod", metadata: { name: "example2", namespace: "default", annotations: { example: ["value2"] } } } + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example3", namespace: "other", annotations: { example: ["value2"] } } } + ] + } + + assert equal $actual $expected +} + + +export def "test patch list drop item existing list with multiple values" []: [ + nothing -> nothing +] { + let resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example1", namespace: "default", annotations: { example: ["value1", "value2", "value3"] } } } + { apiVersion: "v1", kind: "Pod", metadata: { name: "example2", namespace: "default", annotations: { example: ["value1", "value2", "value3"] } } } + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example3", namespace: "other", annotations: { example: ["value1", "value2", "value3"] } } } + ] + } + + let key_path: cell-path = $.metadata.annotations.example + let value: any = "value2" + let actual: record = $resourcelist | list drop item $key_path $value + let expected: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example1", namespace: "default", annotations: { example: ["value1", "value3"] } } } + { apiVersion: "v1", kind: "Pod", metadata: { name: "example2", namespace: "default", annotations: { example: ["value1", "value3"] } } } + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example3", namespace: "other", annotations: { example: ["value1", "value3"] } } } + ] + } + + assert equal $actual $expected +} + + +export def "test patch list drop item existing non-list value" []: [ + nothing -> nothing +] { + let resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example1", namespace: "default", annotations: { example: "value1" } } } + { apiVersion: "v1", kind: "Pod", metadata: { name: "example2", namespace: "default", annotations: { example: "value1" } } } + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example3", namespace: "other", annotations: { example: "value1" } } } + ] + } + + let key_path: cell-path = $.metadata.annotations.example + let value: any = "value1" + + let error_occurred: any = try { + $resourcelist | list drop item $key_path $value + } catch { + |err| $err.msg + } + + assert ($error_occurred | str starts-with "Error: Some matching keys are not lists. Non conformant:") +} + + +export def "test patch list drop item by apiVersion" []: [ + nothing -> nothing +] { + let resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example1", namespace: "default", annotations: { example: ["value1", "value2"] } } } + { apiVersion: "v1", kind: "Pod", metadata: { name: "example2", namespace: "default", annotations: { example: ["value1", "value2"] } } } + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example3", namespace: "other", annotations: { example: ["value1", "value2"] } } } + ] + } + + let key_path: cell-path = $.metadata.annotations.example + let value: any = "value1" + let actual: record = $resourcelist | list drop item $key_path $value "apps/v1" + let expected: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example1", namespace: "default", annotations: { example: ["value2"] } } } + { apiVersion: "v1", kind: "Pod", metadata: { name: "example2", namespace: "default", annotations: { example: ["value1", "value2"] } } } + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example3", namespace: "other", annotations: { example: ["value2"] } } } + ] + } + + assert equal $actual $expected +} + + +export def "test patch list drop item by kind" []: [ + nothing -> nothing +] { + let resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example1", namespace: "default", annotations: { example: ["value1", "value2"] } } } + { apiVersion: "v1", kind: "Pod", metadata: { name: "example2", namespace: "default", annotations: { example: ["value1", "value2"] } } } + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example3", namespace: "other", annotations: { example: ["value1", "value2"] } } } + ] + } + + let key_path: cell-path = $.metadata.annotations.example + let value: any = "value1" + let actual: record = $resourcelist | list drop item $key_path $value '' "Deployment" + let expected: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example1", namespace: "default", annotations: { example: ["value2"] } } } + { apiVersion: "v1", kind: "Pod", metadata: { name: "example2", namespace: "default", annotations: { example: ["value1", "value2"] } } } + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example3", namespace: "other", annotations: { example: ["value2"] } } } + ] + } + + assert equal $actual $expected +} + + +export def "test patch list drop item by name" []: [ + nothing -> nothing +] { + let resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example1", namespace: "default", annotations: { example: ["value1", "value2"] } } } + { apiVersion: "v1", kind: "Pod", metadata: { name: "example2", namespace: "default", annotations: { example: ["value1", "value2"] } } } + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example3", namespace: "other", annotations: { example: ["value1", "value2"] } } } + ] + } + + let key_path: cell-path = $.metadata.annotations.example + let value: any = "value1" + let actual: record = $resourcelist | list drop item $key_path $value '' '' "example1" + let expected: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example1", namespace: "default", annotations: { example: ["value2"] } } } + { apiVersion: "v1", kind: "Pod", metadata: { name: "example2", namespace: "default", annotations: { example: ["value1", "value2"] } } } + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example3", namespace: "other", annotations: { example: ["value1", "value2"] } } } + ] + } + + assert equal $actual $expected +} + + +export def "test patch list drop item by namespace" []: [ + nothing -> nothing +] { + let resourcelist: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example1", namespace: "default", annotations: { example: ["value1", "value2"] } } } + { apiVersion: "v1", kind: "Pod", metadata: { name: "example2", namespace: "default", annotations: { example: ["value1", "value2"] } } } + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example3", namespace: "other", annotations: { example: ["value1", "value2"] } } } + ] + } + + let key_path: cell-path = $.metadata.annotations.example + let value: any = "value1" + let actual: record = $resourcelist | list drop item $key_path $value '' '' '' "default" + let expected: record = { + apiVersion: "config.kubernetes.io/v1" + kind: "ResourceList" + items: [ + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example1", namespace: "default", annotations: { example: ["value2"] } } } + { apiVersion: "v1", kind: "Pod", metadata: { name: "example2", namespace: "default", annotations: { example: ["value2"] } } } + { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "example3", namespace: "other", annotations: { example: ["value1", "value2"] } } } + ] + } + + assert equal $actual $expected +} diff --git a/docker/osm-nushell-krm-functions/krm/tests/strategicmergepatch.nu b/docker/osm-nushell-krm-functions/krm/tests/strategicmergepatch.nu new file mode 100644 index 00000000..095c60a6 --- /dev/null +++ b/docker/osm-nushell-krm-functions/krm/tests/strategicmergepatch.nu @@ -0,0 +1,144 @@ +#!/usr/bin/env -S nu --stdin +####################################################################################### +# Copyright ETSI Contributors and Others. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +####################################################################################### + + +use std assert +use ../../krm/strategicmergepatch.nu * + + +# --- create strategic merge patch tests --- + +export def "test strategicmergepatch create strategic merge patch basic" []: [ + nothing -> nothing +] { + let target: record = { + kind: "Deployment" + name: "podinfo" + } + let patch: record = { + apiVersion: "apps/v1" + kind: "Deployment" + metadata: { + name: "not-used" + } + spec: { + template: { + metadata: { + annotations: { + "cluster-autoscaler.kubernetes.io/safe-to-evict": "true" + } + } + } + } + } + + let actual: record = create $target $patch + let expected: record = { + target: $target, + patch: ($patch | to yaml) + } + + assert equal $actual.target $expected.target + assert equal $actual.patch $expected.patch +} + + +export def "test strategicmergepatch create strategic merge patch with dollar-patch directives" []: [ + nothing -> nothing +] { + let target: record = { + kind: "Deployment" + name: "podinfo" + } + let patch: record = { + apiVersion: "apps/v1" + kind: "Deployment" + metadata: { + name: "not-used" + } + spec: { + template: { + metadata: { + annotations: { + "cluster-autoscaler.kubernetes.io/safe-to-evict": "true" + } + } + } + } + "\$patch": "replace" + } + + let actual: record = create $target $patch + let expected: record = { + target: $target, + patch: ($patch | to yaml) + } + + assert equal $actual.target $expected.target + assert equal $actual.patch $expected.patch +} + + +# export def "test strategicmergepatch create strategic merge patch invalid target" []: [ +# nothing -> nothing +# ] { +# let target: record = {"Invalid target": "Invalid value"} +# let patch: record = { +# apiVersion: "apps/v1" +# kind: "Deployment" +# metadata: { +# name: "not-used" +# } +# spec: { +# template: { +# metadata: { +# annotations: { +# "cluster-autoscaler.kubernetes.io/safe-to-evict": "true" +# } +# } +# } +# } +# } + +# let error_occurred: error = try { +# create $target $patch +# } catch { +# |err| $err.msg +# } + +# assert equal $error_occurred "Expected a record" +# } + + +# export def "test strategicmergepatch create strategic merge patch invalid patch" []: [ +# nothing -> nothing +# ] { +# let target: record = { +# kind: "Deployment" +# name: "podinfo" +# } +# let patch: record = {"Invalid patch": "Invalid value"} + +# let error_occurred: error = try { +# create $target $patch +# } catch { +# |err| $err.msg +# } + +# assert equal $error_occurred "Expected a record" +# } diff --git a/docker/osm-nushell-krm-functions/operations/app.nu b/docker/osm-nushell-krm-functions/operations/app.nu new file mode 100644 index 00000000..09a30856 --- /dev/null +++ b/docker/osm-nushell-krm-functions/operations/app.nu @@ -0,0 +1,211 @@ +####################################################################################### +# Copyright ETSI Contributors and Others. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +####################################################################################### + +# Module with custom functions to manage an App instance, invoking the corresponding KSU renderizations to appropriate target folders in a given profile. + + +# Import required modules +use ../krm * +# use ./replace.nu * +# use ./ksu.nu * +use ./replace.nu +use ./ksu.nu + + +# Create an instance of an App, based on an App instance model received from stdin. +export def create [ + --dry-run # If set, only prints the generated ResourceList(s) along with the target folder(s) (i.e., it does not write to any folder). + --print-target-folders # If set, print the target folder(s). Requires --dry-run. + environment: record # Record with environment variables to load. +]: [ + record -> nothing + record -> table +] { + # TODO: Format checks + + # Save the original app instance record + let in_instance: record = $in + + # Remove from the environment those keys that are reserved, dynamic or forbidden, since they will be overriden or may cause known issues, and add one that mimics the KSU name + const forbidden_keys: list = [ + $.KSU_NAME + $.PATTERN_NAME + $.BRICK_NAME + # Add new reserved keys here as needed: + # . . . + ] + let updated_environment: record = ( + $environment + | reject -i ...$forbidden_keys + ) + + # Load environment variables and update the record + let instance_rendered: record = ( + $in_instance + | replace vars $updated_environment + ) + + # Get the key parts + let app_name: string = ($instance_rendered | get $.metadata.name | str downcase) + let spec: record = ($instance_rendered | get spec) + let ksus: list = ($spec | get ksus) + + # Process all App's KSUs + $ksus | each {|k| + $k + | ksu create --dry-run=$dry_run --print-target-folder=$print_target_folders $updated_environment + } + # Make sure it only returns a value when its's not an empty list + | if ($in | is-not-empty) { $in } else { $in | ignore } +} + + +# Delete an instance of an App, based on an App instance model received from stdin. +export def delete [ + --dry-run # If set, only prints the ResourceList(s) with the resources that would be removed. + --print-target-folders # If set, print the target folder(s) to be removed. Requires --dry-run. + environment: record # Record with environment variables to load. +]: [ + record -> nothing + record -> table +] { + # Save the original app instance record + let in_instance: record = $in + + # Remove from the environment those keys that are reserved, dynamic or forbidden, since they will be overriden or may cause known issues, and add one that mimics the KSU name + const forbidden_keys: list = [ + $.KSU_NAME + $.PATTERN_NAME + $.BRICK_NAME + # Add new reserved keys here as needed: + # . . . + ] + let updated_environment: record = ( + $environment + | reject -i ...$forbidden_keys + ) + + # Load environment variables and update the record + let instance_rendered: record = ( + $in_instance + | replace vars $updated_environment + ) + + # Get the key parts + let app_name: string = ($instance_rendered | get $.metadata.name | str downcase) + let spec: record = ($instance_rendered | get spec) + let ksus: list = ($spec | get ksus) + + # Process all App's KSUs + $ksus | each {|k| + $k + | ksu delete --dry-run=$dry_run --print-target-folder=$print_target_folders $updated_environment + } + # Make sure it only returns a value when its's not an empty list + | if ($in | is-not-empty) { $in } else { $in | ignore } +} + + +# Update an instance of an App, based on an App instance model received from stdin. +export def "update existing" [ + --dry-run # If set, only prints the ResourceList(s) with the resources that would be removed. + --print-target-folders # If set, print the target folder(s) to be updated. Requires --dry-run. + --diff-files # If set, returns the list of files expected to change in the target folder(s). Requires --dry-run. + --diffs # If set, returns the expected full diff expected to change in the target folder(s). Requires --dry-run. It can be combined with `--diff-files` + environment: record # Record with environment variables to load. +]: [ + record -> nothing + record -> table + record -> string +] { + # Save the original app instance record + let in_instance: record = $in + + # Remove from the environment those keys that are reserved, dynamic or forbidden, since they will be overriden or may cause known issues, and add one that mimics the KSU name + const forbidden_keys: list = [ + $.KSU_NAME + $.PATTERN_NAME + $.BRICK_NAME + # Add new reserved keys here as needed: + # . . . + ] + let updated_environment: record = ( + $environment + | reject -i ...$forbidden_keys + ) + + # Load environment variables and update the record + let instance_rendered: record = ( + $in_instance + | replace vars $updated_environment + # Overwrite the ksu section with its original values, since we do not want to replace the placeholders yet + | upsert $.spec.ksus ($in_instance | get $.spec.ksus) + ) + + # Get the key parts + let app_name: string = ($instance_rendered | get $.metadata.name | str downcase) + let spec: record = ($instance_rendered | get spec) + let ksus: list = ($spec | get ksus) + + # Process all App's KSUs + $ksus | each {|k| + $k + | ( + ksu update + --print-target-folder=$print_target_folders + --dry-run=$dry_run + --diff-files=$diff_files + --diff=$diffs + $updated_environment + ) + } + # Make sure it only returns a value when it is not an empty list + | if ($in | is-not-empty) { + let output: any = $in + + # If the output is a list of strings, it better provides their concatenation + let output_type: string = ($output | describe) + if ($output_type == "list") { + $output | str join "\n" + # Otherwise, it returns the value as it is + } else { + $output + } + } else { $in | ignore } +} + +export alias update = update existing + + +# Get the Kustomizations that would be created on an instance of an App, based on an App instance model received from stdin. +export def "get kustomization" [ + environment: record # Record with environment variables to load. +]: [ + record -> record +] { + create --dry-run $environment + | get $.items | default [] + | flatten + | where apiVersion == 'kustomize.toolkit.fluxcd.io/v1' + | where kind == 'Kustomization' + | get $.metadata + | select name namespace + | default 'flux-system' namespace +} + +export alias "get kustomizations" = get kustomization +export alias "get ks" = get kustomization diff --git a/docker/osm-nushell-krm-functions/operations/brick.nu b/docker/osm-nushell-krm-functions/operations/brick.nu new file mode 100644 index 00000000..f0c7f646 --- /dev/null +++ b/docker/osm-nushell-krm-functions/operations/brick.nu @@ -0,0 +1,398 @@ +####################################################################################### +# Copyright ETSI Contributors and Others. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +####################################################################################### + +# Module with custom functions to manage the transformations and generations associated to the different types of Building Blocks supported by OSM. +# +# Supported Brick types are: +# +# - `basic`. Basic transformation of the ResourceList. It performs a cleanup and regularization of the target Kustomization (enforce the right path in the repo, ensure that `wait` is enabled, etc.), unless specified otherwise. In addition, it also supports the commonest transformations for a Kustomization, such as addition of optional components, extra labels and/or annotations, hot replacement of image names and tags, etc. For more details, check out the help for the `brick transform basic` command. +# - `helmreleaseset`. Transformations for a ResourceList with a set of HelmReleases, so that values injected into the specific HelmReleases. It is a superset of the `basic` Brick, and its transformations are applied right after the corresponding basic transformations. For more details, check out the help for the `brick transform helmreleaseset` command. +# - `custom`. Transformation of the ResourceList with a custom (user-provided) "create" transformation after a `basic` regularization is applied. For more details, check out the help for the `custom create` command. +# - `custom-hr`. Transformation of the ResourceList with a custom (user-provided) "create" transformation after a `helmreleaseset` transformation (including `basic` regularizations) is applied. For more details, check out the help for the `custom create` command. +# - `custom-full`. Transformation of the ResourceList with a custom (user-provided) "create" transformation. **No `basic` regularization is applied**, so any regularization (if needed) should be implemented in the custom command. For more details, check out the help for the `custom create` command. + + +use ../krm * +use ./location.nu +use custom + + +# Apply the `basic` transformation to ResourceList received from stdin according to the specification of the Brick. +# The `basic` Brick transformation just does a cleanup and regularization of the target Kustomization +export def "transform basic" [ + brick: record # Brick specification +]: [ + record -> record +] { + let rl: record = $in + + # Get the key parts + let brick_name: string = ($brick | get -i name | default "untitled-brick") + let kustomization_name: string = ($brick | get $.kustomization.name) + let kustomization_namespace: string = ($brick | get -i $.kustomization.namespace | default "flux-system") + let src: string = ($brick | get source | location from base path) + let options: record = ($brick | get -i options | default {}) + ## Should it avoid path regularization? + let keep_path: bool = ($options | get -i keep-path | default false) + ## Should it avoid enforcing the wait? + let enforce_wait: bool = ($options | get -i enforce-wait | default true) + ## Should it avoid enforcing the prune? + let enforce_prune: bool = ($options | get -i enforce-prune | default true) + ## Should it enable (or append) some `components`? + let components: list = ($options | get -i components | default []) + ## Should it set or overwrite `targetNamespace`? + let targetNamespace: string = ($options | get -i targetNamespace | default "") + ## Should it overwrite `interval`? + let interval: string = ($options | get -i interval | default "") + ## Should it set or overwrite `retryInterval`? + let retryInterval: string = ($options | get -i retryInterval | default "") + ## Should it set or overwrite `serviceAccountName`? + let serviceAccountName: string = ($options | get -i serviceAccountName | default "") + ## Should it add custom `healthChecks`? + let healthChecks: list = ($options | get -i healthChecks | default []) + ## Should it add custom `healthCheckExprs`? + let healthCheckExprs: list = ($options | get -i healthCheckExprs | default []) + ## Should it set or overwrite a `namePrefix`? + let namePrefix: string = ($options | get -i namePrefix | default "") + ## Should it set or overwrite a `nameSuffix`? + let nameSuffix: string = ($options | get -i nameSuffix | default "") + ## Should it append additional `.metadata.labels` and `.spec.commonMetadata.labels`? + let new_labels: record = ($options | get -i new_labels | default {}) + ## Should it append additional `.metadata.annotations` and `.spec.commonMetadata.annotations`? + let new_annotations: record = ($options | get -i new_annotations | default {}) + ## Should it append additional `.spec.images` replacements? + let images: list = ($options | get -i images | default []) + + # Transform as per the basic Brick model + $rl + # Path regularization, if applicable + | if $keep_path { $in } else { + $in + | ( + patch resource update key + $.spec.path $src + "kustomize.toolkit.fluxcd.io/v1" Kustomization $kustomization_name $kustomization_namespace + ) + } + # Enforce the wait, if applicable + | if $enforce_wait { + $in + | ( + patch resource upsert key + $.spec.wait $enforce_wait + "kustomize.toolkit.fluxcd.io/v1" Kustomization $kustomization_name $kustomization_namespace + ) + } else { $in } + # Enforce the prune, if applicable + | if $enforce_prune { + $in + | ( + patch resource upsert key + $.spec.prune $enforce_prune + "kustomize.toolkit.fluxcd.io/v1" Kustomization $kustomization_name $kustomization_namespace + ) + } else { $in } + # Enable (or append) some `components`, if applicable + | if ($components | is-not-empty) { + let tmp_rl: record = $in + let existing_components: list = ( + $tmp_rl + | ( + patch resource keep + "kustomize.toolkit.fluxcd.io/v1" Kustomization $kustomization_name $kustomization_namespace + ) + | get -i $.items.0.spec.components + | default [] + ) + let all_components: list = ($existing_components ++ $components) | uniq + + $tmp_rl + | ( + patch resource upsert key + $.spec.components $all_components + "kustomize.toolkit.fluxcd.io/v1" Kustomization $kustomization_name $kustomization_namespace + ) + } else { $in } + # Set or overwrite `targetNamespace`, if applicable + | if ($targetNamespace | is-not-empty) { + $in + | ( + patch resource upsert key + $.spec.targetNamespace $targetNamespace + "kustomize.toolkit.fluxcd.io/v1" Kustomization $kustomization_name $kustomization_namespace + ) + } else { $in } + # Overwrite `interval`, if applicable + | if ($interval | is-not-empty) { + $in + | ( + patch resource update key + $.spec.interval $interval + "kustomize.toolkit.fluxcd.io/v1" Kustomization $kustomization_name $kustomization_namespace + ) + } else { $in } + # Set or overwrite `retryInterval`, if applicable + | if ($retryInterval | is-not-empty) { + $in + | ( + patch resource upsert key + $.spec.retryInterval $retryInterval + "kustomize.toolkit.fluxcd.io/v1" Kustomization $kustomization_name $kustomization_namespace + ) + } else { $in } + # Set or overwrite `serviceAccountName`, if applicable + | if ($serviceAccountName | is-not-empty) { + $in + | ( + patch resource upsert key + $.spec.serviceAccountName $serviceAccountName + "kustomize.toolkit.fluxcd.io/v1" Kustomization $kustomization_name $kustomization_namespace + ) + } else { $in } + # Enable (or append) some `healthChecks`, if applicable + | if ($healthChecks | is-not-empty) { + let tmp_rl: record = $in + let existing_healthChecks: list = ($tmp_rl | get -i $.spec.healthChecks | default []) + let all_healthChecks: list = ($existing_healthChecks ++ $healthChecks) | uniq + + $tmp_rl + | ( + patch resource upsert key + $.spec.healthChecks $all_healthChecks + "kustomize.toolkit.fluxcd.io/v1" Kustomization $kustomization_name $kustomization_namespace + ) + } else { $in } + # Enable (or append) some `healthCheckExprs`, if applicable + | if ($healthCheckExprs | is-not-empty) { + let tmp_rl: record = $in + let existing_healthCheckExprs: list = ($tmp_rl | get -i $.spec.healthCheckExprs | default []) + let all_healthCheckExprs: list = ($existing_healthCheckExprs ++ $healthCheckExprs) | uniq + + $tmp_rl + | ( + patch resource upsert key + $.spec.healthCheckExprs $all_healthCheckExprs + "kustomize.toolkit.fluxcd.io/v1" Kustomization $kustomization_name $kustomization_namespace + ) + } else { $in } + # Set or overwrite `namePrefix`, if applicable + | if ($namePrefix | is-not-empty) { + $in + | ( + patch resource upsert key + $.spec.namePrefix $namePrefix + "kustomize.toolkit.fluxcd.io/v1" Kustomization $kustomization_name $kustomization_namespace + ) + } else { $in } + # Set or overwrite `nameSuffix`, if applicable + | if ($nameSuffix | is-not-empty) { + $in + | ( + patch resource upsert key + $.spec.nameSuffix $nameSuffix + "kustomize.toolkit.fluxcd.io/v1" Kustomization $kustomization_name $kustomization_namespace + ) + } else { $in } + # Enable (or append) some `.metadata.labels` and `.spec.commonMetadata.labels`, if applicable + | if ($new_labels | is-not-empty) { + let tmp_rl: record = $in + let existing_labels: list = ($tmp_rl | get -i $.metadata.labels | default []) + let existing_common_labels: list = ($tmp_rl | get -i $.spec.commonMetadata.labels | default []) + let all_labels: list = ($existing_labels | merge $new_labels) + let all_common_labels: list = ($existing_common_labels | merge $new_labels) + + $tmp_rl + | ( + patch resource upsert key + $.metadata.labels + $all_labels + "kustomize.toolkit.fluxcd.io/v1" Kustomization $kustomization_name $kustomization_namespace + ) + | ( + patch resource upsert key + $.spec.commonMetadata.labels + $all_common_labels + "kustomize.toolkit.fluxcd.io/v1" Kustomization $kustomization_name $kustomization_namespace + ) + } else { $in } + # Enable (or append) some `.metadata.annotations` and `.spec.commonMetadata.annotations`, if applicable + | if ($new_annotations | is-not-empty) { + let tmp_rl: record = $in + let existing_annotations: list = ($tmp_rl | get -i $.metadata.annotations | default []) + let existing_common_annotations: list = ($tmp_rl | get -i $.spec.commonMetadata.annotations | default []) + let all_annotations: list = ($existing_annotations | merge $new_annotations) + let all_common_annotations: list = ($existing_common_annotations | merge $new_annotations) + + $tmp_rl + | ( + patch resource upsert key + $.metadata.annotations + $all_annotations + "kustomize.toolkit.fluxcd.io/v1" Kustomization $kustomization_name $kustomization_namespace + ) + | ( + patch resource upsert key + $.spec.commonMetadata.annotations + $all_common_annotations + "kustomize.toolkit.fluxcd.io/v1" Kustomization $kustomization_name $kustomization_namespace + ) + } else { $in } + # Enable (or append) some `.spec.images` replacements, if applicable + | if ($images | is-not-empty) { + let tmp_rl: record = $in + let existing_images: list = ($tmp_rl | get -i $.spec.images | default []) + let all_images: list = ($existing_images ++ $images) | uniq + + $tmp_rl + | ( + patch resource upsert key + $.spec.images $all_images + "kustomize.toolkit.fluxcd.io/v1" Kustomization $kustomization_name $kustomization_namespace + ) + } else { $in } +} + + +# Apply the `helmreleaseset` transformation to ResourceList received from stdin according to the specification of the Brick. +# The `basic` Brick transformation just does a cleanup and regularization of the target Kustomization +export def "transform helmreleaseset" [ + brick: record # Brick specification +]: [ + record -> record +] { + # Input ReleaseList after basic transformations + let rl: record = $in + + # Get the key parts + let brick_name: string = ($brick | get -i name | default "untitled-brick") + let kustomization_name: string = ($brick | get $.kustomization.name) + let kustomization_namespace: string = ($brick | get -i $.kustomization.namespace | default "flux-system") + let hrset_values: list = ($brick | get "hrset-values" | default []) + let public_age_key: string = ($brick | get -i $.public-age-key | default "") + + # Apply HelmRelease-specific transformations + $hrset_values + | reduce --fold $rl {|elt, acc| + $acc + | ( + overlaypatch helmrelease set values + # --ks-namespace: string + --ks-namespace $kustomization_namespace + # --hr-namespace: string + --hr-namespace ($elt | get $.HelmRelease.namespace) + # --operation: string = "add" + # --cm-key: string = "values.yaml" + --cm-key ($elt | get -i $.valuesFrom.configMapKeyRef.key | default "values.yaml") + # --cm-target-path: string + # --cm-optional + # --create-cm-with-values: record + --create-cm-with-values ($elt | get -i "create-cm" | default {}) + # --secret-key: string = "values.yaml" + --secret-key ($elt | get -i $.valuesFrom.secretKeyRef.key | default "values.yaml") + # --secret-target-path: string + # --secret-optional + # --create-secret-with-values: record + --create-secret-with-values ( + $env + | get -i ($elt | get -i $.create-secret.env-values-reference | default "") + | default {} + ) + # --public-age-key: string + --public-age-key $public_age_key + # kustomization_name: string + $kustomization_name + # helmrelease_name: string + ($elt | get $.HelmRelease.name) + # inline_values?: record + ($elt | get -i "inline-values" | default {}) + # cm_name?: string + ($elt | get -i $.valuesFrom.configMapKeyRef.name | default "") + # secret_name?: string + ($elt | get -i $.valuesFrom.secretKeyRef.name | default "") + ) + } +} + + +# Transform the ResourceList received from stdin according to the specification of a Brick transformation. +# +export def transform [ + brick: record # Brick specification + environment: record = {} # Record with environment variables to load +]: [ + record -> record +] { + # Get input ResourceList + let rl: record = $in + + # Get the brick name + let brick_name: string = ($brick | get -i name | default "untitled-brick") + + # Update the environment to include the brick name + let updated_environment: record = ( + $environment + | upsert $.BRICK_NAME $brick_name + ) + + # Update the brick record accordingly + let updated_brick: record = ( + $brick + | replace vars $updated_environment + ) + + # Get other key parts + let brick_type: string = ($updated_brick | get -i type | default "basic" | str downcase) + + # Apply transformation according to the brick type + with-env $updated_environment { + match $brick_type { + "basic" => { + # Basic transformation of the ResourceList (just cleanup and regularization) + $rl + | transform basic $updated_brick + }, + "helmreleaseset" => { + # Transformation of the ResourceList with a set of HelmReleases + $rl + | transform basic $updated_brick + | transform helmreleaseset $updated_brick + }, + "custom-full" => { + # Transformation of the ResourceList with a custom "create" transformation + $rl + | custom brick create $updated_brick $updated_environment + }, + "custom" => { + # Transformation of the ResourceList with a custom "create" transformation, after a basic cleanup and regularization + $rl + | transform basic $updated_brick + | custom brick create $updated_brick $updated_environment + }, + "custom-hr" => { + # Transformation of the ResourceList with a custom "create" transformation, after a `helmreleaseset` transformation + $rl + | transform basic $updated_brick + | transform helmreleaseset $updated_brick + | custom brick create $updated_brick $updated_environment + }, + _ => { + # Unknown brick type, throw an error + error make { msg: $"Error: Unknown Brick type: ($updated_brick | get type)" } + } + } + } +} diff --git a/docker/osm-nushell-krm-functions/operations/custom/mod.nu b/docker/osm-nushell-krm-functions/operations/custom/mod.nu new file mode 100644 index 00000000..c82b4d36 --- /dev/null +++ b/docker/osm-nushell-krm-functions/operations/custom/mod.nu @@ -0,0 +1,109 @@ +####################################################################################### +# Copyright ETSI Contributors and Others. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +####################################################################################### + +# Placeholder module for supporting custom transformations + +# Import SDK modules +use ../../krm * +use ../location.nu + + +# Placeholder for a custom "create" transformation for a Brick of `custom`, `custom-hr`, or `full-custom` types, to be applied to the ResourceList received from stdin. +# - If the Brick is of `custom` type, a `basic` Brick transformation will be applied right before in the pipeline. +# - If the Brick is of `custom-hr` type, a `helmreleaseset` Brick transformation will be applied right before in the pipeline. +# - If the Brick is of `full-custom` type, only this transformation will be applied to the original ResourceList. +export def "create" [ + brick: record # Brick specification + environment: record = {} # Record with environment variables to load +]: [ + record -> record +] { + let rl: record = $in + + # Get the key parts + let brick_name: string = ($brick | get -i name | default "untitled-brick") + let brick_type: string = ($brick | get -i type | default "basic" | str downcase) + let kustomization_name: string = ($brick | get $.kustomization.name) + let kustomization_namespace: string = ($brick | get -i $.kustomization.namespace | default "flux-system") + + # Here would come your custom transformations over `rl` + # . . . + print $"Here we are applying a custom `create` transformation of '($brick_type)' type." + # The print above is just informative. Please remove in your final custom transformation. + + # Here we should return the result of the custom transformations. + # For the sake of the example, let's return just the original ResouceList with no transformations + $rl +} + + +# Placeholder for a custom "update" transformation for a Brick of `custom`, `custom-hr`, or `full-custom` types, to be applied to the ResourceList received from stdin. +# - If the Brick is of `custom` type, a `basic` Brick transformation will be applied right before in the pipeline. +# - If the Brick is of `custom-hr` type, a `helmreleaseset` Brick transformation will be applied right before in the pipeline. +# - If the Brick is of `full-custom` type, only this transformation will be applied to the original ResourceList. +export def "update" [ + brick: record # Brick specification + environment: record = {} # Record with environment variables to load +]: [ + record -> record +] { + let rl: record = $in + + # Get the key parts + let brick_name: string = ($brick | get -i name | default "untitled-brick") + let brick_type: string = ($brick | get -i type | default "basic" | str downcase) + let kustomization_name: string = ($brick | get $.kustomization.name) + let kustomization_namespace: string = ($brick | get -i $.kustomization.namespace | default "flux-system") + + # Here would come your custom transformations over `rl` + # . . . + print $"Here we are applying a custom `update` transformation of '($brick_type)' type." + # The print above is just informative. Please remove in your final custom transformation. + + # Here we should return the result of the custom transformations. + # For the sake of the example, let's return just the original ResouceList with no transformations + $rl +} + + +# Placeholder for a custom "delete" transformation for a Brick of `custom`, `custom-hr`, or `full-custom` types, to be applied to the ResourceList received from stdin. +# - If the Brick is of `custom` type, a `basic` Brick transformation will be applied right before in the pipeline. +# - If the Brick is of `custom-hr` type, a `helmreleaseset` Brick transformation will be applied right before in the pipeline. +# - If the Brick is of `full-custom` type, only this transformation will be applied to the original ResourceList. +export def "delete" [ + brick: record # Brick specification + environment: record = {} # Record with environment variables to load +]: [ + record -> record +] { + let rl: record = $in + + # Get the key parts + let brick_name: string = ($brick | get -i name | default "untitled-brick") + let brick_type: string = ($brick | get -i type | default "basic" | str downcase) + let kustomization_name: string = ($brick | get $.kustomization.name) + let kustomization_namespace: string = ($brick | get -i $.kustomization.namespace | default "flux-system") + + # Here would come your custom transformations over `rl` + # . . . + print $"Here we are applying a custom `delete` transformation of '($brick_type)' type." + # The print above is just informative. Please remove in your final custom transformation. + + # Here we should return the result of the custom transformations. + # For the sake of the example, let's return just the original ResouceList with no transformations + $rl +} diff --git a/docker/osm-nushell-krm-functions/operations/ksu.nu b/docker/osm-nushell-krm-functions/operations/ksu.nu new file mode 100644 index 00000000..ffcc19f6 --- /dev/null +++ b/docker/osm-nushell-krm-functions/operations/ksu.nu @@ -0,0 +1,233 @@ +####################################################################################### +# Copyright ETSI Contributors and Others. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +####################################################################################### + +# Module with custom functions to manage KSUs and their renderization of the corresponding ResourceList into a given target folder. + + +use ../krm * +use ./location.nu +use ./pattern.nu + + +# Render a KSU, based on a KSU instance model received from stdin. +export def create [ + --dry-run # If set, only prints the generated ResourceList(s) along with the target folder(s) (i.e., it does not write to any folder) + --print-target-folder # If set, prints the target folder. Requires --dry-run + environment: record = {} # Record with environment variables to load +]: [ + record -> record + record -> nothing +] { + # Get KSU structure + let in_ksu: record = $in + + # Get the KSU name + let ksu_name: string = ($in_ksu | get "name") + + # Add to the environment a key with the KSU name, so that it can be replaced + let updated_environment: record = ( + $environment + | upsert $.KSU_NAME $ksu_name + ) + + # Update the KSU record accordingly + let updated_ksu: record = ( + $in_ksu + | replace vars $updated_environment + ) + + # Get the rest of key parts + let sync: bool = ($updated_ksu | get -i "sync" | default false) + let target: string = ( + $updated_ksu + | get "target" + | location to absolute path + ) + let patterns: list = ($updated_ksu | get "patterns") + + # Process all the patterns and create a list of ResourceLists using the updated environment + $patterns + | each {|pat| + $pat + | pattern create $updated_environment + } + # Merge all the ResourceLists + | reduce {|elt, acc| + $acc + | concatenate resourcelists $elt + } + # Render + | if $dry_run { + if $print_target_folder { print $"TARGET FOLDER: ($target)" } + $in + } else { + $in + | convert resourcelist to folder --sync=$sync $target + } +} + + +# Delete a KSU, based on a KSU instance model received from stdin. +export def delete [ + --dry-run # If set, only prints the ResourceList(s) that would be removed (i.e., it does not write to any folder). + --print-target-folder # If set, prints the target folder and the list of files to be delete delete. Requires --dry-run. + environment: record = {} # Record with environment variables to load. +]: [ + record -> record + record -> nothing +] { + # Get KSU structure + let in_ksu: record = $in + + # Get the KSU name + let ksu_name: string = ($in_ksu | get "name") + + # Add to the environment a key with the KSU name, so that it can be replaced + let updated_environment: record = ( + $environment + | upsert $.KSU_NAME $ksu_name + ) + + # Update the KSU record accordingly + let updated_ksu: record = ( + $in_ksu + | replace vars $updated_environment + ) + + # Get the rest of key parts + let target: string = ( + $updated_ksu + | get "target" + | location to absolute path + ) + + # Delete + | if $dry_run { + if $print_target_folder { + print $"TARGET FOLDER: ($target)" + (ls ($"($target)/**/*" | into glob ) | table -e | print) + } + # Returns the ResourceList that would be deleted + {} | convert folder to resourcelist $target + } else { + rm -rf $target + } +} + + +# Update a KSU, based on a KSU instance model received from stdin. +export def update [ + --dry-run # If set, only prints the ResourceList(s) that would be re-generated (i.e., it does not write to any folder). + --print-target-folder # If set, print the target folder(s) to be updated. Requires --dry-run. + --diff-files # If set, lists the expected diff with respect to the existing folder(s). Requires --dry-run. + --diff # If set, prints the expected diff with respect to the existing folder(s). Requires --dry-run. It can be combined with `--diff-files`. + environment: record = {} # Record with environment variables to load. +]: [ + record -> record + record -> string + record -> nothing +] { + # Get KSU structure + let in_ksu: record = $in + + # If it is not a dry-run, we simply need to re-create the KSU and return + if not $dry_run { + ## Note that the raw input variables are used, since the full environment pre-processing already happens in both custom commands + $in_ksu | delete $environment + $in_ksu | create $environment + + return + } + # ... otherwise, all the dry-run calculations will need to be performed + + # Get the KSU name + let ksu_name: string = ($in_ksu | get "name") + + # Calculate the original target folder + let target: string = ( + $in_ksu + | replace vars ($environment | upsert $.KSU_NAME $ksu_name) + | get "target" + | location to absolute path + ) + + # Generate the resource contents of the planned update in a temporary fleet repos base + let tmp_fleet_repos_base: path = (mktemp -t -d) + + let tmp_environment: record = ( + $environment + | upsert $.FLEET_REPOS_BASE $tmp_fleet_repos_base + ) + + let tmp_target: string = ( + $in_ksu + | replace vars ($tmp_environment | upsert $.KSU_NAME $ksu_name) + | get "target" + | location to absolute path + ) + + # Render the desired manifests into a temporary location + $in_ksu | create $tmp_environment + + # If specified, prints the target folder + if $print_target_folder { + print $"TARGET FOLDER: ($target)\n" + } + + # If specified, prints all the differences with respect to the original folder + if ($diff_files or $diff) { + let differences: string = ( + [] + # Add list of different files, if needed + | if $diff_files { + $in + | append ( + ^diff -rqN $target $tmp_target + # Prevent the diff error code, due to potential file differences, ends up breaking the full pipeline + | complete | get stdout + ) + # Add detail of differences, if needed + } else { $in } + | if $diff { + $in + | append ( + ^diff -rN $target $tmp_target + # Prevent the diff error code, due to potential file differences, ends up breaking the full pipeline + | complete | get stdout + ) + } else { $in } + | str join "\n\n" + ) + + # Remove the temporary fleet repos base folder + rm -rf $tmp_fleet_repos_base + + # Return the differences found by diff + $differences + + # Otherwise, just returns the planned ResourceList + } else { + # Converts the planned resources to a ResourceList + let output_rl: record = ( {} | convert folder to resourcelist $tmp_target ) + + # Remove the temporary fleet repos base folder + rm -rf $tmp_fleet_repos_base + + # Finally, returns the calculated ResourceList + $output_rl + } +} diff --git a/docker/osm-nushell-krm-functions/operations/location.nu b/docker/osm-nushell-krm-functions/operations/location.nu new file mode 100644 index 00000000..1f568f3f --- /dev/null +++ b/docker/osm-nushell-krm-functions/operations/location.nu @@ -0,0 +1,173 @@ +####################################################################################### +# Copyright ETSI Contributors and Others. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +####################################################################################### + +# Helper module to manage KSU source or target locations, so that they can be safely translated to well-known paths in the local filesystem. + + +# Helper function to convert a profile type name to the canonical name for that profile type so that it can build a folder path deterministically. +# NOT EXPORTED +def "normalize profile type" [ +]: [ + string -> string +] { + $in + | if $in in ["controller", "infra-controller", "infra-controllers", "infra_controller", "infra_controllers"] { + "infra-controller-profiles" + } else if $in in ["config", "infra-config", "infra-configs", "infra_config", "infra_configs"] { + "infra-config-profiles" + } else if $in in ["managed", "resources", "managed-resources", "managed_resources"] { + "managed-resources" + } else if $in in ["app", "apps", "applications", "cnf", "cnfs", "nf", "nfs"] { + "app-profiles" + } else { + $in + } +} + + +# Helper function to convert an OKA type name to the canonical name for that OKA type so that it can build a folder path deterministically. +# NOT EXPORTED +def "normalize oka type" [ +]: [ + string -> string +] { + $in + | if $in in ["controller", "infra-controller", "infra-controllers", "infra_controller", "infra_controllers"] { + "infra-controllers" + } else if $in in ["config", "infra-config", "infra-configs", "infra_config", "infra_configs"] { + "infra-configs" + } else if $in in ["managed", "resources", "managed-resources", "managed_resources", "cloud-resources", "cloud_resources"] { + "cloud-resources" + } else if $in in ["app", "apps", "applications", "cnf", "cnfs", "nf", "nfs"] { + "apps" + } else { + $in + } +} + + +# Convert a location into its components to determine a path in the local filesystem. +export def "to path components" [ + default_project_name: string = "osm_admin" # Default project name + default_repos_base: string = "/repos" # Base path for the local repo clones +]: [ + record -> list +] { + let in_location: record = $in + + # Absolute path of the local repo clone + let repo: string = ( + $in_location + # Is it a path? + | if ($in | get -i "repo-path" | is-not-empty ) { + # $in_location + $in + | get "repo-path" + | path join + # Maybe it was specified by repo name? + } else if ( + ($in | get -i "repo-name" | is-not-empty ) + ) { + [ + # ($in_location | get -i "repos-base" | default $default_repos_base), + # ($in_location | get "repo-name") + ($in | get -i "repos-base" | default $default_repos_base), + ($in | get "repo-name") + ] + | path join + # Otherwise, throws an error + } else { + error make { msg: $"Error: Invalid location spec. Missing `repo-path` or `repo-name` key. Non conformant: \n($in_location | to yaml)"} + } + # Ensure that the absolute path starts by "/" + | if ($in | str starts-with "/") { + $in + } else { + $"/($in)" + } + ) + + # Get the base path prior to the last item (e.g., profile path or OKA folder) + let base: string = ( + $in_location + # Is it a path? + | if ($in | get -i "base-path" | is-not-empty ) { + $in + | get "base-path" + | path join + # Maybe it is a profile spec? + } else if ( + ($in | get -i "profile-type" | is-not-empty ) and + ($in | get -i "profile-name" | is-not-empty ) + ) { + [ + ($in | get -i "project-name" | default $default_project_name), + ($in | get "profile-type" | normalize profile type), + ($in | get "profile-name") + ] + | path join + # Maybe it is an OKA subfolder spec? + } else if ( + ($in | get -i "oka-type" | is-not-empty ) and + ($in | get -i "oka-name" | is-not-empty ) + ) { + [ + ($in | get "oka-type" | normalize oka type), + ($in | get "oka-name") + ] + | path join + # Otherwise, it is malformed + } else { + error make { msg: $"Error: Invalid location spec. Missing `base-path` or `profile-type`+`profile-name` or `oka-type`+`oka-name` key. Non conformant: \n($in | to yaml)"} + } + ) + + # Check that the final relative path is available + if ($in_location | get -i "relative-path" | is-empty ) { + error make { msg: $"Error: Invalid location spec. Missing `relative-path` key. Non conformant: \n($in_location | to yaml)"} + } + + # Finally, return the path components + [ $repo, $base, ($in_location | get "relative-path" | path join) ] +} + + +# Convert a location to an absolute path in the local filesystem. +export def "to absolute path" [ + default_project_name: string = "osm_admin" # Default project name + default_repos_base: string = "/repos" # Base path for the local repo clones +]: [ + record -> path +] { + $in + | to path components $default_project_name $default_repos_base + | path join +} + + +# Convert a location to a relative path in the local filesystem with respect to the root of the locally cloned repo. +export def "from base path" [ + default_project_name: string = "osm_admin" # Default project name +]: [ + record -> path +] { + $in + | to path components $default_project_name + # Drop the first item (the `repo-path`) + | skip 1 + | path join +} diff --git a/docker/osm-nushell-krm-functions/operations/mod.nu b/docker/osm-nushell-krm-functions/operations/mod.nu new file mode 100644 index 00000000..9be239bb --- /dev/null +++ b/docker/osm-nushell-krm-functions/operations/mod.nu @@ -0,0 +1,30 @@ +####################################################################################### +# Copyright ETSI Contributors and Others. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +####################################################################################### + +# Meta-module of custom commands for common operations managing and modifying App intents in a generalized fashion. +# This meta-module comprises all the modules of with the high-level commands for App Modelling. + + +# Import SDK modules +use ../krm * + +# Import submodules +export module ./app.nu +export module ./ksu.nu +export module ./pattern.nu +export module ./brick.nu +export module ./location.nu diff --git a/docker/osm-nushell-krm-functions/operations/pattern.nu b/docker/osm-nushell-krm-functions/operations/pattern.nu new file mode 100644 index 00000000..d59842e4 --- /dev/null +++ b/docker/osm-nushell-krm-functions/operations/pattern.nu @@ -0,0 +1,83 @@ +####################################################################################### +# Copyright ETSI Contributors and Others. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +####################################################################################### + +# Module with custom functions to manage a Pattern definition, taking into account its corresponding source template and the set of transformations specified for its constituent Bricks. + + +use ../krm * +use ./replace.nu +use ./location.nu +use ./brick.nu + +# Generate a ResourceList based on a Pattern instance model received from stdin. +# +# Initially, the ResourceList will be generated from the templates at the `source` location (replacing environment variables as needed), and then the transformations indicated by the Bricks will be applied. +export def create [ + environment: record = {} # Record with environment variables to load +]: [ + record -> record +] { + let in_pattern: record = $in + + # Get the pattern name and its parameters + let pattern_name: string = ($in_pattern | get "name") + let pattern_params: record = ( + $in_pattern + | get -i "parameters" + | default {} + # If applicable, update placeholder values at the custom environment parameters + | replace vars ( + $environment + | upsert $.PATTERN_NAME $pattern_name + ) + ) + + # Update the environment to include the pattern name + let updated_environment: record = ( + $environment + | upsert $.PATTERN_NAME $pattern_name + | merge $pattern_params + ) + + # Update the pattern record accordingly + let updated_pattern: record = ( + $in_pattern + | replace vars $updated_environment + ) + + # Get other key parts + let src: string = ( + $updated_pattern + | get "source" + | location to absolute path + ) + let bricks: list = ($updated_pattern | get "bricks") + + # Generate ResourceList from source template folder + let rl: record = ( + convert folder to resourcelist $src + | replace vars $updated_environment + ) + + # Apply transformations according to the specified bricks + with-env $updated_environment { + $bricks + | reduce --fold $rl {|elt, acc| + $acc | brick transform $elt $updated_environment + } + } +} diff --git a/docker/osm-nushell-krm-functions/operations/replace.nu b/docker/osm-nushell-krm-functions/operations/replace.nu new file mode 100644 index 00000000..0181c40c --- /dev/null +++ b/docker/osm-nushell-krm-functions/operations/replace.nu @@ -0,0 +1,58 @@ +####################################################################################### +# Copyright ETSI Contributors and Others. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +####################################################################################### + +# Module with helper functions to manage the replacement of placeholder variables by the values of well-known enviroment variables. + + +# Helper function to replace placeholder variables by the content of their homonym environment variables in a record received from stdin. +export def vars [ + environment: record # Record with environment variables to load + defaults: record = { + FLEET_REPOS_BASE: "/repos" + CATALOG_REPOS_BASE: "/repos" + PROJECT_NAME: "osm_admin" + } # Record with default values for the variables to be replaced +]: [ + record -> record +] { + let in_record: record = $in + + # Environment with default values when undefined + let full_environment: record = ( + $defaults + | merge $environment + ) + + let variable_enumeration: string = ( + $full_environment + | columns + | each { |col| + $"\${($col)}" + } + | str join "," + ) + + $in_record + | to yaml + | with-env $full_environment { + $in + | ( + ^envsubst $variable_enumeration + ) + } + | from yaml +} diff --git a/docker/osm-nushell-krm-functions/operations/tests/app.nu b/docker/osm-nushell-krm-functions/operations/tests/app.nu new file mode 100644 index 00000000..ec5cb184 --- /dev/null +++ b/docker/osm-nushell-krm-functions/operations/tests/app.nu @@ -0,0 +1,261 @@ +####################################################################################### +# Copyright ETSI Contributors and Others. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +####################################################################################### + +# Tests of App instance management + +use ../../krm * +use ../app.nu +use ../location.nu +use ../replace.nu + + +# --- all-in-one example (example 1) --- + +export def "test app example one" []: [ + nothing -> nothing +] { + let expected: list = ( + open artifacts/sw-catalogs/apps/example1/expected_result.yaml + ) + + let fleet_repos_base: path = (mktemp -t -d) + + let environment: record = { + FLEET_REPOS_BASE: $fleet_repos_base + CATALOG_REPOS_BASE: ($env.pwd | path join artifacts) + APPNAME: myapp01 + APPNAMESPACE: app-namespace + PROFILE_TYPE: apps + PROFILE_NAME: mycluster01 + secret-values-for-postgres-operator-myapp01: { + POSTGRES_OPERATOR_HOST: postgres-operator-host + POSTGRES_OPERATOR_PORT: 5432 + POSTGRES_OPERATOR_USER: postgres-operator-user + POSTGRES_OPERATOR_PASSWORD: postgres-operator-password + } + secret-values-for-postgres-operator-ui-myapp01: { + POSTGRES_OPERATOR_UI_HOST: postgres-operator-ui-host + POSTGRES_OPERATOR_UI_PORT: 8080 + POSTGRES_OPERATOR_UI_USER: postgres-operator-ui-user + POSTGRES_OPERATOR_UI_PASSWORD: postgres-operator-ui-password + } + } + + let actual: list = ( + open artifacts/sw-catalogs/apps/example1/app-instance-from-model.yaml + | app create --dry-run $environment + ) + + # Overwrites the encrypted part of the secrets in both + let actual_trimmed: list = ( + $actual + # For each KSU's ResourceList + | each {|k| + $k + # Delete sops key from all secrets + | ( patch resource reject key $.sops '' Secret ) + # Replace encrypted value by empty string + | ( patch resource update key $.data."values.yaml" 'ENCRYPTED' '' Secret ) + } + ) + let expected_trimmed: list = ( + $expected + # For each KSU's ResourceList + | each {|k| + $k + # Delete sops key from all secrets + | ( patch resource reject key $.sops '' Secret ) + # Replace encrypted value by empty string + | ( patch resource update key $.data."values.yaml" 'ENCRYPTED' '' Secret ) + } + ) + + # Checks + assert equal $actual_trimmed $expected_trimmed + + # Cleanup + rm -rf $fleet_repos_base +} + + +# --- all-in-one example (example 2) --- + +export def "test app example two" []: [ + nothing -> nothing +] { + let expected: list = ( + open artifacts/sw-catalogs/apps/example2/expected_result.yaml + ) + let fleet_repos_base: path = (mktemp -t -d) + + let environment: record = { + FLEET_REPOS_BASE: $fleet_repos_base + CATALOG_REPOS_BASE: ($env.pwd | path join artifacts) + APPNAME: myapp02 + APPNAMESPACE: app-namespace + PROFILE_TYPE: apps + PROFILE_NAME: mycluster02 + secret-values-for-postgres-operator-myapp02: { + POSTGRES_OPERATOR_HOST: postgres-operator-host + POSTGRES_OPERATOR_PORT: 5432 + POSTGRES_OPERATOR_USER: postgres-operator-user + POSTGRES_OPERATOR_PASSWORD: postgres-operator-password + } + secret-values-for-postgres-operator-ui-myapp01: { + POSTGRES_OPERATOR_UI_HOST: postgres-operator-ui-host + POSTGRES_OPERATOR_UI_PORT: 8080 + POSTGRES_OPERATOR_UI_USER: postgres-operator-ui-user + POSTGRES_OPERATOR_UI_PASSWORD: postgres-operator-ui-password + } + } + + let actual: list = ( + open artifacts/sw-catalogs/apps/example2/app-instance-from-model.yaml + | app create --dry-run $environment + ) + + # Overwrites the encrypted part of the secrets in both + let actual_trimmed: list = ( + $actual + # For each KSU's ResourceList + | each {|k| + $k + # Delete sops key from all secrets + | ( patch resource reject key $.sops '' Secret ) + # Replace encrypted value by empty string + | ( patch resource update key $.data."values.yaml" 'ENCRYPTED' '' Secret ) + } + ) + let expected_trimmed: list = ( + $expected + # For each KSU's ResourceList + | each {|k| + $k + # Delete sops key from all secrets + # | ( patch resource reject key $.sops '' Secret ) + # Replace encrypted value by empty string + # | ( patch resource update key $.data."values.yaml" 'ENCRYPTED' '' Secret ) + } + ) + + # Checks + assert equal $actual_trimmed $expected_trimmed + + # Cleanup + rm -rf $fleet_repos_base +} + + +export def "test app example two written to folder" []: [ + nothing -> nothing +] { + let expected: list = ( + open artifacts/sw-catalogs/apps/example2/expected_result.yaml + ) + let fleet_repos_base: path = (mktemp -t -d) + + let environment: record = { + FLEET_REPOS_BASE: $fleet_repos_base + CATALOG_REPOS_BASE: ($env.pwd | path join artifacts) + APPNAME: myapp02 + APPNAMESPACE: app-namespace + PROFILE_TYPE: apps + PROFILE_NAME: mycluster02 + secret-values-for-postgres-operator-myapp02: { + POSTGRES_OPERATOR_HOST: postgres-operator-host + POSTGRES_OPERATOR_PORT: 5432 + POSTGRES_OPERATOR_USER: postgres-operator-user + POSTGRES_OPERATOR_PASSWORD: postgres-operator-password + } + secret-values-for-postgres-operator-ui-myapp01: { + POSTGRES_OPERATOR_UI_HOST: postgres-operator-ui-host + POSTGRES_OPERATOR_UI_PORT: 8080 + POSTGRES_OPERATOR_UI_USER: postgres-operator-ui-user + POSTGRES_OPERATOR_UI_PASSWORD: postgres-operator-ui-password + } + } + + # Retrieve instance model + let instance_model: record = (open artifacts/sw-catalogs/apps/example2/app-instance-from-model.yaml) + + # Write to folder + $instance_model | app create $environment + + # Calculate the actual ResourceLists from the target folders + let targets: list = ( + $instance_model + | replace vars $environment + | get $.spec.ksus + | each {|k| + $k + | get "target" + | location to absolute path + } + ) + let actual: list = ( + $targets + | each {|t| + {} | convert folder to resourcelist $t + } + ) + + # Overwrites the encrypted part of the secrets in both + let actual_trimmed: list = ( + $actual + # For each KSU's ResourceList + | each {|k| + $k + # Delete sops key from all secrets + | ( patch resource reject key $.sops '' Secret ) + # Replace encrypted value by empty string + | ( patch resource update key $.data."values.yaml" 'ENCRYPTED' '' Secret ) + } + ) + let expected_trimmed: list = ( + $expected + # For each KSU's ResourceList + | each {|k| + $k + # Delete sops key from all secrets + # | ( patch resource reject key $.sops '' Secret ) + # Replace encrypted value by empty string + # | ( patch resource update key $.data."values.yaml" 'ENCRYPTED' '' Secret ) + } + ) + + # Ensures that the items from both ResourceLists are sorted in the same order and removes irrelevant indexes and keys from `kpt` + let actual_fixed: list = ($actual_trimmed | get $.items.0 | sort-by $.kind | sort-by $.metadata.name + | each {|k| + $k + | reject -i $.metadata.annotations."config.kubernetes.io/index" + | reject -i $.metadata.annotations."internal.config.kubernetes.io/index" + | reject -i $.metadata.annotations."internal.config.kubernetes.io/seqindent" + }) + let expected_fixed = ($expected_trimmed | get $.items.0 | sort-by $.kind | sort-by $.metadata.name + | each {|k| + $k + | reject -i $.metadata.annotations."config.kubernetes.io/index" + | reject -i $.metadata.annotations."internal.config.kubernetes.io/index" + | reject -i $.metadata.annotations."internal.config.kubernetes.io/seqindent" + }) + + # Checks + assert equal $actual_fixed $expected_fixed + + # Cleanup + rm -rf $fleet_repos_base +} diff --git a/docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example1/app-instance-from-model.yaml b/docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example1/app-instance-from-model.yaml new file mode 100644 index 00000000..dcd06ac7 --- /dev/null +++ b/docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example1/app-instance-from-model.yaml @@ -0,0 +1,169 @@ +####################################################################################### +# Copyright ETSI Contributors and Others. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +####################################################################################### + +apiVersion: osm.softwaredefinition.io/v1alpha1 +# kind: AppBlueprint +kind: AppInstantiation +metadata: + name: example1-app +spec: + # description: "Example App Blueprint" + description: "Example App Instantiation" + version: "1.0.0" + ksus: + - name: main + target: + # Absolute path to KSU folder: repo-path + base-path + relative-path + repo-path: + - ${FLEET_REPOS_BASE} # Default: `/repos` + - fleet # Repo name. + base-path: + - ${PROJECT_NAME} # Project name. + - ${PROFILE_TYPE} + - ${PROFILE_NAME} + relative-path: # App folder + ksu folder (in case of multiple KSUs) + - ${APPNAME} + - main + patterns: + - name: main-pattern + source: + # Absolute path to OKA folder: repo-path + base-path + relative-path + repo-path: + - ${CATALOG_REPOS_BASE} # Default: `/repos` + - sw-catalogs # Repo name + base-path: # Absolute path to OKA folder + - apps # OKA type + - example1 # OKA folder + relative-path: # Pattern template folder (default: `templates/`) + - templates + - main-pattern + bricks: + - name: main-brick + type: basic + # type: HelmReleaseSet + kustomization: + name: main-kustomization-${APPNAME} + # OPTIONAL: + # namespace: flux-system + source: # Absolute path to manifests referenced by the Kustomization (brick folder): base-path + relative-path + repo-path: + - ${CATALOG_REPOS_BASE} # Default: `/repos` + - sw-catalogs # Repo name + base-path: # Absolute path to OKA folder + - apps # OKA type + - example1 # OKA folder + relative-path: + - manifests + - main-pattern + - main-brick-manifests + - name: database-brick + type: HelmReleaseSet + kustomization: + name: database-kustomization-${APPNAME} + # OPTIONAL: + # namespace: flux-system + source: + repo-path: + - ${CATALOG_REPOS_BASE} # Default: `/repos` + - sw-catalogs # Repo name + base-path: # Absolute path to OKA folder + - apps # OKA type + - example1 # OKA folder + relative-path: + - manifests + - main-pattern + - database-manifests + public-age-key: age18juyaw9kvzgkpqx8kun2n7qtvqva6ajj7ulse435thkgnlfjhf2qj76h64 + hrset-values: + - name: db-operator + HelmRelease: + name: postgres-operator-${APPNAME} + namespace: ${APPNAMESPACE} + inline-values: + key1: value1 + key2: value2 + valuesFrom: + configMapKeyRef: + name: postgres-operator-cm-${APPNAME} + # OPTIONAL: + # key: values.yaml + secretKeyRef: + name: postgres-operator-secret-${APPNAME} + # OPTIONAL: + # key: values.yaml + create-cm: # If the map is empty, the ConfigMap will not be created + cm-key1: cm-value1 + cm-key2: cm-value2 + create-secret: # If the map is empty, the Secret will not be created + env-values-reference: secret-values-for-postgres-operator-${APPNAME} + - name: db-op-user-interface + HelmRelease: + name: postgres-operator-ui-${APPNAME} + namespace: ${APPNAMESPACE} + inline-values: + key1: value1 + key2: value2 + valuesFrom: + configMapKeyRef: + name: postgres-operator-ui-cm-${APPNAME} + # OPTIONAL: + # key: values.yaml + secretKeyRef: + name: postgres-operator-ui-secret-${APPNAME} + # OPTIONAL: + # key: values.yaml + create-cm: # If the map is empty, the ConfigMap will not be created + cm-key1: cm-value1 + cm-key2: cm-value2 + create-secret: # If the map is empty, the Secret will not be created + env-values-reference: secret-values-for-postgres-operator-ui-${APPNAME} + # parameters: + # - name: replicaCount + # description: "Number of replicas" + # # default: 1 + # value: 1 + # - name: ingressHost + # description: "Ingress hostname" + # value: "ingress for-${APPNAME}" + # readinessChecks: + # - resource: + # kind: Deployment + # name: main-deployment-${APPNAME} + # condition: + # type: Available + # status: "True" + # - resource: + # kind: Service + # name: main-service + # condition: + # type: Ready + # TODO: + # outputValues: + # - name: apiEndpoint + # valueFrom: + # serviceIP: + # name: main-service + # - name: adminPassword + # valueFrom: + # secretKeyRef: + # name: app-secrets + # key: admin-password + # TODO: + # dependencies: + # - name: nginx-ingress + # type: InfraController + # version: ">=1.0.0" diff --git a/docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example1/expected_result.yaml b/docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example1/expected_result.yaml new file mode 100644 index 00000000..ae6fa904 --- /dev/null +++ b/docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example1/expected_result.yaml @@ -0,0 +1,218 @@ +####################################################################################### +# Copyright ETSI Contributors and Others. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +####################################################################################### + +- apiVersion: config.kubernetes.io/v1 + kind: ResourceList + items: + - apiVersion: kustomize.toolkit.fluxcd.io/v1 + kind: Kustomization + metadata: + name: database-kustomization-myapp01 + namespace: flux-system + annotations: + config.kubernetes.io/index: '0' + config.kubernetes.io/path: ks-database.yaml + internal.config.kubernetes.io/index: '0' + internal.config.kubernetes.io/path: ks-database.yaml + internal.config.kubernetes.io/seqindent: compact + spec: + interval: 1h0m0s + path: apps/example1/manifests/main-pattern/database-manifests + prune: true + sourceRef: + kind: GitRepository + name: sw-catalogs + namespace: flux-system + targetNamespace: app-namespace + postBuild: + substitute: + appname: myapp01 + appnamespace: app-namespace + wait: true + patches: + - target: + kind: HelmRelease + name: postgres-operator-myapp01 + namespace: app-namespace + patch: | + - op: add + path: /spec/values + value: + key1: value1 + key2: value2 + - target: + kind: HelmRelease + name: postgres-operator-myapp01 + namespace: app-namespace + patch: | + - op: add + path: /spec/valuesFrom/- + value: + kind: ConfigMap + name: postgres-operator-cm-myapp01 + key: values.yaml + - target: + kind: HelmRelease + name: postgres-operator-myapp01 + namespace: app-namespace + patch: | + - op: add + path: /spec/valuesFrom/- + value: + kind: Secret + name: postgres-operator-secret-myapp01 + key: values.yaml + - target: + kind: HelmRelease + name: postgres-operator-ui-myapp01 + namespace: app-namespace + patch: | + - op: add + path: /spec/values + value: + key1: value1 + key2: value2 + - target: + kind: HelmRelease + name: postgres-operator-ui-myapp01 + namespace: app-namespace + patch: | + - op: add + path: /spec/valuesFrom/- + value: + kind: ConfigMap + name: postgres-operator-ui-cm-myapp01 + key: values.yaml + - target: + kind: HelmRelease + name: postgres-operator-ui-myapp01 + namespace: app-namespace + patch: | + - op: add + path: /spec/valuesFrom/- + value: + kind: Secret + name: postgres-operator-ui-secret-myapp01 + key: values.yaml + - apiVersion: kustomize.toolkit.fluxcd.io/v1 + kind: Kustomization + metadata: + name: main-kustomization-myapp01 + namespace: flux-system + annotations: + config.kubernetes.io/index: '0' + config.kubernetes.io/path: ks-main.yaml + internal.config.kubernetes.io/index: '0' + internal.config.kubernetes.io/path: ks-main.yaml + internal.config.kubernetes.io/seqindent: compact + spec: + interval: 1h0m0s + path: apps/example1/manifests/main-pattern/main-brick-manifests + prune: true + sourceRef: + kind: GitRepository + name: sw-catalogs + namespace: flux-system + targetNamespace: app-namespace + postBuild: + substitute: + app_name: myapp01 + wait: true + - apiVersion: v1 + kind: ConfigMap + metadata: + name: postgres-operator-cm-myapp01 + namespace: app-namespace + annotations: + config.kubernetes.io/path: postgres-operator-cm-myapp01.yaml + internal.config.kubernetes.io/path: postgres-operator-cm-myapp01.yaml + data: + values.yaml: |- + cm-key1: cm-value1 + cm-key2: cm-value2 + - apiVersion: v1 + kind: Secret + metadata: + name: postgres-operator-secret-myapp01 + namespace: app-namespace + annotations: + config.kubernetes.io/path: postgres-operator-secret-myapp01.yaml + internal.config.kubernetes.io/path: postgres-operator-secret-myapp01.yaml + data: + values.yaml: ENC[AES256_GCM,data:wzm5y3kpbk+ZVM2TsNd6D4rmEZ7FU5jxM3dZiGILjZ7Hy00oS0ua9nwc0WU+IwSz0S4m7RiETYRnWITELpq0xoogCXRVIz7SkOivWE6/lv+H0SgQOqZ0iDHZgE/kly8S61kh1QGAeCCUZcpUyXwjgZ1S/iwZqAlVl2cizkGvYMMdgJAGBqTrXakM6LH10J5JBZX/05Cy9Y1rSzGDl6XsJwpnj0znyYIAGsq97nsjctzVGUX0TTIvE03de+T5ZB/nxOZrpcsUSbrbHBj4J5/uijOzV4NCjZ6OV7PWl64fKlpVcLt8z5geO9RMVBT/WlhV,iv:bGxvvh4umOmM7iRHiu0i6lWjWeD1yOkhPGSiMGDgkq0=,tag:v0Og8/hjsEuPLB1wmXSWSw==,type:str] + sops: + kms: [] + gcp_kms: [] + azure_kv: [] + hc_vault: [] + age: + - recipient: age18juyaw9kvzgkpqx8kun2n7qtvqva6ajj7ulse435thkgnlfjhf2qj76h64 + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBHV3d4R2JzS0VmazdCUXBR + citpNE9uVmdGbm8vNWdCK3dQR2NObFRaQlVFCjJJZGU5b25pRC8zQ0NOT1J1eEcr + bzBzdHE5ZlpBelJaSnpLVGlhcmp3QUkKLS0tIC9Fam5DcTZHQ0pzK3o5N0o2MVJr + c3krQTlvU2FXckxxTUkraDBRMWJwd3cK0sd3dx5Arzh4XxJVUFOi1cPnF9UIsv18 + VXKeyoIOK8pvPS5c4UvCTfBFdYs69Sg6+6FfbCoFJepaT+VfaM1XPA== + -----END AGE ENCRYPTED FILE----- + lastmodified: 2025-04-29T18:26:22Z + mac: ENC[AES256_GCM,data:io3iCpdT61n7ECEkyM2KhBgp9oZ49I2YBjuUA8HQSLyDDzFrNNeZh0bVOWI3HT89egkZDt8nmj8+A4Bx9sC666iBcqcFx+VuH//Db7SZXYJXFm1qNP+lt+3ic4fesJgYKG5ld+7FtB1ApKccmE7Ri0yrWGYsrMJh2aRK6T3VyzQ=,iv:9lvXUEPwIZxphwEc91QCPPSxkVNIiUAZWQWby502FJA=,tag:0IxYm+VLLKOoZ80FQlLspQ==,type:str] + pgp: [] + encrypted_regex: ^(data|stringData)$ + version: 3.8.1 + - apiVersion: v1 + kind: ConfigMap + metadata: + name: postgres-operator-ui-cm-myapp01 + namespace: app-namespace + annotations: + config.kubernetes.io/path: postgres-operator-ui-cm-myapp01.yaml + internal.config.kubernetes.io/path: postgres-operator-ui-cm-myapp01.yaml + data: + values.yaml: |- + cm-key1: cm-value1 + cm-key2: cm-value2 + - apiVersion: v1 + kind: Secret + metadata: + name: postgres-operator-ui-secret-myapp01 + namespace: app-namespace + annotations: + config.kubernetes.io/path: postgres-operator-ui-secret-myapp01.yaml + internal.config.kubernetes.io/path: postgres-operator-ui-secret-myapp01.yaml + data: + values.yaml: ENC[AES256_GCM,data:37zRz8FafCMe/cfZGI9Ojx+DP1Y09C5C4P6E4wkPZJfqb8brliWAzQ14cOHZgeOQBugcuguMLTacGz+QCDvU9u5ZFs9ouXY3pWbs9wa4Ywoaum8T2gK1edzEoQ9U0QG5wqndwti4p1wo8Im6+A/VOpFXcEGXdzSLfxcryTGA5EXBtwG9/SvhTvx6CCZGvWcNtjqjJjwNOOtFyJJFBuc7CrWFgYliSwwZ2q4INJhDTrrQ81zKDzdkW/ac+uapey/XQ3lYK56n7K+Z+hxoBJLYRfsRdtBCXxXpThfYXPu608WDHUcedjitATsk7NwA/F+mqmm1T5FO7ePspSaTZgvhBMi+OP1HyIgKbmiEhg==,iv:dVYZTjkNYAgNpFUvFTIiyEFM51tv45G1yp1NPG/xJKQ=,tag:xcrQZq1swfcjfPjcNgN0JQ==,type:str] + sops: + kms: [] + gcp_kms: [] + azure_kv: [] + hc_vault: [] + age: + - recipient: age18juyaw9kvzgkpqx8kun2n7qtvqva6ajj7ulse435thkgnlfjhf2qj76h64 + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBqUS9vaVRVbk9DTkpreVNU + Y0t6TW9sbk9Ua1hWSUdVa0hrOWVVSkZIbkVnCnpVcUpxdkQweWw1MjhBVldkaXVX + OTRlYW9jQ3ZxNnhiMGExanJYNU9oREkKLS0tIGl3U201ZmNPM2xaY0FyOWZsZGNs + aFBQTU1BK3BTSWZ6djFMRDFkbjkrYnMKX/Uo7ePikgbnOfewdHSmCXE4aMfZU0IQ + jr9ZlHVGPvaS6yy7fezVZOJI++nwF7fyXZfJW9mm0rSRBy5vnKQYNg== + -----END AGE ENCRYPTED FILE----- + lastmodified: 2025-04-29T18:26:22Z + mac: ENC[AES256_GCM,data:h0ZEFSy0jijGOmjKIFWi5GOfGYgSCWg2BNgMFXT2AwwR2j/xcJWBwFvKPzy7X028Onz0aNmnPa7hf4eI8HS2pp4FEeUbTp8Z+X8it01XDbhmXqnykeHRU40CaKtqaNBbT2uKa/jpT5KgZl+2mnLUL9VU1vlu7BFIQrYPPtOTxgI=,iv:cBSpvEZHFGEAVqWyXUF9OtmuwN4DM3nS3ZgJepUknZ0=,tag:W2utUcIePQpqHkZYV4FiFA==,type:str] + pgp: [] + encrypted_regex: ^(data|stringData)$ + version: 3.8.1 diff --git a/docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example1/manifests/main-pattern/database-manifests/hrset-database.yaml b/docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example1/manifests/main-pattern/database-manifests/hrset-database.yaml new file mode 100644 index 00000000..8623232e --- /dev/null +++ b/docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example1/manifests/main-pattern/database-manifests/hrset-database.yaml @@ -0,0 +1,58 @@ +####################################################################################### +# Copyright ETSI Contributors and Others. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +####################################################################################### + +--- +apiVersion: helm.toolkit.fluxcd.io/v2beta1 +kind: HelmRelease +metadata: + name: postgres-operator-${appname} + namespace: ${appnamespace} +spec: + chart: + spec: + chart: postgres-operator + reconcileStrategy: ChartVersion + sourceRef: + kind: HelmRepository + name: postgres-operator-charts-${appname} + namespace: ${appnamespace} + interval: 3m0s + targetNamespace: ${appnamespace} + values: {} + +--- + +apiVersion: helm.toolkit.fluxcd.io/v2beta1 +kind: HelmRelease +metadata: + name: postgres-operator-ui-${appname} + namespace: ${appnamespace} +spec: + dependsOn: + - name: postgres-operator-${appname} + namespace: ${appnamespace} + chart: + spec: + chart: postgres-operator-ui + reconcileStrategy: ChartVersion + sourceRef: + kind: HelmRepository + name: postgres-operator-charts-${appname} + namespace: ${appnamespace} + interval: 3m0s + targetNamespace: ${appnamespace} + values: {} diff --git a/docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example1/manifests/main-pattern/database-manifests/repo-zalando-postgres.yaml b/docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example1/manifests/main-pattern/database-manifests/repo-zalando-postgres.yaml new file mode 100644 index 00000000..eaf2dda4 --- /dev/null +++ b/docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example1/manifests/main-pattern/database-manifests/repo-zalando-postgres.yaml @@ -0,0 +1,29 @@ +####################################################################################### +# Copyright ETSI Contributors and Others. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +####################################################################################### + +--- +apiVersion: source.toolkit.fluxcd.io/v1beta2 +kind: HelmRepository +metadata: + name: postgres-operator-charts-${appname} + namespace: ${appnamespace} +spec: + interval: 10m0s + # type: oci + # url: oci://registry-1.docker.io/bitnamicharts + type: default + url: https://opensource.zalando.com/postgres-operator/charts/postgres-operator diff --git a/docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example1/manifests/main-pattern/main-brick-manifests/configmap.yaml b/docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example1/manifests/main-pattern/main-brick-manifests/configmap.yaml new file mode 100644 index 00000000..5bd5c509 --- /dev/null +++ b/docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example1/manifests/main-pattern/main-brick-manifests/configmap.yaml @@ -0,0 +1,23 @@ +####################################################################################### +# Copyright ETSI Contributors and Others. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +####################################################################################### + +apiVersion: v1 +kind: ConfigMap +metadata: + name: my-cm-${appname} +data: + my-key: my-value diff --git a/docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example1/templates/main-pattern/ks-database.yaml b/docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example1/templates/main-pattern/ks-database.yaml new file mode 100644 index 00000000..af2831c2 --- /dev/null +++ b/docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example1/templates/main-pattern/ks-database.yaml @@ -0,0 +1,36 @@ +####################################################################################### +# Copyright ETSI Contributors and Others. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +####################################################################################### + +--- +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: database-kustomization-${APPNAME} + namespace: flux-system +spec: + interval: 1h0m0s + path: ./apps/example1/manifests/main-pattern/database-manifests + prune: true + sourceRef: + kind: GitRepository + name: sw-catalogs + namespace: flux-system + targetNamespace: ${APPNAMESPACE} + postBuild: + substitute: + appname: ${APPNAME} + appnamespace: ${APPNAMESPACE} diff --git a/docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example1/templates/main-pattern/ks-main.yaml b/docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example1/templates/main-pattern/ks-main.yaml new file mode 100644 index 00000000..d80fe76d --- /dev/null +++ b/docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example1/templates/main-pattern/ks-main.yaml @@ -0,0 +1,35 @@ +####################################################################################### +# Copyright ETSI Contributors and Others. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +####################################################################################### + +--- +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: main-kustomization-${APPNAME} + namespace: flux-system +spec: + interval: 1h0m0s + path: ./apps/example1/manifests/main-pattern/main-brick-manifests + prune: true + sourceRef: + kind: GitRepository + name: sw-catalogs + namespace: flux-system + targetNamespace: ${APPNAMESPACE} + postBuild: + substitute: + app_name: ${APPNAME} diff --git a/docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example2/app-instance-from-model.yaml b/docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example2/app-instance-from-model.yaml new file mode 100644 index 00000000..fe248e4b --- /dev/null +++ b/docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example2/app-instance-from-model.yaml @@ -0,0 +1,270 @@ +####################################################################################### +# Copyright ETSI Contributors and Others. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +####################################################################################### + +apiVersion: osm.softwaredefinition.io/v1alpha1 +# kind: AppBlueprint +kind: AppInstantiation +metadata: + name: example2-app +spec: + # description: "Example App Blueprint" + description: "Example App Instantiation" + version: "1.0.0" + ksus: + - name: main + target: + # Absolute path to KSU folder: repo-path + base-path + relative-path + repo-path: + - ${FLEET_REPOS_BASE} # Default: `/repos` + - fleet # Repo name. + base-path: + - ${PROJECT_NAME} # Project name. + - ${PROFILE_TYPE} + - ${PROFILE_NAME} + relative-path: # App folder + ksu folder (in case of multiple KSUs) + - ${APPNAME} + - main + patterns: + - name: active-pattern + source: + # Absolute path to OKA folder: repo-path + base-path + relative-path + repo-path: + - ${CATALOG_REPOS_BASE} # Default: `/repos` + - sw-catalogs # Repo name + base-path: # Absolute path to OKA folder + - apps # OKA type + - example2 # OKA folder + relative-path: # Pattern template folder (default: `templates/`) + - templates + - main-pattern + # Pattern-specific parameters, to be added as needed (optional) + parameters: + EXAMPLE_PARAMETER1: active-example-value1 + EXAMPLE_PARAMETER2: active-example-value2 + bricks: + - name: main-brick + type: basic + # type: HelmReleaseSet + kustomization: + name: ks-${APPNAME}-${PATTERN_NAME} + # OPTIONAL: + # namespace: flux-system + source: # Absolute path to manifests referenced by the Kustomization (brick folder): base-path + relative-path + repo-path: + - ${CATALOG_REPOS_BASE} # Default: `/repos` + - sw-catalogs # Repo name + base-path: # Absolute path to OKA folder + - apps # OKA type + - example2 # OKA folder + relative-path: + - manifests + - main-pattern + - main-brick-manifests + - name: database-brick + type: HelmReleaseSet + kustomization: + name: database-kustomization-${APPNAME}-${PATTERN_NAME} + # OPTIONAL: + # namespace: flux-system + source: + repo-path: + - ${CATALOG_REPOS_BASE} # Default: `/repos` + - sw-catalogs # Repo name + base-path: # Absolute path to OKA folder + - apps # OKA type + - example2 # OKA folder + relative-path: + - manifests + - main-pattern + - database-manifests + # To be inserted + public-age-key: age18juyaw9kvzgkpqx8kun2n7qtvqva6ajj7ulse435thkgnlfjhf2qj76h64 + hrset-values: + - name: db-operator + HelmRelease: + name: postgres-operator-${APPNAME}-${PATTERN_NAME} + namespace: ${APPNAMESPACE} + inline-values: + key1: value1 + key2: value2 + valuesFrom: + configMapKeyRef: + name: postgres-operator-cm-${APPNAME}-${PATTERN_NAME} + # OPTIONAL: + # key: values.yaml + secretKeyRef: + name: postgres-operator-secret-${APPNAME}-${PATTERN_NAME} + # OPTIONAL: + # key: values.yaml + create-cm: # If the map is empty, the ConfigMap will not be created + cm-key1: cm-value1 + cm-key2: cm-value2 + create-secret: # If the map is empty, the Secret will not be created + env-values-reference: secret-values-for-postgres-operator-${APPNAME} + - name: db-op-user-interface + HelmRelease: + name: postgres-operator-ui-${APPNAME}-${PATTERN_NAME} + namespace: ${APPNAMESPACE} + inline-values: + key1: value1 + key2: value2 + valuesFrom: + configMapKeyRef: + name: postgres-operator-ui-cm-${APPNAME}-${PATTERN_NAME} + # OPTIONAL: + # key: values.yaml + secretKeyRef: + name: postgres-operator-ui-secret-${APPNAME}-${PATTERN_NAME} + # OPTIONAL: + # key: values.yaml + create-cm: # If the map is empty, the ConfigMap will not be created + cm-key1: cm-value1 + cm-key2: cm-value2 + create-secret: # If the map is empty, the Secret will not be created + env-values-reference: secret-values-for-postgres-operator-ui-${APPNAME} + - name: standby-pattern + source: + # Absolute path to OKA folder: repo-path + base-path + relative-path + repo-path: + - ${CATALOG_REPOS_BASE} # Default: `/repos` + - sw-catalogs # Repo name + base-path: # Absolute path to OKA folder + - apps # OKA type + - example2 # OKA folder + relative-path: # Pattern template folder (default: `templates/`) + - templates + - main-pattern + # Pattern-specific parameters, to be added as needed (optional) + parameters: + EXAMPLE_PARAMETER1: standby-example-value1 + EXAMPLE_PARAMETER2: standby-example-value2 + bricks: + - name: main-brick + type: basic + # type: HelmReleaseSet + kustomization: + name: ks-${APPNAME}-${PATTERN_NAME} + # OPTIONAL: + # namespace: flux-system + source: # Absolute path to manifests referenced by the Kustomization (brick folder): base-path + relative-path + repo-path: + - ${CATALOG_REPOS_BASE} # Default: `/repos` + - sw-catalogs # Repo name + base-path: # Absolute path to OKA folder + - apps # OKA type + - example2 # OKA folder + relative-path: + - manifests + - main-pattern + - main-brick-manifests + - name: database-brick + type: HelmReleaseSet + kustomization: + name: database-kustomization-${APPNAME}-${PATTERN_NAME} + # OPTIONAL: + # namespace: flux-system + source: + repo-path: + - ${CATALOG_REPOS_BASE} # Default: `/repos` + - sw-catalogs # Repo name + base-path: # Absolute path to OKA folder + - apps # OKA type + - example2 # OKA folder + relative-path: + - manifests + - main-pattern + - database-manifests + public-age-key: age18juyaw9kvzgkpqx8kun2n7qtvqva6ajj7ulse435thkgnlfjhf2qj76h64 + hrset-values: + - name: db-operator + HelmRelease: + name: postgres-operator-${APPNAME}-${PATTERN_NAME} + namespace: ${APPNAMESPACE} + inline-values: + key1: value1 + key2: value2 + valuesFrom: + configMapKeyRef: + name: postgres-operator-cm-${APPNAME}-${PATTERN_NAME} + # OPTIONAL: + # key: values.yaml + secretKeyRef: + name: postgres-operator-secret-${APPNAME}-${PATTERN_NAME} + # OPTIONAL: + # key: values.yaml + create-cm: # If the map is empty, the ConfigMap will not be created + cm-key1: cm-value1 + cm-key2: cm-value2 + create-secret: # If the map is empty, the Secret will not be created + env-values-reference: secret-values-for-postgres-operator-${APPNAME} + - name: db-op-user-interface + HelmRelease: + name: postgres-operator-ui-${APPNAME}-${PATTERN_NAME} + namespace: ${APPNAMESPACE} + inline-values: + key1: value1 + key2: value2 + valuesFrom: + configMapKeyRef: + name: postgres-operator-ui-cm-${APPNAME}-${PATTERN_NAME} + # OPTIONAL: + # key: values.yaml + secretKeyRef: + name: postgres-operator-ui-secret-${APPNAME}-${PATTERN_NAME} + # OPTIONAL: + # key: values.yaml + create-cm: # If the map is empty, the ConfigMap will not be created + cm-key1: cm-value1 + cm-key2: cm-value2 + create-secret: # If the map is empty, the Secret will not be created + env-values-reference: secret-values-for-postgres-operator-ui-${APPNAME} + # parameters: + # - name: replicaCount + # description: "Number of replicas" + # # default: 1 + # value: 1 + # - name: ingressHost + # description: "Ingress hostname" + # value: "ingress for-${APPNAME}" + # readinessChecks: + # - resource: + # kind: Deployment + # name: main-deployment-${APPNAME} + # condition: + # type: Available + # status: "True" + # - resource: + # kind: Service + # name: main-service + # condition: + # type: Ready + # TODO: + # outputValues: + # - name: apiEndpoint + # valueFrom: + # serviceIP: + # name: main-service + # - name: adminPassword + # valueFrom: + # secretKeyRef: + # name: app-secrets + # key: admin-password + # TODO: + # dependencies: + # - name: nginx-ingress + # type: InfraController + # version: ">=1.0.0" diff --git a/docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example2/expected_result.yaml b/docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example2/expected_result.yaml new file mode 100644 index 00000000..464b258b --- /dev/null +++ b/docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example2/expected_result.yaml @@ -0,0 +1,320 @@ +####################################################################################### +# Copyright ETSI Contributors and Others. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +####################################################################################### + +- apiVersion: config.kubernetes.io/v1 + kind: ResourceList + items: + - apiVersion: kustomize.toolkit.fluxcd.io/v1 + kind: Kustomization + metadata: + name: database-kustomization-myapp02-active-pattern + namespace: flux-system + annotations: + config.kubernetes.io/index: '0' + config.kubernetes.io/path: ks-database.yaml + internal.config.kubernetes.io/index: '0' + internal.config.kubernetes.io/path: ks-database.yaml + internal.config.kubernetes.io/seqindent: compact + spec: + interval: 1h0m0s + path: apps/example2/manifests/main-pattern/database-manifests + prune: true + sourceRef: + kind: GitRepository + name: sw-catalogs + namespace: flux-system + targetNamespace: app-namespace + postBuild: + substitute: + appname: myapp02 + appnamespace: app-namespace + wait: true + patches: + - target: + kind: HelmRelease + name: postgres-operator-myapp02-active-pattern + namespace: app-namespace + patch: | + - op: add + path: /spec/values + value: + key1: value1 + key2: value2 + - target: + kind: HelmRelease + name: postgres-operator-myapp02-active-pattern + namespace: app-namespace + patch: | + - op: add + path: /spec/valuesFrom/- + value: + kind: ConfigMap + name: postgres-operator-cm-myapp02-active-pattern + key: values.yaml + - target: + kind: HelmRelease + name: postgres-operator-myapp02-active-pattern + namespace: app-namespace + patch: | + - op: add + path: /spec/valuesFrom/- + value: + kind: Secret + name: postgres-operator-secret-myapp02-active-pattern + key: values.yaml + - target: + kind: HelmRelease + name: postgres-operator-ui-myapp02-active-pattern + namespace: app-namespace + patch: | + - op: add + path: /spec/values + value: + key1: value1 + key2: value2 + - target: + kind: HelmRelease + name: postgres-operator-ui-myapp02-active-pattern + namespace: app-namespace + patch: | + - op: add + path: /spec/valuesFrom/- + value: + kind: ConfigMap + name: postgres-operator-ui-cm-myapp02-active-pattern + key: values.yaml + - target: + kind: HelmRelease + name: postgres-operator-ui-myapp02-active-pattern + namespace: app-namespace + patch: | + - op: add + path: /spec/valuesFrom/- + value: + kind: Secret + name: postgres-operator-ui-secret-myapp02-active-pattern + key: values.yaml + - apiVersion: kustomize.toolkit.fluxcd.io/v1 + kind: Kustomization + metadata: + name: ks-myapp02-active-pattern + namespace: flux-system + annotations: + config.kubernetes.io/index: '0' + config.kubernetes.io/path: ks-main.yaml + internal.config.kubernetes.io/index: '0' + internal.config.kubernetes.io/path: ks-main.yaml + internal.config.kubernetes.io/seqindent: compact + spec: + interval: 1h0m0s + path: apps/example2/manifests/main-pattern/main-brick-manifests + prune: true + sourceRef: + kind: GitRepository + name: sw-catalogs + namespace: flux-system + targetNamespace: app-namespace + postBuild: + substitute: + app_name: myapp02 + example_parameter1: active-example-value1 + example_parameter2: active-example-value2 + wait: true + - apiVersion: v1 + kind: ConfigMap + metadata: + name: postgres-operator-cm-myapp02-active-pattern + namespace: app-namespace + annotations: + config.kubernetes.io/path: postgres-operator-cm-myapp02-active-pattern.yaml + internal.config.kubernetes.io/path: postgres-operator-cm-myapp02-active-pattern.yaml + data: + values.yaml: |- + cm-key1: cm-value1 + cm-key2: cm-value2 + - apiVersion: v1 + kind: Secret + metadata: + name: postgres-operator-secret-myapp02-active-pattern + namespace: app-namespace + annotations: + config.kubernetes.io/path: postgres-operator-secret-myapp02-active-pattern.yaml + internal.config.kubernetes.io/path: postgres-operator-secret-myapp02-active-pattern.yaml + data: + values.yaml: ENCRYPTED + - apiVersion: v1 + kind: ConfigMap + metadata: + name: postgres-operator-ui-cm-myapp02-active-pattern + namespace: app-namespace + annotations: + config.kubernetes.io/path: postgres-operator-ui-cm-myapp02-active-pattern.yaml + internal.config.kubernetes.io/path: postgres-operator-ui-cm-myapp02-active-pattern.yaml + data: + values.yaml: |- + cm-key1: cm-value1 + cm-key2: cm-value2 + - apiVersion: kustomize.toolkit.fluxcd.io/v1 + kind: Kustomization + metadata: + name: database-kustomization-myapp02-standby-pattern + namespace: flux-system + annotations: + config.kubernetes.io/index: '0' + config.kubernetes.io/path: ks-database.yaml + internal.config.kubernetes.io/index: '0' + internal.config.kubernetes.io/path: ks-database.yaml + internal.config.kubernetes.io/seqindent: compact + spec: + interval: 1h0m0s + path: apps/example2/manifests/main-pattern/database-manifests + prune: true + sourceRef: + kind: GitRepository + name: sw-catalogs + namespace: flux-system + targetNamespace: app-namespace + postBuild: + substitute: + appname: myapp02 + appnamespace: app-namespace + wait: true + patches: + - target: + kind: HelmRelease + name: postgres-operator-myapp02-standby-pattern + namespace: app-namespace + patch: | + - op: add + path: /spec/values + value: + key1: value1 + key2: value2 + - target: + kind: HelmRelease + name: postgres-operator-myapp02-standby-pattern + namespace: app-namespace + patch: | + - op: add + path: /spec/valuesFrom/- + value: + kind: ConfigMap + name: postgres-operator-cm-myapp02-standby-pattern + key: values.yaml + - target: + kind: HelmRelease + name: postgres-operator-myapp02-standby-pattern + namespace: app-namespace + patch: | + - op: add + path: /spec/valuesFrom/- + value: + kind: Secret + name: postgres-operator-secret-myapp02-standby-pattern + key: values.yaml + - target: + kind: HelmRelease + name: postgres-operator-ui-myapp02-standby-pattern + namespace: app-namespace + patch: | + - op: add + path: /spec/values + value: + key1: value1 + key2: value2 + - target: + kind: HelmRelease + name: postgres-operator-ui-myapp02-standby-pattern + namespace: app-namespace + patch: | + - op: add + path: /spec/valuesFrom/- + value: + kind: ConfigMap + name: postgres-operator-ui-cm-myapp02-standby-pattern + key: values.yaml + - target: + kind: HelmRelease + name: postgres-operator-ui-myapp02-standby-pattern + namespace: app-namespace + patch: | + - op: add + path: /spec/valuesFrom/- + value: + kind: Secret + name: postgres-operator-ui-secret-myapp02-standby-pattern + key: values.yaml + - apiVersion: kustomize.toolkit.fluxcd.io/v1 + kind: Kustomization + metadata: + name: ks-myapp02-standby-pattern + namespace: flux-system + annotations: + config.kubernetes.io/index: '0' + config.kubernetes.io/path: ks-main.yaml + internal.config.kubernetes.io/index: '0' + internal.config.kubernetes.io/path: ks-main.yaml + internal.config.kubernetes.io/seqindent: compact + spec: + interval: 1h0m0s + path: apps/example2/manifests/main-pattern/main-brick-manifests + prune: true + sourceRef: + kind: GitRepository + name: sw-catalogs + namespace: flux-system + targetNamespace: app-namespace + postBuild: + substitute: + app_name: myapp02 + example_parameter1: standby-example-value1 + example_parameter2: standby-example-value2 + wait: true + - apiVersion: v1 + kind: ConfigMap + metadata: + name: postgres-operator-cm-myapp02-standby-pattern + namespace: app-namespace + annotations: + config.kubernetes.io/path: postgres-operator-cm-myapp02-standby-pattern.yaml + internal.config.kubernetes.io/path: postgres-operator-cm-myapp02-standby-pattern.yaml + data: + values.yaml: |- + cm-key1: cm-value1 + cm-key2: cm-value2 + - apiVersion: v1 + kind: Secret + metadata: + name: postgres-operator-secret-myapp02-standby-pattern + namespace: app-namespace + annotations: + config.kubernetes.io/path: postgres-operator-secret-myapp02-standby-pattern.yaml + internal.config.kubernetes.io/path: postgres-operator-secret-myapp02-standby-pattern.yaml + data: + values.yaml: ENCRYPTED + - apiVersion: v1 + kind: ConfigMap + metadata: + name: postgres-operator-ui-cm-myapp02-standby-pattern + namespace: app-namespace + annotations: + config.kubernetes.io/path: postgres-operator-ui-cm-myapp02-standby-pattern.yaml + internal.config.kubernetes.io/path: postgres-operator-ui-cm-myapp02-standby-pattern.yaml + data: + values.yaml: |- + cm-key1: cm-value1 + cm-key2: cm-value2 diff --git a/docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example2/manifests/main-pattern/database-manifests/hrset-database.yaml b/docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example2/manifests/main-pattern/database-manifests/hrset-database.yaml new file mode 100644 index 00000000..8623232e --- /dev/null +++ b/docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example2/manifests/main-pattern/database-manifests/hrset-database.yaml @@ -0,0 +1,58 @@ +####################################################################################### +# Copyright ETSI Contributors and Others. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +####################################################################################### + +--- +apiVersion: helm.toolkit.fluxcd.io/v2beta1 +kind: HelmRelease +metadata: + name: postgres-operator-${appname} + namespace: ${appnamespace} +spec: + chart: + spec: + chart: postgres-operator + reconcileStrategy: ChartVersion + sourceRef: + kind: HelmRepository + name: postgres-operator-charts-${appname} + namespace: ${appnamespace} + interval: 3m0s + targetNamespace: ${appnamespace} + values: {} + +--- + +apiVersion: helm.toolkit.fluxcd.io/v2beta1 +kind: HelmRelease +metadata: + name: postgres-operator-ui-${appname} + namespace: ${appnamespace} +spec: + dependsOn: + - name: postgres-operator-${appname} + namespace: ${appnamespace} + chart: + spec: + chart: postgres-operator-ui + reconcileStrategy: ChartVersion + sourceRef: + kind: HelmRepository + name: postgres-operator-charts-${appname} + namespace: ${appnamespace} + interval: 3m0s + targetNamespace: ${appnamespace} + values: {} diff --git a/docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example2/manifests/main-pattern/database-manifests/repo-zalando-postgres.yaml b/docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example2/manifests/main-pattern/database-manifests/repo-zalando-postgres.yaml new file mode 100644 index 00000000..eaf2dda4 --- /dev/null +++ b/docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example2/manifests/main-pattern/database-manifests/repo-zalando-postgres.yaml @@ -0,0 +1,29 @@ +####################################################################################### +# Copyright ETSI Contributors and Others. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +####################################################################################### + +--- +apiVersion: source.toolkit.fluxcd.io/v1beta2 +kind: HelmRepository +metadata: + name: postgres-operator-charts-${appname} + namespace: ${appnamespace} +spec: + interval: 10m0s + # type: oci + # url: oci://registry-1.docker.io/bitnamicharts + type: default + url: https://opensource.zalando.com/postgres-operator/charts/postgres-operator diff --git a/docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example2/manifests/main-pattern/main-brick-manifests/configmap.yaml b/docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example2/manifests/main-pattern/main-brick-manifests/configmap.yaml new file mode 100644 index 00000000..5bd5c509 --- /dev/null +++ b/docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example2/manifests/main-pattern/main-brick-manifests/configmap.yaml @@ -0,0 +1,23 @@ +####################################################################################### +# Copyright ETSI Contributors and Others. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +####################################################################################### + +apiVersion: v1 +kind: ConfigMap +metadata: + name: my-cm-${appname} +data: + my-key: my-value diff --git a/docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example2/templates/main-pattern/ks-database.yaml b/docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example2/templates/main-pattern/ks-database.yaml new file mode 100644 index 00000000..6ece0e05 --- /dev/null +++ b/docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example2/templates/main-pattern/ks-database.yaml @@ -0,0 +1,36 @@ +####################################################################################### +# Copyright ETSI Contributors and Others. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +####################################################################################### + +--- +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: database-kustomization-${APPNAME}-${PATTERN_NAME} + namespace: flux-system +spec: + interval: 1h0m0s + path: ./apps/example1/manifests/main-pattern/database-manifests + prune: true + sourceRef: + kind: GitRepository + name: sw-catalogs + namespace: flux-system + targetNamespace: ${APPNAMESPACE} + postBuild: + substitute: + appname: ${APPNAME} + appnamespace: ${APPNAMESPACE} diff --git a/docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example2/templates/main-pattern/ks-main.yaml b/docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example2/templates/main-pattern/ks-main.yaml new file mode 100644 index 00000000..7a39e0f1 --- /dev/null +++ b/docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example2/templates/main-pattern/ks-main.yaml @@ -0,0 +1,37 @@ +####################################################################################### +# Copyright ETSI Contributors and Others. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +####################################################################################### + +--- +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: ks-${APPNAME}-${PATTERN_NAME} + namespace: flux-system +spec: + interval: 1h0m0s + path: ./apps/example1/manifests/main-pattern/main-brick-manifests + prune: true + sourceRef: + kind: GitRepository + name: sw-catalogs + namespace: flux-system + targetNamespace: ${APPNAMESPACE} + postBuild: + substitute: + app_name: ${APPNAME} + example_parameter1: ${EXAMPLE_PARAMETER1} + example_parameter2: ${EXAMPLE_PARAMETER2} diff --git a/docker/osm-nushell-krm-functions/operations/tests/brick.nu b/docker/osm-nushell-krm-functions/operations/tests/brick.nu new file mode 100644 index 00000000..602a24ac --- /dev/null +++ b/docker/osm-nushell-krm-functions/operations/tests/brick.nu @@ -0,0 +1,18 @@ +####################################################################################### +# Copyright ETSI Contributors and Others. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +####################################################################################### + +# Module with custom functions to manage the transformations and generations associated to the different types of Building Blocks supported by OSM. diff --git a/docker/osm-nushell-krm-functions/operations/tests/ksu.nu b/docker/osm-nushell-krm-functions/operations/tests/ksu.nu new file mode 100644 index 00000000..31805663 --- /dev/null +++ b/docker/osm-nushell-krm-functions/operations/tests/ksu.nu @@ -0,0 +1,19 @@ +####################################################################################### +# Copyright ETSI Contributors and Others. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +####################################################################################### + +# Module with custom functions to manage KSUs and their renderization of the corresponding ResourceList into a given target folder. + diff --git a/docker/osm-nushell-krm-functions/operations/tests/location.nu b/docker/osm-nushell-krm-functions/operations/tests/location.nu new file mode 100644 index 00000000..6ea15b53 --- /dev/null +++ b/docker/osm-nushell-krm-functions/operations/tests/location.nu @@ -0,0 +1,232 @@ +####################################################################################### +# Copyright ETSI Contributors and Others. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +####################################################################################### + +# Module with custom functions to manage KSUs and their renderization of the corresponding ResourceList into a given target folder. + + +# Imports +use std assert +use ../location.nu * + + +### to path components tests ### + +export def "test location to path components repo-path with base-path" [] { + let input: record = { + "repo-path": ["/custom/repo"], + "base-path": ["base/dir"], # Now unused in base construction + "relative-path": ["file.txt"] + } + let actual: list = ($input | to path components) + let expected: list = [ "/custom/repo" "base/dir" "file.txt" ] + assert equal $actual $expected +} + + +export def "test location to path components repo-path with base-path with strings" [] { + let input: record = { + "repo-path": "/custom/repo", + "base-path": "base/dir", # Now unused in base construction + "relative-path": "file.txt" + } + let actual: list = ($input | to path components) + let expected: list = [ "/custom/repo" "base/dir" "file.txt" ] + assert equal $actual $expected +} + + +export def "test location to path components explicit base-path usage" [] { + let input: record = { + "repo-name": "my_repo", + "base-path": "explicit/base", + "relative-path": "data.csv" + } + let actual: list = ($input | to path components) + let expected: list = [ "/repos/my_repo" "explicit/base" "data.csv" ] + assert equal $actual $expected +} + + +export def "test location to path components empty repo-name errors" [] { + let input: record = { + "repo-name": "", + "relative-path": "file.txt" + } + assert error { $input | to path components } +} + + +export def "test location to path components partial profile spec errors" [] { + let input: record = { + "repo-path": "/valid/repo", + "profile-type": "dev", + "relative-path": "file.txt" + } + assert error { $input | to path components } +} + + +export def "test location to path components mixed spec priorities" [] { + let input: record = { + "repo-path": "/primary/repo", + "repo-name": "secondary", + "base-path": "explicit_base", + "oka-type": "models", + "oka-name": "ai", + "relative-path": "config.yaml" + } + let actual: list = ($input | to path components) + let expected: list = [ "/primary/repo" "explicit_base" "config.yaml" ] + assert equal $actual $expected +} + + +# Updated existing tests with clearer names +export def "test location to path components profile-based with normalization" [] { + let input: record = { + "repo-name": "profile_repo", + "profile-type": "PROD", + "profile-name": "EU_Cluster", + "relative-path": "secrets.env" + } + let actual: list = ($input | to path components) + let expected: list = [ "/repos/profile_repo" "osm_admin/PROD/EU_Cluster" "secrets.env" ] + assert equal $actual $expected +} + + +export def "test location to path components oka-based with normalization" [] { + let input: record = { + "repo-path": "/repos/oka_repo", + "oka-type": "DATA", + "oka-name": "Census2025", + "relative-path": "demographics.csv" + } + let actual: list = ($input | to path components) + let expected: list = [ "/repos/oka_repo" "DATA/Census2025" "demographics.csv" ] + assert equal $actual $expected +} + + +# TODO: + +### to absolute path tests ### + +export def "test location to absolute path basic repo-path" [] { + let input: record = { + "repo-path": ["/main/repo", "sw-catalogs"], + "base-path": ["apps", "example1"], + "relative-path": ["manifests", "main-pattern", "main-brick-manifests"] + } + let actual: string = ($input | to absolute path) + let expected: string = "/main/repo/sw-catalogs/apps/example1/manifests/main-pattern/main-brick-manifests" + assert equal $actual $expected +} + + +export def "test location to absolute path profile-based with defaults" [] { + let input: record = { + "repo-name": "fleet", + "profile-type": "dev", + "profile-name": "TestEnv", + "relative-path": ["app_instance01", "main"] + } + let actual = ($input | to absolute path) + let expected = "/repos/fleet/osm_admin/dev/TestEnv/app_instance01/main" + assert equal $actual $expected +} + + +export def "test location to absolute path oka-based with custom defaults" [] { + let input: record = { + "repo-name": "data_repo", + "oka-type": "app", # It should be converted to "apps" + "oka-name": "upf", + "relative-path": ["2024", "main"] + } + let actual: string = ($input | to absolute path "geo" "/data") + let expected: string = "/data/data_repo/apps/upf/2024/main" + assert equal $actual $expected +} + + +export def "test location to absolute path mixed specifications priority" [] { + let input: record = { + "repo-name": "fleet", + "base-path": ["my_oka"], + "relative-path": ["manifests"] + } + let actual: string = ($input | to absolute path) + let expected: string = "/repos/fleet/my_oka/manifests" + assert equal $actual $expected +} + + +export def "test location to absolute path special characters handling" [] { + let input: record = { + "repo-name": "fleet", + "profile-type": "apps", # Should become "app-profiles" + "profile-name": "mycluster01", + "relative-path": ["configs/prod"] + } + let actual: string = ($input | to absolute path) + let expected: string = "/repos/fleet/osm_admin/app-profiles/mycluster01/configs/prod" + assert equal $actual $expected +} + + +export def "test location to absolute path error missing relative-path" [] { + let input: record = { + "repo-path": ["/valid/repo"], + "base-path": ["valid/base"] + } + assert error { $input | to absolute path } +} + + +export def "test location to absolute path nested relative path" [] { + let input: record = { + "repo-path": ["/repos/core"], + "oka-type": "infra-controllers", + "oka-name": "predictive", + "relative-path": ["mobile", "serverless-web"] + } + let actual: string = ($input | to absolute path) + let expected: string = "/repos/core/infra-controllers/predictive/mobile/serverless-web" + assert equal $actual $expected +} + + +export def "test location to absolute path empty repo-name error" [] { + let input: record = { + "repo-name": "", + "relative-path": ["file.txt"] + } + assert error { $input | to absolute path } +} + + +export def "test location to absolute path minimal valid input" [] { + let input: record = { + "repo-name": "fleet", + "base-path": ["apps"], + "relative-path": ["test-app"] + } + let actual: string = ($input | to absolute path) + let expected: string = "/repos/fleet/apps/test-app" + assert equal $actual $expected +} diff --git a/docker/osm-nushell-krm-functions/operations/tests/mod.nu b/docker/osm-nushell-krm-functions/operations/tests/mod.nu new file mode 100644 index 00000000..21f40d6e --- /dev/null +++ b/docker/osm-nushell-krm-functions/operations/tests/mod.nu @@ -0,0 +1,55 @@ +#!/usr/bin/env -S nu --stdin +####################################################################################### +# Copyright ETSI Contributors and Others. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +####################################################################################### + + +use std assert +# use ../../krm * + +use ./location.nu * +use ./app.nu * +# use ./ksu.nu * +# use ./pattern.nu * +# use ./brick.nu * + + +# Test launcher +def main [] { + print "Running tests..." + + let test_commands: list = ( + scope commands + | where ($it.type == "custom") + and ($it.name | str starts-with "test ") + and not ($it.description | str starts-with "ignore") + | get name + ) + + let count_test_commands: int = ($test_commands | length) + let test_commands_together: string = ( + $test_commands + | enumerate + | each { |test| + [$"print '--> [($test.index + 1)/($count_test_commands)] ($test.item)'", $test.item] + } + | flatten + | str join ";" + ) + + nu --commands $"source `($env.CURRENT_FILE)`; ($test_commands_together)" + print $"\n✅ ALL TESTS COMPLETED SUCCESSFULLY" +} diff --git a/docker/osm-nushell-krm-functions/operations/tests/pattern.nu b/docker/osm-nushell-krm-functions/operations/tests/pattern.nu new file mode 100644 index 00000000..68acc801 --- /dev/null +++ b/docker/osm-nushell-krm-functions/operations/tests/pattern.nu @@ -0,0 +1,18 @@ +####################################################################################### +# Copyright ETSI Contributors and Others. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +####################################################################################### + +# Module with custom functions to manage a Pattern definition, taking into account its corresponding source template and the set of transformations specified for its constituent Bricks. diff --git a/docker/osm-nushell-krm-functions/scripts/entrypoint-config.nu b/docker/osm-nushell-krm-functions/scripts/entrypoint-config.nu new file mode 100644 index 00000000..6f56e7a6 --- /dev/null +++ b/docker/osm-nushell-krm-functions/scripts/entrypoint-config.nu @@ -0,0 +1,119 @@ +####################################################################################### +# Copyright ETSI Contributors and Others. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +####################################################################################### + +# Nushell Config File +# + + +# Default config +$env.config.color_config = { + separator: white + leading_trailing_space_bg: { attr: n } + header: green_bold + empty: blue + bool: light_cyan + int: white + filesize: cyan + duration: white + datetime: purple + range: white + float: white + string: white + nothing: white + binary: white + cell-path: white + row_index: green_bold + record: white + list: white + closure: green_bold + glob:cyan_bold + block: white + hints: dark_gray + search_result: { bg: red fg: white } + shape_binary: purple_bold + shape_block: blue_bold + shape_bool: light_cyan + shape_closure: green_bold + shape_custom: green + shape_datetime: cyan_bold + shape_directory: cyan + shape_external: cyan + shape_externalarg: green_bold + shape_external_resolved: light_yellow_bold + shape_filepath: cyan + shape_flag: blue_bold + shape_float: purple_bold + shape_glob_interpolation: cyan_bold + shape_globpattern: cyan_bold + shape_int: purple_bold + shape_internalcall: cyan_bold + shape_keyword: cyan_bold + shape_list: cyan_bold + shape_literal: blue + shape_match_pattern: green + shape_matching_brackets: { attr: u } + shape_nothing: light_cyan + shape_operator: yellow + shape_pipe: purple_bold + shape_range: yellow_bold + shape_record: cyan_bold + shape_redirection: purple_bold + shape_signature: green_bold + shape_string: green + shape_string_interpolation: cyan_bold + shape_table: blue_bold + shape_variable: purple + shape_vardecl: purple + shape_raw_string: light_purple + shape_garbage: { + fg: white + bg: red + attr: b + } +} + + +# Remove Nushell's welcome message +# -------------------------------- +$env.config.show_banner = false + + +# NU_LIB_DIRS +# ----------- +# Directories in this environment variable are searched by the +# `use` and `source` commands. +# It is searched after the NU_LIB_DIRS constant. +# +$env.NU_LIB_DIRS ++= [ "/app/osm" ] + + +# Load the model and environment parameters +# ----------------------------------------- +let clear_environment_location: path = ($env.CLEAR_ENVIRONMENT_LOCATION? | default "/model/parameters/clear/environment.yaml") +let secret_environment_location: path = ($env.SECRET_ENVIRONMENT_LOCATION? | default "/model/parameters/secret/environment.yaml") +let model_location: path = ($env.MODEL_LOCATION? | default "/model/app_instance_model.yaml") +let environment: record = ( + open $clear_environment_location | default {} + | merge ( + open $secret_environment_location | default {} + ) +) +let model_instance: record = (open $model_location | default {}) + + +# Load the required library +use /app/osm/operations/app.nu diff --git a/docker/osm-nushell-krm-functions/scripts/entrypoint.sh b/docker/osm-nushell-krm-functions/scripts/entrypoint.sh new file mode 100755 index 00000000..9bd7cfe9 --- /dev/null +++ b/docker/osm-nushell-krm-functions/scripts/entrypoint.sh @@ -0,0 +1,40 @@ +#!/bin/sh +####################################################################################### +# Copyright ETSI Contributors and Others. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +####################################################################################### + + +# If the main command is "nu", it should run as if it where a basic "nu" container, but with the expected environment variables and libraries. +# Otherwise, it must be an OSM model operation, so it should be fed by the appropriate instance model in a pipeline + +# Check if the first argument is "nu" +if [ "$1" = "nu" ]; then + # If it is just "nu", with no extra arguments, just runs it with the right environment + if [ "$#" -eq 1 ]; then + exec nu --env-config scripts/entrypoint-config.nu + # Otherwise, adds the rest of arguments after the environment is loaded + else + # Shift the first argument ("nu") off, leaving only the remaining arguments + shift + + # Construct the final command with the joined arguments + exec nu --env-config scripts/entrypoint-config.nu "$@" + fi +else + # Otherwise, launches the command with special configuration and feeding it by the instance model in a pipeline + NU_COMMAND="\$model_instance | $@" + exec nu --env-config scripts/entrypoint-config.nu -c "${NU_COMMAND}" +fi -- 2.25.1