Feature 11074: Enhanced OSM declarative modelling for applications. OSM's SDK for intent manipulation

Change-Id: I6d03faa143eafcf30380b3b854c54f177dcf8f25
Signed-off-by: garciadeblas <gerardo.garciadeblas@telefonica.com>
diff --git a/docker/osm-nushell-krm-functions/operations/app.nu b/docker/osm-nushell-krm-functions/operations/app.nu
new file mode 100644
index 0000000..09a3085
--- /dev/null
+++ b/docker/osm-nushell-krm-functions/operations/app.nu
@@ -0,0 +1,211 @@
+#######################################################################################
+# Copyright ETSI Contributors and Others.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#######################################################################################
+
+# Module with custom functions to manage an App instance, invoking the corresponding KSU renderizations to appropriate target folders in a given profile.
+
+
+# Import required modules
+use ../krm *
+# use ./replace.nu *
+# use ./ksu.nu *
+use ./replace.nu
+use ./ksu.nu
+
+
+# Create an instance of an App, based on an App instance model received from stdin.
+export def create [
+    --dry-run              # If set, only prints the generated ResourceList(s) along with the target folder(s) (i.e., it does not write to any folder).
+    --print-target-folders # If set, print the target folder(s). Requires --dry-run.
+    environment: record    # Record with environment variables to load.
+]: [
+    record -> nothing
+    record -> table
+] {
+    # TODO: Format checks
+
+    # Save the original app instance record
+    let in_instance: record = $in
+
+    # Remove from the environment those keys that are reserved, dynamic or forbidden, since they will be overriden or may cause known issues, and add one that mimics the KSU name
+    const forbidden_keys: list<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
index 0000000..f0c7f64
--- /dev/null
+++ b/docker/osm-nushell-krm-functions/operations/brick.nu
@@ -0,0 +1,398 @@
+#######################################################################################
+# Copyright ETSI Contributors and Others.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#######################################################################################
+
+# Module with custom functions to manage the transformations and generations associated to the different types of Building Blocks supported by OSM.
+#
+# Supported Brick types are:
+#
+# - `basic`. Basic transformation of the ResourceList. It performs a cleanup and regularization of the target Kustomization (enforce the right path in the repo, ensure that `wait` is enabled, etc.), unless specified otherwise. In addition, it also supports the commonest transformations for a Kustomization, such as addition of optional components, extra labels and/or annotations, hot replacement of image names and tags, etc. For more details, check out the help for the `brick transform basic` command.
+# - `helmreleaseset`. Transformations for a ResourceList with a set of HelmReleases, so that values injected into the specific HelmReleases. It is a superset of the `basic` Brick, and its transformations are applied right after the corresponding basic transformations. For more details, check out the help for the `brick transform helmreleaseset` command.
+# - `custom`. Transformation of the ResourceList with a custom (user-provided) "create" transformation after a `basic` regularization is applied. For more details, check out the help for the `custom create` command.
+# - `custom-hr`. Transformation of the ResourceList with a custom (user-provided) "create" transformation after a `helmreleaseset` transformation (including `basic` regularizations) is applied. For more details, check out the help for the `custom create` command.
+# - `custom-full`. Transformation of the ResourceList with a custom (user-provided) "create" transformation. **No `basic` regularization is applied**, so any regularization (if needed) should be implemented in the custom command. For more details, check out the help for the `custom create` command.
+
+
+use ../krm *
+use ./location.nu
+use custom
+
+
+# Apply the `basic` transformation to ResourceList received from stdin according to the specification of the Brick.
+# The `basic` Brick transformation just does a cleanup and regularization of the target Kustomization
+export def "transform basic" [
+    brick: record  # Brick specification
+]: [
+    record -> record
+] {
+    let rl: record = $in
+
+    # Get the key parts
+    let brick_name: string = ($brick | get -i name | default "untitled-brick")
+    let kustomization_name: string = ($brick | get $.kustomization.name)
+    let kustomization_namespace: string = ($brick | get -i $.kustomization.namespace | default "flux-system")
+    let src: string = ($brick | get source | location from base path)
+    let options: record = ($brick | get -i options | default {})
+    ## Should it avoid path regularization?
+    let keep_path: bool = ($options | get -i keep-path | default false)
+    ## Should it avoid enforcing the wait?
+    let enforce_wait: bool = ($options | get -i enforce-wait | default true)
+    ## Should it avoid enforcing the prune?
+    let enforce_prune: bool = ($options | get -i enforce-prune | default true)
+    ## Should it enable (or append) some `components`?
+    let components: list = ($options | get -i components | default [])
+    ## Should it set or overwrite `targetNamespace`?
+    let targetNamespace: string = ($options | get -i targetNamespace | default "")
+    ## Should it overwrite `interval`?
+    let interval: string = ($options | get -i interval | default "")
+    ## Should it set or overwrite `retryInterval`?
+    let retryInterval: string = ($options | get -i retryInterval | default "")
+    ## Should it set or overwrite `serviceAccountName`?
+    let serviceAccountName: string = ($options | get -i serviceAccountName | default "")
+    ## Should it add custom `healthChecks`?
+    let healthChecks: list = ($options | get -i healthChecks | default [])
+    ## Should it add custom `healthCheckExprs`?
+    let healthCheckExprs: list = ($options | get -i healthCheckExprs | default [])
+    ## Should it set or overwrite a `namePrefix`?
+    let namePrefix: string = ($options | get -i namePrefix | default "")
+    ## Should it set or overwrite a `nameSuffix`?
+    let nameSuffix: string = ($options | get -i nameSuffix | default "")
+    ## Should it append additional `.metadata.labels` and `.spec.commonMetadata.labels`?
+    let new_labels: record = ($options | get -i new_labels | default {})
+    ## Should it append additional `.metadata.annotations` and `.spec.commonMetadata.annotations`?
+    let new_annotations: record = ($options | get -i new_annotations | default {})
+    ## Should it append additional `.spec.images` replacements?
+    let images: list = ($options | get -i images | default [])
+
+    # Transform as per the basic Brick model
+    $rl
+    # Path regularization, if applicable
+    | if $keep_path { $in } else {
+        $in
+        | (
+            patch resource update key
+                $.spec.path $src
+                "kustomize.toolkit.fluxcd.io/v1" Kustomization $kustomization_name $kustomization_namespace
+        )
+    }
+    # Enforce the wait, if applicable
+    | if $enforce_wait {
+        $in
+        | (
+            patch resource upsert key
+                $.spec.wait $enforce_wait
+                "kustomize.toolkit.fluxcd.io/v1" Kustomization $kustomization_name $kustomization_namespace
+        )
+    } else { $in }
+    # Enforce the prune, if applicable
+    | if $enforce_prune {
+        $in
+        | (
+            patch resource upsert key
+                $.spec.prune $enforce_prune
+                "kustomize.toolkit.fluxcd.io/v1" Kustomization $kustomization_name $kustomization_namespace
+        )
+    } else { $in }
+    # Enable (or append) some `components`, if applicable
+    | if ($components | is-not-empty) {
+        let tmp_rl: record = $in
+        let existing_components: list = (
+            $tmp_rl
+            | (
+                patch resource keep
+                    "kustomize.toolkit.fluxcd.io/v1" Kustomization $kustomization_name $kustomization_namespace
+            )
+            | get -i $.items.0.spec.components
+            | default []
+        )
+        let all_components: list = ($existing_components ++ $components) | uniq
+
+        $tmp_rl
+        | (
+            patch resource upsert key
+                $.spec.components $all_components
+                "kustomize.toolkit.fluxcd.io/v1" Kustomization $kustomization_name $kustomization_namespace
+        )
+    } else { $in }
+    # Set or overwrite `targetNamespace`, if applicable
+    | if ($targetNamespace | is-not-empty) {
+        $in
+        | (
+            patch resource upsert key
+                $.spec.targetNamespace $targetNamespace
+                "kustomize.toolkit.fluxcd.io/v1" Kustomization $kustomization_name $kustomization_namespace
+        )
+    } else { $in }
+    # Overwrite `interval`, if applicable
+    | if ($interval | is-not-empty) {
+        $in
+        | (
+            patch resource update key
+                $.spec.interval $interval
+                "kustomize.toolkit.fluxcd.io/v1" Kustomization $kustomization_name $kustomization_namespace
+        )
+    } else { $in }
+    # Set or overwrite `retryInterval`, if applicable
+    | if ($retryInterval | is-not-empty) {
+        $in
+        | (
+            patch resource upsert key
+                $.spec.retryInterval $retryInterval
+                "kustomize.toolkit.fluxcd.io/v1" Kustomization $kustomization_name $kustomization_namespace
+        )
+    } else { $in }
+    # Set or overwrite `serviceAccountName`, if applicable
+    | if ($serviceAccountName | is-not-empty) {
+        $in
+        | (
+            patch resource upsert key
+                $.spec.serviceAccountName $serviceAccountName
+                "kustomize.toolkit.fluxcd.io/v1" Kustomization $kustomization_name $kustomization_namespace
+        )
+    } else { $in }
+    # Enable (or append) some `healthChecks`, if applicable
+    | if ($healthChecks | is-not-empty) {
+        let tmp_rl: record = $in
+        let existing_healthChecks: list = ($tmp_rl | get -i $.spec.healthChecks | default [])
+        let all_healthChecks: list = ($existing_healthChecks ++ $healthChecks) | uniq
+
+        $tmp_rl
+        | (
+            patch resource upsert key
+                $.spec.healthChecks $all_healthChecks
+                "kustomize.toolkit.fluxcd.io/v1" Kustomization $kustomization_name $kustomization_namespace
+        )
+    } else { $in }
+    # Enable (or append) some `healthCheckExprs`, if applicable
+    | if ($healthCheckExprs | is-not-empty) {
+        let tmp_rl: record = $in
+        let existing_healthCheckExprs: list = ($tmp_rl | get -i $.spec.healthCheckExprs | default [])
+        let all_healthCheckExprs: list = ($existing_healthCheckExprs ++ $healthCheckExprs) | uniq
+
+        $tmp_rl
+        | (
+            patch resource upsert key
+                $.spec.healthCheckExprs $all_healthCheckExprs
+                "kustomize.toolkit.fluxcd.io/v1" Kustomization $kustomization_name $kustomization_namespace
+        )
+    } else { $in }
+    # Set or overwrite `namePrefix`, if applicable
+    | if ($namePrefix | is-not-empty) {
+        $in
+        | (
+            patch resource upsert key
+                $.spec.namePrefix $namePrefix
+                "kustomize.toolkit.fluxcd.io/v1" Kustomization $kustomization_name $kustomization_namespace
+        )
+    } else { $in }
+    # Set or overwrite `nameSuffix`, if applicable
+    | if ($nameSuffix | is-not-empty) {
+        $in
+        | (
+            patch resource upsert key
+                $.spec.nameSuffix $nameSuffix
+                "kustomize.toolkit.fluxcd.io/v1" Kustomization $kustomization_name $kustomization_namespace
+        )
+    } else { $in }
+    # Enable (or append) some `.metadata.labels` and `.spec.commonMetadata.labels`, if applicable
+    | if ($new_labels | is-not-empty) {
+        let tmp_rl: record = $in
+        let existing_labels: list = ($tmp_rl | get -i $.metadata.labels | default [])
+        let existing_common_labels: list = ($tmp_rl | get -i $.spec.commonMetadata.labels | default [])
+        let all_labels: list = ($existing_labels | merge $new_labels)
+        let all_common_labels: list = ($existing_common_labels | merge $new_labels)
+
+        $tmp_rl
+        | (
+            patch resource upsert key
+                $.metadata.labels
+                $all_labels
+                "kustomize.toolkit.fluxcd.io/v1" Kustomization $kustomization_name $kustomization_namespace
+        )
+        | (
+            patch resource upsert key
+                $.spec.commonMetadata.labels
+                $all_common_labels
+                "kustomize.toolkit.fluxcd.io/v1" Kustomization $kustomization_name $kustomization_namespace
+        )
+    } else { $in }
+    # Enable (or append) some `.metadata.annotations` and `.spec.commonMetadata.annotations`, if applicable
+    | if ($new_annotations | is-not-empty) {
+        let tmp_rl: record = $in
+        let existing_annotations: list = ($tmp_rl | get -i $.metadata.annotations | default [])
+        let existing_common_annotations: list = ($tmp_rl | get -i $.spec.commonMetadata.annotations | default [])
+        let all_annotations: list = ($existing_annotations | merge $new_annotations)
+        let all_common_annotations: list = ($existing_common_annotations | merge $new_annotations)
+
+        $tmp_rl
+        | (
+            patch resource upsert key
+                $.metadata.annotations
+                $all_annotations
+                "kustomize.toolkit.fluxcd.io/v1" Kustomization $kustomization_name $kustomization_namespace
+        )
+        | (
+            patch resource upsert key
+                $.spec.commonMetadata.annotations
+                $all_common_annotations
+                "kustomize.toolkit.fluxcd.io/v1" Kustomization $kustomization_name $kustomization_namespace
+        )
+    } else { $in }
+    # Enable (or append) some `.spec.images` replacements, if applicable
+    | if ($images | is-not-empty) {
+        let tmp_rl: record = $in
+        let existing_images: list = ($tmp_rl | get -i $.spec.images | default [])
+        let all_images: list = ($existing_images ++ $images) | uniq
+
+        $tmp_rl
+        | (
+            patch resource upsert key
+                $.spec.images $all_images
+                "kustomize.toolkit.fluxcd.io/v1" Kustomization $kustomization_name $kustomization_namespace
+        )
+    } else { $in }
+}
+
+
+# Apply the `helmreleaseset` transformation to ResourceList received from stdin according to the specification of the Brick.
+# The `basic` Brick transformation just does a cleanup and regularization of the target Kustomization
+export def "transform helmreleaseset" [
+    brick: record  # Brick specification
+]: [
+    record -> record
+] {
+    # Input ReleaseList after basic transformations
+    let rl: record = $in
+
+    # Get the key parts
+    let brick_name: string = ($brick | get -i name | default "untitled-brick")
+    let kustomization_name: string = ($brick | get $.kustomization.name)
+    let kustomization_namespace: string = ($brick | get -i $.kustomization.namespace | default "flux-system")
+    let hrset_values: list<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
index 0000000..c82b4d3
--- /dev/null
+++ b/docker/osm-nushell-krm-functions/operations/custom/mod.nu
@@ -0,0 +1,109 @@
+#######################################################################################
+# Copyright ETSI Contributors and Others.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#######################################################################################
+
+# Placeholder module for supporting custom transformations
+
+# Import SDK modules
+use ../../krm *
+use ../location.nu
+
+
+# Placeholder for a custom "create" transformation for a Brick of `custom`, `custom-hr`, or `full-custom` types, to be applied to the ResourceList received from stdin.
+# - If the Brick is of `custom` type, a `basic` Brick transformation will be applied right before in the pipeline.
+# - If the Brick is of `custom-hr` type, a `helmreleaseset` Brick transformation will be applied right before in the pipeline.
+# - If the Brick is of `full-custom` type, only this transformation will be applied to the original ResourceList.
+export def "create" [
+    brick: record  # Brick specification
+    environment: record = {}    # Record with environment variables to load
+]: [
+    record -> record
+] {
+    let rl: record = $in
+
+    # Get the key parts
+    let brick_name: string = ($brick | get -i name | default "untitled-brick")
+    let brick_type: string = ($brick | get -i type | default "basic" | str downcase)
+    let kustomization_name: string = ($brick | get $.kustomization.name)
+    let kustomization_namespace: string = ($brick | get -i $.kustomization.namespace | default "flux-system")
+
+    # Here would come your custom transformations over `rl`
+    # . . .
+    print $"Here we are applying a custom `create` transformation of '($brick_type)' type."
+    # The print above is just informative. Please remove in your final custom transformation.
+
+    # Here we should return the result of the custom transformations.
+    # For the sake of the example, let's return just the original ResouceList with no transformations
+    $rl
+}
+
+
+# Placeholder for a custom "update" transformation for a Brick of `custom`, `custom-hr`, or `full-custom` types, to be applied to the ResourceList received from stdin.
+# - If the Brick is of `custom` type, a `basic` Brick transformation will be applied right before in the pipeline.
+# - If the Brick is of `custom-hr` type, a `helmreleaseset` Brick transformation will be applied right before in the pipeline.
+# - If the Brick is of `full-custom` type, only this transformation will be applied to the original ResourceList.
+export def "update" [
+    brick: record  # Brick specification
+    environment: record = {}    # Record with environment variables to load
+]: [
+    record -> record
+] {
+    let rl: record = $in
+
+    # Get the key parts
+    let brick_name: string = ($brick | get -i name | default "untitled-brick")
+    let brick_type: string = ($brick | get -i type | default "basic" | str downcase)
+    let kustomization_name: string = ($brick | get $.kustomization.name)
+    let kustomization_namespace: string = ($brick | get -i $.kustomization.namespace | default "flux-system")
+
+    # Here would come your custom transformations over `rl`
+    # . . .
+    print $"Here we are applying a custom `update` transformation of '($brick_type)' type."
+    # The print above is just informative. Please remove in your final custom transformation.
+
+    # Here we should return the result of the custom transformations.
+    # For the sake of the example, let's return just the original ResouceList with no transformations
+    $rl
+}
+
+
+# Placeholder for a custom "delete" transformation for a Brick of `custom`, `custom-hr`, or `full-custom` types, to be applied to the ResourceList received from stdin.
+# - If the Brick is of `custom` type, a `basic` Brick transformation will be applied right before in the pipeline.
+# - If the Brick is of `custom-hr` type, a `helmreleaseset` Brick transformation will be applied right before in the pipeline.
+# - If the Brick is of `full-custom` type, only this transformation will be applied to the original ResourceList.
+export def "delete" [
+    brick: record  # Brick specification
+    environment: record = {}    # Record with environment variables to load
+]: [
+    record -> record
+] {
+    let rl: record = $in
+
+    # Get the key parts
+    let brick_name: string = ($brick | get -i name | default "untitled-brick")
+    let brick_type: string = ($brick | get -i type | default "basic" | str downcase)
+    let kustomization_name: string = ($brick | get $.kustomization.name)
+    let kustomization_namespace: string = ($brick | get -i $.kustomization.namespace | default "flux-system")
+
+    # Here would come your custom transformations over `rl`
+    # . . .
+    print $"Here we are applying a custom `delete` transformation of '($brick_type)' type."
+    # The print above is just informative. Please remove in your final custom transformation.
+
+    # Here we should return the result of the custom transformations.
+    # For the sake of the example, let's return just the original ResouceList with no transformations
+    $rl
+}
diff --git a/docker/osm-nushell-krm-functions/operations/ksu.nu b/docker/osm-nushell-krm-functions/operations/ksu.nu
new file mode 100644
index 0000000..ffcc19f
--- /dev/null
+++ b/docker/osm-nushell-krm-functions/operations/ksu.nu
@@ -0,0 +1,233 @@
+#######################################################################################
+# Copyright ETSI Contributors and Others.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#######################################################################################
+
+# Module with custom functions to manage KSUs and their renderization of the corresponding ResourceList into a given target folder.
+
+
+use ../krm *
+use ./location.nu
+use ./pattern.nu
+
+
+# Render a KSU, based on a KSU instance model received from stdin.
+export def create [
+    --dry-run                   # If set, only prints the generated ResourceList(s) along with the target folder(s) (i.e., it does not write to any folder)
+    --print-target-folder       # If set, prints the target folder. Requires --dry-run
+    environment: record = {}    # Record with environment variables to load
+]: [
+    record -> record
+    record -> nothing
+] {
+    # Get KSU structure
+    let in_ksu: record = $in
+
+    # Get the KSU name
+    let ksu_name: string = ($in_ksu | get "name")
+
+    # Add to the environment a key with the KSU name, so that it can be replaced
+    let updated_environment: record = (
+        $environment
+        | upsert $.KSU_NAME $ksu_name
+    )
+
+    # Update the KSU record accordingly
+    let updated_ksu: record = (
+        $in_ksu
+        | replace vars $updated_environment
+    )
+
+    # Get the rest of key parts
+    let sync: bool = ($updated_ksu | get -i "sync" | default false)
+    let target: string = (
+        $updated_ksu
+        | get "target"
+        | location to absolute path
+    )
+    let patterns: list<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
index 0000000..1f568f3
--- /dev/null
+++ b/docker/osm-nushell-krm-functions/operations/location.nu
@@ -0,0 +1,173 @@
+#######################################################################################
+# Copyright ETSI Contributors and Others.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#######################################################################################
+
+# Helper module to manage KSU source or target locations, so that they can be safely translated to well-known paths in the local filesystem.
+
+
+# Helper function to convert a profile type name to the canonical name for that profile type so that it can build a folder path deterministically.
+# NOT EXPORTED
+def "normalize profile type" [
+]: [
+    string -> string
+] {
+    $in
+    | if $in in ["controller", "infra-controller", "infra-controllers", "infra_controller", "infra_controllers"] {
+        "infra-controller-profiles"
+    } else if $in in ["config", "infra-config", "infra-configs", "infra_config", "infra_configs"] {
+        "infra-config-profiles"
+    } else if $in in ["managed", "resources", "managed-resources", "managed_resources"] {
+        "managed-resources"
+    } else if $in in ["app", "apps", "applications", "cnf", "cnfs", "nf", "nfs"] {
+        "app-profiles"
+    } else {
+        $in
+    }
+}
+
+
+# Helper function to convert an OKA type name to the canonical name for that OKA type so that it can build a folder path deterministically.
+# NOT EXPORTED
+def "normalize oka type" [
+]: [
+    string -> string
+] {
+    $in
+    | if $in in ["controller", "infra-controller", "infra-controllers", "infra_controller", "infra_controllers"] {
+        "infra-controllers"
+    } else if $in in ["config", "infra-config", "infra-configs", "infra_config", "infra_configs"] {
+        "infra-configs"
+    } else if $in in ["managed", "resources", "managed-resources", "managed_resources", "cloud-resources", "cloud_resources"] {
+        "cloud-resources"
+    } else if $in in ["app", "apps", "applications", "cnf", "cnfs", "nf", "nfs"] {
+        "apps"
+    } else {
+        $in
+    }
+}
+
+
+# Convert a location into its components to determine a path in the local filesystem.
+export def "to path components" [
+    default_project_name: string = "osm_admin"  # Default project name
+    default_repos_base: string = "/repos"  # Base path for the local repo clones
+]: [
+    record -> list<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
index 0000000..9be239b
--- /dev/null
+++ b/docker/osm-nushell-krm-functions/operations/mod.nu
@@ -0,0 +1,30 @@
+#######################################################################################
+# Copyright ETSI Contributors and Others.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#######################################################################################
+
+# Meta-module of custom commands for common operations managing and modifying App intents in a generalized fashion.
+# This meta-module comprises all the modules of with the high-level commands for App Modelling.
+
+
+# Import SDK modules
+use ../krm *
+
+# Import submodules
+export module ./app.nu
+export module ./ksu.nu
+export module ./pattern.nu
+export module ./brick.nu
+export module ./location.nu
diff --git a/docker/osm-nushell-krm-functions/operations/pattern.nu b/docker/osm-nushell-krm-functions/operations/pattern.nu
new file mode 100644
index 0000000..d59842e
--- /dev/null
+++ b/docker/osm-nushell-krm-functions/operations/pattern.nu
@@ -0,0 +1,83 @@
+#######################################################################################
+# Copyright ETSI Contributors and Others.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#######################################################################################
+
+# Module with custom functions to manage a Pattern definition, taking into account its corresponding source template and the set of transformations specified for its constituent Bricks.
+
+
+use ../krm *
+use ./replace.nu
+use ./location.nu
+use ./brick.nu
+
+# Generate a ResourceList based on a Pattern instance model received from stdin.
+#
+# Initially, the ResourceList will be generated from the templates at the `source` location (replacing environment variables as needed), and then the transformations indicated by the Bricks will be applied.
+export def create [
+    environment: record = {}    # Record with environment variables to load
+]: [
+    record -> record
+] {
+    let in_pattern: record = $in
+
+    # Get the pattern name and its parameters
+    let pattern_name: string = ($in_pattern | get "name")
+    let pattern_params: record = (
+        $in_pattern
+        | get -i "parameters"
+        | default {}
+        # If applicable, update placeholder values at the custom environment parameters
+        | replace vars (
+            $environment
+            | upsert $.PATTERN_NAME $pattern_name
+        )
+    )
+
+    # Update the environment to include the pattern name
+    let updated_environment: record = (
+        $environment
+        | upsert $.PATTERN_NAME $pattern_name
+        | merge $pattern_params
+    )
+
+    # Update the pattern record accordingly
+    let updated_pattern: record = (
+        $in_pattern
+        | replace vars $updated_environment
+    )
+
+    # Get other key parts
+    let src: string = (
+        $updated_pattern
+        | get "source"
+        | location to absolute path
+    )
+    let bricks: list<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
index 0000000..0181c40
--- /dev/null
+++ b/docker/osm-nushell-krm-functions/operations/replace.nu
@@ -0,0 +1,58 @@
+#######################################################################################
+# Copyright ETSI Contributors and Others.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#######################################################################################
+
+# Module with helper functions to manage the replacement of placeholder variables by the values of well-known enviroment variables.
+
+
+# Helper function to replace placeholder variables by the content of their homonym environment variables in a record received from stdin.
+export def vars [
+    environment: record   # Record with environment variables to load
+    defaults: record = {
+        FLEET_REPOS_BASE: "/repos"
+        CATALOG_REPOS_BASE: "/repos"
+        PROJECT_NAME: "osm_admin"
+    }  # Record with default values for the variables to be replaced
+]: [
+    record -> record
+] {
+    let in_record: record = $in
+
+    # Environment with default values when undefined
+    let full_environment: record = (
+        $defaults
+        | merge $environment
+    )
+
+    let variable_enumeration: string = (
+        $full_environment
+        | columns
+        | each { |col|
+            $"\${($col)}"
+        }
+        | str join ","
+    )
+
+    $in_record
+    | to yaml
+    | with-env $full_environment {
+        $in
+        | (
+            ^envsubst $variable_enumeration
+        )
+    }
+    | from yaml
+}
diff --git a/docker/osm-nushell-krm-functions/operations/tests/app.nu b/docker/osm-nushell-krm-functions/operations/tests/app.nu
new file mode 100644
index 0000000..ec5cb18
--- /dev/null
+++ b/docker/osm-nushell-krm-functions/operations/tests/app.nu
@@ -0,0 +1,261 @@
+#######################################################################################
+# Copyright ETSI Contributors and Others.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#######################################################################################
+
+# Tests of App instance management
+
+use ../../krm *
+use ../app.nu
+use ../location.nu
+use ../replace.nu
+
+
+# --- all-in-one example (example 1) ---
+
+export def "test app example one" []: [
+    nothing -> nothing
+] {
+    let expected: list<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
index 0000000..dcd06ac
--- /dev/null
+++ b/docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example1/app-instance-from-model.yaml
@@ -0,0 +1,169 @@
+#######################################################################################
+# Copyright ETSI Contributors and Others.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#######################################################################################
+
+apiVersion: osm.softwaredefinition.io/v1alpha1
+# kind: AppBlueprint
+kind: AppInstantiation
+metadata:
+  name: example1-app
+spec:
+  # description: "Example App Blueprint"
+  description: "Example App Instantiation"
+  version: "1.0.0"
+  ksus:
+  - name: main
+    target:
+      # Absolute path to KSU folder: repo-path + base-path + relative-path
+      repo-path:
+      - ${FLEET_REPOS_BASE}     # Default: `/repos`
+      - fleet             # Repo name.
+      base-path:
+      - ${PROJECT_NAME}   # Project name.
+      - ${PROFILE_TYPE}
+      - ${PROFILE_NAME}
+      relative-path:      # App folder + ksu folder (in case of multiple KSUs)
+      - ${APPNAME}
+      - main
+    patterns:
+    - name: main-pattern
+      source:
+        # Absolute path to OKA folder: repo-path + base-path + relative-path
+        repo-path:
+        - ${CATALOG_REPOS_BASE}     # Default: `/repos`
+        - sw-catalogs       # Repo name
+        base-path:          # Absolute path to OKA folder
+        - apps              # OKA type
+        - example1          # OKA folder
+        relative-path:      # Pattern template folder (default: `templates/`)
+        - templates
+        - main-pattern
+      bricks:
+      - name: main-brick
+        type: basic
+        # type: HelmReleaseSet
+        kustomization:
+          name: main-kustomization-${APPNAME}
+          # OPTIONAL:
+          # namespace: flux-system
+        source:             # Absolute path to manifests referenced by the Kustomization (brick folder): base-path + relative-path
+          repo-path:
+          - ${CATALOG_REPOS_BASE}     # Default: `/repos`
+          - sw-catalogs       # Repo name
+          base-path:          # Absolute path to OKA folder
+          - apps              # OKA type
+          - example1          # OKA folder
+          relative-path:
+          - manifests
+          - main-pattern
+          - main-brick-manifests
+      - name: database-brick
+        type: HelmReleaseSet
+        kustomization:
+          name: database-kustomization-${APPNAME}
+          # OPTIONAL:
+          # namespace: flux-system
+        source:
+          repo-path:
+          - ${CATALOG_REPOS_BASE}     # Default: `/repos`
+          - sw-catalogs       # Repo name
+          base-path:          # Absolute path to OKA folder
+          - apps              # OKA type
+          - example1          # OKA folder
+          relative-path:
+          - manifests
+          - main-pattern
+          - database-manifests
+        public-age-key: age18juyaw9kvzgkpqx8kun2n7qtvqva6ajj7ulse435thkgnlfjhf2qj76h64
+        hrset-values:
+          - name: db-operator
+            HelmRelease:
+              name: postgres-operator-${APPNAME}
+              namespace: ${APPNAMESPACE}
+            inline-values:
+              key1: value1
+              key2: value2
+            valuesFrom:
+              configMapKeyRef:
+                name: postgres-operator-cm-${APPNAME}
+                # OPTIONAL:
+                # key: values.yaml
+              secretKeyRef:
+                name: postgres-operator-secret-${APPNAME}
+                # OPTIONAL:
+                # key: values.yaml
+            create-cm:      # If the map is empty, the ConfigMap will not be created
+              cm-key1: cm-value1
+              cm-key2: cm-value2
+            create-secret:  # If the map is empty, the Secret will not be created
+              env-values-reference: secret-values-for-postgres-operator-${APPNAME}
+          - name: db-op-user-interface
+            HelmRelease:
+              name: postgres-operator-ui-${APPNAME}
+              namespace: ${APPNAMESPACE}
+            inline-values:
+              key1: value1
+              key2: value2
+            valuesFrom:
+              configMapKeyRef:
+                name: postgres-operator-ui-cm-${APPNAME}
+                # OPTIONAL:
+                # key: values.yaml
+              secretKeyRef:
+                name: postgres-operator-ui-secret-${APPNAME}
+                # OPTIONAL:
+                # key: values.yaml
+            create-cm:      # If the map is empty, the ConfigMap will not be created
+              cm-key1: cm-value1
+              cm-key2: cm-value2
+            create-secret:  # If the map is empty, the Secret will not be created
+              env-values-reference: secret-values-for-postgres-operator-ui-${APPNAME}
+  # parameters:
+  #   - name: replicaCount
+  #     description: "Number of replicas"
+  #     # default: 1
+  #     value: 1
+  #   - name: ingressHost
+  #     description: "Ingress hostname"
+  #     value: "ingress for-${APPNAME}"
+  # readinessChecks:
+  #   - resource:
+  #       kind: Deployment
+  #       name: main-deployment-${APPNAME}
+  #     condition:
+  #       type: Available
+  #       status: "True"
+  #   - resource:
+  #       kind: Service
+  #       name: main-service
+  #     condition:
+  #       type: Ready
+  # TODO:
+  # outputValues:
+  #   - name: apiEndpoint
+  #     valueFrom:
+  #       serviceIP:
+  #         name: main-service
+  #   - name: adminPassword
+  #     valueFrom:
+  #       secretKeyRef:
+  #         name: app-secrets
+  #         key: admin-password
+  # TODO:
+  # dependencies:
+  #   - name: nginx-ingress
+  #     type: InfraController
+  #     version: ">=1.0.0"
diff --git a/docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example1/expected_result.yaml b/docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example1/expected_result.yaml
new file mode 100644
index 0000000..ae6fa90
--- /dev/null
+++ b/docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example1/expected_result.yaml
@@ -0,0 +1,218 @@
+#######################################################################################
+# Copyright ETSI Contributors and Others.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#######################################################################################
+
+- apiVersion: config.kubernetes.io/v1
+  kind: ResourceList
+  items:
+  - apiVersion: kustomize.toolkit.fluxcd.io/v1
+    kind: Kustomization
+    metadata:
+      name: database-kustomization-myapp01
+      namespace: flux-system
+      annotations:
+        config.kubernetes.io/index: '0'
+        config.kubernetes.io/path: ks-database.yaml
+        internal.config.kubernetes.io/index: '0'
+        internal.config.kubernetes.io/path: ks-database.yaml
+        internal.config.kubernetes.io/seqindent: compact
+    spec:
+      interval: 1h0m0s
+      path: apps/example1/manifests/main-pattern/database-manifests
+      prune: true
+      sourceRef:
+        kind: GitRepository
+        name: sw-catalogs
+        namespace: flux-system
+      targetNamespace: app-namespace
+      postBuild:
+        substitute:
+          appname: myapp01
+          appnamespace: app-namespace
+      wait: true
+      patches:
+      - target:
+          kind: HelmRelease
+          name: postgres-operator-myapp01
+          namespace: app-namespace
+        patch: |
+          - op: add
+            path: /spec/values
+            value:
+              key1: value1
+              key2: value2
+      - target:
+          kind: HelmRelease
+          name: postgres-operator-myapp01
+          namespace: app-namespace
+        patch: |
+          - op: add
+            path: /spec/valuesFrom/-
+            value:
+              kind: ConfigMap
+              name: postgres-operator-cm-myapp01
+              key: values.yaml
+      - target:
+          kind: HelmRelease
+          name: postgres-operator-myapp01
+          namespace: app-namespace
+        patch: |
+          - op: add
+            path: /spec/valuesFrom/-
+            value:
+              kind: Secret
+              name: postgres-operator-secret-myapp01
+              key: values.yaml
+      - target:
+          kind: HelmRelease
+          name: postgres-operator-ui-myapp01
+          namespace: app-namespace
+        patch: |
+          - op: add
+            path: /spec/values
+            value:
+              key1: value1
+              key2: value2
+      - target:
+          kind: HelmRelease
+          name: postgres-operator-ui-myapp01
+          namespace: app-namespace
+        patch: |
+          - op: add
+            path: /spec/valuesFrom/-
+            value:
+              kind: ConfigMap
+              name: postgres-operator-ui-cm-myapp01
+              key: values.yaml
+      - target:
+          kind: HelmRelease
+          name: postgres-operator-ui-myapp01
+          namespace: app-namespace
+        patch: |
+          - op: add
+            path: /spec/valuesFrom/-
+            value:
+              kind: Secret
+              name: postgres-operator-ui-secret-myapp01
+              key: values.yaml
+  - apiVersion: kustomize.toolkit.fluxcd.io/v1
+    kind: Kustomization
+    metadata:
+      name: main-kustomization-myapp01
+      namespace: flux-system
+      annotations:
+        config.kubernetes.io/index: '0'
+        config.kubernetes.io/path: ks-main.yaml
+        internal.config.kubernetes.io/index: '0'
+        internal.config.kubernetes.io/path: ks-main.yaml
+        internal.config.kubernetes.io/seqindent: compact
+    spec:
+      interval: 1h0m0s
+      path: apps/example1/manifests/main-pattern/main-brick-manifests
+      prune: true
+      sourceRef:
+        kind: GitRepository
+        name: sw-catalogs
+        namespace: flux-system
+      targetNamespace: app-namespace
+      postBuild:
+        substitute:
+          app_name: myapp01
+      wait: true
+  - apiVersion: v1
+    kind: ConfigMap
+    metadata:
+      name: postgres-operator-cm-myapp01
+      namespace: app-namespace
+      annotations:
+        config.kubernetes.io/path: postgres-operator-cm-myapp01.yaml
+        internal.config.kubernetes.io/path: postgres-operator-cm-myapp01.yaml
+    data:
+      values.yaml: |-
+        cm-key1: cm-value1
+        cm-key2: cm-value2
+  - apiVersion: v1
+    kind: Secret
+    metadata:
+      name: postgres-operator-secret-myapp01
+      namespace: app-namespace
+      annotations:
+        config.kubernetes.io/path: postgres-operator-secret-myapp01.yaml
+        internal.config.kubernetes.io/path: postgres-operator-secret-myapp01.yaml
+    data:
+      values.yaml: ENC[AES256_GCM,data:wzm5y3kpbk+ZVM2TsNd6D4rmEZ7FU5jxM3dZiGILjZ7Hy00oS0ua9nwc0WU+IwSz0S4m7RiETYRnWITELpq0xoogCXRVIz7SkOivWE6/lv+H0SgQOqZ0iDHZgE/kly8S61kh1QGAeCCUZcpUyXwjgZ1S/iwZqAlVl2cizkGvYMMdgJAGBqTrXakM6LH10J5JBZX/05Cy9Y1rSzGDl6XsJwpnj0znyYIAGsq97nsjctzVGUX0TTIvE03de+T5ZB/nxOZrpcsUSbrbHBj4J5/uijOzV4NCjZ6OV7PWl64fKlpVcLt8z5geO9RMVBT/WlhV,iv:bGxvvh4umOmM7iRHiu0i6lWjWeD1yOkhPGSiMGDgkq0=,tag:v0Og8/hjsEuPLB1wmXSWSw==,type:str]
+    sops:
+      kms: []
+      gcp_kms: []
+      azure_kv: []
+      hc_vault: []
+      age:
+      - recipient: age18juyaw9kvzgkpqx8kun2n7qtvqva6ajj7ulse435thkgnlfjhf2qj76h64
+        enc: |
+          -----BEGIN AGE ENCRYPTED FILE-----
+          YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBHV3d4R2JzS0VmazdCUXBR
+          citpNE9uVmdGbm8vNWdCK3dQR2NObFRaQlVFCjJJZGU5b25pRC8zQ0NOT1J1eEcr
+          bzBzdHE5ZlpBelJaSnpLVGlhcmp3QUkKLS0tIC9Fam5DcTZHQ0pzK3o5N0o2MVJr
+          c3krQTlvU2FXckxxTUkraDBRMWJwd3cK0sd3dx5Arzh4XxJVUFOi1cPnF9UIsv18
+          VXKeyoIOK8pvPS5c4UvCTfBFdYs69Sg6+6FfbCoFJepaT+VfaM1XPA==
+          -----END AGE ENCRYPTED FILE-----
+      lastmodified: 2025-04-29T18:26:22Z
+      mac: ENC[AES256_GCM,data:io3iCpdT61n7ECEkyM2KhBgp9oZ49I2YBjuUA8HQSLyDDzFrNNeZh0bVOWI3HT89egkZDt8nmj8+A4Bx9sC666iBcqcFx+VuH//Db7SZXYJXFm1qNP+lt+3ic4fesJgYKG5ld+7FtB1ApKccmE7Ri0yrWGYsrMJh2aRK6T3VyzQ=,iv:9lvXUEPwIZxphwEc91QCPPSxkVNIiUAZWQWby502FJA=,tag:0IxYm+VLLKOoZ80FQlLspQ==,type:str]
+      pgp: []
+      encrypted_regex: ^(data|stringData)$
+      version: 3.8.1
+  - apiVersion: v1
+    kind: ConfigMap
+    metadata:
+      name: postgres-operator-ui-cm-myapp01
+      namespace: app-namespace
+      annotations:
+        config.kubernetes.io/path: postgres-operator-ui-cm-myapp01.yaml
+        internal.config.kubernetes.io/path: postgres-operator-ui-cm-myapp01.yaml
+    data:
+      values.yaml: |-
+        cm-key1: cm-value1
+        cm-key2: cm-value2
+  - apiVersion: v1
+    kind: Secret
+    metadata:
+      name: postgres-operator-ui-secret-myapp01
+      namespace: app-namespace
+      annotations:
+        config.kubernetes.io/path: postgres-operator-ui-secret-myapp01.yaml
+        internal.config.kubernetes.io/path: postgres-operator-ui-secret-myapp01.yaml
+    data:
+      values.yaml: ENC[AES256_GCM,data:37zRz8FafCMe/cfZGI9Ojx+DP1Y09C5C4P6E4wkPZJfqb8brliWAzQ14cOHZgeOQBugcuguMLTacGz+QCDvU9u5ZFs9ouXY3pWbs9wa4Ywoaum8T2gK1edzEoQ9U0QG5wqndwti4p1wo8Im6+A/VOpFXcEGXdzSLfxcryTGA5EXBtwG9/SvhTvx6CCZGvWcNtjqjJjwNOOtFyJJFBuc7CrWFgYliSwwZ2q4INJhDTrrQ81zKDzdkW/ac+uapey/XQ3lYK56n7K+Z+hxoBJLYRfsRdtBCXxXpThfYXPu608WDHUcedjitATsk7NwA/F+mqmm1T5FO7ePspSaTZgvhBMi+OP1HyIgKbmiEhg==,iv:dVYZTjkNYAgNpFUvFTIiyEFM51tv45G1yp1NPG/xJKQ=,tag:xcrQZq1swfcjfPjcNgN0JQ==,type:str]
+    sops:
+      kms: []
+      gcp_kms: []
+      azure_kv: []
+      hc_vault: []
+      age:
+      - recipient: age18juyaw9kvzgkpqx8kun2n7qtvqva6ajj7ulse435thkgnlfjhf2qj76h64
+        enc: |
+          -----BEGIN AGE ENCRYPTED FILE-----
+          YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBqUS9vaVRVbk9DTkpreVNU
+          Y0t6TW9sbk9Ua1hWSUdVa0hrOWVVSkZIbkVnCnpVcUpxdkQweWw1MjhBVldkaXVX
+          OTRlYW9jQ3ZxNnhiMGExanJYNU9oREkKLS0tIGl3U201ZmNPM2xaY0FyOWZsZGNs
+          aFBQTU1BK3BTSWZ6djFMRDFkbjkrYnMKX/Uo7ePikgbnOfewdHSmCXE4aMfZU0IQ
+          jr9ZlHVGPvaS6yy7fezVZOJI++nwF7fyXZfJW9mm0rSRBy5vnKQYNg==
+          -----END AGE ENCRYPTED FILE-----
+      lastmodified: 2025-04-29T18:26:22Z
+      mac: ENC[AES256_GCM,data:h0ZEFSy0jijGOmjKIFWi5GOfGYgSCWg2BNgMFXT2AwwR2j/xcJWBwFvKPzy7X028Onz0aNmnPa7hf4eI8HS2pp4FEeUbTp8Z+X8it01XDbhmXqnykeHRU40CaKtqaNBbT2uKa/jpT5KgZl+2mnLUL9VU1vlu7BFIQrYPPtOTxgI=,iv:cBSpvEZHFGEAVqWyXUF9OtmuwN4DM3nS3ZgJepUknZ0=,tag:W2utUcIePQpqHkZYV4FiFA==,type:str]
+      pgp: []
+      encrypted_regex: ^(data|stringData)$
+      version: 3.8.1
diff --git a/docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example1/manifests/main-pattern/database-manifests/hrset-database.yaml b/docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example1/manifests/main-pattern/database-manifests/hrset-database.yaml
new file mode 100644
index 0000000..8623232
--- /dev/null
+++ b/docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example1/manifests/main-pattern/database-manifests/hrset-database.yaml
@@ -0,0 +1,58 @@
+#######################################################################################
+# Copyright ETSI Contributors and Others.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#######################################################################################
+
+---
+apiVersion: helm.toolkit.fluxcd.io/v2beta1
+kind: HelmRelease
+metadata:
+  name: postgres-operator-${appname}
+  namespace: ${appnamespace}
+spec:
+  chart:
+    spec:
+      chart: postgres-operator
+      reconcileStrategy: ChartVersion
+      sourceRef:
+        kind: HelmRepository
+        name: postgres-operator-charts-${appname}
+        namespace: ${appnamespace}
+  interval: 3m0s
+  targetNamespace: ${appnamespace}
+  values: {}
+
+---
+
+apiVersion: helm.toolkit.fluxcd.io/v2beta1
+kind: HelmRelease
+metadata:
+  name: postgres-operator-ui-${appname}
+  namespace: ${appnamespace}
+spec:
+  dependsOn:
+  - name: postgres-operator-${appname}
+    namespace: ${appnamespace}
+  chart:
+    spec:
+      chart: postgres-operator-ui
+      reconcileStrategy: ChartVersion
+      sourceRef:
+        kind: HelmRepository
+        name: postgres-operator-charts-${appname}
+        namespace: ${appnamespace}
+  interval: 3m0s
+  targetNamespace: ${appnamespace}
+  values: {}
diff --git a/docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example1/manifests/main-pattern/database-manifests/repo-zalando-postgres.yaml b/docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example1/manifests/main-pattern/database-manifests/repo-zalando-postgres.yaml
new file mode 100644
index 0000000..eaf2dda
--- /dev/null
+++ b/docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example1/manifests/main-pattern/database-manifests/repo-zalando-postgres.yaml
@@ -0,0 +1,29 @@
+#######################################################################################
+# Copyright ETSI Contributors and Others.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#######################################################################################
+
+---
+apiVersion: source.toolkit.fluxcd.io/v1beta2
+kind: HelmRepository
+metadata:
+  name: postgres-operator-charts-${appname}
+  namespace: ${appnamespace}
+spec:
+  interval: 10m0s
+  # type: oci
+  # url: oci://registry-1.docker.io/bitnamicharts
+  type: default
+  url: https://opensource.zalando.com/postgres-operator/charts/postgres-operator
diff --git a/docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example1/manifests/main-pattern/main-brick-manifests/configmap.yaml b/docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example1/manifests/main-pattern/main-brick-manifests/configmap.yaml
new file mode 100644
index 0000000..5bd5c50
--- /dev/null
+++ b/docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example1/manifests/main-pattern/main-brick-manifests/configmap.yaml
@@ -0,0 +1,23 @@
+#######################################################################################
+# Copyright ETSI Contributors and Others.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#######################################################################################
+
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: my-cm-${appname}
+data:
+  my-key: my-value
diff --git a/docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example1/templates/main-pattern/ks-database.yaml b/docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example1/templates/main-pattern/ks-database.yaml
new file mode 100644
index 0000000..af2831c
--- /dev/null
+++ b/docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example1/templates/main-pattern/ks-database.yaml
@@ -0,0 +1,36 @@
+#######################################################################################
+# Copyright ETSI Contributors and Others.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#######################################################################################
+
+---
+apiVersion: kustomize.toolkit.fluxcd.io/v1
+kind: Kustomization
+metadata:
+  name: database-kustomization-${APPNAME}
+  namespace: flux-system
+spec:
+  interval: 1h0m0s
+  path: ./apps/example1/manifests/main-pattern/database-manifests
+  prune: true
+  sourceRef:
+    kind: GitRepository
+    name: sw-catalogs
+    namespace: flux-system
+  targetNamespace: ${APPNAMESPACE}
+  postBuild:
+    substitute:
+      appname: ${APPNAME}
+      appnamespace: ${APPNAMESPACE}
diff --git a/docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example1/templates/main-pattern/ks-main.yaml b/docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example1/templates/main-pattern/ks-main.yaml
new file mode 100644
index 0000000..d80fe76
--- /dev/null
+++ b/docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example1/templates/main-pattern/ks-main.yaml
@@ -0,0 +1,35 @@
+#######################################################################################
+# Copyright ETSI Contributors and Others.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#######################################################################################
+
+---
+apiVersion: kustomize.toolkit.fluxcd.io/v1
+kind: Kustomization
+metadata:
+  name: main-kustomization-${APPNAME}
+  namespace: flux-system
+spec:
+  interval: 1h0m0s
+  path: ./apps/example1/manifests/main-pattern/main-brick-manifests
+  prune: true
+  sourceRef:
+    kind: GitRepository
+    name: sw-catalogs
+    namespace: flux-system
+  targetNamespace: ${APPNAMESPACE}
+  postBuild:
+    substitute:
+      app_name: ${APPNAME}
diff --git a/docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example2/app-instance-from-model.yaml b/docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example2/app-instance-from-model.yaml
new file mode 100644
index 0000000..fe248e4
--- /dev/null
+++ b/docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example2/app-instance-from-model.yaml
@@ -0,0 +1,270 @@
+#######################################################################################
+# Copyright ETSI Contributors and Others.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#######################################################################################
+
+apiVersion: osm.softwaredefinition.io/v1alpha1
+# kind: AppBlueprint
+kind: AppInstantiation
+metadata:
+  name: example2-app
+spec:
+  # description: "Example App Blueprint"
+  description: "Example App Instantiation"
+  version: "1.0.0"
+  ksus:
+  - name: main
+    target:
+      # Absolute path to KSU folder: repo-path + base-path + relative-path
+      repo-path:
+      - ${FLEET_REPOS_BASE}     # Default: `/repos`
+      - fleet             # Repo name.
+      base-path:
+      - ${PROJECT_NAME}   # Project name.
+      - ${PROFILE_TYPE}
+      - ${PROFILE_NAME}
+      relative-path:      # App folder + ksu folder (in case of multiple KSUs)
+      - ${APPNAME}
+      - main
+    patterns:
+    - name: active-pattern
+      source:
+        # Absolute path to OKA folder: repo-path + base-path + relative-path
+        repo-path:
+        - ${CATALOG_REPOS_BASE}     # Default: `/repos`
+        - sw-catalogs       # Repo name
+        base-path:          # Absolute path to OKA folder
+        - apps              # OKA type
+        - example2          # OKA folder
+        relative-path:      # Pattern template folder (default: `templates/`)
+        - templates
+        - main-pattern
+      # Pattern-specific parameters, to be added as needed (optional)
+      parameters:
+        EXAMPLE_PARAMETER1: active-example-value1
+        EXAMPLE_PARAMETER2: active-example-value2
+      bricks:
+      - name: main-brick
+        type: basic
+        # type: HelmReleaseSet
+        kustomization:
+          name: ks-${APPNAME}-${PATTERN_NAME}
+          # OPTIONAL:
+          # namespace: flux-system
+        source:             # Absolute path to manifests referenced by the Kustomization (brick folder): base-path + relative-path
+          repo-path:
+          - ${CATALOG_REPOS_BASE}     # Default: `/repos`
+          - sw-catalogs       # Repo name
+          base-path:          # Absolute path to OKA folder
+          - apps              # OKA type
+          - example2          # OKA folder
+          relative-path:
+          - manifests
+          - main-pattern
+          - main-brick-manifests
+      - name: database-brick
+        type: HelmReleaseSet
+        kustomization:
+          name: database-kustomization-${APPNAME}-${PATTERN_NAME}
+          # OPTIONAL:
+          # namespace: flux-system
+        source:
+          repo-path:
+          - ${CATALOG_REPOS_BASE}     # Default: `/repos`
+          - sw-catalogs       # Repo name
+          base-path:          # Absolute path to OKA folder
+          - apps              # OKA type
+          - example2          # OKA folder
+          relative-path:
+          - manifests
+          - main-pattern
+          - database-manifests
+        # To be inserted
+        public-age-key: age18juyaw9kvzgkpqx8kun2n7qtvqva6ajj7ulse435thkgnlfjhf2qj76h64
+        hrset-values:
+          - name: db-operator
+            HelmRelease:
+              name: postgres-operator-${APPNAME}-${PATTERN_NAME}
+              namespace: ${APPNAMESPACE}
+            inline-values:
+              key1: value1
+              key2: value2
+            valuesFrom:
+              configMapKeyRef:
+                name: postgres-operator-cm-${APPNAME}-${PATTERN_NAME}
+                # OPTIONAL:
+                # key: values.yaml
+              secretKeyRef:
+                name: postgres-operator-secret-${APPNAME}-${PATTERN_NAME}
+                # OPTIONAL:
+                # key: values.yaml
+            create-cm:      # If the map is empty, the ConfigMap will not be created
+              cm-key1: cm-value1
+              cm-key2: cm-value2
+            create-secret:  # If the map is empty, the Secret will not be created
+              env-values-reference: secret-values-for-postgres-operator-${APPNAME}
+          - name: db-op-user-interface
+            HelmRelease:
+              name: postgres-operator-ui-${APPNAME}-${PATTERN_NAME}
+              namespace: ${APPNAMESPACE}
+            inline-values:
+              key1: value1
+              key2: value2
+            valuesFrom:
+              configMapKeyRef:
+                name: postgres-operator-ui-cm-${APPNAME}-${PATTERN_NAME}
+                # OPTIONAL:
+                # key: values.yaml
+              secretKeyRef:
+                name: postgres-operator-ui-secret-${APPNAME}-${PATTERN_NAME}
+                # OPTIONAL:
+                # key: values.yaml
+            create-cm:      # If the map is empty, the ConfigMap will not be created
+              cm-key1: cm-value1
+              cm-key2: cm-value2
+            create-secret:  # If the map is empty, the Secret will not be created
+              env-values-reference: secret-values-for-postgres-operator-ui-${APPNAME}
+    - name: standby-pattern
+      source:
+        # Absolute path to OKA folder: repo-path + base-path + relative-path
+        repo-path:
+        - ${CATALOG_REPOS_BASE}     # Default: `/repos`
+        - sw-catalogs       # Repo name
+        base-path:          # Absolute path to OKA folder
+        - apps              # OKA type
+        - example2          # OKA folder
+        relative-path:      # Pattern template folder (default: `templates/`)
+        - templates
+        - main-pattern
+      # Pattern-specific parameters, to be added as needed (optional)
+      parameters:
+        EXAMPLE_PARAMETER1: standby-example-value1
+        EXAMPLE_PARAMETER2: standby-example-value2
+      bricks:
+      - name: main-brick
+        type: basic
+        # type: HelmReleaseSet
+        kustomization:
+          name: ks-${APPNAME}-${PATTERN_NAME}
+          # OPTIONAL:
+          # namespace: flux-system
+        source:             # Absolute path to manifests referenced by the Kustomization (brick folder): base-path + relative-path
+          repo-path:
+          - ${CATALOG_REPOS_BASE}     # Default: `/repos`
+          - sw-catalogs       # Repo name
+          base-path:          # Absolute path to OKA folder
+          - apps              # OKA type
+          - example2          # OKA folder
+          relative-path:
+          - manifests
+          - main-pattern
+          - main-brick-manifests
+      - name: database-brick
+        type: HelmReleaseSet
+        kustomization:
+          name: database-kustomization-${APPNAME}-${PATTERN_NAME}
+          # OPTIONAL:
+          # namespace: flux-system
+        source:
+          repo-path:
+          - ${CATALOG_REPOS_BASE}     # Default: `/repos`
+          - sw-catalogs       # Repo name
+          base-path:          # Absolute path to OKA folder
+          - apps              # OKA type
+          - example2          # OKA folder
+          relative-path:
+          - manifests
+          - main-pattern
+          - database-manifests
+        public-age-key: age18juyaw9kvzgkpqx8kun2n7qtvqva6ajj7ulse435thkgnlfjhf2qj76h64
+        hrset-values:
+          - name: db-operator
+            HelmRelease:
+              name: postgres-operator-${APPNAME}-${PATTERN_NAME}
+              namespace: ${APPNAMESPACE}
+            inline-values:
+              key1: value1
+              key2: value2
+            valuesFrom:
+              configMapKeyRef:
+                name: postgres-operator-cm-${APPNAME}-${PATTERN_NAME}
+                # OPTIONAL:
+                # key: values.yaml
+              secretKeyRef:
+                name: postgres-operator-secret-${APPNAME}-${PATTERN_NAME}
+                # OPTIONAL:
+                # key: values.yaml
+            create-cm:      # If the map is empty, the ConfigMap will not be created
+              cm-key1: cm-value1
+              cm-key2: cm-value2
+            create-secret:  # If the map is empty, the Secret will not be created
+              env-values-reference: secret-values-for-postgres-operator-${APPNAME}
+          - name: db-op-user-interface
+            HelmRelease:
+              name: postgres-operator-ui-${APPNAME}-${PATTERN_NAME}
+              namespace: ${APPNAMESPACE}
+            inline-values:
+              key1: value1
+              key2: value2
+            valuesFrom:
+              configMapKeyRef:
+                name: postgres-operator-ui-cm-${APPNAME}-${PATTERN_NAME}
+                # OPTIONAL:
+                # key: values.yaml
+              secretKeyRef:
+                name: postgres-operator-ui-secret-${APPNAME}-${PATTERN_NAME}
+                # OPTIONAL:
+                # key: values.yaml
+            create-cm:      # If the map is empty, the ConfigMap will not be created
+              cm-key1: cm-value1
+              cm-key2: cm-value2
+            create-secret:  # If the map is empty, the Secret will not be created
+              env-values-reference: secret-values-for-postgres-operator-ui-${APPNAME}
+  # parameters:
+  #   - name: replicaCount
+  #     description: "Number of replicas"
+  #     # default: 1
+  #     value: 1
+  #   - name: ingressHost
+  #     description: "Ingress hostname"
+  #     value: "ingress for-${APPNAME}"
+  # readinessChecks:
+  #   - resource:
+  #       kind: Deployment
+  #       name: main-deployment-${APPNAME}
+  #     condition:
+  #       type: Available
+  #       status: "True"
+  #   - resource:
+  #       kind: Service
+  #       name: main-service
+  #     condition:
+  #       type: Ready
+  # TODO:
+  # outputValues:
+  #   - name: apiEndpoint
+  #     valueFrom:
+  #       serviceIP:
+  #         name: main-service
+  #   - name: adminPassword
+  #     valueFrom:
+  #       secretKeyRef:
+  #         name: app-secrets
+  #         key: admin-password
+  # TODO:
+  # dependencies:
+  #   - name: nginx-ingress
+  #     type: InfraController
+  #     version: ">=1.0.0"
diff --git a/docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example2/expected_result.yaml b/docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example2/expected_result.yaml
new file mode 100644
index 0000000..464b258
--- /dev/null
+++ b/docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example2/expected_result.yaml
@@ -0,0 +1,320 @@
+#######################################################################################
+# Copyright ETSI Contributors and Others.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#######################################################################################
+
+- apiVersion: config.kubernetes.io/v1
+  kind: ResourceList
+  items:
+  - apiVersion: kustomize.toolkit.fluxcd.io/v1
+    kind: Kustomization
+    metadata:
+      name: database-kustomization-myapp02-active-pattern
+      namespace: flux-system
+      annotations:
+        config.kubernetes.io/index: '0'
+        config.kubernetes.io/path: ks-database.yaml
+        internal.config.kubernetes.io/index: '0'
+        internal.config.kubernetes.io/path: ks-database.yaml
+        internal.config.kubernetes.io/seqindent: compact
+    spec:
+      interval: 1h0m0s
+      path: apps/example2/manifests/main-pattern/database-manifests
+      prune: true
+      sourceRef:
+        kind: GitRepository
+        name: sw-catalogs
+        namespace: flux-system
+      targetNamespace: app-namespace
+      postBuild:
+        substitute:
+          appname: myapp02
+          appnamespace: app-namespace
+      wait: true
+      patches:
+      - target:
+          kind: HelmRelease
+          name: postgres-operator-myapp02-active-pattern
+          namespace: app-namespace
+        patch: |
+          - op: add
+            path: /spec/values
+            value:
+              key1: value1
+              key2: value2
+      - target:
+          kind: HelmRelease
+          name: postgres-operator-myapp02-active-pattern
+          namespace: app-namespace
+        patch: |
+          - op: add
+            path: /spec/valuesFrom/-
+            value:
+              kind: ConfigMap
+              name: postgres-operator-cm-myapp02-active-pattern
+              key: values.yaml
+      - target:
+          kind: HelmRelease
+          name: postgres-operator-myapp02-active-pattern
+          namespace: app-namespace
+        patch: |
+          - op: add
+            path: /spec/valuesFrom/-
+            value:
+              kind: Secret
+              name: postgres-operator-secret-myapp02-active-pattern
+              key: values.yaml
+      - target:
+          kind: HelmRelease
+          name: postgres-operator-ui-myapp02-active-pattern
+          namespace: app-namespace
+        patch: |
+          - op: add
+            path: /spec/values
+            value:
+              key1: value1
+              key2: value2
+      - target:
+          kind: HelmRelease
+          name: postgres-operator-ui-myapp02-active-pattern
+          namespace: app-namespace
+        patch: |
+          - op: add
+            path: /spec/valuesFrom/-
+            value:
+              kind: ConfigMap
+              name: postgres-operator-ui-cm-myapp02-active-pattern
+              key: values.yaml
+      - target:
+          kind: HelmRelease
+          name: postgres-operator-ui-myapp02-active-pattern
+          namespace: app-namespace
+        patch: |
+          - op: add
+            path: /spec/valuesFrom/-
+            value:
+              kind: Secret
+              name: postgres-operator-ui-secret-myapp02-active-pattern
+              key: values.yaml
+  - apiVersion: kustomize.toolkit.fluxcd.io/v1
+    kind: Kustomization
+    metadata:
+      name: ks-myapp02-active-pattern
+      namespace: flux-system
+      annotations:
+        config.kubernetes.io/index: '0'
+        config.kubernetes.io/path: ks-main.yaml
+        internal.config.kubernetes.io/index: '0'
+        internal.config.kubernetes.io/path: ks-main.yaml
+        internal.config.kubernetes.io/seqindent: compact
+    spec:
+      interval: 1h0m0s
+      path: apps/example2/manifests/main-pattern/main-brick-manifests
+      prune: true
+      sourceRef:
+        kind: GitRepository
+        name: sw-catalogs
+        namespace: flux-system
+      targetNamespace: app-namespace
+      postBuild:
+        substitute:
+          app_name: myapp02
+          example_parameter1: active-example-value1
+          example_parameter2: active-example-value2
+      wait: true
+  - apiVersion: v1
+    kind: ConfigMap
+    metadata:
+      name: postgres-operator-cm-myapp02-active-pattern
+      namespace: app-namespace
+      annotations:
+        config.kubernetes.io/path: postgres-operator-cm-myapp02-active-pattern.yaml
+        internal.config.kubernetes.io/path: postgres-operator-cm-myapp02-active-pattern.yaml
+    data:
+      values.yaml: |-
+        cm-key1: cm-value1
+        cm-key2: cm-value2
+  - apiVersion: v1
+    kind: Secret
+    metadata:
+      name: postgres-operator-secret-myapp02-active-pattern
+      namespace: app-namespace
+      annotations:
+        config.kubernetes.io/path: postgres-operator-secret-myapp02-active-pattern.yaml
+        internal.config.kubernetes.io/path: postgres-operator-secret-myapp02-active-pattern.yaml
+    data:
+      values.yaml: ENCRYPTED
+  - apiVersion: v1
+    kind: ConfigMap
+    metadata:
+      name: postgres-operator-ui-cm-myapp02-active-pattern
+      namespace: app-namespace
+      annotations:
+        config.kubernetes.io/path: postgres-operator-ui-cm-myapp02-active-pattern.yaml
+        internal.config.kubernetes.io/path: postgres-operator-ui-cm-myapp02-active-pattern.yaml
+    data:
+      values.yaml: |-
+        cm-key1: cm-value1
+        cm-key2: cm-value2
+  - apiVersion: kustomize.toolkit.fluxcd.io/v1
+    kind: Kustomization
+    metadata:
+      name: database-kustomization-myapp02-standby-pattern
+      namespace: flux-system
+      annotations:
+        config.kubernetes.io/index: '0'
+        config.kubernetes.io/path: ks-database.yaml
+        internal.config.kubernetes.io/index: '0'
+        internal.config.kubernetes.io/path: ks-database.yaml
+        internal.config.kubernetes.io/seqindent: compact
+    spec:
+      interval: 1h0m0s
+      path: apps/example2/manifests/main-pattern/database-manifests
+      prune: true
+      sourceRef:
+        kind: GitRepository
+        name: sw-catalogs
+        namespace: flux-system
+      targetNamespace: app-namespace
+      postBuild:
+        substitute:
+          appname: myapp02
+          appnamespace: app-namespace
+      wait: true
+      patches:
+      - target:
+          kind: HelmRelease
+          name: postgres-operator-myapp02-standby-pattern
+          namespace: app-namespace
+        patch: |
+          - op: add
+            path: /spec/values
+            value:
+              key1: value1
+              key2: value2
+      - target:
+          kind: HelmRelease
+          name: postgres-operator-myapp02-standby-pattern
+          namespace: app-namespace
+        patch: |
+          - op: add
+            path: /spec/valuesFrom/-
+            value:
+              kind: ConfigMap
+              name: postgres-operator-cm-myapp02-standby-pattern
+              key: values.yaml
+      - target:
+          kind: HelmRelease
+          name: postgres-operator-myapp02-standby-pattern
+          namespace: app-namespace
+        patch: |
+          - op: add
+            path: /spec/valuesFrom/-
+            value:
+              kind: Secret
+              name: postgres-operator-secret-myapp02-standby-pattern
+              key: values.yaml
+      - target:
+          kind: HelmRelease
+          name: postgres-operator-ui-myapp02-standby-pattern
+          namespace: app-namespace
+        patch: |
+          - op: add
+            path: /spec/values
+            value:
+              key1: value1
+              key2: value2
+      - target:
+          kind: HelmRelease
+          name: postgres-operator-ui-myapp02-standby-pattern
+          namespace: app-namespace
+        patch: |
+          - op: add
+            path: /spec/valuesFrom/-
+            value:
+              kind: ConfigMap
+              name: postgres-operator-ui-cm-myapp02-standby-pattern
+              key: values.yaml
+      - target:
+          kind: HelmRelease
+          name: postgres-operator-ui-myapp02-standby-pattern
+          namespace: app-namespace
+        patch: |
+          - op: add
+            path: /spec/valuesFrom/-
+            value:
+              kind: Secret
+              name: postgres-operator-ui-secret-myapp02-standby-pattern
+              key: values.yaml
+  - apiVersion: kustomize.toolkit.fluxcd.io/v1
+    kind: Kustomization
+    metadata:
+      name: ks-myapp02-standby-pattern
+      namespace: flux-system
+      annotations:
+        config.kubernetes.io/index: '0'
+        config.kubernetes.io/path: ks-main.yaml
+        internal.config.kubernetes.io/index: '0'
+        internal.config.kubernetes.io/path: ks-main.yaml
+        internal.config.kubernetes.io/seqindent: compact
+    spec:
+      interval: 1h0m0s
+      path: apps/example2/manifests/main-pattern/main-brick-manifests
+      prune: true
+      sourceRef:
+        kind: GitRepository
+        name: sw-catalogs
+        namespace: flux-system
+      targetNamespace: app-namespace
+      postBuild:
+        substitute:
+          app_name: myapp02
+          example_parameter1: standby-example-value1
+          example_parameter2: standby-example-value2
+      wait: true
+  - apiVersion: v1
+    kind: ConfigMap
+    metadata:
+      name: postgres-operator-cm-myapp02-standby-pattern
+      namespace: app-namespace
+      annotations:
+        config.kubernetes.io/path: postgres-operator-cm-myapp02-standby-pattern.yaml
+        internal.config.kubernetes.io/path: postgres-operator-cm-myapp02-standby-pattern.yaml
+    data:
+      values.yaml: |-
+        cm-key1: cm-value1
+        cm-key2: cm-value2
+  - apiVersion: v1
+    kind: Secret
+    metadata:
+      name: postgres-operator-secret-myapp02-standby-pattern
+      namespace: app-namespace
+      annotations:
+        config.kubernetes.io/path: postgres-operator-secret-myapp02-standby-pattern.yaml
+        internal.config.kubernetes.io/path: postgres-operator-secret-myapp02-standby-pattern.yaml
+    data:
+      values.yaml: ENCRYPTED
+  - apiVersion: v1
+    kind: ConfigMap
+    metadata:
+      name: postgres-operator-ui-cm-myapp02-standby-pattern
+      namespace: app-namespace
+      annotations:
+        config.kubernetes.io/path: postgres-operator-ui-cm-myapp02-standby-pattern.yaml
+        internal.config.kubernetes.io/path: postgres-operator-ui-cm-myapp02-standby-pattern.yaml
+    data:
+      values.yaml: |-
+        cm-key1: cm-value1
+        cm-key2: cm-value2
diff --git a/docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example2/manifests/main-pattern/database-manifests/hrset-database.yaml b/docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example2/manifests/main-pattern/database-manifests/hrset-database.yaml
new file mode 100644
index 0000000..8623232
--- /dev/null
+++ b/docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example2/manifests/main-pattern/database-manifests/hrset-database.yaml
@@ -0,0 +1,58 @@
+#######################################################################################
+# Copyright ETSI Contributors and Others.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#######################################################################################
+
+---
+apiVersion: helm.toolkit.fluxcd.io/v2beta1
+kind: HelmRelease
+metadata:
+  name: postgres-operator-${appname}
+  namespace: ${appnamespace}
+spec:
+  chart:
+    spec:
+      chart: postgres-operator
+      reconcileStrategy: ChartVersion
+      sourceRef:
+        kind: HelmRepository
+        name: postgres-operator-charts-${appname}
+        namespace: ${appnamespace}
+  interval: 3m0s
+  targetNamespace: ${appnamespace}
+  values: {}
+
+---
+
+apiVersion: helm.toolkit.fluxcd.io/v2beta1
+kind: HelmRelease
+metadata:
+  name: postgres-operator-ui-${appname}
+  namespace: ${appnamespace}
+spec:
+  dependsOn:
+  - name: postgres-operator-${appname}
+    namespace: ${appnamespace}
+  chart:
+    spec:
+      chart: postgres-operator-ui
+      reconcileStrategy: ChartVersion
+      sourceRef:
+        kind: HelmRepository
+        name: postgres-operator-charts-${appname}
+        namespace: ${appnamespace}
+  interval: 3m0s
+  targetNamespace: ${appnamespace}
+  values: {}
diff --git a/docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example2/manifests/main-pattern/database-manifests/repo-zalando-postgres.yaml b/docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example2/manifests/main-pattern/database-manifests/repo-zalando-postgres.yaml
new file mode 100644
index 0000000..eaf2dda
--- /dev/null
+++ b/docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example2/manifests/main-pattern/database-manifests/repo-zalando-postgres.yaml
@@ -0,0 +1,29 @@
+#######################################################################################
+# Copyright ETSI Contributors and Others.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#######################################################################################
+
+---
+apiVersion: source.toolkit.fluxcd.io/v1beta2
+kind: HelmRepository
+metadata:
+  name: postgres-operator-charts-${appname}
+  namespace: ${appnamespace}
+spec:
+  interval: 10m0s
+  # type: oci
+  # url: oci://registry-1.docker.io/bitnamicharts
+  type: default
+  url: https://opensource.zalando.com/postgres-operator/charts/postgres-operator
diff --git a/docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example2/manifests/main-pattern/main-brick-manifests/configmap.yaml b/docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example2/manifests/main-pattern/main-brick-manifests/configmap.yaml
new file mode 100644
index 0000000..5bd5c50
--- /dev/null
+++ b/docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example2/manifests/main-pattern/main-brick-manifests/configmap.yaml
@@ -0,0 +1,23 @@
+#######################################################################################
+# Copyright ETSI Contributors and Others.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#######################################################################################
+
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: my-cm-${appname}
+data:
+  my-key: my-value
diff --git a/docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example2/templates/main-pattern/ks-database.yaml b/docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example2/templates/main-pattern/ks-database.yaml
new file mode 100644
index 0000000..6ece0e0
--- /dev/null
+++ b/docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example2/templates/main-pattern/ks-database.yaml
@@ -0,0 +1,36 @@
+#######################################################################################
+# Copyright ETSI Contributors and Others.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#######################################################################################
+
+---
+apiVersion: kustomize.toolkit.fluxcd.io/v1
+kind: Kustomization
+metadata:
+  name: database-kustomization-${APPNAME}-${PATTERN_NAME}
+  namespace: flux-system
+spec:
+  interval: 1h0m0s
+  path: ./apps/example1/manifests/main-pattern/database-manifests
+  prune: true
+  sourceRef:
+    kind: GitRepository
+    name: sw-catalogs
+    namespace: flux-system
+  targetNamespace: ${APPNAMESPACE}
+  postBuild:
+    substitute:
+      appname: ${APPNAME}
+      appnamespace: ${APPNAMESPACE}
diff --git a/docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example2/templates/main-pattern/ks-main.yaml b/docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example2/templates/main-pattern/ks-main.yaml
new file mode 100644
index 0000000..7a39e0f
--- /dev/null
+++ b/docker/osm-nushell-krm-functions/operations/tests/artifacts/sw-catalogs/apps/example2/templates/main-pattern/ks-main.yaml
@@ -0,0 +1,37 @@
+#######################################################################################
+# Copyright ETSI Contributors and Others.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#######################################################################################
+
+---
+apiVersion: kustomize.toolkit.fluxcd.io/v1
+kind: Kustomization
+metadata:
+  name: ks-${APPNAME}-${PATTERN_NAME}
+  namespace: flux-system
+spec:
+  interval: 1h0m0s
+  path: ./apps/example1/manifests/main-pattern/main-brick-manifests
+  prune: true
+  sourceRef:
+    kind: GitRepository
+    name: sw-catalogs
+    namespace: flux-system
+  targetNamespace: ${APPNAMESPACE}
+  postBuild:
+    substitute:
+      app_name: ${APPNAME}
+      example_parameter1: ${EXAMPLE_PARAMETER1}
+      example_parameter2: ${EXAMPLE_PARAMETER2}
diff --git a/docker/osm-nushell-krm-functions/operations/tests/brick.nu b/docker/osm-nushell-krm-functions/operations/tests/brick.nu
new file mode 100644
index 0000000..602a24a
--- /dev/null
+++ b/docker/osm-nushell-krm-functions/operations/tests/brick.nu
@@ -0,0 +1,18 @@
+#######################################################################################
+# Copyright ETSI Contributors and Others.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#######################################################################################
+
+# Module with custom functions to manage the transformations and generations associated to the different types of Building Blocks supported by OSM.
diff --git a/docker/osm-nushell-krm-functions/operations/tests/ksu.nu b/docker/osm-nushell-krm-functions/operations/tests/ksu.nu
new file mode 100644
index 0000000..3180566
--- /dev/null
+++ b/docker/osm-nushell-krm-functions/operations/tests/ksu.nu
@@ -0,0 +1,19 @@
+#######################################################################################
+# Copyright ETSI Contributors and Others.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#######################################################################################
+
+# Module with custom functions to manage KSUs and their renderization of the corresponding ResourceList into a given target folder.
+
diff --git a/docker/osm-nushell-krm-functions/operations/tests/location.nu b/docker/osm-nushell-krm-functions/operations/tests/location.nu
new file mode 100644
index 0000000..6ea15b5
--- /dev/null
+++ b/docker/osm-nushell-krm-functions/operations/tests/location.nu
@@ -0,0 +1,232 @@
+#######################################################################################
+# Copyright ETSI Contributors and Others.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#######################################################################################
+
+# Module with custom functions to manage KSUs and their renderization of the corresponding ResourceList into a given target folder.
+
+
+# Imports
+use std assert
+use ../location.nu *
+
+
+### to path components tests ###
+
+export def "test location to path components repo-path with base-path" [] {
+    let input: record = {
+        "repo-path": ["/custom/repo"],
+        "base-path": ["base/dir"],  # Now unused in base construction
+        "relative-path": ["file.txt"]
+    }
+    let actual: list<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
index 0000000..21f40d6
--- /dev/null
+++ b/docker/osm-nushell-krm-functions/operations/tests/mod.nu
@@ -0,0 +1,55 @@
+#!/usr/bin/env -S nu --stdin
+#######################################################################################
+# Copyright ETSI Contributors and Others.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#######################################################################################
+
+
+use std assert
+# use ../../krm *
+
+use ./location.nu *
+use ./app.nu *
+# use ./ksu.nu *
+# use ./pattern.nu *
+# use ./brick.nu *
+
+
+# Test launcher
+def main [] {
+    print "Running tests..."
+
+    let test_commands: list<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
index 0000000..68acc80
--- /dev/null
+++ b/docker/osm-nushell-krm-functions/operations/tests/pattern.nu
@@ -0,0 +1,18 @@
+#######################################################################################
+# Copyright ETSI Contributors and Others.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#######################################################################################
+
+# Module with custom functions to manage a Pattern definition, taking into account its corresponding source template and the set of transformations specified for its constituent Bricks.