---
# System prepended metadata

title: KOTS Variadic Config Proposal
tags: [replicated]

---

---
tags: replicated
---
# KOTS Variadic Config Proposal

Vendors require the ability to dynamically create resources as part of install configuration.
One common use case is installing operators, where the customers need to create dynamic resources, like instances of an application or service, that are unknown until install time.
They also need to be extend existing resources, like mounting _N_ files to a pod, where _N_ is not known until install time.
This proposal outlines a plan to support dynamic/variadic application configuration to facilitate dynamic resource creation. 

## Goals

Two Main Business-Driver Goals
1. Vendors can create "template" resources in the broadest sense; define resources once and they can be used _N_ times as needed by their application.
1. Vendors can extend resources with _N_ additional configuration properties, like environment variables or volume mounts.

Additional Technical Goals
1. Maintain last mile kustomization of of all resources.

## Non Goals

Vendor requests that were left out of scope of this proposal as future tasking:
1. Having Kotsadm parse file(s) to gather config data, including variadic resources
    * This may not have been requested but could have been implied by some vendor requests. 
    * Vendors and customers still interact with config fields the same way through the CLI or UI, although there will be options to create dynamic fields.
    * Individual Files can still be base64 encoded and inserted into resources using template functions.
1. Nested Groups 
    * Template or otherwise, are not supported as part of this proposal.
1. Glob File Dropzone Widget
    * What is it: one dropzone that will create config values for a collection of files instead of clicking a "+" sign multiple times
    * This might have been implied by various customer usage cases (I just want to dump some files here and mount them to a container) 
    * I think this is a straightforward implementation following this proposal, so it is not covered for clarity.
1. Dynamic Preflights
    * Because resources can be created dynamically, preflights may be valuable if they could be modified based on the planned size of deployment.
    * This is not included in this scope. Any modification to preflights would need to be submitted as an independent proposal.
1. Large Binary File Config Items 
    * Even though this was requested, the details of variadic config are considered a pre-requisite and this would need to be follow-on work.

## Background

Application configuration values are currently defined by vendors as static fields with basic scalar value types like integer, string and boolean (the file options can be treated as a special case of string). 
All fields must currently be defined ahead of time by the vendor.

KOTS currently uses resources with the following hierarchy:
1. **Config Spec** - This top-level resource defines the static fields available to configure the application.
It is defined by the vendor.
_A Config Spec w/ values populated is used as request format to change config values._
    1. **Config Group** - Defines collections of config items for navigation and bulk hide/show manipulation.
        1. **Config Item** - The individual scalar fields defined in a group (and optionally their values).
1. **ConfigValues Spec** - This top-level resource is _rendered by kots_ after the configuration is defined by the user into the app upstream archive under /upstream/userdata. 
It is a flat list of field names and values w/o any group mapping.
    1.  **Config Value** - The name, value and default value of each config item.

Examples of these are provided inline in the Detailed Design section of the proposal.

The current configuration pipeline works as follows:
1. Customer passes in config values via CLI or UI
1. ConfigValue spec is saved to the `/userdata` folder along with upstream to the `upstream` directly
1. Kots renders the upstream against the config values and also filters out any unnecessary files (e.g. preflight spec). This goes into the `base` directory along with a Kustomize file.
1. Midstream changes are applied.
1. Downstream changes are applied.
1. Completed manifests are sent to the operator to get deployed.

### Target Use Cases:

1. Template Resources example: create new Kafka instance.
    * Customers can click "Add a Kafka" in the Kotsadm console and specify multi copies of configuration items for dynamic resource creation.
    * There will be some MVP validation of templates resources:
        * At least X instances of templated resources
        * Individual config item validation still works
1. Repeat Config Items examples: mounting config files to a container.
    * Customers can click something like a "+" next to an individual field to make it an array of values
    * Vendor can use the values to amend 
1. BOTH
    * Customers can still specify variadic config information using the CLI
    * Last-mile Kustomization still works, or there is a technical path forward.

## High-Level Design 

