├── .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 | ![ApplicationSet controller vs Argo CD, interaction diagram](assets/Argo-CD-Integration/ApplicationSet-Argo-Relationship-v2.png) 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 | ![Add Webhook](./assets/webhook-config-pull-request.png "Add Webhook Pull Request") 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 | --------------------------------------------------------------------------------