Plugin Generator

Plugin Generator

자신의 커스텀 generator를 플러그인으로 제공할 수 있게 해주는 generator 유형이에요. 미리 결정된 로직을 가진 다른 generator(ArgoCD 시크릿에서 셀렉터로 클러스터를 가져오는 Cluster generator, Git 리포지토리를 사용하는 Git generator 등)와 달리, Plugin generator는 입력·출력 파라미터가 있는 어떤 커스텀 코드든 사용할 수 있어요.

출처: 문서

본문

  • 어떤 언어로든 작성할 수 있어요
  • 간단해요: 플러그인은 RPC HTTP 요청에 응답하기만 하면 돼요
  • 사이드카 또는 독립 배포로 사용할 수 있어요
  • 지금 바로 플러그인을 실행할 수 있어요 — 리뷰, 승인, 머지, Argo 소프트웨어 릴리스를 위해 3-5개월 기다릴 필요 없어요
  • Matrix generator 또는 Merge generator와 결합할 수 있어요

일반적으로 Plugin generator가 있는 ApplicationSet의 흐름은 다음과 같아요:

  • ApplicationSet 컨트롤러는 requeueAfterSeconds마다 baseUrl에 HTTP POST를 보내요. 요청에는 ApplicationSet에 정의된 input.parameters가 포함돼요.
  • 커스텀 플러그인 서비스가 요청을 받고, 입력 파라미터를 읽고, 커스텀 로직을 실행해 필요한 데이터를 가져오고 출력 파라미터 객체 목록을 구성해요.
  • 플러그인 서비스는 응답으로 파라미터 목록을 ApplicationSet 컨트롤러에 반환해요.
  • ApplicationSet 컨트롤러는 파라미터 객체를 반복하며 각각을 사용해 (ApplicationSet 객체에 정의된) 템플릿을 채워 Application을 생성해요.
  • 이를 통해 사용자가 만든 정의된 템플릿, 파라미터, 로직에 기반한 Argo CD Application의 동적 생성이 가능해져요.

자체 플러그인 작업을 시작하려면 예시 applicationset-hello-plugin을 기반으로 새 리포지토리를 생성할 수 있어요.

간단한 예시 (Simple example)

Matrix나 Merge와 결합하지 않고 generator 플러그인을 사용해요.

apiVersion: argoproj.io/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: RPC 호출에 사용할 플러그인 구성을 담고 있는 ConfigMap 이름.
  • input.parameters: 플러그인에 대한 RPC 호출에 포함되는 입력 파라미터. (선택 사항)

[!NOTE] 플러그인의 개념은 데이터를 Git 밖으로 외부화함으로써 GitOps의 정신을 훼손해서는 안 돼요. 목표는 특정 맥락에서 보완적이 되는 것이에요. 예를 들어 PullRequest generator 중 하나를 사용할 때 CI 관련 파라미터를 검색하는 것은 불가능하여(커밋 해시만 사용 가능) 가능성을 제한해요. 플러그인을 사용하면 별도 데이터 소스에서 필요한 파라미터를 검색하고 그걸 사용해 generator의 기능을 확장할 수 있어요.

플러그인 접근 구성용 ConfigMap 추가 (Add a ConfigMap to configure the access of the plugin)

apiVersion: v1
kind: ConfigMap
metadata:
  name: my-plugin
  namespace: argocd
data:
  token: "$plugin.myplugin.token" # Alternatively $<some_K8S_secret>:plugin.myplugin.token
  baseUrl: "http://myplugin.plugin-ns.svc.cluster.local."
  requestTimeout: "60"
  • token: HTTP 요청을 인증하는 데 사용되는 사전 공유 토큰(argocd-secret Secret에 만든 올바른 키를 가리킴)
  • baseUrl: 클러스터에서 플러그인을 노출하는 k8s 서비스의 BaseUrl.
  • requestTimeout: 플러그인으로의 요청 시간 초과(초 단위, 기본값: 30)

자격 증명 저장 (Store credentials)

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

대안 (Alternative)

민감 데이터를 argocd-secret이 아닌 다른 Kubernetes Secret에 저장하려면, ArgoCD는 configmap의 값이 $로 시작할 때마다 Kubernetes Secretdata 아래 키에서 해당 키를 확인하는 방법을 알아요. 그 후 Kubernetes Secret 이름과 :(콜론) 다음에 키 이름이 옵니다.

문법: $<k8s_secret_name>:<a_key_in_that_k8s_secret>

[!NOTE] Secret은 app.kubernetes.io/part-of: argocd 라벨을 가져야 해요.

예시 (Example)

another-secret:

apiVersion: v1
kind: Secret
metadata:
  name: another-secret
  namespace: argocd
  labels:
    app.kubernetes.io/part-of: argocd
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 서버 (HTTP server)

간단한 파이썬 플러그인 (A Simple Python Plugin)

사이드카 또는 독립 배포(후자가 권장)로 배포할 수 있어요.

예시에서 토큰은 /var/run/argo/token 위치의 파일에 저장돼요.

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

with open("/var/run/argo/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()

curl로 getparams 실행:

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

여기서 주목할 몇 가지:

  • /api/v1/getparams.execute 호출만 구현하면 돼요
  • Authorization 헤더가 /var/run/argo/token과 같은 bearer 값을 포함하는지 확인해야 해요. 아니면 403을 반환하세요
  • 입력 파라미터는 요청 본문에 포함되며 input.parameters 변수로 접근할 수 있어요
  • 출력은 항상 맵의 output.parameters 키 아래 중첩된 객체 맵 목록이어야 해요
  • generator.input.parametersvalues는 예약 키예요. 플러그인 출력에 있으면 이 키들은 ApplicationSet의 Plugin generator spec의 input.parametersvalues 키 내용으로 덮어써져요.

matrix 및 pull request 예시 (With matrix and pull request example)

다음 예시에서 플러그인 구현은 주어진 브랜치에 대한 이미지 digest 집합을 반환해요. 반환된 목록은 브랜치에 대해 가장 최근에 빌드된 이미지에 해당하는 항목 하나만 포함해요.

apiVersion: argoproj.io/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}}"

설명하자면:

  • generator pullRequest는 예를 들어 2개의 브랜치 feature-branch-1feature-branch-2를 반환할 거예요.
  • Plugin generator는 그러면 2개의 요청을 이렇게 수행해요:
curl http://localhost:4355/api/v1/getparams.execute -H "Authorization: Bearer ***" -d \
'{
  "applicationSetName": "fb-matrix",
  "input": {
    "parameters": {
      "branch": "feature-branch-1"
    }
  }
}'

그런 다음,

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

각 호출에 대해 다음과 같은 고유한 결과를 반환해요:

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

그런 다음,

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

이 예시에서 둘을 결합함으로써 하나 이상의 pull request가 사용 가능하고 생성된 태그가 제대로 생성되었음을 보장해요. 이것은 해시만으로는 빌드 성공을 보증하지 못하므로 커밋 해시만으로는 불가능했을 거예요.

더 알아보기 (Learn more)