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.