ArgoCD Callback To Pipeline

1.2k Views Asked by At

What I want is to have ArgoCD approve a job that is in the approval step in CircleCI when a deployment was successful.

However, this has been a multi week journey with no results. I first looked at this:

https://argo-cd.readthedocs.io/en/stable/user-guide/resource_hooks/

I spent the time building out an entire argo workflow, and job. Then setup a pipeline to capture the waiting job id (before updating the yaml file) and run the workflow with that id that would generate a PostSync that could use that id to approve the waiting circleci job only AFTER a successful deploy, then destroy itself. I also had one for SyncFail as well. My hopes were dashed when I realized this was generic. I couldn't see a way to only run this job for a specific application in argocd, it appears to run on ANY sync event. (What??)

Now I'm looking at this: https://argo-cd.readthedocs.io/en/stable/operator-manual/notifications/

However, yet again I see hiccups already before I event started. There seems no possible way to run custom code, therefore I can't run the API to approve my job. Has anyone gotten a callback system for a specific app working? I can try and hack something together by calling back to a pipeline to run an API, but that just seems dumb. There has to be a better way....

1

There are 1 best solutions below

0
On BEST ANSWER

Nearly a month of searching, building, and digging through code later and I have something working. Wasn't exactly what I wanted above but I think this is probably better in the long run anyway.

Instead of a callback mechanism I went with a async callback, although I will give you some ideas of ways you can implement the callback approval mechanism below.

Background

I'm using the following services:

    argocd
    argocd-notifications
    argocd-workflows

By default argocd-notifications comes bundled with argocd while argocd-workflows does not and is required to have a separate install. (all done via the community helm charts).

ArgoCD-Notifications Setup

argocd-notifications-cm (configmap MUST be named this, argocd will auto pickup this configmap name) argocd-notifications-secret (secret MUST be named this for the same reason)

argocd-notifications-cm

Please note, if you just use this as is you will overwrite the one that is generated by default. You can append this to the existing map if you want, just be sure to capture that information BEFORE applying your own.

apiVersion: v1
kind: ConfigMap
metadata:
  name: argocd-notifications-cm
  namespace: argocd
data:
  template.run-github-pipeline: |
    webhook:
      trigger-workflow:
        method: POST
        path: /submit
        body: |
          {
            "resourceKind": "WorkflowTemplate",
            "namespace": "argo-workflows",
            "resourceName": "scripts-trigger-pipelines",
            "submitOptions": {
                "entryPoint": "github",
                "generateName": "{{ `{{.app.metadata.name}}` }}-github-pipeline-",
                "parameters": [
                    "app_name={{ `{{.app.metadata.name}}` }}",
                    "trigger_with_data={{ `{{.recipient}}` }}"
                ],
                "labels": "workflows.argoproj.io/workflow-template=scripts-trigger-pipelines",
                "generateName": "{{ `{{.app.metadata.name}}` }}-trigger-github-pipeline-"
          }}
  trigger.on-deployment-success-circleci: |
    - when: app.status.sync.status in ['Synced'] and app.status.operationState != nil and app.status.operationState.phase in ['Succeeded'] and app.status.health.status in ['Healthy']
      oncePer: app.status.operationState.syncResult.revision
      send: [run-circleci-pipeline]
  service.webhook.trigger-workflow: |
    url: "https://{{ .Values.workflows.endpoint }}/api/v1/workflows/argo-workflows"
    headers:
    - name: "Authorization"
      value: "Bearer $ARGO_WORKFLOWS_API_TOKEN"
    - name: "Content-Type"
      value: "application/json"
    insecureSkipVerify: false

argocd-notifications-secret

This secret must also exist so that it will know what the $ARGO_WORKFLOWS_API_TOKEN is:

apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: argocd-notifications-secret
  namespace: argocd
spec:
  secretStoreRef:
    kind: ClusterSecretStore
    name: aws-secrets-manager
  refreshInterval: 1m
  target:
    creationPolicy: Owner
    deletionPolicy: Delete
  # map secret key/value pairs to envvars
  data:
    - secretKey: ARGO_WORKFLOWS_API_TOKEN
      remoteRef:
        key: my-secret-name
        property: argo_workflows_api_token

In the above I'm using an external secret so I can maintain the secret elsewhere and have it synced to kuberentes but you can also just use a plain Secret if you want. Subscription

Now this one got me for a while, but you can ONLY add these annotations to the kubernetes resource kind: Application. This is something that only argocd understands. You can add it like:

notifications.argoproj.io/subscribe.on-deployment-success-github.trigger-workflow: 'repo:my_test_repo|eventtype:my-repository-dispatch-event|payload:{app_name:sample}'

This takes advantage of the {{.recipient}} as defined in the configmap above. It will add the following into the {{.recipient}} entry on that configmap BEFORE firing the event:

repo:my_test_repo|eventtype:my-repository-dispatch-event|payload:{app_name:sample}

Now the argo-notifications side is completed. We need to setup the argo-workflows side....

Argo-Workflows Setup

