├── .gitattributes ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── Dockerfile.dev ├── Gopkg.lock ├── Gopkg.toml ├── LICENSE.md ├── Makefile ├── README.md ├── _examples ├── availability-policy.yaml ├── config-policy.yaml ├── github-repository.yaml ├── health-policy.yaml ├── hello-hlnr │ ├── Dockerfile │ ├── README.md │ ├── github-repository.yaml │ ├── hello-hlnr-preview.yaml │ ├── hello-hlnr-production.yaml │ ├── hello-hlnr-stage.yaml │ └── hello-hlnr.go ├── image-policy-match.yaml ├── image-policy.yaml ├── microservice.yaml └── network-policy.yaml ├── apis └── v1alpha1 │ ├── availability.go │ ├── config_policy.go │ ├── doc.go │ ├── github_repository.go │ ├── health_policy.go │ ├── image_policy.go │ ├── image_policy_test.go │ ├── microservice.go │ ├── network_policy.go │ ├── release.go │ ├── release_test.go │ ├── scheme.go │ ├── security_policy.go │ ├── versioned_microservice.go │ ├── versioning_policy.go │ └── zz_generated.go ├── boilerplate.go.txt ├── cmd └── heighliner │ ├── config_policy.go │ ├── github_repository.go │ ├── image_policy.go │ ├── install.go │ ├── main.go │ ├── network_policy.go │ ├── svc.go │ ├── vsvc.go │ └── zz_generated_data.go ├── docs ├── design │ ├── README.md │ ├── availability-policy.md │ ├── config-policy.md │ ├── full-flow.png │ ├── github-connector.md │ ├── github-tokens.png │ ├── health-policy.md │ ├── image-policy.md │ ├── microservice.md │ ├── network-policy.md │ ├── overview.png │ ├── security-policy.md │ ├── versioned-microservice.md │ └── versioning-policy.md ├── installation.md └── kube │ ├── 00-heighliner-namespace.yaml │ ├── config-policy.yaml │ ├── github-policy.yaml │ ├── image-policy.yaml │ ├── microservice.yaml │ ├── network-policy.yaml │ └── versioned-microservice.yaml ├── go.mod ├── go.sum └── internal ├── configpolicy ├── configpolicy.go ├── controller.go ├── hashed.go └── hashed_test.go ├── githubrepository ├── callbacks.go ├── callbacks_test.go ├── config.go ├── controller.go ├── controller_test.go ├── github_repository.go ├── reconciliation.go └── reconciliation_test.go ├── imagepolicy ├── controller.go ├── controller_test.go └── image_policy.go ├── k8sutils ├── conversion.go ├── glog_silencer.go ├── hash.go ├── patch.go └── random.go ├── meta ├── definitions.go ├── meta.go └── meta_test.go ├── networkpolicy ├── controller.go ├── controller_test.go ├── ingress.go ├── ingress_test.go ├── network_policy.go ├── network_status.go ├── network_status_test.go ├── releaser.go ├── releaser_test.go ├── service.go └── service_test.go ├── registry ├── hub │ ├── docker_hub.go │ └── docker_hub_test.go ├── registry.go └── registry_test.go ├── svc ├── controller.go ├── controller_test.go └── svc.go ├── tester └── kubekit.go └── vsvc ├── affinity.go ├── controller.go ├── deployment.go ├── disruption.go ├── disruption_test.go ├── vsvc.go └── vsvc_test.go /.gitattributes: -------------------------------------------------------------------------------- 1 | *zz_generated* linguist-generated=true 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | /bin/ 3 | /build/ 4 | coverage.txt 5 | 6 | cover.out 7 | cover.html 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | services: 3 | - docker 4 | go: 5 | - 1.10.x 6 | - 1.11.x 7 | before_install: 8 | - curl -o- https://raw.githubusercontent.com/manifoldco/manifold-cli/master/install.sh 9 | | bash 10 | - export PATH=$PATH:$HOME/.manifold/bin/ 11 | install: make bootstrap 12 | cache: 13 | - directories: 14 | - "$GOPATH/bin" 15 | - "$GOPATH/pkg" 16 | branches: 17 | only: 18 | - master 19 | - "/^v([0-9]+)\\.([0-9]+)\\.([0-9]+)(?:\\-(rc\\.[0-9]+)*)?$/" 20 | script: 21 | - make ci 22 | after_success: 23 | - bash <(curl -s https://codecov.io/bash) 24 | deploy: 25 | - provider: script 26 | script: manifold run -- make release 27 | skip_cleanup: true 28 | on: 29 | branch: master 30 | repo: manifoldco/heighliner 31 | - provider: script 32 | script: manifold run -- make release 33 | skip_cleanup: true 34 | on: 35 | tags: true 36 | repo: manifoldco/heighliner 37 | env: 38 | global: 39 | - MANIFOLD_TEAM=manifold 40 | - MANIFOLD_PROJECT=docker-arigatoautomated 41 | - secure: llLkNlJ6eDLVKqo6SLf4pDbR58C3Nx5qQ6Xxlk9H/yV48UOM8twHtDKWXjIZuVKi/95mFw4TqJ7uv3k4QM9SOiGJxEvJ4rPs09cdTznxj1zsnmRTcBIWLXmCb3zySaUTiXQJ1IglZN+KzTPXqMdsDNHTuNNYFTdtGzHLyPnhQ669eAQgSDpVh5GntBjwcl7FUbYBcIoAtPUvnXkmSeXrEqanvCwfHmXoectc9EfEPLidB2FkgoMLpfgRL6fva0BHIy0DSUqWjy0JtGeivuslRJAIQCjiBuPjmNP+ObXM0+sSqe4oNhHBTz07+PneJDu00ScTpbe7++jurH5gCCu0qdXgjSUVc0E/aiws4w2ZlS3E3JwcoFqzwIK+2OY5bQhadO677PJoSt78fPLANvEuvDOC38gAdk51BtBhWuOEIBn6cvNUc62kyYKPVTLiWWbiBm2OjwAnBNrJ0xxPmveOzEKsiHOojyVjEwLJiX25jUsO99IAmaHUeg1GaopYLB0ilvJHJroKykC+P/VjJyez4SK37gu75UIYPIHkLPenckF/8pGBumT2eBOIno06sq0qMfU7qtU6xvgbR02ISjJtKTUwerlSo7hEomT9StMntvTzaVZQH5UsXYrHPR0M1VP9ZU1l0EGm3opo7kjArZ+Zfoodq09MRQB+OcxAnzYToEE= 42 | - secure: gaPv+WKOEAGbXpNC8yFR4dMZ4Ft3+MkiEFtiDtGPD9YguALLhk2tFILb0SI8zrd/GvjaqJtRSpbC7AmDunE9tzU0zBIfoIXO4A+skUgFN1LX8rVUG1CLtVeuo+LfgwipjOF1yJrkyXa+thGY718pay5nTaZ1vo56CU8gF6hczWxyro3bxRfrpxCEDeabL2865sbggNtCNL5iGj8zn9Ay9vdSCJnZLRYKCswcB7/hXWFKc9tIT26AOD3kiwkPK9aDbuwsxGb1e9fLrBtXgJS+tLg5pPEOXyONqrY38fbpVhPXZsy8SBp5gNs4pdNE+AJFGnzct/z0VSXii4fRb4CXnzSRDTbYdHhTPFR4WHOwiQ/xS5M4CL50lHrVWwOH4I+9C4nTAJgRq7NvIZhtxgl1VbOaS5U171anHCxPwgjvEzAmBl52OGPkG3V7bOUqKh2Pl4L3d+TGGLUpKFAIlFscySuxSGsMCIiGhGnPY+FCNZ26O3/8mWPoj6oAxh3Y2xzNqyKBKQN4CKV5jo/h31M44EgAScj7zVupbuUAukGCybMrfEYO+3P6wtucATTSCcjWGfqyGYXn1Zyyp5KIW+PVk78UawCRH/6sPOdaunqLBTMI80RKm+JmJN7OHQaoXIiUFSmPMZcLx6cmlx99sGmk3IvK5/4xVy0U7dtT2deLOFY= 43 | - secure: Q83o72JlWz9bmp3xvciEaxFLkEwteswxZSGmIrFoOYHAVxSDS2snsfETTrJZNvGP2N0wlpbpFiYa5Vw2vSaQXY24wOSC7sNWhijGsLH6DXILa/4HbK6s+O6nUfrooHFMhFwa0XIMHcj02/R9XmKdbuoWgJExZzeIF3RgFS+rpaBN/22QzSYCZk8D5CldPm/d2s9Dv4Q/aJQ783Q0yWPQHfGV8lUrC4SmuOnfNKFoBQ5dco9jBJtcty8kXgSUZqwq346QxfwAKhrmBOwJxmX30hk68KaZkEr87ug4bQr/mXDqo2JhzAmsq2Jf3wM1BjetG+NIfzQskA0Q5MjuhQnMu7prdxAsKQyz2f1SeOb5imrSzkMmitpJvZQe4oiiEoHV8Fdepk06LQDgNgqekd+q7MAtEuXix3IpPhVPSLhiVp5RxDOAB4AVa7IGzbBYklWxshUjz+ehIZj5nnuPA2ctk8SvnOZqKKcQbigwtbmG+oiO5fiTRNZ1XVd3JZbPo6+ZvjQIjG8rVBk9nNSyvAxpIGiZZOHGtM87csiC+wATr7lHp7L6Vx3Op+DdcNu/4K6nWCP+KRpPJWbTwXjwmsTu5LUVN+AEVwVrn11x4vvitQpPoGRl9fCU1k1Bk1wB4Mmo4/vwlY74KZX6Ueu7QlQKY+eZkO1h26aivjdZCDKKq0o= 44 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](http://keepachangelog.com/) 6 | and this project adheres to [Semantic Versioning](http://semver.org/). 7 | 8 | ## Unreleased 9 | 10 | ### BREAKING CHANGES 11 | 12 | - `ImagePolicy.Spec.ImagePullSecrets` is now under 13 | `ImagePolicy.Spec.ContainerRegistry.ImagePullSecrets`. 14 | 15 | Example Before: 16 | 17 | ``` 18 | apiVersion: hlnr.io/v1alpha1 19 | kind: ImagePolicy 20 | spec: 21 | imagePullSecrets: 22 | - name: docker-registry 23 | ``` 24 | 25 | After: 26 | 27 | ``` 28 | apiVersion: hlnr.io/v1alpha1 29 | kind: ImagePolicy 30 | spec: 31 | containerRegistry: 32 | name: docker 33 | imagePullSecrets: 34 | - name: docker-registry 35 | ``` 36 | 37 | ### Added 38 | 39 | - Added a health check for the GitHub Callback Server. 40 | - Added logging to indicate GH Callback Server requests. 41 | - Added `ImagePolicy.Spec.ContainerRegistry` to specify which container registry 42 | to pull the image from. 43 | - Added GitHub reconciliation period. [Read More](docs/design/github-connector.md) 44 | 45 | ### Fixed 46 | 47 | - Fixed the Makefile target for generating files. 48 | - Fixed a bug where the OwnerReference on a Ingress for the Service pointed to the wrong APIGroup. 49 | 50 | ## [0.1.2] - 2018-07-16 51 | 52 | ### Fixed 53 | - Fix image mapping by name only. 54 | - Relax ImagePolicy CRD validation so it can be installed. 55 | - Eliminate registry ping log message 56 | 57 | ## [0.1.1] - 2018-07-16 58 | 59 | ### Added 60 | - Introduce an optional `match` field for ImagePolicy to control how releases 61 | map to container images, based on container image tag name, labels, or both. 62 | 63 | ## [0.1.0] - 2018-07-13 64 | 65 | Docker Image: [arigato/heighliner:0.1.0](https://hub.docker.com/r/arigato/heighliner/tags) 66 | 67 | Initial release 68 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behaviour that contributes to creating a positive environment include: 10 | 11 | Using welcoming and inclusive language 12 | 13 | Being respectful of differing viewpoints and experiences 14 | 15 | Gracefully accepting constructive criticism 16 | 17 | Focusing on what is best for the community 18 | 19 | Showing empathy towards other community members 20 | 21 | Examples of unacceptable behaviour by participants include: 22 | 23 | The use of sexualized language or imagery and unwelcome sexual attention or advances 24 | 25 | Trolling, insulting/derogatory comments, and personal or political attacks 26 | 27 | Public or private harassment 28 | 29 | Publishing others' private information, such as a physical or electronic address, without explicit permission 30 | 31 | Other conduct which could reasonably be considered inappropriate in a professional setting 32 | 33 | ## Our Responsibilities 34 | 35 | Project maintainers are responsible for clarifying the standards of acceptable behaviour and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behaviour. 36 | 37 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviours that they deem inappropriate, threatening, offensive, or harmful. 38 | 39 | ## Scope 40 | 41 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include 42 | 43 | using an official project e-mail address, 44 | 45 | posting via an official social media account or acting as an appointed representative at an online or offline event 46 | 47 | Representation of a project may be further defined and clarified by project maintainers. 48 | 49 | ## Enforcement 50 | 51 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at [hello@manifold.co](mailto:hello@manifold.co). All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 52 | 53 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 54 | 55 | ## Attribution 56 | 57 | This Code of Conduct is adapted from the Contributor Covenant, version 1.4, available at [http://contributor-covenant.org/version/1/4](http://contributor-covenant.org/version/1/4). 58 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thanks for taking the time to join the community and helping out! These 4 | guidelines will help you get started with the Heighliner project. 5 | 6 | Please note that we have a [CLA sign off](https://cla-assistant.io/manifoldco/heighliner). 7 | 8 | ## Building from source 9 | 10 | ### Prerequisites 11 | 12 | 1. Install 1Go1 13 | 14 | Heighliner requires [Go 1.9][1] or later. 15 | 16 | 2. Install `dep` 17 | 18 | Heighliner uses [dep][1] for dependency management. 19 | 20 | ``` 21 | go get -u github.com/golang/dep/cmd/dep 22 | ``` 23 | 24 | ### Downloading the source 25 | 26 | To reduce the size of the repository, Heighliner does not include a copy of its 27 | dependencies. It uses [dep][2] to manage its dependencies. 28 | 29 | We might change this in the future, but for now, you can use the following 30 | commands to fetch the Heighliner source and its dependencies: 31 | 32 | ``` 33 | go get -d github.com/manifoldco/heighliner 34 | cd $GOPATH/src/github.com/manifoldco/heighliner 35 | make vendor 36 | ``` 37 | 38 | Go has strict rules when it comes to the location of the source code in your 39 | `$GOPATH`. The easiest way to develop is to rename the Heighliner git remote 40 | location and substitute your own fork for `origin`. We want to ensure that the 41 | repository remains at `$GOPATH/src/github.com/manifoldco/heighliner` on disk. 42 | 43 | ``` 44 | git remote rename origin upstream 45 | git remote add origin git@github.com:jelmersnoeck/heighliner.git 46 | ``` 47 | 48 | ### Building 49 | 50 | To build the binaries, run: 51 | 52 | ``` 53 | make bins 54 | ``` 55 | 56 | This will put all the binaries into the `./bins` folder. These binaries are 57 | compiled for your local machine. 58 | 59 | To compile a docker image to deploy in your local cluster, there are two 60 | options. The first options is to run 61 | 62 | ``` 63 | make docker-dev 64 | ``` 65 | 66 | This will generate the binary on the host - your machine - and put it in a 67 | Docker image. 68 | 69 | To create a more official image, you can run: 70 | 71 | ``` 72 | make docker 73 | ``` 74 | 75 | This will install all dependencies in the Docker image and build the container 76 | in that image. This means that all build artifacts are linked within the same 77 | Docker structure. 78 | 79 | ### Testing 80 | 81 | Once you have Heighliner built, you can run the tests: 82 | 83 | ``` 84 | make test 85 | ``` 86 | 87 | We also have a set of linters that we require, these can be run as follows: 88 | 89 | ``` 90 | make lint 91 | ``` 92 | 93 | [1]: https://golang.org 94 | [2]: https://github.com/golang/dep 95 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.11 2 | 3 | ARG BINARY 4 | 5 | WORKDIR /go/src/github.com/manifoldco/heighliner 6 | COPY . ./ 7 | 8 | RUN make bootstrap 9 | RUN make vendor 10 | RUN make $BINARY 11 | RUN cp $BINARY /controller 12 | 13 | FROM manifoldco/scratch-certificates 14 | USER 7171:8787 15 | 16 | COPY --from=0 /controller /controller 17 | ENTRYPOINT ["/controller"] 18 | -------------------------------------------------------------------------------- /Dockerfile.dev: -------------------------------------------------------------------------------- 1 | FROM manifoldco/scratch-certificates 2 | USER 7171:8787 3 | 4 | ARG BINARY 5 | 6 | COPY $BINARY ./controller 7 | ENTRYPOINT ["./controller"] 8 | -------------------------------------------------------------------------------- /Gopkg.toml: -------------------------------------------------------------------------------- 1 | required = [ 2 | "k8s.io/code-generator/cmd/deepcopy-gen", 3 | ] 4 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2018, Arigato Machine Inc. 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PKG=github.com/manifoldco/heighliner 2 | API_VERSIONS=$(sort $(patsubst apis/%/,%,$(dir $(wildcard apis/*/)))) 3 | 4 | ci: lint cover 5 | .PHONY: ci 6 | 7 | ################################################# 8 | # Bootstrapping for base golang package deps 9 | ################################################# 10 | BOOTSTRAP=\ 11 | github.com/golang/dep/cmd/dep \ 12 | github.com/alecthomas/gometalinter \ 13 | github.com/jteeuwen/go-bindata/go-bindata 14 | 15 | $(BOOTSTRAP): 16 | go get -u $@ 17 | 18 | bootstrap: $(BOOTSTRAP) 19 | gometalinter --install 20 | 21 | vendor: Gopkg.lock 22 | dep ensure -v -vendor-only 23 | 24 | update-vendor: 25 | dep ensure -v -update 26 | 27 | .PHONY: $(BOOTSTRAP) 28 | 29 | ################################################# 30 | # Testing and linting 31 | ################################################# 32 | LINTERS=\ 33 | gofmt \ 34 | golint \ 35 | gosimple \ 36 | vet \ 37 | misspell \ 38 | ineffassign \ 39 | deadcode 40 | METALINT=gometalinter --tests --disable-all --vendor --deadline=5m -e "zz_.*\.go" \ 41 | ./... --enable 42 | 43 | test: vendor 44 | CGO_ENABLED=0 go test -v ./... 45 | 46 | cover: vendor 47 | CGO_ENABLED=0 go test -v -coverprofile=coverage.txt -covermode=atomic ./... 48 | 49 | cover-html: vendor 50 | CGO_ENABLED=0 go test -coverprofile cover.out ./... 51 | go tool cover -html=cover.out -o cover.html 52 | open cover.html 53 | 54 | lint: $(LINTERS) 55 | 56 | $(LINTERS): vendor 57 | $(METALINT) $@ 58 | 59 | .PHONY: $(LINTERS) test lint 60 | 61 | ################################################# 62 | # Create generated files 63 | ################################################# 64 | GENERATED_FILES=$(API_VERSIONS:%=apis/%/zz_generated.go) 65 | 66 | deepcopy-gen: 67 | go get -u k8s.io/code-generator/cmd/deepcopy-gen 68 | 69 | api-versions: 70 | @echo $(API_VERSIONS) 71 | 72 | $(GENERATED_FILES): 73 | deepcopy-gen -v=5 -h boilerplate.go.txt -i $(PKG)/$(patsubst %/zz_generated.go,%,$@) -O zz_generated 74 | 75 | bindata: 76 | go-bindata -o cmd/heighliner/zz_generated_data.go docs/kube/ 77 | 78 | generated: $(GENERATED_FILES) bindata 79 | 80 | .PHONY: $(GENERATED_FILES) 81 | 82 | ################################################# 83 | # Building 84 | ################################################# 85 | BASE_BRANCH=master 86 | DOCKER_REPOSITORY=arigato 87 | GOOS_OVERRIDE?= 88 | PREFIX?= 89 | 90 | GO_BUILD=CGO_ENABLED=0 go build -i 91 | DOCKER_MAKE=GOOS_OVERRIDE='GOOS=linux' PREFIX=build/docker/$1/ make build/docker/$1/bin/$1 92 | 93 | CMDs=$(sort $(patsubst cmd/%/,%,$(dir $(wildcard cmd/*/)))) 94 | BINS=$(addprefix bin/,$(CMDs)) 95 | DOCKER_IMAGES=$(addprefix docker-,$(CMDs)) 96 | DOCKER_RELEASES=$(addprefix release-,$(CMDs)) 97 | 98 | VCS_SHA?=$(shell git rev-parse --verify HEAD) 99 | BUILD_DATE?=$(shell git show -s --date=iso8601-strict --pretty=format:%cd $$VCS_SHA) 100 | VCS_BRANCH?=$(shell git branch | grep \* | cut -f2 -d' ') 101 | 102 | 103 | RELEASE_VERSION?=$(shell git describe --always --tags --dirty | sed 's/^v//') 104 | ifdef TRAVIS_TAG 105 | RELEASE_VERSION=$(shell echo $(TRAVIS_TAG) | sed 's/^v//') 106 | endif 107 | 108 | 109 | RELEASE_NAME?=$(patsubst docker-%,%,$@) 110 | ifdef TRAVIS_PULL_REQUEST_BRANCH 111 | RELEASE_VERSION=$(TRAVIS_PULL_REQUEST_SHA) 112 | RELEASE_NAME="$(patsubst docker-%,%,$@)-$(shell echo $(TRAVIS_PULL_REQUEST_BRANCH) | sed "s/[^[:alnum:].-]/-/g")" 113 | # Override VCS_BRANCH on travis because it uses the FETCH_HEAD 114 | VCS_BRANCH=$(TRAVIS_PULL_REQUEST_BRANCH) 115 | endif 116 | 117 | $(CMDs:%=build/docker/%/Dockerfile): 118 | mkdir -p $(@D) 119 | cp Dockerfile.dev $@ 120 | 121 | $(BINS:%=$(PREFIX)%): $(PREFIX)bin/%: vendor 122 | $(GOOS_OVERRIDE) $(GO_BUILD) -o $@ $(patsubst $(PREFIX)bin/%,./cmd/%/...,$@) 123 | $(BINS:%=%-dev): 124 | $(call DOCKER_MAKE,$(patsubst bin/%-dev,%,$@)) 125 | bins: $(BINS:%=$(PREFIX)%) 126 | 127 | $(DOCKER_IMAGES): 128 | docker build -t $(DOCKER_REPOSITORY)/$(patsubst docker-%,%,$@):latest \ 129 | --label "org.label-schema.build-date"="$(BUILD_DATE)" \ 130 | --label "org.label-schema.name"="$(RELEASE_NAME)" \ 131 | --label "org.label-schema.vcs-ref"="$(VCS_SHA)" \ 132 | --label "org.label-schema.vendor"="Arigato Machine Inc." \ 133 | --label "org.label-schema.version"="$(RELEASE_VERSION)" \ 134 | --label "org.vcs-branch"="$(VCS_BRANCH)" \ 135 | --build-arg BINARY=$(patsubst docker-%,bin/%,$@) \ 136 | . 137 | $(DOCKER_IMAGES:%=%-dev): docker-%-dev: build/docker/%/Dockerfile bin/%-dev 138 | docker build -t $(DOCKER_REPOSITORY)/$(patsubst docker-%-dev,%,$@):latest \ 139 | --label "org.label-schema.build-date"="$(BUILD_DATE)" \ 140 | --label "org.label-schema.name"="$(RELEASE_NAME)" \ 141 | --label "org.label-schema.vcs-ref"="$(VCS_SHA)" \ 142 | --label "org.label-schema.vendor"="Arigato Machine Inc." \ 143 | --label "org.label-schema.version"="$(RELEASE_VERSION)" \ 144 | --label "org.vcs-branch"="$(VCS_BRANCH)" \ 145 | --build-arg BINARY=bin/$(patsubst docker-%-dev,%,$@) \ 146 | build/docker/$(patsubst docker-%-dev,%,$@) 147 | 148 | docker: $(DOCKER_IMAGES) 149 | docker-dev: $(DOCKER_IMAGES:%=%-dev) 150 | 151 | docker-login: 152 | docker login -u="$$DOCKER_USERNAME" -p="$$DOCKER_PASSWORD" 153 | 154 | $(DOCKER_RELEASES): release-%: docker-login docker-% 155 | docker tag $(DOCKER_REPOSITORY)/$(patsubst release-%,%,$@) $(DOCKER_REPOSITORY)/$(patsubst release-%,%,$@):$(RELEASE_VERSION) 156 | docker push $(DOCKER_REPOSITORY)/$(patsubst release-%,%,$@):$(RELEASE_VERSION) 157 | ifeq ($(VCS_BRANCH),$(BASE_BRANCH)) 158 | # On master, we want to push latest 159 | docker push $(DOCKER_REPOSITORY)/$(patsubst release-%,%,$@):latest 160 | else 161 | # On branches, we want to push specific branch version and latest branch 162 | docker tag $(DOCKER_REPOSITORY)/$(patsubst release-%,%,$@) $(DOCKER_REPOSITORY)/$(patsubst release-%,%,$@):$(RELEASE_VERSION) 163 | docker push $(DOCKER_REPOSITORY)/$(patsubst release-%,%,$@):$(RELEASE_VERSION) 164 | endif 165 | release: $(DOCKER_RELEASES) 166 | 167 | .PHONY: $(BINS:%=$(PREFIX)%) $(DOCKER_IMAGES) $(CMDs:%=build/docker/%/Dockerfile) $(DOCKER_RELEASES) release docker-login 168 | 169 | 170 | ################################################# 171 | # Building the examples 172 | ################################################# 173 | EXAMPLES=hello-hlnr 174 | DOCKER_EXAMPLES=$(addprefix docker-,$(EXAMPLES)) 175 | 176 | $(DOCKER_EXAMPLES): 177 | docker build -t hlnr/$(patsubst docker-%,%,$@):latest _examples/$(patsubst docker-%,%,$@) 178 | 179 | examples: $(DOCKER_EXAMPLES) 180 | 181 | .PHONY: $(DOCKER_EXAMPLES) 182 | 183 | ################################################# 184 | # Cleanup 185 | ################################################# 186 | clean: 187 | rm -rf build 188 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Heighliner 2 | 3 | [![Build Status](https://travis-ci.com/manifoldco/heighliner.svg?token=SbTMbCYMT5HWVmmTnBoj&branch=master)](https://travis-ci.com/manifoldco/heighliner) 4 | [![codecov](https://codecov.io/gh/manifoldco/heighliner/branch/master/graph/badge.svg)](https://codecov.io/gh/manifoldco/heighliner) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/manifoldco/heighliner)](https://goreportcard.com/report/github.com/manifoldco/heighliner) 6 | [![GoDoc](https://godoc.org/github.com/manifoldco/heighliner?status.svg)](https://godoc.org/github.com/manifoldco/heighliner) 7 | 8 | > A Heighliner is truly big. Its hold will tuck all of our frigates and transports 9 | > into a little corner-we'll be just one small part of the ship's manifest. 10 | 11 | Heighliner aims to make your workflow with GitHub and Kubernetes easy. Automatically deploy previews of GitHub pull requests to your cluster and use GitHub Releases to deploy to staging and production. 12 | 13 | **Warning**: this project is still under heavy development and is not 14 | recommended for production usage yet. Breaking changes might occur until v1.0.0. 15 | 16 | ## Goals 17 | 18 | **Cloud Native.** Instead of templating, Heighliner runs your infrastructure as 19 | software, keeping the state of your deployments always as they should be. 20 | 21 | **Connected.** The cluster is aware of container registry and source code 22 | repository state. It reacts to them (creating new deploys), and reflects into 23 | them (updating GitHub PR deployment status). Preview deploys are automatically 24 | created and destroyed. Deploys can auto-update based on Semantic Versioning 25 | policies, or be manually controlled. 26 | 27 | **Complete.** A Heighliner Microservice comes with DNS and TLS out of the box. 28 | 29 | **Convention and Configuration.** Reasonable defaults allow you to get up and 30 | running without much effort, but can be overridded for customization. 31 | 32 | ## Installation 33 | 34 | Heighliner consists out of multiple components, we've explained these in detail 35 | in the [design docs](docs/design/README.md) and in an [introductory blog post](https://medium.com/@jelmersnoeck/1fb233c577ad) 36 | 37 | For a full installation process, have a look at the [installation docs](docs/installation.md) or our [getting started guide](https://docs.manifold.co/docs/heighliner-hUPQ28TwKOayIOYmiCcKM) 38 | 39 | ## Usage 40 | 41 | ### Configure a GitHub Repository 42 | 43 | Ensure that you have an API token installed in your cluster. Follow our [how to](docs/design/github-connector.md#APIToken) 44 | for further instructions. 45 | 46 | The GitHub repository resource is used to synchronize releases and pull requests 47 | with cluster state, and update pull requests with deployment status. 48 | 49 | ```yaml 50 | apiVersion: hlnr.io/v1alpha1 51 | kind: GitHubRepository 52 | metadata: 53 | name: cool-repository 54 | spec: 55 | repo: my-repository 56 | owner: my-account 57 | configSecret: 58 | name: my-github-secret 59 | ``` 60 | 61 | ### Configure a Versioning Policy 62 | 63 | The versioning policy resource defines how microservices are updated based on 64 | available releases. 65 | 66 | ```yaml 67 | apiVersion: hlnr.io/v1alpha1 68 | kind: VersioningPolicy 69 | metadata: 70 | name: release-patch 71 | spec: 72 | semVer: 73 | version: release 74 | level: patch 75 | ``` 76 | 77 | ### Configure an Image Policy 78 | 79 | The image policy resource synchronizes Docker container images with cluster 80 | state. It cross references with GitHub releases, filtering out images that do 81 | not match the versioning policy. 82 | 83 | ```yaml 84 | apiVersion: hlnr.io/v1alpha1 85 | kind: ImagePolicy 86 | metadata: 87 | name: my-image-policy 88 | spec: 89 | image: my-docker/my-image 90 | imagePullSecrets: 91 | - name: my-docker-secrets 92 | versioningPolicy: 93 | name: release-patch 94 | filter: 95 | github: 96 | name: cool-repository 97 | ``` 98 | 99 | ### Configure a Network Policy 100 | 101 | The network policy resource handles exposing instances of versioned 102 | microservices within the cluster, or to the outside world. `domain` can be 103 | templated for use with preview releases (pull requests). 104 | 105 | ```yaml 106 | apiVersion: hlnr.io/v1alpha1 107 | kind: NetworkPolicy 108 | metadata: 109 | name: hlnr-www 110 | spec: 111 | microservice: 112 | name: my-microservice 113 | ports: 114 | - name: headless 115 | port: 80 116 | targetPort: 80 117 | externalDNS: 118 | - domain: my-domain.com 119 | port: headless 120 | tlsGroup: my-cert-manager-tls-group 121 | updateStrategy: 122 | latest: {} 123 | ``` 124 | 125 | ### Configure a Microservice 126 | 127 | The microservice resource is a template for deployments of images that match the 128 | image policy. 129 | 130 | ```yaml 131 | apiVersion: hlnr.io/v1alpha1 132 | kind: Microservice 133 | metadata: 134 | name: my-microservice 135 | spec: 136 | imagePolicy: 137 | name: my-image-policy 138 | ``` 139 | 140 | ## Contributing 141 | 142 | Thanks for taking the time to join the community and helping out! 143 | 144 | - Please familiarize yourself with the [Code of Conduct](./CODE_OF_CONDUCT.md) 145 | before contributing. 146 | - Look at our [Contributing Guidelines](./CONTRIBUTING.md) for more information 147 | about setting up your environment and how to contribute. 148 | -------------------------------------------------------------------------------- /_examples/availability-policy.yaml: -------------------------------------------------------------------------------- 1 | # This is a custom availability policy. Heighliner will install a default 2 | # AvailabilityPolicy which is set up to be highly available with 2 replicas and 3 | # ensures that pods run on different hosts and in different zones. 4 | # It will also set up a PodDisruptionBudget which ensures that there is always 5 | # at minimum 1 pod available before rescheduling. 6 | apiVersion: hlnr.io/v1alpha1 7 | kind: AvailabilityPolicy 8 | metadata: 9 | name: high-availability 10 | spec: 11 | replicas: 4 12 | minAvailable: 1 13 | restartPolicy: Always 14 | deploymentStrategy: 15 | rollingUpdate: 16 | maxSurge: 50% 17 | maxUnavailable: 25% 18 | -------------------------------------------------------------------------------- /_examples/config-policy.yaml: -------------------------------------------------------------------------------- 1 | # This custom ConfigPolicy sets up some Environment Variables and Command 2 | # Arguments for the container. It's also possible to set up Volumes and Volume 3 | # Mounts through the ConfigPolicy. 4 | # 5 | # The ConfigPolicy will be monitored for changes, once changes are detected in 6 | # the configuration or the values of the configuration (envs), a new deployment 7 | # for the Microservices which reference this ConfigPolicy will be created. 8 | apiVersion: hlnr.io/v1alpha1 9 | kind: ConfigPolicy 10 | metadata: 11 | name: demo-application 12 | spec: 13 | command: 14 | - demo-application 15 | args: 16 | - --log-level=4 17 | env: 18 | - name: PORT 19 | value: "3000" 20 | - name: API_TOKEN 21 | valueFrom: 22 | secretKeyRef: 23 | name: demo-application-secrets 24 | key: API_TOKEN 25 | -------------------------------------------------------------------------------- /_examples/github-repository.yaml: -------------------------------------------------------------------------------- 1 | # This secret should be set up manually. It's required to connect to your GH 2 | # repository. 3 | apiVersion: v1 4 | kind: Secret 5 | metadata: 6 | name: gh-token-demo-application 7 | type: Opaque 8 | data: 9 | # Example: "GH API token" Base64 encoded 10 | GITHUB_AUTH_TOKEN: YmFzZTY0IGVuY29kZWQgR0ggQVBJIHRva2Vu 11 | 12 | --- 13 | 14 | # By installing this CRD, we will set up a connection with the 15 | # github.com/my-github-team/demo-application repository. This means that for 16 | # every new release and PR we'll get notified. 17 | apiVersion: hlnr.io/v1alpha1 18 | kind: GitHubRepository 19 | metadata: 20 | name: demo-application 21 | spec: 22 | maxAvailable: 1 23 | repo: demo-application 24 | owner: my-github-team 25 | configSecret: 26 | name: gh-token-demo-application 27 | -------------------------------------------------------------------------------- /_examples/health-policy.yaml: -------------------------------------------------------------------------------- 1 | # This is a custom HealthPolicy which allows you to set up health checks for 2 | # your application. This can be generalized and reused across multiple 3 | # Microservices. 4 | apiVersion: hlnr.io/v1alpha1 5 | kind: HealthPolicy 6 | metadata: 7 | name: node-apps 8 | spec: 9 | readinessProbe: 10 | httpGet: 11 | path: /_healthz 12 | port: 3000 13 | initialDelaySeconds: 3 14 | periodSeconds: 3 15 | livenessProbe: 16 | httpGet: 17 | path: /_healthz 18 | port: 3000 19 | initialDelaySeconds: 5 20 | periodSeconds: 3 21 | -------------------------------------------------------------------------------- /_examples/hello-hlnr/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.10 2 | 3 | WORKDIR /go/src/github.com/manifoldco/hello-hlnr 4 | COPY ./hello-hlnr.go ./ 5 | COPY ./Makefile ./ 6 | COPY ./bin ./ 7 | RUN go build -o ./bin/hello-hlnr hello-hlnr.go 8 | 9 | EXPOSE 8080 10 | ENTRYPOINT [ "./bin/hello-hlnr" ] 11 | -------------------------------------------------------------------------------- /_examples/hello-hlnr/README.md: -------------------------------------------------------------------------------- 1 | # Hello Heighliner, how are you? 2 | 3 | An example of how to use heighliner. Please follow along with our getting started guide [quick start guide](https://docs.manifold.co/docs/heighliner-hUPQ28TwKOayIOYmiCcKM) -------------------------------------------------------------------------------- /_examples/hello-hlnr/github-repository.yaml: -------------------------------------------------------------------------------- 1 | # This secret is your GH API ACCESS token set up with permissions according to [/docs/design/github-connector.md](/docs/design/github-connector.md) 2 | apiVersion: v1 3 | kind: Secret 4 | metadata: 5 | name: hello-hlnr 6 | type: Opaque 7 | data: 8 | # This is an example: "GH API token" Base64 encoded 9 | GITHUB_AUTH_TOKEN: YmFzZTY0IGVuY29kZWQgR0ggQVBJIHRva2Vu 10 | 11 | --- 12 | 13 | apiVersion: hlnr.io/v1alpha1 14 | kind: GitHubRepository 15 | metadata: 16 | name: heighliner 17 | spec: 18 | maxAvailable: 1 19 | repo: heighliner 20 | owner: manifoldco 21 | configSecret: 22 | name: hello-hlnr 23 | -------------------------------------------------------------------------------- /_examples/hello-hlnr/hello-hlnr-preview.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: previews 5 | 6 | --- 7 | 8 | apiVersion: hlnr.io/v1alpha1 9 | kind: VersioningPolicy 10 | metadata: 11 | name: preview-minor 12 | namespace: previews 13 | spec: 14 | semVer: 15 | version: minor 16 | level: preview 17 | 18 | --- 19 | 20 | apiVersion: hlnr.io/v1alpha1 21 | kind: Microservice 22 | metadata: 23 | name: hello-hlnr 24 | namespace: previews 25 | spec: 26 | networkPolicy: 27 | name: hello-hlnr 28 | imagePolicy: 29 | name: hello-hlnr 30 | 31 | --- 32 | 33 | apiVersion: hlnr.io/v1alpha1 34 | kind: ImagePolicy 35 | metadata: 36 | name: hello-hlnr 37 | namespace: previews 38 | spec: 39 | image: manifoldco/hello-hlnr 40 | imagePullSecrets: 41 | - name: manifold-docker-registry # this will be your docker hub secrets for pulling images 42 | versioningPolicy: 43 | name: preview-minor 44 | filter: 45 | github: 46 | name: heighliner 47 | namespace: repos 48 | 49 | --- 50 | 51 | apiVersion: hlnr.io/v1alpha1 52 | kind: NetworkPolicy 53 | metadata: 54 | name: hello-hlnr 55 | namespace: previews 56 | spec: 57 | microservice: 58 | name: hello-hlnr 59 | ports: 60 | - name: headless 61 | port: 80 62 | targetPort: 8080 63 | externalDNS: 64 | - domain: "{{.StreamName}}.arigato.tools" 65 | port: headless 66 | disableTLS: true # lets disable TLS for out previews :) 67 | updateStrategy: 68 | latest: {} 69 | -------------------------------------------------------------------------------- /_examples/hello-hlnr/hello-hlnr-production.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: production 5 | 6 | --- 7 | 8 | apiVersion: hlnr.io/v1alpha1 9 | kind: VersioningPolicy 10 | metadata: 11 | name: release-minor 12 | namespace: production 13 | spec: 14 | semVer: 15 | version: minor 16 | level: release 17 | 18 | --- 19 | 20 | apiVersion: hlnr.io/v1alpha1 21 | kind: Microservice 22 | metadata: 23 | name: hello-hlnr 24 | namespace: production 25 | spec: 26 | networkPolicy: 27 | name: hello-hlnr 28 | imagePolicy: 29 | name: hello-hlnr 30 | 31 | --- 32 | 33 | apiVersion: hlnr.io/v1alpha1 34 | kind: ImagePolicy 35 | metadata: 36 | name: hello-hlnr 37 | namespace: production 38 | spec: 39 | image: manifoldco/hello-hlnr 40 | imagePullSecrets: 41 | - name: manifold-docker-registry 42 | namespace: default 43 | versioningPolicy: 44 | name: release-minor 45 | match: 46 | name: 47 | from: "v{{.Tag}}" 48 | filter: 49 | github: 50 | name: heighliner 51 | namespace: repos 52 | 53 | --- 54 | 55 | apiVersion: hlnr.io/v1alpha1 56 | kind: NetworkPolicy 57 | metadata: 58 | name: hello-hlnr 59 | namespace: production 60 | spec: 61 | microservice: 62 | name: hello-hlnr 63 | ports: 64 | - name: headless 65 | port: 80 66 | targetPort: 8080 67 | externalDNS: 68 | - domain: "hello-hlnr.mywebsite" 69 | port: headless 70 | disableTLS: true 71 | tlsGroup: manifold-websites-tls 72 | updateStrategy: 73 | latest: {} 74 | -------------------------------------------------------------------------------- /_examples/hello-hlnr/hello-hlnr-stage.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: stage 5 | 6 | --- 7 | 8 | apiVersion: hlnr.io/v1alpha1 9 | kind: Microservice 10 | metadata: 11 | name: hello-hlnr 12 | namespace: stage 13 | spec: 14 | networkPolicy: 15 | name: hello-hlnr 16 | imagePolicy: 17 | name: hello-hlnr 18 | 19 | --- 20 | 21 | apiVersion: hlnr.io/v1alpha1 22 | kind: VersioningPolicy 23 | metadata: 24 | name: candidate-minor 25 | namespace: stage 26 | spec: 27 | semVer: 28 | version: minor 29 | level: candidate 30 | 31 | --- 32 | 33 | apiVersion: hlnr.io/v1alpha1 34 | kind: ImagePolicy 35 | metadata: 36 | name: hello-hlnr 37 | namespace: stage 38 | spec: 39 | image: manifoldco/hello-hlnr 40 | imagePullSecrets: 41 | - name: manifold-docker-registry 42 | namespace: default 43 | versioningPolicy: 44 | name: candidate-minor 45 | match: 46 | name: 47 | from: "v{{.Tag}}" 48 | filter: 49 | github: 50 | name: heighliner 51 | namespace: repos 52 | 53 | --- 54 | 55 | apiVersion: hlnr.io/v1alpha1 56 | kind: NetworkPolicy 57 | metadata: 58 | name: hello-hlnr 59 | namespace: stage 60 | spec: 61 | microservice: 62 | name: hello-hlnr 63 | ports: 64 | - name: headless 65 | port: 80 66 | targetPort: 8080 67 | externalDNS: 68 | - domain: "hello-hlnr.stage.mywebsite" 69 | port: headless 70 | tlsGroup: manifold-websites-tls 71 | updateStrategy: 72 | latest: {} 73 | -------------------------------------------------------------------------------- /_examples/hello-hlnr/hello-hlnr.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | ) 7 | 8 | var myHTML = ` 9 | 10 |
11 |

