Feature 11074: Enhanced OSM declarative modelling for applications. OSM's SDK for... 04/15304/4
authorgarciadeblas <gerardo.garciadeblas@telefonica.com>
Wed, 23 Jul 2025 16:35:24 +0000 (18:35 +0200)
committergarciadeblas <gerardo.garciadeblas@telefonica.com>
Thu, 31 Jul 2025 15:14:18 +0000 (17:14 +0200)
Change-Id: I6d03faa143eafcf30380b3b854c54f177dcf8f25
Signed-off-by: garciadeblas <gerardo.garciadeblas@telefonica.com>
57 files changed:
docker/osm-nushell-krm-functions/Dockerfile [new file with mode: 0644]
docker/osm-nushell-krm-functions/krm/concatenate.nu [new file with mode: 0644]
docker/osm-nushell-krm-functions/krm/convert.nu [new file with mode: 0644]
docker/osm-nushell-krm-functions/krm/generator.nu [new file with mode: 0644]
docker/osm-nushell-krm-functions/krm/jsonpatch.nu [new file with mode: 0644]
docker/osm-nushell-krm-functions/krm/keypair.nu [new file with mode: 0644]
docker/osm-nushell-krm-functions/krm/mod.nu [new file with mode: 0644]
docker/osm-nushell-krm-functions/krm/overlaypatch.nu [new file with mode: 0644]
docker/osm-nushell-krm-functions/krm/patch.nu [new file with mode: 0644]
docker/osm-nushell-krm-functions/krm/strategicmergepatch.nu [new file with mode: 0644]
docker/osm-nushell-krm-functions/krm/tests/artifacts/configmap/templates/configmap.yaml [new file with mode: 0644]
docker/osm-nushell-krm-functions/krm/tests/artifacts/empty/.gitkeep [new file with mode: 0644]
docker/osm-nushell-krm-functions/krm/tests/artifacts/jenkins/manifests/bitnamicharts-repo.yaml [new file with mode: 0644]
docker/osm-nushell-krm-functions/krm/tests/artifacts/jenkins/manifests/jenkins-hr.yaml [new file with mode: 0644]
docker/osm-nushell-krm-functions/krm/tests/artifacts/jenkins/templates/jenkins-ks.yaml [new file with mode: 0644]
docker/osm-nushell-krm-functions/krm/tests/artifacts/jenkins/templates/jenkins-ns.yaml [new file with mode: 0644]
docker/osm-nushell-krm-functions/krm/tests/artifacts/namespace/templates/namespace.yaml [new file with mode: 0644]
docker/osm-nushell-krm-functions/krm/tests/artifacts/secret/templates/secret.yaml [new file with mode: 0644]
docker/osm-nushell-krm-functions/krm/tests/concatenate.nu [new file with mode: 0644]
docker/osm-nushell-krm-functions/krm/tests/convert.nu [new file with mode: 0644]
docker/osm-nushell-krm-functions/krm/tests/generator.nu [new file with mode: 0644]
docker/osm-nushell-krm-functions/krm/tests/jsonpatch.nu [new file with mode: 0644]
docker/osm-nushell-krm-functions/krm/tests/keypair.nu [new file with mode: 0644]
docker/osm-nushell-krm-functions/krm/tests/mod.nu [new file with mode: 0644]
docker/osm-nushell-krm-functions/krm/tests/overlaypatch.nu [new file with mode: 0644]
docker/osm-nushell-krm-functions/krm/tests/patch.nu [new file with mode: 0644]
docker/osm-nushell-krm-functions/krm/tests/strategicmergepatch.nu [new file with mode: 0644]
docker/osm-nushell-krm-functions/operations/app.nu [new file with mode: 0644]
docker/osm-nushell-krm-functions/operations/brick.nu [new file with mode: 0644]
docker/osm-nushell-krm-functions/operations/custom/mod.nu [new file with mode: 0644]
docker/osm-nushell-krm-functions/operations/ksu.nu [new file with mode: 0644]
docker/osm-nushell-krm-functions/operations/location.nu [new file with mode: 0644]
docker/osm-nushell-krm-functions/operations/mod.nu [new file with mode: 0644]
docker/osm-nushell-krm-functions/operations/pattern.nu [new file with mode: 0644]
docker/osm-nushell-krm-functions/operations/replace.nu [new file with mode: 0644]
docker/osm-nushell-krm-functions/operations/tests/app.nu [new file with mode: 0644]
docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example1/app-instance-from-model.yaml [new file with mode: 0644]
docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example1/expected_result.yaml [new file with mode: 0644]
docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example1/manifests/main-pattern/database-manifests/hrset-database.yaml [new file with mode: 0644]
docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example1/manifests/main-pattern/database-manifests/repo-zalando-postgres.yaml [new file with mode: 0644]
docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example1/manifests/main-pattern/main-brick-manifests/configmap.yaml [new file with mode: 0644]
docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example1/templates/main-pattern/ks-database.yaml [new file with mode: 0644]
docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example1/templates/main-pattern/ks-main.yaml [new file with mode: 0644]
docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example2/app-instance-from-model.yaml [new file with mode: 0644]
docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example2/expected_result.yaml [new file with mode: 0644]
docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example2/manifests/main-pattern/database-manifests/hrset-database.yaml [new file with mode: 0644]
docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example2/manifests/main-pattern/database-manifests/repo-zalando-postgres.yaml [new file with mode: 0644]
docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example2/manifests/main-pattern/main-brick-manifests/configmap.yaml [new file with mode: 0644]
docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example2/templates/main-pattern/ks-database.yaml [new file with mode: 0644]
docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example2/templates/main-pattern/ks-main.yaml [new file with mode: 0644]
docker/osm-nushell-krm-functions/operations/tests/brick.nu [new file with mode: 0644]
docker/osm-nushell-krm-functions/operations/tests/ksu.nu [new file with mode: 0644]
docker/osm-nushell-krm-functions/operations/tests/location.nu [new file with mode: 0644]
docker/osm-nushell-krm-functions/operations/tests/mod.nu [new file with mode: 0644]
docker/osm-nushell-krm-functions/operations/tests/pattern.nu [new file with mode: 0644]
docker/osm-nushell-krm-functions/scripts/entrypoint-config.nu [new file with mode: 0644]
docker/osm-nushell-krm-functions/scripts/entrypoint.sh [new file with mode: 0755]

