├── .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 | [](https://travis-ci.com/manifoldco/heighliner)
4 | [](https://codecov.io/gh/manifoldco/heighliner)
5 | [](https://goreportcard.com/report/github.com/manifoldco/heighliner)
6 | [](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 | 
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 | 
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 | 
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 |
--------------------------------------------------------------------------------