BundleBee is a light Java package manager for Kubernetes applications. This page explains how to get started with it.

Overall architecture

One of the BundleBee goals is to inherit from the well known and supported Apache Maven infrastructure. In other words, the goal is to be able to reuse Maven registries (central, custom nexus, artifactory, …​) to store application descriptors. It also leverage the well know java packaging (jar or zip) to ensure a strong storage reliability in these systems.

One direct benefit is that it becomes very easy to have an immutable system once deployed and therefore a better tracability of what is deployed.

Overall, BundleBee will manipulate what we call alveolus which are basically just a set of descriptors. An alveolus is a zip or a jar which is structured this way:

.root
 `- bundlebee (1)
      |- manifest.json (2)
      `- kubernetes (3)
           |- my-descriptor-1.yaml
           `- my-descriptor-2.yaml
  1. A bundlebee folder contains all the descriptors

  2. A manifest.json contains the list of application or library this alveolus provides

  3. A kubenetes folder contains the list of descriptors used by alveoli

Manifest

Manifest is the central point of the system. It provides the unique identifier of your recipe (application/library) and what to do to set it up.

Here is its configuration specification:

Manifest Descriptor

BundleBee manifest descriptor.

Table 1. Manifest Descriptor
Name JSON Name Type Description

interpolateAlveoli

interpolateAlveoli

bool

Enables to consider all alveoli have their interpolateDescriptors descriptor set to true, you can still set it to false if you want to disable it for one.

Manifest.alveoli

alveoli

array of object

List of described applications/libraries.

Manifest.ignoredLintingRules

ignoredLintingRules

array of object

Ignored linting rule names when using lint command.

Manifest.references

references

array of object

List of files referenced as other manifests. They are merged with this (main) manifest by appending requirements and alveoli. It is relative to this manifest location. Important: it is only about the same module references, external references are dependencies in an alveoli. It enables to split a huge manifest.json for an easier maintenance.

Manifest.requirements

requirements

array of object

Pre manifest execution checks (bundlebee version typically). Avoids to install using a bundlebee version not compatible with the alveoli. Can be fully omitted.

Manifest.alveoli

List of described applications/libraries.

Table 2. Manifest.alveoli
Name JSON Name Type Description

chainDependencies

chainDependencies

bool

Should dependencies be installed one after the other or in parallel (default). It is useful when you install a namespace for example which must be awaited before next dependencies are installed.

interpolateDescriptors

interpolateDescriptors

bool

Enables to consider all descriptors have their interpolate descriptor set to true, you can still set it to false if you want to disable it for one. If not set, interpolateAlveoli flag from the manifest.

name

name

string

Name of the alveolus (recipe). It must be unique accross the whole classpath. Using maven style identifier, it is recommended to name it <groupId>:<artifactId>:<version> using maven filtering but it is not enforced.

version

version

string

If name does not follow <groupId>:<artifactId>:<version> naming (i.e. version can’t be extracted from the name) then you can specify the version there. Note that if set, this is used in priority (explicit versus deduced).

Alveolus.dependencies

dependencies

array of object

Dependencies of this alveolus. It is a way to import transitively a set of descriptors.

Alveolus.descriptors

descriptors

array of object

List of descriptors to install for this alveolus. This is required even if an empty array.

Alveolus.excludedDescriptors

excludedDescriptors

array of object

List of descriptors to ignore for this alveolus (generally coming from dependencies).

Alveolus.patches

patches

array of object

Patches on descriptors. It enables to inject configuration in descriptors by patching (using JSON-Patch or plain interpolation with ${key} values) their JSON representation. The key is the descriptor name and each time the descriptor is found it will be applied.

placeholders

placeholders

object

Local placeholders for this particular alveolus and its dependencies. It is primarly intended to be able to create a template alveolus and inject the placeholders inline.

Alveolus.dependencies

Dependencies of this alveolus. It is a way to import transitively a set of descriptors.

Table 3. Alveolus.dependencies
Name JSON Name Type Description

location

location

string

Where to find the alveolus. Note it will ensure the jar is present on the local maven repository.

name

name

string

Alveolus name.

Descriptor.includeIf

includeIf

object

Conditions to include this dependency. Enables for example to have an environment variable enabling part of the stack (ex: MONITORING=true)

Alveolus.descriptors

List of descriptors to install for this alveolus. This is required even if an empty array.

Table 4. Alveolus.descriptors
Name JSON Name Type Description

await

await

bool

If set to true, apply/delete commands will await the actual creation of the resource (GET /x returns a HTTP 200) before continuing to process next resources. It is useful for namespaces for example to ensure applications can be created in the newly created namespace. It avoids to run and rerun apply command in practise. For more advanced tests, use awaitConditions.

awaitOnDelete

awaitOnDelete

bool

On delete we rarely want to check the resource exists before but in these rare case you can set this toggle to true.

interpolate

interpolate

bool

If set to true, it will interpolate the descriptor just before applying it - i.e. after it had been patched if needed. You can use --<config-key> <value> to inject bindings set as {{config-key:-default value}}. If not set, interpolateDescriptors flag from the alveolus will be used.

location

location

string

Optional, if coming from another manifest, the dependency to download to get the alveolus.

name

name

string

Name of the descriptor to install. For kubernetes descriptors you can omit the .yaml extension.

type

type

string

Type of this descriptor. For now only kubernetes is supported. It also defines in which folder under bundlebee the descriptor(s) are looked for from its name.

Descriptor.awaitConditions

awaitConditions

array of object

Test to do on created/destroyed resources, enables to synchronize and await kubernetes actually starts some resource. For apply and delete commands, descriptorAwaitTimeout is still applied. Note that if you use multiple array entries for the same command it will be evaluated with an AND.

Descriptor.includeIf

includeIf

object

Conditions to include this descriptor.

Descriptor.includeIf

Conditions to include this descriptor.

Table 5. Descriptor.includeIf
Name JSON Name Type Description

operator

operator

string

Operator to combine the conditions. Potential values: ANY (At least one condition must match.),ALL (All conditions must match.).

Conditions.conditions

conditions

array of object

List of condition to match according operator.

Conditions.conditions

List of condition to match according operator.

Table 6. Conditions.conditions
Name JSON Name Type Description

key

key

string

Expected key. If empty/null condition is ignored. If read value is null it defaults to an empty string.

negate

negate

bool

Should the condition be reversed (ie "not in this case").

type

type

string

Type of condition. Potential values: ENV (Key is read is process environment variables.),SYSTEM_PROPERTY (Key is read is process system properties.).

value

value

string

Expected value. If empty/null, true is assumed. Note that empty is allowed.

Descriptor.awaitConditions

Test to do on created/destroyed resources, enables to synchronize and await kubernetes actually starts some resource. For apply and delete commands, descriptorAwaitTimeout is still applied. Note that if you use multiple array entries for the same command it will be evaluated with an AND.

Table 7. Descriptor.awaitConditions
Name JSON Name Type Description

command

command

string

Command to apply these conditions to, if not set it will be applied on apply command only. Note that for now only apply and delete commands are supported, others will be ignored.

operator

operator

string

Operator to combine the conditions. Potential values: ANY (At least one condition must match.),ALL (All conditions must match.).

AwaitConditions.conditions

conditions

array of object

List of condition to match according operator.

AwaitConditions.conditions

List of condition to match according operator.

Table 8. AwaitConditions.conditions
Name JSON Name Type Description

conditionType

conditionType

string

When condition type is STATUS_CONDITION it is the expected type of the condition. This is ignored when condition type is JSON_POINTER.

operatorType

operatorType

string

The operation to evaluate if this condition is true or not. (for type=JSON_POINTER). Potential values: EXISTS (JSON Pointer exists model.),MISSING (JSON Pointer does not exist in the resource model.),EQUALS (JSON Pointer value is equal to (stringified comparison) value.),NOT_EQUALS (JSON Pointer is different from the provided value.),EQUALS_IGNORE_CASE (JSON Pointer value is equal (ignoring case) to (stringified comparison) value.),NOT_EQUALS_IGNORE_CASE (JSON Pointer is different (ignoring case) from the provided value.),CONTAINS (JSON Pointer contains the configured value.).

pointer

pointer

string

JSON Pointer to read from the resource. It can for example be on /status/phase to await a namespace creation. (for type=JSON_POINTER).

type

type

string

Type of condition. Potential values: JSON_POINTER (JSON Pointer evaluation (fully custom).),STATUS_CONDITION (Evaluate items in /status/conditions.).

value

value

string

When condition type is JSON_POINTER and operatorType needs a value (EQUALS for example), the related value. It can be Active if you test namespace /status/phase for example. When condition type is STATUS_CONDITION it is the expected status.

Alveolus.excludedDescriptors

List of descriptors to ignore for this alveolus (generally coming from dependencies).

Table 9. Alveolus.excludedDescriptors
Name JSON Name Type Description

location

location

string

The container of the descriptor (maven coordinates generally).

name

name

string

Name of the descriptor (as declared, ie potentially without the extension).

Alveolus.patches

Patches on descriptors. It enables to inject configuration in descriptors by patching (using JSON-Patch or plain interpolation with ${key} values) their JSON representation. The key is the descriptor name and each time the descriptor is found it will be applied.

Table 10. Alveolus.patches
Name JSON Name Type Description

descriptorName

descriptorName

string

The descriptor to patch. It can be any descriptor, including transitive ones. It can be * to patch all descriptors (/metadata/label/app for example) or regex:<java pattern> to match descriptor names with a regex.

interpolate

interpolate

bool

If set to true, it will interpolate the patch from the execution configuration which means you can use --<config-key> <value> to inject bindings too. An interesting interpolation is the ability to extract the ip/host of the host machine (minikube ip equivalent) using the kubeconfig file. Syntax is the following one: {{kubeconfig.cluster.minikube.ip}} or more generally {{kubeconfig.cluster.<cluster name>.ip}}. You can also await for some secret with this syntax {{kubernetes.<namespace>.serviceaccount.<account name>.secrets.<secret name prefix>.data.<entry name>[.<timeout in seconds, default to 2mn>]}}. This is particular useful to access freshly created service account tokens for example.

patch

patch

array of object

JSON-Patch to apply on the JSON representation of the descriptor. It enables to inject configuration in descriptors for example, or changing some name/application.

Descriptor.includeIf

includeIf

object

Conditions to include this patch. Enables for example to have an environment variable enabling part of the stack (ex: MONITORING=true)

Manifest.ignoredLintingRules

Ignored linting rule names when using lint command.

Table 11. Manifest.ignoredLintingRules
Name JSON Name Type Description

name

name

string

Name of the rule to ignore.

Manifest.references

List of files referenced as other manifests. They are merged with this (main) manifest by appending requirements and alveoli. It is relative to this manifest location. Important: it is only about the same module references, external references are dependencies in an alveoli. It enables to split a huge manifest.json for an easier maintenance.

Table 12. Manifest.references
Name JSON Name Type Description

path

path

string

Relative or absolute - starting by a / - location (referenced to the base directory of manifest.json). For example my-manifest.json will resolve to /path/to/bundlebee/my-manifest.json in a folder and /bundlebee/my-manifest.json in a jar. Important: for resources (jar/classpath), the classloader is used so ensure your name is unique accross your classpath (we recommend you to prefix it with the module name, ex :`/bundlebee/my-module.sub-manifest.json` or use a dedicated subfolder (/bundlebee/my-module/sub.json).

Manifest.requirements

Pre manifest execution checks (bundlebee version typically). Avoids to install using a bundlebee version not compatible with the alveoli. Can be fully omitted.

Table 13. Manifest.requirements
Name JSON Name Type Description

maxBundlebeeVersion

maxBundlebeeVersion

string

Minimum bundlebee version, use `*`to replace any digit in a segment. Note that snapshot is ignored in the comparison for convenience. It is an inclusive comparison.

minBundlebeeVersion

minBundlebeeVersion

string

Minimum bundlebee version, use * to replace any digit in a segment. Note that snapshot is ignored in the comparison for convenience. It is an inclusive comparison.

forbiddenVersions

forbiddenVersions

array of string

List of forbidden version (due to a bug or equivalent). Here too snapshot suffix is ignored. is usable there too to replace any digit in a segment (ex: 1..). Note that 1. would NOT match 1.., version are always 3 segments.

Tip
manifest content can be wrapped in a bundlebee key (you wrap the whole content) which enables you to add other metadata in the manifest and still reuse BundleeBee JSON-Schema for the validation (by composition).

JSON-Schema

If you use JSON-Schema facilities in your preferred editor, here is the raw JSON-Schema for the manifest descriptor:

{
  "$id":"io_yupiik_bundlebee_core_descriptor_Manifest",
  "type":"object",
  "title":"Manifest Descriptor",
  "description":"BundleBee manifest descriptor.",
  "definitions":{
    "io_yupiik_bundlebee_core_descriptor_Manifest_Conditions":{
      "$id":"io_yupiik_bundlebee_core_descriptor_Manifest_Conditions",
      "type":"object",
      "title":"Descriptor.includeIf",
      "description":"Conditions to include this descriptor.",
      "properties":{
        "conditions":{
          "type":"array",
          "title":"Conditions.conditions",
          "description":"List of condition to match according `operator`.",
          "items":{
            "$id":"io_yupiik_bundlebee_core_descriptor_Manifest_Condition",
            "type":"object",
            "properties":{
              "key":{
                "type":"string",
                "title":"Condition.key",
                "description":"Expected key. If empty/null condition is ignored. If read value is null it defaults to an empty string."
              },
              "negate":{
                "type":"boolean",
                "title":"Condition.negate",
                "description":"Should the condition be reversed (ie \"not in this case\")."
              },
              "type":{
                "$id":"io_yupiik_bundlebee_core_descriptor_Manifest_ConditionType",
                "type":"string",
                "title":"Condition.type",
                "description":"Type of condition.",
                "enum":[
                  "ENV",
                  "SYSTEM_PROPERTY"
                ],
                "nullable":true
              },
              "value":{
                "type":"string",
                "title":"Condition.value",
                "description":"Expected value. If empty/null, `true` is assumed. Note that empty is allowed."
              }
            }
          }
        },
        "operator":{
          "$id":"io_yupiik_bundlebee_core_descriptor_Manifest_ConditionOperator",
          "type":"string",
          "title":"Conditions.operator",
          "description":"Operator to combine the conditions.",
          "enum":[
            "ANY",
            "ALL"
          ],
          "nullable":true
        }
      }
    }
  },
  "properties":{
    "alveoli":{
      "type":"array",
      "title":"Manifest.alveoli",
      "description":"List of described applications/libraries.",
      "items":{
        "$id":"io_yupiik_bundlebee_core_descriptor_Manifest_Alveolus",
        "type":"object",
        "properties":{
          "chainDependencies":{
            "type":"boolean",
            "title":"Alveolus.chainDependencies",
            "description":"Should dependencies be installed one after the other or in parallel (default). It is useful when you install a namespace for example which must be awaited before next dependencies are installed."
          },
          "dependencies":{
            "type":"array",
            "title":"Alveolus.dependencies",
            "description":"Dependencies of this alveolus. It is a way to import transitively a set of descriptors.",
            "items":{
              "$id":"io_yupiik_bundlebee_core_descriptor_Manifest_AlveolusDependency",
              "type":"object",
              "properties":{
                "includeIf":{
                  "$ref":"#/definitions/io_yupiik_bundlebee_core_descriptor_Manifest_Conditions",
                  "type":"object",
                  "title":"AlveolusDependency.includeIf",
                  "description":"Conditions to include this dependency. Enables for example to have an environment variable enabling part of the stack (ex: `MONITORING=true`)"
                },
                "location":{
                  "type":"string",
                  "title":"AlveolusDependency.location",
                  "description":"Where to find the alveolus. Note it will ensure the jar is present on the local maven repository."
                },
                "name":{
                  "type":"string",
                  "title":"AlveolusDependency.name",
                  "description":"Alveolus name."
                }
              }
            }
          },
          "descriptors":{
            "type":"array",
            "title":"Alveolus.descriptors",
            "description":"List of descriptors to install for this alveolus. This is required even if an empty array.",
            "items":{
              "$id":"io_yupiik_bundlebee_core_descriptor_Manifest_Descriptor",
              "type":"object",
              "properties":{
                "await":{
                  "type":"boolean",
                  "title":"Descriptor.await",
                  "description":"If set to `true`, apply/delete commands will await the actual creation of the resource (`GET /x` returns a HTTP 200) before continuing to process next resources. It is useful for namespaces for example to ensure applications can be created in the newly created namespace. It avoids to run and rerun apply command in practise. For more advanced tests, use `awaitConditions`."
                },
                "awaitConditions":{
                  "type":"array",
                  "title":"Descriptor.awaitConditions",
                  "description":"Test to do on created/destroyed resources, enables to synchronize and await kubernetes actually starts some resource. For `apply` and `delete` commands, `descriptorAwaitTimeout` is still applied. Note that if you use multiple array entries for the same command it will be evaluated with an `AND`.",
                  "items":{
                    "$id":"io_yupiik_bundlebee_core_descriptor_Manifest_AwaitConditions",
                    "type":"object",
                    "properties":{
                      "command":{
                        "type":"string",
                        "title":"AwaitConditions.command",
                        "description":"Command to apply these conditions to, if not set it will be applied on `apply` command only. Note that for now only `apply` and `delete` commands are supported, others will be ignored."
                      },
                      "conditions":{
                        "type":"array",
                        "title":"AwaitConditions.conditions",
                        "description":"List of condition to match according `operator`.",
                        "items":{
                          "$id":"io_yupiik_bundlebee_core_descriptor_Manifest_AwaitCondition",
                          "type":"object",
                          "properties":{
                            "conditionType":{
                              "type":"string",
                              "title":"AwaitCondition.conditionType",
                              "description":"When condition type is `STATUS_CONDITION` it is the expected type of the condition. This is ignored when condition type is `JSON_POINTER`."
                            },
                            "operatorType":{
                              "$id":"io_yupiik_bundlebee_core_descriptor_Manifest_JsonPointerOperator",
                              "type":"string",
                              "title":"AwaitCondition.operatorType",
                              "description":"The operation to evaluate if this condition is true or not. (for `type=JSON_POINTER`).",
                              "enum":[
                                "EXISTS",
                                "MISSING",
                                "EQUALS",
                                "NOT_EQUALS",
                                "EQUALS_IGNORE_CASE",
                                "NOT_EQUALS_IGNORE_CASE",
                                "CONTAINS"
                              ],
                              "nullable":true
                            },
                            "pointer":{
                              "type":"string",
                              "title":"AwaitCondition.pointer",
                              "description":"JSON Pointer to read from the resource. It can for example be on `/status/phase` to await a namespace creation. (for `type=JSON_POINTER`)."
                            },
                            "type":{
                              "$id":"io_yupiik_bundlebee_core_descriptor_Manifest_AwaitConditionType",
                              "type":"string",
                              "title":"AwaitCondition.type",
                              "description":"Type of condition.",
                              "enum":[
                                "JSON_POINTER",
                                "STATUS_CONDITION"
                              ],
                              "nullable":true
                            },
                            "value":{
                              "type":"string",
                              "title":"AwaitCondition.value",
                              "description":"When condition type is `JSON_POINTER` and `operatorType` needs a value (`EQUALS` for example), the related value. It can be `Active` if you test namespace `/status/phase` for example. When condition type is `STATUS_CONDITION` it is the expected status."
                            }
                          }
                        }
                      },
                      "operator":{
                        "$id":"io_yupiik_bundlebee_core_descriptor_Manifest_ConditionOperator",
                        "type":"string",
                        "title":"AwaitConditions.operator",
                        "description":"Operator to combine the conditions.",
                        "enum":[
                          "ANY",
                          "ALL"
                        ],
                        "nullable":true
                      }
                    }
                  }
                },
                "awaitOnDelete":{
                  "type":"boolean",
                  "title":"Descriptor.awaitOnDelete",
                  "description":"On delete we rarely want to check the resource exists before but in these rare case you can set this toggle to `true`."
                },
                "includeIf":{
                  "$id":"io_yupiik_bundlebee_core_descriptor_Manifest_Conditions",
                  "type":"object",
                  "title":"Descriptor.includeIf",
                  "description":"Conditions to include this descriptor.",
                  "properties":{
                    "conditions":{
                      "type":"array",
                      "title":"Conditions.conditions",
                      "description":"List of condition to match according `operator`.",
                      "items":{
                        "$id":"io_yupiik_bundlebee_core_descriptor_Manifest_Condition",
                        "type":"object",
                        "properties":{
                          "key":{
                            "type":"string",
                            "title":"Condition.key",
                            "description":"Expected key. If empty/null condition is ignored. If read value is null it defaults to an empty string."
                          },
                          "negate":{
                            "type":"boolean",
                            "title":"Condition.negate",
                            "description":"Should the condition be reversed (ie \"not in this case\")."
                          },
                          "type":{
                            "$id":"io_yupiik_bundlebee_core_descriptor_Manifest_ConditionType",
                            "type":"string",
                            "title":"Condition.type",
                            "description":"Type of condition.",
                            "enum":[
                              "ENV",
                              "SYSTEM_PROPERTY"
                            ],
                            "nullable":true
                          },
                          "value":{
                            "type":"string",
                            "title":"Condition.value",
                            "description":"Expected value. If empty/null, `true` is assumed. Note that empty is allowed."
                          }
                        }
                      }
                    },
                    "operator":{
                      "$id":"io_yupiik_bundlebee_core_descriptor_Manifest_ConditionOperator",
                      "type":"string",
                      "title":"Conditions.operator",
                      "description":"Operator to combine the conditions.",
                      "enum":[
                        "ANY",
                        "ALL"
                      ],
                      "nullable":true
                    }
                  }
                },
                "interpolate":{
                  "type":"boolean",
                  "title":"Descriptor.interpolate",
                  "description":"If set to `true`, it will interpolate the descriptor just before applying it - i.e. after it had been patched if needed. You can use `--<config-key> <value>` to inject bindings set as `{{config-key:-default value}}`. If not set, `interpolateDescriptors` flag from the alveolus will be used.",
                  "nullable":true
                },
                "location":{
                  "type":"string",
                  "title":"Descriptor.location",
                  "description":"Optional, if coming from another manifest, the dependency to download to get the alveolus."
                },
                "name":{
                  "type":"string",
                  "title":"Descriptor.name",
                  "description":"Name of the descriptor to install. For kubernetes descriptors you can omit the `.yaml` extension."
                },
                "type":{
                  "type":"string",
                  "title":"Descriptor.type",
                  "description":"Type of this descriptor. For now only `kubernetes` is supported. It also defines in which folder under `bundlebee` the descriptor(s) are looked for from its name."
                }
              }
            }
          },
          "excludedDescriptors":{
            "type":"array",
            "title":"Alveolus.excludedDescriptors",
            "description":"List of descriptors to ignore for this alveolus (generally coming from dependencies).",
            "items":{
              "$id":"io_yupiik_bundlebee_core_descriptor_Manifest_DescriptorRef",
              "type":"object",
              "properties":{
                "location":{
                  "type":"string",
                  "title":"DescriptorRef.location",
                  "description":"The container of the descriptor (maven coordinates generally)."
                },
                "name":{
                  "type":"string",
                  "title":"DescriptorRef.name",
                  "description":"Name of the descriptor (as declared, ie potentially without the extension)."
                }
              }
            }
          },
          "interpolateDescriptors":{
            "type":"boolean",
            "title":"Alveolus.interpolateDescriptors",
            "description":"Enables to consider all descriptors have their `interpolate` descriptor set to `true`, you can still set it to `false` if you want to disable it for one. If not set, `interpolateAlveoli` flag from the manifest.",
            "nullable":true
          },
          "name":{
            "type":"string",
            "title":"Alveolus.name",
            "description":"Name of the alveolus (recipe). It must be unique accross the whole classpath. Using maven style identifier, it is recommended to name it `<groupId>:<artifactId>:<version>` using maven filtering but it is not enforced."
          },
          "patches":{
            "type":"array",
            "title":"Alveolus.patches",
            "description":"Patches on descriptors. It enables to inject configuration in descriptors by patching (using JSON-Patch or plain interpolation with `${key}` values) their JSON representation. The key is the descriptor name and each time the descriptor is found it will be applied.",
            "items":{
              "$id":"io_yupiik_bundlebee_core_descriptor_Manifest_Patch",
              "type":"object",
              "properties":{
                "descriptorName":{
                  "type":"string",
                  "title":"Patch.descriptorName",
                  "description":"The descriptor to patch. It can be any descriptor, including transitive ones. It can be `*` to patch all descriptors (`/metadata/label/app` for example) or `regex:<java pattern>` to match descriptor names with a regex."
                },
                "includeIf":{
                  "$ref":"#/definitions/io_yupiik_bundlebee_core_descriptor_Manifest_Conditions",
                  "type":"object",
                  "title":"Patch.includeIf",
                  "description":"Conditions to include this patch. Enables for example to have an environment variable enabling part of the stack (ex: `MONITORING=true`)"
                },
                "interpolate":{
                  "type":"boolean",
                  "title":"Patch.interpolate",
                  "description":"If set to `true`, it will interpolate the patch from the execution configuration which means you can use `--<config-key> <value>` to inject bindings too. An interesting interpolation is the ability to extract the ip/host of the host machine (`minikube ip` equivalent) using the kubeconfig file. Syntax is the following one: `{{kubeconfig.cluster.minikube.ip}}` or more generally `{{kubeconfig.cluster.<cluster name>.ip}}`. You can also await for some secret with this syntax `{{kubernetes.<namespace>.serviceaccount.<account name>.secrets.<secret name prefix>.data.<entry name>[.<timeout in seconds, default to 2mn>]}}`. This is particular useful to access freshly created service account tokens for example."
                },
                "patch":{
                  "type":"array",
                  "title":"Patch.patch",
                  "description":"JSON-Patch to apply on the JSON representation of the descriptor. It enables to inject configuration in descriptors for example, or changing some name/application.",
                  "items":{
                    "type":"object",
                    "properties":{

                    }
                  }
                }
              }
            }
          },
          "placeholders":{
            "type":"object",
            "title":"Alveolus.placeholders",
            "description":"Local placeholders for this particular alveolus and its dependencies. It is primarly intended to be able to create a template alveolus and inject the placeholders inline.",
            "properties":{

            }
          },
          "version":{
            "type":"string",
            "title":"Alveolus.version",
            "description":"If name does not follow `<groupId>:<artifactId>:<version>` naming (i.e. version can't be extracted from the name) then you can specify the version there. Note that if set, this is used in priority (explicit versus deduced)."
          }
        }
      }
    },
    "ignoredLintingRules":{
      "type":"array",
      "title":"Manifest.ignoredLintingRules",
      "description":"Ignored linting rule names when using `lint` command.",
      "items":{
        "$id":"io_yupiik_bundlebee_core_descriptor_Manifest_IgnoredLintingRule",
        "type":"object",
        "properties":{
          "name":{
            "type":"string",
            "title":"IgnoredLintingRule.name",
            "description":"Name of the rule to ignore."
          }
        }
      }
    },
    "interpolateAlveoli":{
      "type":"boolean",
      "title":"Manifest.interpolateAlveoli",
      "description":"Enables to consider all alveoli have their `interpolateDescriptors` descriptor set to `true`, you can still set it to `false` if you want to disable it for one.",
      "nullable":true
    },
    "references":{
      "type":"array",
      "title":"Manifest.references",
      "description":"List of files referenced as other manifests. They are merged with this (main) manifest by *appending* _requirements_ and _alveoli_. It is relative to this manifest location. Important: it is only about the same module references, external references are dependencies in an alveoli. It enables to split a huge `manifest.json` for an easier maintenance.",
      "items":{
        "$id":"io_yupiik_bundlebee_core_descriptor_Manifest_ManifestReference",
        "type":"object",
        "properties":{
          "path":{
            "type":"string",
            "title":"ManifestReference.path",
            "description":"Relative or absolute - starting by a `/` - location (referenced to the base directory of `manifest.json`). For example `my-manifest.json` will resolve to `/path/to/bundlebee/my-manifest.json` in a folder and `/bundlebee/my-manifest.json` in a jar. Important: for resources (jar/classpath), the classloader is used so ensure your name is unique accross your classpath (we recommend you to prefix it with the module name, ex :`/bundlebee/my-module.sub-manifest.json` or use a dedicated subfolder (`/bundlebee/my-module/sub.json`)."
          }
        }
      }
    },
    "requirements":{
      "type":"array",
      "title":"Manifest.requirements",
      "description":"Pre manifest execution checks (bundlebee version typically). Avoids to install using a bundlebee version not compatible with the alveoli. Can be fully omitted.",
      "items":{
        "$id":"io_yupiik_bundlebee_core_descriptor_Manifest_Requirement",
        "type":"object",
        "properties":{
          "forbiddenVersions":{
            "type":"array",
            "title":"Requirement.forbiddenVersions",
            "description":"List of forbidden version (due to a bug or equivalent). Here too snapshot suffix is ignored. `*` is usable there too to replace any digit in a segment (ex: `1.*.*`). Note that `1.*` would *NOT* match `1.*.*`, version are always 3 segments.",
            "items":{
              "type":"string"
            }
          },
          "maxBundlebeeVersion":{
            "type":"string",
            "title":"Requirement.maxBundlebeeVersion",
            "description":"Minimum bundlebee version, use `*`to replace any digit in a segment. Note that snapshot is ignored in the comparison for convenience. It is an inclusive comparison."
          },
          "minBundlebeeVersion":{
            "type":"string",
            "title":"Requirement.minBundlebeeVersion",
            "description":"Minimum bundlebee version, use `*` to replace any digit in a segment. Note that snapshot is ignored in the comparison for convenience. It is an inclusive comparison."
          }
        }
      }
    }
  }
}
Tip
you can also use last version from this link.

Deployment and Dependency Management

A manifest can reference descriptors in other alveoli, it just requires to reference them to let bundlebee find them.

There are mainly two ways to deploy an alveolus:

  1. Classpath mode: you put all your zip/jar in the classpath. This mode requires there is no conflict between descriptor names (which is the recommended practise) and it will never look for external descriptors. It is typically a recommended mode to avoid network I/O except with the Kubernetes cluster.

  2. Fetch mode: in this mode, a missing descriptor in the classpath will look for descriptor.location and lookup the jar/zip referenced by this location (generally a maven coordinate in the form groupId:artifactId:version). It will first use your local Maven repository but can download the jar/zip if missing locally. Then the jar/zip is read and descriptors are looked up from there. This mode is nicer when depending on a lot of external alveoli but requires I/O to grab the dependencies.

Customize descriptors

Indeed, you can bundle all your stack and hardcode your configuration or create one alveolus per environment. This is one option which works well and enable to version its configuration.

However, for testing purposes, it is also neat to be able to patch descriptors on the fly. For that you can use descriptor.patches in the manifest which will transitively enable to patch the descriptors - even the ones you don’t own - using JSON-Patch and interpolations.

Here are some examples.

Patch using CLI option
{
  "alveoli": [
    {
      "name": "com.company.alveoli:my-app:${project.version}",
      "descriptors": [
        {
          "name": "com.company.alveoli_my-app.configmap"
        },
        // ...
      ],
      "patches": [ (1)
        "descriptorName": "com.company.other_alveolus.configmap", (2)
        "interpolate": true, (3)
        "patch": [ (4)
          {
            "op": "replace",
            "path": "data",
            "value": {
              "my.config.value": "{{my.arg:-defaultValue}}" (5)
            }
          }
        ]
      ]
    }
  ]
}
  1. We add to the alveolus a list of patches to apply on some descriptors

  2. We reference the descriptor to patch

  3. We enable interpolation from Microprofile Config (which include CLI args)

  4. We define the JSON-Patch to use (optional, interpolation will also interpolate the descriptor if designed to be interpolated)

  5. We use an interpolation in the configuration in the JSON-Patch replace operation which replaces the data of the referenced ConfigMap

Tip
this mecanism is very useful for ConfigMap descriptors but also PersistenceVolume since you can now patch `/spec/hostPath `easily too.

Deploy a dependency (or descriptor) conditionally

In some case it can be neat to be able to not deploy part of the stack. It is typically the case when you wrote a monitoring-full alveolus (ie with some optional services functionally). In such a case you can condition dependencies and descriptor deployment:

Deploy monitoring alveolus only if MONITORING option is set
{
  "alveoli": [
    {
      "name": "com.company.alveoli:my-app:${project.version}",
      "dependencies": [
        {
          "name": "io.yupiik.alveoli:monitoring-alveolus:${bundlebee.version}",
          "location": "io.yupiik.alveoli:monitoring-alveolus:${bundlebee.version}",
          "includeIf": {
            "conditions": [{
              "key": "MONITORING",
              "value": "true"
            }]
          }
        }
      ]
    }
  ]
}
Tip
used on descriptors and using a complete set of conditions for descriptors you can handle environments this way (ENV=preprod for descriptor preprod.configmap for example).

Filter manifest at command time

In some case it can be needed to evaluate some values of the manifest when executing the command. Even if not recommended it can be done using {{xxx}} placeholders - read from environment variables and system properties.

Filter manifest.json at command time
{
  "alveoli": [
    {
      "name": "com.company.alveoli:my-app:{{project.version}}",
      "dependencies": [
        {
          "name": "io.yupiik.alveoli:monitoring-alveolus:{{bundlebee.version}}",
          "location": "io.yupiik.alveoli:monitoring-alveolus:{{bundlebee.version}}"
        }
      ]
    }
  ]
}

With this manifest you can value project.version and bundlebee.version using the command line:

bundlebee \
    [your command and options] \
    --project.version 1.2.1 \
    --bundlebee.version 1.0.1

More placeholders

Placeholders can use some keywords to get some particular values:

  • alveolus.name: name of the alveolus the descriptor comes from,

  • descriptor.name: name of the descriptor,

  • bundlebee-strip:<value>: strips the provided value,

  • bundlebee-strip-leading:<value>: strips the provided value at the beginning,

  • bundlebee-strip-trailing:<value>: strips the provided value at the end,

  • bundlebee-indent:<indent size>:<value>: indents a value with the provided space size, it is generally combined with another interpolation (file ones in particular) as value, ex {{bundlebee-indent:8:{{bundlebee-inline-file:myfile.txt}}}},

  • bundlebee-inline-file:<file path>: load the file content as value,

  • bundlebee-base64-file:<file path>: converts the file in base64 (useful to write data:xxxx,<base64> values for ex keeping the raw file in the filesystem, very helpful for images),

  • bundlebee-base64-decode-file:<file path>: decode the file from a base64 content,

  • bundlebee-base64:<text>: encodes in base64 the text,

  • bundlebee-base64-decode:<text>: decode from base64 to text the value text,

  • bundlebee-digest:BASE64|HEX,<algorithm>,<text>: computes the digest of the text encoded in base64 or hexa format (useful to read files like ConfigMap or Secret and inject their digest value in a Deployment annotations to force its reload for example),

  • bundlebee-quote-escaped-inline-file:<file path>: load the file content as a quoted value,

  • bundlebee-json-inline-file:<file path>: load the file content as a JSON string value - without quotes,

  • bundlebee-json-string:content: escapes a string to be a JSON string (useful when you inject with bundlebee-json-inline-file a string in another string like in JSON ConfigMap in a JSON configuration),

  • bundlebee-maven-server-username:<server id>: extract from your maven settings.xml a server username (see configuration for a custom settings.xml location),

  • bundlebee-maven-server-password:<server id>: extract from your maven settings.xml a server password (potentially deciphered),

  • bundlebee-decipher: use Maven ciphering logic (AES/CBC/PKCS5Padding) to read a clear value. Syntax is as follow {{bundlebee-decipher:$masterKey,$cipheredValue}} with masterKey another placeholder name which contains the master key and cipheredValue the value to decipher. Indeed we recommend to reference the master key with another shared placeholder if you use a single one ({{bundlebee-decipher:{{myMasyterKey}},$cipheredValue}}). Last, since the value will be surrounded by { and }, we tolerate spaces before/after to avoid any confusion with the mustable like syntax we use for placeholders: {{bundlebee-decipher:{{my.master.key}}, {…​base64…​.} }},

  • bundlebee-kubernetes-namespace: the Kubernetes namespace defined in the HttpKubeClient,

  • kubeconfig.cluster.<cluster name>.ip: extract cluster IP from your kubeconfig,

  • timestamp: current time in milliseconds (since epoch),

  • timestampSec: current time in seconds (since epoch),

  • now: OffsetDateTime.now() value,

  • date:<pattern>: format OffsetDateTime.now() value using the provided pattern,

  • nowUTC: OffsetDateTime.now() value with UTC ZoneId,

  • kubernetes.<namespace>.serviceaccount.<account name>.secrets.<secret name prefix>.data.<entry name>[.<timeout in seconds>]: secret value looked up through Kubernetes API.

  • jsr223:<script path as file or resource or inline script>: executes a JSR223 script to get the result, assumes that the script engine implementation is available in the JVM (not by default). To find the engine it uses the file extension or a comment in the file (for inline script) containing bundlebee.language: <lang>. The script contains the two functions lookupByName and lookupByType which can lookup a CDI bean to reuse BundleBee utilities (ex: const bean = lookupByType.apply(getType('io.yupiik.bundlebee.core.kube.KubeClient'))). If no language is found it defaults on js.

  • bundlebee-directory-json-key-value-pairs:/path/to/dir: creates a JSON object from a directory. The path can be either a directory path of a directory path with a glob pattern at the end (/path/to/dir/*.txt for example). The keys are the filenames (simple) and __ are converted to /. The values are the content of the files, filtered. Note it only works with exploded folders, not jar resources as of today.

  • bundlebee-directory-json-key-value-pairs-content:/path/to/dir: same as bundlebee-directory-json-key-value-pairs but dropping enclosing brackets to be able to embed the content in an existing object (like annotations). Ex: {{bundlebee-directory-json-key-value-pairs-content:resources/app/annotations/*.txt}}.

If none of these placeholders match, then Microprofile Config is used to lookup the value (so system properties, environment variables and BundleBee args are used).

Tip
file placeholders can also read a resource from the classpath if the path does not match any existing file.

Use Handlebars instead of plain placeholders

Important
this feature is still experimental and value interpolation can be refined.

Sometimes it can be more convenient to use handlebars language to templatize the descriptors (not the manifest) rather than placeholders. It is the case when you have some logic depending some placeholder or value.

Important
it is also generally the case your template becomes complex so before jumping into handlebars templating please think if you should split your template in multiple imported files or just multiple files.

By replacing the .json or .yaml extension of the descriptor in kubernetes/ (sub)folder(s) by .hb or .handlebars you can use Yupiik fusion-handlebars templating running on java >= 17 and adding the dependency to the classpath (not available yet in native mode).

Then the descriptor, if interpolated is set to true in the manifest (as usual) will be interpolating using Handlerbars engine.

The main difference is that it gives you conditional blocks.

You can still use the plain placeholder interpolation since we try to keep a compatibility layer and default functions with some corner cases but most will work smoothly.

Tip
we still recommend to use JSON format instead of YAML since it is harder to break even using handlebars.

Finally you can use alveolus.name, descriptor.name which will be correctly interpolated.

Windows and CLI

On windows, the usage of the Java mode is recommended and the most reliable, however, if you want to speed up a bit the execution you can run the native bundlebee executable in a docker image.

Tip
ensure to test this solution on your machine, on our test machine gain can be x2 but it depends the docker setup too.

Once done you would be able to replace bundlebee by docker run bundlebee -v $HOME/.kube/config:/kube/config -e KUBECONFIG=/kube/config --network host (use an alias ;)).

Here are the step to use this solution:

  1. Download bundlebee native binary, for that go on https://repo.maven.apache.org/maven2/io/yupiik/bundlebee-core/, select the version you want and download the file ending with Linux-amd64,

  2. Create this Dockerfile file next to the binary you just downloaded and renamed bundlebee:

    FROM ubuntu:20.10 as builder
    
    RUN apt-get update && apt-get install -y zlib1g libgcc-s1 libstdc++6
    
    FROM scratch
    
    ENV LD_LIBRARY_PATH /lib
    
    COPY --from=builder /lib/x86_64-linux-gnu/libc.so.6 /lib/libc.so.6
    COPY --from=builder /lib/x86_64-linux-gnu/libgcc_s.so.1 /lib/libgcc_s.so.1
    COPY --from=builder /lib/x86_64-linux-gnu/libstdc++.so.6.0.28 /lib/libstdc++.so.6
    COPY --from=builder /lib/x86_64-linux-gnu/libpthread.so.0 /lib/libpthread.so.0
    COPY --from=builder /lib/x86_64-linux-gnu/libdl.so.2 /lib/libdl.so.2
    COPY --from=builder /lib/x86_64-linux-gnu/libm.so.6 /lib/libm.so.6
    COPY --from=builder /lib/x86_64-linux-gnu/libz.so.1 /lib/libz.so.1
    COPY --from=builder /lib64/ld-linux-x86-64.so.2 /lib/ld-linux
    
    COPY bundlebee-core /bin/bundlebee-core
    
    CMD ["/lib/ld-linux", "/bin/bundlebee-core"]
  3. Run docker build -t bundlebee . in the folder you put bundlebee binary and previous Dockerfile

Important
this dockerfile works as of today because binaries are build on Ubuntu 20.10, you can need to adjust a bit libraries if this changes - this is why Java mode is simpler.

Linting

Linting is quite specific so it has its own documentation.