├── .dockerignore ├── .github ├── main.workflow └── orca │ ├── Dockerfile │ └── entrypoint.sh ├── .gitignore ├── .goreleaser.yml ├── .travis.yml ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── MAINTAINERS.md ├── Makefile ├── README.md ├── cmd └── orca.go ├── docs ├── commands │ └── README.md ├── credentials │ └── README.md ├── examples │ ├── README.md │ └── gitlab │ │ └── .gitlab-ci.yml └── img │ └── logo.png ├── go.mod ├── go.sum ├── pkg ├── orca │ ├── artifact.go │ ├── buildtype.go │ ├── chart.go │ ├── env.go │ └── resource.go └── utils │ ├── bwg.go │ ├── chart.go │ ├── chart_test.go │ ├── env.go │ ├── general.go │ ├── general_test.go │ ├── git.go │ ├── git_test.go │ ├── helm.go │ ├── helm3.go │ ├── http.go │ ├── kube.go │ ├── testdata │ ├── Chart.yaml │ ├── charts.yaml │ └── circular.yaml │ └── validate.go └── test ├── chart_test.go └── data ├── Chart.yaml ├── charts.yaml └── circular.yaml /.dockerignore: -------------------------------------------------------------------------------- 1 | **/*git* 2 | *.git* 3 | *.md 4 | *.yml 5 | *.toml 6 | LICENSE 7 | bin/ 8 | cmd/ 9 | dist/ 10 | docs/ 11 | pkg/ 12 | test/ 13 | vendor/ -------------------------------------------------------------------------------- /.github/main.workflow: -------------------------------------------------------------------------------- 1 | workflow "New workflow" { 2 | on = "push" 3 | resolves = ["Orca"] 4 | } 5 | 6 | action "Orca" { 7 | uses = "./.github/orca" 8 | env = { 9 | SRC_KUBE_CONTEXT="prod", 10 | SRC_NS="default", 11 | DST_KUBE_CONTEXT="dev", 12 | DST_NS="orca-1", 13 | CHART_NAME="example-chart", 14 | CHART_VERSION="0.1.0" 15 | } 16 | } -------------------------------------------------------------------------------- /.github/orca/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nuvo/orca 2 | 3 | COPY entrypoint.sh / 4 | 5 | ENTRYPOINT ["/entrypoint.sh"] -------------------------------------------------------------------------------- /.github/orca/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | echo "Getting stable environment" 6 | orca get env --name $SRC_NS --kube-context $SRC_KUBE_CONTEXT > charts.yaml 7 | 8 | echo "Deploying dynamic environment" 9 | orca deploy env --name $DST_NS -c charts.yaml --kube-context $DST_KUBE_CONTEXT --override $CHART_NAME=$CHART_VERSION -x 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # dep 15 | vendor/ 16 | 17 | bin/ 18 | dist/ 19 | 20 | .cache/* 21 | .bash_history 22 | 23 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | # .goreleaser.yml 2 | env: 3 | - GO111MODULE=on 4 | 5 | before: 6 | hooks: 7 | - go mod download 8 | 9 | builds: 10 | - main: cmd/orca.go 11 | binary: orca 12 | goos: 13 | - linux 14 | goarch: 15 | - amd64 16 | env: 17 | - CGO_ENABLED=0 18 | ldflags: 19 | - -s -w -X main.GitTag={{.Tag}} -X main.GitCommit={{.ShortCommit}} 20 | 21 | archives: 22 | - 23 | id: orca 24 | name_template: "{{ .ProjectName }}-{{ .Version }}-{{ .Os }}" 25 | format: tar.gz 26 | wrap_in_directory: true 27 | 28 | dockers: 29 | - 30 | binaries: 31 | - orca 32 | dockerfile: Dockerfile 33 | image_templates: 34 | - "nuvo/{{.ProjectName}}:{{ .Tag }}" 35 | - "nuvo/{{.ProjectName}}:latest" 36 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # .travis.yml 2 | language: go 3 | 4 | go: 5 | - 1.12.5 6 | 7 | services: 8 | - docker 9 | 10 | # call goreleaser 11 | deploy: 12 | - provider: script 13 | skip_cleanup: true 14 | script: docker login -u $DOCKER_USER -p $DOCKER_PASSWORD && curl -sL https://git.io/goreleaser | bash 15 | on: 16 | tags: true 17 | condition: $TRAVIS_OS_NAME = linux -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Nuvo uses GitHub to manage reviews of pull requests. 4 | 5 | * If you have a trivial fix or improvement, go ahead and create a pull request, addressing (with `@...`) the maintainer of this repository (see [MAINTAINERS.md](./MAINTAINERS.md)) in the description of the pull request. 6 | 7 | * If you plan to do something more involved, please reach out first to one of the maintenrs to discuss your ideas, in order to avoid unnecessary work and surely give you and us a good deal of inspiration. 8 | 9 | * Relevant coding style guidelines are the [Go Code Review Comments](https://code.google.com/p/go-wiki/wiki/CodeReviewComments) 10 | and the _Formatting and style_ section of Peter Bourgon's [Go: Best Practices for Production Environments](http://peter.bourgon.org/go-in-production/#formatting-and-style). -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.9 2 | 3 | ARG HELM_VERSION=v2.17.0 4 | ARG HELM_OS_ARCH=linux-amd64 5 | 6 | RUN apk --no-cache add ca-certificates git bash curl jq \ 7 | && wget -q https://storage.googleapis.com/kubernetes-helm/helm-${HELM_VERSION}-${HELM_OS_ARCH}.tar.gz \ 8 | && tar -zxvf helm-${HELM_VERSION}-${HELM_OS_ARCH}.tar.gz ${HELM_OS_ARCH}/helm \ 9 | && mv ${HELM_OS_ARCH}/helm /usr/local/bin/helm \ 10 | && rm -rf ${HELM_OS_ARCH} helm-${HELM_VERSION}-${HELM_OS_ARCH}.tar.gz 11 | 12 | COPY orca /usr/local/bin/orca 13 | 14 | RUN addgroup -g 1001 -S orca \ 15 | && adduser -u 1001 -D -S -G orca orca 16 | 17 | USER orca 18 | 19 | WORKDIR /home/orca 20 | 21 | ENV HELM_HOME /home/orca/.helm 22 | 23 | RUN helm init --stable-repo-url=https://charts.helm.sh/stable --client-only \ 24 | && helm repo add "stable" "https://charts.helm.sh/stable" \ 25 | && helm plugin install https://github.com/chartmuseum/helm-push 26 | 27 | CMD ["orca"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /MAINTAINERS.md: -------------------------------------------------------------------------------- 1 | * Hagai Barel -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GIT_TAG := $(shell git describe --tags --always) 2 | GIT_COMMIT := $(shell git rev-parse --short HEAD) 3 | LDFLAGS := "-s -w -X main.GitTag=${GIT_TAG} -X main.GitCommit=${GIT_COMMIT}" 4 | 5 | export GO111MODULE:=on 6 | 7 | all: bootstrap test build 8 | 9 | fmt: 10 | go fmt ./... 11 | 12 | vet: 13 | go vet ./... 14 | 15 | # Run tests 16 | test: fmt vet 17 | go test ./... -coverprofile cover.out 18 | 19 | # Build orca binary 20 | build: test 21 | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags $(LDFLAGS) -o bin/orca cmd/orca.go 22 | 23 | # Build orca docker image 24 | docker: build 25 | cp bin/orca orca 26 | docker build -t nuvo/orca:latest . 27 | rm orca 28 | 29 | bootstrap: 30 | go mod download 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | orca logo 2 | 3 | 4 | [![Release](https://img.shields.io/github/release/nuvo/orca.svg)](https://github.com/nuvo/orca/releases) 5 | [![Travis branch](https://img.shields.io/travis/nuvo/orca/master.svg)](https://travis-ci.org/nuvo/orca) 6 | [![Docker Pulls](https://img.shields.io/docker/pulls/nuvo/orca.svg)](https://hub.docker.com/r/nuvo/orca/) 7 | [![Go Report Card](https://goreportcard.com/badge/github.com/nuvo/orca)](https://goreportcard.com/report/github.com/nuvo/orca) 8 | [![license](https://img.shields.io/github/license/nuvo/orca.svg)](https://github.com/nuvo/orca/blob/master/LICENSE) 9 | 10 | # Orca 11 | 12 | Orca is an advanced CI\CD tool which focuses on the world around Kubernetes, Helm and CI\CD, and it is also handy in daily work. 13 | Orca is a simplifier - It takes complex tasks and makes them easy to accomplish. 14 | Is is important to note that Orca is not intended to replace Helm, but rather to empower it and enable advanced usage with simplicity. 15 | 16 | ## Install 17 | 18 | ### Prerequisites 19 | 20 | 1. git 21 | 2. [dep](https://github.com/golang/dep) 22 | 3. [Helm](https://helm.sh/) (required for `env` and `chart` subcommands) 23 | 4. [ChartMuseum](https://github.com/helm/charts/tree/master/stable/chartmuseum) or any other chart repository implementation (required for `deploy` commands) 24 | 25 | ### From a release 26 | 27 | Download the latest release from the [Releases page](https://github.com/nuvo/orca/releases) or use it in your CI\CD process with a [Docker image](https://hub.docker.com/r/nuvo/orca) 28 | 29 | ### From source 30 | 31 | ``` 32 | mkdir -p $GOPATH/src/github.com/nuvo && cd $_ 33 | git clone https://github.com/nuvo/orca.git && cd orca 34 | make 35 | ``` 36 | 37 | * Please note that the `master` branch and the `latest` docker image are unstable. For a stable and tested version, use a tagged release or a tagged docker image. 38 | 39 | ## Why should you use Orca? 40 | 41 | What Orca does best is manage environments. An Environment is a Kubernetes namespace with a set of Helm charts installed on it. 42 | There are a few use cases you will probably find useful right off the bat. 43 | 44 | ### Create a dynamic environment (as part of a Pull Request for example) 45 | 46 | #### Get the "stable" environment and deploy the same configuration to a new environment 47 | 48 | This will deploy the "stable" configuration (production?) to a destination namespace. 49 | 50 | ``` 51 | orca get env --name $SRC_NS --kube-context $SRC_KUBE_CONTEXT > charts.yaml 52 | orca deploy env --name $DST_NS -c charts.yaml \ 53 | --kube-context $DST_KUBE_CONTEXT \ 54 | --repo myrepo=$REPO_URL 55 | ``` 56 | 57 | Additional flags: 58 | 59 | * Use the `-p` (parallel) flag to specify parallelism of chart deployments. 60 | * Use the `-f` flag to specify different values files to use during deployment. 61 | * Use the `-s` flag to set additional parameters. 62 | 63 | #### Get the "stable" environment and deploy the same configuration to a new environment, with override(s) and environment refresh 64 | 65 | Useful for creating test environments for a single service or for multiple services. 66 | Handy for testing a single feature spanning across one or more services. 67 | This will deploy the "stable" configuration to a destination namespace, except for the specified override(s), which will be deployed with version `CHART_VERSION`. In addition, it will set an annotation stating that `CHART_NAME` is protected and can only be overridden by itself. 68 | If the reference environment has changed between environment deployments, the new environment will be updated with these changes. 69 | 70 | The following commands will be a part of all CI\CD processes in all services: 71 | 72 | ``` 73 | orca get env --name $SRC_NS --kube-context $SRC_KUBE_CONTEXT > charts.yaml 74 | orca deploy env --name $DST_NS -c charts.yaml \ 75 | --kube-context $DST_KUBE_CONTEXT \ 76 | --repo myrepo=$REPO_URL \ 77 | --override $CHART_NAME=$CHART_VERSION \ 78 | --protected-chart $CHART_NAME 79 | ``` 80 | 81 | * When the first service's process starts, it creates the environment and deploys the configuration from the "stable" environment (exactly the same as the previous example). 82 | * When the Nth service's process starts, the environment already exists, and a previous chart that was deployed is `protected`. The current service will also be marked as `protected`, and will update the environment, without changing the previous protected service(s). 83 | * After deploying from (for example) 3 different repositories, the new environment will have the latest "stable" configuration, except for the 3 services which are currently under test, which will be deployed with their respective `CHART_VERSION`s (protected by `--protected-chart`) 84 | * You can add the `--protected-chart` flag even if this service is completely isolated (for consistency). 85 | * Orca also handles a potential race condition between 2 or more services by "locking" the environment during deployment (using a `busy` annotation on the namespace). 86 | 87 | #### Get the "stable" environment and deploy the same configuration to a new environment, with override(s) and without environment refresh 88 | 89 | Useful for creating test environments for a single service or for multiple services. 90 | Handy for testing a single feature spanning across one or more services, when you want to prevent updates from the reference environment after the initial creation of the new environment. 91 | This will deploy the "stable" configuration to a destination namespace, except for the specified override(s), which will be deployed with version `CHART_VERSION`. 92 | If the reference environment has changed between environment deployments, the new environment will NOT be updated with these changes. 93 | 94 | The following commands will be a part of all CI\CD processes in all services: 95 | 96 | ``` 97 | orca get env --name $SRC_NS --kube-context $SRC_KUBE_CONTEXT > charts.yaml 98 | orca deploy env --name $DST_NS -c charts.yaml \ 99 | --kube-context $DST_KUBE_CONTEXT \ 100 | --repo myrepo=$REPO_URL \ 101 | --override $CHART_NAME=$CHART_VERSION \ 102 | -x 103 | ``` 104 | 105 | * When the first service's process starts, it creates the environment and deploys the configuration from the "stable" environment (exactly the same as the previous example). 106 | * When the Nth service's process starts, the environment already exists, so only the specified override(s) will be deployed. 107 | * Assuming each process deploys a different chart (or charts), there is no need to protect them using the `--protected-chart`. 108 | 109 | 110 | ### Create and update static environments 111 | 112 | #### Manage multiple versions of your product without constantly maintaining the CI\CD process for all services 113 | 114 | If you are supporting more then one version of your product, you can use Orca as the CI\CD tool to deploy and update environments with different configurations with ease. 115 | Assuming you are required to create a new environment of your product, create a new Git repository with a single `charts.yaml` file, which you can update and deploy as you need. 116 | 117 | Your CI\CD process may be as slim as: 118 | 119 | ``` 120 | orca deploy env --name $NS -c charts.yaml \ 121 | --kube-context $KUBE_CONTEXT \ 122 | --repo myrepo=$REPO_URL 123 | ``` 124 | 125 | ### Keep track of an environment's state 126 | 127 | This is a bonus! If you need to document changes in your environments, you can use Orca to accomplish it. Trigger an event of your choice whenever an environment is updated and use Orca to get the current state: 128 | 129 | ``` 130 | orca get env --name $SRC_NS --kube-context $SRC_KUBE_CONTEXT -o md 131 | ``` 132 | 133 | This will print the list of currently installed charts in Markdown format. 134 | 135 | ### Prepare for disaster recovery 136 | 137 | You can use Orca to prepare for a rainy day. Trigger an event of your choice whenever an environment is updated and use Orca to get the current state into a file (ideally keep it under source control): 138 | 139 | ``` 140 | orca get env --name $NS --kube-context $KUBE_CONTEXT -o yaml > charts.yaml 141 | ``` 142 | 143 | In case of emergency, you can deploy the same configuration using the `deploy env` command as explained above. 144 | 145 | ## Environment variables support 146 | 147 | Orca commands support the usage of environment variables instead of most of the flags. For example: 148 | The `get env` command can be executed as mentioned in the example: 149 | ``` 150 | orca get env \ 151 | --kube-context \ 152 | --name 153 | ``` 154 | 155 | You can also set the appropriate environment variables (ORCA_FLAG, _ instead of -): 156 | 157 | ``` 158 | export ORCA_KUBE_CONTEXT= 159 | export ORCA_NAME= 160 | 161 | orca get env 162 | ``` 163 | 164 | ## Docs 165 | 166 | ### Commands 167 | 168 | Since Orca is a tool designed for CI\CD, it has additional commands and options to help with common actions: 169 | ``` 170 | deploy artifact Deploy an artifact to Artifactory 171 | deploy chart Deploy a Helm chart from chart repository 172 | push chart Push Helm chart to chart repository 173 | get env Get list of Helm releases in an environment (Kubernetes namespace) 174 | deploy env Deploy a list of Helm charts to an environment (Kubernetes namespace) from chart repository 175 | delete env Delete an environment (Kubernetes namespace) along with all Helm releases in it 176 | diff env Show differences in Helm releases between environments (Kubernetes namespace) 177 | lock env Lock an environment (Kubernetes namespace) 178 | unlock env Unlock an environment (Kubernetes namespace) 179 | validate env Validate an environment (Kubernetes namespace) 180 | create resource Create or update a resource via REST API 181 | get resource Get a resource via REST API 182 | delete resource Delete a resource via REST API 183 | determine buildtype Determine build type based on path filters 184 | ``` 185 | 186 | For a more detailed description of all commands, see the [Commands](/docs/commands) section 187 | 188 | ## Examples 189 | 190 | Be sure to check out the [Examples](/docs/examples) section! 191 | 192 | ## References 193 | 194 | * [The Nuvo Group CI\CD journey](https://medium.com/nuvo-group-tech/the-nuvo-group-ci-cd-journey-132ab70bf452) 195 | * [Dynamic Kubernetes Environments with Orca](https://medium.com/nuvo-group-tech/dynamic-kubernetes-environments-with-orca-a288afd36aa8) 196 | -------------------------------------------------------------------------------- /cmd/orca.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "log" 7 | "os" 8 | 9 | "github.com/nuvo/orca/pkg/orca" 10 | 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | func main() { 15 | cmd := NewRootCmd(os.Args[1:]) 16 | if err := cmd.Execute(); err != nil { 17 | log.Fatal("Failed to execute command") 18 | } 19 | } 20 | 21 | // NewRootCmd represents the base command when called without any subcommands 22 | func NewRootCmd(args []string) *cobra.Command { 23 | cmd := &cobra.Command{ 24 | Use: "orca", 25 | Short: "CI\\CD simplifier", 26 | Long: `Orca is a CI\CD simplifier, the glue behind the process. 27 | Instead of writing scripts on top of scripts, Orca holds all the logic. 28 | `, 29 | } 30 | 31 | out := cmd.OutOrStdout() 32 | 33 | cmd.AddCommand( 34 | NewDeleteCmd(out), 35 | NewDeployCmd(out), 36 | NewDetermineCmd(out), 37 | NewGetCmd(out), 38 | NewPushCmd(out), 39 | NewCreateCmd(out), 40 | NewVersionCmd(out), 41 | NewLockCmd(out), 42 | NewUnlockCmd(out), 43 | NewDiffCmd(out), 44 | NewValidateCmd(out), 45 | ) 46 | 47 | return cmd 48 | } 49 | 50 | // NewDeleteCmd represents the get command 51 | func NewDeleteCmd(out io.Writer) *cobra.Command { 52 | cmd := &cobra.Command{ 53 | Use: "delete", 54 | Short: "Deletion functions", 55 | Long: ``, 56 | } 57 | 58 | cmd.AddCommand( 59 | orca.NewDeleteEnvCmd(out), 60 | orca.NewDeleteResourceCmd(out), 61 | ) 62 | 63 | return cmd 64 | } 65 | 66 | // NewDeployCmd represents the get command 67 | func NewDeployCmd(out io.Writer) *cobra.Command { 68 | cmd := &cobra.Command{ 69 | Use: "deploy", 70 | Short: "Deployment functions", 71 | Long: ``, 72 | } 73 | 74 | cmd.AddCommand( 75 | orca.NewDeployChartCmd(out), 76 | orca.NewDeployEnvCmd(out), 77 | orca.NewDeployArtifactCmd(out), 78 | ) 79 | 80 | return cmd 81 | } 82 | 83 | // NewDetermineCmd represents the get command 84 | func NewDetermineCmd(out io.Writer) *cobra.Command { 85 | cmd := &cobra.Command{ 86 | Use: "determine", 87 | Short: "Determination functions", 88 | Long: ``, 89 | } 90 | 91 | cmd.AddCommand(orca.NewDetermineBuildtype(out)) 92 | 93 | return cmd 94 | } 95 | 96 | // NewGetCmd represents the get command 97 | func NewGetCmd(out io.Writer) *cobra.Command { 98 | cmd := &cobra.Command{ 99 | Use: "get", 100 | Short: "Get functions", 101 | Long: ``, 102 | } 103 | 104 | cmd.AddCommand( 105 | orca.NewGetEnvCmd(out), 106 | orca.NewGetResourceCmd(out), 107 | orca.NewGetArtifactCmd(out), 108 | ) 109 | 110 | return cmd 111 | } 112 | 113 | // NewLockCmd represents the lock command 114 | func NewLockCmd(out io.Writer) *cobra.Command { 115 | cmd := &cobra.Command{ 116 | Use: "lock", 117 | Short: "Lock functions", 118 | Long: ``, 119 | } 120 | 121 | cmd.AddCommand(orca.NewLockEnvCmd(out)) 122 | 123 | return cmd 124 | } 125 | 126 | // NewUnlockCmd represents the unlock command 127 | func NewUnlockCmd(out io.Writer) *cobra.Command { 128 | cmd := &cobra.Command{ 129 | Use: "unlock", 130 | Short: "Unlock functions", 131 | Long: ``, 132 | } 133 | 134 | cmd.AddCommand(orca.NewUnlockEnvCmd(out)) 135 | 136 | return cmd 137 | } 138 | 139 | // NewPushCmd represents the get command 140 | func NewPushCmd(out io.Writer) *cobra.Command { 141 | cmd := &cobra.Command{ 142 | Use: "push", 143 | Short: "Push functions", 144 | Long: ``, 145 | } 146 | 147 | cmd.AddCommand(orca.NewPushChartCmd(out)) 148 | 149 | return cmd 150 | } 151 | 152 | // NewCreateCmd represents the create command 153 | func NewCreateCmd(out io.Writer) *cobra.Command { 154 | cmd := &cobra.Command{ 155 | Use: "create", 156 | Short: "Creation functions", 157 | Long: ``, 158 | } 159 | 160 | cmd.AddCommand(orca.NewCreateResourceCmd(out)) 161 | 162 | return cmd 163 | } 164 | 165 | // NewDiffCmd represents the create command 166 | func NewDiffCmd(out io.Writer) *cobra.Command { 167 | cmd := &cobra.Command{ 168 | Use: "diff", 169 | Short: "Differentiation functions", 170 | Long: ``, 171 | } 172 | 173 | cmd.AddCommand(orca.NewDiffEnvCmd(out)) 174 | 175 | return cmd 176 | } 177 | 178 | // NewValidateCmd represents the validate command 179 | func NewValidateCmd(out io.Writer) *cobra.Command { 180 | cmd := &cobra.Command{ 181 | Use: "validate", 182 | Short: "Validation functions", 183 | Long: ``, 184 | } 185 | 186 | cmd.AddCommand(orca.NewValidateEnvCmd(out)) 187 | 188 | return cmd 189 | } 190 | 191 | var ( 192 | // GitTag stands for a git tag 193 | GitTag string 194 | // GitCommit stands for a git commit hash 195 | GitCommit string 196 | ) 197 | 198 | // NewVersionCmd prints version information 199 | func NewVersionCmd(out io.Writer) *cobra.Command { 200 | cmd := &cobra.Command{ 201 | Use: "version", 202 | Short: "Print version information", 203 | Long: ``, 204 | Run: func(cmd *cobra.Command, args []string) { 205 | fmt.Printf("Version %s (git-%s)\n", GitTag, GitCommit) 206 | }, 207 | } 208 | 209 | return cmd 210 | } 211 | -------------------------------------------------------------------------------- /docs/commands/README.md: -------------------------------------------------------------------------------- 1 | ## All commands 2 | 3 | 4 | ### Deploy artifact 5 | ``` 6 | Deploy an artifact to Artifactory 7 | 8 | Usage: 9 | orca deploy artifact [flags] 10 | 11 | Flags: 12 | --file string path of file to deploy. Overrides $ORCA_FILE 13 | --token string artifactory token to use. Overrides $ORCA_TOKEN 14 | --url string url of file to deploy. Overrides $ORCA_URL 15 | ``` 16 | 17 | 18 | ### Get artifact 19 | ``` 20 | Get an artifact from Artifactory 21 | 22 | Usage: 23 | orca get artifact [flags] 24 | 25 | Flags: 26 | --file string path of file to write. Overrides $ORCA_FILE 27 | --token string artifactory token to use. Overrides $ORCA_TOKEN 28 | --url string url of file to get. Overrides $ORCA_URL 29 | ``` 30 | 31 | ### Deploy chart 32 | ``` 33 | Deploy a Helm chart from chart repository 34 | 35 | Usage: 36 | orca deploy chart [flags] 37 | 38 | Flags: 39 | --helm-tls-store string path to TLS certs and keys. Overrides $HELM_TLS_STORE 40 | --inject enable injection during helm upgrade. Overrides $ORCA_INJECT (requires helm inject plugin: https://github.com/maorfr/helm-inject) 41 | --kube-context string name of the kubeconfig context to use. Overrides $ORCA_KUBE_CONTEXT 42 | --name string name of chart to deploy. Overrides $ORCA_NAME 43 | -n, --namespace string kubernetes namespace to deploy to. Overrides $ORCA_NAMESPACE 44 | --release-name string release name. Overrides $ORCA_RELEASE_NAME 45 | --repo string chart repository (name=url). Overrides $ORCA_REPO 46 | -s, --set strings set additional parameters 47 | --timeout int time in seconds to wait for any individual Kubernetes operation (like Jobs for hooks). Overrides $ORCA_TIMEOUT (default 300) 48 | --tls enable TLS for request. Overrides $ORCA_TLS 49 | --validate perform environment validation after deployment. Overrides $ORCA_VALIDATE 50 | -f, --values strings values file to use (packaged within the chart) 51 | --version string version of chart to deploy. Overrides $ORCA_VERSION 52 | ``` 53 | 54 | `helm-tls-store` - path to directory containing `.cert.pem` and `.key.pem` files 55 | 56 | ### Push chart 57 | ``` 58 | Push Helm chart to chart repository (requires helm push plugin: https://github.com/chartmuseum/helm-push) 59 | 60 | Usage: 61 | orca push chart [flags] 62 | 63 | Flags: 64 | --append string string to append to version. Overrides $ORCA_APPEND 65 | --lint should perform lint before push. Overrides $ORCA_LINT 66 | --path string path to chart. Overrides $ORCA_PATH 67 | --repo string chart repository (name=url). Overrides $ORCA_REPO 68 | ``` 69 | 70 | ### Get env 71 | ``` 72 | Get list of Helm releases in an environment (Kubernetes namespace) 73 | 74 | Usage: 75 | orca get env [flags] 76 | 77 | Flags: 78 | --kube-context string name of the kubeconfig context to use. Overrides $ORCA_KUBE_CONTEXT 79 | -n, --name string name of environment (namespace) to get. Overrides $ORCA_NAME 80 | -o, --output string output format (yaml, md). Overrides $ORCA_OUTPUT 81 | ``` 82 | 83 | ### Deploy env 84 | ``` 85 | Deploy a list of Helm charts to an environment (Kubernetes namespace) 86 | 87 | Usage: 88 | orca deploy env [flags] 89 | 90 | Aliases: 91 | env, environment 92 | 93 | Flags: 94 | --annotations strings additional environment (namespace) annotations (can specify multiple): annotation=value 95 | -c, --charts-file string path to file with list of Helm charts to install. Overrides $ORCA_CHARTS_FILE 96 | -x, --deploy-only-override-if-env-exists if environment exists - deploy only override(s) (avoid environment update). Overrides $ORCA_DEPLOY_ONLY_OVERRIDE_IF_ENV_EXISTS 97 | --helm-tls-store string path to TLS certs and keys. Overrides $HELM_TLS_STORE 98 | --inject enable injection during helm upgrade. Overrides $ORCA_INJECT (requires helm inject plugin: https://github.com/maorfr/helm-inject) 99 | --kube-context string name of the kubeconfig context to use. Overrides $ORCA_KUBE_CONTEXT 100 | --labels strings environment (namespace) labels (can specify multiple): label=value 101 | -n, --name string name of environment (namespace) to deploy to. Overrides $ORCA_NAME 102 | --override strings chart to override with different version (can specify multiple): chart=version 103 | -p, --parallel int number of releases to act on in parallel. set this flag to 0 for full parallelism. Overrides $ORCA_PARALLEL (default 1) 104 | --protected-chart strings chart name to protect from being overridden (can specify multiple) 105 | --repo string chart repository (name=url). Overrides $ORCA_REPO 106 | -s, --set strings set additional parameters 107 | --timeout int time in seconds to wait for any individual Kubernetes operation (like Jobs for hooks). Overrides $ORCA_TIMEOUT (default 300) 108 | --tls enable TLS for request. Overrides $ORCA_TLS 109 | --validate perform environment validation after deployment. Overrides $ORCA_VALIDATE 110 | -f, --values strings values file to use (packaged within the chart) 111 | ``` 112 | 113 | `helm-tls-store` - path to directory containing `.cert.pem` and `.key.pem` files 114 | 115 | ### Delete env 116 | ``` 117 | Delete an environment (Kubernetes namespace) along with all Helm releases in it 118 | 119 | Usage: 120 | orca delete env [flags] 121 | 122 | Flags: 123 | --force force environment deletion. Overrides $ORCA_FORCE 124 | --helm-tls-store string path to TLS certs and keys. Overrides $HELM_TLS_STORE 125 | --kube-context string name of the kubeconfig context to use. Overrides $ORCA_KUBE_CONTEXT 126 | -n, --name string name of environment (namespace) to delete. Overrides $ORCA_NAME 127 | -p, --parallel int number of releases to act on in parallel. set this flag to 0 for full parallelism. Overrides $ORCA_PARALLEL (default 1) 128 | --timeout int time in seconds to wait for any individual Kubernetes operation (like Jobs for hooks). Overrides $ORCA_TIMEOUT (default 300) 129 | --tls enable TLS for request. Overrides $ORCA_TLS 130 | ``` 131 | 132 | `helm-tls-store` - path to directory containing `.cert.pem` and `.key.pem` files 133 | 134 | ### Diff env 135 | ``` 136 | Show differences in Helm releases between environments (Kubernetes namespace) 137 | 138 | Usage: 139 | orca diff env [flags] 140 | 141 | Flags: 142 | -h, --help help for env 143 | --kube-context-left string name of the left kubeconfig context to use. Overrides $ORCA_KUBE_CONTEXT_LEFT 144 | --kube-context-right string name of the right kubeconfig context to use. Overrides $ORCA_KUBE_CONTEXT_RIGHT 145 | --name-left string name of left environment to compare. Overrides $ORCA_NAME_LEFT 146 | --name-right string name of right environment to compare. Overrides $ORCA_NAME_RIGHT 147 | ``` 148 | 149 | ### Lock env 150 | ``` 151 | Lock an environment (Kubernetes namespace) 152 | 153 | Usage: 154 | orca lock env [flags] 155 | 156 | Flags: 157 | --kube-context string name of the kubeconfig context to use. Overrides $ORCA_KUBE_CONTEXT 158 | -n, --name string name of environment (namespace) to lock. Overrides $ORCA_NAME 159 | ``` 160 | 161 | ### Unlock env 162 | ``` 163 | Unlock an environment (Kubernetes namespace) 164 | 165 | Usage: 166 | orca unlock env [flags] 167 | 168 | Flags: 169 | --kube-context string name of the kubeconfig context to use. Overrides $ORCA_KUBE_CONTEXT 170 | -n, --name string name of environment (namespace) to unlock. Overrides $ORCA_NAME 171 | ``` 172 | 173 | ### Validate env 174 | ``` 175 | Validate an environment (Kubernetes namespace) 176 | 177 | Usage: 178 | orca validate env [flags] 179 | 180 | Flags: 181 | --kube-context string name of the kubeconfig context to use. Overrides $ORCA_KUBE_CONTEXT 182 | -n, --name string name of environment (namespace) to validate. Overrides $ORCA_NAME 183 | ``` 184 | 185 | ### Create resource 186 | ``` 187 | Create or update a resource via REST API 188 | 189 | Usage: 190 | orca create resource [flags] 191 | 192 | Flags: 193 | --headers strings headers of the request (supports multiple) 194 | --method string method to use in the request. Overrides $ORCA_METHOD (default "POST") 195 | --update should method be PUT instead of POST. Overrides $ORCA_UPDATE 196 | --url string url to send the request to. Overrides $ORCA_URL 197 | ``` 198 | 199 | ### Get resource 200 | ``` 201 | Get a resource via REST API 202 | 203 | Usage: 204 | orca get resource [flags] 205 | 206 | Flags: 207 | -e, --error-indicator string string indicating an error in the request. Overrides $ORCA_ERROR_INDICATOR (default "E") 208 | --headers strings headers of the request (supports multiple) 209 | --key string find the desired object according to this key. Overrides $ORCA_KEY 210 | --offset int offset of the desired object from the reference key. Overrides $ORCA_OFFSET 211 | -p, --print-key string key to print. If not specified - prints the response. Overrides $ORCA_PRINT_KEY 212 | --url string url to send the request to. Overrides $ORCA_URL 213 | --value string find the desired object according to to key`s value. Overrides $ORCA_VALUE 214 | ``` 215 | 216 | ### Delete resource 217 | ``` 218 | Delete a resource via REST API 219 | 220 | Usage: 221 | orca delete resource [flags] 222 | 223 | Flags: 224 | --headers strings headers of the request (supports multiple) 225 | --url string url to send the request to. Overrides $ORCA_URL 226 | ``` 227 | 228 | ### Determine buildtype 229 | ``` 230 | Determine build type based on path filters 231 | 232 | Usage: 233 | orca determine buildtype [flags] 234 | 235 | Flags: 236 | --allow-multiple-types allow multiple build types. Overrides $ORCA_ALLOW_MULTIPLE_TYPES 237 | --curr-ref string current reference name. Overrides $ORCA_CURR_REF 238 | --default-type string default build type. Overrides $ORCA_DEFAULT_TYPE (default "default") 239 | --main-ref string name of the reference which is the main line. Overrides $ORCA_MAIN_REF 240 | --path-filter strings path filter (supports multiple) in the path=buildtype form (supports regex) 241 | --prev-commit string previous commit for paths comparison. Overrides $ORCA_PREV_COMMIT 242 | --prev-commit-error string identify an error with the previous commit by this string. Overrides $ORCA_PREV_COMMIT_ERROR (default "E") 243 | --rel-ref string release reference name (or regex). Overrides $ORCA_REL_REF 244 | ``` 245 | -------------------------------------------------------------------------------- /docs/credentials/README.md: -------------------------------------------------------------------------------- 1 | ## Credentials 2 | 3 | ### Kubernetes 4 | 5 | Orca tries to get credentials in the following order: 6 | If `KUBECONFIG` environment variable is set - orca will use the current context from that config file. Otherwise it will use `~/.kube/config`. -------------------------------------------------------------------------------- /docs/examples/README.md: -------------------------------------------------------------------------------- 1 | ## Examples 2 | 3 | ### Build type determination 4 | This function provides the ability to execute different tasks on different branches and different changed paths. It is essentially a path filter implementation for tools which do not support path filters. 5 | 6 | In this example, if files changed only in the `src` directory, the build type will be set to `code`, if files changed only in the `config` directory, the build type will be set to `config`. If files changed in both (or anywhere else), it will be set to `code`: 7 | ``` 8 | orca determine buildtype \ 9 | --default-type code \ 10 | --path-filter ^src.*$=code,^config.*$=config \ 11 | --prev-commit 12 | ``` 13 | 14 | In this example, if the current reference is different from the mainline and a release branch, the build type will be set to `default`: 15 | ``` 16 | orca determine buildtype \ 17 | --default-type default \ 18 | --curr-ref develop \ 19 | --main-ref master \ 20 | --rel-ref ^.*/rel-.*$ 21 | ``` 22 | 23 | The two examples can be combined. 24 | 25 | 26 | ### Get resource 27 | This function gets a resource from REST API. 28 | 29 | In this example, it gets the previous commit hash (offset of 1 from the current commit hash), to be able to compare it to the current commit hash (which will help with the previous example): 30 | 31 | ``` 32 | orca get resource \ 33 | --url \ 34 | --headers "
:" \ 35 | --key sha \ 36 | --value \ 37 | --offset 1 \ 38 | --print-key sha 39 | ``` 40 | 41 | ### Get env 42 | This functions gets all Helm installed releases from an environment (Kubernetes namespace). 43 | 44 | In this example, only orca managed releases will be displayed (a managed release is considered one with release name in the form of namespace-chartName): 45 | 46 | ``` 47 | orca get env \ 48 | --kube-context \ 49 | --name 50 | ``` 51 | 52 | ### Deploy chart 53 | This function deploys a Helm chart from a chart repository, using values files which are packed along with the chart. 54 | 55 | In this example, the specified chart repository will be added, the chart will be fetched from it, and deployed using `prod-values.yaml` (packed within the chart) to the specified kubernetes context and namespace: 56 | 57 | ``` 58 | orca deploy chart \ 59 | --name \ 60 | --version \ 61 | --release-name \ 62 | --kube-context \ 63 | --namespace \ 64 | -f prod-values.yaml \ 65 | --repo myrepo= 66 | ``` 67 | 68 | ### Deploy env 69 | This function deploys a list of Helm charts from a chart repository. This function supports runtime dependencies between charts. 70 | 71 | If this is `charts.yaml`: 72 | ``` 73 | charts: 74 | - name: cassandra 75 | version: 0.4.0 76 | - name: mariadb 77 | version: 0.5.4 78 | - name: serviceA 79 | version: 0.1.7 80 | depends_on: 81 | - cassandra 82 | - mariadb 83 | - name: serviceB 84 | version: 0.2.3 85 | depends_on: 86 | - serviceA 87 | ``` 88 | Then the below line will deploy the charts in the following order (using topological sort algorithm): 89 | 1. cassandra, mariadb 90 | 2. serviceA 91 | 3. serviceB 92 | 93 | ``` 94 | orca deploy env \ 95 | --name \ 96 | -c charts.yaml \ 97 | --kube-context \ 98 | -f prod-values.yaml 99 | ``` 100 | -------------------------------------------------------------------------------- /docs/examples/gitlab/.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | image: nuvo/orca 2 | stages: 3 | - build 4 | - update_chart 5 | - commit_chart 6 | - upload_chart 7 | - deploy 8 | - delete 9 | variables: 10 | KUBECONFIG: /etc/kube/config # path to mounted kubeconfig file, change as you see fit 11 | ORCA_KUBE_CONTEXT: mycluster.example.com # name of kube context to operate in (can be overridden at job level variables) 12 | ORCA_REPO: myRepo=https://mychartrepo.example.com # chart repository to use for push and deployment 13 | IMAGE_NAME: $CI_REGISTRY_IMAGE # change as you see fit 14 | IMAGE_TAG: $CI_PIPELINE_ID # change as you see fit 15 | SERVICE_NAME: example-service # change this 16 | 17 | # Repository structure 18 | # 19 | # Assuming the source code is under the src/ directory, 20 | # and the Helm chart is under the kubernetes/$SERVICE_NAME/ directory 21 | 22 | # Source code pipeline 23 | # 24 | # Triggered by changes in the src/ directory (GitLab 11.4+) on all branches and on merge requests (Gitlab 11.6+) 25 | 26 | build: 27 | stage: build 28 | only: 29 | refs: [branches,merge_requests] 30 | changes: [src/**/*] 31 | image: docker:stable 32 | services: 33 | - docker:dind 34 | variables: 35 | DOCKER_HOST: tcp://localhost:2375 36 | script: | 37 | docker build -t $IMAGE_NAME:$IMAGE_TAG src/ 38 | docker login -u $CI_USER_NAME -p $CI_USER_TOKEN $CI_REGISTRY 39 | docker push $IMAGE_NAME:$IMAGE_TAG 40 | 41 | # Triggered by changes in the src/ directory on branch master and on merge requests 42 | 43 | update_chart: 44 | stage: update_chart 45 | only: 46 | refs: [master,merge_requests] 47 | changes: [src/**/*] 48 | image: nuvo/build-utils 49 | artifacts: 50 | paths: [kubernetes] 51 | expire_in: 1 hrs 52 | script: | 53 | yawn set kubernetes/$SERVICE_NAME/values.yaml image.tag $IMAGE_TAG 54 | 55 | # Triggered by changes in the src/ directory on branch master 56 | 57 | commit_chart: 58 | stage: commit_chart 59 | only: 60 | refs: [master] 61 | changes: [src/**/*] 62 | image: nuvo/build-utils 63 | dependencies: [update_chart] 64 | script: | 65 | git remote set-url origin $(git remote get-url origin | sed 's|.*@|https://'$CI_USER_NAME:$CI_USER_TOKEN'@|') 66 | git config --global user.email "$GITLAB_USER_EMAIL" 67 | git config --global user.name "$GITLAB_USER_NAME" 68 | git checkout $CI_BUILD_REF_NAME 69 | git add kubernetes/$SERVICE_NAME/values.yaml 70 | git commit -m "Update chart with new image tag (during pipeline $CI_PIPELINE_ID)" 71 | git push -u origin $CI_BUILD_REF_NAME 72 | 73 | # Helm chart pipeline 74 | # 75 | # Triggered by changes only in the kubernetes/ directory on branch master 76 | 77 | upload_chart_mainline: 78 | stage: upload_chart 79 | only: 80 | refs: [master] 81 | changes: [kubernetes/**/*] 82 | except: 83 | changes: [src/**/*] 84 | artifacts: 85 | paths: [.chartversion] 86 | expire_in: 1 hrs 87 | script: | 88 | # chart version is set to -$CI_PIPELINE_ID. for example, 0.1.0-2357 89 | orca push chart \ 90 | --path kubernetes/$SERVICE_NAME/ \ 91 | --append $CI_PIPELINE_ID \ 92 | --lint > .chartversion 93 | 94 | # Assuming that you want to deploy to a namespace called demo 95 | # Have this namespace created beforehand 96 | 97 | deploy_mainline: 98 | stage: deploy 99 | only: 100 | refs: [master] 101 | changes: [kubernetes/**/*] 102 | except: 103 | changes: [src/**/*] 104 | environment: 105 | name: demo # can be used in the script as $CI_ENVIRONMENT_NAME 106 | dependencies: [upload_chart_mainline] 107 | script: | 108 | orca deploy chart \ 109 | --name $SERVICE_NAME \ 110 | --version $(cat .chartversion) \ 111 | --release-name demo-$SERVICE_NAME \ 112 | --namespace demo \ 113 | -f demo-values.yaml 114 | 115 | # Merge request pipeline 116 | # 117 | # Continuing some jobs from previous steps 118 | 119 | upload_chart_mr: 120 | stage: upload_chart 121 | only: [merge_requests] 122 | dependencies: [update_chart] 123 | artifacts: 124 | paths: [.chartversion] 125 | expire_in: 1 hrs 126 | script: | 127 | orca push chart \ 128 | --path kubernetes/$SERVICE_NAME/ \ 129 | --append $CI_PIPELINE_ID \ 130 | --lint > .chartversion 131 | 132 | deploy_dynamic_mr: 133 | stage: deploy 134 | only: [merge_requests] 135 | environment: 136 | name: $CI_COMMIT_REF_SLUG # assuming branch names are identical across related services, change as you see fit 137 | script: | 138 | orca get env --name demo > charts.yaml 139 | orca deploy env \ 140 | --name $CI_COMMIT_REF_SLUG \ 141 | -c charts.yaml \ 142 | -f demo-values.yaml \ # change according to deploy_mainline 143 | --override $SERVICE_NAME=$(cat .chartversion) \ 144 | --set ingress.host=$CI_COMMIT_REF_SLUG.$ORCA_KUBE_CONTEXT \ # change as you see fit 145 | -x 146 | 147 | delete_dynamic_mr: 148 | stage: delete 149 | when: manual 150 | only: [merge_requests] 151 | script: | 152 | orca delete env --name $CI_COMMIT_REF_SLUG \ # change according to deploy_dynamic_mr 153 | --force 154 | -------------------------------------------------------------------------------- /docs/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nuvo/orca/5017d72866990ee29b3ab407c9fddcb322a0dfde/docs/img/logo.png -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/nuvo/orca 2 | 3 | require ( 4 | contrib.go.opencensus.io/exporter/ocagent v0.2.0 // indirect 5 | github.com/Azure/go-autorest v11.3.1+incompatible // indirect 6 | github.com/census-instrumentation/opencensus-proto v0.1.0 // indirect 7 | github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect 8 | github.com/emirpasic/gods v1.12.0 // indirect 9 | github.com/gogo/protobuf v1.2.0 // indirect 10 | github.com/golang/protobuf v1.2.0 11 | github.com/google/btree v1.0.0 // indirect 12 | github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf // indirect 13 | github.com/googleapis/gnostic v0.2.0 // indirect 14 | github.com/gophercloud/gophercloud v0.0.0-20190117043839-e340f5f89555 // indirect 15 | github.com/gosuri/uitable v0.0.1 16 | github.com/gregjones/httpcache v0.0.0-20181110185634-c63ab54fda8f // indirect 17 | github.com/imdario/mergo v0.3.6 // indirect 18 | github.com/inconshreveable/mousetrap v1.0.0 // indirect 19 | github.com/json-iterator/go v1.1.5 // indirect 20 | github.com/mattn/go-runewidth v0.0.4 // indirect 21 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 22 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 // indirect 23 | github.com/peterbourgon/diskv v2.0.1+incompatible // indirect 24 | github.com/spf13/cobra v0.0.3 25 | github.com/spf13/pflag v1.0.3 // indirect 26 | golang.org/x/crypto v0.0.0-20190103213133-ff983b9c42bc // indirect 27 | golang.org/x/net v0.0.0-20190110200230-915654e7eabc // indirect 28 | golang.org/x/oauth2 v0.0.0-20190115181402-5dab4167f31c // indirect 29 | golang.org/x/sys v0.0.0-20190116161447-11f53e031339 // indirect 30 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c // indirect 31 | google.golang.org/api v0.1.0 // indirect 32 | google.golang.org/genproto v0.0.0-20190111180523-db91494dd46c // indirect 33 | google.golang.org/grpc v1.18.0 // indirect 34 | gopkg.in/inf.v0 v0.9.1 // indirect 35 | gopkg.in/src-d/go-billy.v4 v4.3.0 // indirect 36 | gopkg.in/src-d/go-git.v4 v4.8.1 37 | gopkg.in/yaml.v2 v2.2.2 38 | k8s.io/api v0.0.0-20190111032252-67edc246be36 39 | k8s.io/apimachinery v0.0.0-20181127025237-2b1284ed4c93 40 | k8s.io/client-go v0.0.0-20190111032708-6bf63545bd02 41 | k8s.io/helm v2.12.2+incompatible 42 | k8s.io/klog v0.1.0 // indirect 43 | sigs.k8s.io/yaml v1.1.0 // indirect 44 | ) 45 | 46 | go 1.12 47 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0 h1:eOI3/cP2VTU6uZLDYAoic+eyzzB9YyGmJ7eIjl8rOPg= 3 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 4 | contrib.go.opencensus.io/exporter/ocagent v0.2.0 h1:Q/jXnVbliDYozuWJni9452xsSUuo+y8yrioxRgofBhE= 5 | contrib.go.opencensus.io/exporter/ocagent v0.2.0/go.mod h1:0fnkYHF+ORKj7HWzOExKkUHeFX79gXSKUQbpnAM+wzo= 6 | git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= 7 | github.com/Azure/go-autorest v11.3.1+incompatible h1:Pzn7+3iKqV1UAbwKarPKc4asZMJe9fQvs0csgYl6p4A= 8 | github.com/Azure/go-autorest v11.3.1+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= 9 | github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs= 10 | github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs= 11 | github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA= 12 | github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= 13 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 14 | github.com/census-instrumentation/opencensus-proto v0.0.2-0.20180913191712-f303ae3f8d6a/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 15 | github.com/census-instrumentation/opencensus-proto v0.1.0 h1:VwZ9smxzX8u14/125wHIX7ARV+YhR+L4JADswwxWK0Y= 16 | github.com/census-instrumentation/opencensus-proto v0.1.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 17 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 18 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 19 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 20 | github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= 21 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 22 | github.com/emirpasic/gods v1.9.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= 23 | github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg= 24 | github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= 25 | github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ= 26 | github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= 27 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 28 | github.com/gliderlabs/ssh v0.1.1 h1:j3L6gSLQalDETeEg/Jg0mGY0/y/N6zI2xX1978P0Uqw= 29 | github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= 30 | github.com/gogo/protobuf v1.2.0 h1:xU6/SpYbvkNYiptHJYEDRseDLvYE7wSqhYYNy0QSUzI= 31 | github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 32 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= 33 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 34 | github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= 35 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 36 | github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= 37 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 38 | github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo= 39 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 40 | github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= 41 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 42 | github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf h1:+RRA9JqSOZFfKrOeqr2z77+8R2RKyh8PG66dcu1V0ck= 43 | github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= 44 | github.com/googleapis/gnostic v0.2.0 h1:l6N3VoaVzTncYYW+9yOz2LJJammFZGBO13sqgEhpy9g= 45 | github.com/googleapis/gnostic v0.2.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= 46 | github.com/gophercloud/gophercloud v0.0.0-20190117043839-e340f5f89555 h1:bSCtEN3dNqBAa5yY2hJLLsqVXsWedHJlPk40MgeOD8k= 47 | github.com/gophercloud/gophercloud v0.0.0-20190117043839-e340f5f89555/go.mod h1:3WdhXV3rUYy9p6AUW8d94kr+HS62Y4VL9mBnFxsD8q4= 48 | github.com/gosuri/uitable v0.0.1 h1:M9sMNgSZPyAu1FJZJLpJ16ofL8q5ko2EDUkICsynvlY= 49 | github.com/gosuri/uitable v0.0.1/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo= 50 | github.com/gregjones/httpcache v0.0.0-20181110185634-c63ab54fda8f h1:ShTPMJQes6tubcjzGMODIVG5hlrCeImaBnZzKF2N8SM= 51 | github.com/gregjones/httpcache v0.0.0-20181110185634-c63ab54fda8f/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= 52 | github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= 53 | github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28= 54 | github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= 55 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= 56 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 57 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= 58 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= 59 | github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= 60 | github.com/json-iterator/go v1.1.5 h1:gL2yXlmiIo4+t+y32d4WGwOjKGYcGOuyrg46vadswDE= 61 | github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 62 | github.com/kevinburke/ssh_config v0.0.0-20180830205328-81db2a75821e h1:RgQk53JHp/Cjunrr1WlsXSZpqXn+uREuHvUVcK82CV8= 63 | github.com/kevinburke/ssh_config v0.0.0-20180830205328-81db2a75821e/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= 64 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 65 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 66 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 67 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 68 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 69 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 70 | github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y= 71 | github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= 72 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 73 | github.com/mitchellh/go-homedir v1.0.0 h1:vKb8ShqSby24Yrqr/yDYkuFz8d0WUjys40rvnGC8aR0= 74 | github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 75 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 76 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 77 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= 78 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 79 | github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= 80 | github.com/pelletier/go-buffruneio v0.2.0 h1:U4t4R6YkofJ5xHm3dJzuRpPZ0mr5MMCoAWooScCR7aA= 81 | github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo= 82 | github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= 83 | github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= 84 | github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= 85 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 86 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 87 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 88 | github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 89 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 90 | github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= 91 | github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 92 | github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= 93 | github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= 94 | github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8= 95 | github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= 96 | github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= 97 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 98 | github.com/src-d/gcfg v1.4.0 h1:xXbNR5AlLSA315x2UO+fTSSAXCDf+Ar38/6oyGbDKQ4= 99 | github.com/src-d/gcfg v1.4.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI= 100 | github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= 101 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 102 | github.com/xanzy/ssh-agent v0.2.0 h1:Adglfbi5p9Z0BmK2oKU9nTG+zKfniSfnaMYB+ULd+Ro= 103 | github.com/xanzy/ssh-agent v0.2.0/go.mod h1:0NyE30eGUDliuLEHJgYte/zncp2zdTStcOnWhgSqHD8= 104 | go.opencensus.io v0.17.0/go.mod h1:mp1VrMQxhlqqDpKvH4UcQUa4YwlzNmymAjPrDdfxNpI= 105 | go.opencensus.io v0.18.0 h1:Mk5rgZcggtbvtAun5aJzAtjKKN/t0R3jJPlWILlv938= 106 | go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= 107 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 108 | golang.org/x/crypto v0.0.0-20190103213133-ff983b9c42bc h1:F5tKCVGp+MUAHhKp5MZtGqAlGX3+oCsiL1Q629FL90M= 109 | golang.org/x/crypto v0.0.0-20190103213133-ff983b9c42bc/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 110 | golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 111 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 112 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 113 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 114 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 115 | golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 116 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 117 | golang.org/x/net v0.0.0-20190110200230-915654e7eabc h1:Yx9JGxI1SBhVLFjpAkWMaO1TF+xyqtHLjZpvQboJGiM= 118 | golang.org/x/net v0.0.0-20190110200230-915654e7eabc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 119 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 120 | golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 121 | golang.org/x/oauth2 v0.0.0-20190115181402-5dab4167f31c h1:pcBdqVcrlT+A3i+tWsOROFONQyey9tisIQHI4xqVGLg= 122 | golang.org/x/oauth2 v0.0.0-20190115181402-5dab4167f31c/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 123 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 124 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 125 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= 126 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 127 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 128 | golang.org/x/sys v0.0.0-20180903190138-2b024373dcd9/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 129 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 130 | golang.org/x/sys v0.0.0-20190116161447-11f53e031339 h1:g/Jesu8+QLnA0CPzF3E1pURg0Byr7i6jLoX5sqjcAh0= 131 | golang.org/x/sys v0.0.0-20190116161447-11f53e031339/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 132 | golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= 133 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 134 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c h1:fqgJT0MGcGpPgpWU7VRdRjuArfcOvC4AoJmILihzhDg= 135 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 136 | golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 137 | google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= 138 | google.golang.org/api v0.1.0 h1:K6z2u68e86TPdSdefXdzvXgR1zEMa+459vBSfWYAZkI= 139 | google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y= 140 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 141 | google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 142 | google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= 143 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 144 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 145 | google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 146 | google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg= 147 | google.golang.org/genproto v0.0.0-20190111180523-db91494dd46c h1:LZllHYjdJnynBfmwysp+s4yhMzfc+3BzhdqzAMvwjoc= 148 | google.golang.org/genproto v0.0.0-20190111180523-db91494dd46c/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg= 149 | google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= 150 | google.golang.org/grpc v1.15.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= 151 | google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= 152 | google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= 153 | google.golang.org/grpc v1.18.0 h1:IZl7mfBGfbhYx2p2rKRtYgDFw6SBz+kclmxYrCksPPA= 154 | google.golang.org/grpc v1.18.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= 155 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 156 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 157 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 158 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 159 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 160 | gopkg.in/src-d/go-billy.v4 v4.2.1/go.mod h1:tm33zBoOwxjYHZIE+OV8bxTWFMJLrconzFMd38aARFk= 161 | gopkg.in/src-d/go-billy.v4 v4.3.0 h1:KtlZ4c1OWbIs4jCv5ZXrTqG8EQocr0g/d4DjNg70aek= 162 | gopkg.in/src-d/go-billy.v4 v4.3.0/go.mod h1:tm33zBoOwxjYHZIE+OV8bxTWFMJLrconzFMd38aARFk= 163 | gopkg.in/src-d/go-git-fixtures.v3 v3.1.1 h1:XWW/s5W18RaJpmo1l0IYGqXKuJITWRFuA45iOf1dKJs= 164 | gopkg.in/src-d/go-git-fixtures.v3 v3.1.1/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g= 165 | gopkg.in/src-d/go-git.v4 v4.8.1 h1:aAyBmkdE1QUUEHcP4YFCGKmsMQRAuRmUcPEQR7lOAa0= 166 | gopkg.in/src-d/go-git.v4 v4.8.1/go.mod h1:Vtut8izDyrM8BUVQnzJ+YvmNcem2J89EmfZYCkLokZk= 167 | gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= 168 | gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= 169 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 170 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 171 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 172 | honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 173 | k8s.io/api v0.0.0-20190111032252-67edc246be36 h1:XrFGq/4TDgOxYOxtNROTyp2ASjHjBIITdk/+aJD+zyY= 174 | k8s.io/api v0.0.0-20190111032252-67edc246be36/go.mod h1:iuAfoD4hCxJ8Onx9kaTIt30j7jUFS00AXQi6QMi99vA= 175 | k8s.io/apimachinery v0.0.0-20181127025237-2b1284ed4c93 h1:tT6oQBi0qwLbbZSfDkdIsb23EwaLY85hoAV4SpXfdao= 176 | k8s.io/apimachinery v0.0.0-20181127025237-2b1284ed4c93/go.mod h1:ccL7Eh7zubPUSh9A3USN90/OzHNSVN6zxzde07TDCL0= 177 | k8s.io/client-go v0.0.0-20190111032708-6bf63545bd02 h1:YAM4ZYpIdCtx9iu111ZKIqCKOYnO6ibNZ9uV6I1XsY0= 178 | k8s.io/client-go v0.0.0-20190111032708-6bf63545bd02/go.mod h1:7vJpHMYJwNQCWgzmNV+VYUl1zCObLyodBc8nIyt8L5s= 179 | k8s.io/helm v2.12.2+incompatible h1:xSDfcFN8X6lfMKWQB1GmU18pnzIthU+/c7kkcl8Xlb0= 180 | k8s.io/helm v2.12.2+incompatible/go.mod h1:LZzlS4LQBHfciFOurYBFkCMTaZ0D1l+p0teMg7TSULI= 181 | k8s.io/klog v0.1.0 h1:I5HMfc/DtuVaGR1KPwUrTc476K8NCqNBldC7H4dYEzk= 182 | k8s.io/klog v0.1.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= 183 | sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs= 184 | sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= 185 | -------------------------------------------------------------------------------- /pkg/orca/artifact.go: -------------------------------------------------------------------------------- 1 | package orca 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "io/ioutil" 7 | "log" 8 | "os" 9 | 10 | "github.com/nuvo/orca/pkg/utils" 11 | 12 | "github.com/spf13/cobra" 13 | ) 14 | 15 | type artifactCmd struct { 16 | url string 17 | token string 18 | file string 19 | 20 | out io.Writer 21 | } 22 | 23 | // NewGetArtifactCmd represents the get artifact command 24 | func NewGetArtifactCmd(out io.Writer) *cobra.Command { 25 | a := &artifactCmd{out: out} 26 | 27 | cmd := &cobra.Command{ 28 | Use: "artifact", 29 | Short: "Get an artifact from Artifactory", 30 | Long: ``, 31 | Args: func(cmd *cobra.Command, args []string) error { 32 | if a.url == "" { 33 | return errors.New("url of file to get has to be defined") 34 | } 35 | if a.token == "" { 36 | return errors.New("artifactory token to use has to be defined") 37 | } 38 | if a.file == "" { 39 | return errors.New("path of file to write has to be defined") 40 | } 41 | return nil 42 | }, 43 | Run: func(cmd *cobra.Command, args []string) { 44 | artifact := utils.PerformRequest(utils.PerformRequestOptions{ 45 | Method: "GET", 46 | URL: a.url, 47 | Headers: []string{"X-JFrog-Art-Api:" + a.token}, 48 | ExpectedStatusCode: 200, 49 | }) 50 | err := ioutil.WriteFile(a.file, artifact, 0644) 51 | if err != nil { 52 | log.Fatal(err) 53 | } 54 | }, 55 | } 56 | 57 | f := cmd.Flags() 58 | 59 | f.StringVar(&a.url, "url", os.Getenv("ORCA_URL"), "url of file to get. Overrides $ORCA_URL") 60 | f.StringVar(&a.token, "token", os.Getenv("ORCA_TOKEN"), "artifactory token to use. Overrides $ORCA_TOKEN") 61 | f.StringVar(&a.file, "file", os.Getenv("ORCA_FILE"), "path of file to write. Overrides $ORCA_FILE") 62 | 63 | return cmd 64 | } 65 | 66 | // NewDeployArtifactCmd represents the deploy artifact command 67 | func NewDeployArtifactCmd(out io.Writer) *cobra.Command { 68 | a := &artifactCmd{out: out} 69 | 70 | cmd := &cobra.Command{ 71 | Use: "artifact", 72 | Short: "Deploy an artifact to Artifactory", 73 | Long: ``, 74 | Args: func(cmd *cobra.Command, args []string) error { 75 | if a.url == "" { 76 | return errors.New("url of file to deploy has to be defined") 77 | } 78 | if a.token == "" { 79 | return errors.New("artifactory token to use has to be defined") 80 | } 81 | if a.file == "" { 82 | return errors.New("path of file to deploy has to be defined") 83 | } 84 | if _, err := os.Stat(a.file); err != nil { 85 | return errors.New("artifact to deploy does not exist") 86 | } 87 | return nil 88 | }, 89 | Run: func(cmd *cobra.Command, args []string) { 90 | data, err := os.Open(a.file) 91 | if err != nil { 92 | log.Fatal(err) 93 | } 94 | utils.PerformRequest(utils.PerformRequestOptions{ 95 | Method: "PUT", 96 | URL: a.url, 97 | Headers: []string{"X-JFrog-Art-Api:" + a.token}, 98 | ExpectedStatusCode: 201, 99 | Data: data, 100 | }) 101 | }, 102 | } 103 | 104 | f := cmd.Flags() 105 | 106 | f.StringVar(&a.url, "url", os.Getenv("ORCA_URL"), "url of file to deploy. Overrides $ORCA_URL") 107 | f.StringVar(&a.token, "token", os.Getenv("ORCA_TOKEN"), "artifactory token to use. Overrides $ORCA_TOKEN") 108 | f.StringVar(&a.file, "file", os.Getenv("ORCA_FILE"), "path of file to deploy. Overrides $ORCA_FILE") 109 | 110 | return cmd 111 | } 112 | -------------------------------------------------------------------------------- /pkg/orca/buildtype.go: -------------------------------------------------------------------------------- 1 | package orca 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | 8 | "github.com/nuvo/orca/pkg/utils" 9 | 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | type determineCmd struct { 14 | defaultType string 15 | pathFilter []string 16 | allowMultipleTypes bool 17 | mainRef string 18 | releaseRef string 19 | currentRef string 20 | previousCommit string 21 | previousCommitErrorIndicator string 22 | 23 | out io.Writer 24 | } 25 | 26 | // NewDetermineBuildtype represents the determine buildtype command 27 | func NewDetermineBuildtype(out io.Writer) *cobra.Command { 28 | d := &determineCmd{out: out} 29 | 30 | cmd := &cobra.Command{ 31 | Use: "buildtype", 32 | Short: "Determine build type based on path filters", 33 | Long: ``, 34 | Run: func(cmd *cobra.Command, args []string) { 35 | 36 | if !utils.IsMainlineOrReleaseRef(d.currentRef, d.mainRef, d.releaseRef) { 37 | fmt.Println(d.defaultType) 38 | return 39 | } 40 | 41 | if utils.IsCommitError(d.previousCommit, d.previousCommitErrorIndicator) { 42 | fmt.Println(d.defaultType) 43 | return 44 | } 45 | 46 | // If no path filters are defined - default type 47 | if len(d.pathFilter) == 0 { 48 | fmt.Println(d.defaultType) 49 | return 50 | } 51 | 52 | // Get changed paths 53 | changedPaths := utils.GetChangedPaths(d.previousCommit) 54 | 55 | // Some paths changed, check against path filters 56 | buildTypeByPathFilters := utils.GetBuildTypeByPathFilters(d.defaultType, changedPaths, d.pathFilter, d.allowMultipleTypes) 57 | fmt.Println(buildTypeByPathFilters) 58 | }, 59 | } 60 | 61 | f := cmd.Flags() 62 | 63 | f.StringVar(&d.defaultType, "default-type", utils.GetStringEnvVar("ORCA_DEFAULT_TYPE", "default"), "default build type. Overrides $ORCA_DEFAULT_TYPE") 64 | f.StringSliceVar(&d.pathFilter, "path-filter", []string{}, "path filter (supports multiple) in the path=buildtype form (supports regex)") 65 | f.BoolVar(&d.allowMultipleTypes, "allow-multiple-types", utils.GetBoolEnvVar("ORCA_ALLOW_MULTIPLE_TYPES", false), "allow multiple build types. Overrides $ORCA_ALLOW_MULTIPLE_TYPES") 66 | f.StringVar(&d.mainRef, "main-ref", os.Getenv("ORCA_MAIN_REF"), "name of the reference which is the main line. Overrides $ORCA_MAIN_REF") 67 | f.StringVar(&d.releaseRef, "rel-ref", os.Getenv("ORCA_REL_REF"), "release reference name (or regex). Overrides $ORCA_REL_REF") 68 | f.StringVar(&d.currentRef, "curr-ref", os.Getenv("ORCA_CURR_REF"), "current reference name. Overrides $ORCA_CURR_REF") 69 | f.StringVar(&d.previousCommit, "prev-commit", os.Getenv("ORCA_PREV_COMMIT"), "previous commit for paths comparison. Overrides $ORCA_PREV_COMMIT") 70 | f.StringVar(&d.previousCommitErrorIndicator, "prev-commit-error", utils.GetStringEnvVar("ORCA_PREV_COMMIT_ERROR", "E"), "identify an error with the previous commit by this string. Overrides $ORCA_PREV_COMMIT_ERROR") 71 | 72 | return cmd 73 | } 74 | -------------------------------------------------------------------------------- /pkg/orca/chart.go: -------------------------------------------------------------------------------- 1 | package orca 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "log" 7 | "os" 8 | 9 | "github.com/nuvo/orca/pkg/utils" 10 | 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | type chartCmd struct { 15 | name string 16 | version string 17 | releaseName string 18 | packedValues []string 19 | set []string 20 | kubeContext string 21 | namespace string 22 | tls bool 23 | helmTLSStore string 24 | repo string 25 | inject bool 26 | timeout int 27 | validate bool 28 | 29 | out io.Writer 30 | } 31 | 32 | // NewDeployChartCmd represents the deploy chart command 33 | func NewDeployChartCmd(out io.Writer) *cobra.Command { 34 | c := &chartCmd{out: out} 35 | 36 | cmd := &cobra.Command{ 37 | Use: "chart", 38 | Short: "Deploy a Helm chart from chart repository", 39 | Long: ``, 40 | Args: func(cmd *cobra.Command, args []string) error { 41 | if c.tls && c.helmTLSStore == "" { 42 | return errors.New("tls is set to true and helm-tls-store is not defined") 43 | } 44 | if c.tls && c.kubeContext == "" { 45 | return errors.New("kube-context has to be non-empty when tls is set to true") 46 | } 47 | if c.name == "" { 48 | return errors.New("name can not be empty") 49 | } 50 | if c.version == "" { 51 | return errors.New("version can not be empty") 52 | } 53 | if c.repo == "" { 54 | return errors.New("repo can not be empty") 55 | } 56 | return nil 57 | }, 58 | Run: func(cmd *cobra.Command, args []string) { 59 | if err := utils.DeployChartFromRepository(utils.DeployChartFromRepositoryOptions{ 60 | ReleaseName: c.releaseName, 61 | Name: c.name, 62 | Version: c.version, 63 | KubeContext: c.kubeContext, 64 | Namespace: c.namespace, 65 | Repo: c.repo, 66 | TLS: c.tls, 67 | HelmTLSStore: c.helmTLSStore, 68 | PackedValues: c.packedValues, 69 | SetValues: c.set, 70 | IsIsolated: true, 71 | Inject: c.inject, 72 | Timeout: c.timeout, 73 | Validate: c.validate, 74 | }); err != nil { 75 | log.Fatal(err) 76 | } 77 | }, 78 | } 79 | 80 | f := cmd.Flags() 81 | 82 | f.StringVar(&c.name, "name", os.Getenv("ORCA_NAME"), "name of chart to deploy. Overrides $ORCA_NAME") 83 | f.StringVar(&c.version, "version", os.Getenv("ORCA_VERSION"), "version of chart to deploy. Overrides $ORCA_VERSION") 84 | f.StringVar(&c.repo, "repo", os.Getenv("ORCA_REPO"), "chart repository (name=url). Overrides $ORCA_REPO") 85 | f.StringVar(&c.releaseName, "release-name", os.Getenv("ORCA_RELEASE_NAME"), "release name. Overrides $ORCA_RELEASE_NAME") 86 | f.StringVar(&c.kubeContext, "kube-context", os.Getenv("ORCA_KUBE_CONTEXT"), "name of the kubeconfig context to use. Overrides $ORCA_KUBE_CONTEXT") 87 | f.StringVarP(&c.namespace, "namespace", "n", os.Getenv("ORCA_NAMESPACE"), "kubernetes namespace to deploy to. Overrides $ORCA_NAMESPACE") 88 | f.StringSliceVarP(&c.packedValues, "values", "f", []string{}, "values file to use (packaged within the chart)") 89 | f.StringSliceVarP(&c.set, "set", "s", []string{}, "set additional parameters") 90 | f.BoolVar(&c.tls, "tls", utils.GetBoolEnvVar("ORCA_TLS", false), "enable TLS for request. Overrides $ORCA_TLS") 91 | f.StringVar(&c.helmTLSStore, "helm-tls-store", os.Getenv("HELM_TLS_STORE"), "path to TLS certs and keys. Overrides $HELM_TLS_STORE") 92 | f.BoolVar(&c.inject, "inject", utils.GetBoolEnvVar("ORCA_INJECT", false), "enable injection during helm upgrade. Overrides $ORCA_INJECT (requires helm inject plugin: https://github.com/maorfr/helm-inject)") 93 | f.IntVar(&c.timeout, "timeout", utils.GetIntEnvVar("ORCA_TIMEOUT", 300), "time in seconds to wait for any individual Kubernetes operation (like Jobs for hooks). Overrides $ORCA_TIMEOUT") 94 | f.BoolVar(&c.validate, "validate", utils.GetBoolEnvVar("ORCA_VALIDATE", false), "perform environment validation after deployment. Overrides $ORCA_VALIDATE") 95 | 96 | return cmd 97 | } 98 | 99 | type chartPushCmd struct { 100 | path string 101 | append string 102 | repo string 103 | lint bool 104 | 105 | out io.Writer 106 | } 107 | 108 | // NewPushChartCmd represents the push chart command 109 | func NewPushChartCmd(out io.Writer) *cobra.Command { 110 | c := &chartPushCmd{out: out} 111 | 112 | cmd := &cobra.Command{ 113 | Use: "chart", 114 | Short: "Push Helm chart to chart repository (requires helm push plugin: https://github.com/chartmuseum/helm-push)", 115 | Long: ``, 116 | Args: func(cmd *cobra.Command, args []string) error { 117 | if c.repo == "" { 118 | return errors.New("repo can not be empty") 119 | } 120 | return nil 121 | }, 122 | Run: func(cmd *cobra.Command, args []string) { 123 | if err := utils.PushChartToRepository(utils.PushChartToRepositoryOptions{ 124 | Path: c.path, 125 | Append: c.append, 126 | Repo: c.repo, 127 | Lint: c.lint, 128 | Print: false, 129 | }); err != nil { 130 | log.Fatal(err) 131 | } 132 | }, 133 | } 134 | 135 | f := cmd.Flags() 136 | 137 | f.StringVar(&c.path, "path", os.Getenv("ORCA_PATH"), "path to chart. Overrides $ORCA_PATH") 138 | f.StringVar(&c.append, "append", os.Getenv("ORCA_APPEND"), "string to append to version. Overrides $ORCA_APPEND") 139 | f.StringVar(&c.repo, "repo", os.Getenv("ORCA_REPO"), "chart repository (name=url). Overrides $ORCA_REPO") 140 | f.BoolVar(&c.lint, "lint", utils.GetBoolEnvVar("ORCA_LINT", false), "should perform lint before push. Overrides $ORCA_LINT") 141 | 142 | return cmd 143 | } 144 | -------------------------------------------------------------------------------- /pkg/orca/env.go: -------------------------------------------------------------------------------- 1 | package orca 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "log" 8 | "os" 9 | "strings" 10 | "time" 11 | 12 | "github.com/nuvo/orca/pkg/utils" 13 | 14 | "github.com/spf13/cobra" 15 | ) 16 | 17 | const ( 18 | annotationPrefix string = "orca.nuvocares.com" 19 | stateAnnotation string = annotationPrefix + "/state" 20 | protectedAnnotation string = annotationPrefix + "/protected" 21 | busyState string = "busy" 22 | freeState string = "free" 23 | deleteState string = "delete" 24 | failedState string = "failed" 25 | unknownState string = "unknown" 26 | ) 27 | 28 | type envCmd struct { 29 | chartsFile string 30 | name string 31 | override []string 32 | packedValues []string 33 | set []string 34 | kubeContext string 35 | tls bool 36 | helmTLSStore string 37 | repo string 38 | createNS bool 39 | onlyManaged bool 40 | output string 41 | inject bool 42 | force bool 43 | parallel int 44 | timeout int 45 | annotations []string 46 | labels []string 47 | validate bool 48 | deployOnlyOverrideIfEnvExists bool 49 | protectedCharts []string 50 | refresh bool 51 | 52 | out io.Writer 53 | } 54 | 55 | // NewGetEnvCmd represents the get env command 56 | func NewGetEnvCmd(out io.Writer) *cobra.Command { 57 | e := &envCmd{out: out} 58 | 59 | cmd := &cobra.Command{ 60 | Use: "env", 61 | Short: "Get list of Helm releases in an environment (Kubernetes namespace)", 62 | Long: ``, 63 | Args: func(cmd *cobra.Command, args []string) error { 64 | if e.name == "" { 65 | return errors.New("name can not be empty") 66 | } 67 | return nil 68 | }, 69 | Run: func(cmd *cobra.Command, args []string) { 70 | releases, err := utils.GetInstalledReleases(utils.GetInstalledReleasesOptions{ 71 | KubeContext: e.kubeContext, 72 | Namespace: e.name, 73 | IncludeFailed: false, 74 | }) 75 | if err != nil { 76 | log.Fatal(err) 77 | } 78 | 79 | switch e.output { 80 | case "yaml": 81 | utils.PrintReleasesYaml(releases) 82 | case "md": 83 | utils.PrintReleasesMarkdown(releases) 84 | case "table": 85 | utils.PrintReleasesTable(releases) 86 | case "": 87 | utils.PrintReleasesYaml(releases) 88 | } 89 | }, 90 | } 91 | 92 | f := cmd.Flags() 93 | 94 | f.StringVarP(&e.name, "name", "n", os.Getenv("ORCA_NAME"), "name of environment (namespace) to get. Overrides $ORCA_NAME") 95 | f.StringVar(&e.kubeContext, "kube-context", os.Getenv("ORCA_KUBE_CONTEXT"), "name of the kubeconfig context to use. Overrides $ORCA_KUBE_CONTEXT") 96 | f.StringVarP(&e.output, "output", "o", os.Getenv("ORCA_OUTPUT"), "output format (yaml, md, table). Overrides $ORCA_OUTPUT") 97 | 98 | return cmd 99 | } 100 | 101 | // NewDeployEnvCmd represents the deploy env command 102 | func NewDeployEnvCmd(out io.Writer) *cobra.Command { 103 | e := &envCmd{out: out} 104 | 105 | cmd := &cobra.Command{ 106 | Use: "env", 107 | Aliases: []string{"environment"}, 108 | Short: "Deploy a list of Helm charts to an environment (Kubernetes namespace)", 109 | Long: ``, 110 | Args: func(cmd *cobra.Command, args []string) error { 111 | if e.name == "" { 112 | return errors.New("name can not be empty") 113 | } 114 | if e.repo == "" { 115 | return errors.New("repo can not be empty") 116 | } 117 | if e.tls { 118 | if e.helmTLSStore == "" { 119 | return errors.New("tls is set to true and helm-tls-store is not defined") 120 | } 121 | if e.kubeContext == "" { 122 | return errors.New("kube-context has to be non-empty when tls is set to true") 123 | } 124 | } 125 | if len(e.override) == 0 { 126 | if e.chartsFile == "" { 127 | return errors.New("either charts-file or override has to be defined") 128 | } 129 | if e.deployOnlyOverrideIfEnvExists { 130 | return errors.New("override has to be defined when using using deploy-only-override-if-env-exists") 131 | } 132 | } 133 | if e.chartsFile != "" && utils.CheckCircularDependencies(utils.InitReleasesFromChartsFile(e.chartsFile, e.name)) { 134 | return errors.New("Circular dependency found") 135 | } 136 | return nil 137 | }, 138 | Run: func(cmd *cobra.Command, args []string) { 139 | 140 | log.Println("initializing chart repository configuration") 141 | if err := utils.AddRepository(utils.AddRepositoryOptions{ 142 | Repo: e.repo, 143 | Print: false, 144 | }); err != nil { 145 | log.Fatal(err) 146 | } 147 | if err := utils.UpdateRepositories(false); err != nil { 148 | log.Fatal(err) 149 | } 150 | 151 | log.Printf("deploying environment \"%s\"", e.name) 152 | nsPreExists, err := utils.NamespaceExists(e.name, e.kubeContext) 153 | if err != nil { 154 | log.Fatal(err) 155 | } 156 | if !nsPreExists { 157 | if err := utils.CreateNamespace(e.name, e.kubeContext, false); err != nil { 158 | log.Fatal(err) 159 | } 160 | log.Printf("created environment \"%s\"", e.name) 161 | } 162 | if err := lockEnvironment(e.name, e.kubeContext, true); err != nil { 163 | log.Fatal(err) 164 | } 165 | 166 | annotations := map[string]string{} 167 | for _, a := range e.annotations { 168 | k, v := utils.SplitInTwo(a, "=") 169 | annotations[k] = v 170 | } 171 | labels := map[string]string{} 172 | for _, a := range e.labels { 173 | k, v := utils.SplitInTwo(a, "=") 174 | labels[k] = v 175 | } 176 | if err := utils.UpdateNamespace(e.name, e.kubeContext, annotations, labels, true); err != nil { 177 | log.Fatal(err) 178 | } 179 | 180 | log.Print("initializing releases to deploy") 181 | var desiredReleases []utils.ReleaseSpec 182 | if nsPreExists && e.deployOnlyOverrideIfEnvExists { 183 | desiredReleases = utils.InitReleases(e.name, e.override) 184 | } else { 185 | if e.chartsFile != "" { 186 | desiredReleases = utils.InitReleasesFromChartsFile(e.chartsFile, e.name) 187 | } 188 | desiredReleases = utils.OverrideReleases(desiredReleases, e.override, e.name) 189 | } 190 | 191 | log.Print("getting currently deployed releases") 192 | installedReleases, err := utils.GetInstalledReleases(utils.GetInstalledReleasesOptions{ 193 | KubeContext: e.kubeContext, 194 | Namespace: e.name, 195 | IncludeFailed: false, 196 | }) 197 | if err != nil { 198 | unlockEnvironment(e.name, e.kubeContext, true) 199 | log.Fatal(err) 200 | } 201 | 202 | log.Print("updating protected charts") 203 | protectedCharts, err := updateProtectedCharts(e.name, e.kubeContext, e.protectedCharts, true) 204 | if err != nil { 205 | unlockEnvironment(e.name, e.kubeContext, true) 206 | log.Fatal(err) 207 | } 208 | 209 | for _, ir := range installedReleases { 210 | for _, pc := range protectedCharts { 211 | if pc != ir.ChartName { 212 | continue 213 | } 214 | desiredReleases = utils.OverrideReleases(desiredReleases, []string{ir.ChartName + "=" + ir.ChartVersion}, e.name) 215 | } 216 | } 217 | 218 | log.Print("calculating delta between desired releases and currently deployed releases") 219 | releasesToInstall := utils.GetReleasesDelta(desiredReleases, installedReleases) 220 | 221 | log.Print("deploying releases") 222 | if err := utils.DeployChartsFromRepository(utils.DeployChartsFromRepositoryOptions{ 223 | ReleasesToInstall: releasesToInstall, 224 | KubeContext: e.kubeContext, 225 | Namespace: e.name, 226 | Repo: e.repo, 227 | TLS: e.tls, 228 | HelmTLSStore: e.helmTLSStore, 229 | PackedValues: e.packedValues, 230 | SetValues: e.set, 231 | Inject: e.inject, 232 | Parallel: e.parallel, 233 | Timeout: e.timeout, 234 | }); err != nil { 235 | markEnvironmentAsFailed(e.name, e.kubeContext, true) 236 | log.Fatal(err) 237 | } 238 | 239 | if !e.deployOnlyOverrideIfEnvExists { 240 | log.Print("getting currently deployed releases") 241 | installedReleases, err := utils.GetInstalledReleases(utils.GetInstalledReleasesOptions{ 242 | KubeContext: e.kubeContext, 243 | Namespace: e.name, 244 | IncludeFailed: false, 245 | }) 246 | if err != nil { 247 | markEnvironmentAsUnknown(e.name, e.kubeContext, true) 248 | log.Fatal(err) 249 | } 250 | log.Print("calculating delta between desired releases and currently deployed releases") 251 | releasesToDelete := utils.GetReleasesDelta(installedReleases, desiredReleases) 252 | log.Print("deleting undesired releases") 253 | if err := utils.DeleteReleases(utils.DeleteReleasesOptions{ 254 | ReleasesToDelete: releasesToDelete, 255 | KubeContext: e.kubeContext, 256 | TLS: e.tls, 257 | HelmTLSStore: e.helmTLSStore, 258 | Parallel: e.parallel, 259 | Timeout: e.timeout, 260 | }); err != nil { 261 | markEnvironmentAsFailed(e.name, e.kubeContext, true) 262 | log.Fatal(err) 263 | } 264 | } 265 | log.Printf("deployed environment \"%s\"", e.name) 266 | 267 | var envValid bool 268 | if e.validate { 269 | envValid, err = utils.IsEnvValidWithLoopBackOff(e.name, e.kubeContext) 270 | } 271 | 272 | unlockEnvironment(e.name, e.kubeContext, true) 273 | 274 | if !e.validate { 275 | return 276 | } 277 | if err != nil { 278 | log.Fatal(err) 279 | } 280 | if !envValid { 281 | markEnvironmentAsFailed(e.name, e.kubeContext, true) 282 | log.Fatalf("environment \"%s\" validation failed!", e.name) 283 | } 284 | // If we have made it so far, the environment is validated 285 | log.Printf("environment \"%s\" validated!", e.name) 286 | }, 287 | } 288 | 289 | f := cmd.Flags() 290 | 291 | f.StringVarP(&e.chartsFile, "charts-file", "c", os.Getenv("ORCA_CHARTS_FILE"), "path to file with list of Helm charts to install. Overrides $ORCA_CHARTS_FILE") 292 | f.StringSliceVar(&e.override, "override", []string{}, "chart to override with different version (can specify multiple): chart=version") 293 | f.StringVarP(&e.name, "name", "n", os.Getenv("ORCA_NAME"), "name of environment (namespace) to deploy to. Overrides $ORCA_NAME") 294 | f.StringVar(&e.repo, "repo", os.Getenv("ORCA_REPO"), "chart repository (name=url). Overrides $ORCA_REPO") 295 | f.StringVar(&e.kubeContext, "kube-context", os.Getenv("ORCA_KUBE_CONTEXT"), "name of the kubeconfig context to use. Overrides $ORCA_KUBE_CONTEXT") 296 | f.StringSliceVarP(&e.packedValues, "values", "f", []string{}, "values file to use (packaged within the chart)") 297 | f.StringSliceVarP(&e.set, "set", "s", []string{}, "set additional parameters") 298 | f.BoolVar(&e.tls, "tls", utils.GetBoolEnvVar("ORCA_TLS", false), "enable TLS for request. Overrides $ORCA_TLS") 299 | f.StringVar(&e.helmTLSStore, "helm-tls-store", os.Getenv("HELM_TLS_STORE"), "path to TLS certs and keys. Overrides $HELM_TLS_STORE") 300 | f.BoolVar(&e.inject, "inject", utils.GetBoolEnvVar("ORCA_INJECT", false), "enable injection during helm upgrade. Overrides $ORCA_INJECT (requires helm inject plugin: https://github.com/maorfr/helm-inject)") 301 | f.IntVarP(&e.parallel, "parallel", "p", utils.GetIntEnvVar("ORCA_PARALLEL", 1), "number of releases to act on in parallel. set this flag to 0 for full parallelism. Overrides $ORCA_PARALLEL") 302 | f.IntVar(&e.timeout, "timeout", utils.GetIntEnvVar("ORCA_TIMEOUT", 300), "time in seconds to wait for any individual Kubernetes operation (like Jobs for hooks). Overrides $ORCA_TIMEOUT") 303 | f.StringSliceVar(&e.annotations, "annotations", []string{}, "additional environment (namespace) annotations (can specify multiple): annotation=value") 304 | f.StringSliceVar(&e.labels, "labels", []string{}, "environment (namespace) labels (can specify multiple): label=value") 305 | f.BoolVar(&e.validate, "validate", utils.GetBoolEnvVar("ORCA_VALIDATE", false), "perform environment validation after deployment. Overrides $ORCA_VALIDATE") 306 | f.BoolVarP(&e.deployOnlyOverrideIfEnvExists, "deploy-only-override-if-env-exists", "x", utils.GetBoolEnvVar("ORCA_DEPLOY_ONLY_OVERRIDE_IF_ENV_EXISTS", false), "if environment exists - deploy only override(s) (avoid environment update). Overrides $ORCA_DEPLOY_ONLY_OVERRIDE_IF_ENV_EXISTS") 307 | f.StringSliceVar(&e.protectedCharts, "protected-chart", []string{}, "chart name to protect from being overridden (can specify multiple)") 308 | 309 | f.BoolVar(&e.refresh, "refresh", utils.GetBoolEnvVar("ORCA_REFRESH", false), "refresh the environment based on reference environment. Overrides $ORCA_REFRESH") 310 | f.MarkDeprecated("refresh", "this is now the default behavior. use -x to deploy only overrides") 311 | return cmd 312 | } 313 | 314 | // NewDeleteEnvCmd represents the delete env command 315 | func NewDeleteEnvCmd(out io.Writer) *cobra.Command { 316 | e := &envCmd{out: out} 317 | 318 | cmd := &cobra.Command{ 319 | Use: "env", 320 | Short: "Delete an environment (Kubernetes namespace) along with all Helm releases in it", 321 | Long: ``, 322 | Args: func(cmd *cobra.Command, args []string) error { 323 | if e.name == "" { 324 | return errors.New("name can not be empty") 325 | } 326 | if e.tls && e.helmTLSStore == "" { 327 | return errors.New("tls is set to true and helm-tls-store is not defined") 328 | } 329 | if e.tls && e.kubeContext == "" { 330 | return errors.New("kube-context has to be non-empty when tls is set to true") 331 | } 332 | return nil 333 | }, 334 | Run: func(cmd *cobra.Command, args []string) { 335 | nsExists, err := utils.NamespaceExists(e.name, e.kubeContext) 336 | if err != nil { 337 | log.Fatal(err) 338 | } 339 | if nsExists { 340 | if err := markEnvironmentForDeletion(e.name, e.kubeContext, e.force, true); err != nil { 341 | log.Fatal(err) 342 | } 343 | } else { 344 | log.Printf("environment \"%s\" not found", e.name) 345 | } 346 | 347 | log.Print("getting currently deployed releases") 348 | releases, err := utils.GetInstalledReleases(utils.GetInstalledReleasesOptions{ 349 | KubeContext: e.kubeContext, 350 | Namespace: e.name, 351 | IncludeFailed: true, 352 | }) 353 | if err != nil { 354 | log.Fatal(err) 355 | } 356 | log.Print("deleting releases") 357 | if err := utils.DeleteReleases(utils.DeleteReleasesOptions{ 358 | ReleasesToDelete: releases, 359 | KubeContext: e.kubeContext, 360 | TLS: e.tls, 361 | HelmTLSStore: e.helmTLSStore, 362 | Parallel: e.parallel, 363 | Timeout: e.timeout, 364 | }); err != nil { 365 | markEnvironmentAsFailed(e.name, e.kubeContext, true) 366 | log.Fatal(err) 367 | } 368 | 369 | if nsExists { 370 | if utils.Contains([]string{"default", "kube-system", "kube-public"}, e.name) { 371 | removeAnnotationsFromEnvironment(e.name, e.kubeContext, true) 372 | } else { 373 | utils.DeleteNamespace(e.name, e.kubeContext, false) 374 | } 375 | } 376 | log.Printf("deleted environment \"%s\"", e.name) 377 | }, 378 | } 379 | 380 | f := cmd.Flags() 381 | 382 | f.StringVarP(&e.name, "name", "n", os.Getenv("ORCA_NAME"), "name of environment (namespace) to delete. Overrides $ORCA_NAME") 383 | f.StringVar(&e.kubeContext, "kube-context", os.Getenv("ORCA_KUBE_CONTEXT"), "name of the kubeconfig context to use. Overrides $ORCA_KUBE_CONTEXT") 384 | f.BoolVar(&e.tls, "tls", utils.GetBoolEnvVar("ORCA_TLS", false), "enable TLS for request. Overrides $ORCA_TLS") 385 | f.StringVar(&e.helmTLSStore, "helm-tls-store", os.Getenv("HELM_TLS_STORE"), "path to TLS certs and keys. Overrides $HELM_TLS_STORE") 386 | f.BoolVar(&e.force, "force", utils.GetBoolEnvVar("ORCA_FORCE", false), "force environment deletion. Overrides $ORCA_FORCE") 387 | f.IntVarP(&e.parallel, "parallel", "p", utils.GetIntEnvVar("ORCA_PARALLEL", 1), "number of releases to act on in parallel. set this flag to 0 for full parallelism. Overrides $ORCA_PARALLEL") 388 | f.IntVar(&e.timeout, "timeout", utils.GetIntEnvVar("ORCA_TIMEOUT", 300), "time in seconds to wait for any individual Kubernetes operation (like Jobs for hooks). Overrides $ORCA_TIMEOUT") 389 | 390 | return cmd 391 | } 392 | 393 | // NewLockEnvCmd represents the lock env command 394 | func NewLockEnvCmd(out io.Writer) *cobra.Command { 395 | e := &envCmd{out: out} 396 | 397 | cmd := &cobra.Command{ 398 | Use: "env", 399 | Short: "Lock an environment (Kubernetes namespace)", 400 | Long: ``, 401 | Args: func(cmd *cobra.Command, args []string) error { 402 | if e.name == "" { 403 | return errors.New("name can not be empty") 404 | } 405 | return nil 406 | }, 407 | Run: func(cmd *cobra.Command, args []string) { 408 | nsExists, err := utils.NamespaceExists(e.name, e.kubeContext) 409 | if err != nil { 410 | log.Fatal(err) 411 | } 412 | if !nsExists { 413 | log.Printf("environment \"%s\" not found", e.name) 414 | return 415 | } 416 | if err := lockEnvironment(e.name, e.kubeContext, false); err != nil { 417 | log.Fatal(err) 418 | } 419 | log.Printf("locked environment \"%s\"", e.name) 420 | }, 421 | } 422 | 423 | f := cmd.Flags() 424 | 425 | f.StringVarP(&e.name, "name", "n", os.Getenv("ORCA_NAME"), "name of environment (namespace) to lock. Overrides $ORCA_NAME") 426 | f.StringVar(&e.kubeContext, "kube-context", os.Getenv("ORCA_KUBE_CONTEXT"), "name of the kubeconfig context to use. Overrides $ORCA_KUBE_CONTEXT") 427 | 428 | return cmd 429 | } 430 | 431 | // NewUnlockEnvCmd represents the unlock env command 432 | func NewUnlockEnvCmd(out io.Writer) *cobra.Command { 433 | e := &envCmd{out: out} 434 | 435 | cmd := &cobra.Command{ 436 | Use: "env", 437 | Short: "Unlock an environment (Kubernetes namespace)", 438 | Long: ``, 439 | Args: func(cmd *cobra.Command, args []string) error { 440 | if e.name == "" { 441 | return errors.New("name can not be empty") 442 | } 443 | return nil 444 | }, 445 | Run: func(cmd *cobra.Command, args []string) { 446 | nsExists, err := utils.NamespaceExists(e.name, e.kubeContext) 447 | if err != nil { 448 | log.Fatal(err) 449 | } 450 | if !nsExists { 451 | log.Printf("environment \"%s\" not found", e.name) 452 | return 453 | } 454 | if err := unlockEnvironment(e.name, e.kubeContext, false); err != nil { 455 | log.Fatal(err) 456 | } 457 | log.Printf("unlocked environment \"%s\"", e.name) 458 | }, 459 | } 460 | 461 | f := cmd.Flags() 462 | 463 | f.StringVarP(&e.name, "name", "n", os.Getenv("ORCA_NAME"), "name of environment (namespace) to unlock. Overrides $ORCA_NAME") 464 | f.StringVar(&e.kubeContext, "kube-context", os.Getenv("ORCA_KUBE_CONTEXT"), "name of the kubeconfig context to use. Overrides $ORCA_KUBE_CONTEXT") 465 | 466 | return cmd 467 | } 468 | 469 | type diffEnvCmd struct { 470 | nameLeft string 471 | nameRight string 472 | kubeContextLeft string 473 | kubeContextRight string 474 | output string 475 | 476 | out io.Writer 477 | } 478 | 479 | // NewDiffEnvCmd represents the diff env command 480 | func NewDiffEnvCmd(out io.Writer) *cobra.Command { 481 | e := &diffEnvCmd{out: out} 482 | 483 | cmd := &cobra.Command{ 484 | Use: "env", 485 | Short: "Show differences in Helm releases between environments (Kubernetes namespace)", 486 | Long: ``, 487 | Args: func(cmd *cobra.Command, args []string) error { 488 | if e.nameLeft == "" { 489 | return errors.New("name-left can not be empty") 490 | } 491 | if e.nameRight == "" { 492 | return errors.New("name-right can not be empty") 493 | } 494 | return nil 495 | }, 496 | Run: func(cmd *cobra.Command, args []string) { 497 | releasesLeft, err := utils.GetInstalledReleases(utils.GetInstalledReleasesOptions{ 498 | KubeContext: e.kubeContextLeft, 499 | Namespace: e.nameLeft, 500 | IncludeFailed: false, 501 | }) 502 | if err != nil { 503 | log.Fatal(err) 504 | } 505 | releasesRight, err := utils.GetInstalledReleases(utils.GetInstalledReleasesOptions{ 506 | KubeContext: e.kubeContextRight, 507 | Namespace: e.nameRight, 508 | IncludeFailed: false, 509 | }) 510 | if err != nil { 511 | log.Fatal(err) 512 | } 513 | 514 | diffOptions := utils.DiffOptions{ 515 | KubeContextLeft: e.kubeContextLeft, 516 | KubeContextRight: e.kubeContextRight, 517 | EnvNameLeft: e.nameLeft, 518 | EnvNameRight: e.nameRight, 519 | ReleasesSpecLeft: releasesLeft, 520 | ReleasesSpecRight: releasesRight, 521 | Output: e.output, 522 | } 523 | utils.PrintDiff(diffOptions) 524 | }, 525 | } 526 | 527 | f := cmd.Flags() 528 | 529 | f.StringVar(&e.nameLeft, "name-left", os.Getenv("ORCA_NAME_LEFT"), "name of left environment to compare. Overrides $ORCA_NAME_LEFT") 530 | f.StringVar(&e.nameRight, "name-right", os.Getenv("ORCA_NAME_RIGHT"), "name of right environment to compare. Overrides $ORCA_NAME_RIGHT") 531 | f.StringVar(&e.kubeContextLeft, "kube-context-left", os.Getenv("ORCA_KUBE_CONTEXT_LEFT"), "name of the left kubeconfig context to use. Overrides $ORCA_KUBE_CONTEXT_LEFT") 532 | f.StringVar(&e.kubeContextRight, "kube-context-right", os.Getenv("ORCA_KUBE_CONTEXT_RIGHT"), "name of the right kubeconfig context to use. Overrides $ORCA_KUBE_CONTEXT_RIGHT") 533 | f.StringVarP(&e.output, "output", "o", utils.GetStringEnvVar("ORCA_OUTPUT", "yaml"), "output format (yaml, table). Overrides $ORCA_OUTPUT") 534 | 535 | return cmd 536 | } 537 | 538 | // NewValidateEnvCmd represents the validate env command 539 | func NewValidateEnvCmd(out io.Writer) *cobra.Command { 540 | e := &envCmd{out: out} 541 | 542 | cmd := &cobra.Command{ 543 | Use: "env", 544 | Short: "Validate an environment (Kubernetes namespace)", 545 | Long: ``, 546 | Args: func(cmd *cobra.Command, args []string) error { 547 | if e.name == "" { 548 | return errors.New("name can not be empty") 549 | } 550 | return nil 551 | }, 552 | Run: func(cmd *cobra.Command, args []string) { 553 | log.Printf("validating environment \"%s\"", e.name) 554 | nsExists, err := utils.NamespaceExists(e.name, e.kubeContext) 555 | if err != nil { 556 | log.Fatal(err) 557 | } 558 | if !nsExists { 559 | log.Fatalf("environment \"%s\" not found", e.name) 560 | } 561 | 562 | envValid, err := utils.IsEnvValid(e.name, e.kubeContext) 563 | if err != nil { 564 | log.Fatal(err) 565 | } 566 | 567 | if !envValid { 568 | log.Fatalf("environment \"%s\" validation failed!", e.name) 569 | } 570 | // If we have made it so far, the environment is validated 571 | log.Printf("environment \"%s\" validated!", e.name) 572 | }, 573 | } 574 | 575 | f := cmd.Flags() 576 | 577 | f.StringVarP(&e.name, "name", "n", os.Getenv("ORCA_NAME"), "name of environment (namespace) to validate. Overrides $ORCA_NAME") 578 | f.StringVar(&e.kubeContext, "kube-context", os.Getenv("ORCA_KUBE_CONTEXT"), "name of the kubeconfig context to use. Overrides $ORCA_KUBE_CONTEXT") 579 | 580 | return cmd 581 | } 582 | 583 | // lockEnvironment annotates a namespace with "busy" 584 | func lockEnvironment(name, kubeContext string, print bool) error { 585 | sleepPeriod := 5 * time.Second 586 | ns, err := utils.GetNamespace(name, kubeContext) 587 | if err != nil { 588 | return err 589 | } 590 | state := ns.Annotations[stateAnnotation] 591 | if state != "" { 592 | if state != freeState && state != busyState { 593 | return fmt.Errorf("Environment state is %s", state) 594 | } 595 | for state == busyState { 596 | log.Printf("environment \"%s\" %s, backing off for %d seconds", name, busyState, int(sleepPeriod.Seconds())) 597 | time.Sleep(sleepPeriod) 598 | sleepPeriod += 5 * time.Second 599 | ns, err := utils.GetNamespace(name, kubeContext) 600 | if err != nil { 601 | return err 602 | } 603 | state = ns.Annotations[stateAnnotation] 604 | } 605 | } 606 | // There is a race condition here, may need to attend to it in the future 607 | annotations := map[string]string{stateAnnotation: busyState} 608 | err = utils.UpdateNamespace(name, kubeContext, annotations, map[string]string{}, print) 609 | 610 | return err 611 | } 612 | 613 | // unlockEnvironment annotates a namespace with "free" 614 | func unlockEnvironment(name, kubeContext string, print bool) error { 615 | ns, err := utils.GetNamespace(name, kubeContext) 616 | if err != nil { 617 | return err 618 | } 619 | state := ns.Annotations[stateAnnotation] 620 | if state != "" { 621 | if state != freeState && state != busyState { 622 | return fmt.Errorf("Environment state is %s", state) 623 | } 624 | } 625 | annotations := map[string]string{stateAnnotation: freeState} 626 | err = utils.UpdateNamespace(name, kubeContext, annotations, map[string]string{}, print) 627 | 628 | return err 629 | } 630 | 631 | // markEnvironmentForDeletion annotates a namespace with "delete" 632 | func markEnvironmentForDeletion(name, kubeContext string, force, print bool) error { 633 | if !force { 634 | if err := lockEnvironment(name, kubeContext, print); err != nil { 635 | return err 636 | } 637 | } 638 | annotations := map[string]string{stateAnnotation: deleteState} 639 | err := utils.UpdateNamespace(name, kubeContext, annotations, map[string]string{}, print) 640 | 641 | return err 642 | } 643 | 644 | // markEnvironmentAsFailed annotates a namespace with "failed" 645 | func markEnvironmentAsFailed(name, kubeContext string, print bool) error { 646 | annotations := map[string]string{stateAnnotation: failedState} 647 | err := utils.UpdateNamespace(name, kubeContext, annotations, map[string]string{}, print) 648 | 649 | return err 650 | } 651 | 652 | // markEnvironmentAsUnknown annotates a namespace with "unknown" 653 | func markEnvironmentAsUnknown(name, kubeContext string, print bool) error { 654 | annotations := map[string]string{stateAnnotation: unknownState} 655 | err := utils.UpdateNamespace(name, kubeContext, annotations, map[string]string{}, print) 656 | 657 | return err 658 | } 659 | 660 | // removeAnnotationsFromEnvironment removes annotations from a namespace 661 | func removeAnnotationsFromEnvironment(name, kubeContext string, print bool) error { 662 | annotations := map[string]string{} 663 | err := utils.UpdateNamespace(name, kubeContext, annotations, map[string]string{}, print) 664 | 665 | return err 666 | } 667 | 668 | func updateProtectedCharts(name, kubeContext string, protectedChartsToAdd []string, print bool) ([]string, error) { 669 | ns, err := utils.GetNamespace(name, kubeContext) 670 | if err != nil { 671 | return nil, err 672 | } 673 | 674 | protectedCharts := strings.Split(ns.Annotations[protectedAnnotation], ",") 675 | if len(protectedCharts) == 0 && len(protectedChartsToAdd) == 0 { 676 | return []string{}, nil 677 | } 678 | 679 | for _, pcta := range protectedChartsToAdd { 680 | if utils.Contains(protectedCharts, pcta) { 681 | continue 682 | } 683 | protectedCharts = append(protectedCharts, pcta) 684 | } 685 | 686 | annotationValue := strings.Trim(strings.Join(protectedCharts, ","), ",") 687 | annotations := map[string]string{protectedAnnotation: annotationValue} 688 | 689 | protectedChartsWithoutAdds := []string{} 690 | for _, pr := range protectedCharts { 691 | if utils.Contains(protectedChartsToAdd, pr) { 692 | continue 693 | } 694 | protectedChartsWithoutAdds = append(protectedChartsWithoutAdds, pr) 695 | } 696 | 697 | err = utils.UpdateNamespace(name, kubeContext, annotations, map[string]string{}, print) 698 | return protectedChartsWithoutAdds, err 699 | } 700 | -------------------------------------------------------------------------------- /pkg/orca/resource.go: -------------------------------------------------------------------------------- 1 | package orca 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "log" 8 | "os" 9 | "strings" 10 | 11 | "github.com/nuvo/orca/pkg/utils" 12 | 13 | "github.com/spf13/cobra" 14 | ) 15 | 16 | type resourceCmd struct { 17 | url string 18 | headers []string 19 | key string 20 | value string 21 | offset int 22 | errorIndicator string 23 | printKey string 24 | method string 25 | update bool 26 | 27 | out io.Writer 28 | } 29 | 30 | // NewCreateResourceCmd represents the create resource command 31 | func NewCreateResourceCmd(out io.Writer) *cobra.Command { 32 | r := &resourceCmd{out: out} 33 | 34 | cmd := &cobra.Command{ 35 | Use: "resource", 36 | Short: "Create or update a resource via REST API", 37 | Long: ``, 38 | Run: func(cmd *cobra.Command, args []string) { 39 | method := r.method 40 | if r.update { 41 | method = "PATCH" 42 | } 43 | utils.PerformRequest(utils.PerformRequestOptions{ 44 | Method: method, 45 | URL: r.url, 46 | Headers: r.headers, 47 | ExpectedStatusCode: 201, 48 | Data: nil, 49 | }) 50 | }, 51 | } 52 | 53 | f := cmd.Flags() 54 | 55 | f.StringVar(&r.url, "url", os.Getenv("ORCA_URL"), "url to send the request to. Overrides $ORCA_URL") 56 | f.StringVar(&r.method, "method", utils.GetStringEnvVar("ORCA_METHOD", "POST"), "method to use in the request. Overrides $ORCA_METHOD") 57 | f.BoolVar(&r.update, "update", utils.GetBoolEnvVar("ORCA_UPDATE", false), "should method be PUT instead of POST. Overrides $ORCA_UPDATE") 58 | f.StringSliceVar(&r.headers, "headers", []string{}, "headers of the request (supports multiple)") 59 | 60 | return cmd 61 | } 62 | 63 | // NewGetResourceCmd represents the get resource command 64 | func NewGetResourceCmd(out io.Writer) *cobra.Command { 65 | r := &resourceCmd{out: out} 66 | 67 | cmd := &cobra.Command{ 68 | Use: "resource", 69 | Short: "Get a resource via REST API", 70 | Long: ``, 71 | Run: func(cmd *cobra.Command, args []string) { 72 | 73 | var data []map[string]interface{} 74 | bytes := utils.PerformRequest(utils.PerformRequestOptions{ 75 | Method: "GET", 76 | URL: r.url, 77 | Headers: r.headers, 78 | ExpectedStatusCode: 200, 79 | Data: nil, 80 | }) 81 | if err := json.Unmarshal(bytes, &data); err != nil { 82 | log.Fatal(err) 83 | } 84 | if r.key == "" { 85 | if r.printKey != "" { 86 | fmt.Println(r.errorIndicator) 87 | return 88 | } 89 | fmt.Println(string(bytes)) 90 | return 91 | } 92 | desiredIndex := -1 93 | for i := 0; i < len(data); i++ { 94 | if data[i][r.key] == r.value { 95 | desiredIndex = i 96 | break 97 | } 98 | } 99 | if desiredIndex == -1 { 100 | fmt.Println(r.errorIndicator) 101 | return 102 | } 103 | if desiredIndex+r.offset >= len(data) { 104 | fmt.Println(r.errorIndicator) 105 | return 106 | } 107 | 108 | result, _ := json.Marshal(data[desiredIndex+r.offset]) 109 | if r.printKey != "" { 110 | result, _ = json.Marshal(data[desiredIndex+r.offset][r.printKey]) 111 | } 112 | fmt.Println(strings.Trim(string(result), "\"")) 113 | }, 114 | } 115 | 116 | f := cmd.Flags() 117 | 118 | f.StringVar(&r.url, "url", os.Getenv("ORCA_URL"), "url to send the request to. Overrides $ORCA_URL") 119 | f.StringSliceVar(&r.headers, "headers", []string{}, "headers of the request (supports multiple)") 120 | f.StringVar(&r.key, "key", os.Getenv("ORCA_KEY"), "find the desired object according to this key. Overrides $ORCA_KEY") 121 | f.StringVar(&r.value, "value", os.Getenv("ORCA_VALUE"), "find the desired object according to to key`s value. Overrides $ORCA_VALUE") 122 | f.IntVar(&r.offset, "offset", utils.GetIntEnvVar("ORCA_OFFSET", 0), "offset of the desired object from the reference key. Overrides $ORCA_OFFSET") 123 | f.StringVarP(&r.errorIndicator, "error-indicator", "e", utils.GetStringEnvVar("ORCA_ERROR_INDICATOR", "E"), "string indicating an error in the request. Overrides $ORCA_ERROR_INDICATOR") 124 | f.StringVarP(&r.printKey, "print-key", "p", os.Getenv("ORCA_PRINT_KEY"), "key to print. If not specified - prints the response. Overrides $ORCA_PRINT_KEY") 125 | 126 | return cmd 127 | } 128 | 129 | // NewDeleteResourceCmd represents the delete resource command 130 | func NewDeleteResourceCmd(out io.Writer) *cobra.Command { 131 | r := &resourceCmd{out: out} 132 | 133 | cmd := &cobra.Command{ 134 | Use: "resource", 135 | Short: "Delete a resource via REST API", 136 | Long: ``, 137 | Run: func(cmd *cobra.Command, args []string) { 138 | utils.PerformRequest(utils.PerformRequestOptions{ 139 | Method: "DELETE", 140 | URL: r.url, 141 | Headers: r.headers, 142 | ExpectedStatusCode: 204, 143 | Data: nil, 144 | }) 145 | }, 146 | } 147 | 148 | f := cmd.Flags() 149 | 150 | f.StringVar(&r.url, "url", os.Getenv("ORCA_URL"), "url to send the request to. Overrides $ORCA_URL") 151 | f.StringSliceVar(&r.headers, "headers", []string{}, "headers of the request (supports multiple)") 152 | 153 | return cmd 154 | } 155 | -------------------------------------------------------------------------------- /pkg/utils/bwg.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | // BoundedWaitGroup implements a sized WaitGroup 8 | type BoundedWaitGroup struct { 9 | wg sync.WaitGroup 10 | ch chan struct{} 11 | } 12 | 13 | // NewBoundedWaitGroup initializes a new BoundedWaitGroup 14 | func NewBoundedWaitGroup(cap int) BoundedWaitGroup { 15 | return BoundedWaitGroup{ch: make(chan struct{}, cap)} 16 | } 17 | 18 | // Add performs a WaitGroup Add of a specified delta 19 | func (bwg *BoundedWaitGroup) Add(delta int) { 20 | for i := 0; i > delta; i-- { 21 | <-bwg.ch 22 | } 23 | for i := 0; i < delta; i++ { 24 | bwg.ch <- struct{}{} 25 | } 26 | bwg.wg.Add(delta) 27 | } 28 | 29 | // Done performs a WaitGroup Add of -1 30 | func (bwg *BoundedWaitGroup) Done() { 31 | bwg.Add(-1) 32 | } 33 | 34 | // Wait performs a WaitGroup Wait 35 | func (bwg *BoundedWaitGroup) Wait() { 36 | bwg.wg.Wait() 37 | } 38 | -------------------------------------------------------------------------------- /pkg/utils/chart.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "log" 7 | "sort" 8 | "strings" 9 | 10 | "github.com/gosuri/uitable" 11 | yaml "gopkg.in/yaml.v2" 12 | ) 13 | 14 | // ChartsFile represents the structure of a passed in charts file 15 | type ChartsFile struct { 16 | Releases []ReleaseSpec `yaml:"charts"` 17 | } 18 | 19 | // ReleaseSpec holds data relevant to deploying a release 20 | type ReleaseSpec struct { 21 | ReleaseName string `yaml:"release_name,omitempty"` 22 | ChartName string `yaml:"name,omitempty"` 23 | ChartVersion string `yaml:"version,omitempty"` 24 | Dependencies []string `yaml:"depends_on,omitempty"` 25 | } 26 | 27 | // GetReleasesDelta returns the delta between two slices of ReleaseSpec 28 | func GetReleasesDelta(fromReleases, toReleases []ReleaseSpec) []ReleaseSpec { 29 | var releasesDelta []ReleaseSpec 30 | var releasesExists []ReleaseSpec 31 | 32 | for _, fromRelease := range fromReleases { 33 | exists := false 34 | for _, toRelease := range toReleases { 35 | if fromRelease.Equals(toRelease) { 36 | exists = true 37 | releasesExists = append(releasesExists, toRelease) 38 | break 39 | } 40 | } 41 | if !exists { 42 | releasesDelta = append(releasesDelta, fromRelease) 43 | } 44 | } 45 | 46 | for _, releaseExists := range releasesExists { 47 | releasesDelta = RemoveChartFromDependencies(releasesDelta, releaseExists.ChartName) 48 | } 49 | 50 | return releasesDelta 51 | } 52 | 53 | // InitReleasesFromChartsFile initializes a slice of ReleaseSpec from a yaml formatted charts file 54 | func InitReleasesFromChartsFile(file, env string) []ReleaseSpec { 55 | var releases []ReleaseSpec 56 | 57 | data, err := ioutil.ReadFile(file) 58 | if err != nil { 59 | log.Fatalln(err) 60 | } 61 | 62 | v := ChartsFile{} 63 | err = yaml.Unmarshal(data, &v) 64 | if err != nil { 65 | log.Fatalln(err) 66 | } 67 | 68 | for _, chart := range v.Releases { 69 | 70 | c := ReleaseSpec{ 71 | ReleaseName: env + "-" + chart.ChartName, 72 | ChartName: chart.ChartName, 73 | ChartVersion: chart.ChartVersion, 74 | } 75 | 76 | if chart.Dependencies != nil { 77 | for _, dep := range chart.Dependencies { 78 | c.Dependencies = append(c.Dependencies, dep) 79 | } 80 | } 81 | releases = append(releases, c) 82 | } 83 | 84 | return releases 85 | } 86 | 87 | // InitReleases initializes a slice of ReleaseSpec from a string slice 88 | func InitReleases(env string, releases []string) []ReleaseSpec { 89 | var outReleases []ReleaseSpec 90 | 91 | for _, release := range releases { 92 | chartName, chartVersion := SplitInTwo(release, "=") 93 | 94 | r := ReleaseSpec{ 95 | ReleaseName: env + "-" + chartName, 96 | ChartName: chartName, 97 | ChartVersion: chartVersion, 98 | } 99 | 100 | outReleases = append(outReleases, r) 101 | } 102 | 103 | return outReleases 104 | } 105 | 106 | // CheckCircularDependencies verifies that there are no circular dependencies between ReleaseSpecs 107 | func CheckCircularDependencies(releases []ReleaseSpec) bool { 108 | 109 | startLen := len(releases) 110 | endLen := -1 111 | 112 | // while a release was processed 113 | for startLen != endLen && endLen != 0 { 114 | startLen = len(releases) 115 | var indexesToRemove []int 116 | // find releases to process 117 | for i := 0; i < len(releases); i++ { 118 | if len(releases[i].Dependencies) != 0 { 119 | continue 120 | } 121 | indexesToRemove = append(indexesToRemove, i) 122 | } 123 | // "process" the releases 124 | for i := len(indexesToRemove) - 1; i >= 0; i-- { 125 | releases = RemoveChartFromDependencies(releases, releases[indexesToRemove[i]].ChartName) 126 | releases = RemoveChartFromCharts(releases, indexesToRemove[i]) 127 | } 128 | endLen = len(releases) 129 | } 130 | 131 | // if there are any releases left to process - there is a circular dependency 132 | if endLen != 0 { 133 | return true 134 | } 135 | return false 136 | } 137 | 138 | // OverrideReleases overrides versions of specified overrides 139 | func OverrideReleases(releases []ReleaseSpec, overrides []string, env string) []ReleaseSpec { 140 | if len(overrides) == 0 { 141 | return releases 142 | } 143 | 144 | var outReleases []ReleaseSpec 145 | var overrideFound = make([]bool, len(overrides)) 146 | 147 | for _, r := range releases { 148 | for i := 0; i < len(overrides); i++ { 149 | oChartName, oChartVersion := SplitInTwo(overrides[i], "=") 150 | 151 | if r.ChartName == oChartName && r.ChartVersion != oChartVersion { 152 | overrideFound[i] = true 153 | r.ChartName = oChartName 154 | r.ChartVersion = oChartVersion 155 | } 156 | } 157 | outReleases = append(outReleases, r) 158 | } 159 | 160 | for i := 0; i < len(overrides); i++ { 161 | if overrideFound[i] { 162 | continue 163 | } 164 | oChartName, oChartVersion := SplitInTwo(overrides[i], "=") 165 | r := ReleaseSpec{ 166 | ReleaseName: env + "-" + oChartName, 167 | ChartName: oChartName, 168 | ChartVersion: oChartVersion, 169 | } 170 | outReleases = append(outReleases, r) 171 | } 172 | 173 | return outReleases 174 | } 175 | 176 | // RemoveChartFromDependencies removes a release from other releases ReleaseSpec depends_on field 177 | func RemoveChartFromDependencies(charts []ReleaseSpec, name string) []ReleaseSpec { 178 | 179 | var outCharts []ReleaseSpec 180 | 181 | for _, dependant := range charts { 182 | if Contains(dependant.Dependencies, name) { 183 | 184 | index := -1 185 | for i, elem := range dependant.Dependencies { 186 | if elem == name { 187 | index = i 188 | } 189 | } 190 | if index == -1 { 191 | log.Fatal("Could not find element in dependencies") 192 | } 193 | 194 | dependant.Dependencies[index] = dependant.Dependencies[len(dependant.Dependencies)-1] 195 | dependant.Dependencies[len(dependant.Dependencies)-1] = "" 196 | dependant.Dependencies = dependant.Dependencies[:len(dependant.Dependencies)-1] 197 | } 198 | outCharts = append(outCharts, dependant) 199 | } 200 | 201 | return outCharts 202 | } 203 | 204 | // GetChartIndex returns the index of a desired release by its name 205 | func GetChartIndex(charts []ReleaseSpec, name string) int { 206 | index := -1 207 | for i, elem := range charts { 208 | if elem.ChartName == name { 209 | index = i 210 | } 211 | } 212 | return index 213 | } 214 | 215 | // RemoveChartFromCharts removes a ReleaseSpec from a slice of ReleaseSpec 216 | func RemoveChartFromCharts(charts []ReleaseSpec, index int) []ReleaseSpec { 217 | charts[index] = charts[len(charts)-1] 218 | return charts[:len(charts)-1] 219 | } 220 | 221 | // UpdateChartVersion updates a chart version with desired append value 222 | func UpdateChartVersion(path, append string) string { 223 | filePath := path + "Chart.yaml" 224 | data, err := ioutil.ReadFile(filePath) 225 | if err != nil { 226 | log.Fatalln(err) 227 | } 228 | 229 | var v map[string]interface{} 230 | err = yaml.Unmarshal(data, &v) 231 | if err != nil { 232 | log.Fatalln(err) 233 | } 234 | 235 | version := v["version"].(string) 236 | if append == "" { 237 | return version 238 | } 239 | newVersion := fmt.Sprintf("%s-%s", version, append) 240 | v["version"] = newVersion 241 | 242 | data, err = yaml.Marshal(v) 243 | if err != nil { 244 | log.Fatalln(err) 245 | } 246 | ioutil.WriteFile(filePath, data, 0755) 247 | 248 | return newVersion 249 | } 250 | 251 | // ResetChartVersion resets a chart version to a desired value 252 | func ResetChartVersion(path, version string) { 253 | filePath := path + "Chart.yaml" 254 | data, err := ioutil.ReadFile(filePath) 255 | if err != nil { 256 | log.Fatalln(err) 257 | } 258 | 259 | var v map[string]interface{} 260 | err = yaml.Unmarshal(data, &v) 261 | if err != nil { 262 | log.Fatalln(err) 263 | } 264 | 265 | v["version"] = version 266 | 267 | data, err = yaml.Marshal(v) 268 | if err != nil { 269 | log.Fatalln(err) 270 | } 271 | ioutil.WriteFile(filePath, data, 0755) 272 | } 273 | 274 | // Print prints a ReleaseSpec 275 | func (r ReleaseSpec) Print() { 276 | fmt.Println("release name: " + r.ReleaseName) 277 | fmt.Println("chart name: " + r.ChartName) 278 | fmt.Println("chart version: " + r.ChartVersion) 279 | for _, dep := range r.Dependencies { 280 | fmt.Println("depends_on: " + dep) 281 | } 282 | } 283 | 284 | // Equals compares two ReleaseSpecs 285 | func (r ReleaseSpec) Equals(b ReleaseSpec) bool { 286 | return r.ReleaseName == b.ReleaseName && r.ChartName == b.ChartName && r.ChartVersion == b.ChartVersion 287 | } 288 | 289 | // PrintReleasesYaml prints releases in yaml format 290 | func PrintReleasesYaml(releases []ReleaseSpec) { 291 | if len(releases) == 0 { 292 | return 293 | } 294 | fmt.Println("charts:") 295 | for _, r := range releases { 296 | fmt.Println("- name:", r.ChartName) 297 | fmt.Println(" version:", r.ChartVersion) 298 | } 299 | } 300 | 301 | // PrintReleasesMarkdown prints releases in markdown format 302 | func PrintReleasesMarkdown(releases []ReleaseSpec) { 303 | if len(releases) == 0 { 304 | return 305 | } 306 | fmt.Println("| Name | Version |") 307 | fmt.Println("|------|---------|") 308 | for _, r := range releases { 309 | fmt.Println(fmt.Sprintf("| %s | %s |", r.ChartName, r.ChartVersion)) 310 | } 311 | } 312 | 313 | // PrintReleasesTable prints releases in table format 314 | func PrintReleasesTable(releases []ReleaseSpec) { 315 | if len(releases) == 0 { 316 | return 317 | } 318 | tbl := uitable.New() 319 | tbl.MaxColWidth = 60 320 | tbl.AddRow("NAME", "VERSION") 321 | 322 | for _, r := range releases { 323 | tbl.AddRow(r.ChartName, r.ChartVersion) 324 | } 325 | fmt.Println(tbl.String()) 326 | } 327 | 328 | // DiffOptions are options passed to PrintDiffTable 329 | type DiffOptions struct { 330 | KubeContextLeft string 331 | EnvNameLeft string 332 | KubeContextRight string 333 | EnvNameRight string 334 | ReleasesSpecLeft []ReleaseSpec 335 | ReleasesSpecRight []ReleaseSpec 336 | Output string 337 | } 338 | 339 | type diff struct { 340 | chartName string 341 | versionLeft string 342 | versionRight string 343 | } 344 | 345 | // PrintDiff prints a table of differences between two environments 346 | func PrintDiff(o DiffOptions) { 347 | if len(o.ReleasesSpecLeft) == 0 && len(o.ReleasesSpecRight) == 0 { 348 | return 349 | } 350 | diffs := getDiffs(o.ReleasesSpecLeft, o.ReleasesSpecRight) 351 | if len(diffs) == 0 { 352 | return 353 | } 354 | 355 | switch o.Output { 356 | case "yaml": 357 | printDiffYaml(diffs) 358 | case "table": 359 | printDiffTable(o, diffs) 360 | case "": 361 | printDiffYaml(diffs) 362 | } 363 | 364 | } 365 | 366 | func printDiffYaml(diffs []diff) { 367 | fmt.Println("charts:") 368 | for _, d := range diffs { 369 | fmt.Println("- name:", d.chartName) 370 | fmt.Println(" versionLeft:", d.versionLeft) 371 | fmt.Println(" versionRight:", d.versionRight) 372 | } 373 | } 374 | 375 | func printDiffTable(o DiffOptions, diffs []diff) { 376 | tbl := uitable.New() 377 | tbl.MaxColWidth = 60 378 | leftColHeader := initHeader(o.KubeContextLeft, o.EnvNameLeft) 379 | rightColHeader := initHeader(o.KubeContextRight, o.EnvNameRight) 380 | tbl.AddRow("chart", leftColHeader, rightColHeader) 381 | 382 | for _, d := range diffs { 383 | tbl.AddRow(d.chartName, d.versionLeft, d.versionRight) 384 | } 385 | fmt.Println(tbl.String()) 386 | } 387 | 388 | func initHeader(kubeContext, envName string) string { 389 | if kubeContext != "" { 390 | kubeContext += "/" 391 | } 392 | return fmt.Sprintf("%s%s", kubeContext, envName) 393 | } 394 | 395 | func getDiffs(releasesLeft, releasesRight []ReleaseSpec) []diff { 396 | leftAndRight := mergeReleasesToCompare(releasesLeft, releasesRight) 397 | diffs := removeEquals(leftAndRight) 398 | 399 | return diffs 400 | } 401 | 402 | func mergeReleasesToCompare(releasesLeft, releasesRight []ReleaseSpec) []diff { 403 | // Initialize all left elements 404 | var left []diff 405 | for _, r := range releasesLeft { 406 | d := diff{ 407 | chartName: r.ChartName, 408 | versionLeft: r.ChartVersion, 409 | } 410 | left = append(left, d) 411 | } 412 | // Add right elements to existing elements from left 413 | var leftAndRight []diff 414 | for _, r := range releasesRight { 415 | found := false 416 | for i := 0; i < len(left); i++ { 417 | l := left[i] 418 | if l.chartName == r.ChartName { 419 | found = true 420 | l.versionRight = r.ChartVersion 421 | leftAndRight = append(leftAndRight, l) 422 | left = append(left[:i], left[i+1:]...) 423 | break 424 | } 425 | } 426 | // Add right elements which do not exist in left 427 | if !found { 428 | d := diff{ 429 | chartName: r.ChartName, 430 | versionRight: r.ChartVersion, 431 | } 432 | leftAndRight = append(leftAndRight, d) 433 | } 434 | } 435 | // Add left elements which do not exist in right 436 | for _, r := range left { 437 | leftAndRight = append(leftAndRight, r) 438 | } 439 | 440 | return leftAndRight 441 | } 442 | 443 | func removeEquals(leftAndRight []diff) []diff { 444 | var diffs []diff 445 | for _, lar := range leftAndRight { 446 | if lar.versionLeft == lar.versionRight { 447 | continue 448 | } 449 | diffs = append(diffs, lar) 450 | } 451 | 452 | sort.Slice(diffs[:], func(i, j int) bool { 453 | return strings.Compare(diffs[i].chartName, diffs[j].chartName) <= 0 454 | }) 455 | 456 | return diffs 457 | } 458 | -------------------------------------------------------------------------------- /pkg/utils/chart_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestCheckCircularDependencies(t *testing.T) { 8 | type args struct { 9 | releases []ReleaseSpec 10 | } 11 | tests := []struct { 12 | name string 13 | args args 14 | want bool 15 | }{ 16 | { 17 | name: "no circular dependencies", 18 | args: args{InitReleasesFromChartsFile("./testdata/charts.yaml", "test")}, 19 | want: false, 20 | }, 21 | { 22 | name: "circular dependencies", 23 | args: args{InitReleasesFromChartsFile("./testdata/circular.yaml", "test")}, 24 | want: true, 25 | }, 26 | } 27 | for _, tt := range tests { 28 | t.Run(tt.name, func(t *testing.T) { 29 | if got := CheckCircularDependencies(tt.args.releases); got != tt.want { 30 | t.Errorf("CheckCircularDependencies() = %v, want %v", got, tt.want) 31 | } 32 | }) 33 | } 34 | } 35 | 36 | func TestGetChartIndex(t *testing.T) { 37 | type args struct { 38 | charts []ReleaseSpec 39 | name string 40 | } 41 | tests := []struct { 42 | name string 43 | args args 44 | want int 45 | }{ 46 | { 47 | name: "charts file has this chart", 48 | args: args{InitReleasesFromChartsFile("./testdata/charts.yaml", "test"), "kaa"}, 49 | want: 2, 50 | }, 51 | { 52 | name: "charts file doesn't have this chart", 53 | args: args{InitReleasesFromChartsFile("./testdata/charts.yaml", "test"), "rabbitmq"}, 54 | want: -1, 55 | }, 56 | } 57 | for _, tt := range tests { 58 | t.Run(tt.name, func(t *testing.T) { 59 | if got := GetChartIndex(tt.args.charts, tt.args.name); got != tt.want { 60 | t.Errorf("GetChartIndex() = %v, want %v", got, tt.want) 61 | } 62 | }) 63 | } 64 | } 65 | 66 | func TestGetReleasesDelta(t *testing.T) { 67 | rel1 := ReleaseSpec{ChartName: "chart1", ChartVersion: "1.0.0", ReleaseName: "dev-chart1"} 68 | rel2 := ReleaseSpec{ChartName: "chart2", ChartVersion: "2.0.0", ReleaseName: "dev-chart2"} 69 | 70 | fromReleases := []ReleaseSpec{rel1, rel2} 71 | toReleases := []ReleaseSpec{rel1} 72 | 73 | releasesDelta := GetReleasesDelta(fromReleases, toReleases) 74 | 75 | if len(releasesDelta) != 1 { 76 | t.Errorf("Expected: 1, Actual: " + (string)(len(releasesDelta))) 77 | } 78 | 79 | if !releasesDelta[0].Equals(rel2) { 80 | t.Errorf("Expected: true, Actual: false") 81 | } 82 | } 83 | 84 | func TestChartsYamlToReleases(t *testing.T) { 85 | rel0 := ReleaseSpec{ChartName: "cassandra", ChartVersion: "0.4.0", ReleaseName: "test-cassandra"} 86 | rel1 := ReleaseSpec{ChartName: "mariadb", ChartVersion: "0.5.4", ReleaseName: "test-mariadb"} 87 | rel2 := ReleaseSpec{ChartName: "kaa", ChartVersion: "0.1.7", ReleaseName: "test-kaa"} 88 | 89 | releases := InitReleasesFromChartsFile("testdata/charts.yaml", "test") 90 | 91 | if len(releases) != 3 { 92 | t.Errorf("Expected: 3, Actual: " + (string)(len(releases))) 93 | } 94 | if !releases[0].Equals(rel0) { 95 | t.Errorf("Expected: true, Actual: false") 96 | } 97 | if !releases[1].Equals(rel1) { 98 | t.Errorf("Expected: true, Actual: false") 99 | } 100 | if !releases[2].Equals(rel2) { 101 | t.Errorf("Expected: true, Actual: false") 102 | } 103 | } 104 | 105 | func TestReleaseSpec_Equals(t *testing.T) { 106 | type fields struct { 107 | ReleaseName string 108 | ChartName string 109 | ChartVersion string 110 | Dependencies []string 111 | } 112 | type args struct { 113 | b ReleaseSpec 114 | } 115 | tests := []struct { 116 | name string 117 | fields fields 118 | args args 119 | want bool 120 | }{ 121 | { 122 | name: "equals should be true", 123 | fields: fields{ChartName: "mariadb", ChartVersion: "0.5.4", ReleaseName: "test-mariadb"}, 124 | args: args{b: ReleaseSpec{ChartName: "mariadb", ChartVersion: "0.5.4", ReleaseName: "test-mariadb"}}, 125 | want: true, 126 | }, 127 | { 128 | name: "equals should be false", 129 | fields: fields{ChartName: "mariadb", ChartVersion: "0.5.4", ReleaseName: "test-mariadb"}, 130 | args: args{b: ReleaseSpec{ChartName: "cassandra", ChartVersion: "0.5.4", ReleaseName: "test-mariadb"}}, 131 | want: false, 132 | }, 133 | } 134 | for _, tt := range tests { 135 | t.Run(tt.name, func(t *testing.T) { 136 | r := ReleaseSpec{ 137 | ReleaseName: tt.fields.ReleaseName, 138 | ChartName: tt.fields.ChartName, 139 | ChartVersion: tt.fields.ChartVersion, 140 | Dependencies: tt.fields.Dependencies, 141 | } 142 | if got := r.Equals(tt.args.b); got != tt.want { 143 | t.Errorf("ReleaseSpec.Equals() = %v, want %v", got, tt.want) 144 | } 145 | }) 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /pkg/utils/env.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "os" 5 | "strconv" 6 | "strings" 7 | ) 8 | 9 | // GetIntEnvVar returns 0 if the variable is empty or not int, else the value 10 | func GetIntEnvVar(name string, defVal int) int { 11 | val := os.Getenv(name) 12 | if val == "" { 13 | return defVal 14 | } 15 | iVal, err := strconv.Atoi(val) 16 | if err != nil { 17 | return defVal 18 | } 19 | return iVal 20 | } 21 | 22 | // GetStringEnvVar returns the default value if the variable is empty, else the value 23 | func GetStringEnvVar(name, defVal string) string { 24 | val := os.Getenv(name) 25 | if val == "" { 26 | return defVal 27 | } 28 | return val 29 | } 30 | 31 | // GetBoolEnvVar returns the default value if the variable is empty or not true or false, else the value 32 | func GetBoolEnvVar(name string, defVal bool) bool { 33 | val := os.Getenv(name) 34 | if strings.ToLower(val) == "true" { 35 | return true 36 | } 37 | if strings.ToLower(val) == "false" { 38 | return false 39 | } 40 | return defVal 41 | } 42 | -------------------------------------------------------------------------------- /pkg/utils/general.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | exec "os/exec" 7 | "sort" 8 | "strings" 9 | ) 10 | 11 | // PrintExec takes a command and executes it, with or without printing 12 | func PrintExec(cmd []string, print bool) error { 13 | if print { 14 | fmt.Println(cmd) 15 | } 16 | output, err := Exec(cmd) 17 | if err != nil { 18 | return err 19 | } 20 | if print { 21 | fmt.Print(output) 22 | } 23 | return nil 24 | } 25 | 26 | // Exec takes a command as a string and executes it 27 | func Exec(cmd []string) (string, error) { 28 | binary := cmd[0] 29 | _, err := exec.LookPath(binary) 30 | if err != nil { 31 | return "", err 32 | } 33 | 34 | output, err := exec.Command(binary, cmd[1:]...).CombinedOutput() 35 | if err != nil { 36 | return "", fmt.Errorf(string(output)) 37 | } 38 | return string(output), nil 39 | } 40 | 41 | // AddIfNotContained adds a string to a slice if it is not contained in it and not empty 42 | func AddIfNotContained(s []string, e string) (sout []string) { 43 | if (!Contains(s, e)) && (e != "") { 44 | s = append(s, e) 45 | } 46 | 47 | return s 48 | } 49 | 50 | // Contains checks if a slice contains a given value 51 | func Contains(s []string, e string) bool { 52 | for _, a := range s { 53 | if a == e { 54 | return true 55 | } 56 | } 57 | return false 58 | } 59 | 60 | // SplitInTwo splits a string to two parts by a delimeter 61 | func SplitInTwo(s, sep string) (string, string) { 62 | if !strings.Contains(s, sep) { 63 | log.Fatal(s, "does not contain", sep) 64 | } 65 | split := strings.Split(s, sep) 66 | return split[0], split[1] 67 | } 68 | 69 | // MapToString returns a string representation of a map 70 | func MapToString(m map[string]string) string { 71 | keys := make([]string, 0, len(m)) 72 | for k := range m { 73 | keys = append(keys, k) 74 | } 75 | sort.Strings(keys) 76 | 77 | var output string 78 | for _, k := range keys { 79 | output += fmt.Sprintf("%s=%s, ", k, m[k]) 80 | } 81 | return strings.TrimRight(output, ", ") 82 | } 83 | -------------------------------------------------------------------------------- /pkg/utils/general_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestContains(t *testing.T) { 8 | type args struct { 9 | s []string 10 | e string 11 | } 12 | tests := []struct { 13 | name string 14 | args args 15 | want bool 16 | }{ 17 | { 18 | name: "slice contains string", 19 | args: args{[]string{"cat", "lion", "dog"}, "lion"}, 20 | want: true, 21 | }, 22 | { 23 | name: "slice doesn't contain string", 24 | args: args{[]string{"cat", "lion", "dog"}, "moose"}, 25 | want: false, 26 | }, 27 | } 28 | for _, tt := range tests { 29 | t.Run(tt.name, func(t *testing.T) { 30 | if got := Contains(tt.args.s, tt.args.e); got != tt.want { 31 | t.Errorf("Contains() = %v, want %v", got, tt.want) 32 | } 33 | }) 34 | } 35 | } 36 | 37 | func TestMapToString(t *testing.T) { 38 | type args struct { 39 | m map[string]string 40 | } 41 | tests := []struct { 42 | name string 43 | args args 44 | want string 45 | }{ 46 | { 47 | name: "simple map", 48 | args: args{map[string]string{"animal": "lion"}}, 49 | want: "animal=lion", 50 | }, 51 | { 52 | name: "complex map", 53 | args: args{map[string]string{ 54 | "animal": "lion", 55 | "tool": "hammer", 56 | "car": "honda", 57 | }, 58 | }, 59 | want: "animal=lion, car=honda, tool=hammer", 60 | }, 61 | } 62 | for _, tt := range tests { 63 | t.Run(tt.name, func(t *testing.T) { 64 | if got := MapToString(tt.args.m); got != tt.want { 65 | t.Errorf("MapToString() = %v, want %v", got, tt.want) 66 | } 67 | }) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /pkg/utils/git.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "log" 5 | "regexp" 6 | "strings" 7 | 8 | "gopkg.in/src-d/go-git.v4/plumbing/object" 9 | 10 | git "gopkg.in/src-d/go-git.v4" 11 | "gopkg.in/src-d/go-git.v4/plumbing" 12 | ) 13 | 14 | // GetBuildTypeByPathFilters determines the build type according to path filters 15 | func GetBuildTypeByPathFilters(defaultType string, changedPaths, pathFilter []string, allowMultipleTypes bool) string { 16 | 17 | // If no paths were changed - default type 18 | if len(changedPaths) == 0 { 19 | return defaultType 20 | } 21 | 22 | // Count lines per path filter 23 | changedPathsPerFilter, changedPathsPerFilterCount := CountLinesPerPathFilter(pathFilter, changedPaths) 24 | 25 | // If not all paths matched filters - default type 26 | if changedPathsPerFilterCount != len(changedPaths) { 27 | return defaultType 28 | } 29 | 30 | multipleTypes := "" 31 | for bt, btPathCount := range changedPathsPerFilter { 32 | if (!strings.Contains(multipleTypes, bt)) && (btPathCount != 0) { 33 | multipleTypes = multipleTypes + bt + ";" 34 | } 35 | } 36 | multipleTypes = strings.TrimRight(multipleTypes, ";") 37 | 38 | // If multiple is not allowed and there are multiple - default type 39 | if (allowMultipleTypes == false) && (strings.Contains(multipleTypes, ";")) { 40 | return defaultType 41 | } 42 | 43 | return multipleTypes 44 | } 45 | 46 | // GetChangedPaths compares the current commit (HEAD) with the given commit and returns a list of the paths that were changed between them 47 | func GetChangedPaths(previousCommit string) []string { 48 | r, err := git.PlainOpen(".") 49 | if err != nil { 50 | log.Fatal(err) 51 | } 52 | head, err := r.Head() 53 | if err != nil { 54 | log.Fatal(err) 55 | } 56 | 57 | currentCommitTree := getTreeFromHash(head.Hash(), r) 58 | previousCommitTree := getTreeFromStr(previousCommit, r) 59 | changes, err := currentCommitTree.Diff(previousCommitTree) 60 | if err != nil { 61 | log.Fatal(err) 62 | } 63 | 64 | var changedFiles []string 65 | 66 | for _, change := range changes { 67 | changedFiles = AddIfNotContained(changedFiles, change.From.Name) 68 | changedFiles = AddIfNotContained(changedFiles, change.To.Name) 69 | } 70 | 71 | return changedFiles 72 | } 73 | 74 | // IsMainlineOrReleaseRef returns true if this is the mainline or a release branch 75 | func IsMainlineOrReleaseRef(currentRef, mainRef, releaseRef string) bool { 76 | relPattern, _ := regexp.Compile(releaseRef) 77 | return (currentRef == mainRef) || relPattern.MatchString(currentRef) 78 | } 79 | 80 | // IsCommitError returns true if the commit string equals the error indicator 81 | func IsCommitError(commit, commitErrorIndicator string) bool { 82 | return commit == commitErrorIndicator 83 | } 84 | 85 | // CountLinesPerPathFilter get a list of path filters (regex=type) and counts matches from the paths that were changed 86 | func CountLinesPerPathFilter(pathFilter []string, changedPaths []string) (changedPathsPerFilter map[string]int, changedPathsPerFilterCount int) { 87 | 88 | changedPathsPerFilter = map[string]int{} 89 | changedPathsPerFilterCount = 0 90 | 91 | for _, pf := range pathFilter { 92 | pfPathRegex, pfBuildtype := SplitInTwo(pf, "=") 93 | pfPath, _ := regexp.Compile(pfPathRegex) 94 | 95 | changedPathsPerFilter[pfBuildtype] = 0 96 | 97 | for _, path := range changedPaths { 98 | if pfPath.MatchString(path) { 99 | changedPathsPerFilter[pfBuildtype]++ 100 | changedPathsPerFilterCount++ 101 | } 102 | } 103 | } 104 | 105 | return changedPathsPerFilter, changedPathsPerFilterCount 106 | } 107 | 108 | func getTreeFromStr(hash string, r *git.Repository) *object.Tree { 109 | commitHash := plumbing.NewHash(hash) 110 | 111 | return getTreeFromHash(commitHash, r) 112 | } 113 | 114 | func getTreeFromHash(hash plumbing.Hash, r *git.Repository) *object.Tree { 115 | commitObject, err := r.CommitObject(hash) 116 | if err != nil { 117 | log.Fatal(err) 118 | } 119 | commitTree, err := commitObject.Tree() 120 | if err != nil { 121 | log.Fatal(err) 122 | } 123 | 124 | return commitTree 125 | } 126 | -------------------------------------------------------------------------------- /pkg/utils/git_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestIsCommitError(t *testing.T) { 8 | type args struct { 9 | commit string 10 | commitErrorIndicator string 11 | } 12 | tests := []struct { 13 | name string 14 | args args 15 | want bool 16 | }{ 17 | { 18 | name: "commit is not an error", 19 | args: args{commit: "2f7444d674d79ea111483078e803cf3119c88e59", commitErrorIndicator: "E"}, 20 | want: false, 21 | }, 22 | { 23 | name: "commit is an error", 24 | args: args{commit: "E", commitErrorIndicator: "E"}, 25 | want: true, 26 | }, 27 | } 28 | for _, tt := range tests { 29 | t.Run(tt.name, func(t *testing.T) { 30 | if got := IsCommitError(tt.args.commit, tt.args.commitErrorIndicator); got != tt.want { 31 | t.Errorf("IsCommitError() = %v, want %v", got, tt.want) 32 | } 33 | }) 34 | } 35 | } 36 | 37 | func TestIsMainlineOrReleaseRef(t *testing.T) { 38 | type args struct { 39 | currentRef string 40 | mainRef string 41 | releaseRef string 42 | } 43 | tests := []struct { 44 | name string 45 | args args 46 | want bool 47 | }{ 48 | { 49 | name: "this is the mainline", 50 | args: args{"master", "master", "^./rel-.*$"}, 51 | want: true, 52 | }, 53 | { 54 | name: "this is a release branch", 55 | args: args{"fda/rel-1", "master", "^.*/rel-.*$"}, 56 | want: true, 57 | }, 58 | { 59 | name: "this is neither a release nor mainline", 60 | args: args{"develop", "master", "^.*/rel-.*$"}, 61 | want: false, 62 | }, 63 | } 64 | for _, tt := range tests { 65 | t.Run(tt.name, func(t *testing.T) { 66 | if got := IsMainlineOrReleaseRef(tt.args.currentRef, tt.args.mainRef, tt.args.releaseRef); got != tt.want { 67 | t.Errorf("IsMainlineOrReleaseRef() = %v, want %v", got, tt.want) 68 | } 69 | }) 70 | } 71 | } 72 | 73 | func TestGetBuildTypeByPathFilters(t *testing.T) { 74 | type args struct { 75 | defaultType string 76 | changedPaths []string 77 | pathFilter []string 78 | allowMultipleTypes bool 79 | } 80 | tests := []struct { 81 | name string 82 | args args 83 | want string 84 | }{ 85 | { 86 | name: "multiple not allowed, no changed paths", 87 | args: args{"default", []string{}, []string{"^src.*$=code", "^kubernetes.*$=chart"}, false}, 88 | want: "default", 89 | }, 90 | { 91 | name: "multiple allows, all paths match", 92 | args: args{"default", []string{"src/file1.go", "kubernetes/Chart.yaml"}, []string{"^src.*$=code", "^kubernetes.*$=chart"}, true}, 93 | want: "code;chart", 94 | }, 95 | { 96 | name: "multiple not allowed, not all paths match", 97 | args: args{"default", []string{"src/file1.go", "kubernetes/Chart.yaml", "other/file"}, []string{"^src.*$=code", "^kubernetes.*$=chart"}, false}, 98 | want: "default", 99 | }, 100 | { 101 | name: "multiple allowed, not all paths match", 102 | args: args{"default", []string{"src/file1.go", "kubernetes/Chart.yaml", "other/file"}, []string{"^src.*$=code", "^kubernetes.*$=chart"}, true}, 103 | want: "default", 104 | }, 105 | { 106 | name: "multiple not allowed, all paths match", 107 | args: args{"default", []string{"src/file1.go", "kubernetes/Chart.yaml"}, []string{"^src.*$=code", "^kubernetes.*$=chart"}, false}, 108 | want: "default", 109 | }, 110 | } 111 | for _, tt := range tests { 112 | t.Run(tt.name, func(t *testing.T) { 113 | if got := GetBuildTypeByPathFilters(tt.args.defaultType, tt.args.changedPaths, tt.args.pathFilter, tt.args.allowMultipleTypes); got != tt.want { 114 | t.Errorf("GetBuildTypeByPathFilters() = %v, want %v", got, tt.want) 115 | } 116 | }) 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /pkg/utils/helm.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "log" 7 | "math" 8 | "os" 9 | "sync" 10 | "time" 11 | ) 12 | 13 | // DeployChartsFromRepositoryOptions are options passed to DeployChartsFromRepository 14 | type DeployChartsFromRepositoryOptions struct { 15 | ReleasesToInstall []ReleaseSpec 16 | KubeContext string 17 | Namespace string 18 | Repo string 19 | TLS bool 20 | HelmTLSStore string 21 | PackedValues []string 22 | SetValues []string 23 | Inject bool 24 | Parallel int 25 | Timeout int 26 | } 27 | 28 | // DeployChartsFromRepository deploys a list of Helm charts from a repository in parallel 29 | func DeployChartsFromRepository(o DeployChartsFromRepositoryOptions) error { 30 | releasesToInstall := o.ReleasesToInstall 31 | if len(releasesToInstall) == 0 { 32 | return nil 33 | } 34 | parallel := o.Parallel 35 | 36 | totalReleases := len(releasesToInstall) 37 | if parallel == 0 { 38 | parallel = totalReleases 39 | } 40 | bwgSize := int(math.Min(float64(parallel), float64(totalReleases))) // Very stingy :) 41 | bwg := NewBoundedWaitGroup(bwgSize) 42 | errc := make(chan error, 1) 43 | var mutex = &sync.Mutex{} 44 | 45 | for len(releasesToInstall) > 0 && len(errc) == 0 { 46 | 47 | for _, r := range releasesToInstall { 48 | 49 | if len(errc) != 0 { 50 | break 51 | } 52 | 53 | bwg.Add(1) 54 | go func(r ReleaseSpec) { 55 | defer bwg.Done() 56 | 57 | // If there are (still) any dependencies - leave this chart for a later iteration 58 | if len(r.Dependencies) != 0 { 59 | return 60 | } 61 | 62 | mutex.Lock() 63 | // If there has been an error in a concurrent deployment - don`t deploy anymore 64 | if len(errc) != 0 { 65 | mutex.Unlock() 66 | return 67 | } 68 | // Find index of chart in slice 69 | // may have changed by now since we are using go routines 70 | // If chart was not found - another routine is taking care of it 71 | index := GetChartIndex(releasesToInstall, r.ChartName) 72 | if index == -1 { 73 | mutex.Unlock() 74 | return 75 | } 76 | releasesToInstall = RemoveChartFromCharts(releasesToInstall, index) 77 | mutex.Unlock() 78 | 79 | // deploy chart 80 | log.Println("deploying chart", r.ChartName, "version", r.ChartVersion) 81 | if err := DeployChartFromRepository(DeployChartFromRepositoryOptions{ 82 | ReleaseName: r.ReleaseName, 83 | Name: r.ChartName, 84 | Version: r.ChartVersion, 85 | KubeContext: o.KubeContext, 86 | Namespace: o.Namespace, 87 | Repo: o.Repo, 88 | TLS: o.TLS, 89 | HelmTLSStore: o.HelmTLSStore, 90 | PackedValues: o.PackedValues, 91 | SetValues: o.SetValues, 92 | IsIsolated: false, 93 | Inject: o.Inject, 94 | Timeout: o.Timeout, 95 | }); err != nil { 96 | log.Println("failed deploying chart", r.ChartName, "version", r.ChartVersion) 97 | errc <- err 98 | return 99 | } 100 | log.Println("deployed chart", r.ChartName, "version", r.ChartVersion) 101 | 102 | // Deployment is done, remove chart from dependencies 103 | mutex.Lock() 104 | releasesToInstall = RemoveChartFromDependencies(releasesToInstall, r.ChartName) 105 | mutex.Unlock() 106 | }(r) 107 | 108 | } 109 | time.Sleep(5 * time.Second) 110 | } 111 | bwg.Wait() 112 | 113 | if len(errc) != 0 { 114 | // This is not exactly the correct behavior 115 | // There may be more than 1 error in the channel 116 | // But first let's make it work 117 | err := <-errc 118 | close(errc) 119 | if err != nil { 120 | return err 121 | } 122 | } 123 | 124 | return nil 125 | } 126 | 127 | // DeleteReleasesOptions are options passed to DeleteReleases 128 | type DeleteReleasesOptions struct { 129 | ReleasesToDelete []ReleaseSpec 130 | KubeContext string 131 | TLS bool 132 | HelmTLSStore string 133 | Parallel int 134 | Timeout int 135 | } 136 | 137 | // DeleteReleases deletes a list of releases in parallel 138 | func DeleteReleases(o DeleteReleasesOptions) error { 139 | releasesToDelete := o.ReleasesToDelete 140 | if len(releasesToDelete) == 0 { 141 | return nil 142 | } 143 | parallel := o.Parallel 144 | 145 | print := false 146 | totalReleases := len(releasesToDelete) 147 | if parallel == 0 { 148 | parallel = totalReleases 149 | } 150 | bwgSize := int(math.Min(float64(parallel), float64(totalReleases))) // Very stingy :) 151 | bwg := NewBoundedWaitGroup(bwgSize) 152 | errc := make(chan error, 1) 153 | 154 | for _, r := range releasesToDelete { 155 | bwg.Add(1) 156 | go func(r ReleaseSpec) { 157 | defer bwg.Done() 158 | log.Println("deleting", r.ReleaseName) 159 | if err := DeleteRelease(DeleteReleaseOptions{ 160 | ReleaseName: r.ReleaseName, 161 | KubeContext: o.KubeContext, 162 | TLS: o.TLS, 163 | HelmTLSStore: o.HelmTLSStore, 164 | Timeout: o.Timeout, 165 | Print: print, 166 | }); err != nil { 167 | log.Println("failed deleting chart", r.ReleaseName) 168 | errc <- err 169 | return 170 | } 171 | log.Println("deleted", r.ReleaseName) 172 | }(r) 173 | } 174 | bwg.Wait() 175 | 176 | if len(errc) != 0 { 177 | // This is not exactly the correct behavior 178 | // There may be more than 1 error in the channel 179 | // But first let's make it work 180 | err := <-errc 181 | close(errc) 182 | if err != nil { 183 | return err 184 | } 185 | } 186 | 187 | return nil 188 | } 189 | 190 | // DeployChartFromRepositoryOptions are options passed to DeployChartFromRepository 191 | type DeployChartFromRepositoryOptions struct { 192 | ReleaseName string 193 | Name string 194 | Version string 195 | KubeContext string 196 | Namespace string 197 | Repo string 198 | TLS bool 199 | HelmTLSStore string 200 | PackedValues []string 201 | SetValues []string 202 | IsIsolated bool 203 | Inject bool 204 | Timeout int 205 | Validate bool 206 | } 207 | 208 | // DeployChartFromRepository deploys a Helm chart from a chart repository 209 | func DeployChartFromRepository(o DeployChartFromRepositoryOptions) error { 210 | tempDir, err := ioutil.TempDir("", "") 211 | if err != nil { 212 | return fmt.Errorf("failed to create tmp dir") 213 | } 214 | 215 | defer os.RemoveAll(tempDir) 216 | 217 | if o.ReleaseName == "" { 218 | o.ReleaseName = o.Name 219 | } 220 | if o.IsIsolated { 221 | if err := AddRepository(AddRepositoryOptions{ 222 | Repo: o.Repo, 223 | Print: o.IsIsolated, 224 | }); err != nil { 225 | return err 226 | } 227 | if err := UpdateRepositories(o.IsIsolated); err != nil { 228 | return err 229 | } 230 | } 231 | if err := FetchChart(FetchChartOptions{ 232 | Repo: o.Repo, 233 | Name: o.Name, 234 | Version: o.Version, 235 | Dir: tempDir, 236 | Print: o.IsIsolated, 237 | }); err != nil { 238 | return err 239 | } 240 | 241 | path := fmt.Sprintf("%s/%s", tempDir, o.Name) 242 | if err := UpdateChartDependencies(UpdateChartDependenciesOptions{ 243 | Path: path, 244 | Print: o.IsIsolated, 245 | }); err != nil { 246 | return err 247 | } 248 | valuesChain := createValuesChain(o.Name, tempDir, o.PackedValues) 249 | setChain := createSetChain(o.Name, o.SetValues) 250 | 251 | if err := UpgradeRelease(UpgradeReleaseOptions{ 252 | Name: o.Name, 253 | ReleaseName: o.ReleaseName, 254 | KubeContext: o.KubeContext, 255 | Namespace: o.Namespace, 256 | Values: valuesChain, 257 | Set: setChain, 258 | TLS: o.TLS, 259 | HelmTLSStore: o.HelmTLSStore, 260 | Dir: tempDir, 261 | Print: o.IsIsolated, 262 | Inject: o.Inject, 263 | Timeout: o.Timeout, 264 | }); err != nil { 265 | return err 266 | } 267 | 268 | if !o.Validate { 269 | return nil 270 | } 271 | envValid, err := IsEnvValidWithLoopBackOff(o.Namespace, o.KubeContext) 272 | if err != nil { 273 | return err 274 | } 275 | if !envValid { 276 | return fmt.Errorf("environment \"%s\" validation failed", o.Namespace) 277 | } 278 | // If we have made it so far, the environment is validated 279 | log.Printf("environment \"%s\" validated!", o.Namespace) 280 | 281 | return nil 282 | } 283 | 284 | // PushChartToRepositoryOptions are options passed to PushChartToRepository 285 | type PushChartToRepositoryOptions struct { 286 | Path string 287 | Append string 288 | Repo string 289 | Lint bool 290 | Print bool 291 | } 292 | 293 | // PushChartToRepository packages and pushes a Helm chart to a chart repository 294 | func PushChartToRepository(o PushChartToRepositoryOptions) error { 295 | newVersion := UpdateChartVersion(o.Path, o.Append) 296 | if o.Lint { 297 | if err := Lint(LintOptions{ 298 | Path: o.Path, 299 | Print: o.Print, 300 | }); err != nil { 301 | return err 302 | } 303 | } 304 | if err := AddRepository(AddRepositoryOptions{ 305 | Repo: o.Repo, 306 | Print: o.Print, 307 | }); err != nil { 308 | return err 309 | } 310 | if err := UpdateChartDependencies(UpdateChartDependenciesOptions{ 311 | Path: o.Path, 312 | Print: o.Print, 313 | }); err != nil { 314 | return err 315 | } 316 | if err := PushChart(PushChartOptions{ 317 | Repo: o.Repo, 318 | Path: o.Path, 319 | Print: o.Print, 320 | }); err != nil { 321 | return err 322 | } 323 | fmt.Println(newVersion) 324 | return nil 325 | } 326 | 327 | // LintOptions are options passed to Lint 328 | type LintOptions struct { 329 | Path string 330 | Print bool 331 | } 332 | 333 | // Lint takes a path to a chart and runs a series of tests to verify that the chart is well-formed 334 | func Lint(o LintOptions) error { 335 | cmd := []string{"helm", "lint", o.Path} 336 | err := PrintExec(cmd, o.Print) 337 | 338 | return err 339 | } 340 | 341 | // AddRepositoryOptions are options passed to AddRepository 342 | type AddRepositoryOptions struct { 343 | Repo string 344 | Print bool 345 | } 346 | 347 | // AddRepository adds a chart repository to the repositories file 348 | func AddRepository(o AddRepositoryOptions) error { 349 | repoName, repoURL := SplitInTwo(o.Repo, "=") 350 | 351 | cmd := []string{ 352 | "helm", "repo", 353 | "add", repoName, repoURL, 354 | } 355 | err := PrintExec(cmd, o.Print) 356 | 357 | return err 358 | } 359 | 360 | // UpdateRepositories updates helm repositories 361 | func UpdateRepositories(print bool) error { 362 | cmd := []string{"helm", "repo", "update"} 363 | err := PrintExec(cmd, print) 364 | 365 | return err 366 | } 367 | 368 | // FetchChartOptions are options passed to FetchChart 369 | type FetchChartOptions struct { 370 | Repo string 371 | Name string 372 | Version string 373 | Dir string 374 | Print bool 375 | } 376 | 377 | // FetchChart fetches a chart from chart repository by name and version and untars it in the local directory 378 | func FetchChart(o FetchChartOptions) error { 379 | repoName, _ := SplitInTwo(o.Repo, "=") 380 | 381 | cmd := []string{ 382 | "helm", "fetch", 383 | fmt.Sprintf("%s/%s", repoName, o.Name), 384 | "--version", o.Version, 385 | "--untar", 386 | "-d", o.Dir, 387 | } 388 | err := PrintExec(cmd, o.Print) 389 | 390 | return err 391 | } 392 | 393 | // PushChartOptions are options passed to PushChart 394 | type PushChartOptions struct { 395 | Repo string 396 | Path string 397 | Print bool 398 | } 399 | 400 | // PushChart pushes a helm chart to a chart repository 401 | func PushChart(o PushChartOptions) error { 402 | repoName, _ := SplitInTwo(o.Repo, "=") 403 | 404 | cmd := []string{"helm", "push", o.Path, repoName} 405 | err := PrintExec(cmd, o.Print) 406 | 407 | return err 408 | } 409 | 410 | // UpdateChartDependenciesOptions are options passed to UpdateChartDependencies 411 | type UpdateChartDependenciesOptions struct { 412 | Path string 413 | Print bool 414 | } 415 | 416 | // UpdateChartDependencies performs helm dependency update 417 | func UpdateChartDependencies(o UpdateChartDependenciesOptions) error { 418 | cmd := []string{"helm", "dependency", "update", o.Path} 419 | err := PrintExec(cmd, o.Print) 420 | 421 | return err 422 | } 423 | 424 | // UpgradeReleaseOptions are options passed to UpgradeRelease 425 | type UpgradeReleaseOptions struct { 426 | Name string 427 | ReleaseName string 428 | KubeContext string 429 | Namespace string 430 | Values []string 431 | Set []string 432 | TLS bool 433 | HelmTLSStore string 434 | Dir string 435 | Print bool 436 | Inject bool 437 | Timeout int 438 | } 439 | 440 | // UpgradeRelease performs helm upgrade -i 441 | func UpgradeRelease(o UpgradeReleaseOptions) error { 442 | cmd := []string{"helm"} 443 | kubeContextFlag := "--kube-context" 444 | if o.Inject { 445 | kubeContextFlag = "--kubecontext" 446 | cmd = append(cmd, "inject") 447 | } 448 | cmd = append(cmd, "upgrade", "-i", o.ReleaseName, fmt.Sprintf("%s/%s", o.Dir, o.Name)) 449 | if o.KubeContext != "" { 450 | cmd = append(cmd, kubeContextFlag, o.KubeContext) 451 | } 452 | if o.Namespace != "" { 453 | cmd = append(cmd, "--namespace", o.Namespace) 454 | } 455 | cmd = append(cmd, o.Values...) 456 | cmd = append(cmd, o.Set...) 457 | cmd = append(cmd, "--timeout", fmt.Sprintf("%d", o.Timeout)) 458 | cmd = append(cmd, getTLS(o.TLS, o.KubeContext, o.HelmTLSStore)...) 459 | err := PrintExec(cmd, o.Print) 460 | 461 | return err 462 | } 463 | 464 | // DeleteReleaseOptions are options passed to DeleteRelease 465 | type DeleteReleaseOptions struct { 466 | ReleaseName string 467 | KubeContext string 468 | TLS bool 469 | HelmTLSStore string 470 | Timeout int 471 | Print bool 472 | } 473 | 474 | // DeleteRelease deletes a release from Kubernetes 475 | func DeleteRelease(o DeleteReleaseOptions) error { 476 | cmd := []string{ 477 | "helm", "delete", o.ReleaseName, "--purge", 478 | "--timeout", fmt.Sprintf("%d", o.Timeout), 479 | } 480 | if o.KubeContext != "" { 481 | cmd = append(cmd, "--kube-context", o.KubeContext) 482 | } 483 | cmd = append(cmd, getTLS(o.TLS, o.KubeContext, o.HelmTLSStore)...) 484 | err := PrintExec(cmd, o.Print) 485 | 486 | return err 487 | } 488 | 489 | // createValuesChain will create a chain of values files to use 490 | func createValuesChain(name, dir string, packedValues []string) []string { 491 | var values []string 492 | fileToTest := fmt.Sprintf("%s/%s/%s", dir, name, "values.yaml") 493 | if _, err := os.Stat(fileToTest); err == nil { 494 | values = append(values, "-f", fileToTest) 495 | } else { 496 | log.Printf("WARNING: regular values.yaml file not found in chart\n") 497 | } 498 | for _, v := range packedValues { 499 | if fi, err := os.Stat(v); err == nil && fi.Mode().IsRegular() { 500 | if Contains(values, v) { 501 | continue 502 | } 503 | log.Printf("INFO: values file %s found in working directory\n", v) 504 | values = append(values, "-f", v) 505 | continue 506 | } 507 | fileToTest = fmt.Sprintf("%s/%s/%s", dir, name, v) 508 | if fi, err := os.Stat(fileToTest); err == nil && fi.Mode().IsRegular() { 509 | if Contains(values, fileToTest) { 510 | continue 511 | } 512 | log.Printf("INFO: values file %s found in chart\n", fileToTest) 513 | values = append(values, "-f", fileToTest) 514 | continue 515 | } 516 | log.Printf("WARNING: values file %s not found in working directory or chart\n", v) 517 | } 518 | return values 519 | } 520 | 521 | // createSetChain will create a chain of sets to use 522 | func createSetChain(name string, inputSet []string) []string { 523 | set := []string{"--set", fmt.Sprintf("fullnameOverride=%s", name)} 524 | for _, s := range inputSet { 525 | set = append(set, "--set", s) 526 | } 527 | return set 528 | } 529 | 530 | func getTLS(tls bool, kubeContext, helmTLSStore string) []string { 531 | var tlsStr []string 532 | if tls == true { 533 | tlsStr = []string{ 534 | "--tls", 535 | "--tls-cert", fmt.Sprintf("%s/%s.cert.pem", helmTLSStore, kubeContext), 536 | "--tls-key", fmt.Sprintf("%s/%s.key.pem", helmTLSStore, kubeContext), 537 | } 538 | } 539 | return tlsStr 540 | } 541 | -------------------------------------------------------------------------------- /pkg/utils/helm3.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "bytes" 5 | "compress/gzip" 6 | "encoding/base64" 7 | "fmt" 8 | "io/ioutil" 9 | "os" 10 | "path/filepath" 11 | "sort" 12 | "strings" 13 | "time" 14 | 15 | "github.com/golang/protobuf/proto" 16 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 17 | "k8s.io/client-go/kubernetes" 18 | "k8s.io/client-go/rest" 19 | "k8s.io/client-go/tools/clientcmd" 20 | rspb "k8s.io/helm/pkg/proto/hapi/release" 21 | ) 22 | 23 | // GetInstalledReleasesOptions are options passed to GetInstalledReleases 24 | type GetInstalledReleasesOptions struct { 25 | KubeContext string 26 | Namespace string 27 | IncludeFailed bool 28 | } 29 | 30 | // GetInstalledReleases gets the installed Helm releases in a given namespace 31 | func GetInstalledReleases(o GetInstalledReleasesOptions) ([]ReleaseSpec, error) { 32 | 33 | tillerNamespace := "kube-system" 34 | labels := "OWNER=TILLER,STATUS in (DEPLOYED,FAILED)" 35 | if !o.IncludeFailed { 36 | labels = strings.Replace(labels, "FAILED", "", -1) 37 | } 38 | storage, err := getTillerStorage(o.KubeContext, tillerNamespace) 39 | if err != nil { 40 | return nil, err 41 | } 42 | 43 | var releaseSpecs []ReleaseSpec 44 | list, err := listReleases(o.KubeContext, o.Namespace, storage, tillerNamespace, labels) 45 | if err != nil { 46 | return nil, err 47 | } 48 | 49 | for _, releaseData := range list { 50 | 51 | if releaseData.status != "DEPLOYED" { 52 | continue 53 | } 54 | 55 | var releaseSpec ReleaseSpec 56 | releaseSpec.ReleaseName = releaseData.name 57 | releaseSpec.ChartName = releaseData.chart 58 | releaseSpec.ChartVersion = releaseData.version 59 | 60 | releaseSpecs = append(releaseSpecs, releaseSpec) 61 | } 62 | 63 | if !o.IncludeFailed { 64 | return releaseSpecs, nil 65 | } 66 | 67 | for _, releaseData := range list { 68 | if releaseData.status != "FAILED" { 69 | continue 70 | } 71 | 72 | exists := false 73 | for _, rs := range releaseSpecs { 74 | if releaseData.name == rs.ReleaseName { 75 | exists = true 76 | break 77 | } 78 | } 79 | if exists { 80 | continue 81 | } 82 | 83 | var releaseSpec ReleaseSpec 84 | releaseSpec.ReleaseName = releaseData.name 85 | releaseSpec.ChartName = releaseData.chart 86 | releaseSpec.ChartVersion = releaseData.version 87 | 88 | releaseSpecs = append(releaseSpecs, releaseSpec) 89 | } 90 | 91 | return releaseSpecs, nil 92 | } 93 | 94 | func getTillerStorage(kubeContext, tillerNamespace string) (string, error) { 95 | clientset, err := getClientSet(kubeContext) 96 | if err != nil { 97 | return "", err 98 | } 99 | coreV1 := clientset.CoreV1() 100 | listOptions := metav1.ListOptions{ 101 | LabelSelector: "name=tiller", 102 | } 103 | pods, err := coreV1.Pods(tillerNamespace).List(listOptions) 104 | if err != nil { 105 | return "", err 106 | } 107 | 108 | if len(pods.Items) == 0 { 109 | return "", fmt.Errorf("Found 0 tiller pods") 110 | } 111 | 112 | storage := "cfgmaps" 113 | for _, c := range pods.Items[0].Spec.Containers[0].Command { 114 | if strings.Contains(c, "secret") { 115 | storage = "secrets" 116 | } 117 | } 118 | 119 | return storage, nil 120 | } 121 | 122 | type releaseData struct { 123 | name string 124 | revision int32 125 | updated string 126 | status string 127 | chart string 128 | version string 129 | namespace string 130 | time time.Time 131 | } 132 | 133 | func listReleases(kubeContext, namespace, storage, tillerNamespace, labels string) ([]releaseData, error) { 134 | clientset, err := getClientSet(kubeContext) 135 | if err != nil { 136 | return nil, err 137 | } 138 | var releasesData []releaseData 139 | coreV1 := clientset.CoreV1() 140 | switch storage { 141 | case "secrets": 142 | secrets, err := coreV1.Secrets(tillerNamespace).List(metav1.ListOptions{ 143 | LabelSelector: labels, 144 | }) 145 | if err != nil { 146 | return nil, err 147 | } 148 | for _, item := range secrets.Items { 149 | releaseData := getReleaseData(namespace, (string)(item.Data["release"])) 150 | if releaseData == nil { 151 | continue 152 | } 153 | releasesData = append(releasesData, *releaseData) 154 | } 155 | case "cfgmaps": 156 | configMaps, err := coreV1.ConfigMaps(tillerNamespace).List(metav1.ListOptions{ 157 | LabelSelector: labels, 158 | }) 159 | if err != nil { 160 | return nil, err 161 | } 162 | for _, item := range configMaps.Items { 163 | releaseData := getReleaseData(namespace, item.Data["release"]) 164 | if releaseData == nil { 165 | continue 166 | } 167 | releasesData = append(releasesData, *releaseData) 168 | } 169 | } 170 | 171 | sort.Slice(releasesData[:], func(i, j int) bool { 172 | return strings.Compare(releasesData[i].chart, releasesData[j].chart) <= 0 173 | }) 174 | 175 | return releasesData, nil 176 | } 177 | 178 | func getReleaseData(namespace, itemReleaseData string) *releaseData { 179 | 180 | data, err := decodeRelease(itemReleaseData) 181 | 182 | if err != nil { 183 | return nil 184 | } 185 | 186 | if namespace != "" && data.Namespace != namespace { 187 | return nil 188 | } 189 | 190 | deployTime := time.Unix(data.Info.LastDeployed.Seconds, 0) 191 | chartMeta := data.GetChart().Metadata 192 | releaseData := releaseData{ 193 | time: deployTime, 194 | name: data.Name, 195 | revision: data.Version, 196 | updated: deployTime.Format("Mon Jan _2 15:04:05 2006"), 197 | status: data.GetInfo().Status.Code.String(), 198 | chart: chartMeta.Name, 199 | version: chartMeta.Version, 200 | namespace: data.Namespace, 201 | } 202 | return &releaseData 203 | } 204 | 205 | // GetClientToK8s returns a k8s ClientSet 206 | func GetClientToK8s() (*kubernetes.Clientset, error) { 207 | var kubeconfig string 208 | if kubeConfigPath := os.Getenv("KUBECONFIG"); kubeConfigPath != "" { 209 | kubeconfig = kubeConfigPath // CI process 210 | } else { 211 | kubeconfig = filepath.Join(os.Getenv("HOME"), ".kube", "config") // Development environment 212 | } 213 | 214 | var config *rest.Config 215 | 216 | _, err := os.Stat(kubeconfig) 217 | if err != nil { 218 | // In cluster configuration 219 | config, err = rest.InClusterConfig() 220 | if err != nil { 221 | return nil, err 222 | } 223 | } else { 224 | // Out of cluster configuration 225 | config, err = clientcmd.BuildConfigFromFlags("", kubeconfig) 226 | if err != nil { 227 | return nil, err 228 | } 229 | } 230 | 231 | clientset, err := kubernetes.NewForConfig(config) 232 | if err != nil { 233 | return nil, err 234 | } 235 | 236 | return clientset, nil 237 | } 238 | 239 | var b64 = base64.StdEncoding 240 | var magicGzip = []byte{0x1f, 0x8b, 0x08} 241 | 242 | func decodeRelease(data string) (*rspb.Release, error) { 243 | // base64 decode string 244 | b, err := b64.DecodeString(data) 245 | if err != nil { 246 | return nil, err 247 | } 248 | 249 | // For backwards compatibility with releases that were stored before 250 | // compression was introduced we skip decompression if the 251 | // gzip magic header is not found 252 | if bytes.Equal(b[0:3], magicGzip) { 253 | r, err := gzip.NewReader(bytes.NewReader(b)) 254 | if err != nil { 255 | return nil, err 256 | } 257 | b2, err := ioutil.ReadAll(r) 258 | if err != nil { 259 | return nil, err 260 | } 261 | b = b2 262 | } 263 | 264 | var rls rspb.Release 265 | // unmarshal protobuf bytes 266 | if err := proto.Unmarshal(b, &rls); err != nil { 267 | return nil, err 268 | } 269 | return &rls, nil 270 | } 271 | -------------------------------------------------------------------------------- /pkg/utils/http.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "io" 5 | "io/ioutil" 6 | "log" 7 | "net/http" 8 | ) 9 | 10 | // PerformRequestOptions are options passed to PerformRequest 11 | type PerformRequestOptions struct { 12 | Method string 13 | URL string 14 | Headers []string 15 | ExpectedStatusCode int 16 | Data io.Reader 17 | } 18 | 19 | // PerformRequest performs an HTTP request to a given url with an expected status code (to support testing) and returns the body 20 | func PerformRequest(o PerformRequestOptions) []byte { 21 | req, err := http.NewRequest(o.Method, o.URL, o.Data) 22 | if err != nil { 23 | log.Fatal("NewRequest: ", err) 24 | } 25 | for _, header := range o.Headers { 26 | header, value := SplitInTwo(header, ":") 27 | req.Header.Add(header, value) 28 | } 29 | client := &http.Client{} 30 | res, err := client.Do(req) 31 | if err != nil { 32 | log.Fatal("Do: ", err) 33 | } 34 | defer res.Body.Close() 35 | 36 | body, err := ioutil.ReadAll(res.Body) 37 | if err != nil { 38 | log.Fatalln("ReadAll: ", err) 39 | } 40 | if res.StatusCode != o.ExpectedStatusCode { 41 | log.Fatal(string(body)) 42 | } 43 | 44 | return body 45 | } 46 | -------------------------------------------------------------------------------- /pkg/utils/kube.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "path/filepath" 8 | 9 | v1 "k8s.io/api/core/v1" 10 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 11 | "k8s.io/client-go/kubernetes" 12 | _ "k8s.io/client-go/plugin/pkg/client/auth" //for gcp auth 13 | "k8s.io/client-go/rest" 14 | "k8s.io/client-go/tools/clientcmd" 15 | ) 16 | 17 | // CreateNamespace creates a namespace 18 | func CreateNamespace(name, kubeContext string, print bool) error { 19 | clientset, err := getClientSet(kubeContext) 20 | if err != nil { 21 | return err 22 | } 23 | 24 | nsSpec := &v1.Namespace{ObjectMeta: metav1.ObjectMeta{ 25 | Name: name, 26 | }} 27 | _, err = clientset.CoreV1().Namespaces().Create(nsSpec) 28 | if err != nil { 29 | return err 30 | } 31 | if print { 32 | log.Printf("created namespace \"%s\"", name) 33 | } 34 | return nil 35 | } 36 | 37 | // GetNamespace gets a namespace 38 | func GetNamespace(name, kubeContext string) (*v1.Namespace, error) { 39 | clientset, err := getClientSet(kubeContext) 40 | if err != nil { 41 | return nil, err 42 | } 43 | getOptions := metav1.GetOptions{} 44 | nsSpec, err := clientset.CoreV1().Namespaces().Get(name, getOptions) 45 | if err != nil { 46 | return nil, err 47 | } 48 | return nsSpec, nil 49 | } 50 | 51 | // UpdateNamespace updates a namespace 52 | func UpdateNamespace(name, kubeContext string, annotationsToUpdate, labelsToUpdate map[string]string, print bool) error { 53 | if len(annotationsToUpdate) == 0 && len(labelsToUpdate) == 0 { 54 | return nil 55 | } 56 | 57 | clientset, err := getClientSet(kubeContext) 58 | if err != nil { 59 | return err 60 | } 61 | 62 | ns, err := GetNamespace(name, kubeContext) 63 | if err != nil { 64 | return err 65 | } 66 | annotations := overrideAttributes(ns.Annotations, annotationsToUpdate) 67 | labels := overrideAttributes(ns.Labels, labelsToUpdate) 68 | nsSpec := &v1.Namespace{ObjectMeta: metav1.ObjectMeta{ 69 | Name: name, 70 | Annotations: annotations, 71 | Labels: labels, 72 | }} 73 | _, err = clientset.CoreV1().Namespaces().Update(nsSpec) 74 | if err != nil { 75 | return err 76 | } 77 | if print { 78 | if len(annotationsToUpdate) != 0 { 79 | log.Printf("updated environment \"%s\" with annotations (%s)", name, MapToString(annotations)) 80 | } 81 | if len(labelsToUpdate) != 0 { 82 | log.Printf("updated environment \"%s\" with labels (%s)", name, MapToString(labels)) 83 | } 84 | } 85 | return nil 86 | } 87 | 88 | func overrideAttributes(currentAttributes, attributesToUpdate map[string]string) map[string]string { 89 | attributes := currentAttributes 90 | if len(attributes) == 0 { 91 | attributes = attributesToUpdate 92 | } else { 93 | for k, v := range attributesToUpdate { 94 | attributes[k] = v 95 | } 96 | } 97 | return attributes 98 | } 99 | 100 | // DeleteNamespace deletes a namespace 101 | func DeleteNamespace(name, kubeContext string, print bool) error { 102 | clientset, err := getClientSet(kubeContext) 103 | if err != nil { 104 | return err 105 | } 106 | deleteOptions := &metav1.DeleteOptions{} 107 | err = clientset.CoreV1().Namespaces().Delete(name, deleteOptions) 108 | if err != nil { 109 | return err 110 | } 111 | if print { 112 | log.Printf("deleted namespace \"%s\"", name) 113 | } 114 | return nil 115 | } 116 | 117 | // NamespaceExists returns true if the namespace exists 118 | func NamespaceExists(name, kubeContext string) (bool, error) { 119 | clientset, err := getClientSet(kubeContext) 120 | if err != nil { 121 | return false, err 122 | } 123 | 124 | listOptions := metav1.ListOptions{} 125 | namespaces, err := clientset.CoreV1().Namespaces().List(listOptions) 126 | if err != nil { 127 | return false, err 128 | } 129 | for _, ns := range namespaces.Items { 130 | if ns.Name == name { 131 | nsStatus := ns.Status.Phase 132 | if nsStatus != "Active" { 133 | return false, fmt.Errorf("Environment status is %s", nsStatus) 134 | } 135 | return true, nil 136 | } 137 | } 138 | return false, nil 139 | } 140 | 141 | // GetPods returns a pods list 142 | func getPods(namespace, kubeContext string) (*v1.PodList, error) { 143 | 144 | clientset, err := getClientSet(kubeContext) 145 | if err != nil { 146 | return nil, err 147 | } 148 | pods, err := clientset.CoreV1().Pods(namespace).List(metav1.ListOptions{}) 149 | if err != nil { 150 | return nil, err 151 | } 152 | 153 | return pods, nil 154 | } 155 | 156 | // getEndpoints returns an endpoints list 157 | func getEndpoints(namespace, kubeContext string) (*v1.EndpointsList, error) { 158 | 159 | clientset, err := getClientSet(kubeContext) 160 | if err != nil { 161 | return nil, err 162 | } 163 | endpoints, err := clientset.CoreV1().Endpoints(namespace).List(metav1.ListOptions{}) 164 | if err != nil { 165 | return nil, err 166 | } 167 | 168 | return endpoints, nil 169 | } 170 | 171 | func buildConfigFromFlags(context, kubeconfigPath string) (*rest.Config, error) { 172 | return clientcmd.NewNonInteractiveDeferredLoadingClientConfig( 173 | &clientcmd.ClientConfigLoadingRules{ExplicitPath: kubeconfigPath}, 174 | &clientcmd.ConfigOverrides{ 175 | CurrentContext: context, 176 | }).ClientConfig() 177 | } 178 | 179 | func getClientSet(kubeContext string) (*kubernetes.Clientset, error) { 180 | var kubeconfig string 181 | if kubeConfigPath := os.Getenv("KUBECONFIG"); kubeConfigPath != "" { 182 | kubeconfig = kubeConfigPath 183 | } else { 184 | kubeconfig = filepath.Join(os.Getenv("HOME"), ".kube", "config") 185 | } 186 | 187 | config, err := buildConfigFromFlags(kubeContext, kubeconfig) 188 | if err != nil { 189 | return nil, err 190 | } 191 | 192 | clientset, err := kubernetes.NewForConfig(config) 193 | if err != nil { 194 | return nil, err 195 | } 196 | 197 | return clientset, nil 198 | } 199 | -------------------------------------------------------------------------------- /pkg/utils/testdata/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | appVersion: "1.0" 3 | description: A Helm chart for Kubernetes 4 | icon: https://github.com/kubernetes/kubernetes/blob/master/logo/logo.png 5 | name: example-chart 6 | version: 0.1.1 7 | -------------------------------------------------------------------------------- /pkg/utils/testdata/charts.yaml: -------------------------------------------------------------------------------- 1 | charts: 2 | - name: cassandra 3 | version: 0.4.0 4 | - name: mariadb 5 | version: 0.5.4 6 | - name: kaa 7 | version: 0.1.7 8 | depends_on: 9 | - cassandra 10 | - mariadb 11 | -------------------------------------------------------------------------------- /pkg/utils/testdata/circular.yaml: -------------------------------------------------------------------------------- 1 | charts: 2 | - name: cassandra 3 | version: 0.4.0-5215 4 | depends_on: 5 | - mariadb 6 | - name: mariadb 7 | version: 0.5.4-5317 8 | depends_on: 9 | - kaa 10 | - name: kaa 11 | version: 0.1.7-6190 12 | depends_on: 13 | - cassandra 14 | -------------------------------------------------------------------------------- /pkg/utils/validate.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "log" 5 | "time" 6 | ) 7 | 8 | // IsEnvValidWithLoopBackOff validates the state of a namespace with back off loop 9 | func IsEnvValidWithLoopBackOff(name, kubeContext string) (bool, error) { 10 | log.Printf("validating environment \"%s\"", name) 11 | envValid := false 12 | maxAttempts := 30 13 | for i := 1; i <= maxAttempts; i++ { 14 | envValid, err := IsEnvValid(name, kubeContext) 15 | if err != nil { 16 | return envValid, err 17 | } 18 | if envValid { 19 | return envValid, nil 20 | } 21 | if i < maxAttempts { 22 | log.Printf("environment \"%s\" validation failed, will retry in 30 seconds (attempt %d/%d)", name, i, maxAttempts) 23 | time.Sleep(30 * time.Second) 24 | } 25 | } 26 | return envValid, nil 27 | } 28 | 29 | // IsEnvValid validates the state of a namespace 30 | func IsEnvValid(name, kubeContext string) (bool, error) { 31 | envValid := true 32 | envValid, err := validatePods(name, kubeContext, envValid) 33 | if err != nil { 34 | return envValid, err 35 | } 36 | 37 | envValid, err = validateEndpoints(name, kubeContext, envValid) 38 | if err != nil { 39 | return envValid, err 40 | } 41 | 42 | return envValid, nil 43 | } 44 | 45 | func validatePods(name, kubeContext string, envValid bool) (bool, error) { 46 | log.Println("validating pods") 47 | pods, err := getPods(name, kubeContext) 48 | if err != nil { 49 | return envValid, err 50 | } 51 | 52 | log.Println("validating that all pods are in a valid phase") 53 | for _, pod := range pods.Items { 54 | phase := pod.Status.Phase 55 | if phase == "Running" { 56 | continue 57 | } 58 | if phase == "Succeeded" { 59 | continue 60 | } 61 | log.Printf("pod %s is in phase \"%s\"", pod.Name, phase) 62 | envValid = false 63 | } 64 | 65 | log.Println("validating that all containers are ready") 66 | for _, pod := range pods.Items { 67 | statuses := pod.Status.ContainerStatuses 68 | for _, status := range statuses { 69 | if status.Ready { 70 | continue 71 | } 72 | if pod.OwnerReferences[0].Kind == "Job" { 73 | continue 74 | } 75 | log.Printf("container %s/%s is not in \"Ready\" status", pod.Name, status.Name) 76 | envValid = false 77 | } 78 | } 79 | 80 | return envValid, nil 81 | } 82 | 83 | func validateEndpoints(name, kubeContext string, envValid bool) (bool, error) { 84 | log.Println("validating endpoints") 85 | endpoints, err := getEndpoints(name, kubeContext) 86 | if err != nil { 87 | return envValid, err 88 | } 89 | 90 | log.Println("validating that all endpoints have addresses") 91 | for _, ep := range endpoints.Items { 92 | subsets := ep.Subsets 93 | addresses := 0 94 | for _, subset := range subsets { 95 | addresses += len(subset.Addresses) 96 | } 97 | if addresses != 0 { 98 | continue 99 | } 100 | log.Printf("endpoint %s has no addresses", ep.Name) 101 | envValid = false 102 | } 103 | 104 | return envValid, nil 105 | } 106 | -------------------------------------------------------------------------------- /test/chart_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/nuvo/orca/pkg/utils" 7 | ) 8 | 9 | func TestOverrideReleases_WithOverride(t *testing.T) { 10 | rel0 := utils.ReleaseSpec{ChartName: "cassandra", ChartVersion: "0.4.0", ReleaseName: "test-cassandra"} 11 | rel1 := utils.ReleaseSpec{ChartName: "mariadb", ChartVersion: "0.5.4", ReleaseName: "test-mariadb"} 12 | rel2 := utils.ReleaseSpec{ChartName: "kaa", ChartVersion: "0.1.7", ReleaseName: "test-kaa"} 13 | rel2override := utils.ReleaseSpec{ChartName: "kaa", ChartVersion: "7.1.0", ReleaseName: "test-kaa"} 14 | 15 | releases := []utils.ReleaseSpec{rel0, rel1, rel2} 16 | 17 | overrideReleases := utils.OverrideReleases(releases, []string{"kaa=7.1.0"}, "test") 18 | 19 | if !overrideReleases[2].Equals(rel2override) { 20 | t.Errorf("Expected: true, Actual: false") 21 | } 22 | } 23 | 24 | func TestOverrideReleases_WithNewOverride(t *testing.T) { 25 | rel0 := utils.ReleaseSpec{ChartName: "cassandra", ChartVersion: "0.4.0", ReleaseName: "test-cassandra"} 26 | rel1 := utils.ReleaseSpec{ChartName: "mariadb", ChartVersion: "0.5.4", ReleaseName: "test-mariadb"} 27 | rel2 := utils.ReleaseSpec{ChartName: "kaa", ChartVersion: "0.1.7", ReleaseName: "test-kaa"} 28 | rel2override := utils.ReleaseSpec{ChartName: "example", ChartVersion: "3.3.3", ReleaseName: "test-example"} 29 | 30 | releases := []utils.ReleaseSpec{rel0, rel1, rel2} 31 | 32 | overrideReleases := utils.OverrideReleases(releases, []string{"example=3.3.3"}, "test") 33 | 34 | if !overrideReleases[3].Equals(rel2override) { 35 | t.Errorf("Expected: true, Actual: false") 36 | } 37 | } 38 | 39 | func TestOverrideReleases_WithoutOverride(t *testing.T) { 40 | rel0 := utils.ReleaseSpec{ChartName: "cassandra", ChartVersion: "0.4.0", ReleaseName: "test-cassandra"} 41 | rel1 := utils.ReleaseSpec{ChartName: "mariadb", ChartVersion: "0.5.4", ReleaseName: "test-mariadb"} 42 | rel2 := utils.ReleaseSpec{ChartName: "kaa", ChartVersion: "0.1.7", ReleaseName: "test-kaa"} 43 | 44 | releases := []utils.ReleaseSpec{rel0, rel1, rel2} 45 | 46 | overrideReleases := utils.OverrideReleases(releases, []string{}, "test") 47 | 48 | if !overrideReleases[0].Equals(rel0) { 49 | t.Errorf("Expected: true, Actual: false") 50 | } 51 | if !overrideReleases[1].Equals(rel1) { 52 | t.Errorf("Expected: true, Actual: false") 53 | } 54 | if !overrideReleases[2].Equals(rel2) { 55 | t.Errorf("Expected: true, Actual: false") 56 | } 57 | } 58 | func TestRemoveChartFromDependencies(t *testing.T) { 59 | file := "data/charts.yaml" 60 | releases := utils.InitReleasesFromChartsFile(file, "test") 61 | releases = utils.RemoveChartFromDependencies(releases, "mariadb") 62 | 63 | if len(releases[2].Dependencies) != 1 { 64 | t.Errorf("Expected: 1, Actual: " + (string)(len(releases))) 65 | } 66 | if releases[2].Dependencies[0] != "cassandra" { 67 | t.Errorf("Expected: cassandra, Actual: " + releases[2].Dependencies[0]) 68 | } 69 | } 70 | 71 | func TestRemoveChartFromCharts(t *testing.T) { 72 | rel1 := utils.ReleaseSpec{ChartName: "mariadb", ChartVersion: "0.5.4", ReleaseName: "test-mariadb"} 73 | rel0 := utils.ReleaseSpec{ChartName: "kaa", ChartVersion: "0.1.7", ReleaseName: "test-kaa"} 74 | file := "data/charts.yaml" 75 | releases := utils.InitReleasesFromChartsFile(file, "test") 76 | index := utils.GetChartIndex(releases, "cassandra") 77 | releases = utils.RemoveChartFromCharts(releases, index) 78 | 79 | if len(releases) != 2 { 80 | t.Errorf("Expected: 2, Actual: " + (string)(len(releases))) 81 | } 82 | if !releases[0].Equals(rel0) { 83 | t.Errorf("Expected: true, Actual: false") 84 | } 85 | if !releases[1].Equals(rel1) { 86 | t.Errorf("Expected: true, Actual: false") 87 | } 88 | } 89 | func TestUpdateChartVersion(t *testing.T) { 90 | newVersion := utils.UpdateChartVersion("data/", "1234") 91 | 92 | if newVersion != "0.1.1-1234" { 93 | t.Errorf("Expected: 0.1.1-1234, Actual: " + newVersion) 94 | } 95 | 96 | utils.ResetChartVersion("data/", "0.1.1") 97 | } 98 | -------------------------------------------------------------------------------- /test/data/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | appVersion: "1.0" 3 | description: A Helm chart for Kubernetes 4 | icon: https://github.com/kubernetes/kubernetes/blob/master/logo/logo.png 5 | name: example-chart 6 | version: 0.1.1 7 | -------------------------------------------------------------------------------- /test/data/charts.yaml: -------------------------------------------------------------------------------- 1 | charts: 2 | - name: cassandra 3 | version: 0.4.0 4 | - name: mariadb 5 | version: 0.5.4 6 | - name: kaa 7 | version: 0.1.7 8 | depends_on: 9 | - cassandra 10 | - mariadb 11 | -------------------------------------------------------------------------------- /test/data/circular.yaml: -------------------------------------------------------------------------------- 1 | charts: 2 | - name: cassandra 3 | version: 0.4.0-5215 4 | depends_on: 5 | - mariadb 6 | - name: mariadb 7 | version: 0.5.4-5317 8 | depends_on: 9 | - kaa 10 | - name: kaa 11 | version: 0.1.7-6190 12 | depends_on: 13 | - cassandra 14 | --------------------------------------------------------------------------------