github.com/argoproj/argo-cd/v3@v3.2.1/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 requestTimeout: "60" 81 ``` 82 83 - `token`: Pre-shared token used to authenticate HTTP request (points to the right key you created in the `argocd-secret` Secret) 84 - `baseUrl`: BaseUrl of the k8s service exposing your plugin in the cluster. 85 - `requestTimeout`: Timeout of the request to the plugin in seconds (default: 30) 86 87 ### Store credentials 88 89 ```yaml 90 apiVersion: v1 91 kind: Secret 92 metadata: 93 name: argocd-secret 94 namespace: argocd 95 labels: 96 app.kubernetes.io/name: argocd-secret 97 app.kubernetes.io/part-of: argocd 98 type: Opaque 99 data: 100 # ... 101 # The secret value must be base64 encoded **once**. 102 # this value corresponds to: `printf "strong-password" | base64`. 103 plugin.myplugin.token: "c3Ryb25nLXBhc3N3b3Jk" 104 # ... 105 ``` 106 107 #### Alternative 108 109 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. 110 111 Syntax: `$<k8s_secret_name>:<a_key_in_that_k8s_secret>` 112 113 > NOTE: Secret must have label `app.kubernetes.io/part-of: argocd` 114 115 ##### Example 116 117 `another-secret`: 118 119 ```yaml 120 apiVersion: v1 121 kind: Secret 122 metadata: 123 name: another-secret 124 namespace: argocd 125 labels: 126 app.kubernetes.io/part-of: argocd 127 type: Opaque 128 data: 129 # ... 130 # Store client secret like below. 131 # The secret value must be base64 encoded **once**. 132 # This value corresponds to: `printf "strong-password" | base64`. 133 plugin.myplugin.token: "c3Ryb25nLXBhc3N3b3Jk" 134 ``` 135 136 ### HTTP server 137 138 #### A Simple Python Plugin 139 140 You can deploy it either as a sidecar or as a standalone deployment (the latter is recommended). 141 142 In the example, the token is stored in a file at this location : `/var/run/argo/token` 143 144 ``` 145 strong-password 146 ``` 147 148 ```python 149 import json 150 from http.server import BaseHTTPRequestHandler, HTTPServer 151 152 with open("/var/run/argo/token") as f: 153 plugin_token = f.read().strip() 154 155 156 class Plugin(BaseHTTPRequestHandler): 157 158 def args(self): 159 return json.loads(self.rfile.read(int(self.headers.get('Content-Length')))) 160 161 def reply(self, reply): 162 self.send_response(200) 163 self.end_headers() 164 self.wfile.write(json.dumps(reply).encode("UTF-8")) 165 166 def forbidden(self): 167 self.send_response(403) 168 self.end_headers() 169 170 def unsupported(self): 171 self.send_response(404) 172 self.end_headers() 173 174 def do_POST(self): 175 if self.headers.get("Authorization") != "Bearer " + plugin_token: 176 self.forbidden() 177 178 if self.path == '/api/v1/getparams.execute': 179 args = self.args() 180 self.reply({ 181 "output": { 182 "parameters": [ 183 { 184 "key1": "val1", 185 "key2": "val2" 186 }, 187 { 188 "key1": "val2", 189 "key2": "val2" 190 } 191 ] 192 } 193 }) 194 else: 195 self.unsupported() 196 197 198 if __name__ == '__main__': 199 httpd = HTTPServer(('', 4355), Plugin) 200 httpd.serve_forever() 201 ``` 202 203 Execute getparams with curl : 204 205 ``` 206 curl http://localhost:4355/api/v1/getparams.execute -H "Authorization: Bearer strong-password" -d \ 207 '{ 208 "applicationSetName": "fake-appset", 209 "input": { 210 "parameters": { 211 "param1": "value1" 212 } 213 } 214 }' 215 ``` 216 217 Some things to note here: 218 219 - You only need to implement the calls `/api/v1/getparams.execute` 220 - You should check that the `Authorization` header contains the same bearer value as `/var/run/argo/token`. Return 403 if not 221 - The input parameters are included in the request body and can be accessed using the `input.parameters` variable. 222 - The output must always be a list of object maps nested under the `output.parameters` key in a map. 223 - `generator.input.parameters` and `values` are reserved keys. If present in the plugin output, these keys will be overwritten by the 224 contents of the `input.parameters` and `values` keys in the ApplicationSet's plugin generator spec. 225 226 ## With matrix and pull request example 227 228 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. 229 230 ```yaml 231 apiVersion: argoproj.io/v1alpha1 232 kind: ApplicationSet 233 metadata: 234 name: fb-matrix 235 spec: 236 goTemplate: true 237 goTemplateOptions: ["missingkey=error"] 238 generators: 239 - matrix: 240 generators: 241 - pullRequest: 242 github: ... 243 requeueAfterSeconds: 30 244 - plugin: 245 configMapRef: 246 name: cm-plugin 247 input: 248 parameters: 249 branch: "{{.branch}}" # provided by generator pull request 250 values: 251 branchLink: "https://git.example.com/org/repo/tree/{{.branch}}" 252 template: 253 metadata: 254 name: "fb-matrix-{{.branch}}" 255 spec: 256 source: 257 repoURL: "https://github.com/myorg/myrepo.git" 258 targetRevision: "HEAD" 259 path: charts/my-chart 260 helm: 261 releaseName: fb-matrix-{{.branch}} 262 valueFiles: 263 - values.yaml 264 values: | 265 front: 266 image: myregistry:{{.branch}}@{{ .digestFront }} # digestFront is generated by the plugin 267 back: 268 image: myregistry:{{.branch}}@{{ .digestBack }} # digestBack is generated by the plugin 269 project: default 270 syncPolicy: 271 automated: 272 prune: true 273 selfHeal: true 274 syncOptions: 275 - CreateNamespace=true 276 destination: 277 server: https://kubernetes.default.svc 278 namespace: "{{.branch}}" 279 info: 280 - name: Link to the Application's branch 281 value: "{{values.branchLink}}" 282 ``` 283 284 To illustrate : 285 286 - The generator pullRequest would return, for example, 2 branches: `feature-branch-1` and `feature-branch-2`. 287 288 - The generator plugin would then perform 2 requests as follows : 289 290 ```shell 291 curl http://localhost:4355/api/v1/getparams.execute -H "Authorization: Bearer strong-password" -d \ 292 '{ 293 "applicationSetName": "fb-matrix", 294 "input": { 295 "parameters": { 296 "branch": "feature-branch-1" 297 } 298 } 299 }' 300 ``` 301 302 Then, 303 304 ```shell 305 curl http://localhost:4355/api/v1/getparams.execute -H "Authorization: Bearer strong-password" -d \ 306 '{ 307 "applicationSetName": "fb-matrix", 308 "input": { 309 "parameters": { 310 "branch": "feature-branch-2" 311 } 312 } 313 }' 314 ``` 315 316 For each call, it would return a unique result such as : 317 318 ```json 319 { 320 "output": { 321 "parameters": [ 322 { 323 "digestFront": "sha256:a3f18c17771cc1051b790b453a0217b585723b37f14b413ad7c5b12d4534d411", 324 "digestBack": "sha256:4411417d614d5b1b479933b7420079671facd434fd42db196dc1f4cc55ba13ce" 325 } 326 ] 327 } 328 } 329 ``` 330 331 Then, 332 333 ```json 334 { 335 "output": { 336 "parameters": [ 337 { 338 "digestFront": "sha256:7c20b927946805124f67a0cb8848a8fb1344d16b4d0425d63aaa3f2427c20497", 339 "digestBack": "sha256:e55e7e40700bbab9e542aba56c593cb87d680cefdfba3dd2ab9cfcb27ec384c2" 340 } 341 ] 342 } 343 } 344 ``` 345 346 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.