Supporting the above customer use cases falls into two new feature additions for KOTS:
1. "Repeatable Config Items" supported by to the [Config Item Schema](https://kots.io/reference/v1beta1/config/#items).
1. "Repeatable Config Groups" supported by to the [Config Group Schema](https://kots.io/reference/v1beta1/config/#groups).

Vendors will leverage these new features as part of the Config Spec design. For these repeatable elements, the vendor will define either a target resource (e.g. a particular deployment file) that will need to be copied for each element of the array or a YAML path that will be cloned for the identified resource. This declarative approach was inspired by Kustomize patches. 

One note is that arrays are not used to store the Config values in any spec.
Using named keys rather that YAML arrays is intentional so that when an element is removed from an array, we can disambiguate whether an item was removed from all of the subsequent elements of the array being modified.

Usage examples provided in the Detailed Design section.

### `reapeatable` Config Items

The purpose of adding a `repeatable` attribute to Config Items is to add the capability *EXTEND* resources.

The existing Config Item concept will be augmented with a new property `repeatable` to indicated the value will be an array of values rather than a scalar. The value types will still inherit from the `type` field.

Config Items will also now include a `template` property to allow specifying the YAML document or sub-document to copy for this array of values.  

### `reapeatable` Config Groups

The purpose of adding a `repeatable` attribute to Config Groups is to add the ability to *COPY* collections of resources/config.

The existing Config Group concept will be augmented with a new property `repeatable` to indicated the values in each Config Item will be an array. 

Config Groups will also now include a `template` property to allow specifying the YAML document or sub-document to clone for each of these groups.  

## Detailed Design

While the design considered here is presented in an interleaved fashion, this proposal suggests that work be broken up in the following tasking:
1. Repeatable Config Items
1. Repeatable Config Groups

The first consideration is how the revised API will look to Vendors using these features in their application.

### Example Revised Kotskind Resources 

Vendors will use the revised Config Spec to define repeatable Config Groups and Items. 
Values are inserted by the Kots and returned as part of the API for creating the ConfigValues spec.
Below is a representative resource that will be used for several examples.

#### Config
```yaml
apiVersion: kots.io/v1beta1 
kind: Config 
metadata: 
  creationTimestamp: null 
  name: config-sample 
spec: 
  # UNCHANGED
  groups: 
  - name: nginx_settings 
    title: Nginx Configs 
    description: Config to serve as an example for creating your own
    items: 
    - name: "nginx_port"
      type: "text"
      title: "Nginx port"
      default: "80"
      value: ""

    # NEW!
    # This is a repeatable Config Item
    - name: "static_files"
      type: file
      title: "Static Assets"
      repeatable: true      # NEW! Tells the UI/Kots to expect an array
      minimumCount: 3       # NEW! Validation 
      templates:            # NEW! This desclares how the array of values will be used
        - apiVersion: v1    # By targeting a YAML path, we can clone this node for each element of the item.
          kind: Deployment
          name: my-deploy
          yamlPath: spec.template.spec.volumes[0].projected.sources
        - apiVersion: v1    # By targeting a resource file, we can clone the whole file for each element.
          kind: Secret
          prefix: secret-
      valuesByGroup:        # NEW! Returned to the API filled in from the CLI/console
        nginx_settings:
          static-file-<short guid>: "encoded file value one"
          static-file-<short guid>: "encoded file value two"
          static-file-<short guid>: "encoded file value three"

  # NEW! This is a repeatable Config Group
  - name: nginx             # ID
    title: Proxy Instances  # Group Friendly Name
    repeatable: true        # NEW! Tells the UI/Kots this is a group
    minimumCount: 1         # NEW! How many instances need to be created? Populates this many templates in the UI w/ defaults.
    instanceNames:             # NEW! Declares each instance of a group. The UI/CLI can generate this information.
    - nginx-<short guid 1>
    - nginx-<short guid 2>
    - nginx-<short guid 3>
    - nginx-<short guid 4>
    items: 
    - name: "port"
      type: "text"
      title: "Proxy Port"
      default: "", 
      templates:
      - apiVersion: v1    
        kind: Deployment
        prefix: proxy-
      - apiVersion: v1    
        kind: Service
        prefix: proxy-
      valuesByGroup:
        nginx-<short guid 1>:
            port-<short guid>: 80
        nginx-<short guid 2>:
            port-<short guid>: 443
        nginx-<short guid 3>:
            port-<short guid>: 8080
        nginx-<short guid 4>:
            port-<short guid>: 3000

  # Second Example of Repeatable Config Group
  - name: kafka
    title: Kafka Clusters
    repeatable: true
    repeatGroupName: Kafka Cluster
    minimumCount: 1
    templates:
    - apiVersion: kafka.banzaicloud.io/v1alpha1     # Specifies the resources that will be COPIED as part of the group
      kind: KafkaCluster
      prefix: kafka-cluster-
    # The names for each group name can be anything unique. They can be generated by the UI or made-up by the user
    instanceNames:             # NEW! Declares each instance of a group. The UI/CLI can generate this information.
    - kafka-<short guid 1>
    - kafka-<short guid 2>
    items: 
    - name: "name"
      type: "text"
      title: "Kafka Cluster Name"
      default: ""
      valuesByGroup:   
        kafka-<short guid 1>:     
          name: alpha
        kafka-<short guid 2>: 
          name: bravo
    # Combining both concepts
    - name: "brokers"
      type: "text"
      title: "Kafka Broker IDs"
      repeatName: Broker
      repeatable: true
      default: ""
      templates:
      - apiVersion: v1
        kind: kafka.banzaicloud.io/v1alpha1
        name: KafkaTopic
        yamlPath: spec.brokers[0]
      valuesByGroup:
        kafka-<short guid 1>: 
          broker-<short guid>: "broker A"
          broker-<short guid>: "broker B"
        kafka-<short guid 2>: 
          broker-<short guid>: "broker 1"
          broker-<short guid>: "broker 2"
          broker-<short guid>: "broker 3"
    - name: "topics"
      type: "text" 
      title: "Kafka Topics"
      repeatable: true
      repeatName: Topic
      default: "myTopic"
      minimumCount: 1
      templates:
      - apiVersion: kafka.banzaicloud.io/v1alpha1
        kind: KafkaTopic
        prefix: kafka-
      valuesByGroup:
        kafka-<short guid 1>: 
            topic-<short guid>: "topicA"
            topic-<short guid>: "topicB"
        kafka-<short guid 2>: 
            topic-<short guid>: "mytTopic" 
```

#### ConfigValues

The ConfigValues spec is rendered by Kots and stored as part of the applications release archive.
This will still be maintained as a flat list of values regardless of any new constructs.

**NOTE:** @Marc Why can't this be nested? Problem is that unlike statefulsets, order doesn't matter, i.e. what happens when I delete instance #3

```yaml
apiVersion: kots.io/v1beta1 
kind: ConfigValues 
metadata: 
  creationTimestamp: null 
  name: qa-kots 
spec: 
  values: 
    # EXISTING BEHAVIOR
    <name>:
      default: <value>
      value: <value> 
    # Example
    a_templated_text: 
      default: h6IVctWRdVBhflnQkImZybQUUkBjHQuAHj9QWFfBnFEOrf2CqBlkc70F22lMNHug 
      value: GPyocL_6XLb4uCcvPhmoYKtnlWMX3mIHzopzUediHzRs1SenEpmJQi6fJqHDV6MX 
    ...
    # NEW!
    # Values for a template
    <config-item-unique-name>:
      parent: <Group Unique Name>    # Nice to have?
      default: <value>
      value: <value> 
    # Example
    port-<short guid>:
      parent: nginx-<short guid 1>
      value: 80
    port-<short guid>:
      parent: nginx-<short guid 2>
      value: 443
    port-<short guid>:
      parent: nginx-<short guid 3>
      value: 8080
    port-<short guid>:
      parent: nginx-<short guid 4>
      value: 3000
```

### Resource Templates

None of the existing Replicated ConfigContext methods change for consumers, with the correct array value being selected by KOTS automatically when making copies of YAML documents or sub-documents. 

There are a couple new methods added.

| Method                | Input                     | Output                       | Purpose                                     |
|-----------------------|---------------------------|------------------------------|---------------------------------------------|
| RepeatableConfigGroupName | Config Item Name (string) | Group Instance Name (string) | Grab the Group Instance Name for this value |
| RepeatableConfigOptionName | Config Item Name (string) | Config Item Unique Name (string) | Grab the Unique Name for this Config Item element |


#### Repeatable Config Item Usage

Mounting a bunch of secrets (files) to a container as config data.
The expected output is one deployment that references three dynamically created secrets.

Note that these files were specific in the above Config Spec as applying to the Repeatable Config Item `static_files`.

```yaml
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-deploy
spec:
  template:
    spec:
      containers:
        - name: test
          image: httpd
          env:
          volumeMounts:
            - mountPath: /var/www/html
              name: secret-assets
              readOnly: true
      volumes:
      - name: secret-assets
        projected:
          sources:
            - secret:
              name: repl{{ RepeatableConfigOptionName "static_files" }}
```
```yaml
# secret.yaml
apiVersion: v1
kind: Secret
metadata:
  name: repl{{ RepeatableConfigOptionName "static_files" }}
data:
  file: repl{{ ConfigOption "static_files" }}
```

#### Repeatable Config Group Usage

Repeatable Config Groups are designed to work much the same way with resource targeting.

The expected output based on the Config Spec provided above would be 4 matching deployments and services.

```yaml
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: proxy-repl{{ $RepeatableConfigGroupName "port" }}
  labels:
    app: example
    component: repl{{ $RepeatableConfigGroupName "port" }}
spec:
  template:
    spec:
      containers:
        - name: proxy
          image: nginx
          env:
          - name: NGINX_PORT
            value: {{repl ConfigOption "port" }}  # Returns 80 for the first instance
```
```yaml
# service.yaml
apiVersion: v1
kind: Service
metadata:
  name: proxy-repl{{ $RepeatableConfigGroupName "port"  }}
  labels:
    app: example
    component: repl{{ $RepeatableConfigGroupName "port"  }}
spec:
  type: LoadBalancer
  ports:
  - port: 80
    targetPort: {{repl ConfigOption "port" }}
  selector:
    app: example
    component: repl{{ $RepeatableConfigGroupName "port"  }}
```

#### Combined Usage Example

This is a thought exercise around using an operator that has both Repeatable Config Items and Groups. 

The expected output is 2 KafkaCluster CRs. They each have a different number of brokers. One cluster has 2 KafkaTopic CRs and the other only has 1.

```yaml
apiVersion: kafka.banzaicloud.io/v1alpha1
kind: KafkaCluster
metadata:
    labels:
        controller-tools.k8s.io: "1.0"
    name: kafka-cluster-repl{{ $ConfigOption name }}
spec:
    headlessServiceEnabled: true
    brokers:
    - id: repl{{ $ConfigOptionGroupName broker }}
      brokerConfigGroup: "default"
      brokerConfig:
        # Right now this cannot be templated separately for each kafka cluster
        envs:
          - name: +CLASSPATH
            value: "/opt/kafka/libs/dev/*:"
          - name: CLASSPATH+
            value: ":/opt/kafka/libs/extra-jars/*"
        # Neither can this
        brokerIngressMapping:
          - "ingress-az1"
    ...
```
```yaml
apiVersion: kafka.banzaicloud.io/v1alpha1
kind: KafkaTopic
metadata:
    name: kafka-repl{{ ConfigOption topic }}
spec:
    clusterRef:
        name: kafka-cluster-repl{{ ConfigOption name }}
    name: repl{{ ConfigOption topic }}
    partitions: 1
    replicationFactor: 1
```

### Revised Business Logic Overview

Additions where noted:
1. Customer passes in config values via CLI or UI
1. ConfigValue spec is saved to the `/userdata` folder along with upstream to the `upstream` directly
1. Kots renders the rest of `upstream` against the config values and also filters out any unnecessary files (e.g. preflight spec). 
This goes into the `base` directory along with a kustomize file.
    1. **NEW** First identify repeat groups and iterate through them
    1. **NEW** Inside that loop, identify any repeat items and render the YAML nodes
    1. **NEW** Complete render of other repl functions
    1. **NEW** Copy the file with a unique ID from the group or item into base
    1. **NEW** Repeat evaluation for simple repeat elements
    1. Render everything else.
1. Midstream changes are applied.
1. Downstream changes are applied.
1. Completed manifests are sent to the operator to get deployed.

## Design Limitations

1. This currently doesn't include any nested groups. This will likely be needed at a future point to support complex CRDs.
1. Configmap/secrets can only hold 5MB/1MB of data, respectively. No way to pass in an arbitrarily large file and have it passed along as configuration.
    * This more than likely eliminates the possibility of storing binary files, which has been specifically requested.
1. No ability to bulk-patch resources before they are rendered. Can still use Kustomize targets to accomplish this.

## Testing

Any template rendering based on this design should be refactored in such a way as to allow unit/integration testing of sample manifests against the expected API output. 

Testim tests (both smoke tests and release acceptance tests) will be augmented along with teh QAKots application to test the new UI elements for both features.

At a future point we will need to add a test framework for the CLI (or augment the current acceptance tests) to test that configuration can be passed to kotsadm as part of an unattended install.

## Alternatives Considered

1. Use Go Templating and new pipeline ConfigContext functions instead of targeting resources in the Config File like kustomize.
    1. Probably more intuitive for helm users.
    1. Potentially restricting and requires the use of comments to produce valid YAML.
    1. Philosophically different from the usage of Kustomize.
1. Having KOTS Use very basic search/parse capabilities to look for Config Items that were members of a template group, which would implicitly copy any resource using them N times.
This wasn't proposed because it seemed like it would implement a brute-force search of each file for every possible templateGroup config item.
1. As a more obscure solution, we could build a [custom generator in Go for kustomize](https://kubectl.docs.kubernetes.io/guides/extending_kustomize/) that takes in arbitrary templates and spits out the results directly. This didn't seem to have too many advantages over using the standard go tooling, but would have require more complexity to manage in KOTS.
1. Having `ConfigContext` methods that returned valid YAML and/or JSON was also discussed, but this would require passing in templates as arguments for something complicated like rendering a whole configmap.

## Security Considerations

As configuration is already part of the app definition, this proposal doesn't anticipate any changes to security posture.

Because the resources can be generated or extended dynamically, it's expected that the onus is on the vendor to ensure this doesn't not open any vulnerabilities in their application.

## References

Kustomize Resources
1. [Golang Text/Template Package](https://golang.org/pkg/text/template)
1. [Generic Generator Discussion](https://github.com/kubernetes-sigs/kustomize/issues/126)
1. [JSON Path Example](https://github.com/yubessy/example-kustomize-cronjob-multiple-schedule)

(Thanks to vmware-tanzu/velero for this design template)
