github.com/argoproj/argo-cd/v2@v2.10.9/docs/operator-manual/applicationset/Generators-Plugin.md (about) 1 # Plugin Generator 2 3 Plugins allow you to provide your own generator. 4 5 - You can write in any language 6 - Simple: a plugin just responds to RPC HTTP requests. 7 - You can use it in a sidecar, or standalone deployment. 8 - You can get your plugin running today, no need to wait 3-5 months for review, approval, merge and an Argo software 9 release. 10 - You can combine it with Matrix or Merge. 11 12 To start working on your own plugin, you can generate a new repository based on the example 13 [applicationset-hello-plugin](https://github.com/argoproj-labs/applicationset-hello-plugin). 14 15 ## Simple example 16 17 Using a generator plugin without combining it with Matrix or Merge. 18 19 ```yaml 20 apiVersion: argoproj.io/v1alpha1 21 kind: ApplicationSet 22 metadata: 23 name: myplugin 24 spec: 25 goTemplate: true 26 goTemplateOptions: ["missingkey=error"] 27 generators: 28 - plugin: 29 # Specify the configMap where the plugin configuration is located. 30 configMapRef: 31 name: my-plugin 32 # You can pass arbitrary parameters to the plugin. `input.parameters` is a map, but values may be any type. 33 # These parameters will also be available on the generator's output under the `generator.input.parameters` key. 34 input: 35 parameters: 36 key1: "value1" 37 key2: "value2" 38 list: ["list", "of", "values"] 39 boolean: true 40 map: 41 key1: "value1" 42 key2: "value2" 43 key3: "value3" 44 45 # You can also attach arbitrary values to the generator's output under the `values` key. These values will be 46 # available in templates under the `values` key. 47 values: 48 value1: something 49 50 # When using a Plugin generator, the ApplicationSet controller polls every `requeueAfterSeconds` interval (defaulting to every 30 minutes) to detect changes. 51 requeueAfterSeconds: 30 52 template: 53 metadata: 54 name: myplugin 55 annotations: 56 example.from.input.parameters: "{{ index .generator.input.parameters.map "key1" }}" 57 example.from.values: "{{ .values.value1 }}" 58 # The plugin determines what else it produces. 59 example.from.plugin.output: "{{ .something.from.the.plugin }}" 60 ``` 61 62 - `configMapRef.name`: A `ConfigMap` name containing the plugin configuration to use for RPC call. 63 - `input.parameters`: Input parameters included in the RPC call to the plugin. (Optional) 64 65 !!! note 66 The concept of the plugin should not undermine the spirit of GitOps by externalizing data outside of Git. The goal is to be complementary in specific contexts. 67 For example, when using one of the PullRequest generators, it's impossible to retrieve parameters related to the CI (only the commit hash is available), which limits the possibilities. By using a plugin, it's possible to retrieve the necessary parameters from a separate data source and use them to extend the functionality of the generator. 68 69 ### Add a ConfigMap to configure the access of the plugin 70 71 ```yaml 72 apiVersion: v1 73 kind: ConfigMap 74 metadata: 75 name: my-plugin 76 namespace: argocd 77 data: 78 token: "$plugin.myplugin.token" # Alternatively $<some_K8S_secret>:plugin.myplugin.token 79 baseUrl: "http://myplugin.plugin-ns.svc.cluster.local." 80 ``` 81 82 - `token`: Pre-shared token used to authenticate HTTP request (points to the right key you created in the `argocd-secret` Secret) 83 - `baseUrl`: BaseUrl of the k8s service exposing your plugin in the cluster. 84 85 ### Store credentials 86 87 ```yaml 88 apiVersion: v1 89 kind: Secret 90 metadata: 91 name: argocd-secret 92 namespace: argocd 93 labels: 94 app.kubernetes.io/name: argocd-secret 95 app.kubernetes.io/part-of: argocd 96 type: Opaque 97 data: 98 # ... 99 # The secret value must be base64 encoded **once**. 100 # this value corresponds to: `printf "strong-password" | base64`. 101 plugin.myplugin.token: "c3Ryb25nLXBhc3N3b3Jk" 102 # ... 103 ``` 104 105 #### Alternative 106 107 If you want to store sensitive data in **another** Kubernetes `Secret`, instead of `argocd-secret`, ArgoCD knows how to check the keys under `data` in your Kubernetes `Secret` for a corresponding key whenever a value in a configmap starts with `$`, then your Kubernetes `Secret` name and `:` (colon) followed by the key name. 108 109 Syntax: `$<k8s_secret_name>:<a_key_in_that_k8s_secret>` 110 111 > NOTE: Secret must have label `app.kubernetes.io/part-of: argocd` 112 113 ##### Example 114 115 `another-secret`: 116 117 ```yaml 118 apiVersion: v1 119 kind: Secret 120 metadata: 121 name: another-secret 122 namespace: argocd 123 labels: 124 app.kubernetes.io/part-of: argocd 125 type: Opaque 126 data: 127 # ... 128 # Store client secret like below. 129 # The secret value must be base64 encoded **once**. 130 # This value corresponds to: `printf "strong-password" | base64`. 131 plugin.myplugin.token: "c3Ryb25nLXBhc3N3b3Jk" 132 ``` 133 134 ### HTTP server 135 136 #### A Simple Python Plugin 137 138 You can deploy it either as a sidecar or as a standalone deployment (the latter is recommended). 139 140 In the example, the token is stored in a file at this location : `/var/run/argo/token` 141 142 ``` 143 strong-password 144 ``` 145 146 ```python 147 import json 148 from http.server import BaseHTTPRequestHandler, HTTPServer 149 150 with open("/var/run/argo/token") as f: 151 plugin_token = f.read().strip() 152 153 154 class Plugin(BaseHTTPRequestHandler): 155 156 def args(self): 157 return json.loads(self.rfile.read(int(self.headers.get('Content-Length')))) 158 159 def reply(self, reply): 160 self.send_response(200) 161 self.end_headers() 162 self.wfile.write(json.dumps(reply).encode("UTF-8")) 163 164 def forbidden(self): 165 self.send_response(403) 166 self.end_headers() 167 168 def unsupported(self): 169 self.send_response(404) 170 self.end_headers() 171 172 def do_POST(self): 173 if self.headers.get("Authorization") != "Bearer " + plugin_token: 174 self.forbidden() 175 176 if self.path == '/api/v1/getparams.execute': 177 args = self.args() 178 self.reply({ 179 "output": { 180 "parameters": [ 181 { 182 "key1": "val1", 183 "key2": "val2" 184 }, 185 { 186 "key1": "val2", 187 "key2": "val2" 188 } 189 ] 190 } 191 }) 192 else: 193 self.unsupported() 194 195 196 if __name__ == '__main__': 197 httpd = HTTPServer(('', 4355), Plugin) 198 httpd.serve_forever() 199 ``` 200 201 Execute getparams with curl : 202 203 ``` 204 curl http://localhost:4355/api/v1/getparams.execute -H "Authorization: Bearer strong-password" -d \ 205 '{ 206 "applicationSetName": "fake-appset", 207 "input": { 208 "parameters": { 209 "param1": "value1" 210 } 211 } 212 }' 213 ``` 214 215 Some things to note here: 216 217 - You only need to implement the calls `/api/v1/getparams.execute` 218 - You should check that the `Authorization` header contains the same bearer value as `/var/run/argo/token`. Return 403 if not 219 - The input parameters are included in the request body and can be accessed using the `input.parameters` variable. 220 - The output must always be a list of object maps nested under the `output.parameters` key in a map. 221 - `generator.input.parameters` and `values` are reserved keys. If present in the plugin output, these keys will be overwritten by the 222 contents of the `input.parameters` and `values` keys in the ApplicationSet's plugin generator spec. 223 224 ## With matrix and pull request example 225 226 In the following example, the plugin implementation is returning a set of image digests for the given branch. The returned list contains only one item corresponding to the latest built image for the branch. 227 228 ```yaml 229 apiVersion: argoproj.io/v1alpha1 230 kind: ApplicationSet 231 metadata: 232 name: fb-matrix 233 spec: 234 goTemplate: true 235 goTemplateOptions: ["missingkey=error"] 236 generators: 237 - matrix: 238 generators: 239 - pullRequest: 240 github: ... 241 requeueAfterSeconds: 30 242 - plugin: 243 configMapRef: 244 name: cm-plugin 245 input: 246 parameters: 247 branch: "{{.branch}}" # provided by generator pull request 248 values: 249 branchLink: "https://git.example.com/org/repo/tree/{{.branch}}" 250 template: 251 metadata: 252 name: "fb-matrix-{{.branch}}" 253 spec: 254 source: 255 repoURL: "https://github.com/myorg/myrepo.git" 256 targetRevision: "HEAD" 257 path: charts/my-chart 258 helm: 259 releaseName: fb-matrix-{{.branch}} 260 valueFiles: 261 - values.yaml 262 values: | 263 front: 264 image: myregistry:{{.branch}}@{{ .digestFront }} # digestFront is generated by the plugin 265 back: 266 image: myregistry:{{.branch}}@{{ .digestBack }} # digestBack is generated by the plugin 267 project: default 268 syncPolicy: 269 automated: 270 prune: true 271 selfHeal: true 272 syncOptions: 273 - CreateNamespace=true 274 destination: 275 server: https://kubernetes.default.svc 276 namespace: "{{.branch}}" 277 info: 278 - name: Link to the Application's branch 279 value: "{{values.branchLink}}" 280 ``` 281 282 To illustrate : 283 284 - The generator pullRequest would return, for example, 2 branches: `feature-branch-1` and `feature-branch-2`. 285 286 - The generator plugin would then perform 2 requests as follows : 287 288 ```shell 289 curl http://localhost:4355/api/v1/getparams.execute -H "Authorization: Bearer strong-password" -d \ 290 '{ 291 "applicationSetName": "fb-matrix", 292 "input": { 293 "parameters": { 294 "branch": "feature-branch-1" 295 } 296 } 297 }' 298 ``` 299 300 Then, 301 302 ```shell 303 curl http://localhost:4355/api/v1/getparams.execute -H "Authorization: Bearer strong-password" -d \ 304 '{ 305 "applicationSetName": "fb-matrix", 306 "input": { 307 "parameters": { 308 "branch": "feature-branch-2" 309 } 310 } 311 }' 312 ``` 313 314 For each call, it would return a unique result such as : 315 316 ```json 317 { 318 "output": { 319 "parameters": [ 320 { 321 "digestFront": "sha256:a3f18c17771cc1051b790b453a0217b585723b37f14b413ad7c5b12d4534d411", 322 "digestBack": "sha256:4411417d614d5b1b479933b7420079671facd434fd42db196dc1f4cc55ba13ce" 323 } 324 ] 325 } 326 } 327 ``` 328 329 Then, 330 331 ```json 332 { 333 "output": { 334 "parameters": [ 335 { 336 "digestFront": "sha256:7c20b927946805124f67a0cb8848a8fb1344d16b4d0425d63aaa3f2427c20497", 337 "digestBack": "sha256:e55e7e40700bbab9e542aba56c593cb87d680cefdfba3dd2ab9cfcb27ec384c2" 338 } 339 ] 340 } 341 } 342 ``` 343 344 In this example, by combining the two, you ensure that one or more pull requests are available and that the generated tag has been properly generated. This wouldn't have been possible with just a commit hash because a hash alone does not certify the success of the build.