├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── release-drafter.yml └── workflows │ ├── api-docs.yaml │ ├── docker.yaml │ ├── e2e.yaml │ ├── lint.yaml │ └── release-drafter.yml ├── .gitignore ├── .golangci.yml ├── .kind-cluster.yaml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE.TXT ├── Makefile ├── PROJECT ├── README.md ├── SECURITY.md ├── Tiltfile ├── api └── v1alpha1 │ ├── appgroup_types.go │ ├── doc.go │ ├── groupversion_info.go │ └── zz_generated.deepcopy.go ├── chart └── orkestra │ ├── .helmignore │ ├── Chart.lock │ ├── Chart.yaml │ ├── charts │ ├── argo-workflows-0.2.5.tgz │ ├── chartmuseum-2.15.0.tgz │ ├── helm-controller-0.1.1.tgz │ ├── keptn-0.8.6.tgz │ └── keptn-addons-0.1.0.tgz │ ├── crds │ └── orkestra.azure.microsoft.com_applicationgroups.yaml │ ├── templates │ ├── NOTES.txt │ ├── _helpers.tpl │ ├── deployment.yaml │ ├── helmrepository.yaml │ ├── hooks.yaml │ ├── rbac.yaml │ └── serviceaccount.yaml │ ├── values-ci.yaml │ └── values.yaml ├── config ├── certmanager │ ├── certificate.yaml │ ├── kustomization.yaml │ └── kustomizeconfig.yaml ├── crd │ ├── bases │ │ └── orkestra.azure.microsoft.com_applicationgroups.yaml │ ├── kustomization.yaml │ ├── kustomizeconfig.yaml │ └── patches │ │ ├── cainjection_in_appgroups.yaml │ │ └── webhook_in_appgroups.yaml ├── default │ ├── kustomization.yaml │ ├── manager_auth_proxy_patch.yaml │ ├── manager_webhook_patch.yaml │ └── webhookcainjection_patch.yaml ├── manager │ ├── kustomization.yaml │ └── manager.yaml ├── prometheus │ ├── kustomization.yaml │ └── monitor.yaml ├── rbac │ ├── appgroup_editor_role.yaml │ ├── appgroup_viewer_role.yaml │ ├── auth_proxy_client_clusterrole.yaml │ ├── auth_proxy_role.yaml │ ├── auth_proxy_role_binding.yaml │ ├── auth_proxy_service.yaml │ ├── kustomization.yaml │ ├── leader_election_role.yaml │ ├── leader_election_role_binding.yaml │ ├── role.yaml │ └── role_binding.yaml ├── samples │ └── bookinfo.yaml └── webhook │ ├── kustomization.yaml │ ├── kustomizeconfig.yaml │ └── service.yaml ├── controllers ├── appgroup_controller.go ├── appgroup_controller_test.go ├── suite_test.go ├── utils_test.go └── workflow_status_controller.go ├── docs ├── .bundle │ └── config ├── .gitignore ├── 404.html ├── Gemfile ├── Gemfile.lock ├── _config.yml ├── api.md ├── architecture.md ├── assets │ ├── azure-logo.png │ ├── bridge-to-kubernetes-tutorial.gif │ ├── favicon.ico │ ├── keptn-dashboard-failed.png │ ├── keptn-dashboard.png │ ├── keptn-executor.png │ ├── layers.png │ ├── nf-paas-layers.png │ ├── orkestra-core.png │ ├── orkestra-gif.gif │ ├── reconciler-flow.png │ ├── subchart-dag.png │ └── workflow.png ├── developers.md ├── examples.md └── index.md ├── examples ├── custom │ └── generic-bookinfo.yaml ├── keptn │ ├── README.md │ ├── bookinfo-keptn-cm.yaml │ ├── bookinfo-with-faults.yaml │ ├── bookinfo.yaml │ ├── hey │ │ └── event.json │ ├── job │ │ └── config.yaml │ ├── keptn-dashboard-failed.png │ ├── keptn-dashboard.png │ ├── keptn-executor.png │ ├── prometheus │ │ └── sli.yaml │ ├── shipyard.yaml │ └── slo.yaml └── simple │ ├── README.md │ ├── bookinfo-multi-executors.yaml │ ├── bookinfo.yaml │ └── workflow.png ├── go.mod ├── go.sum ├── hack ├── api-docs │ ├── config.json │ └── template │ │ ├── members.tpl │ │ ├── pkg.tpl │ │ └── type.tpl ├── boilerplate.go.txt ├── create-kind-cluster.sh ├── setup-envtest.sh ├── setup-kubebuilder.sh └── teardown-kind-with-registry.sh ├── main.go ├── okteto.yml └── pkg ├── executor ├── custom.go ├── executor.go ├── helmrelease.go └── keptn.go ├── graph ├── graph.go └── graph_test.go ├── helpers ├── reconcile.go └── status.go ├── meta ├── conditions.go └── errors.go ├── registry ├── config.go ├── options.go ├── pull.go ├── push.go └── registry.go ├── templates ├── templates.go ├── templates_test.go └── utils.go ├── utils ├── chart.go ├── chart_test.go ├── consts.go ├── helm.go ├── helpers.go ├── helpers_test.go └── probe.go └── workflow ├── utils.go ├── workflow.go ├── workflow_forward.go ├── workflow_reverse.go └── workflow_rollback.go /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # This is a comment. 2 | # Each line is a file pattern followed by one or more owners. 3 | 4 | # These owners will be the default owners for everything in 5 | # the repo. Unless a later match takes precedence, 6 | # @global-owner1 and @global-owner2 will be requested for 7 | # review when someone opens a pull request. 8 | 9 | * @nitishm @jonathan-innis -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve Orkestra 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Create ApplicationGroup '...' 16 | 2. Add another Application to the Spec '...' 17 | 18 | **Expected behavior** 19 | A clear and concise description of what you expected to happen. 20 | 21 | **Screenshots** 22 | If applicable, add screenshots to help explain your problem. 23 | 24 | **Environment (please complete the following information):** 25 | - Kubernetes Version [e.g. 1.21.0] 26 | - Kubernetes Distro [e.g. AKS, GKE, etc.] 27 | - Orkestra Version Tag 28 | - Helm Version 29 | 30 | **Additional context** 31 | Add any other context about the problem here. 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for Orkestra 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | categories: 2 | - title: '🚀 Features' 3 | labels: 4 | - 'feat' 5 | - 'enhancement' 6 | - title: '🐛 Bug Fixes' 7 | labels: 8 | - 'fix' 9 | - 'bug' 10 | - title: '🧰 Maintenance' 11 | label: 'chore' 12 | template: | 13 | ## Changes 14 | 15 | $CHANGES -------------------------------------------------------------------------------- /.github/workflows/api-docs.yaml: -------------------------------------------------------------------------------- 1 | name: API Docs 2 | on: 3 | pull_request: 4 | branches: [main] 5 | push: 6 | branches: [main] 7 | 8 | jobs: 9 | check-docs: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v2 14 | - name: Setup Go 15 | uses: actions/setup-go@v2 16 | with: 17 | go-version: '^1.13.1' 18 | - name: gen-crd-api-reference-docs 19 | run: make api-docs 20 | - name: Check if docs are up to date 21 | # This check must fail if there are diffs. 22 | run: git diff --exit-code ./docs/api.md 23 | -------------------------------------------------------------------------------- /.github/workflows/docker.yaml: -------------------------------------------------------------------------------- 1 | name: Docker Build & Push 2 | on: 3 | push: 4 | branches: 5 | - 'main' 6 | tags: 7 | - 'v*' 8 | pull_request: 9 | branches: 10 | - 'main' 11 | 12 | jobs: 13 | docker: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - 17 | name: Checkout 18 | uses: actions/checkout@v2 19 | - 20 | name: Docker meta 21 | id: meta 22 | uses: crazy-max/ghaction-docker-meta@v2 23 | with: 24 | # list of Docker images to use as base name for tags 25 | images: azureorkestra/orkestra 26 | # generate Docker tags based on the following events/attributes 27 | tags: | 28 | type=ref,event=branch 29 | type=ref,event=pr 30 | type=semver,pattern={{raw}} 31 | type=semver,pattern={{version}} 32 | type=semver,pattern={{major}}.{{minor}} 33 | latest 34 | - 35 | name: Login to DockerHub 36 | if: github.event_name != 'pull_request' 37 | uses: docker/login-action@v1 38 | with: 39 | username: ${{ secrets.DOCKERHUB_USERNAME }} 40 | password: ${{ secrets.DOCKERHUB_TOKEN }} 41 | - 42 | name: Build and push 43 | uses: docker/build-push-action@v2 44 | with: 45 | context: . 46 | push: ${{ github.event_name != 'pull_request' }} 47 | tags: ${{ steps.meta.outputs.tags }} 48 | -------------------------------------------------------------------------------- /.github/workflows/e2e.yaml: -------------------------------------------------------------------------------- 1 | name: E2E Testing 2 | on: 3 | pull_request: 4 | branches: [main] 5 | push: 6 | branches: [main] 7 | jobs: 8 | e2e: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v2 13 | - name: Create k8s Kind Cluster 14 | uses: helm/kind-action@v1.2.0 15 | with: 16 | cluster_name: orkestra 17 | config: .kind-cluster.yaml 18 | - name: Deploy 19 | run: | 20 | curl https://baltocdn.com/helm/signing.asc | sudo apt-key add - 21 | sudo apt-get install apt-transport-https --yes 22 | echo "deb https://baltocdn.com/helm/stable/debian/ all main" | sudo tee /etc/apt/sources.list.d/helm-stable-debian.list 23 | curl -s "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh" | bash 24 | sudo apt-get update 25 | sudo apt-get install helm 26 | kind export kubeconfig --name orkestra 27 | helm install orkestra chart/orkestra --atomic -n orkestra --create-namespace --values chart/orkestra/values-ci.yaml 28 | - name: Verify Deployment 29 | run: | 30 | curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" 31 | chmod +x kubectl 32 | sudo mv kubectl /usr/local/bin 33 | kubectl cluster-info 34 | echo "current-context:" $(kubectl config current-context) 35 | - name: Setup Go 36 | uses: actions/setup-go@v2 37 | with: 38 | go-version: '^1.16.0' 39 | - name: Restore Go cache 40 | uses: actions/cache@v1 41 | with: 42 | path: ~/go/pkg/mod 43 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 44 | restore-keys: | 45 | ${{ runner.os }}-go- 46 | - name: Run test with Coverprofile 47 | run: | 48 | make test 49 | - name: Upload coverage to Codecov 50 | uses: codecov/codecov-action@v1 51 | with: 52 | file: ./coverage.txt 53 | 54 | -------------------------------------------------------------------------------- /.github/workflows/lint.yaml: -------------------------------------------------------------------------------- 1 | name: golangci-lint 2 | on: 3 | push: 4 | branches: [main] 5 | pull_request: 6 | branches: [main] 7 | jobs: 8 | golangci: 9 | name: lint 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - name: golangci-lint 14 | uses: golangci/golangci-lint-action@v2 -------------------------------------------------------------------------------- /.github/workflows/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name: Release Drafter 2 | on: 3 | push: 4 | # branches to consider in the event; optional, defaults to all 5 | branches: 6 | - main 7 | # pull_request event is required only for autolabeler 8 | pull_request: 9 | # Only following types are handled by the action, but one can default to all as well 10 | types: [opened, reopened, synchronize] 11 | 12 | jobs: 13 | update_release_draft: 14 | runs-on: ubuntu-latest 15 | steps: 16 | # Drafts your next Release notes as Pull Requests are merged into "master" 17 | - uses: release-drafter/release-drafter@v5 18 | env: 19 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .settings/ 3 | .vscode/ 4 | tsconfig.json 5 | jsconfig.json 6 | 7 | ### Go ### 8 | # Binaries for programs and plugins 9 | *.exe 10 | *.exe~ 11 | *.dll 12 | *.so 13 | *.dylib 14 | 15 | # Test binary, build with `go test -c` 16 | *.test 17 | 18 | *.out 19 | .glide/ 20 | 21 | #dlv binaries 22 | debug 23 | 24 | ### Vim ### 25 | # Swap 26 | [._]*.s[a-v][a-z] 27 | [._]*.sw[a-p] 28 | [._]s[a-rt-v][a-z] 29 | [._]ss[a-gi-z] 30 | [._]sw[a-p] 31 | 32 | # IntelliJ / Goland 33 | .idea/ 34 | .vscode/ 35 | 36 | # Emacs 37 | *~ 38 | *#* 39 | 40 | /bin/ 41 | build/ 42 | testbin/ 43 | .dev/ 44 | .env 45 | _dist/ 46 | 47 | creds.json 48 | screenlog.* 49 | vendor/ 50 | demo/bin/ 51 | certificates 52 | 53 | ## Ignore test coverage files 54 | coverage.json 55 | coverage.txt 56 | coverage.xml 57 | coverage/ 58 | report.xml 59 | testoutput.txt 60 | 61 | # macOS files 62 | .DS_Store 63 | 64 | # Certificates etc 65 | *.pem 66 | ./certs/ 67 | commit_logs 68 | bootstrap.yaml 69 | 70 | # Development tools 71 | tilt_modules 72 | .stignore 73 | 74 | # binary manager 75 | manager 76 | 77 | # artifacts 78 | */**.tgz 79 | 80 | # temporary directories 81 | tmp/ 82 | temp/ 83 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | linters-settings: 2 | depguard: 3 | list-type: blacklist 4 | packages: 5 | # logging is allowed only by logutils.Log, logrus 6 | # is allowed to use only in logutils package 7 | - github.com/sirupsen/logrus 8 | packages-with-error-message: 9 | - github.com/sirupsen/logrus: "logging is allowed only by logutils.Log" 10 | dupl: 11 | threshold: 100 12 | exhaustive: 13 | default-signifies-exhaustive: false 14 | funlen: 15 | lines: 100 16 | statements: 50 17 | gci: 18 | local-prefixes: github.com/golangci/golangci-lint 19 | goconst: 20 | min-len: 2 21 | min-occurrences: 2 22 | gocritic: 23 | enabled-tags: 24 | - diagnostic 25 | - experimental 26 | - opinionated 27 | - performance 28 | disabled-checks: 29 | - dupImport # https://github.com/go-critic/go-critic/issues/845 30 | - ifElseChain 31 | - octalLiteral 32 | - unnamedResult 33 | - whyNoLint 34 | - commentedOutCode 35 | - commentedOutImport 36 | - wrapperFunc 37 | - rangeValCopy 38 | - importShadow 39 | - hugeParam 40 | - commentFormatting 41 | gocyclo: 42 | min-complexity: 15 43 | golint: 44 | min-confidence: 0 45 | gomnd: 46 | settings: 47 | mnd: 48 | # don't include the "operation" and "assign" 49 | checks: argument,case,condition,return 50 | gosec: 51 | settings: 52 | exclude: -G204 53 | govet: 54 | check-shadowing: false 55 | settings: 56 | printf: 57 | funcs: 58 | - (github.com/golangci/golangci-lint/pkg/logutils.Log).Infof 59 | - (github.com/golangci/golangci-lint/pkg/logutils.Log).Warnf 60 | - (github.com/golangci/golangci-lint/pkg/logutils.Log).Errorf 61 | - (github.com/golangci/golangci-lint/pkg/logutils.Log).Fatalf 62 | lll: 63 | line-length: 950 64 | maligned: 65 | suggest-new: true 66 | misspell: 67 | locale: US 68 | nolintlint: 69 | allow-leading-space: true # don't require machine-readable nolint directives (i.e. with no leading space) 70 | allow-unused: false # report any unused nolint directives 71 | require-explanation: false # don't require an explanation for nolint directives 72 | require-specific: false # don't require nolint directives to be specific about which linter is being skipped 73 | 74 | linters: 75 | # please, do not use `enable-all`: it's deprecated and will be removed soon. 76 | # inverted configuration with `enable-all` and `disable` is not scalable during updates of golangci-lint 77 | disable-all: true 78 | enable: 79 | - deadcode 80 | - dogsled 81 | - errcheck 82 | - gocritic 83 | - gofmt 84 | - golint 85 | # todo[kusthedude]: restore gosec check, once this issue is resolved https://github.com/golangci/golangci-lint/issues/177 86 | # - gosec 87 | - govet 88 | - lll 89 | - misspell 90 | - staticcheck 91 | - typecheck 92 | - whitespace 93 | 94 | # don't enable: 95 | # - asciicheck 96 | # - gochecknoglobals 97 | # - gocognit 98 | # - godot 99 | # - godox 100 | # - goerr113 101 | # - maligned 102 | # - nestif 103 | # - prealloc 104 | # - testpackage 105 | # - wsl 106 | 107 | issues: 108 | # Excluding configuration per-path, per-linter, per-text and per-source 109 | exclude-rules: 110 | - path: _test\.go 111 | linters: 112 | - gomnd 113 | 114 | # https://github.com/go-critic/go-critic/issues/926 115 | - linters: 116 | - gocritic 117 | text: "unnecessaryDefer:" 118 | 119 | run: 120 | skip-dirs: 121 | - test/testdata_etc 122 | - internal/cache 123 | - internal/renameio 124 | - internal/robustio 125 | timeout: 5m 126 | 127 | # golangci.com configuration 128 | # https://github.com/golangci/golangci/wiki/Configuration 129 | service: 130 | golangci-lint-version: 1.23.x # use the fixed version to not introduce new linters unexpectedly 131 | prepare: 132 | - echo "here I can run custom commands, but no preparation needed for this repo" -------------------------------------------------------------------------------- /.kind-cluster.yaml: -------------------------------------------------------------------------------- 1 | kind: Cluster 2 | apiVersion: kind.x-k8s.io/v1alpha4 3 | nodes: 4 | - role: control-plane 5 | # port forward 80 on the host to 80 on this node 6 | extraPortMappings: 7 | - containerPort: 30950 8 | hostPort: 8080 9 | # optional: set the bind address on the host 10 | # 0.0.0.0 is the current default 11 | listenAddress: "127.0.0.1" 12 | # optional: set the protocol to one of TCP, UDP, SCTP. 13 | # TCP is the default 14 | protocol: TCP 15 | containerdConfigPatches: 16 | - |- 17 | [plugins."io.containerd.grpc.v1.cri".registry.mirrors."localhost:5000"] 18 | endpoint = ["http://kind-registry:5000"] 19 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Microsoft Open Source Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 4 | 5 | If you have any questions or need more information, here are some helpful resources: 6 | 7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns 10 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Orkestra 2 | 3 | Welcome to Orkestra! We would love to accept your patches and contributions to this project. 4 | 5 | Here is how you can help: 6 | 7 | - Report or fix bugs. 8 | - Add or propose new features. 9 | - Improve our documentation. 10 | 11 | ## Resources 12 | 13 | - Website: https://azure.github.io/orkestra/ 14 | - Documentation: https://azure.github.io/orkestra/api.html 15 | - Developer's Guide: https://azure.github.io/orkestra/developers.html 16 | - Discussion forum: https://github.com/Azure/orkestra/discussions 17 | - Azure Orkestra Slack: Join the Azure Orkestra [Slack](https://join.slack.com/t/azureorkestra/shared_invite/zt-rowzrite-Hm_eaih4GyjjZXWftuoqPQ) 18 | 19 | ## Pull Request Checklist 20 | 21 | Before sending your pull requests, make sure you do the following: 22 | 23 | - Read this contributing guide. 24 | - Read the [Code of Conduct][code-conduct-link]. 25 | - Run the [tests](#running-tests). 26 | - Run `make prepare-for-pr`. For details, see [Prepare Code for PR](#prepare-code-for-pr) section. 27 | 28 | ## How to become a contributor 29 | 30 | ### Contributor License Agreement 31 | 32 | Most contributions to this project require you to agree to a Contributor License Agreement (CLA) declaring that you have the right to, and do, grant us the rights to use your contribution. For details, visit https://cla.microsoft.com. 33 | 34 | When you submit a pull request, a CLA-bot will automatically determine whether you need to provide 35 | a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions 36 | provided by the bot. You will only need to do this once across all repos using our CLA. 37 | 38 | ### Finding something to work on 39 | 40 | If you want to write some code, but don't know where to start or what you might want to do, take a look at the [Good First Issue][good-issue] label. 41 | 42 | ### Developing Orkestra 43 | 44 | Follow the [Developer's Guide][dev-guide] for a full set of instructions to get started with building, running, and debugging Orkestra. 45 | 46 | ### Running tests 47 | 48 | For a full set of instructions to run tests, follow the testing & debugging section of [Developer's Guide][dev-guide-tests]. For some tips, you can run tests with the following `make` target. 49 | 50 | ```shell 51 | make clean && make dev && make test 52 | ``` 53 | 54 | ### Prepare Code for PR 55 | 56 | Before submitting a PR, run the following `make` target. 57 | 58 | ```shell 59 | make prepare-for-pr 60 | ``` 61 | 62 | This will perform following checks: 63 | - Examine Go source code and report suspicious constructs. 64 | - Format Go source code. 65 | - Update API docs if applicable. 66 | 67 | ## Security 68 | 69 | For instructions on reporting security issues and bugs, please see [security][security-link] guide. 70 | 71 | 72 | ## Support 73 | 74 | For questions about building, running, or troubleshooting, start with the [Developer's Guide][dev-guide], and work your way through the process that we've outlined. If that doesn't answer your question(s), try to post on [Discussion][discussion-link] tab or if you think you found a bug, please file an [issue][issue-link]. 75 | 76 | If you still have question(s), join the Azure Orkestra [Slack](https://join.slack.com/t/azureorkestra/shared_invite/zt-rowzrite-Hm_eaih4GyjjZXWftuoqPQ) and someone will help you get answer(s) to your question(s). 77 | 78 | [dev-guide]: https://azure.github.io/orkestra/developers.html 79 | [dev-guide-tests]: https://azure.github.io/orkestra/developers.html#testing--debugging 80 | [code-conduct-link]: https://github.com/Azure/orkestra/blob/main/CODE_OF_CONDUCT.md 81 | [security-link]: https://github.com/Azure/orkestra/blob/main/SECURITY.md 82 | [good-issue]: https://github.com/Azure/orkestra/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22 83 | [discussion-link]: https://github.com/Azure/orkestra/discussions 84 | [issue-link]: https://github.com/Azure/orkestra/issues/new/choose 85 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build the manager binary 2 | FROM golang:1.16 as builder 3 | 4 | WORKDIR /workspace 5 | # Copy the Go Modules manifests 6 | COPY go.mod go.mod 7 | COPY go.sum go.sum 8 | # cache deps before building and copying source so that we don't need to re-download as much 9 | # and so that source changes don't invalidate our downloaded layer 10 | RUN go mod download 11 | 12 | # Copy the go source 13 | COPY main.go main.go 14 | COPY api/ api/ 15 | COPY pkg/ pkg/ 16 | COPY controllers/ controllers/ 17 | # COPY config.yaml config.yaml 18 | 19 | # Build 20 | RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GO111MODULE=on go build -o manager main.go 21 | 22 | FROM alpine:3.7 23 | RUN apk add --no-cache bash 24 | RUN mkdir -p /etc/orkestra/charts/pull/ 25 | 26 | WORKDIR / 27 | COPY --from=builder /workspace/manager . 28 | # COPY --from=builder /workspace/config.yaml /etc/controller/config.yaml 29 | 30 | ENTRYPOINT ["/manager"] 31 | -------------------------------------------------------------------------------- /LICENSE.TXT: -------------------------------------------------------------------------------- 1 | Azure Orkestra 2 | 3 | Copyright (c) Microsoft Corporation. 4 | 5 | MIT License 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | # Image URL to use all building/pushing image targets 3 | IMG ?= azureorkestra/orkestra:latest 4 | # Produce CRDs that work back to Kubernetes 1.11 (no version conversion) 5 | CRD_OPTIONS ?= "crd:trivialVersions=true" 6 | DEBUG_LEVEL ?= 1 7 | CI_VALUES ?= "chart/orkestra/values-ci.yaml" 8 | 9 | # Directories 10 | ROOT_DIR := $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))) 11 | BIN_DIR := $(abspath $(ROOT_DIR)/bin) 12 | 13 | reg_name='kind-registry' 14 | 15 | # Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) 16 | ifeq (,$(shell go env GOBIN)) 17 | GOBIN=$(shell go env GOPATH)/bin 18 | else 19 | GOBIN=$(shell go env GOBIN) 20 | endif 21 | 22 | all: manager 23 | 24 | # Create a local docker registry, start kind cluster, and install Orkestra 25 | dev: kind-create 26 | helm upgrade --install orkestra chart/orkestra --wait --atomic -n orkestra --create-namespace --values ${CI_VALUES} 27 | 28 | debug: dev 29 | go run main.go --debug --log-level ${DEBUG_LEVEL} 30 | 31 | ginkgo-test: install 32 | go get github.com/onsi/ginkgo/ginkgo 33 | ginkgo ./... -cover -coverprofile coverage.txt 34 | 35 | # Run tests 36 | test: install 37 | go test -v ./... -coverprofile coverage.txt -timeout 35m 38 | 39 | # Build manager binary 40 | manager: generate fmt vet 41 | go build -o bin/manager main.go 42 | 43 | # Run against the configured Kubernetes cluster in ~/.kube/config 44 | run: generate fmt vet manifests 45 | go run ./main.go 46 | 47 | # Install CRDs into a cluster 48 | install: manifests 49 | kustomize build config/crd | kubectl apply -f - 50 | 51 | # Uninstall CRDs from a cluster 52 | uninstall: manifests 53 | kustomize build config/crd | kubectl delete -f - 54 | 55 | # Deploy controller in the configured Kubernetes cluster in ~/.kube/config 56 | deploy: manifests 57 | cd config/manager && kustomize edit set image controller=${IMG} 58 | kustomize build config/default | kubectl apply -f - 59 | 60 | # Generate manifests e.g. CRD, RBAC etc. 61 | manifests: controller-gen 62 | $(CONTROLLER_GEN) $(CRD_OPTIONS) rbac:roleName=manager-role webhook paths="./..." output:crd:artifacts:config=config/crd/bases 63 | $(CONTROLLER_GEN) $(CRD_OPTIONS) rbac:roleName=manager-role webhook paths="./..." output:crd:artifacts:config=chart/orkestra/crds 64 | 65 | # Prepare code for PR 66 | prepare-for-pr: vet fmt api-docs 67 | 68 | # Generate API reference documentation 69 | api-docs: gen-crd-api-reference-docs 70 | $(API_REF_GEN) -api-dir=./api/v1alpha1 -config=./hack/api-docs/config.json -template-dir=./hack/api-docs/template -out-file=./docs/api.md 71 | 72 | # Run go fmt against code 73 | fmt: 74 | go fmt ./... 75 | 76 | # Run go vet against code 77 | vet: 78 | go vet ./... 79 | 80 | # Generate code 81 | generate: controller-gen 82 | $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..." 83 | 84 | # Build the docker image 85 | docker-build: test 86 | docker build . -t ${IMG} 87 | 88 | # Push the docker image 89 | docker-push: 90 | docker push ${IMG} 91 | 92 | # setup kubebuilder 93 | setup-kubebuilder: 94 | bash hack/setup-envtest.sh; 95 | bash hack/setup-kubebuilder.sh 96 | 97 | # find or download controller-gen 98 | # download controller-gen if necessary 99 | controller-gen: 100 | ifeq (, $(shell which controller-gen)) 101 | @{ \ 102 | set -e ;\ 103 | CONTROLLER_GEN_TMP_DIR=$$(mktemp -d) ;\ 104 | cd $$CONTROLLER_GEN_TMP_DIR ;\ 105 | go mod init tmp ;\ 106 | go get sigs.k8s.io/controller-tools/cmd/controller-gen@v0.5.0 ;\ 107 | rm -rf $$CONTROLLER_GEN_TMP_DIR ;\ 108 | } 109 | CONTROLLER_GEN=$(GOBIN)/controller-gen 110 | else 111 | CONTROLLER_GEN=$(shell which controller-gen) 112 | endif 113 | 114 | # Find or download gen-crd-api-reference-docs 115 | gen-crd-api-reference-docs: 116 | ifeq (, $(shell which gen-crd-api-reference-docs)) 117 | @{ \ 118 | set -e ;\ 119 | API_REF_GEN_TMP_DIR=$$(mktemp -d) ;\ 120 | cd $$API_REF_GEN_TMP_DIR ;\ 121 | go mod init tmp ;\ 122 | go get github.com/ahmetb/gen-crd-api-reference-docs@v0.2.0 ;\ 123 | rm -rf $$API_REF_GEN_TMP_DIR ;\ 124 | } 125 | API_REF_GEN=$(GOBIN)/gen-crd-api-reference-docs 126 | else 127 | API_REF_GEN=$(shell which gen-crd-api-reference-docs) 128 | endif 129 | ## -------------------------------------- 130 | ## Kind 131 | ## -------------------------------------- 132 | KIND_CLUSTER_NAME ?= orkestra 133 | 134 | kind-create: 135 | ./hack/create-kind-cluster.sh 136 | kind load docker-image $(IMG) --name $(KIND_CLUSTER_NAME) 137 | 138 | kind-delete: 139 | ./hack/teardown-kind-with-registry.sh 140 | 141 | ## -------------------------------------- 142 | ## Cleanup 143 | ## -------------------------------------- 144 | 145 | clean: 146 | ./hack/teardown-kind-with-registry.sh 147 | @rm -rf $(BIN_DIR) 148 | -------------------------------------------------------------------------------- /PROJECT: -------------------------------------------------------------------------------- 1 | domain: azure.microsoft.com 2 | repo: github.com/Azure/Orkestra 3 | resources: 4 | - group: orkestra 5 | kind: ApplicationGroup 6 | version: v1alpha1 7 | version: "2" 8 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | ## Security 2 | 3 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 4 | 5 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets Microsoft's [Microsoft's definition of a security vulnerability](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)), please report it to us as described below. 6 | 7 | ## Reporting Security Issues 8 | 9 | **Please do not report security vulnerabilities through public GitHub issues.** 10 | 11 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report). 12 | 13 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc). 14 | 15 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). 16 | 17 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 18 | 19 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 20 | * Full paths of source file(s) related to the manifestation of the issue 21 | * The location of the affected source code (tag/branch/commit or direct URL) 22 | * Any special configuration required to reproduce the issue 23 | * Step-by-step instructions to reproduce the issue 24 | * Proof-of-concept or exploit code (if possible) 25 | * Impact of the issue, including how an attacker might exploit the issue 26 | 27 | This information will help us triage your report more quickly. 28 | 29 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://microsoft.com/msrc/bounty) page for more details about our active programs. 30 | 31 | ## Preferred Languages 32 | 33 | We prefer all communications to be in English. 34 | 35 | ## Policy 36 | 37 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd). -------------------------------------------------------------------------------- /Tiltfile: -------------------------------------------------------------------------------- 1 | load('ext://namespace', 'namespace_yaml') 2 | k8s_yaml(namespace_yaml("orkestra"),allow_duplicates=True) 3 | 4 | compile_cmd = 'CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o manager main.go' 5 | 6 | local_resource( 7 | 'azure-orkestra', 8 | compile_cmd, 9 | deps=['main.go']) 10 | 11 | docker_build( 12 | 'azureorkestra/orkestra', 13 | '.', 14 | dockerfile='Dockerfile') 15 | 16 | yaml = local('helm template --namespace default orkestra chart/orkestra --no-hooks --include-crds') 17 | 18 | k8s_yaml(yaml,allow_duplicates=True) 19 | 20 | k8s_yaml(['./config/samples/bookinfo.yaml'],allow_duplicates=True) 21 | -------------------------------------------------------------------------------- /api/v1alpha1/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | // Package v1alpha1 contains API Schema definitions for the Orkestra v1alpha1 API group. 5 | // +kubebuilder:object:generate=true 6 | // +groupName=orkestra.azure.microsoft.com 7 | package v1alpha1 8 | -------------------------------------------------------------------------------- /api/v1alpha1/groupversion_info.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | // Package v1alpha1 contains API Schema definitions for the orkestra v1alpha1 API group 5 | // +kubebuilder:object:generate=true 6 | // +groupName=orkestra.azure.microsoft.com 7 | package v1alpha1 8 | 9 | import ( 10 | "k8s.io/apimachinery/pkg/runtime/schema" 11 | "sigs.k8s.io/controller-runtime/pkg/scheme" 12 | ) 13 | 14 | var ( 15 | // GroupVersion is group version used to register these objects 16 | GroupVersion = schema.GroupVersion{Group: "orkestra.azure.microsoft.com", Version: "v1alpha1"} 17 | 18 | // SchemeBuilder is used to add go types to the GroupVersionKind scheme 19 | SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} 20 | 21 | // AddToScheme adds the types in this group-version to the given scheme. 22 | AddToScheme = SchemeBuilder.AddToScheme 23 | ) 24 | -------------------------------------------------------------------------------- /chart/orkestra/.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 | -------------------------------------------------------------------------------- /chart/orkestra/Chart.lock: -------------------------------------------------------------------------------- 1 | dependencies: 2 | - name: chartmuseum 3 | repository: https://chartmuseum.github.io/charts 4 | version: 2.15.0 5 | - name: argo-workflows 6 | repository: https://argoproj.github.io/argo-helm 7 | version: 0.2.5 8 | - name: helm-controller 9 | repository: https://nitishm.github.io/charts 10 | version: 0.1.1 11 | - name: keptn 12 | repository: https://storage.googleapis.com/keptn-installer 13 | version: 0.8.6 14 | - name: keptn-addons 15 | repository: https://nitishm.github.io/charts 16 | version: 0.1.0 17 | digest: sha256:6ad307ffa17440e76db1de3631fd1ac331c6963ff5ec78f2faac0cda04989d9f 18 | generated: "2021-09-28T17:37:51.415157-07:00" 19 | -------------------------------------------------------------------------------- /chart/orkestra/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: orkestra 3 | description: A Helm chart for Azure Orkestra operator and supporting components 4 | 5 | sources: 6 | - https://github.com/Azure/Orkestra 7 | 8 | maintainers: 9 | - name: Nitish Malhotra (nitishm) 10 | email: nitishm@microsoft.com 11 | 12 | # Chart version 13 | version: 0.1.0 14 | 15 | type: application 16 | 17 | # Application version 18 | appVersion: "0.1.0" 19 | 20 | dependencies: 21 | - name: chartmuseum 22 | version: "2.15.0" 23 | repository: "https://chartmuseum.github.io/charts" 24 | - name: argo-workflows 25 | version: "0.2.5" 26 | repository: "https://argoproj.github.io/argo-helm" 27 | - name: helm-controller 28 | condition: helm-controller.enabled 29 | version: "0.1.1" 30 | repository: "https://nitishm.github.io/charts" 31 | - name: keptn 32 | condition: keptn.enabled 33 | version: "0.8.6" 34 | repository: "https://storage.googleapis.com/keptn-installer" 35 | - name: keptn-addons 36 | condition: keptn-addons.enabled 37 | version: "0.1.0" 38 | repository: "https://nitishm.github.io/charts" 39 | 40 | keywords: 41 | - helmops 42 | - application release 43 | - orchestration 44 | - continuous delivery 45 | - conituous deployment 46 | - 5G 47 | -------------------------------------------------------------------------------- /chart/orkestra/charts/argo-workflows-0.2.5.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/orkestra/259e906371227284a32656907c38ebad63a580ba/chart/orkestra/charts/argo-workflows-0.2.5.tgz -------------------------------------------------------------------------------- /chart/orkestra/charts/chartmuseum-2.15.0.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/orkestra/259e906371227284a32656907c38ebad63a580ba/chart/orkestra/charts/chartmuseum-2.15.0.tgz -------------------------------------------------------------------------------- /chart/orkestra/charts/helm-controller-0.1.1.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/orkestra/259e906371227284a32656907c38ebad63a580ba/chart/orkestra/charts/helm-controller-0.1.1.tgz -------------------------------------------------------------------------------- /chart/orkestra/charts/keptn-0.8.6.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/orkestra/259e906371227284a32656907c38ebad63a580ba/chart/orkestra/charts/keptn-0.8.6.tgz -------------------------------------------------------------------------------- /chart/orkestra/charts/keptn-addons-0.1.0.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/orkestra/259e906371227284a32656907c38ebad63a580ba/chart/orkestra/charts/keptn-addons-0.1.0.tgz -------------------------------------------------------------------------------- /chart/orkestra/templates/NOTES.txt: -------------------------------------------------------------------------------- 1 | Happy Helming with Azure/Orkestra -------------------------------------------------------------------------------- /chart/orkestra/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Expand the name of the chart. 3 | */}} 4 | {{- define "orkestra.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 "orkestra.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 "orkestra.chart" -}} 30 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} 31 | {{- end }} 32 | 33 | {{/* 34 | Common labels 35 | */}} 36 | {{- define "orkestra.labels" -}} 37 | helm.sh/chart: {{ include "orkestra.chart" . }} 38 | {{ include "orkestra.selectorLabels" . }} 39 | {{- if .Chart.AppVersion }} 40 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 41 | {{- end }} 42 | app.kubernetes.io/managed-by: {{ .Release.Service }} 43 | {{- end }} 44 | 45 | {{/* 46 | Selector labels 47 | */}} 48 | {{- define "orkestra.selectorLabels" -}} 49 | app.kubernetes.io/name: {{ include "orkestra.name" . }} 50 | app.kubernetes.io/instance: {{ .Release.Name }} 51 | {{- end }} 52 | 53 | {{/* 54 | Create the name of the service account to use 55 | */}} 56 | {{- define "orkestra.serviceAccountName" -}} 57 | {{- if .Values.serviceAccount.create }} 58 | {{- default (include "orkestra.fullname" .) .Values.serviceAccount.name }} 59 | {{- else }} 60 | {{- default "default" .Values.serviceAccount.name }} 61 | {{- end }} 62 | {{- end }} 63 | -------------------------------------------------------------------------------- /chart/orkestra/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | {{ if not .Values.ci.enabled }} 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: {{ include "orkestra.fullname" . }} 6 | labels: 7 | {{- include "orkestra.labels" . | nindent 4 }} 8 | spec: 9 | replicas: {{ .Values.replicaCount }} 10 | selector: 11 | matchLabels: 12 | {{- include "orkestra.selectorLabels" . | nindent 6 }} 13 | template: 14 | metadata: 15 | {{- with .Values.podAnnotations }} 16 | annotations: 17 | {{- toYaml . | nindent 8 }} 18 | {{- end }} 19 | labels: 20 | {{- include "orkestra.selectorLabels" . | nindent 8 }} 21 | spec: 22 | {{- with .Values.imagePullSecrets }} 23 | imagePullSecrets: 24 | {{- toYaml . | nindent 8 }} 25 | {{- end }} 26 | serviceAccountName: {{ include "orkestra.serviceAccountName" . }} 27 | securityContext: 28 | {{- toYaml .Values.podSecurityContext | nindent 8 }} 29 | containers: 30 | - name: {{ .Chart.Name }} 31 | securityContext: 32 | {{- toYaml .Values.securityContext | nindent 12 }} 33 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" 34 | imagePullPolicy: {{ .Values.image.pullPolicy }} 35 | args: 36 | - --staging-repo-url 37 | - http://{{ .Release.Name }}-chartmuseum.{{ .Release.Namespace }}:8080 38 | - --config 39 | - /etc/controller/config.yaml 40 | - --chart-store-path 41 | - {{ .Values.chartStorePath }} 42 | {{- if .Values.remediation.disabled }} 43 | - --disable-remediation 44 | {{- end }} 45 | {{- if .Values.cleanup.enabled }} 46 | - --cleanup-downloaded-charts 47 | {{- end }} 48 | {{ if .Values.debug.enabled }} 49 | - --debug 50 | {{- end }} 51 | - --log-level={{ .Values.logLevel | default 0 }} 52 | env: 53 | - name: WORKFLOW_NAMESPACE 54 | value: {{ .Release.Namespace }} 55 | - name: WORKFLOW_SERVICEACCOUNT_NAME 56 | value: {{ include "orkestra.serviceAccountName" . }} 57 | {{- if .Values.ci.enabled }} 58 | - name: CI_ENVTEST_CHARTMUSEUM_URL 59 | value: {{ .Values.ci.env.chartmuseumURL }} 60 | {{- end }} 61 | resources: 62 | {{- toYaml .Values.resources | nindent 12 }} 63 | # define a liveness probe that checks every 5 seconds, starting after 5 seconds 64 | livenessProbe: 65 | httpGet: 66 | path: /live 67 | port: 8086 68 | initialDelaySeconds: 5 69 | periodSeconds: 5 70 | 71 | # define a readiness probe that checks every 5 seconds 72 | readinessProbe: 73 | httpGet: 74 | path: /ready 75 | port: 8086 76 | periodSeconds: 5 77 | {{- with .Values.nodeSelector }} 78 | nodeSelector: 79 | {{- toYaml . | nindent 8 }} 80 | {{- end }} 81 | {{- with .Values.affinity }} 82 | affinity: 83 | {{- toYaml . | nindent 8 }} 84 | {{- end }} 85 | {{- with .Values.tolerations }} 86 | tolerations: 87 | {{- toYaml . | nindent 8 }} 88 | {{- end }} 89 | {{- end }} 90 | -------------------------------------------------------------------------------- /chart/orkestra/templates/helmrepository.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: source.toolkit.fluxcd.io/v1beta1 2 | kind: HelmRepository 3 | metadata: 4 | name: {{ .Values.chartmuseum.name }} 5 | namespace: {{ .Release.Namespace }} 6 | labels: 7 | {{- include "orkestra.labels" . | nindent 4 }} 8 | spec: 9 | interval: {{ .Values.chartmuseum.interval }} 10 | url: http://{{ .Release.Name }}-chartmuseum.{{ .Release.Namespace }}:8080 -------------------------------------------------------------------------------- /chart/orkestra/templates/hooks.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: batch/v1 2 | kind: Job 3 | metadata: 4 | name: "{{ .Release.Name }}-pre-delete" 5 | labels: 6 | app.kubernetes.io/managed-by: {{ .Release.Service | quote }} 7 | app.kubernetes.io/instance: {{ .Release.Name | quote }} 8 | app.kubernetes.io/version: {{ .Chart.AppVersion }} 9 | helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" 10 | annotations: 11 | "helm.sh/hook": pre-delete 12 | "helm.sh/hook-weight": "-5" 13 | "helm.sh/hook-delete-policy": "before-hook-creation,hook-succeeded" 14 | spec: 15 | template: 16 | metadata: 17 | name: "{{ .Release.Name }}" 18 | labels: 19 | app.kubernetes.io/managed-by: {{ .Release.Service | quote }} 20 | app.kubernetes.io/instance: {{ .Release.Name | quote }} 21 | helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" 22 | spec: 23 | serviceAccountName: {{ include "orkestra.serviceAccountName" . }} 24 | restartPolicy: Never 25 | containers: 26 | - name: pre-delete-job 27 | image: bitnami/kubectl:1.21 28 | env: 29 | - name: RELEASE_NAMESPACE 30 | value: {{ .Release.Namespace }} 31 | - name: HELMREPOSITORY_NAME 32 | value: {{ .Values.chartmuseum.name }} 33 | command: ["kubectl", "delete", "helmrepositories", "-n", "$(RELEASE_NAMESPACE)", "$(HELMREPOSITORY_NAME)", "--ignore-not-found=true"] -------------------------------------------------------------------------------- /chart/orkestra/templates/rbac.yaml: -------------------------------------------------------------------------------- 1 | kind: ClusterRole 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | metadata: 4 | name: {{ include "orkestra.serviceAccountName" . }} 5 | namespace: {{ .Release.Namespace }} 6 | rules: 7 | - apiGroups: 8 | - '*' 9 | resources: 10 | - '*' 11 | verbs: 12 | - '*' 13 | - nonResourceURLs: 14 | - '*' 15 | verbs: 16 | - '*' 17 | 18 | --- 19 | kind: ClusterRoleBinding 20 | apiVersion: rbac.authorization.k8s.io/v1 21 | metadata: 22 | name: {{ include "orkestra.serviceAccountName" . }} 23 | namespace: {{ .Release.Namespace }} 24 | subjects: 25 | - kind: ServiceAccount 26 | name: {{ include "orkestra.serviceAccountName" . }} 27 | namespace: {{ .Release.Namespace }} 28 | roleRef: 29 | kind: ClusterRole 30 | name: {{ include "orkestra.serviceAccountName" . }} 31 | apiGroup: rbac.authorization.k8s.io -------------------------------------------------------------------------------- /chart/orkestra/templates/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.serviceAccount.create -}} 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: {{ include "orkestra.serviceAccountName" . }} 6 | labels: 7 | {{- include "orkestra.labels" . | nindent 4 }} 8 | {{- with .Values.serviceAccount.annotations }} 9 | annotations: 10 | {{- toYaml . | nindent 4 }} 11 | {{- end }} 12 | {{- end }} 13 | -------------------------------------------------------------------------------- /chart/orkestra/values-ci.yaml: -------------------------------------------------------------------------------- 1 | ci: 2 | enabled: true 3 | env: 4 | chartmuseumURL: "http://127.0.0.1:8080" 5 | chartmuseum: 6 | service: 7 | type: NodePort 8 | externalPort: 8080 9 | nodePort: 30950 -------------------------------------------------------------------------------- /chart/orkestra/values.yaml: -------------------------------------------------------------------------------- 1 | # namespace: &namespace orkestra 2 | serviceAccount: &serviceAccount orkestra 3 | 4 | replicaCount: 1 5 | 6 | chartStorePath: "/etc/orkestra/charts/pull" 7 | 8 | image: 9 | repository: azureorkestra/orkestra 10 | pullPolicy: Always 11 | tag: "latest" 12 | 13 | imagePullSecrets: [] 14 | nameOverride: "" 15 | fullnameOverride: "" 16 | 17 | serviceAccount: 18 | create: true 19 | annotations: {} 20 | name: *serviceAccount 21 | 22 | ci: 23 | enabled: false 24 | env: 25 | chartmuseumURL: "http://127.0.0.1:8080" 26 | 27 | podAnnotations: {} 28 | 29 | podSecurityContext: {} 30 | 31 | securityContext: {} 32 | 33 | resources: {} 34 | 35 | nodeSelector: {} 36 | 37 | tolerations: [] 38 | 39 | affinity: {} 40 | 41 | remediation: 42 | disabled: false 43 | 44 | # set to dev mode until MVP 45 | cleanup: 46 | enabled: false 47 | 48 | # set to dev mode until MVP 49 | debug: 50 | enabled: false 51 | 52 | logLevel: 5 53 | 54 | 55 | # Dependency overlay values 56 | chartmuseum: 57 | name: chartmuseum 58 | interval: 10s 59 | env: 60 | open: 61 | DISABLE_API: false 62 | 63 | argo-workflows: 64 | images: 65 | pullPolicy: IfNotPresent 66 | 67 | init: 68 | serviceAccount: *serviceAccount 69 | 70 | workflow: 71 | # namespace: *namespace 72 | serviceAccount: 73 | name: *serviceAccount 74 | rbac: 75 | enabled: false 76 | 77 | controller: 78 | serviceAccount: 79 | create: false 80 | name: *serviceAccount 81 | name: workflow-controller 82 | # workflowNamespaces: 83 | # - *namespace 84 | containerRuntimeExecutor: k8sapi # Most Secure - https://argoproj.github.io/argo-workflows/workflow-executors/#kubernetes-api-k8sapi 85 | 86 | server: 87 | serviceAccount: 88 | create: false 89 | name: *serviceAccount 90 | enabled: true 91 | name: argo-server 92 | 93 | helm-controller: 94 | concurrent: 5 95 | enabled: true 96 | serviceAccount: 97 | create: false 98 | name: *serviceAccount 99 | source-controller: 100 | serviceAccount: 101 | create: false 102 | name: *serviceAccount 103 | 104 | keptn: 105 | enabled: false 106 | continuous-delivery: 107 | enabled: true 108 | control-plane: 109 | apiGatewayNginx: 110 | type: LoadBalancer 111 | 112 | keptn-addons: 113 | enabled: false 114 | prometheus: 115 | namespace: orkestra 116 | server: 117 | name: prometheus-server 118 | port: 80 119 | -------------------------------------------------------------------------------- /config/certmanager/certificate.yaml: -------------------------------------------------------------------------------- 1 | # The following manifests contain a self-signed issuer CR and a certificate CR. 2 | # More document can be found at https://docs.cert-manager.io 3 | # WARNING: Targets CertManager 0.11 check https://docs.cert-manager.io/en/latest/tasks/upgrading/index.html for 4 | # breaking changes 5 | apiVersion: cert-manager.io/v1alpha2 6 | kind: Issuer 7 | metadata: 8 | name: selfsigned-issuer 9 | namespace: system 10 | spec: 11 | selfSigned: {} 12 | --- 13 | apiVersion: cert-manager.io/v1alpha2 14 | kind: Certificate 15 | metadata: 16 | name: serving-cert # this name should match the one appeared in kustomizeconfig.yaml 17 | namespace: system 18 | spec: 19 | # $(SERVICE_NAME) and $(SERVICE_NAMESPACE) will be substituted by kustomize 20 | dnsNames: 21 | - $(SERVICE_NAME).$(SERVICE_NAMESPACE).svc 22 | - $(SERVICE_NAME).$(SERVICE_NAMESPACE).svc.cluster.local 23 | issuerRef: 24 | kind: Issuer 25 | name: selfsigned-issuer 26 | secretName: webhook-server-cert # this secret will not be prefixed, since it's not managed by kustomize 27 | -------------------------------------------------------------------------------- /config/certmanager/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - certificate.yaml 3 | 4 | configurations: 5 | - kustomizeconfig.yaml 6 | -------------------------------------------------------------------------------- /config/certmanager/kustomizeconfig.yaml: -------------------------------------------------------------------------------- 1 | # This configuration is for teaching kustomize how to update name ref and var substitution 2 | nameReference: 3 | - kind: Issuer 4 | group: cert-manager.io 5 | fieldSpecs: 6 | - kind: Certificate 7 | group: cert-manager.io 8 | path: spec/issuerRef/name 9 | 10 | varReference: 11 | - kind: Certificate 12 | group: cert-manager.io 13 | path: spec/commonName 14 | - kind: Certificate 15 | group: cert-manager.io 16 | path: spec/dnsNames 17 | -------------------------------------------------------------------------------- /config/crd/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # This kustomization.yaml is not intended to be run by itself, 2 | # since it depends on service name and namespace that are out of this kustomize package. 3 | # It should be run by config/default 4 | resources: 5 | - bases/orkestra.azure.microsoft.com_applicationgroups.yaml 6 | # +kubebuilder:scaffold:crdkustomizeresource 7 | 8 | patchesStrategicMerge: 9 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. 10 | # patches here are for enabling the conversion webhook for each CRD 11 | #- patches/webhook_in_applicationgroups.yaml 12 | # +kubebuilder:scaffold:crdkustomizewebhookpatch 13 | 14 | # [CERTMANAGER] To enable webhook, uncomment all the sections with [CERTMANAGER] prefix. 15 | # patches here are for enabling the CA injection for each CRD 16 | #- patches/cainjection_in_applicationgroups.yaml 17 | # +kubebuilder:scaffold:crdkustomizecainjectionpatch 18 | 19 | # the following config is for teaching kustomize how to do kustomization for CRDs. 20 | configurations: 21 | - kustomizeconfig.yaml 22 | -------------------------------------------------------------------------------- /config/crd/kustomizeconfig.yaml: -------------------------------------------------------------------------------- 1 | # This file is for teaching kustomize how to substitute name and namespace reference in CRD 2 | nameReference: 3 | - kind: Service 4 | version: v1 5 | fieldSpecs: 6 | - kind: CustomResourceDefinition 7 | group: apiextensions.k8s.io 8 | path: spec/conversion/webhookClientConfig/service/name 9 | 10 | namespace: 11 | - kind: CustomResourceDefinition 12 | group: apiextensions.k8s.io 13 | path: spec/conversion/webhookClientConfig/service/namespace 14 | create: false 15 | 16 | varReference: 17 | - path: metadata/annotations 18 | -------------------------------------------------------------------------------- /config/crd/patches/cainjection_in_appgroups.yaml: -------------------------------------------------------------------------------- 1 | # The following patch adds a directive for certmanager to inject CA into the CRD 2 | # CRD conversion requires k8s 1.13 or later. 3 | apiVersion: apiextensions.k8s.io/v1beta1 4 | kind: CustomResourceDefinition 5 | metadata: 6 | annotations: 7 | cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 8 | name: applicationgroups.orkestra.azure.microsoft.com 9 | -------------------------------------------------------------------------------- /config/crd/patches/webhook_in_appgroups.yaml: -------------------------------------------------------------------------------- 1 | # The following patch enables conversion webhook for CRD 2 | # CRD conversion requires k8s 1.13 or later. 3 | apiVersion: apiextensions.k8s.io/v1beta1 4 | kind: CustomResourceDefinition 5 | metadata: 6 | name: applicationgroups.orkestra.azure.microsoft.com 7 | spec: 8 | conversion: 9 | strategy: Webhook 10 | webhookClientConfig: 11 | # this is "\n" used as a placeholder, otherwise it will be rejected by the apiserver for being blank, 12 | # but we're going to set it later using the cert-manager (or potentially a patch if not using cert-manager) 13 | caBundle: Cg== 14 | service: 15 | namespace: system 16 | name: webhook-service 17 | path: /convert 18 | -------------------------------------------------------------------------------- /config/default/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # Adds namespace to all resources. 2 | namespace: orkestra-system 3 | 4 | # Value of this field is prepended to the 5 | # names of all resources, e.g. a deployment named 6 | # "wordpress" becomes "alices-wordpress". 7 | # Note that it should also match with the prefix (text before '-') of the namespace 8 | # field above. 9 | namePrefix: orkestra- 10 | 11 | # Labels to add to all resources and selectors. 12 | #commonLabels: 13 | # someName: someValue 14 | 15 | bases: 16 | - ../crd 17 | - ../rbac 18 | - ../manager 19 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in 20 | # crd/kustomization.yaml 21 | #- ../webhook 22 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. 23 | #- ../certmanager 24 | # [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. 25 | #- ../prometheus 26 | 27 | patchesStrategicMerge: 28 | # Protect the /metrics endpoint by putting it behind auth. 29 | # If you want your controller-manager to expose the /metrics 30 | # endpoint w/o any authn/z, please comment the following line. 31 | - manager_auth_proxy_patch.yaml 32 | 33 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in 34 | # crd/kustomization.yaml 35 | #- manager_webhook_patch.yaml 36 | 37 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 38 | # Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks. 39 | # 'CERTMANAGER' needs to be enabled to use ca injection 40 | #- webhookcainjection_patch.yaml 41 | 42 | # the following config is for teaching kustomize how to do var substitution 43 | vars: 44 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix. 45 | #- name: CERTIFICATE_NAMESPACE # namespace of the certificate CR 46 | # objref: 47 | # kind: Certificate 48 | # group: cert-manager.io 49 | # version: v1alpha2 50 | # name: serving-cert # this name should match the one in certificate.yaml 51 | # fieldref: 52 | # fieldpath: metadata.namespace 53 | #- name: CERTIFICATE_NAME 54 | # objref: 55 | # kind: Certificate 56 | # group: cert-manager.io 57 | # version: v1alpha2 58 | # name: serving-cert # this name should match the one in certificate.yaml 59 | #- name: SERVICE_NAMESPACE # namespace of the service 60 | # objref: 61 | # kind: Service 62 | # version: v1 63 | # name: webhook-service 64 | # fieldref: 65 | # fieldpath: metadata.namespace 66 | #- name: SERVICE_NAME 67 | # objref: 68 | # kind: Service 69 | # version: v1 70 | # name: webhook-service 71 | -------------------------------------------------------------------------------- /config/default/manager_auth_proxy_patch.yaml: -------------------------------------------------------------------------------- 1 | # This patch inject a sidecar container which is a HTTP proxy for the 2 | # controller manager, it performs RBAC authorization against the Kubernetes API using SubjectAccessReviews. 3 | apiVersion: apps/v1 4 | kind: Deployment 5 | metadata: 6 | name: controller-manager 7 | namespace: system 8 | spec: 9 | template: 10 | spec: 11 | containers: 12 | - name: kube-rbac-proxy 13 | image: gcr.io/kubebuilder/kube-rbac-proxy:v0.5.0 14 | args: 15 | - "--secure-listen-address=0.0.0.0:8443" 16 | - "--upstream=http://127.0.0.1:8080/" 17 | - "--logtostderr=true" 18 | - "--v=10" 19 | ports: 20 | - containerPort: 8443 21 | name: https 22 | - name: manager 23 | args: 24 | - "--metrics-addr=127.0.0.1:8080" 25 | - "--enable-leader-election" 26 | -------------------------------------------------------------------------------- /config/default/manager_webhook_patch.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: controller-manager 5 | namespace: system 6 | spec: 7 | template: 8 | spec: 9 | containers: 10 | - name: manager 11 | ports: 12 | - containerPort: 9443 13 | name: webhook-server 14 | protocol: TCP 15 | volumeMounts: 16 | - mountPath: /tmp/k8s-webhook-server/serving-certs 17 | name: cert 18 | readOnly: true 19 | volumes: 20 | - name: cert 21 | secret: 22 | defaultMode: 420 23 | secretName: webhook-server-cert 24 | -------------------------------------------------------------------------------- /config/default/webhookcainjection_patch.yaml: -------------------------------------------------------------------------------- 1 | # This patch add annotation to admission webhook config and 2 | # the variables $(CERTIFICATE_NAMESPACE) and $(CERTIFICATE_NAME) will be substituted by kustomize. 3 | apiVersion: admissionregistration.k8s.io/v1beta1 4 | kind: MutatingWebhookConfiguration 5 | metadata: 6 | name: mutating-webhook-configuration 7 | annotations: 8 | cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 9 | --- 10 | apiVersion: admissionregistration.k8s.io/v1beta1 11 | kind: ValidatingWebhookConfiguration 12 | metadata: 13 | name: validating-webhook-configuration 14 | annotations: 15 | cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 16 | -------------------------------------------------------------------------------- /config/manager/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - manager.yaml 3 | apiVersion: kustomize.config.k8s.io/v1beta1 4 | kind: Kustomization 5 | images: 6 | - name: controller 7 | newName: controller 8 | newTag: latest 9 | -------------------------------------------------------------------------------- /config/manager/manager.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | labels: 5 | control-plane: controller-manager 6 | name: system 7 | --- 8 | apiVersion: apps/v1 9 | kind: Deployment 10 | metadata: 11 | name: controller-manager 12 | namespace: system 13 | labels: 14 | control-plane: controller-manager 15 | spec: 16 | selector: 17 | matchLabels: 18 | control-plane: controller-manager 19 | replicas: 1 20 | template: 21 | metadata: 22 | labels: 23 | control-plane: controller-manager 24 | spec: 25 | containers: 26 | - command: 27 | - /manager 28 | args: 29 | - --enable-leader-election 30 | image: controller:latest 31 | name: manager 32 | resources: 33 | limits: 34 | cpu: 100m 35 | memory: 30Mi 36 | requests: 37 | cpu: 100m 38 | memory: 20Mi 39 | terminationGracePeriodSeconds: 10 40 | -------------------------------------------------------------------------------- /config/prometheus/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - monitor.yaml 3 | -------------------------------------------------------------------------------- /config/prometheus/monitor.yaml: -------------------------------------------------------------------------------- 1 | 2 | # Prometheus Monitor Service (Metrics) 3 | apiVersion: monitoring.coreos.com/v1 4 | kind: ServiceMonitor 5 | metadata: 6 | labels: 7 | control-plane: controller-manager 8 | name: controller-manager-metrics-monitor 9 | namespace: system 10 | spec: 11 | endpoints: 12 | - path: /metrics 13 | port: https 14 | selector: 15 | matchLabels: 16 | control-plane: controller-manager 17 | -------------------------------------------------------------------------------- /config/rbac/appgroup_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to edit applicationgroups. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: applicationgroup-editor-role 6 | rules: 7 | - apiGroups: 8 | - orkestra.azure.microsoft.com 9 | resources: 10 | - applicationgroups 11 | verbs: 12 | - create 13 | - delete 14 | - get 15 | - list 16 | - patch 17 | - update 18 | - watch 19 | - apiGroups: 20 | - orkestra.azure.microsoft.com 21 | resources: 22 | - applicationgroups/status 23 | verbs: 24 | - get 25 | -------------------------------------------------------------------------------- /config/rbac/appgroup_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to view applicationgroups. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: applicationgroup-viewer-role 6 | rules: 7 | - apiGroups: 8 | - orkestra.azure.microsoft.com 9 | resources: 10 | - applicationgroups 11 | verbs: 12 | - get 13 | - list 14 | - watch 15 | - apiGroups: 16 | - orkestra.azure.microsoft.com 17 | resources: 18 | - applicationgroups/status 19 | verbs: 20 | - get 21 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_client_clusterrole.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1beta1 2 | kind: ClusterRole 3 | metadata: 4 | name: metrics-reader 5 | rules: 6 | - nonResourceURLs: ["/metrics"] 7 | verbs: ["get"] 8 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: proxy-role 5 | rules: 6 | - apiGroups: ["authentication.k8s.io"] 7 | resources: 8 | - tokenreviews 9 | verbs: ["create"] 10 | - apiGroups: ["authorization.k8s.io"] 11 | resources: 12 | - subjectaccessreviews 13 | verbs: ["create"] 14 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | name: proxy-rolebinding 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: ClusterRole 8 | name: proxy-role 9 | subjects: 10 | - kind: ServiceAccount 11 | name: default 12 | namespace: system 13 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | control-plane: controller-manager 6 | name: controller-manager-metrics-service 7 | namespace: system 8 | spec: 9 | ports: 10 | - name: https 11 | port: 8443 12 | targetPort: https 13 | selector: 14 | control-plane: controller-manager 15 | -------------------------------------------------------------------------------- /config/rbac/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - role.yaml 3 | - role_binding.yaml 4 | - leader_election_role.yaml 5 | - leader_election_role_binding.yaml 6 | # Comment the following 4 lines if you want to disable 7 | # the auth proxy (https://github.com/brancz/kube-rbac-proxy) 8 | # which protects your /metrics endpoint. 9 | - auth_proxy_service.yaml 10 | - auth_proxy_role.yaml 11 | - auth_proxy_role_binding.yaml 12 | - auth_proxy_client_clusterrole.yaml 13 | -------------------------------------------------------------------------------- /config/rbac/leader_election_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions to do leader election. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: Role 4 | metadata: 5 | name: leader-election-role 6 | rules: 7 | - apiGroups: 8 | - "" 9 | resources: 10 | - configmaps 11 | verbs: 12 | - get 13 | - list 14 | - watch 15 | - create 16 | - update 17 | - patch 18 | - delete 19 | - apiGroups: 20 | - "" 21 | resources: 22 | - configmaps/status 23 | verbs: 24 | - get 25 | - update 26 | - patch 27 | - apiGroups: 28 | - "" 29 | resources: 30 | - events 31 | verbs: 32 | - create 33 | -------------------------------------------------------------------------------- /config/rbac/leader_election_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: RoleBinding 3 | metadata: 4 | name: leader-election-rolebinding 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: Role 8 | name: leader-election-role 9 | subjects: 10 | - kind: ServiceAccount 11 | name: default 12 | namespace: system 13 | -------------------------------------------------------------------------------- /config/rbac/role.yaml: -------------------------------------------------------------------------------- 1 | 2 | --- 3 | apiVersion: rbac.authorization.k8s.io/v1 4 | kind: ClusterRole 5 | metadata: 6 | creationTimestamp: null 7 | name: manager-role 8 | rules: 9 | - apiGroups: 10 | - argoproj.io 11 | resources: 12 | - workflows 13 | verbs: 14 | - create 15 | - delete 16 | - get 17 | - list 18 | - patch 19 | - update 20 | - watch 21 | - apiGroups: 22 | - argoproj.io 23 | resources: 24 | - workflows/status 25 | verbs: 26 | - get 27 | - patch 28 | - update 29 | - apiGroups: 30 | - orkestra.azure.microsoft.com 31 | resources: 32 | - applicationgroups 33 | verbs: 34 | - create 35 | - delete 36 | - get 37 | - list 38 | - patch 39 | - update 40 | - watch 41 | - apiGroups: 42 | - orkestra.azure.microsoft.com 43 | resources: 44 | - applicationgroups/status 45 | verbs: 46 | - get 47 | - patch 48 | - update 49 | -------------------------------------------------------------------------------- /config/rbac/role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | name: manager-rolebinding 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: ClusterRole 8 | name: manager-role 9 | subjects: 10 | - kind: ServiceAccount 11 | name: default 12 | namespace: system 13 | -------------------------------------------------------------------------------- /config/samples/bookinfo.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: orkestra.azure.microsoft.com/v1alpha1 2 | kind: ApplicationGroup 3 | metadata: 4 | name: bookinfo 5 | spec: 6 | interval: 1m 7 | applications: 8 | - name: ambassador 9 | dependencies: [] 10 | spec: 11 | chart: 12 | url: "https://www.getambassador.io/helm" 13 | name: ambassador 14 | version: 6.6.0 15 | # Authorization Object Reference (Kind: Secret) 16 | # this secret holds all auth information and credentials 17 | # to register a protected/private helm registry 18 | # Uncomment the lines below if you wish to register a private helm registry 19 | # and make sure you deploy the Secret object to the given namespace/name 20 | # as well 21 | # authRef: 22 | # name: 23 | # namespace: 24 | release: 25 | targetNamespace: ambassador 26 | values: 27 | service: 28 | type: ClusterIP 29 | - name: bookinfo 30 | dependencies: [ambassador] 31 | spec: 32 | chart: 33 | url: "https://nitishm.github.io/charts" 34 | name: bookinfo 35 | version: v1 36 | subcharts: 37 | - name: productpage 38 | dependencies: [reviews] 39 | - name: reviews 40 | dependencies: [details, ratings] 41 | - name: ratings 42 | dependencies: [] 43 | - name: details 44 | dependencies: [] 45 | release: 46 | targetNamespace: bookinfo 47 | values: 48 | productpage: 49 | replicaCount: 1 50 | details: 51 | replicaCount: 1 52 | reviews: 53 | replicaCount: 1 54 | ratings: 55 | replicaCount: 1 56 | -------------------------------------------------------------------------------- /config/webhook/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - manifests.yaml 3 | - service.yaml 4 | 5 | configurations: 6 | - kustomizeconfig.yaml 7 | -------------------------------------------------------------------------------- /config/webhook/kustomizeconfig.yaml: -------------------------------------------------------------------------------- 1 | # the following config is for teaching kustomize where to look at when substituting vars. 2 | # It requires kustomize v2.1.0 or newer to work properly. 3 | nameReference: 4 | - kind: Service 5 | version: v1 6 | fieldSpecs: 7 | - kind: MutatingWebhookConfiguration 8 | group: admissionregistration.k8s.io 9 | path: webhooks/clientConfig/service/name 10 | - kind: ValidatingWebhookConfiguration 11 | group: admissionregistration.k8s.io 12 | path: webhooks/clientConfig/service/name 13 | 14 | namespace: 15 | - kind: MutatingWebhookConfiguration 16 | group: admissionregistration.k8s.io 17 | path: webhooks/clientConfig/service/namespace 18 | create: true 19 | - kind: ValidatingWebhookConfiguration 20 | group: admissionregistration.k8s.io 21 | path: webhooks/clientConfig/service/namespace 22 | create: true 23 | 24 | varReference: 25 | - path: metadata/annotations 26 | -------------------------------------------------------------------------------- /config/webhook/service.yaml: -------------------------------------------------------------------------------- 1 | 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | name: webhook-service 6 | namespace: system 7 | spec: 8 | ports: 9 | - port: 443 10 | targetPort: 9443 11 | selector: 12 | control-plane: controller-manager 13 | -------------------------------------------------------------------------------- /controllers/appgroup_controller.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package controllers 5 | 6 | import ( 7 | "context" 8 | "errors" 9 | "github.com/Azure/Orkestra/pkg/meta" 10 | 11 | "github.com/Azure/Orkestra/pkg/helpers" 12 | 13 | "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" 14 | 15 | "github.com/Azure/Orkestra/api/v1alpha1" 16 | "github.com/Azure/Orkestra/pkg/registry" 17 | "github.com/Azure/Orkestra/pkg/workflow" 18 | "github.com/go-logr/logr" 19 | kerrors "k8s.io/apimachinery/pkg/api/errors" 20 | "k8s.io/apimachinery/pkg/runtime" 21 | "k8s.io/client-go/tools/record" 22 | ctrl "sigs.k8s.io/controller-runtime" 23 | "sigs.k8s.io/controller-runtime/pkg/client" 24 | "sigs.k8s.io/controller-runtime/pkg/predicate" 25 | ) 26 | 27 | // ApplicationGroupReconciler reconciles a ApplicationGroup object 28 | type ApplicationGroupReconciler struct { 29 | client.Client 30 | Log logr.Logger 31 | Scheme *runtime.Scheme 32 | 33 | // RegistryClient interacts with the helm registries to pull and push charts 34 | RegistryClient *registry.Client 35 | 36 | // StagingRepoName is the nickname for the repository used for staging artifacts before being deployed using the HelmRelease object 37 | StagingRepoName string 38 | 39 | WorkflowClientBuilder *workflow.Builder 40 | 41 | // TargetDir to stage the charts before pushing 42 | TargetDir string 43 | 44 | // Recorder generates kubernetes events 45 | Recorder record.EventRecorder 46 | 47 | // DisableRemediation for debugging purposes 48 | // The object and associated Workflow, HelmReleases will 49 | // not be cleaned up 50 | DisableRemediation bool 51 | 52 | // CleanupDownloadedCharts signals the controller to delete the 53 | // fetched charts after they have been repackaged and pushed to staging 54 | CleanupDownloadedCharts bool 55 | } 56 | 57 | // +kubebuilder:rbac:groups=orkestra.azure.microsoft.com,resources=applicationgroups,verbs=get;list;watch;create;update;patch;delete 58 | // +kubebuilder:rbac:groups=orkestra.azure.microsoft.com,resources=applicationgroups/status,verbs=get;update;patch 59 | 60 | func (r *ApplicationGroupReconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ctrl.Result, err error) { 61 | appGroup := &v1alpha1.ApplicationGroup{} 62 | 63 | logr := r.Log.WithValues(v1alpha1.AppGroupNameKey, req.NamespacedName.Name) 64 | 65 | if err := r.Get(ctx, req.NamespacedName, appGroup); err != nil { 66 | if kerrors.IsNotFound(err) { 67 | logr.V(3).Info("skip reconciliation since AppGroup instance not found on the cluster") 68 | return ctrl.Result{}, nil 69 | } 70 | logr.Error(err, "unable to fetch ApplicationGroup instance") 71 | return ctrl.Result{}, err 72 | } 73 | patch := client.MergeFrom(appGroup.DeepCopy()) 74 | 75 | statusHelper := &helpers.StatusHelper{ 76 | Client: r.Client, 77 | Logger: logr, 78 | PatchFrom: patch, 79 | Recorder: r.Recorder, 80 | } 81 | reconcileHelper := helpers.ReconcileHelper{ 82 | Client: r.Client, 83 | Logger: logr, 84 | Instance: appGroup, 85 | WorkflowClientBuilder: r.WorkflowClientBuilder, 86 | RegistryClient: r.RegistryClient, 87 | RegistryOptions: helpers.RegistryClientOptions{ 88 | StagingRepoName: r.StagingRepoName, 89 | TargetDir: r.TargetDir, 90 | CleanupDownloadedCharts: r.CleanupDownloadedCharts, 91 | }, 92 | StatusHelper: statusHelper, 93 | } 94 | 95 | // Patch the status before returning from the reconcile loop 96 | defer func() { 97 | // Update the err value which is scoped outside the defer 98 | patchErr := statusHelper.PatchStatus(ctx, appGroup) 99 | if err == nil { 100 | err = patchErr 101 | } 102 | }() 103 | 104 | if !appGroup.DeletionTimestamp.IsZero() { 105 | statusHelper.MarkTerminating(appGroup) 106 | if err := reconcileHelper.Reverse(ctx); errors.Is(err, meta.ErrForwardWorkflowNotFound) { 107 | controllerutil.RemoveFinalizer(appGroup, v1alpha1.AppGroupFinalizer) 108 | if err := r.Patch(ctx, appGroup, patch); err != nil { 109 | logr.Error(err, "failed to patch the release to remove the appgroup finalizer") 110 | return ctrl.Result{}, err 111 | } 112 | } else if err != nil { 113 | logr.Error(err, "failed to generate the reverse workflow") 114 | return ctrl.Result{}, err 115 | } 116 | return ctrl.Result{}, nil 117 | } 118 | // Add finalizer if it doesn't already exist 119 | if !controllerutil.ContainsFinalizer(appGroup, v1alpha1.AppGroupFinalizer) { 120 | controllerutil.AddFinalizer(appGroup, v1alpha1.AppGroupFinalizer) 121 | if err := r.Patch(ctx, appGroup, patch); err != nil { 122 | logr.Error(err, "failed to patch the release with the appgroup finalizer") 123 | return ctrl.Result{}, err 124 | } 125 | } 126 | 127 | // If we have not yet seen this generation, we should reconcile and create the workflow 128 | // Only do this if we have successfully completed a rollback 129 | if appGroup.Generation != appGroup.Status.ObservedGeneration { 130 | // Change the app group spec into a progressing state 131 | statusHelper.MarkProgressing(appGroup) 132 | if err := reconcileHelper.CreateOrUpdate(ctx); err != nil { 133 | logr.Error(err, "failed to reconcile creating or updating the appgroup") 134 | return ctrl.Result{}, err 135 | } 136 | appGroup.Status.ObservedGeneration = appGroup.Generation 137 | } 138 | return ctrl.Result{}, nil 139 | } 140 | 141 | func (r *ApplicationGroupReconciler) SetupWithManager(mgr ctrl.Manager) error { 142 | return ctrl.NewControllerManagedBy(mgr). 143 | For(&v1alpha1.ApplicationGroup{}). 144 | WithEventFilter(predicate.GenerationChangedPredicate{}). 145 | Complete(r) 146 | } 147 | -------------------------------------------------------------------------------- /controllers/suite_test.go: -------------------------------------------------------------------------------- 1 | package controllers_test 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "os" 7 | "testing" 8 | "time" 9 | 10 | v1alpha1 "github.com/Azure/Orkestra/api/v1alpha1" 11 | "github.com/Azure/Orkestra/controllers" 12 | "github.com/Azure/Orkestra/pkg/registry" 13 | "github.com/Azure/Orkestra/pkg/workflow" 14 | v1alpha13 "github.com/argoproj/argo-workflows/v3/pkg/apis/workflow/v1alpha1" 15 | fluxhelmv2beta1 "github.com/fluxcd/helm-controller/api/v2beta1" 16 | "github.com/onsi/ginkgo/config" 17 | "github.com/onsi/gomega/gexec" 18 | 19 | . "github.com/onsi/ginkgo" 20 | . "github.com/onsi/gomega" 21 | 22 | "k8s.io/client-go/kubernetes/scheme" 23 | "k8s.io/client-go/rest" 24 | ctrl "sigs.k8s.io/controller-runtime" 25 | "sigs.k8s.io/controller-runtime/pkg/client" 26 | "sigs.k8s.io/controller-runtime/pkg/envtest" 27 | "sigs.k8s.io/controller-runtime/pkg/envtest/printer" 28 | logf "sigs.k8s.io/controller-runtime/pkg/log" 29 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 30 | // +kubebuilder:scaffold:imports 31 | ) 32 | 33 | // These tests use Ginkgo (BDD-style Go testing framework). Refer to 34 | // http://onsi.github.io/ginkgo/ to learn more about Ginkgo. 35 | 36 | const ( 37 | portForwardStagingRepoURL = "http://127.0.0.1:8080" 38 | inClusterstagingRepoURL = "http://orkestra-chartmuseum.orkestra:8080" 39 | ) 40 | 41 | var ( 42 | k8sClient client.Client 43 | tempChartStoreTargetDir string 44 | testEnv *envtest.Environment 45 | ) 46 | 47 | func init() { 48 | tmp := os.TempDir() 49 | tempChartStoreTargetDir = tmp 50 | } 51 | 52 | func TestAppGroupController(t *testing.T) { 53 | RegisterFailHandler(Fail) 54 | RunSpecsWithDefaultAndCustomReporters(t, "ApplicationGroup Controller Suite", []Reporter{printer.NewlineReporter{}}) 55 | } 56 | 57 | var _ = BeforeSuite(func() { 58 | var ( 59 | cfg *rest.Config 60 | err error 61 | k8sManager ctrl.Manager 62 | ) 63 | 64 | logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) 65 | rand.Seed(time.Now().UnixNano()) 66 | 67 | By("bootstrapping test environment") 68 | testEnv = &envtest.Environment{ 69 | UseExistingCluster: boolToBoolPtr(true), 70 | } 71 | 72 | cfg, err = testEnv.Start() 73 | Expect(err).ToNot(HaveOccurred()) 74 | Expect(cfg).ToNot(BeNil()) 75 | 76 | err = scheme.AddToScheme(scheme.Scheme) 77 | Expect(err).ToNot(HaveOccurred()) 78 | err = v1alpha1.AddToScheme(scheme.Scheme) 79 | Expect(err).ToNot(HaveOccurred()) 80 | err = v1alpha13.AddToScheme(scheme.Scheme) 81 | Expect(err).ToNot(HaveOccurred()) 82 | err = fluxhelmv2beta1.AddToScheme(scheme.Scheme) 83 | Expect(err).ToNot(HaveOccurred()) 84 | 85 | // +kubebuilder:scaffold:scheme 86 | 87 | k8sManager, err = ctrl.NewManager(cfg, ctrl.Options{ 88 | Scheme: scheme.Scheme, 89 | MetricsBindAddress: fmt.Sprintf(":%d", 8081+config.GinkgoConfig.ParallelNode), 90 | Port: 9443, 91 | }) 92 | Expect(err).ToNot(HaveOccurred()) 93 | 94 | rc, err := registry.NewClient(ctrl.Log, registry.TargetDir(tempChartStoreTargetDir)) 95 | Expect(err).ToNot(HaveOccurred()) 96 | 97 | // Register the staging helm repository/registry 98 | err = rc.AddRepo(®istry.Config{ 99 | Name: "staging", 100 | URL: portForwardStagingRepoURL, 101 | }) 102 | Expect(err).ToNot(HaveOccurred()) 103 | 104 | baseLogger := ctrl.Log.WithName("controllers").WithName("ApplicationGroup") 105 | 106 | workflowClientBuilder := workflow.NewBuilder(k8sManager.GetClient(), baseLogger).WithStagingRepo(inClusterstagingRepoURL).WithParallelism(10).InNamespace("orkestra") 107 | 108 | err = (&controllers.ApplicationGroupReconciler{ 109 | Client: k8sManager.GetClient(), 110 | Log: baseLogger, 111 | Scheme: k8sManager.GetScheme(), 112 | RegistryClient: rc, 113 | StagingRepoName: "staging", 114 | WorkflowClientBuilder: workflowClientBuilder, 115 | TargetDir: tempChartStoreTargetDir, 116 | Recorder: k8sManager.GetEventRecorderFor("appgroup-controller"), 117 | DisableRemediation: false, 118 | CleanupDownloadedCharts: false, 119 | }).SetupWithManager(k8sManager) 120 | Expect(err).ToNot(HaveOccurred()) 121 | 122 | err = (&controllers.WorkflowStatusReconciler{ 123 | Client: k8sManager.GetClient(), 124 | Log: baseLogger, 125 | Scheme: k8sManager.GetScheme(), 126 | WorkflowClientBuilder: workflowClientBuilder, 127 | Recorder: k8sManager.GetEventRecorderFor("appgroup-controller"), 128 | }).SetupWithManager(k8sManager) 129 | Expect(err).ToNot(HaveOccurred()) 130 | 131 | go func() { 132 | err = k8sManager.Start(ctrl.SetupSignalHandler()) 133 | Expect(err).ToNot(HaveOccurred()) 134 | }() 135 | 136 | k8sClient = k8sManager.GetClient() 137 | Expect(k8sClient).ToNot(BeNil()) 138 | }, 60) 139 | 140 | var _ = AfterSuite(func() { 141 | By("tearing down the test environment") 142 | gexec.KillAndWait(5 * time.Second) 143 | 144 | if testEnv != nil { 145 | err := testEnv.Stop() 146 | Expect(err).ToNot(HaveOccurred()) 147 | } 148 | }) 149 | -------------------------------------------------------------------------------- /controllers/utils_test.go: -------------------------------------------------------------------------------- 1 | package controllers_test 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "time" 7 | 8 | "github.com/Azure/Orkestra/api/v1alpha1" 9 | "github.com/Azure/Orkestra/pkg/meta" 10 | fluxhelmv2beta1 "github.com/fluxcd/helm-controller/api/v2beta1" 11 | apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 12 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 13 | ) 14 | 15 | const ( 16 | bookinfo = "bookinfo" 17 | ambassador = "ambassador" 18 | podinfo = "podinfo" 19 | 20 | ambassadorChartURL = "https://nitishm.github.io/charts" 21 | ambassadorOldChartVersion = "6.6.0" 22 | ambassadorChartVersion = "6.7.9" 23 | 24 | bookinfoChartURL = "https://nitishm.github.io/charts" 25 | bookinfoChartVersion = "v2" 26 | 27 | podinfoChartURL = "https://stefanprodan.github.io/podinfo" 28 | podinfoChartVersion = "5.2.1" 29 | ) 30 | 31 | var ( 32 | defaultDuration = metav1.Duration{Duration: time.Minute * 5} // treat as const 33 | letterRunes = []rune("abcdefghijklmnopqrstuvwxyz1234567890") // treat as const 34 | ) 35 | 36 | func isAllHelmReleasesInReadyState(helmReleases []fluxhelmv2beta1.HelmRelease) bool { 37 | allReady := true 38 | for _, release := range helmReleases { 39 | condition := meta.GetResourceCondition(&release, meta.ReadyCondition) 40 | if condition.Reason == meta.SucceededReason { 41 | allReady = false 42 | } 43 | } 44 | return allReady 45 | } 46 | 47 | func addApplication(appGroup v1alpha1.ApplicationGroup, app v1alpha1.Application) v1alpha1.ApplicationGroup { 48 | appGroup.Spec.Applications = append(appGroup.Spec.Applications, app) 49 | return appGroup 50 | } 51 | 52 | func defaultAppGroup(groupName, groupNamespace, targetNamespace string) *v1alpha1.ApplicationGroup { 53 | g := &v1alpha1.ApplicationGroup{ 54 | ObjectMeta: metav1.ObjectMeta{ 55 | Name: groupName, 56 | Namespace: groupNamespace, 57 | }, 58 | } 59 | g.Spec.Applications = make([]v1alpha1.Application, 0) 60 | g.Spec.Applications = append(g.Spec.Applications, bookinfoApplication(targetNamespace, ambassador), ambassadorApplication(targetNamespace)) 61 | return g 62 | } 63 | 64 | func smallAppGroup(groupName, groupNamespace, targetNamespace string) *v1alpha1.ApplicationGroup { 65 | g := &v1alpha1.ApplicationGroup{ 66 | ObjectMeta: metav1.ObjectMeta{ 67 | Name: groupName, 68 | Namespace: groupNamespace, 69 | }, 70 | } 71 | g.Spec.Applications = make([]v1alpha1.Application, 0) 72 | g.Spec.Applications = append(g.Spec.Applications, podinfoApplication(targetNamespace)) 73 | return g 74 | } 75 | 76 | func createUniqueAppGroupName(name string) string { 77 | return name + "-" + getRandomStringRunes(10) 78 | } 79 | 80 | func getRandomStringRunes(n int) string { 81 | b := make([]rune, n) 82 | for i := range b { 83 | b[i] = letterRunes[rand.Intn(len(letterRunes))] 84 | } 85 | return string(b) 86 | } 87 | 88 | func boolToBoolPtr(in bool) *bool { 89 | return &in 90 | } 91 | 92 | func ambassadorApplication(targetNamespace string, dependencies ...string) v1alpha1.Application { 93 | values := []byte(fmt.Sprintf(`{ 94 | "nameOverride": "%s", 95 | "service": { 96 | "type": "ClusterIP" 97 | }, 98 | "scope": { 99 | "singleNamespace": true 100 | } 101 | }`, targetNamespace)) 102 | return v1alpha1.Application{ 103 | DAG: v1alpha1.DAG{ 104 | Name: ambassador, 105 | Dependencies: dependencies, 106 | }, 107 | Spec: v1alpha1.ApplicationSpec{ 108 | Chart: &v1alpha1.ChartRef{ 109 | URL: ambassadorChartURL, 110 | Name: ambassador, 111 | Version: ambassadorChartVersion, 112 | }, 113 | Release: &v1alpha1.Release{ 114 | Timeout: &metav1.Duration{Duration: time.Minute * 10}, 115 | TargetNamespace: targetNamespace, 116 | Values: &apiextensionsv1.JSON{ 117 | Raw: values, 118 | }, 119 | Interval: defaultDuration, 120 | }, 121 | }, 122 | } 123 | } 124 | 125 | func bookinfoApplication(targetNamespace string, dependencies ...string) v1alpha1.Application { 126 | values := []byte(`{ 127 | "productpage": { 128 | "replicaCount": 1 129 | }, 130 | "details": { 131 | "replicaCount": 1 132 | }, 133 | "reviews": { 134 | "replicaCount": 1 135 | }, 136 | "ratings": { 137 | "replicaCount": 1 138 | } 139 | }`) 140 | return v1alpha1.Application{ 141 | DAG: v1alpha1.DAG{ 142 | Name: bookinfo, 143 | Dependencies: dependencies, 144 | }, 145 | Spec: v1alpha1.ApplicationSpec{ 146 | Chart: &v1alpha1.ChartRef{ 147 | URL: bookinfoChartURL, 148 | Name: bookinfo, 149 | Version: bookinfoChartVersion, 150 | }, 151 | Release: &v1alpha1.Release{ 152 | TargetNamespace: targetNamespace, 153 | Values: &apiextensionsv1.JSON{ 154 | Raw: values, 155 | }, 156 | Interval: defaultDuration, 157 | }, 158 | Subcharts: []v1alpha1.DAG{ 159 | { 160 | Name: "productpage", 161 | Dependencies: []string{"reviews"}, 162 | }, 163 | { 164 | Name: "reviews", 165 | Dependencies: []string{"details", "ratings"}, 166 | }, 167 | { 168 | Name: "ratings", 169 | Dependencies: []string{}, 170 | }, 171 | { 172 | Name: "details", 173 | Dependencies: []string{}, 174 | }, 175 | }, 176 | }, 177 | } 178 | } 179 | 180 | func podinfoApplication(targetNamespace string, dependencies ...string) v1alpha1.Application { 181 | return v1alpha1.Application{ 182 | DAG: v1alpha1.DAG{ 183 | Name: podinfo, 184 | Dependencies: dependencies, 185 | }, 186 | Spec: v1alpha1.ApplicationSpec{ 187 | Chart: &v1alpha1.ChartRef{ 188 | URL: podinfoChartURL, 189 | Name: podinfo, 190 | Version: podinfoChartVersion, 191 | }, 192 | Release: &v1alpha1.Release{ 193 | TargetNamespace: targetNamespace, 194 | Interval: defaultDuration, 195 | }, 196 | }, 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /docs/.bundle/config: -------------------------------------------------------------------------------- 1 | --- 2 | BUNDLE_PATH: "vendor/bundle" 3 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | _site 2 | .sass-cache 3 | .jekyll-cache 4 | .jekyll-metadata 5 | vendor 6 | -------------------------------------------------------------------------------- /docs/404.html: -------------------------------------------------------------------------------- 1 | --- 2 | permalink: /404.html 3 | layout: default 4 | --- 5 | 6 | 19 | 20 |
21 |

