github.com/argoproj/argo-cd/v2@v2.10.5/docs/proposals/parameterized-config-management-plugins.md (about) 1 --- 2 title: Parameterized-Config-Management-Plugins 3 4 authors: 5 - "@alexmt" 6 - "@crenshaw-dev" 7 - "@leoluz" 8 9 sponsors: 10 - TBD 11 12 reviewers: 13 - TBD 14 15 approvers: 16 - TBD 17 18 creation-date: 2022-01-05 19 20 last-updated: 2022-01-05 21 22 --- 23 24 # Parameterized Config Management Plugins 25 26 Config Management Plugin (CMP) parameterization defines a way for plugins to "announce" and then consume acceptable 27 parameters for an Application. Announcing parameters allows CMPs to provide a UI experience similar to native config 28 management tools (Helm, Kustomize, etc.). 29 30 - [Parameterized Config Management Plugins](#parameterized-config-management-plugins) 31 * [Open Questions](#open-questions) 32 * [Summary](#summary) 33 * [Motivation](#motivation) 34 + [1. CMPs are under-utilized](#1-cmps-are-under-utilized) 35 + [2. Decisions about config management tools are limited by the core code](#2-decisions-about-config-management-tools-are-limited-by-the-core-code) 36 + [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) 37 + [Goals](#goals) 38 + [Non-Goals](#non-goals) 39 * [Proposal](#proposal) 40 + [Use cases](#use-cases) 41 - [Use case 1: building Argo CD without config management dependencies](#use-case-1-building-argo-cd-without-config-management-dependencies) 42 - [Use case 2: writing CMPs with rich UI experiences](#use-case-2-writing-cmps-with-rich-ui-experiences) 43 + [Implementation Details/Notes/Constraints](#implementation-detailsnotesconstraints) 44 - [Prerequisites](#prerequisites) 45 - [Terms](#terms) 46 - [How will the ConfigManagementPlugin spec change?](#how-will-the-configmanagementplugin-spec-change) 47 - [How will the CMP know what parameter values are set?](#how-will-the-cmp-know-what-parameter-values-are-set) 48 - [How will the UI know what parameters may be set?](#how-will-the-ui-know-what-parameters-may-be-set) 49 - [Implementation Q/A](#implementation-qa) 50 + [Detailed examples](#detailed-examples) 51 - [Example 1: trivial parameterized CMP](#example-1-trivial-parameterized-cmp) 52 - [Example 2: Helm parameters from Kustomize dependency](#example-2-helm-parameters-from-kustomize-dependency) 53 - [Example 3: simple Helm CMP](#example-3-simple-helm-cmp) 54 - [Example 4: simple Kustomize CMP](#example-4-simple-kustomize-cmp) 55 + [Security Considerations](#security-considerations) 56 - [Increased scripting](#increased-scripting) 57 + [Risks and Mitigations](#risks-and-mitigations) 58 + [Upgrade / Downgrade Strategy](#upgrade--downgrade-strategy) 59 * [Drawbacks](#drawbacks) 60 * [Alternatives](#alternatives) 61 62 ## Open Questions 63 64 * Should we write examples in documentation in Python instead of shell scripts? 65 66 It's very easy to write an insecure shell script. People copy/paste code from documentation to start their own work. 67 Maybe by using a different language in examples, we can encourage more secure CMP development. 68 69 ## Summary 70 71 [Config Management Plugins](https://argo-cd.readthedocs.io/en/stable/user-guide/config-management-plugins/) 72 allow Argo CD administrators to define custom manifest generation tooling. 73 74 The only existing way for users to parameterize manifest generation is with environment variables. 75 76 This proposed feature will allow a plugin to "announce" acceptable parameters for an Application. It will also allow the 77 plugin to consume parameters once the user has set them. 78 79 Parameters definitions may be simple (advertising a simple key/value string pair) or more complex (accepting an array of 80 strings or a map of string keys to string values). Parameter definitions can also specify a data type (string, number, 81 or boolean) to help the UI present the most relevant input field. 82 83 ## Motivation 84 85 ### 1. CMPs are under-utilized 86 87 CMPs, especially the sidecar type, are under-utilized. Making them more robust will increase adoption. Increased 88 adoption will help us find bugs and then make CMPs more robust. In other words, we need to reach a critical mass of 89 CMP users. 90 91 More robust CMPs will make it easier to start supporting tools like [Tanka](https://tanka.dev/). 92 93 ### 2. Decisions about config management tools are limited by the core code 94 95 For example, there's a [Helm bug](https://github.com/argoproj/argo-cd/issues/7291) affecting Argo CD users. The fix 96 would involve importing the Helm SDK (a very large dependency) into Argo CD. Implementing Helm support as a CMP would 97 allow us to use that SDK without embedding it in the core code. 98 99 ### 3. Ksonnet is deprecated, and CMPs are a good place to maintain support 100 101 Offloading Ksonnet to a plugin would allow us to support existing users without maintaining Ksonnet code in the more 102 actively-developed base. But we need CMP parameters to provide Ksonnet support on-par with native support. 103 104 ### Goals 105 106 Parameterized CMPs must be: 107 * Easy to write 108 * An Argo CD admin should be able to write a simple parameterized CMP in just a few lines of code. 109 * An Argo CD admin should be able to write an _advanced_ parameterized CMP server relying on thorough docs. 110 111 Writing a custom CMP server might be preferable if the parameters announcement code gets too complex to be 112 an inline shell script. 113 * Easy to install 114 * Installing a simple CMP or even a CMP with a custom server should be intuitive and painless. 115 * Easy to use 116 * Argo CD end-users (for example, developers) should be able to 117 1. View and set parameters in the Argo CD Application UI 118 2. See the parameters reflected in the Application manifest 119 3. Easily read/modify the generated parameters in the manifest (they should be structured in a way that's easy to read) 120 * CMPs should be able to announce parameters with more helpful interfaces than a simple text field. 121 * For example, numbers and booleans should be represented in the UI with the appropriate inputs. 122 * Future-proof 123 * Since the rich parameters UI is an important feature for config management tools, the parameter definition schema 124 should be flexible enough to announce new _types_ of parameters so the UI can customize its presentation. 125 * Backwards-compatible 126 * CMPs written before this enhancement should work fine after this enhancement is released. 127 * Proven with a rich demonstration 128 * The initial release of this feature should include a CMP implementation of the Helm config tool. This will 129 1. Serve as a rich example for others CMP developers to mimic 130 2. Allow us to decouple the Helm config management release cycle from the Argo release cycle 131 3. Allow us to work around [this bug](https://github.com/argoproj/argo-cd/issues/7291) without including the Helm 132 SDK in the core Argo CD code 133 * The Helm CMP must be on-par with the native implementation. 134 1. It must present an equivalent parameters UI. 135 2. It must communicate errors back to the repo-server (and then the UI) the same as the native implementation. 136 137 ### Non-Goals 138 139 We should not: 140 * Re-implement config management tools as CMPs (besides Helm) 141 142 ## Proposal 143 144 ### Use cases 145 146 #### Use case 1: building Argo CD without config management dependencies 147 148 As an Argo CD developer, I would like to be able to build Argo CD without including the Helm SDK as a dependency. 149 150 The Helm SDK includes the Kubernetes code base. That's a lot of code, and it will make builds unacceptably slow. 151 152 #### Use case 2: writing CMPs with rich UI experiences 153 154 As an Argo CD user, I would like to be able to parameterize manifests built by a CMP. 155 156 For example, if the Argo CD administrator has installed a CMP which applies a last-mile kustomize overlay to a Helm 157 repo, I would like to be able to pass values to the Helm chart without having to manually discover those parameter names 158 (in other words, they should show up in the Application UI just like with a native Helm Application). I also shouldn't 159 have to ask my Argo CD admin to modify the CMP to accommodate the values as environment variables. 160 161 ### Implementation Details/Notes/Constraints 162 163 #### Prerequisites 164 165 Since this proposal is designed to increase CMP adoption, we need to make sure there aren't any bugs that make CMPs 166 less robust than native tools. 167 168 Bugs to fix: 169 1. [#8145](https://github.com/argoproj/argo-cd/issues/8145) - `argocd app sync/diff --local` doesn't account for sidecar CMPs 170 2. [#8243](https://github.com/argoproj/argo-cd/issues/8243) - "Configure plugin via sidecar" ⇒ child resources not pruned on deletion 171 172 #### Terms 173 174 * **Parameter announcement**: an instance of a data structure which describes an individual parameter that may be applied 175 to a specific Application. (See the [schema](#how-will-the-ui-know-what-parameters-may-be-set) below.) 176 * **Parameters announcement**: a list of parameter announcements. (See the [schema](#how-will-the-ui-know-what-parameters-may-be-set) below.) 177 178 "Parameters" is plural because each "announcement" will be a list of multiple parameter announcements. 179 * **Parameterized CMP**: a CMP which supports rich parameters (i.e. more than environment variables). A CMP is 180 parameterized if either of these is true: 181 1. its configuration includes the sections consumed by the default CMP server to generate parameters announcements 182 2. it is a fully customized CMP server which implements an endpoint to generate parameters announcements 183 184 #### How will the ConfigManagementPlugin spec change? 185 186 This proposal adds a new `parameters` key to the ConfigManagementPlugin config spec. 187 188 ```yaml 189 apiVersion: argoproj.io/v1alpha1 190 kind: ConfigManagementPlugin 191 metadata: 192 name: cmp-plugin 193 spec: 194 version: v1.0 195 generate: 196 command: ["example.sh"] 197 discover: 198 fileName: "./subdir/s*.yaml" 199 # NEW KEY 200 parameters: 201 static: 202 # The static announcement follows the parameters announcement schema. This is where a parameter description 203 # should go if it applies to all apps for this CMP. 204 - name: values-file 205 title: Values File 206 tooltip: Path of a Helm values file to apply to the chart. 207 dynamic: 208 # The (optional) generated announcement is combined with the declarative announcement (if present). This is where 209 # a parameter description should be generated if it applies only to a specific app which the CMP handles. 210 command: ["example-params.sh"] 211 ``` 212 213 The currently-configured parameters (if there are any) will be communicated to both `generate.command` and 214 `parameters.dynamic.command` via an `ARGOCD_APP_PARAMETERS` environment variable. The parameters will be encoded 215 according to the [parameters serialization format](#how-will-the-cmp-know-what-parameter-values-are-set) defined below. 216 217 Passing the parameters to the `parameters.dynamic.command` will allow configuration of parameter discovery. For example, 218 if my CMP is designed to handle Kustomize projects which contain Helm charts, I might have the CMP accept an 219 `ignore-helm-charts` parameter to avoid announcing parameters for those charts. 220 221 ```yaml 222 apiVersion: argoproj.io/v1alpha1 223 kind: Application 224 spec: 225 source: 226 plugin: 227 parameters: 228 - name: ignore-helm-charts 229 array: [chart-a, chart-b] 230 ``` 231 232 #### How will the CMP know what parameter values are set? 233 234 Users persist parameter values in an Application's `spec.source.plugin.parameters` list. 235 236 Each parameter has a `name` and a value stored in the `string`, `array`, or `map` field, according to the parameter's 237 collectionType. The name should match the name of some parameter announced by the CMP. (But 238 the user can set any parameter name, so it's the CMP's job to ignore invalid parameters.) 239 240 This example is for a hypothetical Helm CMP. This CMP accepts a `values` and a `values-files` parameter. 241 242 ```yaml 243 apiVersion: argoproj.io/v1alpha1 244 kind: Application 245 spec: 246 source: 247 repoURL: https://github.com/argoproj/argocd-example-apps.git 248 plugin: 249 parameters: 250 - name: values 251 string: >- 252 resources: 253 cpu: 100m 254 memory: 128Mi 255 - name: values-files 256 array: [values.yaml] 257 - name: helm-parameters 258 map: 259 image.repository: my.example.com/gcr-proxy/heptio-images/ks-guestbook-demo 260 image.tag: "0.1" 261 ``` 262 263 When Argo CD generates manifests (for example, when the user clicks "Hard Refresh" in the UI), Argo CD will send these 264 parameters to the CMP as JSON (using the equivalent structure to what's shown above) on an environment variable called 265 `ARGOCD_APP_PARAMETERS`. 266 267 ```shell 268 echo "$ARGOCD_APP_PARAMETERS" | jq 269 ``` 270 271 That command, when run by a CMP with the above Application manifest, will print the following: 272 273 ```json 274 [ 275 { 276 "name": "values", 277 "string": "resources:\n cpu: 100m\n memory: 128Mi" 278 }, 279 { 280 "name": "values-files", 281 "array": ["values.yaml"] 282 }, 283 { 284 "name": "helm-parameters", 285 "map": { 286 "image.repository": "my.example.com/gcr-proxy/heptio-images/ks-guestbook-demo", 287 "image.tag": "0.1" 288 } 289 } 290 ] 291 ``` 292 293 Another way the CMP can access parameters is via environment variables. For example: 294 295 ```shell 296 echo "$VALUES" > /tmp/values.yaml 297 helm template --values /tmp/values.yaml . 298 ``` 299 300 Environment variable names are set according to these rules: 301 302 1. If a parameter is a `string`, the format is `PARAM_{escaped(name)}` (`escaped` is defined below). 303 2. If a parameter is an `array`, the format is `PARAM_{escaped(name_{index})}` (where the first index is 0). 304 3. If a parameter is a `map`, the format is `PARAM_{escaped(name_key)}`. 305 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/), 306 the build environment variable wins. 307 5. If more than one parameter name produces the same env var name, the env var later in the list wins. 308 309 The `escaped` function will perform the following tasks: 310 1. It will uppercase the input. 311 2. It will replace any characters matching this regex with an underscore: `[^A-Z0-9_]`. 312 313 The above example will produce the following env vars: 314 315 ```shell 316 echo "$PARAM_VALUES" 317 echo "$PARAM_VALUES_FILES_0" 318 echo "$PARAM_HELM_PARAMETERS_IMAGE_REPOSITORY" 319 echo "$PARAM_HELM_PARAMETERS_IMAGE_TAG" 320 ``` 321 322 The parameters in the Application manifest are represented behind the scenes with the following Go types: 323 324 ```go 325 package cmp 326 327 // Parameter represents a single parameter name and its value. One of Value, Map, or Array must be set. 328 type Parameter struct { 329 // Name is the name identifying a parameter. (required) 330 Name string `json:"name,omitempty"` 331 String string `json:"string,omitempty"` 332 Map map[string]string `json:"map,omitempty"` 333 Array []string `json:"array,omitempty"` 334 } 335 336 // Parameters is a list of parameters to be sent to a CMP for manifest generation. 337 type Parameters []Parameter 338 ``` 339 340 #### How will the UI know what parameters may be set? 341 342 The CMP developer will have two ways to announce acceptable parameters: statically (declaratively) and dynamically. 343 344 Static parameter announcements are written directly into the CMP config file: 345 346 ```yaml 347 apiVersion: argoproj.io/v1alpha1 348 kind: ConfigManagementPlugin 349 metadata: 350 name: helm 351 spec: 352 parameters: 353 static: 354 - name: values-files 355 title: Values Files 356 collectionType: array 357 ``` 358 359 Since this hypothetical Helm CMP will accept an array of values.yaml files for every app it handles, the CMP developer 360 can add that parameter as a static parameter announcement in the CMP config. 361 362 Dynamic parameters are generated by a CMP developer-defined command. 363 364 A parameter definition is an object with following schema: 365 366 ```yaml 367 apiVersion: argoproj.io/v1alpha1 368 kind: ConfigManagementPlugin 369 metadata: 370 name: helm 371 spec: 372 parameters: 373 dynamic: 374 command: 375 - sh 376 - -c 377 - | 378 # Use yq to generate a list of parameters. Then use jq to convert that list of parameters to a parameters 379 # announcement list. 380 yq e -o=p values.yaml | jq -nR ' 381 [{ 382 name: "helm-parameters", 383 title: "Helm Parameters", 384 tooltip: "Parameters to override when generating manifests with Helm", 385 collectionType: "map", 386 map: (inputs | capture("(?<key>.*) = (?<value>.*)") | from_entries) 387 }]' 388 ``` 389 390 For a Helm chart with only an `image.repository` and `image.tag` in values.yaml, the parameter announcement would look 391 like this: 392 393 ```json 394 [ 395 { 396 "name": "helm-parameters", 397 "collectionType": "map", 398 "title": "Helm Parameters", 399 "tooltip": "Parameters to override when generating manifests with Helm", 400 "map": { 401 "image.repository": "my.example.com/gcr-proxy/heptio-images/ks-guestbook-demo", 402 "image.tag": "0.1" 403 } 404 } 405 ] 406 ``` 407 408 Before sending a parameters announcement to the UI, the CMP server will combine the static and dynamic parameters. 409 (Behind the scenes, the list is actually communicated to the UI via gRPC, but they're presented here as JSON for 410 readability.) 411 412 ```json 413 414 [ 415 { 416 "name": "values-files", 417 "title": "Values Files", 418 "collectionType": "array" 419 }, 420 { 421 "name": "helm-parameters", 422 "collectionType": "map", 423 "title": "Helm Parameters", 424 "tooltip": "Parameters to override when generating manifests with Helm", 425 "map": { 426 "image.repository": "my.example.com/gcr-proxy/heptio-images/ks-guestbook-demo", 427 "image.tag": "0.1" 428 } 429 } 430 ] 431 ``` 432 433 This is the full parameters announcement schema as Go types. 434 435 ```go 436 package cmp 437 438 // ParameterItemType is the primitive data type of each of the parameter's value (or each of its values, if it's an array or 439 // a map). 440 type ParameterItemType string 441 442 // Anything besides "number" and "boolean" is treated as string. 443 const ( 444 ParameterItemTypeNumber ParameterItemType = "number" 445 ParameterItemTypeBoolean ParameterItemType = "boolean" 446 ) 447 448 // ParameterCollectionType is a parameter's value's type - a single value (like a string) or a collection (like an array or a 449 // map). 450 type ParameterCollectionType string 451 452 // Anything besides "number" and "boolean" is treated as string. 453 const ( 454 ParameterCollectionTypeMap ParameterCollectionType = "map" 455 ParameterCollectionTypeArray ParameterCollectionType = "array" 456 ) 457 458 // ParameterAnnouncement represents a CMP's announcement of one acceptable parameter (though that parameter may contain 459 // multiple elements, if the value holds an array or a map). 460 type ParameterAnnouncement struct { 461 // Name is the name identifying a parameter. (required) 462 Name string `json:"name,omitempty"` 463 // Title is a human-readable text of the parameter name. (optional) 464 Title string `json:"title,omitempty"` 465 // Tooltip is a human-readable description of the parameter. (optional) 466 Tooltip string `json:"tooltip,omitempty"` 467 // Required defines if this given parameter is mandatory. (optional: default false) 468 Required bool `json:"required,omitempty"` 469 // ItemType determines the primitive data type represented by the parameter. Parameters are always encoded as 470 // strings, but ParameterTypes lets them be interpreted as other primitive types. 471 ItemType ParameterItemType `json:"itemType,omitempty"` 472 // CollectionType is the type of value this parameter holds - either a single value (a string) or a collection (array or map). 473 // If Type is set, only the field with that type will be used. If Type is not set, `string` is the default. If Type 474 // is set to an invalid value, a validation error is thrown. 475 CollectionType ParameterCollectionType `json:"collectionType,omitempty"` 476 String string `json:"string,omitempty"` 477 Map map[string]string `json:"map,omitempty"` 478 Array []string `json:"array,omitempty"` 479 } 480 481 // ParametersAnnouncement is a list of announcements. This list represents all the parameters which a CMP is able to 482 // accept. 483 type ParametersAnnouncement []ParameterAnnouncement 484 ``` 485 486 #### Implementation Q/A 487 488 1. **Question**: What do we do if the CMP announcement sets more than one `value.{collection}`? 489 490 **Answer**: We ignore all but the configured `collectionType`. 491 492 ```yaml 493 - name: images 494 collectionType: map 495 array: # this gets ignored because collectionType is 'map' 496 - ubuntu:latest=docker.example.com/proxy/ubuntu:latest 497 - guestbook:v0.1=docker.example.com/proxy/guestbook:v0.1 498 map: 499 ubuntu:latest: docker.example.com/proxy/ubuntu:latest 500 guestbook:v0.1: docker.example.com/proxy/guestbook:v0.1 501 ``` 502 503 2. **Question**: What do we do if the CMP user sets more than one of `value`/`array`/`map` in the Application spec? 504 505 **Answer**: We send all given information to the CMP and allow it to select the relevant field. 506 507 ```yaml 508 apiVersion: argoproj.io/v1alpha1 509 kind: Application 510 spec: 511 source: 512 plugin: 513 parameters: 514 - name: images 515 array: # this gets sent to the CMP, but the CMP should ignore it 516 - ubuntu:latest=docker.example.com/proxy/ubuntu:latest 517 - guestbook:v0.1=docker.example.com/proxy/guestbook:v0.1 518 map: 519 ubuntu:latest: docker.example.com/proxy/ubuntu:latest 520 guestbook:v0.1: docker.example.com/proxy/guestbook:v0.1 521 ``` 522 523 3. **Question**: How will the UI know that adding more items to an array or a map is allowed? 524 525 **Answer**: Always assume it's allowed to add to a map or array. 526 527 ```yaml 528 - name: images 529 collectionType: map # users will be allowed to add new items, because this is a map 530 map: 531 ubuntu:latest: docker.example.com/proxy/ubuntu:latest 532 guestbook:v0.1: docker.example.com/proxy/guestbook:v0.1 533 ``` 534 535 If the CMP author wants an immutable array or map, they should just break it into individual parameters. 536 537 ```yaml 538 - name: ubuntu:latest 539 string: docker.example.com/proxy/ubuntu:latest 540 - name: guestbook:v0.1 541 string: docker.example.com/proxy/guestbook:v0.1 542 ``` 543 544 4. **Question**: What do we do if a CMP announcement doesn't include a `collectionType`? 545 546 **Answer**: Default to `string`. 547 548 ```yaml 549 - name: name-prefix # expects a string 550 - name: helm-parameters-incorrect # expects a string, the map is ignored 551 map: 552 global.image.repository: quay.io/argoproj/argocd 553 - name: helm-parameters # expects a map 554 collectionType: map 555 map: 556 global.image.repository: quay.io/argoproj/argocd 557 ``` 558 559 5. **Question**: What do we do if a parameter has a missing or absent top-level `name` field? 560 561 **Answer**: Throw a validation error in the CMP server when handling an announcement. Throw a validation error 562 in the controller and mark the Application as unhealthy if the invalid spec is in the Application. Throw an error 563 in the CMP server and refuse to generate manifests in the CMP server if given invalid parameters. 564 565 ```yaml 566 # needs a `name` field 567 - title: Parameter Overrides 568 collectionType: map 569 map: 570 global.image.repository: quay.io/argoproj/argocd 571 ``` 572 573 ### Detailed examples 574 575 #### Example 1: trivial parameterized CMP 576 577 ```yaml 578 apiVersion: argoproj.io/v1alpha1 579 kind: ConfigManagementPlugin 580 metadata: 581 name: trivial-cmp 582 spec: 583 version: v1.0 584 generate: 585 command: 586 - sh 587 - -c 588 - | 589 # Pull one parameter value from the "main" section of the given parameters. 590 CM_NAME_SUFFIX=$(echo "$ARGOCD_APP_PARAMETERS" | jq -r '.["main"][] | select(.name == "cm-name-suffix").value') 591 cat << EOM 592 { 593 "kind": "ConfigMap", 594 "apiVersion": "v1", 595 "metadata": { 596 "name": "$ARGOCD_APP_NAME-$CM_NAME_SUFFIX", 597 "namespace": "$ARGOCD_APP_NAMESPACE" 598 } 599 } 600 EOM 601 discover: 602 fileName: "./trivial-cmp" 603 parameters: 604 command: 605 - sh 606 - -c 607 - | 608 echo '[{"name": "cm-name-suffix"}]' 609 ``` 610 611 #### Example 2: Helm parameters from Kustomize dependency 612 613 **Plugin config** 614 615 ```yaml 616 apiVersion: argoproj.io/v1alpha1 617 kind: ConfigManagementPlugin 618 metadata: 619 name: kustomize-helm-proxy-cmp 620 spec: 621 version: v1.0 622 generate: 623 command: [/home/argocd/generate.sh] 624 discover: 625 fileName: "./kustomization.yaml" 626 parameters: 627 static: 628 - name: version 629 title: VERSION 630 string: v4.3.0 631 - name: name-prefix 632 title: NAME PREFIX 633 - name: name-suffix 634 title: NAME SUFFIX 635 dynamic: 636 command: [/home/argocd/get-parameters.sh] 637 ``` 638 639 **generate.sh** 640 641 This script would be non-trivial. Kustomize only accepts YAML-formatted values for Helm charts. The script would have to 642 convert the dot-notated parameters to a YAML file. 643 644 **get-parameters.sh** 645 646 ```shell 647 kustomize build . --enable-helm > /dev/null 648 649 get_parameters() { 650 while read -r chart; do 651 yq e -o=p "charts/$chart/values.yaml" | jq --arg chart "$chart" --slurp --raw-input ' 652 { 653 name: "\($chart)-helm-parameters", 654 title: "\($chart) Helm parameters", 655 tooltip: "Parameter overrides for the \($chart) Helm chart.", 656 collectionType: "map", 657 map: split("\\n") | map(capture("(?<key>.*) = (?<value>.*)")) | from_entries 658 }' 659 done << EOF 660 $(yq e '.helmCharts[].name' kustomization.yaml) 661 EOF 662 } 663 664 # Collect the parameters generated for each chart into one array. 665 get_parameters | jq --slurp 666 ``` 667 668 **Dockerfile** 669 670 ```dockerfile 671 FROM ubuntu:20.04 672 673 RUN apt install jq yq helm kustomize -y 674 675 ADD get-parameters.sh /home/argocd/get-parameters.sh 676 ``` 677 678 #### Example 3: simple Helm CMP 679 680 This example demonstrates how the Helm parameters interface could be achieved with a parameterized CMP. 681 682  683 684 ```yaml 685 apiVersion: argoproj.io/v1alpha1 686 kind: ConfigManagementPlugin 687 metadata: 688 name: simple-helm-cmp 689 spec: 690 version: v1.0 691 generate: 692 command: [/home/argocd/generate.sh] 693 discover: 694 fileName: "./values.yaml" 695 parameters: 696 static: 697 - name: values-files 698 title: VALUES FILES 699 collectionType: array 700 dynamic: 701 command: [/home/argocd/get-parameters.sh] 702 ``` 703 704 **generate.sh** 705 706 ```shell 707 # Convert the values-files parameter value to a newline-delimited list of Helm CLI arguments. 708 ARGUMENTS=$(echo "$ARGOCD_APP_PARAMETERS" | jq -r '.[] | select(.name == "values-files").array | .[] | "--values=" + .') 709 # Convert JSON parameters to comma-delimited k=v pairs. 710 PARAMETERS=$(echo "$ARGOCD_APP_PARAMETERS" | jq -r '.[] | select(.name == "helm-parameters").map | to_entries | map("\(.key)=\(.value)") | .[] | "--set=" + .') 711 # Add parameters to the arguments variable. 712 ARGUMENTS="$ARGUMENTS\n$PARAMETERS" 713 echo "$ARGUMENTS" | xargs helm template . 714 ``` 715 716 The manifest generation command will be 717 `helm template . --values=a.yaml --values=b.yaml --set=image.repo=alpine --set=image.tag=latest` 718 for the following value of `$ARGOCD_APP_PARAMETERS`: 719 720 ```json 721 [ 722 { 723 "name": "values-files", 724 "array": ["a.yaml", "b.yaml"] 725 }, 726 { 727 "name": "helm-parameters", 728 "map": { 729 "image.repo": "alpine", 730 "image.tag": "latest" 731 } 732 } 733 ] 734 ``` 735 736 **get-parameters.sh** 737 738 ```shell 739 yq e -o=p values.yaml | jq --slurp --raw-input ' 740 [{ 741 name: "helm-parameters", 742 title: "Helm Parameters", 743 collectionType: "map", 744 map: split("\\n") | map(capture("(?<key>.*) = (?<value>.*)")) | from_entries 745 }]' 746 ``` 747 748 Consider a very simple values.yaml: 749 750 ```yaml 751 image: 752 repo: quay.io/argoproj/argocd 753 tag: latest 754 ``` 755 756 The script above will produce the following parameters announcement: 757 758 ```json 759 [ 760 { 761 "name": "helm-parameters", 762 "title": "Helm Parameters", 763 "collectionType": "map", 764 "map": { 765 "image.repo": "quay.io/argoproj/argocd", 766 "image.tag": "latest" 767 } 768 } 769 ] 770 ``` 771 772 #### Example 4: simple Kustomize CMP 773 774 ```yaml 775 apiVersion: argoproj.io/v1alpha1 776 kind: ConfigManagementPlugin 777 metadata: 778 name: kustomize 779 spec: 780 parameters: 781 static: 782 - name: version 783 title: VERSION 784 string: v4.3.0 785 - name: name-prefix 786 title: NAME PREFIX 787 - name: name-suffix 788 title: NAME SUFFIX 789 dynamic: 790 command: ["generate-params.sh"] 791 ``` 792 793 `parameters.dynamic.command` will produce something like this: 794 795 ```yaml 796 [ 797 { 798 "name": "images", 799 "title": "Image Overrides", 800 "collectionType": "map", 801 "map": { 802 "quay.io/argoproj/argocd": "docker.example.com/proxy/argoproj/argocd", 803 "ubuntu:latest": "docker.example.com/proxy/argoproj/argocd" 804 } 805 } 806 ] 807 ``` 808 809 ### Security Considerations 810 811 #### Increased scripting 812 813 Our examples will have shell scripts, and users will write shell scripts. Scripts are difficult 814 to write securely - this is especially true when the scripts are embedded in YAML, and developers don't get helpful 815 warnings from the IDE. 816 817 Our docs should emphasize the importance of handling input carefully in any scripts (or other programs) which will be 818 executed as part of CMPs. 819 820 The docs should also warn against embedding large scripts in YAML and recommend plugin authors instead build custom 821 images with the script invoked as its own file. The docs should also recommend taking advantage of IDE plugins as 822 well as image and source code scanning tools in CI/CD. 823 824 ### Risks and Mitigations 825 826 1. Risk: encouraging CMP adoption while missing critical features from native tools. 827 828 Mitigation: rewrite the Helm config management tool as a CMP and test as many common use cases as possible. Write a 829 document before starting on the Helm CMP documenting all major features which must be tested. 830 831 ### Upgrade / Downgrade Strategy 832 833 Upgrading will only require using a new version of Argo CD and adding the `parameters` settings to the plugin config. 834 835 Downgrading will only require using an older version of Argo CD. The `parameters` section of the plugin config will 836 simply be ignored. 837 838 ## Drawbacks 839 840 Sidecar CMPs aren't really battle-tested. If there are major issues we've missed, then moving more users towards CMPs 841 could involve a lot of growing pains. 842 843 ## Alternatives