diff --git a/docker/osm-nushell-krm-functions/Dockerfile b/docker/osm-nushell-krm-functions/Dockerfile
new file mode 100644 (file)
index 0000000..e3f1db3
--- /dev/null
@@ -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 (file)
index 0000000..dde5095
--- /dev/null
@@ -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<any>
+] {
+
+    # Gather the input and convert to list
+    # let manifest1: list<any> = if $in == null { [] } else { $in }
+    # let manifest2: list<any> = if $mnfst2 == null { [] } else { $mnfst2 }
+    let manifest1: list<any> = (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<any> = (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 (file)
index 0000000..07578a0
--- /dev/null
@@ -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<string>" {
+        # 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<any> = (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 (file)
index 0000000..98d52b5
--- /dev/null
@@ -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<any> = (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 (file)
index 0000000..1718b9b
--- /dev/null
@@ -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 <https://github.com/kubernetes-sigs/kustomize/blob/master/examples/patchMultipleObjects.md>
+    ...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 (file)
index 0000000..83689e3
--- /dev/null
@@ -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 (file)
index 0000000..57b4507
--- /dev/null
@@ -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 (file)
index 0000000..1fcaf0d
--- /dev/null
@@ -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 <https://github.com/kubernetes-sigs/kustomize/blob/master/examples/patchMultipleObjects.md>
+    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 <https://github.com/kubernetes-sigs/kustomize/blob/master/examples/patchMultipleObjects.md>
+    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 <https://github.com/kubernetes-sigs/kustomize/blob/master/examples/patchMultipleObjects.md>
+    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 (file)
index 0000000..91403d1
--- /dev/null
@@ -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<any> = (
+        $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<any> = (
+        $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 (file)
index 0000000..895acfc
--- /dev/null
@@ -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 <https://github.com/kubernetes/community/blob/master/contributors/devel/sig-api-machinery/strategic-merge-patch.md>
+export def "create" [
+    target: record, # Target resource specification as per <https://github.com/kubernetes-sigs/kustomize/blob/master/examples/patchMultipleObjects.md>
+    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 (file)
index 0000000..a758a7b
--- /dev/null
@@ -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 (file)
index 0000000..e69de29
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 (file)
index 0000000..354b837
--- /dev/null
@@ -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 (file)
index 0000000..c87a95e
--- /dev/null
@@ -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 (file)
index 0000000..bbf4d7b
--- /dev/null
@@ -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 (file)
index 0000000..ca2fff8
--- /dev/null
@@ -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 (file)
index 0000000..d4c0386
--- /dev/null
@@ -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 (file)
index 0000000..54346c5
--- /dev/null
@@ -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 (file)
index 0000000..ae674b8
--- /dev/null
@@ -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<any> = manifests $mnfst2
+    let expected: list<any> = []
+
+    assert equal $actual $expected
+}
+
+
+export def "test concatenate manifests empty stdin" []: [
+    nothing -> nothing
+] {
+    let mnfst2: record = {
+        name: "example"
+        kind: "Deployment"
+    }
+
+    let actual: list<any> = manifests $mnfst2
+    let expected: list<any> = [ $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<any> = (
+        echo $mnfst1 | manifests $mnfst2
+    )
+    let expected: list<any> = [ $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<any> = (
+        echo $mnfst1 | manifests $mnfst2
+    )
+    let expected: list<any> = [ $mnfst1, $mnfst2 ]
+
+    assert equal $actual $expected
+}
+
+
+export def "test concatenate manifests lists of records" []: [
+    nothing -> nothing
+] {
+    let mnfst1: list<any> = [
+        { name: "example1", kind: "Deployment" }
+        { name: "example2", kind: "Service" }
+    ]
+    let mnfst2: list<any> = [
+        { name: "example3", kind: "Pod" }
+        { name: "example4", kind: "ConfigMap" }
+    ]
+
+    let actual: list<any> = (
+        echo $mnfst1 | manifests $mnfst2
+    )
+    let expected: list<any> = [
+        { 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 (file)
index 0000000..09710eb
--- /dev/null
@@ -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<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 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<string> = ["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<string> = ["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<string> = ["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<any> = [
+        { 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<string> = (
+        ls --short-names $target_folder
+        | get name
+        | sort
+    )
+
+    # Cleanup
+    rm -rf $target_folder
+
+    # Expected
+    let expected_contents: list<string> = (
+        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<string> = (
+        ls --short-names $target_folder
+        | get name
+        | sort
+    )
+
+    # Cleanup
+    rm -rf $target_folder
+
+    # Expected
+    let expected_contents: list<string> = (
+        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<string> = (
+        ls --short-names $target_folder
+        | get name
+        | sort
+    )
+
+    # Cleanup
+    rm -rf $temp_folder
+
+    # Expected
+    let expected_contents: list<string> = (
+        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 (file)
index 0000000..5f133a0
--- /dev/null
@@ -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<any> = [
+        { 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 (file)
index 0000000..77cbcd4
--- /dev/null
@@ -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 (file)
index 0000000..e3a1ab7
--- /dev/null
@@ -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 (file)
index 0000000..d206563
--- /dev/null
@@ -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<string> = (
+        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 (file)
index 0000000..5473093
--- /dev/null
@@ -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 (file)
index 0000000..ec2ae70
--- /dev/null
@@ -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 (file)
index 0000000..095c60a
--- /dev/null
@@ -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 (file)
index 0000000..09a3085
--- /dev/null
@@ -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<cell-path> = [
+        $.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<record> = ($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<cell-path> = [
+        $.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<record> = ($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<cell-path> = [
+        $.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<record> = ($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<string>") {
+            $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 (file)
index 0000000..f0c7f64
--- /dev/null
@@ -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<record> = ($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 (file)
index 0000000..c82b4d3
--- /dev/null
@@ -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 (file)
index 0000000..ffcc19f
--- /dev/null
@@ -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<record> = ($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 (file)
index 0000000..1f568f3
--- /dev/null
@@ -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<path>
+] {
+    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 (file)
index 0000000..9be239b
--- /dev/null
@@ -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 (file)
index 0000000..d59842e
--- /dev/null
@@ -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<record> = ($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 (file)
index 0000000..0181c40
--- /dev/null
@@ -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 (file)
index 0000000..ec5cb18
--- /dev/null
@@ -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<record> = (
+        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<record> = (
+        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<record> = (
+        $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<record> = (
+        $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<record> = (
+        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<record> = (
+        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<record> = (
+        $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<record> = (
+        $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<record> = (
+        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<string> = (
+        $instance_model
+        | replace vars $environment
+        | get $.spec.ksus
+        | each {|k|
+            $k
+            | get "target"
+            | location to absolute path
+        }
+    )
+    let actual: list<record> = (
+        $targets
+        | each {|t|
+            {} | convert folder to resourcelist $t
+        }
+    )
+
+    # Overwrites the encrypted part of the secrets in both
+    let actual_trimmed: list<record> = (
+        $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<record> = (
+        $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<record> = ($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 (file)
index 0000000..dcd06ac
--- /dev/null
@@ -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 (file)
index 0000000..ae6fa90
--- /dev/null
@@ -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 (file)
index 0000000..8623232
--- /dev/null
@@ -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 (file)
index 0000000..eaf2dda
--- /dev/null
@@ -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 (file)
index 0000000..5bd5c50
--- /dev/null
@@ -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 (file)
index 0000000..af2831c
--- /dev/null
@@ -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 (file)
index 0000000..d80fe76
--- /dev/null
@@ -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 (file)
index 0000000..fe248e4
--- /dev/null
@@ -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 (file)
index 0000000..464b258
--- /dev/null
@@ -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 (file)
index 0000000..8623232
--- /dev/null
@@ -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 (file)
index 0000000..eaf2dda
--- /dev/null
@@ -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 (file)
index 0000000..5bd5c50
--- /dev/null
@@ -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 (file)
index 0000000..6ece0e0
--- /dev/null
@@ -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 (file)
index 0000000..7a39e0f
--- /dev/null
@@ -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 (file)
index 0000000..602a24a
--- /dev/null
@@ -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 (file)
index 0000000..3180566
--- /dev/null
@@ -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 (file)
index 0000000..6ea15b5
--- /dev/null
@@ -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<string> = ($input | to path components)
+    let expected: list<string> = [ "/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<string> = ($input | to path components)
+    let expected: list<string> = [ "/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<string> = ($input | to path components)
+    let expected: list<string> = [ "/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<string> = ($input | to path components)
+    let expected: list<string> = [ "/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<string> = ($input | to path components)
+    let expected: list<string> = [ "/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<string> = ($input | to path components)
+    let expected: list<string> = [ "/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 (file)
index 0000000..21f40d6
--- /dev/null
@@ -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<string> = (
+        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 (file)
index 0000000..68acc80
--- /dev/null
@@ -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 (file)
index 0000000..6f56e7a
--- /dev/null
@@ -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 (executable)
index 0000000..9bd7cfe
--- /dev/null
@@ -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