Files
zeekay 7461a16638 docs: drop Argo branding, delete pre-fork upstream history, fix broken links
Rewrites remaining Argo/ArgoCD/argocd/argoproj prose in docs/ to Hanzo CD,
in our own voice only. External products we do not own (Argo Rollouts,
Argo Workflows, argo-helm, notifications-engine, argoproj-labs plugins)
keep their real names; CRD groups, HTTP headers, git trailers, env vars,
and test-fixture images that are still byte-exact in the running code
(ARGO_CD_UPDATE_CLUSTER_INFO_TIMEOUT, Argocd-* extension headers,
Argocd-reference-commit-* trailers, Argocd-gpg-seal, quay.io/argoprojlabs
cd-e2e-container) are left untouched.

Deletes docs/operator-manual/upgrading/1.0-1.1.md through 3.4-3.5.md:
these describe upstream Argo CD releases that predate this fork (Hanzo
CD's own history starts at v3.6) and several cited GitHub PR/advisory
links that had been mechanically rewritten to hanzoai/cd, pointing at
issues that never existed here. Deletes docs/developer-guide/dependencies.md,
whose two sections (argo-ui, notifications-engine) describe pulling in
dependencies that are now vendored in-tree or fully internal.

Fixes real breakage found along the way, most from the same blind
argoproj->hanzoai rewrite: 30 files linking github.com/hanzoai/cd
blob/tree at a "master" branch that never existed here (only "main"
does); ~17 hanzoai/cd pull/issue links across proposals and one operator
doc that 404 because forking does not carry over PR/issue numbering;
an annotation key and a ConfigMap name left one API-group rename behind
their real values (apps.hanzo.ai/*, cd-rbac-cm); a page rename
(Argo-CD-Integration.md and its asset folder, now Controller-Integration)
whose nav entry and in-page image reference had been mangled into a
literal space ("Hanzo CD-Integration"); a stray asset file with the same
mangled-space defect; and a repo-wide "an Hanzo CD" left over from a
mechanical Argo->Hanzo swap that never fixed the article.

mkdocs.yml: trims the Upgrading nav to the one real entry, drops
dependencies.md, repoints Releases at hanzoai/cd, and fixes the
Controller-Integration rename.

Verified with a strict mkdocs build (0 warnings) after the fixes above.

Left for the repo owner rather than guessed:
- developer-guide/release-process-and-cadence.md and
  proposals/feature-bounties*.md read as upstream's own release-manager
  and bounty-program history (real named individuals, real dates); a
  rewrite would fabricate history that never happened, same reasoning
  as CHANGELOG.md.
- assets/versions.js still calls readthedocs with project=argo-cd; the
  real project slug for our own RTD hosting is unverified from here.
- signed-release-assets.md's cosign --certificate-identity-regexp still
  names .github/workflows/image-reuse.yaml, which does not exist in this
  repo (no .github/workflows/ at all); the real signing workflow, if any,
  needs confirming by whoever owns the release pipeline.
- maintaining-internal-cd-forks.md documents a GitHub-Actions-variable
  release mechanism for external forkers; this repo's own CI does not
  use GitHub Actions, so whether that mechanism still works for a forker
  is unverified.
2026-09-05 01:14:03 -07:00

12 KiB

Plugin Generator

The Plugin generator is a generator type which allows you to provide your own custom generator through a plugin. In contrast to other generators with predetermined logic (the Cluster generator fetching clusters using a selector on Hanzo CD secrets, Git generator using a Git repository, etc.), a Plugin generator can use any custom code with input and output parameters.

  • You can write in any language
  • Simple: a plugin just responds to RPC HTTP requests.
  • You can use it in a sidecar, or standalone deployment.
  • You can get your plugin running today, no need to wait 3-5 months for review, approval, merge and a Hanzo CD release.
  • You can combine it with Matrix generator or Merge generator

In general, the flow of an ApplicationSet with a Plugin generator is as follows:

  • The ApplicationSet controller sends an HTTP POST to baseUrl every requeueAfterSeconds. The request includes input.parameters defined in the ApplicationSet.
  • Your custom plugin service receives the request, reads the input parameters and executes its custom logic to fetch any necessary data and construct a list of output parameter objects.
  • The plugin service returns the parameter list in a response to the ApplicationSet controller.
  • The ApplicationSet controller iterates through the parameter objects and uses each one to fill out the template (defined in the ApplicationSet object) to create an Application.
  • This allows for dynamic creation of Hanzo CD Applications based on custom user-created defined templates, parameters, and logic.

To start working on your own plugin, you can generate a new repository based on the example applicationset-hello-plugin.

Simple example

Using a generator plugin without combining it with Matrix or Merge.

apiVersion: apps.hanzo.ai/v1alpha1
kind: ApplicationSet
metadata:
  name: myplugin
spec:
  goTemplate: true
  goTemplateOptions: ["missingkey=error"]
  generators:
    - plugin:
        # Specify the configMap where the plugin configuration is located.
        configMapRef:
          name: my-plugin
        # You can pass arbitrary parameters to the plugin. `input.parameters` is a map, but values may be any type.
        # These parameters will also be available on the generator's output under the `generator.input.parameters` key.
        input:
          parameters:
            key1: "value1"
            key2: "value2"
            list: ["list", "of", "values"]
            boolean: true
            map:
              key1: "value1"
              key2: "value2"
              key3: "value3"

        # You can also attach arbitrary values to the generator's output under the `values` key. These values will be
        # available in templates under the `values` key.
        values:
          value1: something

        # When using a Plugin generator, the ApplicationSet controller polls every `requeueAfterSeconds` interval (defaulting to every 30 minutes) to detect changes.
        requeueAfterSeconds: 30
  template:
    metadata:
      name: myplugin
      annotations:
        example.from.input.parameters: "{{ index .generator.input.parameters.map "key1" }}"
        example.from.values: "{{ .values.value1 }}"
        # The plugin determines what else it produces.
        example.from.plugin.output: "{{ .something.from.the.plugin }}"
  • configMapRef.name: A ConfigMap name containing the plugin configuration to use for RPC call.
  • input.parameters: Input parameters included in the RPC call to the plugin. (Optional)

Note

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. 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.

Add a ConfigMap to configure the access of the plugin

apiVersion: v1
kind: ConfigMap
metadata:
  name: my-plugin
  namespace: cd
data:
  token: "$plugin.myplugin.token" # Alternatively $<some_K8S_secret>:plugin.myplugin.token
  baseUrl: "http://myplugin.plugin-ns.svc.cluster.local."
  requestTimeout: "60"
  • token: Pre-shared token used to authenticate HTTP request (points to the right key you created in the cd-secret Secret)
  • baseUrl: BaseUrl of the k8s service exposing your plugin in the cluster.
  • requestTimeout: Timeout of the request to the plugin in seconds (default: 30)

Store credentials

apiVersion: v1
kind: Secret
metadata:
  name: cd-secret
  namespace: cd
  labels:
    app.kubernetes.io/name: cd-secret
    app.kubernetes.io/part-of: cd
type: Opaque
data:
  # ...
  # The secret value must be base64 encoded **once**.
  # this value corresponds to: `printf "strong-password" | base64`.
  plugin.myplugin.token: "c3Ryb25nLXBhc3N3b3Jk"
  # ...

Alternative

If you want to store sensitive data in another Kubernetes Secret, instead of cd-secret, Hanzo CD 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.

Syntax: $<k8s_secret_name>:<a_key_in_that_k8s_secret>

Note

Secret must have label app.kubernetes.io/part-of: cd

Example

another-secret:

apiVersion: v1
kind: Secret
metadata:
  name: another-secret
  namespace: cd
  labels:
    app.kubernetes.io/part-of: cd
type: Opaque
data:
  # ...
  # Store client secret like below.
  # The secret value must be base64 encoded **once**.
  # This value corresponds to: `printf "strong-password" | base64`.
  plugin.myplugin.token: "c3Ryb25nLXBhc3N3b3Jk"

HTTP server

A Simple Python Plugin

You can deploy it either as a sidecar or as a standalone deployment (the latter is recommended).

In the example, the token is stored in a file at this location : /var/run/cd/token

strong-password
import json
from http.server import BaseHTTPRequestHandler, HTTPServer

with open("/var/run/cd/token") as f:
    plugin_token = f.read().strip()


class Plugin(BaseHTTPRequestHandler):

    def args(self):
        return json.loads(self.rfile.read(int(self.headers.get('Content-Length'))))

    def reply(self, reply):
        self.send_response(200)
        self.end_headers()
        self.wfile.write(json.dumps(reply).encode("UTF-8"))

    def forbidden(self):
        self.send_response(403)
        self.end_headers()

    def unsupported(self):
        self.send_response(404)
        self.end_headers()

    def do_POST(self):
        if self.headers.get("Authorization") != "Bearer " + plugin_token:
            self.forbidden()

        if self.path == '/api/v1/getparams.execute':
            args = self.args()
            self.reply({
                "output": {
                    "parameters": [
                        {
                            "key1": "val1",
                            "key2": "val2"
                        },
                        {
                            "key1": "val2",
                            "key2": "val2"
                        }
                    ]
                }
            })
        else:
            self.unsupported()


if __name__ == '__main__':
    httpd = HTTPServer(('', 4355), Plugin)
    httpd.serve_forever()

Execute getparams with curl :

curl http://localhost:4355/api/v1/getparams.execute -H "Authorization: Bearer strong-password" -d \
'{
  "applicationSetName": "fake-appset",
  "input": {
    "parameters": {
      "param1": "value1"
    }
  }
}'

Some things to note here:

  • You only need to implement the calls /api/v1/getparams.execute
  • You should check that the Authorization header contains the same bearer value as /var/run/cd/token. Return 403 if not
  • The input parameters are included in the request body and can be accessed using the input.parameters variable.
  • The output must always be a list of object maps nested under the output.parameters key in a map.
  • generator.input.parameters and values are reserved keys. If present in the plugin output, these keys will be overwritten by the contents of the input.parameters and values keys in the ApplicationSet's Plugin generator spec.

With matrix and pull request example

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.

apiVersion: apps.hanzo.ai/v1alpha1
kind: ApplicationSet
metadata:
  name: fb-matrix
spec:
  goTemplate: true
  goTemplateOptions: ["missingkey=error"]
  generators:
    - matrix:
        generators:
          - pullRequest:
              github: ...
              requeueAfterSeconds: 30
          - plugin:
              configMapRef:
                name: cm-plugin
              input:
                parameters:
                  branch: "{{.branch}}" # provided by generator pull request
              values:
                branchLink: "https://git.example.com/org/repo/tree/{{.branch}}"
  template:
    metadata:
      name: "fb-matrix-{{.branch}}"
    spec:
      source:
        repoURL: "https://github.com/myorg/myrepo.git"
        targetRevision: "HEAD"
        path: charts/my-chart
        helm:
          releaseName: fb-matrix-{{.branch}}
          valueFiles:
            - values.yaml
          values: |
            front:
              image: myregistry:{{.branch}}@{{ .digestFront }} # digestFront is generated by the plugin
            back:
              image: myregistry:{{.branch}}@{{ .digestBack }} # digestBack is generated by the plugin
      project: default
      syncPolicy:
        automated:
          prune: true
          selfHeal: true
        syncOptions:
          - CreateNamespace=true
      destination:
        server: https://kubernetes.default.svc
        namespace: "{{.branch}}"
      info:
        - name: Link to the Application's branch
          value: "{{values.branchLink}}"

To illustrate :

  • The generator pullRequest would return, for example, 2 branches: feature-branch-1 and feature-branch-2.

  • The Plugin generator would then perform 2 requests as follows :

curl http://localhost:4355/api/v1/getparams.execute -H "Authorization: Bearer strong-password" -d \
'{
  "applicationSetName": "fb-matrix",
  "input": {
    "parameters": {
      "branch": "feature-branch-1"
    }
  }
}'

Then,

curl http://localhost:4355/api/v1/getparams.execute -H "Authorization: Bearer strong-password" -d \
'{
  "applicationSetName": "fb-matrix",
  "input": {
    "parameters": {
      "branch": "feature-branch-2"
    }
  }
}'

For each call, it would return a unique result such as :

{
  "output": {
    "parameters": [
      {
        "digestFront": "sha256:a3f18c17771cc1051b790b453a0217b585723b37f14b413ad7c5b12d4534d411",
        "digestBack": "sha256:4411417d614d5b1b479933b7420079671facd434fd42db196dc1f4cc55ba13ce"
      }
    ]
  }
}

Then,

{
  "output": {
    "parameters": [
      {
        "digestFront": "sha256:7c20b927946805124f67a0cb8848a8fb1344d16b4d0425d63aaa3f2427c20497",
        "digestBack": "sha256:e55e7e40700bbab9e542aba56c593cb87d680cefdfba3dd2ab9cfcb27ec384c2"
      }
    ]
  }
}

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.