├── .dockerignore ├── .editorconfig ├── .envrc ├── .github ├── actions │ └── build-image │ │ └── action.yaml ├── cr.yaml ├── ct.yaml ├── dependabot.yml ├── helm-unittest.sh ├── kubeconform.sh └── workflows │ ├── codeql.yml │ ├── nightly.yaml │ ├── on_pull-request_closed.yaml │ ├── on_pull-request_docs.yaml │ ├── on_pull-request_helm.yaml │ ├── on_pull_request.yaml │ ├── on_pull_request_go.yaml │ ├── on_push_to_main.yaml │ └── on_release_published.yaml ├── .gitignore ├── .golangci.yml ├── .mockery.yaml ├── .readthedocs.yaml ├── .secret.example ├── .tilt ├── terraform │ └── Tiltfile └── utils │ └── Tiltfile ├── .tool-versions ├── Earthfile ├── LICENSE ├── README.md ├── Tiltfile ├── charts ├── kubechecks-rbac │ ├── Chart.yaml │ ├── README.md │ ├── templates │ │ ├── role.yaml │ │ └── rolebinding.yaml │ ├── tests │ │ ├── role_test.yaml │ │ └── rolebinding_test.yaml │ ├── values.schema.json │ └── values.yaml └── kubechecks │ ├── .helmignore │ ├── Chart.yaml │ ├── templates │ ├── _helpers.tpl │ ├── clusterrole.yaml │ ├── clusterrolebinding.yaml │ ├── configmap.yaml │ ├── deployment.yaml │ ├── hpa.yaml │ ├── ingress.yaml │ ├── role.yaml │ ├── rolebinding.yaml │ ├── secrets.yaml │ ├── service.yaml │ └── serviceaccount.yaml │ ├── tests │ ├── autoscaling_test.yaml │ ├── basics.min-values.yaml │ ├── configmap_test.yaml │ ├── deployment_test.yaml │ └── ingress_test.yaml │ ├── values.schema.json │ └── values.yaml ├── cmd ├── controller.go ├── flags.go ├── flags_test.go ├── locations.go ├── locations_test.go ├── process.go ├── processors.go ├── root.go └── version.go ├── docs ├── architecture.md ├── contributing.md ├── gif │ └── kubechecks.gif ├── img │ ├── checkevent.png │ ├── client.png │ ├── eventflowdiagram.png │ ├── flow.png │ ├── kubechecks.gif │ ├── repo.png │ ├── tilt-1.png │ └── tilt-2.png ├── index.md ├── requirements.txt ├── usage.md └── usage.md.tpl ├── earthly.sh ├── go.mod ├── go.sum ├── hacks ├── env-to-docs.go └── exit-on-changed-files.sh ├── justfile ├── localdev ├── .gitignore ├── argocd │ ├── Tiltfile │ ├── argocd-application-controller-statefulset-patch.yaml │ ├── argocd-applicationset-controller-deployment-patch.yaml │ ├── argocd-cm-patch.yaml │ ├── argocd-initial-admin-secret.yaml │ ├── argocd-rbac-cm-patch.yaml │ ├── argocd-redis-deployment-patch.yaml │ ├── argocd-repo-server-deployment-patch.yaml │ ├── argocd-secret-patch.yaml │ ├── argocd-server-patch.yaml │ ├── delete-apps.sh │ ├── force-cleanup-apps.sh │ ├── kustomization.yaml │ └── manifests │ │ ├── base │ │ ├── application-controller │ │ │ ├── argocd-application-controller-network-policy.yaml │ │ │ ├── argocd-application-controller-role.yaml │ │ │ ├── argocd-application-controller-rolebinding.yaml │ │ │ ├── argocd-application-controller-sa.yaml │ │ │ ├── argocd-application-controller-statefulset.yaml │ │ │ ├── argocd-metrics.yaml │ │ │ └── kustomization.yaml │ │ ├── applicationset-controller │ │ │ ├── argocd-applicationset-controller-deployment.yaml │ │ │ ├── argocd-applicationset-controller-network-policy.yaml │ │ │ ├── argocd-applicationset-controller-role.yaml │ │ │ ├── argocd-applicationset-controller-rolebinding.yaml │ │ │ ├── argocd-applicationset-controller-sa.yaml │ │ │ ├── argocd-applicationset-controller-service.yaml │ │ │ └── kustomization.yaml │ │ ├── config │ │ │ ├── argocd-cm.yaml │ │ │ ├── argocd-cmd-params-cm.yaml │ │ │ ├── argocd-gpg-keys-cm.yaml │ │ │ ├── argocd-rbac-cm.yaml │ │ │ ├── argocd-secret.yaml │ │ │ ├── argocd-ssh-known-hosts-cm.yaml │ │ │ ├── argocd-tls-certs-cm.yaml │ │ │ └── kustomization.yaml │ │ ├── dex │ │ │ ├── argocd-dex-server-deployment.yaml │ │ │ ├── argocd-dex-server-network-policy.yaml │ │ │ ├── argocd-dex-server-role.yaml │ │ │ ├── argocd-dex-server-rolebinding.yaml │ │ │ ├── argocd-dex-server-sa.yaml │ │ │ ├── argocd-dex-server-service.yaml │ │ │ └── kustomization.yaml │ │ ├── kustomization.yaml │ │ ├── redis │ │ │ ├── argocd-redis-deployment.yaml │ │ │ ├── argocd-redis-network-policy.yaml │ │ │ ├── argocd-redis-rolebinding.yaml │ │ │ ├── argocd-redis-sa.yaml │ │ │ ├── argocd-redis-service.yaml │ │ │ └── kustomization.yaml │ │ ├── repo-server │ │ │ ├── argocd-repo-server-deployment.yaml │ │ │ ├── argocd-repo-server-network-policy.yaml │ │ │ ├── argocd-repo-server-sa.yaml │ │ │ ├── argocd-repo-server-service.yaml │ │ │ └── kustomization.yaml │ │ └── server │ │ │ ├── argocd-server-deployment.yaml │ │ │ ├── argocd-server-metrics.yaml │ │ │ ├── argocd-server-network-policy.yaml │ │ │ ├── argocd-server-role.yaml │ │ │ ├── argocd-server-rolebinding.yaml │ │ │ ├── argocd-server-sa.yaml │ │ │ ├── argocd-server-service.yaml │ │ │ └── kustomization.yaml │ │ ├── cluster-rbac │ │ ├── application-controller │ │ │ ├── argocd-application-controller-clusterrole.yaml │ │ │ ├── argocd-application-controller-clusterrolebinding.yaml │ │ │ └── kustomization.yaml │ │ ├── kustomization.yaml │ │ └── server │ │ │ ├── argocd-server-clusterrole.yaml │ │ │ ├── argocd-server-clusterrolebinding.yaml │ │ │ └── kustomization.yaml │ │ └── crds │ │ ├── application-crd.yaml │ │ ├── applicationset-crd.yaml │ │ ├── appproject-crd.yaml │ │ └── kustomization.yaml ├── kubechecks │ └── values.yaml ├── ngrok │ ├── Tiltfile │ ├── envoy.yaml │ ├── envoy_deploy.yaml │ ├── kustomization.yaml │ ├── tls.crt │ ├── tls.key │ └── update-url.sh ├── terraform │ ├── github │ │ ├── github.tf │ │ ├── main.tf │ │ └── outputs.tf │ ├── gitlab │ │ ├── gitlab.tf │ │ ├── main.tf │ │ └── outputs.tf │ ├── main.tf │ └── modules │ │ └── vcs_files │ │ ├── base_files │ │ └── apps │ │ │ ├── app-set-echo-server │ │ │ └── in-cluster │ │ │ │ ├── Chart.yaml │ │ │ │ └── values.yaml │ │ │ ├── echo-server │ │ │ └── in-cluster │ │ │ │ ├── Chart.yaml │ │ │ │ └── values.yaml │ │ │ ├── httpbin │ │ │ ├── base │ │ │ │ ├── deployment.yaml │ │ │ │ ├── kustomization.yaml │ │ │ │ ├── service.yaml │ │ │ │ └── serviceAccount.yaml │ │ │ └── overlays │ │ │ │ └── in-cluster │ │ │ │ └── kustomization.yaml │ │ │ └── httpdump │ │ │ ├── base │ │ │ ├── deployment.yaml │ │ │ ├── kustomization.yaml │ │ │ ├── service.yaml │ │ │ └── serviceAccount.yaml │ │ │ └── overlays │ │ │ ├── a │ │ │ └── kustomization.yaml │ │ │ └── b │ │ │ └── kustomization.yaml │ │ ├── main.tf │ │ ├── mr1_files │ │ └── apps │ │ │ └── httpbin │ │ │ └── overlays │ │ │ └── in-cluster │ │ │ ├── kustomization.yaml │ │ │ └── replica-patch.yaml │ │ ├── mr2_files │ │ └── apps │ │ │ └── echo-server │ │ │ └── in-cluster │ │ │ ├── Chart.yaml │ │ │ └── values.yaml │ │ ├── mr3_files │ │ └── apps │ │ │ └── httpbin │ │ │ └── base │ │ │ ├── external-ingress.yaml │ │ │ ├── internal-ingress.yaml │ │ │ └── kustomization.yaml │ │ ├── mr4_files │ │ └── apps │ │ │ ├── echo-server │ │ │ └── in-cluster │ │ │ │ ├── Chart.yaml │ │ │ │ └── values.yaml │ │ │ └── httpbin │ │ │ └── base │ │ │ ├── external-ingress.yaml │ │ │ ├── internal-ingress.yaml │ │ │ └── kustomization.yaml │ │ ├── mr5_files │ │ └── apps │ │ │ └── httpdump │ │ │ └── overlays │ │ │ └── a │ │ │ ├── kustomization.yaml │ │ │ └── replica-patch.yaml │ │ └── mr6_files │ │ └── apps │ │ └── httpdump │ │ └── base │ │ ├── external-ingress.yaml │ │ ├── internal-ingress.yaml │ │ └── kustomization.yaml ├── test_apps │ ├── Tiltfile │ ├── app-root.yaml │ ├── echo-server.yaml │ └── httpbin.yaml └── test_appsets │ ├── Tiltfile │ ├── echo-server.yaml │ └── httpdump.yaml ├── main.go ├── mkdocs.yml ├── mocks ├── affected_apps │ └── mocks │ │ ├── mock_Matcher.go │ │ └── mock_argoClient.go ├── generator │ └── mocks │ │ ├── mock_AppsGenerator.go │ │ └── mock_Generator.go ├── github_client │ └── mocks │ │ ├── mock_IssuesServices.go │ │ ├── mock_PullRequestsServices.go │ │ └── mock_RepositoriesServices.go ├── gitlab_client │ └── mocks │ │ ├── mock_CommitsServices.go │ │ ├── mock_MergeRequestsServices.go │ │ ├── mock_NotesServices.go │ │ ├── mock_PipelinesServices.go │ │ ├── mock_ProjectsServices.go │ │ └── mock_RepositoryFilesServices.go └── vcs │ └── mocks │ └── mock_Client.go ├── pkg ├── affected_apps │ ├── argocd_matcher.go │ ├── argocd_matcher_test.go │ ├── config_matcher.go │ ├── config_matcher_test.go │ ├── matcher.go │ ├── matcher_test.go │ ├── multi_matcher.go │ └── multi_matcher_test.go ├── aisummary │ ├── diff_summary.go │ └── openai_client.go ├── app_watcher │ ├── app_watcher.go │ ├── app_watcher_test.go │ ├── appset_watcher.go │ └── appset_watcher_test.go ├── appdir │ ├── app_directory.go │ ├── app_directory_test.go │ ├── appset_directory.go │ ├── appset_directory_test.go │ ├── vcstoargomap.go │ └── vcstoargomap_test.go ├── argo_client │ ├── applications.go │ ├── client.go │ ├── kustomize.go │ ├── manifests.go │ ├── manifests_test.go │ └── metrics.go ├── checks │ ├── diff │ │ ├── ai_summary.go │ │ ├── ai_summary_test.go │ │ └── diff.go │ ├── hooks │ │ ├── check.go │ │ ├── check_test.go │ │ └── grouped.go │ ├── kubeconform │ │ ├── check.go │ │ ├── validate.go │ │ └── validate_test.go │ ├── preupgrade │ │ ├── check.go │ │ └── kubepug.go │ ├── rego │ │ ├── check.go │ │ └── check_test.go │ └── types.go ├── commitState.go ├── config │ ├── config.go │ └── config_test.go ├── container │ └── main.go ├── events │ ├── check.go │ ├── check_test.go │ ├── metrics.go │ ├── runner.go │ ├── worker.go │ └── worker_test.go ├── generator │ ├── README.md │ ├── applicationsets.go │ ├── cluster.go │ ├── cluster_test.go │ ├── generator_spec_processor.go │ ├── generator_spec_processor_test.go │ ├── interface.go │ ├── list.go │ ├── list_test.go │ ├── matrix.go │ ├── matrix_test.go │ ├── merge.go │ ├── merge_test.go │ └── value_interpolation.go ├── git │ ├── manager.go │ ├── repo.go │ └── repo_test.go ├── kubernetes │ ├── api_client.go │ ├── api_eks_client.go │ ├── docs.go │ └── interface.go ├── kustomize │ ├── process.go │ └── process_test.go ├── msg │ ├── message.go │ └── message_test.go ├── repoUrl.go ├── repoUrl_test.go ├── repo_config │ ├── config.go │ ├── config_test.go │ ├── loader.go │ ├── loader_test.go │ └── testdata │ │ ├── 1 │ │ └── .kubechecks.yaml │ │ ├── 2 │ │ └── .kubechecks.yml │ │ └── 3 │ │ └── .kubechecks.yaml ├── server │ ├── hook_handler.go │ ├── server.go │ └── server_test.go ├── utils.go └── vcs │ ├── client.go │ ├── github_client │ ├── client.go │ ├── client_test.go │ ├── emoji.go │ ├── issue.go │ ├── message.go │ ├── pullrequest.go │ └── repo.go │ ├── gitlab_client │ ├── backoff.go │ ├── client.go │ ├── client_test.go │ ├── emoji.go │ ├── merge.go │ ├── message.go │ ├── pipeline.go │ ├── project.go │ └── status.go │ ├── repo.go │ └── types.go ├── telemetry ├── helpers.go ├── metric.go └── telemetry.go └── tools └── dump_crds ├── cmd ├── README.md ├── dumpcrd │ ├── dump.go │ └── root.go └── main.go ├── go.mod ├── go.sum └── internal └── logger └── logger.go /.dockerignore: -------------------------------------------------------------------------------- 1 | .env 2 | .env* 3 | .readthedocs.yaml 4 | .tool-versions 5 | Earthfile 6 | justfile 7 | mkdocs.yml 8 | ngrok.url 9 | README.md 10 | tilt_config.json 11 | Tiltfile 12 | 13 | dist/** 14 | .tilt/** 15 | .github/** 16 | charts/** 17 | dist/** 18 | docs/** 19 | localdev/** 20 | tools/** 21 | 22 | **/.terraform/* 23 | **/terraform.tfstate* -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.go] 2 | ij_any_blank_lines_after_imports = 1 3 | ij_go_add_parentheses_for_single_import = true 4 | ij_go_GROUP_CURRENT_PROJECT_IMPORTS = true 5 | ij_go_group_stdlib_imports = true 6 | ij_go_import_sorting = gofmt 7 | ij_go_move_all_imports_in_one_declaration = true 8 | ij_go_move_all_stdlib_imports_in_one_group = true 9 | ij_go_remove_redundant_import_aliases = true 10 | ij_go_use_back_quotes_for_imports = false 11 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | dotenv_if_exists 2 | -------------------------------------------------------------------------------- /.github/actions/build-image/action.yaml: -------------------------------------------------------------------------------- 1 | name: Build multiarch image 2 | description: Builds a multiarch image 3 | 4 | inputs: 5 | image_tag: 6 | description: The image tag 7 | required: true 8 | push: 9 | description: True to push image after building, false otherwise 10 | required: false 11 | default: "false" 12 | tag_latest: 13 | description: Tag latest as well as the provided tag 14 | default: "false" 15 | token: 16 | description: Github token 17 | required: true 18 | 19 | outputs: 20 | image: 21 | description: The full image name and tag 22 | value: ghcr.io/${{ github.repository }}:${{ inputs.image_tag }} 23 | 24 | runs: 25 | using: composite 26 | 27 | steps: 28 | - uses: docker/setup-qemu-action@v3 29 | with: 30 | image: tonistiigi/binfmt:latest 31 | platforms: all 32 | 33 | - uses: wistia/parse-tool-versions@v1.0 34 | 35 | - uses: earthly/actions-setup@v1 36 | with: { version: "v${{ env.EARTHLY_TOOL_VERSION }}" } 37 | 38 | - name: login to registry 39 | uses: docker/login-action@v3 40 | with: 41 | registry: ghcr.io 42 | username: ${{ github.repository_owner }} 43 | password: ${{ inputs.token }} 44 | 45 | - name: Build and push the Docker image 46 | shell: bash 47 | run: >- 48 | ./earthly.sh 49 | ${{ inputs.push == 'true' && '--push' || '' }} 50 | +docker-multiarch 51 | ${{ inputs.tag_latest != 'false' && format('--LATEST_IMAGE_NAME=ghcr.io/{0}:latest', github.repository) || '' }} 52 | --GIT_TAG=${{ inputs.image_tag }} 53 | --IMAGE_NAME=ghcr.io/${{ github.repository }}:${{ inputs.image_tag }} 54 | -------------------------------------------------------------------------------- /.github/cr.yaml: -------------------------------------------------------------------------------- 1 | git-repo: kubechecks 2 | -------------------------------------------------------------------------------- /.github/ct.yaml: -------------------------------------------------------------------------------- 1 | # See https://github.com/helm/chart-testing#configuration 2 | remote: origin 3 | target-branch: main 4 | helm-extra-args: --kube-version v1.24.0 5 | check-version-increment: true 6 | validate-maintainers: false 7 | additional-commands: 8 | - "./.github/kubeconform.sh 1.23.0 {{ .Path }}" 9 | - "./.github/kubeconform.sh 1.24.0 {{ .Path }}" 10 | - "./.github/kubeconform.sh 1.25.0 {{ .Path }}" 11 | - "./.github/kubeconform.sh 1.26.0 {{ .Path }}" 12 | - "./.github/helm-unittest.sh {{ .Path }}" 13 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - directory: "/" 4 | groups: 5 | minor-patch: 6 | update-types: 7 | - "minor" 8 | - "patch" 9 | ignore: 10 | # projects that aren't at 1.0 yet tend to have breaking changes, 11 | # so we don't batch those up with this "stable small changes" group 12 | - dependency-name: "*" 13 | versions: "<1" 14 | package-ecosystem: "gomod" 15 | schedule: 16 | interval: "weekly" 17 | -------------------------------------------------------------------------------- /.github/helm-unittest.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | DIR=$1 3 | 4 | echo "#######################" 5 | echo " helm-unittest.sh ${DIR}" 6 | echo "#######################" 7 | 8 | ############################################################################### 9 | # We always use Helm 3 as Helm 2 is now deprecated 10 | helm unittest "${1}" -------------------------------------------------------------------------------- /.github/kubeconform.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | KUBE_VERSION=$1 4 | DIR=$2 5 | 6 | echo "#######################" 7 | echo " kubeconform.sh ${DIR}" 8 | echo "#######################" 9 | 10 | if [ ! "$KUBE_VERSION" ] || [ ! "$DIR" ]; then 11 | echo "usage: $0 KUBE_VERSION DIR" 12 | exit 1 13 | fi 14 | 15 | if [ ! -d "${DIR}" ]; then 16 | echo "error: ${DIR} not found" 17 | exit 0 18 | fi 19 | 20 | function testFile () { 21 | dir=$1 22 | file=$2 23 | 24 | if [ -n "$file" ]; then 25 | file="-f${file}" 26 | fi 27 | 28 | if ! helm template "${file}" promagg "${dir}" \ 29 | | kubeconform \ 30 | -strict \ 31 | -kubernetes-version "${KUBE_VERSION}" \ 32 | -summary \ 33 | -verbose \ 34 | -schema-location default \ 35 | -schema-location 'https://raw.githubusercontent.com/datreeio/CRDs-catalog/main/{{.Group}}/{{.ResourceKind}}_{{.ResourceAPIVersion}}.json'; then 36 | return 1 37 | fi 38 | return 0 39 | } 40 | 41 | HAS_FAILING_TEST=0 42 | 43 | # Run a check on default values. 44 | echo "## Running kubeconform against $KUBE_VERSION with ./values.yaml" 45 | if ! testFile "${DIR}" "${DIR}/values.yaml"; then 46 | HAS_FAILING_TEST=1 47 | fi 48 | 49 | if [ -d "${DIR}/ci" ]; then 50 | FILES="${DIR}/ci/*" 51 | for FILE in $FILES; do 52 | echo "## Running kubeconform against $KUBE_VERSION with ${FILE}" 53 | if ! testFile "${DIR}" "${FILE}"; then 54 | HAS_FAILING_TEST=1 55 | fi 56 | done 57 | fi 58 | 59 | exit ${HAS_FAILING_TEST} -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | schedule: 9 | - cron: '39 5 * * 3' 10 | 11 | jobs: 12 | analyze: 13 | name: Analyze (${{ matrix.language }}) 14 | runs-on: ubuntu-latest 15 | timeout-minutes: 360 16 | permissions: 17 | # required for all workflows 18 | security-events: write 19 | 20 | # required to fetch internal or private CodeQL packs 21 | packages: read 22 | 23 | # only required for workflows in private repositories 24 | actions: read 25 | contents: read 26 | 27 | strategy: 28 | fail-fast: false 29 | matrix: 30 | include: 31 | - language: go 32 | build-mode: autobuild 33 | 34 | steps: 35 | - name: Checkout repository 36 | uses: actions/checkout@v4 37 | 38 | - name: Initialize CodeQL 39 | uses: github/codeql-action/init@v3 40 | with: 41 | languages: ${{ matrix.language }} 42 | build-mode: ${{ matrix.build-mode }} 43 | 44 | - if: matrix.build-mode == 'manual' 45 | run: | 46 | echo 'If you are using a "manual" build mode for one or more of the' \ 47 | 'languages you are analyzing, replace this with the commands to build' \ 48 | 'your code, for example:' 49 | echo ' make bootstrap' 50 | echo ' make release' 51 | exit 1 52 | 53 | - name: Perform CodeQL Analysis 54 | uses: github/codeql-action/analyze@v3 55 | with: 56 | category: "/language:${{matrix.language}}" 57 | -------------------------------------------------------------------------------- /.github/workflows/nightly.yaml: -------------------------------------------------------------------------------- 1 | name: maintenance 2 | 3 | on: 4 | schedule: 5 | - cron: "0 9 * * 1" # 9 am on Monday 6 | 7 | jobs: 8 | clean-up: 9 | runs-on: ubuntu-22.04 10 | 11 | permissions: 12 | issues: write 13 | pull-requests: write 14 | 15 | steps: 16 | - uses: actions/stale@v9.0.0 17 | with: 18 | days-before-stale: '120' 19 | -------------------------------------------------------------------------------- /.github/workflows/on_pull-request_closed.yaml: -------------------------------------------------------------------------------- 1 | name: PR closed 2 | 3 | on: 4 | pull_request: 5 | types: 6 | - closed 7 | 8 | env: 9 | FS_IMAGE: ghcr.io/${{ github.repository }} 10 | FS_TAG: 0.0.0-pr${{ github.event.pull_request.number }} 11 | 12 | jobs: 13 | remove-temp-image: 14 | runs-on: ubuntu-22.04 15 | continue-on-error: true 16 | 17 | # should match env.FS_TAG, in both pr-open.yaml and pr-close.yaml 18 | concurrency: pr-${{ github.event.pull_request.number }} 19 | 20 | permissions: 21 | packages: write 22 | pull-requests: write 23 | 24 | steps: 25 | - name: Delete all images without tags 26 | uses: bots-house/ghcr-delete-image-action@v1.1.0 27 | with: 28 | owner: zapier 29 | name: kubechecks 30 | token: ${{ secrets.GITHUB_TOKEN }} 31 | tag: ${{ env.FS_TAG }} 32 | 33 | - uses: mshick/add-pr-comment@v2 34 | with: 35 | message: | 36 | Temporary image deleted. 37 | -------------------------------------------------------------------------------- /.github/workflows/on_pull-request_docs.yaml: -------------------------------------------------------------------------------- 1 | name: docs ci 2 | on: 3 | pull_request: 4 | paths: 5 | - '.github/workflows/on_pull_request_docs.yaml' 6 | - 'Earthfile' 7 | - '*/**.go' 8 | - '*.go' 9 | - 'go.mod' 10 | - 'go.sum' 11 | - 'docs/usage.md*' 12 | jobs: 13 | lint-docs: 14 | runs-on: ubuntu-22.04 15 | steps: 16 | - uses: actions/checkout@v3 17 | 18 | - uses: wistia/parse-tool-versions@v1.0 19 | 20 | - uses: earthly/actions-setup@v1 21 | with: { version: "${{ env.EARTHLY_TOOL_VERSION }}" } 22 | 23 | - name: rebuild the docs 24 | run: ./earthly.sh +rebuild-docs 25 | 26 | - name: verify that the checked in file has not changed 27 | run: ./hacks/exit-on-changed-files.sh "Please run './earthly +rebuild-docs' and commit the results to this PR" 28 | -------------------------------------------------------------------------------- /.github/workflows/on_pull-request_helm.yaml: -------------------------------------------------------------------------------- 1 | name: helm ci 2 | on: 3 | pull_request: 4 | paths: 5 | - '.github/workflows/on_pull_request_helm.yaml' 6 | - 'Earthfile' 7 | - 'charts/**' 8 | 9 | jobs: 10 | ci-helm: 11 | runs-on: ubuntu-22.04 12 | steps: 13 | - uses: actions/checkout@v3 14 | 15 | - uses: wistia/parse-tool-versions@v1.0 16 | 17 | - uses: earthly/actions-setup@v1 18 | with: { version: "v${{ env.EARTHLY_TOOL_VERSION }}" } 19 | 20 | - run: ./earthly.sh +ci-helm 21 | -------------------------------------------------------------------------------- /.github/workflows/on_pull_request.yaml: -------------------------------------------------------------------------------- 1 | name: pr_build 2 | 3 | on: 4 | pull_request: 5 | paths: 6 | - '*' 7 | - '*/**' 8 | - '!README.md' 9 | - '!COPYING.LGPL-3' 10 | - '!.gitattributes' 11 | - '!.gitignore' 12 | types: 13 | - opened 14 | - reopened 15 | - synchronize 16 | 17 | env: 18 | FS_TAG: 0.0.0-pr${{ github.event.pull_request.number }} 19 | 20 | jobs: 21 | build: 22 | runs-on: ubuntu-22.04 23 | 24 | permissions: 25 | contents: read 26 | packages: write 27 | pull-requests: write 28 | 29 | steps: 30 | - uses: actions/checkout@v3 31 | 32 | - uses: ./.github/actions/build-image 33 | id: build-image 34 | with: 35 | push: '${{ github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name }}' 36 | image_tag: ${{ env.FS_TAG }} 37 | token: ${{ secrets.GITHUB_TOKEN }} 38 | 39 | - uses: mshick/add-pr-comment@v2 40 | with: 41 | message: | 42 | Temporary image available at `${{ steps.build-image.outputs.image }}`. 43 | if: github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name 44 | -------------------------------------------------------------------------------- /.github/workflows/on_pull_request_go.yaml: -------------------------------------------------------------------------------- 1 | name: go ci 2 | on: 3 | pull_request: 4 | paths: 5 | - '.github/workflows/on_pull_request_go.yaml' 6 | - 'Earthfile' 7 | - '*/**.go' 8 | - '*.go' 9 | - 'go.mod' 10 | - 'go.sum' 11 | jobs: 12 | ci-golang: 13 | runs-on: ubuntu-22.04 14 | steps: 15 | - uses: actions/checkout@v3 16 | 17 | - uses: djeebus/parse-tool-versions@v2.1 18 | 19 | - uses: earthly/actions-setup@v1 20 | with: { version: "v${{ env.EARTHLY }}" } 21 | 22 | - run: ./earthly.sh +ci-golang 23 | -------------------------------------------------------------------------------- /.github/workflows/on_push_to_main.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | 6 | env: 7 | FS_IMAGE: ghcr.io/${{ github.repository }} 8 | 9 | jobs: 10 | release-docker: 11 | runs-on: ubuntu-22.04 12 | 13 | permissions: 14 | contents: read 15 | packages: write 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | with: 20 | fetch-depth: 0 # necessary for 'git describe' to work 21 | 22 | - run: echo "GIT_TAG=$(git describe --tags)" >> $GITHUB_ENV 23 | 24 | - uses: ./.github/actions/build-image 25 | with: 26 | push: 'true' 27 | image_tag: ${{ env.GIT_TAG }} 28 | token: ${{ secrets.GITHUB_TOKEN }} 29 | 30 | release-helm: 31 | runs-on: ubuntu-22.04 32 | 33 | permissions: 34 | contents: write 35 | 36 | steps: 37 | - name: checkout the source code 38 | uses: actions/checkout@v3 39 | 40 | - uses: wistia/parse-tool-versions@v1.0 41 | 42 | - uses: earthly/actions-setup@v1 43 | with: { version: "v${{ env.EARTHLY_TOOL_VERSION }}" } 44 | 45 | - name: Build and push the helm charts 46 | run: | 47 | ./earthly.sh \ 48 | --push \ 49 | +release-helm \ 50 | --repo_owner ${{ github.repository_owner }} \ 51 | --token ${{ secrets.GITHUB_TOKEN }} 52 | 53 | -------------------------------------------------------------------------------- /.github/workflows/on_release_published.yaml: -------------------------------------------------------------------------------- 1 | name: release new version 2 | 3 | on: 4 | workflow_dispatch: 5 | release: 6 | types: [published] 7 | 8 | jobs: 9 | release-docker: 10 | runs-on: ubuntu-22.04 11 | 12 | permissions: 13 | contents: read 14 | packages: write 15 | 16 | steps: 17 | - uses: actions/checkout@v3 18 | 19 | - uses: ./.github/actions/build-image 20 | with: 21 | image_tag: ${{ github.ref_name }} 22 | tag_latest: true 23 | token: ${{ secrets.GITHUB_TOKEN }} 24 | push: true 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/** 2 | 3 | arghh 4 | .env 5 | 6 | # localdev temporary files 7 | localdev/**/*.db 8 | localdev/**/*.lock 9 | localdev/Dockerfile-* 10 | localdev/**/localdev.auto.tfvars 11 | # localdev/ngrok.url 12 | # .ngrok.env 13 | # localdev/manifests/tfbuddy.env 14 | localdev/terraform/terraform.auto.tfvars 15 | localdev/terraform/**/parent.auto.tfvars 16 | build/ 17 | 18 | **/.terraform/ 19 | 20 | ngrok.url 21 | 22 | tilt_config.json 23 | .vscode 24 | localdev/terraform/gitlab/project.url 25 | *.tgz 26 | *.DS_Store 27 | /kubechecks 28 | localdev/terraform/github/project.url 29 | .secret 30 | .arg 31 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | run: 3 | tests: false 4 | 5 | linters: 6 | enable: 7 | - bodyclose 8 | - durationcheck 9 | - errcheck 10 | - govet 11 | - ineffassign 12 | - staticcheck 13 | - sloglint 14 | - unparam 15 | - unused 16 | - usestdlibvars 17 | - usetesting 18 | -------------------------------------------------------------------------------- /.mockery.yaml: -------------------------------------------------------------------------------- 1 | with-expecter: true 2 | dir: "mocks/{{.PackageName}}/mocks" 3 | packages: 4 | github.com/zapier/kubechecks/pkg/vcs: 5 | config: 6 | all: true 7 | github.com/zapier/kubechecks/pkg/vcs/github_client: 8 | # place your package-specific config here 9 | config: 10 | all: true 11 | github.com/zapier/kubechecks/pkg/vcs/gitlab_client: 12 | # place your package-specific config here 13 | config: 14 | all: true 15 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # Read the Docs configuration file for MkDocs projects 2 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 3 | 4 | # Required 5 | version: 2 6 | 7 | # Set the version of Python and other tools you might need 8 | build: 9 | os: ubuntu-22.04 10 | tools: 11 | python: "3.11" 12 | 13 | mkdocs: 14 | configuration: mkdocs.yml 15 | 16 | # Optionally declare the Python requirements required to build your docs 17 | python: 18 | install: 19 | - requirements: docs/requirements.txt -------------------------------------------------------------------------------- /.secret.example: -------------------------------------------------------------------------------- 1 | GITLAB_TOKEN=xyz 2 | OPENAI_API_TOKEN=xyz 3 | GITHUB_TOKEN=xyz 4 | KUBECHECKS_WEBHOOK_SECRET=xyz -------------------------------------------------------------------------------- /.tilt/utils/Tiltfile: -------------------------------------------------------------------------------- 1 | 2 | def check_env_set(key): 3 | if not os.getenv(key): 4 | fail("{} not set".format(key)) -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | earthly 0.8.15 2 | golang 1.24.2 3 | golangci-lint 2.1.1 4 | helm 3.16.3 5 | helm-cr 1.6.1 6 | helm-ct 3.11.0 7 | kubeconform 0.6.7 8 | kustomize 5.6.0 9 | mockery 2.46.3 10 | tilt 0.33.2 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![logo-transparent](https://github.com/zapier/kubechecks/assets/33305772/ea7947eb-b7db-4e08-b047-cf16ab22a1d3) 2 | # kubechecks - Fearless Kubernetes App Updates 3 | 4 | [![Documentation Status](https://readthedocs.org/projects/kubechecks/badge/?version=latest)](https://kubechecks.readthedocs.io/en/latest/?badge=latest) 5 | 6 | `kubechecks` allows users of Github and Gitlab to see exactly what their changes will affect on their current ArgoCD deployments, as well as automatically run various conformance test suites prior to merge. 7 | 8 | ## Pull/Merge Request driven checks 9 | 10 | When using ArgoCD, it can be difficult to tell just how your Pull/Merge Request (PR/MR) will impact your live deployment. `kubechecks` was designed to address this problem; every time a new PR/MR is created, `kubechecks` will automatically determine what's changed and how it will impact your `main`/default branch's state, informing you of the details directly on the PR/MR. As a bonus, it also lints and checks your Kubernetes manifests to let you know ahead of time if something is outdated, invalid, or otherwise not good practice. 11 | 12 | ![Demo](./docs/gif/kubechecks.gif) 13 | 14 | ### How it works 15 | 16 | This tool provides a server function that processes webhooks from Gitlab/Github, clones the repository at the `HEAD` SHA of the PR/MR, and runs various check suites, commenting the output of each check in a single comment on your PR/MR. `kubechecks` talks directly to ArgoCD to get the live state of your deployments and talks directly to ArgoCD's repo server to generate the new resources to ensure that you have the most accurate information about how your changes will affect your production code. 17 | 18 | ### Architecture 19 | 20 | `kubechecks` consists of a high level structures for communicating with your VCS provider of choice, representing a PR/MR internally, and running checks for that code; read more in [the docs](./docs/architecture.md) 21 | 22 | ![](./docs/img/flow.png) 23 | 24 | ## Installation 25 | 26 | ### Helm 27 | 28 | See [Installation Docs](https://kubechecks.readthedocs.io/en/stable/usage/) 29 | 30 | ## Contributing 31 | 32 | The [contributing](https://kubechecks.readthedocs.io/en/stable/contributing/) has everything you need to start working on `kubechecks`. 33 | 34 | ## Documentation 35 | 36 | To learn more about `kubechecks` [go to the complete documentation](https://kubechecks.readthedocs.io/). 37 | 38 | ## Contributors 39 | 40 | ![contributors](https://contrib.rocks/image?repo=zapier/kubechecks) 41 | 42 | --- 43 | 44 | Made by SRE Team @ ![zapier](https://zapier-media.s3.amazonaws.com/zapier/images/logo60orange.png) 45 | -------------------------------------------------------------------------------- /charts/kubechecks-rbac/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: kubechecks-rbac 3 | description: A Helm chart for kubechecks Role and RoleBinding 4 | version: 0.4.5 5 | type: application 6 | maintainers: 7 | - name: zapier 8 | -------------------------------------------------------------------------------- /charts/kubechecks-rbac/README.md: -------------------------------------------------------------------------------- 1 | # kubechecks-rbac 2 | 3 | This chart deploys the Cluster Role and Cluster Role binding for the kubechecks running outside of existing cluster. 4 | 5 | It is not required if you're operating all within the same cluster. 6 | 7 | -------------------------------------------------------------------------------- /charts/kubechecks-rbac/templates/role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: {{ .Values.clusterRoleName | default "kubechecks-remote-clusterrole" }} 5 | rules: 6 | - apiGroups: ['argoproj.io'] 7 | resources: ['applications', 'appprojects', 'applicationsets', 'services'] 8 | verbs: ['get', 'list', 'watch'] 9 | - apiGroups: [''] # The core API group, which is indicated by an empty string 10 | resources: ['secrets'] 11 | verbs: ['get', 'list', 'watch'] -------------------------------------------------------------------------------- /charts/kubechecks-rbac/templates/rolebinding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | name: {{ .Values.clusterRoleBindingName | default "kubechecks-remote-role-binding" }} 5 | namespace: {{ .Values.namespace | default "argocd" }} 6 | subjects: 7 | - kind: Group 8 | apiGroup: rbac.authorization.k8s.io 9 | name: {{ .Values.clusterRoleBindingGroup | default "kubechecks-remote-group" }} 10 | roleRef: 11 | kind: ClusterRole 12 | name: {{ .Values.clusterRoleName | default "kubechecks-remote-role" }} 13 | apiGroup: rbac.authorization.k8s.io 14 | -------------------------------------------------------------------------------- /charts/kubechecks-rbac/tests/role_test.yaml: -------------------------------------------------------------------------------- 1 | suite: role tests 2 | 3 | templates: 4 | - role.yaml 5 | 6 | tests: 7 | - it: should create a Role with the correct name 8 | set: 9 | clusterRoleName: "kubechecks-test-role" 10 | asserts: 11 | - isKind: 12 | of: ClusterRole 13 | - equal: 14 | path: metadata.name 15 | value: kubechecks-test-role 16 | -------------------------------------------------------------------------------- /charts/kubechecks-rbac/tests/rolebinding_test.yaml: -------------------------------------------------------------------------------- 1 | suite: role binding tests 2 | 3 | templates: 4 | - rolebinding.yaml 5 | 6 | tests: 7 | - it: should create a RoleBinding with the correct name with EKS IAM role 8 | set: 9 | clusterRoleBindingName: "kubechecks-test-rolebinding-rbac" 10 | clusterRoleBindingGroup: "kubechecks-remote-group" 11 | asserts: 12 | - isKind: 13 | of: ClusterRoleBinding 14 | - equal: 15 | path: metadata.name 16 | value: kubechecks-test-rolebinding-rbac 17 | -------------------------------------------------------------------------------- /charts/kubechecks-rbac/values.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "title": "Kubechecks Values Schema", 4 | "type": "object", 5 | "properties": { 6 | "clusterRoleName": { 7 | "type": "string", 8 | "description": "The name of the Cluster Role to be created.", 9 | "default": "kubechecks-remote-role" 10 | }, 11 | "clusterRoleBindingName": { 12 | "type": "string", 13 | "description": "The name of the ClusterRoleBinding to be created.", 14 | "default": "kubechecks-remote-role-binding" 15 | }, 16 | "clusterRoleBindingGroup": { 17 | "type": "string", 18 | "description": "The name of the Group to be created.", 19 | "default": "kubechecks-remote-group" 20 | }, 21 | "namespace": { 22 | "type": "string", 23 | "description": "The namespace where the Role and RoleBinding will be created.", 24 | "default": "argocd" 25 | } 26 | }, 27 | "required": ["clusterRoleName", "clusterRoleBindingName", "clusterRoleBindingGroup", "namespace"], 28 | "additionalProperties": false 29 | } 30 | -------------------------------------------------------------------------------- /charts/kubechecks-rbac/values.yaml: -------------------------------------------------------------------------------- 1 | clusterRoleName: "kubechecks-remote-role" 2 | clusterRoleBindingName: "kubechecks-remote-role-binding" 3 | clusterRoleBindingGroup: "kubechecks-remote-group" 4 | 5 | # namespace to create the ClusterRole and RoleBinding, this has to match the argocd is operating. 6 | namespace: "argocd" 7 | -------------------------------------------------------------------------------- /charts/kubechecks/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | tests 25 | -------------------------------------------------------------------------------- /charts/kubechecks/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: kubechecks 3 | description: A Helm chart for kubechecks 4 | version: 0.5.5 5 | type: application 6 | maintainers: 7 | - name: zapier 8 | -------------------------------------------------------------------------------- /charts/kubechecks/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Expand the name of the chart. 3 | */}} 4 | {{- define "kubechecks.name" -}} 5 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} 6 | {{- end }} 7 | 8 | {{/* 9 | Create a default fully qualified app name. 10 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 11 | If release name contains chart name it will be used as a full name. 12 | */}} 13 | {{- define "kubechecks.fullname" -}} 14 | {{- if .Values.fullnameOverride }} 15 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} 16 | {{- else }} 17 | {{- $name := default .Chart.Name .Values.nameOverride }} 18 | {{- if contains $name .Release.Name }} 19 | {{- .Release.Name | trunc 63 | trimSuffix "-" }} 20 | {{- else }} 21 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} 22 | {{- end }} 23 | {{- end }} 24 | {{- end }} 25 | 26 | {{/* 27 | Create chart name and version as used by the chart label. 28 | */}} 29 | {{- define "kubechecks.chart" -}} 30 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} 31 | {{- end }} 32 | 33 | {{/* 34 | Common labels 35 | */}} 36 | {{- define "kubechecks.labels" -}} 37 | helm.sh/chart: {{ include "kubechecks.chart" . }} 38 | {{ include "kubechecks.selectorLabels" . }} 39 | app.kubernetes.io/version: {{ .Values.deployment.image.tag }} 40 | app.kubernetes.io/managed-by: {{ .Release.Service }} 41 | {{- with .Values.commonLabels }} 42 | {{ . | toYaml }} 43 | {{- end }} 44 | {{- end }} 45 | 46 | {{/* 47 | {{- end }} 48 | 49 | {{/* 50 | Selector labels 51 | */}} 52 | {{- define "kubechecks.selectorLabels" -}} 53 | app.kubernetes.io/name: {{ include "kubechecks.name" . }} 54 | app.kubernetes.io/instance: {{ .Release.Name }} 55 | {{- end }} 56 | 57 | {{/* 58 | Create the name of the service account to use 59 | */}} 60 | {{- define "kubechecks.serviceAccountName" -}} 61 | {{- tpl .Values.serviceAccount.name . }} 62 | {{- end }} 63 | 64 | {{/* 65 | Create the name of the secret file to use 66 | */}} 67 | {{- define "kubechecks.secretsName" -}} 68 | {{- tpl .Values.secrets.name . }} 69 | {{- end -}} 70 | -------------------------------------------------------------------------------- /charts/kubechecks/templates/clusterrole.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: {{ include "kubechecks.fullname" . }} 5 | rules: 6 | - apiGroups: ['argoproj.io'] 7 | resources: ['applications', 'appprojects', 'applicationsets', 'services'] 8 | verbs: ['get', 'list', 'watch'] 9 | - apiGroups: [''] # The core API group, which is indicated by an empty string 10 | resources: ['secrets'] 11 | verbs: ['get', 'list', 'watch'] 12 | -------------------------------------------------------------------------------- /charts/kubechecks/templates/clusterrolebinding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | name: {{ include "kubechecks.fullname" . }} 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: ClusterRole 8 | name: {{ include "kubechecks.fullname" . }} 9 | subjects: 10 | - kind: ServiceAccount 11 | name: {{ include "kubechecks.serviceAccountName" . }} 12 | namespace: {{ .Release.Namespace }} 13 | -------------------------------------------------------------------------------- /charts/kubechecks/templates/configmap.yaml: -------------------------------------------------------------------------------- 1 | {{ if .Values.configMap.create }} 2 | apiVersion: v1 3 | kind: ConfigMap 4 | metadata: 5 | name: {{ include "kubechecks.name" . }} 6 | labels: 7 | {{ include "kubechecks.labels" . | indent 4 }} 8 | data: 9 | {{- range $key, $value := .Values.configMap.env }} 10 | {{ $key | quote }}: {{ $value | quote }} 11 | {{- end }} 12 | {{ end }} 13 | -------------------------------------------------------------------------------- /charts/kubechecks/templates/hpa.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.autoscaling.create }} 2 | {{ $apiVersion := "" }} 3 | {{- if .Capabilities.APIVersions.Has "autoscaling/v2" }} 4 | {{ $apiVersion = "autoscaling/v2" }} 5 | {{- else if .Capabilities.APIVersions.Has "autoscaling/v2beta2" }} 6 | {{ $apiVersion = "autoscaling/v2beta2" }} 7 | {{- else if .Capabilities.APIVersions.Has "autoscaling/v2beta1" }} 8 | {{ $apiVersion = "autoscaling/v2beta1" }} 9 | {{- else }} 10 | {{ fail "server has no support for autoscaling" }} 11 | {{- end }} 12 | apiVersion: {{ $apiVersion }} 13 | kind: HorizontalPodAutoscaler 14 | metadata: 15 | name: {{ include "kubechecks.fullname" . }} 16 | labels: 17 | {{- include "kubechecks.labels" . | nindent 4 }} 18 | spec: 19 | scaleTargetRef: 20 | apiVersion: apps/v1 21 | kind: Deployment 22 | name: {{ include "kubechecks.fullname" . }} 23 | minReplicas: {{ .Values.autoscaling.minReplicas }} 24 | maxReplicas: {{ .Values.autoscaling.maxReplicas }} 25 | metrics: 26 | {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} 27 | - type: Resource 28 | resource: 29 | name: cpu 30 | {{- if or (eq $apiVersion "autoscaling/v2") (eq $apiVersion "autoscaling/v2beta2") }} 31 | target: 32 | type: Utilization 33 | averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} 34 | {{- else }} 35 | targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} 36 | {{- end }} 37 | {{- end }} 38 | {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} 39 | - type: Resource 40 | resource: 41 | name: memory 42 | {{- if or (eq $apiVersion "autoscaling/v2") (eq $apiVersion "autoscaling/v2beta2") }} 43 | target: 44 | type: Utilization 45 | averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} 46 | {{- else }} 47 | targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} 48 | {{- end }} 49 | {{- end }} 50 | {{- end }} 51 | -------------------------------------------------------------------------------- /charts/kubechecks/templates/ingress.yaml: -------------------------------------------------------------------------------- 1 | {{- $servicePort := .Values.service.port -}} 2 | {{- $serviceName := include "kubechecks.fullname" . -}} 3 | {{- if .Values.ingress.create }} 4 | --- 5 | apiVersion: networking.k8s.io/v1 6 | kind: Ingress 7 | metadata: 8 | name: {{ include "kubechecks.fullname" . }} 9 | annotations: 10 | {{ toYaml .Values.ingress.annotations | indent 4 }} 11 | labels: 12 | {{ include "kubechecks.labels" . | indent 4 }} 13 | {{- if .Values.ingress.labels }} 14 | {{ toYaml .Values.ingress.labels | indent 4 }} 15 | {{- end }} 16 | spec: 17 | {{- with .Values.ingress.className }} 18 | ingressClassName: {{ . }} 19 | {{- end}} 20 | rules: 21 | {{- range $host, $paths := .Values.ingress.hosts }} 22 | - host: {{ $host }} 23 | http: 24 | paths: 25 | {{- range $path := $paths }} 26 | {{- range $path }} 27 | - path: {{ .path }} 28 | pathType: {{ .pathType }} 29 | backend: 30 | service: 31 | name: {{ $serviceName }} 32 | port: 33 | number: {{ $servicePort }} 34 | {{- end -}} 35 | {{- end -}} 36 | {{- end -}} 37 | {{- if .Values.ingress.tls }} 38 | tls: 39 | {{ toYaml .Values.ingress.tls | indent 4 }} 40 | {{- end -}} 41 | {{- end -}} 42 | 43 | -------------------------------------------------------------------------------- /charts/kubechecks/templates/role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: Role 3 | metadata: 4 | name: {{ include "kubechecks.fullname" . }} 5 | namespace: {{ .Values.argocd.namespace }} 6 | rules: 7 | - apiGroups: 8 | - "" 9 | resources: 10 | - configmaps 11 | - secrets 12 | verbs: 13 | - get 14 | - list 15 | - watch 16 | -------------------------------------------------------------------------------- /charts/kubechecks/templates/rolebinding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: RoleBinding 3 | metadata: 4 | name: {{ include "kubechecks.fullname" . }} 5 | namespace: {{ .Values.argocd.namespace }} 6 | roleRef: 7 | kind: Role 8 | name: {{ include "kubechecks.fullname" . }} 9 | apiGroup: rbac.authorization.k8s.io 10 | subjects: 11 | - kind: ServiceAccount 12 | name: {{ include "kubechecks.serviceAccountName" . }} 13 | namespace: {{ .Release.Namespace }} 14 | -------------------------------------------------------------------------------- /charts/kubechecks/templates/secrets.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.secrets.create -}} 2 | apiVersion: v1 3 | kind: Secret 4 | metadata: 5 | name: "{{ include "kubechecks.secretsName" . }}" 6 | labels: 7 | {{- include "kubechecks.labels" . | nindent 4 }} 8 | type: Opaque 9 | data: 10 | {{- range $k, $v := .Values.secrets.env }} 11 | {{ $k| quote }}: {{ $v | b64enc | quote }} 12 | {{- end }} 13 | {{- end }} 14 | -------------------------------------------------------------------------------- /charts/kubechecks/templates/service.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.service.create -}} 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | name: {{ include "kubechecks.fullname" . }} 6 | annotations: 7 | {{ .Values.service.annotations | toYaml | nindent 4 }} 8 | labels: 9 | {{- include "kubechecks.labels" . | nindent 4 }} 10 | spec: 11 | type: {{ .Values.service.type }} 12 | ports: 13 | - port: {{ .Values.service.port }} 14 | targetPort: {{ .Values.service.name }} 15 | protocol: TCP 16 | name: {{ .Values.service.name }} 17 | selector: 18 | {{- include "kubechecks.selectorLabels" . | nindent 4 }} 19 | {{- end }} 20 | -------------------------------------------------------------------------------- /charts/kubechecks/templates/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.serviceAccount.create -}} 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: {{ include "kubechecks.serviceAccountName" . }} 6 | labels: 7 | {{- include "kubechecks.labels" . | nindent 4 }} 8 | {{- with .Values.serviceAccount.annotations }} 9 | annotations: 10 | {{ toYaml . | indent 4 }} 11 | {{- end}} 12 | {{- end }} 13 | -------------------------------------------------------------------------------- /charts/kubechecks/tests/basics.min-values.yaml: -------------------------------------------------------------------------------- 1 | configMap: 2 | create: true 3 | env: 4 | KUBECHECKS_ARGOCD_API_INSECURE: "false" 5 | KUBECHECKS_TEST_VALUE: "Hello" 6 | 7 | ingress: 8 | create: true 9 | annotations: 10 | kubernetes.io/ingress.class: an-ingress-class 11 | -------------------------------------------------------------------------------- /charts/kubechecks/tests/configmap_test.yaml: -------------------------------------------------------------------------------- 1 | suite: basics 2 | 3 | templates: 4 | - "*.yaml" 5 | 6 | values: [basics.min-values.yaml] 7 | 8 | tests: 9 | - it: should render 10 | template: templates/configmap.yaml 11 | chart: 12 | version: 0.1.0 13 | set: 14 | deployment: 15 | image: 16 | tag: 1.0.0 17 | release: 18 | name: kubechecks 19 | asserts: 20 | - isKind: 21 | of: ConfigMap 22 | - equal: 23 | path: data 24 | value: 25 | KUBECHECKS_ARGOCD_API_INSECURE: "false" 26 | KUBECHECKS_TEST_VALUE: Hello 27 | - equal: 28 | path: metadata.labels 29 | value: 30 | app.kubernetes.io/instance: kubechecks 31 | app.kubernetes.io/managed-by: Helm 32 | app.kubernetes.io/name: kubechecks 33 | app.kubernetes.io/version: 1.0.0 34 | helm.sh/chart: kubechecks-0.1.0 35 | -------------------------------------------------------------------------------- /charts/kubechecks/tests/ingress_test.yaml: -------------------------------------------------------------------------------- 1 | suite: basics 2 | 3 | templates: 4 | - "*.yaml" 5 | 6 | values: [basics.min-values.yaml] 7 | 8 | tests: 9 | - it: should render 10 | template: templates/ingress.yaml 11 | chart: 12 | version: 999.9.9 13 | release: 14 | name: kubechecks 15 | asserts: 16 | - isKind: 17 | of: Ingress 18 | - equal: 19 | path: spec.rules[0] 20 | value: 21 | host: kubechecks.local 22 | http: 23 | paths: 24 | - backend: 25 | service: 26 | name: kubechecks 27 | port: 28 | number: 8080 29 | path: /hooks 30 | pathType: Prefix 31 | - equal: 32 | path: metadata.annotations 33 | value: 34 | kubernetes.io/ingress.class: an-ingress-class 35 | -------------------------------------------------------------------------------- /cmd/process.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | 7 | "github.com/argoproj/argo-cd/v2/common" 8 | "github.com/rs/zerolog/log" 9 | "github.com/spf13/cobra" 10 | 11 | "github.com/zapier/kubechecks/pkg" 12 | "github.com/zapier/kubechecks/pkg/config" 13 | "github.com/zapier/kubechecks/pkg/container" 14 | "github.com/zapier/kubechecks/pkg/server" 15 | ) 16 | 17 | var processCmd = &cobra.Command{ 18 | Use: "process", 19 | Short: "Process a pull request", 20 | Long: "", 21 | Run: func(cmd *cobra.Command, args []string) { 22 | ctx := cmd.Context() 23 | 24 | tempPath, err := os.MkdirTemp("", "") 25 | if err != nil { 26 | log.Fatal().Err(err).Msg("fail to create ssh data dir") 27 | } 28 | defer pkg.WithErrorLogging(func() error { return os.RemoveAll(tempPath) }, "failed to remove temp directory") 29 | 30 | // symlink local ssh known hosts to argocd ssh known hosts 31 | homeDir, err := os.UserHomeDir() 32 | if err != nil { 33 | log.Fatal().Err(err).Msg("failed to get user home dir") 34 | } 35 | source := filepath.Join(homeDir, ".ssh", "known_hosts") 36 | target := filepath.Join(tempPath, common.DefaultSSHKnownHostsName) 37 | 38 | if err := os.Symlink(source, target); err != nil { 39 | log.Fatal().Err(err).Msg("fail to symlink ssh_known_hosts file") 40 | } 41 | 42 | if err := os.Setenv("ARGOCD_SSH_DATA_PATH", tempPath); err != nil { 43 | log.Fatal().Err(err).Msg("fail to set ARGOCD_SSH_DATA_PATH") 44 | } 45 | 46 | cfg, err := config.New() 47 | if err != nil { 48 | log.Fatal().Err(err).Msg("failed to generate config") 49 | } 50 | 51 | if len(args) != 1 { 52 | log.Fatal().Msg("usage: kubechecks process PR_REF") 53 | } 54 | 55 | ctr, err := container.New(ctx, cfg) 56 | if err != nil { 57 | log.Fatal().Err(err).Msg("failed to create clients") 58 | } 59 | 60 | log.Info().Msg("initializing git settings") 61 | if err = initializeGit(ctr); err != nil { 62 | log.Fatal().Err(err).Msg("failed to initialize git settings") 63 | } 64 | 65 | repo, err := ctr.VcsClient.LoadHook(ctx, args[0]) 66 | if err != nil { 67 | log.Fatal().Err(err).Msg("failed to load hook") 68 | return 69 | } 70 | 71 | processors, err := getProcessors(ctr) 72 | if err != nil { 73 | log.Fatal().Err(err).Msg("failed to create processors") 74 | } 75 | 76 | server.ProcessCheckEvent(ctx, repo, ctr, processors) 77 | }, 78 | } 79 | 80 | func init() { 81 | RootCmd.AddCommand(processCmd) 82 | } 83 | -------------------------------------------------------------------------------- /cmd/processors.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/pkg/errors" 5 | 6 | "github.com/zapier/kubechecks/pkg/checks" 7 | "github.com/zapier/kubechecks/pkg/checks/diff" 8 | "github.com/zapier/kubechecks/pkg/checks/hooks" 9 | "github.com/zapier/kubechecks/pkg/checks/kubeconform" 10 | "github.com/zapier/kubechecks/pkg/checks/preupgrade" 11 | "github.com/zapier/kubechecks/pkg/checks/rego" 12 | "github.com/zapier/kubechecks/pkg/container" 13 | ) 14 | 15 | func getProcessors(ctr container.Container) ([]checks.ProcessorEntry, error) { 16 | var procs []checks.ProcessorEntry 17 | 18 | procs = append(procs, checks.ProcessorEntry{ 19 | Name: "generating diff for app", 20 | Processor: diff.Check, 21 | }) 22 | 23 | if ctr.Config.EnableHooksRenderer { 24 | procs = append(procs, checks.ProcessorEntry{ 25 | Name: "render hooks", 26 | Processor: hooks.Check, 27 | WorstState: ctr.Config.WorstHooksState, 28 | }) 29 | } 30 | 31 | if ctr.Config.EnableKubeConform { 32 | procs = append(procs, checks.ProcessorEntry{ 33 | Name: "validating app against schema", 34 | Processor: kubeconform.Check, 35 | WorstState: ctr.Config.WorstKubeConformState, 36 | }) 37 | } 38 | 39 | if ctr.Config.EnablePreupgrade { 40 | procs = append(procs, checks.ProcessorEntry{ 41 | Name: "running pre-upgrade check", 42 | Processor: preupgrade.Check, 43 | WorstState: ctr.Config.WorstPreupgradeState, 44 | }) 45 | } 46 | 47 | if ctr.Config.EnableConfTest { 48 | checker, err := rego.NewChecker(ctr.Config) 49 | if err != nil { 50 | return nil, errors.Wrap(err, "failed to create rego checker") 51 | } 52 | 53 | procs = append(procs, checks.ProcessorEntry{ 54 | Name: "validation policy", 55 | Processor: checker.Check, 56 | WorstState: ctr.Config.WorstConfTestState, 57 | }) 58 | } 59 | 60 | return procs, nil 61 | } 62 | -------------------------------------------------------------------------------- /cmd/version.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | 8 | "github.com/zapier/kubechecks/pkg" 9 | ) 10 | 11 | var versionCmd = &cobra.Command{ 12 | Use: "version", 13 | Short: "List version information", 14 | Long: ``, 15 | Run: func(cmd *cobra.Command, args []string) { 16 | fmt.Printf("kubechecks\nVersion:%s\nSHA%s\n", pkg.GitTag, pkg.GitCommit) 17 | }, 18 | } 19 | 20 | func init() { 21 | RootCmd.AddCommand(versionCmd) 22 | } 23 | -------------------------------------------------------------------------------- /docs/gif/kubechecks.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zapier/kubechecks/c353b7a3fd42123bce980f5e5e8dff3cb880dbe9/docs/gif/kubechecks.gif -------------------------------------------------------------------------------- /docs/img/checkevent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zapier/kubechecks/c353b7a3fd42123bce980f5e5e8dff3cb880dbe9/docs/img/checkevent.png -------------------------------------------------------------------------------- /docs/img/client.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zapier/kubechecks/c353b7a3fd42123bce980f5e5e8dff3cb880dbe9/docs/img/client.png -------------------------------------------------------------------------------- /docs/img/eventflowdiagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zapier/kubechecks/c353b7a3fd42123bce980f5e5e8dff3cb880dbe9/docs/img/eventflowdiagram.png -------------------------------------------------------------------------------- /docs/img/flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zapier/kubechecks/c353b7a3fd42123bce980f5e5e8dff3cb880dbe9/docs/img/flow.png -------------------------------------------------------------------------------- /docs/img/kubechecks.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zapier/kubechecks/c353b7a3fd42123bce980f5e5e8dff3cb880dbe9/docs/img/kubechecks.gif -------------------------------------------------------------------------------- /docs/img/repo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zapier/kubechecks/c353b7a3fd42123bce980f5e5e8dff3cb880dbe9/docs/img/repo.png -------------------------------------------------------------------------------- /docs/img/tilt-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zapier/kubechecks/c353b7a3fd42123bce980f5e5e8dff3cb880dbe9/docs/img/tilt-1.png -------------------------------------------------------------------------------- /docs/img/tilt-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zapier/kubechecks/c353b7a3fd42123bce980f5e5e8dff3cb880dbe9/docs/img/tilt-2.png -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # kubechecks - Fearless Kubernetes App Updates 2 | 3 | ## Check your Kubernetes manifests before it hits the cluster 4 | 5 | You're deep into developing new projects for your Kubernetes cluster. Local testing shows all green. Unit tests? Passing. 6 | You merge your changes into your main branch... and ArgoCD starts showing red. Sound familiar? 7 | 8 | This is where `kubechecks` enters the picture. `kubechecks` is a handy tool that detects changes between your live deployments 9 | managed via ArgoCD and changes made in your PR/MRs; letting you know _before_ you merge that branch what will change. On top of 10 | that, `kubechecks` runs handy linting reports from [`kubepug`](https://github.com/rikatz/kubepug), [`kubeconform`](https://github.com/yannh/kubeconform), and [`conftest`](https://www.conftest.dev/) to build an even better picture of those changes. 11 | 12 | Think of it like `terraform plan` but for Kubernetes; with `kubechecks`, you'll have greater confidence than ever before in your changes. 13 | 14 | ## Why kubechecks? 15 | 16 | kubechecks was built out a desire to simplify the amount of separate pipelines required to be run for pull requests at Zapier. We've 17 | been using it internally for awhile now, and we think it's pretty great; and we hope you find it useful, too! 18 | 19 | Some great features: 20 | 21 | - Supports Github and Gitlab. 22 | - Clear visibility into what new commits will actually change against your live applications 23 | - Validate your manifests are production-ready via multiple checks automatically 24 | 25 | ## Documentation 26 | 27 | To learn more about kubechecks [see our documentation](https://kubechecks.readthedocs.io/). 28 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | mkdocs>=1.4.2; python_version >= "3.7" 2 | mkdocs-material>=9.1.17 3 | pymdown-extensions>=10.0.1 -------------------------------------------------------------------------------- /docs/usage.md.tpl: -------------------------------------------------------------------------------- 1 | # Usage 2 | 3 | ## Installation 4 | 5 | `kubechecks` currently only officially supports deployment to a Kubernetes Cluster via Helm. 6 | 7 | ### Requirements 8 | 9 | 1. Kubernetes Cluster 10 | 2. Github/Gitlab token (for authenticating to the repository) 11 | 3. ArgoCD 12 | 13 | ### Helm Installation 14 | 15 | To get started, add the `kubechecks` repository to Helm: 16 | 17 | # Add kubechecks helm chart repo 18 | 19 | ```console 20 | helm repo add kubechecks https://zapier.github.io/kubechecks/ 21 | ``` 22 | 23 | Once installed, simply run: 24 | 25 | ```console 26 | helm install kubechecks charts/kubechecks -n kubechecks --create-namespace 27 | ``` 28 | 29 | Refer to [configuration](#configuration) for details about the various options available for customising `kubechecks`. You **must** provide the required secrets in some capacity; refer to the chart for more details 30 | 31 | ## Configuration 32 | 33 | `kubechecks` can be configured to meet your specific set up through the use of enviornment variables defined in your provided `values.yaml`. 34 | 35 | The full list of supported environment variables is described below: 36 | 37 | |Env Var|Description|Default Value| 38 | |-----------|-------------|------| 39 | {{- range .Options }} 40 | |`{{ .Env }}`|{{ .Usage }}|{{ if .Default }}`{{ .Default }}`{{ end }}| 41 | {{- end }} 42 | -------------------------------------------------------------------------------- /earthly.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | to_echo() { 4 | if [ "$1" -eq 1 ]; then 5 | echo "$2" 6 | fi 7 | } 8 | 9 | read_tool_versions_write_to_env() { 10 | local -r tool_versions_file="$1" 11 | cat $tool_versions_file 12 | # loop over each line of the .tool-versions file 13 | while read -r line; do 14 | # split the line into a bash array using the default space delimeter 15 | IFS=" " read -r -a lineArray <<<"$line" 16 | 17 | # get the key and value from the array, set the key to all uppercase 18 | key="${lineArray[0],,}" 19 | value="${lineArray[1]}" 20 | 21 | # ignore comments, comments always start with # 22 | if [[ ${key:0:1} != "#" ]]; then 23 | full_key="${key/-/_}_tool_version" 24 | export "${full_key/-/_}=${value}" 25 | fi 26 | done <"$tool_versions_file" 27 | } 28 | 29 | read_tool_versions_write_to_env '.tool-versions' 30 | 31 | set -x 32 | 33 | # shellcheck disable=SC2048 34 | earthly $* \ 35 | --CHART_RELEASER_VERSION=${helm_cr_tool_version} \ 36 | --GOLANG_VERSION=${golang_tool_version} \ 37 | --GOLANGCI_LINT_VERSION=${golangci_lint_tool_version} \ 38 | --HELM_VERSION=${helm_tool_version} \ 39 | --KUBECONFORM_VERSION=${kubeconform_tool_version} \ 40 | --KUSTOMIZE_VERSION=${kustomize_tool_version} \ 41 | --GIT_COMMIT=$(git rev-parse --short HEAD) \ 42 | --KUBECHECKS_LOG_LEVEL=debug 43 | -------------------------------------------------------------------------------- /hacks/env-to-docs.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "regexp" 7 | "sort" 8 | "strings" 9 | "text/template" 10 | 11 | "github.com/spf13/cobra" 12 | "github.com/spf13/pflag" 13 | 14 | "github.com/zapier/kubechecks/cmd" 15 | ) 16 | 17 | type option struct { 18 | Option string 19 | Env string 20 | Usage string 21 | Default string 22 | } 23 | 24 | var UsageEnvVar = regexp.MustCompile(` \(KUBECHECKS_[_A-Z0-9]+\)`) 25 | var UsageDefaultValue = regexp.MustCompile(`Defaults to \.?(.*)+\.`) 26 | 27 | func main() { 28 | outputFilename := filepath.Join("docs", "usage.md") 29 | templateFilename := outputFilename + ".tpl" 30 | 31 | data, err := os.ReadFile(templateFilename) 32 | if err != nil { 33 | panic(err) 34 | } 35 | t, err := template.New("usage").Parse(string(data)) 36 | if err != nil { 37 | panic(err) 38 | } 39 | 40 | _, err = os.Stat(outputFilename) 41 | if err != nil && !os.IsNotExist(err) { 42 | panic(err) 43 | } else if err == nil { 44 | if err = os.Remove(outputFilename); err != nil { 45 | panic(err) 46 | } 47 | } 48 | 49 | flagUsage := make(map[string]option) 50 | 51 | cleanUpUsage := func(s string) string { 52 | s = UsageEnvVar.ReplaceAllString(s, "") 53 | s = UsageDefaultValue.ReplaceAllString(s, "") 54 | s = strings.TrimSpace(s) 55 | return s 56 | } 57 | 58 | visitFlag := func(flag *pflag.Flag) { 59 | flagUsage[flag.Name] = option{ 60 | Default: flag.DefValue, 61 | Env: cmd.ViperNameToEnv(flag.Name), 62 | Option: flag.Name, 63 | Usage: cleanUpUsage(flag.Usage), 64 | } 65 | } 66 | 67 | addFlags(cmd.RootCmd, visitFlag) 68 | addFlags(cmd.ControllerCmd, visitFlag) 69 | 70 | vars := getSortedFlags(flagUsage) 71 | 72 | f, err := os.OpenFile(outputFilename, os.O_WRONLY|os.O_CREATE, 0o666) 73 | if err != nil { 74 | panic(err) 75 | } 76 | 77 | type templateVars struct { 78 | Options []option 79 | } 80 | if err = t.Execute(f, templateVars{vars}); err != nil { 81 | panic(err) 82 | } 83 | } 84 | 85 | func getSortedFlags(flagUsage map[string]option) []option { 86 | var keys []string 87 | for key := range flagUsage { 88 | keys = append(keys, key) 89 | } 90 | sort.Strings(keys) 91 | var vars []option 92 | for _, key := range keys { 93 | vars = append(vars, flagUsage[key]) 94 | } 95 | return vars 96 | } 97 | 98 | func addFlags(cmd *cobra.Command, visitFlag func(flag *pflag.Flag)) { 99 | cmd.Flags().VisitAll(visitFlag) 100 | cmd.PersistentFlags().VisitAll(visitFlag) 101 | } 102 | -------------------------------------------------------------------------------- /hacks/exit-on-changed-files.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | exitCode=0 6 | message=$1 7 | 8 | # Log the actual diff for debugging purposes 9 | git diff --name-only | cat 10 | if ! git diff --exit-code --quiet; then 11 | echo "$message" 12 | exitCode=1 13 | fi 14 | 15 | exit $exitCode 16 | -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | help: 2 | @just --list 3 | 4 | system_check: 5 | #!/usr/bin/env python3 6 | import os 7 | import subprocess 8 | devnull = open(os.devnull, 'w') 9 | check = "\u2713" 10 | cross = "\u2717" 11 | space = "\t" 12 | FAIL = 0 13 | print("Checking dependencies:") 14 | 15 | print(f"...Minkube{space}: ", end="") 16 | try: 17 | mini_version = subprocess.check_output("minikube version".split()).decode().split("\n")[0].strip() 18 | print(f"{check} {mini_version}") 19 | except: 20 | FAIL += 1 21 | print(f"{cross} NOT FOUND") 22 | 23 | print(f"...GoLang{space}: ", end="") 24 | try: 25 | go_version = subprocess.check_output("go version".split()).decode().strip() 26 | print(f"{check} {go_version}") 27 | except: 28 | FAIL += 1 29 | print(f"{cross} NOT FOUND") 30 | 31 | print(f"...Ngrok{space}: ", end="") 32 | try: 33 | ngrok_version = subprocess.check_output("ngrok version".split()).decode().strip() 34 | print(f"{check} {ngrok_version}") 35 | subprocess.check_output("ngrok config check".split()).decode().strip() 36 | except: 37 | FAIL += 1 38 | print(f"{cross} NOT FOUND") 39 | 40 | if FAIL > 0: 41 | print(f"\n\u274c {FAIL} of the dependency and configuration checks have failed.\n") 42 | exit(FAIL) 43 | 44 | cluster_up: 45 | minikube start --driver=docker --addons=dashboard,ingress --cpus='4' --memory='12g' --nodes=1 46 | 47 | cluster_down: 48 | minikube delete 49 | 50 | # Creates a minikube tunnel 51 | cluster_tunnel: 52 | minikube tunnel 53 | 54 | # Starts a minikube cluster and tilt 55 | start: system_check cluster_up 56 | tilt up 57 | 58 | dump_crds: 59 | cd tools/dump_crds/; go mod tidy; go run -v dump_crds.go ../../schemas 60 | 61 | unit_test: 62 | go test ./... 63 | 64 | unit_test_race: 65 | go test -race ./... 66 | 67 | rebuild_docs: 68 | ./earthly.sh +rebuild-docs 69 | 70 | ci-golang: 71 | ./earthly.sh +ci-golang 72 | -------------------------------------------------------------------------------- /localdev/.gitignore: -------------------------------------------------------------------------------- 1 | terraform.tfstate* 2 | .terraform.lock.hcl 3 | .terraform/** 4 | .terraform.tfstate.lock.info 5 | /terraform/modules/vcs_files/base_files/appsets/httpdump/httpdump.yaml 6 | /terraform/modules/vcs_files/base_files/appsets/echo-server/echo-server.yaml 7 | -------------------------------------------------------------------------------- /localdev/argocd/argocd-application-controller-statefulset-patch.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: StatefulSet 3 | metadata: 4 | name: argocd-application-controller 5 | spec: 6 | template: 7 | spec: 8 | containers: 9 | - name: argocd-application-controller 10 | lifecycle: 11 | preStop: 12 | exec: 13 | command: 14 | - "/usr/bin/sleep" 15 | - "25" -------------------------------------------------------------------------------- /localdev/argocd/argocd-applicationset-controller-deployment-patch.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: argocd-applicationset-controller 5 | spec: 6 | template: 7 | spec: 8 | containers: 9 | - name: argocd-applicationset-controller 10 | lifecycle: 11 | preStop: 12 | exec: 13 | command: 14 | - "/usr/bin/sleep" 15 | - "25" -------------------------------------------------------------------------------- /localdev/argocd/argocd-cm-patch.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: argocd-cm 5 | data: 6 | accounts.kubechecks: apiKey, login -------------------------------------------------------------------------------- /localdev/argocd/argocd-initial-admin-secret.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | data: 3 | # admin123 4 | password: YWRtaW4xMjMK 5 | kind: Secret 6 | metadata: 7 | name: argocd-initial-admin-secret 8 | type: Opaque 9 | -------------------------------------------------------------------------------- /localdev/argocd/argocd-rbac-cm-patch.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: argocd-rbac-cm 5 | data: 6 | policy.csv: | 7 | g, kubechecks, role:admin -------------------------------------------------------------------------------- /localdev/argocd/argocd-redis-deployment-patch.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: argocd-redis 5 | spec: 6 | template: 7 | spec: 8 | containers: 9 | - name: redis 10 | lifecycle: 11 | preStop: 12 | exec: 13 | command: 14 | - "/bin/sleep" 15 | - "25" -------------------------------------------------------------------------------- /localdev/argocd/argocd-repo-server-deployment-patch.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: argocd-repo-server 5 | spec: 6 | template: 7 | spec: 8 | containers: 9 | - name: argocd-repo-server 10 | lifecycle: 11 | preStop: 12 | exec: 13 | command: 14 | - "/usr/bin/sleep" 15 | - "25" -------------------------------------------------------------------------------- /localdev/argocd/argocd-server-patch.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: argocd-server 5 | spec: 6 | template: 7 | spec: 8 | containers: 9 | - name: argocd-server 10 | command: 11 | - argocd-server 12 | - --repo-server 13 | - argocd-repo-server:8081 14 | - --basehref 15 | - /argocd 16 | - --rootpath 17 | - /argocd 18 | 19 | -------------------------------------------------------------------------------- /localdev/argocd/delete-apps.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | echo "Deleting ArgoCD test Applications & AppSets (gracefully)..." 4 | 5 | # Delete ApplicationSets 6 | for a in $(kubectl get ApplicationSet -n kubechecks -o=jsonpath='{.items[*].metadata.name}'); do 7 | kubectl delete ApplicationSet $a -n kubechecks --timeout=10s; 8 | done; 9 | 10 | # Delete Applications 11 | for a in $(kubectl get application -n kubechecks -o=jsonpath='{.items[*].metadata.name}'); do 12 | kubectl delete application $a -n kubechecks --timeout=10s; 13 | done; 14 | 15 | exit 0; -------------------------------------------------------------------------------- /localdev/argocd/force-cleanup-apps.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # check if Applications CRD still exists 4 | exists=$(kubectl get crd | grep "applications.argoproj.io" | wc -l) 5 | if [ "$exists" -eq 0 ]; then 6 | echo "Applications CRD doesn't exist. Exiting..." 7 | exit 0; 8 | fi 9 | 10 | # give time for other processes to cleanup properly 11 | echo "ArgoCD Cleanup: waiting 20 seconds..." 12 | sleep 20 13 | echo "Cleaning up ArgoCD test Applications and CRDs..." 14 | 15 | # Cleanup Applications 16 | for a in $(kubectl get application -n kubechecks -o=jsonpath='{.items[*].metadata.name}'); do 17 | # remove finalizer from Applications (ArgoCD is probably shutdown by now and deleting apps will hang) 18 | kubectl patch application $a -n kubechecks --type json -p='[{"op": "remove", "path": "/metadata/finalizers"}]'; 19 | kubectl delete application $a -n kubechecks; 20 | done; 21 | 22 | # Cleanup ArgoCD CRDs 23 | for crd in applications.argoproj.io applicationsets.argoproj.io appprojects.argoproj.io; do 24 | kubectl delete crd $crd; 25 | done; 26 | 27 | exit 0; -------------------------------------------------------------------------------- /localdev/argocd/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | 4 | namespace: kubechecks 5 | 6 | images: 7 | - name: quay.io/argoproj/argocd 8 | newName: quay.io/argoproj/argocd 9 | newTag: v2.11.3 10 | 11 | resources: 12 | - argocd-initial-admin-secret.yaml 13 | - ./manifests/crds 14 | - ./manifests/cluster-rbac 15 | - ./manifests/base/application-controller 16 | - ./manifests/base/applicationset-controller 17 | - ./manifests/base/dex 18 | - ./manifests/base/repo-server 19 | - ./manifests/base/server 20 | - ./manifests/base/config 21 | - ./manifests/base/redis 22 | 23 | patchesStrategicMerge: 24 | - ./argocd-cm-patch.yaml 25 | - ./argocd-rbac-cm-patch.yaml 26 | - ./argocd-secret-patch.yaml 27 | - ./argocd-server-patch.yaml 28 | - ./argocd-application-controller-statefulset-patch.yaml 29 | - ./argocd-applicationset-controller-deployment-patch.yaml 30 | - ./argocd-redis-deployment-patch.yaml 31 | - ./argocd-repo-server-deployment-patch.yaml -------------------------------------------------------------------------------- /localdev/argocd/manifests/base/application-controller/argocd-application-controller-network-policy.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.k8s.io/v1 2 | kind: NetworkPolicy 3 | metadata: 4 | name: argocd-application-controller-network-policy 5 | spec: 6 | podSelector: 7 | matchLabels: 8 | app.kubernetes.io/name: argocd-application-controller 9 | ingress: 10 | - from: 11 | - namespaceSelector: { } 12 | ports: 13 | - port: 8082 14 | policyTypes: 15 | - Ingress -------------------------------------------------------------------------------- /localdev/argocd/manifests/base/application-controller/argocd-application-controller-role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: Role 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: argocd-application-controller 6 | app.kubernetes.io/part-of: argocd 7 | app.kubernetes.io/component: application-controller 8 | name: argocd-application-controller 9 | rules: 10 | - apiGroups: 11 | - "" 12 | resources: 13 | - secrets 14 | - configmaps 15 | verbs: 16 | - get 17 | - list 18 | - watch 19 | - apiGroups: 20 | - argoproj.io 21 | resources: 22 | - applications 23 | - appprojects 24 | verbs: 25 | - create 26 | - get 27 | - list 28 | - watch 29 | - update 30 | - patch 31 | - delete 32 | - apiGroups: 33 | - "" 34 | resources: 35 | - events 36 | verbs: 37 | - create 38 | - list 39 | -------------------------------------------------------------------------------- /localdev/argocd/manifests/base/application-controller/argocd-application-controller-rolebinding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: RoleBinding 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: argocd-application-controller 6 | app.kubernetes.io/part-of: argocd 7 | app.kubernetes.io/component: application-controller 8 | name: argocd-application-controller 9 | roleRef: 10 | apiGroup: rbac.authorization.k8s.io 11 | kind: Role 12 | name: argocd-application-controller 13 | subjects: 14 | - kind: ServiceAccount 15 | name: argocd-application-controller 16 | -------------------------------------------------------------------------------- /localdev/argocd/manifests/base/application-controller/argocd-application-controller-sa.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: argocd-application-controller 6 | app.kubernetes.io/part-of: argocd 7 | app.kubernetes.io/component: application-controller 8 | name: argocd-application-controller 9 | -------------------------------------------------------------------------------- /localdev/argocd/manifests/base/application-controller/argocd-metrics.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: argocd-metrics 6 | app.kubernetes.io/part-of: argocd 7 | app.kubernetes.io/component: metrics 8 | name: argocd-metrics 9 | spec: 10 | ports: 11 | - name: metrics 12 | protocol: TCP 13 | port: 8082 14 | targetPort: 8082 15 | selector: 16 | app.kubernetes.io/name: argocd-application-controller 17 | -------------------------------------------------------------------------------- /localdev/argocd/manifests/base/application-controller/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | 4 | resources: 5 | - argocd-application-controller-sa.yaml 6 | - argocd-application-controller-role.yaml 7 | - argocd-application-controller-rolebinding.yaml 8 | - argocd-application-controller-statefulset.yaml 9 | - argocd-metrics.yaml 10 | - argocd-application-controller-network-policy.yaml -------------------------------------------------------------------------------- /localdev/argocd/manifests/base/applicationset-controller/argocd-applicationset-controller-network-policy.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.k8s.io/v1 2 | kind: NetworkPolicy 3 | metadata: 4 | name: argocd-applicationset-controller-network-policy 5 | spec: 6 | podSelector: 7 | matchLabels: 8 | app.kubernetes.io/name: argocd-applicationset-controller 9 | ingress: 10 | - from: 11 | - namespaceSelector: { } 12 | ports: 13 | - protocol: TCP 14 | port: 7000 15 | - protocol: TCP 16 | port: 8080 17 | policyTypes: 18 | - Ingress 19 | -------------------------------------------------------------------------------- /localdev/argocd/manifests/base/applicationset-controller/argocd-applicationset-controller-role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: Role 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 | rules: 10 | - apiGroups: 11 | - argoproj.io 12 | resources: 13 | - applications 14 | - applicationsets 15 | - applicationsets/finalizers 16 | verbs: 17 | - create 18 | - delete 19 | - get 20 | - list 21 | - patch 22 | - update 23 | - watch 24 | - apiGroups: 25 | - argoproj.io 26 | resources: 27 | - appprojects 28 | verbs: 29 | - get 30 | - apiGroups: 31 | - argoproj.io 32 | resources: 33 | - applicationsets/status 34 | verbs: 35 | - get 36 | - patch 37 | - update 38 | - apiGroups: 39 | - '' 40 | resources: 41 | - events 42 | verbs: 43 | - create 44 | - get 45 | - list 46 | - patch 47 | - watch 48 | - apiGroups: 49 | - '' 50 | resources: 51 | - secrets 52 | - configmaps 53 | verbs: 54 | - get 55 | - list 56 | - watch 57 | - apiGroups: 58 | - apps 59 | - extensions 60 | resources: 61 | - deployments 62 | verbs: 63 | - get 64 | - list 65 | - watch -------------------------------------------------------------------------------- /localdev/argocd/manifests/base/applicationset-controller/argocd-applicationset-controller-rolebinding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: RoleBinding 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 | roleRef: 10 | apiGroup: rbac.authorization.k8s.io 11 | kind: Role 12 | name: argocd-applicationset-controller 13 | subjects: 14 | - kind: ServiceAccount 15 | name: argocd-applicationset-controller 16 | -------------------------------------------------------------------------------- /localdev/argocd/manifests/base/applicationset-controller/argocd-applicationset-controller-sa.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 -------------------------------------------------------------------------------- /localdev/argocd/manifests/base/applicationset-controller/argocd-applicationset-controller-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 | - name: metrics 16 | port: 8080 17 | protocol: TCP 18 | targetPort: metrics 19 | selector: 20 | app.kubernetes.io/name: argocd-applicationset-controller -------------------------------------------------------------------------------- /localdev/argocd/manifests/base/applicationset-controller/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | 4 | resources: 5 | - argocd-applicationset-controller-rolebinding.yaml 6 | - argocd-applicationset-controller-sa.yaml 7 | - argocd-applicationset-controller-deployment.yaml 8 | - argocd-applicationset-controller-role.yaml 9 | - argocd-applicationset-controller-service.yaml 10 | - argocd-applicationset-controller-network-policy.yaml 11 | -------------------------------------------------------------------------------- /localdev/argocd/manifests/base/config/argocd-cm.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: argocd-cm 5 | labels: 6 | app.kubernetes.io/name: argocd-cm 7 | app.kubernetes.io/part-of: argocd 8 | -------------------------------------------------------------------------------- /localdev/argocd/manifests/base/config/argocd-cmd-params-cm.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: argocd-cmd-params-cm 5 | labels: 6 | app.kubernetes.io/name: argocd-cmd-params-cm 7 | app.kubernetes.io/part-of: argocd 8 | -------------------------------------------------------------------------------- /localdev/argocd/manifests/base/config/argocd-gpg-keys-cm.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: argocd-gpg-keys-cm 6 | app.kubernetes.io/part-of: argocd 7 | name: argocd-gpg-keys-cm 8 | -------------------------------------------------------------------------------- /localdev/argocd/manifests/base/config/argocd-rbac-cm.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: argocd-rbac-cm 5 | labels: 6 | app.kubernetes.io/name: argocd-rbac-cm 7 | app.kubernetes.io/part-of: argocd 8 | -------------------------------------------------------------------------------- /localdev/argocd/manifests/base/config/argocd-secret.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: argocd-secret 5 | labels: 6 | app.kubernetes.io/name: argocd-secret 7 | app.kubernetes.io/part-of: argocd 8 | type: Opaque 9 | -------------------------------------------------------------------------------- /localdev/argocd/manifests/base/config/argocd-ssh-known-hosts-cm.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: argocd-ssh-known-hosts-cm 6 | app.kubernetes.io/part-of: argocd 7 | name: argocd-ssh-known-hosts-cm 8 | data: 9 | ssh_known_hosts: | 10 | bitbucket.org ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAubiN81eDcafrgMeLzaFPsw2kNvEcqTKl/VqLat/MaB33pZy0y3rJZtnqwR2qOOvbwKZYKiEO1O6VqNEBxKvJJelCq0dTXWT5pbO2gDXC6h6QDXCaHo6pOHGPUy+YBaGQRGuSusMEASYiWunYN0vCAI8QaXnWMXNMdFP3jHAJH0eDsoiGnLPBlBp4TNm6rYI74nMzgz3B9IikW4WVK+dc8KZJZWYjAuORU3jc1c/NPskD2ASinf8v3xnfXeukU0sJ5N6m5E8VLjObPEO+mN2t/FZTMZLiFqPWc/ALSqnMnnhwrNi2rbfg/rd/IpL8Le3pSBne8+seeFVBoGqzHM9yXw== 11 | github.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ== 12 | gitlab.com ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBFSMqzJeV9rUzU4kWitGjeR4PWSa29SPqJ1fVkhtj3Hw9xjLVXVYrU9QlYWrOLXBpQ6KWjbjTDTdDkoohFzgbEY= 13 | gitlab.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAfuCHKVTjquxvt6CM6tdG4SLp1Btn/nOeHHE5UOzRdf 14 | gitlab.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCsj2bNKTBSpIYDEGk9KxsGh3mySTRgMtXL583qmBpzeQ+jqCMRgBqB98u3z++J1sKlXHWfM9dyhSevkMwSbhoR8XIq/U0tCNyokEi/ueaBMCvbcTHhO7FcwzY92WK4Yt0aGROY5qX2UKSeOvuP4D6TPqKF1onrSzH9bx9XUf2lEdWT/ia1NEKjunUqu1xOB/StKDHMoX4/OKyIzuS0q/T1zOATthvasJFoPrAjkohTyaDUz2LN5JoH839hViyEG82yB+MjcFV5MU3N1l1QL3cVUCh93xSaua1N85qivl+siMkPGbO5xR/En4iEY6K2XPASUEMaieWVNTRCtJ4S8H+9 15 | ssh.dev.azure.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC7Hr1oTWqNqOlzGJOfGJ4NakVyIzf1rXYd4d7wo6jBlkLvCA4odBlL0mDUyZ0/QUfTTqeu+tm22gOsv+VrVTMk6vwRU75gY/y9ut5Mb3bR5BV58dKXyq9A9UeB5Cakehn5Zgm6x1mKoVyf+FFn26iYqXJRgzIZZcZ5V6hrE0Qg39kZm4az48o0AUbf6Sp4SLdvnuMa2sVNwHBboS7EJkm57XQPVU3/QpyNLHbWDdzwtrlS+ez30S3AdYhLKEOxAG8weOnyrtLJAUen9mTkol8oII1edf7mWWbWVf0nBmly21+nZcmCTISQBtdcyPaEno7fFQMDD26/s0lfKob4Kw8H 16 | vs-ssh.visualstudio.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC7Hr1oTWqNqOlzGJOfGJ4NakVyIzf1rXYd4d7wo6jBlkLvCA4odBlL0mDUyZ0/QUfTTqeu+tm22gOsv+VrVTMk6vwRU75gY/y9ut5Mb3bR5BV58dKXyq9A9UeB5Cakehn5Zgm6x1mKoVyf+FFn26iYqXJRgzIZZcZ5V6hrE0Qg39kZm4az48o0AUbf6Sp4SLdvnuMa2sVNwHBboS7EJkm57XQPVU3/QpyNLHbWDdzwtrlS+ez30S3AdYhLKEOxAG8weOnyrtLJAUen9mTkol8oII1edf7mWWbWVf0nBmly21+nZcmCTISQBtdcyPaEno7fFQMDD26/s0lfKob4Kw8H 17 | github.com ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEmKSENjQEezOmxkZMy7opKgwFB9nkt5YRrYMjNuG5N87uRgg6CLrbo5wAdT/y6v0mKV0U2w0WZ2YB/++Tpockg= 18 | github.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl -------------------------------------------------------------------------------- /localdev/argocd/manifests/base/config/argocd-tls-certs-cm.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: argocd-tls-certs-cm 6 | app.kubernetes.io/part-of: argocd 7 | name: argocd-tls-certs-cm 8 | -------------------------------------------------------------------------------- /localdev/argocd/manifests/base/config/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | 4 | resources: 5 | - argocd-cm.yaml 6 | - argocd-cmd-params-cm.yaml 7 | - argocd-secret.yaml 8 | - argocd-rbac-cm.yaml 9 | - argocd-ssh-known-hosts-cm.yaml 10 | - argocd-tls-certs-cm.yaml 11 | - argocd-gpg-keys-cm.yaml 12 | 13 | -------------------------------------------------------------------------------- /localdev/argocd/manifests/base/dex/argocd-dex-server-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: argocd-dex-server 6 | app.kubernetes.io/part-of: argocd 7 | app.kubernetes.io/component: dex-server 8 | name: argocd-dex-server 9 | spec: 10 | selector: 11 | matchLabels: 12 | app.kubernetes.io/name: argocd-dex-server 13 | template: 14 | metadata: 15 | labels: 16 | app.kubernetes.io/name: argocd-dex-server 17 | spec: 18 | serviceAccountName: argocd-dex-server 19 | initContainers: 20 | - name: copyutil 21 | image: quay.io/argoproj/argocd:latest 22 | imagePullPolicy: Always 23 | command: [cp, -n, /usr/local/bin/argocd, /shared/argocd-dex] 24 | volumeMounts: 25 | - mountPath: /shared 26 | name: static-files 27 | - mountPath: /tmp 28 | name: dexconfig 29 | securityContext: 30 | capabilities: 31 | drop: 32 | - ALL 33 | allowPrivilegeEscalation: false 34 | readOnlyRootFilesystem: true 35 | runAsNonRoot: true 36 | seccompProfile: 37 | type: RuntimeDefault 38 | containers: 39 | - name: dex 40 | image: ghcr.io/dexidp/dex:v2.35.3 41 | imagePullPolicy: Always 42 | command: [/shared/argocd-dex, rundex] 43 | env: 44 | - name: ARGOCD_DEX_SERVER_DISABLE_TLS 45 | valueFrom: 46 | configMapKeyRef: 47 | name: argocd-cmd-params-cm 48 | key: dexserver.disable.tls 49 | optional: true 50 | securityContext: 51 | capabilities: 52 | drop: 53 | - ALL 54 | allowPrivilegeEscalation: false 55 | readOnlyRootFilesystem: true 56 | runAsNonRoot: true 57 | seccompProfile: 58 | type: RuntimeDefault 59 | ports: 60 | - containerPort: 5556 61 | - containerPort: 5557 62 | - containerPort: 5558 63 | volumeMounts: 64 | - mountPath: /shared 65 | name: static-files 66 | - mountPath: /tmp 67 | name: dexconfig 68 | - mountPath: /tls 69 | name: argocd-dex-server-tls 70 | volumes: 71 | - emptyDir: {} 72 | name: static-files 73 | - emptyDir: {} 74 | name: dexconfig 75 | - name: argocd-dex-server-tls 76 | secret: 77 | secretName: argocd-dex-server-tls 78 | optional: true 79 | items: 80 | - key: tls.crt 81 | path: tls.crt 82 | - key: tls.key 83 | path: tls.key 84 | - key: ca.crt 85 | path: ca.crt 86 | affinity: 87 | podAntiAffinity: 88 | preferredDuringSchedulingIgnoredDuringExecution: 89 | - weight: 5 90 | podAffinityTerm: 91 | labelSelector: 92 | matchLabels: 93 | app.kubernetes.io/part-of: argocd 94 | topologyKey: kubernetes.io/hostname 95 | -------------------------------------------------------------------------------- /localdev/argocd/manifests/base/dex/argocd-dex-server-network-policy.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.k8s.io/v1 2 | kind: NetworkPolicy 3 | metadata: 4 | name: argocd-dex-server-network-policy 5 | spec: 6 | podSelector: 7 | matchLabels: 8 | app.kubernetes.io/name: argocd-dex-server 9 | policyTypes: 10 | - Ingress 11 | ingress: 12 | - from: 13 | - podSelector: 14 | matchLabels: 15 | app.kubernetes.io/name: argocd-server 16 | ports: 17 | - protocol: TCP 18 | port: 5556 19 | - protocol: TCP 20 | port: 5557 21 | - from: 22 | - namespaceSelector: { } 23 | ports: 24 | - port: 5558 25 | protocol: TCP 26 | -------------------------------------------------------------------------------- /localdev/argocd/manifests/base/dex/argocd-dex-server-role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: Role 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: argocd-dex-server 6 | app.kubernetes.io/part-of: argocd 7 | app.kubernetes.io/component: dex-server 8 | name: argocd-dex-server 9 | rules: 10 | - apiGroups: 11 | - "" 12 | resources: 13 | - secrets 14 | - configmaps 15 | verbs: 16 | - get 17 | - list 18 | - watch 19 | -------------------------------------------------------------------------------- /localdev/argocd/manifests/base/dex/argocd-dex-server-rolebinding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: RoleBinding 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: argocd-dex-server 6 | app.kubernetes.io/part-of: argocd 7 | app.kubernetes.io/component: dex-server 8 | name: argocd-dex-server 9 | roleRef: 10 | apiGroup: rbac.authorization.k8s.io 11 | kind: Role 12 | name: argocd-dex-server 13 | subjects: 14 | - kind: ServiceAccount 15 | name: argocd-dex-server 16 | -------------------------------------------------------------------------------- /localdev/argocd/manifests/base/dex/argocd-dex-server-sa.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: argocd-dex-server 6 | app.kubernetes.io/part-of: argocd 7 | app.kubernetes.io/component: dex-server 8 | name: argocd-dex-server 9 | -------------------------------------------------------------------------------- /localdev/argocd/manifests/base/dex/argocd-dex-server-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: argocd-dex-server 6 | app.kubernetes.io/part-of: argocd 7 | app.kubernetes.io/component: dex-server 8 | name: argocd-dex-server 9 | spec: 10 | ports: 11 | - name: http 12 | protocol: TCP 13 | port: 5556 14 | targetPort: 5556 15 | - name: grpc 16 | protocol: TCP 17 | port: 5557 18 | targetPort: 5557 19 | - name: metrics 20 | port: 5558 21 | protocol: TCP 22 | targetPort: 5558 23 | selector: 24 | app.kubernetes.io/name: argocd-dex-server 25 | -------------------------------------------------------------------------------- /localdev/argocd/manifests/base/dex/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | 4 | resources: 5 | - argocd-dex-server-deployment.yaml 6 | - argocd-dex-server-role.yaml 7 | - argocd-dex-server-rolebinding.yaml 8 | - argocd-dex-server-sa.yaml 9 | - argocd-dex-server-service.yaml 10 | - argocd-dex-server-network-policy.yaml -------------------------------------------------------------------------------- /localdev/argocd/manifests/base/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | 4 | 5 | images: 6 | - name: quay.io/argoproj/argocd 7 | newName: quay.io/argoproj/argocd 8 | newTag: v2.6.12 9 | resources: 10 | - ./application-controller 11 | - ./dex 12 | - ./repo-server 13 | - ./server 14 | - ./config 15 | - ./redis 16 | - ./notification 17 | - ./applicationset-controller 18 | -------------------------------------------------------------------------------- /localdev/argocd/manifests/base/redis/argocd-redis-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: argocd-redis 6 | app.kubernetes.io/part-of: argocd 7 | app.kubernetes.io/component: redis 8 | name: argocd-redis 9 | spec: 10 | selector: 11 | matchLabels: 12 | app.kubernetes.io/name: argocd-redis 13 | template: 14 | metadata: 15 | labels: 16 | app.kubernetes.io/name: argocd-redis 17 | spec: 18 | securityContext: 19 | runAsNonRoot: true 20 | runAsUser: 999 21 | seccompProfile: 22 | type: RuntimeDefault 23 | serviceAccountName: argocd-redis 24 | containers: 25 | - name: redis 26 | image: redis:7.0.7-alpine 27 | imagePullPolicy: Always 28 | args: 29 | - "--save" 30 | - "" 31 | - "--appendonly" 32 | - "no" 33 | ports: 34 | - containerPort: 6379 35 | securityContext: 36 | allowPrivilegeEscalation: false 37 | capabilities: 38 | drop: 39 | - ALL 40 | affinity: 41 | podAntiAffinity: 42 | preferredDuringSchedulingIgnoredDuringExecution: 43 | - weight: 100 44 | podAffinityTerm: 45 | labelSelector: 46 | matchLabels: 47 | app.kubernetes.io/name: argocd-redis 48 | topologyKey: kubernetes.io/hostname 49 | - weight: 5 50 | podAffinityTerm: 51 | labelSelector: 52 | matchLabels: 53 | app.kubernetes.io/part-of: argocd 54 | topologyKey: kubernetes.io/hostname 55 | -------------------------------------------------------------------------------- /localdev/argocd/manifests/base/redis/argocd-redis-network-policy.yaml: -------------------------------------------------------------------------------- 1 | kind: NetworkPolicy 2 | apiVersion: networking.k8s.io/v1 3 | metadata: 4 | name: argocd-redis-network-policy 5 | spec: 6 | podSelector: 7 | matchLabels: 8 | app.kubernetes.io/name: argocd-redis 9 | policyTypes: 10 | - Ingress 11 | - Egress 12 | ingress: 13 | - from: 14 | - podSelector: 15 | matchLabels: 16 | app.kubernetes.io/name: argocd-server 17 | - podSelector: 18 | matchLabels: 19 | app.kubernetes.io/name: argocd-repo-server 20 | - podSelector: 21 | matchLabels: 22 | app.kubernetes.io/name: argocd-application-controller 23 | ports: 24 | - protocol: TCP 25 | port: 6379 26 | egress: 27 | - ports: 28 | - port: 53 29 | protocol: UDP 30 | - port: 53 31 | protocol: TCP 32 | -------------------------------------------------------------------------------- /localdev/argocd/manifests/base/redis/argocd-redis-rolebinding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: RoleBinding 3 | metadata: 4 | labels: 5 | app.kubernetes.io/component: redis 6 | app.kubernetes.io/name: argocd-redis 7 | app.kubernetes.io/part-of: argocd 8 | name: argocd-redis 9 | roleRef: 10 | apiGroup: rbac.authorization.k8s.io 11 | kind: Role 12 | name: argocd-redis 13 | subjects: 14 | - kind: ServiceAccount 15 | name: argocd-redis 16 | -------------------------------------------------------------------------------- /localdev/argocd/manifests/base/redis/argocd-redis-sa.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | labels: 5 | app.kubernetes.io/component: redis 6 | app.kubernetes.io/name: argocd-redis 7 | app.kubernetes.io/part-of: argocd 8 | name: argocd-redis -------------------------------------------------------------------------------- /localdev/argocd/manifests/base/redis/argocd-redis-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: argocd-redis 6 | app.kubernetes.io/part-of: argocd 7 | app.kubernetes.io/component: redis 8 | name: argocd-redis 9 | spec: 10 | ports: 11 | - name: tcp-redis 12 | port: 6379 13 | targetPort: 6379 14 | selector: 15 | app.kubernetes.io/name: argocd-redis 16 | -------------------------------------------------------------------------------- /localdev/argocd/manifests/base/redis/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | 4 | resources: 5 | - argocd-redis-deployment.yaml 6 | - argocd-redis-rolebinding.yaml 7 | - argocd-redis-sa.yaml 8 | - argocd-redis-service.yaml 9 | - argocd-redis-network-policy.yaml 10 | 11 | vars: 12 | - name: ARGOCD_REDIS_SERVICE 13 | objref: 14 | kind: Service 15 | name: argocd-redis 16 | apiVersion: v1 17 | fieldref: 18 | fieldpath: metadata.name -------------------------------------------------------------------------------- /localdev/argocd/manifests/base/repo-server/argocd-repo-server-network-policy.yaml: -------------------------------------------------------------------------------- 1 | kind: NetworkPolicy 2 | apiVersion: networking.k8s.io/v1 3 | metadata: 4 | name: argocd-repo-server-network-policy 5 | spec: 6 | podSelector: 7 | matchLabels: 8 | app.kubernetes.io/name: argocd-repo-server 9 | policyTypes: 10 | - Ingress 11 | ingress: 12 | - from: 13 | - podSelector: 14 | matchLabels: 15 | app.kubernetes.io/name: argocd-server 16 | - podSelector: 17 | matchLabels: 18 | app.kubernetes.io/name: argocd-application-controller 19 | - podSelector: 20 | matchLabels: 21 | app.kubernetes.io/name: argocd-notifications-controller 22 | - podSelector: 23 | matchLabels: 24 | app.kubernetes.io/name: kubechecks 25 | ports: 26 | - protocol: TCP 27 | port: 8081 28 | - from: 29 | - namespaceSelector: { } 30 | ports: 31 | - port: 8084 32 | -------------------------------------------------------------------------------- /localdev/argocd/manifests/base/repo-server/argocd-repo-server-sa.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: argocd-repo-server 6 | app.kubernetes.io/part-of: argocd 7 | app.kubernetes.io/component: repo-server 8 | name: argocd-repo-server 9 | -------------------------------------------------------------------------------- /localdev/argocd/manifests/base/repo-server/argocd-repo-server-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: argocd-repo-server 6 | app.kubernetes.io/part-of: argocd 7 | app.kubernetes.io/component: repo-server 8 | name: argocd-repo-server 9 | spec: 10 | ports: 11 | - name: server 12 | protocol: TCP 13 | port: 8081 14 | targetPort: 8081 15 | - name: metrics 16 | protocol: TCP 17 | port: 8084 18 | targetPort: 8084 19 | selector: 20 | app.kubernetes.io/name: argocd-repo-server 21 | -------------------------------------------------------------------------------- /localdev/argocd/manifests/base/repo-server/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | 4 | resources: 5 | - argocd-repo-server-sa.yaml 6 | - argocd-repo-server-deployment.yaml 7 | - argocd-repo-server-service.yaml 8 | - argocd-repo-server-network-policy.yaml 9 | -------------------------------------------------------------------------------- /localdev/argocd/manifests/base/server/argocd-server-metrics.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: argocd-server-metrics 6 | app.kubernetes.io/part-of: argocd 7 | app.kubernetes.io/component: server 8 | name: argocd-server-metrics 9 | spec: 10 | ports: 11 | - name: metrics 12 | protocol: TCP 13 | port: 8083 14 | targetPort: 8083 15 | selector: 16 | app.kubernetes.io/name: argocd-server 17 | -------------------------------------------------------------------------------- /localdev/argocd/manifests/base/server/argocd-server-network-policy.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.k8s.io/v1 2 | kind: NetworkPolicy 3 | metadata: 4 | name: argocd-server-network-policy 5 | spec: 6 | podSelector: 7 | matchLabels: 8 | app.kubernetes.io/name: argocd-server 9 | ingress: 10 | - {} 11 | policyTypes: 12 | - Ingress 13 | -------------------------------------------------------------------------------- /localdev/argocd/manifests/base/server/argocd-server-role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: Role 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: argocd-server 6 | app.kubernetes.io/part-of: argocd 7 | app.kubernetes.io/component: server 8 | name: argocd-server 9 | rules: 10 | - apiGroups: 11 | - "" 12 | resources: 13 | - secrets 14 | - configmaps 15 | verbs: 16 | - create 17 | - get 18 | - list 19 | - watch 20 | - update 21 | - patch 22 | - delete 23 | - apiGroups: 24 | - argoproj.io 25 | resources: 26 | - applications 27 | - appprojects 28 | - applicationsets 29 | verbs: 30 | - create 31 | - get 32 | - list 33 | - watch 34 | - update 35 | - delete 36 | - patch 37 | - apiGroups: 38 | - "" 39 | resources: 40 | - events 41 | verbs: 42 | - create 43 | - list 44 | -------------------------------------------------------------------------------- /localdev/argocd/manifests/base/server/argocd-server-rolebinding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: RoleBinding 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: argocd-server 6 | app.kubernetes.io/part-of: argocd 7 | app.kubernetes.io/component: server 8 | name: argocd-server 9 | roleRef: 10 | apiGroup: rbac.authorization.k8s.io 11 | kind: Role 12 | name: argocd-server 13 | subjects: 14 | - kind: ServiceAccount 15 | name: argocd-server 16 | -------------------------------------------------------------------------------- /localdev/argocd/manifests/base/server/argocd-server-sa.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: argocd-server 6 | app.kubernetes.io/part-of: argocd 7 | app.kubernetes.io/component: server 8 | name: argocd-server 9 | -------------------------------------------------------------------------------- /localdev/argocd/manifests/base/server/argocd-server-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: argocd-server 6 | app.kubernetes.io/part-of: argocd 7 | app.kubernetes.io/component: server 8 | name: argocd-server 9 | spec: 10 | ports: 11 | - name: http 12 | protocol: TCP 13 | port: 80 14 | targetPort: 8080 15 | - name: https 16 | protocol: TCP 17 | port: 443 18 | targetPort: 8080 19 | selector: 20 | app.kubernetes.io/name: argocd-server 21 | -------------------------------------------------------------------------------- /localdev/argocd/manifests/base/server/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | 4 | resources: 5 | - argocd-server-deployment.yaml 6 | - argocd-server-role.yaml 7 | - argocd-server-rolebinding.yaml 8 | - argocd-server-sa.yaml 9 | - argocd-server-service.yaml 10 | - argocd-server-metrics.yaml 11 | - argocd-server-network-policy.yaml -------------------------------------------------------------------------------- /localdev/argocd/manifests/cluster-rbac/application-controller/argocd-application-controller-clusterrole.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: argocd-application-controller 6 | app.kubernetes.io/part-of: argocd 7 | app.kubernetes.io/component: application-controller 8 | name: argocd-application-controller 9 | rules: 10 | - apiGroups: 11 | - '*' 12 | resources: 13 | - '*' 14 | verbs: 15 | - '*' 16 | - nonResourceURLs: 17 | - '*' 18 | verbs: 19 | - '*' 20 | -------------------------------------------------------------------------------- /localdev/argocd/manifests/cluster-rbac/application-controller/argocd-application-controller-clusterrolebinding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: argocd-application-controller 6 | app.kubernetes.io/part-of: argocd 7 | app.kubernetes.io/component: application-controller 8 | name: argocd-application-controller 9 | roleRef: 10 | apiGroup: rbac.authorization.k8s.io 11 | kind: ClusterRole 12 | name: argocd-application-controller 13 | subjects: 14 | - kind: ServiceAccount 15 | name: argocd-application-controller 16 | namespace: kubechecks 17 | -------------------------------------------------------------------------------- /localdev/argocd/manifests/cluster-rbac/application-controller/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | 4 | resources: 5 | - argocd-application-controller-clusterrole.yaml 6 | - argocd-application-controller-clusterrolebinding.yaml 7 | -------------------------------------------------------------------------------- /localdev/argocd/manifests/cluster-rbac/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | 4 | namePrefix: kubechecks- 5 | 6 | resources: 7 | - ./application-controller 8 | - ./server 9 | -------------------------------------------------------------------------------- /localdev/argocd/manifests/cluster-rbac/server/argocd-server-clusterrole.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: argocd-server 6 | app.kubernetes.io/part-of: argocd 7 | app.kubernetes.io/component: server 8 | name: argocd-server 9 | rules: 10 | - apiGroups: 11 | - '*' 12 | resources: 13 | - '*' 14 | verbs: 15 | - delete # supports deletion a live object in UI 16 | - get # supports viewing live object manifest in UI 17 | - patch # supports `argocd app patch` 18 | - apiGroups: 19 | - "" 20 | resources: 21 | - events 22 | verbs: 23 | - list # supports listing events in UI 24 | - apiGroups: 25 | - "" 26 | resources: 27 | - pods 28 | - pods/log 29 | verbs: 30 | - get # supports viewing pod logs from UI 31 | - apiGroups: 32 | - "argoproj.io" 33 | resources: 34 | - "applications" 35 | verbs: 36 | - get 37 | - list 38 | - watch 39 | -------------------------------------------------------------------------------- /localdev/argocd/manifests/cluster-rbac/server/argocd-server-clusterrolebinding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: argocd-server 6 | app.kubernetes.io/part-of: argocd 7 | app.kubernetes.io/component: server 8 | name: argocd-server 9 | roleRef: 10 | apiGroup: rbac.authorization.k8s.io 11 | kind: ClusterRole 12 | name: argocd-server 13 | subjects: 14 | - kind: ServiceAccount 15 | name: argocd-server 16 | namespace: kubechecks 17 | -------------------------------------------------------------------------------- /localdev/argocd/manifests/cluster-rbac/server/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | 4 | resources: 5 | - argocd-server-clusterrole.yaml 6 | - argocd-server-clusterrolebinding.yaml 7 | -------------------------------------------------------------------------------- /localdev/argocd/manifests/crds/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | 4 | resources: 5 | - application-crd.yaml 6 | - appproject-crd.yaml 7 | - applicationset-crd.yaml 8 | -------------------------------------------------------------------------------- /localdev/kubechecks/values.yaml: -------------------------------------------------------------------------------- 1 | configMap: 2 | create: true 3 | env: 4 | GRPC_ENFORCE_ALPN_ENABLED: false 5 | KUBECHECKS_ADDITIONAL_APPS_NAMESPACES: "*" 6 | KUBECHECKS_ARGOCD_REPOSITORY_ENDPOINT: argocd-repo-server.kubechecks:8081 7 | KUBECHECKS_LOG_LEVEL: debug 8 | KUBECHECKS_ENABLE_WEBHOOK_CONTROLLER: "false" 9 | KUBECHECKS_ARGOCD_API_INSECURE: "true" 10 | KUBECHECKS_ARGOCD_API_PATH_PREFIX : '/argocd' 11 | KUBECHECKS_ARGOCD_API_NAMESPACE: 'kubechecks' 12 | KUBECHECKS_WEBHOOK_URL_PREFIX: 'kubechecks' 13 | KUBECHECKS_NAMESPACE: 'kubechecks' 14 | KUBECHECKS_FALLBACK_K8S_VERSION: "1.25.0" 15 | KUBECHECKS_SHOW_DEBUG_INFO: "true" 16 | # OTEL 17 | KUBECHECKS_OTEL_COLLECTOR_PORT: "4317" 18 | KUBECHECKS_OTEL_ENABLED: "false" 19 | # Webhook Management 20 | KUBECHECKS_ENSURE_WEBHOOKS: "true" 21 | KUBECHECKS_MONITOR_ALL_APPLICATIONS: "true" 22 | # 23 | # KUBECHECKS_LABEL_FILTER: "test" # On your PR/MR, prefix this with "kubechecks:" 24 | # KUBECHECKS_SCHEMAS_LOCATION: https://github.com/zapier/kubecheck-schemas.git 25 | KUBECHECKS_REPO_REFRESH_INTERVAL: 30s 26 | KUBECHECKS_TIDY_OUTDATED_COMMENTS_MODE: "delete" 27 | KUBECHECKS_ENABLE_CONFTEST: "false" 28 | KUBECHECKS_REPO_SHALLOW_CLONE: "true" 29 | KUBECHECKS_IDENTIFIER: "test" 30 | 31 | deployment: 32 | annotations: 33 | reloader.stakater.com/auto: "true" 34 | 35 | image: 36 | pullPolicy: IfNotPresent 37 | name: "kubechecks" 38 | tag: "" 39 | 40 | secrets: 41 | create: true 42 | env: 43 | KUBECHECKS_ARGOCD_API_TOKEN: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJhcmdvY2QiLCJzdWIiOiJrdWJlY2hlY2tzOmFwaUtleSIsIm5iZiI6MTY3ODg1Mjc3NywiaWF0IjoxNjc4ODUyNzc3LCJqdGkiOiJrdWJlY2hlY2tzLXRpbHQifQ.58-noxH2GO_8J3gfSiKJdLdniBx4j8wbCqzzGxcwxGU' 44 | 45 | 46 | reloader: 47 | enabled: true 48 | 49 | argocd: 50 | namespace: kubechecks 51 | -------------------------------------------------------------------------------- /localdev/ngrok/Tiltfile: -------------------------------------------------------------------------------- 1 | # ///////////////////////////////////////////////////////////////////////////// 2 | # N G R O K 3 | # ///////////////////////////////////////////////////////////////////////////// 4 | 5 | check_cmd='/usr/bin/curl -v http://localhost:4040/api/tunnels | jq --raw-output ".tunnels[0].public_url"' 6 | 7 | def get_ngrok_url(cfg): 8 | if cfg.get("ngrok_fqdn"): 9 | return "https://"+cfg.get("ngrok_fqdn") 10 | 11 | elif config.tilt_subcommand == 'down': 12 | local('rm ngrok.url || true') 13 | return 'http://xyz.ngrok.io' 14 | 15 | else: 16 | url=str(read_file('ngrok.url', "")).rstrip('\n') 17 | 18 | return url 19 | 20 | def deploy_ngrok(cfg): 21 | # Deploy envoy proxy that ngrok will tunnel to 22 | k8s_yaml( 23 | kustomize('./localdev/ngrok/') 24 | ) 25 | 26 | k8s_resource( 27 | objects=['envoy-config:configmap'], 28 | labels=["ngrok"], 29 | new_name='envoy-config', 30 | resource_deps=['k8s:namespace'] 31 | ) 32 | k8s_resource( 33 | 'envoy', 34 | labels=["ngrok"], 35 | resource_deps=['k8s:namespace', 'envoy-config'], 36 | port_forwards=["8000:8080", "9901:9901"], 37 | ) 38 | 39 | hostnameArg = "" 40 | if cfg.get("ngrok_fqdn"): 41 | hostnameArg = " --hostname {}".format(cfg.get("ngrok_fqdn")) 42 | 43 | local_resource( 44 | "ngrok", 45 | serve_cmd="ngrok http localhost:8000 {}".format(hostnameArg), 46 | readiness_probe=probe( 47 | initial_delay_secs=5, 48 | period_secs=10, 49 | exec=exec_action(["bash", "-c", 'localdev/ngrok/update-url.sh']) 50 | ), 51 | labels=["ngrok"], 52 | links=["http://localhost:4040"] 53 | ) 54 | 55 | watch_file('./ngrok.url') 56 | -------------------------------------------------------------------------------- /localdev/ngrok/envoy_deploy.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: envoy 5 | labels: 6 | app: envoy 7 | annotations: 8 | reloader.stakater.com/auto: "true" 9 | spec: 10 | replicas: 1 11 | template: 12 | metadata: 13 | name: envoy 14 | labels: 15 | app: envoy 16 | spec: 17 | containers: 18 | - name: envoy 19 | image: envoyproxy/envoy:v1.18-latest 20 | imagePullPolicy: IfNotPresent 21 | command: 22 | - /usr/local/bin/envoy 23 | args: 24 | - --config-path 25 | - /etc/envoy/envoy.yaml 26 | - --log-level 27 | - info 28 | ports: 29 | - containerPort: 8080 30 | volumeMounts: 31 | - name: envoy-config 32 | mountPath: /etc/envoy 33 | volumes: 34 | - name: envoy-config 35 | configMap: 36 | name: envoy-config 37 | restartPolicy: Always 38 | selector: 39 | matchLabels: 40 | app: envoy 41 | --- 42 | apiVersion: v1 43 | kind: Service 44 | metadata: 45 | name: envoy 46 | spec: 47 | selector: 48 | app: envoy 49 | ports: 50 | - port: 8080 51 | type: NodePort -------------------------------------------------------------------------------- /localdev/ngrok/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | generatorOptions: 4 | disableNameSuffixHash: true 5 | 6 | namespace: kubechecks 7 | 8 | resources: 9 | - envoy_deploy.yaml 10 | 11 | configMapGenerator: 12 | - name: envoy-config 13 | files: 14 | - envoy.yaml 15 | - tls.key 16 | - tls.crt -------------------------------------------------------------------------------- /localdev/ngrok/tls.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDazCCAlOgAwIBAgIUYmn2lwZY6AvKf5jmiLfCUJfXZbEwDQYJKoZIhvcNAQEL 3 | BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM 4 | GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMzAyMjQwMzIwMzZaFw0yMzAz 5 | MjYwMzIwMzZaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw 6 | HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB 7 | AQUAA4IBDwAwggEKAoIBAQDNYxmux3hhN9HkXuWgQbS3dfMs0TVR1mA5oRDQ+80q 8 | Ot0UKCdFZpg82dhjhSok0QqLybzB7hk+V4UcC9+Uruqbecnzfx8Od53WN4IayQkP 9 | duaOJOl0ZSwXk5gRzp9JxdzWF+9vE3Db9A8C9qHeb9aUi0wq0IT0Smf8TXugV/y3 10 | SINfvMFX9zGPk+TwYAf/DTg4zoBMvbf863zm4JTxN/AkoV2IORzrg7H2D2OH0lXL 11 | rJfeivc91o0bAtsZaqdO5cTUFjvjFopKrrNRRi03EUL/lZ5o1gdtPoQkDX5oLgFT 12 | YZndKLAQxQoXAAmiuWv3gabx62MJ8p+WYvxzME3lngwpAgMBAAGjUzBRMB0GA1Ud 13 | DgQWBBSKd1U/8jINBD66nzoNIMLp05b+jzAfBgNVHSMEGDAWgBSKd1U/8jINBD66 14 | nzoNIMLp05b+jzAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBX 15 | LKKNehGI0wUqBIwcSNcLLS6ypgCMZirVcJd+2aTfGIvCV3CjDLS9NgOUX3UOGOeg 16 | IGcmQStxu/+MWCaQyL1Omcyqocv2ZN8LktTfO97mr6DbNKbHqOuwLsuEqVUG5roU 17 | sWLLu6j34y7IOgLJWbpMyMNRJzYZy/GA5zeyo2XnHFV6YP12v57RS+Kuw8xDHjLE 18 | F7PXswXXSBSFJUHpV/73xTputAeQdt7EDrfLCXSZF6szmTUyLqnJeffV45bTws85 19 | Y2hRrcREMuL9eo8Ni1nQzlgi/ZGlQFJ14f4DpINkLwjdGnQfrH1CsNlKwNA+zqhR 20 | OBB2mnF3YwKyd/A3m1z7 21 | -----END CERTIFICATE----- 22 | -------------------------------------------------------------------------------- /localdev/ngrok/tls.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDNYxmux3hhN9Hk 3 | XuWgQbS3dfMs0TVR1mA5oRDQ+80qOt0UKCdFZpg82dhjhSok0QqLybzB7hk+V4Uc 4 | C9+Uruqbecnzfx8Od53WN4IayQkPduaOJOl0ZSwXk5gRzp9JxdzWF+9vE3Db9A8C 5 | 9qHeb9aUi0wq0IT0Smf8TXugV/y3SINfvMFX9zGPk+TwYAf/DTg4zoBMvbf863zm 6 | 4JTxN/AkoV2IORzrg7H2D2OH0lXLrJfeivc91o0bAtsZaqdO5cTUFjvjFopKrrNR 7 | Ri03EUL/lZ5o1gdtPoQkDX5oLgFTYZndKLAQxQoXAAmiuWv3gabx62MJ8p+WYvxz 8 | ME3lngwpAgMBAAECgf9EAj3hUz9OLN7dXXtJRsdVWvQ+gGpih4ZBtk7HgiNZ2HK+ 9 | sBbubfbACCd3kbRiy0ue/hpvCS1Ssm05yI1T6YPtWhfT66aEXvAMJlU9TkCeGV6N 10 | ZCHCQZam1XOswJcR1po11C1NEKzEd0dGKhXlkefKqqQ/OlaktRJXPhXZAl1JtVv6 11 | lKo/z5EkXNiVSfSFiRKS7qgFyJJDcCO68wv95gdYtFazlVFbvpNQchPLlks2eG6w 12 | bojgAL18gaYlt3jZE03FWBBPgFmJvZUwZdzroTx8HVQtlZ3143d6j4E7HykqZY35 13 | RiT1ntmR/mTBwzXDx9KtsArEx3mB5JQUN1P2xvkCgYEA1OL3l+HjT6KTs5/HTrDV 14 | WBHSlGGanD0xt+MIMKry6Vyau7JPN25QOCu4QC2U0dgf9QuTzcs/xBz87PwNqgba 15 | U/REAGRvxrUp9Zye7nGUoiXJS+l/ZBffxhetAYLDx/+9D8yeR4Npf6YAmMhvbPQ+ 16 | 9nq0BtxTdzcyvbUMomXL9A8CgYEA9vtTPx1LQvB3KzTG8/Q6frfthSTQeP5rbWRy 17 | nkvHDiwGp1i0aHP0BihcOlH2KUKMUFagw+7f9jVDi/dShgerReXDZlpU22R2NKBE 18 | 0revug7dnGp168sEA8rI16j7ZJjP5Wc+UgKvSB93a1VnXdOqnPWslrwZi2W+nwCA 19 | hkBQ5EcCgYEAnu2qZarvZh47IhggVPDS9OKpULjlEcrleSB2ls6oder6YTGmzfz+ 20 | ylBpRxBAT8CHawrvlu0rd58ke09YbBydlZt/wMM6ZyAMaR450Eze54ZKFvAEeJcS 21 | KfK256/VtVOIs2jQqRbEBdXKEEViWfaloqDMEWserJt6uAGXow1YC6UCgYEAr/lc 22 | VADkSfqZfhBpnRIrx3P3aUFUxJDKLDRAsmbdmkxmJUA/spjDisuhAvC5Cqbe4LMI 23 | cvI1YvCKgySiCNtX/kJ6GehMw9DtpAt5XgYAz/mdjsAP6wRIhQcsWPSOwhtbLWGF 24 | dttw1luNM82zC5gv3Qvyf6fgL4E784BhEsaqnCsCgYAtvVDYDnRxZOOTZJx875+v 25 | hvJG7DYq6/4aT+q/8xOvxHr6Z31irRY+EgMsCPd4AUegIQun/HChGWXPwZc2hjiF 26 | ku9PME9Pw5+CJUEHbvJU8GUmyLoAYWpOKd4i4PBVHFrhgxdDJBmS7+KMO9XfC+4Z 27 | 1SVYmyPqoKbeIS5Kvdqb4Q== 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /localdev/ngrok/update-url.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eu 3 | 4 | url=$(curl -s http://localhost:4040/api/tunnels | jq --raw-output ".tunnels[0].public_url") 5 | 6 | if grep -q $url ngrok.url; then 7 | echo "URL already set" 8 | else 9 | echo "Updating ngrok.url" 10 | echo -n $url > ngrok.url 11 | fi -------------------------------------------------------------------------------- /localdev/terraform/github/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | github = { 4 | source = "integrations/github" 5 | version = "~> 5.1" 6 | } 7 | } 8 | } 9 | 10 | # Configure the GitHub Provider 11 | provider "github" { 12 | # Use GITHUB_TOKEN as an env var to auth 13 | } 14 | 15 | variable "parent_vars" { 16 | type = map(string) 17 | default = {} 18 | } 19 | 20 | variable "kubechecks_github_hook_secret_key" { 21 | default = "asdf" 22 | } 23 | 24 | variable "ngrok_url" { 25 | default = "https://httpbin.org/post" 26 | } 27 | 28 | variable "kubecheck_webhook_prefix" { 29 | default = "kubechecks/hooks" 30 | } 31 | 32 | module "vcs_files" { 33 | source = "../modules/vcs_files" 34 | } 35 | 36 | locals { 37 | random_pet = try(var.parent_vars.random_pet, "") 38 | } 39 | 40 | # Make a backup of the settings provided by parent TF workspace 41 | # If the parent is destroyed it will remove the tfvars file that this 42 | # workspace would need to also do a destroy. 43 | # TF loads the tfvars in alphabetical order, so the parent.auto.tfvars 44 | # will take precedence. 45 | resource "local_file" "localdev_auto_tfvars" { 46 | filename = "localdev.auto.tfvars" 47 | content = < file("${path.module}/base_files/${f}") 8 | } 9 | } 10 | 11 | output "mr_files" { 12 | value = { 13 | 1 = { 14 | for f in fileset("${path.module}/mr1_files", "**/*") : f => file("${path.module}/mr1_files/${f}") 15 | } 16 | 2 = { 17 | for f in fileset("${path.module}/mr2_files", "**/*") : f => file("${path.module}/mr2_files/${f}") 18 | } 19 | 3 = { 20 | for f in fileset("${path.module}/mr3_files", "**/*") : f => file("${path.module}/mr3_files/${f}") 21 | } 22 | 4 = { 23 | for f in fileset("${path.module}/mr4_files", "**/*") : f => file("${path.module}/mr4_files/${f}") 24 | } 25 | 5 = { 26 | for f in fileset("${path.module}/mr5_files", "**/*") : f => file("${path.module}/mr5_files/${f}") 27 | } 28 | 6 = { 29 | for f in fileset("${path.module}/mr6_files", "**/*") : f => file("${path.module}/mr6_files/${f}") 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /localdev/terraform/modules/vcs_files/mr1_files/apps/httpbin/overlays/in-cluster/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | 4 | resources: 5 | - ../../base 6 | 7 | patchesStrategicMerge: 8 | - replica-patch.yaml -------------------------------------------------------------------------------- /localdev/terraform/modules/vcs_files/mr1_files/apps/httpbin/overlays/in-cluster/replica-patch.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: httpbin 5 | spec: 6 | replicas: 2 -------------------------------------------------------------------------------- /localdev/terraform/modules/vcs_files/mr2_files/apps/echo-server/in-cluster/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | version: 1.0.0 3 | name: echo-server 4 | dependencies: 5 | - name: echo-server 6 | version: 0.5.0 7 | repository: https://ealenn.github.io/charts -------------------------------------------------------------------------------- /localdev/terraform/modules/vcs_files/mr2_files/apps/echo-server/in-cluster/values.yaml: -------------------------------------------------------------------------------- 1 | echo-server: 2 | replicaCount: 3 -------------------------------------------------------------------------------- /localdev/terraform/modules/vcs_files/mr3_files/apps/httpbin/base/external-ingress.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.k8s.io/v1 2 | kind: Ingress 3 | metadata: 4 | name: httpbin-external 5 | annotations: 6 | kubernetes.io/ingress.class: myingressclass 7 | nginx.ingress.kubernetes.io/rewrite-target: /$1 8 | spec: 9 | rules: 10 | - http: 11 | paths: 12 | - path: /httpbin/(.*) 13 | backend: 14 | service: 15 | name: httpbin 16 | port: 17 | name: http 18 | pathType: ImplementationSpecific 19 | -------------------------------------------------------------------------------- /localdev/terraform/modules/vcs_files/mr3_files/apps/httpbin/base/internal-ingress.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.k8s.io/v1 2 | kind: Ingress 3 | metadata: 4 | name: httpbin-internal 5 | annotations: 6 | kubernetes.io/ingress.class: myingressclass 7 | nginx.ingress.kubernetes.io/rewrite-target: /$1 8 | spec: 9 | rules: 10 | - http: 11 | paths: 12 | - path: /helloworld/(.*) 13 | backend: 14 | service: 15 | name: helloworld-svc 16 | port: 17 | name: http 18 | pathType: ImplementationSpecific 19 | -------------------------------------------------------------------------------- /localdev/terraform/modules/vcs_files/mr3_files/apps/httpbin/base/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | 4 | resources: 5 | - deployment.yaml 6 | - service.yaml 7 | - serviceAccount.yaml 8 | - internal-ingress.yaml 9 | - external-ingress.yaml -------------------------------------------------------------------------------- /localdev/terraform/modules/vcs_files/mr4_files/apps/echo-server/in-cluster/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | version: 1.0.0 3 | name: echo-server 4 | dependencies: 5 | - name: echo-server 6 | version: 0.5.0 7 | repository: https://ealenn.github.io/charts -------------------------------------------------------------------------------- /localdev/terraform/modules/vcs_files/mr4_files/apps/echo-server/in-cluster/values.yaml: -------------------------------------------------------------------------------- 1 | echo-server: 2 | replicaCount: 3 -------------------------------------------------------------------------------- /localdev/terraform/modules/vcs_files/mr4_files/apps/httpbin/base/external-ingress.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.k8s.io/v1beta1 2 | kind: Ingress 3 | metadata: 4 | name: httpbin-external 5 | annotations: 6 | kubernetes.io/ingress.class: myingressclass 7 | nginx.ingress.kubernetes.io/rewrite-target: /$1 8 | spec: 9 | rules: 10 | - http: 11 | paths: 12 | - path: /httpbin/(.*) 13 | backend: 14 | serviceName: httpbin 15 | servicePort: 8080 -------------------------------------------------------------------------------- /localdev/terraform/modules/vcs_files/mr4_files/apps/httpbin/base/internal-ingress.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.k8s.io/v1 2 | kind: Ingress 3 | metadata: 4 | name: httpbin-internal 5 | annotations: 6 | kubernetes.io/ingress.class: myingressclass 7 | nginx.ingress.kubernetes.io/rewrite-target: /$1 8 | spec: 9 | rules: 10 | - http: 11 | paths: 12 | - path: /helloworld/(.*) 13 | backend: 14 | serviceName: helloworld-svc 15 | servicePort: 8080 -------------------------------------------------------------------------------- /localdev/terraform/modules/vcs_files/mr4_files/apps/httpbin/base/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | 4 | resources: 5 | - deployment.yaml 6 | - service.yaml 7 | - serviceAccount.yaml 8 | - internal-ingress.yaml 9 | - external-ingress.yaml -------------------------------------------------------------------------------- /localdev/terraform/modules/vcs_files/mr5_files/apps/httpdump/overlays/a/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | 4 | resources: 5 | - ../../base 6 | 7 | patchesStrategicMerge: 8 | - replica-patch.yaml -------------------------------------------------------------------------------- /localdev/terraform/modules/vcs_files/mr5_files/apps/httpdump/overlays/a/replica-patch.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: httpdump 5 | spec: 6 | replicas: 2 -------------------------------------------------------------------------------- /localdev/terraform/modules/vcs_files/mr6_files/apps/httpdump/base/external-ingress.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.k8s.io/v1 2 | kind: Ingress 3 | metadata: 4 | name: httpdump-external 5 | annotations: 6 | kubernetes.io/ingress.class: ingressclass 7 | nginx.ingress.kubernetes.io/rewrite-target: /$1 8 | spec: 9 | rules: 10 | - http: 11 | paths: 12 | - path: /httpbin/(.*) 13 | backend: 14 | service: 15 | name: httpbin 16 | port: 17 | name: http 18 | pathType: ImplementationSpecific 19 | -------------------------------------------------------------------------------- /localdev/terraform/modules/vcs_files/mr6_files/apps/httpdump/base/internal-ingress.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.k8s.io/v1 2 | kind: Ingress 3 | metadata: 4 | name: httpdump-internal 5 | annotations: 6 | kubernetes.io/ingress.class: myingressclass 7 | nginx.ingress.kubernetes.io/rewrite-target: /$1 8 | spec: 9 | rules: 10 | - http: 11 | paths: 12 | - path: /httpbin/(.*) 13 | backend: 14 | service: 15 | name: httpbin 16 | port: 17 | name: http 18 | pathType: ImplementationSpecific 19 | -------------------------------------------------------------------------------- /localdev/terraform/modules/vcs_files/mr6_files/apps/httpdump/base/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | 4 | resources: 5 | - deployment.yaml 6 | - service.yaml 7 | - serviceAccount.yaml 8 | - internal-ingress.yaml 9 | - external-ingress.yaml -------------------------------------------------------------------------------- /localdev/test_apps/Tiltfile: -------------------------------------------------------------------------------- 1 | 2 | # ///////////////////////////////////////////////////////////////////////////// 3 | # Test ArgoCD Applications 4 | # ///////////////////////////////////////////////////////////////////////////// 5 | 6 | k8s_kind('Applications', api_version="apiextensions.k8s.io/v1") 7 | 8 | def install_test_apps(cfg): 9 | if config.tilt_subcommand != 'down': 10 | # Load the terraform url we output, default to gitlab if cant find a vcs-type variable 11 | vcsPath = "./localdev/terraform/{}/project.url".format(cfg.get('vcs-type', 'gitlab')) 12 | print("Path to url: " + vcsPath) 13 | projectUrl=str(read_file(vcsPath, "")).strip('\n') 14 | print("Remote Project URL: " + projectUrl) 15 | 16 | for app in ["echo-server", "httpbin", "app-root"]: 17 | print("Creating Test App: " + app) 18 | 19 | # read the application YAML and patch the repoURL 20 | objects = read_yaml_stream("localdev/test_apps/{}.yaml".format(app)) 21 | 22 | for o in objects: 23 | o['metadata']['namespace'] = "kubechecks" 24 | o['spec']['source']['repoURL'] = projectUrl 25 | k8s_yaml(encode_yaml_stream(objects)) 26 | 27 | k8s_resource( 28 | new_name=app, 29 | objects=['in-cluster-{}:application'.format(app)], 30 | labels=["test_apps"], 31 | resource_deps=["argocd-crds","argocd"], 32 | ) 33 | -------------------------------------------------------------------------------- /localdev/test_apps/app-root.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: argoproj.io/v1alpha1 2 | kind: Application 3 | metadata: 4 | name: in-cluster-app-root 5 | namespace: kubechecks 6 | finalizers: 7 | - resources-finalizer.argocd.argoproj.io 8 | spec: 9 | destination: 10 | name: '' 11 | namespace: approot 12 | server: https://kubernetes.default.svc 13 | source: 14 | directory: 15 | recurse: true 16 | path: appsets/ 17 | repoURL: ${REPO_URL} 18 | targetRevision: HEAD 19 | 20 | sources: [] 21 | project: default 22 | syncPolicy: 23 | automated: 24 | prune: true 25 | selfHeal: false 26 | syncOptions: 27 | - CreateNamespace=true 28 | - ServerSideApply=true 29 | - CreateProjects=true 30 | -------------------------------------------------------------------------------- /localdev/test_apps/echo-server.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: argoproj.io/v1alpha1 2 | kind: Application 3 | metadata: 4 | name: in-cluster-echo-server 5 | namespace: kubechecks 6 | finalizers: 7 | - resources-finalizer.argocd.argoproj.io 8 | spec: 9 | destination: 10 | name: '' 11 | namespace: echo-server 12 | server: 'https://kubernetes.default.svc' 13 | source: 14 | path: apps/echo-server/in-cluster 15 | repoURL: ${REPO_URL} 16 | targetRevision: HEAD 17 | helm: 18 | valueFiles: 19 | - values.yaml 20 | sources: [] 21 | project: default 22 | syncPolicy: 23 | automated: 24 | prune: true 25 | selfHeal: false 26 | syncOptions: 27 | - CreateNamespace=true 28 | - ServerSideApply=true 29 | -------------------------------------------------------------------------------- /localdev/test_apps/httpbin.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: argoproj.io/v1alpha1 2 | kind: Application 3 | metadata: 4 | name: in-cluster-httpbin 5 | namespace: kubechecks 6 | finalizers: 7 | - resources-finalizer.argocd.argoproj.io 8 | spec: 9 | destination: 10 | namespace: httpbin 11 | server: https://kubernetes.default.svc 12 | project: default 13 | source: 14 | path: apps/httpbin/overlays/in-cluster/ 15 | repoURL: ${REPO_URL} 16 | targetRevision: HEAD 17 | kustomize: {} 18 | syncPolicy: 19 | automated: 20 | prune: true 21 | syncOptions: 22 | - CreateNamespace=true 23 | 24 | -------------------------------------------------------------------------------- /localdev/test_appsets/Tiltfile: -------------------------------------------------------------------------------- 1 | 2 | # ///////////////////////////////////////////////////////////////////////////// 3 | # Test ArgoCD Applications 4 | # ///////////////////////////////////////////////////////////////////////////// 5 | 6 | def copy_test_appsets(cfg): 7 | # Load the terraform url we output, default to gitlab if cant find a vcs-type variable 8 | vcsPath = "./localdev/terraform/{}/project.url".format(cfg.get('vcs-type', 'gitlab')) 9 | print("Path to url: " + vcsPath) 10 | projectUrl=str(read_file(vcsPath, "")).strip('\n') 11 | print("Remote Project URL: " + projectUrl) 12 | 13 | if projectUrl != "": 14 | for appset in ["httpdump","echo-server"]: 15 | source_file = "./localdev/test_appsets/{}.yaml".format(appset) 16 | dest_file = "./localdev/terraform/modules/vcs_files/base_files/appsets/{}/{}.yaml".format(appset,appset) 17 | 18 | # Copy the file to the specific terraform directory 19 | local("mkdir -p ./localdev/terraform/modules/vcs_files/base_files/appsets/{} && cp {} {}".format(appset, source_file, dest_file)) 20 | 21 | # Modify the copied file to replace ${REPO_URL} with projectUrl 22 | local("sed -i '' 's#REPO_URL#{}#g' {}".format(projectUrl, dest_file)) 23 | -------------------------------------------------------------------------------- /localdev/test_appsets/echo-server.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: argoproj.io/v1alpha1 2 | kind: ApplicationSet 3 | metadata: 4 | name: app-set-echoserver 5 | namespace: kubechecks 6 | spec: 7 | generators: 8 | - clusters: 9 | selector: 10 | matchLabels: 11 | environment: development 12 | values: 13 | cluster: 'in-cluster' 14 | url: https://kubernetes.default.svc 15 | template: 16 | metadata: 17 | finalizers: 18 | - resources-finalizer.argocd.argoproj.io 19 | name: "in-cluster-echo-server-{{ metadata.labels.environment }}" 20 | namespace: kubechecks 21 | labels: 22 | argocd.argoproj.io/application-set-name: "echo-server" 23 | spec: 24 | destination: 25 | namespace: "echo-server-{{ metadata.labels.environment }}" 26 | server: '{{ values.url }}' 27 | project: default 28 | source: 29 | repoURL: REPO_URL 30 | targetRevision: HEAD 31 | path: 'apps/app-set-echo-server/{{ values.cluster }}' 32 | helm: 33 | valueFiles: 34 | - values.yaml 35 | - values-{{ metadata.labels.environment }}.yaml 36 | ignoreMissingValueFiles: true 37 | values: |- 38 | echo-server: 39 | replicaCount: 2 40 | syncPolicy: 41 | automated: 42 | prune: true 43 | syncOptions: 44 | - CreateNamespace=true 45 | sources: [] 46 | -------------------------------------------------------------------------------- /localdev/test_appsets/httpdump.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: argoproj.io/v1alpha1 2 | kind: ApplicationSet 3 | metadata: 4 | name: httpdump 5 | namespace: kubechecks 6 | spec: 7 | generators: 8 | # this is a simple list generator 9 | - list: 10 | elements: 11 | - name: a 12 | url: https://kubernetes.default.svc 13 | - name: b 14 | url: https://kubernetes.default.svc 15 | template: 16 | metadata: 17 | finalizers: 18 | - resources-finalizer.argocd.argoproj.io 19 | name: "in-cluster-{{ name }}-httpdump" 20 | namespace: kubechecks 21 | labels: 22 | argocd.argoproj.io/application-set-name: "httpdump" 23 | spec: 24 | destination: 25 | namespace: "httpdump-{{ name }}" 26 | server: '{{ url }}' 27 | project: default 28 | source: 29 | repoURL: REPO_URL 30 | targetRevision: HEAD 31 | path: 'apps/httpdump/overlays/{{ name }}/' 32 | syncPolicy: 33 | automated: 34 | prune: true 35 | syncOptions: 36 | - CreateNamespace=true 37 | sources: [] 38 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/zapier/kubechecks/cmd" 5 | ) 6 | 7 | func main() { 8 | cmd.Execute() 9 | } 10 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: kubechecks 2 | nav: 3 | - Home: index.md 4 | - Usage: usage.md 5 | - Architecture: architecture.md 6 | - Contributing: contributing.md 7 | theme: 8 | name: material 9 | 10 | markdown_extensions: 11 | - attr_list -------------------------------------------------------------------------------- /pkg/affected_apps/argocd_matcher.go: -------------------------------------------------------------------------------- 1 | package affected_apps 2 | 3 | import ( 4 | "context" 5 | "os" 6 | 7 | "github.com/rs/zerolog/log" 8 | "github.com/zapier/kubechecks/pkg/appdir" 9 | "github.com/zapier/kubechecks/pkg/git" 10 | ) 11 | 12 | type ArgocdMatcher struct { 13 | appsDirectory *appdir.AppDirectory 14 | appSetsDirectory *appdir.AppSetDirectory 15 | } 16 | 17 | func NewArgocdMatcher(vcsToArgoMap appdir.VcsToArgoMap, repo *git.Repo) (*ArgocdMatcher, error) { 18 | repoApps := getArgocdApps(vcsToArgoMap, repo) 19 | kustomizeAppFiles := getKustomizeApps(vcsToArgoMap, repo, repo.Directory) 20 | 21 | appDirectory := appdir.NewAppDirectory(). 22 | Union(repoApps). 23 | Union(kustomizeAppFiles) 24 | 25 | repoAppSets := getArgocdAppSets(vcsToArgoMap, repo) 26 | appSetDirectory := appdir.NewAppSetDirectory(). 27 | Union(repoAppSets) 28 | 29 | return &ArgocdMatcher{ 30 | appsDirectory: appDirectory, 31 | appSetsDirectory: appSetDirectory, 32 | }, nil 33 | } 34 | 35 | func logCounts(repoApps *appdir.AppDirectory) { 36 | if repoApps == nil { 37 | log.Debug().Msg("found no apps") 38 | } else { 39 | log.Debug().Int("apps", repoApps.AppsCount()). 40 | Int("app_files", repoApps.AppFilesCount()). 41 | Int("app_dirs", repoApps.AppDirsCount()). 42 | Msg("mapped apps") 43 | } 44 | } 45 | 46 | func getKustomizeApps(vcsToArgoMap appdir.VcsToArgoMap, repo *git.Repo, repoPath string) *appdir.AppDirectory { 47 | log.Debug().Msgf("creating fs for %s", repoPath) 48 | fs := os.DirFS(repoPath) 49 | 50 | log.Debug().Msg("following kustomize apps") 51 | kustomizeAppFiles := vcsToArgoMap.WalkKustomizeApps(repo.CloneURL, fs) 52 | 53 | logCounts(kustomizeAppFiles) 54 | return kustomizeAppFiles 55 | } 56 | 57 | func getArgocdApps(vcsToArgoMap appdir.VcsToArgoMap, repo *git.Repo) *appdir.AppDirectory { 58 | log.Debug().Msgf("looking for %s repos", repo.CloneURL) 59 | repoApps := vcsToArgoMap.GetAppsInRepo(repo.CloneURL) 60 | 61 | logCounts(repoApps) 62 | return repoApps 63 | } 64 | 65 | func getArgocdAppSets(vcsToArgoMap appdir.VcsToArgoMap, repo *git.Repo) *appdir.AppSetDirectory { 66 | log.Debug().Msgf("looking for %s repos", repo.CloneURL) 67 | repoApps := vcsToArgoMap.GetAppSetsInRepo(repo.CloneURL) 68 | 69 | if repoApps == nil { 70 | log.Debug().Msg("found no appSets") 71 | } else { 72 | log.Debug().Msgf("found %d appSets", repoApps.Count()) 73 | } 74 | return repoApps 75 | } 76 | 77 | func (a *ArgocdMatcher) AffectedApps(_ context.Context, changeList []string, targetBranch string, repo *git.Repo) (AffectedItems, error) { 78 | if a.appsDirectory == nil { 79 | return AffectedItems{}, nil 80 | } 81 | 82 | appsSlice := a.appsDirectory.FindAppsBasedOnChangeList(changeList, targetBranch) 83 | appSetsSlice := a.appSetsDirectory.FindAppSetsBasedOnChangeList(changeList, repo) 84 | 85 | // and return both apps and appSets 86 | return AffectedItems{ 87 | Applications: appsSlice, 88 | ApplicationSets: appSetsSlice, 89 | }, nil 90 | } 91 | 92 | var _ Matcher = new(ArgocdMatcher) 93 | -------------------------------------------------------------------------------- /pkg/affected_apps/matcher.go: -------------------------------------------------------------------------------- 1 | package affected_apps 2 | 3 | import ( 4 | "context" 5 | "path" 6 | 7 | "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" 8 | "github.com/zapier/kubechecks/pkg/git" 9 | ) 10 | 11 | type AffectedItems struct { 12 | Applications []v1alpha1.Application 13 | ApplicationSets []v1alpha1.ApplicationSet 14 | } 15 | 16 | func (ai AffectedItems) Union(other AffectedItems) AffectedItems { 17 | // merge apps 18 | appNameSet := make(map[string]struct{}) 19 | for _, app := range ai.Applications { 20 | appNameSet[app.Name] = struct{}{} 21 | } 22 | for _, app := range other.Applications { 23 | if _, ok := appNameSet[app.Name]; ok { 24 | continue 25 | } 26 | 27 | ai.Applications = append(ai.Applications, app) 28 | } 29 | 30 | // merge appsets 31 | appSetNameSet := make(map[string]struct{}) 32 | for _, appSet := range ai.ApplicationSets { 33 | appSetNameSet[appSet.Name] = struct{}{} 34 | } 35 | for _, appSet := range other.ApplicationSets { 36 | if _, ok := appSetNameSet[appSet.Name]; ok { 37 | continue 38 | } 39 | 40 | ai.ApplicationSets = append(ai.ApplicationSets, appSet) 41 | } 42 | 43 | // return the merge 44 | return ai 45 | } 46 | 47 | type ApplicationSet struct { 48 | Name string 49 | } 50 | 51 | type Matcher interface { 52 | AffectedApps(ctx context.Context, changeList []string, targetBranch string, repo *git.Repo) (AffectedItems, error) 53 | } 54 | 55 | // modifiedDirs filters a list of changed files down to a list 56 | // the unique dirs containing the changed files 57 | func modifiedDirs(changeList []string) []string { 58 | dirMap := map[string]bool{} 59 | for _, file := range changeList { 60 | dir := path.Dir(file) 61 | dirMap[dir] = true 62 | } 63 | 64 | dirs := []string{} 65 | for k := range dirMap { 66 | dirs = append(dirs, k) 67 | } 68 | 69 | return dirs 70 | } 71 | -------------------------------------------------------------------------------- /pkg/affected_apps/matcher_test.go: -------------------------------------------------------------------------------- 1 | package affected_apps 2 | 3 | import ( 4 | "reflect" 5 | "sort" 6 | "testing" 7 | ) 8 | 9 | func Test_modifiedDirs(t *testing.T) { 10 | type args struct { 11 | changeList []string 12 | } 13 | tests := []struct { 14 | name string 15 | args args 16 | want []string 17 | }{ 18 | { 19 | "basic", 20 | args{ 21 | changeList: []string{ 22 | "foo/bar/file.txt", 23 | "foo/bar/file2.yaml", 24 | "foo/baz/thing", 25 | }, 26 | }, 27 | []string{ 28 | "foo/bar", 29 | "foo/baz", 30 | }, 31 | }, 32 | } 33 | for _, tt := range tests { 34 | t.Run(tt.name, func(t *testing.T) { 35 | got := modifiedDirs(tt.args.changeList) 36 | sort.Strings(got) 37 | sort.Strings(tt.want) 38 | if !reflect.DeepEqual(got, tt.want) { 39 | t.Errorf("modifiedDirs() = %v, want %v", got, tt.want) 40 | } 41 | }) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /pkg/affected_apps/multi_matcher.go: -------------------------------------------------------------------------------- 1 | package affected_apps 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/pkg/errors" 7 | "github.com/zapier/kubechecks/pkg/git" 8 | ) 9 | 10 | func NewMultiMatcher(matchers ...Matcher) Matcher { 11 | return MultiMatcher{matchers: matchers} 12 | } 13 | 14 | type MultiMatcher struct { 15 | matchers []Matcher 16 | } 17 | 18 | func (m MultiMatcher) AffectedApps(ctx context.Context, changeList []string, targetBranch string, repo *git.Repo) (AffectedItems, error) { 19 | var total AffectedItems 20 | 21 | for index, matcher := range m.matchers { 22 | items, err := matcher.AffectedApps(ctx, changeList, targetBranch, repo) 23 | if err != nil { 24 | return total, errors.Wrapf(err, "failed to find items in matcher #%d", index) 25 | } 26 | total = total.Union(items) 27 | } 28 | 29 | return total, nil 30 | } 31 | -------------------------------------------------------------------------------- /pkg/aisummary/diff_summary.go: -------------------------------------------------------------------------------- 1 | package aisummary 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/sashabaranov/go-openai" 8 | "go.opentelemetry.io/otel" 9 | 10 | "github.com/zapier/kubechecks/telemetry" 11 | ) 12 | 13 | var tracer = otel.Tracer("pkg/aisummary") 14 | 15 | // SummarizeDiff uses ChatGPT to summarize changes to a Kubernetes application. 16 | func (c *OpenAiClient) SummarizeDiff(ctx context.Context, appName, diff string) (string, error) { 17 | ctx, span := tracer.Start(ctx, "SummarizeDiff") 18 | defer span.End() 19 | 20 | model := openai.GPT4o 21 | if len(diff) < 3500 { 22 | model = openai.GPT3Dot5Turbo 23 | } 24 | 25 | if c.enabled { 26 | req := createCompletionRequest( 27 | model, 28 | appName, 29 | summarizeManifestDiffPrompt, 30 | diff, 31 | "\n**AI Summary**\n", 32 | ) 33 | 34 | resp, err := c.makeCompletionRequestWithBackoff(ctx, req) 35 | if err != nil { 36 | telemetry.SetError(span, err, "ChatCompletionStream error") 37 | fmt.Printf("ChatCompletionStream error: %v\n", err) 38 | return "", err 39 | } 40 | 41 | return resp.Choices[0].Message.Content, nil 42 | 43 | } 44 | return "", nil 45 | } 46 | 47 | const completionSystemPrompt = `You are a helpful Kubernetes expert. 48 | You can summarize Kubernetes YAML manifests for application developers that may not be familiar with all Kubernetes resource types. 49 | Answer as concisely as possible.` 50 | 51 | const summarizeManifestDiffPrompt = `Provide a concise summary of the diff (surrounded by the chars "#***") 52 | that will be applied to the Kubernetes YAML manifests for an application named: %s 53 | Use natural language, bullet points, emoji and format as Gitlab flavored markdown. 54 | Describe the impact of each change. 55 | 56 | #*** 57 | %s 58 | #*** 59 | ` 60 | -------------------------------------------------------------------------------- /pkg/aisummary/openai_client.go: -------------------------------------------------------------------------------- 1 | package aisummary 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | "sync" 8 | "time" 9 | 10 | "github.com/cenkalti/backoff/v4" 11 | "github.com/rs/zerolog/log" 12 | "github.com/sashabaranov/go-openai" 13 | ) 14 | 15 | type OpenAiClient struct { 16 | client *openai.Client 17 | enabled bool 18 | } 19 | 20 | var openAiClient *OpenAiClient 21 | var once sync.Once 22 | 23 | func GetOpenAiClient(apiToken string) *OpenAiClient { 24 | once.Do(func() { 25 | if apiToken != "" { 26 | log.Info().Msg("enabling OpenAI client") 27 | client := openai.NewClient(apiToken) 28 | openAiClient = &OpenAiClient{client: client, enabled: true} 29 | } else { 30 | log.Debug().Msg("OpenAI client not enabled") 31 | openAiClient = &OpenAiClient{enabled: false} 32 | } 33 | }) 34 | return openAiClient 35 | } 36 | 37 | func createCompletionRequest(model, appName string, prompt string, content string, prefix string) openai.ChatCompletionRequest { 38 | var summarizeRequest = openai.ChatCompletionRequest{ 39 | Model: model, 40 | MaxTokens: 500, 41 | Temperature: 0.4, 42 | Messages: []openai.ChatCompletionMessage{ 43 | { 44 | Role: openai.ChatMessageRoleSystem, 45 | Content: completionSystemPrompt, 46 | }, 47 | { 48 | Role: openai.ChatMessageRoleUser, 49 | Content: fmt.Sprintf(prompt, appName, content), 50 | }, 51 | { 52 | Role: openai.ChatMessageRoleAssistant, 53 | Content: prefix, 54 | }, 55 | }, 56 | Stream: false, 57 | } 58 | return summarizeRequest 59 | } 60 | 61 | func (c *OpenAiClient) makeCompletionRequestWithBackoff(ctx context.Context, req openai.ChatCompletionRequest) (openai.ChatCompletionResponse, error) { 62 | ctx, span := tracer.Start(ctx, "MakeCompletionRequestWithBackoff") 63 | defer span.End() 64 | // Lets setup backoff logic to retry this request for 1 minute 65 | bOff := backoff.NewExponentialBackOff() 66 | bOff.MaxInterval = 10 * time.Second 67 | bOff.RandomizationFactor = 0 68 | bOff.MaxElapsedTime = 2 * time.Minute 69 | 70 | var resp openai.ChatCompletionResponse 71 | err := backoff.Retry(func() error { 72 | var err error 73 | resp, err = c.client.CreateChatCompletion(ctx, req) 74 | if err != nil { 75 | if !strings.Contains(err.Error(), "status code: 429") && !strings.Contains(err.Error(), "status code: 5") { 76 | return backoff.Permanent(err) 77 | } 78 | log.Debug().Msgf("%v - %s", resp, err) 79 | } 80 | 81 | return err 82 | }, bOff) 83 | return resp, err 84 | } 85 | -------------------------------------------------------------------------------- /pkg/argo_client/kustomize.go: -------------------------------------------------------------------------------- 1 | package argo_client 2 | 3 | import ( 4 | "io" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/pkg/errors" 9 | "github.com/zapier/kubechecks/pkg" 10 | ) 11 | 12 | // addFile copies a file from the repository to the temp directory to prepare to be sent to the ArgoCD API. 13 | func addFile(repoRoot string, tempDir string, relPath string) error { 14 | absDepPath := filepath.Clean(filepath.Join(repoRoot, relPath)) 15 | 16 | // Get relative path from repo root 17 | relPath, err := filepath.Rel(repoRoot, absDepPath) 18 | if err != nil { 19 | return errors.Wrapf(err, "failed to get relative path for %s", absDepPath) 20 | } 21 | 22 | // check if the file exists in the temp directory 23 | // skip copying if it exists 24 | tempPath := filepath.Join(tempDir, relPath) 25 | if _, err := os.Stat(tempPath); err == nil { 26 | return nil 27 | } 28 | 29 | dstdir := filepath.Dir(tempPath) 30 | if err := os.MkdirAll(dstdir, 0o777); err != nil { 31 | return errors.Wrap(err, "failed to make directories") 32 | } 33 | 34 | r, err := os.Open(absDepPath) 35 | if err != nil { 36 | return err 37 | } 38 | defer pkg.WithErrorLogging(r.Close, "failed to close file") 39 | 40 | w, err := os.Create(tempPath) 41 | if err != nil { 42 | return err 43 | } 44 | 45 | defer func() { 46 | // Report the error, if any, from Close, but do so 47 | // only if there isn't already an outgoing error. 48 | if c := w.Close(); err == nil { 49 | err = c 50 | } 51 | }() 52 | 53 | _, err = io.Copy(w, r) 54 | return errors.Wrap(err, "failed to copy file") 55 | } 56 | -------------------------------------------------------------------------------- /pkg/argo_client/metrics.go: -------------------------------------------------------------------------------- 1 | package argo_client 2 | 3 | import "github.com/prometheus/client_golang/prometheus" 4 | 5 | var ( 6 | commonLabels = []string{ 7 | "application", 8 | } 9 | getManifestsSuccess = prometheus.NewCounterVec( 10 | prometheus.CounterOpts{ 11 | Namespace: "kubechecks", 12 | Name: "get_manifests_success", 13 | Help: "Count of all attempts to get application manifests that succeeded", 14 | }, 15 | commonLabels, 16 | ) 17 | getManifestsFailed = prometheus.NewCounterVec( 18 | prometheus.CounterOpts{ 19 | Namespace: "kubechecks", 20 | Name: "get_manifests_failure", 21 | Help: "Count of all attempts to get application manifests that failed", 22 | }, 23 | commonLabels, 24 | ) 25 | 26 | buckets = []float64{1, 30, 60, 300} 27 | 28 | getManifestsDuration = prometheus.NewHistogramVec(prometheus.HistogramOpts{ 29 | Namespace: "kubechecks", 30 | Name: "get_manifests_duration_seconds", 31 | Help: "Histogram of response time for application manifest generation", 32 | Buckets: buckets, 33 | }, 34 | commonLabels, 35 | ) 36 | ) 37 | 38 | func init() { 39 | r := prometheus.DefaultRegisterer 40 | 41 | r.MustRegister(getManifestsFailed) 42 | r.MustRegister(getManifestsSuccess) 43 | r.MustRegister(getManifestsDuration) 44 | } 45 | -------------------------------------------------------------------------------- /pkg/checks/diff/ai_summary.go: -------------------------------------------------------------------------------- 1 | package diff 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | 7 | "github.com/rs/zerolog/log" 8 | "go.opentelemetry.io/otel" 9 | "golang.org/x/net/context" 10 | 11 | "github.com/zapier/kubechecks/pkg" 12 | "github.com/zapier/kubechecks/pkg/aisummary" 13 | "github.com/zapier/kubechecks/pkg/config" 14 | "github.com/zapier/kubechecks/pkg/msg" 15 | "github.com/zapier/kubechecks/telemetry" 16 | ) 17 | 18 | var tracer = otel.Tracer("pkg/checks/diff") 19 | 20 | func aiDiffSummary(ctx context.Context, mrNote *msg.Message, cfg config.ServerConfig, name, diff string) { 21 | ctx, span := tracer.Start(ctx, "aiDiffSummary") 22 | defer span.End() 23 | 24 | log.Debug().Str("name", name).Msg("generating ai diff summary for application...") 25 | if mrNote == nil { 26 | return 27 | } 28 | 29 | aiClient := aisummary.GetOpenAiClient(cfg.OpenAIAPIToken) 30 | aiSummary, err := aiClient.SummarizeDiff(ctx, name, diff) 31 | if err != nil { 32 | telemetry.SetError(span, err, "OpenAI SummarizeDiff") 33 | log.Error().Err(err).Msg("failed to summarize diff") 34 | cr := msg.Result{State: pkg.StateNone, Summary: "failed to summarize diff", Details: err.Error()} 35 | mrNote.AddToAppMessage(ctx, name, cr) 36 | return 37 | } 38 | 39 | aiSummary = cleanUpAiSummary(aiSummary) 40 | if aiSummary == "" { 41 | return 42 | } 43 | 44 | cr := msg.Result{State: pkg.StateNone, Summary: "Show AI Summary Diff", Details: aiSummary} 45 | mrNote.AddToAppMessage(ctx, name, cr) 46 | } 47 | 48 | var codeBlockBegin = regexp.MustCompile("^```(\\w+)") 49 | 50 | func cleanUpAiSummary(aiSummary string) string { 51 | aiSummary = strings.TrimSpace(aiSummary) 52 | 53 | // occasionally the model thinks it should wrap it in a code block. 54 | // comments do not need this, as they are already rendered as markdown. 55 | for { 56 | newSummary := aiSummary 57 | 58 | newSummary = codeBlockBegin.ReplaceAllString(newSummary, "") 59 | newSummary = strings.TrimPrefix(newSummary, "#***") 60 | newSummary = strings.TrimSuffix(newSummary, "```") 61 | newSummary = strings.TrimSuffix(newSummary, "#***") 62 | newSummary = strings.TrimSpace(newSummary) 63 | 64 | if newSummary == aiSummary { 65 | break 66 | } 67 | 68 | aiSummary = newSummary 69 | } 70 | 71 | return strings.TrimSpace(aiSummary) 72 | } 73 | -------------------------------------------------------------------------------- /pkg/checks/diff/ai_summary_test.go: -------------------------------------------------------------------------------- 1 | package diff 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestCleanUpAiSummary(t *testing.T) { 10 | t.Run("prefix", func(t *testing.T) { 11 | input := "```markdown\nhello\nworld" 12 | expected := "hello\nworld" 13 | 14 | actual := cleanUpAiSummary(input) 15 | assert.Equal(t, expected, actual) 16 | }) 17 | 18 | t.Run("suffix", func(t *testing.T) { 19 | input := "\nhello\nworld```" 20 | expected := "hello\nworld" 21 | 22 | actual := cleanUpAiSummary(input) 23 | assert.Equal(t, expected, actual) 24 | }) 25 | 26 | t.Run("prefix and suffix", func(t *testing.T) { 27 | input := "```markdown\n\nhello\nworld```" 28 | expected := "hello\nworld" 29 | 30 | actual := cleanUpAiSummary(input) 31 | assert.Equal(t, expected, actual) 32 | }) 33 | 34 | t.Run("weird prefix and suffix", func(t *testing.T) { 35 | input := "```plaintext\n#***\n- Added environment variables FF_TIMESTAMPS and FF_SCRIPT_SECTIONS\n#***" 36 | expected := "- Added environment variables FF_TIMESTAMPS and FF_SCRIPT_SECTIONS" 37 | 38 | actual := cleanUpAiSummary(input) 39 | assert.Equal(t, expected, actual) 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /pkg/checks/hooks/grouped.go: -------------------------------------------------------------------------------- 1 | package hooks 2 | 3 | import ( 4 | "sort" 5 | 6 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 7 | ) 8 | 9 | type waveNum int32 10 | 11 | var waveNumBits = 32 12 | 13 | type groupedSyncWaves map[argocdSyncPhase]map[waveNum][]*unstructured.Unstructured 14 | 15 | func (g groupedSyncWaves) addResource(phase argocdSyncPhase, wave waveNum, resource *unstructured.Unstructured) { 16 | syncWaves, ok := g[phase] 17 | if !ok { 18 | syncWaves = make(map[waveNum][]*unstructured.Unstructured) 19 | g[phase] = syncWaves 20 | } 21 | 22 | phaseResources := syncWaves[wave] 23 | phaseResources = append(phaseResources, resource) 24 | syncWaves[wave] = phaseResources 25 | } 26 | 27 | // include all hooks that argocd uses: https://argo-cd.readthedocs.io/en/stable/user-guide/helm/#helm-hooks 28 | var sortedPhases = []argocdSyncPhase{ 29 | PreSyncPhase, 30 | SyncPhase, 31 | PostSyncPhase, 32 | SyncFailPhase, 33 | PostDeletePhase, 34 | } 35 | 36 | // argocd sync phases: https://argo-cd.readthedocs.io/en/stable/user-guide/resource_hooks/#usage 37 | type argocdSyncPhase string 38 | 39 | const ( 40 | PreSyncPhase argocdSyncPhase = "PreSync" 41 | SyncPhase argocdSyncPhase = "Sync" 42 | SkipPhase argocdSyncPhase = "Skip" 43 | PostSyncPhase argocdSyncPhase = "PostSync" 44 | SyncFailPhase argocdSyncPhase = "SyncFail" 45 | PostDeletePhase argocdSyncPhase = "PostDelete" 46 | ) 47 | 48 | type phaseWaveResources struct { 49 | phase argocdSyncPhase 50 | waves []waveResources 51 | } 52 | 53 | type waveResources struct { 54 | wave waveNum 55 | resources []*unstructured.Unstructured 56 | } 57 | 58 | func (g groupedSyncWaves) getSortedPhasesAndWaves() []phaseWaveResources { 59 | var result []phaseWaveResources 60 | usedPhases := make(map[argocdSyncPhase]struct{}) 61 | for _, phase := range sortedPhases { 62 | waves, ok := g[phase] 63 | if !ok { 64 | continue 65 | } 66 | 67 | usedPhases[phase] = struct{}{} 68 | 69 | var wavesNums []waveNum 70 | for wave := range waves { 71 | wavesNums = append(wavesNums, wave) 72 | } 73 | sort.Sort(byNum(wavesNums)) 74 | 75 | pwr := phaseWaveResources{ 76 | phase: phase, 77 | } 78 | 79 | for _, wave := range wavesNums { 80 | wr := waveResources{ 81 | wave: wave, 82 | resources: waves[wave], 83 | } 84 | 85 | pwr.waves = append(pwr.waves, wr) 86 | } 87 | 88 | result = append(result, pwr) 89 | } 90 | 91 | return result 92 | } 93 | 94 | type byNum []waveNum 95 | 96 | func (b byNum) Len() int { 97 | return len(b) 98 | } 99 | 100 | func (b byNum) Less(i, j int) bool { 101 | return b[i] < b[j] 102 | } 103 | 104 | func (b byNum) Swap(i, j int) { 105 | b[i], b[j] = b[j], b[i] 106 | } 107 | -------------------------------------------------------------------------------- /pkg/checks/kubeconform/check.go: -------------------------------------------------------------------------------- 1 | package kubeconform 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/zapier/kubechecks/pkg/checks" 7 | "github.com/zapier/kubechecks/pkg/msg" 8 | ) 9 | 10 | func Check(ctx context.Context, request checks.Request) (msg.Result, error) { 11 | return argoCdAppValidate(ctx, request.Container, request.AppName, request.KubernetesVersion, request.YamlManifests) 12 | } 13 | -------------------------------------------------------------------------------- /pkg/checks/kubeconform/validate_test.go: -------------------------------------------------------------------------------- 1 | package kubeconform 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | "testing" 8 | 9 | fixtures "github.com/go-git/go-git-fixtures/v4" 10 | "github.com/spf13/viper" 11 | 12 | "github.com/stretchr/testify/assert" 13 | 14 | "github.com/zapier/kubechecks/pkg/container" 15 | ) 16 | 17 | func TestDefaultGetSchemaLocations(t *testing.T) { 18 | ctr := container.Container{} 19 | schemaLocations := getSchemaLocations(ctr) 20 | 21 | // default schema location is "./schemas" 22 | assert.Len(t, schemaLocations, 1) 23 | assert.Equal(t, "default", schemaLocations[0]) 24 | } 25 | 26 | func TestGetRemoteSchemaLocations(t *testing.T) { 27 | ctr := container.Container{} 28 | 29 | if os.Getenv("CI") == "" { 30 | t.Skip("Skipping testing. Only for CI environments") 31 | } 32 | 33 | basic := fixtures.Basic() 34 | fixture := basic.One() 35 | fmt.Println(fixture.URL) 36 | 37 | // t.Setenv("KUBECHECKS_SCHEMAS_LOCATION", fixture.URL) // doesn't work because viper needs to initialize from root, which doesn't happen 38 | viper.Set("schemas-location", []string{fixture.URL}) 39 | schemaLocations := getSchemaLocations(ctr) 40 | hasTmpDirPrefix := strings.HasPrefix(schemaLocations[0], "/tmp/schemas") 41 | assert.Equal(t, hasTmpDirPrefix, true, "invalid schemas location. Schema location should have prefix /tmp/schemas but has %s", schemaLocations[0]) 42 | } 43 | -------------------------------------------------------------------------------- /pkg/checks/preupgrade/check.go: -------------------------------------------------------------------------------- 1 | package preupgrade 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/zapier/kubechecks/pkg/checks" 7 | "github.com/zapier/kubechecks/pkg/msg" 8 | ) 9 | 10 | func Check(ctx context.Context, request checks.Request) (msg.Result, error) { 11 | return checkApp(ctx, request.Container, request.AppName, request.KubernetesVersion, request.YamlManifests) 12 | } 13 | -------------------------------------------------------------------------------- /pkg/checks/types.go: -------------------------------------------------------------------------------- 1 | package checks 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" 7 | "github.com/rs/zerolog" 8 | 9 | "github.com/zapier/kubechecks/pkg" 10 | "github.com/zapier/kubechecks/pkg/container" 11 | "github.com/zapier/kubechecks/pkg/git" 12 | "github.com/zapier/kubechecks/pkg/msg" 13 | ) 14 | 15 | type ProcessorEntry struct { 16 | Name string 17 | Processor func(ctx context.Context, request Request) (msg.Result, error) 18 | WorstState pkg.CommitState 19 | } 20 | 21 | type Processor interface { 22 | Name() string 23 | Command() 24 | } 25 | 26 | type Request struct { 27 | Log zerolog.Logger 28 | Note *msg.Message 29 | App v1alpha1.Application 30 | Repo *git.Repo 31 | Container container.Container 32 | 33 | QueueApp func(app v1alpha1.Application) 34 | RemoveApp func(app v1alpha1.Application) 35 | 36 | AppName string 37 | KubernetesVersion string 38 | JsonManifests []string 39 | YamlManifests []string 40 | } 41 | -------------------------------------------------------------------------------- /pkg/commitState.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // CommitState is an enum for represnting the state of a commit for posting via CommitStatus 9 | type CommitState uint8 10 | 11 | // must be in order of best to worst, in order for WorstState to work 12 | const ( 13 | StateNone CommitState = iota 14 | StateSkip 15 | StateSuccess 16 | StateRunning 17 | StateWarning 18 | StateFailure 19 | StateError 20 | StatePanic 21 | ) 22 | 23 | func (s CommitState) BareString() string { 24 | text, ok := stateString[s] 25 | if !ok { 26 | text = defaultString 27 | } 28 | return text 29 | } 30 | 31 | var stateString = map[CommitState]string{ 32 | StateNone: "", 33 | StateSkip: "Skipped", 34 | StateSuccess: "Passed", 35 | StateRunning: "Running", 36 | StateWarning: "Warning", 37 | StateFailure: "Failed", 38 | StateError: "Error", 39 | StatePanic: "Panic", 40 | } 41 | 42 | const defaultString = "Unknown" 43 | 44 | func WorstState(l1, l2 CommitState) CommitState { 45 | return max(l1, l2) 46 | } 47 | 48 | func BestState(l1, l2 CommitState) CommitState { 49 | return min(l1, l2) 50 | } 51 | 52 | func ParseCommitState(s string) (CommitState, error) { 53 | switch strings.ToLower(s) { 54 | case "success": 55 | return StateSuccess, nil 56 | case "running": 57 | return StateRunning, nil 58 | case "warning": 59 | return StateWarning, nil 60 | case "failure": 61 | return StateFailure, nil 62 | case "error": 63 | return StateError, nil 64 | case "panic": 65 | return StatePanic, nil 66 | case "skip", "skipped": 67 | return StateSkip, nil 68 | default: 69 | return StateNone, fmt.Errorf("unknown commit state: %s", s) 70 | 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /pkg/config/config_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/rs/zerolog" 8 | "github.com/spf13/viper" 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/require" 11 | 12 | "github.com/zapier/kubechecks/pkg" 13 | ) 14 | 15 | func TestNew(t *testing.T) { 16 | v := viper.New() 17 | v.Set("log-level", "info") 18 | v.Set("argocd-api-insecure", "true") 19 | v.Set("argocd-api-plaintext", "true") 20 | v.Set("worst-conftest-state", "warning") 21 | v.Set("repo-refresh-interval", "10m") 22 | v.Set("additional-apps-namespaces", "default,kube-system") 23 | 24 | cfg, err := NewWithViper(v) 25 | require.NoError(t, err) 26 | assert.Equal(t, zerolog.InfoLevel, cfg.LogLevel) 27 | assert.Equal(t, true, cfg.ArgoCDInsecure) 28 | assert.Equal(t, true, cfg.ArgoCDPlainText) 29 | assert.Equal(t, pkg.StateWarning, cfg.WorstConfTestState, "worst states can be overridden") 30 | assert.Equal(t, time.Minute*10, cfg.RepoRefreshInterval) 31 | assert.Equal(t, []string{"default", "kube-system"}, cfg.AdditionalAppsNamespaces) 32 | } 33 | -------------------------------------------------------------------------------- /pkg/events/metrics.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | var inFlight int32 4 | 5 | func GetInFlight() int { 6 | return int(inFlight) 7 | } 8 | -------------------------------------------------------------------------------- /pkg/events/runner.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "sync" 7 | 8 | "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" 9 | "github.com/rs/zerolog" 10 | 11 | "github.com/zapier/kubechecks/pkg" 12 | "github.com/zapier/kubechecks/pkg/checks" 13 | "github.com/zapier/kubechecks/pkg/container" 14 | "github.com/zapier/kubechecks/pkg/msg" 15 | "github.com/zapier/kubechecks/telemetry" 16 | ) 17 | 18 | type Runner struct { 19 | checks.Request 20 | 21 | wg sync.WaitGroup 22 | } 23 | 24 | func newRunner( 25 | ctr container.Container, 26 | app v1alpha1.Application, 27 | appName, k8sVersion string, 28 | jsonManifests, yamlManifests []string, 29 | logger zerolog.Logger, 30 | note *msg.Message, 31 | queueApp, removeApp func(application v1alpha1.Application), 32 | ) *Runner { 33 | return &Runner{ 34 | Request: checks.Request{ 35 | App: app, 36 | AppName: appName, 37 | Container: ctr, 38 | JsonManifests: jsonManifests, 39 | KubernetesVersion: k8sVersion, 40 | Log: logger, 41 | Note: note, 42 | QueueApp: queueApp, 43 | RemoveApp: removeApp, 44 | YamlManifests: yamlManifests, 45 | }, 46 | } 47 | } 48 | 49 | type checkFunction func(ctx context.Context, data checks.Request) (msg.Result, error) 50 | 51 | func (r *Runner) Run(ctx context.Context, desc string, fn checkFunction, worstState pkg.CommitState) { 52 | r.wg.Add(1) 53 | 54 | go func() { 55 | logger := r.Log.With().Str("check", desc).Logger() 56 | 57 | ctx, span := tracer.Start(ctx, desc) 58 | 59 | addToAppMessage := func(result msg.Result) { 60 | result.State = pkg.BestState(result.State, worstState) 61 | r.Note.AddToAppMessage(ctx, r.AppName, result) 62 | } 63 | 64 | defer func() { 65 | r.wg.Done() 66 | 67 | if err := recover(); err != nil { 68 | logger.Error().Str("check", desc).Msgf("panic while running check") 69 | 70 | telemetry.SetError(span, fmt.Errorf("%v", err), desc) 71 | result := msg.Result{ 72 | State: pkg.StatePanic, 73 | Summary: desc, 74 | Details: fmt.Sprintf(errorCommentFormat, desc, err), 75 | } 76 | addToAppMessage(result) 77 | } 78 | }() 79 | 80 | logger.Info().Msgf("running check") 81 | result, err := fn(ctx, r.Request) 82 | logger.Info(). 83 | Err(err). 84 | Str("result", result.State.BareString()). 85 | Msg("check result") 86 | 87 | if err != nil { 88 | telemetry.SetError(span, err, desc) 89 | result = msg.Result{State: pkg.StateError, Summary: desc, Details: fmt.Sprintf(errorCommentFormat, desc, err)} 90 | addToAppMessage(result) 91 | return 92 | } 93 | 94 | addToAppMessage(result) 95 | }() 96 | } 97 | 98 | func (r *Runner) Wait() { 99 | r.wg.Wait() 100 | } 101 | -------------------------------------------------------------------------------- /pkg/events/worker_test.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestConvertJsonToYamlManifests(t *testing.T) { 10 | testcases := map[string]struct { 11 | input, expected []string 12 | }{ 13 | "empty": { 14 | input: []string{}, 15 | expected: nil, 16 | }, 17 | "easy json": { 18 | input: []string{ 19 | `{"hello": "world"}`, 20 | }, 21 | expected: []string{ 22 | `--- 23 | hello: world 24 | `, 25 | }, 26 | }, 27 | } 28 | for name, tc := range testcases { 29 | t.Run(name, func(t *testing.T) { 30 | actual := convertJsonToYamlManifests(tc.input) 31 | assert.Equal(t, tc.expected, actual) 32 | }) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /pkg/generator/README.md: -------------------------------------------------------------------------------- 1 | # Argo CD Generators 2 | This directory contains the code for Argo CD generators. Generators dynamically create Kubernetes resources from various sources of truth, such as Kustomize, Helm, Ksonnet, and others. They are a key component in Argo CD for automating resource creation and management. 3 | 4 | ## Overview 5 | Generators in Argo CD enable the dynamic generation of Kubernetes manifests based on the desired state defined in different configurations. By leveraging these generators, Argo CD can efficiently manage and deploy resources across different environments. 6 | 7 | ## Why Forked? 8 | This code is a fork of the Argo CD (v2.12) generator code. The fork was necessary due to an incompatibility between Kubechecks' use of the go-gitlab library and Argo CD's generator code. To resolve this, the generator code has been forked and adapted for compatibility with Kubechecks. 9 | 10 | ## Supported Generators 11 | * Lists 12 | * Clusters 13 | 14 | ## Unsupported Generators 15 | * Git 16 | * Pull Requests 17 | 18 | ## Usage 19 | You can use these generators to automate the creation and management of Kubernetes resources in your environment, ensuring consistency and repeatability. 20 | -------------------------------------------------------------------------------- /pkg/generator/interface.go: -------------------------------------------------------------------------------- 1 | package generator 2 | 3 | import ( 4 | "time" 5 | 6 | argoprojiov1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" 7 | "github.com/pkg/errors" 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]interface{}, error) 16 | 17 | // GetRequeueAfter is 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 = errors.New("ApplicationSet is empty") 27 | var NoRequeueAfter time.Duration 28 | -------------------------------------------------------------------------------- /pkg/generator/list.go: -------------------------------------------------------------------------------- 1 | package generator 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "time" 7 | 8 | "sigs.k8s.io/yaml" 9 | 10 | argoprojiov1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" 11 | ) 12 | 13 | var _ Generator = (*ListGenerator)(nil) 14 | 15 | type ListGenerator struct { 16 | } 17 | 18 | func NewListGenerator() Generator { 19 | g := &ListGenerator{} 20 | return g 21 | } 22 | 23 | func (g *ListGenerator) GetRequeueAfter(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator) time.Duration { 24 | return NoRequeueAfter 25 | } 26 | 27 | func (g *ListGenerator) GetTemplate(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator) *argoprojiov1alpha1.ApplicationSetTemplate { 28 | return &appSetGenerator.List.Template 29 | } 30 | 31 | func (g *ListGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator, appSet *argoprojiov1alpha1.ApplicationSet) ([]map[string]interface{}, error) { 32 | if appSetGenerator == nil { 33 | return nil, EmptyAppSetGeneratorError 34 | } 35 | 36 | if appSetGenerator.List == nil { 37 | return nil, EmptyAppSetGeneratorError 38 | } 39 | 40 | res := make([]map[string]interface{}, len(appSetGenerator.List.Elements)) 41 | 42 | for i, tmpItem := range appSetGenerator.List.Elements { 43 | params := map[string]interface{}{} 44 | var element map[string]interface{} 45 | err := json.Unmarshal(tmpItem.Raw, &element) 46 | if err != nil { 47 | return nil, fmt.Errorf("error unmarshling list element %v", err) 48 | } 49 | 50 | if appSet.Spec.GoTemplate { 51 | res[i] = element 52 | } else { 53 | for key, value := range element { 54 | if key == "values" { 55 | values, ok := (value).(map[string]interface{}) 56 | if !ok { 57 | return nil, fmt.Errorf("error parsing values map") 58 | } 59 | for k, v := range values { 60 | value, ok := v.(string) 61 | if !ok { 62 | return nil, fmt.Errorf("error parsing value as string %v", err) 63 | } 64 | params[fmt.Sprintf("values.%s", k)] = value 65 | } 66 | } else { 67 | v, ok := value.(string) 68 | if !ok { 69 | return nil, fmt.Errorf("error parsing value as string %v", err) 70 | } 71 | params[key] = v 72 | } 73 | res[i] = params 74 | } 75 | } 76 | } 77 | 78 | // Append elements from ElementsYaml to the response 79 | if len(appSetGenerator.List.ElementsYaml) > 0 { 80 | 81 | var yamlElements []map[string]interface{} 82 | err := yaml.Unmarshal([]byte(appSetGenerator.List.ElementsYaml), &yamlElements) 83 | if err != nil { 84 | return nil, fmt.Errorf("error unmarshling decoded ElementsYaml %v", err) 85 | } 86 | res = append(res, yamlElements...) 87 | } 88 | 89 | return res, nil 90 | } 91 | -------------------------------------------------------------------------------- /pkg/generator/list_test.go: -------------------------------------------------------------------------------- 1 | package generator 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 8 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 | 10 | argoprojiov1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" 11 | ) 12 | 13 | func TestGenerateListParams(t *testing.T) { 14 | testCases := []struct { 15 | elements []apiextensionsv1.JSON 16 | expected []map[string]interface{} 17 | }{ 18 | { 19 | elements: []apiextensionsv1.JSON{{Raw: []byte(`{"cluster": "cluster","url": "url"}`)}}, 20 | expected: []map[string]interface{}{{"cluster": "cluster", "url": "url"}}, 21 | }, { 22 | elements: []apiextensionsv1.JSON{{Raw: []byte(`{"cluster": "cluster","url": "url","values":{"foo":"bar"}}`)}}, 23 | expected: []map[string]interface{}{{"cluster": "cluster", "url": "url", "values.foo": "bar"}}, 24 | }, 25 | } 26 | 27 | for _, testCase := range testCases { 28 | 29 | var listGenerator = NewListGenerator() 30 | 31 | applicationSetInfo := argoprojiov1alpha1.ApplicationSet{ 32 | ObjectMeta: metav1.ObjectMeta{ 33 | Name: "set", 34 | }, 35 | Spec: argoprojiov1alpha1.ApplicationSetSpec{}, 36 | } 37 | 38 | got, err := listGenerator.GenerateParams(&argoprojiov1alpha1.ApplicationSetGenerator{ 39 | List: &argoprojiov1alpha1.ListGenerator{ 40 | Elements: testCase.elements, 41 | }}, &applicationSetInfo) 42 | 43 | assert.NoError(t, err) 44 | assert.ElementsMatch(t, testCase.expected, got) 45 | 46 | } 47 | } 48 | 49 | func TestGenerateListParamsGoTemplate(t *testing.T) { 50 | testCases := []struct { 51 | elements []apiextensionsv1.JSON 52 | expected []map[string]interface{} 53 | }{ 54 | { 55 | elements: []apiextensionsv1.JSON{{Raw: []byte(`{"cluster": "cluster","url": "url"}`)}}, 56 | expected: []map[string]interface{}{{"cluster": "cluster", "url": "url"}}, 57 | }, { 58 | elements: []apiextensionsv1.JSON{{Raw: []byte(`{"cluster": "cluster","url": "url","values":{"foo":"bar"}}`)}}, 59 | expected: []map[string]interface{}{{"cluster": "cluster", "url": "url", "values": map[string]interface{}{"foo": "bar"}}}, 60 | }, 61 | } 62 | 63 | for _, testCase := range testCases { 64 | 65 | var listGenerator = NewListGenerator() 66 | 67 | applicationSetInfo := argoprojiov1alpha1.ApplicationSet{ 68 | ObjectMeta: metav1.ObjectMeta{ 69 | Name: "set", 70 | }, 71 | Spec: argoprojiov1alpha1.ApplicationSetSpec{ 72 | GoTemplate: true, 73 | }, 74 | } 75 | 76 | got, err := listGenerator.GenerateParams(&argoprojiov1alpha1.ApplicationSetGenerator{ 77 | List: &argoprojiov1alpha1.ListGenerator{ 78 | Elements: testCase.elements, 79 | }}, &applicationSetInfo) 80 | 81 | assert.NoError(t, err) 82 | assert.ElementsMatch(t, testCase.expected, got) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /pkg/generator/value_interpolation.go: -------------------------------------------------------------------------------- 1 | package generator 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | func appendTemplatedValues(values map[string]string, params map[string]interface{}, useGoTemplate bool, goTemplateOptions []string) error { 8 | // We create a local map to ensure that we do not fall victim to a billion-laughs attack. We iterate through the 9 | // cluster values map and only replace values in said map if it has already been allowlisted in the params map. 10 | // Once we iterate through all the cluster values we can then safely merge the `tmp` map into the main params map. 11 | tmp := map[string]interface{}{} 12 | 13 | for key, value := range values { 14 | result, err := replaceTemplatedString(value, params, useGoTemplate, goTemplateOptions) 15 | 16 | if err != nil { 17 | return fmt.Errorf("failed to replace templated string: %w", err) 18 | } 19 | 20 | if useGoTemplate { 21 | if tmp["values"] == nil { 22 | tmp["values"] = map[string]string{} 23 | } 24 | tmp["values"].(map[string]string)[key] = result 25 | } else { 26 | tmp[fmt.Sprintf("values.%s", key)] = result 27 | } 28 | } 29 | 30 | for key, value := range tmp { 31 | params[key] = value 32 | } 33 | 34 | return nil 35 | } 36 | 37 | func replaceTemplatedString(value string, params map[string]interface{}, useGoTemplate bool, goTemplateOptions []string) (string, error) { 38 | replacedTmplStr, err := render.Replace(value, params, useGoTemplate, goTemplateOptions) 39 | if err != nil { 40 | return "", fmt.Errorf("failed to replace templated string with rendered values: %w", err) 41 | } 42 | return replacedTmplStr, nil 43 | } 44 | -------------------------------------------------------------------------------- /pkg/git/manager.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | 7 | "github.com/pkg/errors" 8 | "go.opentelemetry.io/otel" 9 | 10 | "github.com/zapier/kubechecks/pkg/config" 11 | ) 12 | 13 | var tracer = otel.Tracer("pkg/git") 14 | 15 | type RepoManager struct { 16 | lock sync.Mutex 17 | repos []*Repo 18 | cfg config.ServerConfig 19 | } 20 | 21 | func NewRepoManager(cfg config.ServerConfig) *RepoManager { 22 | return &RepoManager{cfg: cfg} 23 | } 24 | 25 | func (rm *RepoManager) Clone(ctx context.Context, cloneUrl, branchName string, shallow bool) (*Repo, error) { 26 | repo := New(rm.cfg, cloneUrl, branchName) 27 | if shallow { 28 | repo.Shallow = true 29 | } 30 | 31 | if err := repo.Clone(ctx); err != nil { 32 | return nil, errors.Wrap(err, "failed to clone repository") 33 | } 34 | 35 | rm.lock.Lock() 36 | defer rm.lock.Unlock() // just for safety's sake 37 | rm.repos = append(rm.repos, repo) 38 | 39 | return repo, nil 40 | } 41 | 42 | func (rm *RepoManager) Cleanup() { 43 | rm.lock.Lock() 44 | defer rm.lock.Unlock() 45 | 46 | for _, repo := range rm.repos { 47 | repo.Wipe() 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /pkg/git/repo_test.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "os/exec" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/require" 12 | 13 | "github.com/zapier/kubechecks/pkg/config" 14 | ) 15 | 16 | func wipe(t *testing.T, path string) { 17 | err := os.RemoveAll(path) 18 | require.NoError(t, err) 19 | } 20 | 21 | func TestRepoRoundTrip(t *testing.T) { 22 | originRepo, err := os.MkdirTemp("", "kubechecks-test-") 23 | require.NoError(t, err) 24 | defer wipe(t, originRepo) 25 | 26 | // initialize the test repo 27 | cmd := exec.Command("/bin/sh", "-c", `#!/usr/bin/env bash 28 | set -e 29 | set -x 30 | 31 | # set up git repo 32 | cd $TEMPDIR 33 | git init 34 | git config user.email "user@test.com" 35 | git config user.name "Zap Zap" 36 | 37 | # set up main branch 38 | git branch -m main 39 | 40 | echo "one" > abc.txt 41 | git add abc.txt 42 | git commit -m "commit one on main" 43 | 44 | # set up testing branch 45 | git checkout -b testing 46 | echo "three" > abc.txt 47 | git add abc.txt 48 | git commit -m "commit two on testing" 49 | 50 | # add commit back to main 51 | git checkout main 52 | echo "four" > def.txt 53 | git add def.txt 54 | git commit -m "commit two on main" 55 | 56 | # pull main into testing 57 | git checkout testing 58 | git merge main 59 | echo "two" > ghi.txt 60 | git add ghi.txt 61 | git commit -m "commit three" 62 | `) 63 | cmd.Env = append(cmd.Env, "TEMPDIR="+originRepo) 64 | cmd.Stderr = os.Stderr 65 | cmd.Stdout = os.Stdout 66 | err = cmd.Run() 67 | require.NoError(t, err) 68 | 69 | cmd = exec.Command("git", "rev-parse", "HEAD") 70 | cmd.Dir = originRepo 71 | output, err := cmd.Output() 72 | require.NoError(t, err) 73 | sha := strings.TrimSpace(string(output)) 74 | 75 | var cfg config.ServerConfig 76 | ctx := context.Background() 77 | repo := New(cfg, originRepo, "main") 78 | 79 | err = repo.Clone(ctx) 80 | require.NoError(t, err) 81 | defer wipe(t, repo.Directory) 82 | 83 | err = repo.MergeIntoTarget(ctx, sha) 84 | require.NoError(t, err) 85 | 86 | files, err := repo.GetListOfChangedFiles(ctx) 87 | require.NoError(t, err) 88 | assert.Equal(t, []string{"abc.txt", "ghi.txt"}, files) 89 | } 90 | 91 | func TestRepoGetRemoteHead(t *testing.T) { 92 | cfg := config.ServerConfig{} 93 | ctx := context.TODO() 94 | 95 | repo := New(cfg, "https://github.com/zapier/kubechecks.git", "") 96 | repo.Shallow = true 97 | repo.BranchName = "gh-pages" 98 | err := repo.Clone(ctx) 99 | require.NoError(t, err) 100 | 101 | t.Cleanup(repo.Wipe) 102 | 103 | branch, err := repo.GetRemoteHead() 104 | require.NoError(t, err) 105 | assert.Equal(t, "main", branch) 106 | currentBranch, err := repo.GetCurrentBranch() 107 | require.NoError(t, err) 108 | assert.Equal(t, "gh-pages", currentBranch) 109 | } 110 | -------------------------------------------------------------------------------- /pkg/kubernetes/docs.go: -------------------------------------------------------------------------------- 1 | // Package client provides utilities for creating and managing Kubernetes clients. 2 | // 3 | // This package includes functions and types for configuring and creating 4 | // Kubernetes clients tailored to specific requirements, such as accessing 5 | // AWS EKS clusters with appropriate authentication. 6 | // 7 | // Example usage: 8 | // 9 | // # specify the opts with Kubernetes Cluster type, such as EKSClientOption, otherwise the default local client will be created. 10 | // 11 | // * for EKS setup: 12 | // 13 | // client, err := client.New(&NewClientInput{ClusterType: ClusterTypeEKS}, EKSClientOption(ctx, "test-cluster-01", "us-east-1")) 14 | // if err != nil { 15 | // fmt.Printf("failed to create client: %v", err) 16 | // return 17 | // } 18 | // 19 | // * for localhost setup: 20 | // 21 | // client, err := client.New(&NewClientInput{ClusterType: ClusterTypeLOCAL, KuberKubernetesConfigPath: *configFilePath}) 22 | package client 23 | -------------------------------------------------------------------------------- /pkg/kubernetes/interface.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "k8s.io/client-go/kubernetes" 5 | "k8s.io/client-go/rest" 6 | controllerClient "sigs.k8s.io/controller-runtime/pkg/client" 7 | ) 8 | 9 | type Interface interface { 10 | // ClientSet returns the rest clientset to be used. 11 | ClientSet() kubernetes.Interface 12 | // ControllerClient returns the controller-runtime client to be used. 13 | ControllerClient() *controllerClient.Client 14 | // Config returns the rest.Config to be used. 15 | Config() *rest.Config 16 | } 17 | -------------------------------------------------------------------------------- /pkg/repoUrl.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | "strings" 7 | 8 | "github.com/chainguard-dev/git-urls" 9 | "github.com/pkg/errors" 10 | "github.com/rs/zerolog/log" 11 | ) 12 | 13 | type RepoURL struct { 14 | Host, Path string 15 | } 16 | 17 | func (r RepoURL) CloneURL(username string) string { 18 | if username != "" { 19 | return fmt.Sprintf("https://%s@%s/%s", username, r.Host, r.Path) 20 | } 21 | return fmt.Sprintf("https://%s/%s", r.Host, r.Path) 22 | } 23 | 24 | func NormalizeRepoUrl(s string) (RepoURL, url.Values, error) { 25 | var parser func(string) (*url.URL, error) 26 | 27 | if strings.HasPrefix(s, "http") { 28 | parser = url.Parse 29 | } else { 30 | parser = giturls.Parse 31 | } 32 | 33 | r, err := parser(s) 34 | if err != nil { 35 | return RepoURL{}, nil, err 36 | } 37 | 38 | r.Path = strings.TrimPrefix(r.Path, "/") 39 | r.Path = strings.TrimSuffix(r.Path, ".git") 40 | 41 | return RepoURL{ 42 | Host: r.Host, 43 | Path: r.Path, 44 | }, r.Query(), nil 45 | } 46 | 47 | func Canonicalize(cloneURL string) (RepoURL, error) { 48 | parsed, _, err := NormalizeRepoUrl(cloneURL) 49 | if err != nil { 50 | return RepoURL{}, errors.Wrap(err, "failed to parse clone url") 51 | } 52 | 53 | return parsed, nil 54 | } 55 | 56 | func AreSameRepos(url1, url2 string) bool { 57 | repo1, err := Canonicalize(url1) 58 | if err != nil { 59 | log.Warn().Msgf("failed to canonicalize %q", url1) 60 | return false 61 | } 62 | 63 | repo2, err := Canonicalize(url2) 64 | if err != nil { 65 | log.Warn().Msgf("failed to canonicalize %q", url2) 66 | return false 67 | } 68 | 69 | return repo1 == repo2 70 | } 71 | -------------------------------------------------------------------------------- /pkg/repoUrl_test.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestNormalizeStrings(t *testing.T) { 14 | type expected struct { 15 | RepoURL RepoURL 16 | Query url.Values 17 | } 18 | testCases := []struct { 19 | input, name string 20 | expected expected 21 | }{ 22 | { 23 | name: "simple github over ssh", 24 | input: "git@github.com:one/two", 25 | expected: expected{ 26 | RepoURL: RepoURL{Host: "github.com", Path: "one/two"}, 27 | Query: make(url.Values), 28 | }, 29 | }, 30 | { 31 | name: "simple github over https", 32 | input: "https://github.com/one/two", 33 | expected: expected{ 34 | RepoURL: RepoURL{Host: "github.com", Path: "one/two"}, 35 | Query: make(url.Values), 36 | }, 37 | }, 38 | { 39 | name: "simple gitlab over ssh", 40 | input: "git@gitlab.com:djeebus/helm-test.git", 41 | expected: expected{ 42 | RepoURL: RepoURL{Host: "gitlab.com", Path: "djeebus/helm-test"}, 43 | Query: make(url.Values), 44 | }, 45 | }, 46 | { 47 | name: "simple gitlab over https", 48 | input: "https://gitlab.com/djeebus/helm-test.git", 49 | expected: expected{ 50 | RepoURL: RepoURL{Host: "gitlab.com", Path: "djeebus/helm-test"}, 51 | Query: make(url.Values), 52 | }, 53 | }, 54 | { 55 | name: "simple gitlab over https with query", 56 | input: "https://gitlab.com/djeebus/helm-test.git?subdir=/blah", 57 | expected: expected{ 58 | RepoURL: RepoURL{Host: "gitlab.com", Path: "djeebus/helm-test"}, 59 | Query: url.Values{"subdir": []string{"/blah"}}, 60 | }, 61 | }, 62 | } 63 | 64 | for _, tc := range testCases { 65 | t.Run(fmt.Sprintf("case %s", tc.input), func(t *testing.T) { 66 | repoURL, query, err := NormalizeRepoUrl(tc.input) 67 | require.NoError(t, err) 68 | assert.Equal(t, tc.expected.RepoURL, repoURL) 69 | assert.Equal(t, tc.expected.Query, query) 70 | }) 71 | } 72 | } 73 | 74 | func TestAreSameRepos(t *testing.T) { 75 | testcases := map[string]struct { 76 | input1, input2 string 77 | expected bool 78 | }{ 79 | "empty": {"", "", true}, 80 | "empty1": {"", "blah", false}, 81 | "empty2": {"blah", "", false}, 82 | "git-to-git": {"git@github.com:zapier/kubechecks.git", "git@github.com:zapier/kubechecks.git", true}, 83 | "no-git-suffix-to-git": {"git@github.com:zapier/kubechecks", "git@github.com:zapier/kubechecks.git", true}, 84 | "https-to-git": {"https://github.com/zapier/kubechecks", "git@github.com:zapier/kubechecks.git", true}, 85 | } 86 | for name, tc := range testcases { 87 | t.Run(name, func(t *testing.T) { 88 | actual := AreSameRepos(tc.input1, tc.input2) 89 | assert.Equal(t, tc.expected, actual) 90 | }) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /pkg/repo_config/config.go: -------------------------------------------------------------------------------- 1 | package repo_config 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/creasty/defaults" 7 | ) 8 | 9 | type Config struct { 10 | Applications []*ArgoCdApplicationConfig `yaml:"applications"` 11 | ApplicationSets []*ArgocdApplicationSetConfig `yaml:"applicationSets"` 12 | } 13 | 14 | type ArgoCdApplicationConfig struct { 15 | Name string `yaml:"name" validate:"empty=false"` 16 | Cluster string `yaml:"cluster" validate:"empty=false"` 17 | Path string `yaml:"path" validate:"empty=false"` 18 | AdditionalPaths []string `yaml:"additionalPaths"` 19 | EnableConfTest bool `yaml:"enableConfTest" default:"true"` 20 | EnableKubeConform bool `yaml:"enableKubeConform" default:"true"` 21 | EnableKubePug bool `yaml:"enableKubePug" default:"true"` 22 | } 23 | 24 | func (s *ArgoCdApplicationConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { 25 | err := defaults.Set(s) 26 | if err != nil { 27 | return fmt.Errorf("failed to set defaults for project config: %v", err) 28 | } 29 | 30 | type plain ArgoCdApplicationConfig 31 | if err := unmarshal((*plain)(s)); err != nil { 32 | return err 33 | } 34 | 35 | return nil 36 | } 37 | 38 | type ArgocdApplicationSetConfig struct { 39 | Name string `yaml:"name" validate:"empty=false"` 40 | Paths []string `yaml:"paths" validate:"empty=false"` 41 | EnableConfTest bool `yaml:"enableConfTest" default:"true"` 42 | EnableKubeConform bool `yaml:"enableKubeConform" default:"true"` 43 | EnableKubePug bool `yaml:"enableKubePug" default:"true"` 44 | } 45 | 46 | func (s *ArgocdApplicationSetConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { 47 | err := defaults.Set(s) 48 | if err != nil { 49 | return fmt.Errorf("failed to set defaults for project config: %v", err) 50 | } 51 | 52 | type plain ArgocdApplicationSetConfig 53 | if err := unmarshal((*plain)(s)); err != nil { 54 | return err 55 | } 56 | 57 | return nil 58 | } 59 | -------------------------------------------------------------------------------- /pkg/repo_config/config_test.go: -------------------------------------------------------------------------------- 1 | package repo_config 2 | 3 | import ( 4 | "github.com/creasty/defaults" 5 | "github.com/rs/zerolog/log" 6 | ) 7 | 8 | // Test helpers 9 | 10 | func defaultArgoCdApplicationConfig() *ArgoCdApplicationConfig { 11 | app := &ArgoCdApplicationConfig{} 12 | err := defaults.Set(app) 13 | if err != nil { 14 | log.Warn().Err(err).Msg("could not set App defaults") 15 | } 16 | 17 | return app 18 | } 19 | 20 | func (a *ArgoCdApplicationConfig) withName(name string) *ArgoCdApplicationConfig { 21 | a.Name = name 22 | return a 23 | } 24 | 25 | func (a *ArgoCdApplicationConfig) withCluster(cluster string) *ArgoCdApplicationConfig { 26 | a.Cluster = cluster 27 | return a 28 | } 29 | 30 | func (a *ArgoCdApplicationConfig) withPath(path string) *ArgoCdApplicationConfig { 31 | a.Path = path 32 | return a 33 | } 34 | 35 | func (a *ArgoCdApplicationConfig) withAdditionalPaths(paths ...string) *ArgoCdApplicationConfig { 36 | a.AdditionalPaths = paths 37 | return a 38 | } 39 | 40 | func defaultArgoCdApplicationSetConfig() *ArgocdApplicationSetConfig { 41 | appset := &ArgocdApplicationSetConfig{} 42 | err := defaults.Set(appset) 43 | if err != nil { 44 | log.Warn().Err(err).Msg("could not set App defaults") 45 | } 46 | 47 | return appset 48 | } 49 | 50 | func (a *ArgocdApplicationSetConfig) withName(name string) *ArgocdApplicationSetConfig { 51 | a.Name = name 52 | return a 53 | } 54 | 55 | func (a *ArgocdApplicationSetConfig) withPaths(paths ...string) *ArgocdApplicationSetConfig { 56 | a.Paths = paths 57 | return a 58 | } 59 | -------------------------------------------------------------------------------- /pkg/repo_config/loader.go: -------------------------------------------------------------------------------- 1 | package repo_config 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/pkg/errors" 9 | "github.com/rs/zerolog/log" 10 | "gopkg.in/dealancer/validate.v2" 11 | "gopkg.in/yaml.v3" 12 | ) 13 | 14 | const RepoConfigFilenamePrefix = `.kubechecks` 15 | 16 | var RepoConfigFileExtensions = []string{".yaml", ".yml"} 17 | 18 | var ErrConfigFileNotFound = errors.New("project config file not found") 19 | 20 | // LoadRepoConfig attempts to load a config file from the given directory 21 | // it searches the dir for all the config file name variations. 22 | func LoadRepoConfig(repoDir string) (*Config, error) { 23 | file, err := searchConfigFile(repoDir) 24 | if err != nil { 25 | if errors.Is(err, ErrConfigFileNotFound) { 26 | return nil, nil 27 | } 28 | 29 | return nil, errors.Wrap(err, "failed to find config file") 30 | } 31 | 32 | cfg, err := loadRepoConfigFile(file) 33 | if err != nil { 34 | return nil, errors.Wrap(err, "failed to load repo config file") 35 | } 36 | 37 | return cfg, nil 38 | } 39 | 40 | func RepoConfigFilenameVariations() []string { 41 | var filenames []string 42 | for _, ext := range RepoConfigFileExtensions { 43 | filenames = append(filenames, RepoConfigFilenamePrefix+ext) 44 | } 45 | return filenames 46 | } 47 | 48 | func searchConfigFile(repoDir string) (string, error) { 49 | for _, ext := range RepoConfigFileExtensions { 50 | fn := filepath.Join(repoDir, RepoConfigFilenamePrefix+ext) 51 | fi, err := os.Stat(fn) 52 | if err != nil && !os.IsNotExist(err) { 53 | log.Warn().Err(err).Str("filename", fn).Msg("error while attempting to read project config file") 54 | continue 55 | } 56 | if fi != nil && !fi.IsDir() { 57 | return fn, nil 58 | } 59 | } 60 | 61 | return "", ErrConfigFileNotFound 62 | } 63 | 64 | func loadRepoConfigFile(file string) (*Config, error) { 65 | b, err := os.ReadFile(file) 66 | if err != nil { 67 | log.Error().Err(err).Str("filename", file).Msg("could not read project config file") 68 | } 69 | return LoadRepoConfigBytes(b) 70 | } 71 | 72 | func LoadRepoConfigBytes(b []byte) (*Config, error) { 73 | cfg := &Config{} 74 | err := yaml.Unmarshal(b, cfg) 75 | if err != nil { 76 | return nil, fmt.Errorf("could not parse Project config file (.kubechecks.yaml): %v", err) 77 | } 78 | 79 | if err := validate.Validate(cfg); err != nil { 80 | return nil, err 81 | } 82 | 83 | return cfg, nil 84 | } 85 | -------------------------------------------------------------------------------- /pkg/repo_config/testdata/1/.kubechecks.yaml: -------------------------------------------------------------------------------- 1 | applications: 2 | - name: prod-k8s-01-httpbin 3 | cluster: prod-k8s-01 4 | path: k8s/prod-k8s-01/ 5 | 6 | applicationSets: 7 | - name: httpdump 8 | paths: 9 | - apps/httpdump/base -------------------------------------------------------------------------------- /pkg/repo_config/testdata/2/.kubechecks.yml: -------------------------------------------------------------------------------- 1 | applications: 2 | - name: prod-k8s-01-httpbin 3 | cluster: prod-k8s-01 4 | path: k8s/prod-k8s-01/ 5 | additionalPaths: 6 | - k8s/env/prod/ 7 | 8 | applicationSets: 9 | - name: httpdump 10 | paths: 11 | - apps/httpdump/base 12 | - apps/httpdump/overlays/in-cluster -------------------------------------------------------------------------------- /pkg/repo_config/testdata/3/.kubechecks.yaml: -------------------------------------------------------------------------------- 1 | applications: 2 | - name: prod-k8s-01-httpbin 3 | cluster: prod-k8s-01 4 | path: k8s/prod-k8s-01/ 5 | additionalPaths: 6 | - k8s/env/prod/ 7 | 8 | - name: prod-k8s-02-httpbin 9 | cluster: prod-k8s-02 10 | path: k8s/prod-k8s-02/ 11 | additionalPaths: 12 | - k8s/env/prod/ 13 | 14 | applicationSets: 15 | - name: httpdump 16 | paths: 17 | - apps/httpdump/base 18 | - apps/httpdump/overlays/in-cluster 19 | - name: echo-server 20 | paths: 21 | - apps/echo-server 22 | - apps/echo-server/in-cluster -------------------------------------------------------------------------------- /pkg/server/server_test.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/zapier/kubechecks/pkg/checks" 7 | "github.com/zapier/kubechecks/pkg/config" 8 | "github.com/zapier/kubechecks/pkg/container" 9 | ) 10 | 11 | func TestHooksPrefix(t *testing.T) { 12 | tests := []struct { 13 | name string 14 | want string 15 | cfg config.ServerConfig 16 | }{ 17 | { 18 | name: "no-prefix", 19 | want: "/hooks", 20 | cfg: config.ServerConfig{ 21 | UrlPrefix: "", 22 | }, 23 | }, 24 | { 25 | name: "prefix-no-slash", 26 | want: "/test/hooks", 27 | cfg: config.ServerConfig{ 28 | UrlPrefix: "test", 29 | }, 30 | }, 31 | { 32 | name: "prefix-trailing-slash", 33 | want: "/test/hooks", 34 | cfg: config.ServerConfig{ 35 | UrlPrefix: "test/", 36 | }, 37 | }, 38 | { 39 | name: "prefix-leading-slash", 40 | want: "/test/hooks", 41 | cfg: config.ServerConfig{ 42 | UrlPrefix: "/test", 43 | }, 44 | }, 45 | { 46 | name: "prefix-slash-sandwich", 47 | want: "/test/hooks", 48 | cfg: config.ServerConfig{ 49 | UrlPrefix: "/test/", 50 | }, 51 | }, 52 | } 53 | for _, tt := range tests { 54 | t.Run(tt.name, func(t *testing.T) { 55 | s := NewServer(container.Container{Config: tt.cfg}, []checks.ProcessorEntry{}) 56 | if got := s.hooksPrefix(); got != tt.want { 57 | t.Errorf("hooksPrefix() = %v, want %v", got, tt.want) 58 | } 59 | }) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /pkg/utils.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/rs/zerolog/log" 7 | ) 8 | 9 | var ( 10 | GitTag = "" 11 | GitCommit = "" 12 | ) 13 | 14 | func Pointer[T interface{}](item T) *T { 15 | return &item 16 | } 17 | 18 | func WipeDir(dir string) { 19 | log.Debug().Str("path", dir).Msg("wiping path") 20 | if err := os.RemoveAll(dir); err != nil { 21 | log.Error(). 22 | Err(err). 23 | Str("path", dir). 24 | Msg("failed to wipe path") 25 | } 26 | } 27 | 28 | // WithErrorLogging returns a function that will execute the given function and log any errors that occur. 29 | func WithErrorLogging(f func() error, msg string) { 30 | if err := f(); err != nil { 31 | log.Error().Err(err).Msg(msg) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /pkg/vcs/client.go: -------------------------------------------------------------------------------- 1 | package vcs 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | const ( 8 | DefaultVcsUsername = "kubechecks" 9 | DefaultVcsEmail = "kubechecks@zapier.com" 10 | ) 11 | 12 | var ( 13 | // ErrInvalidType is a sentinel error for use in client implementations 14 | ErrInvalidType = errors.New("invalid event type") 15 | ErrHookNotFound = errors.New("webhook not found") 16 | ) 17 | -------------------------------------------------------------------------------- /pkg/vcs/github_client/emoji.go: -------------------------------------------------------------------------------- 1 | package github_client 2 | 3 | import "github.com/zapier/kubechecks/pkg" 4 | 5 | var stateEmoji = map[pkg.CommitState]string{ 6 | pkg.StateNone: "", 7 | pkg.StateSuccess: ":white_check_mark:", 8 | pkg.StateRunning: ":runner:", 9 | pkg.StateWarning: ":warning:", 10 | pkg.StateFailure: ":red_circle:", 11 | pkg.StateError: ":exclamation:", 12 | pkg.StatePanic: ":skull:", 13 | } 14 | 15 | const defaultEmoji = ":interrobang:" 16 | 17 | // ToEmoji returns a string representation of this state for use in the request 18 | func (c *Client) ToEmoji(s pkg.CommitState) string { 19 | if emoji, ok := stateEmoji[s]; ok { 20 | return emoji 21 | } else { 22 | return defaultEmoji 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /pkg/vcs/github_client/issue.go: -------------------------------------------------------------------------------- 1 | package github_client 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/google/go-github/v62/github" 7 | ) 8 | 9 | type IssuesServices interface { 10 | CreateComment(ctx context.Context, owner string, repo string, number int, comment *github.IssueComment) (*github.IssueComment, *github.Response, error) 11 | DeleteComment(ctx context.Context, owner string, repo string, commentID int64) (*github.Response, error) 12 | ListComments(ctx context.Context, owner string, repo string, number int, opts *github.IssueListCommentsOptions) ([]*github.IssueComment, *github.Response, error) 13 | EditComment(ctx context.Context, owner string, repo string, commentID int64, comment *github.IssueComment) (*github.IssueComment, *github.Response, error) 14 | } 15 | 16 | type IssuesService struct { 17 | IssuesServices 18 | } 19 | -------------------------------------------------------------------------------- /pkg/vcs/github_client/pullrequest.go: -------------------------------------------------------------------------------- 1 | package github_client 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/google/go-github/v62/github" 7 | ) 8 | 9 | type PullRequestsServices interface { 10 | List(ctx context.Context, owner string, repo string, opts *github.PullRequestListOptions) ([]*github.PullRequest, *github.Response, error) 11 | ListFiles(ctx context.Context, owner string, repo string, number int, opts *github.ListOptions) ([]*github.CommitFile, *github.Response, error) 12 | GetRaw(ctx context.Context, owner string, repo string, number int, opts github.RawOptions) (string, *github.Response, error) 13 | Get(ctx context.Context, owner string, repo string, number int) (*github.PullRequest, *github.Response, error) 14 | } 15 | 16 | type PullRequestsService struct { 17 | PullRequestsServices 18 | } 19 | -------------------------------------------------------------------------------- /pkg/vcs/github_client/repo.go: -------------------------------------------------------------------------------- 1 | package github_client 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/google/go-github/v62/github" 7 | ) 8 | 9 | type RepositoriesServices interface { 10 | GetContents(ctx context.Context, owner, repo, path string, opts *github.RepositoryContentGetOptions) (fileContent *github.RepositoryContent, directoryContent []*github.RepositoryContent, resp *github.Response, err error) 11 | Get(ctx context.Context, owner, repo string) (*github.Repository, *github.Response, error) 12 | CreateStatus(ctx context.Context, owner, repo, ref string, status *github.RepoStatus) (*github.RepoStatus, *github.Response, error) 13 | CreateHook(ctx context.Context, owner, repo string, hook *github.Hook) (*github.Hook, *github.Response, error) 14 | ListHooks(ctx context.Context, owner, repo string, opts *github.ListOptions) ([]*github.Hook, *github.Response, error) 15 | } 16 | 17 | type RepositoriesService struct { 18 | RepositoriesServices 19 | } 20 | -------------------------------------------------------------------------------- /pkg/vcs/gitlab_client/backoff.go: -------------------------------------------------------------------------------- 1 | package gitlab_client 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "time" 7 | 8 | "github.com/cenkalti/backoff/v4" 9 | "github.com/rs/zerolog/log" 10 | "github.com/xanzy/go-gitlab" 11 | ) 12 | 13 | // getBackOff returns a backoff pointer to use to retry requests 14 | func getBackOff() *backoff.ExponentialBackOff { 15 | 16 | // Lets setup backoff logic to retry this request for 1 minute 17 | bOff := backoff.NewExponentialBackOff() 18 | bOff.InitialInterval = 60 * time.Second 19 | bOff.MaxInterval = 10 * time.Second 20 | bOff.RandomizationFactor = 0 21 | bOff.MaxElapsedTime = 180 * time.Second 22 | 23 | return bOff 24 | } 25 | 26 | func checkReturnForBackoff(resp *gitlab.Response, err error) error { 27 | // if the error is nil lets check it out 28 | if resp != nil { 29 | if resp.StatusCode == http.StatusTooManyRequests { 30 | log.Warn().Msg("being rate limited doing backoff") 31 | return fmt.Errorf("%s", "Rate Limited") 32 | } 33 | } 34 | if err != nil { 35 | // Lets check the error and see if we need to trigger backoff 36 | switch err.(type) { 37 | default: 38 | // If it is not one of the above errors lets skip the backoff logic 39 | return &backoff.PermanentError{Err: err} 40 | } 41 | } 42 | 43 | // Return nil as the error passed in must have been nil as it passed the switch statement 44 | return nil 45 | } 46 | -------------------------------------------------------------------------------- /pkg/vcs/gitlab_client/emoji.go: -------------------------------------------------------------------------------- 1 | package gitlab_client 2 | 3 | import "github.com/zapier/kubechecks/pkg" 4 | 5 | var stateEmoji = map[pkg.CommitState]string{ 6 | pkg.StateNone: "", 7 | pkg.StateSuccess: ":white_check_mark:", 8 | pkg.StateRunning: ":runner:", 9 | pkg.StateWarning: ":warning:", 10 | pkg.StateFailure: ":red_circle:", 11 | pkg.StateError: ":exclamation:", 12 | pkg.StatePanic: ":skull:", 13 | } 14 | 15 | const defaultEmoji = ":interrobang:" 16 | 17 | // ToEmoji returns a string representation of this state for use in the request 18 | func (c *Client) ToEmoji(s pkg.CommitState) string { 19 | if emoji, ok := stateEmoji[s]; ok { 20 | return emoji 21 | } else { 22 | return defaultEmoji 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /pkg/vcs/gitlab_client/pipeline.go: -------------------------------------------------------------------------------- 1 | package gitlab_client 2 | 3 | import ( 4 | "github.com/rs/zerolog/log" 5 | "github.com/xanzy/go-gitlab" 6 | 7 | "github.com/zapier/kubechecks/pkg" 8 | ) 9 | 10 | func (c *Client) GetLastPipelinesForCommit(projectName string, commitSHA string) *gitlab.PipelineInfo { 11 | pipelines, _, err := c.c.Pipelines.ListProjectPipelines(projectName, &gitlab.ListProjectPipelinesOptions{ 12 | SHA: pkg.Pointer(commitSHA), 13 | }) 14 | if err != nil { 15 | log.Error().Err(err).Msg("gitlab client: could not get last pipeline for commit") 16 | return nil 17 | } 18 | 19 | log.Debug().Int("pipline_count", len(pipelines)).Msg("gitlab client: retrieve pipelines for commit") 20 | 21 | for _, p := range pipelines { 22 | log.Debug(). 23 | Int("pipeline_id", p.ID). 24 | Str("source", p.Source). 25 | Msg("gitlab client: pipeline details") 26 | } 27 | 28 | // check for merge_requests_event 29 | for _, p := range pipelines { 30 | if p.Source == "merge_request_event" { 31 | return p 32 | } 33 | } 34 | 35 | // check for external_pull_request_events next 36 | for _, p := range pipelines { 37 | if p.Source == "pipeline" { 38 | return p 39 | } 40 | } 41 | 42 | // check for external_pull_request_events next 43 | for _, p := range pipelines { 44 | if p.Source == "external_pull_request_event" { 45 | return p 46 | } 47 | } 48 | 49 | for _, p := range pipelines { 50 | if p.Source == "external" { 51 | return p 52 | } 53 | } 54 | 55 | return nil 56 | } 57 | 58 | type PipelinesServices interface { 59 | ListProjectPipelines(pid interface{}, opt *gitlab.ListProjectPipelinesOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.PipelineInfo, *gitlab.Response, error) 60 | } 61 | 62 | type PipelinesService struct { 63 | PipelinesServices 64 | } 65 | -------------------------------------------------------------------------------- /pkg/vcs/gitlab_client/project.go: -------------------------------------------------------------------------------- 1 | package gitlab_client 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | "github.com/cenkalti/backoff/v4" 8 | "github.com/xanzy/go-gitlab" 9 | 10 | "github.com/zapier/kubechecks/pkg" 11 | "github.com/zapier/kubechecks/pkg/repo_config" 12 | ) 13 | 14 | // GetProjectByID gets a project by the given Project Name or ID 15 | func (c *Client) GetProjectByID(project int) (*gitlab.Project, error) { 16 | var proj *gitlab.Project 17 | err := backoff.Retry(func() error { 18 | var err error 19 | var resp *gitlab.Response 20 | proj, resp, err = c.c.Projects.GetProject(project, nil) 21 | return checkReturnForBackoff(resp, err) 22 | }, getBackOff()) 23 | return proj, err 24 | } 25 | 26 | func (c *Client) GetRepoConfigFile(ctx context.Context, projectId int, mergeReqId int) ([]byte, error) { 27 | _, span := tracer.Start(ctx, "GetRepoConfigFile") 28 | defer span.End() 29 | 30 | // check MR branch 31 | for _, file := range repo_config.RepoConfigFilenameVariations() { 32 | b, _, err := c.c.RepositoryFiles.GetRawFile( 33 | projectId, 34 | file, 35 | &gitlab.GetRawFileOptions{Ref: pkg.Pointer("HEAD")}, 36 | ) 37 | if err != nil { 38 | continue 39 | } 40 | return b, nil 41 | } 42 | 43 | return nil, errors.New(".kubecheck.yaml file not found") 44 | } 45 | 46 | type ProjectsServices interface { 47 | AddProjectHook(pid interface{}, opt *gitlab.AddProjectHookOptions, options ...gitlab.RequestOptionFunc) (*gitlab.ProjectHook, *gitlab.Response, error) 48 | ListProjectHooks(pid interface{}, opt *gitlab.ListProjectHooksOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.ProjectHook, *gitlab.Response, error) 49 | EditProjectHook(pid interface{}, hook int, opt *gitlab.EditProjectHookOptions, options ...gitlab.RequestOptionFunc) (*gitlab.ProjectHook, *gitlab.Response, error) 50 | GetProject(pid interface{}, opt *gitlab.GetProjectOptions, options ...gitlab.RequestOptionFunc) (*gitlab.Project, *gitlab.Response, error) 51 | } 52 | 53 | type ProjectsService struct { 54 | ProjectsServices 55 | } 56 | 57 | type RepositoryFilesServices interface { 58 | GetRawFile(pid interface{}, fileName string, opt *gitlab.GetRawFileOptions, options ...gitlab.RequestOptionFunc) ([]byte, *gitlab.Response, error) 59 | } 60 | 61 | type RepositoryFilesService struct { 62 | RepositoryFilesServices 63 | } 64 | -------------------------------------------------------------------------------- /pkg/vcs/repo.go: -------------------------------------------------------------------------------- 1 | package vcs 2 | 3 | import ( 4 | "github.com/zapier/kubechecks/pkg/config" 5 | ) 6 | 7 | // PullRequest represents an PR/MR 8 | type PullRequest struct { 9 | BaseRef string // base ref is the branch that the PR is being merged into 10 | HeadRef string // head ref is the branch that the PR is coming from 11 | DefaultBranch string // Some repos have default branches we need to capture 12 | Remote string // Remote address 13 | CloneURL string // Where we clone the repo from 14 | Name string // Name of the repo 15 | Owner string // Owner of the repo (in Gitlab this is the namespace) 16 | CheckID int // MR/PR id that generated this Repo 17 | SHA string // SHA of the MR/PRs head 18 | FullName string // Owner/Name combined (ie zapier/kubechecks) 19 | Username string // Username of auth'd client 20 | Email string // Email of auth'd client 21 | Labels []string // Labels associated with the MR/PR 22 | 23 | Config config.ServerConfig 24 | } 25 | -------------------------------------------------------------------------------- /pkg/vcs/types.go: -------------------------------------------------------------------------------- 1 | package vcs 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | 7 | "github.com/zapier/kubechecks/pkg" 8 | "github.com/zapier/kubechecks/pkg/msg" 9 | ) 10 | 11 | type WebHookConfig struct { 12 | Url string 13 | SecretKey string 14 | Events []string 15 | } 16 | 17 | // Client represents a VCS client 18 | type Client interface { 19 | // PostMessage takes in project name in form "owner/repo" (ie zapier/kubechecks), the PR/MR id, and the actual message 20 | PostMessage(context.Context, PullRequest, string) (*msg.Message, error) 21 | // UpdateMessage update a message with new content 22 | UpdateMessage(context.Context, *msg.Message, string) error 23 | // VerifyHook validates a webhook secret and return the body; must be called even if no secret 24 | VerifyHook(*http.Request, string) ([]byte, error) 25 | // ParseHook parses webook payload for valid events, with context for request-scoped values 26 | ParseHook(context.Context, *http.Request, []byte) (PullRequest, error) 27 | // CommitStatus sets a status for a specific commit on the remote VCS 28 | CommitStatus(context.Context, PullRequest, pkg.CommitState) error 29 | // GetHookByUrl gets a webhook by url 30 | GetHookByUrl(ctx context.Context, repoName, webhookUrl string) (*WebHookConfig, error) 31 | // CreateHook creates a webhook that points at kubechecks 32 | CreateHook(ctx context.Context, repoName, webhookUrl, webhookSecret string) error 33 | // GetName returns the VCS client name (e.g. "github" or "gitlab") 34 | GetName() string 35 | // TidyOutdatedComments either by hiding or deleting them 36 | TidyOutdatedComments(context.Context, PullRequest) error 37 | // LoadHook creates an EventRequest from the ID of an actual request 38 | LoadHook(ctx context.Context, repoAndId string) (PullRequest, error) 39 | 40 | Username() string 41 | Email() string 42 | ToEmoji(pkg.CommitState) string 43 | } 44 | -------------------------------------------------------------------------------- /telemetry/helpers.go: -------------------------------------------------------------------------------- 1 | package telemetry 2 | 3 | import ( 4 | "context" 5 | "encoding/binary" 6 | "encoding/hex" 7 | "strconv" 8 | 9 | "go.opentelemetry.io/otel/codes" 10 | "go.opentelemetry.io/otel/trace" 11 | ) 12 | 13 | type otelSpanInfo struct { 14 | spanID trace.SpanID 15 | traceID trace.TraceID 16 | } 17 | 18 | func GetOtelSpanInfoFromContext(ctx context.Context) otelSpanInfo { 19 | s := trace.SpanFromContext(ctx) 20 | 21 | return otelSpanInfo{ 22 | spanID: s.SpanContext().SpanID(), 23 | traceID: s.SpanContext().TraceID(), 24 | } 25 | } 26 | 27 | func (o otelSpanInfo) SpanIDValid() bool { 28 | return o.spanID.IsValid() 29 | } 30 | 31 | func (o otelSpanInfo) SpanID() string { 32 | return o.spanID.String() 33 | } 34 | 35 | func (o otelSpanInfo) TraceID() string { 36 | return o.traceID.String() 37 | } 38 | 39 | func GetTraceID(ctx context.Context) string { 40 | tID, err := decodeTraceID(GetOtelSpanInfoFromContext(ctx).TraceID()) 41 | if err != nil { 42 | return "" 43 | } 44 | return strconv.FormatUint(traceIDToUint64(tID), 10) 45 | } 46 | 47 | // traceIDToUint64 converts 128bit traceId to 64 bit uint64 48 | func traceIDToUint64(b [16]byte) uint64 { 49 | return binary.BigEndian.Uint64(b[len(b)-8:]) 50 | } 51 | 52 | func decodeTraceID(traceID string) ([16]byte, error) { 53 | var ret [16]byte 54 | _, err := hex.Decode(ret[:], []byte(traceID)) 55 | return ret, err 56 | } 57 | 58 | func SetError(span trace.Span, err error, event string) { 59 | span.RecordError(err) 60 | span.SetStatus(codes.Error, event) 61 | } 62 | -------------------------------------------------------------------------------- /telemetry/metric.go: -------------------------------------------------------------------------------- 1 | package telemetry 2 | 3 | import ( 4 | "go.opentelemetry.io/otel/attribute" 5 | ) 6 | 7 | type MetricType int 8 | 9 | const ( 10 | GaugeInt MetricType = iota 11 | GaugeFloat 12 | HistogramInt 13 | HistogramFloat 14 | CounterInt 15 | CounterFloat 16 | ) 17 | 18 | func (mt MetricType) String() string { 19 | switch mt { 20 | case GaugeInt: 21 | return "GaugeInt" 22 | case GaugeFloat: 23 | return "GaugeFloat" 24 | case HistogramInt: 25 | return "HistogramInt" 26 | case HistogramFloat: 27 | return "HistogramFloat" 28 | case CounterInt: 29 | return "CounterInt" 30 | case CounterFloat: 31 | return "CounterFloat" 32 | } 33 | 34 | return "TypeUnknown" 35 | } 36 | 37 | type AttributeType uint 38 | 39 | const ( 40 | // INVALID is used for a Value with no value set. 41 | INVALID AttributeType = iota 42 | // BOOL is a boolean Type Value. 43 | BOOL 44 | // INT64 is a 64-bit signed integral Type Value. 45 | INT64 46 | // FLOAT64 is a 64-bit floating point Type Value. 47 | FLOAT64 48 | // STRING is a string Type Value. 49 | STRING 50 | // BOOLSLICE is a slice of booleans Type Value. 51 | BOOLSLICE 52 | // INT64SLICE is a slice of 64-bit signed integral numbers Type Value. 53 | INT64SLICE 54 | // FLOAT64SLICE is a slice of 64-bit floating point numbers Type Value. 55 | FLOAT64SLICE 56 | // STRINGSLICE is a slice of strings Type Value. 57 | STRINGSLICE 58 | ) 59 | 60 | type Attrs struct { 61 | Key string 62 | Value AttrsValue 63 | } 64 | 65 | type AttrsValue struct { 66 | Type AttributeType 67 | Numberic int64 68 | Stringly string 69 | Slice interface{} 70 | } 71 | 72 | func intToBool(i int64) bool { 73 | return i == 1 74 | } 75 | 76 | type MetricData struct { 77 | Name string 78 | MetricType MetricType 79 | Value interface{} 80 | Attrs []Attrs 81 | } 82 | 83 | func (md MetricData) ConvertAttrs() []attribute.KeyValue { 84 | attrs := []attribute.KeyValue{} 85 | 86 | for _, attr := range md.Attrs { 87 | switch attr.Value.Type { 88 | case STRING: 89 | attrs = append(attrs, attribute.String(attr.Key, attr.Value.Stringly)) 90 | case BOOL: 91 | attrs = append(attrs, attribute.Bool(attr.Key, intToBool(attr.Value.Numberic))) 92 | case STRINGSLICE: 93 | attrs = append(attrs, attribute.StringSlice(attr.Key, attr.Value.Slice.([]string))) 94 | default: 95 | attrs = append(attrs, attribute.String(attr.Key, attr.Value.Stringly)) 96 | } 97 | } 98 | 99 | return attrs 100 | } 101 | -------------------------------------------------------------------------------- /tools/dump_crds/cmd/README.md: -------------------------------------------------------------------------------- 1 | # dumpcrds command 2 | 3 | ## SUMMARY 4 | 5 | `dump` command is used to generate schemas for the Kubernetes custom resource definitions. 6 | The schemas are used to validate the kubechecks changes. 7 | 8 | ## How does it work 9 | 10 | 1. Connect to the cluster you want to generate schemas from and get current version 11 | ```json 12 | { 13 | "clientVersion": { 14 | "major": "1", 15 | "minor": "30", 16 | "gitVersion": "v1.30.1", 17 | "gitCommit": "6911225c3f747e1cd9d109c305436d08b668f086", 18 | "gitTreeState": "clean", 19 | "buildDate": "2024-05-14T10:50:53Z", 20 | "goVersion": "go1.22.2", 21 | "compiler": "gc", 22 | "platform": "darwin/arm64" 23 | }, 24 | "kustomizeVersion": "v5.0.4-0.20230601165947-6ce0bf390ce3" 25 | } 26 | ``` 27 | 28 | use the gitVersion attributes to find the current version. 29 | 30 | 2. list all CRDs in the cluster 31 | 3. create a directory `` for the matching version (e.g. v1.30.1 -> v1.30.0) 32 | 4. for each CRD, get the `version.schema.openAPIV3Schema` and save it to a file 33 | 5. file name is in the format `crd--.json` 34 | 35 | ## How to add a new command 36 | 37 | 1. create a new cobra.Command file in the `dumpcrd` directory. 38 | 2. make sure the file attaches itself to the rootCmd. (e.g. `rootCmd.AddCommand(newCmd)`) 39 | -------------------------------------------------------------------------------- /tools/dump_crds/cmd/dumpcrd/root.go: -------------------------------------------------------------------------------- 1 | package dumpcrd 2 | 3 | import ( 4 | "log/slog" 5 | "os" 6 | 7 | "github.com/spf13/cobra" 8 | "github.com/spf13/viper" 9 | ) 10 | 11 | var cfgFile string 12 | 13 | // rootCmd represents the base command when called without any subcommands 14 | var rootCmd = &cobra.Command{ 15 | Use: "dumpcrds", 16 | Short: "extract crd from kubernetes cluster", 17 | } 18 | 19 | // Execute adds all child commands to the root command and sets flags appropriately. 20 | // This is called by main.main(). It only needs to happen once to the rootCmd. 21 | func Execute() { 22 | err := rootCmd.Execute() 23 | if err != nil { 24 | os.Exit(1) 25 | } 26 | } 27 | 28 | func init() { 29 | cobra.OnInitialize(initConfig) 30 | 31 | // Here you will define your flags and configuration settings. 32 | // Cobra supports persistent flags, which, if defined here, 33 | // will be global for your application. 34 | rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.dumpcrd.yaml)") 35 | 36 | // Cobra also supports local flags, which will only run 37 | // when this action is called directly. 38 | rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") 39 | } 40 | 41 | // initConfig reads in config file and ENV variables if set. 42 | func initConfig() { 43 | if cfgFile != "" { 44 | // Use config file from the flag. 45 | viper.SetConfigFile(cfgFile) 46 | } else { 47 | // Find home directory. 48 | home, err := os.UserHomeDir() 49 | cobra.CheckErr(err) 50 | 51 | // Search config in home directory with name ".dumpcrd" (without extension). 52 | viper.AddConfigPath(home) 53 | viper.SetConfigType("yaml") 54 | viper.SetConfigName(".dumpcrd") 55 | } 56 | 57 | viper.AutomaticEnv() // read in environment variables that match 58 | 59 | // If a config file is found, read it in. 60 | if err := viper.ReadInConfig(); err == nil { 61 | slog.Info("dumpcrds config file", "config", viper.ConfigFileUsed()) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /tools/dump_crds/cmd/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2024 NAME HERE 3 | */ 4 | package main 5 | 6 | import "github.com/zapier/kubechecks/tools/dump_crds/cmd/dumpcrd" 7 | 8 | func main() { 9 | dumpcrd.Execute() 10 | } 11 | -------------------------------------------------------------------------------- /tools/dump_crds/internal/logger/logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "log/slog" 5 | "os" 6 | "path/filepath" 7 | "time" 8 | 9 | "github.com/lmittmann/tint" 10 | ) 11 | 12 | func InitLogger(jsonOut, debug bool) *slog.Logger { 13 | 14 | var logLevel slog.Level 15 | switch os.Getenv("LOG_LEVEL") { 16 | case "debug": 17 | logLevel = slog.LevelDebug 18 | case "info": 19 | logLevel = slog.LevelInfo 20 | case "warning": 21 | logLevel = slog.LevelWarn 22 | case "error": 23 | logLevel = slog.LevelError 24 | default: 25 | logLevel = slog.LevelInfo 26 | } 27 | if debug { 28 | logLevel = slog.LevelDebug 29 | } 30 | 31 | var handler slog.Handler 32 | if jsonOut { 33 | handler = slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ 34 | AddSource: true, 35 | Level: logLevel, 36 | ReplaceAttr: slogFormatter, 37 | }) 38 | 39 | } else { 40 | handler = tint.NewHandler(os.Stdout, 41 | &tint.Options{ 42 | AddSource: debug, 43 | Level: logLevel, 44 | ReplaceAttr: nil, 45 | NoColor: false, 46 | TimeFormat: time.RFC3339, 47 | }, 48 | ) 49 | } 50 | return slog.New(handler) 51 | } 52 | 53 | func slogFormatter(_ []string, a slog.Attr) slog.Attr { 54 | if a.Key == slog.TimeKey { 55 | if _, ok := a.Value.Any().(*time.Time); !ok { 56 | return a 57 | } 58 | a.Value = slog.StringValue(a.Value.Time().Format(time.RFC3339)) 59 | } 60 | if a.Key == slog.SourceKey { 61 | if _, ok := a.Value.Any().(*slog.Source); !ok { 62 | return a 63 | } 64 | source := a.Value.Any().(*slog.Source) 65 | source.File = filepath.Base(source.File) 66 | // Rename attribute name "source" to "source_info" 67 | // to avoid conflict with "source" attribute in Opensearch index. 68 | a.Key = "source_info" 69 | } 70 | return a 71 | } 72 | --------------------------------------------------------------------------------