diff --git a/docs/proposals/001-proposal-template.md b/docs/proposals/001-proposal-template.md index 5d0d25f935db1..240769a9e5845 100644 --- a/docs/proposals/001-proposal-template.md +++ b/docs/proposals/001-proposal-template.md @@ -3,7 +3,7 @@ title: Neat-enhancement-idea authors: - "@sbose78" # Authors' github accounts here. sponsors: - - TBD # List all intereste parties here. + - TBD # List all interested parties here. reviewers: - "@alexmt" - TBD @@ -58,10 +58,10 @@ This is where we get down to details of what the proposal is about. Add a list of detailed use cases this enhancement intends to take care of. -## Use case 1: +#### Use case 1: As a user, I would like to understand the drift. (This is an example) -## Use case 2: +#### Use case 2: As a user, I would like to take an action on the deviation/drift. (This is an example) ### Implementation Details/Notes/Constraints [optional] diff --git a/docs/proposals/images/helm-parameters.png b/docs/proposals/images/helm-parameters.png new file mode 100644 index 0000000000000..34a7f49c0d14e Binary files /dev/null and b/docs/proposals/images/helm-parameters.png differ diff --git a/docs/proposals/parameterized-config-management-plugins.md b/docs/proposals/parameterized-config-management-plugins.md new file mode 100644 index 0000000000000..fa3061b2c3686 --- /dev/null +++ b/docs/proposals/parameterized-config-management-plugins.md @@ -0,0 +1,843 @@ +--- +title: Parameterized-Config-Management-Plugins + +authors: +- "@alexmt" +- "@crenshaw-dev" +- "@leoluz" + +sponsors: +- TBD + +reviewers: +- TBD + +approvers: +- TBD + +creation-date: 2022-01-05 + +last-updated: 2022-01-05 + +--- + +# Parameterized Config Management Plugins + +Config Management Plugin (CMP) parameterization defines a way for plugins to "announce" and then consume acceptable +parameters for an Application. Announcing parameters allows CMPs to provide a UI experience similar to native config +management tools (Helm, Kustomize, etc.). + +- [Parameterized Config Management Plugins](#parameterized-config-management-plugins) + * [Open Questions](#open-questions) + * [Summary](#summary) + * [Motivation](#motivation) + + [1. CMPs are under-utilized](#1-cmps-are-under-utilized) + + [2. Decisions about config management tools are limited by the core code](#2-decisions-about-config-management-tools-are-limited-by-the-core-code) + + [3. Ksonnet is deprecated, and CMPs are a good place to maintain support](#3-ksonnet-is-deprecated-and-cmps-are-a-good-place-to-maintain-support) + + [Goals](#goals) + + [Non-Goals](#non-goals) + * [Proposal](#proposal) + + [Use cases](#use-cases) + - [Use case 1: building Argo CD without config management dependencies](#use-case-1-building-argo-cd-without-config-management-dependencies) + - [Use case 2: writing CMPs with rich UI experiences](#use-case-2-writing-cmps-with-rich-ui-experiences) + + [Implementation Details/Notes/Constraints](#implementation-detailsnotesconstraints) + - [Prerequisites](#prerequisites) + - [Terms](#terms) + - [How will the ConfigManagementPlugin spec change?](#how-will-the-configmanagementplugin-spec-change) + - [How will the CMP know what parameter values are set?](#how-will-the-cmp-know-what-parameter-values-are-set) + - [How will the UI know what parameters may be set?](#how-will-the-ui-know-what-parameters-may-be-set) + - [Implementation Q/A](#implementation-qa) + + [Detailed examples](#detailed-examples) + - [Example 1: trivial parameterized CMP](#example-1-trivial-parameterized-cmp) + - [Example 2: Helm parameters from Kustomize dependency](#example-2-helm-parameters-from-kustomize-dependency) + - [Example 3: simple Helm CMP](#example-3-simple-helm-cmp) + - [Example 4: simple Kustomize CMP](#example-4-simple-kustomize-cmp) + + [Security Considerations](#security-considerations) + - [Increased scripting](#increased-scripting) + + [Risks and Mitigations](#risks-and-mitigations) + + [Upgrade / Downgrade Strategy](#upgrade--downgrade-strategy) + * [Drawbacks](#drawbacks) + * [Alternatives](#alternatives) + +## Open Questions + +* Should we write examples in documentation in Python instead of shell scripts? + + It's very easy to write an insecure shell script. People copy/paste code from documentation to start their own work. + Maybe by using a different language in examples, we can encourage more secure CMP development. + +## Summary + +[Config Management Plugins](https://argo-cd.readthedocs.io/en/stable/user-guide/config-management-plugins/) +allow Argo CD administrators to define custom manifest generation tooling. + +The only existing way for users to parameterize manifest generation is with environment variables. + +This proposed feature will allow a plugin to "announce" acceptable parameters for an Application. It will also allow the +plugin to consume parameters once the user has set them. + +Parameters definitions may be simple (advertising a simple key/value string pair) or more complex (accepting an array of +strings or a map of string keys to string values). Parameter definitions can also specify a data type (string, number, +or boolean) to help the UI present the most relevant input field. + +## Motivation + +### 1. CMPs are under-utilized + +CMPs, especially the sidecar type, are under-utilized. Making them more robust will increase adoption. Increased +adoption will help us find bugs and then make CMPs more robust. In other words, we need to reach a critical mass of +CMP users. + +More robust CMPs will make it easier to start supporting tools like [Tanka](https://tanka.dev/). + +### 2. Decisions about config management tools are limited by the core code + +For example, there's a [Helm bug](https://github.com/argoproj/argo-cd/issues/7291) affecting Argo CD users. The fix +would involve importing the Helm SDK (a very large dependency) into Argo CD. Implementing Helm support as a CMP would +allow us to use that SDK without embedding it in the core code. + +### 3. Ksonnet is deprecated, and CMPs are a good place to maintain support + +Offloading Ksonnet to a plugin would allow us to support existing users without maintaining Ksonnet code in the more +actively-developed base. But we need CMP parameters to provide Ksonnet support on-par with native support. + +### Goals + +Parameterized CMPs must be: +* Easy to write + * An Argo CD admin should be able to write a simple parameterized CMP in just a few lines of code. + * An Argo CD admin should be able to write an _advanced_ parameterized CMP server relying on thorough docs. + + Writing a custom CMP server might be preferable if the parameters announcement code gets too complex to be + an inline shell script. +* Easy to install + * Installing a simple CMP or even a CMP with a custom server should be intuitive and painless. +* Easy to use + * Argo CD end-users (for example, developers) should be able to + 1. View and set parameters in the Argo CD Application UI + 2. See the parameters reflected in the Application manifest + 3. Easily read/modify the generated parameters in the manifest (they should be structured in a way that's easy to read) + * CMPs should be able to announce parameters with more helpful interfaces than a simple text field. + * For example, numbers and booleans should be represented in the UI with the appropriate inputs. +* Future-proof + * Since the rich parameters UI is an important feature for config management tools, the parameter definition schema + should be flexible enough to announce new _types_ of parameters so the UI can customize its presentation. +* Backwards-compatible + * CMPs written before this enhancement should work fine after this enhancement is released. +* Proven with a rich demonstration + * The initial release of this feature should include a CMP implementation of the Helm config tool. This will + 1. Serve as a rich example for others CMP developers to mimic + 2. Allow us to decouple the Helm config management release cycle from the Argo release cycle + 3. Allow us to work around [this bug](https://github.com/argoproj/argo-cd/issues/7291) without including the Helm + SDK in the core Argo CD code + * The Helm CMP must be on-par with the native implementation. + 1. It must present an equivalent parameters UI. + 2. It must communicate errors back to the repo-server (and then the UI) the same as the native implementation. + +### Non-Goals + +We should not: +* Re-implement config management tools as CMPs (besides Helm) + +## Proposal + +### Use cases + +#### Use case 1: building Argo CD without config management dependencies + +As an Argo CD developer, I would like to be able to build Argo CD without including the Helm SDK as a dependency. + +The Helm SDK includes the Kubernetes code base. That's a lot of code, and it will make builds unacceptably slow. + +#### Use case 2: writing CMPs with rich UI experiences + +As an Argo CD user, I would like to be able to parameterize manifests built by a CMP. + +For example, if the Argo CD administrator has installed a CMP which applies a last-mile kustomize overlay to a Helm +repo, I would like to be able to pass values to the Helm chart without having to manually discover those parameter names +(in other words, they should show up in the Application UI just like with a native Helm Application). I also shouldn't +have to ask my Argo CD admin to modify the CMP to accommodate the values as environment variables. + +### Implementation Details/Notes/Constraints + +#### Prerequisites + +Since this proposal is designed to increase CMP adoption, we need to make sure there aren't any bugs that make CMPs +less robust than native tools. + +Bugs to fix: +1. [#8145](https://github.com/argoproj/argo-cd/issues/8145) - `argocd app sync/diff --local` doesn't account for sidecar CMPs +2. [#8243](https://github.com/argoproj/argo-cd/issues/8243) - "Configure plugin via sidecar" ⇒ child resources not pruned on deletion + +#### Terms + +* **Parameter announcement**: an instance of a data structure which describes an individual parameter that may be applied + to a specific Application. (See the [schema](#how-will-the-ui-know-what-parameters-may-be-set) below.) +* **Parameters announcement**: a list of parameter announcements. (See the [schema](#how-will-the-ui-know-what-parameters-may-be-set) below.) + + "Parameters" is plural because each "announcement" will be a list of multiple parameter announcements. +* **Parameterized CMP**: a CMP which supports rich parameters (i.e. more than environment variables). A CMP is + parameterized if either of these is true: + 1. its configuration includes the sections consumed by the default CMP server to generate parameters announcements + 2. it is a fully customized CMP server which implements an endpoint to generate parameters announcements + +#### How will the ConfigManagementPlugin spec change? + +This proposal adds a new `parameters` key to the ConfigManagementPlugin config spec. + +```yaml +apiVersion: argoproj.io/v1alpha1 +kind: ConfigManagementPlugin +metadata: + name: cmp-plugin +spec: + version: v1.0 + generate: + command: ["example.sh"] + discover: + fileName: "./subdir/s*.yaml" + # NEW KEY + parameters: + static: + # The static announcement follows the parameters announcement schema. This is where a parameter description + # should go if it applies to all apps for this CMP. + - name: values-file + title: Values File + tooltip: Path of a Helm values file to apply to the chart. + dynamic: + # The (optional) generated announcement is combined with the declarative announcement (if present). This is where + # a parameter description should be generated if it applies only to a specific app which the CMP handles. + command: ["example-params.sh"] +``` + +The currently-configured parameters (if there are any) will be communicated to both `generate.command` and +`parameters.dynamic.command` via an `ARGOCD_APP_PARAMETERS` environment variable. The parameters will be encoded +according to the [parameters serialization format](#how-will-the-cmp-know-what-parameter-values-are-set) defined below. + +Passing the parameters to the `parameters.dynamic.command` will allow configuration of parameter discovery. For example, +if my CMP is designed to handle Kustomize projects which contain Helm charts, I might have the CMP accept an +`ignore-helm-charts` parameter to avoid announcing parameters for those charts. + +```yaml +apiVersion: argoproj.io/v1alpha1 +kind: Application +spec: + source: + plugin: + parameters: + - name: ignore-helm-charts + array: [chart-a, chart-b] +``` + +#### How will the CMP know what parameter values are set? + +Users persist parameter values in an Application's `spec.source.plugin.parameters` list. + +Each parameter has a `name` and a value stored in the `string`, `array`, or `map` field, according to the parameter's +collectionType. The name should match the name of some parameter announced by the CMP. (But +the user can set any parameter name, so it's the CMP's job to ignore invalid parameters.) + +This example is for a hypothetical Helm CMP. This CMP accepts a `values` and a `values-files` parameter. + +```yaml +apiVersion: argoproj.io/v1alpha1 +kind: Application +spec: + source: + repoURL: https://github.com/argoproj/argocd-example-apps.git + plugin: + parameters: + - name: values + string: >- + resources: + cpu: 100m + memory: 128Mi + - name: values-files + array: [values.yaml] + - name: helm-parameters + map: + image.repository: my.company.com/gcr-proxy/heptio-images/ks-guestbook-demo + image.tag: "0.1" +``` + +When Argo CD generates manifests (for example, when the user clicks "Hard Refresh" in the UI), Argo CD will send these +parameters to the CMP as JSON (using the equivalent structure to what's shown above) on an environment variable called +`ARGOCD_APP_PARAMETERS`. + +```shell +echo "$ARGOCD_APP_PARAMETERS" | jq +``` + +That command, when run by a CMP with the above Application manifest, will print the following: + +```json +[ + { + "name": "values", + "string": "resources:\n cpu: 100m\n memory: 128Mi" + }, + { + "name": "values-files", + "array": ["values.yaml"] + }, + { + "name": "helm-parameters", + "map": { + "image.repository": "my.company.com/gcr-proxy/heptio-images/ks-guestbook-demo", + "image.tag": "0.1" + } + } +] +``` + +Another way the CMP can access parameters is via environment variables. For example: + +```shell +echo "$VALUES" > /tmp/values.yaml +helm template --values /tmp/values.yaml . +``` + +Environment variable names are set according to these rules: + +1. If a parameter is a `string`, the format is `PARAM_{escaped(name)}` (`escaped` is defined below). +2. If a parameter is an `array`, the format is `PARAM_{escaped(name_{index})}` (where the first index is 0). +3. If a parameter is a `map`, the format is `PARAM_{escaped(name_key)}`. +4. If an escaped env var name matches one in the [build environment](https://argo-cd-docs.readthedocs.io/en/latest/user-guide/build-environment/), + the build environment variable wins. +5. If more than one parameter name produces the same env var name, the env var later in the list wins. + +The `escaped` function will perform the following tasks: +1. It will uppercase the input. +2. It will replace any characters matching this regex with an underscore: `[^A-Z0-9_]`. + +The above example will produce the following env vars: + +```shell +echo "$PARAM_VALUES" +echo "$PARAM_VALUES_FILES_0" +echo "$PARAM_HELM_PARAMETERS_IMAGE_REPOSITORY" +echo "$PARAM_HELM_PARAMETERS_IMAGE_TAG" +``` + +The parameters in the Application manifest are represented behind the scenes with the following Go types: + +```go +package cmp + +// Parameter represents a single parameter name and its value. One of Value, Map, or Array must be set. +type Parameter struct { + // Name is the name identifying a parameter. (required) + Name string `json:"name,omitempty"` + String string `json:"string,omitempty"` + Map map[string]string `json:"map,omitempty"` + Array []string `json:"array,omitempty"` +} + +// Parameters is a list of parameters to be sent to a CMP for manifest generation. +type Parameters []Parameter +``` + +#### How will the UI know what parameters may be set? + +The CMP developer will have two ways to announce acceptable parameters: statically (declaratively) and dynamically. + +Static parameter announcements are written directly into the CMP config file: + +```yaml +apiVersion: argoproj.io/v1alpha1 +kind: ConfigManagementPlugin +metadata: + name: helm +spec: + parameters: + static: + - name: values-files + title: Values Files + collectionType: array +``` + +Since this hypothetical Helm CMP will accept an array of values.yaml files for every app it handles, the CMP developer +can add that parameter as a static parameter announcement in the CMP config. + +Dynamic parameters are generated by a CMP developer-defined command. + +A parameter definition is an object with following schema: + +```yaml +apiVersion: argoproj.io/v1alpha1 +kind: ConfigManagementPlugin +metadata: + name: helm +spec: + parameters: + dynamic: + command: + - sh + - -c + - | + # Use yq to generate a list of parameters. Then use jq to convert that list of parameters to a parameters + # announcement list. + yq e -o=p values.yaml | jq -nR ' + [{ + name: "helm-parameters", + title: "Helm Parameters", + tooltip: "Parameters to override when generating manifests with Helm", + collectionType: "map", + map: (inputs | capture("(?.*) = (?.*)") | from_entries) + }]' +``` + +For a Helm chart with only an `image.repository` and `image.tag` in values.yaml, the parameter announcement would look +like this: + +```json +[ + { + "name": "helm-parameters", + "collectionType": "map", + "title": "Helm Parameters", + "tooltip": "Parameters to override when generating manifests with Helm", + "map": { + "image.repository": "my.company.com/gcr-proxy/heptio-images/ks-guestbook-demo", + "image.tag": "0.1" + } + } +] +``` + +Before sending a parameters announcement to the UI, the CMP server will combine the static and dynamic parameters. +(Behind the scenes, the list is actually communicated to the UI via gRPC, but they're presented here as JSON for +readability.) + +```json + +[ + { + "name": "values-files", + "title": "Values Files", + "collectionType": "array" + }, + { + "name": "helm-parameters", + "collectionType": "map", + "title": "Helm Parameters", + "tooltip": "Parameters to override when generating manifests with Helm", + "map": { + "image.repository": "my.company.com/gcr-proxy/heptio-images/ks-guestbook-demo", + "image.tag": "0.1" + } + } +] +``` + +This is the full parameters announcement schema as Go types. + +```go +package cmp + +// ParameterItemType is the primitive data type of each of the parameter's value (or each of its values, if it's an array or +// a map). +type ParameterItemType string + +// Anything besides "number" and "boolean" is treated as string. +const ( + ParameterItemTypeNumber ParameterItemType = "number" + ParameterItemTypeBoolean ParameterItemType = "boolean" +) + +// ParameterCollectionType is a parameter's value's type - a single value (like a string) or a collection (like an array or a +// map). +type ParameterCollectionType string + +// Anything besides "number" and "boolean" is treated as string. +const ( + ParameterCollectionTypeMap ParameterCollectionType = "map" + ParameterCollectionTypeArray ParameterCollectionType = "array" +) + +// ParameterAnnouncement represents a CMP's announcement of one acceptable parameter (though that parameter may contain +// multiple elements, if the value holds an array or a map). +type ParameterAnnouncement struct { + // Name is the name identifying a parameter. (required) + Name string `json:"name,omitempty"` + // Title is a human-readable text of the parameter name. (optional) + Title string `json:"title,omitempty"` + // Tooltip is a human-readable description of the parameter. (optional) + Tooltip string `json:"tooltip,omitempty"` + // Required defines if this given parameter is mandatory. (optional: default false) + Required bool `json:"required,omitempty"` + // ItemType determines the primitive data type represented by the parameter. Parameters are always encoded as + // strings, but ParameterTypes lets them be interpreted as other primitive types. + ItemType ParameterItemType `json:"itemType,omitempty"` + // CollectionType is the type of value this parameter holds - either a single value (a string) or a collection (array or map). + // If Type is set, only the field with that type will be used. If Type is not set, `string` is the default. If Type + // is set to an invalid value, a validation error is thrown. + CollectionType ParameterCollectionType `json:"collectionType,omitempty"` + String string `json:"string,omitempty"` + Map map[string]string `json:"map,omitempty"` + Array []string `json:"array,omitempty"` +} + +// ParametersAnnouncement is a list of announcements. This list represents all the parameters which a CMP is able to +// accept. +type ParametersAnnouncement []ParameterAnnouncement +``` + +#### Implementation Q/A + +1. **Question**: What do we do if the CMP announcement sets more than one `value.{collection}`? + + **Answer**: We ignore all but the configured `collectionType`. + + ```yaml + - name: images + collectionType: map + array: # this gets ignored because collectionType is 'map' + - ubuntu:latest=docker.company.com/proxy/ubuntu:latest + - guestbook:v0.1=docker.company.com/proxy/guestbook:v0.1 + map: + ubuntu:latest: docker.company.com/proxy/ubuntu:latest + guestbook:v0.1: docker.company.com/proxy/guestbook:v0.1 + ``` + +2. **Question**: What do we do if the CMP user sets more than one of `value`/`array`/`map` in the Application spec? + + **Answer**: We send all given information to the CMP and allow it to select the relevant field. + + ```yaml + apiVersion: argoproj.io/v1alpha1 + kind: Application + spec: + source: + plugin: + parameters: + - name: images + array: # this gets sent to the CMP, but the CMP should ignore it + - ubuntu:latest=docker.company.com/proxy/ubuntu:latest + - guestbook:v0.1=docker.company.com/proxy/guestbook:v0.1 + map: + ubuntu:latest: docker.company.com/proxy/ubuntu:latest + guestbook:v0.1: docker.company.com/proxy/guestbook:v0.1 + ``` + +3. **Question**: How will the UI know that adding more items to an array or a map is allowed? + + **Answer**: Always assume it's allowed to add to a map or array. + + ```yaml + - name: images + collectionType: map # users will be allowed to add new items, because this is a map + map: + ubuntu:latest: docker.company.com/proxy/ubuntu:latest + guestbook:v0.1: docker.company.com/proxy/guestbook:v0.1 + ``` + + If the CMP author wants an immutable array or map, they should just break it into individual parameters. + + ```yaml + - name: ubuntu:latest + string: docker.company.com/proxy/ubuntu:latest + - name: guestbook:v0.1 + string: docker.company.com/proxy/guestbook:v0.1 + ``` + +4. **Question**: What do we do if a CMP announcement doesn't include a `collectionType`? + + **Answer**: Default to `string`. + + ```yaml + - name: name-prefix # expects a string + - name: helm-parameters-incorrect # expects a string, the map is ignored + map: + global.image.repository: quay.io/argoproj/argocd + - name: helm-parameters # expects a map + collectionType: map + map: + global.image.repository: quay.io/argoproj/argocd + ``` + +5. **Question**: What do we do if a parameter has a missing or absent top-level `name` field? + + **Answer**: Throw a validation error in the CMP server when handling an announcement. Throw a validation error + in the controller and mark the Application as unhealthy if the invalid spec is in the Application. Throw an error + in the CMP server and refuse to generate manifests in the CMP server if given invalid parameters. + + ```yaml + # needs a `name` field + - title: Parameter Overrides + collectionType: map + map: + global.image.repository: quay.io/argoproj/argocd + ``` + +### Detailed examples + +#### Example 1: trivial parameterized CMP + +```yaml +apiVersion: argoproj.io/v1alpha1 +kind: ConfigManagementPlugin +metadata: + name: trivial-cmp +spec: + version: v1.0 + generate: + command: + - sh + - -c + - | + # Pull one parameter value from the "main" section of the given parameters. + CM_NAME_SUFFIX=$(echo "$ARGOCD_APP_PARAMETERS" | jq -r '.["main"][] | select(.name == "cm-name-suffix").value') + cat << EOM + { + "kind": "ConfigMap", + "apiVersion": "v1", + "metadata": { + "name": "$ARGOCD_APP_NAME-$CM_NAME_SUFFIX", + "namespace": "$ARGOCD_APP_NAMESPACE" + } + } + EOM + discover: + fileName: "./trivial-cmp" + parameters: + command: + - sh + - -c + - | + echo '[{"name": "cm-name-suffix"}]' +``` + +#### Example 2: Helm parameters from Kustomize dependency + +**Plugin config** + +```yaml +apiVersion: argoproj.io/v1alpha1 +kind: ConfigManagementPlugin +metadata: + name: kustomize-helm-proxy-cmp +spec: + version: v1.0 + generate: + command: [/home/argocd/generate.sh] + discover: + fileName: "./kustomization.yaml" + parameters: + static: + - name: version + title: VERSION + string: v4.3.0 + - name: name-prefix + title: NAME PREFIX + - name: name-suffix + title: NAME SUFFIX + dynamic: + command: [/home/argocd/get-parameters.sh] +``` + +**generate.sh** + +This script would be non-trivial. Kustomize only accepts YAML-formatted values for Helm charts. The script would have to +convert the dot-notated parameters to a YAML file. + +**get-parameters.sh** + +```shell +kustomize build . --enable-helm > /dev/null + +get_parameters() { +while read -r chart; do + yq e -o=p "charts/$chart/values.yaml" | jq --arg chart "$chart" --slurp --raw-input ' + { + name: "\($chart)-helm-parameters", + title: "\($chart) Helm parameters", + tooltip: "Parameter overrides for the \($chart) Helm chart.", + collectionType: "map", + map: split("\\n") | map(capture("(?.*) = (?.*)")) | from_entries + }' +done << EOF +$(yq e '.helmCharts[].name' kustomization.yaml) +EOF +} + +# Collect the parameters generated for each chart into one array. +get_parameters | jq --slurp +``` + +**Dockerfile** + +```dockerfile +FROM ubuntu:20.04 + +RUN apt install jq yq helm kustomize -y + +ADD get-parameters.sh /home/argocd/get-parameters.sh +``` + +#### Example 3: simple Helm CMP + +This example demonstrates how the Helm parameters interface could be achieved with a parameterized CMP. + +![Helm parameters interface](images/helm-parameters.png) + +```yaml +apiVersion: argoproj.io/v1alpha1 +kind: ConfigManagementPlugin +metadata: + name: simple-helm-cmp +spec: + version: v1.0 + generate: + command: [/home/argocd/generate.sh] + discover: + fileName: "./values.yaml" + parameters: + static: + - name: values-files + title: VALUES FILES + collectionType: array + dynamic: + command: [/home/argocd/get-parameters.sh] +``` + +**generate.sh** + +```shell +# Convert the values-files parameter value to a newline-delimited list of Helm CLI arguments. +ARGUMENTS=$(echo "$ARGOCD_APP_PARAMETERS" | jq -r '.[] | select(.name == "values-files").array | .[] | "--values=" + .') +# Convert JSON parameters to comma-delimited k=v pairs. +PARAMETERS=$(echo "$ARGOCD_APP_PARAMETERS" | jq -r '.[] | select(.name == "helm-parameters").map | to_entries | map("\(.key)=\(.value)") | .[] | "--set=" + .') +# Add parameters to the arguments variable. +ARGUMENTS="$ARGUMENTS\n$PARAMETERS" +echo "$ARGUMENTS" | xargs helm template . +``` + +The manifest generation command will be +`helm template . --values=a.yaml --values=b.yaml --set=image.repo=alpine --set=image.tag=latest` +for the following value of `$ARGOCD_APP_PARAMETERS`: + +```json +[ + { + "name": "values-files", + "array": ["a.yaml", "b.yaml"] + }, + { + "name": "helm-parameters", + "map": { + "image.repo": "alpine", + "image.tag": "latest" + } + } +] +``` + +**get-parameters.sh** + +```shell +yq e -o=p values.yaml | jq --slurp --raw-input ' + [{ + name: "helm-parameters", + title: "Helm Parameters", + collectionType: "map", + map: split("\\n") | map(capture("(?.*) = (?.*)")) | from_entries + }]' +``` + +Consider a very simple values.yaml: + +```yaml +image: + repo: quay.io/argoproj/argocd + tag: latest +``` + +The script above will produce the following parameters announcement: + +```json +[ + { + "name": "helm-parameters", + "title": "Helm Parameters", + "collectionType": "map", + "map": { + "image.repo": "quay.io/argoproj/argocd", + "image.tag": "latest" + } + } +] +``` + +#### Example 4: simple Kustomize CMP + +```yaml +apiVersion: argoproj.io/v1alpha1 +kind: ConfigManagementPlugin +metadata: + name: kustomize +spec: + parameters: + static: + - name: version + title: VERSION + string: v4.3.0 + - name: name-prefix + title: NAME PREFIX + - name: name-suffix + title: NAME SUFFIX + dynamic: + command: ["generate-params.sh"] +``` + +`parameters.dynamic.command` will produce something like this: + +```yaml +[ + { + "name": "images", + "title": "Image Overrides", + "collectionType": "map", + "map": { + "quay.io/argoproj/argocd": "docker.company.com/proxy/argoproj/argocd", + "ubuntu:latest": "docker.company.com/proxy/argoproj/argocd" + } + } +] +``` + +### Security Considerations + +#### Increased scripting + +Our examples will have shell scripts, and users will write shell scripts. Scripts are difficult +to write securely - this is especially true when the scripts are embedded in YAML, and developers don't get helpful +warnings from the IDE. + +Our docs should emphasize the importance of handling input carefully in any scripts (or other programs) which will be +executed as part of CMPs. + +The docs should also warn against embedding large scripts in YAML and recommend plugin authors instead build custom +images with the script invoked as its own file. The docs should also recommend taking advantage of IDE plugins as +well as image and source code scanning tools in CI/CD. + +### Risks and Mitigations + +1. Risk: encouraging CMP adoption while missing critical features from native tools. + + Mitigation: rewrite the Helm config management tool as a CMP and test as many common use cases as possible. Write a + document before starting on the Helm CMP documenting all major features which must be tested. + +### Upgrade / Downgrade Strategy + +Upgrading will only require using a new version of Argo CD and adding the `parameters` settings to the plugin config. + +Downgrading will only require using an older version of Argo CD. The `parameters` section of the plugin config will +simply be ignored. + +## Drawbacks + +Sidecar CMPs aren't really battle-tested. If there are major issues we've missed, then moving more users towards CMPs +could involve a lot of growing pains. + +## Alternatives