├── .dockerignore
├── .github
└── workflows
│ ├── ci-build.yaml
│ ├── cron-master-on-stable.yaml
│ ├── cron-master.yaml
│ ├── cron-stable-on-stable.yaml
│ ├── image.yaml
│ └── static.yml
├── .gitignore
├── .golangci.yml
├── .readthedocs.yml
├── CHANGELOG.md
├── Dockerfile
├── LICENSE
├── Makefile
├── PROJECT
├── README.md
├── VERSION
├── api
└── v1alpha1
│ ├── applicationset_types.go
│ ├── applicationset_types_test.go
│ ├── groupversion_info.go
│ └── zz_generated.deepcopy.go
├── common
├── annotations.go
└── version.go
├── docs
├── Application-Deletion.md
├── Argo-CD-Integration.md
├── Controlling-Resource-Modification.md
├── Development.md
├── E2E-Tests.md
├── Generators-Cluster-Decision-Resource.md
├── Generators-Cluster.md
├── Generators-Git.md
├── Generators-List.md
├── Generators-Matrix.md
├── Generators-Merge.md
├── Generators-Pull-Request.md
├── Generators-SCM-Provider.md
├── Generators.md
├── Geting-Started.md
├── Getting-Started.md
├── Release-Checklist-Template.md
├── Releasing.md
├── Template.md
├── Use-Cases.md
├── assets
│ ├── Argo-CD-Integration
│ │ ├── ApplicationSet-Argo-Diagram-v2.odp
│ │ ├── ApplicationSet-Argo-Relationship-v2.png
│ │ └── ApplicationSet-Argo-Relationship.png
│ ├── Introduction
│ │ └── List-Example-In-Argo-CD-Web-UI.png
│ ├── Use-Cases
│ │ ├── Cluster-Add-Ons.png
│ │ └── Monorepos.png
│ ├── broken-link-ignore-list.txt
│ ├── logo.png
│ ├── webhook-config-pull-request.png
│ └── webhook-config.png
├── index.md
├── js
│ ├── jquery-2.1.1.min.js
│ ├── modernizr-2.8.3.min.js
│ └── theme.js
├── requirements.txt
└── upgrading
│ └── v0.2.0-to-v0.3.0.md
├── entrypoint.sh
├── examples
├── cluster
│ └── cluster-example.yaml
├── clusterDecisionResource
│ ├── README.md
│ ├── configMap.yaml
│ ├── ducktype-example.yaml
│ └── placementdecision.yaml
├── design-doc
│ ├── applicationset.yaml
│ ├── clusters.yaml
│ ├── git-directory-discovery.yaml
│ ├── git-files-discovery.yaml
│ ├── git-files-literal.yaml
│ ├── list.yaml
│ ├── proposal
│ │ ├── README.md
│ │ └── filters.yaml
│ └── template-override.yaml
├── git-generator-directory
│ ├── cluster-addons
│ │ ├── argo-workflows
│ │ │ ├── kustomization.yaml
│ │ │ └── namespace-install.yaml
│ │ └── prometheus-operator
│ │ │ ├── Chart.yaml
│ │ │ ├── requirements.yaml
│ │ │ └── values.yaml
│ ├── excludes
│ │ ├── cluster-addons
│ │ │ ├── argo-workflows
│ │ │ │ ├── kustomization.yaml
│ │ │ │ └── namespace-install.yaml
│ │ │ ├── exclude-helm-guestbook
│ │ │ │ ├── Chart.yaml
│ │ │ │ ├── templates
│ │ │ │ │ ├── NOTES.txt
│ │ │ │ │ ├── _helpers.tpl
│ │ │ │ │ ├── deployment.yaml
│ │ │ │ │ └── service.yaml
│ │ │ │ ├── values-production.yaml
│ │ │ │ └── values.yaml
│ │ │ └── prometheus-operator
│ │ │ │ ├── Chart.yaml
│ │ │ │ ├── requirements.yaml
│ │ │ │ └── values.yaml
│ │ └── git-directories-exclude-example.yaml
│ └── git-directories-example.yaml
├── git-generator-files-discovery
│ ├── apps
│ │ └── guestbook
│ │ │ ├── guestbook-ui-deployment.yaml
│ │ │ ├── guestbook-ui-svc.yaml
│ │ │ └── kustomization.yaml
│ ├── cluster-config
│ │ └── engineering
│ │ │ ├── dev
│ │ │ └── config.json
│ │ │ └── prod
│ │ │ └── config.json
│ └── git-generator-files.yaml
├── list-generator
│ ├── guestbook
│ │ ├── engineering-dev
│ │ │ ├── guestbook-ui-deployment.yaml
│ │ │ └── guestbook-ui-svc.yaml
│ │ └── engineering-prod
│ │ │ ├── guestbook-ui-deployment.yaml
│ │ │ └── guestbook-ui-svc.yaml
│ └── list-example.yaml
├── matrix
│ ├── cluster-addons
│ │ ├── argo-workflows
│ │ │ ├── kustomization.yaml
│ │ │ └── namespace-install.yaml
│ │ └── prometheus-operator
│ │ │ ├── Chart.yaml
│ │ │ ├── requirements.yaml
│ │ │ └── values.yaml
│ ├── cluster-and-git.yaml
│ ├── list-and-git.yaml
│ ├── list-and-list.yaml
│ └── matrix-and-union-in-matrix.yaml
├── merge
│ ├── merge-clusters-and-list.yaml
│ └── merge-two-matrixes.yaml
├── pull-request-generator
│ └── pull-request-example.yaml
├── scm-provider-generator
│ └── scm-provider-example.yaml
└── template-override
│ ├── default
│ ├── guestbook-ui-deployment.yaml
│ ├── guestbook-ui-svc.yaml
│ └── kustomization.yaml
│ ├── engineering-dev-override
│ ├── guestbook-ui-deployment.yaml
│ ├── guestbook-ui-svc.yaml
│ └── kustomization.yaml
│ └── template-overrides-example.yaml
├── go.mod
├── go.sum
├── hack
├── boilerplate.go.txt
├── ci-e2e-run.sh
├── ci-e2e-setup.sh
├── from-argo-cd
│ ├── git-ask-pass.sh
│ ├── git-verify-wrapper.sh
│ └── gpg-wrapper.sh
├── generate-manifests.sh
├── release.sh
├── set-docs-redirects.sh
└── verify-argo-cd-versions.sh
├── main.go
├── manifests
├── base
│ ├── deployment.yaml
│ ├── kustomization.yaml
│ ├── rbac.yaml
│ └── service.yaml
├── crds
│ ├── argoproj.io_applicationsets.yaml
│ └── kustomization.yaml
├── install.yaml
└── namespace-install
│ └── kustomization.yaml
├── mkdocs.yml
├── pkg
├── controllers
│ ├── applicationset_controller.go
│ ├── applicationset_controller_test.go
│ ├── clustereventhandler.go
│ └── clustereventhandler_test.go
├── generators
│ ├── cluster.go
│ ├── cluster_test.go
│ ├── duck_type.go
│ ├── duck_type_test.go
│ ├── generator_spec_processor.go
│ ├── generators_test.go
│ ├── git.go
│ ├── git_test.go
│ ├── interface.go
│ ├── list.go
│ ├── list_test.go
│ ├── matrix.go
│ ├── matrix_test.go
│ ├── merge.go
│ ├── merge_test.go
│ ├── pull_request.go
│ ├── pull_request_test.go
│ ├── scm_provider.go
│ └── scm_provider_test.go
├── services
│ ├── pull_request
│ │ ├── fake.go
│ │ ├── github.go
│ │ ├── github_test.go
│ │ └── interface.go
│ ├── repo_service.go
│ ├── repo_service_test.go
│ └── scm_provider
│ │ ├── bitbucket_cloud.go
│ │ ├── bitbucket_cloud_test.go
│ │ ├── github.go
│ │ ├── github_test.go
│ │ ├── gitlab.go
│ │ ├── gitlab_test.go
│ │ ├── mock.go
│ │ ├── types.go
│ │ ├── utils.go
│ │ └── utils_test.go
└── utils
│ ├── clusterUtils.go
│ ├── clusterUtils_test.go
│ ├── constants.go
│ ├── createOrUpdate.go
│ ├── map.go
│ ├── map_test.go
│ ├── policy.go
│ ├── testdata
│ ├── github-commit-event.json
│ ├── github-pull-request-assigned-event.json
│ ├── github-pull-request-opened-event.json
│ ├── gitlab-event.json
│ └── invalid-event.json
│ ├── util.go
│ ├── util_test.go
│ ├── webhook.go
│ └── webhook_test.go
└── test
└── e2e
├── applicationset
├── applicationset_test.go
├── cluster_e2e_test.go
├── crl_e2e_test.go
├── matrix_e2e_test.go
└── merge_e2e_test.go
└── fixture
└── applicationsets
├── actions.go
├── consequences.go
├── context.go
├── expectation.go
└── utils
├── cmd.go
├── errors.go
└── fixture.go
/.dockerignore:
--------------------------------------------------------------------------------
1 |
2 | # Binaries for programs and plugins
3 | *.exe
4 | *.exe~
5 | *.dll
6 | *.so
7 | *.dylib
8 | bin
9 | vendor
10 |
11 | # Test binary, build with `go test -c`
12 | *.test
13 |
14 | # Output of the go coverage tool, specifically when used with LiteIDE
15 | *.out
16 |
17 | # Kubernetes Generated files - skip generated files, except for vendored files
18 |
19 | !vendor/**/zz_generated.*
20 |
21 | # editor and IDE paraphernalia
22 | .idea
23 | *.swp
24 | *.swo
25 | *~
26 | dist/
--------------------------------------------------------------------------------
/.github/workflows/cron-master-on-stable.yaml:
--------------------------------------------------------------------------------
1 | name: Nightly test of ApplicationSet 'master' on latest stable ArgoCD releases
2 | # This is useful for detecting regressions of ApplicationSet master branch against released Argo CD releases.
3 |
4 | on:
5 | schedule:
6 | - cron: '5 2 * * *'
7 |
8 | jobs:
9 |
10 | test-e2e:
11 | name: "Run E2E tests - K8s/Argo CD:"
12 | runs-on: ubuntu-latest
13 | strategy:
14 | matrix:
15 | k3s-version: [v1.20.2]
16 | argo-release: [Latest, v2.1, v2.2, v2.3]
17 | env:
18 | GOPATH: /home/runner/go
19 | ARGOCD_FAKE_IN_CLUSTER: 'true'
20 | ARGOCD_SSH_DATA_PATH: '/tmp/argo-e2e/app/config/ssh'
21 | ARGOCD_TLS_DATA_PATH: '/tmp/argo-e2e/app/config/tls'
22 | ARGOCD_E2E_SSH_KNOWN_HOSTS: '../fixture/certs/ssh_known_hosts'
23 | ARGOCD_E2E_K3S: 'true'
24 | ARGOCD_IN_CI: 'true'
25 | ARGOCD_E2E_APISERVER_PORT: '8088'
26 | ARGOCD_SERVER: '127.0.0.1:8088'
27 | INSTALL_K3S_VERSION: ${{ matrix.k3s-version }}+k3s1
28 | RELEASE_LIST_SEARCH_STRING: ${{ matrix.argo-release }}
29 | GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
30 | GITLAB_TOKEN: ${{ secrets.GITLAB_TOKEN }}
31 |
32 | #
33 | steps:
34 |
35 | - name: Call GitHub CLI to find latest stable Argo CD release
36 | id: get-argocd-stable
37 | run: |
38 | LATEST_RELEASE=`gh release list --repo argoproj/argo-cd | grep "$RELEASE_LIST_SEARCH_STRING" | head -n 1 | cut -f 1`
39 | echo Latest release for $RELEASE_LIST_SEARCH_STRING is $LATEST_RELEASE
40 | echo "::set-output name=latestRelease::$LATEST_RELEASE"
41 |
42 | - name: Call GitHub CLI to find latest stable ApplicationSet release
43 | id: get-appset-stable
44 | run: |
45 | LATEST_RELEASE=`gh release list --repo argoproj-labs/applicationset | grep "Latest" | cut -f 1`
46 | echo Latest release for ApplicationSet is $LATEST_RELEASE
47 | echo "::set-output name=latestRelease::$LATEST_RELEASE"
48 |
49 |
50 | - name: Checkout latest Argo CD code
51 | uses: actions/checkout@v2
52 | with:
53 | repository: argoproj/argo-cd
54 | ref: ${{steps.get-argocd-stable.outputs.latestRelease}}
55 | path: argo-cd
56 |
57 | - name: Setup Golang
58 | uses: actions/setup-go@v2
59 | with:
60 | go-version: '1.17.6'
61 |
62 | - name: Restore go build cache
63 | uses: actions/cache@v1
64 | with:
65 | path: ~/.cache/go-build
66 | key: ${{ runner.os }}-go-build-v1-${{ github.run_id }}
67 |
68 | - name: Checkout latest applicationset code from latest stable release
69 | uses: actions/checkout@v2
70 | with:
71 | path: applicationset
72 | ref: ${{steps.get-appset-stable.outputs.latestRelease}}
73 |
74 | - name: Checkout latest applicationset code from master
75 | uses: actions/checkout@v2
76 | with:
77 | path: applicationset
78 |
79 | - name: Run E2E test setup
80 | timeout-minutes: 20
81 | run: |
82 | cd "$GITHUB_WORKSPACE/applicationset"
83 | "hack/ci-e2e-setup.sh"
84 |
85 | - name: Run E2E tests
86 | timeout-minutes: 20
87 | run: |
88 | echo Running tests against Argo CD ${{steps.get-argocd-stable.outputs.latestRelease}} and ApplicationSet ${{steps.get-appset-stable.outputs.latestRelease}}
89 | cd "$GITHUB_WORKSPACE/applicationset"
90 | "hack/ci-e2e-run.sh"
91 |
92 | - name: Upload e2e-server logs
93 | uses: actions/upload-artifact@v2
94 | with:
95 | name: appset-e2e-server-k8s${{ matrix.k3s-version }}.log
96 | path: /tmp/appset-e2e-server.log
97 | if: ${{ failure() }}
98 | - name: Upload other Argo CD server log
99 | uses: actions/upload-artifact@v2
100 | with:
101 | name: argocd-e2e-server-k8s${{ matrix.k3s-version }}.log
102 | path: /tmp/e2e-server.log
103 | if: ${{ failure() }}
104 |
--------------------------------------------------------------------------------
/.github/workflows/cron-master.yaml:
--------------------------------------------------------------------------------
1 | name: Nightly test of ApplicationSet 'master' branch against latest Argo CD 'master' branch
2 | # This is useful for catching recent regressions between master branches of Argo CD and ApplicationSet.
3 |
4 | on:
5 | schedule:
6 | - cron: '5 2 * * *'
7 |
8 | push:
9 | branches:
10 | - 'cron-test-argocd'
11 |
12 |
13 | jobs:
14 |
15 | test-e2e:
16 | name: Run end-to-end tests
17 | runs-on: ubuntu-latest
18 | strategy:
19 | matrix:
20 | k3s-version: [v1.20.2]
21 | env:
22 | GOPATH: /home/runner/go
23 | ARGOCD_FAKE_IN_CLUSTER: 'true'
24 | ARGOCD_SSH_DATA_PATH: '/tmp/argo-e2e/app/config/ssh'
25 | ARGOCD_TLS_DATA_PATH: '/tmp/argo-e2e/app/config/tls'
26 | ARGOCD_E2E_SSH_KNOWN_HOSTS: '../fixture/certs/ssh_known_hosts'
27 | ARGOCD_E2E_K3S: 'true'
28 | ARGOCD_IN_CI: 'true'
29 | ARGOCD_E2E_APISERVER_PORT: '8088'
30 | ARGOCD_SERVER: '127.0.0.1:8088'
31 | INSTALL_K3S_VERSION: ${{ matrix.k3s-version }}+k3s1
32 | GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
33 | GITLAB_TOKEN: ${{ secrets.GITLAB_TOKEN }}
34 |
35 | steps:
36 |
37 | - name: Checkout latest Argo CD code
38 | uses: actions/checkout@v2
39 | with:
40 | repository: argoproj/argo-cd
41 | ref: master
42 | path: argo-cd
43 |
44 | - name: Setup Golang
45 | uses: actions/setup-go@v2
46 | with:
47 | go-version: '1.17.6'
48 |
49 | - name: Restore go build cache
50 | uses: actions/cache@v1
51 | with:
52 | path: ~/.cache/go-build
53 | key: ${{ runner.os }}-go-build-v1-${{ github.run_id }}
54 |
55 | - name: Checkout latest applicationset code
56 | uses: actions/checkout@v2
57 | with:
58 | path: applicationset
59 |
60 | - name: Run E2E test setup
61 | timeout-minutes: 20
62 | run: |
63 | cd "$GITHUB_WORKSPACE/applicationset"
64 | "hack/ci-e2e-setup.sh"
65 |
66 | - name: Run E2E tests
67 | timeout-minutes: 20
68 | run: |
69 | cd "$GITHUB_WORKSPACE/applicationset"
70 | "hack/ci-e2e-run.sh"
71 |
72 | - name: Upload e2e-server logs
73 | uses: actions/upload-artifact@v2
74 | with:
75 | name: appset-e2e-server-k8s${{ matrix.k3s-version }}.log
76 | path: /tmp/appset-e2e-server.log
77 | if: ${{ failure() }}
78 | - name: Upload other Argo CD server log
79 | uses: actions/upload-artifact@v2
80 | with:
81 | name: argocd-e2e-server-k8s${{ matrix.k3s-version }}.log
82 | path: /tmp/e2e-server.log
83 | if: ${{ failure() }}
84 |
--------------------------------------------------------------------------------
/.github/workflows/cron-stable-on-stable.yaml:
--------------------------------------------------------------------------------
1 | name: Nightly test of latest stable releases of ApplicationSet against latest stable releases of ArgoCD
2 | # This is useful for catching regressions of released ApplicationSet versions, against release Argo CD versions.
3 |
4 | on:
5 | schedule:
6 | - cron: '5 2 * * *'
7 |
8 | jobs:
9 |
10 | test-e2e:
11 | name: "Run E2E tests - K8s/Argo CD:"
12 | runs-on: ubuntu-latest
13 | strategy:
14 | matrix:
15 | k3s-version: [v1.20.2]
16 | argo-release: [Latest, v2.1, v2.2, v2.3]
17 | env:
18 | GOPATH: /home/runner/go
19 | ARGOCD_FAKE_IN_CLUSTER: 'true'
20 | ARGOCD_SSH_DATA_PATH: '/tmp/argo-e2e/app/config/ssh'
21 | ARGOCD_TLS_DATA_PATH: '/tmp/argo-e2e/app/config/tls'
22 | ARGOCD_E2E_SSH_KNOWN_HOSTS: '../fixture/certs/ssh_known_hosts'
23 | ARGOCD_E2E_K3S: 'true'
24 | ARGOCD_IN_CI: 'true'
25 | ARGOCD_E2E_APISERVER_PORT: '8088'
26 | ARGOCD_SERVER: '127.0.0.1:8088'
27 | INSTALL_K3S_VERSION: ${{ matrix.k3s-version }}+k3s1
28 | RELEASE_LIST_SEARCH_STRING: ${{ matrix.argo-release }}
29 | GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
30 | GITLAB_TOKEN: ${{ secrets.GITLAB_TOKEN }}
31 |
32 | steps:
33 |
34 | - name: Call GitHub CLI to find latest stable Argo CD release
35 | id: get-argocd-stable
36 | run: |
37 | LATEST_RELEASE=`gh release list --repo argoproj/argo-cd | grep "$RELEASE_LIST_SEARCH_STRING" | head -n 1 | cut -f 1`
38 | echo Latest release for $RELEASE_LIST_SEARCH_STRING is $LATEST_RELEASE
39 | echo "::set-output name=latestRelease::$LATEST_RELEASE"
40 |
41 | - name: Call GitHub CLI to find latest stable ApplicationSet release
42 | id: get-appset-stable
43 | run: |
44 | LATEST_RELEASE=`gh release list --repo argoproj-labs/applicationset | grep "Latest" | cut -f 1`
45 | echo Latest release for ApplicationSet is $LATEST_RELEASE
46 | echo "::set-output name=latestRelease::$LATEST_RELEASE"
47 |
48 |
49 | - name: Checkout latest Argo CD code
50 | uses: actions/checkout@v2
51 | with:
52 | repository: argoproj/argo-cd
53 | ref: ${{steps.get-argocd-stable.outputs.latestRelease}}
54 | path: argo-cd
55 |
56 | - name: Setup Golang
57 | uses: actions/setup-go@v2
58 | with:
59 | go-version: '1.17.6'
60 |
61 | - name: Restore go build cache
62 | uses: actions/cache@v1
63 | with:
64 | path: ~/.cache/go-build
65 | key: ${{ runner.os }}-go-build-v1-${{ github.run_id }}
66 |
67 | - name: Checkout latest applicationset code from latest stable release
68 | uses: actions/checkout@v2
69 | with:
70 | path: applicationset
71 | ref: ${{steps.get-appset-stable.outputs.latestRelease}}
72 |
73 | - name: Checkout latest applicationset code from master
74 | uses: actions/checkout@v2
75 | with:
76 | path: applicationset
77 |
78 |
79 | - name: Checkout latest applicationset code from latest stable release
80 | uses: actions/checkout@v2
81 | with:
82 | path: applicationset
83 | ref: ${{steps.get-appset-stable.outputs.latestRelease}}
84 |
85 | - name: Checkout latest applicationset code from master
86 | uses: actions/checkout@v2
87 | with:
88 | path: applicationset-master
89 |
90 | - name: Add test runner scripts from AppSet master to release branch
91 | run: |
92 | mkdir -p "$GITHUB_WORKSPACE/applicationset/hack"
93 | cp "$GITHUB_WORKSPACE"/applicationset-master/hack/ci-e2e-*.sh "$GITHUB_WORKSPACE/applicationset/hack"
94 | ls -l "$GITHUB_WORKSPACE/applicationset/hack"
95 |
96 |
97 | - name: Run E2E test setup
98 | timeout-minutes: 20
99 | run: |
100 | cd "$GITHUB_WORKSPACE/applicationset"
101 | "hack/ci-e2e-setup.sh"
102 |
103 | - name: Run E2E tests
104 | timeout-minutes: 20
105 | run: |
106 | echo Running tests against Argo CD ${{steps.get-argocd-stable.outputs.latestRelease}} and ApplicationSet ${{steps.get-appset-stable.outputs.latestRelease}}
107 | cd "$GITHUB_WORKSPACE/applicationset"
108 | "hack/ci-e2e-run.sh"
109 |
110 | - name: Upload e2e-server logs
111 | uses: actions/upload-artifact@v2
112 | with:
113 | name: appset-e2e-server-k8s${{ matrix.k3s-version }}.log
114 | path: /tmp/appset-e2e-server.log
115 | if: ${{ failure() }}
116 | - name: Upload other Argo CD server log
117 | uses: actions/upload-artifact@v2
118 | with:
119 | name: argocd-e2e-server-k8s${{ matrix.k3s-version }}.log
120 | path: /tmp/e2e-server.log
121 | if: ${{ failure() }}
122 |
--------------------------------------------------------------------------------
/.github/workflows/image.yaml:
--------------------------------------------------------------------------------
1 | name: Build image on commit to master
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | pull_request:
8 | branches:
9 | - master
10 | types: [ labeled, unlabeled, opened, synchronize, reopened ]
11 |
12 | jobs:
13 | publish:
14 | runs-on: ubuntu-latest
15 | env:
16 | GOPATH: /home/runner/work/applicationset
17 | steps:
18 | - uses: actions/setup-go@v1
19 | with:
20 | go-version: '1.17.6'
21 | - uses: actions/checkout@master
22 | with:
23 | path: src/applicationset
24 |
25 | # Build the image
26 | - uses: docker/setup-qemu-action@v1
27 | - uses: docker/setup-buildx-action@v1
28 | - run: |
29 | IMAGE_PLATFORMS=linux/amd64
30 | if [[ "${{ github.event_name }}" == "push" || "${{ contains(github.event.pull_request.labels.*.name, 'test-arm-image') }}" == "true" ]]
31 | then
32 | IMAGE_PLATFORMS=linux/amd64,linux/arm64
33 | fi
34 | echo "Building image for platforms: $IMAGE_PLATFORMS"
35 | make image DOCKER_PUSH=false CONTAINER_REGISTRY=quay.io IMAGE_NAMESPACE=argoproj IMAGE_TAG=latest IMAGE_PLATFORMS=${IMAGE_PLATFORMS}
36 | working-directory: ./src/applicationset
37 | env:
38 | GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
39 | GITLAB_TOKEN: ${{ secrets.GITLAB_TOKEN }}
40 |
41 |
42 | # Publish the image
43 | - run: |
44 | docker login quay.io --username $USERNAME --password $PASSWORD
45 | docker push quay.io/argoproj/argocd-applicationset:latest
46 | if: github.event_name == 'push'
47 | env:
48 | USERNAME: ${{ secrets.USERNAME }}
49 | PASSWORD: ${{ secrets.TOKEN }}
50 |
--------------------------------------------------------------------------------
/.github/workflows/static.yml:
--------------------------------------------------------------------------------
1 | name: static check
2 | on:
3 | push:
4 | branches:
5 | - 'master'
6 | pull_request:
7 | branches:
8 | - 'master'
9 |
10 | jobs:
11 | gofmt:
12 | name: Ensure that code is gofmt-ed
13 | runs-on: ubuntu-latest
14 | steps:
15 | - uses: actions/checkout@master
16 | - name: check
17 | uses: grandcolline/golang-github-actions@v1.1.0
18 | with:
19 | run: fmt
20 | comment: false
21 |
22 | lint-docs:
23 | name: Ensure docs are linted
24 | runs-on: ubuntu-latest
25 | steps:
26 | - name: Checkout code
27 | uses: actions/checkout@v2
28 | - name: Setup Python
29 | uses: actions/setup-python@v2
30 | with:
31 | python-version: '3.x'
32 | - name: Install dependencies
33 | run: |
34 | pip install -r docs/requirements.txt
35 | - name: Lint docs
36 | run: |
37 | make lint-docs
38 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | # Binaries for programs and plugins
3 | *.exe
4 | *.exe~
5 | *.dll
6 | *.so
7 | *.dylib
8 | bin
9 | vendor
10 |
11 | # Test binary, build with `go test -c`
12 | *.test
13 |
14 | # Output of the go coverage tool, specifically when used with LiteIDE
15 | *.out
16 |
17 | # Kubernetes Generated files - skip generated files, except for vendored files
18 |
19 | !vendor/**/zz_generated.*
20 |
21 | # editor and IDE paraphernalia
22 | .idea
23 | *.swp
24 | *.swo
25 | *~
26 | dist/
27 |
28 | # mkdocs site
29 | site/
30 |
--------------------------------------------------------------------------------
/.golangci.yml:
--------------------------------------------------------------------------------
1 | run:
2 | timeout: 5m
3 | skip-files:
4 | skip-dirs:
5 | linters:
6 | enable:
7 | - vet
8 | - deadcode
9 | - goimports
10 | - varcheck
11 | - structcheck
12 | - ineffassign
13 | - unconvert
14 | - unparam
15 | output:
16 | format: github-actions
17 | issues:
18 | exclude:
19 | - SA5011
20 | - S1005
21 |
--------------------------------------------------------------------------------
/.readthedocs.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | formats: all
3 | mkdocs:
4 | fail_on_warning: false
5 | python:
6 | install:
7 | - requirements: docs/requirements.txt
8 | build:
9 | os: ubuntu-22.04
10 | tools:
11 | python: "3.12"
12 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # https://github.com/argoproj/argo-cd/pull/8516 now requires us to copy Argo CD binary into the ApplicationSet controller
2 |
3 |
4 | # Build the binary
5 | FROM docker.io/library/golang:1.17.6 as builder
6 |
7 | WORKDIR /workspace
8 |
9 | # Copy the Go Modules manifests
10 | COPY go.mod .
11 | COPY go.sum .
12 | # cache deps before building and copying source so that we don't need to re-download as much
13 | # and so that source changes don't invalidate our downloaded layer
14 | RUN go mod download
15 |
16 | # Copy the go source
17 | COPY . .
18 |
19 | RUN rm -f ./bin/*
20 |
21 | # Build
22 | RUN make build
23 |
24 | FROM docker.io/library/ubuntu:21.10
25 |
26 | ENV DEBIAN_FRONTEND=noninteractive
27 | RUN apt-get update && apt-get dist-upgrade -y && \
28 | apt-get install -y git git-lfs gpg tini && \
29 | apt-get clean && \
30 | rm -rf /var/lib/apt/lists /var/cache/apt/archives /tmp/* /var/tmp/*
31 |
32 |
33 | # Add Argo CD helper scripts that are required by 'github.com/argoproj/argo-cd/util/git' package
34 | COPY hack/from-argo-cd/gpg-wrapper.sh /usr/local/bin/gpg-wrapper.sh
35 | COPY hack/from-argo-cd/git-verify-wrapper.sh /usr/local/bin/git-verify-wrapper.sh
36 | COPY hack/from-argo-cd/git-ask-pass.sh /usr/local/bin/git-ask-pass.sh
37 |
38 | COPY entrypoint.sh /usr/local/bin/entrypoint.sh
39 |
40 | # Support for mounting configuration from a configmap
41 | RUN mkdir -p /app/config/ssh && \
42 | touch /app/config/ssh/ssh_known_hosts && \
43 | ln -s /app/config/ssh/ssh_known_hosts /etc/ssh/ssh_known_hosts
44 |
45 | RUN mkdir -p /app/config/tls
46 | RUN mkdir -p /app/config/gpg/source && \
47 | mkdir -p /app/config/gpg/keys
48 | # chown argocd /app/config/gpg/keys && \
49 | # chmod 0700 /app/config/gpg/keys
50 |
51 | WORKDIR /
52 | COPY --from=builder /workspace/dist/argocd-applicationset /usr/local/bin/applicationset-controller
53 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | VERSION_PACKAGE=github.com/argoproj/applicationset/common
2 | VERSION?=$(shell cat VERSION)
3 | IMAGE_NAMESPACE?=argoproj
4 | IMAGE_PLATFORMS?=linux/amd64,linux/arm64
5 | IMAGE_NAME?=argocd-applicationset
6 | IMAGE_TAG?=latest
7 | CONTAINER_REGISTRY?=quay.io
8 | GIT_COMMIT = $(shell git rev-parse HEAD)
9 | LDFLAGS = -w -s -X ${VERSION_PACKAGE}.version=${VERSION} \
10 | -X ${VERSION_PACKAGE}.gitCommit=${GIT_COMMIT}
11 |
12 | MKDOCS_DOCKER_IMAGE?=squidfunk/mkdocs-material:4.1.1
13 | MKDOCS_RUN_ARGS?=
14 |
15 | CURRENT_DIR=$(shell pwd)
16 |
17 | KUSTOMIZE = $(shell pwd)/bin/kustomize
18 | CONTROLLER_GEN = $(shell pwd)/bin/controller-gen
19 |
20 | ifdef IMAGE_NAMESPACE
21 |
22 | ifdef CONTAINER_REGISTRY
23 | IMAGE_PREFIX=${CONTAINER_REGISTRY}/${IMAGE_NAMESPACE}/
24 | else
25 | IMAGE_PREFIX=${IMAGE_NAMESPACE}/
26 | endif
27 |
28 | else
29 | IMAGE_PREFIX=
30 | endif
31 |
32 |
33 | # Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set)
34 | ifeq (,$(shell go env GOBIN))
35 | GOBIN=$(shell go env GOPATH)/bin
36 | else
37 | GOBIN=$(shell go env GOBIN)
38 | endif
39 |
40 | .PHONY: build
41 | build: manifests fmt vet
42 | CGO_ENABLED=0 go build -ldflags="${LDFLAGS}" -o ./dist/argocd-applicationset .
43 |
44 | .PHONY: test
45 | test: generate fmt vet manifests
46 | go test -race -count=1 -coverprofile=coverage.out `go list ./... | grep -v 'test/e2e'`
47 |
48 | .PHONY: image
49 | image: test
50 | docker buildx build --platform $(IMAGE_PLATFORMS) -t ${IMAGE_PREFIX}${IMAGE_NAME}:${IMAGE_TAG} .
51 |
52 | .PHONY: image-push
53 | image-push: image
54 | docker push ${IMAGE_PREFIX}${IMAGE_NAME}:${IMAGE_TAG}
55 |
56 | .PHONY: deploy
57 | deploy: kustomize manifests
58 | ${KUSTOMIZE} build manifests/namespace-install | kubectl apply -f -
59 | kubectl patch deployment -n argocd argocd-applicationset-controller --type='json' -p='[{"op": "replace", "path": "/spec/template/spec/containers/0/image", "value": "$(IMAGE)"}]'
60 |
61 | # Generate manifests e.g. CRD, RBAC etc.
62 | .PHONY: manifests
63 | manifests: kustomize generate
64 | $(CONTROLLER_GEN) crd:crdVersions=v1,maxDescLen=0 paths="./..." output:crd:artifacts:config=./manifests/crds/
65 | KUSTOMIZE=${KUSTOMIZE} CONTAINER_REGISTRY=${CONTAINER_REGISTRY} hack/generate-manifests.sh
66 |
67 | .PHONY: lint
68 | lint:
69 | golangci-lint --version
70 | GOMAXPROCS=2 golangci-lint run --fix --verbose --timeout 300s
71 |
72 | # Run go fmt against code
73 | .PHONY: fmt
74 | fmt:
75 | go fmt ./...
76 |
77 | # Run go vet against code
78 | .PHONY: vet
79 | vet:
80 | go vet ./...
81 |
82 | # Start the standalone controller for the purpose of running e2e tests
83 | .PHONY: start-e2e
84 | start-e2e: # Ensure the PlacementDecision CRD is present for the ClusterDecisionManagement tests
85 | kubectl apply -f https://raw.githubusercontent.com/open-cluster-management/api/a6845f2ebcb186ec26b832f60c988537a58f3859/cluster/v1alpha1/0000_04_clusters.open-cluster-management.io_placementdecisions.crd.yaml
86 | NAMESPACE=argocd-e2e "dist/argocd-applicationset" --metrics-addr=:12345 --probe-addr=:12346 --argocd-repo-server=localhost:8081 --namespace=argocd-e2e
87 |
88 | # Begin the tests, targeting the standalone controller (started by make start-e2e) and the e2e argo-cd (started by make start-e2e)
89 | .PHONY: test-e2e
90 | test-e2e:
91 | NAMESPACE=argocd-e2e go test -race -count=1 -v -timeout 480s ./test/e2e/applicationset
92 |
93 | # Generate code
94 | generate: controller-gen
95 | $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..."
96 |
97 | .PHONY: build-docs-local
98 | build-docs-local:
99 | mkdocs build
100 |
101 | .PHONY: build-docs
102 | build-docs:
103 | docker run ${MKDOCS_RUN_ARGS} --rm -it -p 8000:8000 -v ${CURRENT_DIR}:/docs ${MKDOCS_DOCKER_IMAGE} build
104 |
105 | .PHONY: serve-docs-local
106 | serve-docs-local:
107 | mkdocs serve
108 |
109 | .PHONY: serve-docs
110 | serve-docs:
111 | docker run ${MKDOCS_RUN_ARGS} --rm -it -p 8000:8000 -v ${CURRENT_DIR}:/docs ${MKDOCS_DOCKER_IMAGE} serve -a 0.0.0.0:8000
112 |
113 | .PHONY: lint-docs
114 | lint-docs:
115 | # https://github.com/dkhamsing/awesome_bot
116 | find docs -name '*.md' -exec grep -l http {} + | xargs docker run --rm -v $(PWD):/mnt:ro dkhamsing/awesome_bot -t 3 --allow-dupe --allow-redirect --white-list `cat docs/assets/broken-link-ignore-list.txt | grep -v "#" | tr "\n" ','` --skip-save-results --
117 |
118 |
119 | controller-gen: ## Download controller-gen to '(project root)/bin', if not already present.
120 | $(call go-get-tool,$(CONTROLLER_GEN),sigs.k8s.io/controller-tools/cmd/controller-gen@v0.3.0)
121 |
122 |
123 | kustomize: ## Download kustomize to '(project root)/bin', if not already present.
124 | $(call go-get-tool,$(KUSTOMIZE),sigs.k8s.io/kustomize/kustomize/v3@v3.9.4)
125 |
126 | # go-get-tool will 'go get' any package $2 and install it to $1.
127 | PROJECT_DIR := $(shell dirname $(abspath $(lastword $(MAKEFILE_LIST))))
128 | define go-get-tool
129 | @[ -f $(1) ] || { \
130 | set -e ;\
131 | TMP_DIR=$$(mktemp -d) ;\
132 | cd $$TMP_DIR ;\
133 | go mod init tmp ;\
134 | echo "Downloading $(2)" ;\
135 | GOBIN=$(PROJECT_DIR)/bin go get $(2) ;\
136 | rm -rf $$TMP_DIR ;\
137 | }
138 | endef
139 |
--------------------------------------------------------------------------------
/PROJECT:
--------------------------------------------------------------------------------
1 | repo: github.com/argoproj/applicationset
2 | resources:
3 | - group: argoproj.io
4 | kind: ApplicationSet
5 | version: v1alpha1
6 | version: "2"
7 |
--------------------------------------------------------------------------------
/VERSION:
--------------------------------------------------------------------------------
1 | 0.5.0
2 |
--------------------------------------------------------------------------------
/api/v1alpha1/groupversion_info.go:
--------------------------------------------------------------------------------
1 | /*
2 |
3 |
4 | Licensed under the Apache License, Version 2.0 (the "License");
5 | you may not use this file except in compliance with the License.
6 | You may obtain a copy of the License at
7 |
8 | http://www.apache.org/licenses/LICENSE-2.0
9 |
10 | Unless required by applicable law or agreed to in writing, software
11 | distributed under the License is distributed on an "AS IS" BASIS,
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | See the License for the specific language governing permissions and
14 | limitations under the License.
15 | */
16 |
17 | // Package v1alpha1 contains API Schema definitions for the argoproj.io v1alpha1 API group
18 | // +kubebuilder:object:generate=true
19 | // +groupName=argoproj.io
20 | package v1alpha1
21 |
22 | import (
23 | "k8s.io/apimachinery/pkg/runtime/schema"
24 | "sigs.k8s.io/controller-runtime/pkg/scheme"
25 | )
26 |
27 | var (
28 | // GroupVersion is group version used to register these objects
29 | GroupVersion = schema.GroupVersion{Group: "argoproj.io", Version: "v1alpha1"}
30 |
31 | // SchemeBuilder is used to add go types to the GroupVersionKind scheme
32 | SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion}
33 |
34 | // AddToScheme adds the types in this group-version to the given scheme.
35 | AddToScheme = SchemeBuilder.AddToScheme
36 | )
37 |
--------------------------------------------------------------------------------
/common/annotations.go:
--------------------------------------------------------------------------------
1 | package common
2 |
3 | const (
4 | // AnnotationApplicationRefresh is an annotation that is added when an ApplicationSet is requested to be refreshed by a webhook. The ApplicationSet controller will remove this annotation at the end of reconcilation.
5 | AnnotationApplicationSetRefresh = "argocd.argoproj.io/application-set-refresh"
6 | )
7 |
--------------------------------------------------------------------------------
/common/version.go:
--------------------------------------------------------------------------------
1 | package common
2 |
3 | // version information populated during build time
4 | var (
5 | version = "" // value from VERSION file
6 | gitCommit = "" // output of git rev-parse HEAD
7 | )
8 |
9 | // Version of the applicationset controller
10 | type Version struct {
11 | Version string
12 | GitCommit string
13 | }
14 |
15 | // GetVersion returns the version of the applicationset controller
16 | func GetVersion() Version {
17 | return Version{
18 | Version: "v" + version,
19 | GitCommit: gitCommit,
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/docs/Application-Deletion.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | !!! important "This page has moved"
4 | This page has moved to [https://argo-cd.readthedocs.io/en/latest/operator-manual/applicationset/Application-Deletion//](https://argo-cd.readthedocs.io/en/latest/operator-manual/applicationset/Application-Deletion//). Redirecting to the new page.
5 |
6 | # Application Pruning & Resource Deletion
7 |
8 | All `Application` resources created by the ApplicationSet controller (from an ApplicationSet) will contain:
9 |
10 | - A `.metadata.ownerReferences` reference back to the *parent* `ApplicationSet` resource
11 | - An Argo CD `resources-finalizer.argocd.argoproj.io` finalizer in `.metadata.finalizers` of the Application if `.syncPolicy.preserveResourcesOnDeletion` is set to false.
12 |
13 | The end result is that when an ApplicationSet is deleted, the following occurs (in rough order):
14 |
15 | - The `ApplicationSet` resource itself is deleted
16 | - Any `Application` resources that were created from this `ApplicationSet` (as identified by owner reference)
17 | - Any deployed resources (`Deployments`, `Services`, `ConfigMaps`, etc) on the managed cluster, that were created from that `Application` resource (by Argo CD), will be deleted.
18 | - Argo CD is responsible for handling this deletion, via [the deletion finalizer](https://argo-cd.readthedocs.io/en/stable/user-guide/app_deletion/#about-the-deletion-finalizer).
19 | - To preserve deployed resources, set `.syncPolicy.preserveResourcesOnDeletion` to true in the ApplicationSet.
20 |
21 | Thus the lifecycle of the `ApplicationSet`, the `Application`, and the `Application`'s resources, are equivalent.
22 |
23 | !!! note
24 | See also the [controlling resource modification](Controlling-Resource-Modification.md) page for more information about how to prevent deletion or modification of Application resources by the ApplicationSet controller.
25 |
26 | It *is* still possible to delete an `ApplicationSet` resource, while preventing `Application`s (and their deployed resources) from also being deleted, using a non-cascading delete:
27 | ```
28 | kubectl delete ApplicationSet (NAME) --cascade=orphan
29 | ```
30 |
31 | !!! warning
32 | Even if using a non-cascaded delete, the `resources-finalizer.argocd.argoproj.io` is still specified on the `Application`. Thus, when the `Application` is deleted, all of its deployed resources will also be deleted. (The lifecycle of the Application, and its *child* objects, are still equivalent.)
33 |
34 | To prevent the deletion of the resources of the Application, such as Services, Deployments, etc, set `.syncPolicy.preserveResourcesOnDeletion` to true in the ApplicationSet. This syncPolicy parameter prevents the finalizer from being added to the Application.
35 |
--------------------------------------------------------------------------------
/docs/Argo-CD-Integration.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | !!! important "This page has moved"
4 | This page has moved to [https://argo-cd.readthedocs.io/en/latest/operator-manual/applicationset/Argo-CD-Integration//](https://argo-cd.readthedocs.io/en/latest/operator-manual/applicationset/Argo-CD-Integration//). Redirecting to the new page.
5 |
6 | # How ApplicationSet controller interacts with Argo CD
7 |
8 | When you create, update, or delete an `ApplicationSet` resource, the ApplicationSet controller responds by creating, updating, or deleting one or more corresponding Argo CD `Application` resources.
9 |
10 | In fact, the *sole* responsibility of the ApplicationSet controller is to create, update, and delete `Application` resources within the Argo CD namespace. The controller's only job is to ensure that the `Application` resources remain consistent with the defined declarative `ApplicationSet` resource, and nothing more.
11 |
12 | Thus the ApplicationSet controller:
13 |
14 | - Does not create/modify/delete Kubernetes resources (other than the `Application` CR)
15 | - Does not connect to clusters other than the one Argo CD is deployed to
16 | - Does not interact with namespaces other than the one Argo CD is deployed within
17 |
18 | !!!important "Use the Argo CD namespace"
19 | All ApplicationSet resources and the ApplicationSet controller must be installed in the same namespace as Argo CD.
20 | ApplicationSet resources in a different namespace will be ignored.
21 |
22 | It is Argo CD itself that is responsible for the actual deployment of the generated child `Application` resources, such as Deployments, Services, and ConfigMaps.
23 |
24 | The ApplicationSet controller can thus be thought of as an `Application` 'factory', taking an `ApplicationSet` resource as input, and outputting one or more Argo CD `Application` resources that correspond to the parameters of that set.
25 |
26 | 
27 |
28 | In this diagram an `ApplicationSet` resource is defined, and it is the responsibility of the ApplicationSet controller to create the corresponding `Application` resources. The resulting `Application` resources are then managed Argo CD: that is, Argo CD is responsible for actually deploying the child resources.
29 |
30 | Argo CD generates the application's Kubernetes resources based on the contents of the Git repository defined within the Application `spec` field, deploying e.g. Deployments, Service, and other resources.
31 |
32 | Creation, update, or deletion of ApplicationSets will have a direct effect on the Applications present in the Argo CD namespace. Likewise, cluster events (the addition/deletion of Argo CD cluster secrets, when using Cluster generator), or changes in Git (when using Git generator), will be used as input to the ApplicationSet controller in constructing `Application` resources.
33 |
34 | Argo CD and the ApplicationSet controller work together to ensure a consistent set of Application resources exist, and are deployed across the target clusters.
35 |
--------------------------------------------------------------------------------
/docs/Development.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | !!! important "This page has moved"
4 | This page has moved to [https://argo-cd.readthedocs.io/en/latest/operator-manual/applicationset/](https://argo-cd.readthedocs.io/en/latest/operator-manual/applicationset/). Redirecting to the new page.
5 |
6 | # Development
7 |
8 | ## Running the ApplicationSet controller as an image within Kubernetes
9 |
10 | The following assumes you have:
11 |
12 | 1. Installed a recent version of [kustomize](https://github.com/kubernetes-sigs/kustomize) (3.x+).
13 | 2. Created a container repository for your development image.
14 | - For example, by creating a repository "(your username)/argocd-applicationset" using [Docker Hub](https://hub.docker.com/) or [Red Hat Quay.io](https://quay.io/).
15 | 3. Ran `docker login` from the CLI, and provided your registry credentials.
16 | 4. Deployed ArgoCD into the `argocd` namespace.
17 | - To install Argo CD, follow the [Argo CD Getting Started](https://argo-cd.readthedocs.io/en/stable/getting_started/) guide.
18 |
19 | To build and push a container with your current code, and deploy Kubernetes manifests for the controller Deployment:
20 |
21 | ```bash
22 | # Build and push the image to container registry
23 | IMAGE="(username)/argocd-applicationset:v0.0.1" make image-push
24 |
25 | # Deploy the ApplicationSet controller manifests
26 | IMAGE="(username)/argocd-applicationset:v0.0.1" make deploy
27 | ```
28 |
29 | The ApplicationSet controller should now be running in the `argocd` namespace.
30 |
31 |
32 | ## Running the ApplicationSet Controller as a standalone process from the CLI
33 |
34 | When iteratively developing a Kubernetes controller, it is often easier to run the controller process from your local CLI, rather than requiring a container rebuild and push for new code changes.
35 |
36 | 1. First, setup a local Argo CD development environment:
37 | - Clone the Argo CD source, and setup an Argo CD dev environment:
38 | - [Setting up your development environment](https://argo-cd.readthedocs.io/en/stable/developer-guide/contributing/#setting-up-your-development-environment)
39 | - [Install the must-have requirements](https://argo-cd.readthedocs.io/en/stable/developer-guide/contributing/#install-the-must-have-requirements)
40 | - [Build your code and run unit tests](https://argo-cd.readthedocs.io/en/stable/developer-guide/contributing/#build-your-code-and-run-unit-tests)
41 |
42 | 2. Ensure that port 8081 is exposed in the Argo CD test server container:
43 | - In the `Makefile` file at the root of the Argo CD repo:
44 | - Add the following to [this location in the Makefile](https://github.com/argoproj/argo-cd/blob/27912a08f151fab038ddb804a618ca8cde01d68e/Makefile#L75)
45 | - Replace: `-p 4000:4000 \`
46 | - With: `-p 4000:4000 -p 8081:8081 \`
47 | - This exposes port 8081 (the repo-server listen port), which is required for ApplicationSet Git generator functionality.
48 |
49 | 3. Start Argo CD and wait for startup completion:
50 | - Ensure your active namespace is set to `argocd` (for example, `kubectl config view --minify | grep namespace:`).
51 | - Run `make start` under the Argo CD dev environment.
52 | - Wait for the Argo CD processes to start within the container.
53 | - These processes should remaining running, alongside the local ApplicationSet controller, during the following steps.
54 | - Verify that:
55 | - You have exposed port 8081 in the Makefile (as described in prerequisites). `docker ps` should show port 8081 as mapped to an accessible IP.
56 |
57 | 4. Apply the ApplicationSet CRDs into the `argocd` namespace, and build the controller:
58 | - `kubectl apply -f manifests/crds/argoproj.io_applicationsets.yaml`
59 | - `make build`
60 |
61 | 5. Run the Application Set Controller from the CLI:
62 | ```
63 | ./dist/argocd-applicationset --metrics-addr=":18081" --probe-addr=":18082" --argocd-repo-server=localhost:8081 --debug --namespace=argocd
64 | ```
65 |
66 | On success, you should see the following (amongst other text):
67 | ```
68 | INFO controller-runtime.controller Starting Controller {"controller": "applicationset"}
69 | INFO controller-runtime.controller Starting workers {"controller": "applicationset", "worker count": 1}
70 | ```
71 |
72 | ## Building docs locally
73 |
74 | ```sh
75 | pip3 install -r docs/requirements.txt
76 | make build-docs-local
77 | make serve-docs-local
78 | ```
79 |
--------------------------------------------------------------------------------
/docs/E2E-Tests.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | !!! important "This page has moved"
4 | This page has moved to [https://argo-cd.readthedocs.io/en/latest/operator-manual/applicationset/](https://argo-cd.readthedocs.io/en/latest/operator-manual/applicationset/). Redirecting to the new page.
5 |
6 | # Running Application Set E2E tests
7 |
8 | The E2E tests will run automatically on each PR/commit as part of a GitHub action. You may also run these tests locally to verify the functionality, as described below.
9 |
10 | ## Argo CD Prerequisites
11 |
12 | If/when ApplicationSet functionality is integrated with Argo CD, this will become significantly easier to setup, but in the mean time you must setup a standalone Argo CD dev environment and start Argo CD configured for E2E tests.
13 |
14 | #### A) Setup Argo CD dev environment
15 |
16 | - Clone the Argo CD source, and setup an Argo CD dev environment:
17 | - [Setting up your development environment](https://argo-cd.readthedocs.io/en/stable/developer-guide/toolchain-guide/#setting-up-your-development-environment)
18 | - [Install the must-have requirements](https://argo-cd.readthedocs.io/en/stable/developer-guide/toolchain-guide/#install-the-must-have-requirements)
19 | - [Build your code and run unit tests](https://argo-cd.readthedocs.io/en/stable/developer-guide/toolchain-guide/#build-your-code-and-run-unit-tests)
20 | - Next, run `make start-e2e` and wait for Argo CD to startup successfully
21 | - Then `make test-e2e`, and wait for a significant number of the tests to run successfully, in order to verify that your environment is correctly setup
22 | - Stop the `make test-e2e` and `make start-e2e` processes
23 |
24 | #### B) Ensure that port 8081 is exposed in the Argo CD test server container:
25 | - In the `Makefile` file at the root of the Argo CD repo:
26 | - Add the following to [this location in the Makefile](https://github.com/argoproj/argo-cd/blob/27912a08f151fab038ddb804a618ca8cde01d68e/Makefile#L75)
27 | - Replace: `-p 4000:4000 \`
28 | - With: `-p 4000:4000 -p 8081:8081 \`
29 | - This exposes port 8081, which is required for ApplicationSets functionality
30 |
31 |
32 |
33 | ## Steps
34 |
35 | #### A) Ensure that Argo CD is running and configured for E2E test:
36 | - Run `make start-e2e` under Argo CD dev environment
37 | - Wait for the Argo CD processes to start within the container
38 | - This process should remaining running through the tests
39 | - Verify that:
40 | - `make test-e2e` should have set your active namespace so that it is now the `argocd-e2e` namespace (`kubectl config view --minify | grep namespace:`)
41 | - You have exposed port 8081 in the Makefile (as described in prerequisites). `docker ps` should show port 8081 as mapped to an accessible IP.
42 |
43 |
44 | #### B) Apply the ApplicationSet CRDs, and build the controller:
45 | ```
46 | kubectl apply -f manifests/crds/argoproj.io_applicationsets.yaml
47 | make build
48 | ```
49 |
50 | #### C) Run the application set controller configured for E2E tests:
51 | - `make start-e2e`
52 | - This process should remain running while the Application Set E2E tests run.
53 |
54 | #### D) Run the tests:
55 | - `make test-e2e`
56 |
--------------------------------------------------------------------------------
/docs/Generators-Cluster-Decision-Resource.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | !!! important "This page has moved"
4 | This page has moved to [https://argo-cd.readthedocs.io/en/latest/operator-manual/applicationset/Generators-Cluster-Decision-Resource//](https://argo-cd.readthedocs.io/en/latest/operator-manual/applicationset/Generators-Cluster-Decision-Resource//). Redirecting to the new page.
5 |
6 | # Cluster Decision Resource Generator
7 |
8 | The cluster decision resource generates a list of Argo CD clusters. This is done using [duck-typing](https://pkg.go.dev/knative.dev/pkg/apis/duck), which does not require knowledge of the full shape of the referenced kubernetes resource. The following is an example of a cluster-decision-resource-based ApplicationSet generator:
9 | ```yaml
10 | apiVersion: argoproj.io/v1alpha1
11 | kind: ApplicationSet
12 | metadata:
13 | name: guestbook
14 | namespace: argocd
15 | spec:
16 | generators:
17 | - clusterDecisionResource:
18 | # ConfigMap with GVK information for the duck type resource
19 | configMapRef: my-configmap
20 | name: quak # Choose either "name" of the resource or "labelSelector"
21 | labelSelector:
22 | matchLabels: # OPTIONAL
23 | duck: spotted
24 | matchExpressions: # OPTIONAL
25 | - key: duck
26 | operator: In
27 | values:
28 | - "spotted"
29 | - "canvasback"
30 | # OPTIONAL: Checks for changes every 60sec (default 3min)
31 | requeueAfterSeconds: 60
32 | template:
33 | metadata:
34 | name: '{{name}}-guestbook'
35 | spec:
36 | project: "default"
37 | source:
38 | repoURL: https://github.com/argoproj/argocd-example-apps/
39 | targetRevision: HEAD
40 | path: guestbook
41 | destination:
42 | server: '{{clusterName}}' # 'server' field of the secret
43 | namespace: guestbook
44 | ```
45 | The `quak` resource, referenced by the ApplicationSet `clusterDecisionResource` generator:
46 | ```yaml
47 | apiVersion: mallard.io/v1beta1
48 | kind: Duck
49 | metadata:
50 | name: quak
51 | spec: {}
52 | status:
53 | # Duck-typing ignores all other aspects of the resource except
54 | # the "decisions" list
55 | decisions:
56 | - clusterName: cluster-01
57 | - clusterName: cluster-02
58 | ```
59 | The `ApplicationSet` resource references a `ConfigMap` that defines the resource to be used in this duck-typing. Only one ConfigMap is required per `ArgoCD` instance, to identify a resource. You can support multiple resource types by creating a `ConfigMap` for each.
60 | ```yaml
61 | apiVersion: v1
62 | kind: ConfigMap
63 | metadata:
64 | name: my-configmap
65 | data:
66 | # apiVersion of the target resource
67 | apiVersion: mallard.io/v1beta1
68 | # kind of the target resource
69 | kind: ducks
70 | # status key name that holds the list of Argo CD clusters
71 | statusListKey: decisions
72 | # The key in the status list whose value is the cluster name found in Argo CD
73 | matchKey: clusterName
74 | ```
75 |
76 | (*The full example can be found [here](https://github.com/argoproj/applicationset/tree/master/examples/clusterDecisionResource).*)
77 |
78 | This example leverages the cluster management capabilities of the [open-cluster-management.io community](https://open-cluster-management.io/). By creating a `ConfigMap` with the GVK for the `open-cluster-management.io` Placement rule, your ApplicationSet can provision to different clusters in a number of novel ways. One example is to have the ApplicationSet maintain only two Argo CD Applications across 3 or more clusters. Then as maintenance or outages occur, the ApplicationSet will always maintain two Applications, moving the application to available clusters under the Placement rule's direction.
79 |
80 | ## How it works
81 | The ApplicationSet needs to be created in the Argo CD namespace, placing the `ConfigMap` in the same namespace allows the ClusterDecisionResource generator to read it. The `ConfigMap` stores the GVK information as well as the status key definitions. In the open-cluster-management example, the ApplicationSet generator will read the kind `placementrules` with an apiVersion of `apps.open-cluster-management.io/v1`. It will attempt to extract the **list** of clusters from the key `decisions`. It then validates the actual cluster name as defined in Argo CD against the **value** from the key `clusterName` in each of the elements in the list.
82 |
83 | The ClusterDecisionResource generator passes the 'name', 'server' and any other key/value in the duck-type resource's status list as parameters into the ApplicationSet template. In this example, the decision array contained an additional key `clusterName`, which is now available to the ApplicationSet template.
84 |
85 | !!! note "Clusters listed as `Status.Decisions` must be predefined in Argo CD"
86 | The cluster names listed in the `Status.Decisions` *must* be defined within Argo CD, in order to generate applications for these values. The ApplicationSet controller does not create clusters within Argo CD.
87 |
88 | The Default Cluster list key is `clusters`.
--------------------------------------------------------------------------------
/docs/Generators-List.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | !!! important "This page has moved"
4 | This page has moved to [https://argo-cd.readthedocs.io/en/latest/operator-manual/applicationset/Generators-List//](https://argo-cd.readthedocs.io/en/latest/operator-manual/applicationset/Generators-List//). Redirecting to the new page.
5 |
6 | # List Generator
7 |
8 | The List generator generates parameters based on an arbitrary list of key/value pairs (as long as the values are string values). In this example, we're targeting a local cluster named `engineering-dev`:
9 | ```yaml
10 | apiVersion: argoproj.io/v1alpha1
11 | kind: ApplicationSet
12 | metadata:
13 | name: guestbook
14 | namespace: argocd
15 | spec:
16 | generators:
17 | - list:
18 | elements:
19 | - cluster: engineering-dev
20 | url: https://kubernetes.default.svc
21 | # - cluster: engineering-prod
22 | # url: https://kubernetes.default.svc
23 | # foo: bar
24 | template:
25 | metadata:
26 | name: '{{cluster}}-guestbook'
27 | spec:
28 | project: default
29 | source:
30 | repoURL: https://github.com/argoproj/applicationset.git
31 | targetRevision: HEAD
32 | path: examples/list-generator/guestbook/{{cluster}}
33 | destination:
34 | server: '{{url}}'
35 | namespace: guestbook
36 | ```
37 | (*The full example can be found [here](https://github.com/argoproj/applicationset/tree/master/examples/list-generator).*)
38 |
39 | In this example, the List generator passes the `url` and `cluster` fields as parameters into the template. If we wanted to add a second environment, we could uncomment the second element and the ApplicationSet controller would automatically target it with the defined application.
40 |
41 | With the ApplicationSet v0.1.0 release, one could *only* specify `url` and `cluster` element fields (plus arbitrary `values`). As of ApplicationSet v0.2.0, any key/value `element` pair is supported (which is also fully backwards compatible with the v0.1.0 form):
42 | ```yaml
43 | spec:
44 | generators:
45 | - list:
46 | elements:
47 | # v0.1.0 form - requires cluster/url keys:
48 | - cluster: engineering-dev
49 | url: https://kubernetes.default.svc
50 | values:
51 | additional: value
52 | # v0.2.0+ form - does not require cluster/URL keys
53 | # (but they are still supported).
54 | - staging: "true"
55 | gitRepo: https://kubernetes.default.svc
56 | # (...)
57 | ```
58 |
59 | !!! note "Clusters must be predefined in Argo CD"
60 | These clusters *must* already be defined within Argo CD, in order to generate applications for these values. The ApplicationSet controller does not create clusters within Argo CD (for instance, it does not have the credentials to do so).
61 |
--------------------------------------------------------------------------------
/docs/Generators-Pull-Request.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | !!! important "This page has moved"
4 | This page has moved to [https://argo-cd.readthedocs.io/en/latest/operator-manual/applicationset/Generators-Pull-Request//](https://argo-cd.readthedocs.io/en/latest/operator-manual/applicationset/Generators-Pull-Request//). Redirecting to the new page.
5 |
6 | # Pull Request Generator
7 |
8 | The Pull Request generator uses the API of an SCMaaS provider (eg GitHub/GitLab) to automatically discover open pull requests within an repository. This fits well with the style of building a test environment when you create a pull request.
9 |
10 |
11 | ```yaml
12 | apiVersion: argoproj.io/v1alpha1
13 | kind: ApplicationSet
14 | metadata:
15 | name: myapps
16 | spec:
17 | generators:
18 | - pullRequest:
19 | # See below for provider specific options.
20 | github:
21 | # ...
22 | ```
23 |
24 | ## GitHub
25 |
26 | Specify the repository from which to fetch the Github Pull requests.
27 |
28 | ```yaml
29 | apiVersion: argoproj.io/v1alpha1
30 | kind: ApplicationSet
31 | metadata:
32 | name: myapps
33 | spec:
34 | generators:
35 | - pullRequest:
36 | github:
37 | # The GitHub organization or user.
38 | owner: myorg
39 | # The Github repository
40 | repo: myrepository
41 | # For GitHub Enterprise (optional)
42 | api: https://git.example.com/
43 | # Reference to a Secret containing an access token. (optional)
44 | tokenRef:
45 | secretName: github-token
46 | key: token
47 | # Labels is used to filter the PRs that you want to target. (optional)
48 | labels:
49 | - preview
50 | requeueAfterSeconds: 1800
51 | template:
52 | # ...
53 | ```
54 |
55 | * `owner`: Required name of the GitHub organization or user.
56 | * `repo`: Required name of the Github repositry.
57 | * `api`: If using GitHub Enterprise, the URL to access it. (Optional)
58 | * `tokenRef`: A `Secret` name and key containing the GitHub access token to use for requests. If not specified, will make anonymous requests which have a lower rate limit and can only see public repositories. (Optional)
59 | * `labels`: Labels is used to filter the PRs that you want to target. (Optional)
60 |
61 | ## Template
62 |
63 | As with all generators, several keys are available for replacement in the generated application.
64 |
65 | ```yaml
66 | apiVersion: argoproj.io/v1alpha1
67 | kind: ApplicationSet
68 | metadata:
69 | name: myapps
70 | spec:
71 | generators:
72 | - pullRequest:
73 | # ...
74 | template:
75 | metadata:
76 | name: 'myapp-{{branch}}-{{number}}'
77 | spec:
78 | source:
79 | repoURL: 'https://github.com/myorg/myrepo.git'
80 | targetRevision: '{{head_sha}}'
81 | path: kubernetes/
82 | helm:
83 | parameters:
84 | - name: "image.tag"
85 | value: "pull-{{head_sha}}"
86 | project: default
87 | destination:
88 | server: https://kubernetes.default.svc
89 | namespace: default
90 | ```
91 |
92 | * `number`: The ID number of the pull request.
93 | * `branch`: The name of the branch of the pull request head.
94 | * `head_sha`: This is the SHA of the head of the pull request.
95 |
96 | ## Webhook Configuration
97 |
98 | When using a Pull Request generator, the ApplicationSet controller polls every `requeueAfterSeconds` interval (defaulting to every 30 minutes) to detect changes. To eliminate this delay from polling, the ApplicationSet webhook server can be configured to receive webhook events, which will trigger Application generation by the Pull Request generator.
99 |
100 | The configuration is almost the same as the one described [in the Git generator](Generators-Git.md), but there is one difference: if you want to use the Pull Request Generator as well, additionally configure the following settings.
101 |
102 | In section 1, _"Create the webhook in the Git provider"_, add an event so that a webhook request will be sent when a pull request is created, closed, or label changed.
103 | Select `Let me select individual events` and enable the checkbox for `Pull requests`.
104 |
105 | 
106 |
107 | The Pull Request Generator will requeue when the next action occurs.
108 |
109 | - `opened`
110 | - `closed`
111 | - `reopened`
112 | - `labeled`
113 | - `unlabeled`
114 | - `synchronized`
115 |
116 | For more information about each event, please refer to the [official documentation](https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads).
117 |
--------------------------------------------------------------------------------
/docs/Generators.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | !!! important "This page has moved"
4 | This page has moved to [https://argo-cd.readthedocs.io/en/latest/operator-manual/applicationset/Generators//](https://argo-cd.readthedocs.io/en/latest/operator-manual/applicationset/Generators//). Redirecting to the new page.
5 |
6 | # Generators
7 |
8 | Generators are responsible for generating *parameters*, which are then rendered into the `template:` fields of the ApplicationSet resource. See the [Introduction](index.md) for an example of how generators work with templates, to create Argo CD Applications.
9 |
10 | Generators are primarily based on the data source that they use to generate the template parameters. For example: the List generator provides a set of parameters from a *literal list*, the Cluster generator uses the *Argo CD cluster list* as a source, the Git generator uses files/directories from a *Git repository*, and so.
11 |
12 | As of this writing there are seven generators:
13 |
14 | - [List generator](Generators-List.md): The List generator allows you to target Argo CD Applications to clusters based on a fixed list of cluster name/URL values.
15 | - [Cluster generator](Generators-Cluster.md): The Cluster generator allows you to target Argo CD Applications to clusters, based on the list of clusters defined within (and managed by) Argo CD (which includes automatically responding to cluster addition/removal events from Argo CD).
16 | - [Git generator](Generators-Git.md): The Git generator allows you to create Applications based on files within a Git repository, or based on the directory structure of a Git repository.
17 | - [Matrix generator](Generators-Matrix.md): The Matrix generator may be used to combine the generated parameters of two separate generators.
18 | - [Merge generator](Generators-Merge.md): The Merge generator may be used to merge the generated parameters of two or more generators. Additional generators can override the values of the base generator.
19 | - [SCM Provider generator](Generators-SCM-Provider.md): The SCM Provider generator uses the API of an SCM provider (eg GitHub) to automatically discover repositories within an organization.
20 | - [Pull Request generator](Generators-Pull-Request.md): The Pull Request generator uses the API of an SCMaaS provider (eg GitHub) to automatically discover open pull requests within an repository.
21 | - [Cluster Decision Resource generator](Generators-Cluster-Decision-Resource.md): The Cluster Decision Resource generator is used to interface with Kubernetes custom resources that use custom resource-specific logic to decide which set of Argo CD clusters to deploy to.
22 |
23 | If you are new to generators, begin with the **List** and **Cluster** generators. For more advanced use cases, see the documentation for the remaining generators above.
24 |
--------------------------------------------------------------------------------
/docs/Geting-Started.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | !!! important "This page has moved"
4 | This page has moved to [https://argo-cd.readthedocs.io/en/latest/operator-manual/applicationset/](https://argo-cd.readthedocs.io/en/latest/operator-manual/applicationset/). Redirecting to the new page.
5 |
6 | # Getting Started
7 |
8 | The Getting Started document has moved [here](Getting-Started.md).
9 |
--------------------------------------------------------------------------------
/docs/assets/Argo-CD-Integration/ApplicationSet-Argo-Diagram-v2.odp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/argoproj/applicationset/ed59975a41a7e632cf3a03378e4002519ac56b62/docs/assets/Argo-CD-Integration/ApplicationSet-Argo-Diagram-v2.odp
--------------------------------------------------------------------------------
/docs/assets/Argo-CD-Integration/ApplicationSet-Argo-Relationship-v2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/argoproj/applicationset/ed59975a41a7e632cf3a03378e4002519ac56b62/docs/assets/Argo-CD-Integration/ApplicationSet-Argo-Relationship-v2.png
--------------------------------------------------------------------------------
/docs/assets/Argo-CD-Integration/ApplicationSet-Argo-Relationship.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/argoproj/applicationset/ed59975a41a7e632cf3a03378e4002519ac56b62/docs/assets/Argo-CD-Integration/ApplicationSet-Argo-Relationship.png
--------------------------------------------------------------------------------
/docs/assets/Introduction/List-Example-In-Argo-CD-Web-UI.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/argoproj/applicationset/ed59975a41a7e632cf3a03378e4002519ac56b62/docs/assets/Introduction/List-Example-In-Argo-CD-Web-UI.png
--------------------------------------------------------------------------------
/docs/assets/Use-Cases/Cluster-Add-Ons.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/argoproj/applicationset/ed59975a41a7e632cf3a03378e4002519ac56b62/docs/assets/Use-Cases/Cluster-Add-Ons.png
--------------------------------------------------------------------------------
/docs/assets/Use-Cases/Monorepos.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/argoproj/applicationset/ed59975a41a7e632cf3a03378e4002519ac56b62/docs/assets/Use-Cases/Monorepos.png
--------------------------------------------------------------------------------
/docs/assets/broken-link-ignore-list.txt:
--------------------------------------------------------------------------------
1 | https://1.2.3.4
2 | https://2.4.6.8
3 | https://9.8.7.6
4 | https://applicationset.example.com/api/webhook
5 | https://argocd-applicationset.readthedocs.io/en/(version)/Getting-Started/
6 | https://git.example.com/
7 | https://github.com/argoproj/applicationset/commit
8 | https://github.com/infra-team/cluster-deployments.git
9 | https://gitlab.example.com/
10 | https://kubernetes.default.svc
11 |
--------------------------------------------------------------------------------
/docs/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/argoproj/applicationset/ed59975a41a7e632cf3a03378e4002519ac56b62/docs/assets/logo.png
--------------------------------------------------------------------------------
/docs/assets/webhook-config-pull-request.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/argoproj/applicationset/ed59975a41a7e632cf3a03378e4002519ac56b62/docs/assets/webhook-config-pull-request.png
--------------------------------------------------------------------------------
/docs/assets/webhook-config.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/argoproj/applicationset/ed59975a41a7e632cf3a03378e4002519ac56b62/docs/assets/webhook-config.png
--------------------------------------------------------------------------------
/docs/js/theme.js:
--------------------------------------------------------------------------------
1 | $( document ).ready(function() {
2 | // Shift nav in mobile when clicking the menu.
3 | $(document).on('click', "[data-toggle='wy-nav-top']", function() {
4 | $("[data-toggle='wy-nav-shift']").toggleClass("shift");
5 | $("[data-toggle='rst-versions']").toggleClass("shift");
6 | });
7 |
8 | // Close menu when you click a link.
9 | $(document).on('click', ".wy-menu-vertical .current ul li a", function() {
10 | $("[data-toggle='wy-nav-shift']").removeClass("shift");
11 | $("[data-toggle='rst-versions']").toggleClass("shift");
12 | });
13 |
14 | // Keyboard navigation
15 | document.addEventListener("keydown", function(e) {
16 | var key = e.which || e.keyCode || window.event && window.event.keyCode;
17 | var page;
18 | switch (key) {
19 | case 78: // n
20 | page = $('[role="navigation"] a:contains(Next):first').prop('href');
21 | break;
22 | case 80: // p
23 | page = $('[role="navigation"] a:contains(Previous):first').prop('href');
24 | break;
25 | case 13: // enter
26 | if (e.target === document.getElementById('mkdocs-search-query')) {
27 | e.preventDefault();
28 | }
29 | break;
30 | default: break;
31 | }
32 | if ($(e.target).is(':input')) {
33 | return true;
34 | } else if (page) {
35 | window.location.href = page;
36 | }
37 | });
38 |
39 | $(document).on('click', "[data-toggle='rst-current-version']", function() {
40 | $("[data-toggle='rst-versions']").toggleClass("shift-up");
41 | });
42 |
43 | // Make tables responsive
44 | $("table.docutils:not(.field-list)").wrap("
");
45 |
46 | $('table').addClass('docutils');
47 |
48 | /*
49 | * Custom rtd-dropdown
50 | */
51 | toggleCurrent = function (elem) {
52 | var parent_li = elem.closest('li');
53 | var menu_li = parent_li.next();
54 | var menu_ul = menu_li.children('ul');
55 | parent_li.siblings('li').not(menu_li).removeClass('current').removeClass('with-children');
56 | parent_li.siblings().find('> ul').not(menu_ul).removeClass('current').addClass('toc-hidden');
57 | parent_li.toggleClass('current').toggleClass('with-children');
58 | menu_li.toggleClass('current');
59 | menu_ul.toggleClass('current').toggleClass('toc-hidden');
60 | }
61 |
62 | // https://github.com/rtfd/sphinx_rtd_theme/blob/master/js/theme.js
63 | $('.tocbase').find('.toctree-expand').each(function () {
64 | var link = $(this).parent();
65 | $(this).on('click', function (ev) {
66 | console.log('click expand');
67 | toggleCurrent(link);
68 | ev.stopPropagation();
69 | return false;
70 | });
71 | link.on('click', function (ev) {
72 | console.log('click link');
73 | toggleCurrent(link);
74 | });
75 | });
76 | });
77 |
78 | window.SphinxRtdTheme = (function (jquery) {
79 | var stickyNav = (function () {
80 | var navBar,
81 | win,
82 | stickyNavCssClass = 'stickynav',
83 | applyStickNav = function () {
84 | if (navBar.height() <= win.height()) {
85 | navBar.addClass(stickyNavCssClass);
86 | } else {
87 | navBar.removeClass(stickyNavCssClass);
88 | }
89 | },
90 | enable = function () {
91 | applyStickNav();
92 | win.on('resize', applyStickNav);
93 | },
94 | init = function () {
95 | navBar = jquery('nav.wy-nav-side:first');
96 | win = jquery(window);
97 | };
98 | jquery(init);
99 | return {
100 | enable : enable
101 | };
102 | }());
103 | return {
104 | StickyNav : stickyNav
105 | };
106 | }($));
107 |
108 | // The code below is a copy of @seanmadsen code posted Jan 10, 2017 on issue 803.
109 | // https://github.com/mkdocs/mkdocs/issues/803
110 | // This just incorporates the auto scroll into the theme itself without
111 | // the need for additional custom.js file.
112 | //
113 | $(function() {
114 | $.fn.isFullyWithinViewport = function(){
115 | var viewport = {};
116 | viewport.top = $(window).scrollTop();
117 | viewport.bottom = viewport.top + $(window).height();
118 | var bounds = {};
119 | bounds.top = this.offset().top;
120 | bounds.bottom = bounds.top + this.outerHeight();
121 | return ( ! (
122 | (bounds.top <= viewport.top) ||
123 | (bounds.bottom >= viewport.bottom)
124 | ) );
125 | };
126 | if( $('li.toctree-l1.current').length && !$('li.toctree-l1.current').isFullyWithinViewport() ) {
127 | $('.wy-nav-side')
128 | .scrollTop(
129 | $('li.toctree-l1.current').offset().top -
130 | $('.wy-nav-side').offset().top -
131 | 60
132 | );
133 | }
134 | });
--------------------------------------------------------------------------------
/docs/requirements.txt:
--------------------------------------------------------------------------------
1 | mkdocs-material
2 | pygments==2.7.4
3 |
--------------------------------------------------------------------------------
/docs/upgrading/v0.2.0-to-v0.3.0.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | !!! important "This page has moved"
4 | This page has moved to [https://argo-cd.readthedocs.io/en/latest/operator-manual/applicationset/](https://argo-cd.readthedocs.io/en/latest/operator-manual/applicationset/). Redirecting to the new page.
5 |
6 | # Upgrading from ApplicationSet controller v0.2.0 to v0.3.0.
7 |
8 | When moving from ApplicationSet v0.2.0 to v0.3.0, there are a couple of behaviour changes to be aware of.
9 |
10 | ## Cluster generator: `{{name}}` parameter value will no longer be normalized, but existing behaviour is preserved in a new `{{nameNormalized}}` parameter
11 |
12 | The Cluster generator `{{name}}` parameter has now reverted to it's original behaviour: the cluster name within Argo CD will no longer be [normalized](https://github.com/argoproj/applicationset/blob/11f1fe893b019c9a530865fa83ee78b16af2c090/pkg/generators/cluster.go#L168). The `{{name}}` parameter generated by the Cluster generator within the ApplicationSet will now be passed unmodified to the ApplicationSet template.
13 |
14 | A new parameter, `{{nameNormalized}}` has been introduced which preserves the 0.2.0 behaviour. This allows you to choose which behaviour you wish to use in your ApplicationSet, based on the context in which it is used: either using the parameter as defined, or in a normalized form (which allows it to be used in the `name` field of a Application resource.)
15 |
16 | If your Argo CD cluster names are already valid, no change is required. However, to preserve the v0.2.0 behaviour of your ApplicationSet, replace `{{name}}` with `{{nameNormalized}}` within your ApplicationSet template.
17 |
18 | More information about this change is [available from the issue](https://github.com/argoproj/applicationset/pull/390).
19 |
20 | ## If an ApplicationSet generates an invalid Application, the valid generated Applications will now be processed
21 |
22 | The responsibility of the ApplicationSet controller is to convert an `ApplicationSet` resource into one or more `Application` resources. However, with the previous releases, if at least one of the generated `Application` resources was invalid (eg it failed the internal validation logic), none of the generated Applications would be processed (they would not be neither created nor modified).
23 |
24 | With the latest ApplicationSet release, if a generator generates invalid Applications, those invalid generated Applications will still be skipped, **but** the valid generated Applications will now be processed (created/modified).
25 |
26 | Thus no `ApplicationSet` resource changes are required by this upgrade, but it is worth keeping in mind that your ApplicationSets which were previously blocked by a failing Application may no longer be blocked. This change might cause valid Applications to now be created/modified, whereas previously they were prevented from being processed.
27 |
--------------------------------------------------------------------------------
/entrypoint.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # If we're started as PID 1, we should wrap command execution through tini to
4 | # prevent leakage of orphaned processes ("zombies").
5 | if test "$$" = "1"; then
6 | exec tini -- $@
7 | else
8 | exec "$@"
9 | fi
--------------------------------------------------------------------------------
/examples/cluster/cluster-example.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: argoproj.io/v1alpha1
2 | kind: ApplicationSet
3 | metadata:
4 | name: guestbook
5 | spec:
6 | generators:
7 | - clusters: {}
8 | template:
9 | metadata:
10 | name: '{{name}}-guestbook'
11 | spec:
12 | project: "default"
13 | source:
14 | repoURL: https://github.com/argoproj/argocd-example-apps/
15 | targetRevision: HEAD
16 | path: guestbook
17 | destination:
18 | server: '{{server}}'
19 | namespace: guestbook
20 |
--------------------------------------------------------------------------------
/examples/clusterDecisionResource/README.md:
--------------------------------------------------------------------------------
1 | # How the Cluster Decision Resource generator works for clusterDecisionResource
2 | 1. The Cluster Decision Resource generator reads a configurable status format:
3 | ```yaml
4 | status:
5 | clusters:
6 | - name: cluster-01
7 | - name: cluster-02
8 | ```
9 | This is a common status format. Another format that could be read looks like this:
10 | ```yaml
11 | status:
12 | decisions:
13 | - clusterName: cluster-01
14 | namespace: cluster-01
15 | - clusterName: cluster-02
16 | namespace: cluster-02
17 | ```
18 | 2. Any resource that has a list of key / value pairs, where the value matches ArgoCD cluster names can be used.
19 | 3. The key / value pairs found in each element of the list will be available to the template. As well, `name` and `server` will still be available to the template.
20 | 4. The Service Account used by the ApplicationSet controller must have access to `Get` the resource you want to retrieve the duck type definition from
21 | 5. A configMap is used to identify the resource to read status of generated ArgoCD clusters from. You can use multiple resources by creating a ConfigMap for each one in the ArgoCD namespace.
22 | ```yaml
23 | apiVersion: v1
24 | kind: ConfigMap
25 | metadata:
26 | name: my-configmap
27 | data:
28 | apiVersion: group.io/v1
29 | kind: mykinds
30 | statusListKey: clusters
31 | matchKey: name
32 | ```
33 | * `apiVersion` - This is the apiVersion of your resource
34 | * `kind` - This is the plural kind of your resource
35 | * `statusListKey` - Default is 'clusters', this is the key found in your resource's status that is a list of ArgoCD clusters.
36 | * `matchKey` - Is the key name found in the cluster list, `name` and `clusterName` are the keys in the examples above.
37 |
38 | # Applying the example
39 | 1. Connect to a cluster with the ApplicationSet controller running
40 | 2. Edit the Role for the ApplicationSet service account, and grant it permission to `list` the `placementdecisions` resources, from apiGroups `cluster.open-cluster-management.io/v1alpha1`
41 | ```yaml
42 | - apiGroups:
43 | - "cluster.open-cluster-management.io/v1alpha1"
44 | resources:
45 | - placementdecisions
46 | verbs:
47 | - list
48 | ```
49 | 3. Apply the following controller and associated ManagedCluster CRD's:
50 | https://github.com/open-cluster-management/placement
51 | 4. Now apply the PlacementDecision and an ApplicationSet:
52 | ```bash
53 | kubectl apply -f ./placementdecision.yaml
54 | kubectl apply -f ./configMap.yaml
55 | kubectl apply -f ./ducktype-example.yaml
56 | ```
57 | 5. For now this won't do anything until you create a controller that populates the `Status.Decisions` array.
--------------------------------------------------------------------------------
/examples/clusterDecisionResource/configMap.yaml:
--------------------------------------------------------------------------------
1 | # To generate a Status.Decisions from this CRD, requires https://github.com/open-cluster-management/multicloud-operators-placementrule be deployed
2 | ---
3 | apiVersion: v1
4 | kind: ConfigMap
5 | metadata:
6 | name: ocm-placement
7 | data:
8 | apiVersion: apps.open-cluster-management.io/v1
9 | kind: placementrules
10 | statusListKey: decisions
11 | matchKey: clusterName
12 |
--------------------------------------------------------------------------------
/examples/clusterDecisionResource/ducktype-example.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: argoproj.io/v1alpha1
2 | kind: ApplicationSet
3 | metadata:
4 | name: book-import
5 | spec:
6 | generators:
7 | - clusterDecisionResource:
8 | configMapRef: ocm-placement
9 | name: test-placement
10 | requeueAfterSeconds: 30
11 | template:
12 | metadata:
13 | name: '{{clusterName}}-book-import'
14 | spec:
15 | project: "default"
16 | source:
17 | repoURL: https://github.com/open-cluster-management/application-samples.git
18 | targetRevision: HEAD
19 | path: book-import
20 | destination:
21 | name: '{{clusterName}}'
22 | namespace: bookimport
23 | syncPolicy:
24 | automated:
25 | prune: true
26 | syncOptions:
27 | - CreateNamespace=true
28 |
--------------------------------------------------------------------------------
/examples/clusterDecisionResource/placementdecision.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | apiVersion: apps.open-cluster-management.io/v1
3 | kind: PlacementRule
4 | metadata:
5 | name: test-placement
6 | spec:
7 | clusterReplicas: 1 # Availability choice, maximum number of clusters to provision at once
8 | clusterSelector:
9 | matchLabels:
10 | 'usage': 'development'
11 | clusterConditions:
12 | - type: ManagedClusterConditionAvailable
13 | status: "True"
14 | # Below is sample output the generator can consume.
15 | status:
16 | decisions:
17 | - clusterName: cluster-01
18 | - clusterName: cluster-02
--------------------------------------------------------------------------------
/examples/design-doc/applicationset.yaml:
--------------------------------------------------------------------------------
1 | # This is an example of a typical ApplicationSet which uses the cluster generator.
2 | # An ApplicationSet is comprised with two stanzas:
3 | # - spec.generator - producer of a list of values supplied as arguments to an app template
4 | # - spec.template - an application template, which has been parameterized
5 | apiVersion: argoproj.io/v1alpha1
6 | kind: ApplicationSet
7 | metadata:
8 | name: guestbook
9 | spec:
10 | generators:
11 | - clusters: {}
12 | template:
13 | metadata:
14 | name: '{{name}}-guestbook'
15 | spec:
16 | source:
17 | repoURL: https://github.com/infra-team/cluster-deployments.git
18 | targetRevision: HEAD
19 | chart: guestbook
20 | destination:
21 | server: '{{server}}'
22 | namespace: guestbook
23 |
--------------------------------------------------------------------------------
/examples/design-doc/clusters.yaml:
--------------------------------------------------------------------------------
1 | # The cluster generator produces an items list from all clusters registered to Argo CD.
2 | # It automatically provides the following fields as values to the app template:
3 | # - name
4 | # - server
5 | # - metadata.labels.
6 | # - metadata.annotations.
7 | # - values.
8 | apiVersion: argoproj.io/v1alpha1
9 | kind: ApplicationSet
10 | metadata:
11 | name: guestbook
12 | spec:
13 | generators:
14 | - clusters:
15 | selector:
16 | matchLabels:
17 | argocd.argoproj.io/secret-type: cluster
18 | values:
19 | project: default
20 | template:
21 | metadata:
22 | name: '{{name}}-guestbook'
23 | labels:
24 | environment: '{{metadata.labels.environment}}'
25 | spec:
26 | project: '{{values.project}}'
27 | source:
28 | repoURL: https://github.com/infra-team/cluster-deployments.git
29 | targetRevision: HEAD
30 | chart: guestbook
31 | destination:
32 | server: '{{server}}'
33 | namespace: guestbook
34 |
--------------------------------------------------------------------------------
/examples/design-doc/git-directory-discovery.yaml:
--------------------------------------------------------------------------------
1 | # This example demonstrates the git directory generator, which produces an items list
2 | # based on discovery of directories in a git repo matching a specified pattern.
3 | # Git generators automatically provide {{path}} and {{path.basename}} as available
4 | # variables to the app template.
5 | #
6 | # Suppose the following git directory structure (note the use of different config tools):
7 | #
8 | # cluster-deployments
9 | # └── add-ons
10 | # ├── argo-rollouts
11 | # │ ├── all.yaml
12 | # │ └── kustomization.yaml
13 | # ├── argo-workflows
14 | # │ └── install.yaml
15 | # ├── grafana
16 | # │ ├── Chart.yaml
17 | # │ └── values.yaml
18 | # └── prometheus-operator
19 | # ├── Chart.yaml
20 | # └── values.yaml
21 | #
22 | # The following ApplicationSet would produce four applications (in different namespaces),
23 | # using the directory basename as both the namespace and application name.
24 | apiVersion: argoproj.io/v1alpha1
25 | kind: ApplicationSet
26 | metadata:
27 | name: cluster-addons
28 | spec:
29 | generators:
30 | - git:
31 | repoURL: https://github.com/infra-team/cluster-deployments.git
32 | directories:
33 | - path: add-ons/*
34 | template:
35 | metadata:
36 | name: '{{path.basename}}'
37 | spec:
38 | source:
39 | repoURL: https://github.com/infra-team/cluster-deployments.git
40 | targetRevision: HEAD
41 | path: '{{path}}'
42 | destination:
43 | server: http://kubernetes.default.svc
44 | namespace: '{{path.basename}}'
45 |
--------------------------------------------------------------------------------
/examples/design-doc/git-files-discovery.yaml:
--------------------------------------------------------------------------------
1 | # This example demonstrates a git file generator which traverses the directory structure of a git
2 | # repository to discover items based on a filename convention. For each file discovered, the
3 | # contents of the discovered files themselves, act as the set of inputs to the app template.
4 | #
5 | # Suppose the following git directory structure:
6 | #
7 | # cluster-deployments
8 | # ├── apps
9 | # │ └── guestbook
10 | # │ └── install.yaml
11 | # └── cluster-config
12 | # ├── engineering
13 | # │ ├── dev
14 | # │ │ └── config.json
15 | # │ └── prod
16 | # │ └── config.json
17 | # └── finance
18 | # ├── dev
19 | # │ └── config.json
20 | # └── prod
21 | # └── config.json
22 | #
23 | # The discovered files (e.g. config.json) files can be any structured data supplied to the
24 | # generated application. e.g.:
25 | # {
26 | # "aws_account": "123456",
27 | # "asset_id": "11223344"
28 | # "cluster": {
29 | # "owner": "Jesse_Suen@intuit.com",
30 | # "name": "engineering-dev",
31 | # "address": "http://1.2.3.4"
32 | # }
33 | # }
34 | #
35 | apiVersion: argoproj.io/v1alpha1
36 | kind: ApplicationSet
37 | metadata:
38 | name: guestbook
39 | spec:
40 | generators:
41 | - git:
42 | repoURL: https://github.com/infra-team/cluster-deployments.git
43 | files:
44 | - path: "**/config.json"
45 | template:
46 | metadata:
47 | name: '{{cluster.name}}-guestbook'
48 | spec:
49 | source:
50 | repoURL: https://github.com/infra-team/cluster-deployments.git
51 | targetRevision: HEAD
52 | path: apps/guestbook
53 | destination:
54 | server: '{{cluster.address}}'
55 | namespace: guestbook
56 |
--------------------------------------------------------------------------------
/examples/design-doc/git-files-literal.yaml:
--------------------------------------------------------------------------------
1 | # This example demonstrates a git file generator which produces its items based on one or
2 | # more files referenced in a git repo. The referenced files would contain a json/yaml list of
3 | # arbitrary structured objects. Each item of the list would become a set of parameters to a
4 | # generated application.
5 | #
6 | # Suppose the following git directory structure:
7 | #
8 | # cluster-deployments
9 | # ├── apps
10 | # │ └── guestbook
11 | # │ ├── v1.0
12 | # │ │ └── install.yaml
13 | # │ └── v2.0
14 | # │ └── install.yaml
15 | # └── config
16 | # └── clusters.json
17 | #
18 | # In this example, the `clusters.json` file is json list of structured data:
19 | # [
20 | # {
21 | # "account": "123456",
22 | # "asset_id": "11223344",
23 | # "cluster": {
24 | # "owner": "Jesse_Suen@intuit.com",
25 | # "name": "engineering-dev",
26 | # "address": "http://1.2.3.4"
27 | # },
28 | # "appVersions": {
29 | # "prometheus-operator": "v0.38",
30 | # "guestbook": "v2.0"
31 | # }
32 | # },
33 | # {
34 | # "account": "456789",
35 | # "asset_id": "55667788",
36 | # "cluster": {
37 | # "owner": "Alexander_Matyushentsev@intuit.com",
38 | # "name": "engineering-prod",
39 | # "address": "http://2.4.6.8"
40 | # },
41 | # "appVersions": {
42 | # "prometheus-operator": "v0.38",
43 | # "guestbook": "v1.0"
44 | # }
45 | # }
46 | # ]
47 | #
48 | apiVersion: argoproj.io/v1alpha1
49 | kind: ApplicationSet
50 | metadata:
51 | name: guestbook
52 | spec:
53 | generators:
54 | - git:
55 | repoURL: https://github.com/infra-team/cluster-deployments.git
56 | files:
57 | - path: config/clusters.json
58 | template:
59 | metadata:
60 | name: '{{cluster.name}}-guestbook'
61 | spec:
62 | source:
63 | repoURL: https://github.com/infra-team/cluster-deployments.git
64 | targetRevision: HEAD
65 | path: apps/guestbook/{{appVersions.guestbook}}
66 | destination:
67 | server: http://kubernetes.default.svc
68 | namespace: guestbook
69 |
--------------------------------------------------------------------------------
/examples/design-doc/list.yaml:
--------------------------------------------------------------------------------
1 | # The list generator specifies a literal list of argument values to the app spec template.
2 | apiVersion: argoproj.io/v1alpha1
3 | kind: ApplicationSet
4 | metadata:
5 | name: guestbook
6 | spec:
7 | generators:
8 | - list:
9 | elements:
10 | - cluster: engineering-dev
11 | url: https://1.2.3.4
12 | values:
13 | project: dev
14 | - cluster: engineering-prod
15 | url: https://2.4.6.8
16 | values:
17 | project: prod
18 | - cluster: finance-preprod
19 | url: https://9.8.7.6
20 | values:
21 | project: preprod
22 | template:
23 | metadata:
24 | name: '{{cluster}}-guestbook'
25 | spec:
26 | project: '{{values.project}}'
27 | source:
28 | repoURL: https://github.com/infra-team/cluster-deployments.git
29 | targetRevision: HEAD
30 | path: guestbook/{{cluster}}
31 | destination:
32 | server: '{{url}}'
33 | namespace: guestbook
34 |
--------------------------------------------------------------------------------
/examples/design-doc/proposal/README.md:
--------------------------------------------------------------------------------
1 | # Proposal Examples
2 | This directory contains examples that are not yet implemented.
3 | They are part of the project to indicate future progress, and we are welcome any contribution that will add an implementation
4 |
--------------------------------------------------------------------------------
/examples/design-doc/proposal/filters.yaml:
--------------------------------------------------------------------------------
1 | # For all generators, filters can be applied to reduce the generated items to a smaller subset.
2 | # A powerful set of filter expressions are supported using syntax provided by the
3 | # https://github.com/antonmedv/expr library. Examples expressions are demonstrated below
4 | apiVersion: argoproj.io/v1alpha1
5 | kind: ApplicationSet
6 | metadata:
7 | name: guestbook
8 | spec:
9 | generators:
10 | # Match all clusters who meet ALL of the following conditions:
11 | # 1. name matches the regex `sales-.*`
12 | # 2. environment label is either 'staging' or 'prod'
13 | - clusters:
14 | filters:
15 | - expr: '{{name}} matches "sales-.*"'
16 | - expr: '{{metadata.labels.environment}} in [staging, prod]'
17 | values:
18 | version: '2.0.0'
19 | # Filter items from `config/clusters.json` in the `cluster-deployments` git repo,
20 | # to only those having the `cluster.enabled == true` property. e.g.:
21 | # {
22 | # ...
23 | # "cluster": {
24 | # "enabled": true,
25 | # ...
26 | # }
27 | # }
28 | - git:
29 | repoURL: https://github.com/infra-team/cluster-deployments.git
30 | files:
31 | - path: config/clusters.json
32 | filters:
33 | - expr: '{{cluster.enabled}} == true'
34 | template:
35 | metadata:
36 | name: '{{name}}-guestbook'
37 | spec:
38 | source:
39 | repoURL: https://github.com/infra-team/cluster-deployments.git
40 | targetRevision: "{{values.version}}"
41 | chart: guestbook
42 | helm:
43 | parameters:
44 | - name: foo
45 | value: "{{metadata.annotations.foo}}"
46 | destination:
47 | server: '{{server}}'
48 | namespace: guestbook
49 |
--------------------------------------------------------------------------------
/examples/design-doc/template-override.yaml:
--------------------------------------------------------------------------------
1 | # App templates can also be defined as part of the generator's template stanza. Sometimes it is
2 | # useful to do this in order to override the spec.template stanza, and when simple string
3 | # parameterization are insufficient. In the below examples, the generators[].XXX.template is
4 | # a partial definition, which overrides/patch the default template.
5 | apiVersion: argoproj.io/v1alpha1
6 | kind: ApplicationSet
7 | metadata:
8 | name: guestbook
9 | spec:
10 | generators:
11 | - list:
12 | elements:
13 | - cluster: engineering-dev
14 | url: https://1.2.3.4
15 | template:
16 | metadata: {}
17 | spec:
18 | project: "project"
19 | source:
20 | repoURL: https://github.com/infra-team/cluster-deployments.git
21 | path: '{{cluster}}-override'
22 | destination: {}
23 |
24 | - list:
25 | elements:
26 | - cluster: engineering-prod
27 | url: https://1.2.3.4
28 | template:
29 | metadata: {}
30 | spec:
31 | project: "project2"
32 | source:
33 | repoURL: https://github.com/infra-team/cluster-deployments.git
34 | path: '{{cluster}}-override2'
35 | destination: {}
36 |
37 | template:
38 | metadata:
39 | name: '{{cluster}}-guestbook'
40 | spec:
41 | project: "project"
42 | source:
43 | repoURL: https://github.com/infra-team/cluster-deployments.git
44 | targetRevision: HEAD
45 | path: guestbook/{{cluster}}
46 | destination:
47 | server: '{{url}}'
48 | namespace: guestbook
49 |
--------------------------------------------------------------------------------
/examples/git-generator-directory/cluster-addons/argo-workflows/kustomization.yaml:
--------------------------------------------------------------------------------
1 | #namePrefix: kustomize-
2 |
3 | resources:
4 | - namespace-install.yaml
5 | apiVersion: kustomize.config.k8s.io/v1beta1
6 | kind: Kustomization
7 |
--------------------------------------------------------------------------------
/examples/git-generator-directory/cluster-addons/prometheus-operator/Chart.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v2
2 | name: helm-prometheus-operator
3 |
4 | type: application
5 |
6 | # This is the chart version. This version number should be incremented each time you make changes
7 | # to the chart and its templates, including the app version.
8 | # Versions are expected to follow Semantic Versioning (https://semver.org/)
9 | version: 0.1.0
10 |
11 | # This is the version number of the application being deployed. This version number should be
12 | # incremented each time you make changes to the application. Versions are not expected to
13 | # follow Semantic Versioning. They should reflect the version the application is using.
14 | appVersion: "1.0"
--------------------------------------------------------------------------------
/examples/git-generator-directory/cluster-addons/prometheus-operator/requirements.yaml:
--------------------------------------------------------------------------------
1 | dependencies:
2 | - name: kube-prometheus-stack
3 | version: 9.4.10
4 | repository: https://prometheus-community.github.io/helm-charts
5 |
--------------------------------------------------------------------------------
/examples/git-generator-directory/cluster-addons/prometheus-operator/values.yaml:
--------------------------------------------------------------------------------
1 | # Blank values.yaml
2 |
--------------------------------------------------------------------------------
/examples/git-generator-directory/excludes/cluster-addons/argo-workflows/kustomization.yaml:
--------------------------------------------------------------------------------
1 | #namePrefix: kustomize-
2 |
3 | resources:
4 | - namespace-install.yaml
5 | apiVersion: kustomize.config.k8s.io/v1beta1
6 | kind: Kustomization
7 |
--------------------------------------------------------------------------------
/examples/git-generator-directory/excludes/cluster-addons/exclude-helm-guestbook/Chart.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v2
2 | name: helm-guestbook
3 | description: A Helm chart for Kubernetes
4 |
5 | # A chart can be either an 'application' or a 'library' chart.
6 | #
7 | # Application charts are a collection of templates that can be packaged into versioned archives
8 | # to be deployed.
9 | #
10 | # Library charts provide useful utilities or functions for the chart developer. They're included as
11 | # a dependency of application charts to inject those utilities and functions into the rendering
12 | # pipeline. Library charts do not define any templates and therefore cannot be deployed.
13 | type: application
14 |
15 | # This is the chart version. This version number should be incremented each time you make changes
16 | # to the chart and its templates, including the app version.
17 | # Versions are expected to follow Semantic Versioning (https://semver.org/)
18 | version: 0.1.0
19 |
20 | # This is the version number of the application being deployed. This version number should be
21 | # incremented each time you make changes to the application. Versions are not expected to
22 | # follow Semantic Versioning. They should reflect the version the application is using.
23 | appVersion: "1.0"
24 |
--------------------------------------------------------------------------------
/examples/git-generator-directory/excludes/cluster-addons/exclude-helm-guestbook/templates/NOTES.txt:
--------------------------------------------------------------------------------
1 | 1. Get the application URL by running these commands:
2 | {{- if .Values.ingress.enabled }}
3 | {{- range .Values.ingress.hosts }}
4 | http{{ if $.Values.ingress.tls }}s{{ end }}://{{ . }}{{ $.Values.ingress.path }}
5 | {{- end }}
6 | {{- else if contains "NodePort" .Values.service.type }}
7 | export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ template "helm-guestbook.fullname" . }})
8 | export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
9 | echo http://$NODE_IP:$NODE_PORT
10 | {{- else if contains "LoadBalancer" .Values.service.type }}
11 | NOTE: It may take a few minutes for the LoadBalancer IP to be available.
12 | You can watch the status of by running 'kubectl get svc -w {{ template "helm-guestbook.fullname" . }}'
13 | export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ template "helm-guestbook.fullname" . }} -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
14 | echo http://$SERVICE_IP:{{ .Values.service.port }}
15 | {{- else if contains "ClusterIP" .Values.service.type }}
16 | export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app={{ template "helm-guestbook.name" . }},release={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
17 | echo "Visit http://127.0.0.1:8080 to use your application"
18 | kubectl port-forward $POD_NAME 8080:80
19 | {{- end }}
20 |
--------------------------------------------------------------------------------
/examples/git-generator-directory/excludes/cluster-addons/exclude-helm-guestbook/templates/_helpers.tpl:
--------------------------------------------------------------------------------
1 | {{/* vim: set filetype=mustache: */}}
2 | {{/*
3 | Expand the name of the chart.
4 | */}}
5 | {{- define "helm-guestbook.name" -}}
6 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
7 | {{- end -}}
8 |
9 | {{/*
10 | Create a default fully qualified app name.
11 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
12 | If release name contains chart name it will be used as a full name.
13 | */}}
14 | {{- define "helm-guestbook.fullname" -}}
15 | {{- if .Values.fullnameOverride -}}
16 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
17 | {{- else -}}
18 | {{- $name := default .Chart.Name .Values.nameOverride -}}
19 | {{- if contains $name .Release.Name -}}
20 | {{- .Release.Name | trunc 63 | trimSuffix "-" -}}
21 | {{- else -}}
22 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
23 | {{- end -}}
24 | {{- end -}}
25 | {{- end -}}
26 |
27 | {{/*
28 | Create chart name and version as used by the chart label.
29 | */}}
30 | {{- define "helm-guestbook.chart" -}}
31 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
32 | {{- end -}}
33 |
--------------------------------------------------------------------------------
/examples/git-generator-directory/excludes/cluster-addons/exclude-helm-guestbook/templates/deployment.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: Deployment
3 | metadata:
4 | name: {{ template "helm-guestbook.fullname" . }}
5 | labels:
6 | app: {{ template "helm-guestbook.name" . }}
7 | chart: {{ template "helm-guestbook.chart" . }}
8 | release: {{ .Release.Name }}
9 | heritage: {{ .Release.Service }}
10 | spec:
11 | replicas: {{ .Values.replicaCount }}
12 | revisionHistoryLimit: 3
13 | selector:
14 | matchLabels:
15 | app: {{ template "helm-guestbook.name" . }}
16 | release: {{ .Release.Name }}
17 | template:
18 | metadata:
19 | labels:
20 | app: {{ template "helm-guestbook.name" . }}
21 | release: {{ .Release.Name }}
22 | spec:
23 | containers:
24 | - name: {{ .Chart.Name }}
25 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
26 | imagePullPolicy: {{ .Values.image.pullPolicy }}
27 | ports:
28 | - name: http
29 | containerPort: 80
30 | protocol: TCP
31 | livenessProbe:
32 | httpGet:
33 | path: /
34 | port: http
35 | readinessProbe:
36 | httpGet:
37 | path: /
38 | port: http
39 | resources:
40 | {{ toYaml .Values.resources | indent 12 }}
41 | {{- with .Values.nodeSelector }}
42 | nodeSelector:
43 | {{ toYaml . | indent 8 }}
44 | {{- end }}
45 | {{- with .Values.affinity }}
46 | affinity:
47 | {{ toYaml . | indent 8 }}
48 | {{- end }}
49 | {{- with .Values.tolerations }}
50 | tolerations:
51 | {{ toYaml . | indent 8 }}
52 | {{- end }}
53 |
--------------------------------------------------------------------------------
/examples/git-generator-directory/excludes/cluster-addons/exclude-helm-guestbook/templates/service.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: Service
3 | metadata:
4 | name: {{ template "helm-guestbook.fullname" . }}
5 | labels:
6 | app: {{ template "helm-guestbook.name" . }}
7 | chart: {{ template "helm-guestbook.chart" . }}
8 | release: {{ .Release.Name }}
9 | heritage: {{ .Release.Service }}
10 | spec:
11 | type: {{ .Values.service.type }}
12 | ports:
13 | - port: {{ .Values.service.port }}
14 | targetPort: http
15 | protocol: TCP
16 | name: http
17 | selector:
18 | app: {{ template "helm-guestbook.name" . }}
19 | release: {{ .Release.Name }}
20 |
--------------------------------------------------------------------------------
/examples/git-generator-directory/excludes/cluster-addons/exclude-helm-guestbook/values-production.yaml:
--------------------------------------------------------------------------------
1 | service:
2 | type: LoadBalancer
3 |
--------------------------------------------------------------------------------
/examples/git-generator-directory/excludes/cluster-addons/exclude-helm-guestbook/values.yaml:
--------------------------------------------------------------------------------
1 | # Default values for helm-guestbook.
2 | # This is a YAML-formatted file.
3 | # Declare variables to be passed into your templates.
4 |
5 | replicaCount: 1
6 |
7 | image:
8 | repository: gcr.io/heptio-images/ks-guestbook-demo
9 | tag: 0.1
10 | pullPolicy: IfNotPresent
11 |
12 | service:
13 | type: ClusterIP
14 | port: 80
15 |
16 | ingress:
17 | enabled: false
18 | annotations: {}
19 | # kubernetes.io/ingress.class: nginx
20 | # kubernetes.io/tls-acme: "true"
21 | path: /
22 | hosts:
23 | - chart-example.local
24 | tls: []
25 | # - secretName: chart-example-tls
26 | # hosts:
27 | # - chart-example.local
28 |
29 | resources: {}
30 | # We usually recommend not to specify default resources and to leave this as a conscious
31 | # choice for the user. This also increases chances charts run on environments with little
32 | # resources, such as Minikube. If you do want to specify resources, uncomment the following
33 | # lines, adjust them as necessary, and remove the curly braces after 'resources:'.
34 | # limits:
35 | # cpu: 100m
36 | # memory: 128Mi
37 | # requests:
38 | # cpu: 100m
39 | # memory: 128Mi
40 |
41 | nodeSelector: {}
42 |
43 | tolerations: []
44 |
45 | affinity: {}
46 |
--------------------------------------------------------------------------------
/examples/git-generator-directory/excludes/cluster-addons/prometheus-operator/Chart.yaml:
--------------------------------------------------------------------------------
1 | name: helm-prometheus-operator
2 |
--------------------------------------------------------------------------------
/examples/git-generator-directory/excludes/cluster-addons/prometheus-operator/requirements.yaml:
--------------------------------------------------------------------------------
1 | dependencies:
2 | - name: kube-prometheus-stack
3 | version: 9.4.10
4 | repository: https://prometheus-community.github.io/helm-charts
5 |
--------------------------------------------------------------------------------
/examples/git-generator-directory/excludes/cluster-addons/prometheus-operator/values.yaml:
--------------------------------------------------------------------------------
1 | # Blank values.yaml
2 |
--------------------------------------------------------------------------------
/examples/git-generator-directory/excludes/git-directories-exclude-example.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: argoproj.io/v1alpha1
2 | kind: ApplicationSet
3 | metadata:
4 | name: cluster-addons
5 | spec:
6 | generators:
7 | - git:
8 | repoURL: https://github.com/argoproj/applicationset.git
9 | revision: HEAD
10 | directories:
11 | - path: examples/git-generator-directory/excludes/cluster-addons/*
12 | - exclude: true
13 | path: examples/git-generator-directory/excludes/cluster-addons/exclude-helm-guestbook
14 | template:
15 | metadata:
16 | name: '{{path.basename}}'
17 | spec:
18 | project: default
19 | source:
20 | repoURL: https://github.com/argoproj/applicationset.git
21 | targetRevision: HEAD
22 | path: '{{path}}'
23 | destination:
24 | server: https://kubernetes.default.svc
25 | namespace: '{{path.basename}}'
26 |
--------------------------------------------------------------------------------
/examples/git-generator-directory/git-directories-example.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: argoproj.io/v1alpha1
2 | kind: ApplicationSet
3 | metadata:
4 | name: cluster-addons
5 | spec:
6 | generators:
7 | - git:
8 | repoURL: https://github.com/argoproj/applicationset.git
9 | revision: HEAD
10 | directories:
11 | - path: examples/git-generator-directory/cluster-addons/*
12 | template:
13 | metadata:
14 | name: '{{path.basename}}'
15 | spec:
16 | project: default
17 | source:
18 | repoURL: https://github.com/argoproj/applicationset.git
19 | targetRevision: HEAD
20 | path: '{{path}}'
21 | destination:
22 | server: https://kubernetes.default.svc
23 | namespace: '{{path.basename}}'
24 |
--------------------------------------------------------------------------------
/examples/git-generator-files-discovery/apps/guestbook/guestbook-ui-deployment.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: Deployment
3 | metadata:
4 | name: guestbook-ui
5 | spec:
6 | replicas: 1
7 | revisionHistoryLimit: 3
8 | selector:
9 | matchLabels:
10 | app: guestbook-ui
11 | template:
12 | metadata:
13 | labels:
14 | app: guestbook-ui
15 | spec:
16 | containers:
17 | - image: gcr.io/heptio-images/ks-guestbook-demo:0.2
18 | name: guestbook-ui
19 | ports:
20 | - containerPort: 80
21 |
--------------------------------------------------------------------------------
/examples/git-generator-files-discovery/apps/guestbook/guestbook-ui-svc.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: Service
3 | metadata:
4 | name: guestbook-ui
5 | spec:
6 | ports:
7 | - port: 80
8 | targetPort: 80
9 | selector:
10 | app: guestbook-ui
11 |
--------------------------------------------------------------------------------
/examples/git-generator-files-discovery/apps/guestbook/kustomization.yaml:
--------------------------------------------------------------------------------
1 | namePrefix: kustomize-
2 |
3 | resources:
4 | - guestbook-ui-deployment.yaml
5 | - guestbook-ui-svc.yaml
6 | apiVersion: kustomize.config.k8s.io/v1beta1
7 | kind: Kustomization
8 |
--------------------------------------------------------------------------------
/examples/git-generator-files-discovery/cluster-config/engineering/dev/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "aws_account": "123456",
3 | "asset_id": "11223344",
4 | "cluster": {
5 | "owner": "cluster-admin@company.com",
6 | "name": "engineering-dev",
7 | "address": "http://1.2.3.4"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/examples/git-generator-files-discovery/cluster-config/engineering/prod/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "aws_account": "123456",
3 | "asset_id": "11223344",
4 | "cluster": {
5 | "owner": "cluster-admin@company.com",
6 | "name": "engineering-prod",
7 | "address": "http://1.2.3.4"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/examples/git-generator-files-discovery/git-generator-files.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: argoproj.io/v1alpha1
2 | kind: ApplicationSet
3 | metadata:
4 | name: guestbook
5 | spec:
6 | generators:
7 | - git:
8 | repoURL: https://github.com/argoproj/applicationset.git
9 | revision: HEAD
10 | files:
11 | - path: "examples/git-generator-files-discovery/cluster-config/**/config.json"
12 | template:
13 | metadata:
14 | name: '{{cluster.name}}-guestbook'
15 | spec:
16 | project: default
17 | source:
18 | repoURL: https://github.com/argoproj/applicationset.git
19 | targetRevision: HEAD
20 | path: "examples/git-generator-files-discovery/apps/guestbook"
21 | destination:
22 | server: https://kubernetes.default.svc
23 | #server: '{{cluster.address}}'
24 | namespace: guestbook
25 |
--------------------------------------------------------------------------------
/examples/list-generator/guestbook/engineering-dev/guestbook-ui-deployment.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: Deployment
3 | metadata:
4 | name: guestbook-ui
5 | spec:
6 | replicas: 1
7 | revisionHistoryLimit: 3
8 | selector:
9 | matchLabels:
10 | app: guestbook-ui
11 | template:
12 | metadata:
13 | labels:
14 | app: guestbook-ui
15 | spec:
16 | containers:
17 | - image: gcr.io/heptio-images/ks-guestbook-demo:0.2
18 | name: guestbook-ui
19 | ports:
20 | - containerPort: 80
21 |
--------------------------------------------------------------------------------
/examples/list-generator/guestbook/engineering-dev/guestbook-ui-svc.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: Service
3 | metadata:
4 | name: guestbook-ui
5 | spec:
6 | ports:
7 | - port: 80
8 | targetPort: 80
9 | selector:
10 | app: guestbook-ui
11 |
--------------------------------------------------------------------------------
/examples/list-generator/guestbook/engineering-prod/guestbook-ui-deployment.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: Deployment
3 | metadata:
4 | name: guestbook-ui
5 | spec:
6 | replicas: 1
7 | revisionHistoryLimit: 3
8 | selector:
9 | matchLabels:
10 | app: guestbook-ui
11 | template:
12 | metadata:
13 | labels:
14 | app: guestbook-ui
15 | spec:
16 | containers:
17 | - image: gcr.io/heptio-images/ks-guestbook-demo:0.2
18 | name: guestbook-ui
19 | ports:
20 | - containerPort: 80
21 |
--------------------------------------------------------------------------------
/examples/list-generator/guestbook/engineering-prod/guestbook-ui-svc.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: Service
3 | metadata:
4 | name: guestbook-ui
5 | spec:
6 | ports:
7 | - port: 80
8 | targetPort: 80
9 | selector:
10 | app: guestbook-ui
11 |
--------------------------------------------------------------------------------
/examples/list-generator/list-example.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: argoproj.io/v1alpha1
2 | kind: ApplicationSet
3 | metadata:
4 | name: guestbook
5 | spec:
6 | generators:
7 | - list:
8 | elements:
9 | - cluster: engineering-dev
10 | url: https://kubernetes.default.svc
11 | - cluster: engineering-prod
12 | url: https://kubernetes.default.svc
13 | template:
14 | metadata:
15 | name: '{{cluster}}-guestbook'
16 | spec:
17 | project: default
18 | source:
19 | repoURL: https://github.com/argoproj/applicationset.git
20 | targetRevision: HEAD
21 | path: examples/list-generator/guestbook/{{cluster}}
22 | destination:
23 | server: '{{url}}'
24 | namespace: guestbook
25 |
--------------------------------------------------------------------------------
/examples/matrix/cluster-addons/argo-workflows/kustomization.yaml:
--------------------------------------------------------------------------------
1 | #namePrefix: kustomize-
2 |
3 | resources:
4 | - namespace-install.yaml
5 | apiVersion: kustomize.config.k8s.io/v1beta1
6 | kind: Kustomization
7 |
--------------------------------------------------------------------------------
/examples/matrix/cluster-addons/prometheus-operator/Chart.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v2
2 | name: helm-prometheus-operator
3 |
4 | type: application
5 |
6 | # This is the chart version. This version number should be incremented each time you make changes
7 | # to the chart and its templates, including the app version.
8 | # Versions are expected to follow Semantic Versioning (https://semver.org/)
9 | version: 0.1.0
10 |
11 | # This is the version number of the application being deployed. This version number should be
12 | # incremented each time you make changes to the application. Versions are not expected to
13 | # follow Semantic Versioning. They should reflect the version the application is using.
14 | appVersion: "1.0"
--------------------------------------------------------------------------------
/examples/matrix/cluster-addons/prometheus-operator/requirements.yaml:
--------------------------------------------------------------------------------
1 | dependencies:
2 | - name: kube-prometheus-stack
3 | version: 9.4.10
4 | repository: https://prometheus-community.github.io/helm-charts
5 |
--------------------------------------------------------------------------------
/examples/matrix/cluster-addons/prometheus-operator/values.yaml:
--------------------------------------------------------------------------------
1 | # Blank values.yaml
2 |
--------------------------------------------------------------------------------
/examples/matrix/cluster-and-git.yaml:
--------------------------------------------------------------------------------
1 | # This example demonstrates the combining of the git generator with a cluster generator
2 | # The expected output would be an application per git directory and a cluster (application_count = git directory * clusters)
3 | #
4 | #
5 | apiVersion: argoproj.io/v1alpha1
6 | kind: ApplicationSet
7 | metadata:
8 | name: cluster-git
9 | spec:
10 | generators:
11 | - matrix:
12 | generators:
13 | - git:
14 | repoURL: https://github.com/argoproj/applicationset.git
15 | revision: HEAD
16 | directories:
17 | - path: examples/matrix/cluster-addons/*
18 | - clusters:
19 | selector:
20 | matchLabels:
21 | argocd.argoproj.io/secret-type: cluster
22 | template:
23 | metadata:
24 | name: '{{path.basename}}-{{name}}'
25 | spec:
26 | project: '{{metadata.labels.environment}}'
27 | source:
28 | repoURL: https://github.com/argoproj/applicationset.git
29 | targetRevision: HEAD
30 | path: '{{path}}'
31 | destination:
32 | server: '{{server}}'
33 | namespace: '{{path.basename}}'
34 |
--------------------------------------------------------------------------------
/examples/matrix/list-and-git.yaml:
--------------------------------------------------------------------------------
1 | # This example demonstrates the combining of the git generator with a list generator
2 | # The expected output would be an application per git directory and a list entry (application_count = git directory * list entries)
3 | #
4 | #
5 | apiVersion: argoproj.io/v1alpha1
6 | kind: ApplicationSet
7 | metadata:
8 | name: list-git
9 | spec:
10 | generators:
11 | - matrix:
12 | generators:
13 | - git:
14 | repoURL: https://github.com/argoproj/applicationset.git
15 | revision: HEAD
16 | directories:
17 | - path: examples/matrix/cluster-addons/*
18 | - list:
19 | elements:
20 | - cluster: engineering-dev
21 | url: https://1.2.3.4
22 | values:
23 | project: dev
24 | - cluster: engineering-prod
25 | url: https://2.4.6.8
26 | values:
27 | project: prod
28 | template:
29 | metadata:
30 | name: '{{path.basename}}-{{cluster}}'
31 | spec:
32 | project: '{{values.project}}'
33 | source:
34 | repoURL: https://github.com/argoproj/applicationset.git
35 | targetRevision: HEAD
36 | path: '{{path}}'
37 | destination:
38 | server: '{{url}}'
39 | namespace: '{{path.basename}}'
40 |
--------------------------------------------------------------------------------
/examples/matrix/list-and-list.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: argoproj.io/v1alpha1
2 | kind: ApplicationSet
3 | metadata:
4 | name: list-and-list
5 | namespace: argocd
6 | spec:
7 | generators:
8 | - matrix:
9 | generators:
10 | - list:
11 | elements:
12 | - cluster: engineering-dev
13 | url: https://kubernetes.default.svc
14 | values:
15 | project: default
16 | - cluster: engineering-prod
17 | url: https://kubernetes.default.svc
18 | values:
19 | project: default
20 | - list:
21 | elements:
22 | - values:
23 | suffix: '1'
24 | - values:
25 | suffix: '2'
26 | template:
27 | metadata:
28 | name: '{{cluster}}-{{values.suffix}}'
29 | spec:
30 | project: '{{values.project}}'
31 | source:
32 | repoURL: https://github.com/argoproj/applicationset.git
33 | targetRevision: HEAD
34 | path: '{{path}}'
35 | destination:
36 | server: '{{url}}'
37 | namespace: '{{path.basename}}'
38 |
--------------------------------------------------------------------------------
/examples/matrix/matrix-and-union-in-matrix.yaml:
--------------------------------------------------------------------------------
1 | # The matrix generator can contain other combination-type generators (matrix and union). But nested matrix and union
2 | # generators cannot contain further-nested matrix or union generators.
3 | #
4 | # The generators are evaluated from most-nested to least-nested. In this case:
5 | # 1. The union generator joins two lists to make 3 parameter sets.
6 | # 2. The inner matrix generator takes the cartesian product of the two lists to make 4 parameters sets.
7 | # 3. The outer matrix generator takes the cartesian product of the 3 union and the 4 inner matrix parameter sets to
8 | # make 3*4=12 final parameter sets.
9 | # 4. The 12 final parameter sets are evaluated against the top-level template to generate 12 Applications.
10 | apiVersion: argoproj.io/v1alpha1
11 | kind: ApplicationSet
12 | metadata:
13 | name: matrix-and-union-in-matrix
14 | spec:
15 | generators:
16 | - matrix:
17 | generators:
18 | - union:
19 | mergeKeys:
20 | - cluster
21 | generators:
22 | - list:
23 | elements:
24 | - cluster: engineering-dev
25 | url: https://kubernetes.default.svc
26 | values:
27 | project: default
28 | - cluster: engineering-prod
29 | url: https://kubernetes.default.svc
30 | values:
31 | project: default
32 | - list:
33 | elements:
34 | - cluster: engineering-dev
35 | url: https://kubernetes.default.svc
36 | values:
37 | project: default
38 | - cluster: engineering-test
39 | url: https://kubernetes.default.svc
40 | values:
41 | project: default
42 | - matrix:
43 | generators:
44 | - list:
45 | elements:
46 | - values:
47 | suffix: '1'
48 | - values:
49 | suffix: '2'
50 | - list:
51 | elements:
52 | - values:
53 | prefix: 'first'
54 | - values:
55 | prefix: 'second'
56 | template:
57 | metadata:
58 | name: '{{values.prefix}}-{{cluster}}-{{values.suffix}}'
59 | spec:
60 | project: '{{values.project}}'
61 | source:
62 | repoURL: https://github.com/argoproj/applicationset.git
63 | targetRevision: HEAD
64 | path: '{{path}}'
65 | destination:
66 | server: '{{url}}'
67 | namespace: '{{path.basename}}'
68 |
--------------------------------------------------------------------------------
/examples/merge/merge-clusters-and-list.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: argoproj.io/v1alpha1
2 | kind: ApplicationSet
3 | metadata:
4 | name: merge-clusters-and-list
5 | spec:
6 | generators:
7 | - merge:
8 | mergeKeys:
9 | - server
10 | generators:
11 | - clusters:
12 | values:
13 | kafka: 'true'
14 | redis: 'false'
15 | # For clusters with a specific label, enable Kafka.
16 | - clusters:
17 | selector:
18 | matchLabels:
19 | use-kafka: 'false'
20 | values:
21 | kafka: 'false'
22 | # For a specific cluster, enable Redis.
23 | - list:
24 | elements:
25 | - server: https://some-specific-cluster
26 | values.redis: 'true'
27 | template:
28 | metadata:
29 | name: '{{name}}'
30 | spec:
31 | project: default
32 | source:
33 | repoURL: https://github.com/argoproj/argocd-example-apps/
34 | targetRevision: HEAD
35 | path: helm-guestbook
36 | helm:
37 | parameters:
38 | - name: kafka
39 | value: '{{values.kafka}}'
40 | - name: redis
41 | value: '{{values.redis}}'
42 | destination:
43 | server: '{{server}}'
44 | namespace: default
45 |
--------------------------------------------------------------------------------
/examples/merge/merge-two-matrixes.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: argoproj.io/v1alpha1
2 | kind: ApplicationSet
3 | metadata:
4 | name: merge-two-matrixes
5 | spec:
6 | generators:
7 | - merge:
8 | mergeKeys:
9 | - server
10 | - environment
11 | generators:
12 | - matrix:
13 | generators:
14 | - clusters:
15 | values:
16 | replicaCount: '2'
17 | - list:
18 | elements:
19 | - environment: staging
20 | namespace: guestbook-non-prod
21 | - environment: prod
22 | namespace: guestbook
23 | - list:
24 | elements:
25 | - server: https://kubernetes.default.svc
26 | environment: staging
27 | values.replicaCount: '1'
28 | template:
29 | metadata:
30 | name: '{{name}}-guestbook-{{environment}}'
31 | spec:
32 | project: default
33 | source:
34 | repoURL: https://github.com/argoproj/argocd-example-apps/
35 | targetRevision: HEAD
36 | path: helm-guestbook
37 | helm:
38 | parameters:
39 | - name: replicaCount
40 | value: '{{values.replicaCount}}'
41 | destination:
42 | server: '{{server}}'
43 | namespace: '{{namespace}}'
44 |
--------------------------------------------------------------------------------
/examples/pull-request-generator/pull-request-example.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: argoproj.io/v1alpha1
2 | kind: ApplicationSet
3 | metadata:
4 | name: myapp
5 | spec:
6 | generators:
7 | - pullRequest:
8 | github:
9 | # The GitHub organization or user.
10 | owner: myorg
11 | # The Github repository
12 | repo: myrepo
13 | # For GitHub Enterprise. (optional)
14 | api: https://git.example.com/
15 | # Reference to a Secret containing an access token. (optional)
16 | tokenRef:
17 | secretName: github-token
18 | key: token
19 | # Labels is used to filter the PRs that you want to target. (optional)
20 | labels:
21 | - preview
22 | template:
23 | metadata:
24 | name: 'myapp-{{ branch }}-{{ number }}'
25 | spec:
26 | source:
27 | repoURL: 'https://github.com/myorg/myrepo.git'
28 | targetRevision: '{{ head_sha }}'
29 | path: helm-guestbook
30 | helm:
31 | parameters:
32 | - name: "image.tag"
33 | value: "pull-{{ head_sha }}"
34 | project: default
35 | destination:
36 | server: https://kubernetes.default.svc
37 | namespace: "{{ branch }}-{{ number }}"
38 | syncPolicy:
39 | syncOptions:
40 | - CreateNamespace=true
41 |
--------------------------------------------------------------------------------
/examples/scm-provider-generator/scm-provider-example.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: argoproj.io/v1alpha1
2 | kind: ApplicationSet
3 | metadata:
4 | name: guestbook
5 | spec:
6 | generators:
7 | - scmProvider:
8 | github:
9 | organization: argoproj
10 | cloneProtocol: https
11 | filters:
12 | - repositoryMatch: example-apps
13 | template:
14 | metadata:
15 | name: '{{ repository }}-guestbook'
16 | spec:
17 | project: "default"
18 | source:
19 | repoURL: '{{ url }}'
20 | targetRevision: '{{ branch }}'
21 | path: guestbook
22 | destination:
23 | server: https://kubernetes.default.svc
24 | namespace: guestbook
25 |
--------------------------------------------------------------------------------
/examples/template-override/default/guestbook-ui-deployment.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: Deployment
3 | metadata:
4 | name: guestbook-ui
5 | spec:
6 | replicas: 1
7 | revisionHistoryLimit: 3
8 | selector:
9 | matchLabels:
10 | app: guestbook-ui
11 | template:
12 | metadata:
13 | labels:
14 | app: guestbook-ui
15 | spec:
16 | containers:
17 | - image: gcr.io/heptio-images/ks-guestbook-demo:0.2
18 | name: guestbook-ui
19 | ports:
20 | - containerPort: 80
21 |
--------------------------------------------------------------------------------
/examples/template-override/default/guestbook-ui-svc.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: Service
3 | metadata:
4 | name: guestbook-ui
5 | spec:
6 | ports:
7 | - port: 80
8 | targetPort: 80
9 | selector:
10 | app: guestbook-ui
11 |
--------------------------------------------------------------------------------
/examples/template-override/default/kustomization.yaml:
--------------------------------------------------------------------------------
1 | namePrefix: kustomize-
2 |
3 | resources:
4 | - guestbook-ui-deployment.yaml
5 | - guestbook-ui-svc.yaml
6 | apiVersion: kustomize.config.k8s.io/v1beta1
7 | kind: Kustomization
8 |
--------------------------------------------------------------------------------
/examples/template-override/engineering-dev-override/guestbook-ui-deployment.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: Deployment
3 | metadata:
4 | name: guestbook-ui
5 | spec:
6 | replicas: 1
7 | revisionHistoryLimit: 3
8 | selector:
9 | matchLabels:
10 | app: guestbook-ui
11 | template:
12 | metadata:
13 | labels:
14 | app: guestbook-ui
15 | spec:
16 | containers:
17 | - image: gcr.io/heptio-images/ks-guestbook-demo:0.2
18 | name: guestbook-ui
19 | ports:
20 | - containerPort: 80
21 |
--------------------------------------------------------------------------------
/examples/template-override/engineering-dev-override/guestbook-ui-svc.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: Service
3 | metadata:
4 | name: guestbook-ui
5 | spec:
6 | ports:
7 | - port: 80
8 | targetPort: 80
9 | selector:
10 | app: guestbook-ui
11 |
--------------------------------------------------------------------------------
/examples/template-override/engineering-dev-override/kustomization.yaml:
--------------------------------------------------------------------------------
1 | namePrefix: kustomize-
2 |
3 | resources:
4 | - guestbook-ui-deployment.yaml
5 | - guestbook-ui-svc.yaml
6 | apiVersion: kustomize.config.k8s.io/v1beta1
7 | kind: Kustomization
8 |
--------------------------------------------------------------------------------
/examples/template-override/template-overrides-example.yaml:
--------------------------------------------------------------------------------
1 | # App templates can also be defined as part of the generator's template stanza. Sometimes it is
2 | # useful to do this in order to override the spec.template stanza, and when simple string
3 | # parameterization are insufficient. In the below examples, the generators[].XXX.template is
4 | # a partial definition, which overrides/patch the default template.
5 | apiVersion: argoproj.io/v1alpha1
6 | kind: ApplicationSet
7 | metadata:
8 | name: guestbook
9 | spec:
10 | generators:
11 | - list:
12 | elements:
13 | - cluster: engineering-dev
14 | url: https://kubernetes.default.svc
15 | template:
16 | metadata: {}
17 | spec:
18 | project: "default"
19 | source:
20 | targetRevision: HEAD
21 | repoURL: https://github.com/argoproj/applicationset.git
22 | path: 'examples/template-override/{{cluster}}-override'
23 | destination: {}
24 |
25 | template:
26 | metadata:
27 | name: '{{cluster}}-guestbook'
28 | spec:
29 | project: "default"
30 | source:
31 | repoURL: https://github.com/argoproj/applicationset.git
32 | targetRevision: HEAD
33 | path: examples/template-override/default
34 | destination:
35 | server: '{{url}}'
36 | namespace: guestbook
37 |
--------------------------------------------------------------------------------
/hack/boilerplate.go.txt:
--------------------------------------------------------------------------------
1 | /*
2 | Licensed under the Apache License, Version 2.0 (the "License");
3 | you may not use this file except in compliance with the License.
4 | You may obtain a copy of the License at
5 |
6 | http://www.apache.org/licenses/LICENSE-2.0
7 |
8 | Unless required by applicable law or agreed to in writing, software
9 | distributed under the License is distributed on an "AS IS" BASIS,
10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 | See the License for the specific language governing permissions and
12 | limitations under the License.
13 | */
--------------------------------------------------------------------------------
/hack/ci-e2e-run.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | if [ "$ARGOCD_IN_CI" != "true" ] ; then
4 | echo "This script should only be run from GitHub actions."
5 | exit 1
6 | fi
7 |
8 | set -e
9 |
10 | # Add ~/go/bin to PATH --------------------------------------------------------
11 | # Add /usr/local/bin to PATH
12 |
13 | export PATH=/home/runner/go/bin:/usr/local/bin:$PATH
14 |
15 |
16 | # Run E2E test suite ----------------------------------------------------------
17 | set -x
18 | cd "$GITHUB_WORKSPACE/applicationset"
19 | kubectl apply -f manifests/crds/argoproj.io_applicationsets.yaml
20 | make build
21 | make start-e2e 2>&1 | tee /tmp/appset-e2e-server.log &
22 | # Uncomment this to see the Argo CD output alongside test output:
23 | # tail -f /tmp/e2e-server.log &
24 | make test-e2e
25 |
--------------------------------------------------------------------------------
/hack/ci-e2e-setup.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | if [ "$ARGOCD_IN_CI" != "true" ] ; then
4 | echo "This script should only be run from GitHub actions."
5 | exit 1
6 | fi
7 |
8 | set -e
9 |
10 | # GH actions workaround - Kill XSP4 process -----------------------------------
11 |
12 |
13 | # Add same workaround for port 8084 as argoproj/argo-cd #5658
14 | sudo pkill mono || true
15 |
16 | # Install K3S -----------------------------------------------------------------
17 | echo "Installing k3s"
18 | set -x
19 | curl -sfL https://get.k3s.io | sh -
20 | sudo chmod -R a+rw /etc/rancher/k3s
21 | sudo mkdir -p $HOME/.kube && sudo chown -R runner $HOME/.kube
22 | sudo k3s kubectl config view --raw > $HOME/.kube/config
23 | sudo chown runner $HOME/.kube/config
24 | kubectl version
25 | set +x
26 |
27 | # Add ~/go/bin to PATH --------------------------------------------------------
28 | # Add /usr/local/bin to PATH
29 |
30 | export PATH=/home/runner/go/bin:/usr/local/bin:$PATH
31 |
32 | # Download Go dependencies ----------------------------------------------------
33 | go mod download
34 | go get github.com/mattn/goreman
35 |
36 | # Install all tools required for building & testing ---------------------------
37 |
38 | cd "$GITHUB_WORKSPACE/argo-cd"
39 |
40 | echo "Install all tools required for building & testing"
41 | make install-test-tools-local
42 |
43 |
44 | # Setup git username and email ------------------------------------------------
45 | git config --global user.name "John Doe"
46 | git config --global user.email "john.doe@example.com"
47 |
48 | # Pull Docker image required for tests ----------------------------------------
49 | docker pull quay.io/dexidp/dex:v2.25.0
50 | docker pull argoproj/argo-cd-ci-builder:v1.0.0
51 | docker pull redis:6.2.4-alpine
52 |
53 | # Create target directory for binaries in the build-process -------------------
54 | mkdir -p dist
55 | chown runner dist
56 |
57 |
58 | # Run Argo CD E2E server and wait for it being available ----------------------
59 |
60 | echo "Run Argo CD E2E server and wait for it being available"
61 |
62 | set -x
63 | # Something is weird in GH runners -- there's a phantom listener for
64 | # port 8080 which is not visible in netstat -tulpen, but still there
65 | # with a HTTP listener. We have API server listening on port 8088
66 | # instead.
67 | make start-e2e-local 2>&1 | sed -r "s/[[:cntrl:]]\[[0-9]{1,3}m//g" > /tmp/e2e-server.log &
68 | count=1
69 | until curl -f http://127.0.0.1:8088/healthz; do
70 | sleep 10;
71 | if test $count -ge 180; then
72 | echo "Timeout"
73 | exit 1
74 | fi
75 | count=$((count+1))
76 | done
77 |
--------------------------------------------------------------------------------
/hack/from-argo-cd/git-ask-pass.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | # This script is used as the command supplied to GIT_ASKPASS as a way to supply username/password
3 | # credentials to git, without having to use git credentials helpers, or having on-disk config.
4 | case "$1" in
5 | Username*) echo "${GIT_USERNAME}" ;;
6 | Password*) echo "${GIT_PASSWORD}" ;;
7 | esac
--------------------------------------------------------------------------------
/hack/from-argo-cd/git-verify-wrapper.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | # Wrapper script to perform GPG signature validation on git commit SHAs and
3 | # annotated tags.
4 | #
5 | # We capture stderr to stdout, so we can have the output in the logs. Also,
6 | # we ignore error codes that are emitted if signature verification failed.
7 | #
8 | if test "$1" = ""; then
9 | echo "Wrong usage of git-verify-wrapper.sh" >&2
10 | exit 1
11 | fi
12 |
13 | REVISION="$1"
14 | TYPE=
15 |
16 | # Figure out we have an annotated tag or a commit SHA
17 | if git describe --exact-match "${REVISION}" >/dev/null 2>&1; then
18 | IFS=''
19 | TYPE=tag
20 | OUTPUT=$(git verify-tag "$REVISION" 2>&1)
21 | RET=$?
22 | else
23 | IFS=''
24 | TYPE=commit
25 | OUTPUT=$(git verify-commit "$REVISION" 2>&1)
26 | RET=$?
27 | fi
28 |
29 | case "$RET" in
30 | 0)
31 | echo "$OUTPUT"
32 | ;;
33 | 1)
34 | # git verify-tag emits error messages if no signature is found on tag,
35 | # which we don't want in the output.
36 | if test "$TYPE" = "tag" -a "${OUTPUT%%:*}" = "error"; then
37 | OUTPUT=""
38 | fi
39 | echo "$OUTPUT"
40 | RET=0
41 | ;;
42 | *)
43 | echo "$OUTPUT" >&2
44 | ;;
45 | esac
46 | exit $RET
47 |
--------------------------------------------------------------------------------
/hack/from-argo-cd/gpg-wrapper.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | # Simple wrapper around gpg to prevent exit code != 0
3 | ARGS=$*
4 | OUTPUT=$(gpg $ARGS 2>&1)
5 | IFS=''
6 | RET=$?
7 | case "$RET" in
8 | 0)
9 | echo $OUTPUT
10 | ;;
11 | 1)
12 | echo $OUTPUT
13 | RET=0
14 | ;;
15 | *)
16 | echo $OUTPUT >&2
17 | ;;
18 | esac
19 | exit $RET
20 |
--------------------------------------------------------------------------------
/hack/generate-manifests.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | set -eo pipefail
4 | set -x
5 |
6 | SRCROOT="$( CDPATH='' cd -- "$(dirname "$0")/.." && pwd -P )"
7 |
8 | AUTOGENMSG="# This is an auto-generated file. DO NOT EDIT"
9 |
10 |
11 |
12 | KUSTOMIZE=${KUSTOMIZE:-kustomize}
13 |
14 | TEMPFILE=$(mktemp /tmp/appset-manifests.XXXXXX)
15 |
16 | if [ "$CONTAINER_REGISTRY" != "" ]; then
17 | CONTAINER_REGISTRY="${CONTAINER_REGISTRY}/"
18 | fi
19 |
20 | IMAGE_NAME="${IMAGE_NAME:-argocd-applicationset}"
21 | IMAGE_NAMESPACE="${IMAGE_NAMESPACE:-argoproj}"
22 | IMAGE_TAG="${IMAGE_TAG:-}"
23 |
24 | # if the tag has not been declared, and we are on a release branch, use the VERSION file.
25 | if [ "$IMAGE_TAG" = "" ]; then
26 | branch=$(git rev-parse --abbrev-ref HEAD || true)
27 | if [[ $branch = release-* ]]; then
28 | pwd
29 | IMAGE_TAG=v$(cat $SRCROOT/VERSION)
30 | fi
31 | fi
32 | # otherwise, use latest
33 | if [ "$IMAGE_TAG" = "" ]; then
34 | IMAGE_TAG=latest
35 | fi
36 |
37 | cd ${SRCROOT}/manifests/base && ${KUSTOMIZE} edit set image quay.io/argoproj/argocd-applicationset=${CONTAINER_REGISTRY}${IMAGE_NAMESPACE}/$IMAGE_NAME:${IMAGE_TAG}
38 |
39 | # Use kustomize to render 'manifests/install.yaml'
40 | echo "${AUTOGENMSG}" > ${TEMPFILE}
41 | cd ${SRCROOT}/manifests/namespace-install && ${KUSTOMIZE} build . >> ${TEMPFILE}
42 | mv ${TEMPFILE} ${SRCROOT}/manifests/install.yaml
43 | cd ${SRCROOT} && chmod 644 manifests/install.yaml
44 |
45 | # Verify that the GitHub Actions is targeting the expected Argo CD version
46 | "${SRCROOT}/hack/verify-argo-cd-versions.sh"
47 |
--------------------------------------------------------------------------------
/hack/release.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Simple script to do a release
4 |
5 | TARGET_REMOTE=upstream
6 | TARGET_VERSION="$1"
7 | set -eu
8 | set -o pipefail
9 |
10 | if test "${TARGET_VERSION}" = ""; then
11 | echo "USAGE: $0 " >&2
12 | exit 1
13 | fi
14 |
15 | TARGET_TAG="v${TARGET_VERSION}"
16 |
17 | if ! echo "${TARGET_VERSION}" | egrep -q '^[0-9]\.[0-9]\.[0-9]$'; then
18 | echo "Error: Target version '${TARGET_VERSION}' is not well-formed. Must be X.Y.Z" >&2
19 | exit 1
20 | fi
21 |
22 | echo "*** checking for current git branch"
23 | RELEASE_BRANCH=$(git rev-parse --abbrev-ref HEAD || true)
24 | if [[ $RELEASE_BRANCH = release-* ]]; then
25 | echo "*** branch is $RELEASE_BRANCH"
26 | IMAGE_TAG=${TARGET_TAG}
27 | else
28 | echo "Error: Branch $RELEASE_BRANCH is not release branch" >&2
29 | exit 1
30 | fi
31 |
32 | if ! test -f VERSION; then
33 | echo "Error: You should be in repository root." >&2
34 | exit 1
35 | fi
36 |
37 | echo "${TARGET_VERSION}" > VERSION
38 |
39 | echo
40 | echo "*** checking for existence of git tag ${TARGET_TAG}"
41 | if git tag -l "${TARGET_VERSION}" | grep -q "${TARGET_TAG}"; then
42 | echo "Error: Tag with version ${TARGET_TAG} already exists." >&2
43 | exit 1
44 | fi
45 |
46 | echo
47 | echo "*** generating new manifests"
48 | export IMAGE_TAG="${TARGET_TAG}"
49 | make manifests
50 |
51 | echo
52 | echo "*** performing release commit"
53 | git commit -s -m "Release ${TARGET_TAG}" VERSION manifests/ docs/ .github hack/ go.mod go.sum CHANGELOG.md examples/
54 | git tag ${TARGET_TAG}
55 |
56 | echo
57 | echo "*** build docker image"
58 | make image
59 |
60 |
61 | # Include optional parameters in the command output below
62 | set +u
63 | CONTAINER_REGISTRY_OUTPUT=""
64 | if test "${CONTAINER_REGISTRY}" != ""; then
65 | CONTAINER_REGISTRY_OUTPUT="CONTAINER_REGISTRY='$CONTAINER_REGISTRY'"
66 | fi
67 |
68 | IMAGE_NAMESPACE_OUTPUT=""
69 | if test "${IMAGE_NAMESPACE}" != ""; then
70 | IMAGE_NAMESPACE_OUTPUT="IMAGE_NAMESPACE='$IMAGE_NAMESPACE'"
71 | fi
72 | set -u
73 |
74 | echo "*** done"
75 | echo
76 | echo "If everything is fine, push changes to GitHub and the destination container registry:"
77 | echo
78 | echo " git push ${TARGET_REMOTE} $RELEASE_BRANCH ${TARGET_TAG}"
79 | echo " make ${CONTAINER_REGISTRY_OUTPUT} ${IMAGE_NAMESPACE_OUTPUT} IMAGE_TAG='${TARGET_TAG}' image-push"
80 | echo
81 | echo "Then, create release tag"
82 |
--------------------------------------------------------------------------------
/hack/set-docs-redirects.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # ApplicationSet docs now live at https://argo-cd.readthedocs.io/en/latest/operator-manual/applicationset/
4 | # This script adds redirects to the top of each ApplicationSet doc to redirect to the new location.
5 |
6 | set -e pipefail
7 |
8 | new_docs_base_path="https://argo-cd.readthedocs.io/en/latest/operator-manual/applicationset/"
9 | new_docs_base_path_regex=$(echo "$new_docs_base_path" | sed 's/\//\\\//g')
10 |
11 | # Loop over files in the docs directory recursively. For each file, use sed to add the following redirect to the top:
12 | #
13 | # FILE_PATH should be the path to the file relative to the docs directory, stripped of the .md extension.
14 |
15 | files=$(find docs -type f -name '*.md')
16 | for file in $files; do
17 | file_path=$(echo "$file" | sed 's/^docs\///' | sed 's/\.md$/\//')
18 | echo "Adding redirect to $file_path"
19 | # If a redirect is already present at the top of the file, remove it.
20 | sed '1s/ "$file.tmp"
21 | mv "$file.tmp" "$file"
22 |
23 | # Add the new redirect.
24 | # Default to an empty path.
25 | file_path_plain=""
26 | file_path_regex=""
27 | if curl -s -o /dev/null -w "%{http_code}" "$new_docs_base_path$file_path" | grep -q 200; then
28 | # If the destination path exists, use it.
29 | file_path_plain="$file_path/"
30 | file_path_regex=$(echo "$file_path" | sed 's/\//\\\//g')
31 | else
32 | echo "WARNING: $new_docs_base_path$file_path does not exist. Using empty path."
33 | fi
34 |
35 | notice="!!! important \"This page has moved\"\n This page has moved to [$new_docs_base_path$file_path_plain]($new_docs_base_path$file_path_plain). Redirecting to the new page.\n"
36 |
37 | notice_regex=$(echo "$notice" | sed 's/\//\\\//g')
38 |
39 | sed "1s/^/\\n\\n$notice_regex\\n/" "$file" > "$file.tmp"
40 | mv "$file.tmp" "$file"
41 | done
42 |
--------------------------------------------------------------------------------
/hack/verify-argo-cd-versions.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # To adopt a new version of Argo CD:
4 | # 1) Update this value to the GitHub tag of the target 'argoproj/argo-cd' release (example: 'v1.8.4').
5 | # 2) Fix the errors that are reported below (by editing the version string in the file reported in the error)
6 | TARGET_ARGO_CD_VERSION=v2.3.0
7 |
8 | # Extract the Argo CD repository string from ci-build.yaml, which SHOULD contain the target Argo CD version
9 | VERSION_FROM_CI_BUILD=$( awk '/BEGIN-ARGO-CD-VERSION/,/END-ARGO-CD-VERSION/' .github/workflows/ci-build.yaml )
10 |
11 | if [[ $VERSION_FROM_CI_BUILD != *"$TARGET_ARGO_CD_VERSION"* ]]; then
12 | echo
13 | echo "ERROR: '.github/workflows/ci-build.yaml' does not target the expected Argo CD version: $TARGET_ARGO_CD_VERSION"
14 | echo "- Found: $VERSION_FROM_CI_BUILD"
15 | exit 1
16 | fi
17 |
--------------------------------------------------------------------------------
/manifests/base/deployment.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: Deployment
3 | metadata:
4 | labels:
5 | app.kubernetes.io/name: argocd-applicationset-controller
6 | app.kubernetes.io/part-of: argocd-applicationset
7 | app.kubernetes.io/component: controller
8 | name: argocd-applicationset-controller
9 | spec:
10 | selector:
11 | matchLabels:
12 | app.kubernetes.io/name: argocd-applicationset-controller
13 | template:
14 | metadata:
15 | labels:
16 | app.kubernetes.io/name: argocd-applicationset-controller
17 | spec:
18 | containers:
19 | - command:
20 | - entrypoint.sh
21 | - applicationset-controller
22 | image: quay.io/argoproj/argocd-applicationset:latest
23 | imagePullPolicy: Always
24 | name: argocd-applicationset-controller
25 | ports:
26 | - containerPort: 7000
27 | name: webhook
28 | env:
29 | - name: NAMESPACE
30 | valueFrom:
31 | fieldRef:
32 | fieldPath: metadata.namespace
33 | volumeMounts:
34 | - mountPath: /app/config/ssh
35 | name: ssh-known-hosts
36 | - mountPath: /app/config/tls
37 | name: tls-certs
38 | - mountPath: /app/config/gpg/source
39 | name: gpg-keys
40 | - mountPath: /app/config/gpg/keys
41 | name: gpg-keyring
42 | serviceAccountName: argocd-applicationset-controller
43 | volumes:
44 | - configMap:
45 | name: argocd-ssh-known-hosts-cm
46 | name: ssh-known-hosts
47 | - configMap:
48 | name: argocd-tls-certs-cm
49 | name: tls-certs
50 | - configMap:
51 | name: argocd-gpg-keys-cm
52 | name: gpg-keys
53 | - emptyDir: {}
54 | name: gpg-keyring
55 |
--------------------------------------------------------------------------------
/manifests/base/kustomization.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: kustomize.config.k8s.io/v1beta1
2 | kind: Kustomization
3 |
4 | images:
5 | - name: quay.io/argoproj/argocd-applicationset
6 | newName: quay.io/argoproj/argocd-applicationset
7 | newTag: latest
8 |
9 | resources:
10 | - deployment.yaml
11 | - rbac.yaml
12 | - service.yaml
13 |
--------------------------------------------------------------------------------
/manifests/base/rbac.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | apiVersion: v1
3 | kind: ServiceAccount
4 | metadata:
5 | labels:
6 | app.kubernetes.io/name: argocd-applicationset-controller
7 | app.kubernetes.io/part-of: argocd-applicationset
8 | app.kubernetes.io/component: controller
9 | name: argocd-applicationset-controller
10 |
11 | ---
12 | apiVersion: rbac.authorization.k8s.io/v1
13 | kind: Role
14 | metadata:
15 | labels:
16 | app.kubernetes.io/name: argocd-applicationset-controller
17 | app.kubernetes.io/part-of: argocd-applicationset
18 | app.kubernetes.io/component: controller
19 | name: argocd-applicationset-controller
20 | rules:
21 | - apiGroups:
22 | - argoproj.io
23 | resources:
24 | - applications
25 | - appprojects
26 | - applicationsets
27 | - applicationsets/finalizers
28 | verbs:
29 | - create
30 | - delete
31 | - get
32 | - list
33 | - patch
34 | - update
35 | - watch
36 | - apiGroups:
37 | - argoproj.io
38 | resources:
39 | - applicationsets/status
40 | verbs:
41 | - get
42 | - patch
43 | - update
44 | - apiGroups:
45 | - ''
46 | resources:
47 | - events
48 | verbs:
49 | - create
50 | - get
51 | - list
52 | - patch
53 | - watch
54 | - apiGroups:
55 | - ''
56 | resources:
57 | - secrets
58 | - configmaps
59 | verbs:
60 | - get
61 | - list
62 | - watch
63 | - apiGroups:
64 | - apps
65 | - extensions
66 | resources:
67 | - deployments
68 | verbs:
69 | - get
70 | - list
71 | - watch
72 |
73 | ---
74 | apiVersion: rbac.authorization.k8s.io/v1
75 | kind: RoleBinding
76 | metadata:
77 | labels:
78 | app.kubernetes.io/name: argocd-applicationset-controller
79 | app.kubernetes.io/part-of: argocd-applicationset
80 | app.kubernetes.io/component: controller
81 | name: argocd-applicationset-controller
82 | roleRef:
83 | apiGroup: rbac.authorization.k8s.io
84 | kind: Role
85 | name: argocd-applicationset-controller
86 | subjects:
87 | - kind: ServiceAccount
88 | name: argocd-applicationset-controller
89 |
--------------------------------------------------------------------------------
/manifests/base/service.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: Service
3 | metadata:
4 | labels:
5 | app.kubernetes.io/component: controller
6 | app.kubernetes.io/name: argocd-applicationset-controller
7 | app.kubernetes.io/part-of: argocd-applicationset
8 | name: argocd-applicationset-controller
9 | spec:
10 | ports:
11 | - name: webhook
12 | port: 7000
13 | protocol: TCP
14 | targetPort: webhook
15 | selector:
16 | app.kubernetes.io/name: argocd-applicationset-controller
17 |
--------------------------------------------------------------------------------
/manifests/crds/kustomization.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: kustomize.config.k8s.io/v1beta1
2 | kind: Kustomization
3 |
4 | resources:
5 | - argoproj.io_applicationsets.yaml
6 |
--------------------------------------------------------------------------------
/manifests/namespace-install/kustomization.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: kustomize.config.k8s.io/v1beta1
2 | kind: Kustomization
3 |
4 | resources:
5 | - ../base
6 | - ../crds
7 |
--------------------------------------------------------------------------------
/mkdocs.yml:
--------------------------------------------------------------------------------
1 |
2 | site_name: ApplicationSet Controller
3 | site_url: 'https://argocd-applicationset.readthedocs.io/'
4 | repo_url: https://github.com/argoproj/applicationset
5 | use_directory_urls: true
6 | strict: true
7 | theme:
8 | name: material
9 | palette:
10 | primary: teal
11 | font:
12 | text: 'Work Sans'
13 | logo: 'assets/logo.png'
14 | extra_javascript:
15 | - js/jquery-2.1.1.min.js
16 | - js/modernizr-2.8.3.min.js
17 | - js/theme.js
18 | markdown_extensions:
19 | - admonition
20 | - pymdownx.highlight
21 | - pymdownx.superfences
22 | - toc:
23 | permalink: true
24 | nav:
25 | - Introduction: index.md
26 | - Getting Started: Getting-Started.md
27 | - Use Cases: Use-Cases.md
28 | - How ApplicationSet controller interacts with Argo CD: Argo-CD-Integration.md
29 | - Generators:
30 | - Generators.md
31 | - Generators-List.md
32 | - Generators-Cluster.md
33 | - Generators-Git.md
34 | - Generators-Matrix.md
35 | - Generators-Merge.md
36 | - Generators-SCM-Provider.md
37 | - Generators-Cluster-Decision-Resource.md
38 | - Generators-Pull-Request.md
39 | - Template fields: Template.md
40 | - Controlling Resource Modification: Controlling-Resource-Modification.md
41 | - Application Pruning & Resource Deletion: Application-Deletion.md
42 | - Developer Guide:
43 | - Building and Running the Controller: Development.md
44 | - Running E2E Tests: E2E-Tests.md
45 | - Release Process: Releasing.md
46 | - Release Checklist Template: Release-Checklist-Template.md
47 |
--------------------------------------------------------------------------------
/pkg/controllers/clustereventhandler.go:
--------------------------------------------------------------------------------
1 | package controllers
2 |
3 | import (
4 | "context"
5 |
6 | log "github.com/sirupsen/logrus"
7 |
8 | "k8s.io/apimachinery/pkg/types"
9 | "k8s.io/client-go/util/workqueue"
10 | ctrl "sigs.k8s.io/controller-runtime"
11 | "sigs.k8s.io/controller-runtime/pkg/client"
12 | "sigs.k8s.io/controller-runtime/pkg/event"
13 |
14 | argoprojiov1alpha1 "github.com/argoproj/applicationset/api/v1alpha1"
15 | "github.com/argoproj/applicationset/pkg/generators"
16 | )
17 |
18 | // clusterSecretEventHandler is used when watching Secrets to check if they are ArgoCD Cluster Secrets, and if so
19 | // requeue any related ApplicationSets.
20 | type clusterSecretEventHandler struct {
21 | //handler.EnqueueRequestForOwner
22 | Log log.FieldLogger
23 | Client client.Client
24 | }
25 |
26 | func (h *clusterSecretEventHandler) Create(e event.CreateEvent, q workqueue.RateLimitingInterface) {
27 | h.queueRelatedAppGenerators(q, e.Object)
28 | }
29 |
30 | func (h *clusterSecretEventHandler) Update(e event.UpdateEvent, q workqueue.RateLimitingInterface) {
31 | h.queueRelatedAppGenerators(q, e.ObjectNew)
32 | }
33 |
34 | func (h *clusterSecretEventHandler) Delete(e event.DeleteEvent, q workqueue.RateLimitingInterface) {
35 | h.queueRelatedAppGenerators(q, e.Object)
36 | }
37 |
38 | func (h *clusterSecretEventHandler) Generic(e event.GenericEvent, q workqueue.RateLimitingInterface) {
39 | h.queueRelatedAppGenerators(q, e.Object)
40 | }
41 |
42 | // addRateLimitingInterface defines the Add method of workqueue.RateLimitingInterface, allow us to easily mock
43 | // it for testing purposes.
44 | type addRateLimitingInterface interface {
45 | Add(item interface{})
46 | }
47 |
48 | func (h *clusterSecretEventHandler) queueRelatedAppGenerators(q addRateLimitingInterface, object client.Object) {
49 |
50 | // Check for label, lookup all ApplicationSets that might match the cluster, queue them all
51 | if object.GetLabels()[generators.ArgoCDSecretTypeLabel] != generators.ArgoCDSecretTypeCluster {
52 | return
53 | }
54 |
55 | h.Log.WithFields(log.Fields{
56 | "namespace": object.GetNamespace(),
57 | "name": object.GetName(),
58 | }).Info("processing event for cluster secret")
59 |
60 | appSetList := &argoprojiov1alpha1.ApplicationSetList{}
61 | err := h.Client.List(context.Background(), appSetList)
62 | if err != nil {
63 | h.Log.WithError(err).Error("unable to list ApplicationSets")
64 | return
65 | }
66 |
67 | h.Log.WithField("count", len(appSetList.Items)).Info("listed ApplicationSets")
68 | for _, appSet := range appSetList.Items {
69 |
70 | foundClusterGenerator := false
71 | for _, generator := range appSet.Spec.Generators {
72 | if generator.Clusters != nil {
73 | foundClusterGenerator = true
74 | break
75 | }
76 | }
77 | if foundClusterGenerator {
78 |
79 | // TODO: only queue the AppGenerator if the labels match this cluster
80 | req := ctrl.Request{NamespacedName: types.NamespacedName{Namespace: appSet.Namespace, Name: appSet.Name}}
81 | q.Add(req)
82 | }
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/pkg/generators/generator_spec_processor.go:
--------------------------------------------------------------------------------
1 | package generators
2 |
3 | import (
4 | "reflect"
5 |
6 | argoprojiov1alpha1 "github.com/argoproj/applicationset/api/v1alpha1"
7 | "github.com/imdario/mergo"
8 | log "github.com/sirupsen/logrus"
9 | )
10 |
11 | func GetRelevantGenerators(requestedGenerator *argoprojiov1alpha1.ApplicationSetGenerator, generators map[string]Generator) []Generator {
12 | var res []Generator
13 |
14 | v := reflect.Indirect(reflect.ValueOf(requestedGenerator))
15 | for i := 0; i < v.NumField(); i++ {
16 | field := v.Field(i)
17 | if !field.CanInterface() {
18 | continue
19 | }
20 |
21 | if !reflect.ValueOf(field.Interface()).IsNil() {
22 | res = append(res, generators[v.Type().Field(i).Name])
23 | }
24 | }
25 |
26 | return res
27 | }
28 |
29 | type TransformResult struct {
30 | Params []map[string]string
31 | Template argoprojiov1alpha1.ApplicationSetTemplate
32 | }
33 |
34 | //Transform a spec generator to list of paramSets and a template
35 | func Transform(requestedGenerator argoprojiov1alpha1.ApplicationSetGenerator, allGenerators map[string]Generator, baseTemplate argoprojiov1alpha1.ApplicationSetTemplate, appSet *argoprojiov1alpha1.ApplicationSet) ([]TransformResult, error) {
36 | res := []TransformResult{}
37 | var firstError error
38 |
39 | generators := GetRelevantGenerators(&requestedGenerator, allGenerators)
40 | for _, g := range generators {
41 | // we call mergeGeneratorTemplate first because GenerateParams might be more costly so we want to fail fast if there is an error
42 | mergedTemplate, err := mergeGeneratorTemplate(g, &requestedGenerator, baseTemplate)
43 | if err != nil {
44 | log.WithError(err).WithField("generator", g).
45 | Error("error generating params")
46 | if firstError == nil {
47 | firstError = err
48 | }
49 | continue
50 | }
51 |
52 | params, err := g.GenerateParams(&requestedGenerator, appSet)
53 | if err != nil {
54 | log.WithError(err).WithField("generator", g).
55 | Error("error generating params")
56 | if firstError == nil {
57 | firstError = err
58 | }
59 | continue
60 | }
61 |
62 | res = append(res, TransformResult{
63 | Params: params,
64 | Template: mergedTemplate,
65 | })
66 |
67 | }
68 |
69 | return res, firstError
70 |
71 | }
72 |
73 | func mergeGeneratorTemplate(g Generator, requestedGenerator *argoprojiov1alpha1.ApplicationSetGenerator, applicationSetTemplate argoprojiov1alpha1.ApplicationSetTemplate) (argoprojiov1alpha1.ApplicationSetTemplate, error) {
74 |
75 | // Make a copy of the value from `GetTemplate()` before merge, rather than copying directly into
76 | // the provided parameter (which will touch the original resource object returned by client-go)
77 | dest := g.GetTemplate(requestedGenerator).DeepCopy()
78 |
79 | err := mergo.Merge(dest, applicationSetTemplate)
80 |
81 | return *dest, err
82 | }
83 |
--------------------------------------------------------------------------------
/pkg/generators/generators_test.go:
--------------------------------------------------------------------------------
1 | package generators
2 |
3 | import (
4 | "fmt"
5 | "reflect"
6 | "testing"
7 |
8 | "github.com/stretchr/testify/assert"
9 |
10 | "github.com/argoproj/applicationset/api/v1alpha1"
11 | )
12 |
13 | func TestGetRelevantGenerators(t *testing.T) {
14 | requestedGenerator := &v1alpha1.ApplicationSetGenerator{
15 | List: &v1alpha1.ListGenerator{},
16 | }
17 | allGenerators := map[string]Generator{
18 | "List": NewListGenerator(),
19 | }
20 | relevantGenerators := GetRelevantGenerators(requestedGenerator, allGenerators)
21 |
22 | for _, generator := range relevantGenerators {
23 | if generator == nil {
24 | t.Fatal(`GetRelevantGenerators produced a nil generator`)
25 | }
26 | }
27 |
28 | numRelevantGenerators := len(relevantGenerators)
29 | if numRelevantGenerators != 1 {
30 | t.Fatalf(`GetRelevantGenerators produced %d generators instead of the expected 1`, numRelevantGenerators)
31 | }
32 | }
33 |
34 | func TestNoGeneratorNilReferenceError(t *testing.T) {
35 | generators := []Generator{
36 | &ClusterGenerator{},
37 | &DuckTypeGenerator{},
38 | &GitGenerator{},
39 | &ListGenerator{},
40 | &MatrixGenerator{},
41 | &MergeGenerator{},
42 | &PullRequestGenerator{},
43 | &SCMProviderGenerator{},
44 | }
45 |
46 | for _, generator := range generators {
47 | testCaseCopy := generator // since tests may run in parallel
48 |
49 | generatorName := reflect.TypeOf(testCaseCopy).Elem().Name()
50 | t.Run(fmt.Sprintf("%s does not throw a nil reference error when all generator fields are nil", generatorName), func(t *testing.T) {
51 | t.Parallel()
52 |
53 | params, err := generator.GenerateParams(&v1alpha1.ApplicationSetGenerator{}, &v1alpha1.ApplicationSet{})
54 |
55 | assert.ErrorIs(t, err, EmptyAppSetGeneratorError)
56 | assert.Nil(t, params)
57 | })
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/pkg/generators/interface.go:
--------------------------------------------------------------------------------
1 | package generators
2 |
3 | import (
4 | "fmt"
5 | "time"
6 |
7 | argoprojiov1alpha1 "github.com/argoproj/applicationset/api/v1alpha1"
8 | )
9 |
10 | // Generator defines the interface implemented by all ApplicationSet generators.
11 | type Generator interface {
12 | // GenerateParams interprets the ApplicationSet and generates all relevant parameters for the application template.
13 | // The expected / desired list of parameters is returned, it then will be render and reconciled
14 | // against the current state of the Applications in the cluster.
15 | GenerateParams(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator, applicationSetInfo *argoprojiov1alpha1.ApplicationSet) ([]map[string]string, error)
16 |
17 | // GetRequeueAfter is the the generator can controller the next reconciled loop
18 | // In case there is more then one generator the time will be the minimum of the times.
19 | // In case NoRequeueAfter is empty, it will be ignored
20 | GetRequeueAfter(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator) time.Duration
21 |
22 | // GetTemplate returns the inline template from the spec if there is any, or an empty object otherwise
23 | GetTemplate(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator) *argoprojiov1alpha1.ApplicationSetTemplate
24 | }
25 |
26 | var EmptyAppSetGeneratorError = fmt.Errorf("ApplicationSet is empty")
27 | var NoRequeueAfter time.Duration
28 |
29 | // DefaultRequeueAfterSeconds is used when GetRequeueAfter is not specified, it is the default time to wait before the next reconcile loop
30 | const (
31 | DefaultRequeueAfterSeconds = 3 * time.Minute
32 | )
33 |
--------------------------------------------------------------------------------
/pkg/generators/list.go:
--------------------------------------------------------------------------------
1 | package generators
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "time"
7 |
8 | argoprojiov1alpha1 "github.com/argoproj/applicationset/api/v1alpha1"
9 | )
10 |
11 | var _ Generator = (*ListGenerator)(nil)
12 |
13 | type ListGenerator struct {
14 | }
15 |
16 | func NewListGenerator() Generator {
17 | g := &ListGenerator{}
18 | return g
19 | }
20 |
21 | func (g *ListGenerator) GetRequeueAfter(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator) time.Duration {
22 | return NoRequeueAfter
23 | }
24 |
25 | func (g *ListGenerator) GetTemplate(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator) *argoprojiov1alpha1.ApplicationSetTemplate {
26 | return &appSetGenerator.List.Template
27 | }
28 |
29 | func (g *ListGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator, _ *argoprojiov1alpha1.ApplicationSet) ([]map[string]string, error) {
30 | if appSetGenerator == nil {
31 | return nil, EmptyAppSetGeneratorError
32 | }
33 |
34 | if appSetGenerator.List == nil {
35 | return nil, EmptyAppSetGeneratorError
36 | }
37 |
38 | res := make([]map[string]string, len(appSetGenerator.List.Elements))
39 |
40 | for i, tmpItem := range appSetGenerator.List.Elements {
41 | params := map[string]string{}
42 | var element map[string]interface{}
43 | err := json.Unmarshal(tmpItem.Raw, &element)
44 | if err != nil {
45 | return nil, fmt.Errorf("error unmarshling list element %v", err)
46 | }
47 |
48 | for key, value := range element {
49 | if key == "values" {
50 | values, ok := (value).(map[string]interface{})
51 | if !ok {
52 | return nil, fmt.Errorf("error parsing values map")
53 | }
54 | for k, v := range values {
55 | value, ok := v.(string)
56 | if !ok {
57 | return nil, fmt.Errorf("error parsing value as string %v", err)
58 | }
59 | params[fmt.Sprintf("values.%s", k)] = value
60 | }
61 | } else {
62 | v, ok := value.(string)
63 | if !ok {
64 | return nil, fmt.Errorf("error parsing value as string %v", err)
65 | }
66 | params[key] = v
67 | }
68 | }
69 |
70 | res[i] = params
71 | }
72 |
73 | return res, nil
74 | }
75 |
--------------------------------------------------------------------------------
/pkg/generators/list_test.go:
--------------------------------------------------------------------------------
1 | package generators
2 |
3 | import (
4 | "testing"
5 |
6 | argoprojiov1alpha1 "github.com/argoproj/applicationset/api/v1alpha1"
7 | "github.com/stretchr/testify/assert"
8 | apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
9 | )
10 |
11 | func TestGenerateListParams(t *testing.T) {
12 | testCases := []struct {
13 | elements []apiextensionsv1.JSON
14 | expected []map[string]string
15 | }{
16 | {
17 | elements: []apiextensionsv1.JSON{{Raw: []byte(`{"cluster": "cluster","url": "url"}`)}},
18 | expected: []map[string]string{{"cluster": "cluster", "url": "url"}},
19 | }, {
20 | elements: []apiextensionsv1.JSON{{Raw: []byte(`{"cluster": "cluster","url": "url","values":{"foo":"bar"}}`)}},
21 | expected: []map[string]string{{"cluster": "cluster", "url": "url", "values.foo": "bar"}},
22 | },
23 | }
24 |
25 | for _, testCase := range testCases {
26 |
27 | var listGenerator = NewListGenerator()
28 |
29 | got, err := listGenerator.GenerateParams(&argoprojiov1alpha1.ApplicationSetGenerator{
30 | List: &argoprojiov1alpha1.ListGenerator{
31 | Elements: testCase.elements,
32 | }}, nil)
33 |
34 | assert.NoError(t, err)
35 | assert.ElementsMatch(t, testCase.expected, got)
36 |
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/pkg/generators/pull_request.go:
--------------------------------------------------------------------------------
1 | package generators
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "strconv"
7 | "time"
8 |
9 | corev1 "k8s.io/api/core/v1"
10 | "sigs.k8s.io/controller-runtime/pkg/client"
11 |
12 | argoprojiov1alpha1 "github.com/argoproj/applicationset/api/v1alpha1"
13 | pullrequest "github.com/argoproj/applicationset/pkg/services/pull_request"
14 | )
15 |
16 | var _ Generator = (*PullRequestGenerator)(nil)
17 |
18 | const (
19 | DefaultPullRequestRequeueAfterSeconds = 30 * time.Minute
20 | )
21 |
22 | type PullRequestGenerator struct {
23 | client client.Client
24 | selectServiceProviderFunc func(context.Context, *argoprojiov1alpha1.PullRequestGenerator, *argoprojiov1alpha1.ApplicationSet) (pullrequest.PullRequestService, error)
25 | }
26 |
27 | func NewPullRequestGenerator(client client.Client) Generator {
28 | g := &PullRequestGenerator{
29 | client: client,
30 | }
31 | g.selectServiceProviderFunc = g.selectServiceProvider
32 | return g
33 | }
34 |
35 | func (g *PullRequestGenerator) GetRequeueAfter(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator) time.Duration {
36 | // Return a requeue default of 30 minutes, if no default is specified.
37 |
38 | if appSetGenerator.PullRequest.RequeueAfterSeconds != nil {
39 | return time.Duration(*appSetGenerator.PullRequest.RequeueAfterSeconds) * time.Second
40 | }
41 |
42 | return DefaultPullRequestRequeueAfterSeconds
43 | }
44 |
45 | func (g *PullRequestGenerator) GetTemplate(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator) *argoprojiov1alpha1.ApplicationSetTemplate {
46 | return &appSetGenerator.PullRequest.Template
47 | }
48 |
49 | func (g *PullRequestGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator, applicationSetInfo *argoprojiov1alpha1.ApplicationSet) ([]map[string]string, error) {
50 | if appSetGenerator == nil {
51 | return nil, EmptyAppSetGeneratorError
52 | }
53 |
54 | if appSetGenerator.PullRequest == nil {
55 | return nil, EmptyAppSetGeneratorError
56 | }
57 |
58 | ctx := context.Background()
59 | svc, err := g.selectServiceProviderFunc(ctx, appSetGenerator.PullRequest, applicationSetInfo)
60 | if err != nil {
61 | return nil, fmt.Errorf("failed to select pull request service provider: %v", err)
62 | }
63 |
64 | pulls, err := svc.List(ctx)
65 | if err != nil {
66 | return nil, fmt.Errorf("error listing repos: %v", err)
67 | }
68 | params := make([]map[string]string, 0, len(pulls))
69 | for _, pull := range pulls {
70 | params = append(params, map[string]string{
71 | "number": strconv.Itoa(pull.Number),
72 | "branch": pull.Branch,
73 | "head_sha": pull.HeadSHA,
74 | })
75 | }
76 | return params, nil
77 | }
78 |
79 | // selectServiceProvider selects the provider to get pull requests from the configuration
80 | func (g *PullRequestGenerator) selectServiceProvider(ctx context.Context, generatorConfig *argoprojiov1alpha1.PullRequestGenerator, applicationSetInfo *argoprojiov1alpha1.ApplicationSet) (pullrequest.PullRequestService, error) {
81 | if generatorConfig.Github != nil {
82 | providerConfig := generatorConfig.Github
83 | token, err := g.getSecretRef(ctx, providerConfig.TokenRef, applicationSetInfo.Namespace)
84 | if err != nil {
85 | return nil, fmt.Errorf("error fetching Secret token: %v", err)
86 | }
87 | return pullrequest.NewGithubService(ctx, token, providerConfig.API, providerConfig.Owner, providerConfig.Repo, providerConfig.Labels)
88 | }
89 | return nil, fmt.Errorf("no Pull Request provider implementation configured")
90 | }
91 |
92 | // getSecretRef gets the value of the key for the specified Secret resource.
93 | func (g *PullRequestGenerator) getSecretRef(ctx context.Context, ref *argoprojiov1alpha1.SecretRef, namespace string) (string, error) {
94 | if ref == nil {
95 | return "", nil
96 | }
97 |
98 | secret := &corev1.Secret{}
99 | err := g.client.Get(
100 | ctx,
101 | client.ObjectKey{
102 | Name: ref.SecretName,
103 | Namespace: namespace,
104 | },
105 | secret)
106 | if err != nil {
107 | return "", fmt.Errorf("error fetching secret %s/%s: %v", namespace, ref.SecretName, err)
108 | }
109 | tokenBytes, ok := secret.Data[ref.Key]
110 | if !ok {
111 | return "", fmt.Errorf("key %q in secret %s/%s not found", ref.Key, namespace, ref.SecretName)
112 | }
113 | return string(tokenBytes), nil
114 | }
115 |
--------------------------------------------------------------------------------
/pkg/generators/pull_request_test.go:
--------------------------------------------------------------------------------
1 | package generators
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "testing"
7 |
8 | "github.com/stretchr/testify/assert"
9 | corev1 "k8s.io/api/core/v1"
10 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
11 | "sigs.k8s.io/controller-runtime/pkg/client/fake"
12 |
13 | argoprojiov1alpha1 "github.com/argoproj/applicationset/api/v1alpha1"
14 | pullrequest "github.com/argoproj/applicationset/pkg/services/pull_request"
15 | )
16 |
17 | func TestPullRequestGithubGenerateParams(t *testing.T) {
18 | ctx := context.Background()
19 | cases := []struct {
20 | selectFunc func(context.Context, *argoprojiov1alpha1.PullRequestGenerator, *argoprojiov1alpha1.ApplicationSet) (pullrequest.PullRequestService, error)
21 | expected []map[string]string
22 | expectedErr error
23 | }{
24 | {
25 | selectFunc: func(context.Context, *argoprojiov1alpha1.PullRequestGenerator, *argoprojiov1alpha1.ApplicationSet) (pullrequest.PullRequestService, error) {
26 | return pullrequest.NewFakeService(
27 | ctx,
28 | []*pullrequest.PullRequest{
29 | &pullrequest.PullRequest{
30 | Number: 1,
31 | Branch: "branch1",
32 | HeadSHA: "089d92cbf9ff857a39e6feccd32798ca700fb958",
33 | },
34 | },
35 | nil,
36 | )
37 | },
38 | expected: []map[string]string{
39 | {
40 | "number": "1",
41 | "branch": "branch1",
42 | "head_sha": "089d92cbf9ff857a39e6feccd32798ca700fb958",
43 | },
44 | },
45 | expectedErr: nil,
46 | },
47 | {
48 | selectFunc: func(context.Context, *argoprojiov1alpha1.PullRequestGenerator, *argoprojiov1alpha1.ApplicationSet) (pullrequest.PullRequestService, error) {
49 | return pullrequest.NewFakeService(
50 | ctx,
51 | nil,
52 | fmt.Errorf("fake error"),
53 | )
54 | },
55 | expected: nil,
56 | expectedErr: fmt.Errorf("error listing repos: fake error"),
57 | },
58 | }
59 |
60 | for _, c := range cases {
61 | gen := PullRequestGenerator{
62 | selectServiceProviderFunc: c.selectFunc,
63 | }
64 | generatorConfig := argoprojiov1alpha1.ApplicationSetGenerator{
65 | PullRequest: &argoprojiov1alpha1.PullRequestGenerator{},
66 | }
67 | got, gotErr := gen.GenerateParams(&generatorConfig, nil)
68 | assert.Equal(t, c.expectedErr, gotErr)
69 | assert.ElementsMatch(t, c.expected, got)
70 | }
71 | }
72 |
73 | func TestPullRequestGetSecretRef(t *testing.T) {
74 | secret := &corev1.Secret{
75 | ObjectMeta: metav1.ObjectMeta{Name: "test-secret", Namespace: "test"},
76 | Data: map[string][]byte{
77 | "my-token": []byte("secret"),
78 | },
79 | }
80 | gen := &PullRequestGenerator{client: fake.NewClientBuilder().WithObjects(secret).Build()}
81 | ctx := context.Background()
82 |
83 | cases := []struct {
84 | name, namespace, token string
85 | ref *argoprojiov1alpha1.SecretRef
86 | hasError bool
87 | }{
88 | {
89 | name: "valid ref",
90 | ref: &argoprojiov1alpha1.SecretRef{SecretName: "test-secret", Key: "my-token"},
91 | namespace: "test",
92 | token: "secret",
93 | hasError: false,
94 | },
95 | {
96 | name: "nil ref",
97 | ref: nil,
98 | namespace: "test",
99 | token: "",
100 | hasError: false,
101 | },
102 | {
103 | name: "wrong name",
104 | ref: &argoprojiov1alpha1.SecretRef{SecretName: "other", Key: "my-token"},
105 | namespace: "test",
106 | token: "",
107 | hasError: true,
108 | },
109 | {
110 | name: "wrong key",
111 | ref: &argoprojiov1alpha1.SecretRef{SecretName: "test-secret", Key: "other-token"},
112 | namespace: "test",
113 | token: "",
114 | hasError: true,
115 | },
116 | {
117 | name: "wrong namespace",
118 | ref: &argoprojiov1alpha1.SecretRef{SecretName: "test-secret", Key: "my-token"},
119 | namespace: "other",
120 | token: "",
121 | hasError: true,
122 | },
123 | }
124 |
125 | for _, c := range cases {
126 | t.Run(c.name, func(t *testing.T) {
127 | token, err := gen.getSecretRef(ctx, c.ref, c.namespace)
128 | if c.hasError {
129 | assert.NotNil(t, err)
130 | } else {
131 | assert.Nil(t, err)
132 | }
133 | assert.Equal(t, c.token, token)
134 | })
135 | }
136 | }
137 |
--------------------------------------------------------------------------------
/pkg/generators/scm_provider.go:
--------------------------------------------------------------------------------
1 | package generators
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "strings"
7 | "time"
8 |
9 | corev1 "k8s.io/api/core/v1"
10 | "sigs.k8s.io/controller-runtime/pkg/client"
11 |
12 | argoprojiov1alpha1 "github.com/argoproj/applicationset/api/v1alpha1"
13 | "github.com/argoproj/applicationset/pkg/services/scm_provider"
14 | )
15 |
16 | var _ Generator = (*SCMProviderGenerator)(nil)
17 |
18 | const (
19 | DefaultSCMProviderRequeueAfterSeconds = 30 * time.Minute
20 | )
21 |
22 | type SCMProviderGenerator struct {
23 | client client.Client
24 | // Testing hooks.
25 | overrideProvider scm_provider.SCMProviderService
26 | }
27 |
28 | func NewSCMProviderGenerator(client client.Client) Generator {
29 | return &SCMProviderGenerator{client: client}
30 | }
31 |
32 | func (g *SCMProviderGenerator) GetRequeueAfter(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator) time.Duration {
33 | // Return a requeue default of 30 minutes, if no default is specified.
34 |
35 | if appSetGenerator.SCMProvider.RequeueAfterSeconds != nil {
36 | return time.Duration(*appSetGenerator.SCMProvider.RequeueAfterSeconds) * time.Second
37 | }
38 |
39 | return DefaultSCMProviderRequeueAfterSeconds
40 | }
41 |
42 | func (g *SCMProviderGenerator) GetTemplate(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator) *argoprojiov1alpha1.ApplicationSetTemplate {
43 | return &appSetGenerator.SCMProvider.Template
44 | }
45 |
46 | func (g *SCMProviderGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator, applicationSetInfo *argoprojiov1alpha1.ApplicationSet) ([]map[string]string, error) {
47 | if appSetGenerator == nil {
48 | return nil, EmptyAppSetGeneratorError
49 | }
50 |
51 | if appSetGenerator.SCMProvider == nil {
52 | return nil, EmptyAppSetGeneratorError
53 | }
54 |
55 | ctx := context.Background()
56 |
57 | // Create the SCM provider helper.
58 | providerConfig := appSetGenerator.SCMProvider
59 | var provider scm_provider.SCMProviderService
60 | if g.overrideProvider != nil {
61 | provider = g.overrideProvider
62 | } else if providerConfig.Github != nil {
63 | token, err := g.getSecretRef(ctx, providerConfig.Github.TokenRef, applicationSetInfo.Namespace)
64 | if err != nil {
65 | return nil, fmt.Errorf("error fetching Github token: %v", err)
66 | }
67 | provider, err = scm_provider.NewGithubProvider(ctx, providerConfig.Github.Organization, token, providerConfig.Github.API, providerConfig.Github.AllBranches)
68 | if err != nil {
69 | return nil, fmt.Errorf("error initializing Github service: %v", err)
70 | }
71 | } else if providerConfig.Gitlab != nil {
72 | token, err := g.getSecretRef(ctx, providerConfig.Gitlab.TokenRef, applicationSetInfo.Namespace)
73 | if err != nil {
74 | return nil, fmt.Errorf("error fetching Gitlab token: %v", err)
75 | }
76 | provider, err = scm_provider.NewGitlabProvider(ctx, providerConfig.Gitlab.Group, token, providerConfig.Gitlab.API, providerConfig.Gitlab.AllBranches, providerConfig.Gitlab.IncludeSubgroups)
77 | if err != nil {
78 | return nil, fmt.Errorf("error initializing Gitlab service: %v", err)
79 | }
80 | } else if providerConfig.Bitbucket != nil {
81 | appPassword, err := g.getSecretRef(ctx, providerConfig.Bitbucket.AppPasswordRef, applicationSetInfo.Namespace)
82 | if err != nil {
83 | return nil, fmt.Errorf("error fetching Bitbucket cloud appPassword: %v", err)
84 | }
85 | provider, err = scm_provider.NewBitBucketCloudProvider(ctx, providerConfig.Bitbucket.Owner, providerConfig.Bitbucket.User, appPassword, providerConfig.Bitbucket.AllBranches)
86 | if err != nil {
87 | return nil, fmt.Errorf("error initializing Bitbucket cloud service: %v", err)
88 | }
89 | } else {
90 | return nil, fmt.Errorf("no SCM provider implementation configured")
91 | }
92 |
93 | // Find all the available repos.
94 | repos, err := scm_provider.ListRepos(ctx, provider, providerConfig.Filters, providerConfig.CloneProtocol)
95 | if err != nil {
96 | return nil, fmt.Errorf("error listing repos: %v", err)
97 | }
98 | params := make([]map[string]string, 0, len(repos))
99 | for _, repo := range repos {
100 | params = append(params, map[string]string{
101 | "organization": repo.Organization,
102 | "repository": repo.Repository,
103 | "url": repo.URL,
104 | "branch": repo.Branch,
105 | "sha": repo.SHA,
106 | "labels": strings.Join(repo.Labels, ","),
107 | })
108 | }
109 | return params, nil
110 | }
111 |
112 | func (g *SCMProviderGenerator) getSecretRef(ctx context.Context, ref *argoprojiov1alpha1.SecretRef, namespace string) (string, error) {
113 | if ref == nil {
114 | return "", nil
115 | }
116 |
117 | secret := &corev1.Secret{}
118 | err := g.client.Get(
119 | ctx,
120 | client.ObjectKey{
121 | Name: ref.SecretName,
122 | Namespace: namespace,
123 | },
124 | secret)
125 | if err != nil {
126 | return "", fmt.Errorf("error fetching secret %s/%s: %v", namespace, ref.SecretName, err)
127 | }
128 | tokenBytes, ok := secret.Data[ref.Key]
129 | if !ok {
130 | return "", fmt.Errorf("key %q in secret %s/%s not found", ref.Key, namespace, ref.SecretName)
131 | }
132 | return string(tokenBytes), nil
133 | }
134 |
--------------------------------------------------------------------------------
/pkg/generators/scm_provider_test.go:
--------------------------------------------------------------------------------
1 | package generators
2 |
3 | import (
4 | "context"
5 | "testing"
6 |
7 | "github.com/stretchr/testify/assert"
8 | corev1 "k8s.io/api/core/v1"
9 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
10 | "sigs.k8s.io/controller-runtime/pkg/client/fake"
11 |
12 | argoprojiov1alpha1 "github.com/argoproj/applicationset/api/v1alpha1"
13 | "github.com/argoproj/applicationset/pkg/services/scm_provider"
14 | )
15 |
16 | func TestSCMProviderGetSecretRef(t *testing.T) {
17 | secret := &corev1.Secret{
18 | ObjectMeta: metav1.ObjectMeta{Name: "test-secret", Namespace: "test"},
19 | Data: map[string][]byte{
20 | "my-token": []byte("secret"),
21 | },
22 | }
23 | gen := &SCMProviderGenerator{client: fake.NewClientBuilder().WithObjects(secret).Build()}
24 | ctx := context.Background()
25 |
26 | cases := []struct {
27 | name, namespace, token string
28 | ref *argoprojiov1alpha1.SecretRef
29 | hasError bool
30 | }{
31 | {
32 | name: "valid ref",
33 | ref: &argoprojiov1alpha1.SecretRef{SecretName: "test-secret", Key: "my-token"},
34 | namespace: "test",
35 | token: "secret",
36 | hasError: false,
37 | },
38 | {
39 | name: "nil ref",
40 | ref: nil,
41 | namespace: "test",
42 | token: "",
43 | hasError: false,
44 | },
45 | {
46 | name: "wrong name",
47 | ref: &argoprojiov1alpha1.SecretRef{SecretName: "other", Key: "my-token"},
48 | namespace: "test",
49 | token: "",
50 | hasError: true,
51 | },
52 | {
53 | name: "wrong key",
54 | ref: &argoprojiov1alpha1.SecretRef{SecretName: "test-secret", Key: "other-token"},
55 | namespace: "test",
56 | token: "",
57 | hasError: true,
58 | },
59 | {
60 | name: "wrong namespace",
61 | ref: &argoprojiov1alpha1.SecretRef{SecretName: "test-secret", Key: "my-token"},
62 | namespace: "other",
63 | token: "",
64 | hasError: true,
65 | },
66 | }
67 |
68 | for _, c := range cases {
69 | t.Run(c.name, func(t *testing.T) {
70 | token, err := gen.getSecretRef(ctx, c.ref, c.namespace)
71 | if c.hasError {
72 | assert.NotNil(t, err)
73 | } else {
74 | assert.Nil(t, err)
75 | }
76 | assert.Equal(t, c.token, token)
77 |
78 | })
79 | }
80 | }
81 |
82 | func TestSCMProviderGenerateParams(t *testing.T) {
83 | mockProvider := &scm_provider.MockProvider{
84 | Repos: []*scm_provider.Repository{
85 | {
86 | Organization: "myorg",
87 | Repository: "repo1",
88 | URL: "git@github.com:myorg/repo1.git",
89 | Branch: "main",
90 | SHA: "abcd1234",
91 | Labels: []string{"prod", "staging"},
92 | },
93 | {
94 | Organization: "myorg",
95 | Repository: "repo2",
96 | URL: "git@github.com:myorg/repo2.git",
97 | Branch: "main",
98 | SHA: "00000000",
99 | },
100 | },
101 | }
102 | gen := &SCMProviderGenerator{overrideProvider: mockProvider}
103 | params, err := gen.GenerateParams(&argoprojiov1alpha1.ApplicationSetGenerator{
104 | SCMProvider: &argoprojiov1alpha1.SCMProviderGenerator{},
105 | }, nil)
106 | assert.Nil(t, err)
107 | assert.Len(t, params, 2)
108 | assert.Equal(t, "myorg", params[0]["organization"])
109 | assert.Equal(t, "repo1", params[0]["repository"])
110 | assert.Equal(t, "git@github.com:myorg/repo1.git", params[0]["url"])
111 | assert.Equal(t, "main", params[0]["branch"])
112 | assert.Equal(t, "abcd1234", params[0]["sha"])
113 | assert.Equal(t, "prod,staging", params[0]["labels"])
114 | assert.Equal(t, "repo2", params[1]["repository"])
115 | }
116 |
--------------------------------------------------------------------------------
/pkg/services/pull_request/fake.go:
--------------------------------------------------------------------------------
1 | package pull_request
2 |
3 | import (
4 | "context"
5 | )
6 |
7 | type FakeService struct {
8 | listPullReuests []*PullRequest
9 | listError error
10 | }
11 |
12 | var _ PullRequestService = (*FakeService)(nil)
13 |
14 | func NewFakeService(_ context.Context, listPullReuests []*PullRequest, listError error) (PullRequestService, error) {
15 | return &FakeService{
16 | listPullReuests: listPullReuests,
17 | listError: listError,
18 | }, nil
19 | }
20 |
21 | func (g *FakeService) List(ctx context.Context) ([]*PullRequest, error) {
22 | return g.listPullReuests, g.listError
23 | }
24 |
--------------------------------------------------------------------------------
/pkg/services/pull_request/github.go:
--------------------------------------------------------------------------------
1 | package pull_request
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "os"
7 |
8 | "github.com/google/go-github/v35/github"
9 | "golang.org/x/oauth2"
10 | )
11 |
12 | type GithubService struct {
13 | client *github.Client
14 | owner string
15 | repo string
16 | labels []string
17 | }
18 |
19 | var _ PullRequestService = (*GithubService)(nil)
20 |
21 | func NewGithubService(ctx context.Context, token, url, owner, repo string, labels []string) (PullRequestService, error) {
22 | var ts oauth2.TokenSource
23 | // Undocumented environment variable to set a default token, to be used in testing to dodge anonymous rate limits.
24 | if token == "" {
25 | token = os.Getenv("GITHUB_TOKEN")
26 | }
27 | if token != "" {
28 | ts = oauth2.StaticTokenSource(
29 | &oauth2.Token{AccessToken: token},
30 | )
31 | }
32 | httpClient := oauth2.NewClient(ctx, ts)
33 | var client *github.Client
34 | if url == "" {
35 | client = github.NewClient(httpClient)
36 | } else {
37 | var err error
38 | client, err = github.NewEnterpriseClient(url, url, httpClient)
39 | if err != nil {
40 | return nil, err
41 | }
42 | }
43 | return &GithubService{
44 | client: client,
45 | owner: owner,
46 | repo: repo,
47 | labels: labels,
48 | }, nil
49 | }
50 |
51 | func (g *GithubService) List(ctx context.Context) ([]*PullRequest, error) {
52 | opts := &github.PullRequestListOptions{
53 | ListOptions: github.ListOptions{
54 | PerPage: 100,
55 | },
56 | }
57 | pullRequests := []*PullRequest{}
58 | for {
59 | pulls, resp, err := g.client.PullRequests.List(ctx, g.owner, g.repo, opts)
60 | if err != nil {
61 | return nil, fmt.Errorf("error listing pull requests for %s/%s: %v", g.owner, g.repo, err)
62 | }
63 | for _, pull := range pulls {
64 | if !containLabels(g.labels, pull.Labels) {
65 | continue
66 | }
67 | pullRequests = append(pullRequests, &PullRequest{
68 | Number: *pull.Number,
69 | Branch: *pull.Head.Ref,
70 | HeadSHA: *pull.Head.SHA,
71 | })
72 | }
73 | if resp.NextPage == 0 {
74 | break
75 | }
76 | opts.Page = resp.NextPage
77 | }
78 | return pullRequests, nil
79 | }
80 |
81 | // containLabels returns true if gotLabels contains expectedLabels
82 | func containLabels(expectedLabels []string, gotLabels []*github.Label) bool {
83 | for _, expected := range expectedLabels {
84 | found := false
85 | for _, got := range gotLabels {
86 | if got.Name == nil {
87 | continue
88 | }
89 | if expected == *got.Name {
90 | found = true
91 | break
92 | }
93 | }
94 | if !found {
95 | return false
96 | }
97 | }
98 | return true
99 | }
100 |
--------------------------------------------------------------------------------
/pkg/services/pull_request/github_test.go:
--------------------------------------------------------------------------------
1 | package pull_request
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/google/go-github/v35/github"
7 | )
8 |
9 | func toPtr(s string) *string {
10 | return &s
11 | }
12 |
13 | func TestContainLabels(t *testing.T) {
14 | cases := []struct {
15 | Name string
16 | Labels []string
17 | PullLabels []*github.Label
18 | Expect bool
19 | }{
20 | {
21 | Name: "Match labels",
22 | Labels: []string{"label1", "label2"},
23 | PullLabels: []*github.Label{
24 | &github.Label{Name: toPtr("label1")},
25 | &github.Label{Name: toPtr("label2")},
26 | &github.Label{Name: toPtr("label3")},
27 | },
28 | Expect: true,
29 | },
30 | {
31 | Name: "Not match labels",
32 | Labels: []string{"label1", "label4"},
33 | PullLabels: []*github.Label{
34 | &github.Label{Name: toPtr("label1")},
35 | &github.Label{Name: toPtr("label2")},
36 | &github.Label{Name: toPtr("label3")},
37 | },
38 | Expect: false,
39 | },
40 | {
41 | Name: "No specify",
42 | Labels: []string{},
43 | PullLabels: []*github.Label{
44 | &github.Label{Name: toPtr("label1")},
45 | &github.Label{Name: toPtr("label2")},
46 | &github.Label{Name: toPtr("label3")},
47 | },
48 | Expect: true,
49 | },
50 | }
51 |
52 | for _, c := range cases {
53 | t.Run(c.Name, func(t *testing.T) {
54 | if got := containLabels(c.Labels, c.PullLabels); got != c.Expect {
55 | t.Errorf("expect: %v, got: %v", c.Expect, got)
56 | }
57 | })
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/pkg/services/pull_request/interface.go:
--------------------------------------------------------------------------------
1 | package pull_request
2 |
3 | import "context"
4 |
5 | type PullRequest struct {
6 | // Number is a number that will be the ID of the pull request.
7 | Number int
8 | // Branch is the name of the branch from which the pull request originated.
9 | Branch string
10 | // HeadSHA is the SHA of the HEAD from which the pull request originated.
11 | HeadSHA string
12 | }
13 |
14 | type PullRequestService interface {
15 | // List gets a list of pull requests.
16 | List(ctx context.Context) ([]*PullRequest, error)
17 | }
18 |
--------------------------------------------------------------------------------
/pkg/services/repo_service.go:
--------------------------------------------------------------------------------
1 | package services
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "os"
7 | "path/filepath"
8 | "strings"
9 |
10 | "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
11 | "github.com/argoproj/argo-cd/v2/util/db"
12 | "github.com/argoproj/argo-cd/v2/util/git"
13 | )
14 |
15 | // RepositoryDB Is a lean facade for ArgoDB,
16 | // Using a lean interface makes it more easy to test the functionality the git generator uses
17 | type RepositoryDB interface {
18 | GetRepository(ctx context.Context, url string) (*v1alpha1.Repository, error)
19 | }
20 |
21 | type argoCDService struct {
22 | repositoriesDB RepositoryDB
23 | }
24 |
25 | type Repos interface {
26 |
27 | // GetFiles returns content of files (not directories) within the target repo
28 | GetFiles(ctx context.Context, repoURL string, revision string, pattern string) (map[string][]byte, error)
29 |
30 | // GetDirectories returns a list of directories (not files) within the target repo
31 | GetDirectories(ctx context.Context, repoURL string, revision string) ([]string, error)
32 | }
33 |
34 | func NewArgoCDService(db db.ArgoDB, repoServerAddress string) Repos {
35 |
36 | return &argoCDService{
37 | repositoriesDB: db.(RepositoryDB),
38 | }
39 | }
40 |
41 | func (a *argoCDService) GetFiles(ctx context.Context, repoURL string, revision string, pattern string) (map[string][]byte, error) {
42 | repo, err := a.repositoriesDB.GetRepository(ctx, repoURL)
43 | if err != nil {
44 | return nil, fmt.Errorf("Error in GetRepository: %w", err)
45 | }
46 |
47 | gitRepoClient, err := git.NewClient(repo.Repo, repo.GetGitCreds(), repo.IsInsecure(), repo.IsLFSEnabled(), repo.Proxy)
48 |
49 | if err != nil {
50 | return nil, err
51 | }
52 |
53 | err = checkoutRepo(gitRepoClient, revision)
54 | if err != nil {
55 | return nil, err
56 | }
57 |
58 | paths, err := gitRepoClient.LsFiles(pattern)
59 | if err != nil {
60 | return nil, fmt.Errorf("Error during listing files of local repo: %w", err)
61 | }
62 |
63 | res := map[string][]byte{}
64 | for _, filePath := range paths {
65 | bytes, err := os.ReadFile(filepath.Join(gitRepoClient.Root(), filePath))
66 | if err != nil {
67 | return nil, err
68 | }
69 | res[filePath] = bytes
70 | }
71 |
72 | return res, nil
73 | }
74 |
75 | func (a *argoCDService) GetDirectories(ctx context.Context, repoURL string, revision string) ([]string, error) {
76 |
77 | repo, err := a.repositoriesDB.GetRepository(ctx, repoURL)
78 | if err != nil {
79 | return nil, fmt.Errorf("Error in GetRepository: %w", err)
80 | }
81 |
82 | gitRepoClient, err := git.NewClient(repo.Repo, repo.GetGitCreds(), repo.IsInsecure(), repo.IsLFSEnabled(), repo.Proxy)
83 | if err != nil {
84 | return nil, err
85 | }
86 |
87 | err = checkoutRepo(gitRepoClient, revision)
88 | if err != nil {
89 | return nil, err
90 | }
91 |
92 | filteredPaths := []string{}
93 |
94 | repoRoot := gitRepoClient.Root()
95 |
96 | if err := filepath.Walk(repoRoot, func(path string, info os.FileInfo, fnErr error) error {
97 | if fnErr != nil {
98 | return fnErr
99 | }
100 | if !info.IsDir() { // Skip files: directories only
101 | return nil
102 | }
103 |
104 | fname := info.Name()
105 | if strings.HasPrefix(fname, ".") { // Skip all folders starts with "."
106 | return filepath.SkipDir
107 | }
108 |
109 | relativePath, err := filepath.Rel(repoRoot, path)
110 | if err != nil {
111 | return err
112 | }
113 |
114 | if relativePath == "." { // Exclude '.' from results
115 | return nil
116 | }
117 |
118 | filteredPaths = append(filteredPaths, relativePath)
119 |
120 | return nil
121 | }); err != nil {
122 | return nil, err
123 | }
124 |
125 | return filteredPaths, nil
126 |
127 | }
128 |
129 | func checkoutRepo(gitRepoClient git.Client, revision string) error {
130 | err := gitRepoClient.Init()
131 | if err != nil {
132 | return fmt.Errorf("Error during initializing repo: %w", err)
133 | }
134 |
135 | err = gitRepoClient.Fetch(revision)
136 | if err != nil {
137 | return fmt.Errorf("Error during fetching repo: %w", err)
138 | }
139 |
140 | commitSHA, err := gitRepoClient.LsRemote(revision)
141 | if err != nil {
142 | return fmt.Errorf("Error during fetching commitSHA: %w", err)
143 | }
144 | err = gitRepoClient.Checkout(commitSHA, true)
145 | if err != nil {
146 | return fmt.Errorf("Error during repo checkout: %w", err)
147 | }
148 | return nil
149 | }
150 |
--------------------------------------------------------------------------------
/pkg/services/scm_provider/github.go:
--------------------------------------------------------------------------------
1 | package scm_provider
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "fmt"
7 | "os"
8 |
9 | "github.com/google/go-github/v35/github"
10 | "golang.org/x/oauth2"
11 | )
12 |
13 | type GithubProvider struct {
14 | client *github.Client
15 | organization string
16 | allBranches bool
17 | }
18 |
19 | var _ SCMProviderService = &GithubProvider{}
20 |
21 | func NewGithubProvider(ctx context.Context, organization string, token string, url string, allBranches bool) (*GithubProvider, error) {
22 | var ts oauth2.TokenSource
23 | // Undocumented environment variable to set a default token, to be used in testing to dodge anonymous rate limits.
24 | if token == "" {
25 | token = os.Getenv("GITHUB_TOKEN")
26 | }
27 | if token != "" {
28 | ts = oauth2.StaticTokenSource(
29 | &oauth2.Token{AccessToken: token},
30 | )
31 | }
32 | httpClient := oauth2.NewClient(ctx, ts)
33 | var client *github.Client
34 | if url == "" {
35 | client = github.NewClient(httpClient)
36 | } else {
37 | var err error
38 | client, err = github.NewEnterpriseClient(url, url, httpClient)
39 | if err != nil {
40 | return nil, err
41 | }
42 | }
43 | return &GithubProvider{client: client, organization: organization, allBranches: allBranches}, nil
44 | }
45 |
46 | func (g *GithubProvider) GetBranches(ctx context.Context, repo *Repository) ([]*Repository, error) {
47 | repos := []*Repository{}
48 | branches, err := g.listBranches(ctx, repo)
49 | if err != nil {
50 | return nil, fmt.Errorf("error listing branches for %s/%s: %v", repo.Organization, repo.Repository, err)
51 | }
52 |
53 | for _, branch := range branches {
54 | repos = append(repos, &Repository{
55 | Organization: repo.Organization,
56 | Repository: repo.Repository,
57 | URL: repo.URL,
58 | Branch: branch.GetName(),
59 | SHA: branch.GetCommit().GetSHA(),
60 | Labels: repo.Labels,
61 | RepositoryId: repo.RepositoryId,
62 | })
63 | }
64 | return repos, nil
65 | }
66 |
67 | func (g *GithubProvider) ListRepos(ctx context.Context, cloneProtocol string) ([]*Repository, error) {
68 | opt := &github.RepositoryListByOrgOptions{
69 | ListOptions: github.ListOptions{PerPage: 100},
70 | }
71 | repos := []*Repository{}
72 | for {
73 | githubRepos, resp, err := g.client.Repositories.ListByOrg(ctx, g.organization, opt)
74 | if err != nil {
75 | return nil, fmt.Errorf("error listing repositories for %s: %v", g.organization, err)
76 | }
77 | for _, githubRepo := range githubRepos {
78 | var url string
79 | switch cloneProtocol {
80 | // Default to SSH if unspecified (i.e. if "").
81 | case "", "ssh":
82 | url = githubRepo.GetSSHURL()
83 | case "https":
84 | url = githubRepo.GetCloneURL()
85 | default:
86 | return nil, fmt.Errorf("unknown clone protocol for GitHub %v", cloneProtocol)
87 | }
88 | repos = append(repos, &Repository{
89 | Organization: githubRepo.Owner.GetLogin(),
90 | Repository: githubRepo.GetName(),
91 | Branch: githubRepo.GetDefaultBranch(),
92 | URL: url,
93 | Labels: githubRepo.Topics,
94 | RepositoryId: githubRepo.ID,
95 | })
96 | }
97 | if resp.NextPage == 0 {
98 | break
99 | }
100 | opt.Page = resp.NextPage
101 | }
102 | return repos, nil
103 | }
104 |
105 | func (g *GithubProvider) RepoHasPath(ctx context.Context, repo *Repository, path string) (bool, error) {
106 | _, _, resp, err := g.client.Repositories.GetContents(ctx, repo.Organization, repo.Repository, path, &github.RepositoryContentGetOptions{
107 | Ref: repo.Branch,
108 | })
109 | // 404s are not an error here, just a normal false.
110 | if resp != nil && resp.StatusCode == 404 {
111 | return false, nil
112 | }
113 | if err != nil {
114 | return false, err
115 | }
116 | return true, nil
117 | }
118 |
119 | func (g *GithubProvider) listBranches(ctx context.Context, repo *Repository) ([]github.Branch, error) {
120 | // If we don't specifically want to query for all branches, just use the default branch and call it a day.
121 | if !g.allBranches {
122 | defaultBranch, _, err := g.client.Repositories.GetBranch(ctx, repo.Organization, repo.Repository, repo.Branch)
123 | if err != nil {
124 | var githubErrorResponse *github.ErrorResponse
125 | if errors.As(err, &githubErrorResponse) {
126 | if githubErrorResponse.Response.StatusCode == 404 {
127 | // Default branch doesn't exist, so the repo is empty.
128 | return []github.Branch{}, nil
129 | }
130 | }
131 | return nil, err
132 | }
133 | return []github.Branch{*defaultBranch}, nil
134 | }
135 | // Otherwise, scrape the ListBranches API.
136 | opt := &github.BranchListOptions{
137 | ListOptions: github.ListOptions{PerPage: 100},
138 | }
139 | branches := []github.Branch{}
140 | for {
141 | githubBranches, resp, err := g.client.Repositories.ListBranches(ctx, repo.Organization, repo.Repository, opt)
142 | if err != nil {
143 | return nil, err
144 | }
145 | for _, githubBranch := range githubBranches {
146 | branches = append(branches, *githubBranch)
147 | }
148 |
149 | if resp.NextPage == 0 {
150 | break
151 | }
152 | opt.Page = resp.NextPage
153 | }
154 | return branches, nil
155 | }
156 |
--------------------------------------------------------------------------------
/pkg/services/scm_provider/github_test.go:
--------------------------------------------------------------------------------
1 | package scm_provider
2 |
3 | import (
4 | "context"
5 | "os"
6 | "strings"
7 | "testing"
8 |
9 | "github.com/argoproj/applicationset/api/v1alpha1"
10 | "github.com/stretchr/testify/assert"
11 | )
12 |
13 | func checkRateLimit(t *testing.T, err error) {
14 | // Check if we've hit a rate limit, don't fail the test if so.
15 | if err != nil && (strings.Contains(err.Error(), "rate limit exceeded") ||
16 | (strings.Contains(err.Error(), "API rate limit") && strings.Contains(err.Error(), "still exceeded"))) {
17 |
18 | // GitHub Actions add this environment variable to indicate branch ref you are running on
19 | githubRef := os.Getenv("GITHUB_REF")
20 |
21 | // Only report rate limit errors as errors, when:
22 | // - We are running in a GitHub action
23 | // - AND, we are running that action on the 'master' or 'release-*' branch
24 | // (unfortunately, for PRs, we don't have access to GitHub secrets that would allow us to embed a token)
25 | failOnRateLimitErrors := os.Getenv("CI") != "" && (strings.Contains(githubRef, "/master") || strings.Contains(githubRef, "/release-"))
26 |
27 | t.Logf("Got a rate limit error, consider setting $GITHUB_TOKEN to increase your GitHub API rate limit: %v\n", err)
28 | if failOnRateLimitErrors {
29 | t.FailNow()
30 | } else {
31 | t.SkipNow()
32 | }
33 |
34 | }
35 | }
36 |
37 | func TestGithubListRepos(t *testing.T) {
38 | cases := []struct {
39 | name, proto, url string
40 | hasError, allBranches bool
41 | branches []string
42 | filters []v1alpha1.SCMProviderGeneratorFilter
43 | }{
44 | {
45 | name: "blank protocol",
46 | url: "git@github.com:argoproj/applicationset.git",
47 | branches: []string{"master"},
48 | },
49 | {
50 | name: "ssh protocol",
51 | proto: "ssh",
52 | url: "git@github.com:argoproj/applicationset.git",
53 | },
54 | {
55 | name: "https protocol",
56 | proto: "https",
57 | url: "https://github.com/argoproj/applicationset.git",
58 | },
59 | {
60 | name: "other protocol",
61 | proto: "other",
62 | hasError: true,
63 | },
64 | {
65 | name: "all branches",
66 | allBranches: true,
67 | url: "git@github.com:argoproj/applicationset.git",
68 | branches: []string{"master", "release-0.1.0"},
69 | },
70 | }
71 |
72 | for _, c := range cases {
73 | t.Run(c.name, func(t *testing.T) {
74 | provider, _ := NewGithubProvider(context.Background(), "argoproj", "", "", c.allBranches)
75 | rawRepos, err := ListRepos(context.Background(), provider, c.filters, c.proto)
76 | if c.hasError {
77 | assert.Error(t, err)
78 | } else {
79 | checkRateLimit(t, err)
80 | assert.NoError(t, err)
81 | // Just check that this one project shows up. Not a great test but better thing nothing?
82 | repos := []*Repository{}
83 | branches := []string{}
84 | for _, r := range rawRepos {
85 | if r.Repository == "applicationset" {
86 | repos = append(repos, r)
87 | branches = append(branches, r.Branch)
88 | }
89 | }
90 | assert.NotEmpty(t, repos)
91 | assert.Equal(t, c.url, repos[0].URL)
92 | for _, b := range c.branches {
93 | assert.Contains(t, branches, b)
94 | }
95 | }
96 | })
97 | }
98 | }
99 |
100 | func TestGithubHasPath(t *testing.T) {
101 | host, _ := NewGithubProvider(context.Background(), "argoproj", "", "", false)
102 | repo := &Repository{
103 | Organization: "argoproj",
104 | Repository: "applicationset",
105 | Branch: "master",
106 | }
107 | ok, err := host.RepoHasPath(context.Background(), repo, "pkg/")
108 | checkRateLimit(t, err)
109 | assert.Nil(t, err)
110 | assert.True(t, ok)
111 |
112 | ok, err = host.RepoHasPath(context.Background(), repo, "notathing/")
113 | checkRateLimit(t, err)
114 | assert.Nil(t, err)
115 | assert.False(t, ok)
116 | }
117 |
--------------------------------------------------------------------------------
/pkg/services/scm_provider/gitlab.go:
--------------------------------------------------------------------------------
1 | package scm_provider
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "os"
7 |
8 | gitlab "github.com/xanzy/go-gitlab"
9 | )
10 |
11 | type GitlabProvider struct {
12 | client *gitlab.Client
13 | organization string
14 | allBranches bool
15 | includeSubgroups bool
16 | }
17 |
18 | var _ SCMProviderService = &GitlabProvider{}
19 |
20 | func NewGitlabProvider(ctx context.Context, organization string, token string, url string, allBranches, includeSubgroups bool) (*GitlabProvider, error) {
21 | // Undocumented environment variable to set a default token, to be used in testing to dodge anonymous rate limits.
22 | if token == "" {
23 | token = os.Getenv("GITLAB_TOKEN")
24 | }
25 | var client *gitlab.Client
26 | if url == "" {
27 | var err error
28 | client, err = gitlab.NewClient(token)
29 | if err != nil {
30 | return nil, err
31 | }
32 | } else {
33 | var err error
34 | client, err = gitlab.NewClient(token, gitlab.WithBaseURL(url))
35 | if err != nil {
36 | return nil, err
37 | }
38 | }
39 | return &GitlabProvider{client: client, organization: organization, allBranches: allBranches, includeSubgroups: includeSubgroups}, nil
40 | }
41 |
42 | func (g *GitlabProvider) GetBranches(ctx context.Context, repo *Repository) ([]*Repository, error) {
43 | repos := []*Repository{}
44 | branches, err := g.listBranches(ctx, repo)
45 | if err != nil {
46 | return nil, fmt.Errorf("error listing branches for %s/%s: %v", repo.Organization, repo.Repository, err)
47 | }
48 |
49 | for _, branch := range branches {
50 | repos = append(repos, &Repository{
51 | Organization: repo.Organization,
52 | Repository: repo.Repository,
53 | URL: repo.URL,
54 | Branch: branch.Name,
55 | SHA: branch.Commit.ID,
56 | Labels: repo.Labels,
57 | RepositoryId: repo.RepositoryId,
58 | })
59 | }
60 | return repos, nil
61 | }
62 |
63 | func (g *GitlabProvider) ListRepos(ctx context.Context, cloneProtocol string) ([]*Repository, error) {
64 | opt := &gitlab.ListGroupProjectsOptions{
65 | ListOptions: gitlab.ListOptions{PerPage: 100},
66 | IncludeSubgroups: &g.includeSubgroups,
67 | }
68 | repos := []*Repository{}
69 | for {
70 | gitlabRepos, resp, err := g.client.Groups.ListGroupProjects(g.organization, opt)
71 | if err != nil {
72 | return nil, fmt.Errorf("error listing projects for %s: %v", g.organization, err)
73 | }
74 | for _, gitlabRepo := range gitlabRepos {
75 | var url string
76 | switch cloneProtocol {
77 | // Default to SSH if unspecified (i.e. if "").
78 | case "", "ssh":
79 | url = gitlabRepo.SSHURLToRepo
80 | case "https":
81 | url = gitlabRepo.HTTPURLToRepo
82 | default:
83 | return nil, fmt.Errorf("unknown clone protocol for Gitlab %v", cloneProtocol)
84 | }
85 |
86 | repos = append(repos, &Repository{
87 | Organization: gitlabRepo.Namespace.FullPath,
88 | Repository: gitlabRepo.Path,
89 | URL: url,
90 | Branch: gitlabRepo.DefaultBranch,
91 | Labels: gitlabRepo.TagList,
92 | RepositoryId: gitlabRepo.ID,
93 | })
94 | }
95 | if resp.CurrentPage >= resp.TotalPages {
96 | break
97 | }
98 | opt.Page = resp.NextPage
99 | }
100 | return repos, nil
101 | }
102 |
103 | func (g *GitlabProvider) RepoHasPath(_ context.Context, repo *Repository, path string) (bool, error) {
104 | p, _, err := g.client.Projects.GetProject(repo.Organization+"/"+repo.Repository, nil)
105 | if err != nil {
106 | return false, err
107 | }
108 | _, resp, err := g.client.Repositories.ListTree(p.ID, &gitlab.ListTreeOptions{
109 | Path: &path,
110 | Ref: &repo.Branch,
111 | })
112 | if err != nil {
113 | return false, err
114 | }
115 | if resp.TotalItems == 0 {
116 | return false, nil
117 | }
118 | return true, nil
119 | }
120 |
121 | func (g *GitlabProvider) listBranches(_ context.Context, repo *Repository) ([]gitlab.Branch, error) {
122 | branches := []gitlab.Branch{}
123 | // If we don't specifically want to query for all branches, just use the default branch and call it a day.
124 | if !g.allBranches {
125 | gitlabBranch, _, err := g.client.Branches.GetBranch(repo.RepositoryId, repo.Branch, nil)
126 | if err != nil {
127 | return nil, err
128 | }
129 | branches = append(branches, *gitlabBranch)
130 | return branches, nil
131 | }
132 | // Otherwise, scrape the ListBranches API.
133 | opt := &gitlab.ListBranchesOptions{
134 | ListOptions: gitlab.ListOptions{PerPage: 100},
135 | }
136 | for {
137 | gitlabBranches, resp, err := g.client.Branches.ListBranches(repo.RepositoryId, opt)
138 | if err != nil {
139 | return nil, err
140 | }
141 | for _, gitlabBranch := range gitlabBranches {
142 | branches = append(branches, *gitlabBranch)
143 | }
144 |
145 | if resp.NextPage == 0 {
146 | break
147 | }
148 | opt.Page = resp.NextPage
149 | }
150 | return branches, nil
151 | }
152 |
--------------------------------------------------------------------------------
/pkg/services/scm_provider/gitlab_test.go:
--------------------------------------------------------------------------------
1 | package scm_provider
2 |
3 | import (
4 | "context"
5 | "testing"
6 |
7 | "github.com/argoproj/applicationset/api/v1alpha1"
8 | "github.com/stretchr/testify/assert"
9 | )
10 |
11 | func TestGitlabListRepos(t *testing.T) {
12 | cases := []struct {
13 | name, proto, url string
14 | hasError, allBranches, includeSubgroups bool
15 | branches []string
16 | filters []v1alpha1.SCMProviderGeneratorFilter
17 | }{
18 | {
19 | name: "blank protocol",
20 | url: "git@gitlab.com:test-argocd-proton/argocd.git",
21 | branches: []string{"master"},
22 | },
23 | {
24 | name: "ssh protocol",
25 | proto: "ssh",
26 | url: "git@gitlab.com:test-argocd-proton/argocd.git",
27 | },
28 | {
29 | name: "https protocol",
30 | proto: "https",
31 | url: "https://gitlab.com/test-argocd-proton/argocd.git",
32 | },
33 | {
34 | name: "other protocol",
35 | proto: "other",
36 | hasError: true,
37 | },
38 | {
39 | name: "all branches",
40 | allBranches: true,
41 | url: "git@gitlab.com:test-argocd-proton/argocd.git",
42 | branches: []string{"master", "pipeline-1310077506"},
43 | },
44 | }
45 |
46 | for _, c := range cases {
47 | t.Run(c.name, func(t *testing.T) {
48 | provider, _ := NewGitlabProvider(context.Background(), "test-argocd-proton", "", "", c.allBranches, c.includeSubgroups)
49 | rawRepos, err := ListRepos(context.Background(), provider, c.filters, c.proto)
50 | if c.hasError {
51 | assert.NotNil(t, err)
52 | } else {
53 | checkRateLimit(t, err)
54 | assert.Nil(t, err)
55 | // Just check that this one project shows up. Not a great test but better thing nothing?
56 | repos := []*Repository{}
57 | branches := []string{}
58 | for _, r := range rawRepos {
59 | if r.Repository == "argocd" {
60 | repos = append(repos, r)
61 | branches = append(branches, r.Branch)
62 | }
63 | }
64 | assert.NotEmpty(t, repos)
65 | assert.Equal(t, c.url, repos[0].URL)
66 | for _, b := range c.branches {
67 | assert.Contains(t, branches, b)
68 | }
69 | }
70 | })
71 | }
72 | }
73 |
74 | func TestGitlabHasPath(t *testing.T) {
75 | host, _ := NewGitlabProvider(context.Background(), "test-argocd-proton", "", "", false, true)
76 | repo := &Repository{
77 | Organization: "test-argocd-proton",
78 | Repository: "argocd",
79 | Branch: "master",
80 | }
81 | ok, err := host.RepoHasPath(context.Background(), repo, "argocd")
82 | assert.Nil(t, err)
83 | assert.True(t, ok)
84 |
85 | ok, err = host.RepoHasPath(context.Background(), repo, "notathing")
86 | assert.Nil(t, err)
87 | assert.False(t, ok)
88 | }
89 |
--------------------------------------------------------------------------------
/pkg/services/scm_provider/mock.go:
--------------------------------------------------------------------------------
1 | package scm_provider
2 |
3 | import "context"
4 |
5 | type MockProvider struct {
6 | Repos []*Repository
7 | }
8 |
9 | var _ SCMProviderService = &MockProvider{}
10 |
11 | func (m *MockProvider) ListRepos(_ context.Context, _ string) ([]*Repository, error) {
12 | repos := []*Repository{}
13 | for _, candidateRepo := range m.Repos {
14 | found := false
15 | for _, alreadySetRepo := range repos {
16 | if alreadySetRepo.Repository == candidateRepo.Repository {
17 | found = true
18 | break
19 | }
20 | }
21 | if !found {
22 | repos = append(repos, candidateRepo)
23 | }
24 | }
25 | return repos, nil
26 | }
27 |
28 | func (*MockProvider) RepoHasPath(_ context.Context, repo *Repository, path string) (bool, error) {
29 | return path == repo.Repository, nil
30 | }
31 |
32 | func (m *MockProvider) GetBranches(_ context.Context, repo *Repository) ([]*Repository, error) {
33 | branchRepos := []*Repository{}
34 | for _, candidateRepo := range m.Repos {
35 | if candidateRepo.Repository == repo.Repository {
36 | found := false
37 | for _, alreadySetRepo := range branchRepos {
38 | if alreadySetRepo.Branch == candidateRepo.Branch {
39 | found = true
40 | break
41 | }
42 | }
43 | if !found {
44 | branchRepos = append(branchRepos, candidateRepo)
45 | }
46 | }
47 |
48 | }
49 | return branchRepos, nil
50 | }
51 |
--------------------------------------------------------------------------------
/pkg/services/scm_provider/types.go:
--------------------------------------------------------------------------------
1 | package scm_provider
2 |
3 | import (
4 | "context"
5 | "regexp"
6 | )
7 |
8 | // An abstract repository from an API provider.
9 | type Repository struct {
10 | Organization string
11 | Repository string
12 | URL string
13 | Branch string
14 | SHA string
15 | Labels []string
16 | RepositoryId interface{}
17 | }
18 |
19 | type SCMProviderService interface {
20 | ListRepos(context.Context, string) ([]*Repository, error)
21 | RepoHasPath(context.Context, *Repository, string) (bool, error)
22 | GetBranches(context.Context, *Repository) ([]*Repository, error)
23 | }
24 |
25 | // A compiled version of SCMProviderGeneratorFilter for performance.
26 | type Filter struct {
27 | RepositoryMatch *regexp.Regexp
28 | PathsExist []string
29 | LabelMatch *regexp.Regexp
30 | BranchMatch *regexp.Regexp
31 | FilterType FilterType
32 | }
33 |
34 | // A convenience type for indicating where to apply a filter
35 | type FilterType int64
36 |
37 | // The enum of filter types
38 | const (
39 | FilterTypeUndefined FilterType = iota
40 | FilterTypeBranch
41 | FilterTypeRepo
42 | )
43 |
--------------------------------------------------------------------------------
/pkg/utils/constants.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | const (
4 | ClusterListGeneratorKeyName = "cluster"
5 | UrlGeneratorKeyName = "url"
6 | )
7 |
--------------------------------------------------------------------------------
/pkg/utils/createOrUpdate.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "context"
5 | "fmt"
6 |
7 | argov1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
8 | "k8s.io/apimachinery/pkg/api/errors"
9 | "k8s.io/apimachinery/pkg/api/resource"
10 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
11 | "k8s.io/apimachinery/pkg/conversion"
12 | "k8s.io/apimachinery/pkg/fields"
13 | "k8s.io/apimachinery/pkg/labels"
14 | "sigs.k8s.io/controller-runtime/pkg/client"
15 | "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
16 | )
17 |
18 | // CreateOrUpdate overrides "sigs.k8s.io/controller-runtime" function
19 | // in sigs.k8s.io/controller-runtime/pkg/controller/controllerutil/controllerutil.go
20 | // to add equality for argov1alpha1.ApplicationDestination
21 | // argov1alpha1.ApplicationDestination has a private variable, so the default
22 | // implementation fails to compare it.
23 | //
24 | // CreateOrUpdate creates or updates the given object in the Kubernetes
25 | // cluster. The object's desired state must be reconciled with the existing
26 | // state inside the passed in callback MutateFn.
27 | //
28 | // The MutateFn is called regardless of creating or updating an object.
29 | //
30 | // It returns the executed operation and an error.
31 | func CreateOrUpdate(ctx context.Context, c client.Client, obj client.Object, f controllerutil.MutateFn) (controllerutil.OperationResult, error) {
32 |
33 | key := client.ObjectKeyFromObject(obj)
34 | if err := c.Get(ctx, key, obj); err != nil {
35 | if !errors.IsNotFound(err) {
36 | return controllerutil.OperationResultNone, err
37 | }
38 | if err := mutate(f, key, obj); err != nil {
39 | return controllerutil.OperationResultNone, err
40 | }
41 | if err := c.Create(ctx, obj); err != nil {
42 | return controllerutil.OperationResultNone, err
43 | }
44 | return controllerutil.OperationResultCreated, nil
45 | }
46 |
47 | existing := obj.DeepCopyObject()
48 | if err := mutate(f, key, obj); err != nil {
49 | return controllerutil.OperationResultNone, err
50 | }
51 |
52 | equality := conversion.EqualitiesOrDie(
53 | func(a, b resource.Quantity) bool {
54 | // Ignore formatting, only care that numeric value stayed the same.
55 | // TODO: if we decide it's important, it should be safe to start comparing the format.
56 | //
57 | // Uninitialized quantities are equivalent to 0 quantities.
58 | return a.Cmp(b) == 0
59 | },
60 | func(a, b metav1.MicroTime) bool {
61 | return a.UTC() == b.UTC()
62 | },
63 | func(a, b metav1.Time) bool {
64 | return a.UTC() == b.UTC()
65 | },
66 | func(a, b labels.Selector) bool {
67 | return a.String() == b.String()
68 | },
69 | func(a, b fields.Selector) bool {
70 | return a.String() == b.String()
71 | },
72 | func(a, b argov1alpha1.ApplicationDestination) bool {
73 | return a.Namespace == b.Namespace && a.Name == b.Name && a.Server == b.Server
74 | },
75 | )
76 |
77 | if equality.DeepEqual(existing, obj) {
78 | return controllerutil.OperationResultNone, nil
79 | }
80 |
81 | if err := c.Update(ctx, obj); err != nil {
82 | return controllerutil.OperationResultNone, err
83 | }
84 | return controllerutil.OperationResultUpdated, nil
85 | }
86 |
87 | // mutate wraps a MutateFn and applies validation to its result
88 | func mutate(f controllerutil.MutateFn, key client.ObjectKey, obj client.Object) error {
89 | if err := f(); err != nil {
90 | return err
91 | }
92 | if newKey := client.ObjectKeyFromObject(obj); key != newKey {
93 | return fmt.Errorf("MutateFn cannot mutate object name and/or object namespace")
94 | }
95 | return nil
96 | }
97 |
--------------------------------------------------------------------------------
/pkg/utils/map.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "fmt"
5 | )
6 |
7 | func CombineStringMaps(a map[string]string, b map[string]string) (map[string]string, error) {
8 | res := map[string]string{}
9 |
10 | for k, v := range a {
11 | res[k] = v
12 | }
13 |
14 | for k, v := range b {
15 | current, present := res[k]
16 | if present && current != v {
17 | return nil, fmt.Errorf("found duplicate key %s with different value, a: %s ,b: %s", k, current, v)
18 | }
19 | res[k] = v
20 | }
21 |
22 | return res, nil
23 | }
24 |
25 | // CombineStringMapsAllowDuplicates merges two maps. Where there are duplicates, take the latter map's value.
26 | func CombineStringMapsAllowDuplicates(a map[string]string, b map[string]string) (map[string]string, error) {
27 | res := map[string]string{}
28 |
29 | for k, v := range a {
30 | res[k] = v
31 | }
32 |
33 | for k, v := range b {
34 | res[k] = v
35 | }
36 |
37 | return res, nil
38 | }
39 |
--------------------------------------------------------------------------------
/pkg/utils/map_test.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "fmt"
5 | "testing"
6 |
7 | "github.com/stretchr/testify/assert"
8 | )
9 |
10 | func TestCombineStringMaps(t *testing.T) {
11 | testCases := []struct {
12 | name string
13 | left map[string]string
14 | right map[string]string
15 | expected map[string]string
16 | expectedErr error
17 | }{
18 | {
19 | name: "combines the maps",
20 | left: map[string]string{"foo": "bar"},
21 | right: map[string]string{"a": "b"},
22 | expected: map[string]string{"a": "b", "foo": "bar"},
23 | expectedErr: nil,
24 | },
25 | {
26 | name: "fails if keys are the same but value isn't",
27 | left: map[string]string{"foo": "bar", "a": "fail"},
28 | right: map[string]string{"a": "b", "c": "d"},
29 | expected: map[string]string{"a": "b", "foo": "bar"},
30 | expectedErr: fmt.Errorf("found duplicate key a with different value, a: fail ,b: b"),
31 | },
32 | {
33 | name: "pass if keys & values are the same",
34 | left: map[string]string{"foo": "bar", "a": "b"},
35 | right: map[string]string{"a": "b", "c": "d"},
36 | expected: map[string]string{"a": "b", "c": "d", "foo": "bar"},
37 | expectedErr: nil,
38 | },
39 | }
40 |
41 | for _, testCase := range testCases {
42 | testCaseCopy := testCase
43 |
44 | t.Run(testCaseCopy.name, func(t *testing.T) {
45 | t.Parallel()
46 |
47 | got, err := CombineStringMaps(testCaseCopy.left, testCaseCopy.right)
48 |
49 | if testCaseCopy.expectedErr != nil {
50 | assert.EqualError(t, err, testCaseCopy.expectedErr.Error())
51 | } else {
52 | assert.NoError(t, err)
53 | assert.Equal(t, testCaseCopy.expected, got)
54 | }
55 |
56 | })
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/pkg/utils/policy.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | // Policy allows to apply different rules to a set of changes.
4 | type Policy interface {
5 | Update() bool
6 | Delete() bool
7 | }
8 |
9 | // Policies is a registry of available policies.
10 | var Policies = map[string]Policy{
11 | "sync": &SyncPolicy{},
12 | "create-only": &CreateOnlyPolicy{},
13 | "create-update": &CreateUpdatePolicy{},
14 | }
15 |
16 | type SyncPolicy struct{}
17 |
18 | func (p *SyncPolicy) Update() bool {
19 | return true
20 | }
21 |
22 | func (p *SyncPolicy) Delete() bool {
23 | return true
24 | }
25 |
26 | type CreateUpdatePolicy struct{}
27 |
28 | func (p *CreateUpdatePolicy) Update() bool {
29 | return true
30 | }
31 |
32 | func (p *CreateUpdatePolicy) Delete() bool {
33 | return false
34 | }
35 |
36 | type CreateOnlyPolicy struct{}
37 |
38 | func (p *CreateOnlyPolicy) Update() bool {
39 | return false
40 | }
41 |
42 | func (p *CreateOnlyPolicy) Delete() bool {
43 | return false
44 | }
45 |
--------------------------------------------------------------------------------
/pkg/utils/testdata/gitlab-event.json:
--------------------------------------------------------------------------------
1 | {
2 | "object_kind": "push",
3 | "event_name": "push",
4 | "before": "e5ba5f6c13b64670048daa88e4c053d60b0e115a",
5 | "after": "bb0748feaa336d841c251017e4e374c22d0c8a98",
6 | "ref": "refs/heads/master",
7 | "checkout_sha": "bb0748feaa336d841c251017e4e374c22d0c8a98",
8 | "message": null,
9 | "user_id": 1,
10 | "user_name": "name",
11 | "user_username": "username",
12 | "user_email": "",
13 | "user_avatar": "",
14 | "project_id": 1,
15 | "project": {
16 | "id": 1,
17 | "name": "project",
18 | "description": "",
19 | "web_url": "https://gitlab/group/name",
20 | "avatar_url": null,
21 | "git_ssh_url": "ssh://git@gitlab:2222/group/name.git",
22 | "git_http_url": "https://gitlab/group/name.git",
23 | "namespace": "group",
24 | "visibility_level": 1,
25 | "path_with_namespace": "group/name",
26 | "default_branch": "master",
27 | "ci_config_path": null,
28 | "homepage": "https://gitlab/group/name",
29 | "url": "ssh://git@gitlab:2222/group/name.git",
30 | "ssh_url": "ssh://git@gitlab:2222/group/name.git",
31 | "http_url": "https://gitlab/group/name.git"
32 | },
33 | "commits": [
34 | {
35 | "id": "bb0748feaa336d841c251017e4e374c22d0c8a98",
36 | "message": "Test commit message\n",
37 | "timestamp": "2020-01-06T03:47:55Z",
38 | "url": "https://gitlab/group/name/commit/bb0748feaa336d841c251017e4e374c22d0c8a98",
39 | "author": {
40 | "name": "User",
41 | "email": "user@example.com"
42 | },
43 | "added": [
44 | "file.yaml"
45 | ],
46 | "modified": [
47 | ],
48 | "removed": [
49 |
50 | ]
51 | }
52 | ],
53 | "total_commits_count": 1,
54 | "push_options": {
55 | },
56 | "repository": {
57 | "name": "name",
58 | "url": "ssh://git@gitlab:2222/group/name.git",
59 | "description": "",
60 | "homepage": "https://gitlab/group/name",
61 | "git_http_url": "https://gitlab/group/name.git",
62 | "git_ssh_url": "ssh://git@gitlab:2222/group/name.git",
63 | "visibility_level": 10
64 | }
65 | }
--------------------------------------------------------------------------------
/pkg/utils/testdata/invalid-event.json:
--------------------------------------------------------------------------------
1 | {
2 | "event":"invalid"
3 | }
--------------------------------------------------------------------------------
/test/e2e/fixture/applicationsets/consequences.go:
--------------------------------------------------------------------------------
1 | package applicationsets
2 |
3 | import (
4 | "context"
5 | "encoding/json"
6 | "time"
7 |
8 | "github.com/argoproj/applicationset/api/v1alpha1"
9 | "github.com/argoproj/applicationset/test/e2e/fixture/applicationsets/utils"
10 | argov1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
11 | "github.com/argoproj/pkg/errors"
12 | log "github.com/sirupsen/logrus"
13 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
14 | )
15 |
16 | // this implements the "then" part of given/when/then
17 | type Consequences struct {
18 | context *Context
19 | actions *Actions
20 | }
21 |
22 | func (c *Consequences) Expect(e Expectation) *Consequences {
23 | return c.ExpectWithDuration(e, time.Duration(30)*time.Second)
24 | }
25 |
26 | func (c *Consequences) ExpectWithDuration(e Expectation, timeout time.Duration) *Consequences {
27 |
28 | // this invocation makes sure this func is not reported as the cause of the failure - we are a "test helper"
29 | c.context.t.Helper()
30 | var message string
31 | var state state
32 | for start := time.Now(); time.Since(start) < timeout; time.Sleep(3 * time.Second) {
33 | state, message = e(c)
34 | switch state {
35 | case succeeded:
36 | log.Infof("expectation succeeded: %s", message)
37 | return c
38 | case failed:
39 | c.context.t.Fatalf("failed expectation: %s", message)
40 | return c
41 | }
42 | log.Infof("expectation pending: %s", message)
43 | }
44 | c.context.t.Fatal("timeout waiting for: " + message)
45 | return c
46 | }
47 |
48 | func (c *Consequences) And(block func()) *Consequences {
49 | c.context.t.Helper()
50 | block()
51 | return c
52 | }
53 |
54 | func (c *Consequences) Given() *Context {
55 | return c.context
56 | }
57 |
58 | func (c *Consequences) When() *Actions {
59 | return c.actions
60 | }
61 |
62 | func (c *Consequences) app(name string) *argov1alpha1.Application {
63 | apps := c.apps()
64 |
65 | for index, app := range apps {
66 | if app.Name == name {
67 | return &apps[index]
68 | }
69 | }
70 |
71 | return nil
72 | }
73 |
74 | func (c *Consequences) apps() []argov1alpha1.Application {
75 |
76 | fixtureClient := utils.GetE2EFixtureK8sClient()
77 | list, err := fixtureClient.AppClientset.ArgoprojV1alpha1().Applications(utils.ArgoCDNamespace).List(context.Background(), metav1.ListOptions{})
78 | errors.CheckError(err)
79 |
80 | if list == nil {
81 | list = &argov1alpha1.ApplicationList{}
82 | }
83 |
84 | return list.Items
85 | }
86 |
87 | func (c *Consequences) applicationSet(applicationSetName string) *v1alpha1.ApplicationSet {
88 |
89 | fixtureClient := utils.GetE2EFixtureK8sClient()
90 | list, err := fixtureClient.AppSetClientset.Get(context.Background(), c.actions.context.name, metav1.GetOptions{})
91 | errors.CheckError(err)
92 |
93 | var appSet v1alpha1.ApplicationSet
94 |
95 | bytes, err := list.MarshalJSON()
96 | if err != nil {
97 | return &v1alpha1.ApplicationSet{}
98 | }
99 |
100 | err = json.Unmarshal(bytes, &appSet)
101 | if err != nil {
102 | return &v1alpha1.ApplicationSet{}
103 | }
104 |
105 | if appSet.Name == applicationSetName {
106 | return &appSet
107 | }
108 |
109 | return &v1alpha1.ApplicationSet{}
110 | }
111 |
--------------------------------------------------------------------------------
/test/e2e/fixture/applicationsets/context.go:
--------------------------------------------------------------------------------
1 | package applicationsets
2 |
3 | import (
4 | "testing"
5 | "time"
6 |
7 | . "github.com/argoproj/applicationset/test/e2e/fixture/applicationsets/utils"
8 | )
9 |
10 | // Context implements the "given" part of given/when/then
11 | type Context struct {
12 | t *testing.T
13 |
14 | // name is the ApplicationSet's name, created by a Create action
15 | name string
16 | }
17 |
18 | func Given(t *testing.T) *Context {
19 | EnsureCleanState(t)
20 | return &Context{t: t}
21 | }
22 |
23 | func (c *Context) When() *Actions {
24 | // in case any settings have changed, pause for 1s, not great, but fine
25 | time.Sleep(1 * time.Second)
26 | return &Actions{context: c}
27 | }
28 |
29 | func (c *Context) Sleep(seconds time.Duration) *Context {
30 | time.Sleep(seconds * time.Second)
31 | return c
32 | }
33 |
34 | func (c *Context) And(block func()) *Context {
35 | block()
36 | return c
37 | }
38 |
--------------------------------------------------------------------------------
/test/e2e/fixture/applicationsets/utils/cmd.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "os"
5 | "os/exec"
6 | "strings"
7 |
8 | argoexec "github.com/argoproj/pkg/exec"
9 | )
10 |
11 | func Run(workDir, name string, args ...string) (string, error) {
12 | return RunWithStdin("", workDir, name, args...)
13 | }
14 |
15 | func RunWithStdin(stdin, workDir, name string, args ...string) (string, error) {
16 | cmd := exec.Command(name, args...)
17 | if stdin != "" {
18 | cmd.Stdin = strings.NewReader(stdin)
19 | }
20 | cmd.Env = os.Environ()
21 | cmd.Dir = workDir
22 |
23 | return argoexec.RunCommandExt(cmd, argoexec.CmdOpts{})
24 | }
25 |
--------------------------------------------------------------------------------
/test/e2e/fixture/applicationsets/utils/errors.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "os"
5 | "runtime/debug"
6 |
7 | log "github.com/sirupsen/logrus"
8 | )
9 |
10 | const (
11 | // ErrorCommandSpecific is reserved for command specific indications
12 | ErrorCommandSpecific = 1
13 | // ErrorConnectionFailure is returned on connection failure to API endpoint
14 | ErrorConnectionFailure = 11
15 | // ErrorAPIResponse is returned on unexpected API response, i.e. authorization failure
16 | ErrorAPIResponse = 12
17 | // ErrorResourceDoesNotExist is returned when the requested resource does not exist
18 | ErrorResourceDoesNotExist = 13
19 | // ErrorGeneric is returned for generic error
20 | ErrorGeneric = 20
21 | )
22 |
23 | // CheckError logs a fatal message and exits with ErrorGeneric if err is not nil
24 | func CheckError(err error) {
25 | if err != nil {
26 | debug.PrintStack()
27 | Fatal(ErrorGeneric, err)
28 | }
29 | }
30 |
31 | // CheckErrorWithCode is a convenience function to exit if an error is non-nil and exit if it was
32 | func CheckErrorWithCode(err error, exitcode int) {
33 | if err != nil {
34 | Fatal(exitcode, err)
35 | }
36 | }
37 |
38 | // FailOnErr panics if there is an error. It returns the first value so you can use it if you cast it:
39 | // text := FailOrErr(Foo)).(string)
40 | func FailOnErr(v interface{}, err error) interface{} {
41 | CheckError(err)
42 | return v
43 | }
44 |
45 | // Fatal is a wrapper for logrus.Fatal() to exit with custom code
46 | func Fatal(exitcode int, args ...interface{}) {
47 | exitfunc := func() {
48 | os.Exit(exitcode)
49 | }
50 | log.RegisterExitHandler(exitfunc)
51 | log.Fatal(args...)
52 | }
53 |
54 | // Fatalf is a wrapper for logrus.Fatalf() to exit with custom code
55 | func Fatalf(exitcode int, format string, args ...interface{}) {
56 | exitfunc := func() {
57 | os.Exit(exitcode)
58 | }
59 | log.RegisterExitHandler(exitfunc)
60 | log.Fatalf(format, args...)
61 | }
62 |
--------------------------------------------------------------------------------