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.