diff --git a/.flake8 b/.flake8
deleted file mode 100644
index fa22783..0000000
--- a/.flake8
+++ /dev/null
@@ -1,5 +0,0 @@
-[flake8]
-ignore =
- E128, # Continuation line under-indented for visual indent
- E501, # Long lines
- W504 # Line break after binary operator
\ No newline at end of file
diff --git a/.github/workflows/cd.yaml b/.github/workflows/cd.yaml
deleted file mode 100644
index fff5079..0000000
--- a/.github/workflows/cd.yaml
+++ /dev/null
@@ -1,57 +0,0 @@
-name: CD
-
-on:
- workflow_run:
- workflows: ["CI"]
- branches: [main]
- types:
- - completed
-
-jobs:
- deploy_to_dev:
- runs-on: ubuntu-latest
- environment: dev
- if: ${{ github.event.workflow_run.conclusion == 'success' }}
- steps:
- - name: Checkout
- uses: actions/checkout@v2
- - name: Download Image Tags
- uses: dawidd6/action-download-artifact@v2
- with:
- name: image_tags
- workflow: ci.yml
- run_id: ${{ github.event.workflow_run.id}}
- path: ${{ github.workspace }}
- - name: Download Manifests
- uses: dawidd6/action-download-artifact@v2
- with:
- name: manifests
- workflow: ci.yml
- run_id: ${{ github.event.workflow_run.id}}
- path: ${{ github.workspace }}
- - name: Read Image Tags
- run: |
- echo "IMAGE_TAG=$(cat ${{ github.workspace }}/IMAGE_TAG)" >> $GITHUB_ENV
- - name: Generate Manifests
- run: |
- .github/workflows/utils/generate-manifests.sh ${{ github.workspace }}/manifests gen_manifests all
- env:
- APP_BUILD_VERSION: ${{ env.IMAGE_TAG }}
-
- GIT_REPOSITORY_TYPE: ${{ secrets.GIT_REPOSITORY_TYPE }}
- CICD_ORCHESTRATOR_TYPE: ${{ secrets.CICD_ORCHESTRATOR_TYPE }}
- GITOPS_OPERATOR_TYPE: ${{ secrets.GITOPS_OPERATOR_TYPE }}
- GITHUB_GITOPS_REPO_NAME: ${{ secrets.GH_GITOPS_REPO_NAME }}
- GITHUB_GITOPS_MANIFEST_REPO_NAME: ${{ secrets.GH_GITOPS_MANIFEST_REPO_NAME }}
- GITHUB_ORG_URL: ${{ secrets.GH_ORG_URL }}
-
- GITOPS_APP_URL: ${{ secrets.GITOPS_APP_URL }}
- AZDO_GITOPS_REPO_NAME: ${{ secrets.AZDO_GITOPS_REPO_NAME }}
- AZDO_PR_REPO_NAME: ${{ secrets.AZDO_PR_REPO_NAME }}
- AZDO_ORG_URL: ${{ secrets.AZDO_ORG_URL }}
- ORCHESTRATOR_PAT: ${{ secrets.ORCHESTRATOR_PAT }}
- - name: Create PR
- run: |
- .github/workflows/utils//create-pr.sh -s ${{ github.workspace }}/gen_manifests -d ${{ secrets.MANIFESTS_FOLDER }} -r ${{ secrets.MANIFESTS_REPO }} -b ${{ secrets.MANIFESTS_BRANCH }} -i ${{ github.event.workflow_run.id}} -t ${{ secrets.MANIFESTS_PAT }} -e ${{ secrets.ENVIRONMENT_NAME }}
-
-
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
deleted file mode 100644
index d22794a..0000000
--- a/.github/workflows/ci.yaml
+++ /dev/null
@@ -1,66 +0,0 @@
-
-name: CI
-
-on:
- push:
- branches:
- - main
- - eedorenko/update-home
-
-jobs:
- code_quality_checks:
- runs-on: ubuntu-latest
- steps:
- - name: Checkout
- uses: actions/checkout@v2
- - uses: TrueBrain/actions-flake8@master
- name: Linting
- if: "true"
- with:
- path: ./src
- - uses: hadolint/hadolint-action@v1.5.0
- name: Docker Linting
- with:
- dockerfile: ./src/Dockerfile
- - name: MD Linting
- uses: actionshub/markdownlint@main
- - name: Stay woke
- uses: get-woke/woke-action@v0
- with:
- # Cause the check to fail on any broke rules
- fail-on-error: true
-
- Build_Push_Image:
- runs-on: ubuntu-latest
- needs: code_quality_checks
- steps:
- - name: Checkout
- uses: actions/checkout@v2
- - name: Login to DockerHub
- uses: docker/login-action@v1
- with:
- registry: ghcr.io
- username: ${{ github.actor }}
- password: ${{ secrets.GITHUB_TOKEN }}
- - name: Generate Image Tag
- run: |
- IMAGE_TAG=${{ secrets.MAJOR_VERSION }}.${{ secrets.MINOR_VERSION }}.${{ secrets.HF_VERSION }}-${{ github.run_number }}
- echo "IMAGE_TAG=$IMAGE_TAG" >> $GITHUB_ENV
- echo $IMAGE_TAG > $GITHUB_WORKSPACE/IMAGE_TAG
- - name: Build and Push to Docker Hub
- uses: docker/build-push-action@v2
- with:
- push: true
- context: ./src
- tags: ghcr.io/azure/gitops-connector:${{ env.IMAGE_TAG }}, ghcr.io/azure/gitops-connector:latest
- - name: Upload Image Tags
- uses: actions/upload-artifact@v2.2.2
- with:
- name: image_tags
- path: ${{ github.workspace }}/IMAGE_TAG
- - name: Upload Manifests Templates
- uses: actions/upload-artifact@v2.2.2
- with:
- name: manifests
- path: ${{ github.workspace }}/manifests
-
\ No newline at end of file
diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml
deleted file mode 100644
index 297a7dc..0000000
--- a/.github/workflows/pr.yaml
+++ /dev/null
@@ -1,29 +0,0 @@
-name: PR
-
-on:
- pull_request:
- branches: [main]
-
-jobs:
- code_quality_checks:
- runs-on: ubuntu-latest
- steps:
- - name: Checkout
- uses: actions/checkout@v2
- - uses: TrueBrain/actions-flake8@master
- name: Linting
- if: "true"
- with:
- path: ./src
- - uses: hadolint/hadolint-action@v1.5.0
- name: Docker Linting
- with:
- dockerfile: ./src/Dockerfile
- - name: MD Linting
- uses: actionshub/markdownlint@main
- - name: Stay woke
- uses: get-woke/woke-action@v0
- with:
- # Cause the check to fail on any broke rules
- fail-on-error: true
-
diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml
deleted file mode 100644
index da5357b..0000000
--- a/.github/workflows/publish.yaml
+++ /dev/null
@@ -1,49 +0,0 @@
-name: Publish
-
-on:
- # repository_dispatch:
- # types: [sync-success]
- workflow_run:
- workflows: ["CI"]
- branches: [main, eedorenko/update-home]
- types:
- - completed
-
-jobs:
- update_helm_chart:
- name: "Update Helm Chart"
- environment: prod
- runs-on: ubuntu-latest
- steps:
- - name: Checkout
- uses: actions/checkout@v2.3.4
- - name: Download Image Tags
- uses: dawidd6/action-download-artifact@v2
- with:
- name: image_tags
- workflow: ci.yml
- # run_id: ${{ github.event.client_payload.runid }}
- run_id: ${{ github.event.workflow_run.id }}
- path: ${{ github.workspace }}
- - name: Download Manifests
- uses: dawidd6/action-download-artifact@v2
- with:
- name: manifests
- workflow: ci.yml
- # run_id: ${{ github.event.client_payload.runid }}
- run_id: ${{ github.event.workflow_run.id }}
- path: ${{ github.workspace }}
- - name: Read Image Tags
- run: |
- echo "IMAGE_TAG=$(cat ${{ github.workspace }}/IMAGE_TAG)" >> $GITHUB_ENV
- - name: Generate Manifests
- run: |
- .github/workflows/utils/generate-manifests.sh ${{ github.workspace }}/manifests gen_manifests hld_only
- env:
- APP_BUILD_VERSION: ${{ env.IMAGE_TAG }}
- ORCHESTRATOR_PAT: ${{ secrets.ORCHESTRATOR_PAT }}
- - name: Publish Helm Chart
- run: |
- .github/workflows/utils/publish_helm_chart.sh gen_manifests/hld/helm ${{ secrets.HELM_CHARTS_REPO_NAME }} ${{ secrets.HELM_CHARTS_URL }}
- env:
- TOKEN: ${{ secrets.HELM_CHARTS_PAT }}
diff --git a/.github/workflows/utils/create-pr.sh b/.github/workflows/utils/create-pr.sh
deleted file mode 100755
index 10a17ee..0000000
--- a/.github/workflows/utils/create-pr.sh
+++ /dev/null
@@ -1,82 +0,0 @@
-# Copyright (c) Microsoft Corporation.
-# Licensed under the MIT License.
-
-#!/usr/bin/env bash
-
-while getopts "s:d:r:b:i:t:e:p:" option;
- do
- case "$option" in
- s ) SOURCE_FOLDER=${OPTARG};;
- d ) DEST_FOLDER=${OPTARG};;
- r ) DEST_REPO=${OPTARG};;
- b ) DEST_BRANCH=${OPTARG};;
- i ) DEPLOY_ID=${OPTARG};;
- t ) TOKEN=${OPTARG};;
- e ) ENV_NAME=${OPTARG};;
- esac
-done
-echo "List input params"
-echo $SOURCE_FOLDER
-echo $DEST_FOLDER
-echo $DEST_REPO
-echo $DEST_BRANCH
-echo $DEPLOY_ID
-echo $ENV_NAME
-echo "end of list"
-
-set -euo pipefail # fail on error
-
-pr_user_name="Git Ops"
-pr_user_email="agent@gitops.com"
-
-git config --global user.email $pr_user_email
-git config --global user.name $pr_user_name
-
-# Clone manifests repo
-echo "Clone manifests repo"
-repo_url="${DEST_REPO#http://}"
-repo_url="${DEST_REPO#https://}"
-repo_url="https://automated:$TOKEN@$repo_url"
-
-echo "git clone $repo_url -b $DEST_BRANCH --depth 1 --single-branch"
-git clone $repo_url -b $DEST_BRANCH --depth 1 --single-branch
-repo=${DEST_REPO##*/}
-repo_name=${repo%.*}
-cd "$repo_name"
-echo "git status"
-git status
-
-# Create a new branch
-deploy_branch_name=deploy/$DEPLOY_ID/$IMAGE_TAG/$DEST_BRANCH
-
-echo "Create a new branch $deploy_branch_name"
-git checkout -b $deploy_branch_name
-
-# Add generated manifests to the new deploy branch
-mkdir -p $DEST_FOLDER
-cp -r $SOURCE_FOLDER/* $DEST_FOLDER/
-git add -A
-echo "git status"
-git status
-echo `git status --porcelain | head -1`
-if [[ `git status --porcelain | head -1` ]]; then
- git commit -m "deployment $DEPLOY_ID"
-
- # Push to the deploy branch
- echo "Push to the deploy branch $deploy_branch_name"
- echo "git push --set-upstream $repo_url $deploy_branch_name"
- git push --set-upstream $repo_url $deploy_branch_name
-
- # Create a PR
- echo "Create a PR to $DEST_BRANCH"
-
- owner_repo="${DEST_REPO#https://github.com/}"
- echo $owner_repo
- pr_response=$(curl -H "Authorization: token $TOKEN" -H "Content-Type: application/json" --fail \
- -d '{"head":"refs/heads/'$deploy_branch_name'", "base":"refs/heads/'$DEST_BRANCH'", "body":"Deploy to '$ENV_NAME'", "title":"deployment '$DEPLOY_ID'"}' \
- "https://api.github.com/repos/$owner_repo/pulls")
- # This cli is still very buggy:
- # echo $TOKEN | gh auth login --with-token
- # pr_response=$(gh pr create --base $DEST_BRANCH --head $deploy_branch_name --title "deployment '$DEPLOY_ID'")
- echo $pr_response
-fi
\ No newline at end of file
diff --git a/.github/workflows/utils/generate-manifests.sh b/.github/workflows/utils/generate-manifests.sh
deleted file mode 100755
index 8e98133..0000000
--- a/.github/workflows/utils/generate-manifests.sh
+++ /dev/null
@@ -1,52 +0,0 @@
-# Copyright (c) Microsoft Corporation.
-# Licensed under the MIT License.
-
-#!/bin/bash
-
-# Generates K8s manifests from Helm + Kustomize templates
-# Uses env variables to substitute values
-# Requires to be installed:
-# - helm
-# - kubectl
-# - envsubst (https://command-not-found.com/envsubst)
-#
-echo $1
-echo $2
-echo $3
-
-set -euo pipefail # fail on error
-
-export gen_manifests_file_name='gen_manifests.yaml'
-
-# Usage:
-# generate-manifests.sh FOLDER_WITH_MANIFESTS GENERATED_MANIFESTS_FOLDER
-# e.g.:
-# generate-manifests.sh cloud-native-ops/azure-vote/manifests gen_manifests
-#
-# the script will put Helm + Kustomize manifests with substituted variable values
-# to gen_manifests/hld folder and plain yaml manifests to gen_manifests/gen_manifests.yaml file
-
-
-mkdir -p $2
-mkdir -p $2/hld
-mkdir -p $2/manifest
-
-# Substitute env variables in Helm yaml files in the manifest folder
-for file in `find $1 -type f \( -name "values.yaml" -o -name "Chart.yaml" \)`; do envsubst <"$file" > "$file"1 && mv "$file"1 "$file"; done
-
-# Generate manifests
-# for app in `find $1 -type d -maxdepth 1 -mindepth 1`; do \
-cp -r "$1"/helm $2/hld/
-
-if [[ $3 == "all" ]]; then
-helm template "$1"/helm > $2/manifest/$gen_manifests_file_name && \
-cat $2/manifest/$gen_manifests_file_name
-if [ $? -gt 0 ]
- then
- echo "Could not render manifests"
- exit 1
- fi
-fi
-# done
-pwd
-
diff --git a/.github/workflows/utils/publish_helm_chart.sh b/.github/workflows/utils/publish_helm_chart.sh
deleted file mode 100755
index 84e56f9..0000000
--- a/.github/workflows/utils/publish_helm_chart.sh
+++ /dev/null
@@ -1,48 +0,0 @@
-# Copyright (c) Microsoft Corporation.
-# Licensed under the MIT License.
-
-#!/usr/bin/env bash
-
-# Usage:
-# publish_helm_chart.sh FOLDER_WITH_CHART CHART_REPO_NAME CHART_REPO_URL
-
-set -euo pipefail # fail on error
-
-FOLDER_WITH_CHART=$1
-CHART_REPO_NAME=$2
-CHART_REPO_URL=$3
-
-helm package $FOLDER_WITH_CHART
-
-DEST_BRANCH="gh-pages"
-
-pr_user_name="Git Ops"
-pr_user_email="agent@gitops.com"
-
-git config --global user.email $pr_user_email
-git config --global user.name $pr_user_name
-
-# Clone manifests repo
-echo "Clone manifests repo"
-repo_url="${CHART_REPO_NAME#http://}"
-repo_url="${CHART_REPO_NAME#https://}"
-repo_url="https://automated:$TOKEN@$repo_url"
-
-echo "git clone $repo_url -b $DEST_BRANCH --depth 1 --single-branch"
-
-git clone $repo_url -b $DEST_BRANCH --depth 1 --single-branch
-
-echo "git clone"
-
-repo=${CHART_REPO_NAME##*/}
-repo_name=${repo%.*}
-cp *.tgz $repo_name/
-cd $repo_name
-helm repo index . --url $CHART_REPO_URL
-
-git add -A
-echo "git status"
-git status
-
-git commit -m "add a new chart"
-git push origin
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
deleted file mode 100644
index 3b89c95..0000000
--- a/.gitignore
+++ /dev/null
@@ -1,132 +0,0 @@
-# Byte-compiled / optimized / DLL files
-__pycache__/
-*.py[cod]
-*$py.class
-
-# C extensions
-*.so
-
-# Distribution / packaging
-.Python
-build/
-develop-eggs/
-dist/
-downloads/
-eggs/
-.eggs/
-lib/
-lib64/
-parts/
-sdist/
-var/
-wheels/
-pip-wheel-metadata/
-share/python-wheels/
-*.egg-info/
-.installed.cfg
-*.egg
-MANIFEST
-
-# PyInstaller
-# Usually these files are written by a python script from a template
-# before PyInstaller builds the exe, so as to inject date/other infos into it.
-*.manifest
-*.spec
-
-# Installer logs
-pip-log.txt
-pip-delete-this-directory.txt
-
-# Unit test / coverage reports
-htmlcov/
-.tox/
-.nox/
-.coverage
-.coverage.*
-.cache
-nosetests.xml
-coverage.xml
-*.cover
-*.py,cover
-.hypothesis/
-.pytest_cache/
-
-# Translations
-*.mo
-*.pot
-
-# Django stuff:
-*.log
-local_settings.py
-db.sqlite3
-db.sqlite3-journal
-
-# Flask stuff:
-instance/
-.webassets-cache
-
-# Scrapy stuff:
-.scrapy
-
-# Sphinx documentation
-docs/_build/
-
-# PyBuilder
-target/
-
-# Jupyter Notebook
-.ipynb_checkpoints
-
-# IPython
-profile_default/
-ipython_config.py
-
-# pyenv
-.python-version
-
-# pipenv
-# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
-# However, in case of collaboration, if having platform-specific dependencies or dependencies
-# having no cross-platform support, pipenv may install dependencies that don't work, or not
-# install all needed dependencies.
-#Pipfile.lock
-
-# PEP 582; used by e.g. github.com/David-OConnor/pyflow
-__pypackages__/
-
-# Celery stuff
-celerybeat-schedule
-celerybeat.pid
-
-# SageMath parsed files
-*.sage.py
-
-# Environments
-.env
-.venv
-env/
-venv/
-ENV/
-env.bak/
-venv.bak/
-
-# Spyder project settings
-.spyderproject
-.spyproject
-
-# Rope project settings
-.ropeproject
-
-# mkdocs documentation
-/site
-
-# mypy
-.mypy_cache/
-.dmypy.json
-dmypy.json
-
-# Pyre type checker
-.pyre/
-
-# VSCode
-.vscode
diff --git a/.mdlrc b/.mdlrc
deleted file mode 100644
index d0b3b43..0000000
--- a/.mdlrc
+++ /dev/null
@@ -1,3 +0,0 @@
-rules "~MD013, ~MD009, ~MD012, ~MD033, ~MD034" # Disable "long line", "Trailing spaces", "Multiple consecutive blank lines", "Multiple consecutive blank lines", "Bare URL used" warnings
-
-
diff --git a/LICENSE b/LICENSE
deleted file mode 100644
index f90094b..0000000
--- a/LICENSE
+++ /dev/null
@@ -1,21 +0,0 @@
-MIT License
-
-Copyright (c) 2021 Microsoft Corporation.
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
diff --git a/README.md b/README.md
index a588aa0..adb19da 100644
--- a/README.md
+++ b/README.md
@@ -1,341 +1,9 @@
-
-
-
+# GitOps Connector Helm Repository
-# GitOps Connector
-
-GitOps Connector is a custom component with the goal of enriching the integration of a GitOps operator and a CI/CD orchestrator so the user experience in the entire CI/CD process is smoother and more observable. The whole process can be handled and monitored from a CI/CD orchestrator.
-
-
-
-During the reconciliation process a GitOps operator notifies on every phase change and every health check the GitOps connector. This component serves as an adapter, it "knows" how to communicate to a Git repository and it updates the Git commit status so the synchronization progress is visible in the Manifests repository. When the reconciliation including health check has successfully finished or failed the connector notifies a CI/CD orchestrator, so the CD pipelines/workflows may perform corresponding actions such as testing, post-deployment activities and moving on to the next stage in the deployment chain.
-
-Refer to the following implementations to understand the role the GitOps Connector plays in various GitOps flows:
-
-- [GitOps with Azure DevOps and ArgoCD/Flux](https://github.com/kaizentm/cloud-native-ops/blob/master/docs/azdo-gitops.md)
-- [HLD based CI/CD Pipeline with GitOps connector](https://github.com/kaizentm/cloud-native-ops/blob/master/docs/cicd-hld-pipeline.md)
-- [GitOps with GitHub and Flux v2](https://github.com/kaizentm/cloud-native-ops/blob/master/docs/azdo-gitops-githubfluxv2.md)
-
-## Motivation
-
-One of the biggest challenges in GitOps is observability. When a CI/CD pipeline orchestrates the deployment process by manipulating manifests in the repository, the real deployment is handled by a GitOps operator. When it happens, how it goes and when it finishes, successfully or not, we don't know. Neither does the CI/CD pipeline. To build a multistage CD pipeline there must be a backward connection from a GitOps operator to CI/CD orchestrator that reports the deployment state/result so the CD pipeline can proceed with testing, notifying, deploying to next environment.
-
-Partly this challenge can be addressed by leveraging notifications components of GitOps operators such as [FluxCD Notification Controller](https://fluxcd.io/docs/components/notification/) and [ArgoCD Notifications](https://argocd-notifications.readthedocs.io/en/stable/). It's possible to configure basic notifications from Flux/ArgoCD to Azure DevOps/GitHub so the deployment phases are reflected with the Git Commit status in the repo.
-
-#### So why cannot we "just" use the existing notification functionality of Argo/Flux events?
-
-The thing is that "basic" is not enough.
-
-What we need:
- - A CD pipeline (e.g. Azure Pipeline) creates a PR to the manifests repo (e.g. GitHub) and waits until it's merged and deployment is finished. When the deployment is finished we need to find out which PR caused this deployment and invoke an Azure Pipelines API to notify a corresponding pipeline run to resume or fail.
- - In Git Commit status we want to see more details on what resources have been applied and what resources didn't pass the health check.
-
-We simply can't do what we need just by configuring notifications in FluxCD/ArgoCD. To implement the logic of what we need we want to write some code and run it somewhere. We need some freedom in getting out of the boundaries of what is "out-of-the-box" in FluxCD/ArgoCD and extending it with custom logic to build a comprehensive CD flow with Azure Pipelines and/or GitHub actions. At the same time we want to keep pipelines simple and not dependent on underlying GitOps operator. Hence, the GitOps connector.
-
-#### Why cannot we enhance ArgoCD/FluxCD notification functionality to achieve the same?
-
-Actually we can. The only thing that we'd have to do that twice. And again, when we support another GitOps operator. And separately for GitHub and Azure DevOps. And again when we support another CI/CD orchestrator. So should we? The GitOps connector decouples ArgoCD/FluxCD from GitHub/Azure DevOps. It reduces dependencies serving as a glue between them.
-Think of the GitOps connector as of an extension of FluxCD/ArgoCD (one for both) or, in terms of FluxCD notifications, a custom provider for GitHub, Azure DevOps, various observability dashboards (e.g. Spektate). A custom provider extracted as a separate microservice with an independent lifecycle. When we want to build same CI/CD flow with another GitOps operator, we will update the connector keeping the same pipelines setup. When we want to enrich look-n-feel of Git Commit status or use a new feature in Azure DevOps / GitHub or start supporting another CI/CD orchestrator we will update the connector without touching FluxCD/ArgoCD at all. Hence, the GitOps connector.
-
-## GitOps operators
-
-The GitOps Connector supports the following Git Ops operators:
-
-- [FluxCD](https://fluxcd.io)
-- [ArgoCD](https://argoproj.github.io/argo-cd/)
-
-## Git Commit Status Update
-
-The GitOps Connector supports the following Git repositories:
-
-- [Azure Repos](https://azure.microsoft.com/services/devops/repos/)
-- [GitHub](https://github.com)
-
-The connector reports the deployment process updates to the Git repositories with the Git commit status:
-
-|Commit Status in GitHub|Commit Status in Azure Repos|
-|---------|-----------|
-|||
-
-
-In addition to updating the Git commit status in Git repositories, the GitOps connector may optionally notify a list of custom subscribers with a json payload:
-
-|Attribute|Description|Sample|
-|---------|-----------|------|
-|commit_id| Commit Id in Manifests repo|42e2e5af9d49de268cd1fda3587788da4ace418a|
-|status_name| Event context | Sync; Health|
-|state| Event state | Progressing; Failed|
-|message| Full event message | Pending - Fetched revision 42e2e5af9d49de268cd1fda3587788da4ace418a|
-|callback_url| Callback URL with the details | https://github.com/kaizentm/gitops-manifests/commit/42e2e5af9d49de268cd1fda3587788da4ace418a|
-|gitops_operator| GitOps operator (Flux or ArgoCD) | Flux|
-|genre| Message category | GitRepository |
-
-
-Refer to [installation guide](#installation) for the details on configuring a list of custom subscribers.
-
-
-## Notification on Deployment Completion
-
-The GitOps connector analyzes the incoming messages from the GitOps operator and figures out when the deployment is finished, successfully or not. It notifies on this event the CI/CD orchestrator, so the orchestrator can proceed with the CD process.
-
-The notification mechanism on deployment completion varies for different CI/CD orchestrators.
-
-### Azure Pipelines
-
-An Azure CD pipeline is supposed to create a PR to the manifests repo and wait in agentless mode until the PR is merged and the GitOps operator finishes the deployment. It may use PR properties as a storage for the agentless task callback parameters.
-When the deployment is finished, the GitOps connector seeks the PR to the manifest repo that caused the deployment and looks for the PR's properties under the **/callback-task-id** path. It expects to find a json object with the following data:
-
-```
-{"taskid":"$(System.TaskInstanceId)",
- "jobid:"$(System.JobId)",
- "planurl":"$(System.CollectionUri)",
- "planid":"$(System.PlanId)",
- "projectid":"$(System.TeamProjectId)",
- "pr_num":"$(pr_num)"}
-```
-
-The GitOps connector uses this data to provide a callback to the agentless task with the following API:
+Add GitOps Connector repository to Helm repos:
```
-POST {planurl}{projectid}/_apis/distributedtask/hubs/build/plans/{planid}/events?api-version=2.0-preview.1
-payload = {
- 'name': "TaskCompleted",
- 'taskId': {taskid},
- 'jobid': {jobid},
- 'result': "succeeded"/"failed"
-}
+helm repo add gitops-connector https://azure.github.io/gitops-connector/
```
-Refer to [a sample of such agentless task](https://github.com/kaizentm/cloud-native-ops/blob/master/.pipelines/pr-completion-task-template.yaml) for the implementation details.
-
-### GitHub Actions
-
-If the CI/CD orchestrator is GitHub Actions, the GitOps connector sends a [dispatch event](https://docs.github.com/en/rest/reference/repos#create-a-repository-dispatch-event) on the successful deployment completion:
-
-```
-POST /repos/{owner}/{repo}/dispatches
-payload = {
- 'event_type': "sync-success",
- 'client_payload': {'sha': {commmit_id}, //Commit Id in source repo that started the CI/CD process
- 'runid': {github_workflow_run_id} //GitHub Actions Workflow RunId that produced artifacts (e.g. Docker Image Tags)
- }
-```
-
-If the deployment fails, the connector doesn't send a dispatch event to GitHub Actions.
-
-For the implementation details refer to [the CD workflow in this repo](.github/workflows/cd.yaml) that consumes CI artifacts, generates manifests and issues a PR to the manifests repo. Also, look at [the publish workflow](.github/workflows/publish.yaml) which is triggered on the deployment completion by the dispatch event from the GitOps connector.
-
-
-## Installation
-
-### Install GitOps Connector with Helm
-
-Add **kaizentm** repository to Helm repos:
-
-```
-helm repo add kaizentm https://kaizentm.github.io/charts/
-```
-
-
-Prepare **values.yaml** file with the following attributes:
-
-|Attribute|Description|Sample|
-|---------|-----------|------|
-|gitRepositoryType| Git Repository Type (**AZDO** or **GITHUB**)| GITHUB |
-|ciCdOrchestratorType| CI/CD Orchestrator Type (**AZDO** or **GITHUB**)| GITHUB |
-|gitOpsOperatorType| GitOps Operator Type (**FLUX** or **ARGOCD**)| FLUX |
-|gitOpsAppURL| Call back URL from the Commit Status Window| https://github.com/kaizentm/gitops-manifests/commit; https://github.com/microsoft/spektate|
-|orchestratorPAT| GitHub or Azure DevOps personal access token |
-
-
-
-If Git Repository Type is AZDO, add the following attributes:
-|Attribute|Description|Sample|
-|---------|-----------|------|
-|azdoGitOpsRepoName| Azure DevOps Mainifests repository name| gen3-manifest |
-|azdoOrgUrl| Azure DevOps Organization URL| https://dev.azure.com/DataCommons/ProjectDataCommons |
-
-
-
-If CI/CD Orchestrator Type is AZDO, add the following attributes:
-|Attribute|Description|Sample|
-|---------|-----------|------|
-|azdoPrRepoName| Optional. When PRs are not issued to the manifests repo, but to a [separate HLD repo](https://github.com/kaizentm/cloud-native-ops/blob/master/docs/cicd-hld-pipeline.md) | gen3-hld |
-|azdoOrgUrl| Azure DevOps Organization URL| https://dev.azure.com/DataCommons/ProjectDataCommons |
-
-
-
-If Git Repository Type is GITHUB, add the following attributes:
-|Attribute|Description|Sample|
-|---------|-----------|------|
-|gitHubGitOpsManifestsRepoName| GitHub Mainifests repository name| gitops-manifests |
-|gitHubOrgUrl| API url for the GitHub org| https://api.github.com/repos/kaizentm |
-
-
-
-If CI/CD Orchestrator Type is GITHUB, add the following attributes:
-|Attribute|Description|Sample|
-|---------|-----------|------|
-|gitHubGitOpsRepoName| GitHub Actions repository name| gitops-connector |
-|gitHubOrgUrl| API url for the GitHub org| https://api.github.com/repos/kaizentm |
-
-
-
-Optional. If there are custom Git Commit status subscribers:
-|Attribute|Description|Sample|
-|---------|-----------|------|
-|subscribers| List of key:value pairs defining subscriber name and endpoint | subscribers:
spektate: 'http://spektate-server:5000/api/flux'
-
-
-A sample **values.yaml** file for Flux and GitHub might look like this one:
-
-```
-gitRepositoryType: GITHUB
-ciCdOrchestratorType: GITHUB
-gitOpsOperatorType: FLUX
-gitHubGitOpsRepoName: gitops-connector
-gitHubGitOpsManifestsRepoName: gitops-manifests
-gitHubOrgUrl: https://api.github.com/repos/kaizentm
-gitOpsAppURL: https://github.com/kaizentm/gitops-manifests/commit
-orchestratorPAT:
-subscribers:
- spektate: 'http://spektate-server:5000/api/flux'
-```
-
-A sample **values.yaml** file for Flux and Azure DevOps might look like this one:
-
-```
-gitRepositoryType: AZDO
-ciCdOrchestratorType: AZDO
-gitOpsOperatorType: FLUX
-azdoGitOpsRepoName: manifest-repo
-azdoOrgUrl: https://dev.azure.com/MyOrg/MyProject
-azdoPrRepoName: hld-repo
-gitOpsAppURL: https://github.com/microsoft/spektate
-orchestratorPAT:
-subscribers:
- spektate: 'http://spektate-server:5000/api/flux'
-```
-
-Install GitOps connector with the following command:
-
-```
-helm upgrade -i gitops-connector kaizentm/gitops-connector \
---namespace \
---values values.yaml
-
-# Check GitOps connector is up and running:
-kubectl get pods -l=app=gitops-connector -n
-
-# Check the connector logs:
-kubectl logs -l=app=gitops-connector -n -f
-# DEBUG:root:0 subscribers added.
-# INFO:timeloop:Starting Timeloop..
-# [2021-05-25 00:19:23,595] [timeloop] [INFO] Starting Timeloop..
-# [2021-05-25 00:19:23,595] [timeloop] [INFO] Registered job
-# [2021-05-25 00:19:23,596] [timeloop] [INFO] Timeloop now started. Jobs will run based on the interval set
-# INFO:timeloop:Registered job
-# INFO:timeloop:Timeloop now started. Jobs will run based on the interval set
-# INFO:root:Starting commit status thread
-# INFO:root:Starting periodic PR cleanup
-# INFO:root:Finished PR cleanup, sleeping for 30 seconds...
-
-```
-
-### Configure FluxCD to send notifications to GitOps connector
-
-[FluxCD Notification Controller](https://fluxcd.io/docs/components/notification/) sends notifications to GitOps connector on events related to **GitRepository** and **Kustomization** Flux resources. Apply the following yaml to the cluster to subscribe GitOps connector instance on Flux notifications:
-
-```
-apiVersion: notification.toolkit.fluxcd.io/v1beta1
-kind: Alert
-metadata:
- name: gitops-connector
- namespace:
-spec:
- eventSeverity: info
- eventSources:
- - kind: GitRepository
- name:
- - kind: Kustomization
- name:
- providerRef:
- name: gitops-connector
----
-apiVersion: notification.toolkit.fluxcd.io/v1beta1
-kind: Provider
-metadata:
- name: gitops-connector
- namespace:
-spec:
- type: generic
- address: http://gitops-connector:8080/gitopsphase
-```
-
-### Configure ArgoCD to send notifications to GitOps connector
-
-[ArgoCD Notifications](https://argocd-notifications.readthedocs.io/en/stable/) sends updates on every deployment phase. Apply the following yaml to the cluster to subscribe GitOps connector instance on ArgoCD notifications:
-
-```
-apiVersion: v1
-kind: ConfigMap
-metadata:
- name: argocd-notifications-cm
- namespace:
-data:
- config.yaml: |
- triggers:
- - name: sync-operation-failed
- condition: app.status.operationState.phase in ['Error', 'Failed']
- template: sync-operation-status-change
- - name: sync-operation-succeeded
- condition: app.status.operationState.phase in ['Succeeded']
- template: sync-operation-status-change
- - name: sync-operation-running
- condition: app.status.operationState.phase in ['Running']
- template: sync-operation-status-change
- - name: sync-operation-progressing
- condition: app.status.health.status in ['Progressing']
- template: sync-operation-status-change
- - name: sync-operation-healthy
- condition: app.status.health.status in ['Healthy'] && app.status.operationState.phase in ['Succeeded']
- template: sync-operation-status-change
- - name: sync-operation-unhealthy
- condition: app.status.health.status in ['Unknown', 'Suspended', 'Degraded', 'Missing']
- template: sync-operation-status-change
- templates:
- - name: sync-operation-status-change
- webhook:
- test-receiver:
- method: POST
- body: |
- {
- "commitid": "{{.app.status.operationState.operation.sync.revision}}",
- "phase": "{{.app.status.operationState.phase}}",
- "sync_status": "{{.app.status.sync.status}}",
- "health": "{{.app.status.health.status}}",
- "message": "{{.app.status.operationState.message}}",
- "resources": {{toJson .app.status.resources}}
- }
----
-apiVersion: v1
-kind: Secret
-metadata:
- name: argocd-notifications-secret
- namespace:
-stringData:
- notifiers.yaml: |
- webhook:
- - name: test-receiver
- url: http://gitops-connector:8080/gitopsphase
- headers:
- - name: Content-Type
- value: application/json
-type: Opaque
-```
-
-## Contributing
-
-This project welcomes contributions and suggestions. Most contributions require you to agree to a Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us the rights to use your contribution. For details, visit
-
-This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
+- [Install GitOps Connector](https://github.com/azure/gitops-connector#installation)
diff --git a/gitops-connector-1.0.0.tgz b/gitops-connector-1.0.0.tgz
new file mode 100644
index 0000000..568b706
Binary files /dev/null and b/gitops-connector-1.0.0.tgz differ
diff --git a/gitops-connector-1.0.1-115.tgz b/gitops-connector-1.0.1-115.tgz
new file mode 100644
index 0000000..062d79c
Binary files /dev/null and b/gitops-connector-1.0.1-115.tgz differ
diff --git a/gitops-connector-1.0.1-89.tgz b/gitops-connector-1.0.1-89.tgz
new file mode 100644
index 0000000..a381d1a
Binary files /dev/null and b/gitops-connector-1.0.1-89.tgz differ
diff --git a/gitops-connector-1.0.1-90.tgz b/gitops-connector-1.0.1-90.tgz
new file mode 100644
index 0000000..1ec8334
Binary files /dev/null and b/gitops-connector-1.0.1-90.tgz differ
diff --git a/gitops-connector-1.0.1-91.tgz b/gitops-connector-1.0.1-91.tgz
new file mode 100644
index 0000000..7889da9
Binary files /dev/null and b/gitops-connector-1.0.1-91.tgz differ
diff --git a/gitops-connector-1.0.1-93.tgz b/gitops-connector-1.0.1-93.tgz
new file mode 100644
index 0000000..f8bab37
Binary files /dev/null and b/gitops-connector-1.0.1-93.tgz differ
diff --git a/gitops-connector-1.0.1-94.tgz b/gitops-connector-1.0.1-94.tgz
new file mode 100644
index 0000000..3e9a589
Binary files /dev/null and b/gitops-connector-1.0.1-94.tgz differ
diff --git a/gitops-connector-1.0.1-97.tgz b/gitops-connector-1.0.1-97.tgz
new file mode 100644
index 0000000..2359d33
Binary files /dev/null and b/gitops-connector-1.0.1-97.tgz differ
diff --git a/gitops-connector-1.0.1-98.tgz b/gitops-connector-1.0.1-98.tgz
new file mode 100644
index 0000000..b8dd607
Binary files /dev/null and b/gitops-connector-1.0.1-98.tgz differ
diff --git a/gitops-connector-1.1.0.tgz b/gitops-connector-1.1.0.tgz
new file mode 100644
index 0000000..bc7c214
Binary files /dev/null and b/gitops-connector-1.1.0.tgz differ
diff --git a/gitops-connector-1.2.1.tgz b/gitops-connector-1.2.1.tgz
new file mode 100644
index 0000000..dd117a5
Binary files /dev/null and b/gitops-connector-1.2.1.tgz differ
diff --git a/img/README.md b/img/README.md
deleted file mode 100644
index c73caac..0000000
--- a/img/README.md
+++ /dev/null
@@ -1 +0,0 @@
-Images, diagrams used in md documents.
\ No newline at end of file
diff --git a/img/azdo-commit-status.png b/img/azdo-commit-status.png
deleted file mode 100644
index ebb6d46..0000000
Binary files a/img/azdo-commit-status.png and /dev/null differ
diff --git a/img/gh-commit-status.png b/img/gh-commit-status.png
deleted file mode 100644
index 347e86f..0000000
Binary files a/img/gh-commit-status.png and /dev/null differ
diff --git a/img/gitops-connector.png b/img/gitops-connector.png
deleted file mode 100644
index 0f2819f..0000000
Binary files a/img/gitops-connector.png and /dev/null differ
diff --git a/img/src/README.md b/img/src/README.md
deleted file mode 100644
index 625c84a..0000000
--- a/img/src/README.md
+++ /dev/null
@@ -1 +0,0 @@
-[draw.io](https://app.diagrams.net) sources of the diagrams.
\ No newline at end of file
diff --git a/img/src/gitops-connector.drawio b/img/src/gitops-connector.drawio
deleted file mode 100644
index c27cee9..0000000
--- a/img/src/gitops-connector.drawio
+++ /dev/null
@@ -1 +0,0 @@
-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
\ No newline at end of file
diff --git a/index.yaml b/index.yaml
new file mode 100644
index 0000000..f2bd514
--- /dev/null
+++ b/index.yaml
@@ -0,0 +1,124 @@
+apiVersion: v1
+entries:
+ gitops-connector:
+ - apiVersion: v2
+ appVersion: 1.2.1
+ created: "2023-07-10T23:33:05.792967334Z"
+ description: A Helm chart for GitOps connector
+ digest: 2f14f87f29bb57d9764f2f92e21739c92c3528ed1f19a8d2890682f4e8790a08
+ name: gitops-connector
+ type: application
+ urls:
+ - https://azure.github.io/gitops-connector/gitops-connector-1.2.1.tgz
+ version: 1.2.1
+ - apiVersion: v2
+ appVersion: 1.2.0
+ created: "2023-07-10T23:33:05.792673833Z"
+ description: A Helm chart for GitOps connector
+ digest: ddd66e2ad8bc051320a58b897caad9ac7ddd5becc46ee6b1a650e36a28b3e609
+ name: gitops-connector
+ type: application
+ urls:
+ - https://azure.github.io/gitops-connector/gitops-connector-1.2.0.tgz
+ version: 1.2.0
+ - apiVersion: v2
+ appVersion: 1.0.1-115
+ created: "2023-07-10T23:33:05.790330919Z"
+ description: A Helm chart for GitOps connector
+ digest: 012ec3bbad6ee7a67cfe55f366c2e18acc9cc0bef09840ad2877ca9a2a1ea183
+ name: gitops-connector
+ type: application
+ urls:
+ - https://azure.github.io/gitops-connector/gitops-connector-1.0.1-115.tgz
+ version: 1.0.1-115
+ - apiVersion: v2
+ appVersion: 1.0.1-115
+ created: "2023-07-10T23:33:05.792429831Z"
+ description: A Helm chart for GitOps connector
+ digest: 8d319429ba0b2fa3fc6511aa493b88358bd12a1ddb055dc3afab7a802067834b
+ name: gitops-connector
+ type: application
+ urls:
+ - https://azure.github.io/gitops-connector/gitops-connector-1.1.0.tgz
+ version: 1.0.1-115
+ - apiVersion: v2
+ appVersion: 1.0.1-98
+ created: "2023-07-10T23:33:05.792151729Z"
+ description: A Helm chart for GitOps connector
+ digest: 26effd72a6d615b7d0ac63612cf12a852ca0257074ede29134a993b80b81e124
+ name: gitops-connector
+ type: application
+ urls:
+ - https://azure.github.io/gitops-connector/gitops-connector-1.0.1-98.tgz
+ version: 1.0.1-98
+ - apiVersion: v2
+ appVersion: 1.0.1-97
+ created: "2023-07-10T23:33:05.791891728Z"
+ description: A Helm chart for GitOps connector
+ digest: b58c85cc65e2c0fb971ead9212830b3271fc03cdf8d31686cc5fa8cefc50f150
+ name: gitops-connector
+ type: application
+ urls:
+ - https://azure.github.io/gitops-connector/gitops-connector-1.0.1-97.tgz
+ version: 1.0.1-97
+ - apiVersion: v2
+ appVersion: 1.0.1-94
+ created: "2023-07-10T23:33:05.791635426Z"
+ description: A Helm chart for GitOps connector
+ digest: 7fea9b9ac149cdabad30f7dc7d3770a85da81837a0ba50d178c1ce0ec2653844
+ name: gitops-connector
+ type: application
+ urls:
+ - https://azure.github.io/gitops-connector/gitops-connector-1.0.1-94.tgz
+ version: 1.0.1-94
+ - apiVersion: v2
+ appVersion: 1.0.1-93
+ created: "2023-07-10T23:33:05.791373025Z"
+ description: A Helm chart for GitOps connector
+ digest: 2d2110accd27b00ee4eb5795b71beababbe9f3b707ec2b835ad4370de3e2ed7b
+ name: gitops-connector
+ type: application
+ urls:
+ - https://azure.github.io/gitops-connector/gitops-connector-1.0.1-93.tgz
+ version: 1.0.1-93
+ - apiVersion: v2
+ appVersion: 1.0.1-91
+ created: "2023-07-10T23:33:05.791132324Z"
+ description: A Helm chart for GitOps connector
+ digest: f908260a2755c57961e102bbff6b272d4032eae3740428be4424dbf3f0232110
+ name: gitops-connector
+ type: application
+ urls:
+ - https://azure.github.io/gitops-connector/gitops-connector-1.0.1-91.tgz
+ version: 1.0.1-91
+ - apiVersion: v2
+ appVersion: 1.0.1-90
+ created: "2023-07-10T23:33:05.790864322Z"
+ description: A Helm chart for GitOps connector
+ digest: 57f5b8f35867f44ddc3f83bd1a080a70ed7c68c8fae1f3401023199385a6672b
+ name: gitops-connector
+ type: application
+ urls:
+ - https://azure.github.io/gitops-connector/gitops-connector-1.0.1-90.tgz
+ version: 1.0.1-90
+ - apiVersion: v2
+ appVersion: 1.0.1-89
+ created: "2023-07-10T23:33:05.79058592Z"
+ description: A Helm chart for GitOps connector
+ digest: 8a9bd3ac6414345bdf7ec16354787d4d5df2c6caad8a0a3ec02535ee44630056
+ name: gitops-connector
+ type: application
+ urls:
+ - https://azure.github.io/gitops-connector/gitops-connector-1.0.1-89.tgz
+ version: 1.0.1-89
+ - apiVersion: v2
+ appVersion: 1.0.0
+ created: "2023-07-10T23:33:05.790022517Z"
+ description: A Helm chart for GitOps connector
+ digest: 6f48603dc3a95dacdf2ee620d23ee65c6e6a3b1b88d11456bba90ef4a18a0acf
+ name: gitops-connector
+ type: application
+ urls:
+ - https://azure.github.io/gitops-connector/gitops-connector-1.0.0.tgz
+ version: 1.0.0
+generated: "2023-07-10T23:33:05.789550114Z"
diff --git a/internal/setup/flux/README.md b/internal/setup/flux/README.md
deleted file mode 100644
index 4f16b84..0000000
--- a/internal/setup/flux/README.md
+++ /dev/null
@@ -1 +0,0 @@
-A set of Flux resources to setup GitOps connector development environment. It's supposed to be used by the maintainers of this repository.
\ No newline at end of file
diff --git a/internal/setup/flux/flux-notifications.yaml b/internal/setup/flux/flux-notifications.yaml
deleted file mode 100644
index 89d0262..0000000
--- a/internal/setup/flux/flux-notifications.yaml
+++ /dev/null
@@ -1,24 +0,0 @@
-apiVersion: notification.toolkit.fluxcd.io/v1beta1
-kind: Alert
-metadata:
- name: gitops-connector
- namespace: flux-system
-spec:
- eventSeverity: info
- eventSources:
- - kind: GitRepository
- name: gitops-connector-dev
- - kind: Kustomization
- name: gitops-connector-dev
- providerRef:
- name: gitops-connector
----
-apiVersion: notification.toolkit.fluxcd.io/v1beta1
-kind: Provider
-metadata:
- name: gitops-connector
- namespace: flux-system
-spec:
- type: generic
- address: http://gitops-connector:8080/gitopsphase
-
diff --git a/internal/setup/flux/prod-helm-release.yaml b/internal/setup/flux/prod-helm-release.yaml
deleted file mode 100644
index 81b6def..0000000
--- a/internal/setup/flux/prod-helm-release.yaml
+++ /dev/null
@@ -1,34 +0,0 @@
-apiVersion: source.toolkit.fluxcd.io/v1beta1
-kind: HelmRepository
-metadata:
- name: kaizentm
- namespace: flux-system
-spec:
- url: https://kaizentm.github.io/charts/
- interval: 1m
----
-apiVersion: helm.toolkit.fluxcd.io/v2beta1
-kind: HelmRelease
-metadata:
- name: gitops-connector
- namespace: flux-system
-spec:
- chart:
- spec:
- chart: gitops-connector
- version: ">=1.x.x-0"
- sourceRef:
- kind: HelmRepository
- name: kaizentm
- namespace: flux-system
- interval: 2m
- interval: 1m
- values:
- gitRepositoryType: GITHUB
- ciCdOrchestratorType: GITHUB
- gitOpsOperatorType: FLUX
- gitHubGitOpsRepoName: gitops-connector
- gitHubGitOpsManifestsRepoName: gitops-manifests
- gitHubOrgUrl: https://api.github.com/repos/kaizentm
- gitOpsAppURL: https://github.com/kaizentm/gitops-manifests/commit
- orchestratorPAT:
diff --git a/internal/setup/flux/sync.yaml b/internal/setup/flux/sync.yaml
deleted file mode 100644
index aabd400..0000000
--- a/internal/setup/flux/sync.yaml
+++ /dev/null
@@ -1,31 +0,0 @@
----
-apiVersion: source.toolkit.fluxcd.io/v1beta1
-kind: GitRepository
-metadata:
- name: gitops-connector-dev
- namespace: flux-system
-spec:
- interval: 30s
- ref:
- branch: dev
- timeout: 20s
- url: https://github.com/kaizentm/gitops-manifests
----
-apiVersion: kustomize.toolkit.fluxcd.io/v1beta1
-kind: Kustomization
-metadata:
- name: gitops-connector-dev
- namespace: flux-system
-spec:
- targetNamespace: dev
- interval: 0m10s
- path: ./gitops-connector/manifest
- prune: false
- healthChecks:
- - kind: Deployment
- name: gitops-connector
- namespace: dev
- sourceRef:
- kind: GitRepository
- name: gitops-connector-dev
-
diff --git a/manifests/helm/Chart.yaml b/manifests/helm/Chart.yaml
deleted file mode 100644
index d15b7e8..0000000
--- a/manifests/helm/Chart.yaml
+++ /dev/null
@@ -1,10 +0,0 @@
-apiVersion: v2
-name: gitops-connector
-description: A Helm chart for GitOps connector
-
-type: application
-
-version: $APP_BUILD_VERSION
-
-appVersion: $APP_BUILD_VERSION
-
diff --git a/manifests/helm/README.md b/manifests/helm/README.md
deleted file mode 100644
index 26f5e4f..0000000
--- a/manifests/helm/README.md
+++ /dev/null
@@ -1 +0,0 @@
-GitOps connector Helm Chart template.
\ No newline at end of file
diff --git a/manifests/helm/templates/deployment.yaml b/manifests/helm/templates/deployment.yaml
deleted file mode 100644
index f473a91..0000000
--- a/manifests/helm/templates/deployment.yaml
+++ /dev/null
@@ -1,135 +0,0 @@
-apiVersion: v1
-kind: ConfigMap
-metadata:
- name: gitops-connector-cm
-data:
- GIT_REPOSITORY_TYPE: {{ required "Provide a value for gitRepositoryType" .Values.gitRepositoryType}}
- CICD_ORCHESTRATOR_TYPE: {{ required "Provide a value for ciCdOrchestratorType" .Values.ciCdOrchestratorType}}
- GITOPS_OPERATOR_TYPE: {{ required "Provide a value for gitOpsOperatorType" .Values.gitOpsOperatorType}}
- GITOPS_APP_URL: {{ required "Provide a value for gitOpsAppURL" .Values.gitOpsAppURL}}
-
- {{- if eq .Values.gitRepositoryType "AZDO"}}
- AZDO_GITOPS_REPO_NAME: {{ required "Provide a value for azdoGitOpsRepoName" .Values.azdoGitOpsRepoName}}
- {{- end }}
-
- {{- if and (eq .Values.ciCdOrchestratorType "AZDO") (.Values.azdoPrRepoName)}}
- AZDO_PR_REPO_NAME: {{ .Values.azdoPrRepoName}}
- {{- end }}
-
- {{- if or (eq .Values.gitRepositoryType "AZDO") (eq .Values.ciCdOrchestratorType "AZDO")}}
- AZDO_ORG_URL: {{ required "Provide a value for azdoOrgUrl" .Values.azdoOrgUrl}}
- {{- end }}
-
-
- {{- if eq .Values.ciCdOrchestratorType "GITHUB"}}
- GITHUB_GITOPS_REPO_NAME: {{ required "Provide a value for gitHubGitOpsRepoName" .Values.gitHubGitOpsRepoName}}
- {{- end }}
-
- {{- if eq .Values.gitRepositoryType "GITHUB"}}
- GITHUB_GITOPS_MANIFEST_REPO_NAME: {{ required "Provide a value for gitHubGitOpsManifestsRepoName" .Values.gitHubGitOpsManifestsRepoName}}
- {{- end }}
-
- {{- if or (eq .Values.gitRepositoryType "GITHUB") (eq .Values.ciCdOrchestratorType "GITHUB")}}
- GITHUB_ORG_URL: {{ required "Provide a value for gitHubOrgUrl" .Values.gitHubOrgUrl}}
- {{- end }}
----
-apiVersion: apps/v1
-kind: Deployment
-metadata:
- name: gitops-connector
- labels:
- app: gitops-connector
-spec:
- replicas: 1
- selector:
- matchLabels:
- app: gitops-connector
- template:
- metadata:
- labels:
- app: gitops-connector
- spec:
- containers:
- - name: connector
- image: {{ .Values.imageName}}:{{ .Values.imageTag}}
- env:
- - name: GIT_REPOSITORY_TYPE
- valueFrom:
- configMapKeyRef:
- name: gitops-connector-cm
- key: GIT_REPOSITORY_TYPE
- - name: CICD_ORCHESTRATOR_TYPE
- valueFrom:
- configMapKeyRef:
- name: gitops-connector-cm
- key: CICD_ORCHESTRATOR_TYPE
- - name: GITOPS_OPERATOR_TYPE
- valueFrom:
- configMapKeyRef:
- name: gitops-connector-cm
- key: GITOPS_OPERATOR_TYPE
- - name: GITOPS_APP_URL
- valueFrom:
- configMapKeyRef:
- name: gitops-connector-cm
- key: GITOPS_APP_URL
- {{- if eq .Values.gitRepositoryType "AZDO"}}
- - name: AZDO_GITOPS_REPO_NAME
- valueFrom:
- configMapKeyRef:
- name: gitops-connector-cm
- key: AZDO_GITOPS_REPO_NAME
- {{- end}}
- {{- if and (eq .Values.ciCdOrchestratorType "AZDO") (.Values.azdoPrRepoName)}}
- - name: AZDO_PR_REPO_NAME
- valueFrom:
- configMapKeyRef:
- name: gitops-connector-cm
- key: AZDO_PR_REPO_NAME
- {{- end}}
- {{- if or (eq .Values.gitRepositoryType "AZDO") (eq .Values.ciCdOrchestratorType "AZDO")}}
- - name: AZDO_ORG_URL
- valueFrom:
- configMapKeyRef:
- name: gitops-connector-cm
- key: AZDO_ORG_URL
- {{- end}}
- {{- if eq .Values.ciCdOrchestratorType "GITHUB"}}
- - name: GITHUB_GITOPS_REPO_NAME
- valueFrom:
- configMapKeyRef:
- name: gitops-connector-cm
- key: GITHUB_GITOPS_REPO_NAME
- {{- end}}
- {{- if eq .Values.gitRepositoryType "GITHUB"}}
- - name: GITHUB_GITOPS_MANIFEST_REPO_NAME
- valueFrom:
- configMapKeyRef:
- name: gitops-connector-cm
- key: GITHUB_GITOPS_MANIFEST_REPO_NAME
- {{- end}}
- {{- if or (eq .Values.gitRepositoryType "GITHUB") (eq .Values.ciCdOrchestratorType "GITHUB")}}
- - name: GITHUB_ORG_URL
- valueFrom:
- configMapKeyRef:
- name: gitops-connector-cm
- key: GITHUB_ORG_URL
- {{- end}}
- - name: PAT
- valueFrom:
- secretKeyRef:
- name: gitops-connector-secret
- key: PAT
- imagePullPolicy: Always
- ports:
- - name: http
- containerPort: {{ .Values.containerPort}}
-{{ if .Values.subscribers }}
- volumeMounts:
- - name: subscribers
- mountPath: /subscribers
- volumes:
- - name: subscribers
- configMap:
- name: gitops-connector-subscribers-config
-{{ end }}
diff --git a/manifests/helm/templates/secret.yaml b/manifests/helm/templates/secret.yaml
deleted file mode 100644
index f55b39a..0000000
--- a/manifests/helm/templates/secret.yaml
+++ /dev/null
@@ -1,9 +0,0 @@
-{{ if .Values.orchestratorPAT }}
-apiVersion: v1
-kind: Secret
-metadata:
- name: gitops-connector-secret
-stringData:
- PAT: {{ .Values.orchestratorPAT}}
-type: Opaque
-{{ end }}
\ No newline at end of file
diff --git a/manifests/helm/templates/service.yaml b/manifests/helm/templates/service.yaml
deleted file mode 100644
index ef16cf3..0000000
--- a/manifests/helm/templates/service.yaml
+++ /dev/null
@@ -1,10 +0,0 @@
-apiVersion: v1
-kind: Service
-metadata:
- name: gitops-connector
-spec:
- selector:
- app: gitops-connector
- ports:
- - port: {{ .Values.containerPort}}
- targetPort: {{ .Values.containerPort}}
diff --git a/manifests/helm/templates/subscribers.yaml b/manifests/helm/templates/subscribers.yaml
deleted file mode 100644
index ca6e982..0000000
--- a/manifests/helm/templates/subscribers.yaml
+++ /dev/null
@@ -1,10 +0,0 @@
-{{ if .Values.subscribers }}
-apiVersion: v1
-kind: ConfigMap
-metadata:
- name: gitops-connector-subscribers-config
-data:
- {{- range $key, $val := .Values.subscribers }}
- {{ $key }}: {{ $val | quote }}
- {{- end }}
-{{ end }}
diff --git a/manifests/helm/values.yaml b/manifests/helm/values.yaml
deleted file mode 100644
index d244c05..0000000
--- a/manifests/helm/values.yaml
+++ /dev/null
@@ -1,21 +0,0 @@
-imageName: kaizentm/gitops-connector
-imageTag: $IMAGE_TAG
-containerPort: 8080
-
-gitRepositoryType: $GIT_REPOSITORY_TYPE
-ciCdOrchestratorType: $CICD_ORCHESTRATOR_TYPE
-gitOpsOperatorType: $GITOPS_OPERATOR_TYPE
-gitOpsAppURL: $GITOPS_APP_URL
-azdoGitOpsRepoName: $AZDO_GITOPS_REPO_NAME
-azdoPrRepoName: $AZDO_PR_REPO_NAME
-azdoOrgUrl: $AZDO_ORG_URL
-orchestratorPAT: $ORCHESTRATOR_PAT
-
-gitHubGitOpsRepoName: $GITHUB_GITOPS_REPO_NAME
-gitHubGitOpsManifestsRepoName: $GITHUB_GITOPS_MANIFEST_REPO_NAME
-gitHubOrgUrl: $GITHUB_ORG_URL
-
-# Optional list of subscriber endpoints to send raw JSON to
-#subscribers:
-# sub1: http://localhost:8080/gitopsphase
-# sub2: http://127.0.0.1:1234/
diff --git a/src/Dockerfile b/src/Dockerfile
deleted file mode 100644
index 0112a29..0000000
--- a/src/Dockerfile
+++ /dev/null
@@ -1,14 +0,0 @@
-FROM python:3.9-slim
-
-ENV APP_HOME /app
-ENV WORKERS 1
-ENV THREADS 1
-ENV PREDICTIVE_UNIT_SERVICE_PORT 8080
-WORKDIR $APP_HOME
-COPY . ./
-ENV PYTHONUNBUFFERED=1
-
-
-RUN pip install --no-cache-dir -r ./requirements.txt
-
-CMD ["sh","-c","gunicorn --bind 0.0.0.0:$PREDICTIVE_UNIT_SERVICE_PORT --workers $WORKERS --threads $THREADS gitops_event_handler"]
\ No newline at end of file
diff --git a/src/README.md b/src/README.md
deleted file mode 100644
index 1a2e136..0000000
--- a/src/README.md
+++ /dev/null
@@ -1 +0,0 @@
-GitOps Connector source code.
\ No newline at end of file
diff --git a/src/clients/azdo_client.py b/src/clients/azdo_client.py
deleted file mode 100644
index 4088742..0000000
--- a/src/clients/azdo_client.py
+++ /dev/null
@@ -1,22 +0,0 @@
-# Copyright (c) Microsoft Corporation.
-# Licensed under the MIT License.
-
-import base64
-import utils
-
-
-class AzdoClient:
-
- def __init__(self):
- # https://dev.azure.com/csedevops/GitOps
- self.org_url = utils.getenv("AZDO_ORG_URL")
- # token is supposed to be stored in a secret without any transformations
- token = base64.b64encode(f':{utils.getenv("PAT")}'.encode("ascii")).decode("ascii")
- self.headers = {'authorization': f'Basic {token}',
- 'Content-Type': 'application/json'}
-
- def get_rest_api_headers(self) -> dict:
- return self.headers
-
- def get_rest_api_url(self) -> str:
- return self.org_url
diff --git a/src/clients/github_client.py b/src/clients/github_client.py
deleted file mode 100644
index 762547d..0000000
--- a/src/clients/github_client.py
+++ /dev/null
@@ -1,19 +0,0 @@
-# Copyright (c) Microsoft Corporation.
-# Licensed under the MIT License.
-
-import utils
-
-
-class GitHubClient:
-
- def __init__(self):
- self.org_url = utils.getenv("GITHUB_ORG_URL") # https://api.github.com/repos/kaizentm
- # token is supposed to be stored in a secret without any transformations
- self.token = utils.getenv("PAT")
- self.headers = {'Authorization': f'token {self.token}'}
-
- def get_rest_api_headers(self) -> dict:
- return self.headers
-
- def get_rest_api_url(self) -> str:
- return self.org_url
diff --git a/src/gitops_connector.py b/src/gitops_connector.py
deleted file mode 100644
index 356226f..0000000
--- a/src/gitops_connector.py
+++ /dev/null
@@ -1,82 +0,0 @@
-# Copyright (c) Microsoft Corporation.
-# Licensed under the MIT License.
-
-import logging
-from queue import PriorityQueue
-
-from operators.gitops_operator_factory import GitopsOperatorFactory
-from repositories.git_repository_factory import GitRepositoryFactory
-from repositories.raw_subscriber import RawSubscriberFactory
-from orchestrators.cicd_orchestrator_factory import CicdOrchestratorFactory
-
-
-# Instance is shared across threads.
-class GitopsConnector:
-
- def __init__(self):
- self._gitops_operator = GitopsOperatorFactory.new_gitops_operator()
- self._git_repository = GitRepositoryFactory.new_git_repository()
- self._cicd_orchestrator = CicdOrchestratorFactory.new_cicd_orchestrator(self._git_repository)
-
- # Subscribers that take unprocessed JSON, forwarded from the notifications
- self._raw_subscribers = RawSubscriberFactory.new_raw_subscribers()
-
- # Commit status notification queue
- self._global_message_queue = PriorityQueue()
-
- def process_gitops_phase(self, phase_data, req_time):
- if self._gitops_operator.is_supported_message(phase_data):
- self._queue_commit_statuses(phase_data, req_time)
- self._notify_orchestrator(phase_data)
- else:
- logging.debug(f'Message is not supported: {phase_data}')
-
- def _queue_commit_statuses(self, phase_data, req_time):
- commit_statuses = self._gitops_operator.extract_commit_statuses(phase_data)
- for commit_status in commit_statuses:
- self._global_message_queue.put(item=(req_time, commit_status))
-
- def _notify_orchestrator(self, phase_data):
- is_finished, is_successful = self._gitops_operator.is_finished(phase_data)
- if is_finished:
- commit_id = self._gitops_operator.get_commit_id(phase_data)
- self._cicd_orchestrator.notify_on_deployment_completion(commit_id, is_successful)
-
- # Entrypoint for the periodic task to search for abandoned PRs linked to
- # agentless tasks.
- def notify_abandoned_pr_tasks(self):
- try:
- self._cicd_orchestrator.notify_abandoned_pr_tasks()
- except Exception as e:
- logging.error(f'Failed to notify abandoned PRs: {e}')
-
- # Entrypoint for the commit status thread.
- # The thread waits for items in the priority queue and sends the messages
- # in the order of the request received time.
- def drain_commit_status_queue(self):
- while (True):
- try:
- # Blocking get
- commit_status = self._global_message_queue.get()
-
- if not commit_status:
- break
-
- # Queue entry is (received time, commit_status)
- commit_status = commit_status[1]
-
- # Handling an exception as it crashes the draining thread
- try:
- self._git_repository.post_commit_status(commit_status)
-
- for subscriber in self._raw_subscribers:
- subscriber.post_commit_status(commit_status)
- except Exception as e:
- logging.error(f'Failed to update GitCommit Status: {e}')
-
- except Exception as e:
- logging.error(f'Unexpected exception in the message queue draining thread: {e}')
-
-
-if __name__ == "__main__":
- git_ops_connector = GitopsConnector()
diff --git a/src/gitops_event_handler.py b/src/gitops_event_handler.py
deleted file mode 100644
index 2239580..0000000
--- a/src/gitops_event_handler.py
+++ /dev/null
@@ -1,67 +0,0 @@
-# Copyright (c) Microsoft Corporation.
-# Licensed under the MIT License.
-
-from flask import Flask, request
-import logging
-from timeloop import Timeloop
-from datetime import timedelta
-import atexit
-import time
-from threading import Thread
-from gitops_connector import GitopsConnector
-
-# Time in seconds between background PR cleanup jobs
-PR_CLEANUP_INTERVAL = 1 * 30
-DISABLE_POLLING_PR_TASK = False
-
-logging.basicConfig(level=logging.DEBUG)
-
-application = Flask(__name__)
-
-gitops_connector = GitopsConnector()
-
-
-@application.route("/gitopsphase", methods=['POST'])
-def gitopsphase():
- # Use per process timer to stash the time we got the request
- req_time = time.monotonic_ns()
-
- payload = request.get_json()
-
- logging.debug(f'GitOps phase: {payload}')
-
- gitops_connector.process_gitops_phase(payload, req_time)
-
- return f'GitOps phase: {payload}', 200
-
-
-# Periodic PR cleanup task
-cleanup_task = Timeloop()
-
-
-@cleanup_task.job(interval=timedelta(seconds=PR_CLEANUP_INTERVAL))
-def pr_polling_thread_worker():
- logging.info("Starting periodic PR cleanup")
- gitops_connector.notify_abandoned_pr_tasks()
- logging.info(f'Finished PR cleanup, sleeping for {PR_CLEANUP_INTERVAL} seconds...')
-
-
-# Git status queue drain task
-def init_commit_status_thread():
- logging.info("Starting commit status thread")
- status_thread = Thread(target=gitops_connector.drain_commit_status_queue)
- status_thread.start()
-
-
-def interrupt():
- if not DISABLE_POLLING_PR_TASK:
- cleanup_task.stop()
-
-
-if not DISABLE_POLLING_PR_TASK:
- cleanup_task.start()
- init_commit_status_thread()
- atexit.register(interrupt)
-
-if __name__ == "__main__":
- application.run(host='0.0.0.0')
diff --git a/src/operators/argo_gitops_operator.py b/src/operators/argo_gitops_operator.py
deleted file mode 100644
index 45d1482..0000000
--- a/src/operators/argo_gitops_operator.py
+++ /dev/null
@@ -1,68 +0,0 @@
-# Copyright (c) Microsoft Corporation.
-# Licensed under the MIT License.
-
-from operators.gitops_operator import GitopsOperatorInterface
-from operators.git_commit_status import GitCommitStatus
-
-
-class ArgoGitopsOperator(GitopsOperatorInterface):
-
- def extract_commit_statuses(self, phase_data):
- commit_statuses = []
-
- commit_id = self.get_commit_id(phase_data)
- phase_status, sync_status, health_status = self._get_statuses(phase_data)
-
- phase_status = self._new_git_commit_status(
- commit_id=commit_id,
- status_name='Phase',
- state=phase_status,
- message=phase_data['phase'] + ": " + phase_data['message'])
- commit_statuses.append(phase_status)
-
- (health_summary, sync_summary) = self._get_deployment_status_summary(phase_data['resources'])
-
- sync_status = self._new_git_commit_status(
- commit_id=commit_id,
- status_name='Sync',
- state=sync_status,
- message=sync_summary)
- commit_statuses.append(sync_status)
-
- health_status = self._new_git_commit_status(
- commit_id=commit_id,
- status_name='Health',
- state=health_status,
- message=health_summary)
- commit_statuses.append(health_status)
-
- return commit_statuses
-
- def is_finished(self, phase_data):
- phase_status, _, health_status = self._get_statuses(phase_data)
-
- is_finished = \
- phase_status != 'Inconclusive' \
- and phase_status != 'Running' \
- and health_status != 'Progressing'
-
- is_successful = phase_status == 'Succeeded' and health_status == 'Healthy'
-
- return is_finished, is_successful
-
- def get_commit_id(self, phase_data) -> str:
- return phase_data['commitid']
-
- def _get_statuses(self, phase_data):
- return phase_data['phase'], phase_data['sync_status'], phase_data['health']
-
- def _new_git_commit_status(self, commit_id, status_name, state, message: str):
- return GitCommitStatus(commit_id=commit_id,
- status_name=status_name,
- state=state,
- message=message,
- callback_url=self.callback_url,
- gitops_operator='ArgoCD')
-
- def is_supported_message(self, phase_data) -> bool:
- return True
diff --git a/src/operators/flux_gitops_operator.py b/src/operators/flux_gitops_operator.py
deleted file mode 100644
index 2b77c84..0000000
--- a/src/operators/flux_gitops_operator.py
+++ /dev/null
@@ -1,207 +0,0 @@
-# Copyright (c) Microsoft Corporation.
-# Licensed under the MIT License.
-
-from collections import defaultdict
-import logging
-from operators.gitops_operator import GitopsOperatorInterface
-from operators.git_commit_status import GitCommitStatus
-
-KUSTOMIZATION_PHASE = "Kustomization"
-PROGRESSING_STATE = "Progressing"
-HEALTH_CHECK_FAILED_STATE = "HealthCheckFailed"
-
-
-class FluxGitopsOperator(GitopsOperatorInterface):
-
- def extract_commit_statuses(self, phase_data):
- commit_statuses = []
-
- commit_id = self.get_commit_id(phase_data)
-
- reason_state = phase_data['reason']
- reason_message = self._map_reason_to_description(
- reason_state,
- phase_data['message'])
- kind = self._get_message_kind(phase_data)
-
- # Generic status message regardless of kind.
- status = self._new_git_commit_status(
- commit_id=commit_id,
- status_name='Status',
- state=reason_state,
- message=reason_message,
- kind=kind)
- commit_statuses.append(status)
-
- # For Kustomization we have more detailed data to parse in addition to status.
- if self._get_message_kind(phase_data) == KUSTOMIZATION_PHASE:
- if phase_data['reason'] == PROGRESSING_STATE:
- self._add_progression_summary(phase_data, commit_id, commit_statuses, kind)
- # For Progressive state adding a generic message again so the overall Status will be "pending"
- # (Bug in AzDO)
- status = self._new_git_commit_status(
- commit_id=commit_id,
- status_name='Status',
- state=reason_state,
- message=reason_message,
- kind=kind)
- commit_statuses.append(status)
- elif phase_data['reason'] == HEALTH_CHECK_FAILED_STATE:
- self._add_health_check_summary(phase_data, commit_id, commit_statuses, reason_message, kind)
-
- return commit_statuses
-
- def _add_progression_summary(self, phase_data, commit_id, commit_statuses, kind):
- progression_summary = self._parse_kustomization_progression_summary(phase_data)
- if progression_summary:
- for (resource_name, status_msg) in progression_summary.items():
- status = self._new_git_commit_status(
- commit_id=commit_id,
- status_name=resource_name,
- # As far as the Kustomize controller is concerned, these are finished
- # before reconciliation starts. We don't want this to affect the overall
- # status, so map to the relevant N/A status in the Git repo provider.
- state="NotApplicable",
- message=status_msg,
- kind=kind)
- commit_statuses.append(status)
-
- def _add_health_check_summary(self, phase_data, commit_id, commit_statuses, reason_message, kind):
- health_check_summary = self._parse_health_check_summary(phase_data)
- if health_check_summary:
- for resource_name in health_check_summary:
- status = self._new_git_commit_status(
- commit_id=commit_id,
- status_name=resource_name,
- state=HEALTH_CHECK_FAILED_STATE,
- message=reason_message,
- kind=kind)
- commit_statuses.append(status)
-
- def _parse_health_check_summary(self, phase_data):
- raw_message = phase_data['message']
- resources = []
- resources_array_start = raw_message.index("[")
- resources_array_end = raw_message.index("]")
- if resources_array_start > 0 and resources_array_end > 0 and resources_array_end > resources_array_start:
- resources_string = raw_message[resources_array_start + 1: resources_array_end - 1]
- if resources_string:
- resources = [r.strip() for r in resources_string.split(", ")]
-
- if not resources:
- resources.append(raw_message)
- return resources
-
- def _new_git_commit_status(self, commit_id, status_name, state, message, kind):
- return GitCommitStatus(
- commit_id=commit_id,
- status_name=status_name,
- state=state,
- message=message,
- callback_url=self.callback_url,
- gitops_operator='Flux',
- genre=kind)
-
- def is_finished(self, phase_data):
- status = phase_data['reason']
- kind = self._get_message_kind(phase_data)
-
- is_finished = kind == "Kustomization" and status != 'Progressing'
-
- is_successful = status == 'ReconciliationSucceeded'
-
- return is_finished, is_successful
-
- def get_commit_id(self, phase_data) -> str:
- if self._get_message_kind(phase_data) == "Kustomization":
- revision = phase_data['metadata']['revision']
- elif self._get_message_kind(phase_data) == 'GitRepository':
- # 'Fetched revision: user/blah/githash'
- revision = phase_data['message']
- revisionArray = revision.split('/')
- commit_id = revisionArray[-1]
-
- return commit_id
-
- def is_supported_message(self, phase_data) -> bool:
- kind = self._get_message_kind(phase_data)
- logging.debug(f'Kind: {kind}')
-
- return (kind == 'Kustomization' or kind == 'GitRepository')
-
- def _get_message_kind(self, phase_data) -> str:
- return phase_data['involvedObject']['kind']
-
- def _map_reason_to_description(self, reason, original_message):
- # Explicitly handle all statuses so we make sure we don't silently miss any.
- reason_description_map = {
- "ReconciliationSucceeded": original_message,
- "ReconciliationFailed": original_message,
- "Progressing": "Reconcilation underway.",
- "DependencyNotReady": original_message,
- "PruneFailed": original_message,
- "ArtifactFailed": original_message,
- "BuildFailed": original_message,
- "HealthCheckFailed": "Health Check Failed",
- "ValidationFailed": "Manifests validation failed.",
- "info": original_message,
- "error": original_message
- }
- return reason_description_map[reason]
-
- # Build and return an array of progression summaries.
- # For example, ["service: 6 configured"]
- def _parse_kustomization_progression_summary(self, phase_data):
- if phase_data['reason'] != "Progressing":
- return []
-
- # The message contains kubectl output of newline separated
- # resources and states. May contain a trailing newline.
- raw_message = phase_data['message']
- entries = raw_message.rstrip().split("\n")
-
- if not entries or len(entries) == 0:
- return
-
- # Iterate on the entries and build our map.
- # Raw entry example:
- # deployment.apps/abc configured
- # deployment.apps/def configured
- status_map = defaultdict(lambda: defaultdict(int))
- warning_count = 0
- status_arr = {}
- try:
- for entry in entries:
- # Split into resource and status
- if entry.startswith("Warning"):
- warning_count += 1
- continue
-
- entry_tuple = entry.split(" ")
- if len(entry_tuple) != 2:
- raise RuntimeError("Parsing error")
- (resource, status) = entry_tuple
-
- # Disregard the resource name
- (resource_type, _, _) = resource.partition("/")
- status_map[resource_type][status] += 1
-
- # Build the status string array
- for (resource_name, statuses) in status_map.items():
- # service:
- summary = ""
- first = True
- for (status_name, status_count) in statuses.items():
- if not first:
- summary += ", "
- first = False
- # E.g. "5 configured"
- summary += f'{status_count} {status_name}'
- status_arr[resource_name] = summary
-
- if warning_count > 0:
- status_arr['warnings'] = f'Warnings: {warning_count}'
- except RuntimeError:
- status_arr['Info'] = raw_message
-
- return status_arr
diff --git a/src/operators/git_commit_status.py b/src/operators/git_commit_status.py
deleted file mode 100644
index bb08066..0000000
--- a/src/operators/git_commit_status.py
+++ /dev/null
@@ -1,19 +0,0 @@
-# Copyright (c) Microsoft Corporation.
-# Licensed under the MIT License.
-
-from dataclasses import dataclass
-
-
-@dataclass
-class GitCommitStatus:
- commit_id: str
- status_name: str
- state: str
- message: str
- callback_url: str
- gitops_operator: str
- genre: str
-
- def __lt__(self, other):
- # Status messages need to come last.
- return self.genre != "Status"
diff --git a/src/operators/gitops_operator.py b/src/operators/gitops_operator.py
deleted file mode 100644
index 1554d46..0000000
--- a/src/operators/gitops_operator.py
+++ /dev/null
@@ -1,27 +0,0 @@
-# Copyright (c) Microsoft Corporation.
-# Licensed under the MIT License.
-
-import utils
-from abc import ABC, abstractmethod
-
-
-class GitopsOperatorInterface(ABC):
-
- def __init__(self):
- self.callback_url = utils.getenv("GITOPS_APP_URL")
-
- @abstractmethod
- def extract_commit_statuses(self, phase_data):
- pass
-
- @abstractmethod
- def is_finished(self, phase_data) -> bool:
- pass
-
- @abstractmethod
- def get_commit_id(self, phase_data) -> str:
- pass
-
- @abstractmethod
- def is_supported_message(self, phase_data) -> bool:
- pass
diff --git a/src/operators/gitops_operator_factory.py b/src/operators/gitops_operator_factory.py
deleted file mode 100644
index f7d114f..0000000
--- a/src/operators/gitops_operator_factory.py
+++ /dev/null
@@ -1,24 +0,0 @@
-# Copyright (c) Microsoft Corporation.
-# Licensed under the MIT License.
-
-import utils
-from operators.argo_gitops_operator import ArgoGitopsOperator
-from operators.flux_gitops_operator import FluxGitopsOperator
-from operators.gitops_operator import GitopsOperatorInterface
-
-FLUX_TYPE = "FLUX"
-ARGOCD_TYPE = "ARGOCD"
-
-
-class GitopsOperatorFactory:
-
- @staticmethod
- def new_gitops_operator() -> GitopsOperatorInterface:
- gitops_operator_type = utils.getenv("GITOPS_OPERATOR_TYPE", FLUX_TYPE)
-
- if gitops_operator_type == FLUX_TYPE:
- return FluxGitopsOperator()
- elif gitops_operator_type == ARGOCD_TYPE:
- return ArgoGitopsOperator()
- else:
- raise NotImplementedError(f'The GitOps operator {gitops_operator_type} is not supported')
diff --git a/src/orchestrators/azdo_cicd_orchestrator.py b/src/orchestrators/azdo_cicd_orchestrator.py
deleted file mode 100644
index f3662e8..0000000
--- a/src/orchestrators/azdo_cicd_orchestrator.py
+++ /dev/null
@@ -1,122 +0,0 @@
-# Copyright (c) Microsoft Corporation.
-# Licensed under the MIT License.
-
-import logging
-import requests
-from datetime import datetime, timedelta
-import dateutil.parser
-from orchestrators.cicd_orchestrator import CicdOrchestratorInterface
-from repositories.git_repository import GitRepositoryInterface
-from clients.azdo_client import AzdoClient
-
-# Callback task timeout in minutes. PRs abandoned before this time will not be processed.
-MAX_TASK_TIMEOUT = 72 * 60
-TASK_CUTOFF_DURATION = timedelta(minutes=MAX_TASK_TIMEOUT)
-
-
-class AzdoCicdOrchestrator(CicdOrchestratorInterface):
-
- def __init__(self, git_repository: GitRepositoryInterface):
- super().__init__(git_repository)
- self.azdo_client = AzdoClient()
- self.headers = self.azdo_client.get_rest_api_headers()
-
- def notify_on_deployment_completion(self, commit_id, is_successful):
- pr_num = self.git_repository.get_pr_num(commit_id)
- if pr_num:
- self._update_pr_task(is_successful, pr_num)
-
- # Update the Azure Pipeline task waiting for the PR to complete.
- # is_alive: If true, the PR is active and absence of task data is an error.
- def _update_pr_task(self, is_successful, pr_num, is_alive=True):
- pr_task = self._get_pr_task_data(pr_num, is_alive)
- if not pr_task:
- if is_alive:
- logging.error(f'PR {pr_num} has no metadata! Cannot complete task callback.')
- return False
- logging.info(f'PR {pr_num}: Rollout {is_successful}, attempting task completion callback...')
-
- if is_successful:
- state = 'succeeded'
- else:
- state = 'failed'
-
- # The build task may have been cancelled, timed out, etc.
- # Working with the plan in this state can cause 500 errors.
- # Finish gracefully so ArgoCD doesn't keep calling us.
- if self._plan_already_completed(pr_task):
- return False
-
- planurl = pr_task['planurl']
- projectid = pr_task['projectid']
- planid = pr_task['planid']
- url = f'{planurl}{projectid}/_apis/distributedtask/hubs/build/plans/{planid}/events?api-version=2.0-preview.1'
- data = {
- 'name': "TaskCompleted",
- 'taskId': pr_task['taskid'],
- 'jobid': pr_task['jobid'],
- 'result': state
- }
- response = requests.post(url=url, headers=self.headers, json=data)
- logging.debug(f'Update PR task response content{response.content}')
- # Throw appropriate exception if request failed
- response.raise_for_status()
-
- logging.info(f'PR {pr_num}: Successfully completed task {pr_task["taskid"]}')
- return True
-
- def _get_pr_task_data(self, pr_num, is_alive=True):
- return self.git_repository.get_pr_metadata(pr_num)
-
- # Given a PR task, check if it's parent plan has already completed.
- # Note: Completed does not necessarily mean it succeeded.
- def _plan_already_completed(self, pr_task):
- planurl = pr_task['planurl']
- projectid = pr_task['projectid']
- planid = pr_task['planid']
- url = f'{planurl}{projectid}/_apis/distributedtask/hubs/build/plans/{planid}'
-
- response = requests.get(url=url, headers=self.headers)
- # Throw appropriate exception if request failed
- response.raise_for_status()
-
- plan_info = response.json()
- return plan_info['state'] == 'completed'
-
- def notify_abandoned_pr_tasks(self):
- update_count = 0
- prs = self.git_repository.get_prs('abandoned')
-
- if prs:
- for pr in prs:
- if not self._should_update_abandoned_pr(pr):
- continue
-
- pr_num = pr['pullRequestId']
- if not self._update_abandoned_pr(pr_num, pr_data=pr):
- update_count += 1
- logging.debug(f'Updated abandoned PR {pr_num}')
-
- if update_count > 0:
- logging.info(f'Processed {update_count} abandoned PRs via query')
-
- def _should_update_abandoned_pr(self, pr_data):
- closed_date = pr_data.get('closedDate')
- if not closed_date:
- return True
-
- # Azure DevOps returns a ISO 8601 formatted datetime string.
- closed_datetime = dateutil.parser.isoparse(closed_date)
-
- # Azure DevOps returns a timezone, so make now() relative to that.
- now = datetime.now(closed_datetime.tzinfo)
-
- return now - TASK_CUTOFF_DURATION <= closed_datetime
-
- # Returns False if the PR is no longer alive and we notified the task.
- def _update_abandoned_pr(self, pr_num, pr_data):
- pr_status = pr_data['status']
- if (pr_status == 'abandoned'):
- # update_pr_task returns True if the task was updated.
- return not self._update_pr_task(False, str(pr_num), is_alive=False)
- return True
diff --git a/src/orchestrators/cicd_orchestrator.py b/src/orchestrators/cicd_orchestrator.py
deleted file mode 100644
index d530cc6..0000000
--- a/src/orchestrators/cicd_orchestrator.py
+++ /dev/null
@@ -1,19 +0,0 @@
-# Copyright (c) Microsoft Corporation.
-# Licensed under the MIT License.
-
-from abc import ABC, abstractmethod
-from repositories.git_repository import GitRepositoryInterface
-
-
-class CicdOrchestratorInterface(ABC):
-
- def __init__(self, git_repository: GitRepositoryInterface):
- self.git_repository = git_repository
-
- @abstractmethod
- def notify_on_deployment_completion(self, commit_id, is_successful):
- pass
-
- @abstractmethod
- def notify_abandoned_pr_tasks(self):
- pass
diff --git a/src/orchestrators/cicd_orchestrator_factory.py b/src/orchestrators/cicd_orchestrator_factory.py
deleted file mode 100644
index 21b3808..0000000
--- a/src/orchestrators/cicd_orchestrator_factory.py
+++ /dev/null
@@ -1,26 +0,0 @@
-# Copyright (c) Microsoft Corporation.
-# Licensed under the MIT License.
-
-import utils
-from orchestrators.cicd_orchestrator import CicdOrchestratorInterface
-from repositories.git_repository import GitRepositoryInterface
-from orchestrators.azdo_cicd_orchestrator import AzdoCicdOrchestrator
-from orchestrators.github_cicd_orchestrator import GitHubCicdOrchestrator
-
-
-GITHUB_TYPE = "GITHUB"
-AZDO_TYPE = "AZDO"
-
-
-class CicdOrchestratorFactory:
-
- @staticmethod
- def new_cicd_orchestrator(git_repository: GitRepositoryInterface) -> CicdOrchestratorInterface:
- cicd_orchestrator_type = utils.getenv("CICD_ORCHESTRATOR_TYPE", AZDO_TYPE)
-
- if cicd_orchestrator_type == AZDO_TYPE:
- return AzdoCicdOrchestrator(git_repository)
- elif cicd_orchestrator_type == GITHUB_TYPE:
- return GitHubCicdOrchestrator(git_repository)
- else:
- raise NotImplementedError(f'The CI/CD orchestrator {cicd_orchestrator_type} is not supported')
diff --git a/src/orchestrators/github_cicd_orchestrator.py b/src/orchestrators/github_cicd_orchestrator.py
deleted file mode 100644
index bc03c70..0000000
--- a/src/orchestrators/github_cicd_orchestrator.py
+++ /dev/null
@@ -1,43 +0,0 @@
-# Copyright (c) Microsoft Corporation.
-# Licensed under the MIT License.
-
-import logging
-import utils
-import requests
-from orchestrators.cicd_orchestrator import CicdOrchestratorInterface
-from repositories.git_repository import GitRepositoryInterface
-from clients.github_client import GitHubClient
-
-
-class GitHubCicdOrchestrator(CicdOrchestratorInterface):
-
- def __init__(self, git_repository: GitRepositoryInterface):
- super().__init__(git_repository)
- self.gitops_repo_name = utils.getenv("GITHUB_GITOPS_REPO_NAME") # cloud-native-ops
- self.github_client = GitHubClient()
- self.headers = self.github_client.get_rest_api_headers()
- self.rest_api_url = self.github_client.get_rest_api_url()
-
- def notify_on_deployment_completion(self, commit_id, is_successful):
- if is_successful:
- source_commit_id, run_id = self._get_source_commit_id_run_id(commit_id)
- self._send_repo_dispatch_event(source_commit_id, run_id)
-
- def notify_abandoned_pr_tasks(self):
- pass
-
- def _get_source_commit_id_run_id(self, manifest_commitid):
- commitMessage = self.git_repository.get_commit_message(manifest_commitid)
- commitMessageArray = commitMessage.split('/', 5)
- runid = commitMessageArray[2]
- commitid = commitMessageArray[3]
- logging.info(f'CommitId {commitid}')
- return commitid, runid
-
- def _send_repo_dispatch_event(self, commmit_id, run_id):
- url = f'{self.rest_api_url}/{self.gitops_repo_name}/dispatches'
- event_type = 'sync-success'
- data = {'event_type': event_type, 'client_payload': {'sha': commmit_id, 'runid': run_id}}
- response = requests.post(url=url, headers=self.headers, json=data)
- # Throw appropriate exception if request failed
- response.raise_for_status()
diff --git a/src/repositories/azdo_git_repository.py b/src/repositories/azdo_git_repository.py
deleted file mode 100644
index 9f8647f..0000000
--- a/src/repositories/azdo_git_repository.py
+++ /dev/null
@@ -1,131 +0,0 @@
-# Copyright (c) Microsoft Corporation.
-# Licensed under the MIT License.
-
-import os
-import json
-import requests
-import utils
-from clients.azdo_client import AzdoClient
-from repositories.git_repository import GitRepositoryInterface
-
-
-PR_METADATA_KEY = "callback-task-id"
-
-
-class AzdoGitRepository(GitRepositoryInterface):
-
- def __init__(self):
- self.gitops_repo_name = utils.getenv("AZDO_GITOPS_REPO_NAME")
- self.pr_repo_name = os.getenv("AZDO_PR_REPO_NAME", self.gitops_repo_name)
- self.azdo_client = AzdoClient()
- self.repository_api = f'{self.azdo_client.get_rest_api_url()}/_apis/git/repositories/{self.gitops_repo_name}'
- self.pr_repository_api = f'{self.azdo_client.get_rest_api_url()}/_apis/git/repositories/{self.pr_repo_name}'
- self.headers = self.azdo_client.get_rest_api_headers()
-
- def post_commit_status(self, commit_status):
- url = f'{self.repository_api}/commits/{commit_status.commit_id}/statuses?api-version=6.0'
-
- azdo_status = self._map_to_azdo_status(commit_status.state)
-
- # Context and targetUrl must be unique for multiple statuses to appear,
- # otherwise the previous context/targetUrl message will be replaced.
- data = {
- 'state': azdo_status,
- 'description': commit_status.status_name + ": " + commit_status.message,
- 'targetUrl': commit_status.callback_url + "?noop=" + commit_status.status_name,
- # Shows up as "genre/name" underneath the message and status.
- 'context': {
- 'name': commit_status.status_name,
- 'genre': commit_status.genre
- }
- }
- response = requests.post(url=url, headers=self.headers, json=data)
-
- # Throw appropriate exception if request failed
- response.raise_for_status()
-
- def get_pr_metadata(self, pr_num):
- # https://docs.microsoft.com/en-us/rest/api/azure/devops/git/pull%20request%20properties/list?view=azure-devops-rest-6.0
- url = f'{self.pr_repository_api}/pullRequests/{pr_num}/properties?api-version=6.0-preview'
-
- response = requests.get(url=url, headers=self.headers)
- # Throw appropriate exception if request failed
- response.raise_for_status()
-
- # Navigate the properties response structure
- result = response.json()
- if (result['count'] > 0):
- properties = result['value']
- entry = properties.get(PR_METADATA_KEY)
- if entry:
- # At this point, we have the original JSON string we stored.
- return json.loads(entry['$value'])
- return None
-
- # Returns an array of PR dictionaries with an optional status filter
- # pr_status values: https://docs.microsoft.com/en-us/rest/api/azure/devops/git/pull%20requests/get%20pull%20requests?view=azure-devops-rest-6.0#pullrequeststatus
- def get_prs(self, pr_status):
- pr_status_param = ''
- if pr_status:
- pr_status_param = f'searchCriteria.status={pr_status}&'
- url = f'{self.pr_repository_api}/pullRequests?{pr_status_param}api-version=6.0'
- response = requests.get(url=url, headers=self.headers)
- # Throw appropriate exception if request failed
- response.raise_for_status()
-
- pr_response = json.loads(response.content)
- if pr_response['count'] == 0:
- return None
-
- return pr_response['value']
-
- def _map_to_azdo_status(self, status):
- status_map = {
- "Succeeded": "succeeded",
- "Failed": "failed",
- "Error": "error",
- "Inconclusive": "pending",
- "Running": "pending",
- "OutOfSync": "pending",
- "Synced": "succeeded",
- "Unknown": "notApplicable",
- "Progressing": "pending",
- "Degraded": "error",
- "Healthy": "succeeded",
- "Missing": "failed",
-
- "Suspended": "error",
- "ReconciliationSucceeded": "succeeded",
- "ReconciliationFailed": "failed",
- "DependencyNotReady": "error",
- "PruneFailed": "failed",
- "ArtifactFailed": "failed",
- "BuildFailed": "failed",
- "HealthCheckFailed": "failed",
- "ValidationFailed": "failed",
- "NotApplicable": "notApplicable",
- "info": "pending",
- "error": "failed"
- }
- return status_map[status]
-
- def get_commit_message(self, commit_id):
- url = f'{self.repository_api}/commits/{commit_id}?api-version=6.0'
-
- response = requests.get(url=url, headers=self.headers)
- # Throw appropriate exception if request failed
- response.raise_for_status()
-
- commit = response.json()
- comment = commit['comment']
-
- return comment
-
- def get_pr_num(self, commit_id) -> str:
- comment = self.get_commit_message(commit_id)
- MERGED_PR = "Merged PR "
- pr_num = None
- if MERGED_PR in comment:
- merged_pr_index = comment.index(MERGED_PR)
- pr_num = comment[merged_pr_index + len(MERGED_PR): comment.index(":", merged_pr_index)]
- return pr_num
diff --git a/src/repositories/git_repository.py b/src/repositories/git_repository.py
deleted file mode 100644
index 57f8984..0000000
--- a/src/repositories/git_repository.py
+++ /dev/null
@@ -1,27 +0,0 @@
-# Copyright (c) Microsoft Corporation.
-# Licensed under the MIT License.
-
-from abc import ABC, abstractmethod
-
-
-class GitRepositoryInterface(ABC):
-
- @abstractmethod
- def post_commit_status(self, commit_status):
- pass
-
- @abstractmethod
- def get_pr_num(self, commit_id) -> str:
- pass
-
- @abstractmethod
- def get_pr_metadata(self, commit_id):
- pass
-
- @abstractmethod
- def get_prs(self, pr_status):
- pass
-
- @abstractmethod
- def get_commit_message(self, commit_id):
- pass
diff --git a/src/repositories/git_repository_factory.py b/src/repositories/git_repository_factory.py
deleted file mode 100644
index e0615fd..0000000
--- a/src/repositories/git_repository_factory.py
+++ /dev/null
@@ -1,25 +0,0 @@
-# Copyright (c) Microsoft Corporation.
-# Licensed under the MIT License.
-
-import os
-from repositories.git_repository import GitRepositoryInterface
-from repositories.azdo_git_repository import AzdoGitRepository
-from repositories.github_git_repository import GitHubGitRepository
-
-
-AZDO_TYPE = "AZDO"
-GITHUB_TYPE = "GITHUB"
-
-
-class GitRepositoryFactory:
-
- @staticmethod
- def new_git_repository() -> GitRepositoryInterface:
- git_repository_type = os.getenv("GIT_REPOSITORY_TYPE", AZDO_TYPE)
-
- if git_repository_type == AZDO_TYPE:
- return AzdoGitRepository()
- elif git_repository_type == GITHUB_TYPE:
- return GitHubGitRepository()
- else:
- raise NotImplementedError(f'The Git repository {git_repository_type} is not supported')
diff --git a/src/repositories/github_git_repository.py b/src/repositories/github_git_repository.py
deleted file mode 100644
index 703ad55..0000000
--- a/src/repositories/github_git_repository.py
+++ /dev/null
@@ -1,86 +0,0 @@
-# Copyright (c) Microsoft Corporation.
-# Licensed under the MIT License.
-
-import requests
-import utils
-import logging
-from clients.github_client import GitHubClient
-from repositories.git_repository import GitRepositoryInterface
-
-
-class GitHubGitRepository(GitRepositoryInterface):
-
- MAX_DESCR_LENGTH = 140
-
- def __init__(self):
- self.gitops_repo_name = utils.getenv("GITHUB_GITOPS_MANIFEST_REPO_NAME") # gitops-manifests
- self.github_client = GitHubClient()
- self.headers = self.github_client.get_rest_api_headers()
- self.rest_api_url = self.github_client.get_rest_api_url()
-
- def post_commit_status(self, commit_status):
- url = f'{self.rest_api_url}/{self.gitops_repo_name}/statuses/{commit_status.commit_id}'
-
- github_state = self._map_to_github_state(commit_status.state)
- message = commit_status.message
- if len(message) > self.MAX_DESCR_LENGTH:
- message = message[:self.MAX_DESCR_LENGTH]
-
- data = {'state': github_state, 'description': message, 'context': commit_status.status_name}
- logging.info(f'Url {url}: Headers {self.headers}: Data {data}')
- response = requests.post(url=url, headers=self.headers, json=data)
- # Throw appropriate exception if request failed
- response.raise_for_status()
-
- def _map_to_github_state(self, reason):
- state_map = {
- "Suspended": "error",
- "ReconciliationSucceeded": "success",
- "ReconciliationFailed": "failure",
- "Progressing": "pending",
- "DependencyNotReady": "error",
- "PruneFailed": "failure",
- "ArtifactFailed": "failure",
- "BuildFailed": "failure",
- "HealthCheckFailed": "failure",
- "ValidationFailed": "failure",
- "NotApplicable": "success",
- "info": "pending",
- "error": "failure",
-
- "Succeeded": "success",
- "Failed": "failure",
- "Error": "error",
- "Inconclusive": "pending",
- "Running": "pending",
- "OutOfSync": "pending",
- "Synced": "success",
- "Unknown": "success",
- "Progressing": "pending",
- "Degraded": "error",
- "Healthy": "success",
- "Missing": "failure"
-
- }
- return state_map[reason]
-
- def get_commit_message(self, commit_id):
- url = f'{self.rest_api_url}/{self.gitops_repo_name}/commits/{commit_id}'
-
- response = requests.get(url=url, headers=self.headers)
- # Throw appropriate exception if request failed
- response.raise_for_status()
-
- responseJSON = response.json()
- commitMessage = responseJSON['commit']['message']
-
- return commitMessage
-
- def get_pr_num(self, commit_id) -> str:
- pass
-
- def get_pr_metadata(self, commit_id):
- pass
-
- def get_prs(self, pr_status):
- pass
diff --git a/src/repositories/raw_subscriber.py b/src/repositories/raw_subscriber.py
deleted file mode 100644
index c0f0e16..0000000
--- a/src/repositories/raw_subscriber.py
+++ /dev/null
@@ -1,72 +0,0 @@
-
-# Copyright (c) Microsoft Corporation.
-# Licensed under the MIT License.
-
-import dataclasses
-import json
-import logging
-import os
-import os.path
-from urllib.parse import urlparse
-import requests
-
-SUBSCRIBERS_DIR = '/subscribers'
-
-
-# An endpoint that handles unprocessed JSON forwarded from notifications.
-class RawSubscriber:
- def __init__(self, url_endpoint):
- self._url_endpoint = url_endpoint
-
- def post_commit_status(self, commit_status):
- json_data = dataclasses.asdict(commit_status)
- logging.debug("Sending raw json to subscriber: " + json.dumps(json_data))
- response = requests.post(url=self._url_endpoint, json=json_data)
- response.raise_for_status()
-
-
-class RawSubscriberFactory:
- @staticmethod
- def new_raw_subscribers() -> list[RawSubscriber]:
- logging.debug("Adding configured subscribers...")
- subscribers = RawSubscriberFactory._read_subscribers()
- logging.debug(f'{len(subscribers)} subscribers added.')
-
- return subscribers
-
- @staticmethod
- def _read_subscribers():
- subscribers = []
-
- try:
- subscriber_files = os.listdir(SUBSCRIBERS_DIR)
- except FileNotFoundError:
- logging.error("Subscriber config not found. Defaulting to no subscribers.")
- return subscribers
- if not subscriber_files:
- return subscribers
-
- for subscriber_file in subscriber_files:
- subscriber_file = os.path.join(SUBSCRIBERS_DIR, subscriber_file)
- if not os.path.isfile(subscriber_file):
- continue
-
- try:
- with open(subscriber_file, 'r') as subscriber_fh:
- url = subscriber_fh.readline()
-
- try:
- urlparse(url)
- except ValueError:
- logging.error(f"URL is invalid, subscriber has not been added: {url}")
- continue
-
- subscriber = RawSubscriber(url)
-
- subscribers.append(subscriber)
- logging.info(f"Added subscriber {subscriber_file} with endpoint {url}")
- except OSError:
- logging.error(f"Error opening subscriber config at {subscriber_file}")
- continue
-
- return subscribers
diff --git a/src/requirements.txt b/src/requirements.txt
deleted file mode 100644
index 78c71fd..0000000
--- a/src/requirements.txt
+++ /dev/null
@@ -1,5 +0,0 @@
-Flask==1.1.1
-gunicorn==20.0.4
-requests
-timeloop
-python-dateutil
\ No newline at end of file
diff --git a/src/utils.py b/src/utils.py
deleted file mode 100644
index d0d3602..0000000
--- a/src/utils.py
+++ /dev/null
@@ -1,13 +0,0 @@
-# Copyright (c) Microsoft Corporation.
-# Licensed under the MIT License.
-
-import os
-
-
-def getenv(key, default=None):
- env_value = os.getenv(key, default)
-
- if not env_value:
- raise IndentationError(f'The env variable {key} is not initialized')
-
- return env_value