404

22 | 23 |

Page not found :(

24 |

The requested page could not be found.

25 |
26 | -------------------------------------------------------------------------------- /docs/Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | # Hello! This is where you manage which Jekyll version is used to run. 3 | # When you want to use a different version, change it below, save the 4 | # file and run `bundle install`. Run Jekyll with `bundle exec`, like so: 5 | # 6 | # bundle exec jekyll serve 7 | # 8 | # This will help ensure the proper Jekyll version is running. 9 | # Happy Jekylling! 10 | gem "jekyll", "~> 3.9.0" 11 | # This is the default theme for new Jekyll sites. You may change this to anything you like. 12 | gem "minima", "~> 2.5" 13 | # If you want to use GitHub Pages, remove the "gem "jekyll"" above and 14 | # uncomment the line below. To upgrade, run `bundle update github-pages`. 15 | gem "github-pages","~> 214",group: :jekyll_plugins 16 | # If you have any plugins, put them here! 17 | group :jekyll_plugins do 18 | gem "jekyll-feed", "~> 0.12" 19 | end 20 | 21 | # Windows and JRuby does not include zoneinfo files, so bundle the tzinfo-data gem 22 | # and associated library. 23 | platforms :mingw, :x64_mingw, :mswin, :jruby do 24 | gem "tzinfo", "~> 1.2" 25 | gem "tzinfo-data" 26 | end 27 | 28 | # Performance-booster for watching directories on Windows 29 | gem "wdm", "~> 0.1.1", :platforms => [:mingw, :x64_mingw, :mswin] 30 | 31 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | title: Azure Orkestra 2 | email: nitishm@microsoft.com 3 | logo: "./assets/azure-logo.png" 4 | 5 | description: >- # this means to ignore newlines until "baseurl:" 6 | Orkestra is a cloud-native release orchestration and lifecycle management (LCM) platform for fine-grained orchestration a group of inter-dependent “Applications”. An “Application” may be defined as a Helm chart or artifact, with or without subchart dependencies. Orkestra works by generating a DAG workflow from the ApplicationGroup spec. to orchestrate the deployment and upgrade of multiple applications within a Kubernetes cluster. At a finer-grain, Orkestra can also order the deployment of subcharts within an application chart by generating an embedded DAG workflow. 7 | baseurl: "/" # the subpath of your site, e.g. /blog 8 | url: "https://azure.github.io" # the base hostname & protocol for your site, e.g. http://example.com 9 | twitter_username: nitishmalhotra9 10 | github_username: nitishm 11 | 12 | # Build settings 13 | remote_theme: pmarsceill/just-the-docs 14 | color_scheme: "light" 15 | plugins: 16 | - jekyll-feed 17 | 18 | syntax_highlighter: rouge 19 | 20 | exclude: 21 | - .sass-cache/ 22 | - .jekyll-cache/ 23 | - gemfiles/ 24 | - Gemfile 25 | - Gemfile.lock 26 | - node_modules/ 27 | - vendor/bundle/ 28 | - vendor/cache/ 29 | - vendor/gems/ 30 | - vendor/ruby/ 31 | 32 | # Footer last edited timestamp 33 | last_edit_timestamp: true # show or hide edit time - page must have `last_modified_date` defined in the frontmatter 34 | last_edit_time_format: "%b %e %Y at %I:%M %p" # uses ruby's time format: https://ruby-doc.org/stdlib-2.7.0/libdoc/time/rdoc/Time.html 35 | 36 | # Footer "Edit this page on GitHub" link text 37 | gh_edit_link: true # show or hide edit this page link 38 | gh_edit_link_text: "Edit this page on GitHub." 39 | gh_edit_repository: "https://github.com/Azure/orkestra" # the github URL for your repo 40 | gh_edit_branch: "main" # the branch that your docs is served from 41 | gh_edit_source: docs # the source that your files originate from 42 | gh_edit_view_mode: "tree" # "tree" or "edit" if you want the user to jump into the editor immediately 43 | -------------------------------------------------------------------------------- /docs/architecture.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Architecture 4 | nav_order: 2 5 | --- 6 | # Architecture 7 | 8 | ## How it works 9 | 10 | To solve the complex application orchestration problem Orkestra builds a [Directed Acyclic Graph](https://en.wikipedia.org/wiki/Directed_acyclic_graph) using the application, and it's dependencies and submits it to Argo Workflow. The Workflow nodes use [`workflow-executor`](https://argoproj.github.io/argo-workflows/workflow-executors/) nodes to deploy a [`HelmRelease`](https://fluxcd.io/docs/components/helm/api/#helm.toolkit.fluxcd.io/v2beta1.HelmReleaseSpec) object into the cluster. This `HelmRelease` object signals Flux's HelmOperator to perform a "Helm Action" on the referenced chart. 11 | 12 |

13 | 14 | ### Sequence 15 | 16 | 1. Submit an `ApplicationGroup` custom resource object 17 | 2. For each "application" in `ApplicationGroup` download the Helm chart from “primary” Helm Registry 18 | 3. For each dependency in the Application chart, if subcharts found in `charts/` directory, push the subcharts and the application chart to the ”staging” Helm Registry (Chart-museum). 19 | 4. Generate and submit the Argo (DAG) Workflow 20 | 5. In parallel, 21 | 22 | - (*Executor nodes aka workflow pods will*) submit and probe the status of the deployed `HelmRelease` CR (`.Status.Phase`) 23 | - (*helm-controller will*) watch and deploy Helm charts referred to by each `HelmRelease` CR to the Kubernetes cluster 24 | 25 | ### Key Pieces 26 | 27 | - The ApplicationGroup custom resource type, on which workflow definitions are based. Orkestra basically uses this as its own deployment template, wrapping the Helm releases within. 28 | - Orkestra’s own operator, which interprets the ApplicationGroup input, initiating the actual workflow steps within Argo Workflow. 29 | - Helm charts are referenced in ApplicationGroup documents. Orkestra caches/stages the required Helm charts in a local repository for which it uses ChartMuseum. 30 | - The actual Helm releases as per workflow steps triggered by Argu are executed through a Helm operator that is part of Orkestra. -------------------------------------------------------------------------------- /docs/assets/azure-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/orkestra/259e906371227284a32656907c38ebad63a580ba/docs/assets/azure-logo.png -------------------------------------------------------------------------------- /docs/assets/bridge-to-kubernetes-tutorial.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/orkestra/259e906371227284a32656907c38ebad63a580ba/docs/assets/bridge-to-kubernetes-tutorial.gif -------------------------------------------------------------------------------- /docs/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/orkestra/259e906371227284a32656907c38ebad63a580ba/docs/assets/favicon.ico -------------------------------------------------------------------------------- /docs/assets/keptn-dashboard-failed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/orkestra/259e906371227284a32656907c38ebad63a580ba/docs/assets/keptn-dashboard-failed.png -------------------------------------------------------------------------------- /docs/assets/keptn-dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/orkestra/259e906371227284a32656907c38ebad63a580ba/docs/assets/keptn-dashboard.png -------------------------------------------------------------------------------- /docs/assets/keptn-executor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/orkestra/259e906371227284a32656907c38ebad63a580ba/docs/assets/keptn-executor.png -------------------------------------------------------------------------------- /docs/assets/layers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/orkestra/259e906371227284a32656907c38ebad63a580ba/docs/assets/layers.png -------------------------------------------------------------------------------- /docs/assets/nf-paas-layers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/orkestra/259e906371227284a32656907c38ebad63a580ba/docs/assets/nf-paas-layers.png -------------------------------------------------------------------------------- /docs/assets/orkestra-core.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/orkestra/259e906371227284a32656907c38ebad63a580ba/docs/assets/orkestra-core.png -------------------------------------------------------------------------------- /docs/assets/orkestra-gif.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/orkestra/259e906371227284a32656907c38ebad63a580ba/docs/assets/orkestra-gif.gif -------------------------------------------------------------------------------- /docs/assets/reconciler-flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/orkestra/259e906371227284a32656907c38ebad63a580ba/docs/assets/reconciler-flow.png -------------------------------------------------------------------------------- /docs/assets/subchart-dag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/orkestra/259e906371227284a32656907c38ebad63a580ba/docs/assets/subchart-dag.png -------------------------------------------------------------------------------- /docs/assets/workflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/orkestra/259e906371227284a32656907c38ebad63a580ba/docs/assets/workflow.png -------------------------------------------------------------------------------- /docs/developers.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Developers 4 | nav_order: 4 5 | --- 6 | # Guide for contributors and developers 7 | 8 | ## Install prerequisites 9 | 10 | For getting started, you will need: 11 | 12 | - **Go installed** - see this [Getting Started](https://golang.org/doc/install) guide for Go. 13 | - **Docker installed** - see this [Getting Started](https://docs.docker.com/install/) guide for Docker. 14 | - **Kubernetes Cluster** *v0.10.0* or higher. Some options are: 15 | - Locally hosted cluster, such as 16 | - [Kind](https://kind.sigs.k8s.io/docs/user/quick-start/) (preferred; used by `Makefile` and this guide) 17 | - [Minikube](https://minikube.sigs.k8s.io/docs/start/) 18 | - Cloud-based, such as 19 | - [AKS](https://azure.microsoft.com/en-us/services/kubernetes-service/) 20 | - [GKE](https://cloud.google.com/kubernetes-engine) 21 | - [EKS](https://aws.amazon.com/eks/) 22 | - kubectl *v1.18* or higher - see this [Getting started](https://kubernetes.io/docs/tasks/tools/) guide for kubectl. 23 | - helm *v3.5.2* or higher - see this [Getting started](https://helm.sh/docs/intro/install/) guide for helm. 24 | - `kubebuilder` *v2.3.1* or higher - Install using `make setup-kubebuilder`. 25 | - `controller-gen` *v0.5.0* or higher - Install using `make controller-gen`. This is required to generate the ApplicationGroup CRDS. 26 | 27 | > **NOTE**: `controller-gen` versions *< v0.5.0* will generate an incompatible CRD type. 28 | 29 | ## Build & Run 30 | 31 | To solely build the source code, invoke the `make all` target to, 32 | 33 | 1. Update the CRDs and associated resource on modifying the API types. 34 | 2. Build the orkestra controller binary. 35 | 36 | To setup a local environment for debugging and/or testing invoke the `make dev` target. 37 | 38 | The `dev` target creates a new `KinD` cluster with a local container registry and deploys all the components of Orkestra apart from the orkestra controller deployment. 39 | 40 | ```shell 41 | make dev 42 | 43 | kind create cluster --config .kind-cluster.yaml --name orkestra 44 | Creating cluster "orkestra" ... 45 | ✓ Ensuring node image (kindest/node:v1.20.2) 🖼 46 | ✓ Preparing nodes 📦 47 | ✓ Writing configuration 📜 48 | ✓ Starting control-plane 🕹️ 49 | ✓ Installing CNI 🔌 50 | ✓ Installing StorageClass 💾 51 | Set kubectl context to "kind-orkestra" 52 | You can now use your cluster with: 53 | 54 | kubectl cluster-info --context kind-orkestra 55 | 56 | Have a question, bug, or feature request? Let us know! https://kind.sigs.k8s.io/#community 🙂 57 | helm upgrade --install orkestra chart/orkestra --wait --atomic -n orkestra --create-namespace --values "chart/orkestra/values-ci.yaml" 58 | Release "orkestra" does not exist. Installing it now. 59 | manifest_sorter.go:192: info: skipping unknown hook: "crd-install" 60 | manifest_sorter.go:192: info: skipping unknown hook: "crd-install" 61 | manifest_sorter.go:192: info: skipping unknown hook: "crd-install" 62 | manifest_sorter.go:192: info: skipping unknown hook: "crd-install" 63 | NAME: orkestra 64 | LAST DEPLOYED: Mon Jun 7 18:05:28 2021 65 | NAMESPACE: orkestra 66 | STATUS: deployed 67 | REVISION: 1 68 | TEST SUITE: None 69 | NOTES: 70 | Happy Helming with Azure/Orkestra 71 | ``` 72 | 73 | To runs E2E and UTs invoke the `make test` target as follows, 74 | 75 | ```shell 76 | make test 77 | 78 | go test -v ./... -coverprofile coverage.txt -timeout 25m 79 | ? github.com/Azure/Orkestra [no test files] 80 | ? github.com/Azure/Orkestra/api/v1alpha1 [no test files] 81 | === RUN TestAPIs 82 | Running Suite: Controller Suite 83 | =============================== 84 | Random Seed: 1622919020 85 | Will run 7 of 7 specs 86 | 87 | • [SLOW TEST:195.349 seconds] 88 | ApplicationGroup Controller 89 | /home/runner/work/orkestra/orkestra/controllers/appgroup_controller_test.go:23 90 | ApplicationGroup 91 | /home/runner/work/orkestra/orkestra/controllers/appgroup_controller_test.go:25 92 | Should create Bookinfo spec successfully 93 | /home/runner/work/orkestra/orkestra/controllers/appgroup_controller_test.go:53 94 | ... truncated for brevity ... 95 | --- PASS: TestAPIs (985.79s) 96 | PASS 97 | coverage: 67.4% of statements 98 | ok github.com/Azure/Orkestra/controllers 985.849s coverage: 67.4% of statements 99 | ? github.com/Azure/Orkestra/pkg/meta [no test files] 100 | ? github.com/Azure/Orkestra/pkg/registry [no test files] 101 | ? github.com/Azure/Orkestra/pkg/utils [no test files] 102 | === RUN Test_subchartValues 103 | === RUN Test_subchartValues/withGlobalSuchart 104 | === RUN Test_subchartValues/withOnlyGlobal 105 | === RUN Test_subchartValues/withOnlySubchart 106 | === RUN Test_subchartValues/withNone 107 | --- PASS: Test_subchartValues (0.00s) 108 | --- PASS: Test_subchartValues/withGlobalSuchart (0.00s) 109 | --- PASS: Test_subchartValues/withOnlyGlobal (0.00s) 110 | --- PASS: Test_subchartValues/withOnlySubchart (0.00s) 111 | --- PASS: Test_subchartValues/withNone (0.00s) 112 | PASS 113 | coverage: 3.3% of statements 114 | ok github.com/Azure/Orkestra/pkg/workflow 0.044s coverage: 3.3% of statements 115 | ``` 116 | 117 | ### Debugging using `delve` 118 | 119 | - **Debugging using [Visual Studio Code](https://code.visualstudio.com/) and [delve](https://github.com/go-delve/delve)** 120 | 121 | - [Built-in Debugger](https://code.visualstudio.com/docs/languages/go#_debugging) 122 | - Required extensions: 123 | - ["Golang"](https://marketplace.visualstudio.com/items?itemName=golang.go) 124 | 125 | `.vscode/launch.json` 126 | 127 | > set `--disable-remediation` if you do not wish for the controller to automatically rollback or garbage collect the owned resources (pods, jobs, etc.) 128 | 129 | ```json 130 | { 131 | "version": "0.2.0", 132 | "configurations": [ 133 | { 134 | "name": "Launch Package", 135 | "type": "go", 136 | "request": "launch", 137 | "mode": "auto", 138 | "program": "${workspaceFolder}", 139 | "args": [ 140 | "--debug", 141 | "--log-level", "3", 142 | // "--disable-remediation" 143 | ] 144 | } 145 | ] 146 | } 147 | ``` 148 | 149 | ## Cleanup 150 | 151 | ```shell 152 | make clean 153 | 154 | ./hack/teardown-kind-with-registry.sh 155 | Deleting cluster "orkestra" ... 156 | ``` 157 | 158 | ## Opening a Pull Request 159 | 160 | - Fork the [repository](https://github.com/Azure/orkestra). 161 | - Check-in all changed files 162 | 163 | - 🚨 Update API docs if any of the types are changed 164 | 165 | ```shell 166 | make api-docs 167 | ``` 168 | 169 | - Create a new PR against the upstream repository and reference the relevant issue(s) in the PR description. 170 | 171 | ## Supported Workflow Executors 172 | 173 | ### Helmrelease Workflow Executor Repository 174 | 175 | The code for the *default* workflow executor container can be found at [Orkestra Helmrelease Workflow Executor](https://github.com/Azure/helmrelease-workflow-executor). 176 | 177 | ### Keptn Workflow Executor Repository 178 | 179 | The code for the *keptn* workflow executor container can be found at [Orkestra Keptn Workflow Executor](https://github.com/Azure/keptn-workflow-executor). 180 | 181 | ### Custom Workflow Executor Repository 182 | 183 | A *custom* workflow executor can be used to run a custom workflow node as part of the generated Orkestra workflow. This comes in handy when you want to run a workflow node that is not part of the Orkestra workflow. For example, you might want to run your own deployment executor to deploy an application. Or you might wish to chain the Orkestra workflow with a custom workflow node, in addition to the default and/or keptn workflow nodes. 184 | 185 | The code for a *custom* workflow executor container, that prints the payload passed via input arguments to the executor, can be found at [Orkestra Generic Workflow Executor](https://github.com/nitishm/generic-workflow-executor). 186 | -------------------------------------------------------------------------------- /examples/custom/generic-bookinfo.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: orkestra.azure.microsoft.com/v1alpha1 2 | kind: ApplicationGroup 3 | metadata: 4 | name: bookinfo 5 | spec: 6 | applications: 7 | - name: bookinfo 8 | spec: 9 | chart: 10 | url: "https://nitishm.github.io/charts" 11 | name: bookinfo 12 | version: v1 13 | release: 14 | targetNamespace: bookinfo 15 | workflow: 16 | - name: generic-executor 17 | type: custom 18 | image: 19 | name: generic-executor 20 | image: nmalhotra/generic:latest 21 | params: 22 | data: 23 | foo: bar 24 | 25 | 26 | -------------------------------------------------------------------------------- /examples/keptn/README.md: -------------------------------------------------------------------------------- 1 | # Evaluation & Quality Gate based promotions using Keptn 2 | 3 | ## Prequisites 4 | 5 | - A Kubernetes cluster with sufficient resources to run the Keptn Controller 6 | 7 | > ⚠️ Avoid using a cluster with a low number of nodes and low CPU/RAM or a KinD, Minikube or microk8s cluster 8 | 9 | - [kubectl](https://kubernetes.io/docs/tasks/tools/) 10 | - [helm](https://helm.sh/docs/intro/install/) 11 | - [Argo Workflow CLI](https://github.com/argoproj/argo-workflows/releases/tag/v3.0.0) 12 | - [Keptn CLI](https://keptn.sh/docs/0.9.x/operate/install/) 13 | 14 | ## Description 15 | 16 | In this example we will show how to use Keptn to perform a promotion based on the evaluation of a Quality Gate. 17 | 18 | The Application Group deploys an application that relies on multiple supporting layers to be deployed and running before the application can be started. The *bookinfo* application relies on the following layers: 19 | 20 | - *Istio CRDs* - This layer (*istio-base*) contains the Istio CRDs that are used to configure the Istio service mesh. 21 | - *Prometheus* - This layer contains the Prometheus component that is used to monitor the application and leveraged by Keptn to perform the evaluation of the Quality Gate. 22 | - *Istio* - This layer (*istiod) )contains the Istio service mesh that is used to deploy the application. 23 | - *Istio Ingress Gateway* - This layer contains the Istio Ingress Gateway that is used to expose the application. 24 | - *Bookinfo* - This layer contains the *bookinfo* application. 25 | 26 | The scenarios that we will use in this example are: 27 | 28 | The *bookinfo* application is deployed with the Istio sidecar injection enabled. The application is configured to use the Keptn executor to perform evaluation of the Quality Gate. 29 | 30 | ![Orkestra Workflow](keptn-executor.png) 31 | 32 | 1. The *productpage* sidecar is configured to serve traffic without any issues. 33 | We expect the `Workflow` and subsequently the `ApplicationGroup` to succeed. 34 | 35 | ### Keptn dashboard - Success 36 | 37 | > ⚠️ monitoring failed is a known, benign issue when submitting the `ApplicationGroup` multiple times. 38 | 39 | Authenticate with Keptn Controller for the dashboard: 40 | 41 | ```shell 42 | export KEPTN_ENDPOINT=http://$(kubectl get svc api-gateway-nginx -n orkestra -ojsonpath='{.status.loadBalancer.ingress[0].ip}')/api \ 43 | export KEPTN_ENDPOINT=http://$(kubectl get svc api-gateway-nginx -n orkestra -ojsonpath='{.status.loadBalancer.ingress[0].ip}')/api \ 44 | keptn auth --endpoint=$KEPTN_ENDPOINT --api-token=$KEPTN_API_TOKEN 45 | ``` 46 | 47 | Retrieve the dashboard URL, Username and Password: 48 | 49 | > The IPs and password will differ for each cluster. 50 | 51 | ```shell 52 | keptn configure bridge --output 53 | Your Keptn Bridge is available under: http://20.75.119.32/bridge 54 | 55 | These are your credentials 56 | user: keptn 57 | password: UxUqN6XvWMpsrLqp6BeL 58 | ``` 59 | 60 | ![Keptn Dashboard](./keptn-dashboard.png) 61 | 62 | 2. The *productpage* sidecar is configured to inject faults (return status code 500, 80% of the time) using the [`VirtualService`](https://istio.io/latest/docs/tasks/traffic-management/fault-injection/). We expect the `Workflow` and subsequently the `ApplicationGroup` to fail & rollback to the previous `ApplicationGroup` spec (i.e. Scenario 1). 63 | 64 | ### Keptn dashboard - Failure 65 | 66 | ![Keptn Dashboard](./keptn-dashboard-failed.png) 67 | 68 | ## Installation 69 | 70 | ### Orkestra Helm Chart 71 | 72 | From the root directory of the repository, run: 73 | 74 | ```shell 75 | helm upgrade --install orkestra chart/orkestra -n orkestra --create-namespace --set=keptn.enabled=true --set=keptn-addons.enabled=true 76 | ``` 77 | 78 | > 💡 Note: If prometheus is expected to run in a different namespace the user must specify the namespace in the `--set` option as follows, 79 | > 80 | > ```shell 81 | > export PROM_NS=prometheus 82 | > helm upgrade --install orkestra chart/orkestra -n orkestra --create-namespace --set=keptn.enabled=true --set=keptn-addons.enabled=true --set=keptn-addons.prometheus.namespace=$PROM_NS 83 | > ``` 84 | 85 | ## Scenario 1 : Successful Reconciliation 86 | 87 | The *bookinfo* application is deployed using the following Kubernetes manifests: 88 | 89 | The ConfigMap is used to configure the Keptn executor and contains the following: 90 | 91 | - *keptn-config.yaml* - This file contains the Keptn configuration. 92 | - *sli.yaml* - This file contains the SLI configuration. 93 | - *slo.yaml* - This file contains the SLO configuration. 94 | - *config.yaml* - This file contains the configuration for the `hey` load generator. 95 | 96 | ```shell 97 | kubectl create -f examples/keptn/bookinfo.yaml -n orkestra \ 98 | kubectl create -f examples/keptn/bookinfo-keptn-cm.yaml -n orkestra 99 | ``` 100 | 101 | ## Scenario 2 : Failed Reconciliation leading to Rollback 102 | 103 | ```shell 104 | kubectl apply -f examples/keptn/bookinfo-with-faults.yaml -n orkestra 105 | ``` 106 | 107 | ## Cleanup 108 | 109 | 1. Delete the *bookinfo* `ApplicationGroup` and wait for the reverse workflow to complete 110 | 111 | ```shell 112 | kubectl delete -f examples/keptn/bookinfo.yaml -n orkestra 113 | ``` 114 | 115 | 2. Once the `ApplicationGroup` is deleted, delete the Keptn configuration configMap 116 | 117 | > ⚠️ Deleting the Keptn ConfigMap before the `ApplicationGroup` will cause the reverse `Workflow` to fail causing cleanup to fail. 118 | 119 | ```shell 120 | kubectl delete -f examples/keptn/bookinfo-keptn-cm.yaml -n orkestra 121 | ``` 122 | 123 | 156 | -------------------------------------------------------------------------------- /examples/keptn/bookinfo-keptn-cm.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: keptn-bookinfo-config 5 | namespace: orkestra 6 | data: 7 | config.yaml: | 8 | apiVersion: v2 9 | actions: 10 | - name: "Run hey" 11 | events: 12 | - name: "sh.keptn.event.test.triggered" 13 | tasks: 14 | - name: "Run hey load tests" 15 | image: "azureorkestra/hey" 16 | cmd: ["hey"] 17 | args: ["-host", "example.com", "-z", "5m", "http://gateway.istio-system.svc.cluster.local/productpage"] 18 | maxPollDuration: 10000 19 | keptn-config.json: |- 20 | { 21 | "url": "http://api-gateway-nginx.orkestra.svc.cluster.local/api", 22 | "namespace": "orkestra", 23 | "timeframe": "5m", 24 | "token": { 25 | "secretRef": { 26 | "name": "keptn-api-token", 27 | "namespace": "orkestra" 28 | } 29 | } 30 | } 31 | shipyard.yaml: |- 32 | apiVersion: "spec.keptn.sh/0.2.2" 33 | kind: "Shipyard" 34 | metadata: 35 | name: "shipyard-bookinfo" 36 | spec: 37 | stages: 38 | - name: "dev" 39 | sequences: 40 | - name: "evaluation" 41 | tasks: 42 | - name: "test" 43 | properties: 44 | teststrategy: "functional" 45 | - name: "evaluation" 46 | sli.yaml: | 47 | spec_version: "1.0" 48 | indicators: 49 | error_percentage: sum(rate(istio_requests_total{app="gateway", response_code="500"}[$DURATION_SECONDS])) / sum(rate(istio_requests_total{app="gateway"}[$DURATION_SECONDS])) * 100 50 | slo.yaml: |- 51 | spec_version: '1.0' 52 | comparison: 53 | compare_with: "single_result" 54 | include_result_with_score: "pass" 55 | aggregate_function: avg 56 | objectives: 57 | - sli: error_percentage 58 | pass: 59 | - criteria: 60 | - "<10" 61 | warning: 62 | - criteria: 63 | - "<=5" 64 | total_score: 65 | pass: "100%" 66 | warning: "75%" 67 | -------------------------------------------------------------------------------- /examples/keptn/bookinfo-with-faults.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: orkestra.azure.microsoft.com/v1alpha1 2 | kind: ApplicationGroup 3 | metadata: 4 | name: bookinfo-with-keptn 5 | spec: 6 | applications: 7 | - name: istio-base 8 | dependencies: [] 9 | spec: 10 | chart: 11 | url: "https://istio-release.storage.googleapis.com/charts" 12 | name: base 13 | version: 1.12.0-alpha.1 14 | release: 15 | targetNamespace: istio-system 16 | - name: prometheus 17 | dependencies: [] 18 | spec: 19 | chart: 20 | url: "https://nitishm.github.io/charts" 21 | name: prometheus 22 | version: 14.8.0 23 | release: 24 | targetNamespace: orkestra 25 | - name: istiod 26 | dependencies: 27 | - istio-base 28 | - prometheus 29 | spec: 30 | chart: 31 | url: "https://istio-release.storage.googleapis.com/charts" 32 | name: istiod 33 | version: 1.12.0-alpha.1 34 | release: 35 | targetNamespace: istio-system 36 | timeout: 10m 37 | values: 38 | service: 39 | type: ClusterIP 40 | - name: istio-ingressgateway 41 | dependencies: 42 | - istiod 43 | spec: 44 | chart: 45 | url: "https://istio-release.storage.googleapis.com/charts" 46 | name: gateway 47 | version: 1.12.0-alpha.1 48 | release: 49 | targetNamespace: istio-system 50 | timeout: 10m 51 | - name: bookinfo 52 | dependencies: 53 | - istio-ingressgateway 54 | spec: 55 | chart: 56 | url: "https://nitishm.github.io/charts" 57 | name: bookinfo 58 | version: v3 59 | release: 60 | targetNamespace: bookinfo 61 | timeout: 10m 62 | values: 63 | istio: 64 | enabled: true 65 | fault: 66 | enabled: true 67 | ambassador: 68 | enabled: false 69 | workflow: 70 | - name: helmrelease 71 | dependencies: [] 72 | type: helmrelease 73 | params: nil 74 | - name: keptn 75 | dependencies: 76 | - helmrelease 77 | type: keptn 78 | params: 79 | configmapRef: 80 | name: keptn-bookinfo-config 81 | namespace: orkestra -------------------------------------------------------------------------------- /examples/keptn/bookinfo.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: orkestra.azure.microsoft.com/v1alpha1 2 | kind: ApplicationGroup 3 | metadata: 4 | name: bookinfo-with-keptn 5 | spec: 6 | applications: 7 | - name: istio-base 8 | dependencies: [] 9 | spec: 10 | chart: 11 | url: "https://istio-release.storage.googleapis.com/charts" 12 | name: base 13 | version: 1.12.0-alpha.1 14 | release: 15 | targetNamespace: istio-system 16 | - name: prometheus 17 | dependencies: [] 18 | spec: 19 | chart: 20 | url: "https://nitishm.github.io/charts" 21 | name: prometheus 22 | version: 14.8.0 23 | release: 24 | targetNamespace: orkestra 25 | - name: istiod 26 | dependencies: 27 | - istio-base 28 | - prometheus 29 | spec: 30 | chart: 31 | url: "https://istio-release.storage.googleapis.com/charts" 32 | name: istiod 33 | version: 1.12.0-alpha.1 34 | release: 35 | targetNamespace: istio-system 36 | timeout: 10m 37 | values: 38 | service: 39 | type: ClusterIP 40 | - name: istio-ingressgateway 41 | dependencies: 42 | - istiod 43 | spec: 44 | chart: 45 | url: "https://istio-release.storage.googleapis.com/charts" 46 | name: gateway 47 | version: 1.12.0-alpha.1 48 | release: 49 | targetNamespace: istio-system 50 | timeout: 10m 51 | - name: bookinfo 52 | dependencies: 53 | - istio-ingressgateway 54 | spec: 55 | chart: 56 | url: "https://nitishm.github.io/charts" 57 | name: bookinfo 58 | version: v3 59 | release: 60 | targetNamespace: bookinfo 61 | timeout: 10m 62 | values: 63 | istio: 64 | enabled: true 65 | fault: 66 | enabled: false 67 | ambassador: 68 | enabled: false 69 | workflow: 70 | - name: helmrelease 71 | dependencies: [] 72 | type: helmrelease 73 | params: nil 74 | - name: keptn 75 | dependencies: 76 | - helmrelease 77 | type: keptn 78 | params: 79 | configmapRef: 80 | name: keptn-bookinfo-config 81 | namespace: orkestra -------------------------------------------------------------------------------- /examples/keptn/hey/event.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "sh.keptn.event.test.triggered", 3 | "specversion": "1.0", 4 | "source": "test-events", 5 | "contenttype": "application/json", 6 | "data": { 7 | "project": "hey", 8 | "stage": "dev", 9 | "service": "bookinfo" 10 | } 11 | } -------------------------------------------------------------------------------- /examples/keptn/job/config.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | actions: 3 | - name: "Run hey" 4 | events: 5 | - name: "sh.keptn.event.test.triggered" 6 | tasks: 7 | - name: "Run hey load tests" 8 | image: "azureorkestra/hey" 9 | cmd: ["hey"] 10 | args: ["-host", "example.com", "-z", "5m", "http://gateway.istio-system.svc.cluster.local/productpage"] 11 | maxPollDuration: 10000 -------------------------------------------------------------------------------- /examples/keptn/keptn-dashboard-failed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/orkestra/259e906371227284a32656907c38ebad63a580ba/examples/keptn/keptn-dashboard-failed.png -------------------------------------------------------------------------------- /examples/keptn/keptn-dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/orkestra/259e906371227284a32656907c38ebad63a580ba/examples/keptn/keptn-dashboard.png -------------------------------------------------------------------------------- /examples/keptn/keptn-executor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/orkestra/259e906371227284a32656907c38ebad63a580ba/examples/keptn/keptn-executor.png -------------------------------------------------------------------------------- /examples/keptn/prometheus/sli.yaml: -------------------------------------------------------------------------------- 1 | spec_version: "1.0" 2 | indicators: 3 | error_percentage: sum(rate(istio_requests_total{app="gateway", response_code="500"}[$DURATION_SECONDS])) / sum(rate(istio_requests_total{app="gateway"}[$DURATION_SECONDS])) * 100 -------------------------------------------------------------------------------- /examples/keptn/shipyard.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: "spec.keptn.sh/0.2.2" 2 | kind: "Shipyard" 3 | metadata: 4 | name: "shipyard-bookinfo" 5 | spec: 6 | stages: 7 | - name: "dev" 8 | sequences: 9 | - name: "evaluation" 10 | tasks: 11 | - name: "test" 12 | properties: 13 | teststrategy: "functional" 14 | - name: "evaluation" -------------------------------------------------------------------------------- /examples/keptn/slo.yaml: -------------------------------------------------------------------------------- 1 | spec_version: '1.0' 2 | comparison: 3 | compare_with: "single_result" 4 | include_result_with_score: "pass" 5 | aggregate_function: avg 6 | objectives: 7 | - sli: error_percentage 8 | pass: 9 | - criteria: 10 | - "<10" 11 | warning: 12 | - criteria: 13 | - "<=5" 14 | total_score: 15 | pass: "100%" 16 | warning: "75%" -------------------------------------------------------------------------------- /examples/simple/README.md: -------------------------------------------------------------------------------- 1 | # Instructions 2 | 3 | In this example we deploy an application group consisting of two demo applications, 4 | 5 | - Istio bookinfo app (with subcharts) : [source](https://istio.io/latest/docs/examples/bookinfo/) 6 | - Ambassador : [source](https://www.getambassador.io/) 7 | 8 | ## Prerequisites 9 | 10 | - [kubectl](https://kubernetes.io/docs/tasks/tools/) 11 | - [helm](https://helm.sh/docs/intro/install/) 12 | 13 | Install the `ApplicationGroup`: 14 | 15 | ```shell 16 | kubectl apply -f examples/simple/bookinfo.yaml 17 | 18 | applicationgroup.orkestra.azure.microsoft.com/bookinfo created 19 | ``` 20 | 21 | The orkestra controller logs should look as follows on success, 22 | 23 | ```log 24 | orkestra-885c5ff4-kh7n9 orkestra 2021-03-23T07:53:24.452Z INFO setup starting manager 25 | orkestra-885c5ff4-kh7n9 orkestra 2021-03-23T07:53:24.453Z INFO controller-runtime.manager starting metrics server {"path": "/metrics"} 26 | orkestra-885c5ff4-kh7n9 orkestra 2021-03-23T07:53:24.453Z INFO controller-runtime.controller Starting EventSource {"controller": "applicationgroup", "source": "kind source: /, Kind="} 27 | orkestra-885c5ff4-kh7n9 orkestra 2021-03-23T07:53:24.554Z INFO controller-runtime.controller Starting Controller {"controller": "applicationgroup"} 28 | orkestra-885c5ff4-kh7n9 orkestra 2021-03-23T07:53:24.554Z INFO controller-runtime.controller Starting workers {"controller": "applicationgroup", "worker count": 1} 29 | ... truncated for brevity ... 30 | orkestra-885c5ff4-kh7n9 orkestra 2021-03-23T08:04:18.875Z DEBUG controllers.ApplicationGroup workflow ran to completion and succeeded {"appgroup": "bookinfo"} 31 | orkestra-885c5ff4-kh7n9 orkestra 2021-03-23T08:04:18.901Z DEBUG controller-runtime.controller Successfully Reconciled {"controller": "applicationgroup", "request": "/bookinfo"} 32 | orkestra-885c5ff4-kh7n9 orkestra 2021-03-23T08:04:18.902Z DEBUG controller-runtime.manager.events Normal {"object": {"kind":"ApplicationGroup","name":"bookinfo","uid":"52c5095e-0aa1-4067-a434-f1155ebbbdcd","apiVersion":"orkestra.azure.microsoft.com/v1alpha1","resourceVersion":"30145"}, "reason": "ReconcileSuccess", "message": "Successfully reconciled ApplicationGroup bookinfo"} 33 | ``` 34 | 35 | (_optional_) The Argo dashboard should show the DAG nodes in Green 36 | 37 | ![workflow](workflow.png) 38 | 39 | ### Verify that the Application helm release have been successfully deployed 40 | 41 | ```shell 42 | helm ls 43 | 44 | NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION 45 | orkestra orkestra 1 2021-03-23 08:02:15.0044864 +0000 UTC deployed orkestra-0.1.0 0.1.0 46 | ambassador ambassador 1 2021-03-23 08:02:35.0044864 +0000 UTC deployed ambassador-6.6.0 1.12.1 47 | bookinfo bookinfo 1 2021-03-23 08:04:08.6088786 +0000 UTC deployed bookinfo-v1 0.16.2 48 | details bookinfo 1 2021-03-23 08:03:26.1043919 +0000 UTC deployed details-v1 1.16.2 49 | productpage bookinfo 1 2021-03-23 08:03:47.4150589 +0000 UTC deployed productpage-v1 1.16.2 50 | ratings bookinfo 1 2021-03-23 08:03:25.9770024 +0000 UTC deployed ratings-v1 1.16.2 51 | reviews bookinfo 1 2021-03-23 08:03:36.9634599 +0000 UTC deployed reviews-v1 1.16.2 52 | ``` 53 | 54 | ## Send request to `productpage` via Ambassador gateway/proxy 55 | 56 | ```shell 57 | kubectl -n default exec curl -- curl -ksS https://ambassador.ambassador:443/bookinfo/ | grep -o ".*" 58 | Simple Bookstore App 59 | ``` 60 | -------------------------------------------------------------------------------- /examples/simple/bookinfo-multi-executors.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: orkestra.azure.microsoft.com/v1alpha1 2 | kind: ApplicationGroup 3 | metadata: 4 | name: bookinfo 5 | spec: 6 | applications: 7 | - name: bookinfo 8 | dependencies: [ambassador] 9 | spec: 10 | chart: 11 | url: "https://nitishm.github.io/charts" 12 | name: bookinfo 13 | version: v1 14 | release: 15 | targetNamespace: bookinfo 16 | values: 17 | productpage: 18 | replicaCount: 1 19 | details: 20 | replicaCount: 1 21 | reviews: 22 | replicaCount: 1 23 | ratings: 24 | replicaCount: 1 25 | workflow: 26 | - name: helmrelease 27 | dependencies: [] 28 | # image: azureorkestra/executor:v0.4.2 29 | type: helmrelease 30 | params: nil 31 | - name: keptn 32 | dependencies: ["helmrelease"] 33 | # image: azureorkestra/keptn-executor:v0.1.0 34 | type: keptn 35 | params: 36 | configmapRef: 37 | name: keptn-config 38 | namespace: orkestra 39 | - name: my-custom-executor 40 | dependencies: ["helmrelease"] 41 | image: azureorkestra/my-custom-executor:v0.1.0 42 | params: 43 | foo: 44 | bar: value 45 | baz: value 46 | 47 | 48 | -------------------------------------------------------------------------------- /examples/simple/bookinfo.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: orkestra.azure.microsoft.com/v1alpha1 2 | kind: ApplicationGroup 3 | metadata: 4 | name: bookinfo 5 | spec: 6 | applications: 7 | - name: ambassador 8 | dependencies: [] 9 | spec: 10 | chart: 11 | url: "https://nitishm.github.io/charts" 12 | name: ambassador 13 | version: 6.6.0 14 | # Authorization Object Reference (Kind: Secret) 15 | # this secret holds all auth information and credentials 16 | # to register a protected/private helm registry 17 | # Uncomment the lines below if you wish to register a private helm registry 18 | # and make sure you deploy the Secret object to the given namespace/name 19 | # as well 20 | # authRef: 21 | # name: 22 | # namespace: 23 | release: 24 | timeout: 10m 25 | targetNamespace: ambassador 26 | values: 27 | service: 28 | type: ClusterIP 29 | - name: bookinfo 30 | dependencies: [ambassador] 31 | spec: 32 | chart: 33 | url: "https://nitishm.github.io/charts" 34 | name: bookinfo 35 | version: v1 36 | subcharts: 37 | - name: productpage 38 | dependencies: [reviews] 39 | - name: reviews 40 | dependencies: [details, ratings] 41 | - name: ratings 42 | dependencies: [] 43 | - name: details 44 | dependencies: [] 45 | release: 46 | targetNamespace: bookinfo 47 | values: 48 | productpage: 49 | replicaCount: 1 50 | details: 51 | replicaCount: 1 52 | reviews: 53 | replicaCount: 1 54 | ratings: 55 | replicaCount: 1 56 | -------------------------------------------------------------------------------- /examples/simple/workflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/orkestra/259e906371227284a32656907c38ebad63a580ba/examples/simple/workflow.png -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Azure/Orkestra 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/argoproj/argo-workflows/v3 v3.1.8 7 | github.com/chartmuseum/helm-push v0.9.0 8 | github.com/fluxcd/helm-controller/api v0.11.2 9 | github.com/fluxcd/pkg/apis/meta v0.10.0 10 | github.com/fluxcd/source-controller/api v0.10.0 11 | github.com/go-logr/logr v0.4.0 12 | github.com/gofrs/flock v0.8.0 13 | github.com/google/go-cmp v0.5.5 14 | github.com/heptiolabs/healthcheck v0.0.0-20180807145615-6ff867650f40 15 | github.com/jinzhu/copier v0.3.0 16 | github.com/onsi/ginkgo v1.16.4 17 | github.com/onsi/gomega v1.16.0 18 | go.opencensus.io v0.22.5 // indirect 19 | golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43 // indirect 20 | gopkg.in/DATA-DOG/go-sqlmock.v1 v1.3.0 // indirect 21 | gopkg.in/yaml.v2 v2.4.0 22 | helm.sh/helm/v3 v3.6.2 23 | k8s.io/api v0.21.3 24 | k8s.io/apiextensions-apiserver v0.21.3 25 | k8s.io/apimachinery v0.21.3 26 | k8s.io/client-go v0.21.3 27 | sigs.k8s.io/controller-runtime v0.9.5 28 | sigs.k8s.io/yaml v1.2.0 29 | ) 30 | 31 | replace ( 32 | github.com/docker/distribution => github.com/docker/distribution v0.0.0-20191216044856-a8371794149d 33 | github.com/docker/docker => github.com/moby/moby v1.4.2-0.20200203170920-46ec8731fbce 34 | github.com/go-openapi/spec => github.com/go-openapi/spec v0.19.8 35 | k8s.io/api => k8s.io/api v0.21.0 36 | k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.21.0 37 | k8s.io/apimachinery => k8s.io/apimachinery v0.21.0 38 | k8s.io/client-go => k8s.io/client-go v0.21.0 39 | sigs.k8s.io/controller-runtime => sigs.k8s.io/controller-runtime v0.9.5 40 | ) 41 | -------------------------------------------------------------------------------- /hack/api-docs/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "hideMemberFields": [ 3 | "TypeMeta" 4 | ], 5 | "hideTypePatterns": [ 6 | "ParseError$", 7 | "List$" 8 | ], 9 | "externalPackages": [ 10 | { 11 | "typeMatchPrefix": "^k8s\\.io/apimachinery/pkg/apis/meta/v1\\.Duration$", 12 | "docsURLTemplate": "https://godoc.org/k8s.io/apimachinery/pkg/apis/meta/v1#Duration" 13 | }, 14 | { 15 | "typeMatchPrefix": "^k8s\\.io/apiextensions-apiserver/pkg/apis/apiextensions/v1\\.JSON$", 16 | "docsURLTemplate": "https://pkg.go.dev/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1?tab=doc#JSON" 17 | }, 18 | { 19 | "typeMatchPrefix": "^k8s\\.io/(api|apimachinery/pkg/apis)/", 20 | "docsURLTemplate": "https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#{{lower .TypeIdentifier}}-{{arrIndex .PackageSegments -1}}-{{arrIndex .PackageSegments -2}}" 21 | }, 22 | { 23 | "typeMatchPrefix": "^github.com/fluxcd/helm-controller/api/v2beta1", 24 | "docsURLTemplate": "https://pkg.go.dev/github.com/fluxcd/helm-controller/api/v2beta1#{{ .TypeIdentifier }}" 25 | } 26 | ], 27 | "typeDisplayNamePrefixOverrides": { 28 | "k8s.io/api/": "Kubernetes ", 29 | "k8s.io/apimachinery/pkg/apis/": "Kubernetes ", 30 | "k8s.io/apiextensions-apiserver/": "Kubernetes ", 31 | "github.com/fluxcd/helm-controller/api/": "helm-controller " 32 | }, 33 | "markdownDisabled": false 34 | } 35 | -------------------------------------------------------------------------------- /hack/api-docs/template/members.tpl: -------------------------------------------------------------------------------- 1 | {{ define "members" }} 2 | {{ range .Members }} 3 | {{ if not (hiddenMember .)}} 4 | 5 | 6 | {{ fieldName . }}
7 | 8 | {{ if linkForType .Type }} 9 | 10 | {{ typeDisplayName .Type }} 11 | 12 | {{ else }} 13 | {{ typeDisplayName .Type }} 14 | {{ end }} 15 | 16 | 17 | 18 | {{ if fieldEmbedded . }} 19 |

20 | (Members of {{ fieldName . }} are embedded into this type.) 21 |

22 | {{ end}} 23 | 24 | {{ if isOptionalMember .}} 25 | (Optional) 26 | {{ end }} 27 | 28 | {{ safe (renderComments .CommentLines) }} 29 | 30 | {{ if and (eq (.Type.Name.Name) "ObjectMeta") }} 31 | Refer to the Kubernetes API documentation for the fields of the 32 | metadata field. 33 | {{ end }} 34 | 35 | {{ if or (eq (fieldName .) "spec") }} 36 |
37 |
38 | 39 | {{ template "members" .Type }} 40 |
41 | {{ end }} 42 | 43 | 44 | {{ end }} 45 | {{ end }} 46 | {{ end }} 47 | -------------------------------------------------------------------------------- /hack/api-docs/template/pkg.tpl: -------------------------------------------------------------------------------- 1 | {{ define "packages" }} 2 | --- 3 | layout: default 4 | title: API Reference 5 | nav_order: 3 6 | --- 7 |

Orkestra API Reference

8 | 9 | {{ with .packages}} 10 |

Packages:

11 | 18 | {{ end}} 19 | 20 | {{ range .packages }} 21 |

22 | {{- packageDisplayName . -}} 23 |

24 | 25 | {{ with (index .GoPackages 0 )}} 26 | {{ with .DocComments }} 27 | {{ safe (renderComments .) }} 28 | {{ end }} 29 | {{ end }} 30 | 31 |

Resource Types:

32 | 33 |
    34 | {{- range (visibleTypes (sortedTypes .Types)) -}} 35 | {{ if isExportedType . -}} 36 |
  • 37 | {{ typeDisplayName . }} 38 |
  • 39 | {{- end }} 40 | {{- end -}} 41 |
42 | 43 | {{ range (visibleTypes (sortedTypes .Types))}} 44 | {{ template "type" . }} 45 | {{ end }} 46 | {{ end }} 47 | 48 |

This page was automatically generated with gen-crd-api-reference-docs

49 | {{ end }} 50 | -------------------------------------------------------------------------------- /hack/api-docs/template/type.tpl: -------------------------------------------------------------------------------- 1 | {{ define "type" }} 2 |

3 | {{- .Name.Name }} 4 | {{ if eq .Kind "Alias" }}({{.Underlying}} alias){{ end -}} 5 |

6 | 7 | {{ with (typeReferences .) }} 8 |

9 | (Appears on: 10 | {{- $prev := "" -}} 11 | {{- range . -}} 12 | {{- if $prev -}}, {{ end -}} 13 | {{ $prev = . }} 14 | {{ typeDisplayName . }} 15 | {{- end -}} 16 | ) 17 |

18 | {{ end }} 19 | 20 | {{ with .CommentLines }} 21 | {{ safe (renderComments .) }} 22 | {{ end }} 23 | 24 | {{ if .Members }} 25 |
26 |
27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | {{ if isExportedType . }} 36 | 37 | 40 | 43 | 44 | 45 | 49 | 52 | 53 | {{ end }} 54 | {{ template "members" . }} 55 | 56 |
FieldDescription
38 | apiVersion
39 | string
41 | {{ apiGroup . }} 42 |
46 | kind
47 | string 48 |
50 | {{ .Name.Name }} 51 |
57 |
58 |
59 | {{ end }} 60 | {{ end }} 61 | -------------------------------------------------------------------------------- /hack/boilerplate.go.txt: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. -------------------------------------------------------------------------------- /hack/create-kind-cluster.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Adapted from: 4 | # https://kind.sigs.k8s.io/docs/user/local-registry/ 5 | 6 | set -o errexit 7 | 8 | # If you wish to change the cluster name, reg_name or reg_port, make sure to also update 9 | # the following files: 10 | # teardown-kind-with-registry.sh 11 | # .kind-cluster.yaml 12 | KIND_CLUSTER_NAME="${KIND_CLUSTER_NAME:-orkestra}" 13 | reg_name='kind-registry' 14 | reg_port='5000' 15 | 16 | if kind get clusters | grep -q ^"${KIND_CLUSTER_NAME}"$ ; then 17 | echo "cluster already exists, moving on" 18 | exit 0 19 | fi 20 | 21 | # Create registry container unless it already exists 22 | running="$(docker inspect -f '{{.State.Running}}' "${reg_name}" 2>/dev/null || true)" 23 | if [ "${running}" != 'true' ]; then 24 | echo "> Creating kind Registry container..." 25 | docker run \ 26 | -d --restart=always -p "127.0.0.1:${reg_port}:5000" --name "${reg_name}" \ 27 | registry:2 28 | fi 29 | 30 | # create a kind cluster with the local registry enabled in containerd 31 | 32 | kind create cluster --name "${KIND_CLUSTER_NAME}" --config=.kind-cluster.yaml 33 | 34 | # connect the registry to the cluster network 35 | # (the network may already be connected) 36 | docker network connect "kind" "${reg_name}" || true 37 | 38 | # Document the local registry 39 | # https://github.com/kubernetes/enhancements/tree/master/keps/sig-cluster-lifecycle/generic/1755-communicating-a-local-registry 40 | cat </dev/null || true)" 10 | if [ "${running}" == 'true' ]; then 11 | cid="$(docker inspect -f '{{.ID}}' "${reg_name}")" 12 | echo "> Stopping and deleting Kind Registry container..." 13 | docker stop $cid >/dev/null 14 | docker rm $cid >/dev/null 15 | fi 16 | 17 | kind delete cluster --name=$KIND_CLUSTER_NAME 2>&1 18 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package main 5 | 6 | import ( 7 | "context" 8 | "flag" 9 | "os" 10 | "time" 11 | 12 | "github.com/Azure/Orkestra/pkg/utils" 13 | "github.com/Azure/Orkestra/pkg/workflow" 14 | 15 | "github.com/Azure/Orkestra/pkg/registry" 16 | "k8s.io/apimachinery/pkg/runtime" 17 | clientgoscheme "k8s.io/client-go/kubernetes/scheme" 18 | _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" 19 | ctrl "sigs.k8s.io/controller-runtime" 20 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 21 | 22 | orkestrav1alpha1 "github.com/Azure/Orkestra/api/v1alpha1" 23 | "github.com/Azure/Orkestra/controllers" 24 | v1alpha13 "github.com/argoproj/argo-workflows/v3/pkg/apis/workflow/v1alpha1" 25 | fluxhelmv2beta1 "github.com/fluxcd/helm-controller/api/v2beta1" 26 | // +kubebuilder:scaffold:imports 27 | ) 28 | 29 | const ( 30 | stagingRepoURLEnv = "STAGING_REPO_URL" 31 | ) 32 | 33 | var ( 34 | scheme = runtime.NewScheme() 35 | setupLog = ctrl.Log.WithName("setup") 36 | ) 37 | 38 | func init() { 39 | _ = clientgoscheme.AddToScheme(scheme) 40 | 41 | _ = orkestrav1alpha1.AddToScheme(scheme) 42 | // +kubebuilder:scaffold:scheme 43 | 44 | // Add Argo Workflow scheme to operator 45 | _ = v1alpha13.AddToScheme(scheme) 46 | 47 | // Add HelmRelease scheme to operator 48 | _ = fluxhelmv2beta1.AddToScheme(scheme) 49 | } 50 | 51 | func main() { 52 | var ( 53 | metricsAddr string 54 | enableLeaderElection bool 55 | configPath string 56 | stagingRepoURL string 57 | tempChartStoreTargetDir string 58 | disableRemediation bool 59 | cleanupDownloadedCharts bool 60 | debug bool 61 | workflowParallelism int64 62 | logLevel int 63 | enableZapLogDevMode bool 64 | ) 65 | 66 | flag.StringVar(&metricsAddr, "metrics-addr", ":8081", "The address the metric endpoint binds to.") 67 | flag.BoolVar(&enableLeaderElection, "enable-leader-election", false, 68 | "Enable leader election for controller manager. "+ 69 | "Enabling this will ensure there is only one active controller manager.") 70 | flag.StringVar(&configPath, "config", "", "The path to the controller config file") 71 | flag.StringVar(&stagingRepoURL, "staging-repo-url", "", "The URL for the helm registry used for staging artifacts (ENV - STAGING_REPO_URL). NOTE: Flag overrides env value") 72 | flag.StringVar(&tempChartStoreTargetDir, "chart-store-path", "", "The temporary storage path for the downloaded and staged chart artifacts") 73 | flag.BoolVar(&disableRemediation, "disable-remediation", false, "Disable the remediation (delete/rollback) of the workflow on failure (useful if you wish to debug failures in the workflow/executor container") 74 | flag.BoolVar(&cleanupDownloadedCharts, "cleanup-downloaded-charts", false, "Enable/disable the cleanup of the charts downloaded to the chart-store-path") 75 | flag.BoolVar(&debug, "debug", false, "Enable debug run of the appgroup controller") 76 | flag.Int64Var(&workflowParallelism, "workflow-parallelism", 10, "Specifies the max number of workflow pods that can be executed in parallel") 77 | flag.IntVar(&logLevel, "log-level", 0, "Log Level") 78 | flag.Parse() 79 | 80 | if logLevel < 0 { 81 | enableZapLogDevMode = true 82 | } 83 | ctrl.SetLogger(zap.New(zap.UseDevMode(enableZapLogDevMode))) 84 | 85 | // Start the probe at the very beginning 86 | probe, err := utils.ProbeHandler(stagingRepoURL, "health") 87 | if err != nil { 88 | setupLog.Error(err, "unable to start readiness/liveness probes", "controller", "ApplicationGroup") 89 | os.Exit(1) 90 | } 91 | 92 | probe.Start("8086") 93 | 94 | ctrl.Log.V(logLevel) 95 | 96 | mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ 97 | Scheme: scheme, 98 | MetricsBindAddress: metricsAddr, 99 | Port: 9443, 100 | LeaderElection: enableLeaderElection, 101 | LeaderElectionID: "fdcf4a0d.azure.microsoft.com", 102 | }) 103 | if err != nil { 104 | setupLog.Error(err, "unable to start manager") 105 | os.Exit(1) 106 | } 107 | 108 | // Grabbing the values based on the passed helm flags, these values change if we run in debug mode 109 | stagingHelmURL, workflowHelmURL, tempChartStoreTargetDir := getValues(stagingRepoURL, tempChartStoreTargetDir, debug) 110 | 111 | if stagingHelmURL == "" { 112 | s := os.Getenv(stagingRepoURLEnv) 113 | if s == "" { 114 | setupLog.Error(err, "staging repo URL must be set") 115 | os.Exit(1) 116 | } 117 | stagingHelmURL = s 118 | } 119 | 120 | rc, err := registry.NewClient( 121 | ctrl.Log, 122 | registry.TargetDir(tempChartStoreTargetDir), 123 | ) 124 | if err != nil { 125 | setupLog.Error(err, "unable to create new registry client", "controller", "registry-client") 126 | os.Exit(1) 127 | } 128 | 129 | // Register the staging helm repository/registry 130 | // We perform retry on this so that we don't go into a crash loop backoff 131 | retryChan := make(chan bool) 132 | retryCtx, cancel := context.WithTimeout(context.Background(), time.Minute*5) 133 | go func() { 134 | for { 135 | err = rc.AddRepo(®istry.Config{ 136 | Name: "staging", 137 | URL: stagingHelmURL, 138 | }) 139 | if err != nil { 140 | setupLog.Error(err, "failed to add staging helm repo, retrying...") 141 | time.Sleep(time.Second * 5) 142 | } else { 143 | retryChan <- true 144 | break 145 | } 146 | } 147 | }() 148 | select { 149 | case <-retryChan: 150 | cancel() 151 | close(retryChan) 152 | setupLog.Info("successfully set-up the local chartmuseum helm repository") 153 | case <-retryCtx.Done(): 154 | cancel() 155 | close(retryChan) 156 | setupLog.Error(err, "pod timed out while trying to setup the helm chart museum...") 157 | os.Exit(1) 158 | } 159 | 160 | baseLogger := ctrl.Log.WithName("controllers").WithName("ApplicationGroup") 161 | 162 | if err = (&controllers.ApplicationGroupReconciler{ 163 | Client: mgr.GetClient(), 164 | Log: baseLogger, 165 | Scheme: mgr.GetScheme(), 166 | RegistryClient: rc, 167 | StagingRepoName: "staging", 168 | WorkflowClientBuilder: workflow.NewBuilder(mgr.GetClient(), baseLogger).WithStagingRepo(workflowHelmURL).WithParallelism(workflowParallelism).InNamespace(workflow.GetNamespace()), 169 | TargetDir: tempChartStoreTargetDir, 170 | Recorder: mgr.GetEventRecorderFor("appgroup-controller"), 171 | DisableRemediation: disableRemediation, 172 | CleanupDownloadedCharts: cleanupDownloadedCharts, 173 | }).SetupWithManager(mgr); err != nil { 174 | setupLog.Error(err, "unable to create controller", "controller", "ApplicationGroup") 175 | os.Exit(1) 176 | } 177 | 178 | if err = (&controllers.WorkflowStatusReconciler{ 179 | Client: mgr.GetClient(), 180 | Log: baseLogger, 181 | Scheme: mgr.GetScheme(), 182 | WorkflowClientBuilder: workflow.NewBuilder(mgr.GetClient(), baseLogger).WithStagingRepo(workflowHelmURL).WithParallelism(workflowParallelism).InNamespace(workflow.GetNamespace()), 183 | Recorder: mgr.GetEventRecorderFor("appgroup-controller"), 184 | }).SetupWithManager(mgr); err != nil { 185 | setupLog.Error(err, "unable to create controller", "controller", "WorkflowStatus") 186 | os.Exit(1) 187 | } 188 | 189 | // +kubebuilder:scaffold:builder 190 | 191 | setupLog.Info("starting manager") 192 | if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { 193 | setupLog.Error(err, "problem running manager") 194 | os.Exit(1) 195 | } 196 | } 197 | 198 | // getValues returns the stagingRepoUrl unless the appGroup controller 199 | // is run in a debug mode, then it returns the port forwarded url 200 | func getValues(stagingHelmURL, tempChartStoreTargetDir string, debug bool) (string, string, string) { 201 | if debug { 202 | return "http://127.0.0.1:8080", "http://orkestra-chartmuseum.orkestra:8080", os.TempDir() 203 | } 204 | return stagingHelmURL, stagingHelmURL, tempChartStoreTargetDir 205 | } 206 | -------------------------------------------------------------------------------- /okteto.yml: -------------------------------------------------------------------------------- 1 | name: orkestra 2 | namespace: orkestra 3 | image: okteto/golang:1 4 | command: bash 5 | securityContext: 6 | capabilities: 7 | add: 8 | - SYS_PTRACE 9 | volumes: 10 | - /go/pkg/ 11 | - /root/.cache/go-build/ 12 | - /root/.vscode-server # persist vscode extensions 13 | sync: 14 | - .:/usr/src/app 15 | -------------------------------------------------------------------------------- /pkg/executor/custom.go: -------------------------------------------------------------------------------- 1 | package executor 2 | 3 | import ( 4 | "encoding/base64" 5 | "encoding/json" 6 | "fmt" 7 | 8 | "github.com/Azure/Orkestra/pkg/utils" 9 | v1alpha13 "github.com/argoproj/argo-workflows/v3/pkg/apis/workflow/v1alpha1" 10 | corev1 "k8s.io/api/core/v1" 11 | apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 12 | ) 13 | 14 | type CustomForward struct { 15 | Image *corev1.Container 16 | } 17 | 18 | func (exec CustomForward) Reverse() Executor { 19 | return CustomReverse{exec.Image} 20 | } 21 | 22 | func (exec CustomForward) GetName() string { 23 | return "custom-forward-executor" 24 | } 25 | 26 | func (exec CustomForward) GetTemplate() v1alpha13.Template { 27 | return customBaseTemplate(exec.GetName(), Install, exec.Image) 28 | } 29 | 30 | func (exec CustomForward) GetTask(name string, dependencies []string, timeout, hrStr string, taskParams *apiextensionsv1.JSON) (v1alpha13.DAGTask, error) { 31 | return customBaseTask(exec.GetName(), name, dependencies, timeout, hrStr, taskParams) 32 | } 33 | 34 | type CustomReverse struct { 35 | Image *corev1.Container 36 | } 37 | 38 | func (exec CustomReverse) Reverse() Executor { 39 | return CustomForward{exec.Image} 40 | } 41 | 42 | func (exec CustomReverse) GetName() string { 43 | return "custom-reverse-executor" 44 | } 45 | 46 | func (exec CustomReverse) GetTemplate() v1alpha13.Template { 47 | return customBaseTemplate(exec.GetName(), Delete, exec.Image) 48 | } 49 | 50 | func (exec CustomReverse) GetTask(name string, dependencies []string, timeout, hrStr string, taskParams *apiextensionsv1.JSON) (v1alpha13.DAGTask, error) { 51 | return customBaseTask(exec.GetName(), name, dependencies, timeout, hrStr, taskParams) 52 | } 53 | 54 | func customBaseTemplate(executorName string, action Action, image *corev1.Container) v1alpha13.Template { 55 | executorArgs := []string{"--spec", "{{inputs.parameters.helmrelease}}", "--action", string(action), "--data", "{{inputs.parameters.data}}", "--timeout", "{{inputs.parameters.timeout}}", "--interval", "1s"} 56 | return v1alpha13.Template{ 57 | Name: executorName, 58 | ServiceAccountName: workflowServiceAccountName(), 59 | Inputs: v1alpha13.Inputs{ 60 | Parameters: []v1alpha13.Parameter{ 61 | { 62 | Name: HelmReleaseArg, 63 | }, 64 | { 65 | Name: TimeoutArg, 66 | Default: utils.ToAnyStringPtr(DefaultTimeout), 67 | }, 68 | { 69 | Name: OpaqueDataArg, 70 | }, 71 | }, 72 | }, 73 | Executor: &v1alpha13.ExecutorConfig{ 74 | ServiceAccountName: workflowServiceAccountName(), 75 | }, 76 | Container: &corev1.Container{ 77 | Name: image.Name, 78 | Image: image.Image, 79 | Args: executorArgs, 80 | }, 81 | } 82 | } 83 | 84 | func customBaseTask(executorName, name string, dependencies []string, timeout, hrStr string, taskParams *apiextensionsv1.JSON) (v1alpha13.DAGTask, error) { 85 | expectedParameters := &CustomParameters{} 86 | if taskParams == nil { 87 | return v1alpha13.DAGTask{}, fmt.Errorf("task parameters are required for the custom executor task") 88 | } 89 | if err := json.Unmarshal(taskParams.Raw, expectedParameters); err != nil { 90 | return v1alpha13.DAGTask{}, err 91 | } 92 | 93 | // Data must always be base64 encoded for the custom executor 94 | b64Data := base64.StdEncoding.EncodeToString(expectedParameters.Data.Raw) 95 | 96 | data := string(b64Data) 97 | 98 | return v1alpha13.DAGTask{ 99 | Name: utils.ConvertToDNS1123(name), 100 | Template: executorName, 101 | Arguments: v1alpha13.Arguments{ 102 | Parameters: []v1alpha13.Parameter{ 103 | { 104 | Name: HelmReleaseArg, 105 | Value: utils.ToAnyStringPtr(hrStr), 106 | }, 107 | { 108 | Name: TimeoutArg, 109 | Value: utils.ToAnyStringPtr(timeout), 110 | }, 111 | { 112 | Name: OpaqueDataArg, 113 | Value: utils.ToAnyStringPtr(data), 114 | }, 115 | }, 116 | }, 117 | Dependencies: dependencies, 118 | }, nil 119 | } 120 | 121 | type CustomParameters struct { 122 | Data apiextensionsv1.JSON 123 | } 124 | -------------------------------------------------------------------------------- /pkg/executor/executor.go: -------------------------------------------------------------------------------- 1 | package executor 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/Azure/Orkestra/api/v1alpha1" 7 | corev1 "k8s.io/api/core/v1" 8 | apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 9 | 10 | v1alpha13 "github.com/argoproj/argo-workflows/v3/pkg/apis/workflow/v1alpha1" 11 | ) 12 | 13 | // Action defines the set of executor actions which can be performed on a helmrelease object 14 | type Action string 15 | 16 | const ( 17 | Install Action = "install" 18 | Delete Action = "delete" 19 | ) 20 | 21 | const ( 22 | DefaultTimeout = "5m" 23 | ExecutorName = "executor" 24 | 25 | HelmReleaseArg = "helmrelease" 26 | TimeoutArg = "timeout" 27 | 28 | // OpaqueDataArg is a base64 encoded string containing the data to be passed to the executor 29 | OpaqueDataArg = "data" 30 | ) 31 | 32 | func workflowServiceAccountName() string { 33 | if sa, ok := os.LookupEnv("WORKFLOW_SERVICEACCOUNT_NAME"); ok { 34 | return sa 35 | } 36 | return "orkestra" 37 | } 38 | 39 | type Executor interface { 40 | GetName() string 41 | Reverse() Executor 42 | GetTemplate() v1alpha13.Template 43 | GetTask(name string, dependencies []string, timeout, hrStr string, parameters *apiextensionsv1.JSON) (v1alpha13.DAGTask, error) 44 | } 45 | 46 | func ForwardFactory(executorType v1alpha1.ExecutorType, image *corev1.Container) Executor { 47 | switch executorType { 48 | case v1alpha1.KeptnExecutor: 49 | return KeptnForward{} 50 | case v1alpha1.CustomExecutor: 51 | return CustomForward{ 52 | Image: image, 53 | } 54 | default: 55 | return HelmReleaseForward{} 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /pkg/executor/helmrelease.go: -------------------------------------------------------------------------------- 1 | package executor 2 | 3 | import ( 4 | "fmt" 5 | "github.com/Azure/Orkestra/pkg/utils" 6 | v1alpha13 "github.com/argoproj/argo-workflows/v3/pkg/apis/workflow/v1alpha1" 7 | corev1 "k8s.io/api/core/v1" 8 | apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 9 | ) 10 | 11 | const ( 12 | HelmReleaseImage = "azureorkestra/executor" 13 | HelmReleaseTag = "v0.4.2" 14 | ) 15 | 16 | type HelmReleaseForward struct{} 17 | 18 | func (exec HelmReleaseForward) Reverse() Executor { 19 | return HelmReleaseReverse{} 20 | } 21 | 22 | func (exec HelmReleaseForward) GetName() string { 23 | return "helmrelease-forward-executor" 24 | } 25 | 26 | func (exec HelmReleaseForward) GetTemplate() v1alpha13.Template { 27 | return helmReleaseBaseTemplate(exec.GetName(), Install) 28 | } 29 | 30 | func (exec HelmReleaseForward) GetTask(name string, dependencies []string, timeout, hrStr string, taskParams *apiextensionsv1.JSON) (v1alpha13.DAGTask, error) { 31 | return helmReleaseBaseTask(exec.GetName(), name, dependencies, timeout, hrStr), nil 32 | } 33 | 34 | type HelmReleaseReverse struct{} 35 | 36 | func (exec HelmReleaseReverse) Reverse() Executor { 37 | return HelmReleaseForward{} 38 | } 39 | 40 | func (exec HelmReleaseReverse) GetName() string { 41 | return "helmrelease-reverse-executor" 42 | } 43 | 44 | func (exec HelmReleaseReverse) GetTemplate() v1alpha13.Template { 45 | return helmReleaseBaseTemplate(exec.GetName(), Delete) 46 | } 47 | 48 | func (exec HelmReleaseReverse) GetTask(name string, dependencies []string, timeout, hrStr string, taskParams *apiextensionsv1.JSON) (v1alpha13.DAGTask, error) { 49 | return helmReleaseBaseTask(exec.GetName(), name, dependencies, timeout, hrStr), nil 50 | } 51 | 52 | func helmReleaseBaseTemplate(executorName string, action Action) v1alpha13.Template { 53 | executorArgs := []string{"--spec", "{{inputs.parameters.helmrelease}}", "--action", string(action), "--timeout", "{{inputs.parameters.timeout}}", "--interval", "1s"} 54 | return v1alpha13.Template{ 55 | Name: executorName, 56 | ServiceAccountName: workflowServiceAccountName(), 57 | Inputs: v1alpha13.Inputs{ 58 | Parameters: []v1alpha13.Parameter{ 59 | { 60 | Name: HelmReleaseArg, 61 | }, 62 | { 63 | Name: TimeoutArg, 64 | Default: utils.ToAnyStringPtr(DefaultTimeout), 65 | }, 66 | }, 67 | }, 68 | Executor: &v1alpha13.ExecutorConfig{ 69 | ServiceAccountName: workflowServiceAccountName(), 70 | }, 71 | Container: &corev1.Container{ 72 | Name: ExecutorName, 73 | Image: fmt.Sprintf("%s:%s", HelmReleaseImage, HelmReleaseTag), 74 | Args: executorArgs, 75 | }, 76 | } 77 | } 78 | 79 | func helmReleaseBaseTask(executorName, name string, dependencies []string, timeout, hrStr string) v1alpha13.DAGTask { 80 | return v1alpha13.DAGTask{ 81 | Name: utils.ConvertToDNS1123(name), 82 | Template: executorName, 83 | Arguments: v1alpha13.Arguments{ 84 | Parameters: []v1alpha13.Parameter{ 85 | { 86 | Name: HelmReleaseArg, 87 | Value: utils.ToAnyStringPtr(hrStr), 88 | }, 89 | { 90 | Name: TimeoutArg, 91 | Value: utils.ToAnyStringPtr(timeout), 92 | }, 93 | }, 94 | }, 95 | Dependencies: dependencies, 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /pkg/executor/keptn.go: -------------------------------------------------------------------------------- 1 | package executor 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/Azure/Orkestra/pkg/utils" 8 | v1alpha13 "github.com/argoproj/argo-workflows/v3/pkg/apis/workflow/v1alpha1" 9 | corev1 "k8s.io/api/core/v1" 10 | apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 11 | ) 12 | 13 | const ( 14 | KeptnImage = "azureorkestra/keptn-executor" 15 | KeptnTag = "v0.1.0" 16 | ) 17 | 18 | const ( 19 | configMapName = "configMapName" 20 | configMapNamespace = "configMapNamespace" 21 | ) 22 | 23 | type KeptnForward struct{} 24 | 25 | func (exec KeptnForward) Reverse() Executor { 26 | return KeptnReverse{} 27 | } 28 | 29 | func (exec KeptnForward) GetName() string { 30 | return "keptn-forward-executor" 31 | } 32 | 33 | func (exec KeptnForward) GetTemplate() v1alpha13.Template { 34 | return keptnBaseTemplate(exec.GetName(), Install) 35 | } 36 | 37 | func (exec KeptnForward) GetTask(name string, dependencies []string, timeout, hrStr string, taskParams *apiextensionsv1.JSON) (v1alpha13.DAGTask, error) { 38 | return keptnBaseTask(exec.GetName(), name, dependencies, timeout, hrStr, taskParams) 39 | } 40 | 41 | type KeptnReverse struct{} 42 | 43 | func (exec KeptnReverse) Reverse() Executor { 44 | return KeptnForward{} 45 | } 46 | 47 | func (exec KeptnReverse) GetName() string { 48 | return "keptn-reverse-executor" 49 | } 50 | 51 | func (exec KeptnReverse) GetTemplate() v1alpha13.Template { 52 | return keptnBaseTemplate(exec.GetName(), Delete) 53 | } 54 | 55 | func (exec KeptnReverse) GetTask(name string, dependencies []string, timeout, hrStr string, taskParams *apiextensionsv1.JSON) (v1alpha13.DAGTask, error) { 56 | return keptnBaseTask(exec.GetName(), name, dependencies, timeout, hrStr, taskParams) 57 | } 58 | 59 | func keptnBaseTemplate(executorName string, action Action) v1alpha13.Template { 60 | executorArgs := []string{"--spec", "{{inputs.parameters.helmrelease}}", "--action", string(action), "--configmap-name", "{{inputs.parameters.configMapName}}", "--configmap-namespace", "{{inputs.parameters.configMapNamespace}}", "--timeout", "{{inputs.parameters.timeout}}", "--interval", "1s"} 61 | return v1alpha13.Template{ 62 | Name: executorName, 63 | ServiceAccountName: workflowServiceAccountName(), 64 | Inputs: v1alpha13.Inputs{ 65 | Parameters: []v1alpha13.Parameter{ 66 | { 67 | Name: HelmReleaseArg, 68 | }, 69 | { 70 | Name: TimeoutArg, 71 | Default: utils.ToAnyStringPtr(DefaultTimeout), 72 | }, 73 | { 74 | Name: configMapName, 75 | }, 76 | { 77 | Name: configMapNamespace, 78 | }, 79 | }, 80 | }, 81 | Executor: &v1alpha13.ExecutorConfig{ 82 | ServiceAccountName: workflowServiceAccountName(), 83 | }, 84 | Container: &corev1.Container{ 85 | Name: executorName, 86 | Image: fmt.Sprintf("%s:%s", KeptnImage, KeptnTag), 87 | Args: executorArgs, 88 | }, 89 | } 90 | } 91 | 92 | func keptnBaseTask(executorName, name string, dependencies []string, timeout, hrStr string, taskParams *apiextensionsv1.JSON) (v1alpha13.DAGTask, error) { 93 | expectedParameters := &KeptnParameters{} 94 | if taskParams == nil { 95 | return v1alpha13.DAGTask{}, fmt.Errorf("task parameters are required for the keptn executor task") 96 | } 97 | if err := json.Unmarshal(taskParams.Raw, expectedParameters); err != nil { 98 | return v1alpha13.DAGTask{}, err 99 | } 100 | return v1alpha13.DAGTask{ 101 | Name: utils.ConvertToDNS1123(name), 102 | Template: executorName, 103 | Arguments: v1alpha13.Arguments{ 104 | Parameters: []v1alpha13.Parameter{ 105 | { 106 | Name: HelmReleaseArg, 107 | Value: utils.ToAnyStringPtr(hrStr), 108 | }, 109 | { 110 | Name: TimeoutArg, 111 | Value: utils.ToAnyStringPtr(timeout), 112 | }, 113 | { 114 | Name: configMapName, 115 | Value: utils.ToAnyStringPtr(expectedParameters.ConfigMapRef.Name), 116 | }, 117 | { 118 | Name: configMapNamespace, 119 | Value: utils.ToAnyStringPtr(expectedParameters.ConfigMapRef.Namespace), 120 | }, 121 | }, 122 | }, 123 | Dependencies: dependencies, 124 | }, nil 125 | } 126 | 127 | type KeptnParameters struct { 128 | ConfigMapRef corev1.ObjectReference `json:"configMapRef"` 129 | } 130 | -------------------------------------------------------------------------------- /pkg/meta/conditions.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Flux authors 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package meta 15 | 16 | import ( 17 | fluxhelmv2beta1 "github.com/fluxcd/helm-controller/api/v2beta1" 18 | apimeta "k8s.io/apimachinery/pkg/api/meta" 19 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 20 | ) 21 | 22 | const ( 23 | // ReadyCondition is the name of the workflow condition 24 | // This captures the status of the entire ApplicationGroup 25 | ReadyCondition string = "Ready" 26 | 27 | ForwardWorkflowSucceededCondition string = "ForwardWorkflowSucceeded" 28 | 29 | ReverseWorkflowSucceededCondition string = "ReverseWorkflowSucceeded" 30 | 31 | RollbackWorkflowSucceededCondition string = "RollbackWorkflowSucceeded" 32 | ) 33 | 34 | const ( 35 | // SucceededReason represents the condition succeeding 36 | SucceededReason string = "Succeeded" 37 | 38 | // FailedReason represents the fact that the the reconciliation failed 39 | FailedReason string = "Failed" 40 | 41 | // ProgressingReason represents the fact that the application group reconciler 42 | // is reconciling the app group and the forward workflow has not completed 43 | ProgressingReason string = "Progressing" 44 | 45 | // TerminatingReason represents that the application group is deleting 46 | // and waiting for the reverse workflow to complete 47 | TerminatingReason string = "Terminating" 48 | 49 | // SuspendedReason represents that the workflow is in a suspended state 50 | SuspendedReason string = "Suspended" 51 | 52 | // ChartPullFailedReason represents the fact that the application group reconcile 53 | // was unable to pull from the chart repo specified 54 | ChartPullFailedReason string = "ChartPullFailed" 55 | 56 | // WorkflowFailedReason represents the fact that a workflow step failed and is the reason 57 | // why the application group was unable to successfully reconcile 58 | WorkflowFailedReason string = "WorkflowFailed" 59 | 60 | // WorkflowTemplateGenerationFailedReason represents the fact that the application group was unable 61 | // to generate the templates for the workflow reconciliation 62 | WorkflowTemplateGenerationFailedReason string = "WorkflowTemplateGenerationFailed" 63 | ) 64 | 65 | // ObjectWithStatusConditions is an interface that describes kubernetes resource 66 | // type structs with Status Conditions 67 | // +k8s:deepcopy-gen=false 68 | type ObjectWithStatusConditions interface { 69 | GetStatusConditions() *[]metav1.Condition 70 | } 71 | 72 | // SetResourceCondition sets the given condition with the given status, 73 | // reason and message on a resource. 74 | func SetResourceCondition(obj ObjectWithStatusConditions, condition string, status metav1.ConditionStatus, reason, message string) { 75 | conditions := obj.GetStatusConditions() 76 | newCondition := metav1.Condition{ 77 | Type: condition, 78 | Status: status, 79 | Reason: reason, 80 | Message: message, 81 | } 82 | apimeta.SetStatusCondition(conditions, newCondition) 83 | } 84 | 85 | func GetResourceCondition(obj ObjectWithStatusConditions, condition string) *metav1.Condition { 86 | conditions := obj.GetStatusConditions() 87 | return apimeta.FindStatusCondition(*conditions, condition) 88 | } 89 | 90 | func IsFailedHelmReason(reason string) bool { 91 | switch reason { 92 | case fluxhelmv2beta1.InstallFailedReason, fluxhelmv2beta1.UpgradeFailedReason, fluxhelmv2beta1.UninstallFailedReason, 93 | fluxhelmv2beta1.ArtifactFailedReason, fluxhelmv2beta1.InitFailedReason, fluxhelmv2beta1.GetLastReleaseFailedReason: 94 | return true 95 | } 96 | return false 97 | } 98 | -------------------------------------------------------------------------------- /pkg/meta/errors.go: -------------------------------------------------------------------------------- 1 | package meta 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | var ( 8 | ErrInvalidSpec = errors.New("custom resource spec is invalid") 9 | ErrWorkflowFailure = errors.New("workflow in failure status") 10 | ErrHelmReleaseStatusFailure = errors.New("helmrelease in failure status") 11 | 12 | ErrForwardWorkflowNotFound = errors.New("forward workflow not found") 13 | ErrPreviousSpecNotSet = errors.New("failed to generate rollback workflow, previous spec is unset") 14 | ) 15 | -------------------------------------------------------------------------------- /pkg/registry/config.go: -------------------------------------------------------------------------------- 1 | package registry 2 | 3 | // Config struct captures the configuration fields as per the repoAddOptions - https://github.com/helm/helm/blob/v3.1.2/cmd/helm/repo_add.go#L39 4 | type Config struct { 5 | Name string `yaml:"name" json:"name,omitempty"` 6 | URL string `yaml:"url" json:"url,omitempty"` 7 | Username string `yaml:"username" json:"username,omitempty"` 8 | Password string `yaml:"password" json:"password,omitempty"` 9 | AuthHeader string `yaml:"authHeader" json:"auth_header,omitempty"` 10 | CaFile string `yaml:"caFile" json:"ca_file,omitempty"` 11 | CertFile string `yaml:"certFile" json:"cert_file,omitempty"` 12 | KeyFile string `yaml:"keyFile" json:"key_file,omitempty"` 13 | InsecureSkipVerify bool `yaml:"insecureSkipVerify" json:"insecure_skip_verify,omitempty"` 14 | AccessToken string `yaml:"accessToken" json:"access_token,omitempty"` 15 | } 16 | -------------------------------------------------------------------------------- /pkg/registry/options.go: -------------------------------------------------------------------------------- 1 | package registry 2 | 3 | import ( 4 | "os" 5 | 6 | "helm.sh/helm/v3/pkg/action" 7 | ) 8 | 9 | const ( 10 | ReadWritePerm = 0777 11 | ) 12 | 13 | type Option func(c *Client) 14 | 15 | type PullOption func(p *action.Pull) 16 | 17 | // Client Options 18 | 19 | func TargetDir(d string) Option { 20 | // check if target dir exists. 21 | // if doesnt exist create one. 22 | if _, err := os.Stat(d); os.IsNotExist(err) { 23 | _ = os.Mkdir(d, ReadWritePerm) 24 | } 25 | 26 | return func(c *Client) { 27 | c.TargetDir = d 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /pkg/registry/pull.go: -------------------------------------------------------------------------------- 1 | package registry 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | 8 | "github.com/go-logr/logr" 9 | "helm.sh/helm/v3/pkg/chart" 10 | "helm.sh/helm/v3/pkg/chart/loader" 11 | ) 12 | 13 | func (c *Client) PullChart(l logr.Logger, repoKey, chartName, version string) (string, *chart.Chart, error) { 14 | // logic is derived from the "helm pull" command from the helm cli package 15 | l.WithValues("repo-key", repoKey, "chart-name", chartName, "chart-version", version) 16 | 17 | l.V(3).Info("pulling chart") 18 | 19 | rCfg, err := c.RegistryConfig(repoKey) 20 | if err != nil { 21 | l.Error(err, "failed to find registry with provided key in registries map") 22 | return "", nil, fmt.Errorf("failed to find registry with repoKey %s Name %s Version %s in registries map : %w", repoKey, chartName, version, err) 23 | } 24 | 25 | c.cfg.pull.Username = rCfg.Username 26 | c.cfg.pull.Password = rCfg.Password 27 | c.cfg.pull.CaFile = rCfg.CaFile 28 | c.cfg.pull.CaFile = rCfg.CaFile 29 | c.cfg.pull.CertFile = rCfg.CertFile 30 | c.cfg.pull.KeyFile = rCfg.KeyFile 31 | c.cfg.pull.DestDir = c.TargetDir 32 | 33 | filePath := fmt.Sprintf("%s/%s-%s.tgz", strings.TrimSuffix(c.TargetDir, "/"), chartName, version) 34 | 35 | if _, err = os.Stat(filePath); os.IsNotExist(err) { 36 | l.V(3).Info("chart artifact not found in target directory - downloading") 37 | _, err = c.cfg.pull.Run(chartURL(rCfg.URL, chartName, version)) 38 | if err != nil { 39 | l.Error(err, "failed to pull chart from repo") 40 | return "", nil, fmt.Errorf("failed to pull chart from repoKey %s Name %s Version %s in registries map : %w", repoKey, chartName, version, err) 41 | } 42 | } else { 43 | l.V(3).Info("chart artifact found in target directory - skip downloading") 44 | } 45 | 46 | _, err = c.cfg.pull.ChartPathOptions.LocateChart(filePath, c.cfg.pull.Settings) 47 | if err != nil { 48 | l.Error(err, "failed to locate chart in filesystem") 49 | return "", nil, fmt.Errorf("failed to locate chart in filesystem at path %s : %w", filePath, err) 50 | } 51 | 52 | var ch *chart.Chart 53 | 54 | ch, err = loader.LoadFile(filePath) 55 | 56 | if err != nil { 57 | l.Error(err, "failed to load chart") 58 | return "", nil, fmt.Errorf("failed to load chart: %w", err) 59 | } 60 | 61 | if !(ch.Metadata.Type == "application" || ch.Metadata.Type == "") { 62 | return "", nil, fmt.Errorf("%s charts are not installable", ch.Metadata.Type) 63 | } 64 | 65 | return filePath, ch, nil 66 | } 67 | -------------------------------------------------------------------------------- /pkg/registry/push.go: -------------------------------------------------------------------------------- 1 | package registry 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | "os" 9 | 10 | "github.com/chartmuseum/helm-push/pkg/chartmuseum" 11 | "github.com/go-logr/logr" 12 | "helm.sh/helm/v3/pkg/chart" 13 | ) 14 | 15 | // PushChart pushes the chart to the repository specified by the repoKey. The repository setting is fetched from the associated registry config file 16 | func (c *Client) PushChart(l logr.Logger, repoKey, pkgPath string, ch *chart.Chart) error { 17 | // logic is derived from the "helm push" extension from the chartmuseum folks 18 | chartName := ch.Name() 19 | version := ch.Metadata.Version 20 | 21 | l.WithValues("repo-key", repoKey, "chart-name", chartName, "chart-version", version) 22 | l.V(3).Info("pushing chart") 23 | 24 | rCfg, err := c.RegistryConfig(repoKey) 25 | if err != nil { 26 | l.Error(err, "failed to find registry with provided key in registries map") 27 | return fmt.Errorf("failed to find registry with repoKey %s Name %s Version %s in registries map : %w", repoKey, chartName, version, err) 28 | } 29 | 30 | // Set the URL to the port-forward address:port of chartmuseum (http://localhost:8080) 31 | if url := os.Getenv("CI_ENVTEST_CHARTMUSEUM_URL"); url != "" { 32 | rCfg.URL = url 33 | } 34 | 35 | c.cfg.push, err = chartmuseum.NewClient( 36 | chartmuseum.URL(rCfg.URL), 37 | chartmuseum.Username(rCfg.Username), 38 | chartmuseum.Password(rCfg.Password), 39 | chartmuseum.AccessToken(rCfg.AccessToken), 40 | chartmuseum.AuthHeader(rCfg.AuthHeader), 41 | chartmuseum.CAFile(rCfg.CaFile), 42 | chartmuseum.CertFile(rCfg.CertFile), 43 | chartmuseum.KeyFile(rCfg.KeyFile), 44 | chartmuseum.InsecureSkipVerify(rCfg.InsecureSkipVerify), 45 | ) 46 | if err != nil { 47 | l.Error(err, "failed to create new helm push client") 48 | return fmt.Errorf("failed to create new helm push client : %w", err) 49 | } 50 | 51 | resp, err := c.cfg.push.UploadChartPackage(pkgPath, true) 52 | if err != nil { 53 | l.Error(err, "failed to upload chart package") 54 | return fmt.Errorf("failed to upload chart package with repoKey %s Name %s Version %s : %w", repoKey, chartName, version, err) 55 | } 56 | 57 | err = handlePushResponse(resp) 58 | defer resp.Body.Close() 59 | if err != nil { 60 | l.Error(err, "failed to handle upload/push http response") 61 | return fmt.Errorf("failed to handle upload/push http response for chart package with repoKey %s Name %s Version %s : %w", repoKey, chartName, version, err) 62 | } 63 | 64 | return nil 65 | } 66 | 67 | func handlePushResponse(resp *http.Response) error { 68 | if resp.StatusCode != 201 { 69 | b, err := io.ReadAll(resp.Body) 70 | if err != nil { 71 | return err 72 | } 73 | return getChartmuseumError(b, resp.StatusCode) 74 | } 75 | return nil 76 | } 77 | 78 | func getChartmuseumError(b []byte, code int) error { 79 | var er struct { 80 | Error string `json:"error"` 81 | } 82 | err := json.Unmarshal(b, &er) 83 | if err != nil || er.Error == "" { 84 | return fmt.Errorf("%d: could not properly parse response JSON: %s", code, string(b)) 85 | } 86 | return fmt.Errorf("%d: %s", code, er.Error) 87 | } 88 | -------------------------------------------------------------------------------- /pkg/registry/registry.go: -------------------------------------------------------------------------------- 1 | package registry 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "net/url" 8 | "os" 9 | "path/filepath" 10 | "strings" 11 | "time" 12 | 13 | "github.com/chartmuseum/helm-push/pkg/chartmuseum" 14 | "github.com/chartmuseum/helm-push/pkg/helm" 15 | "github.com/go-logr/logr" 16 | "github.com/gofrs/flock" 17 | "gopkg.in/yaml.v2" 18 | "helm.sh/helm/v3/pkg/action" 19 | "helm.sh/helm/v3/pkg/chart" 20 | "helm.sh/helm/v3/pkg/cli" 21 | "helm.sh/helm/v3/pkg/getter" 22 | "helm.sh/helm/v3/pkg/repo" 23 | v1 "k8s.io/api/core/v1" 24 | "k8s.io/apimachinery/pkg/types" 25 | "sigs.k8s.io/controller-runtime/pkg/client" 26 | 27 | orkestrav1alpha1 "github.com/Azure/Orkestra/api/v1alpha1" 28 | ) 29 | 30 | const ( 31 | defaultTargetDir = "/etc/orkestra/charts/pull/" 32 | ) 33 | 34 | var ( 35 | errEmptyKey = errors.New("key cannot be an empty string") 36 | errEmptyRegistries = errors.New("registries map cannot be nil or empty") 37 | errRegistryNotFound = errors.New("registry entry not found in registries map") 38 | ) 39 | 40 | type helmActionConfig struct { 41 | pull *action.Pull 42 | push *chartmuseum.Client 43 | } 44 | 45 | type Client struct { 46 | l logr.Logger 47 | // rfile is the handle to the helm repo file configuration 48 | rfile *repo.File 49 | // repoFilePath is the location of the helm repo file 50 | repoFilePath string 51 | // cfg stores the helm pull and push configurations 52 | cfg helmActionConfig 53 | // TargetDir is the location where the downloaded chart is saved 54 | TargetDir string 55 | // settings 56 | settings *cli.EnvSettings 57 | 58 | // Registries maps the registry name to it's configuration data 59 | registries map[string]*Config 60 | } 61 | 62 | // NewClient is the constructor for the registry client 63 | func NewClient(l logr.Logger, opts ...Option) (*Client, error) { 64 | cm, err := chartmuseum.NewClient() 65 | if err != nil { 66 | return nil, err 67 | } 68 | 69 | c := &Client{ 70 | l: l, 71 | TargetDir: defaultTargetDir, 72 | rfile: repo.NewFile(), 73 | cfg: helmActionConfig{ 74 | pull: action.NewPull(), 75 | push: cm, 76 | }, 77 | settings: cli.New(), 78 | registries: make(map[string]*Config), 79 | } 80 | 81 | for _, opt := range opts { 82 | opt(c) 83 | } 84 | 85 | err = c.init() 86 | if err != nil { 87 | return nil, err 88 | } 89 | 90 | return c, nil 91 | } 92 | 93 | func (c *Client) init() error { 94 | c.repoFilePath = c.settings.RepositoryConfig 95 | 96 | // Initialize the helm repo file 97 | repoFile := c.settings.RepositoryConfig 98 | err := os.MkdirAll(filepath.Dir(repoFile), os.ModePerm) 99 | if err != nil && !os.IsExist(err) { 100 | return err 101 | } 102 | 103 | // Acquire a file lock for process synchronization 104 | fileLock := flock.New(strings.Replace(repoFile, filepath.Ext(repoFile), ".lock", 1)) 105 | lockCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second) 106 | defer cancel() 107 | locked, err := fileLock.TryLockContext(lockCtx, time.Second) 108 | if err == nil && locked { 109 | defer fileLock.Unlock() //nolint:errcheck 110 | } 111 | if err != nil { 112 | return err 113 | } 114 | 115 | b, err := os.ReadFile(repoFile) 116 | if err != nil && !os.IsNotExist(err) { 117 | return err 118 | } 119 | 120 | if err := yaml.Unmarshal(b, c.rfile); err != nil { 121 | return err 122 | } 123 | 124 | // If no TargetDir option was passed, set to default location 125 | if c.TargetDir == "" { 126 | c.TargetDir = defaultTargetDir 127 | } 128 | // Set destination directory where we download the chart 129 | c.cfg.pull.DestDir = c.TargetDir 130 | 131 | // Initialize the pull and push clients 132 | // Pull client config 133 | actionCfg := new(action.Configuration) 134 | helmDriver := "memory" 135 | 136 | if err := actionCfg.Init(c.settings.RESTClientGetter(), c.settings.Namespace(), helmDriver, c.l.Info); err != nil { 137 | return fmt.Errorf("unable to initialize action configuration: %w", err) 138 | } 139 | 140 | c.cfg.pull.Settings = c.settings 141 | 142 | // Push Client 143 | // no init required 144 | 145 | return nil 146 | } 147 | 148 | func (c *Client) AddRepo(cfg *Config) error { 149 | e := repo.Entry{ 150 | Name: cfg.Name, 151 | URL: cfg.URL, 152 | Username: cfg.Username, 153 | Password: cfg.Password, 154 | CertFile: cfg.CertFile, 155 | KeyFile: cfg.KeyFile, 156 | CAFile: cfg.CaFile, 157 | } 158 | 159 | r, err := repo.NewChartRepository(&e, getter.All(c.settings)) 160 | if err != nil { 161 | return err 162 | } 163 | 164 | if _, err := r.DownloadIndexFile(); err != nil { 165 | return fmt.Errorf("%q is not a valid chart repository or cannot be reached: %w", cfg.URL, err) 166 | } 167 | 168 | c.rfile.Update(&e) 169 | 170 | if err := c.rfile.WriteFile(c.repoFilePath, 0644); err != nil { 171 | return err 172 | } 173 | 174 | c.registries[cfg.Name] = cfg 175 | 176 | return nil 177 | } 178 | 179 | func (c *Client) RegistryConfig(name string) (*Config, error) { 180 | if name == "" { 181 | return nil, errEmptyKey 182 | } 183 | if c.registries == nil { 184 | return nil, errEmptyRegistries 185 | } 186 | v, ok := c.registries[name] 187 | if !ok { 188 | return nil, errRegistryNotFound 189 | } 190 | 191 | return v, nil 192 | } 193 | 194 | func chartURL(repo, chart, version string) string { 195 | s := fmt.Sprintf("%s/%s-%s.tgz", 196 | strings.Trim(repo, "/"), 197 | strings.Trim(chart, "/"), 198 | version, 199 | ) 200 | 201 | // Validate the URL 202 | if u, err := url.ParseRequestURI(s); u != nil || err != nil { 203 | return s 204 | } 205 | return "" 206 | } 207 | 208 | func SaveChartPackage(ch *chart.Chart, dir string) (string, error) { 209 | return helm.CreateChartPackage(&helm.Chart{V3: ch}, dir) 210 | } 211 | 212 | func GetHelmRepoConfig(app *orkestrav1alpha1.Application, c client.Client) (*Config, error) { 213 | cfg := &Config{ 214 | Name: app.Name, 215 | URL: app.Spec.Chart.URL, 216 | } 217 | 218 | if app.Spec.Chart.AuthSecretRef != nil { 219 | if app.Spec.Chart.AuthSecretRef.Namespace == "" { 220 | app.Spec.Chart.AuthSecretRef.Namespace = "default" 221 | } 222 | creds := &v1.Secret{} 223 | key := types.NamespacedName{ 224 | Name: app.Spec.Chart.AuthSecretRef.Name, 225 | Namespace: app.Spec.Chart.AuthSecretRef.Namespace, 226 | } 227 | 228 | err := c.Get(context.Background(), key, creds) 229 | if err != nil { 230 | return nil, err 231 | } 232 | 233 | data := creds.Data 234 | 235 | if v, ok := data["username"]; ok { 236 | cfg.Username = string(v) 237 | } 238 | 239 | if v, ok := data["password"]; ok { 240 | cfg.Password = string(v) 241 | } 242 | 243 | if v, ok := data["username"]; ok { 244 | cfg.Username = string(v) 245 | } 246 | 247 | if v, ok := data["tls.crt"]; ok { 248 | cfg.CertFile = string(v) 249 | } 250 | 251 | if v, ok := data["tls.key"]; ok { 252 | cfg.KeyFile = string(v) 253 | } 254 | 255 | if v, ok := data["ca.crt"]; ok { 256 | cfg.CaFile = string(v) 257 | } 258 | } 259 | return cfg, nil 260 | } 261 | 262 | type CredentialsObjectReference struct { 263 | // Name of the referent 264 | Name string `yaml:"name" json:"name,omitempty"` 265 | 266 | // Namespace of the referent, 267 | Namespace string `yaml:"namespace" json:"namespace,omitempty"` 268 | } 269 | -------------------------------------------------------------------------------- /pkg/templates/templates.go: -------------------------------------------------------------------------------- 1 | package templates 2 | 3 | import ( 4 | "github.com/Azure/Orkestra/api/v1alpha1" 5 | "github.com/Azure/Orkestra/pkg/graph" 6 | "github.com/Azure/Orkestra/pkg/utils" 7 | v1alpha13 "github.com/argoproj/argo-workflows/v3/pkg/apis/workflow/v1alpha1" 8 | fluxhelmv2beta1 "github.com/fluxcd/helm-controller/api/v2beta1" 9 | fluxsourcev1beta1 "github.com/fluxcd/source-controller/api/v1beta1" 10 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 11 | ) 12 | 13 | const ( 14 | EntrypointTemplateName = "entry" 15 | ChartMuseumName = "chartmuseum" 16 | ) 17 | 18 | func GenerateWorkflow(name, namespace string, parallelism *int64) *v1alpha13.Workflow { 19 | return &v1alpha13.Workflow{ 20 | ObjectMeta: v1.ObjectMeta{ 21 | Name: name, 22 | Namespace: namespace, 23 | Labels: map[string]string{v1alpha1.HeritageLabel: v1alpha1.HeritageValue}, 24 | }, 25 | TypeMeta: v1.TypeMeta{ 26 | APIVersion: v1alpha13.WorkflowSchemaGroupVersionKind.GroupVersion().String(), 27 | Kind: v1alpha13.WorkflowSchemaGroupVersionKind.Kind, 28 | }, 29 | Spec: v1alpha13.WorkflowSpec{ 30 | Entrypoint: EntrypointTemplateName, 31 | Templates: make([]v1alpha13.Template, 0), 32 | Parallelism: parallelism, 33 | PodGC: &v1alpha13.PodGC{ 34 | Strategy: v1alpha13.PodGCOnWorkflowCompletion, 35 | }, 36 | }, 37 | } 38 | } 39 | 40 | type TemplateGenerator struct { 41 | EntryTemplate v1alpha13.Template 42 | Templates []v1alpha13.Template 43 | Namespace string 44 | Parallelism *int64 45 | } 46 | 47 | func NewTemplateGenerator(namespace string, parallelism *int64) *TemplateGenerator { 48 | return &TemplateGenerator{ 49 | Namespace: namespace, 50 | Parallelism: parallelism, 51 | } 52 | } 53 | 54 | func (tg *TemplateGenerator) AssignWorkflowTemplates(wf *v1alpha13.Workflow) { 55 | wf.Spec.Templates = append(wf.Spec.Templates, tg.Templates...) 56 | wf.Spec.Templates = append(wf.Spec.Templates, tg.EntryTemplate) 57 | } 58 | 59 | func (tg *TemplateGenerator) GenerateTemplates(graph *graph.Graph) error { 60 | tg.EntryTemplate = v1alpha13.Template{ 61 | Name: EntrypointTemplateName, 62 | DAG: &v1alpha13.DAGTemplate{}, 63 | Parallelism: tg.Parallelism, 64 | } 65 | 66 | // Create the entry template from the app dag templates 67 | for _, node := range graph.Nodes { 68 | template, err := tg.createNodeTemplate(node, graph.Name) 69 | if err != nil { 70 | return err 71 | } 72 | tg.Templates = append(tg.Templates, template) 73 | tg.EntryTemplate.DAG.Tasks = append(tg.EntryTemplate.DAG.Tasks, v1alpha13.DAGTask{ 74 | Name: template.Name, 75 | Template: template.Name, 76 | Dependencies: utils.ConvertSliceToDNS1123(node.Dependencies), 77 | }) 78 | } 79 | // Finally, add the executor templates in the graph to the set of templates 80 | tg.addExecutorTemplates(graph) 81 | return nil 82 | } 83 | 84 | func (tg *TemplateGenerator) createNodeTemplate(node *graph.AppNode, graphName string) (v1alpha13.Template, error) { 85 | template := v1alpha13.Template{ 86 | Name: utils.ConvertToDNS1123(node.Name), 87 | Parallelism: tg.Parallelism, 88 | DAG: &v1alpha13.DAGTemplate{ 89 | Tasks: []v1alpha13.DAGTask{}, 90 | }, 91 | } 92 | for _, task := range node.Tasks { 93 | hrStr := utils.HrToB64(tg.createHelmRelease(task, graphName)) 94 | if len(task.Executors) == 1 { 95 | // If we only have one executor, we don't need a sub-template 96 | // Just add this task to the application template 97 | for _, executorNode := range task.Executors { 98 | executorTask, err := executorNode.Executor.GetTask(task.Name, task.Dependencies, getTimeout(task.Release.Timeout), hrStr, executorNode.Params) 99 | if err != nil { 100 | return template, err 101 | } 102 | template.DAG.Tasks = append(template.DAG.Tasks, executorTask) 103 | } 104 | } else { 105 | // If we have more than one executor, we need to create the task 106 | // sub-template with executor dependencies 107 | taskTemplate, err := tg.createTaskTemplate(task, graphName) 108 | if err != nil { 109 | return template, err 110 | } 111 | tg.Templates = append(tg.Templates, taskTemplate) 112 | template.DAG.Tasks = append(template.DAG.Tasks, v1alpha13.DAGTask{ 113 | Name: taskTemplate.Name, 114 | Template: taskTemplate.Name, 115 | Dependencies: task.Dependencies, 116 | }) 117 | } 118 | } 119 | return template, nil 120 | } 121 | 122 | func (tg *TemplateGenerator) createTaskTemplate(task *graph.TaskNode, graphName string) (v1alpha13.Template, error) { 123 | hrStr := utils.HrToB64(tg.createHelmRelease(task, graphName)) 124 | taskTemplate := v1alpha13.Template{ 125 | Name: utils.ConvertToDNS1123(task.Name), 126 | Parallelism: tg.Parallelism, 127 | DAG: &v1alpha13.DAGTemplate{ 128 | Tasks: []v1alpha13.DAGTask{}, 129 | }, 130 | } 131 | for _, executorNode := range task.Executors { 132 | executorTask, err := executorNode.Executor.GetTask(executorNode.Name, executorNode.Dependencies, getTimeout(task.Release.Timeout), hrStr, executorNode.Params) 133 | if err != nil { 134 | return taskTemplate, err 135 | } 136 | taskTemplate.DAG.Tasks = append(taskTemplate.DAG.Tasks, executorTask) 137 | } 138 | return taskTemplate, nil 139 | } 140 | 141 | func (tg *TemplateGenerator) addExecutorTemplates(g *graph.Graph) { 142 | for _, executor := range g.AllExecutors { 143 | tg.Templates = append(tg.Templates, executor.GetTemplate()) 144 | } 145 | } 146 | 147 | func (tg *TemplateGenerator) createHelmRelease(task *graph.TaskNode, graphName string) *fluxhelmv2beta1.HelmRelease { 148 | helmRelease := &fluxhelmv2beta1.HelmRelease{ 149 | TypeMeta: v1.TypeMeta{ 150 | Kind: fluxhelmv2beta1.HelmReleaseKind, 151 | APIVersion: fluxhelmv2beta1.GroupVersion.String(), 152 | }, 153 | ObjectMeta: v1.ObjectMeta{ 154 | Name: utils.ConvertToDNS1123(task.ChartName), 155 | Namespace: task.Release.TargetNamespace, 156 | Labels: map[string]string{ 157 | v1alpha1.ChartLabel: task.ChartName, 158 | v1alpha1.OwnershipLabel: graphName, 159 | v1alpha1.HeritageLabel: v1alpha1.HeritageValue, 160 | }, 161 | }, 162 | Spec: fluxhelmv2beta1.HelmReleaseSpec{ 163 | Chart: fluxhelmv2beta1.HelmChartTemplate{ 164 | Spec: fluxhelmv2beta1.HelmChartTemplateSpec{ 165 | Chart: utils.ConvertToDNS1123(task.ChartName), 166 | Version: task.ChartVersion, 167 | SourceRef: fluxhelmv2beta1.CrossNamespaceObjectReference{ 168 | Kind: fluxsourcev1beta1.HelmRepositoryKind, 169 | Name: ChartMuseumName, 170 | Namespace: tg.Namespace, 171 | }, 172 | }, 173 | }, 174 | ReleaseName: utils.ConvertToDNS1123(task.ChartName), 175 | TargetNamespace: task.Release.TargetNamespace, 176 | Timeout: task.Release.Timeout, 177 | Install: task.Release.Install, 178 | Upgrade: task.Release.Upgrade, 179 | Rollback: task.Release.Rollback, 180 | Uninstall: task.Release.Uninstall, 181 | Interval: task.Release.Interval, 182 | Values: task.Release.Values, 183 | }, 184 | } 185 | if task.Parent != "" { 186 | helmRelease.Annotations = map[string]string{ 187 | v1alpha1.ParentChartAnnotation: task.Parent, 188 | } 189 | } 190 | return helmRelease 191 | } 192 | -------------------------------------------------------------------------------- /pkg/templates/utils.go: -------------------------------------------------------------------------------- 1 | package templates 2 | 3 | import ( 4 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 5 | ) 6 | 7 | const ( 8 | DefaultTimeout = "5m" 9 | ) 10 | 11 | func getTimeout(t *v1.Duration) string { 12 | if t == nil { 13 | return DefaultTimeout 14 | } 15 | return t.Duration.String() 16 | } 17 | -------------------------------------------------------------------------------- /pkg/utils/chart.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | func GetSubchartName(appName, scName string) string { 4 | appName = TruncateString(GetHash(appName), hashedAppNameMaxLen) 5 | return ConvertToDNS1123(appName + "-" + scName) 6 | } 7 | -------------------------------------------------------------------------------- /pkg/utils/chart_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestGetSubchartName(t *testing.T) { 8 | type args struct { 9 | appName string 10 | scName string 11 | } 12 | tests := []struct { 13 | name string 14 | args args 15 | want string 16 | }{ 17 | { 18 | name: "testing empty", 19 | args: args{ 20 | appName: "", 21 | scName: "", 22 | }, 23 | want: GetHash("")[0:hashedAppNameMaxLen] + "-", 24 | }, 25 | { 26 | name: "testing empty subchart name", 27 | args: args{ 28 | appName: "10charhash", 29 | scName: "", 30 | }, 31 | want: GetHash("10charhash")[0:hashedAppNameMaxLen] + "-", 32 | }, 33 | { 34 | name: "testing empty application name", 35 | args: args{ 36 | appName: "", 37 | scName: "myapp-name", 38 | }, 39 | want: GetHash("")[0:hashedAppNameMaxLen] + "-myapp-name", 40 | }, 41 | { 42 | name: "testing subchart name length == 52", 43 | args: args{ 44 | appName: "appHash", 45 | scName: "thisismychart-withbigname-equalto53chars000000000009", 46 | }, 47 | want: GetHash("appHash")[0:hashedAppNameMaxLen] + "-thisismychart-withbigname-equalto53chars000000000009", 48 | }, 49 | { 50 | name: "testing subchart name length > 52", 51 | args: args{ 52 | appName: "appHash", 53 | scName: "thisismychart-withbigname-greaterthan53chars0987654321abcde", 54 | }, 55 | want: GetHash("appHash")[0:hashedAppNameMaxLen] + "-thisismychart-withbigname-greaterthan53chars09876543", 56 | }, 57 | { 58 | name: "testing subchart name length > 63", 59 | args: args{ 60 | appName: "appHash", 61 | scName: "thisismyappchart-withbigname-greaterthan63chars0987654321abcde123456789", 62 | }, 63 | want: GetHash("appHash")[0:hashedAppNameMaxLen] + "-thisismyappchart-withbigname-greaterthan63chars09876", 64 | }, 65 | { 66 | name: "testing DNS1123 incompatible subchart name", 67 | args: args{ 68 | appName: "appHash", 69 | scName: "thisismyappchart_withbigname_greaterthan63chars0987654321abcde123456789", 70 | }, 71 | want: GetHash("appHash")[0:hashedAppNameMaxLen] + "-thisismyappchart-withbigname-greaterthan63chars09876", 72 | }, 73 | } 74 | for _, tt := range tests { 75 | t.Run(tt.name, func(t *testing.T) { 76 | if got := GetSubchartName(tt.args.appName, tt.args.scName); got != tt.want { 77 | t.Errorf("TestGetSubchartReleaseName() = %v, want %v", got, tt.want) 78 | } 79 | }) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /pkg/utils/consts.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "regexp" 5 | ) 6 | 7 | const ( 8 | DNS1123NameMaximumLength = 63 9 | 10 | // hashedAppNameMaxLen is the maximum length of application name hash that is 11 | hashedAppNameMaxLen = 10 12 | ) 13 | 14 | var ( 15 | dns1123NotAllowedCharsRegexp = regexp.MustCompile("[^-a-z0-9]") // treat as const 16 | dns1123NotAllowedStartCharsRegexp = regexp.MustCompile("^[^a-z0-9]+") // treat as const 17 | ) 18 | -------------------------------------------------------------------------------- /pkg/utils/helm.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "os" 5 | "time" 6 | 7 | "helm.sh/helm/v3/pkg/action" 8 | "helm.sh/helm/v3/pkg/cli" 9 | ) 10 | 11 | func HelmUninstall(release, namespace string) error { 12 | os.Setenv("HELM_NAMESPACE", namespace) 13 | var settings = cli.New() 14 | settings.Debug = false 15 | 16 | actionConfig := new(action.Configuration) 17 | helmDriver := os.Getenv("HELM_DRIVER") 18 | if err := actionConfig.Init(settings.RESTClientGetter(), settings.Namespace(), helmDriver, debug); err != nil { 19 | return err 20 | } 21 | client := action.NewUninstall(actionConfig) 22 | _, err := client.Run(release) 23 | if err != nil { 24 | return err 25 | } 26 | return nil 27 | } 28 | 29 | func HelmRollback(release, namespace string) error { 30 | os.Setenv("HELM_NAMESPACE", namespace) 31 | var settings = cli.New() 32 | settings.Debug = false 33 | 34 | actionConfig := new(action.Configuration) 35 | helmDriver := os.Getenv("HELM_DRIVER") 36 | if err := actionConfig.Init(settings.RESTClientGetter(), settings.Namespace(), helmDriver, debug); err != nil { 37 | return err 38 | } 39 | client := action.NewRollback(actionConfig) 40 | client.Wait = true 41 | client.Recreate = true 42 | client.Timeout = time.Minute * 5 43 | err := client.Run(release) 44 | if err != nil { 45 | return err 46 | } 47 | return nil 48 | } 49 | func debug(format string, v ...interface{}) { 50 | 51 | } 52 | -------------------------------------------------------------------------------- /pkg/utils/helpers.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "crypto/sha256" 5 | "encoding/base64" 6 | "encoding/hex" 7 | "fmt" 8 | "strings" 9 | 10 | v1alpha13 "github.com/argoproj/argo-workflows/v3/pkg/apis/workflow/v1alpha1" 11 | fluxhelmv2beta1 "github.com/fluxcd/helm-controller/api/v2beta1" 12 | "helm.sh/helm/v3/pkg/chart" 13 | "sigs.k8s.io/yaml" 14 | ) 15 | 16 | func ConvertToDNS1123(in string) string { 17 | name := strings.ToLower(in) 18 | name = dns1123NotAllowedCharsRegexp.ReplaceAllString(name, "-") 19 | name = dns1123NotAllowedStartCharsRegexp.ReplaceAllString(name, "") 20 | 21 | if name == "" { 22 | name = GetHash(in) 23 | } 24 | return TruncateString(name, DNS1123NameMaximumLength) 25 | } 26 | 27 | func ConvertSliceToDNS1123(in []string) []string { 28 | out := []string{} 29 | for _, s := range in { 30 | out = append(out, ConvertToDNS1123(s)) 31 | } 32 | return out 33 | } 34 | 35 | func GetHash(in string) string { 36 | h := sha256.New() 37 | h.Write([]byte(in)) 38 | return hex.EncodeToString(h.Sum(nil)) 39 | } 40 | 41 | func TruncateString(in string, num int) string { 42 | out := in 43 | if len(in) > num { 44 | out = in[0:num] 45 | } 46 | return out 47 | } 48 | 49 | func RemoveStringFromSlice(r string, s []string) []string { 50 | for i, v := range s { 51 | if v == r { 52 | return append(s[:i], s[i+1:]...) 53 | } 54 | } 55 | return s 56 | } 57 | 58 | func ToAnyString(in string) v1alpha13.AnyString { 59 | return v1alpha13.AnyString(in) 60 | } 61 | 62 | func ToAnyStringPtr(in string) *v1alpha13.AnyString { 63 | anystr := ToAnyString(in) 64 | return &anystr 65 | } 66 | 67 | func HrToYaml(hr fluxhelmv2beta1.HelmRelease) string { 68 | b, err := yaml.Marshal(hr) 69 | if err != nil { 70 | return "" 71 | } 72 | 73 | return string(b) 74 | } 75 | 76 | func HrToB64(hr *fluxhelmv2beta1.HelmRelease) string { 77 | yaml := HrToYaml(*hr) 78 | return base64.StdEncoding.EncodeToString([]byte(yaml)) 79 | } 80 | 81 | func TemplateContainsYaml(ch *chart.Chart) (bool, error) { 82 | if ch == nil { 83 | return false, fmt.Errorf("chart cannot be nil") 84 | } 85 | 86 | for _, f := range ch.Templates { 87 | if IsFileYaml(f.Name) { 88 | return true, nil 89 | } 90 | } 91 | return false, nil 92 | } 93 | 94 | func IsFileYaml(f string) bool { 95 | f = strings.ToLower(f) 96 | if strings.HasSuffix(f, ".yml") || strings.HasSuffix(f, ".yaml") { 97 | return true 98 | } 99 | return false 100 | } 101 | 102 | func AddAppChartNameToFile(name, a string) string { 103 | prefix := "templates/" 104 | name = strings.TrimPrefix(name, prefix) 105 | name = a + "_" + name 106 | return prefix + name 107 | } 108 | -------------------------------------------------------------------------------- /pkg/utils/helpers_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestConvertToDNS1123(t *testing.T) { 9 | type args struct { 10 | in string 11 | } 12 | tests := []struct { 13 | name string 14 | args args 15 | want string 16 | }{ 17 | { 18 | name: "testing valid name", 19 | args: args{ 20 | in: "good-small-name", 21 | }, 22 | want: "good-small-name", 23 | }, 24 | { 25 | name: "testing invalid name", 26 | args: args{ 27 | in: "tOk3_??ofTHE-Runner", 28 | }, 29 | want: "tok3---ofthe-runner", 30 | }, 31 | { 32 | name: "testing all characters are invalid", 33 | args: args{ 34 | in: "?.?&^%#$@_??", 35 | }, 36 | want: GetHash("?.?&^%#$@_??")[0:63], 37 | }, 38 | { 39 | name: "testing invalid start chars", 40 | args: args{ 41 | in: "----??tOk3_??ofTHE-Runner", 42 | }, 43 | want: "tok3---ofthe-runner", 44 | }, 45 | { 46 | name: "testing very long name", 47 | args: args{ 48 | in: "very-long-name------------------------------------------------end", 49 | }, 50 | want: "very-long-name------------------------------------------------e", 51 | }, 52 | } 53 | for _, tt := range tests { 54 | t.Run(tt.name, func(t *testing.T) { 55 | if got := ConvertToDNS1123(tt.args.in); !reflect.DeepEqual(got, tt.want) { 56 | t.Errorf("ConvertDNS1123() = %v, want %v", got, tt.want) 57 | } 58 | }) 59 | } 60 | } 61 | 62 | func TestTruncateString(t *testing.T) { 63 | type args struct { 64 | in string 65 | num int 66 | } 67 | tests := []struct { 68 | name string 69 | args args 70 | want string 71 | }{ 72 | { 73 | name: "testing empty", 74 | args: args{ 75 | in: "", 76 | num: 0, 77 | }, 78 | want: "", 79 | }, 80 | { 81 | name: "testing empty with > 0 requested length", 82 | args: args{ 83 | in: "", 84 | num: 5, 85 | }, 86 | want: "", 87 | }, 88 | { 89 | name: "testing input string length == requested length", 90 | args: args{ 91 | in: "hello, don't truncate me", 92 | num: 24, 93 | }, 94 | want: "hello, don't truncate me", 95 | }, 96 | { 97 | name: "testing input string length < requested length", 98 | args: args{ 99 | in: "hello again, don't truncate me", 100 | num: 63, 101 | }, 102 | want: "hello again, don't truncate me", 103 | }, 104 | { 105 | name: "testing input string length > requested length", 106 | args: args{ 107 | in: "truncate_this_string_so_that_its_length_is_less_than_sixty_three_characters", 108 | num: 63, 109 | }, 110 | want: "truncate_this_string_so_that_its_length_is_less_than_sixty_thre", 111 | }, 112 | } 113 | for _, tt := range tests { 114 | t.Run(tt.name, func(t *testing.T) { 115 | if got := TruncateString(tt.args.in, tt.args.num); !reflect.DeepEqual(got, tt.want) { 116 | t.Errorf("TruncateString() = %v, want %v", got, tt.want) 117 | } 118 | }) 119 | } 120 | } 121 | 122 | func TestIsFileYaml(t *testing.T) { 123 | type args struct { 124 | f string 125 | } 126 | tests := []struct { 127 | name string 128 | args args 129 | want bool 130 | }{ 131 | { 132 | name: "testing empty", 133 | args: args{ 134 | f: "", 135 | }, 136 | want: false, 137 | }, 138 | { 139 | name: "testing .yaml file", 140 | args: args{ 141 | f: "templates/filename.yaml", 142 | }, 143 | want: true, 144 | }, 145 | { 146 | name: "testing .yml file", 147 | args: args{ 148 | f: "templates/bin/myfile.yml", 149 | }, 150 | want: true, 151 | }, 152 | { 153 | name: "testing .yaml file", 154 | args: args{ 155 | f: "templates/bin/myfile.txt", 156 | }, 157 | want: false, 158 | }, 159 | { 160 | name: "testing filename extension that contains yaml but not yaml file", 161 | args: args{ 162 | f: "templates/bin/myfile.myyaml", 163 | }, 164 | want: false, 165 | }, 166 | { 167 | name: "testing .txt file with name containing yaml substring.", 168 | args: args{ 169 | f: "templates/bin/yamlFileGuide.txt", 170 | }, 171 | want: false, 172 | }, 173 | } 174 | for _, tt := range tests { 175 | t.Run(tt.name, func(t *testing.T) { 176 | if got := IsFileYaml(tt.args.f); !reflect.DeepEqual(got, tt.want) { 177 | t.Errorf("IsFileYaml() = %v, want %v", got, tt.want) 178 | } 179 | }) 180 | } 181 | } 182 | 183 | func TestRemoveStringFromSlice(t *testing.T) { 184 | type args struct { 185 | s string 186 | v []string 187 | } 188 | tests := []struct { 189 | name string 190 | args args 191 | want []string 192 | }{ 193 | { 194 | name: "removing with single item", 195 | args: args{ 196 | s: "apple", 197 | v: []string{"apple"}, 198 | }, 199 | want: []string{}, 200 | }, 201 | { 202 | name: "removing from middle of slice", 203 | args: args{ 204 | s: "orange", 205 | v: []string{"apple", "orange", "banana"}, 206 | }, 207 | want: []string{"apple", "banana"}, 208 | }, 209 | { 210 | name: "not finding the item in the slice", 211 | args: args{ 212 | s: "papaya", 213 | v: []string{"apple", "orange", "banana"}, 214 | }, 215 | want: []string{"apple", "orange", "banana"}, 216 | }, 217 | { 218 | name: "finding item at the end of slice", 219 | args: args{ 220 | s: "banana", 221 | v: []string{"apple", "orange", "banana"}, 222 | }, 223 | want: []string{"apple", "orange"}, 224 | }, 225 | { 226 | name: "passing empty slice", 227 | args: args{ 228 | s: "banana", 229 | v: []string{}, 230 | }, 231 | want: []string{}, 232 | }, 233 | } 234 | for _, tt := range tests { 235 | t.Run(tt.name, func(t *testing.T) { 236 | if got := RemoveStringFromSlice(tt.args.s, tt.args.v); !reflect.DeepEqual(got, tt.want) { 237 | t.Errorf("IsFileYaml() = %v, want %v", got, tt.want) 238 | } 239 | }) 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /pkg/utils/probe.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "net" 5 | "net/http" 6 | "time" 7 | 8 | "github.com/heptiolabs/healthcheck" 9 | ) 10 | 11 | type Probe struct { 12 | health healthcheck.Handler 13 | } 14 | 15 | func ProbeHandler(chartmuseumAddress, endpoint string) (*Probe, error) { 16 | health := healthcheck.NewHandler() 17 | // Liveness check verifies that the number of goroutines are below threshold 18 | health.AddLivenessCheck("goroutine-threshold", healthcheck.GoroutineCountCheck(100)) 19 | // Readiness check verifies that chartmuseum is up and serving traffic 20 | health.AddReadinessCheck("chartmuseum-ready", healthcheck.HTTPGetCheck(chartmuseumAddress+"/"+endpoint, time.Second*5)) 21 | 22 | return &Probe{ 23 | health: health, 24 | }, nil 25 | } 26 | 27 | func (p *Probe) Start(port string) { 28 | go http.ListenAndServe(net.JoinHostPort("0.0.0.0", port), p.health) //nolint:errcheck 29 | } 30 | -------------------------------------------------------------------------------- /pkg/workflow/utils.go: -------------------------------------------------------------------------------- 1 | package workflow 2 | 3 | import ( 4 | "os" 5 | ) 6 | 7 | func GetNamespace() string { 8 | if ns, ok := os.LookupEnv("WORKFLOW_NAMESPACE"); ok { 9 | return ns 10 | } 11 | return "orkestra" 12 | } 13 | -------------------------------------------------------------------------------- /pkg/workflow/workflow_forward.go: -------------------------------------------------------------------------------- 1 | package workflow 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | v1alpha13 "github.com/argoproj/argo-workflows/v3/pkg/apis/workflow/v1alpha1" 8 | 9 | "github.com/Azure/Orkestra/pkg/graph" 10 | "github.com/Azure/Orkestra/pkg/templates" 11 | 12 | "github.com/Azure/Orkestra/api/v1alpha1" 13 | "github.com/go-logr/logr" 14 | corev1 "k8s.io/api/core/v1" 15 | "k8s.io/apimachinery/pkg/api/errors" 16 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 17 | "sigs.k8s.io/controller-runtime/pkg/client" 18 | "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" 19 | ) 20 | 21 | func (wc *ForwardWorkflowClient) GetLogger() logr.Logger { 22 | return wc.Logger 23 | } 24 | 25 | func (wc *ForwardWorkflowClient) GetClient() client.Client { 26 | return wc.Client 27 | } 28 | 29 | func (wc *ForwardWorkflowClient) GetType() v1alpha1.WorkflowType { 30 | return v1alpha1.Forward 31 | } 32 | 33 | func (wc *ForwardWorkflowClient) GetName() string { 34 | return wc.appGroup.Name 35 | } 36 | 37 | func (wc *ForwardWorkflowClient) GetNamespace() string { 38 | return wc.Namespace 39 | } 40 | 41 | func (wc *ForwardWorkflowClient) GetOptions() ClientOptions { 42 | return wc.ClientOptions 43 | } 44 | 45 | func (wc *ForwardWorkflowClient) GetAppGroup() *v1alpha1.ApplicationGroup { 46 | return wc.appGroup 47 | } 48 | 49 | func (wc *ForwardWorkflowClient) GetWorkflow() *v1alpha13.Workflow { 50 | return wc.workflow 51 | } 52 | 53 | func (wc *ForwardWorkflowClient) Generate(ctx context.Context) error { 54 | if wc.appGroup == nil { 55 | return fmt.Errorf("applicationGroup object cannot be nil") 56 | } 57 | 58 | // Suspend the rollback or reverse workflows if they are running 59 | reverseClient := NewClientFromClient(wc, v1alpha1.Reverse) 60 | rollbackClient := NewClientFromClient(wc, v1alpha1.Rollback) 61 | if err := Suspend(ctx, reverseClient); err != nil { 62 | return fmt.Errorf("failed to suspend reverse workflow: %w", err) 63 | } 64 | if err := Suspend(ctx, rollbackClient); err != nil { 65 | return fmt.Errorf("failed to suspend rollback workflow: %w", err) 66 | } 67 | 68 | wc.workflow = templates.GenerateWorkflow(wc.appGroup.Name, wc.Namespace, wc.Parallelism) 69 | graph := graph.NewForwardGraph(wc.GetAppGroup()) 70 | 71 | templateGenerator := templates.NewTemplateGenerator(wc.Namespace, wc.Parallelism) 72 | if err := templateGenerator.GenerateTemplates(graph); err != nil { 73 | return fmt.Errorf("failed to generate templates: %w", err) 74 | } 75 | templateGenerator.AssignWorkflowTemplates(wc.workflow) 76 | return nil 77 | } 78 | 79 | func (wc *ForwardWorkflowClient) Submit(ctx context.Context) error { 80 | if err := wc.createTargetNamespaces(ctx); err != nil { 81 | return fmt.Errorf("failed to create the target namespaces: %w", err) 82 | } 83 | wc.workflow.Labels[v1alpha1.WorkflowTypeLabel] = string(v1alpha1.Forward) 84 | if err := controllerutil.SetControllerReference(wc.appGroup, wc.workflow, wc.Scheme()); err != nil { 85 | return fmt.Errorf("unable to set ApplicationGroup as owner of Argo Workflow: %w", err) 86 | } 87 | if err := Submit(ctx, wc); err != nil { 88 | return err 89 | } 90 | return nil 91 | } 92 | 93 | func (wc *ForwardWorkflowClient) createTargetNamespaces(ctx context.Context) error { 94 | namespaces := []string{} 95 | // Add namespaces we need to create while removing duplicates 96 | for _, app := range wc.appGroup.Spec.Applications { 97 | found := false 98 | for _, namespace := range namespaces { 99 | if app.Spec.Release.TargetNamespace == namespace { 100 | found = true 101 | break 102 | } 103 | } 104 | if !found { 105 | namespaces = append(namespaces, app.Spec.Release.TargetNamespace) 106 | } 107 | } 108 | 109 | // Create any of the target namespaces 110 | for _, namespace := range namespaces { 111 | ns := &corev1.Namespace{ 112 | ObjectMeta: v1.ObjectMeta{ 113 | Name: namespace, 114 | Labels: map[string]string{ 115 | "name": namespace, 116 | }, 117 | }, 118 | } 119 | if err := controllerutil.SetControllerReference(wc.appGroup, ns, wc.Scheme()); err != nil { 120 | return fmt.Errorf("failed to set OwnerReference for Namespace %s: %w", ns.Name, err) 121 | } 122 | if err := wc.Create(ctx, ns); !errors.IsAlreadyExists(err) && err != nil { 123 | return fmt.Errorf("failed to CREATE namespace %s object: %w", ns.Name, err) 124 | } 125 | } 126 | return nil 127 | } 128 | -------------------------------------------------------------------------------- /pkg/workflow/workflow_reverse.go: -------------------------------------------------------------------------------- 1 | package workflow 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/Azure/Orkestra/api/v1alpha1" 8 | "github.com/Azure/Orkestra/pkg/graph" 9 | "github.com/Azure/Orkestra/pkg/meta" 10 | "github.com/Azure/Orkestra/pkg/templates" 11 | v1alpha13 "github.com/argoproj/argo-workflows/v3/pkg/apis/workflow/v1alpha1" 12 | "k8s.io/apimachinery/pkg/api/errors" 13 | "sigs.k8s.io/controller-runtime/pkg/client" 14 | 15 | "github.com/go-logr/logr" 16 | "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" 17 | ) 18 | 19 | func (wc *ReverseWorkflowClient) GetLogger() logr.Logger { 20 | return wc.Logger 21 | } 22 | 23 | func (wc *ReverseWorkflowClient) GetClient() client.Client { 24 | return wc.Client 25 | } 26 | 27 | func (wc *ReverseWorkflowClient) GetType() v1alpha1.WorkflowType { 28 | return v1alpha1.Rollback 29 | } 30 | 31 | func (wc *ReverseWorkflowClient) GetAppGroup() *v1alpha1.ApplicationGroup { 32 | return wc.appGroup 33 | } 34 | 35 | func (wc *ReverseWorkflowClient) GetWorkflow() *v1alpha13.Workflow { 36 | return wc.workflow 37 | } 38 | 39 | func (wc *ReverseWorkflowClient) GetOptions() ClientOptions { 40 | return wc.ClientOptions 41 | } 42 | 43 | func (wc *ReverseWorkflowClient) GetName() string { 44 | return fmt.Sprintf("%s-reverse", wc.appGroup.Name) 45 | } 46 | 47 | func (wc *ReverseWorkflowClient) GetNamespace() string { 48 | return wc.Namespace 49 | } 50 | 51 | func (wc *ReverseWorkflowClient) Generate(ctx context.Context) error { 52 | forwardClient := NewClientFromClient(wc, v1alpha1.Forward) 53 | rollbackClient := NewClientFromClient(wc, v1alpha1.Rollback) 54 | 55 | if err := Suspend(ctx, forwardClient); err != nil { 56 | return fmt.Errorf("failed to suspend forward workflow: %w", err) 57 | } 58 | if err := Suspend(ctx, rollbackClient); err != nil { 59 | return fmt.Errorf("failed to suspend rollback workflow: %w", err) 60 | } 61 | 62 | wc.workflow = templates.GenerateWorkflow(wc.GetName(), wc.Namespace, wc.Parallelism) 63 | graph := graph.NewReverseGraph(wc.GetAppGroup()) 64 | 65 | templateGenerator := templates.NewTemplateGenerator(wc.Namespace, wc.Parallelism) 66 | if err := templateGenerator.GenerateTemplates(graph); err != nil { 67 | return fmt.Errorf("failed to generate templates: %w", err) 68 | } 69 | templateGenerator.AssignWorkflowTemplates(wc.workflow) 70 | return nil 71 | } 72 | 73 | func (wc *ReverseWorkflowClient) Submit(ctx context.Context) error { 74 | forwardClient := NewClientFromClient(wc, v1alpha1.Forward) 75 | forwardWorkflow, err := GetWorkflow(ctx, forwardClient) 76 | if errors.IsNotFound(err) { 77 | return meta.ErrForwardWorkflowNotFound 78 | } else if err != nil { 79 | return err 80 | } 81 | wc.workflow.Labels[v1alpha1.WorkflowTypeLabel] = string(v1alpha1.Reverse) 82 | if err := controllerutil.SetControllerReference(forwardWorkflow, wc.workflow, wc.Scheme()); err != nil { 83 | return fmt.Errorf("unable to set forward workflow as owner of Argo reverse Workflow: %w", err) 84 | } 85 | if err := Submit(ctx, wc); err != nil { 86 | return err 87 | } 88 | return nil 89 | } 90 | -------------------------------------------------------------------------------- /pkg/workflow/workflow_rollback.go: -------------------------------------------------------------------------------- 1 | package workflow 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | v1alpha13 "github.com/argoproj/argo-workflows/v3/pkg/apis/workflow/v1alpha1" 8 | 9 | "github.com/Azure/Orkestra/pkg/graph" 10 | "github.com/Azure/Orkestra/pkg/templates" 11 | 12 | "github.com/Azure/Orkestra/api/v1alpha1" 13 | "github.com/Azure/Orkestra/pkg/meta" 14 | "github.com/go-logr/logr" 15 | "sigs.k8s.io/controller-runtime/pkg/client" 16 | "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" 17 | ) 18 | 19 | func (wc *RollbackWorkflowClient) GetLogger() logr.Logger { 20 | return wc.Logger 21 | } 22 | 23 | func (wc *RollbackWorkflowClient) GetClient() client.Client { 24 | return wc.Client 25 | } 26 | 27 | func (wc *RollbackWorkflowClient) GetType() v1alpha1.WorkflowType { 28 | return v1alpha1.Rollback 29 | } 30 | 31 | func (wc *RollbackWorkflowClient) GetName() string { 32 | return fmt.Sprintf("%s-rollback", wc.appGroup.Name) 33 | } 34 | 35 | func (wc *RollbackWorkflowClient) GetNamespace() string { 36 | return wc.Namespace 37 | } 38 | 39 | func (wc *RollbackWorkflowClient) GetOptions() ClientOptions { 40 | return wc.ClientOptions 41 | } 42 | 43 | func (wc *RollbackWorkflowClient) GetAppGroup() *v1alpha1.ApplicationGroup { 44 | return wc.appGroup 45 | } 46 | 47 | func (wc *RollbackWorkflowClient) GetWorkflow() *v1alpha13.Workflow { 48 | return wc.workflow 49 | } 50 | 51 | func (wc *RollbackWorkflowClient) Generate(ctx context.Context) error { 52 | if wc.appGroup == nil { 53 | return fmt.Errorf("applicationGroup object cannot be nil") 54 | } 55 | 56 | rollbackAppGroup := wc.appGroup.DeepCopy() 57 | lastSuccessful := wc.appGroup.GetLastSuccessful() 58 | if lastSuccessful == nil { 59 | return meta.ErrPreviousSpecNotSet 60 | } 61 | rollbackAppGroup.Spec = *lastSuccessful 62 | 63 | currGraph := graph.NewForwardGraph(wc.appGroup) 64 | lastGraph := graph.NewForwardGraph(rollbackAppGroup) 65 | diffGraph := graph.Diff(currGraph, lastGraph) 66 | 67 | wc.workflow = templates.GenerateWorkflow(wc.GetName(), wc.Namespace, wc.Parallelism) 68 | combinedGraph := graph.Combine(lastGraph, diffGraph.Reverse()) 69 | 70 | templateGenerator := templates.NewTemplateGenerator(wc.Namespace, wc.Parallelism) 71 | if err := templateGenerator.GenerateTemplates(combinedGraph); err != nil { 72 | return fmt.Errorf("failed to generate templates: %w", err) 73 | } 74 | templateGenerator.AssignWorkflowTemplates(wc.workflow) 75 | return nil 76 | } 77 | 78 | func (wc *RollbackWorkflowClient) Submit(ctx context.Context) error { 79 | wc.workflow.Labels[v1alpha1.WorkflowTypeLabel] = string(v1alpha1.Rollback) 80 | if err := controllerutil.SetControllerReference(wc.appGroup, wc.workflow, wc.Scheme()); err != nil { 81 | return fmt.Errorf("unable to set ApplicationGroup as owner of Argo Workflow: %w", err) 82 | } 83 | if err := Submit(ctx, wc); err != nil { 84 | return err 85 | } 86 | return nil 87 | } 88 | --------------------------------------------------------------------------------