You will need to setup some way to communicate with argo-workflows. That means you will need to setup a webhook (sorry won't go into detail how to do that here, just know you need to have it). Get API Token

Next we need to setup a service account, rolebinding, and role that will we use to access the api of argo-workflows. In order to get the api token that we use in the configmap above for notifications we need to generate a secret:

apiVersion: v1
kind: Secret
metadata:
  name: argo-workflows-pipelines.service-account-token
  annotations:
    kubernetes.io/service-account.name: argo-workflows-pipelines
  type: kubernetes.io/service-account-token

Doing this will then allow us to run the following command to get the api token and place it either into the external secret or the argocd-notifications-secret directly:

kg secret argo-workflows-pipelines.service-account-token -o=jsonpath='{.data.token}' -n argo-workflows | base64 --decode

Setup Job Manifest

Now that we have the token we can setup a job manifest that will be triggered by the noficiation which will be in charge of calling back to any pipeline we want (circleci, github, bitbucket, etc.) -- in this example I'm using github:

apiVersion: argoproj.io/v1alpha1
kind: WorkflowTemplate
metadata:
  name: scripts-trigger-pipelines
spec:
  ttlStrategy:
    secondsAfterCompletion: 320    # Auto delete workflow in 5 minutes after it completes
  entrypoint: github
  arguments:
    parameters:
      - name: trigger_with_data
        value: "{}"
      - name: app_name
        value: ""
  templates:
    - name: github
      serviceAccountName: <my-sa-name>
      inputs:
        parameters:
          - name: trigger_with_data
          - name: app_name
      metadata:
        annotations:
          kubectl.kubernetes.io/default-container: main
      resource:
        action: create
        setOwnerReference: true
        manifest: |
          apiVersion: batch/v1
          kind: Job
          metadata:
            generateName: "{{ `{{inputs.parameters.app_name}}` }}-trigger-github-pipeline-"
          spec:
            template:
              spec:
                serviceAccountName: argo-workflows-pipelines
                volumes:
                - name: secret-volume
                  secret:
                    secretName: <my-secret-volume>
                containers:
                - name: trigger-github-pipeline
                  image: {{ .Values.awsAccountID }}.dkr.ecr.us-east-1.amazonaws.com/{{ .Values.pipelineCallbacks.docker.image }}:{{ .Values.pipelineCallbacks.docker.tag }}
                  command: ["/bin/bash", "-c"]
                  args:
                    - |-
                      echo "Input App: {{ `{{inputs.parameters.app_name}}` }}"
                      INPUT_DATA='{{ `{{inputs.parameters.trigger_with_data}}` }}'
                      ...


                      # Trigger target github repository dispatch event with the generated token and parsed data
                      echo "Issuing curl to url:https://api.github.com/repos/<owner>/$REPO/dispatches -- with data: {\"event_type\":\"$EVENT_TYPE\",\"client_payload\":{\"dispatch_payload\":$PAYLOAD}}"
                      curl \
                        -X POST \
                        -H "Accept: application/vnd.github+json" \
                        -H "Authorization: Bearer $GITHUB_TOKEN"\
                        -H "X-GitHub-Api-Version: 2022-11-28" \
                        https://api.github.com/repos/<owner>/$REPO/dispatches \
                        -d "{\
                          \"event_type\":\"$EVENT_TYPE\",\
                          \"client_payload\":$PAYLOAD
                        }"

                  volumeMounts:
                  - name: secret-volume
                    readOnly: true
                    mountPath: "/secrets"
                restartPolicy: Never
            backoffLimit: 2

Note: I don't spell everything out for you here, as you will need to get your GITHUB_TOKEN into the workflow. I would just use my provided example for "secrets" via a volume mount. I would use a github app to dynamically generate the token as well, more secure.

NOTE2:

ttlStrategy:
    secondsAfterCompletion: 320

While first building I would probably remove this until you're ready for it. Github Setup

Now that we have argo-notifications and argo-workflows setup we can setup a repository dispatch event inside our github pipeline (or something equivalent for other pipelines)

name: "Argo Callback"
on:
  repository_dispatch:
    types: [my-repository-dispatch-event]
    inputs:
      app_name:
        description: 'The name of the app you want to update in your own triggering repo.'
        required: true
        type: string

jobs:
  argo-callback:
    name: "Init Callback"
    runs-on: ubuntu-latest
    steps:
      - name: Output payload
        id: verify
        run: |
          echo "RECEIVED CALLBACK FOR APP: ${{ github.event.client_payload.app_name }}"
          echo "${{ github.event.client_payload }}"

As long as I didn't typo something and you filled everything out it should be working.

However, what if it isn't? Lets talk about debugging. Debugging

Argo-Notifications

kubectl get pods -n argocd | grep argocd-notifications

Should spit out the notifications controller. Then you can look at it's logs:

kubectl logs <pod_name> -n argocd --tail=100 -f

I added -f if you want to follow along the logs in realtime.

Argo-Workflows

kubectl get pods -n argo-workflows

there is a server and a controller just like argo-notifications. Look at those logs using the same method. That should give you all the information you need to succeed! Good luck!

Argo-Notifications documentation was hell. Also here is some helpful info on un-documented things like app.status etc.

https://github.com/argoproj/gitops-engine/blob/187312fe86ed627b1cf4147c13dccf9ff331424b/pkg/sync/sync_task.go

https://github.com/argoproj/gitops-engine/blob/master/pkg/sync/common/types.go#L54

Callback Approval

Now with all the above you can achieve a callback approval if you use a database, something like dynamo db to store the hash that is triggering the deployment. Then use the undocument app.status.operationState.syncResult.revision (Which is basically the short hash for the commit) parameter inside your configmap to send it to argo-workflows. Then argo-workflows will use that hash to lookup that information in the database to know who it should respond to.

I know this is going to be a super valuable resource for anyone working with workflows or notifications.