Hello Heighliner!

12 |
13 | 14 | ` 15 | 16 | func index(w http.ResponseWriter, r *http.Request) { 17 | fmt.Fprintf(w, myHTML) 18 | } 19 | 20 | func main() { 21 | http.HandleFunc("/", index) 22 | http.ListenAndServe(":8080", nil) 23 | } 24 | -------------------------------------------------------------------------------- /_examples/image-policy-match.yaml: -------------------------------------------------------------------------------- 1 | # These ImagePolicies demonstrate how to use the `match` field to select 2 | # container images. 3 | # All examples here assume the Secret and VersioningPolicy from 4 | # `image-policy.yaml` exist. 5 | 6 | # This is the default match configuration. If no value is set, Heighliner 7 | # attempts to find tags in the container registry that have the same name 8 | # as any GitHub releases that have passed through the VersioningPolicy. 9 | apiVersion: hlnr.io/v1alpha1 10 | kind: ImagePolicy 11 | metadata: 12 | name: default-match 13 | spec: 14 | image: your/demo-application 15 | imagePullSecrets: 16 | - name: docker-registry 17 | versioningPolicy: 18 | name: previews 19 | filter: 20 | github: 21 | name: demo-application 22 | match: 23 | name: 24 | from: "{{.Tag}}" 25 | to: "{{.Tag}}" 26 | 27 | --- 28 | 29 | # This configuration uses pattern matching and templating. Heighliner will 30 | # find a release with the tag `v1.0.0` (for example) and match it to a tag in the 31 | # container registry with the value `cool-1.0.0`. 32 | apiVersion: hlnr.io/v1alpha1 33 | kind: ImagePolicy 34 | metadata: 35 | name: convert-name 36 | spec: 37 | image: your/demo-application 38 | imagePullSecrets: 39 | - name: docker-registry 40 | versioningPolicy: 41 | name: previews 42 | filter: 43 | github: 44 | name: demo-application 45 | match: 46 | name: 47 | from: "v{{.Tag}}" 48 | to: "cool-{{.Tag}}" 49 | 50 | --- 51 | 52 | # This configuration uses label matching. You can match on any number of labels, 53 | # with or without name. If a given label does not exist on a container image, 54 | # it will not match. 55 | # 56 | # If a match value is the empty object `{}`, it is treated as an identity 57 | # mapping, as in the default value. 58 | apiVersion: hlnr.io/v1alpha1 59 | kind: ImagePolicy 60 | metadata: 61 | name: convert-name 62 | spec: 63 | image: your/demo-application 64 | imagePullSecrets: 65 | - name: docker-registry 66 | versioningPolicy: 67 | name: previews 68 | filter: 69 | github: 70 | name: demo-application 71 | match: 72 | labels: 73 | org.me.my-tag-label: {} 74 | -------------------------------------------------------------------------------- /_examples/image-policy.yaml: -------------------------------------------------------------------------------- 1 | # This is a secret which holds information about a private Docker Registry. This 2 | # allows us to use images that are privately hosted. 3 | # See https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ 4 | # for more information. 5 | apiVersion: v1 6 | kind: Secret 7 | metadata: 8 | name: docker-registry 9 | type: kubernetes.io/dockercfg 10 | data: 11 | .dockercfg: # base64 encoded registry information 12 | 13 | --- 14 | 15 | # This Versioning Policy defines that we only want to pull in information 16 | # related to previews. In our case, this means PullRequests. 17 | apiVersion: hlnr.io/v1alpha1 18 | kind: VersioningPolicy 19 | metadata: 20 | name: previews 21 | spec: 22 | semVer: 23 | version: major 24 | level: preview 25 | 26 | --- 27 | 28 | # This ImagePolicy uses the given GitHub filter to get a list of available 29 | # releases. It will then match these with what's available on docker hub for the 30 | # given image and for the given VersioningPolicy. 31 | # We've set up an ImagePullSecret here to illustrate that we can also pull in 32 | # private images. 33 | apiVersion: hlnr.io/v1alpha1 34 | kind: ImagePolicy 35 | metadata: 36 | name: demo-application 37 | spec: 38 | image: arigato/manifold-www 39 | imagePullSecrets: 40 | - name: docker-registry 41 | versioningPolicy: 42 | name: previews 43 | filter: 44 | github: 45 | name: demo-application 46 | -------------------------------------------------------------------------------- /_examples/microservice.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: hlnr.io/v1alpha1 2 | kind: Microservice 3 | metadata: 4 | name: manifold-www 5 | spec: 6 | imagePolicy: 7 | name: demo-application 8 | configPolicy: 9 | name: demo-application 10 | availabilityPolicy: 11 | name: high-availability 12 | healthPolicy: 13 | name: node-apps 14 | -------------------------------------------------------------------------------- /_examples/network-policy.yaml: -------------------------------------------------------------------------------- 1 | # This NetworkPolicy will allow Heighliner to create Services and Ingresses for 2 | # the references Microservice. It will create a Service and Ingress per release 3 | # that has been deployed. 4 | apiVersion: hlnr.io/v1alpha1 5 | kind: NetworkPolicy 6 | metadata: 7 | name: manifold-www 8 | namespace: previews 9 | spec: 10 | microservice: 11 | name: demo-application 12 | ports: 13 | - name: headless 14 | port: 80 15 | targetPort: 3000 16 | externalDNS: 17 | - domain: "{{.StreamName}}.previews.heighliner.com" 18 | port: headless 19 | tlsGroup: previews-tls 20 | updateStrategy: 21 | latest: {} 22 | 23 | --- 24 | 25 | # This will install a CertManager certificate into the cluster that allows us to 26 | # use a wildcard certificate. See https://github.com/jetstack/cert-manager for 27 | # more information. 28 | apiVersion: certmanager.k8s.io/v1alpha1 29 | kind: Certificate 30 | metadata: 31 | name: previews-tls 32 | spec: 33 | secretName: previews-tls 34 | issuerRef: 35 | name: letsencrypt-prod 36 | kind: ClusterIssuer 37 | commonName: '*.previews.heighliner.com' 38 | acme: 39 | config: 40 | - dns01: 41 | provider: route53-dns 42 | domains: 43 | - '*.previews.heighliner.com' 44 | -------------------------------------------------------------------------------- /apis/v1alpha1/availability.go: -------------------------------------------------------------------------------- 1 | package v1alpha1 2 | 3 | import ( 4 | "github.com/manifoldco/heighliner/internal/k8sutils" 5 | 6 | corev1 "k8s.io/api/core/v1" 7 | "k8s.io/api/extensions/v1beta1" 8 | apiextv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" 9 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 10 | "k8s.io/apimachinery/pkg/util/intstr" 11 | ) 12 | 13 | // AvailabilityPolicy defines the configuration options for the AvailabilityPolicy. 14 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 15 | type AvailabilityPolicy struct { 16 | metav1.TypeMeta `json:",inline"` 17 | metav1.ObjectMeta `json:"metadata"` 18 | 19 | Spec AvailabilityPolicySpec `json:"spec"` 20 | } 21 | 22 | // AvailabilityPolicyList is a list of AvailabilityPolicy CRDs. 23 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 24 | type AvailabilityPolicyList struct { 25 | metav1.TypeMeta `json:",inline"` 26 | metav1.ListMeta `json:"metadata"` 27 | Items []AvailabilityPolicy `json:"items"` 28 | } 29 | 30 | // AvailabilityPolicySpec is the specification for Availability. 31 | type AvailabilityPolicySpec struct { 32 | // Number of desired replicas of the service. 33 | Replicas *int32 `json:"replicas"` 34 | 35 | // An eviction is allowed if at least "minAvailable" pods selected by 36 | // "selector" will still be available after the eviction, i.e. even in the 37 | // absence of the evicted pod. So for example you can prevent all voluntary 38 | // evictions by specifying "100%". 39 | MinAvailable *intstr.IntOrString `json:"minAvailable,omitempty"` 40 | 41 | // An eviction is allowed if at most "maxUnavailable" pods selected by 42 | // "selector" are unavailable after the eviction, i.e. even in absence of 43 | // the evicted pod. For example, one can prevent all voluntary evictions 44 | // by specifying 0. This is a mutually exclusive setting with "minAvailable". 45 | MaxUnavailable *intstr.IntOrString `json:"maxUnavailable,omitempty"` 46 | 47 | // RestartPolicy describes how the container should be restarted. Only one 48 | // of `Always`, `OnFailure` or `Never` restart policies may be specified. 49 | // If none of the policies is specified, the default one is `Always`. 50 | RestartPolicy corev1.RestartPolicy `json:"restartPolicy,omitempty"` 51 | 52 | // The deployment strategy to use to replace existing pods with new ones. 53 | DeploymentStrategy v1beta1.DeploymentStrategy `json:"deploymentStrategy,omitempty"` 54 | 55 | // The microservice's scheduling constraints. 56 | Affinity *corev1.Affinity `json:"affinity,omitempty"` 57 | } 58 | 59 | // DefaultAvailabilityPolicySpec is the default availability spec that will be 60 | // used when it's not defined. 61 | // Affinity is DeploymentSpecific so will be filled in later on. 62 | var DefaultAvailabilityPolicySpec = AvailabilityPolicySpec{ 63 | Replicas: func(i int32) *int32 { return &i }(2), 64 | MinAvailable: k8sutils.PtrIntOrString(intstr.FromInt(1)), 65 | MaxUnavailable: k8sutils.PtrIntOrString(intstr.FromString("25%")), 66 | RestartPolicy: corev1.RestartPolicyAlways, 67 | DeploymentStrategy: v1beta1.DeploymentStrategy{ 68 | Type: v1beta1.RollingUpdateDeploymentStrategyType, 69 | RollingUpdate: &v1beta1.RollingUpdateDeployment{ 70 | MaxUnavailable: k8sutils.PtrIntOrString(intstr.FromString("25%")), 71 | MaxSurge: k8sutils.PtrIntOrString(intstr.FromString("25%")), 72 | }, 73 | }, 74 | } 75 | 76 | // AvailabilityPolicyValidationSchema represents the OpenAPIV3Schema validation for 77 | // the Availability CRD. 78 | var AvailabilityPolicyValidationSchema = apiextv1beta1.JSONSchemaProps{} 79 | -------------------------------------------------------------------------------- /apis/v1alpha1/config_policy.go: -------------------------------------------------------------------------------- 1 | package v1alpha1 2 | 3 | import ( 4 | corev1 "k8s.io/api/core/v1" 5 | "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" 6 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 7 | ) 8 | 9 | // ConfigPolicy describes the configuration options for the ConfigPolicy. 10 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 11 | type ConfigPolicy struct { 12 | metav1.TypeMeta `json:",inline"` 13 | metav1.ObjectMeta `json:"metadata"` 14 | 15 | Spec ConfigPolicySpec `json:"spec"` 16 | Status ConfigPolicyStatus `json:"status"` 17 | } 18 | 19 | // ConfigPolicyList is a list of ConfigPolicy CRDs. 20 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 21 | type ConfigPolicyList struct { 22 | metav1.TypeMeta `json:",inline"` 23 | metav1.ListMeta `json:"metadata"` 24 | Items []ConfigPolicy `json:"items"` 25 | } 26 | 27 | // ConfigPolicySpec describes the specification for Config. 28 | type ConfigPolicySpec struct { 29 | Args []string `json:"args,omitempty"` 30 | Command []string `json:"command,omitempty"` 31 | Env []corev1.EnvVar `json:"env,omitempty"` 32 | EnvFrom []corev1.EnvFromSource `json:"envFrom,omitempty"` 33 | VolumeMounts []corev1.VolumeMount `json:"volumeMounts,omitempty"` 34 | Volumes []corev1.Volume `json:"volumes,omitempty"` 35 | } 36 | 37 | // ConfigPolicyStatus represents the current status of a ConfigPolicy. 38 | type ConfigPolicyStatus struct { 39 | LastUpdatedTime metav1.Time `json:"lastUpdatedTime"` 40 | Hashed string `json:"hashed"` 41 | } 42 | 43 | // ConfigPolicyValidationSchema represents the OpenAPIV3Schema validation for 44 | // the ConfigPolicy CRD. 45 | var ConfigPolicyValidationSchema = &v1beta1.CustomResourceValidation{ 46 | OpenAPIV3Schema: &v1beta1.JSONSchemaProps{ 47 | Properties: map[string]v1beta1.JSONSchemaProps{ 48 | "status": { 49 | Required: []string{"lastUpdatedTime", "hashed"}, 50 | }, 51 | }, 52 | }, 53 | } 54 | -------------------------------------------------------------------------------- /apis/v1alpha1/doc.go: -------------------------------------------------------------------------------- 1 | // +k8s:deepcopy-gen=package 2 | 3 | // Package v1alpha1 contains data types for Heighliner components v1alpha1. 4 | package v1alpha1 5 | -------------------------------------------------------------------------------- /apis/v1alpha1/github_repository.go: -------------------------------------------------------------------------------- 1 | package v1alpha1 2 | 3 | import ( 4 | "fmt" 5 | 6 | corev1 "k8s.io/api/core/v1" 7 | "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" 8 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 | ) 10 | 11 | // GitHubRepository represents the configuration for a specific GitHub 12 | // repository. 13 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 14 | type GitHubRepository struct { 15 | metav1.TypeMeta `json:",inline"` 16 | metav1.ObjectMeta `json:"metadata"` 17 | 18 | Spec GitHubRepositorySpec `json:"spec"` 19 | Status GitHubRepositoryStatus `json:"status"` 20 | } 21 | 22 | // GitHubRepositoryList is a list of GitHubRepositories. 23 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 24 | type GitHubRepositoryList struct { 25 | metav1.TypeMeta `json:",inline"` 26 | metav1.ListMeta `json:"metadata"` 27 | Items []GitHubRepository `json:"items"` 28 | } 29 | 30 | // GitHubRepositorySpec represents the specification for a GitHubRepository. 31 | type GitHubRepositorySpec struct { 32 | // MaxAvailable is the maximum number of releases for a specific level that 33 | // should be kept. When the number of releases grows over this amount, the 34 | // oldes release will be sunsetted. 35 | MaxAvailable int `json:"maxAvailable"` 36 | 37 | // Repo is the name of the repository we want to monitor 38 | Repo string `json:"repo"` 39 | 40 | // Owner is the owner of the repository, often specified as team. 41 | Owner string `json:"owner"` 42 | 43 | // ConfigSecret represent the secret that houses the API token to 44 | // communicate with the given repository. 45 | ConfigSecret corev1.LocalObjectReference `json:"configSecret"` 46 | } 47 | 48 | // Slug returns the slug of the repository. 49 | func (r *GitHubRepositorySpec) Slug() string { 50 | return fmt.Sprintf("%s/%s", r.Owner, r.Repo) 51 | } 52 | 53 | // GitHubRepositoryStatus represents the current status for the GitHubRepository. 54 | type GitHubRepositoryStatus struct { 55 | // Releases represents the available releases on GitHub for the associated 56 | // repositories. 57 | Releases []GitHubRelease `json:"releases"` 58 | 59 | // Webhook represents the installed Webhook information for the GitHub 60 | // Repository. 61 | Webhook *GitHubHook `json:"webhook"` 62 | 63 | // Reconciliation represents the status of the repository reconciliation. 64 | Reconciliation GitHubReconciliation `json:"reconciliation"` 65 | } 66 | 67 | // GitHubHook represents the status object for a GiHub Webhook for the CRD. 68 | type GitHubHook struct { 69 | // ID is the ID on GitHub for the installed hooks. This is needed to perform 70 | // updates and deletes. 71 | ID *int64 `json:"id"` 72 | 73 | // Secret is the secret used in GHs communication to our server. 74 | Secret string `json:"secret"` 75 | } 76 | 77 | // GitHubRelease represents a release made in GitHub 78 | type GitHubRelease struct { 79 | Name string `json:"name"` 80 | Tag string `json:"tag"` 81 | Level SemVerLevel `json:"level"` 82 | ReleaseTime metav1.Time `json:"releaseTime"` 83 | Deployment *Deployment `json:"deployment,omitempty"` 84 | } 85 | 86 | // GitHubReconciliation represents the status of the repository reconciliation. 87 | type GitHubReconciliation struct { 88 | LastUpdate *metav1.Time `json:"last_update"` 89 | } 90 | 91 | // Deployment represents a linking between a GitHub deployment and a network 92 | // policy. Through the release information we can determine a specific domain. 93 | type Deployment struct { 94 | ID *int64 `json:"deployment"` 95 | NetworkPolicy corev1.ObjectReference `json:"networkPolicy"` 96 | State string `json:"state"` 97 | URL *string `json:"url,omitempty"` 98 | } 99 | 100 | // GitHubRepositoryValidationSchema represents the OpenAPIV3Schema 101 | // validation for the GitHubRepository CRD. 102 | var GitHubRepositoryValidationSchema = &v1beta1.CustomResourceValidation{ 103 | OpenAPIV3Schema: &v1beta1.JSONSchemaProps{ 104 | Required: []string{"spec"}, 105 | Properties: map[string]v1beta1.JSONSchemaProps{ 106 | "spec": { 107 | Required: []string{"repo", "owner", "configSecret"}, 108 | }, 109 | }, 110 | }, 111 | } 112 | -------------------------------------------------------------------------------- /apis/v1alpha1/health_policy.go: -------------------------------------------------------------------------------- 1 | package v1alpha1 2 | 3 | import ( 4 | v1 "k8s.io/api/core/v1" 5 | "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" 6 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 7 | ) 8 | 9 | // HealthPolicy describes the configuration options for the HealthPolicy. 10 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 11 | type HealthPolicy struct { 12 | metav1.TypeMeta `json:",inline"` 13 | metav1.ObjectMeta `json:"metadata"` 14 | 15 | Spec HealthPolicySpec `json:"spec"` 16 | } 17 | 18 | // HealthPolicyList is a list of HealthPolicy CRDs. 19 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 20 | type HealthPolicyList struct { 21 | metav1.TypeMeta `json:",inline"` 22 | metav1.ListMeta `json:"metadata"` 23 | Items []HealthPolicy `json:"items"` 24 | } 25 | 26 | // HealthPolicySpec describes the specification which will be used for health 27 | // checks. 28 | type HealthPolicySpec struct { 29 | LivenessProbe *v1.Probe `json:"livenessProbe,omitempty"` 30 | ReadinessProbe *v1.Probe `json:"readinessProbe,omitempty"` 31 | } 32 | 33 | // HealthPolicyValidationSchema represents the OpenAPIV3Schema validation for 34 | // the NetworkPolicy CRD. 35 | var HealthPolicyValidationSchema = &v1beta1.CustomResourceValidation{ 36 | OpenAPIV3Schema: &v1beta1.JSONSchemaProps{ 37 | Required: []string{"spec"}, 38 | }, 39 | } 40 | -------------------------------------------------------------------------------- /apis/v1alpha1/image_policy_test.go: -------------------------------------------------------------------------------- 1 | package v1alpha1 2 | 3 | import "testing" 4 | 5 | func TestImagePolicyMatchConfig(t *testing.T) { 6 | tcs := []struct { 7 | name string 8 | 9 | hasName bool 10 | hasLabels bool 11 | 12 | match *ImagePolicyMatch 13 | }{ 14 | {"nil", true, false, nil}, 15 | {"empty", true, false, &ImagePolicyMatch{}}, 16 | 17 | {"both", true, true, &ImagePolicyMatch{ 18 | Name: &ImagePolicyMatchMapping{}, 19 | Labels: map[string]ImagePolicyMatchMapping{ 20 | "org.fake.label": {}, 21 | }, 22 | }}, 23 | } 24 | 25 | for _, tc := range tcs { 26 | t.Run(tc.name, func(t *testing.T) { 27 | 28 | n, l := tc.match.Config() 29 | 30 | if n != tc.hasName { 31 | t.Error("wrong value for name. expected:", tc.hasName, "got:", n) 32 | } 33 | 34 | if l != tc.hasLabels { 35 | t.Error("wrong value for labels. expected:", tc.hasLabels, "got:", l) 36 | } 37 | }) 38 | } 39 | } 40 | 41 | func TestImagePolicyMatchMapName(t *testing.T) { 42 | tcs := []struct { 43 | name string 44 | 45 | in string 46 | out string 47 | noErr bool 48 | 49 | match *ImagePolicyMatch 50 | }{ 51 | {"nil match is default", "tag", "tag", true, nil}, 52 | {"zero value is default", "tag", "tag", true, &ImagePolicyMatch{}}, 53 | 54 | {"err is propagated", "tag", "", false, &ImagePolicyMatch{ 55 | Name: &ImagePolicyMatchMapping{From: "{{"}, 56 | }}, 57 | 58 | {"name is mapped", "tag", "vtag", true, &ImagePolicyMatch{ 59 | Name: &ImagePolicyMatchMapping{To: "v{{.Tag}}"}, 60 | }}, 61 | } 62 | 63 | for _, tc := range tcs { 64 | t.Run(tc.name, func(t *testing.T) { 65 | out, err := tc.match.MapName(tc.in) 66 | 67 | if tc.noErr && err != nil { 68 | t.Fatal("expected no err but got one:", err) 69 | } 70 | 71 | if !tc.noErr && err == nil { 72 | t.Fatal("expected err but got none.") 73 | } 74 | 75 | if out != tc.out { 76 | t.Error("wrong mapping. expected:", tc.out, "got:", out) 77 | } 78 | }) 79 | } 80 | } 81 | 82 | func TestImagePolicyMatchMatches(t *testing.T) { 83 | tcs := []struct { 84 | name string 85 | 86 | in string 87 | tag string 88 | labels map[string]string 89 | expected bool 90 | noErr bool 91 | 92 | match *ImagePolicyMatch 93 | }{ 94 | {"nil match is default", "tag", "tag", nil, true, true, nil}, 95 | {"zero value is default", "tag", "tag", nil, true, true, &ImagePolicyMatch{}}, 96 | 97 | {"default no match", "tag", "not tag", nil, false, true, nil}, 98 | 99 | {"err is propagated in name", "tag", "tag", nil, false, false, &ImagePolicyMatch{ 100 | Name: &ImagePolicyMatchMapping{ 101 | From: "{{", 102 | }, 103 | }}, 104 | 105 | {"match on labels", "tag", "not tag", 106 | map[string]string{ 107 | "org.fake.label": "vtag", 108 | }, 109 | true, true, 110 | &ImagePolicyMatch{ 111 | Labels: map[string]ImagePolicyMatchMapping{ 112 | "org.fake.label": {To: "v{{.Tag}}"}, 113 | }, 114 | }, 115 | }, 116 | 117 | {"exclude on labels", "tag", "not tag", 118 | map[string]string{ 119 | "org.fake.label": "not tag", 120 | }, 121 | false, true, 122 | &ImagePolicyMatch{ 123 | Labels: map[string]ImagePolicyMatchMapping{ 124 | "org.fake.label": {}, 125 | }, 126 | }, 127 | }, 128 | 129 | {"err is propagated in labels", "tag", "not tag", 130 | map[string]string{ 131 | "org.fake.label": "tag", 132 | }, 133 | false, false, 134 | &ImagePolicyMatch{ 135 | Labels: map[string]ImagePolicyMatchMapping{ 136 | "org.fake.label": {From: "{{."}, 137 | }, 138 | }, 139 | }, 140 | 141 | {"exclude on missing label", "tag", "tag", 142 | map[string]string{}, 143 | false, true, 144 | &ImagePolicyMatch{ 145 | Name: &ImagePolicyMatchMapping{}, 146 | Labels: map[string]ImagePolicyMatchMapping{ 147 | "org.fake.label": {}, 148 | }, 149 | }, 150 | }, 151 | 152 | {"match on all values", "tag", "tag", 153 | map[string]string{ 154 | "org.fake.label": "tag", 155 | "org.fake.other.label": "tasty", 156 | }, 157 | true, true, 158 | &ImagePolicyMatch{ 159 | Name: &ImagePolicyMatchMapping{}, 160 | Labels: map[string]ImagePolicyMatchMapping{ 161 | "org.fake.label": {}, 162 | "org.fake.other.label": {From: "{{.Tag}}g", To: "{{.Tag}}sty"}, 163 | }, 164 | }, 165 | }, 166 | } 167 | 168 | for _, tc := range tcs { 169 | t.Run(tc.name, func(t *testing.T) { 170 | out, err := tc.match.Matches(tc.in, tc.tag, tc.labels) 171 | if tc.noErr && err != nil { 172 | t.Fatal("expected no err but got one:", err) 173 | } 174 | 175 | if !tc.noErr && err == nil { 176 | t.Fatal("expected err but got none.") 177 | } 178 | 179 | if out != tc.expected { 180 | t.Error("wrong result. expected:", tc.expected, "got:", out) 181 | } 182 | }) 183 | } 184 | } 185 | 186 | func TestImagePolicyMatchMapping(t *testing.T) { 187 | tcs := []struct { 188 | name string 189 | in string 190 | out string 191 | noErr bool 192 | 193 | from string 194 | to string 195 | }{ 196 | {"identity", "any", "any", true, "", ""}, 197 | 198 | {"map (from)", "v1.0.0", "1.0.0", true, "v{{.Tag}}", ""}, 199 | {"map (suffix)", "v1.0.0-thing", "1.0.0", true, "v{{.Tag}}-thing", ""}, 200 | {"map (to)", "1.0.0", "great-1.0.0-thing", true, "", "great-{{.Tag}}-thing"}, 201 | {"map (both)", "v1.0.0", "marketplace-1.0.0", true, "v{{.Tag}}", "marketplace-{{.Tag}}"}, 202 | 203 | {"no match in from", "1.0.0", "", false, "v{{.Tag}}", ""}, 204 | 205 | {"error (from / no template)", "any", "", false, "v", ""}, 206 | {"error (from / bad template)", "any", "", false, "{{.Tag", ""}, 207 | {"error (to / no template)", "any", "", false, "", "v"}, 208 | {"error (to / other template)", "any", "", false, "", "v{{.Food}}"}, 209 | {"error (to / bad template)", "any", "", false, "", "{{.Tag"}, 210 | } 211 | 212 | for _, tc := range tcs { 213 | t.Run(tc.name, func(t *testing.T) { 214 | m := ImagePolicyMatchMapping{From: tc.from, To: tc.to} 215 | out, err := m.Map(tc.in) 216 | if tc.noErr && err != nil { 217 | t.Fatal("expected no err but got one:", err) 218 | } 219 | 220 | if !tc.noErr && err == nil { 221 | t.Fatal("expected err but got none.") 222 | } 223 | 224 | if out != tc.out { 225 | t.Error("wrong mapping. expected:", tc.out, "got:", out) 226 | } 227 | }) 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /apis/v1alpha1/microservice.go: -------------------------------------------------------------------------------- 1 | package v1alpha1 2 | 3 | import ( 4 | "k8s.io/api/core/v1" 5 | "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" 6 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 7 | ) 8 | 9 | // Microservice represents the definition which we'll use to define a deployable 10 | // microservice. 11 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 12 | type Microservice struct { 13 | metav1.TypeMeta `json:",inline"` 14 | metav1.ObjectMeta `json:"metadata"` 15 | 16 | Spec MicroserviceSpec `json:"spec"` 17 | Status MicroserviceStatus `json:"status"` 18 | } 19 | 20 | // MicroserviceList is a list of Microservices. 21 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 22 | type MicroserviceList struct { 23 | metav1.TypeMeta `json:",inline"` 24 | metav1.ListMeta `json:"metadata"` 25 | Items []Microservice `json:"items"` 26 | } 27 | 28 | // MicroserviceSpec represents the specification for a Microservice. It houses 29 | // all the policies which we'll use to build a VersionedMicroservice. 30 | type MicroserviceSpec struct { 31 | // Local object references, microservice specific 32 | ImagePolicy v1.LocalObjectReference `json:"imagePolicy"` 33 | ConfigPolicy v1.LocalObjectReference `json:"configPolicy,omitempty"` 34 | 35 | // Global Object References, not Microservice specific. 36 | AvailabilityPolicy v1.ObjectReference `json:"availabilityPolicy,omitempty"` 37 | SecurityPolicy v1.ObjectReference `json:"securityPolicy,omitempty"` 38 | HealthPolicy v1.ObjectReference `json:"healthPolicy,omitempty"` 39 | } 40 | 41 | // MicroserviceStatus represents the status a specific Microservice is in. 42 | type MicroserviceStatus struct { 43 | Releases []Release `json:"releases"` 44 | } 45 | 46 | // MicroserviceValidationSchema represents the OpenAPIV3Scheme which 47 | // defines the validation for the MicroserviceSpec. 48 | var MicroserviceValidationSchema = &v1beta1.CustomResourceValidation{ 49 | OpenAPIV3Schema: &v1beta1.JSONSchemaProps{ 50 | Properties: map[string]v1beta1.JSONSchemaProps{ 51 | "spec": { 52 | Required: []string{"imagePolicy"}, 53 | Properties: map[string]v1beta1.JSONSchemaProps{ 54 | "imagePolicy": requiredObjectReference, 55 | }, 56 | }, 57 | "status": ReleaseValidationSchema, 58 | }, 59 | Required: []string{"spec"}, 60 | }, 61 | } 62 | 63 | var requiredObjectReference = v1beta1.JSONSchemaProps{ 64 | Required: []string{"name"}, 65 | } 66 | -------------------------------------------------------------------------------- /apis/v1alpha1/network_policy.go: -------------------------------------------------------------------------------- 1 | package v1alpha1 2 | 3 | import ( 4 | corev1 "k8s.io/api/core/v1" 5 | "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" 6 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 7 | "k8s.io/kube-openapi/pkg/util/proto" 8 | ) 9 | 10 | // NetworkPolicy describes the configuration options for the NetworkPolicy. 11 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 12 | type NetworkPolicy struct { 13 | metav1.TypeMeta `json:",inline"` 14 | metav1.ObjectMeta `json:"metadata"` 15 | 16 | Spec NetworkPolicySpec `json:"spec"` 17 | Status NetworkPolicyStatus `json:"status"` 18 | } 19 | 20 | // NetworkPolicyList is a list of NetworkPolicy CRDs. 21 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 22 | type NetworkPolicyList struct { 23 | metav1.TypeMeta `json:",inline"` 24 | metav1.ListMeta `json:"metadata"` 25 | Items []NetworkPolicy `json:"items"` 26 | } 27 | 28 | // NetworkPolicySpec describes the specification for Network. 29 | type NetworkPolicySpec struct { 30 | // Microservice represents the name of the Microservice which we want to 31 | // create DNS entries for. 32 | // If the Microservice Name is not provided, the name of the NetworkPolicy 33 | // CRD will be used. 34 | Microservice *corev1.LocalObjectReference `json:"microservice,omitempty"` 35 | 36 | // SessionAffinity lets you define a config for SessionAffinity. If no 37 | // config is provided, SessionAffinity will be "None". 38 | SessionAffinity *corev1.SessionAffinityConfig `json:"sessionAffinity"` 39 | 40 | // Ports which we want to be accessible for the associated Microservice. 41 | Ports []NetworkPort `json:"ports"` 42 | 43 | // ExternalDNS represents the domain specification for a Microservice 44 | // externally. 45 | ExternalDNS []ExternalDNS `json:"externalDNS"` 46 | 47 | // UpdateStrategy defines how Heighliner will transition DNS from one 48 | // version to another. 49 | UpdateStrategy UpdateStrategy `json:"updateStrategy"` 50 | } 51 | 52 | // NetworkPort describes a port that is exposed for a given service. 53 | type NetworkPort struct { 54 | // The name the port will be given. This will be used to link DNS entries. 55 | Name string `json:"name"` 56 | 57 | // The port that is exposed within the service container and which the 58 | // application is running on. 59 | TargetPort int32 `json:"targetPort"` 60 | 61 | // The port this service will be available on from within the cluster. 62 | Port int32 `json:"port"` 63 | } 64 | 65 | // ExternalDNS describes a DNS entry for a given service, allowing external 66 | // access to the service. 67 | // If no port is provided but a DNS entry is provided, a default headless port 68 | // will be created with the internalPort `8080`. 69 | type ExternalDNS struct { 70 | // IngressClass represents the class that is given to the Ingress controller 71 | // to handle DNS entries. This defaults to the default at the controller 72 | // configuration level. 73 | IngressClass string `json:"ingressClass"` 74 | 75 | // The domain name that will be linked to the service. This can be a full 76 | // fledged domain like `dashboard.heighliner.com` or it could be a templated 77 | // domain like `{.StreamName}.pr.heighliner.com`. Templated domains get 78 | // the data from a Release object, possible values are `Name`, `StreamName`, 79 | // and `FullName`. 80 | Domain string `json:"domain"` 81 | 82 | // TTL in seconds for the DNS entry, defaults to `300`. 83 | // Note: if multiple DNS entries are provided, the TTL of the first record 84 | // will be used. 85 | TTL int32 `json:"ttl"` 86 | 87 | // By default, TLS will be enabled for external access to a service. 88 | // Defaults to `false`. 89 | DisableTLS bool `json:"disableTLS"` 90 | 91 | // TLSGroup specifies the certificate group in which we'll store the SSL 92 | // Certificates. This defaults to "heighliner-components". It is recommended 93 | // to set this up per group of applications, this way the certificates will 94 | // be stored together. 95 | TLSGroup string `json:"tlsGroup"` 96 | 97 | // Port links back to a NetworkPort and will be used to guide traffic for 98 | // this hostname through the specified port. Defaults to `headless`. 99 | Port string `json:"port"` 100 | } 101 | 102 | // UpdateStrategy allows a strategy to be defined which will allow the 103 | // NetworkPolicy controller to determine when and how to transition from one 104 | // version to another for a specific Microservice. 105 | // The fields defined on each strategy will be used as label selectors to select 106 | // the correct VersionedMicroservice. 107 | type UpdateStrategy struct { 108 | Manual *ManualUpdateStrategy `json:"manual"` 109 | Latest *LatestUpdateStrategy `json:"latest"` 110 | } 111 | 112 | // ManualUpdateStrategy is an UpdateStrategy that is purely manual. The 113 | // Controller will put in the labels as provided and won't take any other action 114 | // to detect possible versions. 115 | type ManualUpdateStrategy struct { 116 | // SemVer is the SemVer annotation of the specific release we want to use 117 | // for this Microservice. 118 | SemVer *SemVerRelease `json:"semVer"` 119 | } 120 | 121 | // LatestUpdateStrategy will monitor the available release for a given 122 | // Microservice and use the latest available release to link to the internal and 123 | // external DNS. 124 | type LatestUpdateStrategy struct{} 125 | 126 | // NetworkPolicyStatus provides external domains and associated SemVer from the release 127 | type NetworkPolicyStatus struct { 128 | Domains []Domain `json:"domains"` 129 | } 130 | 131 | // Domain is represents a url associated with the NetworkPolicy and the associated SemVer 132 | type Domain struct { 133 | // Url is the url that points to the application 134 | URL string `json:"url"` 135 | 136 | // SemVer is the SemVer release object linked to this NetworkPolicyStatus if the 137 | // VersioningPolicy associated with it is SemVer. 138 | SemVer *SemVerRelease `json:"semVer,omitempty"` 139 | } 140 | 141 | // NetworkPolicyValidationSchema represents the OpenAPIV3Schema validation for 142 | // the NetworkPolicy CRD. 143 | var NetworkPolicyValidationSchema = &v1beta1.CustomResourceValidation{ 144 | OpenAPIV3Schema: &v1beta1.JSONSchemaProps{ 145 | Required: []string{"spec"}, 146 | Properties: map[string]v1beta1.JSONSchemaProps{ 147 | "spec": { 148 | Required: []string{"updateStrategy"}, 149 | Properties: map[string]v1beta1.JSONSchemaProps{ 150 | "ingressClass": { 151 | Type: proto.String, 152 | }, 153 | "ports": { 154 | Items: &v1beta1.JSONSchemaPropsOrArray{ 155 | Schema: &v1beta1.JSONSchemaProps{ 156 | Required: []string{"name", "targetPort", "port"}, 157 | }, 158 | }, 159 | }, 160 | "externalDNS": { 161 | Items: &v1beta1.JSONSchemaPropsOrArray{ 162 | Schema: &v1beta1.JSONSchemaProps{ 163 | Required: []string{"domain"}, 164 | Properties: map[string]v1beta1.JSONSchemaProps{ 165 | "ttl": { 166 | Type: proto.Integer, 167 | }, 168 | "disableTLS": { 169 | Type: proto.Boolean, 170 | }, 171 | "tlsGroup": { 172 | Type: proto.String, 173 | }, 174 | "port": { 175 | Type: proto.String, 176 | }, 177 | }, 178 | }, 179 | }, 180 | }, 181 | }, 182 | }, 183 | }, 184 | }, 185 | } 186 | -------------------------------------------------------------------------------- /apis/v1alpha1/release.go: -------------------------------------------------------------------------------- 1 | package v1alpha1 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/manifoldco/heighliner/internal/k8sutils" 7 | "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" 8 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 | ) 10 | 11 | // Release represents a specific release for a version of an image. 12 | type Release struct { 13 | // OwnerReferences represents who the owner is of this release. This will 14 | // be set by the Microservice controller and reference a 15 | // VersionedMicroservice. 16 | OwnerReferences []metav1.OwnerReference `json:"ownerReference,omitempty"` 17 | 18 | // Image is the fully qualified image name that can be used to download the 19 | // image. 20 | Image string `json:"image"` 21 | 22 | // ReleaseTime represents when this version became available to be deployed. 23 | ReleaseTime metav1.Time `json:"releaseTime"` 24 | 25 | // SemVer is the SemVer release object linked to this Release if the 26 | // VersioningPolicy associated with it is SemVer. 27 | SemVer *SemVerRelease `json:"semVer,omitempty"` 28 | 29 | // Level is the detected maturity level for this release 30 | Level SemVerLevel `json:"level"` 31 | } 32 | 33 | // String concatenates the Release values into a single unique string. 34 | func (r Release) String() string { 35 | return fmt.Sprintf("%s-%s", r.SemVer.String(), r.ReleaseTime) 36 | } 37 | 38 | // StreamName creates the release stream name for a release. It takes the name 39 | // of a Microservice as a prefix. 40 | // 41 | // Stream names are based on release level: 42 | // release - `` 43 | // candidate - `-rc` 44 | // preview - `-pr- 45 | func (r Release) StreamName(prefix string) string { 46 | if r.SemVer != nil { 47 | switch r.Level { 48 | case SemVerLevelRelease: 49 | return prefix 50 | case SemVerLevelReleaseCandidate: 51 | return fmt.Sprintf("%s-rc", prefix) 52 | case SemVerLevelPreview: 53 | return fmt.Sprintf("%s-pr-%s", prefix, k8sutils.ShortHash(r.SemVer.Name, 8)) 54 | default: 55 | panic("Unknown SemVerLevel") 56 | } 57 | } 58 | 59 | panic("No release type specified") 60 | } 61 | 62 | // FullName creates the full name for a release. This is the stream name 63 | // suffixed by a version derived hash. 64 | // Microservice as a prefix. 65 | func (r Release) FullName(prefix string) string { 66 | if r.SemVer != nil { 67 | return fmt.Sprintf("%s-%s", r.StreamName(prefix), k8sutils.ShortHash(r.SemVer.Version, 8)) 68 | } 69 | 70 | panic("No release type specified") 71 | } 72 | 73 | // Name returns the name of the actual version. 74 | func (r Release) Name() string { 75 | if r.SemVer != nil { 76 | return r.SemVer.Name 77 | } 78 | 79 | panic("No release type specified") 80 | } 81 | 82 | // Version returns the version of the release. 83 | func (r Release) Version() string { 84 | if r.SemVer != nil { 85 | return r.SemVer.Version 86 | } 87 | 88 | panic("No release type specified") 89 | } 90 | 91 | // SemVerRelease represents a release which is linked to a SemVer 92 | // VersioningPolicy. 93 | type SemVerRelease struct { 94 | // Name represents the name of the service to be released. For releases and 95 | // release candidates, this will be the name of the application, for 96 | // previews this will be the preview tag (generally the branch name). 97 | Name string `json:"name"` 98 | 99 | // Version is the specific version for this release in a SemVer annotation. 100 | Version string `json:"version"` 101 | } 102 | 103 | // String concatenates the SemVer Release values into a single unique string. 104 | func (r *SemVerRelease) String() string { 105 | return fmt.Sprintf("%s-%s", r.Name, r.Version) 106 | } 107 | 108 | // ReleaseValidationSchema represents the OpenAPIv3 validation schema for a 109 | // release object. 110 | var ReleaseValidationSchema = v1beta1.JSONSchemaProps{ 111 | Properties: map[string]v1beta1.JSONSchemaProps{ 112 | "releases": { 113 | Required: []string{"semVer", "image", "releaseTime"}, 114 | Properties: map[string]v1beta1.JSONSchemaProps{ 115 | "semVer": semVerReleaseValidation, 116 | }, 117 | }, 118 | }, 119 | } 120 | 121 | var semVerReleaseValidation = v1beta1.JSONSchemaProps{ 122 | Required: []string{"version"}, 123 | } 124 | -------------------------------------------------------------------------------- /apis/v1alpha1/release_test.go: -------------------------------------------------------------------------------- 1 | package v1alpha1 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestReleaseNaming(t *testing.T) { 8 | t.Run("with SemVer Release", func(t *testing.T) { 9 | tcs := []struct { 10 | tcName string 11 | name string 12 | semVerName string 13 | semVerVersion string 14 | semVerLevel SemVerLevel 15 | streamName string 16 | fullName string 17 | }{ 18 | {"release level", "hello-world", "hello-world", "v1.2.3", SemVerLevelRelease, "hello-world", "hello-world-hqo6t73v"}, 19 | {"release level ignores semver name", "hello-world", "other-world", "v1.2.3", SemVerLevelRelease, "hello-world", "hello-world-hqo6t73v"}, 20 | {"release level full name uses version", "hello-world", "hello-world", "v1.2.4", SemVerLevelRelease, "hello-world", "hello-world-ubdj93q6"}, 21 | 22 | {"candidate level", "hello-world", "hello-world", "v1.2.3", SemVerLevelReleaseCandidate, "hello-world-rc", "hello-world-rc-hqo6t73v"}, 23 | {"candidate level ignores semver name", "hello-world", "other-world", "v1.2.3", SemVerLevelReleaseCandidate, "hello-world-rc", "hello-world-rc-hqo6t73v"}, 24 | {"candidate level full name uses version", "hello-world", "hello-world", "v1.2.4", SemVerLevelReleaseCandidate, "hello-world-rc", "hello-world-rc-ubdj93q6"}, 25 | 26 | {"pr level", "hello-world", "hello-world", "v1.2.3", SemVerLevelPreview, "hello-world-pr-cmqolv9f", "hello-world-pr-cmqolv9f-hqo6t73v"}, 27 | {"pr level uses semver name", "hello-world", "other-world", "v1.2.3", SemVerLevelPreview, "hello-world-pr-hulm66p0", "hello-world-pr-hulm66p0-hqo6t73v"}, 28 | {"pr level full name uses version", "hello-world", "hello-world", "v1.2.4", SemVerLevelPreview, "hello-world-pr-cmqolv9f", "hello-world-pr-cmqolv9f-ubdj93q6"}, 29 | } 30 | 31 | for _, tc := range tcs { 32 | release := &Release{ 33 | SemVer: &SemVerRelease{ 34 | Name: tc.semVerName, 35 | Version: tc.semVerVersion, 36 | }, 37 | Level: tc.semVerLevel, 38 | } 39 | 40 | t.Run(tc.tcName+" (stream name)", func(t *testing.T) { 41 | if name := release.StreamName(tc.name); name != tc.streamName { 42 | t.Errorf("Expected '%s', got '%s'", tc.streamName, name) 43 | } 44 | }) 45 | 46 | t.Run(tc.tcName+" (full name)", func(t *testing.T) { 47 | if name := release.FullName(tc.name); name != tc.fullName { 48 | t.Errorf("Expected '%s', got '%s'", tc.fullName, name) 49 | } 50 | }) 51 | } 52 | }) 53 | } 54 | -------------------------------------------------------------------------------- /apis/v1alpha1/scheme.go: -------------------------------------------------------------------------------- 1 | package v1alpha1 2 | 3 | import ( 4 | "k8s.io/api/rbac/v1alpha1" 5 | "k8s.io/apimachinery/pkg/apis/meta/v1" 6 | "k8s.io/apimachinery/pkg/runtime" 7 | "k8s.io/apimachinery/pkg/runtime/schema" 8 | ) 9 | 10 | const ( 11 | // GroupName defines the name of the group we'll use for our components. 12 | GroupName = "hlnr.io" 13 | 14 | // Version defines the version of this API. 15 | Version = "v1alpha1" 16 | ) 17 | 18 | var ( 19 | // SchemeBuilder for the svc CRD 20 | SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes) 21 | 22 | // AddToScheme method for the svc CRD 23 | AddToScheme = SchemeBuilder.AddToScheme 24 | 25 | // SchemeGroupVersion is the Group Version used for this scheme. 26 | SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: Version} 27 | ) 28 | 29 | // addKnownTypes adds the set of types defined in this package to the supplied scheme. 30 | func addKnownTypes(scheme *runtime.Scheme) error { 31 | scheme.AddKnownTypes( 32 | SchemeGroupVersion, 33 | &Microservice{}, 34 | &MicroserviceList{}, 35 | &VersionedMicroservice{}, 36 | &VersionedMicroserviceList{}, 37 | &ImagePolicy{}, 38 | &ImagePolicyList{}, 39 | &NetworkPolicy{}, 40 | &NetworkPolicyList{}, 41 | &AvailabilityPolicy{}, 42 | &AvailabilityPolicyList{}, 43 | &VersioningPolicy{}, 44 | &VersioningPolicyList{}, 45 | &ConfigPolicy{}, 46 | &ConfigPolicyList{}, 47 | &SecurityPolicy{}, 48 | &SecurityPolicyList{}, 49 | &GitHubRepository{}, 50 | &GitHubRepositoryList{}, 51 | &HealthPolicy{}, 52 | &HealthPolicyList{}, 53 | ) 54 | 55 | v1.AddToGroupVersion(scheme, v1alpha1.SchemeGroupVersion) 56 | return nil 57 | } 58 | -------------------------------------------------------------------------------- /apis/v1alpha1/security_policy.go: -------------------------------------------------------------------------------- 1 | package v1alpha1 2 | 3 | import ( 4 | corev1 "k8s.io/api/core/v1" 5 | "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" 6 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 7 | ) 8 | 9 | // SecurityPolicy describes the configuration options for the SecurityPolicy. 10 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 11 | type SecurityPolicy struct { 12 | metav1.TypeMeta `json:",inline"` 13 | metav1.ObjectMeta `json:"metadata"` 14 | 15 | Spec SecurityPolicySpec `json:"spec"` 16 | } 17 | 18 | // SecurityPolicyList is a list of SecurityPolicy CRDs. 19 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 20 | type SecurityPolicyList struct { 21 | metav1.TypeMeta `json:",inline"` 22 | metav1.ListMeta `json:"metadata"` 23 | Items []SecurityPolicy `json:"items"` 24 | } 25 | 26 | // SecurityPolicySpec describes the specification for Security. 27 | type SecurityPolicySpec struct { 28 | ServiceAccountName string `json:"serviceAccountName,omitempty"` 29 | AutomountServiceAccountToken bool `json:"automountServiceAccountToken,omitempty"` 30 | SecurityContext *corev1.PodSecurityContext `json:"securityContext,omitempty"` 31 | } 32 | 33 | // SecurityPolicyValidationSchema represents the OpenAPIV3Schema validation for 34 | // the SecurityPolicy CRD. 35 | var SecurityPolicyValidationSchema = &v1beta1.CustomResourceValidation{ 36 | OpenAPIV3Schema: &v1beta1.JSONSchemaProps{}, 37 | } 38 | -------------------------------------------------------------------------------- /apis/v1alpha1/versioned_microservice.go: -------------------------------------------------------------------------------- 1 | package v1alpha1 2 | 3 | import ( 4 | "github.com/manifoldco/heighliner/internal/k8sutils" 5 | 6 | corev1 "k8s.io/api/core/v1" 7 | apiextv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" 8 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 | ) 10 | 11 | // VersionedMicroservice represents the combined state of different components 12 | // in time which form a single Microservice. 13 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 14 | type VersionedMicroservice struct { 15 | metav1.TypeMeta `json:",inline"` 16 | metav1.ObjectMeta `json:"metadata"` 17 | 18 | Spec VersionedMicroserviceSpec `json:"spec"` 19 | } 20 | 21 | // VersionedMicroserviceList is a list of VersionedMicroservices. 22 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 23 | type VersionedMicroserviceList struct { 24 | metav1.TypeMeta `json:",inline"` 25 | metav1.ListMeta `json:"metadata"` 26 | Items []VersionedMicroservice `json:"items"` 27 | } 28 | 29 | // VersionedMicroserviceSpec represents the specification for a 30 | // VersionedMicroservice. 31 | type VersionedMicroserviceSpec struct { 32 | Availability *AvailabilityPolicySpec `json:"availability,omitempty"` 33 | Config *ConfigPolicySpec `json:"config,omitempty"` 34 | Security *SecurityPolicySpec `json:"security,omitempty"` 35 | Containers []corev1.Container `json:"containers"` 36 | ImagePullSecrets []corev1.LocalObjectReference `json:"imagePullSecrets"` 37 | } 38 | 39 | // VersionedMicroserviceValidationSchema represents the OpenAPIV3Scheme which 40 | // defines the validation for the VersionedMicroserviceSpec. 41 | var VersionedMicroserviceValidationSchema = apiextv1beta1.JSONSchemaProps{ 42 | Properties: map[string]apiextv1beta1.JSONSchemaProps{ 43 | "availability": AvailabilityPolicyValidationSchema, 44 | "config": *ConfigPolicyValidationSchema.OpenAPIV3Schema, 45 | "security": *SecurityPolicyValidationSchema.OpenAPIV3Schema, 46 | "containers": { 47 | MinItems: k8sutils.PtrInt64(1), 48 | }, 49 | }, 50 | Required: []string{ 51 | "containers", 52 | }, 53 | } 54 | -------------------------------------------------------------------------------- /apis/v1alpha1/versioning_policy.go: -------------------------------------------------------------------------------- 1 | package v1alpha1 2 | 3 | import ( 4 | "github.com/manifoldco/heighliner/internal/k8sutils" 5 | apiextv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" 6 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 7 | "k8s.io/kube-openapi/pkg/util/proto" 8 | ) 9 | 10 | // VersioningPolicy describes the configuration options for the VersioningPolicy. 11 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 12 | type VersioningPolicy struct { 13 | metav1.TypeMeta `json:",inline"` 14 | metav1.ObjectMeta `json:"metadata"` 15 | 16 | Spec VersioningPolicySpec `json:"spec"` 17 | } 18 | 19 | // VersioningPolicyList is a list of VersioningPolicy CRDs. 20 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 21 | type VersioningPolicyList struct { 22 | metav1.TypeMeta `json:",inline"` 23 | metav1.ListMeta `json:"metadata"` 24 | Items []VersioningPolicy `json:"items"` 25 | } 26 | 27 | // VersioningPolicySpec describes the specification for Versioning. 28 | type VersioningPolicySpec struct { 29 | SemVer *SemVerSource `json:"semVer"` 30 | } 31 | 32 | type ( 33 | // SemVerLevel indicates a level which we want to monitor the image registry 34 | // for. It should be in the format of format. 35 | // Examples: 36 | // v1.2.3, v1.2.4-rc.0, v1.2.4-pr.1+201804011533 37 | // 1.2.3, 1.2.4-rc.0, 1.2.4-pr.1+201804011533 38 | SemVerLevel string 39 | 40 | // SemVerVersion represents the type of version we want to monitor for. 41 | SemVerVersion string 42 | ) 43 | 44 | var ( 45 | // SemVerLevelRelease is used for a release that is ready to be rolled out 46 | // to production. 47 | SemVerLevelRelease SemVerLevel = "release" 48 | 49 | // SemVerLevelReleaseCandidate is used for a release-candidate that is ready 50 | // for QA. 51 | SemVerLevelReleaseCandidate SemVerLevel = "candidate" 52 | 53 | // SemVerLevelPreview is used for a preview release. This is generally 54 | // associated with development deploys. 55 | SemVerLevelPreview SemVerLevel = "preview" 56 | 57 | // SemVerVersionMajor indicates that we will release major, minor and patch 58 | // releases. 59 | SemVerVersionMajor SemVerVersion = "major" 60 | 61 | // SemVerVersionMinor indicates that we will release minor and patch 62 | // releases. 63 | SemVerVersionMinor SemVerVersion = "minor" 64 | 65 | // SemVerVersionPatch indicates that we will release only patch releases. 66 | SemVerVersionPatch SemVerVersion = "patch" 67 | ) 68 | 69 | // SemVerSource is a versioning policy based on semver. 70 | // When semver is selected, Heighliner can watch for images on 3 different 71 | // levels. 72 | type SemVerSource struct { 73 | // Version is the type of Version we want to start tracking with this 74 | // Policy. 75 | Version SemVerVersion `json:"version"` 76 | 77 | // Level is the level we want to fetch images for this Microservice for. 78 | Level SemVerLevel `json:"level"` 79 | 80 | // MinVersion is the minimum version that we want to track for this Policy. 81 | MinVersion string `json:"minVersion"` 82 | } 83 | 84 | // VersioningPolicyValidationSchema represents the OpenAPIV3Schema validation for 85 | // the NetworkPolicy CRD. 86 | var VersioningPolicyValidationSchema = apiextv1beta1.JSONSchemaProps{ 87 | Properties: map[string]apiextv1beta1.JSONSchemaProps{ 88 | "semVer": { 89 | Properties: map[string]apiextv1beta1.JSONSchemaProps{ 90 | "version": { 91 | Type: proto.String, 92 | Enum: []apiextv1beta1.JSON{ 93 | {Raw: k8sutils.JSONBytes(SemVerVersionMajor)}, 94 | {Raw: k8sutils.JSONBytes(SemVerVersionMinor)}, 95 | {Raw: k8sutils.JSONBytes(SemVerVersionPatch)}, 96 | }, 97 | }, 98 | "level": { 99 | Type: proto.String, 100 | Enum: []apiextv1beta1.JSON{ 101 | {Raw: k8sutils.JSONBytes(SemVerLevelRelease)}, 102 | {Raw: k8sutils.JSONBytes(SemVerLevelReleaseCandidate)}, 103 | {Raw: k8sutils.JSONBytes(SemVerLevelPreview)}, 104 | }, 105 | }, 106 | }, 107 | Required: []string{"version", "level"}, 108 | }, 109 | }, 110 | } 111 | -------------------------------------------------------------------------------- /boilerplate.go.txt: -------------------------------------------------------------------------------- 1 | /* 2 | BSD 3-Clause License 3 | 4 | Copyright (c) YEAR, Arigato Machine Inc. 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without 8 | modification, are permitted provided that the following conditions are met: 9 | 10 | * Redistributions of source code must retain the above copyright notice, this 11 | list of conditions and the following disclaimer. 12 | 13 | * Redistributions in binary form must reproduce the above copyright notice, 14 | this list of conditions and the following disclaimer in the documentation 15 | and/or other materials provided with the distribution. 16 | 17 | * Neither the name of the copyright holder nor the names of its 18 | contributors may be used to endorse or promote products derived from 19 | this software without specific prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 22 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 23 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 25 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 26 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 27 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 28 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 29 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | */ 32 | -------------------------------------------------------------------------------- /cmd/heighliner/config_policy.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | 7 | "github.com/jelmersnoeck/kubekit" 8 | flags "github.com/jessevdk/go-flags" 9 | "github.com/manifoldco/heighliner/internal/configpolicy" 10 | 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | var ( 15 | cpwCmd = &cobra.Command{ 16 | Use: "config-policy-watcher", 17 | Aliases: []string{"cpw"}, 18 | Short: "Run the ConfigPolicy Watcher", 19 | RunE: cpwCommand, 20 | } 21 | 22 | cpwFlags struct { 23 | Namespace string `long:"namespace" env:"NAMESPACE" description:"The namespace to run the controller in. By default we'll watch all namespaces."` 24 | } 25 | ) 26 | 27 | func cpwCommand(cmd *cobra.Command, args []string) error { 28 | if _, err := flags.ParseArgs(&cpwFlags, append(args, os.Args...)); err != nil { 29 | log.Printf("Could not parse flags: %s", err) 30 | return err 31 | } 32 | 33 | cfg, cs, acs, err := kubekit.InClusterClientsets() 34 | if err != nil { 35 | log.Printf("Could not get Clientset: %s\n", err) 36 | return err 37 | } 38 | 39 | if err := kubekit.CreateCRD(acs, configpolicy.ConfigPolicyResource); err != nil { 40 | log.Printf("Could not create ConfigPolicy CRD: %s\n", err) 41 | return err 42 | } 43 | 44 | ctrl, err := configpolicy.NewController(cfg, cs, cpwFlags.Namespace) 45 | if err != nil { 46 | log.Printf("Could not create controller: %s\n", err) 47 | return err 48 | } 49 | 50 | if err := ctrl.Run(); err != nil { 51 | log.Printf("Error running controller: %s\n", err) 52 | return err 53 | } 54 | 55 | return nil 56 | } 57 | 58 | func init() { 59 | rootCmd.AddCommand(cpwCmd) 60 | } 61 | -------------------------------------------------------------------------------- /cmd/heighliner/github_repository.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "time" 7 | 8 | "github.com/jelmersnoeck/kubekit" 9 | flags "github.com/jessevdk/go-flags" 10 | "github.com/manifoldco/heighliner/internal/githubrepository" 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | var ( 15 | ghpcCmd = &cobra.Command{ 16 | Use: "github-repository-controller", 17 | Aliases: []string{"ghrc"}, 18 | Short: "Run the GitHub Repository Controller", 19 | RunE: ghpcCommand, 20 | } 21 | 22 | ghpcFlags struct { 23 | Namespace string `long:"namespace" env:"NAMESPACE" description:"The namespace we'll watch for CRDs. By default we'll watch all namespaces."` 24 | Domain string `long:"domain" env:"DOMAIN" description:"The domain name used for callbacks" required:"true"` 25 | InsecureSSL bool `long:"insecure-ssl" env:"INSECURE_SSL" description:"Allow insecure callbacks to the webhook"` 26 | CallbackPort string `long:"callback-port" env:"CALLBACK_PORT" description:"The port to run the callbacks server on" default:":8080"` 27 | ReconciliationPeriod string `long:"reconciliation-period" env:"RECONCILIATION_PERIOD" description:"How often the controller should check for Github changes missed by webhooks" default:"10m"` 28 | } 29 | ) 30 | 31 | func ghpcCommand(cmd *cobra.Command, args []string) error { 32 | if _, err := flags.ParseArgs(&ghpcFlags, append(args, os.Args...)); err != nil { 33 | log.Printf("Could not parse flags: %s", err) 34 | return err 35 | } 36 | 37 | rcfg, cs, acs, err := kubekit.InClusterClientsets() 38 | if err != nil { 39 | log.Printf("Could not get Clientset: %s\n", err) 40 | return err 41 | } 42 | 43 | if err := kubekit.CreateCRD(acs, githubrepository.GitHubRepositoryResource); err != nil { 44 | log.Printf("Could not create GitHubRepository CRD: %s\n", err) 45 | return err 46 | } 47 | 48 | period, err := time.ParseDuration(ghpcFlags.ReconciliationPeriod) 49 | if err != nil { 50 | log.Printf("Could not parse Reconciation Period duration %s: %s\n", 51 | ghpcFlags.ReconciliationPeriod, err) 52 | return err 53 | } 54 | 55 | cfg := githubrepository.Config{ 56 | Domain: ghpcFlags.Domain, 57 | InsecureSSL: ghpcFlags.InsecureSSL, 58 | CallbackPort: ghpcFlags.CallbackPort, 59 | ReconciliationPeriod: period, 60 | } 61 | 62 | ctrl, err := githubrepository.NewController(rcfg, cs, ghpcFlags.Namespace, cfg) 63 | if err != nil { 64 | log.Printf("Could not create controller: %s\n", err) 65 | return err 66 | } 67 | 68 | if err := ctrl.Run(); err != nil { 69 | log.Printf("Error running controller: %s\n", err) 70 | return err 71 | } 72 | 73 | return nil 74 | } 75 | 76 | func init() { 77 | rootCmd.AddCommand(ghpcCmd) 78 | } 79 | -------------------------------------------------------------------------------- /cmd/heighliner/image_policy.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | 7 | "github.com/jelmersnoeck/kubekit" 8 | flags "github.com/jessevdk/go-flags" 9 | "github.com/manifoldco/heighliner/internal/imagepolicy" 10 | _ "github.com/manifoldco/heighliner/internal/registry/hub" 11 | 12 | "github.com/spf13/cobra" 13 | ) 14 | 15 | var ( 16 | ipcCmd = &cobra.Command{ 17 | Use: "ipc", 18 | Short: "Run the Image Policy Controller", 19 | RunE: ipcCommand, 20 | } 21 | 22 | ipcFlags struct { 23 | Namespace string `long:"namespace" env:"NAMESPACE" description:"The namespace to run the controller in. By default we'll watch all namespaces."` 24 | } 25 | ) 26 | 27 | func ipcCommand(cmd *cobra.Command, args []string) error { 28 | if _, err := flags.ParseArgs(&ipcFlags, append(args, os.Args...)); err != nil { 29 | log.Printf("Could not parse flags: %s", err) 30 | return err 31 | } 32 | 33 | cfg, cs, acs, err := kubekit.InClusterClientsets() 34 | if err != nil { 35 | log.Printf("Could not get Clientset: %s\n", err) 36 | return err 37 | } 38 | 39 | if err := kubekit.CreateCRD(acs, imagepolicy.ImagePolicyResource); err != nil { 40 | log.Printf("Could not create ImagePolicy CRD: %s\n", err) 41 | return err 42 | } 43 | 44 | ctrl, err := imagepolicy.NewController(cfg, cs, ipcFlags.Namespace) 45 | if err != nil { 46 | log.Printf("Could not create controller: %s\n", err) 47 | return err 48 | } 49 | 50 | if err := ctrl.Run(); err != nil { 51 | log.Printf("Error running controller: %s\n", err) 52 | return err 53 | } 54 | 55 | return nil 56 | } 57 | 58 | func init() { 59 | rootCmd.AddCommand(ipcCmd) 60 | } 61 | -------------------------------------------------------------------------------- /cmd/heighliner/install.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "html/template" 6 | "os" 7 | 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | const namespaceName = "docs/kube/00-heighliner-namespace.yaml" 12 | 13 | var ( 14 | installCmd = &cobra.Command{ 15 | Use: "install", 16 | Short: "Create installation Manifests to install Heighliner in your Kubernetes cluster.", 17 | RunE: installCommand, 18 | } 19 | 20 | installFlags struct { 21 | GitHubCallbackDomain string 22 | Version string 23 | DNSProvider string 24 | } 25 | ) 26 | 27 | func installCommand(cmd *cobra.Command, args []string) error { 28 | data := bytes.NewBuffer(nil) 29 | 30 | // the namespace should be created first, otherwise all other components 31 | // won't be installed. 32 | nsData, err := Asset(namespaceName) 33 | if err != nil { 34 | return err 35 | } 36 | data.Write(nsData) 37 | 38 | for _, name := range AssetNames() { 39 | if name == namespaceName { 40 | continue 41 | } 42 | 43 | assetData, err := Asset(name) 44 | if err != nil { 45 | return err 46 | } 47 | 48 | data.Write([]byte("\n---\n")) 49 | data.Write(assetData) 50 | } 51 | 52 | tpl, err := template.New("heighliner-install").Parse(data.String()) 53 | if err != nil { 54 | return err 55 | } 56 | 57 | if err := tpl.Execute(os.Stdout, installFlags); err != nil { 58 | return err 59 | } 60 | 61 | return nil 62 | } 63 | 64 | func init() { 65 | installCmd.Flags().StringVar(&installFlags.GitHubCallbackDomain, "github-callback-domain", "", "The domain used for GitHub to do callbacks to") 66 | installCmd.MarkFlagRequired("github-callback-url") 67 | installCmd.Flags().StringVar(&installFlags.Version, "version", "latest", "The version of Heighliner to install") 68 | installCmd.Flags().StringVar(&installFlags.DNSProvider, "dns-provider", "route53-dns", "The DNS Provider configured through ExternalDNS") 69 | 70 | rootCmd.AddCommand(installCmd) 71 | } 72 | -------------------------------------------------------------------------------- /cmd/heighliner/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | var rootCmd = &cobra.Command{ 10 | Use: "heighliner", 11 | Hidden: true, 12 | } 13 | 14 | func main() { 15 | if err := rootCmd.Execute(); err != nil { 16 | fmt.Printf("heighliner error: %s\n", err) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /cmd/heighliner/network_policy.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | 7 | "github.com/jelmersnoeck/kubekit" 8 | flags "github.com/jessevdk/go-flags" 9 | "github.com/manifoldco/heighliner/internal/networkpolicy" 10 | 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | var ( 15 | npwCmd = &cobra.Command{ 16 | Use: "network-policy-watcher", 17 | Aliases: []string{"npw"}, 18 | Short: "Run the NetworkPolicy Watcher", 19 | RunE: npwCommand, 20 | } 21 | 22 | npwFlags struct { 23 | Namespace string `long:"namespace" env:"NAMESPACE" description:"The namespace we'll watch for CRDs. By default we'll watch all namespaces."` 24 | IngressClass string `long:"ingress-class" env:"HLNR_INGRESS_CLASS" description:"The default ingress class which will be used by Ingresses to have external DNS handled." default:"nginx"` 25 | } 26 | ) 27 | 28 | func npwCommand(cmd *cobra.Command, args []string) error { 29 | if _, err := flags.ParseArgs(&npwFlags, append(args, os.Args...)); err != nil { 30 | log.Printf("Could not parse flags: %s", err) 31 | return err 32 | } 33 | 34 | cfg, cs, acs, err := kubekit.InClusterClientsets() 35 | if err != nil { 36 | log.Printf("Could not get Clientset: %s\n", err) 37 | return err 38 | } 39 | 40 | if err := kubekit.CreateCRD(acs, networkpolicy.NetworkPolicyResource); err != nil { 41 | log.Printf("Could not create NetworkPolicy CRD: %s\n", err) 42 | return err 43 | } 44 | 45 | if err := kubekit.CreateCRD(acs, networkpolicy.VersioningPolicyResource); err != nil { 46 | log.Printf("Could not create VersioningPolicy CRD: %s\n", err) 47 | return err 48 | } 49 | 50 | ctrl, err := networkpolicy.NewController(cfg, cs, npwFlags.Namespace) 51 | if err != nil { 52 | log.Printf("Could not create controller: %s\n", err) 53 | return err 54 | } 55 | 56 | if err := ctrl.Run(); err != nil { 57 | log.Printf("Error running controller: %s\n", err) 58 | return err 59 | } 60 | 61 | return nil 62 | } 63 | 64 | func init() { 65 | rootCmd.AddCommand(npwCmd) 66 | } 67 | -------------------------------------------------------------------------------- /cmd/heighliner/svc.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | 7 | "github.com/manifoldco/heighliner/internal/svc" 8 | 9 | "github.com/jelmersnoeck/kubekit" 10 | flags "github.com/jessevdk/go-flags" 11 | 12 | "github.com/spf13/cobra" 13 | ) 14 | 15 | var ( 16 | svcCmd = &cobra.Command{ 17 | Use: "msvc", 18 | Short: "Run the Microservice Controller", 19 | RunE: svcCommand, 20 | } 21 | 22 | svcFlags struct { 23 | Namespace string `long:"namespace" env:"NAMESPACE" description:"The namespace to run the controller in. By default we'll watch all namespaces."` 24 | } 25 | ) 26 | 27 | func svcCommand(cmd *cobra.Command, args []string) error { 28 | if _, err := flags.ParseArgs(&svcFlags, append(args, os.Args...)); err != nil { 29 | log.Printf("Could not parse flags: %s", err) 30 | return err 31 | } 32 | 33 | cfg, cs, acs, err := kubekit.InClusterClientsets() 34 | if err != nil { 35 | log.Printf("Could not get Clientset: %s\n", err) 36 | return err 37 | } 38 | 39 | if err := kubekit.CreateCRD(acs, svc.CustomResource); err != nil { 40 | log.Printf("Could not create Microservice CRD: %s\n", err) 41 | return err 42 | } 43 | 44 | if err := kubekit.CreateCRD(acs, svc.AvailabilityPolicyResource); err != nil { 45 | log.Printf("Could not create AvailabilityPolicy CRD: %s\n", err) 46 | return err 47 | } 48 | 49 | if err := kubekit.CreateCRD(acs, svc.HealthPolicyResource); err != nil { 50 | log.Printf("Could not create HealthPolicy CRD: %s\n", err) 51 | return err 52 | } 53 | 54 | if err := kubekit.CreateCRD(acs, svc.SecurityPolicyResource); err != nil { 55 | log.Printf("Could not create SecurityPolicy CRD: %s\n", err) 56 | return err 57 | } 58 | 59 | ctrl, err := svc.NewController(cfg, cs, svcFlags.Namespace) 60 | if err != nil { 61 | log.Printf("Could not create controller: %s\n", err) 62 | return err 63 | } 64 | 65 | if err := ctrl.Run(); err != nil { 66 | log.Printf("Error running controller: %s\n", err) 67 | return err 68 | } 69 | 70 | return nil 71 | } 72 | 73 | func init() { 74 | rootCmd.AddCommand(svcCmd) 75 | } 76 | -------------------------------------------------------------------------------- /cmd/heighliner/vsvc.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | 7 | "github.com/manifoldco/heighliner/internal/vsvc" 8 | 9 | "github.com/jelmersnoeck/kubekit" 10 | flags "github.com/jessevdk/go-flags" 11 | 12 | "github.com/spf13/cobra" 13 | ) 14 | 15 | var ( 16 | vsvcCmd = &cobra.Command{ 17 | Use: "vsvc", 18 | Short: "Run the VersionedMicroservice Controller", 19 | RunE: vsvcCommand, 20 | } 21 | 22 | vsvcFlags struct { 23 | Namespace string `long:"namespace" env:"NAMESPACE" description:"The namespace to run the controller in. By default we'll watch all namespaces."` 24 | } 25 | ) 26 | 27 | func vsvcCommand(cmd *cobra.Command, args []string) error { 28 | if _, err := flags.ParseArgs(&vsvcFlags, append(args, os.Args...)); err != nil { 29 | log.Printf("Could not parse flags: %s", err) 30 | return err 31 | } 32 | 33 | cfg, cs, acs, err := kubekit.InClusterClientsets() 34 | if err != nil { 35 | log.Printf("Could not get Clientset: %s\n", err) 36 | return err 37 | } 38 | 39 | if err := kubekit.CreateCRD(acs, vsvc.CustomResource); err != nil { 40 | log.Printf("Could not create CRD: %s\n", err) 41 | return err 42 | } 43 | 44 | ctrl, err := vsvc.NewController(cfg, cs, vsvcFlags.Namespace) 45 | if err != nil { 46 | log.Printf("Could not create controller: %s\n", err) 47 | return err 48 | } 49 | 50 | if err := ctrl.Run(); err != nil { 51 | log.Printf("Error running controller: %s\n", err) 52 | return err 53 | } 54 | 55 | return nil 56 | } 57 | 58 | func init() { 59 | rootCmd.AddCommand(vsvcCmd) 60 | } 61 | -------------------------------------------------------------------------------- /docs/design/README.md: -------------------------------------------------------------------------------- 1 | # Heighliner Design Decisions 2 | 3 | Heighliner has a number of moving parts. In the documents listed below, we'll 4 | describe our design decisions regarding these moving parts and explain what each 5 | component does and why we've architected it this way. 6 | 7 | ![Heighliner Architectural Overview](overview.png) 8 | 9 | - [Microservice](./microservice.md) 10 | - [Versioned Microservice](./versioned-microservice.md) 11 | - [Image Policy](./image-policy.md) 12 | - [Versioning Policy](./versioning-policy.md) 13 | - [GitHub Connector](./github-connector.md) 14 | - [Config Policy](./config-policy.md) 15 | - [Network Policy](./network-policy.md) 16 | -------------------------------------------------------------------------------- /docs/design/availability-policy.md: -------------------------------------------------------------------------------- 1 | # Availability Policy 2 | -------------------------------------------------------------------------------- /docs/design/config-policy.md: -------------------------------------------------------------------------------- 1 | # Config Policy 2 | 3 | The Config Policy within Heighliner exists out of multiple parts and is supposed 4 | to be pretty flexible. 5 | 6 | ## Definitions 7 | 8 | We'll provide several ways to specify Config Policies for a resource. In this 9 | section, we'll go over both possible solutions. 10 | 11 | ### CRDs 12 | 13 | The first solution is using regular CRDs. Here we'll define a set of 14 | configuration references like one could do on a Kubernetes Deployment. This 15 | exists out of `ConfigMap`, `Secret` and `Volume`. When these sections are 16 | defined within a Custom Resource Definition, it can be detected by the 17 | Microservice controller and will be parsed into the right format for a 18 | VersionedMicroservice. 19 | 20 | ### Annotations 21 | 22 | It's also possible to set up `Secret` and `ConfigMap` resources individually and 23 | annotate them with the `hlnr.io/config-policy: ` annotation. This will 24 | then be pulled into an auto-generated ConfigPolicy which can then be used 25 | by the Microservice controller. 26 | 27 | This can be useful for when your secrets are automatically generated through 28 | controllers like the [Manifold Credentials Controller](https://github.com/manifoldco/kubernetes-credentials). 29 | 30 | *Note*: volumes have to be defined through CRDs. 31 | 32 | ### Mixture 33 | 34 | It's also possible to mix both scenarios. This can be useful for combining 35 | volumes and secrets. 36 | 37 | ## Controller 38 | 39 | The Config Policy controller is responsible for watching all the associated 40 | secrets with a definition and mark itself as being updated or not for the 41 | Microservice controller. This will be done by setting up a `last-updated` status 42 | which the Microservice controller can use to validate it's own state against. 43 | 44 | This controller is also responsible for detecting new definitions based on 45 | annotations. 46 | -------------------------------------------------------------------------------- /docs/design/full-flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manifoldco/heighliner/3f4bdcf679d03daef3d639b23e9595aa3a90394a/docs/design/full-flow.png -------------------------------------------------------------------------------- /docs/design/github-connector.md: -------------------------------------------------------------------------------- 1 | # GitHub Connector 2 | 3 | The GitHub connector is an essential part in the workflow pipeline. It's the 4 | part that knows about what releases are available on GH and which Pull Requests 5 | have been opened. 6 | 7 | To do this, there is a CRD installed which allows you to specify a repository 8 | and credentials to connect to this repository. The connector will configure the 9 | appropriate webhooks with this repository to get callback information based on 10 | Release and PullRequest events. 11 | 12 | The connector also takes care of setting up a callback server, allowing GitHub 13 | to send these events to the cluster. Once a new Release or PullRequest is 14 | detected, it will be stored accordingly to the associated GitHub Repository CRD. 15 | 16 | GitHub webhooks are not retried in case of failure, to mitigate any synchronization 17 | problems, the connector tries to reconciliate its known releases with GitHub's list 18 | of releases and opened pull requests every 10 minutes. This period can be configured 19 | with the flag `--reconciliation-period`. 20 | 21 | Lastly, the connector also monitors the NetworkPolicies. These policies indicate 22 | which Microservices have associated releases. If these releases are Preview 23 | releases, the connector will create Deployment objects and link the generated 24 | URL from the NetworkPolicy. 25 | 26 | ## Installation 27 | 28 | To install the GitHub connector, there's a few steps required, these are listed 29 | below. 30 | 31 | ### API Token 32 | 33 | First, an API token will be needed if a CRD is set up. This [GitHub API Token](https://github.com/settings/tokens) 34 | should have the `admin:repo_hook` and `repo` permissions. Once you have a token 35 | you can manually add it to your secrets in development in the namespace that 36 | your app expects. The expected token key is `GITHUB_AUTH_TOKEN`. 37 | 38 | This Token is only needed when installing a new GitHubRepository as it is 39 | repository bound. 40 | 41 | To install a token, go to the [GitHub Personal Access Tokens](https://github.com/settings/tokens) page. 42 | 43 | ![GitHub API Tokens](github-tokens.png) 44 | 45 | On the next page, you'll see the API token, this will be in the format of 46 | `888fe32217e96eaaa0709c37a488fd4a457015eb`. 47 | 48 | With this token, you can now generate a new Kubernetes Secret: 49 | 50 | ``` 51 | $ kubectl create secret generic github-auth-token --from-literal=GITHUB_AUTH_TOKEN=888fe32217e96eaaa0709c37a488fd4a457015eb 52 | ``` 53 | 54 | This will put the secret in your cluster in a format Heighliner understands. You 55 | can now use `github-auth-token` as a reference to the secret. 56 | 57 | *Note*: this needs to be installed in the namespace where you install the 58 | GitHubRepository. 59 | 60 | ### Domain 61 | 62 | The connector needs a domain to start with. In your production or cloud cluster, 63 | this would be a domain you will link to your service and set up in [the Ingress](https://github.com/manifoldco/heighliner/blob/90e33f43b6b61e6aca2e3d68e3452d762887def5/docs/kube/github-policy.yaml#L98) 64 | and [Deployment](https://github.com/manifoldco/heighliner/blob/90e33f43b6b61e6aca2e3d68e3452d762887def5/docs/kube/github-policy.yaml#L59). 65 | In development, we recommend you use [ngrok](https://ngrok.com/) to set up a tunnel. 66 | -------------------------------------------------------------------------------- /docs/design/github-tokens.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manifoldco/heighliner/3f4bdcf679d03daef3d639b23e9595aa3a90394a/docs/design/github-tokens.png -------------------------------------------------------------------------------- /docs/design/health-policy.md: -------------------------------------------------------------------------------- 1 | # Health Policy 2 | -------------------------------------------------------------------------------- /docs/design/image-policy.md: -------------------------------------------------------------------------------- 1 | # Image Policy 2 | 3 | ImagePolicy is responsible for tracking the versions of an Image and making sure 4 | the cluster is aware of the latest version that matches it's VersioningPolicy. 5 | 6 | ImagePolicies have Filters, these filters define where the releases will come 7 | from. The ImagePolicy is then responsible for validating that the desired images 8 | are available in the linked registry. 9 | 10 | ImagePolicies can optionally define a match configuration. Match is used to 11 | control how GitHub releases map to container registry images. By default, the 12 | release name is directly mapped to an image tag name. You can use `from` and 13 | `to` values to do pattern matching on the GitHub release, and templating to 14 | match container image tags. You can also define container image labels to match 15 | on in the same way. 16 | 17 | The ImagePolicy also matches the releases with the Versioning Policy, this means 18 | that there could be multiple releases available, but the ImagePolicy will filter 19 | these out further to only select the ones that match the VersioningPolicy. 20 | -------------------------------------------------------------------------------- /docs/design/microservice.md: -------------------------------------------------------------------------------- 1 | # Microservice 2 | 3 | ## Custom Resource Definition 4 | 5 | The Microservice CRD is responsible for defining what components make a 6 | Microservice. 7 | 8 | ## Controller 9 | 10 | The Microservice controller is responsible for creating a set of 11 | VersionedMicroservices. These VersionedMicroservices depend on available 12 | releases, which are defined by the [ImagePolicy](./image-policy.md) and 13 | [ConfigPolicy](./config-policy.md). 14 | 15 | For each new Release or Configuration, the Microservice will create a new 16 | VersionedMicroservice and annotate it appropriately. These annotations are 17 | important so we can use them in other parts of the system. 18 | 19 | The Microservice Controller also takes care of deleting deprecated versions from 20 | the system. It does this by looking at which releases should be deployed and 21 | which ones are currently deployed. If there are versions available that 22 | shouldn't be released anymore, it will delete them. 23 | 24 | ![diagram](full-flow.png) 25 | -------------------------------------------------------------------------------- /docs/design/network-policy.md: -------------------------------------------------------------------------------- 1 | # Network Policy 2 | 3 | The NetworkPolicy defines if a Microservice needs to be discoverable through 4 | either internal DNS or external DNS (or both). If defined, the NetworkPolicy 5 | controller will create a Service for each deployed version of a Microservice 6 | (VersionedMicroservice) that is specific for that version. This allows for 7 | users to test their specific application by pointing it to a very specific 8 | service. 9 | 10 | When an UpdateStrategy is provided, we'll automatically create a global Service 11 | for the application and point it to the correct version. This means that instead 12 | of specifying a very specific internal domain, the application name can be used. 13 | 14 | When a domain is set up, the NetworkPolicy will also link an Ingress to the 15 | correct Service. This means that the external domain will always point to the 16 | correct version depending on the provided UpdateStrategy. 17 | 18 | ## UpdateStrategies 19 | 20 | The NetworkPolicy can have several UpdateStrategies. These Strategies are used 21 | to set up internal and external DNS entries. 22 | 23 | ### Latest 24 | 25 | Latest will use the `Released` attribute on a status Release of a Microservice 26 | and pick the last version based on the name and link the Service to this 27 | VersionedMicroservice. 28 | 29 | ### Manual 30 | 31 | When Manual is selected, the NetworkPolicy controller won't do anything and use 32 | the provided labels to point to the desired application. 33 | -------------------------------------------------------------------------------- /docs/design/overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manifoldco/heighliner/3f4bdcf679d03daef3d639b23e9595aa3a90394a/docs/design/overview.png -------------------------------------------------------------------------------- /docs/design/security-policy.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | The Security Policy allows users to set up certain security aspects related to 4 | their applications. 5 | 6 | It's also the place where we will enforce some sensible defaults, like disabling 7 | auto mounting the default service account. 8 | -------------------------------------------------------------------------------- /docs/design/versioned-microservice.md: -------------------------------------------------------------------------------- 1 | # Versioned Microservice 2 | 3 | Versioned Microservices represent the combined state of a Microservice at a 4 | given point in time. It's a snapshot of how a certain Microservice should be 5 | configured at a given moment in time. 6 | 7 | A Versioned Microservice can be seen as the lowest level component of 8 | Heighliner. 9 | 10 | ## Design Overview 11 | 12 | The Versioned Microservice resource exists out of 2 parts, a Custom Resource 13 | Definition and a Controller. Each of these have their own specific design goals 14 | which are described below. 15 | 16 | ### Custom Resource Definition 17 | 18 | As the Versioned Microservice is the lowest level component, it is very 19 | declarative. It knows everything that is needed to know on how to deploy an 20 | application. It also comes with a set of sensitive defaults for optional 21 | configuration. 22 | 23 | The goal of for these Versioned Microservice CRDs is to be completely managed by 24 | a [Microservice](./microsesrvice.md) instead of manually defining these CRDs. 25 | 26 | #### Metadata 27 | 28 | As with all Kubernetes resources, Versioned Microservice accepts metadata. 29 | `name`, `namespace` and `labels` will be inherited by the underlying components 30 | which are created by the controller. 31 | 32 | #### Specification 33 | 34 | The specification for the Versioned Microservice CRD consists out of different 35 | parts, each highlighted below. In the future, these parts will be pulled in from 36 | other resources, linked together in the Microservice resource. 37 | 38 | ##### Availability 39 | 40 | Availability defines a lot of parameters on how we should run our service in a 41 | High Availability setup. 42 | 43 | This will be automatically filled in through the [Availability Policy](./availability-policy.md) 44 | 45 | ```yaml 46 | availability: 47 | replicas: 2 48 | minAvailable: 1 49 | maxUnavailable: 1 50 | restartPolicy: Always 51 | affinity: 52 | multiHost: 53 | weight: 100 54 | multiZone: 55 | weight: 90 56 | deploymentStrategy: 57 | rollingUpdate: 58 | maxSurge: 25% 59 | maxUnavailable: 25% 60 | ``` 61 | 62 | ##### Network 63 | 64 | The Network specification is what we'll translate into Services and Ingresses. 65 | Here we can define what ports a Microservice should use, what the domain name is 66 | on which - if at all - it should be accessible and if it should use SSL or not. 67 | 68 | This will be automatically filled in through the [Network Policy](./network-policy.md). 69 | 70 | ```yaml 71 | network: 72 | ports: 73 | - name: headless 74 | externalPort: 80 75 | internalPort: 8080 76 | dns: 77 | - hostname: api.hlnr.io 78 | ttl: 3600 79 | tls: true 80 | port: headless 81 | ``` 82 | 83 | ##### Volumes 84 | 85 | This is a list of [Volumes](https://godoc.org/k8s.io/kubernetes/pkg/apis/core#Volume) 86 | 87 | ```yaml 88 | volumes: 89 | - name: jwt-public 90 | emptyDir: {} 91 | ``` 92 | 93 | ##### Containers 94 | 95 | This is a list of [Containers](https://godoc.org/k8s.io/kubernetes/pkg/apis/core#Container) 96 | and can be configured at will. 97 | 98 | This will be automatically generated from a different set of Policies, like the 99 | [Image Policy](./image-policy.md), [Health Policy](./health-policy.md) and [Config Policy](./config-policy.md). 100 | 101 | ```yaml 102 | containers: 103 | - name: api 104 | image: hlnr.io/api:latest 105 | imagePullPolicy: IfNotPresent 106 | env: 107 | - name: PORT 108 | value: 8080 109 | readinessProbe: 110 | httpGet: 111 | path: /_healthz 112 | port: 8080 113 | initialDelaySeconds: 3 114 | periodSeconds: 3 115 | livenessProbe: 116 | httpGet: 117 | path: /_healthz 118 | port: 8080 119 | initialDelaySeconds: 5 120 | periodSeconds: 3 121 | resources: {} 122 | volumeMounts: 123 | - name: jwt-public 124 | mountPath: /jwt 125 | readOnly: true 126 | ``` 127 | 128 | ### Controller 129 | 130 | The Versioned Microservice Controller is a [Custom Controller](https://kubernetes.io/docs/concepts/api-extension/custom-resources/#custom-controllers) which takes 131 | action to achieve the desired state of the cluster. 132 | 133 | #### Responsibilities 134 | 135 | The controller is responsible for a limited set of actions. It will delegate a 136 | lot of its responsibilities to other components. 137 | 138 | - fill in correct defaults to the CRD 139 | - creating new components which form a microservice ([Ingress](https://kubernetes.io/docs/concepts/services-networking/ingress/), [Service](https://kubernetes.io/docs/concepts/services-networking/service/), [Deployment](https://kubernetes.io/docs/concepts/workloads/controllers/deployment/), ...) 140 | - apply updates to the created components 141 | - cascade deletion of a resource 142 | 143 | #### Exemptions 144 | 145 | - keep track of changes to the underlying specifications, like secrets 146 | - track multiple microservice versions and delete older versions 147 | -------------------------------------------------------------------------------- /docs/design/versioning-policy.md: -------------------------------------------------------------------------------- 1 | # Versioning Policy 2 | 3 | Each Image Policy will define a Versioning Policy. The Versioning Policy is what 4 | helps the system decide which releases we want to be tracking. 5 | 6 | For now, we'll follow the [SemVer](semver.org) format. This could potentially 7 | change in the future. 8 | 9 | All release types are treated equally. 10 | 11 | ## Release 12 | 13 | The release type relates to an actual production release. This should be in the 14 | form of `v1.2.3`. 15 | 16 | ## Release Candidate 17 | 18 | Release Candidates are used for staging environments. These should be in the 19 | form off `v1.2.3-rc.0`. This indicates that we can also have multiple release 20 | candidates for the same release. 21 | 22 | ## Preview 23 | 24 | Previews are development versions. They are usually associated with Pull 25 | Requests and are tagged by a unique version, usually a commit sha. 26 | -------------------------------------------------------------------------------- /docs/installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | In our installation process we've made a few assumptions, these are all about 4 | what is already installed in your cluster. If your configuration is different, 5 | it's still possible to install Heighliner, but you might have to change some of 6 | the installation files found in [docs/kube](./kube). 7 | 8 | 9 | ## Automated installation 10 | 11 | We've provided an installation command which lets you generate the correct 12 | manifest files which you can then apply to your cluster. These generated 13 | manifests make some assumptions of operators installed in your cluster: 14 | 15 | - [Ingress Nginx](https://github.com/kubernetes/ingress-nginx) as an Ingress 16 | - [Cert Manager](https://github.com/jetstack/cert-manager) for TLS Certificate management 17 | - [External DNS](https://github.com/kubernetes-incubator/external-dns) for DNS configuration 18 | 19 | To generated these manifests and apply them to your cluster, run the following 20 | commands: 21 | 22 | ``` 23 | $ make bins && ./bin/heighliner install --version \ 24 | --github-callback-domain \ 25 | --dns-provider \ 26 | | kubectl apply -f - 27 | ``` 28 | 29 | We recommend you use a specific version, like `0.1.0` instead of `latest`. This 30 | will prevent unexpected breaking changes if your Deployment gets rescheduled. 31 | 32 | The DNS Provider should match what is provisioned through [External DNS](https://github.com/kubernetes-incubator/external-dns). 33 | 34 | The GitHub Callback Domain is used to set up webhooks with GitHub. This should 35 | be the domain only. More information can be found [here](./design/github-connector.md#Domain). 36 | 37 | ## Manual Installation 38 | 39 | We've templated our installation files so we can install things dynamically. The 40 | key attributes that should be filled in are: 41 | 42 | - **Version**: the version of Heighliner to install 43 | - **GitHubAPIToken**: the API token to use when communicating with GitHub 44 | - **DNSProvider**: the DNS provider that is set up with ExternalDNS 45 | 46 | ### GitHub 47 | 48 | The GitHubCallbackDomain is used to link the cluster with GitHub. This is 49 | further described in [the GitHub Connector documentation](./design/github-connector.md#Domain). 50 | 51 | We'll also need an [API Token](./design/github-connector.md#APIToken) which will 52 | allow us to actually communicate with GitHub. This API Token is only needed when 53 | you install a new GitHubRepository into your cluster. 54 | 55 | ### Applying the files 56 | 57 | Once the attributes are filled in, we can go ahead and apply the files: 58 | 59 | ``` 60 | $ kubectl apply -f docs/kube 61 | ``` 62 | 63 | This will set up all the controllers and install the necessary RBAC rules. The 64 | controllers will then install the CRDs accordingly. 65 | 66 | Now that we have Heighliner up and running, we can start installing 67 | Microservices. 68 | -------------------------------------------------------------------------------- /docs/kube/00-heighliner-namespace.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: hlnr-system 5 | -------------------------------------------------------------------------------- /docs/kube/config-policy.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1beta1 2 | kind: ClusterRole 3 | metadata: 4 | name: heighliner:configpolicy 5 | rules: 6 | - apiGroups: ["hlnr.io"] 7 | resources: ["configpolicies"] 8 | verbs: ["*"] 9 | - apiGroups: ["v1"] 10 | resources: ["secrets", "configmaps"] 11 | verbs: ["get"] 12 | - apiGroups: ["apiextensions.k8s.io"] 13 | resources: ["customresourcedefinitions"] 14 | verbs: ["*"] 15 | 16 | --- 17 | 18 | apiVersion: rbac.authorization.k8s.io/v1beta1 19 | kind: ClusterRoleBinding 20 | metadata: 21 | name: heighliner:configpolicy 22 | roleRef: 23 | apiGroup: rbac.authorization.k8s.io 24 | kind: ClusterRole 25 | name: heighliner:configpolicy 26 | subjects: 27 | - name: heighliner-configpolicy 28 | namespace: hlnr-system 29 | kind: ServiceAccount 30 | 31 | --- 32 | 33 | apiVersion: v1 34 | kind: ServiceAccount 35 | metadata: 36 | name: heighliner-configpolicy 37 | namespace: hlnr-system 38 | 39 | --- 40 | 41 | apiVersion: extensions/v1beta1 42 | kind: Deployment 43 | metadata: 44 | name: configpolicy-controller 45 | namespace: hlnr-system 46 | spec: 47 | replicas: 1 48 | template: 49 | metadata: 50 | labels: 51 | app: configpolicy-controller 52 | spec: 53 | serviceAccountName: heighliner-configpolicy 54 | containers: 55 | - name: configpolicy-controller 56 | image: arigato/heighliner:{{.Version}} 57 | imagePullPolicy: IfNotPresent 58 | args: 59 | - cpw 60 | resources: 61 | requests: 62 | cpu: 100m 63 | memory: 10Mi 64 | -------------------------------------------------------------------------------- /docs/kube/github-policy.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1beta1 2 | kind: ClusterRole 3 | metadata: 4 | name: heighliner:github-repository 5 | rules: 6 | - apiGroups: ["hlnr.io"] 7 | resources: 8 | - "githubrepositories" 9 | verbs: ["*"] 10 | - apiGroups: ["hlnr.io"] 11 | resources: 12 | - "microservices" 13 | - "networkpolicies" 14 | - "imagepolicies" 15 | verbs: ["get", "list", "watch"] 16 | - apiGroups: ["apiextensions.k8s.io"] 17 | resources: ["customresourcedefinitions"] 18 | verbs: ["*"] 19 | - apiGroups: [""] 20 | resources: 21 | - "secrets" 22 | verbs: ["get", "list"] 23 | 24 | --- 25 | 26 | apiVersion: rbac.authorization.k8s.io/v1beta1 27 | kind: ClusterRoleBinding 28 | metadata: 29 | name: heighliner:github-repository 30 | roleRef: 31 | apiGroup: rbac.authorization.k8s.io 32 | kind: ClusterRole 33 | name: heighliner:github-repository 34 | subjects: 35 | - name: heighliner-github-repository 36 | namespace: hlnr-system 37 | kind: ServiceAccount 38 | 39 | --- 40 | 41 | apiVersion: v1 42 | kind: ServiceAccount 43 | metadata: 44 | name: heighliner-github-repository 45 | namespace: hlnr-system 46 | 47 | --- 48 | 49 | apiVersion: extensions/v1beta1 50 | kind: Deployment 51 | metadata: 52 | name: github-repository-controller 53 | namespace: hlnr-system 54 | spec: 55 | replicas: 1 56 | template: 57 | metadata: 58 | labels: 59 | app: github-repository-controller 60 | spec: 61 | serviceAccountName: heighliner-github-repository 62 | containers: 63 | - name: github-repository-controller 64 | image: arigato/heighliner:{{.Version}} 65 | imagePullPolicy: Never 66 | args: 67 | - github-repository-controller 68 | env: 69 | - name: DOMAIN 70 | value: {{.GitHubCallbackDomain}} 71 | resources: 72 | requests: 73 | cpu: 100m 74 | memory: 10Mi 75 | readinessProbe: 76 | httpGet: 77 | path: /_healthz 78 | port: 8080 79 | initialDelaySeconds: 3 80 | periodSeconds: 3 81 | livenessProbe: 82 | httpGet: 83 | path: /_healthz 84 | port: 8080 85 | initialDelaySeconds: 5 86 | periodSeconds: 3 87 | 88 | --- 89 | 90 | apiVersion: v1 91 | kind: Service 92 | metadata: 93 | labels: 94 | service: github-repository-controller 95 | name: github-repository-controller 96 | namespace: hlnr-system 97 | spec: 98 | type: NodePort 99 | ports: 100 | - name: headless 101 | port: 80 102 | targetPort: 8080 103 | selector: 104 | app: github-repository-controller 105 | status: 106 | loadBalancer: {} 107 | 108 | --- 109 | 110 | apiVersion: certmanager.k8s.io/v1alpha1 111 | kind: Certificate 112 | metadata: 113 | name: github-callback-tls 114 | namespace: hlnr-system 115 | spec: 116 | secretName: github-callback-tls 117 | issuerRef: 118 | name: letsencrypt-prod 119 | kind: ClusterIssuer 120 | commonName: '{{.GitHubCallbackDomain}}' 121 | acme: 122 | config: 123 | - dns01: 124 | provider: {{.DNSProvider}} 125 | domains: 126 | - '{{.GitHubCallbackDomain}}' 127 | 128 | --- 129 | 130 | apiVersion: extensions/v1beta1 131 | kind: Ingress 132 | metadata: 133 | name: github-repository-controller 134 | namespace: hlnr-system 135 | annotations: 136 | kubernetes.io/ingress.class: "nginx" 137 | external-dns.alpha.kubernetes.io/hostname: {{.GitHubCallbackDomain}}. 138 | external-dns.alpha.kubernetes.io/ttl: "300" 139 | spec: 140 | tls: 141 | - hosts: 142 | - {{.GitHubCallbackDomain}} 143 | secretName: github-callback-tls 144 | rules: 145 | - host: {{.GitHubCallbackDomain}} 146 | http: 147 | paths: 148 | - path: / 149 | backend: 150 | serviceName: github-repository-controller 151 | servicePort: 80 152 | -------------------------------------------------------------------------------- /docs/kube/image-policy.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1beta1 2 | kind: ClusterRole 3 | metadata: 4 | name: heighliner:image-policy 5 | rules: 6 | - apiGroups: ["hlnr.io"] 7 | resources: 8 | - "imagepolicies" 9 | verbs: ["*"] 10 | - apiGroups: ["hlnr.io"] 11 | resources: 12 | - "githubrepositories" 13 | - "versioningpolicies" 14 | - "microservices" 15 | verbs: ["get", "list", "watch"] 16 | - apiGroups: [""] 17 | resources: ["secrets"] 18 | verbs: ["get", "list"] 19 | - apiGroups: ["apiextensions.k8s.io"] 20 | resources: ["customresourcedefinitions"] 21 | verbs: ["*"] 22 | 23 | --- 24 | 25 | apiVersion: rbac.authorization.k8s.io/v1beta1 26 | kind: ClusterRoleBinding 27 | metadata: 28 | name: heighliner:image-policy 29 | roleRef: 30 | apiGroup: rbac.authorization.k8s.io 31 | kind: ClusterRole 32 | name: heighliner:image-policy 33 | subjects: 34 | - name: heighliner-image-policy 35 | namespace: hlnr-system 36 | kind: ServiceAccount 37 | 38 | --- 39 | 40 | apiVersion: v1 41 | kind: ServiceAccount 42 | metadata: 43 | name: heighliner-image-policy 44 | namespace: hlnr-system 45 | 46 | --- 47 | 48 | apiVersion: extensions/v1beta1 49 | kind: Deployment 50 | metadata: 51 | name: image-policy-controller 52 | namespace: hlnr-system 53 | spec: 54 | replicas: 1 55 | template: 56 | metadata: 57 | labels: 58 | app: image-policy-controller 59 | spec: 60 | serviceAccountName: heighliner-image-policy 61 | containers: 62 | - name: image-policy-controller 63 | image: arigato/heighliner:{{.Version}} 64 | imagePullPolicy: IfNotPresent 65 | args: 66 | - ipc 67 | resources: 68 | requests: 69 | cpu: 100m 70 | memory: 10Mi 71 | -------------------------------------------------------------------------------- /docs/kube/microservice.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1beta1 2 | kind: ClusterRole 3 | metadata: 4 | name: heighliner:microservice 5 | rules: 6 | - apiGroups: ["hlnr.io"] 7 | resources: ["microservices"] 8 | verbs: ["*"] 9 | - apiGroups: ["hlnr.io"] 10 | resources: ["versionedmicroservices"] 11 | verbs: ["get", "update", "create", "delete", "patch"] 12 | - apiGroups: ["hlnr.io"] 13 | resources: 14 | - "imagepolicies" 15 | - "availabilitypolicies" 16 | - "configpolicies" 17 | - "securitypolicies" 18 | verbs: ["get", "list"] 19 | - apiGroups: ["apiextensions.k8s.io"] 20 | resources: ["customresourcedefinitions"] 21 | verbs: ["*"] 22 | 23 | --- 24 | 25 | apiVersion: rbac.authorization.k8s.io/v1beta1 26 | kind: ClusterRoleBinding 27 | metadata: 28 | name: heighliner:microservice 29 | roleRef: 30 | apiGroup: rbac.authorization.k8s.io 31 | kind: ClusterRole 32 | name: heighliner:microservice 33 | subjects: 34 | - name: heighliner-microservice 35 | namespace: hlnr-system 36 | kind: ServiceAccount 37 | 38 | --- 39 | 40 | apiVersion: v1 41 | kind: ServiceAccount 42 | metadata: 43 | name: heighliner-microservice 44 | namespace: hlnr-system 45 | 46 | --- 47 | 48 | apiVersion: extensions/v1beta1 49 | kind: Deployment 50 | metadata: 51 | name: microservice-controller 52 | namespace: hlnr-system 53 | spec: 54 | replicas: 1 55 | template: 56 | metadata: 57 | labels: 58 | app: microservice-controller 59 | spec: 60 | serviceAccountName: heighliner-microservice 61 | containers: 62 | - name: microservice-controller 63 | image: arigato/heighliner:{{.Version}} 64 | imagePullPolicy: Never 65 | args: 66 | - msvc 67 | resources: 68 | requests: 69 | cpu: 100m 70 | memory: 10Mi 71 | -------------------------------------------------------------------------------- /docs/kube/network-policy.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1beta1 2 | kind: ClusterRole 3 | metadata: 4 | name: heighliner:network-policy 5 | rules: 6 | - apiGroups: ["hlnr.io"] 7 | resources: ["networkpolicies"] 8 | verbs: ["*"] 9 | - apiGroups: ["hlnr.io"] 10 | resources: ["microservices"] 11 | verbs: ["get", "list"] 12 | - apiGroups: ["extensions"] 13 | resources: ["ingresses"] 14 | verbs: ["*"] 15 | - apiGroups: [""] 16 | resources: ["services"] 17 | verbs: ["*"] 18 | - apiGroups: ["apiextensions.k8s.io"] 19 | resources: ["customresourcedefinitions"] 20 | verbs: ["*"] 21 | 22 | --- 23 | 24 | apiVersion: rbac.authorization.k8s.io/v1beta1 25 | kind: ClusterRoleBinding 26 | metadata: 27 | name: heighliner:network-policy 28 | roleRef: 29 | apiGroup: rbac.authorization.k8s.io 30 | kind: ClusterRole 31 | name: heighliner:network-policy 32 | subjects: 33 | - name: heighliner-network-policy 34 | namespace: hlnr-system 35 | kind: ServiceAccount 36 | 37 | --- 38 | 39 | apiVersion: v1 40 | kind: ServiceAccount 41 | metadata: 42 | name: heighliner-network-policy 43 | namespace: hlnr-system 44 | 45 | --- 46 | 47 | apiVersion: extensions/v1beta1 48 | kind: Deployment 49 | metadata: 50 | name: network-policy-controller 51 | namespace: hlnr-system 52 | spec: 53 | replicas: 1 54 | template: 55 | metadata: 56 | labels: 57 | app: network-policy-controller 58 | spec: 59 | serviceAccountName: heighliner-network-policy 60 | containers: 61 | - name: network-policy-controller 62 | image: arigato/heighliner:{{.Version}} 63 | imagePullPolicy: IfNotPresent 64 | args: 65 | - npw 66 | resources: 67 | requests: 68 | cpu: 100m 69 | memory: 10Mi 70 | -------------------------------------------------------------------------------- /docs/kube/versioned-microservice.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1beta1 2 | kind: ClusterRole 3 | metadata: 4 | name: heighliner:versionedmicroservice 5 | rules: 6 | - apiGroups: ["hlnr.io"] 7 | resources: ["versionedmicroservices"] 8 | verbs: ["*"] 9 | - apiGroups: ["extensions"] 10 | resources: ["deployments"] 11 | verbs: ["*"] 12 | - apiGroups: ["policy"] 13 | resources: ["poddisruptionbudgets"] 14 | verbs: ["*"] 15 | - apiGroups: ["apiextensions.k8s.io"] 16 | resources: ["customresourcedefinitions"] 17 | verbs: ["*"] 18 | 19 | --- 20 | 21 | apiVersion: rbac.authorization.k8s.io/v1beta1 22 | kind: ClusterRoleBinding 23 | metadata: 24 | name: heighliner:versionedmicroservice 25 | roleRef: 26 | apiGroup: rbac.authorization.k8s.io 27 | kind: ClusterRole 28 | name: heighliner:versionedmicroservice 29 | subjects: 30 | - name: heighliner-versionedmicroservice 31 | namespace: hlnr-system 32 | kind: ServiceAccount 33 | 34 | --- 35 | 36 | apiVersion: v1 37 | kind: ServiceAccount 38 | metadata: 39 | name: heighliner-versionedmicroservice 40 | namespace: hlnr-system 41 | 42 | --- 43 | 44 | apiVersion: extensions/v1beta1 45 | kind: Deployment 46 | metadata: 47 | name: versioned-microservice-controller 48 | namespace: hlnr-system 49 | spec: 50 | replicas: 1 51 | template: 52 | metadata: 53 | labels: 54 | app: versioned-microservice-controller 55 | spec: 56 | serviceAccountName: heighliner-versionedmicroservice 57 | containers: 58 | - name: versioned-microservice-controller 59 | image: arigato/heighliner:{{.Version}} 60 | imagePullPolicy: IfNotPresent 61 | args: 62 | - vsvc 63 | resources: 64 | requests: 65 | cpu: 100m 66 | memory: 10Mi 67 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/manifoldco/heighliner 2 | 3 | require ( 4 | 4d63.com/gochecknoglobals v0.0.0-20180908201037-5090db600a84 // indirect 5 | 4d63.com/gochecknoinits v0.0.0-20180528051558-14d5915061e5 // indirect 6 | cloud.google.com/go v0.23.0 7 | github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 8 | github.com/Azure/go-autorest v9.9.0+incompatible 9 | github.com/MakeNowJust/heredoc v0.0.0-20171113091838-e9091a26100e 10 | github.com/PuerkitoBio/purell v1.1.0 11 | github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 12 | github.com/alecthomas/gocyclo v0.0.0-20150208221726-aa8f8b160214 // indirect 13 | github.com/alexflint/go-arg v0.0.0-20180516182405-f7c0423bd11e // indirect 14 | github.com/alexflint/go-scalar v0.0.0-20170216020425-e80c3b7ed292 // indirect 15 | github.com/alexkohler/nakedret v0.0.0-20171106223215-c0e305a4f690 // indirect 16 | github.com/asaskevich/govalidator v0.0.0-20180315120708-ccb8e960c48f 17 | github.com/aws/aws-sdk-go v1.13.54 18 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 19 | github.com/client9/misspell v0.3.4 // indirect 20 | github.com/davecgh/go-spew v1.1.1 21 | github.com/dchest/blake2b v1.0.0 22 | github.com/dgrijalva/jwt-go v3.2.0+incompatible 23 | github.com/docker/distribution v0.0.0-20170726174610-edc3ab29cdff 24 | github.com/docker/docker v0.0.0-20180710110222-56b14b8c2596 25 | github.com/docker/go-connections v0.3.0 26 | github.com/docker/go-units v0.3.3 27 | github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 28 | github.com/docker/spdystream v0.0.0-20170912183627-bc6354cbbc29 29 | github.com/emicklei/go-restful v2.7.0+incompatible 30 | github.com/evanphx/json-patch v3.0.0+incompatible 31 | github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d 32 | github.com/fatih/camelcase v1.0.0 33 | github.com/ghodss/yaml v1.0.0 34 | github.com/go-ini/ini v1.36.0 35 | github.com/go-openapi/analysis v0.0.0-20180520152044-5957818e1003 36 | github.com/go-openapi/errors v0.0.0-20180515155515-b2b2befaf267 37 | github.com/go-openapi/jsonpointer v0.0.0-20180322222829-3a0015ad55fa 38 | github.com/go-openapi/jsonreference v0.0.0-20180322222742-3fb327e6747d 39 | github.com/go-openapi/loads v0.0.0-20171207192234-2a2b323bab96 40 | github.com/go-openapi/runtime v0.0.0-20180509184547-c0cae94704c7 41 | github.com/go-openapi/spec v0.0.0-20180415031709-bcff419492ee 42 | github.com/go-openapi/strfmt v0.0.0-20180407011102-481808443b00 43 | github.com/go-openapi/swag v0.0.0-20180405201759-811b1089cde9 44 | github.com/go-openapi/validate v0.0.0-20180502213133-9286f6d0e5c1 45 | github.com/gogo/protobuf v1.0.0 46 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b 47 | github.com/golang/groupcache v0.0.0-20180513044358-24b0969c4cb7 48 | github.com/golang/lint v0.0.0-20180702182130-06c8688daad7 // indirect 49 | github.com/golang/protobuf v1.2.0 50 | github.com/google/btree v0.0.0-20180124185431-e89373fe6b4a 51 | github.com/google/go-github v15.0.0+incompatible 52 | github.com/google/go-querystring v0.0.0-20170111101155-53e6ce116135 53 | github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf 54 | github.com/googleapis/gnostic v0.2.0 55 | github.com/gophercloud/gophercloud v0.0.0-20180515014705-282f25e4025d 56 | github.com/gordonklaus/ineffassign v0.0.0-20180909121442-1003c8bd00dc // indirect 57 | github.com/gorilla/context v1.1.1 58 | github.com/gorilla/mux v1.6.2 59 | github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 60 | github.com/hashicorp/golang-lru v0.0.0-20180201235237-0fb14efe8c47 61 | github.com/heroku/docker-registry-client v0.0.0-20171019183014-fd2fe8034968 62 | github.com/howeyc/gopass v0.0.0-20170109162249-bf9dde6d0d2c 63 | github.com/imdario/mergo v0.3.4 64 | github.com/inconshreveable/mousetrap v1.0.0 65 | github.com/jelmersnoeck/kubekit v0.0.0-20180605073004-c5b8ec922b30 66 | github.com/jessevdk/go-flags v1.4.0 67 | github.com/jgautheron/goconst v0.0.0-20170703170152-9740945f5dcb // indirect 68 | github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8 69 | github.com/json-iterator/go v0.0.0-20180315132816-ca39e5af3ece 70 | github.com/juju/ratelimit v1.0.1 71 | github.com/kisielk/errcheck v1.1.0 // indirect 72 | github.com/mailru/easyjson v0.0.0-20180323154445-8b799c424f57 73 | github.com/matttproud/golang_protobuf_extensions v1.0.0 74 | github.com/mdempsky/maligned v0.0.0-20180708014732-6e39bd26a8c8 // indirect 75 | github.com/mdempsky/unconvert v0.0.0-20180703203632-1a9a0a0a3594 // indirect 76 | github.com/mibk/dupl v1.0.0 // indirect 77 | github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 78 | github.com/mitchellh/mapstructure v0.0.0-20180511142126-bb74f1db0675 79 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd 80 | github.com/modern-go/reflect2 v0.0.0-20180228065516-1df9eeb2bb81 81 | github.com/opencontainers/go-digest v1.0.0-rc1 82 | github.com/opencontainers/image-spec v1.0.1 83 | github.com/opennota/check v0.0.0-20180911053232-0c771f5545ff // indirect 84 | github.com/pborman/uuid v0.0.0-20170612153648-e790cca94e6c 85 | github.com/petar/GoLLRB v0.0.0-20130427215148-53be0d36a84c 86 | github.com/peterbourgon/diskv v2.0.1+incompatible 87 | github.com/pmezard/go-difflib v1.0.0 88 | github.com/prometheus/client_golang v0.8.0 89 | github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5 90 | github.com/prometheus/common v0.0.0-20180518154759-7600349dcfe1 91 | github.com/prometheus/procfs v0.0.0-20180408092902-8b1c2da0d56d 92 | github.com/russross/blackfriday v0.0.0-20151117072312-300106c228d5 93 | github.com/securego/gosec v0.0.0-20181005111228-e0a150bfa369 // indirect 94 | github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95 95 | github.com/sirupsen/logrus v1.0.4 96 | github.com/spf13/cobra v0.0.0-20180531180338-1e58aa3361fd 97 | github.com/spf13/pflag v1.0.1 98 | github.com/stretchr/testify v1.2.2 99 | github.com/stripe/safesql v0.0.0-20171221195208-cddf355596fe // indirect 100 | github.com/tsenart/deadcode v0.0.0-20160724212837-210d2dc333e9 // indirect 101 | github.com/walle/lll v0.0.0-20160702150637-8b13b3fbf731 // indirect 102 | golang.org/x/crypto v0.0.0-20180524125353-159ae71589f3 103 | golang.org/x/lint v0.0.0-20180702182130-06c8688daad7 // indirect 104 | golang.org/x/net v0.0.0-20180524181706-dfa909b99c79 105 | golang.org/x/oauth2 v0.0.0-20180523224158-770e5ebd4ab2 106 | golang.org/x/sys v0.0.0-20180524135853-04b83988a018 107 | golang.org/x/text v0.3.0 108 | golang.org/x/tools v0.0.0-20181008205924-a2b3f7f249e9 109 | google.golang.org/appengine v1.0.0 110 | gopkg.in/gcfg.v1 v1.2.3 111 | gopkg.in/inf.v0 v0.9.1 112 | gopkg.in/mgo.v2 v2.0.0-20160818020120-3f83fa500528 113 | gopkg.in/warnings.v0 v0.1.2 114 | gopkg.in/yaml.v2 v2.2.1 115 | honnef.co/go/tools v0.0.0-20180920025451-e3ad64cb4ed3 // indirect 116 | k8s.io/api v0.0.0-20180127130940-acf347b865f2 117 | k8s.io/apiextensions-apiserver v0.0.0-20180206093320-f1425805c033 118 | k8s.io/apimachinery v0.0.0-20180126010752-19e3f5aa3adc 119 | k8s.io/apiserver v0.0.0-20180201051917-40b00dd493d8 120 | k8s.io/client-go v6.0.0+incompatible 121 | k8s.io/code-generator v0.0.0-20180515212316-d9b16e114e8c 122 | k8s.io/gengo v0.0.0-20180223161844-01a732e01d00 123 | k8s.io/kube-openapi v0.0.0-20180522000509-67793630244c 124 | k8s.io/kubernetes v1.9.8 125 | k8s.io/utils v0.0.0-20180208044234-258e2a2fa645 126 | mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed // indirect 127 | mvdan.cc/unparam v0.0.0-20181009154202-6669894d00e9 // indirect 128 | vbom.ml/util v0.0.0-20170409195630-256737ac55c4 129 | ) 130 | -------------------------------------------------------------------------------- /internal/configpolicy/configpolicy.go: -------------------------------------------------------------------------------- 1 | package configpolicy 2 | 3 | import ( 4 | "github.com/manifoldco/heighliner/apis/v1alpha1" 5 | 6 | "github.com/jelmersnoeck/kubekit" 7 | "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" 8 | ) 9 | 10 | var ( 11 | // ConfigPolicyResource describes the CRD configuration for the 12 | // ConfigPolicy CRD. 13 | ConfigPolicyResource = kubekit.CustomResource{ 14 | Name: "configpolicy", 15 | Plural: "configpolicies", 16 | Group: v1alpha1.GroupName, 17 | Version: v1alpha1.Version, 18 | Scope: v1beta1.NamespaceScoped, 19 | Aliases: []string{"cp"}, 20 | Object: &v1alpha1.ConfigPolicy{}, 21 | Validation: v1alpha1.ConfigPolicyValidationSchema, 22 | } 23 | ) 24 | -------------------------------------------------------------------------------- /internal/configpolicy/controller.go: -------------------------------------------------------------------------------- 1 | package configpolicy 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "os" 8 | "os/signal" 9 | "syscall" 10 | 11 | "github.com/manifoldco/heighliner/apis/v1alpha1" 12 | "github.com/manifoldco/heighliner/internal/k8sutils" 13 | 14 | "github.com/jelmersnoeck/kubekit" 15 | "github.com/jelmersnoeck/kubekit/patcher" 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/cache" 20 | cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" 21 | ) 22 | 23 | // Controller represents the MicroserviceController. This controller 24 | // takes care of creating, updating and deleting lower level Kubernetes 25 | // components that are associated with a specific Microservice. 26 | type Controller struct { 27 | rc *rest.RESTClient 28 | cs kubernetes.Interface 29 | namespace string 30 | patcher *patcher.Patcher 31 | } 32 | 33 | // NewController returns a new ConfigPolicy Controller. 34 | func NewController(cfg *rest.Config, cs kubernetes.Interface, namespace string) (*Controller, error) { 35 | rc, err := kubekit.RESTClient(cfg, &v1alpha1.SchemeGroupVersion, v1alpha1.AddToScheme) 36 | if err != nil { 37 | return nil, err 38 | } 39 | 40 | return &Controller{ 41 | cs: cs, 42 | rc: rc, 43 | namespace: namespace, 44 | patcher: patcher.New("hlnr-configpolicy", cmdutil.NewFactory(nil)), 45 | }, nil 46 | } 47 | 48 | // Run runs the Controller in the background and sets up watchers to take action 49 | // when the desired state is altered. 50 | func (c *Controller) Run() error { 51 | log.Printf("Starting controller...") 52 | ctx, cancel := context.WithCancel(context.Background()) 53 | 54 | go c.run(ctx) 55 | 56 | quit := make(chan os.Signal, 1) 57 | signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) 58 | 59 | <-quit 60 | log.Printf("Shutdown requested...") 61 | cancel() 62 | 63 | <-ctx.Done() 64 | log.Printf("Shutting down...") 65 | 66 | return nil 67 | } 68 | 69 | func (c *Controller) run(ctx context.Context) { 70 | watcher := kubekit.NewWatcher( 71 | c.rc, 72 | c.namespace, 73 | &ConfigPolicyResource, 74 | cache.ResourceEventHandlerFuncs{ 75 | AddFunc: func(obj interface{}) { 76 | c.hashConfigValues(obj) 77 | }, 78 | UpdateFunc: func(old, new interface{}) { 79 | c.hashConfigValues(new) 80 | }, 81 | DeleteFunc: func(obj interface{}) { 82 | cp := obj.(*v1alpha1.ConfigPolicy).DeepCopy() 83 | log.Printf("Deleting ConfigPolicy %s", cp.Name) 84 | }, 85 | }, 86 | ) 87 | 88 | go watcher.Run(ctx.Done()) 89 | } 90 | 91 | func (c *Controller) hashConfigValues(obj interface{}) error { 92 | cp := obj.(*v1alpha1.ConfigPolicy).DeepCopy() 93 | 94 | hashedConfig, err := c.getHashedConfig(cp) 95 | if err != nil { 96 | log.Printf("Error getting hashed configuration for %s: %s", cp.Name, err) 97 | return err 98 | } 99 | hashedString := fmt.Sprintf("%x", hashedConfig) 100 | 101 | // some values of our config have changed, update the CRD status so 102 | // depending resources get notified. 103 | if hashedString != cp.Status.Hashed { 104 | cp.Status.LastUpdatedTime = metav1.Now() 105 | cp.Status.Hashed = hashedString 106 | 107 | cp.TypeMeta = metav1.TypeMeta{ 108 | Kind: "ConfigPolicy", 109 | APIVersion: "hlnr.io/v1alpha1", 110 | } 111 | patch, err := c.patcher.Apply(cp) 112 | if err != nil { 113 | log.Printf("Could not update ConfigPolicy %s: %s", cp.Name, err) 114 | } 115 | 116 | patch, err = k8sutils.CleanupPatchAnnotations(patch, "hlnr-configpolicy") 117 | if err == nil && !patcher.IsEmptyPatch(patch) { 118 | log.Printf("Updated ConfigPolicy %s", cp.Name) 119 | } 120 | } 121 | 122 | return nil 123 | } 124 | 125 | func (c *Controller) getHashedConfig(crd *v1alpha1.ConfigPolicy) ([]byte, error) { 126 | envVarHash, err := getEnvVarHash(c.patcher, crd.Namespace, crd.Spec.Env) 127 | if err != nil { 128 | return nil, err 129 | } 130 | 131 | envFromSourceHash, err := getEnvFromSourceHash(c.patcher, crd.Namespace, crd.Spec.EnvFrom) 132 | if err != nil { 133 | return nil, err 134 | } 135 | 136 | return append(envVarHash, envFromSourceHash...), nil 137 | } 138 | -------------------------------------------------------------------------------- /internal/configpolicy/hashed.go: -------------------------------------------------------------------------------- 1 | package configpolicy 2 | 3 | import ( 4 | "crypto/md5" 5 | "fmt" 6 | "io" 7 | "sort" 8 | 9 | corev1 "k8s.io/api/core/v1" 10 | "k8s.io/apimachinery/pkg/api/errors" 11 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 12 | "k8s.io/apimachinery/pkg/runtime/schema" 13 | ) 14 | 15 | type objectGetter interface { 16 | Get(interface{}, string, string) error 17 | } 18 | 19 | // TODO(jelmer): stronger encryption? The value will be stored on the CRD status 20 | // so it could be seen by others. 21 | func getEnvVarHash(p objectGetter, namespace string, envs []corev1.EnvVar) ([]byte, error) { 22 | h := md5.New() 23 | 24 | // envs are an array of vars. This means looping over them is always in the 25 | // same order. We don't need to do any sort of special sorting on this. 26 | for _, env := range envs { 27 | value := env.Value 28 | if env.ValueFrom != nil { 29 | byteValue, err := valueFromEnvVar(p, namespace, env) 30 | if err != nil { 31 | return nil, err 32 | } 33 | value = string(byteValue) 34 | } 35 | 36 | io.WriteString(h, fmt.Sprintf("%s:%s;", env.Name, value)) 37 | } 38 | 39 | return h.Sum(nil), nil 40 | } 41 | 42 | func getEnvFromSourceHash(p objectGetter, namespace string, envs []corev1.EnvFromSource) ([]byte, error) { 43 | h := md5.New() 44 | 45 | // the array of references is already sorted, no need to rearrange these 46 | for _, env := range envs { 47 | values, err := valuesFromSource(p, namespace, env) 48 | if err != nil { 49 | return nil, err 50 | } 51 | 52 | // the data for the source values is a map. Ranging over maps is random 53 | // so we need to sort it to make sure we always have the same end 54 | // result. 55 | sortedKeys := sort.StringSlice{} 56 | for k := range values { 57 | sortedKeys = append(sortedKeys, k) 58 | } 59 | sortedKeys.Sort() 60 | 61 | for _, mapKey := range sortedKeys { 62 | io.WriteString(h, fmt.Sprintf("%s:%s;", mapKey, string(values[mapKey]))) 63 | } 64 | } 65 | 66 | return h.Sum(nil), nil 67 | } 68 | 69 | func valuesFromSource(p objectGetter, ns string, env corev1.EnvFromSource) (map[string][]byte, error) { 70 | data := map[string][]byte{} 71 | switch { 72 | case env.ConfigMapRef != nil: 73 | cmr := env.ConfigMapRef 74 | config := &corev1.ConfigMap{ 75 | TypeMeta: metav1.TypeMeta{ 76 | Kind: "ConfigMap", 77 | APIVersion: "v1", 78 | }, 79 | } 80 | 81 | if err := p.Get(config, ns, cmr.Name); err != nil { 82 | return nil, err 83 | } 84 | 85 | for k, v := range config.Data { 86 | data[fmt.Sprintf("%s%s", env.Prefix, k)] = []byte(v) 87 | } 88 | case env.SecretRef != nil: 89 | sr := env.SecretRef 90 | 91 | secret := &corev1.Secret{ 92 | TypeMeta: metav1.TypeMeta{ 93 | Kind: "Secret", 94 | APIVersion: "v1", 95 | }, 96 | } 97 | 98 | if err := p.Get(secret, ns, sr.Name); err != nil { 99 | return nil, err 100 | } 101 | 102 | data = secret.Data 103 | } 104 | 105 | return data, nil 106 | } 107 | 108 | func valueFromEnvVar(p objectGetter, ns string, env corev1.EnvVar) ([]byte, error) { 109 | if env.ValueFrom == nil { 110 | return nil, nil 111 | } 112 | vf := env.ValueFrom 113 | 114 | var data []byte 115 | switch { 116 | case vf.ConfigMapKeyRef != nil: 117 | ckr := vf.ConfigMapKeyRef 118 | config := &corev1.ConfigMap{ 119 | TypeMeta: metav1.TypeMeta{ 120 | Kind: "ConfigMap", 121 | APIVersion: "v1", 122 | }, 123 | } 124 | 125 | if err := p.Get(config, ns, ckr.Name); err != nil { 126 | if errors.IsNotFound(err) && ckr.Optional != nil && *ckr.Optional { 127 | return nil, nil 128 | } 129 | 130 | return nil, err 131 | } 132 | 133 | rawData, ok := config.Data[ckr.Key] 134 | if !ok && isRequired(ckr.Optional) { 135 | return nil, errors.NewNotFound( 136 | schema.GroupResource{Group: "v1", Resource: "ConfigMap"}, 137 | fmt.Sprintf("%s: %s", ckr.Name, ckr.Key), 138 | ) 139 | } 140 | 141 | // TODO(jelmer): in 1.10 there's `BinaryData` 142 | data = []byte(rawData) 143 | case vf.SecretKeyRef != nil: 144 | skr := vf.SecretKeyRef 145 | secret := &corev1.Secret{ 146 | TypeMeta: metav1.TypeMeta{ 147 | Kind: "Secret", 148 | APIVersion: "v1", 149 | }, 150 | } 151 | 152 | if err := p.Get(secret, ns, skr.Name); err != nil { 153 | if errors.IsNotFound(err) && skr.Optional != nil && *skr.Optional { 154 | return nil, nil 155 | } 156 | 157 | return nil, err 158 | } 159 | 160 | rawData, ok := secret.Data[skr.Key] 161 | if !ok && isRequired(skr.Optional) { 162 | return nil, errors.NewNotFound( 163 | schema.GroupResource{Group: "v1", Resource: "Secret"}, 164 | fmt.Sprintf("%s: %s", skr.Name, skr.Key), 165 | ) 166 | } 167 | 168 | data = rawData 169 | } 170 | 171 | return data, nil 172 | } 173 | 174 | func isRequired(optional *bool) bool { 175 | return optional == nil || (optional != nil && !*optional) 176 | } 177 | -------------------------------------------------------------------------------- /internal/githubrepository/config.go: -------------------------------------------------------------------------------- 1 | package githubrepository 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | // Config is the configuration required to start the GitHub Controller. 9 | type Config struct { 10 | Domain string 11 | InsecureSSL bool 12 | CallbackPort string 13 | ReconciliationPeriod time.Duration 14 | } 15 | 16 | // PayloadURL is returns the fully qualified URL used to do payload callbacks to. 17 | func (c Config) PayloadURL(owner, repo string) string { 18 | scheme := "https://" 19 | if c.InsecureSSL { 20 | scheme = "http://" 21 | } 22 | 23 | return fmt.Sprintf("%s%s/payload/%s/%s", scheme, c.Domain, owner, repo) 24 | } 25 | -------------------------------------------------------------------------------- /internal/githubrepository/github_repository.go: -------------------------------------------------------------------------------- 1 | package githubrepository 2 | 3 | import ( 4 | "github.com/manifoldco/heighliner/apis/v1alpha1" 5 | 6 | "github.com/jelmersnoeck/kubekit" 7 | "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" 8 | ) 9 | 10 | var ( 11 | // GitHubRepositoryResource describes the CRD configuration for the 12 | // GitHubRepository CRD. 13 | GitHubRepositoryResource = kubekit.CustomResource{ 14 | Name: "githubrepository", 15 | Plural: "githubrepositories", 16 | Group: v1alpha1.GroupName, 17 | Version: v1alpha1.Version, 18 | Scope: v1beta1.NamespaceScoped, 19 | Aliases: []string{"ghr"}, 20 | Object: &v1alpha1.GitHubRepository{}, 21 | Validation: v1alpha1.GitHubRepositoryValidationSchema, 22 | } 23 | ) 24 | -------------------------------------------------------------------------------- /internal/githubrepository/reconciliation.go: -------------------------------------------------------------------------------- 1 | package githubrepository 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "net/http" 7 | "time" 8 | 9 | "github.com/google/go-github/github" 10 | "github.com/manifoldco/heighliner/apis/v1alpha1" 11 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 12 | ) 13 | 14 | // reconciliateRepository checks whether the reconciliation period has changed and if a new sync is 15 | // required. If so, it gets the repository releases and opened pull-requests and update 16 | // .Status.Releases. 17 | func reconciliateRepository(ctx context.Context, ghClient reconcilationClient, 18 | ghp *v1alpha1.GitHubRepository, period time.Duration) error { 19 | 20 | if ghp.Status.Reconciliation.LastUpdate == nil { 21 | ghp.Status.Reconciliation.LastUpdate = metaTime(time.Now()) 22 | } 23 | 24 | last := ghp.Status.Reconciliation.LastUpdate.Time 25 | 26 | next := last.Add(period) 27 | now := time.Now() 28 | 29 | if now.Before(next) { 30 | return nil 31 | } 32 | 33 | // GitHub doesn't have a way to sort or filter releases. Instead of getting all releases 34 | // all the time, we check first if we already have the latest release. If so, there is no 35 | // need to get all releases. 36 | lastestRelease, resp, err := ghClient.GetLatestRelease(ctx, ghp.Spec.Owner, ghp.Spec.Repo) 37 | if err != nil && resp.StatusCode != http.StatusNotFound { 38 | return err 39 | } 40 | 41 | fetchAllReleases := true 42 | if lastestRelease != nil { 43 | for _, r := range ghp.Status.Releases { 44 | if lastestRelease.TagName != nil && *lastestRelease.TagName == r.Tag { 45 | fetchAllReleases = false 46 | break 47 | } 48 | } 49 | } 50 | 51 | var releases []v1alpha1.GitHubRelease 52 | 53 | // If we need to fetch all releases, we loop over all release pages and collect all 54 | // releases. We then override the current list of .Status.Releases with this new one. 55 | if fetchAllReleases { 56 | var allReleases []*github.RepositoryRelease 57 | 58 | opt := &github.ListOptions{} 59 | 60 | for { 61 | releases, resp, err := ghClient.ListReleases(ctx, ghp.Spec.Owner, ghp.Spec.Repo, opt) 62 | if err != nil { 63 | return err 64 | } 65 | 66 | allReleases = append(allReleases, releases...) 67 | if resp.NextPage == 0 { 68 | break 69 | } 70 | 71 | opt.Page = resp.NextPage 72 | } 73 | 74 | for _, release := range allReleases { 75 | r, active := convertRelease(release) 76 | if active { 77 | releases = append(releases, *r) 78 | } 79 | } 80 | } else { 81 | currentReleases := ghp.Status.Releases 82 | 83 | // Remove previews from the current list because we are fetching all PRs below. 84 | for _, r := range currentReleases { 85 | if r.Level != v1alpha1.SemVerLevelPreview { 86 | releases = append(releases, r) 87 | } 88 | } 89 | } 90 | 91 | opt := &github.PullRequestListOptions{ 92 | State: "open", 93 | Sort: "updated", 94 | Direction: "desc", 95 | } 96 | 97 | // Get the updated PRs. This will only get the latest 30. It should be enough for 98 | // most use-cases. 99 | prs, _, err := ghClient.ListPullRequests(ctx, ghp.Spec.Owner, ghp.Spec.Repo, opt) 100 | if err != nil { 101 | return err 102 | } 103 | 104 | for _, p := range prs { 105 | pr, _ := convertPullRequest(p) 106 | releases = append(releases, *pr) 107 | } 108 | 109 | diffReleases(ghp.Status.Releases, releases) 110 | 111 | ghp.Status.Releases = releases 112 | 113 | ghp.Status.Reconciliation.LastUpdate = metaTime(time.Now()) 114 | 115 | return nil 116 | } 117 | 118 | // diffReleases logs the number of releases added or removed. 119 | func diffReleases(old, new []v1alpha1.GitHubRelease) { 120 | diff := make(map[string]bool) 121 | 122 | added := len(new) 123 | removed := 0 124 | 125 | for _, n := range new { 126 | diff[n.Tag] = true 127 | } 128 | 129 | for _, o := range old { 130 | _, ok := diff[o.Tag] 131 | if ok { 132 | added-- 133 | } else { 134 | removed++ 135 | } 136 | } 137 | 138 | if removed > 0 { 139 | log.Printf("Removed %d releases", removed) 140 | } 141 | 142 | if added > 0 { 143 | log.Printf("Added %d releases", added) 144 | } 145 | } 146 | 147 | // reconciliationClient is an inteface with a subset of functions the GitHub client must implement 148 | // to allow reconciliation of releases and opened pull-requests. 149 | type reconcilationClient interface { 150 | GetLatestRelease(ctx context.Context, owner, repo string) (*github.RepositoryRelease, 151 | *github.Response, error) 152 | ListReleases(ctx context.Context, owner, repo string, opt *github.ListOptions) ( 153 | []*github.RepositoryRelease, *github.Response, error) 154 | ListPullRequests(ctx context.Context, owner string, repo string, 155 | opt *github.PullRequestListOptions) ([]*github.PullRequest, *github.Response, error) 156 | } 157 | 158 | type githubReconciliationClient struct { 159 | *github.Client 160 | } 161 | 162 | func (gh *githubReconciliationClient) GetLatestRelease(ctx context.Context, owner, repo string) ( 163 | *github.RepositoryRelease, *github.Response, error) { 164 | return gh.Client.Repositories.GetLatestRelease(ctx, owner, repo) 165 | } 166 | 167 | func (gh *githubReconciliationClient) ListReleases(ctx context.Context, owner, repo string, 168 | opt *github.ListOptions) ([]*github.RepositoryRelease, *github.Response, error) { 169 | return gh.Client.Repositories.ListReleases(ctx, owner, repo, opt) 170 | } 171 | func (gh *githubReconciliationClient) ListPullRequests(ctx context.Context, owner string, 172 | repo string, opt *github.PullRequestListOptions) ([]*github.PullRequest, *github.Response, 173 | error) { 174 | return gh.Client.PullRequests.List(ctx, owner, repo, opt) 175 | } 176 | 177 | func metaTime(t time.Time) *metav1.Time { 178 | mt := metav1.NewTime(t) 179 | return &mt 180 | } 181 | -------------------------------------------------------------------------------- /internal/imagepolicy/image_policy.go: -------------------------------------------------------------------------------- 1 | package imagepolicy 2 | 3 | import ( 4 | "github.com/jelmersnoeck/kubekit" 5 | "github.com/manifoldco/heighliner/apis/v1alpha1" 6 | "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" 7 | ) 8 | 9 | var ( 10 | // ImagePolicyResource describes the CRD configuration for the ImagePolicy CRD. 11 | ImagePolicyResource = kubekit.CustomResource{ 12 | Name: "imagepolicy", 13 | Plural: "imagepolicies", 14 | Group: v1alpha1.GroupName, 15 | Version: v1alpha1.Version, 16 | Scope: v1beta1.NamespaceScoped, 17 | Aliases: []string{"ip"}, 18 | Object: &v1alpha1.ImagePolicy{}, 19 | Validation: v1alpha1.ImagePolicyValidationSchema, 20 | } 21 | ) 22 | -------------------------------------------------------------------------------- /internal/k8sutils/conversion.go: -------------------------------------------------------------------------------- 1 | package k8sutils 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "k8s.io/apimachinery/pkg/util/intstr" 7 | ) 8 | 9 | // PtrBool converts a boolean value to a pointer of that boolean value. 10 | func PtrBool(b bool) *bool { 11 | return &b 12 | } 13 | 14 | // PtrIntOrString converts a value of the intstr.IntOrString value to a pointer 15 | // of that value. 16 | func PtrIntOrString(i intstr.IntOrString) *intstr.IntOrString { 17 | return &i 18 | } 19 | 20 | // PtrInt64 converts a value of int64 to the pointer of that value. 21 | func PtrInt64(i int64) *int64 { 22 | return &i 23 | } 24 | 25 | // PtrString converts a value of string to the pointer of that value. 26 | func PtrString(s string) *string { 27 | return &s 28 | } 29 | 30 | // JSONBytes converts an interface value to a set of bytes encoded as JSON. 31 | func JSONBytes(val interface{}) []byte { 32 | bts, err := json.Marshal(val) 33 | if err != nil { 34 | panic(err) 35 | } 36 | 37 | return bts 38 | } 39 | -------------------------------------------------------------------------------- /internal/k8sutils/glog_silencer.go: -------------------------------------------------------------------------------- 1 | package k8sutils 2 | 3 | import "flag" 4 | 5 | func init() { 6 | // we're getting a lot of errors about logging before flag parsing, this 7 | // should resolve that. Seeing that it's a common package, we don't need to 8 | // include this for every controller. 9 | flag.Parse() 10 | } 11 | -------------------------------------------------------------------------------- /internal/k8sutils/hash.go: -------------------------------------------------------------------------------- 1 | package k8sutils 2 | 3 | import ( 4 | "encoding/base32" 5 | "strings" 6 | 7 | "github.com/dchest/blake2b" 8 | ) 9 | 10 | var encoder = base32.HexEncoding.WithPadding(base32.NoPadding) 11 | 12 | // ShortHash creates a shortened hash from the given string. The hash is 13 | // lowercase base32 encoded, suitable for DNS use, and at most "len" characters 14 | // long. 15 | func ShortHash(data string, len int) string { 16 | b2b, _ := blake2b.New(&blake2b.Config{Size: uint8(len * 5 / 8)}) 17 | b2b.Write([]byte(data)) 18 | return strings.ToLower(encoder.EncodeToString(b2b.Sum(nil))) 19 | } 20 | -------------------------------------------------------------------------------- /internal/k8sutils/patch.go: -------------------------------------------------------------------------------- 1 | package k8sutils 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | ) 7 | 8 | // CleanupPatchAnnotations cleans up a patch diff to remove the kubekit 9 | // annotations. 10 | // This is useful for when a patch is applied and we don't want to print the 11 | // annotations but just the actual diff. 12 | func CleanupPatchAnnotations(patch []byte, name string) ([]byte, error) { 13 | data := map[string]interface{}{} 14 | if err := json.Unmarshal(patch, &data); err != nil { 15 | return patch, err 16 | } 17 | 18 | data = cleanKeys(data, fmt.Sprintf("kubekit-%s/last-applied-configuration", name), "status", "$retainKeys") 19 | return json.Marshal(data) 20 | } 21 | 22 | // cleanKeys is a recursive function which cleans specific keys from a nested 23 | // map. 24 | func cleanKeys(data map[string]interface{}, keys ...string) map[string]interface{} { 25 | keyData := map[string]interface{}{} 26 | 27 | for k, v := range data { 28 | if cleanupKey(k, keys...) { 29 | continue 30 | } 31 | 32 | valueData := v 33 | if rawData, ok := v.(map[string]interface{}); ok { 34 | mappedData := cleanKeys(rawData, keys...) 35 | if len(mappedData) == 0 { 36 | continue 37 | } 38 | 39 | valueData = mappedData 40 | } 41 | 42 | if valueData != nil { 43 | keyData[k] = valueData 44 | } 45 | } 46 | 47 | return keyData 48 | } 49 | 50 | func cleanupKey(key string, keys ...string) bool { 51 | for _, k := range keys { 52 | if k == key { 53 | return true 54 | } 55 | } 56 | 57 | return false 58 | } 59 | -------------------------------------------------------------------------------- /internal/k8sutils/random.go: -------------------------------------------------------------------------------- 1 | package k8sutils 2 | 3 | import ( 4 | "math/rand" 5 | "time" 6 | ) 7 | 8 | func init() { 9 | rand.Seed(time.Now().UnixNano()) 10 | } 11 | 12 | var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") 13 | 14 | // RandomString will return a string of length n with random characters. 15 | func RandomString(n int) string { 16 | b := make([]rune, n) 17 | for i := range b { 18 | b[i] = letterRunes[rand.Intn(len(letterRunes))] 19 | } 20 | 21 | return string(b) 22 | } 23 | -------------------------------------------------------------------------------- /internal/meta/definitions.go: -------------------------------------------------------------------------------- 1 | package meta 2 | 3 | const ( 4 | // LabelServiceKey is used to annotate the application with a specific 5 | // service key set by Heighliner. This way we always have at least one label 6 | // available for LabelSelectors. 7 | LabelServiceKey = "hlnr.io/service" 8 | ) 9 | -------------------------------------------------------------------------------- /internal/meta/meta.go: -------------------------------------------------------------------------------- 1 | package meta 2 | 3 | import ( 4 | "regexp" 5 | "unicode/utf8" 6 | 7 | "github.com/jelmersnoeck/kubekit" 8 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 | "k8s.io/apimachinery/pkg/runtime" 10 | 11 | "github.com/manifoldco/heighliner/apis/v1alpha1" 12 | ) 13 | 14 | // Annotations returns a set of annotations annotated with the Heighliner 15 | // defaults. 16 | func Annotations(ann map[string]string, version string, resource runtime.Object) map[string]string { 17 | if ann == nil { 18 | ann = map[string]string{} 19 | } 20 | 21 | ann["hlnr.io/version"] = version 22 | ann["hlnr.io/component"] = kubekit.TypeName(resource) 23 | return ann 24 | } 25 | 26 | // Labels returns a new set of labels annotated with Heighliner specific 27 | // defaults. 28 | func Labels(labels map[string]string, m metav1.Object) map[string]string { 29 | if labels == nil { 30 | labels = map[string]string{} 31 | } 32 | 33 | labels[LabelServiceKey] = labelize(m.GetName()) 34 | return labels 35 | } 36 | 37 | // MicroserviceLabels returns a new set of labels annotated with Heighliner specific 38 | // defaults (as from Label), and release specific values. 39 | func MicroserviceLabels(ms *v1alpha1.Microservice, r *v1alpha1.Release, parent metav1.Object) map[string]string { 40 | labels := Labels(parent.GetLabels(), parent) 41 | 42 | labels["hlnr.io/microservice.full_name"] = labelize(r.FullName(ms.Name)) 43 | labels["hlnr.io/microservice.name"] = labelize(ms.Name) 44 | labels["hlnr.io/microservice.release"] = labelize(r.Name()) 45 | labels["hlnr.io/microservice.version"] = labelize(r.Version()) 46 | 47 | return labels 48 | } 49 | 50 | // trim a string to at most len runes from a utf8 byte sequence. 51 | func trim(s string, l int) string { 52 | var ns string 53 | var i int 54 | for _, c := range s { 55 | i++ 56 | ns += string(c) 57 | if i >= l { 58 | break 59 | } 60 | } 61 | 62 | return ns 63 | } 64 | 65 | // elide trims a string to l chars, with '...0' in the end, included in l. 66 | func elide(s string, l int) string { 67 | if l <= 4 { 68 | return trim(s, l) 69 | } 70 | 71 | n := utf8.RuneCount([]byte(s)) 72 | if n <= l { 73 | return s 74 | } 75 | 76 | return trim(s, l-4) + "...0" 77 | } 78 | 79 | var unlabelChars = regexp.MustCompile(`([^a-zA-Z0-9._-])`) 80 | var unlabelStart = regexp.MustCompile(`^([^a-zA-Z0-9])`) 81 | var unlabelEnd = regexp.MustCompile(`([^a-zA-Z0-9])$`) 82 | 83 | // labelize coerces a string into a format valid for k8s label values, as 84 | // outlined here: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#syntax-and-character-set 85 | // 86 | // Our rules: 87 | // - If the value starts or ends with a non-alphanumeric value, 0 is 88 | // prepended/appended. 89 | // - characters that are not within [a-zA-Z0-9._-] are replaced with _. 90 | // - values greater than 63 characters are elided, with a trailing 0. 91 | // 92 | // Values passed through labelize for normalization should be for 93 | // informational/debugging purposes only. If you rely on the label for 94 | // something, make sure you normalize it yourself. 95 | // 96 | // Note that Name fields are up to 253 chars long, and so should be passed 97 | // through labelize as well. 98 | func labelize(s string) string { 99 | s = unlabelStart.ReplaceAllString(s, "0${1}") 100 | s = unlabelEnd.ReplaceAllString(s, "${1}0") 101 | s = unlabelChars.ReplaceAllString(s, "_") 102 | return elide(s, 63) 103 | } 104 | -------------------------------------------------------------------------------- /internal/meta/meta_test.go: -------------------------------------------------------------------------------- 1 | package meta 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/manifoldco/heighliner/apis/v1alpha1" 8 | ) 9 | 10 | func TestMicroserviceLabels(t *testing.T) { 11 | 12 | ms := &v1alpha1.Microservice{} 13 | ms.Name = "test" 14 | r := &v1alpha1.Release{ 15 | SemVer: &v1alpha1.SemVerRelease{ 16 | Name: "a-branch", 17 | Version: "0.0.1", 18 | }, 19 | Level: v1alpha1.SemVerLevelPreview, 20 | } 21 | 22 | l := MicroserviceLabels(ms, r, ms) 23 | 24 | expected := map[string]string{ 25 | "hlnr.io/service": "test", 26 | "hlnr.io/microservice.name": "test", 27 | "hlnr.io/microservice.full_name": "test-pr-ebq4dofr-svek39uq", 28 | "hlnr.io/microservice.release": "a-branch", 29 | "hlnr.io/microservice.version": "0.0.1", 30 | } 31 | 32 | if !reflect.DeepEqual(l, expected) { 33 | t.Error("labels did not match. got:", l, "wanted:", expected) 34 | } 35 | } 36 | 37 | func TestTrim(t *testing.T) { 38 | tcs := []struct { 39 | name string 40 | in string 41 | l int 42 | out string 43 | }{ 44 | {"empty", "", 2, ""}, 45 | {"short", "abc", 4, "abc"}, 46 | {"long", "abcdef", 4, "abcd"}, 47 | {"unicode", "😍cool stuff", 4, "😍coo"}, 48 | } 49 | 50 | for _, tc := range tcs { 51 | t.Run(tc.name, func(t *testing.T) { 52 | if trim(tc.in, tc.l) != tc.out { 53 | t.Error("trim did not match. got:", trim(tc.in, tc.l), "wanted:", tc.out) 54 | } 55 | }) 56 | } 57 | } 58 | 59 | func TestElide(t *testing.T) { 60 | tcs := []struct { 61 | name string 62 | in string 63 | l int 64 | out string 65 | }{ 66 | {"empty", "", 4, ""}, 67 | {"skip elide", "abc", 2, "ab"}, 68 | {"short", "abc", 4, "abc"}, 69 | {"long", "abcdef", 5, "a...0"}, 70 | {"unicode", "😍cool stuff", 7, "😍co...0"}, 71 | } 72 | 73 | for _, tc := range tcs { 74 | t.Run(tc.name, func(t *testing.T) { 75 | if elide(tc.in, tc.l) != tc.out { 76 | t.Error("elide did not match. got:", elide(tc.in, tc.l), "wanted:", tc.out) 77 | } 78 | }) 79 | } 80 | } 81 | 82 | func TestLabelize(t *testing.T) { 83 | tcs := []struct { 84 | name string 85 | in string 86 | out string 87 | }{ 88 | {"empty", "", ""}, 89 | {"same", "ab0-c", "ab0-c"}, 90 | {"pad with 0s", ".abc.", "0.abc.0"}, 91 | {"replace chars", "replace space? great", "replace_space__great"}, 92 | {"replace unicode", "😍cool stuff", "0_cool_stuff"}, 93 | {"elided", 94 | "The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog", 95 | "The_quick_brown_fox_jumps_over_the_lazy_dog._The_quick_brow...0"}, 96 | } 97 | 98 | for _, tc := range tcs { 99 | t.Run(tc.name, func(t *testing.T) { 100 | if labelize(tc.in) != tc.out { 101 | t.Error("labelize did not match. got:", labelize(tc.in), "wanted:", tc.out) 102 | } 103 | }) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /internal/networkpolicy/ingress.go: -------------------------------------------------------------------------------- 1 | package networkpolicy 2 | 3 | import ( 4 | "bytes" 5 | "html/template" 6 | "strconv" 7 | "strings" 8 | 9 | "github.com/manifoldco/heighliner/apis/v1alpha1" 10 | "github.com/manifoldco/heighliner/internal/meta" 11 | 12 | "github.com/jelmersnoeck/kubekit" 13 | 14 | corev1 "k8s.io/api/core/v1" 15 | "k8s.io/api/extensions/v1beta1" 16 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 17 | "k8s.io/apimachinery/pkg/util/intstr" 18 | ) 19 | 20 | func buildIngressForRelease(ms *v1alpha1.Microservice, np *v1alpha1.NetworkPolicy, release *v1alpha1.Release, srv metav1.Object) (*v1beta1.Ingress, error) { 21 | if len(np.Spec.ExternalDNS) == 0 { 22 | return nil, nil 23 | } 24 | 25 | // TODO (jelmer): if there's different ingress classes, this should deploy 26 | // different ingress objects. For now, this will do. 27 | ingressClass := np.Spec.ExternalDNS[0].IngressClass 28 | if ingressClass == "" { 29 | ingressClass = "nginx" 30 | } 31 | 32 | domains := make([]string, len(np.Spec.ExternalDNS)) 33 | for i, record := range np.Spec.ExternalDNS { 34 | var err error 35 | if domains[i], err = templatedDomain(ms, release, record.Domain); err != nil { 36 | return nil, err 37 | } 38 | } 39 | 40 | labels := meta.MicroserviceLabels(ms, release, np) 41 | 42 | annotations := meta.Annotations(np.Annotations, v1alpha1.Version, np) 43 | annotations["kubernetes.io/ingress.class"] = ingressClass 44 | annotations["external-dns.alpha.kubernetes.io/hostname"] = strings.Join(domains, ",") 45 | // TODO (jelmer): different TTLs should mean different Ingresses 46 | annotations["external-dns.alpha.kubernetes.io/ttl"] = ttlValue(np.Spec.ExternalDNS[0].TTL) 47 | 48 | // Disable SSL redirects when we don't have TLS enabled. 49 | if np.Spec.ExternalDNS[0].DisableTLS { 50 | annotations["nginx.ingress.kubernetes.io/ssl-redirect"] = "false" 51 | } 52 | 53 | ingressTLS, err := getIngressTLS(ms, release, np.Spec.ExternalDNS) 54 | if err != nil { 55 | return nil, err 56 | } 57 | 58 | ingressRules, err := getIngressRules(ms, release, np.Spec.ExternalDNS) 59 | if err != nil { 60 | return nil, err 61 | } 62 | 63 | ing := &v1beta1.Ingress{ 64 | TypeMeta: metav1.TypeMeta{ 65 | Kind: "Ingress", 66 | APIVersion: "extensions/v1beta1", 67 | }, 68 | ObjectMeta: metav1.ObjectMeta{ 69 | Name: release.StreamName(ms.Name), 70 | Namespace: ms.Namespace, 71 | Labels: labels, 72 | Annotations: annotations, 73 | OwnerReferences: []metav1.OwnerReference{ 74 | *metav1.NewControllerRef( 75 | srv, 76 | corev1.SchemeGroupVersion.WithKind(kubekit.TypeName(srv)), 77 | ), 78 | }, 79 | }, 80 | Spec: v1beta1.IngressSpec{ 81 | TLS: ingressTLS, 82 | Rules: ingressRules, 83 | }, 84 | } 85 | 86 | return ing, nil 87 | } 88 | 89 | func getIngressRules(ms *v1alpha1.Microservice, release *v1alpha1.Release, records []v1alpha1.ExternalDNS) ([]v1beta1.IngressRule, error) { 90 | rules := make([]v1beta1.IngressRule, len(records)) 91 | for i, r := range records { 92 | servicePort := "headless" 93 | if r.Port != "" { 94 | servicePort = r.Port 95 | } 96 | 97 | domain, err := templatedDomain(ms, release, r.Domain) 98 | if err != nil { 99 | return nil, err 100 | } 101 | 102 | rules[i] = v1beta1.IngressRule{ 103 | Host: domain, 104 | IngressRuleValue: v1beta1.IngressRuleValue{ 105 | HTTP: &v1beta1.HTTPIngressRuleValue{ 106 | Paths: []v1beta1.HTTPIngressPath{ 107 | { 108 | Path: "/", 109 | Backend: v1beta1.IngressBackend{ 110 | ServiceName: release.StreamName(ms.Name), 111 | ServicePort: intstr.FromString(servicePort), 112 | }, 113 | }, 114 | }, 115 | }, 116 | }, 117 | } 118 | } 119 | 120 | return rules, nil 121 | } 122 | 123 | func getIngressTLS(ms *v1alpha1.Microservice, release *v1alpha1.Release, records []v1alpha1.ExternalDNS) ([]v1beta1.IngressTLS, error) { 124 | tls := make([]v1beta1.IngressTLS, len(records)) 125 | 126 | for i, dns := range records { 127 | if dns.DisableTLS { 128 | continue 129 | } 130 | 131 | secretName := "heighliner-components" 132 | if dns.TLSGroup != "" { 133 | secretName = dns.TLSGroup 134 | } 135 | 136 | domain, err := templatedDomain(ms, release, dns.Domain) 137 | if err != nil { 138 | return nil, err 139 | } 140 | 141 | tls[i] = v1beta1.IngressTLS{ 142 | Hosts: []string{domain}, 143 | SecretName: secretName, 144 | } 145 | } 146 | 147 | return tls, nil 148 | } 149 | 150 | func templatedDomain(ms *v1alpha1.Microservice, release *v1alpha1.Release, domain string) (string, error) { 151 | tmpl, err := template.New("domain").Parse(domain) 152 | if err != nil { 153 | return "", err 154 | } 155 | 156 | data := struct { 157 | FullName string 158 | StreamName string 159 | Name string 160 | }{ 161 | 162 | FullName: release.FullName(ms.Name), 163 | StreamName: release.StreamName(ms.Name), 164 | Name: release.Name(), 165 | } 166 | 167 | buf := bytes.NewBufferString("") 168 | if err := tmpl.Execute(buf, data); err != nil { 169 | return "", err 170 | } 171 | return buf.String(), nil 172 | } 173 | 174 | func ttlValue(ttl int32) string { 175 | if ttl == 0 { 176 | return "300" 177 | } 178 | 179 | return strconv.Itoa(int(ttl)) 180 | } 181 | -------------------------------------------------------------------------------- /internal/networkpolicy/ingress_test.go: -------------------------------------------------------------------------------- 1 | package networkpolicy 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/jelmersnoeck/kubekit" 8 | "github.com/manifoldco/heighliner/apis/v1alpha1" 9 | corev1 "k8s.io/api/core/v1" 10 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 11 | ) 12 | 13 | func TestBuildIngressForRelease(t *testing.T) { 14 | ms := &v1alpha1.Microservice{ 15 | ObjectMeta: metav1.ObjectMeta{ 16 | Name: "hello-world", 17 | }, 18 | } 19 | 20 | release := &v1alpha1.Release{ 21 | SemVer: &v1alpha1.SemVerRelease{ 22 | Name: "hello-world", 23 | }, 24 | Level: v1alpha1.SemVerLevelRelease, 25 | } 26 | 27 | np := &v1alpha1.NetworkPolicy{} 28 | 29 | srv := &corev1.Service{ 30 | TypeMeta: metav1.TypeMeta{ 31 | Kind: "Service", 32 | }, 33 | ObjectMeta: metav1.ObjectMeta{ 34 | Name: "srv", 35 | }, 36 | } 37 | 38 | t.Run("Sets OwnerReference to the service", func(t *testing.T) { 39 | np := &v1alpha1.NetworkPolicy{ 40 | Spec: v1alpha1.NetworkPolicySpec{ 41 | ExternalDNS: []v1alpha1.ExternalDNS{ 42 | {Domain: "fake.fake"}, 43 | }, 44 | }, 45 | } 46 | 47 | ing, err := buildIngressForRelease(ms, np, release, srv) 48 | if err != nil { 49 | t.Error("Expected no err. got:", err) 50 | } 51 | 52 | if len(ing.OwnerReferences) != 1 { 53 | t.Error("Wrong number of owners:", len(ing.OwnerReferences)) 54 | } 55 | 56 | ownerReference := *metav1.NewControllerRef( 57 | srv, 58 | corev1.SchemeGroupVersion.WithKind(kubekit.TypeName(srv)), 59 | ) 60 | 61 | if !reflect.DeepEqual(ing.OwnerReferences[0], ownerReference) { 62 | t.Errorf("Bad OwnerReference seen. got\n%#v\n\nwanted\n%#v", ing.OwnerReferences[0], ownerReference) 63 | } 64 | }) 65 | 66 | t.Run("It is nil with no external dns", func(t *testing.T) { 67 | 68 | ing, err := buildIngressForRelease(ms, np, release, srv) 69 | if err != nil { 70 | t.Error("Expected no err. got:", err) 71 | } 72 | 73 | if ing != nil { 74 | t.Error("Expected no ingress. got:", ing) 75 | } 76 | }) 77 | } 78 | 79 | func TestTemplatedDomain(t *testing.T) { 80 | release := &v1alpha1.Release{ 81 | SemVer: &v1alpha1.SemVerRelease{ 82 | Name: "hello-world", 83 | Version: "0.0.1", 84 | }, 85 | Level: v1alpha1.SemVerLevelPreview, 86 | } 87 | 88 | ms := &v1alpha1.Microservice{ 89 | ObjectMeta: metav1.ObjectMeta{ 90 | Name: "hello-world", 91 | }, 92 | } 93 | 94 | testData := []struct { 95 | domain string 96 | expected string 97 | err error 98 | }{ 99 | {"{{.FullName}}.arigato.tools", "hello-world-pr-cmqolv9f-svek39uq.arigato.tools", nil}, 100 | {"{{.Name}}.arigato.tools", "hello-world.arigato.tools", nil}, 101 | {"{{.StreamName}}.arigato.tools", "hello-world-pr-cmqolv9f.arigato.tools", nil}, 102 | } 103 | 104 | for _, item := range testData { 105 | str, err := templatedDomain(ms, release, item.domain) 106 | if err != item.err { 107 | t.Errorf("Expected error to be '%s', got '%s'", item.err, err) 108 | continue 109 | } 110 | 111 | if str != item.expected { 112 | t.Errorf("Expected domain to be '%s', got '%s'", item.expected, str) 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /internal/networkpolicy/network_policy.go: -------------------------------------------------------------------------------- 1 | package networkpolicy 2 | 3 | import ( 4 | "github.com/manifoldco/heighliner/apis/v1alpha1" 5 | 6 | "github.com/jelmersnoeck/kubekit" 7 | "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" 8 | ) 9 | 10 | var ( 11 | // NetworkPolicyResource describes the CRD networkuration for the 12 | // NetworkPolicy CRD. 13 | NetworkPolicyResource = kubekit.CustomResource{ 14 | Name: "networkpolicy", 15 | Plural: "networkpolicies", 16 | Group: v1alpha1.GroupName, 17 | Version: v1alpha1.Version, 18 | Scope: v1beta1.NamespaceScoped, 19 | Aliases: []string{"hnp"}, // hnp: heighliner network policy 20 | Object: &v1alpha1.NetworkPolicy{}, 21 | Validation: v1alpha1.NetworkPolicyValidationSchema, 22 | } 23 | 24 | // VersioningPolicyResource describes the CRD configuration for the 25 | // VersioningPolicy CRD. 26 | VersioningPolicyResource = kubekit.CustomResource{ 27 | Name: "versioningpolicy", 28 | Plural: "versioningpolicies", 29 | Group: v1alpha1.GroupName, 30 | Version: v1alpha1.Version, 31 | Scope: v1beta1.NamespaceScoped, 32 | Aliases: []string{"vp"}, 33 | Object: &v1alpha1.VersioningPolicy{}, 34 | Validation: &v1beta1.CustomResourceValidation{ 35 | OpenAPIV3Schema: &v1beta1.JSONSchemaProps{ 36 | Properties: map[string]v1beta1.JSONSchemaProps{ 37 | "spec": v1alpha1.VersioningPolicyValidationSchema, 38 | }, 39 | }, 40 | }, 41 | } 42 | ) 43 | -------------------------------------------------------------------------------- /internal/networkpolicy/network_status.go: -------------------------------------------------------------------------------- 1 | package networkpolicy 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/manifoldco/heighliner/apis/v1alpha1" 7 | ) 8 | 9 | func buildNetworkStatusDomainsForRelease(ms *v1alpha1.Microservice, np *v1alpha1.NetworkPolicy, release *v1alpha1.Release) ([]v1alpha1.Domain, error) { 10 | domains := []v1alpha1.Domain{} 11 | 12 | for _, record := range np.Spec.ExternalDNS { 13 | url, err := templatedDomain(ms, release, getFullURL(record)) 14 | if err != nil { 15 | // XXX: handle gracefully 16 | panic(err) 17 | } 18 | 19 | domain := v1alpha1.Domain{ 20 | URL: url, 21 | SemVer: release.SemVer, 22 | } 23 | domains = append(domains, domain) 24 | } 25 | 26 | return domains, nil 27 | } 28 | 29 | func getFullURL(dns v1alpha1.ExternalDNS) string { 30 | scheme := "https://" 31 | if dns.DisableTLS { 32 | scheme = "http://" 33 | } 34 | 35 | return fmt.Sprintf("%s%s", scheme, dns.Domain) 36 | } 37 | 38 | func statusDomainsEqual(old, new []v1alpha1.Domain) bool { 39 | if len(old) != len(new) { 40 | return false 41 | } 42 | 43 | oldLoop: 44 | for _, o := range old { 45 | for _, n := range new { 46 | if o.URL != n.URL { 47 | continue 48 | } 49 | 50 | if o.SemVer == nil && n.SemVer != nil || 51 | o.SemVer != nil && n.SemVer == nil { 52 | continue 53 | } 54 | 55 | if o.SemVer != nil && 56 | (o.SemVer.Name != n.SemVer.Name || 57 | o.SemVer.Version != n.SemVer.Version) { 58 | continue 59 | } 60 | 61 | continue oldLoop // found a match! 62 | } 63 | 64 | return false // didn't find a match :( 65 | } 66 | 67 | return true 68 | } 69 | -------------------------------------------------------------------------------- /internal/networkpolicy/network_status_test.go: -------------------------------------------------------------------------------- 1 | package networkpolicy 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/manifoldco/heighliner/apis/v1alpha1" 8 | ) 9 | 10 | func TestNetworkStatus(t *testing.T) { 11 | release := &v1alpha1.Release{ 12 | SemVer: &v1alpha1.SemVerRelease{ 13 | Name: "hello-world", 14 | }, 15 | Level: v1alpha1.SemVerLevelRelease, 16 | } 17 | 18 | ms := &v1alpha1.Microservice{} 19 | 20 | t.Run("with a single domain network policy", func(t *testing.T) { 21 | np := &v1alpha1.NetworkPolicy{ 22 | Spec: v1alpha1.NetworkPolicySpec{ 23 | ExternalDNS: []v1alpha1.ExternalDNS{ 24 | { 25 | Domain: "my.cool.domain", 26 | }, 27 | }, 28 | }, 29 | } 30 | 31 | domains, _ := buildNetworkStatusDomainsForRelease(ms, np, release) 32 | if len(domains) != 1 { 33 | t.Errorf("Expected domains to be of length 1, got '%d'", len(domains)) 34 | } 35 | 36 | actualDomainURL := domains[0].URL 37 | expectedDomainURL := "https://my.cool.domain" 38 | if actualDomainURL != expectedDomainURL { 39 | t.Errorf("Expected domain URL to be %s, got '%s'", expectedDomainURL, actualDomainURL) 40 | } 41 | 42 | }) 43 | 44 | t.Run("with a multi domain network policy", func(t *testing.T) { 45 | np := &v1alpha1.NetworkPolicy{ 46 | Spec: v1alpha1.NetworkPolicySpec{ 47 | ExternalDNS: []v1alpha1.ExternalDNS{ 48 | { 49 | Domain: "my.cool.domain", 50 | }, 51 | { 52 | Domain: "my.other.cool.domain", 53 | }, 54 | }, 55 | }, 56 | } 57 | 58 | domains, _ := buildNetworkStatusDomainsForRelease(ms, np, release) 59 | if len(domains) != 2 { 60 | t.Errorf("Expected domains to be of length 2, got '%d'", len(domains)) 61 | } 62 | 63 | actualFirstDomainURL := domains[0].URL 64 | expectedFirstDomainURL := "https://my.cool.domain" 65 | if actualFirstDomainURL != expectedFirstDomainURL { 66 | t.Errorf("Expected domain URL to be %s, got '%s'", expectedFirstDomainURL, actualFirstDomainURL) 67 | } 68 | 69 | actualSecondDomainURL := domains[1].URL 70 | expectedSecondDomainURL := "https://my.other.cool.domain" 71 | if actualSecondDomainURL != expectedSecondDomainURL { 72 | t.Errorf("Expected domain URL to be %s, got '%s'", expectedSecondDomainURL, actualSecondDomainURL) 73 | } 74 | 75 | }) 76 | } 77 | 78 | func TestFullDomain(t *testing.T) { 79 | t.Run("without TLS disabled", func(t *testing.T) { 80 | domain := "my.cool.domain" 81 | url := fmt.Sprintf("https://%s", domain) 82 | 83 | dns := v1alpha1.ExternalDNS{ 84 | Domain: domain, 85 | } 86 | 87 | if actual := getFullURL(dns); actual != url { 88 | t.Errorf("Expected url to be '%s', got '%s'", url, actual) 89 | } 90 | }) 91 | 92 | t.Run("with TLS disabled", func(t *testing.T) { 93 | domain := "my.cool.domain" 94 | url := fmt.Sprintf("http://%s", domain) 95 | 96 | dns := v1alpha1.ExternalDNS{ 97 | Domain: domain, 98 | DisableTLS: true, 99 | } 100 | 101 | if actual := getFullURL(dns); actual != url { 102 | t.Errorf("Expected url to be '%s', got '%s'", url, actual) 103 | } 104 | }) 105 | } 106 | 107 | func TestStatusDomainsEqual(t *testing.T) { 108 | tcs := []struct { 109 | name string 110 | old []v1alpha1.Domain 111 | new []v1alpha1.Domain 112 | expected bool 113 | }{ 114 | {"empty (nil/nil)", nil, nil, true}, 115 | {"empty (nil/0)", nil, []v1alpha1.Domain{}, true}, 116 | 117 | {"one entry (equal)", 118 | []v1alpha1.Domain{{URL: "https://fake.fake"}}, 119 | []v1alpha1.Domain{{URL: "https://fake.fake"}}, 120 | true, 121 | }, 122 | 123 | {"one entry (equal with semver)", 124 | []v1alpha1.Domain{{URL: "https://fake.fake", SemVer: &v1alpha1.SemVerRelease{Name: "foo", Version: "0.1.0"}}}, 125 | []v1alpha1.Domain{{URL: "https://fake.fake", SemVer: &v1alpha1.SemVerRelease{Name: "foo", Version: "0.1.0"}}}, 126 | true, 127 | }, 128 | 129 | {"one entry (mismatched semver)", 130 | []v1alpha1.Domain{{URL: "https://fake.fake", SemVer: &v1alpha1.SemVerRelease{Name: "foo", Version: "0.1.0"}}}, 131 | []v1alpha1.Domain{{URL: "https://fake.fake", SemVer: &v1alpha1.SemVerRelease{Name: "foo", Version: "0.1.1"}}}, 132 | false, 133 | }, 134 | 135 | {"one entry (mismatched URL)", 136 | []v1alpha1.Domain{{URL: "https://fake.fake", SemVer: &v1alpha1.SemVerRelease{Name: "foo", Version: "0.1.0"}}}, 137 | []v1alpha1.Domain{{URL: "https://anotherfake.fake", SemVer: &v1alpha1.SemVerRelease{Name: "foo", Version: "0.1.0"}}}, 138 | false, 139 | }, 140 | 141 | {"two entries out of order", 142 | []v1alpha1.Domain{{URL: "https://fake.fake"}, {URL: "https://other.fake"}}, 143 | []v1alpha1.Domain{{URL: "https://other.fake"}, {URL: "https://fake.fake"}}, 144 | true, 145 | }, 146 | 147 | {"mismatched length", 148 | []v1alpha1.Domain{{URL: "https://fake.fake"}}, 149 | []v1alpha1.Domain{{URL: "https://other.fake"}, {URL: "https://fake.fake"}}, 150 | false, 151 | }, 152 | } 153 | 154 | for _, tc := range tcs { 155 | t.Run(tc.name, func(t *testing.T) { 156 | if statusDomainsEqual(tc.old, tc.new) != tc.expected { 157 | t.Error("bad result for statusDomainsEqual") 158 | } 159 | }) 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /internal/networkpolicy/releaser.go: -------------------------------------------------------------------------------- 1 | package networkpolicy 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/manifoldco/heighliner/apis/v1alpha1" 7 | ) 8 | 9 | // LatestReleaser is able to select a release based on the releasetime date. 10 | type LatestReleaser struct{} 11 | 12 | // ExternalRelease goes over all releases and releases the latest release based 13 | // on the releaseTime timestamp. 14 | func (r *LatestReleaser) ExternalRelease(releases []v1alpha1.Release) (*v1alpha1.Release, error) { 15 | if len(releases) == 0 { 16 | return nil, errors.New("Need at least one release to link to an external release") 17 | } 18 | 19 | latestRelease := releases[0] 20 | for _, release := range releases { 21 | if latestRelease.ReleaseTime.Before(&release.ReleaseTime) { 22 | latestRelease = release 23 | } 24 | } 25 | 26 | return &latestRelease, nil 27 | } 28 | -------------------------------------------------------------------------------- /internal/networkpolicy/releaser_test.go: -------------------------------------------------------------------------------- 1 | package networkpolicy 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/manifoldco/heighliner/apis/v1alpha1" 8 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 | ) 10 | 11 | func TestLatestReleaser(t *testing.T) { 12 | releases := []v1alpha1.Release{ 13 | { 14 | Image: "1", 15 | ReleaseTime: metav1.Date(2018, time.April, 28, 13, 42, 01, 0, time.UTC), 16 | }, 17 | { 18 | Image: "2", 19 | ReleaseTime: metav1.Date(2018, time.April, 29, 13, 52, 01, 0, time.UTC), 20 | }, 21 | { 22 | Image: "3", 23 | ReleaseTime: metav1.Date(2018, time.April, 27, 13, 32, 01, 0, time.UTC), 24 | }, 25 | } 26 | 27 | releaser := &LatestReleaser{} 28 | release, err := releaser.ExternalRelease(releases) 29 | if err != nil { 30 | t.Errorf("Expected no error, got '%s'", err) 31 | } 32 | 33 | if release.Image != "2" { 34 | t.Errorf("Expected release to be '2', got '%s'", release.Image) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /internal/networkpolicy/service.go: -------------------------------------------------------------------------------- 1 | package networkpolicy 2 | 3 | import ( 4 | "github.com/manifoldco/heighliner/apis/v1alpha1" 5 | "github.com/manifoldco/heighliner/internal/meta" 6 | 7 | corev1 "k8s.io/api/core/v1" 8 | "k8s.io/apimachinery/pkg/util/intstr" 9 | ) 10 | 11 | func buildServiceForRelease(srv *corev1.Service, svc *v1alpha1.Microservice, np *v1alpha1.NetworkPolicy, release *v1alpha1.Release) *corev1.Service { 12 | labels := meta.MicroserviceLabels(svc, release, np) 13 | if srv.Labels == nil { 14 | srv.Labels = labels 15 | } else { 16 | // maintain any existing labels, adding our new ones 17 | for k, v := range labels { 18 | srv.Labels[k] = v 19 | } 20 | } 21 | 22 | annotations := meta.Annotations(np.Annotations, v1alpha1.Version, np) 23 | if srv.Annotations == nil { 24 | srv.Annotations = annotations 25 | } else { 26 | // maintain any existing annotations, adding our new ones 27 | for k, v := range annotations { 28 | srv.Annotations[k] = v 29 | } 30 | } 31 | 32 | selector := make(map[string]string) 33 | for k, v := range labels { 34 | selector[k] = v 35 | } 36 | delete(selector, meta.LabelServiceKey) 37 | 38 | sessionAffinity := corev1.ServiceAffinityNone 39 | if np.Spec.SessionAffinity != nil && np.Spec.SessionAffinity.ClientIP != nil { 40 | sessionAffinity = corev1.ServiceAffinityClientIP 41 | } 42 | 43 | srv.OwnerReferences = release.OwnerReferences 44 | srv.Spec.Type = corev1.ServiceTypeNodePort 45 | srv.Spec.Ports = getServicePorts(np.Spec.Ports) 46 | srv.Spec.Selector = selector 47 | srv.Spec.SessionAffinity = sessionAffinity 48 | srv.Spec.SessionAffinityConfig = np.Spec.SessionAffinity 49 | 50 | return srv 51 | } 52 | 53 | func getServicePorts(networkPorts []v1alpha1.NetworkPort) []corev1.ServicePort { 54 | ports := make([]corev1.ServicePort, len(networkPorts)) 55 | 56 | for i, port := range networkPorts { 57 | ports[i] = corev1.ServicePort{ 58 | Protocol: corev1.ProtocolTCP, 59 | Name: port.Name, 60 | Port: port.Port, 61 | TargetPort: intstr.FromInt(int(port.TargetPort)), 62 | } 63 | } 64 | 65 | return ports 66 | } 67 | -------------------------------------------------------------------------------- /internal/networkpolicy/service_test.go: -------------------------------------------------------------------------------- 1 | package networkpolicy 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/manifoldco/heighliner/apis/v1alpha1" 7 | corev1 "k8s.io/api/core/v1" 8 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 | ) 10 | 11 | func TestBuildServiceForRelease(t *testing.T) { 12 | release := &v1alpha1.Release{ 13 | SemVer: &v1alpha1.SemVerRelease{ 14 | Name: "test-application", 15 | Version: "1.2.3", 16 | }, 17 | Level: v1alpha1.SemVerLevelRelease, 18 | } 19 | 20 | ms := &v1alpha1.Microservice{ 21 | ObjectMeta: metav1.ObjectMeta{ 22 | Name: "test-application", 23 | }, 24 | } 25 | 26 | np := &v1alpha1.NetworkPolicy{ 27 | Spec: v1alpha1.NetworkPolicySpec{ 28 | Ports: []v1alpha1.NetworkPort{ 29 | { 30 | Name: "headless", 31 | TargetPort: 8080, 32 | Port: 80, 33 | }, 34 | }, 35 | }, 36 | } 37 | 38 | t.Run("with a set of ports", func(t *testing.T) { 39 | srv := &corev1.Service{} 40 | 41 | obj := buildServiceForRelease(srv, ms, np, release) 42 | if obj == nil { 43 | t.Error("Expected object to not be nil") 44 | } 45 | }) 46 | 47 | t.Run("Maintains existing labels", func(t *testing.T) { 48 | srv := &corev1.Service{} 49 | srv.Labels = map[string]string{ 50 | "dummy-label": "value", 51 | } 52 | 53 | obj := buildServiceForRelease(srv, ms, np, release) 54 | if obj.Labels["dummy-label"] != "value" { 55 | t.Error("Expected object to maintain existing label") 56 | } 57 | }) 58 | 59 | t.Run("Maintains existing annotations", func(t *testing.T) { 60 | srv := &corev1.Service{} 61 | srv.Annotations = map[string]string{ 62 | "dummy-annotation": "value", 63 | } 64 | 65 | obj := buildServiceForRelease(srv, ms, np, release) 66 | if obj.Annotations["dummy-annotation"] != "value" { 67 | t.Error("Expected object to maintain existing annotation") 68 | } 69 | }) 70 | 71 | t.Run("Sets client IP session affinity", func(t *testing.T) { 72 | srv := &corev1.Service{} 73 | 74 | np = np.DeepCopy() 75 | np.Spec.SessionAffinity = &corev1.SessionAffinityConfig{ 76 | ClientIP: &corev1.ClientIPConfig{}, 77 | } 78 | 79 | obj := buildServiceForRelease(srv, ms, np, release) 80 | if obj.Spec.SessionAffinity != corev1.ServiceAffinityClientIP { 81 | t.Error("Expected service to have client ip session affinity") 82 | } 83 | }) 84 | 85 | } 86 | -------------------------------------------------------------------------------- /internal/registry/hub/docker_hub.go: -------------------------------------------------------------------------------- 1 | // Package hub represents the registry implementation for Docker Hub. 2 | package hub 3 | 4 | import ( 5 | "encoding/json" 6 | "errors" 7 | "io" 8 | "net/http" 9 | "net/url" 10 | "strings" 11 | 12 | "github.com/docker/distribution/manifest/schema2" 13 | "github.com/heroku/docker-registry-client/registry" 14 | "github.com/opencontainers/go-digest" 15 | "k8s.io/api/core/v1" 16 | 17 | "github.com/manifoldco/heighliner/apis/v1alpha1" 18 | "github.com/manifoldco/heighliner/internal/imagepolicy" 19 | reg "github.com/manifoldco/heighliner/internal/registry" 20 | ) 21 | 22 | func init() { 23 | imagepolicy.AddRegistry("docker", func(secret *v1.Secret) (reg.Registry, error) { 24 | c, err := New(secret) 25 | if err != nil { 26 | return nil, err 27 | } 28 | 29 | return c, nil 30 | }) 31 | } 32 | 33 | const dockerHubRegistryURL string = "https://registry-1.docker.io" 34 | 35 | var errNoUsername = errors.New("username missing from configuration") 36 | var errNoPassword = errors.New("password missing from configuration") 37 | 38 | type regClient interface { 39 | Tags(string) ([]string, error) 40 | ManifestV2(string, string) (*schema2.DeserializedManifest, error) 41 | DownloadLayer(string, digest.Digest) (io.ReadCloser, error) 42 | } 43 | 44 | // Client is a docker registry client 45 | type Client struct { 46 | c regClient 47 | } 48 | 49 | // New creates a new registry client for Docker Hub. 50 | func New(secret *v1.Secret) (*Client, error) { 51 | // TODO(jelmer): we need to abstract this out. Docker Hub - hosted - has a 52 | // different interface than a local registry. We can do this detection based 53 | // on the hostname. 54 | // For now, we'll focus on docker hub. 55 | 56 | // get cfg from k8s secret 57 | u, p, err := configFromSecret(secret) 58 | if err != nil { 59 | return nil, err 60 | } 61 | 62 | url := strings.TrimSuffix(dockerHubRegistryURL, "/") 63 | transport := registry.WrapTransport(http.DefaultTransport, url, u, p) 64 | c := ®istry.Registry{ 65 | URL: url, 66 | Client: &http.Client{ 67 | Transport: transport, 68 | }, 69 | Logf: registry.Quiet, 70 | } 71 | 72 | return &Client{c: c}, nil 73 | } 74 | 75 | type auth struct { 76 | Username string `json:"username"` 77 | Password string `json:"password"` 78 | } 79 | 80 | func configFromSecret(secret *v1.Secret) (string, string, error) { 81 | var creds map[string]auth 82 | credsJSON := secret.Data[".dockercfg"] 83 | if err := json.Unmarshal(credsJSON, &creds); err != nil { 84 | return "", "", err 85 | } 86 | 87 | // .dockercfg has one key which is the url of the docker registry 88 | var stanza auth 89 | for _, s := range creds { 90 | stanza = s 91 | break 92 | } 93 | 94 | if stanza.Username == "" { 95 | return "", "", errNoUsername 96 | } 97 | 98 | if stanza.Password == "" { 99 | return "", "", errNoPassword 100 | } 101 | 102 | return stanza.Username, stanza.Password, nil 103 | } 104 | 105 | type containerConfig struct { 106 | Labels map[string]string 107 | } 108 | 109 | type config struct { 110 | ContainerConfig containerConfig `json:"container_config"` 111 | } 112 | 113 | // TagFor returns the tag name that matches the provided repo and release. 114 | // It returns a registry.TagNotFound error if no matching tag is found. 115 | func (c *Client) TagFor(repo string, release string, matcher *v1alpha1.ImagePolicyMatch) (string, error) { 116 | 117 | hasName, hasLabels := matcher.Config() 118 | 119 | ts := []string{} 120 | 121 | if hasName { 122 | n, err := matcher.MapName(release) 123 | if err != nil { 124 | return "", err 125 | } 126 | ts = append(ts, n) 127 | } else { 128 | var err error 129 | ts, err = c.c.Tags(repo) 130 | if err != nil { 131 | return "", normalizeErr(repo, release, err) 132 | } 133 | } 134 | 135 | for _, t := range ts { 136 | var labels map[string]string 137 | if hasLabels { 138 | m, err := c.c.ManifestV2(repo, t) 139 | if err != nil { 140 | return "", normalizeErr(repo, release, err) 141 | } 142 | 143 | l, err := c.c.DownloadLayer(repo, m.Config.Digest) 144 | if err != nil { 145 | return "", normalizeErr(repo, release, err) 146 | } 147 | 148 | defer l.Close() 149 | 150 | var c config 151 | d := json.NewDecoder(l) 152 | if err := d.Decode(&c); err != nil { 153 | return "", err 154 | } 155 | 156 | labels = c.ContainerConfig.Labels 157 | } 158 | 159 | matches, err := matcher.Matches(release, t, labels) 160 | if err != nil { 161 | return "", err 162 | } 163 | 164 | if matches { 165 | return t, nil 166 | } 167 | } 168 | 169 | return "", reg.NewTagNotFoundError(repo, release) 170 | } 171 | 172 | func normalizeErr(repo, release string, err error) error { 173 | if u, ok := err.(*url.Error); ok { 174 | if t, ok := u.Err.(*registry.HttpStatusError); ok { 175 | if t.Response.StatusCode == http.StatusNotFound { 176 | return reg.NewTagNotFoundError(repo, release) 177 | } 178 | } 179 | } 180 | 181 | return err 182 | } 183 | -------------------------------------------------------------------------------- /internal/registry/registry.go: -------------------------------------------------------------------------------- 1 | package registry 2 | 3 | import ( 4 | "fmt" 5 | "github.com/manifoldco/heighliner/apis/v1alpha1" 6 | ) 7 | 8 | // Registry represents the interface any registry needs to provide to query it. 9 | type Registry interface { 10 | TagFor(string, string, *v1alpha1.ImagePolicyMatch) (string, error) 11 | } 12 | 13 | type tagNotFoundError string 14 | 15 | func (t tagNotFoundError) Error() string { return string(t) } 16 | 17 | // NewTagNotFoundError returns an error that satisfies IsTagNotFoundError. 18 | func NewTagNotFoundError(repository, release string) error { 19 | return tagNotFoundError(fmt.Sprintf("no suitable tag was found in '%s' for '%s'", repository, release)) 20 | } 21 | 22 | // IsTagNotFoundError returns a bool indicating if the provided error is for 23 | // no matching tag being found. 24 | func IsTagNotFoundError(err error) bool { 25 | _, ok := err.(tagNotFoundError) 26 | return ok 27 | } 28 | -------------------------------------------------------------------------------- /internal/registry/registry_test.go: -------------------------------------------------------------------------------- 1 | package registry 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | ) 7 | 8 | func TestIsTagNotFoundError(t *testing.T) { 9 | t.Run("ok", func(t *testing.T) { 10 | err := NewTagNotFoundError("arigato/beans", "1.0.0") 11 | 12 | if !IsTagNotFoundError(err) { 13 | t.Error("Error not reported as tag not found") 14 | } 15 | }) 16 | 17 | t.Run("not ok", func(t *testing.T) { 18 | err := errors.New("fake") 19 | 20 | if IsTagNotFoundError(err) { 21 | t.Error("Error incorrectly reported as tag not found") 22 | } 23 | }) 24 | } 25 | -------------------------------------------------------------------------------- /internal/svc/svc.go: -------------------------------------------------------------------------------- 1 | // Package svc manages Microservices. 2 | package svc 3 | 4 | import ( 5 | "github.com/manifoldco/heighliner/apis/v1alpha1" 6 | 7 | "github.com/jelmersnoeck/kubekit" 8 | "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" 9 | ) 10 | 11 | const ( 12 | // CustomResourceName is the name we'll use for the Microservice CRD. 13 | CustomResourceName = "microservice" 14 | 15 | // CustomResourceNamePlural is the plural version of CustomResourceName. 16 | CustomResourceNamePlural = "microservices" 17 | ) 18 | 19 | var ( 20 | // CustomResource describes the CRD configuration for the Microservice CRD. 21 | CustomResource = kubekit.CustomResource{ 22 | Name: CustomResourceName, 23 | Plural: CustomResourceNamePlural, 24 | Group: v1alpha1.GroupName, 25 | Version: v1alpha1.Version, 26 | Scope: v1beta1.NamespaceScoped, 27 | Aliases: []string{"msvc"}, 28 | Object: &v1alpha1.Microservice{}, 29 | Validation: v1alpha1.MicroserviceValidationSchema, 30 | } 31 | 32 | // AvailabilityPolicyResource describes the CRD configuration for the 33 | // AvailabilityPolicy CRD. 34 | AvailabilityPolicyResource = kubekit.CustomResource{ 35 | Name: "availabilitypolicy", 36 | Plural: "availabilitypolicies", 37 | Group: v1alpha1.GroupName, 38 | Version: v1alpha1.Version, 39 | Scope: v1beta1.NamespaceScoped, 40 | Aliases: []string{"ap"}, 41 | Object: &v1alpha1.AvailabilityPolicy{}, 42 | Validation: &v1beta1.CustomResourceValidation{ 43 | OpenAPIV3Schema: &v1beta1.JSONSchemaProps{ 44 | Properties: map[string]v1beta1.JSONSchemaProps{ 45 | "spec": v1alpha1.AvailabilityPolicyValidationSchema, 46 | }, 47 | }, 48 | }, 49 | } 50 | 51 | // HealthPolicyResource describes the CRD configuration for the HealthPolicy CRD. 52 | HealthPolicyResource = kubekit.CustomResource{ 53 | Name: "healthpolicy", 54 | Plural: "healthpolicies", 55 | Group: v1alpha1.GroupName, 56 | Version: v1alpha1.Version, 57 | Scope: v1beta1.NamespaceScoped, 58 | Aliases: []string{"hp"}, 59 | Object: &v1alpha1.HealthPolicy{}, 60 | Validation: v1alpha1.HealthPolicyValidationSchema, 61 | } 62 | 63 | // SecurityPolicyResource describes the CRD configuration for the 64 | // SecurityPolicy CRD. 65 | SecurityPolicyResource = kubekit.CustomResource{ 66 | Name: "securitypolicy", 67 | Plural: "securitypolicies", 68 | Group: v1alpha1.GroupName, 69 | Version: v1alpha1.Version, 70 | Scope: v1beta1.NamespaceScoped, 71 | Aliases: []string{"sp"}, 72 | Object: &v1alpha1.SecurityPolicy{}, 73 | Validation: v1alpha1.SecurityPolicyValidationSchema, 74 | } 75 | ) 76 | -------------------------------------------------------------------------------- /internal/tester/kubekit.go: -------------------------------------------------------------------------------- 1 | package tester 2 | 3 | import ( 4 | "github.com/jelmersnoeck/kubekit/patcher" 5 | "k8s.io/apimachinery/pkg/runtime" 6 | ) 7 | 8 | // PatchClient is a dummy kubekit client which is used for testing purposes. 9 | type PatchClient struct { 10 | ApplyFunc func(obj runtime.Object, opts ...patcher.OptionFunc) ([]byte, error) 11 | GetFunc func(obj interface{}, namespace, name string) error 12 | DeleteFunc func(runtime.Object, ...patcher.OptionFunc) error 13 | } 14 | 15 | // Flush resets all the patcher functions so we can ensure it's not being used 16 | // improperly from other test cases. 17 | // One should use `defer PatchClient.Flush()` in every test case. 18 | func (c *PatchClient) Flush() { 19 | c.ApplyFunc = nil 20 | c.GetFunc = nil 21 | c.DeleteFunc = nil 22 | } 23 | 24 | // Apply mimics the Apply behaviour of the patch client by calling the 25 | // ApplyFunc. 26 | func (c *PatchClient) Apply(obj runtime.Object, opts ...patcher.OptionFunc) ([]byte, error) { 27 | return c.ApplyFunc(obj, opts...) 28 | } 29 | 30 | // Get mimics the Get behaviour of the get client by calling the GetFunc. 31 | func (c *PatchClient) Get(obj interface{}, namespace, name string) error { 32 | return c.GetFunc(obj, namespace, name) 33 | } 34 | 35 | // Delete mimics the Get behaviour of the clinet by calling DeleteFunc. 36 | func (c *PatchClient) Delete(obj runtime.Object, ops ...patcher.OptionFunc) error { 37 | return c.DeleteFunc(obj, ops...) 38 | } 39 | -------------------------------------------------------------------------------- /internal/vsvc/affinity.go: -------------------------------------------------------------------------------- 1 | package vsvc 2 | 3 | import ( 4 | corev1 "k8s.io/api/core/v1" 5 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 6 | ) 7 | 8 | // DefaultAffinity creates a set of affinity rules for a specific service. 9 | func DefaultAffinity(key, value string) *corev1.Affinity { 10 | return &corev1.Affinity{ 11 | PodAntiAffinity: &corev1.PodAntiAffinity{ 12 | PreferredDuringSchedulingIgnoredDuringExecution: []corev1.WeightedPodAffinityTerm{ 13 | { 14 | Weight: 100, 15 | PodAffinityTerm: corev1.PodAffinityTerm{ 16 | LabelSelector: &metav1.LabelSelector{ 17 | MatchExpressions: []metav1.LabelSelectorRequirement{ 18 | { 19 | Key: key, 20 | Operator: metav1.LabelSelectorOpIn, 21 | Values: []string{value}, 22 | }, 23 | }, 24 | }, 25 | TopologyKey: "kubernetes.io/hostname", 26 | }, 27 | }, 28 | { 29 | Weight: 90, 30 | PodAffinityTerm: corev1.PodAffinityTerm{ 31 | LabelSelector: &metav1.LabelSelector{ 32 | MatchExpressions: []metav1.LabelSelectorRequirement{ 33 | { 34 | Key: key, 35 | Operator: metav1.LabelSelectorOpIn, 36 | Values: []string{value}, 37 | }, 38 | }, 39 | }, 40 | TopologyKey: "failure-domain.beta.kubernetes.io/zone", 41 | }, 42 | }, 43 | }, 44 | }, 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /internal/vsvc/controller.go: -------------------------------------------------------------------------------- 1 | package vsvc 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "os" 7 | "os/signal" 8 | "syscall" 9 | 10 | "github.com/manifoldco/heighliner/apis/v1alpha1" 11 | "github.com/manifoldco/heighliner/internal/k8sutils" 12 | 13 | "github.com/jelmersnoeck/kubekit" 14 | "github.com/jelmersnoeck/kubekit/errors" 15 | "github.com/jelmersnoeck/kubekit/patcher" 16 | "k8s.io/apimachinery/pkg/runtime" 17 | "k8s.io/client-go/kubernetes" 18 | "k8s.io/client-go/rest" 19 | "k8s.io/client-go/tools/cache" 20 | cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" 21 | ) 22 | 23 | // Controller represents the VersionedMicroserviceController. This controller 24 | // takes care of creating, updating and deleting lower level Kubernetes 25 | // components that are associated with a specific VersionedMicroservice. 26 | type Controller struct { 27 | rc *rest.RESTClient 28 | cs kubernetes.Interface 29 | namespace string 30 | patcher *patcher.Patcher 31 | } 32 | 33 | // NewController returns a new VersionedMicroservice Controller. 34 | func NewController(cfg *rest.Config, cs kubernetes.Interface, namespace string) (*Controller, error) { 35 | rc, err := kubekit.RESTClient(cfg, &v1alpha1.SchemeGroupVersion, v1alpha1.AddToScheme) 36 | if err != nil { 37 | return nil, err 38 | } 39 | 40 | return &Controller{ 41 | cs: cs, 42 | rc: rc, 43 | namespace: namespace, 44 | patcher: patcher.New("hlnr-versioned-microservice", cmdutil.NewFactory(nil)), 45 | }, nil 46 | } 47 | 48 | // Run runs the Controller in the background and sets up watchers to take action 49 | // when the desired state is altered. 50 | func (c *Controller) Run() error { 51 | log.Printf("Starting controller...") 52 | ctx, cancel := context.WithCancel(context.Background()) 53 | 54 | go c.run(ctx) 55 | 56 | quit := make(chan os.Signal, 1) 57 | signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) 58 | 59 | <-quit 60 | log.Printf("Shutdown requested...") 61 | cancel() 62 | 63 | <-ctx.Done() 64 | log.Printf("Shutting down...") 65 | 66 | return nil 67 | } 68 | 69 | func (c *Controller) run(ctx context.Context) { 70 | watcher := kubekit.NewWatcher( 71 | c.rc, 72 | c.namespace, 73 | &CustomResource, 74 | cache.ResourceEventHandlerFuncs{ 75 | AddFunc: func(obj interface{}) { 76 | c.applyCRD(obj, patcher.DisableUpdate()) 77 | }, 78 | UpdateFunc: func(old, new interface{}) { 79 | c.applyCRD(new, patcher.DisableCreate()) 80 | }, 81 | DeleteFunc: func(obj interface{}) { 82 | svc := obj.(*v1alpha1.VersionedMicroservice).DeepCopy() 83 | log.Printf("Deleting VersionedMicroservice %s", svc.Name) 84 | }, 85 | }, 86 | ) 87 | 88 | go watcher.Run(ctx.Done()) 89 | } 90 | 91 | func (c *Controller) applyCRD(obj interface{}, opts ...patcher.OptionFunc) error { 92 | vsvc := obj.(*v1alpha1.VersionedMicroservice).DeepCopy() 93 | 94 | // This is managed by a kubekit controller, lets remove that annotation. 95 | delete(vsvc.Annotations, "kubekit-hlnr-microservice/last-applied-configuration") 96 | 97 | if err := updateObject("Deployment", vsvc, c.patcher, getDeployment); err != nil { 98 | return err 99 | } 100 | 101 | if err := updateObject("PodDisruptionBudget", vsvc, c.patcher, getPodDisruptionBudget, patcher.WithDeleteFirst()); err != nil && !errors.IsNoObjectGiven(err) { 102 | return err 103 | } 104 | 105 | return nil 106 | } 107 | 108 | type objectFunc func(*v1alpha1.VersionedMicroservice) (runtime.Object, error) 109 | 110 | func updateObject(name string, vsvc *v1alpha1.VersionedMicroservice, p *patcher.Patcher, f objectFunc, opts ...patcher.OptionFunc) error { 111 | obj, err := f(vsvc) 112 | if err != nil { 113 | log.Printf("Could not configure %s for %s: %s", name, vsvc.Name, err) 114 | return err 115 | } 116 | 117 | patch, err := p.Apply(obj, opts...) 118 | if err != nil && !errors.IsNoObjectGiven(err) { 119 | log.Printf("Could not apply %s for %s: %s", name, vsvc.Name, err) 120 | return err 121 | } 122 | 123 | patch, _ = k8sutils.CleanupPatchAnnotations(patch, "hlnr-versioned-microservice") 124 | patch, err = k8sutils.CleanupPatchAnnotations(patch, "hlnr-microservice") 125 | if err == nil && !patcher.IsEmptyPatch(patch) { 126 | log.Printf("Synced %s %s with new data: %s", name, vsvc.Name, string(patch)) 127 | } 128 | 129 | return err 130 | } 131 | -------------------------------------------------------------------------------- /internal/vsvc/deployment.go: -------------------------------------------------------------------------------- 1 | package vsvc 2 | 3 | import ( 4 | "github.com/manifoldco/heighliner/apis/v1alpha1" 5 | "github.com/manifoldco/heighliner/internal/k8sutils" 6 | "github.com/manifoldco/heighliner/internal/meta" 7 | 8 | "github.com/jelmersnoeck/kubekit" 9 | corev1 "k8s.io/api/core/v1" 10 | "k8s.io/api/extensions/v1beta1" 11 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 12 | "k8s.io/apimachinery/pkg/runtime" 13 | ) 14 | 15 | // getDeployment creates the Deployment Object for a VersionedMicroservice. 16 | func getDeployment(crd *v1alpha1.VersionedMicroservice) (runtime.Object, error) { 17 | availability := crd.Spec.Availability 18 | if availability == nil { 19 | availability = &v1alpha1.DefaultAvailabilityPolicySpec 20 | } 21 | 22 | labels := meta.Labels(crd.Labels, crd) 23 | annotations := meta.Annotations(crd.Annotations, v1alpha1.Version, crd) 24 | 25 | affinity := availability.Affinity 26 | if affinity == nil { 27 | affinity = DefaultAffinity("hlnr.io/service", crd.Name) 28 | } 29 | 30 | security := crd.Spec.Security 31 | if security == nil { 32 | security = &v1alpha1.SecurityPolicySpec{} 33 | } 34 | 35 | populateContainers(crd) 36 | 37 | dpl := &v1beta1.Deployment{ 38 | TypeMeta: metav1.TypeMeta{ 39 | Kind: "Deployment", 40 | APIVersion: "extensions/v1beta1", 41 | }, 42 | ObjectMeta: metav1.ObjectMeta{ 43 | Name: crd.Name, 44 | Namespace: crd.Namespace, 45 | Labels: labels, 46 | Annotations: annotations, 47 | OwnerReferences: []metav1.OwnerReference{ 48 | *metav1.NewControllerRef( 49 | crd, 50 | v1alpha1.SchemeGroupVersion.WithKind(kubekit.TypeName(crd)), 51 | ), 52 | }, 53 | }, 54 | Spec: v1beta1.DeploymentSpec{ 55 | Replicas: availability.Replicas, 56 | Strategy: availability.DeploymentStrategy, 57 | Selector: &metav1.LabelSelector{ 58 | MatchLabels: labels, 59 | }, 60 | Template: corev1.PodTemplateSpec{ 61 | ObjectMeta: metav1.ObjectMeta{ 62 | Name: crd.Name, 63 | Namespace: crd.Namespace, 64 | Labels: labels, 65 | Annotations: annotations, 66 | }, 67 | Spec: corev1.PodSpec{ 68 | ImagePullSecrets: crd.Spec.ImagePullSecrets, 69 | ServiceAccountName: security.ServiceAccountName, 70 | AutomountServiceAccountToken: k8sutils.PtrBool(security.AutomountServiceAccountToken), 71 | SecurityContext: security.SecurityContext, 72 | Affinity: affinity, 73 | RestartPolicy: availability.RestartPolicy, 74 | Containers: crd.Spec.Containers, 75 | Volumes: podVolumes(crd), 76 | }, 77 | }, 78 | }, 79 | } 80 | 81 | return dpl, nil 82 | } 83 | 84 | func populateContainers(crd *v1alpha1.VersionedMicroservice) { 85 | if crd.Spec.Config == nil { 86 | return 87 | } 88 | 89 | for i, container := range crd.Spec.Containers { 90 | container.VolumeMounts = crd.Spec.Config.VolumeMounts 91 | container.EnvFrom = crd.Spec.Config.EnvFrom 92 | container.Env = crd.Spec.Config.Env 93 | container.Args = crd.Spec.Config.Args 94 | container.Command = crd.Spec.Config.Command 95 | 96 | // reassign the container in the CRD 97 | crd.Spec.Containers[i] = container 98 | } 99 | 100 | if crd.Spec.Security == nil { 101 | crd.Spec.Security = &v1alpha1.SecurityPolicySpec{} 102 | } 103 | } 104 | 105 | func podVolumes(crd *v1alpha1.VersionedMicroservice) []corev1.Volume { 106 | if crd.Spec.Config == nil { 107 | return nil 108 | } 109 | 110 | return crd.Spec.Config.Volumes 111 | } 112 | -------------------------------------------------------------------------------- /internal/vsvc/disruption.go: -------------------------------------------------------------------------------- 1 | package vsvc 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/manifoldco/heighliner/apis/v1alpha1" 7 | "github.com/manifoldco/heighliner/internal/meta" 8 | 9 | "github.com/jelmersnoeck/kubekit" 10 | "k8s.io/api/policy/v1beta1" 11 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 12 | "k8s.io/apimachinery/pkg/runtime" 13 | "k8s.io/apimachinery/pkg/util/intstr" 14 | ) 15 | 16 | var ( 17 | // ErrMinMaxAvailabilitySet is used when the Availability Configuration has 18 | // both MinAvailable and MaxUnavailable set. 19 | ErrMinMaxAvailabilitySet = errors.New("Can't have both MinAvailable and MaxUnavailable configured") 20 | ) 21 | 22 | func getPodDisruptionBudget(crd *v1alpha1.VersionedMicroservice) (runtime.Object, error) { 23 | budget := defaultDisruptionBudget.DeepCopy() 24 | 25 | labels := meta.Labels(crd.Labels, crd) 26 | annotations := meta.Annotations(crd.Annotations, v1alpha1.Version, crd) 27 | 28 | budget.ObjectMeta = metav1.ObjectMeta{ 29 | Name: crd.Name, 30 | Namespace: crd.Namespace, 31 | Labels: labels, 32 | Annotations: annotations, 33 | OwnerReferences: []metav1.OwnerReference{ 34 | *metav1.NewControllerRef( 35 | crd, 36 | v1alpha1.SchemeGroupVersion.WithKind(kubekit.TypeName(crd)), 37 | ), 38 | }, 39 | } 40 | budget.Spec.Selector.MatchLabels[meta.LabelServiceKey] = crd.Name 41 | if crd.Spec.Availability != nil { 42 | av := crd.Spec.Availability 43 | 44 | if av.MinAvailable != nil && av.MaxUnavailable != nil { 45 | return nil, ErrMinMaxAvailabilitySet 46 | } 47 | 48 | if av.MinAvailable != nil && av.MaxUnavailable == nil { 49 | budget.Spec.MinAvailable = av.MinAvailable 50 | budget.Spec.MaxUnavailable = nil 51 | } 52 | if av.MaxUnavailable != nil && av.MinAvailable == nil { 53 | budget.Spec.MaxUnavailable = av.MaxUnavailable 54 | budget.Spec.MinAvailable = nil 55 | } 56 | } 57 | 58 | return budget, nil 59 | } 60 | 61 | var defaultDisruptionBudget = &v1beta1.PodDisruptionBudget{ 62 | TypeMeta: metav1.TypeMeta{ 63 | Kind: "PodDisruptionBudget", 64 | APIVersion: "policy/v1beta1", 65 | }, 66 | Spec: v1beta1.PodDisruptionBudgetSpec{ 67 | MinAvailable: ptrIntOrStringFromInt(1), 68 | Selector: &metav1.LabelSelector{ 69 | MatchLabels: map[string]string{}, 70 | }, 71 | }, 72 | Status: v1beta1.PodDisruptionBudgetStatus{ 73 | DisruptedPods: map[string]metav1.Time{}, 74 | }, 75 | } 76 | 77 | func ptrIntOrStringFromInt(i int) *intstr.IntOrString { 78 | return ptrIntOrString(intstr.FromInt(i)) 79 | } 80 | 81 | func ptrIntOrString(i intstr.IntOrString) *intstr.IntOrString { 82 | return &i 83 | } 84 | -------------------------------------------------------------------------------- /internal/vsvc/disruption_test.go: -------------------------------------------------------------------------------- 1 | package vsvc 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/manifoldco/heighliner/apis/v1alpha1" 7 | "github.com/manifoldco/heighliner/internal/meta" 8 | 9 | "github.com/stretchr/testify/assert" 10 | "k8s.io/api/policy/v1beta1" 11 | "k8s.io/apimachinery/pkg/util/intstr" 12 | ) 13 | 14 | func TestGetPodDisruptionBudget(t *testing.T) { 15 | resultFunc := func(t *testing.T, crd *v1alpha1.VersionedMicroservice, min, max *intstr.IntOrString) { 16 | obj, err := getPodDisruptionBudget(crd) 17 | pdb := obj.(*v1beta1.PodDisruptionBudget) 18 | assert.NoError(t, err) 19 | assert.Equal(t, min, pdb.Spec.MinAvailable) 20 | assert.Equal(t, max, pdb.Spec.MaxUnavailable) 21 | assert.Equal(t, crd.Name, pdb.Spec.Selector.MatchLabels[meta.LabelServiceKey]) 22 | } 23 | 24 | t.Run("without config", func(t *testing.T) { 25 | crd := &v1alpha1.VersionedMicroservice{} 26 | crd.Name = "test-app" 27 | 28 | resultFunc(t, crd, ptrIntOrStringFromInt(1), nil) 29 | }) 30 | 31 | t.Run("with minAvailable configured", func(t *testing.T) { 32 | crd := &v1alpha1.VersionedMicroservice{ 33 | Spec: v1alpha1.VersionedMicroserviceSpec{ 34 | Availability: &v1alpha1.AvailabilityPolicySpec{ 35 | MinAvailable: ptrIntOrStringFromInt(5), 36 | }, 37 | }, 38 | } 39 | crd.Name = "my-test" 40 | 41 | resultFunc(t, crd, ptrIntOrStringFromInt(5), nil) 42 | }) 43 | 44 | t.Run("with maxUnavailable configured", func(t *testing.T) { 45 | crd := &v1alpha1.VersionedMicroservice{ 46 | Spec: v1alpha1.VersionedMicroserviceSpec{ 47 | Availability: &v1alpha1.AvailabilityPolicySpec{ 48 | MaxUnavailable: ptrIntOrStringFromInt(2), 49 | }, 50 | }, 51 | } 52 | crd.Name = "unavailable-test" 53 | 54 | resultFunc(t, crd, nil, ptrIntOrStringFromInt(2)) 55 | }) 56 | 57 | t.Run("with both values configured", func(t *testing.T) { 58 | crd := &v1alpha1.VersionedMicroservice{ 59 | Spec: v1alpha1.VersionedMicroserviceSpec{ 60 | Availability: &v1alpha1.AvailabilityPolicySpec{ 61 | MaxUnavailable: ptrIntOrStringFromInt(2), 62 | MinAvailable: ptrIntOrStringFromInt(2), 63 | }, 64 | }, 65 | } 66 | crd.Name = "invalid" 67 | _, err := getPodDisruptionBudget(crd) 68 | assert.Equal(t, ErrMinMaxAvailabilitySet, err) 69 | }) 70 | } 71 | -------------------------------------------------------------------------------- /internal/vsvc/vsvc.go: -------------------------------------------------------------------------------- 1 | // Package vsvc manages Versioned Microservices. 2 | package vsvc 3 | 4 | import ( 5 | "github.com/manifoldco/heighliner/apis/v1alpha1" 6 | 7 | "github.com/jelmersnoeck/kubekit" 8 | "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" 9 | ) 10 | 11 | const ( 12 | // CustomResourceName is the name we'll use for the Versioned Microservice 13 | // CRD. 14 | CustomResourceName = "versionedmicroservice" 15 | 16 | // CustomResourceNamePlural is the plural version of CustomResourceName. 17 | CustomResourceNamePlural = "versionedmicroservices" 18 | ) 19 | 20 | var ( 21 | // CustomResource describes the CRD configuration for the VersionedMicroservice CRD. 22 | CustomResource = kubekit.CustomResource{ 23 | Name: CustomResourceName, 24 | Plural: CustomResourceNamePlural, 25 | Group: v1alpha1.GroupName, 26 | Version: v1alpha1.Version, 27 | Scope: v1beta1.NamespaceScoped, 28 | Aliases: []string{"vsvc"}, 29 | Object: &v1alpha1.VersionedMicroservice{}, 30 | Validation: &v1beta1.CustomResourceValidation{ 31 | OpenAPIV3Schema: &v1beta1.JSONSchemaProps{ 32 | Properties: map[string]v1beta1.JSONSchemaProps{ 33 | "spec": v1alpha1.VersionedMicroserviceValidationSchema, 34 | }, 35 | }, 36 | }, 37 | } 38 | ) 39 | -------------------------------------------------------------------------------- /internal/vsvc/vsvc_test.go: -------------------------------------------------------------------------------- 1 | package vsvc_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/manifoldco/heighliner/apis/v1alpha1" 7 | "github.com/manifoldco/heighliner/internal/vsvc" 8 | 9 | "github.com/jelmersnoeck/kubekit/kubetest" 10 | corev1 "k8s.io/api/core/v1" 11 | "k8s.io/apiextensions-apiserver/pkg/apiserver/validation" 12 | ) 13 | 14 | func TestCRD_VersionedMicroservice_Containers(t *testing.T) { 15 | validator, err := kubetest.GetValidator(vsvc.CustomResource) 16 | if err != nil { 17 | t.Fatalf("Couldn't get validator: %s", err) 18 | } 19 | 20 | t.Run("without any containers specified", func(t *testing.T) { 21 | crd := &v1alpha1.VersionedMicroservice{ 22 | Spec: v1alpha1.VersionedMicroserviceSpec{ 23 | Containers: []corev1.Container{}, 24 | }, 25 | } 26 | if err := validation.ValidateCustomResource(crd, validator); err == nil { 27 | t.Errorf("Expected error, got none") 28 | } 29 | }) 30 | 31 | t.Run("with a single container specified", func(t *testing.T) { 32 | // TODO(jelmer): figure out a way to pull in the k8s validation for 33 | // these objects and make sure they're validated as well. 34 | crd := &v1alpha1.VersionedMicroservice{ 35 | Spec: v1alpha1.VersionedMicroserviceSpec{ 36 | Containers: []corev1.Container{ 37 | {}, 38 | }, 39 | }, 40 | } 41 | if err := validation.ValidateCustomResource(crd, validator); err != nil { 42 | t.Errorf("Expected no error, got '%s'", err) 43 | } 44 | }) 45 | } 46 | --------------------------------------------------------------------------------