├── .dockerignore ├── .github ├── actions │ ├── build │ │ └── action.yml │ ├── integration-test │ │ └── action.yml │ └── unit-test │ │ └── action.yml ├── dependabot.yml └── workflows │ ├── codeql.yml │ ├── master.yml │ └── pr.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── Makefile ├── PROJECT ├── README.md ├── api └── v1alpha1 │ ├── groupversion_info.go │ ├── valhalla_types.go │ └── zz_generated.deepcopy.go ├── config ├── crd │ ├── bases │ │ └── valhalla.itayankri_valhallas.yaml │ ├── kustomization.yaml │ ├── kustomizeconfig.yaml │ └── patches │ │ ├── cainjection_in_valhallas.yaml │ │ └── webhook_in_valhallas.yaml ├── default │ ├── kustomization.yaml │ ├── manager_auth_proxy_patch.yaml │ └── manager_config_patch.yaml ├── manager │ ├── controller_manager_config.yaml │ ├── kustomization.yaml │ └── manager.yaml ├── manifests │ └── kustomization.yaml ├── prometheus │ ├── kustomization.yaml │ └── monitor.yaml ├── rbac │ ├── auth_proxy_client_clusterrole.yaml │ ├── auth_proxy_role.yaml │ ├── auth_proxy_role_binding.yaml │ ├── auth_proxy_service.yaml │ ├── kustomization.yaml │ ├── leader_election_role.yaml │ ├── leader_election_role_binding.yaml │ ├── role.yaml │ ├── role_binding.yaml │ ├── service_account.yaml │ ├── valhalla_editor_role.yaml │ └── valhalla_viewer_role.yaml ├── samples │ ├── kustomization.yaml │ └── valhalla_v1alpha1_valhalla.yaml └── scorecard │ ├── bases │ └── config.yaml │ ├── kustomization.yaml │ └── patches │ ├── basic.config.yaml │ └── olm.config.yaml ├── controllers ├── suite_test.go ├── valhalla_controller.go └── valhalla_controller_test.go ├── docker ├── builder │ ├── Dockerfile │ └── build.sh ├── predicted-traffic-fetcher │ ├── Dockerfile │ └── fetch.sh └── worker │ ├── Dockerfile │ └── run.sh ├── examples ├── Readme.md ├── example.yaml └── kind-config.yaml ├── go.mod ├── go.sum ├── hack └── boilerplate.go.txt ├── internal ├── metadata │ ├── annotations.go │ ├── annotations_test.go │ └── suite_test.go ├── resource │ ├── constants.go │ ├── cron_job.go │ ├── deployment.go │ ├── deployment_test.go │ ├── horizontal_pod_autoscaler.go │ ├── horizontal_pod_autoscaler_test.go │ ├── job.go │ ├── job_test.go │ ├── persistent_volume_claim.go │ ├── persistent_volume_claim_test.go │ ├── pod_disruption_budget.go │ ├── pod_disruption_budget_test.go │ ├── resource_builder.go │ ├── service.go │ ├── service_test.go │ └── suite_test.go └── status │ ├── status.go │ ├── status_test.go │ └── suite_test.go └── main.go /.dockerignore: -------------------------------------------------------------------------------- 1 | # More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file 2 | # Ignore build and test binaries. 3 | bin/ 4 | testbin/ 5 | -------------------------------------------------------------------------------- /.github/actions/build/action.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | inputs: 4 | dockerhub-username: 5 | description: "DockerHub username" 6 | required: true 7 | dockerhub-token: 8 | description: "DockerHub token" 9 | required: true 10 | 11 | runs: 12 | using: "composite" 13 | steps: 14 | - name: Install Go 15 | uses: actions/setup-go@v2 16 | with: 17 | go-version: ${{ env.GO_VERSION }} 18 | - name: Setup Golang caches 19 | uses: actions/cache@v3 20 | with: 21 | path: | 22 | ~/.cache/go-build 23 | ~/go/pkg/mod 24 | key: ${{ runner.os }}-golang-${{ hashFiles('**/go.sum') }} 25 | restore-keys: | 26 | ${{ runner.os }}-golang- 27 | - name: Check out code into the Go module directory 28 | uses: actions/checkout@v2 29 | with: 30 | ref: ${{ github.event.pull_request.head.sha }} 31 | - name: Login to Docker Hub 32 | uses: docker/login-action@v2 33 | with: 34 | username: ${{ inputs.dockerhub-username }} 35 | password: ${{ inputs.dockerhub-token }} 36 | - name: Build image 37 | shell: bash 38 | run: make docker-build docker-push -------------------------------------------------------------------------------- /.github/actions/integration-test/action.yml: -------------------------------------------------------------------------------- 1 | name: Integration test 2 | 3 | runs: 4 | using: "composite" 5 | steps: 6 | - name: Install Go 7 | uses: actions/setup-go@v2 8 | with: 9 | go-version: ${{ env.GO_VERSION }} 10 | - name: Setup Golang caches 11 | uses: actions/cache@v3 12 | with: 13 | path: | 14 | ~/.cache/go-build 15 | ~/go/pkg/mod 16 | key: ${{ runner.os }}-golang-${{ hashFiles('**/go.sum') }} 17 | restore-keys: | 18 | ${{ runner.os }}-golang- 19 | - name: Check out code into the Go module directory 20 | uses: actions/checkout@v2 21 | with: 22 | ref: ${{ github.event.pull_request.head.sha }} 23 | - uses: azure/setup-helm@v3 24 | with: 25 | version: 3.7.1 26 | - name: Install Kind 27 | uses: helm/kind-action@v1.3.0 28 | with: 29 | install_only: true 30 | - name: Run tests 31 | shell: bash 32 | run: | 33 | export GOPATH=$HOME/go 34 | export PATH=$PATH:$GOPATH/bin 35 | cat /etc/docker/daemon.json | jq '. += {"storage-driver":"vfs"}' > daemon.json 36 | sudo rm /etc/docker/daemon.json 37 | sudo mv daemon.json /etc/docker/daemon.json 38 | sudo systemctl restart docker.service 39 | kind create cluster --name valhalla --config examples/kind-config.yaml 40 | kubectl create -f https://raw.githubusercontent.com/kubernetes-csi/csi-driver-nfs/master/deploy/example/nfs-provisioner/nfs-server.yaml 41 | helm repo add csi-driver-nfs https://raw.githubusercontent.com/kubernetes-csi/csi-driver-nfs/master/charts 42 | helm install csi-driver-nfs csi-driver-nfs/csi-driver-nfs --namespace kube-system --version v3.1.0 43 | kubectl create -f https://raw.githubusercontent.com/kubernetes-csi/csi-driver-nfs/master/deploy/example/storageclass-nfs.yaml 44 | kubectl wait --for condition=Available deployment/nfs-server --timeout=60s 45 | sleep 5 46 | kubectl logs $(kubectl get pods | grep nfs | awk '{print $1}') 47 | make install deploy 48 | kubectl -n valhalla-system wait --for condition=Available deployment/valhalla-controller-manager --timeout=60s 49 | make integration-test -------------------------------------------------------------------------------- /.github/actions/unit-test/action.yml: -------------------------------------------------------------------------------- 1 | name: Unit test 2 | 3 | runs: 4 | using: "composite" 5 | steps: 6 | - name: Install Go 7 | uses: actions/setup-go@v2 8 | with: 9 | go-version: ${{ env.GO_VERSION }} 10 | - name: Setup Golang caches 11 | uses: actions/cache@v3 12 | with: 13 | path: | 14 | ~/.cache/go-build 15 | ~/go/pkg/mod 16 | key: ${{ runner.os }}-golang-${{ hashFiles('**/go.sum') }} 17 | restore-keys: | 18 | ${{ runner.os }}-golang- 19 | - name: Check out code into the Go module directory 20 | uses: actions/checkout@v2 21 | - name: Run tests 22 | shell: bash 23 | run: make unit-test -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gomod 4 | directory: / 5 | schedule: 6 | interval: daily -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | 18 | jobs: 19 | analyze: 20 | name: Analyze 21 | runs-on: ubuntu-latest 22 | 23 | strategy: 24 | fail-fast: false 25 | matrix: 26 | language: [ 'go' ] 27 | 28 | steps: 29 | - name: Checkout repository 30 | uses: actions/checkout@v2 31 | 32 | # Initializes the CodeQL tools for scanning. 33 | - name: Initialize CodeQL 34 | uses: github/codeql-action/init@v2 35 | with: 36 | languages: ${{ matrix.language }} 37 | 38 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 39 | # If this step fails, then you should remove it and run the build manually (see below) 40 | - name: Autobuild 41 | uses: github/codeql-action/autobuild@v2 42 | 43 | - name: Perform CodeQL Analysis 44 | uses: github/codeql-action/analyze@v2 45 | -------------------------------------------------------------------------------- /.github/workflows/master.yml: -------------------------------------------------------------------------------- 1 | name: Master 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | 7 | env: 8 | GO_VERSION: 1.19.2 9 | K8S_VERSION: v1.24.1 10 | ENV: production 11 | 12 | jobs: 13 | unit-tests: 14 | name: Unit tests 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Run test 19 | uses: ./.github/actions/unit-test 20 | 21 | build: 22 | name: Build 23 | needs: ["unit-tests"] 24 | runs-on: ubuntu-latest 25 | steps: 26 | - uses: actions/checkout@v2 27 | - name: Build image 28 | uses: ./.github/actions/build 29 | with: 30 | dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }} 31 | dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }} 32 | 33 | integration-tests: 34 | needs: ["build"] 35 | name: Integration tests 36 | timeout-minutes: 15 37 | runs-on: ubuntu-latest 38 | steps: 39 | - uses: actions/checkout@v2 40 | - name: Run tests 41 | uses: ./.github/actions/integration-test 42 | -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | name: PR 2 | 3 | on: 4 | pull_request: 5 | branches: [ master ] 6 | 7 | env: 8 | GO_VERSION: 1.19.2 9 | K8S_VERSION: v1.24.1 10 | ENV: test 11 | 12 | jobs: 13 | unit-tests: 14 | name: Unit tests 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Run test 19 | uses: ./.github/actions/unit-test 20 | 21 | build: 22 | name: Build 23 | needs: ["unit-tests"] 24 | runs-on: ubuntu-latest 25 | steps: 26 | - uses: actions/checkout@v2 27 | - name: Build image 28 | uses: ./.github/actions/build 29 | with: 30 | dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }} 31 | dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }} 32 | 33 | integration-tests: 34 | needs: ["build"] 35 | name: Integration tests 36 | timeout-minutes: 15 37 | runs-on: ubuntu-latest 38 | steps: 39 | - uses: actions/checkout@v2 40 | - name: Run tests 41 | uses: ./.github/actions/integration-test 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | bin/ 15 | 16 | # Dependency directories (remove the comment below to include it) 17 | # vendor/ 18 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build the manager binary 2 | FROM golang:1.19 as builder 3 | 4 | WORKDIR /workspace 5 | # Copy the Go Modules manifests 6 | COPY go.mod go.mod 7 | COPY go.sum go.sum 8 | # cache deps before building and copying source so that we don't need to re-download as much 9 | # and so that source changes don't invalidate our downloaded layer 10 | RUN go mod download 11 | 12 | # Copy the go source 13 | COPY main.go main.go 14 | COPY api/ api/ 15 | COPY controllers/ controllers/ 16 | COPY internal/ internal/ 17 | 18 | # Build 19 | RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -o manager main.go 20 | 21 | # Use distroless as minimal base image to package the manager binary 22 | # Refer to https://github.com/GoogleContainerTools/distroless for more details 23 | FROM gcr.io/distroless/static:nonroot 24 | WORKDIR / 25 | COPY --from=builder /workspace/manager . 26 | USER 65532:65532 27 | 28 | ENTRYPOINT ["/manager"] 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Itay Ankri 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # VERSION defines the project version for the bundle. 2 | # Update this value when you upgrade the version of your project. 3 | # To re-generate a bundle for another specific version without changing the standard setup, you can: 4 | # - use the VERSION as arg of the bundle target (e.g make bundle VERSION=0.0.2) 5 | # - use environment variables to overwrite this value (e.g export VERSION=0.0.2) 6 | VERSION ?= 0.0.1 7 | 8 | # CHANNELS define the bundle channels used in the bundle. 9 | # Add a new line here if you would like to change its default config. (E.g CHANNELS = "candidate,fast,stable") 10 | # To re-generate a bundle for other specific channels without changing the standard setup, you can: 11 | # - use the CHANNELS as arg of the bundle target (e.g make bundle CHANNELS=candidate,fast,stable) 12 | # - use environment variables to overwrite this value (e.g export CHANNELS="candidate,fast,stable") 13 | ifneq ($(origin CHANNELS), undefined) 14 | BUNDLE_CHANNELS := --channels=$(CHANNELS) 15 | endif 16 | 17 | # DEFAULT_CHANNEL defines the default channel used in the bundle. 18 | # Add a new line here if you would like to change its default config. (E.g DEFAULT_CHANNEL = "stable") 19 | # To re-generate a bundle for any other default channel without changing the default setup, you can: 20 | # - use the DEFAULT_CHANNEL as arg of the bundle target (e.g make bundle DEFAULT_CHANNEL=stable) 21 | # - use environment variables to overwrite this value (e.g export DEFAULT_CHANNEL="stable") 22 | ifneq ($(origin DEFAULT_CHANNEL), undefined) 23 | BUNDLE_DEFAULT_CHANNEL := --default-channel=$(DEFAULT_CHANNEL) 24 | endif 25 | BUNDLE_METADATA_OPTS ?= $(BUNDLE_CHANNELS) $(BUNDLE_DEFAULT_CHANNEL) 26 | 27 | # IMAGE_TAG_BASE defines the docker.io namespace and part of the image name for remote images. 28 | # This variable is used to construct full image tags for bundle and catalog images. 29 | # 30 | # For example, running 'make bundle-build bundle-push catalog-build catalog-push' will build and push both 31 | # itayankri/valhalla-bundle:$VERSION and itayankri/valhalla-catalog:$VERSION. 32 | IMAGE_TAG_BASE ?= itayankri/valhalla-operator 33 | 34 | # BUNDLE_IMG defines the image:tag used for the bundle. 35 | # You can use it as an arg. (E.g make bundle-build BUNDLE_IMG=/:) 36 | BUNDLE_IMG ?= $(IMAGE_TAG_BASE)-bundle:v$(VERSION) 37 | 38 | # In this image the tag is the branch name. 39 | TEST_IMG ?= $(IMAGE_TAG_BASE):$(shell git rev-parse --short HEAD) 40 | 41 | # Image URL to use all building/pushing image targets 42 | ifeq ($(ENV), production) 43 | IMG ?= itayankri/valhalla-operator:latest 44 | else 45 | IMG ?= $(TEST_IMG) 46 | endif 47 | 48 | # ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary. 49 | ENVTEST_K8S_VERSION = 1.22 50 | 51 | # Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) 52 | ifeq (,$(shell go env GOBIN)) 53 | GOBIN=$(shell go env GOPATH)/bin 54 | else 55 | GOBIN=$(shell go env GOBIN) 56 | endif 57 | 58 | # Setting SHELL to bash allows bash commands to be executed by recipes. 59 | # This is a requirement for 'setup-envtest.sh' in the test target. 60 | # Options are set to exit when a recipe line exits non-zero or a piped command fails. 61 | SHELL = /usr/bin/env bash -o pipefail 62 | .SHELLFLAGS = -ec 63 | 64 | .PHONY: all 65 | all: build 66 | 67 | ##@ General 68 | 69 | # The help target prints out all targets with their descriptions organized 70 | # beneath their categories. The categories are represented by '##@' and the 71 | # target descriptions by '##'. The awk commands is responsible for reading the 72 | # entire set of makefiles included in this invocation, looking for lines of the 73 | # file as xyz: ## something, and then pretty-format the target and help. Then, 74 | # if there's a line with ##@ something, that gets pretty-printed as a category. 75 | # More info on the usage of ANSI control characters for terminal formatting: 76 | # https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters 77 | # More info on the awk command: 78 | # http://linuxcommand.org/lc3_adv_awk.php 79 | 80 | .PHONY: help 81 | help: ## Display this help. 82 | @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) 83 | 84 | ##@ Development 85 | 86 | .PHONY: manifests 87 | manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. 88 | $(CONTROLLER_GEN) rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases 89 | 90 | .PHONY: generate 91 | generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. 92 | $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..." 93 | 94 | .PHONY: fmt 95 | fmt: ## Run go fmt against code. 96 | go fmt ./... 97 | 98 | .PHONY: vet 99 | vet: ## Run go vet against code. 100 | go vet ./... 101 | 102 | .PHONY: unit-test 103 | unit-test: manifests generate fmt vet envtest ## Run tests. 104 | KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) -p path)" go test ./api/... ./internal/... 105 | 106 | .PHONY: integration-test 107 | integration-test: manifests generate fmt vet envtest ## Run tests. 108 | KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) -p path)" USE_EXISTING_CLUSTER=true go test -timeout 900s ./controllers/... 109 | ##@ Build 110 | 111 | .PHONY: build 112 | build: generate fmt vet ## Build manager binary. 113 | go build -o bin/manager main.go 114 | 115 | .PHONY: run 116 | run: manifests generate fmt vet ## Run a controller from your host. 117 | go run ./main.go 118 | 119 | .PHONY: docker-build 120 | docker-build: ## Build docker image with the manager. 121 | docker build -t ${IMG} . 122 | 123 | .PHONY: docker-push 124 | docker-push: ## Push docker image with the manager. 125 | docker push ${IMG} 126 | 127 | ##@ Deployment 128 | 129 | ifndef ignore-not-found 130 | ignore-not-found = false 131 | endif 132 | 133 | .PHONY: install 134 | install: manifests kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config. 135 | $(KUSTOMIZE) build config/crd | kubectl apply -f - 136 | 137 | .PHONY: uninstall 138 | uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. 139 | $(KUSTOMIZE) build config/crd | kubectl delete --ignore-not-found=$(ignore-not-found) -f - 140 | 141 | .PHONY: deploy 142 | deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config. 143 | cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} 144 | $(KUSTOMIZE) build config/default | kubectl apply -f - 145 | 146 | .PHONY: undeploy 147 | undeploy: ## Undeploy controller from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. 148 | $(KUSTOMIZE) build config/default | kubectl delete --ignore-not-found=$(ignore-not-found) -f - 149 | 150 | CONTROLLER_GEN = $(shell pwd)/bin/controller-gen 151 | .PHONY: controller-gen 152 | controller-gen: ## Download controller-gen locally if necessary. 153 | $(call go-get-tool,$(CONTROLLER_GEN),sigs.k8s.io/controller-tools/cmd/controller-gen@v0.10.0) 154 | 155 | KUSTOMIZE = $(shell pwd)/bin/kustomize 156 | .PHONY: kustomize 157 | kustomize: ## Download kustomize locally if necessary. 158 | $(call go-get-tool,$(KUSTOMIZE),sigs.k8s.io/kustomize/kustomize/v4@v4.5.7) 159 | 160 | ENVTEST = $(shell pwd)/bin/setup-envtest 161 | .PHONY: envtest 162 | envtest: ## Download envtest-setup locally if necessary. 163 | $(call go-get-tool,$(ENVTEST),sigs.k8s.io/controller-runtime/tools/setup-envtest@latest) 164 | 165 | # go-get-tool will 'go get' any package $2 and install it to $1. 166 | PROJECT_DIR := $(shell dirname $(abspath $(lastword $(MAKEFILE_LIST)))) 167 | define go-get-tool 168 | @[ -f $(1) ] || { \ 169 | set -e ;\ 170 | TMP_DIR=$$(mktemp -d) ;\ 171 | cd $$TMP_DIR ;\ 172 | go mod init tmp ;\ 173 | echo "Downloading $(2)" ;\ 174 | GOBIN=$(PROJECT_DIR)/bin go install $(2) ;\ 175 | rm -rf $$TMP_DIR ;\ 176 | } 177 | endef 178 | 179 | .PHONY: bundle 180 | bundle: manifests kustomize ## Generate bundle manifests and metadata, then validate generated files. 181 | operator-sdk generate kustomize manifests -q 182 | cd config/manager && $(KUSTOMIZE) edit set image controller=$(IMG) 183 | $(KUSTOMIZE) build config/manifests | operator-sdk generate bundle -q --overwrite --version $(VERSION) $(BUNDLE_METADATA_OPTS) 184 | operator-sdk bundle validate ./bundle 185 | 186 | .PHONY: bundle-build 187 | bundle-build: ## Build the bundle image. 188 | docker build -f bundle.Dockerfile -t $(BUNDLE_IMG) . 189 | 190 | .PHONY: bundle-push 191 | bundle-push: ## Push the bundle image. 192 | $(MAKE) docker-push IMG=$(BUNDLE_IMG) 193 | 194 | .PHONY: opm 195 | OPM = ./bin/opm 196 | opm: ## Download opm locally if necessary. 197 | ifeq (,$(wildcard $(OPM))) 198 | ifeq (,$(shell which opm 2>/dev/null)) 199 | @{ \ 200 | set -e ;\ 201 | mkdir -p $(dir $(OPM)) ;\ 202 | OS=$(shell go env GOOS) && ARCH=$(shell go env GOARCH) && \ 203 | curl -sSLo $(OPM) https://github.com/operator-framework/operator-registry/releases/download/v1.19.1/$${OS}-$${ARCH}-opm ;\ 204 | chmod +x $(OPM) ;\ 205 | } 206 | else 207 | OPM = $(shell which opm) 208 | endif 209 | endif 210 | 211 | # A comma-separated list of bundle images (e.g. make catalog-build BUNDLE_IMGS=example.com/operator-bundle:v0.1.0,example.com/operator-bundle:v0.2.0). 212 | # These images MUST exist in a registry and be pull-able. 213 | BUNDLE_IMGS ?= $(BUNDLE_IMG) 214 | 215 | # The image tag given to the resulting catalog image (e.g. make catalog-build CATALOG_IMG=example.com/operator-catalog:v0.2.0). 216 | CATALOG_IMG ?= $(IMAGE_TAG_BASE)-catalog:v$(VERSION) 217 | 218 | # Set CATALOG_BASE_IMG to an existing catalog image tag to add $BUNDLE_IMGS to that image. 219 | ifneq ($(origin CATALOG_BASE_IMG), undefined) 220 | FROM_INDEX_OPT := --from-index $(CATALOG_BASE_IMG) 221 | endif 222 | 223 | # Build a catalog image by adding bundle images to an empty catalog using the operator package manager tool, 'opm'. 224 | # This recipe invokes 'opm' in 'semver' bundle add mode. For more information on add modes, see: 225 | # https://github.com/operator-framework/community-operators/blob/7f1438c/docs/packaging-operator.md#updating-your-existing-operator 226 | .PHONY: catalog-build 227 | catalog-build: opm ## Build a catalog image. 228 | $(OPM) index add --container-tool docker --mode semver --tag $(CATALOG_IMG) --bundles $(BUNDLE_IMGS) $(FROM_INDEX_OPT) 229 | 230 | # Push the catalog image. 231 | .PHONY: catalog-push 232 | catalog-push: ## Push a catalog image. 233 | $(MAKE) docker-push IMG=$(CATALOG_IMG) 234 | -------------------------------------------------------------------------------- /PROJECT: -------------------------------------------------------------------------------- 1 | domain: itayankri 2 | layout: 3 | - go.kubebuilder.io/v3 4 | plugins: 5 | manifests.sdk.operatorframework.io/v2: {} 6 | scorecard.sdk.operatorframework.io/v2: {} 7 | projectName: valhalla-operator 8 | repo: github.com/itayankri/valhalla-operator 9 | resources: 10 | - api: 11 | crdVersion: v1 12 | namespaced: true 13 | controller: true 14 | domain: itayankri 15 | group: valhalla 16 | kind: Valhalla 17 | path: github.com/itayankri/valhalla-operator/api/v1alpha1 18 | version: v1alpha1 19 | version: "3" 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Valhalla Kubernetes Operator 2 | A kubernetes operator to deploy and manage [Valhalla](https://valhalla.readthedocs.io/en/latest/valhalla-intro/) routing engine. This operator efficiently deploys Valhalla instances by sharing map data accross all pods of a specific instance. 3 | 4 | ## Quickstart 5 | First, make sure you have a running Kubernetes cluster and kubectl installed to access it. Then run the following command to install the operator: 6 | ``` 7 | kubectl apply -f https://github.com/itayankri/valhalla-operator/releases/latest/download/valhalla-operator.yaml 8 | ``` 9 | 10 | Then you can deploy a Valhalla instance: 11 | ``` 12 | kubectl apply -f https://github.com/itayankri/valhalla-operator/blob/master/examples/example.yaml 13 | ``` 14 | For a full setup from scratch checkout this [Medium](https://medium.com/@itay.ankri/deploying-valhalla-routing-engine-on-kubernetes-using-valhalla-operator-2426e79ac746). 15 | 16 | ## Pausing the Operator 17 | The reconciliation can be paused by adding the following annotation to the Valhalla resource: 18 | ```bash 19 | valhalla.itayankri/operator.paused: "true" 20 | ``` 21 | The operator will not react to any changes to the Valhalla resource or any of the watched resources. If a paused Valhalla resource is deleted, the dependent resources will still be cleaned up because thay all have an ownerReference. 22 | -------------------------------------------------------------------------------- /api/v1alpha1/groupversion_info.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package v1alpha1 contains API Schema definitions for the valhalla v1alpha1 API group 18 | // +kubebuilder:object:generate=true 19 | // +groupName=valhalla.itayankri 20 | package v1alpha1 21 | 22 | import ( 23 | "k8s.io/apimachinery/pkg/runtime/schema" 24 | "sigs.k8s.io/controller-runtime/pkg/scheme" 25 | ) 26 | 27 | var ( 28 | // GroupVersion is group version used to register these objects 29 | GroupVersion = schema.GroupVersion{Group: "valhalla.itayankri", Version: "v1alpha1"} 30 | 31 | // SchemeBuilder is used to add go types to the GroupVersionKind scheme 32 | SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} 33 | 34 | // AddToScheme adds the types in this group-version to the given scheme. 35 | AddToScheme = SchemeBuilder.AddToScheme 36 | ) 37 | -------------------------------------------------------------------------------- /api/v1alpha1/valhalla_types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package v1alpha1 18 | 19 | import ( 20 | "strings" 21 | 22 | "github.com/itayankri/valhalla-operator/internal/status" 23 | corev1 "k8s.io/api/core/v1" 24 | "k8s.io/apimachinery/pkg/api/resource" 25 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 | "k8s.io/apimachinery/pkg/runtime" 27 | "k8s.io/apimachinery/pkg/util/intstr" 28 | ) 29 | 30 | const OperatorPausedAnnotation = "valhalla.itayankri/operator.paused" 31 | 32 | // Phase is the current phase of the deployment 33 | type Phase string 34 | 35 | const ( 36 | // PhaseBuildingMap signals that the map building phase is in progress 37 | PhaseBuildingMap Phase = "BuildingMap" 38 | 39 | // PhaseDeployingWorkers signals that the workers are being deployed 40 | PhaseDeployingWorkers Phase = "DeployingWorkers" 41 | 42 | // PhaseWorkersDeployed signals that the resources are successfully deployed 43 | PhaseWorkersDeployed Phase = "WorkersDeployed" 44 | 45 | // PhaseDeleting signals that the resources are being removed 46 | PhaseDeleting Phase = "Deleting" 47 | 48 | // PhaseDeleted signals that the resources are deleted 49 | PhaseDeleted Phase = "Deleted" 50 | 51 | // PhaseError signals that the deployment is in an error state 52 | PhaseError Phase = "Error" 53 | 54 | // PhaseEmpty is an uninitialized phase 55 | PhaseEmpty Phase = "" 56 | ) 57 | 58 | // EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! 59 | // NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. 60 | 61 | // ValhallaSpec defines the desired state of Valhalla 62 | type ValhallaSpec struct { 63 | // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster 64 | // Important: Run "make" to regenerate code after modifying this file 65 | PBFURL string `json:"pbfUrl,omitempty"` 66 | Image *string `json:"image,omitempty"` 67 | Persistence PersistenceSpec `json:"persistence,omitempty"` 68 | Service *ServiceSpec `json:"service,omitempty"` 69 | MinReplicas *int32 `json:"minReplicas,omitempty"` 70 | MaxReplicas *int32 `json:"maxReplicas,omitempty"` 71 | MinAvailable *int32 `json:"minAvailable,omitempty"` 72 | ThreadsPerPod *int32 `json:"threadsPerPod,omitempty"` 73 | Resources *corev1.ResourceRequirements `json:"resources,omitempty"` 74 | PredictedTraffic *PredictedTrafficSpec `json:"predictedTraffic,omitempty"` 75 | } 76 | 77 | func (spec *ValhallaSpec) GetResources() *corev1.ResourceRequirements { 78 | if spec.Resources == nil { 79 | return &corev1.ResourceRequirements{} 80 | } 81 | return spec.Resources 82 | } 83 | 84 | func (spec *ValhallaSpec) GetThreadsPerPod() int32 { 85 | if spec.ThreadsPerPod == nil { 86 | return 2 87 | } 88 | return *spec.ThreadsPerPod 89 | } 90 | 91 | func (spec *ValhallaSpec) GetMinAvailable() *intstr.IntOrString { 92 | if spec.MinAvailable != nil { 93 | return &intstr.IntOrString{IntVal: *spec.MinAvailable} 94 | } 95 | 96 | return &intstr.IntOrString{IntVal: 1} 97 | } 98 | 99 | func (spec *ValhallaSpec) GetPbfFileName() string { 100 | split := strings.Split(spec.PBFURL, "/") 101 | return split[len(split)-1] 102 | } 103 | 104 | type PersistenceSpec struct { 105 | StorageClassName string `json:"storageClassName,omitempty"` 106 | Storage *resource.Quantity `json:"storage,omitempty"` 107 | AccessMode *corev1.PersistentVolumeAccessMode `json:"accessMode,omitempty"` 108 | } 109 | 110 | func (spec *PersistenceSpec) GetAccessMode() corev1.PersistentVolumeAccessMode { 111 | if spec.AccessMode != nil { 112 | return *spec.AccessMode 113 | } 114 | return corev1.ReadWriteOnce 115 | } 116 | 117 | type ServiceSpec struct { 118 | Type corev1.ServiceType `json:"type,omitempty"` 119 | Annotations map[string]string `json:"annotations,omitempty"` 120 | LoadBalancerIP *string `json:"loadBalancerIP,omitempty"` 121 | } 122 | 123 | type PredictedTrafficSpec struct { 124 | URL string `json:"url,omitempty"` 125 | Schedule string `json:"schedule,omitempty"` 126 | Image *string `json:"image,omitempty"` 127 | } 128 | 129 | // ValhallaStatus defines the observed state of Valhalla 130 | type ValhallaStatus struct { 131 | // Paused is true when the operator notices paused annotation. 132 | Paused bool `json:"paused,omitempty"` 133 | 134 | // ObservedGeneration is the latest generation observed by the operator. 135 | ObservedGeneration int64 `json:"observedGeneration,omitempty"` 136 | 137 | Phase Phase `json:"phase,omitempty"` 138 | 139 | Conditions []metav1.Condition `json:"conditions,omitempty"` 140 | } 141 | 142 | func (valhallaStatus *ValhallaStatus) SetConditions(resources []runtime.Object) { 143 | var oldAvailableCondition *metav1.Condition 144 | var oldAllReplicasReadyCondition *metav1.Condition 145 | var oldReconciliationSuccessCondition *metav1.Condition 146 | 147 | for _, condition := range valhallaStatus.Conditions { 148 | switch condition.Type { 149 | case status.ConditionAllReplicasReady: 150 | oldAllReplicasReadyCondition = condition.DeepCopy() 151 | case status.ConditionAvailable: 152 | oldAvailableCondition = condition.DeepCopy() 153 | case status.ConditionReconciliationSuccess: 154 | oldReconciliationSuccessCondition = condition.DeepCopy() 155 | } 156 | } 157 | 158 | var reconciliationSuccessCondition metav1.Condition 159 | if oldReconciliationSuccessCondition != nil { 160 | reconciliationSuccessCondition = *oldReconciliationSuccessCondition 161 | } else { 162 | reconciliationSuccessCondition = status.ReconcileSuccessCondition(metav1.ConditionUnknown, "Initialising", "") 163 | } 164 | 165 | availableCondition := status.AvailableCondition(resources, oldAvailableCondition) 166 | allReplicasReadyCondition := status.AllReplicasReadyCondition(resources, oldAllReplicasReadyCondition) 167 | valhallaStatus.Conditions = []metav1.Condition{ 168 | availableCondition, 169 | allReplicasReadyCondition, 170 | reconciliationSuccessCondition, 171 | } 172 | } 173 | 174 | func (status *ValhallaStatus) SetCondition(condition metav1.Condition) { 175 | for i := range status.Conditions { 176 | if status.Conditions[i].Type == condition.Type { 177 | if status.Conditions[i].Status != condition.Status { 178 | status.Conditions[i].LastTransitionTime = metav1.Now() 179 | } 180 | status.Conditions[i].Status = condition.Status 181 | status.Conditions[i].Reason = condition.Reason 182 | status.Conditions[i].Message = condition.Message 183 | break 184 | } 185 | } 186 | } 187 | 188 | //+kubebuilder:object:root=true 189 | //+kubebuilder:subresource:status 190 | 191 | // Valhalla is the Schema for the valhallas API 192 | type Valhalla struct { 193 | metav1.TypeMeta `json:",inline"` 194 | metav1.ObjectMeta `json:"metadata,omitempty"` 195 | 196 | Spec ValhallaSpec `json:"spec,omitempty"` 197 | Status ValhallaStatus `json:"status,omitempty"` 198 | } 199 | 200 | func (valhalla Valhalla) ChildResourceName(name string) string { 201 | return strings.TrimSuffix(strings.Join([]string{valhalla.Name, name}, "-"), "-") 202 | } 203 | 204 | //+kubebuilder:object:root=true 205 | 206 | // ValhallaList contains a list of Valhalla 207 | type ValhallaList struct { 208 | metav1.TypeMeta `json:",inline"` 209 | metav1.ListMeta `json:"metadata,omitempty"` 210 | Items []Valhalla `json:"items"` 211 | } 212 | 213 | func init() { 214 | SchemeBuilder.Register(&Valhalla{}, &ValhallaList{}) 215 | } 216 | -------------------------------------------------------------------------------- /api/v1alpha1/zz_generated.deepcopy.go: -------------------------------------------------------------------------------- 1 | //go:build !ignore_autogenerated 2 | // +build !ignore_autogenerated 3 | 4 | /* 5 | Copyright 2022. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | 20 | // Code generated by controller-gen. DO NOT EDIT. 21 | 22 | package v1alpha1 23 | 24 | import ( 25 | "k8s.io/api/core/v1" 26 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 27 | "k8s.io/apimachinery/pkg/runtime" 28 | ) 29 | 30 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 31 | func (in *PersistenceSpec) DeepCopyInto(out *PersistenceSpec) { 32 | *out = *in 33 | if in.Storage != nil { 34 | in, out := &in.Storage, &out.Storage 35 | x := (*in).DeepCopy() 36 | *out = &x 37 | } 38 | if in.AccessMode != nil { 39 | in, out := &in.AccessMode, &out.AccessMode 40 | *out = new(v1.PersistentVolumeAccessMode) 41 | **out = **in 42 | } 43 | } 44 | 45 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PersistenceSpec. 46 | func (in *PersistenceSpec) DeepCopy() *PersistenceSpec { 47 | if in == nil { 48 | return nil 49 | } 50 | out := new(PersistenceSpec) 51 | in.DeepCopyInto(out) 52 | return out 53 | } 54 | 55 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 56 | func (in *PredictedTrafficSpec) DeepCopyInto(out *PredictedTrafficSpec) { 57 | *out = *in 58 | if in.Image != nil { 59 | in, out := &in.Image, &out.Image 60 | *out = new(string) 61 | **out = **in 62 | } 63 | } 64 | 65 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PredictedTrafficSpec. 66 | func (in *PredictedTrafficSpec) DeepCopy() *PredictedTrafficSpec { 67 | if in == nil { 68 | return nil 69 | } 70 | out := new(PredictedTrafficSpec) 71 | in.DeepCopyInto(out) 72 | return out 73 | } 74 | 75 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 76 | func (in *ServiceSpec) DeepCopyInto(out *ServiceSpec) { 77 | *out = *in 78 | if in.Annotations != nil { 79 | in, out := &in.Annotations, &out.Annotations 80 | *out = make(map[string]string, len(*in)) 81 | for key, val := range *in { 82 | (*out)[key] = val 83 | } 84 | } 85 | if in.LoadBalancerIP != nil { 86 | in, out := &in.LoadBalancerIP, &out.LoadBalancerIP 87 | *out = new(string) 88 | **out = **in 89 | } 90 | } 91 | 92 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceSpec. 93 | func (in *ServiceSpec) DeepCopy() *ServiceSpec { 94 | if in == nil { 95 | return nil 96 | } 97 | out := new(ServiceSpec) 98 | in.DeepCopyInto(out) 99 | return out 100 | } 101 | 102 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 103 | func (in *Valhalla) DeepCopyInto(out *Valhalla) { 104 | *out = *in 105 | out.TypeMeta = in.TypeMeta 106 | in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) 107 | in.Spec.DeepCopyInto(&out.Spec) 108 | in.Status.DeepCopyInto(&out.Status) 109 | } 110 | 111 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Valhalla. 112 | func (in *Valhalla) DeepCopy() *Valhalla { 113 | if in == nil { 114 | return nil 115 | } 116 | out := new(Valhalla) 117 | in.DeepCopyInto(out) 118 | return out 119 | } 120 | 121 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 122 | func (in *Valhalla) DeepCopyObject() runtime.Object { 123 | if c := in.DeepCopy(); c != nil { 124 | return c 125 | } 126 | return nil 127 | } 128 | 129 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 130 | func (in *ValhallaList) DeepCopyInto(out *ValhallaList) { 131 | *out = *in 132 | out.TypeMeta = in.TypeMeta 133 | in.ListMeta.DeepCopyInto(&out.ListMeta) 134 | if in.Items != nil { 135 | in, out := &in.Items, &out.Items 136 | *out = make([]Valhalla, len(*in)) 137 | for i := range *in { 138 | (*in)[i].DeepCopyInto(&(*out)[i]) 139 | } 140 | } 141 | } 142 | 143 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ValhallaList. 144 | func (in *ValhallaList) DeepCopy() *ValhallaList { 145 | if in == nil { 146 | return nil 147 | } 148 | out := new(ValhallaList) 149 | in.DeepCopyInto(out) 150 | return out 151 | } 152 | 153 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 154 | func (in *ValhallaList) DeepCopyObject() runtime.Object { 155 | if c := in.DeepCopy(); c != nil { 156 | return c 157 | } 158 | return nil 159 | } 160 | 161 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 162 | func (in *ValhallaSpec) DeepCopyInto(out *ValhallaSpec) { 163 | *out = *in 164 | if in.Image != nil { 165 | in, out := &in.Image, &out.Image 166 | *out = new(string) 167 | **out = **in 168 | } 169 | in.Persistence.DeepCopyInto(&out.Persistence) 170 | if in.Service != nil { 171 | in, out := &in.Service, &out.Service 172 | *out = new(ServiceSpec) 173 | (*in).DeepCopyInto(*out) 174 | } 175 | if in.MinReplicas != nil { 176 | in, out := &in.MinReplicas, &out.MinReplicas 177 | *out = new(int32) 178 | **out = **in 179 | } 180 | if in.MaxReplicas != nil { 181 | in, out := &in.MaxReplicas, &out.MaxReplicas 182 | *out = new(int32) 183 | **out = **in 184 | } 185 | if in.MinAvailable != nil { 186 | in, out := &in.MinAvailable, &out.MinAvailable 187 | *out = new(int32) 188 | **out = **in 189 | } 190 | if in.ThreadsPerPod != nil { 191 | in, out := &in.ThreadsPerPod, &out.ThreadsPerPod 192 | *out = new(int32) 193 | **out = **in 194 | } 195 | if in.Resources != nil { 196 | in, out := &in.Resources, &out.Resources 197 | *out = new(v1.ResourceRequirements) 198 | (*in).DeepCopyInto(*out) 199 | } 200 | if in.PredictedTraffic != nil { 201 | in, out := &in.PredictedTraffic, &out.PredictedTraffic 202 | *out = new(PredictedTrafficSpec) 203 | (*in).DeepCopyInto(*out) 204 | } 205 | } 206 | 207 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ValhallaSpec. 208 | func (in *ValhallaSpec) DeepCopy() *ValhallaSpec { 209 | if in == nil { 210 | return nil 211 | } 212 | out := new(ValhallaSpec) 213 | in.DeepCopyInto(out) 214 | return out 215 | } 216 | 217 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 218 | func (in *ValhallaStatus) DeepCopyInto(out *ValhallaStatus) { 219 | *out = *in 220 | if in.Conditions != nil { 221 | in, out := &in.Conditions, &out.Conditions 222 | *out = make([]metav1.Condition, len(*in)) 223 | for i := range *in { 224 | (*in)[i].DeepCopyInto(&(*out)[i]) 225 | } 226 | } 227 | } 228 | 229 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ValhallaStatus. 230 | func (in *ValhallaStatus) DeepCopy() *ValhallaStatus { 231 | if in == nil { 232 | return nil 233 | } 234 | out := new(ValhallaStatus) 235 | in.DeepCopyInto(out) 236 | return out 237 | } 238 | -------------------------------------------------------------------------------- /config/crd/bases/valhalla.itayankri_valhallas.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | controller-gen.kubebuilder.io/version: v0.10.0 7 | creationTimestamp: null 8 | name: valhallas.valhalla.itayankri 9 | spec: 10 | group: valhalla.itayankri 11 | names: 12 | kind: Valhalla 13 | listKind: ValhallaList 14 | plural: valhallas 15 | singular: valhalla 16 | scope: Namespaced 17 | versions: 18 | - name: v1alpha1 19 | schema: 20 | openAPIV3Schema: 21 | description: Valhalla is the Schema for the valhallas API 22 | properties: 23 | apiVersion: 24 | description: 'APIVersion defines the versioned schema of this representation 25 | of an object. Servers should convert recognized schemas to the latest 26 | internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' 27 | type: string 28 | kind: 29 | description: 'Kind is a string value representing the REST resource this 30 | object represents. Servers may infer this from the endpoint the client 31 | submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' 32 | type: string 33 | metadata: 34 | type: object 35 | spec: 36 | description: ValhallaSpec defines the desired state of Valhalla 37 | properties: 38 | image: 39 | type: string 40 | maxReplicas: 41 | format: int32 42 | type: integer 43 | minAvailable: 44 | format: int32 45 | type: integer 46 | minReplicas: 47 | format: int32 48 | type: integer 49 | pbfUrl: 50 | description: 'INSERT ADDITIONAL SPEC FIELDS - desired state of cluster 51 | Important: Run "make" to regenerate code after modifying this file' 52 | type: string 53 | persistence: 54 | properties: 55 | accessMode: 56 | type: string 57 | storage: 58 | anyOf: 59 | - type: integer 60 | - type: string 61 | pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ 62 | x-kubernetes-int-or-string: true 63 | storageClassName: 64 | type: string 65 | type: object 66 | predictedTraffic: 67 | properties: 68 | image: 69 | type: string 70 | schedule: 71 | type: string 72 | url: 73 | type: string 74 | type: object 75 | resources: 76 | description: ResourceRequirements describes the compute resource requirements. 77 | properties: 78 | limits: 79 | additionalProperties: 80 | anyOf: 81 | - type: integer 82 | - type: string 83 | pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ 84 | x-kubernetes-int-or-string: true 85 | description: 'Limits describes the maximum amount of compute resources 86 | allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' 87 | type: object 88 | requests: 89 | additionalProperties: 90 | anyOf: 91 | - type: integer 92 | - type: string 93 | pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ 94 | x-kubernetes-int-or-string: true 95 | description: 'Requests describes the minimum amount of compute 96 | resources required. If Requests is omitted for a container, 97 | it defaults to Limits if that is explicitly specified, otherwise 98 | to an implementation-defined value. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' 99 | type: object 100 | type: object 101 | service: 102 | properties: 103 | annotations: 104 | additionalProperties: 105 | type: string 106 | type: object 107 | loadBalancerIP: 108 | type: string 109 | type: 110 | description: Service Type string describes ingress methods for 111 | a service 112 | type: string 113 | type: object 114 | threadsPerPod: 115 | format: int32 116 | type: integer 117 | type: object 118 | status: 119 | description: ValhallaStatus defines the observed state of Valhalla 120 | properties: 121 | conditions: 122 | items: 123 | description: "Condition contains details for one aspect of the current 124 | state of this API Resource. --- This struct is intended for direct 125 | use as an array at the field path .status.conditions. For example, 126 | \n type FooStatus struct{ // Represents the observations of a 127 | foo's current state. // Known .status.conditions.type are: \"Available\", 128 | \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge 129 | // +listType=map // +listMapKey=type Conditions []metav1.Condition 130 | `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" 131 | protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" 132 | properties: 133 | lastTransitionTime: 134 | description: lastTransitionTime is the last time the condition 135 | transitioned from one status to another. This should be when 136 | the underlying condition changed. If that is not known, then 137 | using the time when the API field changed is acceptable. 138 | format: date-time 139 | type: string 140 | message: 141 | description: message is a human readable message indicating 142 | details about the transition. This may be an empty string. 143 | maxLength: 32768 144 | type: string 145 | observedGeneration: 146 | description: observedGeneration represents the .metadata.generation 147 | that the condition was set based upon. For instance, if .metadata.generation 148 | is currently 12, but the .status.conditions[x].observedGeneration 149 | is 9, the condition is out of date with respect to the current 150 | state of the instance. 151 | format: int64 152 | minimum: 0 153 | type: integer 154 | reason: 155 | description: reason contains a programmatic identifier indicating 156 | the reason for the condition's last transition. Producers 157 | of specific condition types may define expected values and 158 | meanings for this field, and whether the values are considered 159 | a guaranteed API. The value should be a CamelCase string. 160 | This field may not be empty. 161 | maxLength: 1024 162 | minLength: 1 163 | pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ 164 | type: string 165 | status: 166 | description: status of the condition, one of True, False, Unknown. 167 | enum: 168 | - "True" 169 | - "False" 170 | - Unknown 171 | type: string 172 | type: 173 | description: type of condition in CamelCase or in foo.example.com/CamelCase. 174 | --- Many .condition.type values are consistent across resources 175 | like Available, but because arbitrary conditions can be useful 176 | (see .node.status.conditions), the ability to deconflict is 177 | important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) 178 | maxLength: 316 179 | pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ 180 | type: string 181 | required: 182 | - lastTransitionTime 183 | - message 184 | - reason 185 | - status 186 | - type 187 | type: object 188 | type: array 189 | observedGeneration: 190 | description: ObservedGeneration is the latest generation observed 191 | by the operator. 192 | format: int64 193 | type: integer 194 | paused: 195 | description: Paused is true when the operator notices paused annotation. 196 | type: boolean 197 | phase: 198 | description: Phase is the current phase of the deployment 199 | type: string 200 | type: object 201 | type: object 202 | served: true 203 | storage: true 204 | subresources: 205 | status: {} 206 | -------------------------------------------------------------------------------- /config/crd/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # This kustomization.yaml is not intended to be run by itself, 2 | # since it depends on service name and namespace that are out of this kustomize package. 3 | # It should be run by config/default 4 | resources: 5 | - bases/valhalla.itayankri_valhallas.yaml 6 | #+kubebuilder:scaffold:crdkustomizeresource 7 | 8 | patchesStrategicMerge: 9 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. 10 | # patches here are for enabling the conversion webhook for each CRD 11 | #- patches/webhook_in_valhallas.yaml 12 | #+kubebuilder:scaffold:crdkustomizewebhookpatch 13 | 14 | # [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. 15 | # patches here are for enabling the CA injection for each CRD 16 | #- patches/cainjection_in_valhallas.yaml 17 | #+kubebuilder:scaffold:crdkustomizecainjectionpatch 18 | 19 | # the following config is for teaching kustomize how to do kustomization for CRDs. 20 | configurations: 21 | - kustomizeconfig.yaml 22 | -------------------------------------------------------------------------------- /config/crd/kustomizeconfig.yaml: -------------------------------------------------------------------------------- 1 | # This file is for teaching kustomize how to substitute name and namespace reference in CRD 2 | nameReference: 3 | - kind: Service 4 | version: v1 5 | fieldSpecs: 6 | - kind: CustomResourceDefinition 7 | version: v1 8 | group: apiextensions.k8s.io 9 | path: spec/conversion/webhook/clientConfig/service/name 10 | 11 | namespace: 12 | - kind: CustomResourceDefinition 13 | version: v1 14 | group: apiextensions.k8s.io 15 | path: spec/conversion/webhook/clientConfig/service/namespace 16 | create: false 17 | 18 | varReference: 19 | - path: metadata/annotations 20 | -------------------------------------------------------------------------------- /config/crd/patches/cainjection_in_valhallas.yaml: -------------------------------------------------------------------------------- 1 | # The following patch adds a directive for certmanager to inject CA into the CRD 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 7 | name: valhallas.valhalla.itayankri 8 | -------------------------------------------------------------------------------- /config/crd/patches/webhook_in_valhallas.yaml: -------------------------------------------------------------------------------- 1 | # The following patch enables a conversion webhook for the CRD 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | name: valhallas.valhalla.itayankri 6 | spec: 7 | conversion: 8 | strategy: Webhook 9 | webhook: 10 | clientConfig: 11 | service: 12 | namespace: system 13 | name: webhook-service 14 | path: /convert 15 | conversionReviewVersions: 16 | - v1 17 | -------------------------------------------------------------------------------- /config/default/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # Adds namespace to all resources. 2 | namespace: valhalla-system 3 | 4 | # Value of this field is prepended to the 5 | # names of all resources, e.g. a deployment named 6 | # "wordpress" becomes "alices-wordpress". 7 | # Note that it should also match with the prefix (text before '-') of the namespace 8 | # field above. 9 | namePrefix: valhalla- 10 | 11 | # Labels to add to all resources and selectors. 12 | #commonLabels: 13 | # someName: someValue 14 | 15 | bases: 16 | - ../crd 17 | - ../rbac 18 | - ../manager 19 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in 20 | # crd/kustomization.yaml 21 | #- ../webhook 22 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. 23 | #- ../certmanager 24 | # [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. 25 | #- ../prometheus 26 | 27 | patchesStrategicMerge: 28 | # Protect the /metrics endpoint by putting it behind auth. 29 | # If you want your controller-manager to expose the /metrics 30 | # endpoint w/o any authn/z, please comment the following line. 31 | - manager_auth_proxy_patch.yaml 32 | 33 | # Mount the controller config file for loading manager configurations 34 | # through a ComponentConfig type 35 | #- manager_config_patch.yaml 36 | 37 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in 38 | # crd/kustomization.yaml 39 | #- manager_webhook_patch.yaml 40 | 41 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 42 | # Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks. 43 | # 'CERTMANAGER' needs to be enabled to use ca injection 44 | #- webhookcainjection_patch.yaml 45 | 46 | # the following config is for teaching kustomize how to do var substitution 47 | vars: 48 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix. 49 | #- name: CERTIFICATE_NAMESPACE # namespace of the certificate CR 50 | # objref: 51 | # kind: Certificate 52 | # group: cert-manager.io 53 | # version: v1 54 | # name: serving-cert # this name should match the one in certificate.yaml 55 | # fieldref: 56 | # fieldpath: metadata.namespace 57 | #- name: CERTIFICATE_NAME 58 | # objref: 59 | # kind: Certificate 60 | # group: cert-manager.io 61 | # version: v1 62 | # name: serving-cert # this name should match the one in certificate.yaml 63 | #- name: SERVICE_NAMESPACE # namespace of the service 64 | # objref: 65 | # kind: Service 66 | # version: v1 67 | # name: webhook-service 68 | # fieldref: 69 | # fieldpath: metadata.namespace 70 | #- name: SERVICE_NAME 71 | # objref: 72 | # kind: Service 73 | # version: v1 74 | # name: webhook-service 75 | -------------------------------------------------------------------------------- /config/default/manager_auth_proxy_patch.yaml: -------------------------------------------------------------------------------- 1 | # This patch inject a sidecar container which is a HTTP proxy for the 2 | # controller manager, it performs RBAC authorization against the Kubernetes API using SubjectAccessReviews. 3 | apiVersion: apps/v1 4 | kind: Deployment 5 | metadata: 6 | name: controller-manager 7 | namespace: system 8 | spec: 9 | template: 10 | spec: 11 | containers: 12 | - name: kube-rbac-proxy 13 | image: gcr.io/kubebuilder/kube-rbac-proxy:v0.8.0 14 | args: 15 | - "--secure-listen-address=0.0.0.0:8443" 16 | - "--upstream=http://127.0.0.1:8080/" 17 | - "--logtostderr=true" 18 | - "--v=10" 19 | ports: 20 | - containerPort: 8443 21 | protocol: TCP 22 | name: https 23 | - name: manager 24 | args: 25 | - "--health-probe-bind-address=:8081" 26 | - "--metrics-bind-address=127.0.0.1:8080" 27 | - "--leader-elect" 28 | -------------------------------------------------------------------------------- /config/default/manager_config_patch.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: controller-manager 5 | namespace: system 6 | spec: 7 | template: 8 | spec: 9 | containers: 10 | - name: manager 11 | args: 12 | - "--config=controller_manager_config.yaml" 13 | volumeMounts: 14 | - name: manager-config 15 | mountPath: /controller_manager_config.yaml 16 | subPath: controller_manager_config.yaml 17 | volumes: 18 | - name: manager-config 19 | configMap: 20 | name: manager-config 21 | -------------------------------------------------------------------------------- /config/manager/controller_manager_config.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: controller-runtime.sigs.k8s.io/v1alpha1 2 | kind: ControllerManagerConfig 3 | health: 4 | healthProbeBindAddress: :8081 5 | metrics: 6 | bindAddress: 127.0.0.1:8080 7 | webhook: 8 | port: 9443 9 | leaderElection: 10 | leaderElect: true 11 | resourceName: 6593e2bd.itayankri 12 | -------------------------------------------------------------------------------- /config/manager/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - manager.yaml 3 | 4 | generatorOptions: 5 | disableNameSuffixHash: true 6 | 7 | configMapGenerator: 8 | - files: 9 | - controller_manager_config.yaml 10 | name: manager-config 11 | apiVersion: kustomize.config.k8s.io/v1beta1 12 | kind: Kustomization 13 | images: 14 | - name: controller 15 | newName: itayankri/valhalla-operator 16 | newTag: b096d29 17 | -------------------------------------------------------------------------------- /config/manager/manager.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | labels: 5 | control-plane: controller-manager 6 | name: system 7 | --- 8 | apiVersion: apps/v1 9 | kind: Deployment 10 | metadata: 11 | name: controller-manager 12 | namespace: system 13 | labels: 14 | control-plane: controller-manager 15 | spec: 16 | selector: 17 | matchLabels: 18 | control-plane: controller-manager 19 | replicas: 1 20 | template: 21 | metadata: 22 | annotations: 23 | kubectl.kubernetes.io/default-container: manager 24 | labels: 25 | control-plane: controller-manager 26 | spec: 27 | securityContext: 28 | runAsNonRoot: true 29 | containers: 30 | - command: 31 | - /manager 32 | args: 33 | - --leader-elect 34 | image: controller:latest 35 | name: manager 36 | securityContext: 37 | allowPrivilegeEscalation: false 38 | livenessProbe: 39 | httpGet: 40 | path: /healthz 41 | port: 8081 42 | initialDelaySeconds: 15 43 | periodSeconds: 20 44 | readinessProbe: 45 | httpGet: 46 | path: /readyz 47 | port: 8081 48 | initialDelaySeconds: 5 49 | periodSeconds: 10 50 | # TODO(user): Configure the resources accordingly based on the project requirements. 51 | # More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ 52 | resources: 53 | limits: 54 | cpu: 500m 55 | memory: 128Mi 56 | requests: 57 | cpu: 10m 58 | memory: 64Mi 59 | serviceAccountName: controller-manager 60 | terminationGracePeriodSeconds: 10 61 | -------------------------------------------------------------------------------- /config/manifests/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # These resources constitute the fully configured set of manifests 2 | # used to generate the 'manifests/' directory in a bundle. 3 | resources: 4 | - bases/valhalla.clusterserviceversion.yaml 5 | - ../default 6 | - ../samples 7 | - ../scorecard 8 | 9 | # [WEBHOOK] To enable webhooks, uncomment all the sections with [WEBHOOK] prefix. 10 | # Do NOT uncomment sections with prefix [CERTMANAGER], as OLM does not support cert-manager. 11 | # These patches remove the unnecessary "cert" volume and its manager container volumeMount. 12 | #patchesJson6902: 13 | #- target: 14 | # group: apps 15 | # version: v1 16 | # kind: Deployment 17 | # name: controller-manager 18 | # namespace: system 19 | # patch: |- 20 | # # Remove the manager container's "cert" volumeMount, since OLM will create and mount a set of certs. 21 | # # Update the indices in this path if adding or removing containers/volumeMounts in the manager's Deployment. 22 | # - op: remove 23 | # path: /spec/template/spec/containers/1/volumeMounts/0 24 | # # Remove the "cert" volume, since OLM will create and mount a set of certs. 25 | # # Update the indices in this path if adding or removing volumes in the manager's Deployment. 26 | # - op: remove 27 | # path: /spec/template/spec/volumes/0 28 | -------------------------------------------------------------------------------- /config/prometheus/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - monitor.yaml 3 | -------------------------------------------------------------------------------- /config/prometheus/monitor.yaml: -------------------------------------------------------------------------------- 1 | 2 | # Prometheus Monitor Service (Metrics) 3 | apiVersion: monitoring.coreos.com/v1 4 | kind: ServiceMonitor 5 | metadata: 6 | labels: 7 | control-plane: controller-manager 8 | name: controller-manager-metrics-monitor 9 | namespace: system 10 | spec: 11 | endpoints: 12 | - path: /metrics 13 | port: https 14 | scheme: https 15 | bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token 16 | tlsConfig: 17 | insecureSkipVerify: true 18 | selector: 19 | matchLabels: 20 | control-plane: controller-manager 21 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_client_clusterrole.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: metrics-reader 5 | rules: 6 | - nonResourceURLs: 7 | - "/metrics" 8 | verbs: 9 | - get 10 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: proxy-role 5 | rules: 6 | - apiGroups: 7 | - authentication.k8s.io 8 | resources: 9 | - tokenreviews 10 | verbs: 11 | - create 12 | - apiGroups: 13 | - authorization.k8s.io 14 | resources: 15 | - subjectaccessreviews 16 | verbs: 17 | - create 18 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | name: proxy-rolebinding 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: ClusterRole 8 | name: proxy-role 9 | subjects: 10 | - kind: ServiceAccount 11 | name: controller-manager 12 | namespace: system 13 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | control-plane: controller-manager 6 | name: controller-manager-metrics-service 7 | namespace: system 8 | spec: 9 | ports: 10 | - name: https 11 | port: 8443 12 | protocol: TCP 13 | targetPort: https 14 | selector: 15 | control-plane: controller-manager 16 | -------------------------------------------------------------------------------- /config/rbac/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | # All RBAC will be applied under this service account in 3 | # the deployment namespace. You may comment out this resource 4 | # if your manager will use a service account that exists at 5 | # runtime. Be sure to update RoleBinding and ClusterRoleBinding 6 | # subjects if changing service account names. 7 | - service_account.yaml 8 | - role.yaml 9 | - role_binding.yaml 10 | - leader_election_role.yaml 11 | - leader_election_role_binding.yaml 12 | # Comment the following 4 lines if you want to disable 13 | # the auth proxy (https://github.com/brancz/kube-rbac-proxy) 14 | # which protects your /metrics endpoint. 15 | - auth_proxy_service.yaml 16 | - auth_proxy_role.yaml 17 | - auth_proxy_role_binding.yaml 18 | - auth_proxy_client_clusterrole.yaml 19 | -------------------------------------------------------------------------------- /config/rbac/leader_election_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions to do leader election. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: Role 4 | metadata: 5 | name: leader-election-role 6 | rules: 7 | - apiGroups: 8 | - "" 9 | resources: 10 | - configmaps 11 | verbs: 12 | - get 13 | - list 14 | - watch 15 | - create 16 | - update 17 | - patch 18 | - delete 19 | - apiGroups: 20 | - coordination.k8s.io 21 | resources: 22 | - leases 23 | verbs: 24 | - get 25 | - list 26 | - watch 27 | - create 28 | - update 29 | - patch 30 | - delete 31 | - apiGroups: 32 | - "" 33 | resources: 34 | - events 35 | verbs: 36 | - create 37 | - patch 38 | -------------------------------------------------------------------------------- /config/rbac/leader_election_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: RoleBinding 3 | metadata: 4 | name: leader-election-rolebinding 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: Role 8 | name: leader-election-role 9 | subjects: 10 | - kind: ServiceAccount 11 | name: controller-manager 12 | namespace: system 13 | -------------------------------------------------------------------------------- /config/rbac/role.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | creationTimestamp: null 6 | name: manager-role 7 | rules: 8 | - apiGroups: 9 | - "" 10 | resources: 11 | - persistentvolumeclaims 12 | verbs: 13 | - create 14 | - get 15 | - list 16 | - update 17 | - watch 18 | - apiGroups: 19 | - "" 20 | resources: 21 | - pods 22 | verbs: 23 | - get 24 | - list 25 | - update 26 | - watch 27 | - apiGroups: 28 | - "" 29 | resources: 30 | - pods/exec 31 | verbs: 32 | - create 33 | - apiGroups: 34 | - "" 35 | resources: 36 | - services 37 | verbs: 38 | - create 39 | - get 40 | - list 41 | - update 42 | - watch 43 | - apiGroups: 44 | - apps 45 | resources: 46 | - deployments 47 | verbs: 48 | - create 49 | - get 50 | - list 51 | - update 52 | - watch 53 | - apiGroups: 54 | - autoscaling 55 | resources: 56 | - horizontalpodautoscalers 57 | verbs: 58 | - create 59 | - get 60 | - list 61 | - update 62 | - watch 63 | - apiGroups: 64 | - batch 65 | resources: 66 | - cronjobs 67 | verbs: 68 | - create 69 | - get 70 | - list 71 | - update 72 | - watch 73 | - apiGroups: 74 | - batch 75 | resources: 76 | - jobs 77 | verbs: 78 | - create 79 | - get 80 | - list 81 | - update 82 | - watch 83 | - apiGroups: 84 | - policy 85 | resources: 86 | - poddisruptionbudgets 87 | verbs: 88 | - create 89 | - get 90 | - list 91 | - update 92 | - watch 93 | - apiGroups: 94 | - rbac.authorization.k8s.io 95 | resources: 96 | - rolebindings 97 | verbs: 98 | - create 99 | - get 100 | - list 101 | - update 102 | - watch 103 | - apiGroups: 104 | - rbac.authorization.k8s.io 105 | resources: 106 | - roles 107 | verbs: 108 | - create 109 | - get 110 | - list 111 | - update 112 | - watch 113 | - apiGroups: 114 | - valhalla.itayankri 115 | resources: 116 | - valhallas 117 | verbs: 118 | - create 119 | - delete 120 | - get 121 | - list 122 | - patch 123 | - update 124 | - watch 125 | - apiGroups: 126 | - valhalla.itayankri 127 | resources: 128 | - valhallas/finalizers 129 | verbs: 130 | - update 131 | - apiGroups: 132 | - valhalla.itayankri 133 | resources: 134 | - valhallas/status 135 | verbs: 136 | - get 137 | - patch 138 | - update 139 | -------------------------------------------------------------------------------- /config/rbac/role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | name: manager-rolebinding 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: ClusterRole 8 | name: manager-role 9 | subjects: 10 | - kind: ServiceAccount 11 | name: controller-manager 12 | namespace: system 13 | -------------------------------------------------------------------------------- /config/rbac/service_account.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: controller-manager 5 | namespace: system 6 | -------------------------------------------------------------------------------- /config/rbac/valhalla_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to edit valhallas. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: valhalla-editor-role 6 | rules: 7 | - apiGroups: 8 | - valhalla.itayankri 9 | resources: 10 | - valhallas 11 | verbs: 12 | - create 13 | - delete 14 | - get 15 | - list 16 | - patch 17 | - update 18 | - watch 19 | - apiGroups: 20 | - valhalla.itayankri 21 | resources: 22 | - valhallas/status 23 | verbs: 24 | - get 25 | -------------------------------------------------------------------------------- /config/rbac/valhalla_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to view valhallas. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: valhalla-viewer-role 6 | rules: 7 | - apiGroups: 8 | - valhalla.itayankri 9 | resources: 10 | - valhallas 11 | verbs: 12 | - get 13 | - list 14 | - watch 15 | - apiGroups: 16 | - valhalla.itayankri 17 | resources: 18 | - valhallas/status 19 | verbs: 20 | - get 21 | -------------------------------------------------------------------------------- /config/samples/kustomization.yaml: -------------------------------------------------------------------------------- 1 | ## Append samples you want in your CSV to this file as resources ## 2 | resources: 3 | - valhalla_v1alpha1_valhalla.yaml 4 | #+kubebuilder:scaffold:manifestskustomizesamples 5 | -------------------------------------------------------------------------------- /config/samples/valhalla_v1alpha1_valhalla.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: valhalla.itayankri/v1alpha1 2 | kind: Valhalla 3 | metadata: 4 | name: valhalla-sample 5 | spec: 6 | # TODO(user): Add fields here 7 | -------------------------------------------------------------------------------- /config/scorecard/bases/config.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: scorecard.operatorframework.io/v1alpha3 2 | kind: Configuration 3 | metadata: 4 | name: config 5 | stages: 6 | - parallel: true 7 | tests: [] 8 | -------------------------------------------------------------------------------- /config/scorecard/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - bases/config.yaml 3 | patchesJson6902: 4 | - path: patches/basic.config.yaml 5 | target: 6 | group: scorecard.operatorframework.io 7 | version: v1alpha3 8 | kind: Configuration 9 | name: config 10 | - path: patches/olm.config.yaml 11 | target: 12 | group: scorecard.operatorframework.io 13 | version: v1alpha3 14 | kind: Configuration 15 | name: config 16 | #+kubebuilder:scaffold:patchesJson6902 17 | -------------------------------------------------------------------------------- /config/scorecard/patches/basic.config.yaml: -------------------------------------------------------------------------------- 1 | - op: add 2 | path: /stages/0/tests/- 3 | value: 4 | entrypoint: 5 | - scorecard-test 6 | - basic-check-spec 7 | image: quay.io/operator-framework/scorecard-test:v1.16.0 8 | labels: 9 | suite: basic 10 | test: basic-check-spec-test 11 | -------------------------------------------------------------------------------- /config/scorecard/patches/olm.config.yaml: -------------------------------------------------------------------------------- 1 | - op: add 2 | path: /stages/0/tests/- 3 | value: 4 | entrypoint: 5 | - scorecard-test 6 | - olm-bundle-validation 7 | image: quay.io/operator-framework/scorecard-test:v1.16.0 8 | labels: 9 | suite: olm 10 | test: olm-bundle-validation-test 11 | - op: add 12 | path: /stages/0/tests/- 13 | value: 14 | entrypoint: 15 | - scorecard-test 16 | - olm-crds-have-validation 17 | image: quay.io/operator-framework/scorecard-test:v1.16.0 18 | labels: 19 | suite: olm 20 | test: olm-crds-have-validation-test 21 | - op: add 22 | path: /stages/0/tests/- 23 | value: 24 | entrypoint: 25 | - scorecard-test 26 | - olm-crds-have-resources 27 | image: quay.io/operator-framework/scorecard-test:v1.16.0 28 | labels: 29 | suite: olm 30 | test: olm-crds-have-resources-test 31 | - op: add 32 | path: /stages/0/tests/- 33 | value: 34 | entrypoint: 35 | - scorecard-test 36 | - olm-spec-descriptors 37 | image: quay.io/operator-framework/scorecard-test:v1.16.0 38 | labels: 39 | suite: olm 40 | test: olm-spec-descriptors-test 41 | - op: add 42 | path: /stages/0/tests/- 43 | value: 44 | entrypoint: 45 | - scorecard-test 46 | - olm-status-descriptors 47 | image: quay.io/operator-framework/scorecard-test:v1.16.0 48 | labels: 49 | suite: olm 50 | test: olm-status-descriptors-test 51 | -------------------------------------------------------------------------------- /controllers/suite_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package controllers_test 18 | 19 | import ( 20 | "context" 21 | "path/filepath" 22 | "testing" 23 | 24 | . "github.com/onsi/ginkgo" 25 | . "github.com/onsi/gomega" 26 | "k8s.io/client-go/kubernetes" 27 | "k8s.io/client-go/kubernetes/scheme" 28 | "k8s.io/client-go/rest" 29 | "k8s.io/client-go/util/retry" 30 | "sigs.k8s.io/controller-runtime/pkg/client" 31 | "sigs.k8s.io/controller-runtime/pkg/envtest" 32 | "sigs.k8s.io/controller-runtime/pkg/envtest/printer" 33 | logf "sigs.k8s.io/controller-runtime/pkg/log" 34 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 35 | 36 | valhallav1alpha1 "github.com/itayankri/valhalla-operator/api/v1alpha1" 37 | //+kubebuilder:scaffold:imports 38 | ) 39 | 40 | // These tests use Ginkgo (BDD-style Go testing framework). Refer to 41 | // http://onsi.github.io/ginkgo/ to learn more about Ginkgo. 42 | 43 | var cfg *rest.Config 44 | var k8sClient client.Client 45 | var clientSet *kubernetes.Clientset 46 | var testEnv *envtest.Environment 47 | var ctx context.Context 48 | var updateWithRetry = func(v *valhallav1alpha1.Valhalla, callback func(v *valhallav1alpha1.Valhalla)) error { 49 | return retry.RetryOnConflict(retry.DefaultRetry, func() error { 50 | if err := k8sClient.Get(ctx, client.ObjectKeyFromObject(v), v); err != nil { 51 | return err 52 | } 53 | callback(v) 54 | return k8sClient.Update(ctx, v) 55 | }) 56 | } 57 | 58 | func TestAPIs(t *testing.T) { 59 | RegisterFailHandler(Fail) 60 | 61 | RunSpecsWithDefaultAndCustomReporters(t, 62 | "Controller Suite", 63 | []Reporter{printer.NewlineReporter{}}) 64 | } 65 | 66 | var _ = BeforeSuite(func() { 67 | logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) 68 | 69 | By("bootstrapping test environment") 70 | testEnv = &envtest.Environment{ 71 | CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases")}, 72 | ErrorIfCRDPathMissing: true, 73 | } 74 | 75 | cfg, err := testEnv.Start() 76 | Expect(err).NotTo(HaveOccurred()) 77 | Expect(cfg).NotTo(BeNil()) 78 | 79 | err = valhallav1alpha1.AddToScheme(scheme.Scheme) 80 | Expect(err).NotTo(HaveOccurred()) 81 | 82 | //+kubebuilder:scaffold:scheme 83 | 84 | k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) 85 | Expect(err).NotTo(HaveOccurred()) 86 | Expect(k8sClient).NotTo(BeNil()) 87 | 88 | clientSet, err = kubernetes.NewForConfig(cfg) 89 | Expect(err).NotTo(HaveOccurred()) 90 | 91 | ctx = context.Background() 92 | 93 | }, 60) 94 | 95 | var _ = AfterSuite(func() { 96 | By("tearing down the test environment") 97 | err := testEnv.Stop() 98 | Expect(err).NotTo(HaveOccurred()) 99 | }) 100 | -------------------------------------------------------------------------------- /controllers/valhalla_controller.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package controllers 18 | 19 | import ( 20 | "context" 21 | "encoding/json" 22 | "fmt" 23 | "strconv" 24 | "time" 25 | 26 | "github.com/itayankri/valhalla-operator/internal/resource" 27 | "github.com/itayankri/valhalla-operator/internal/status" 28 | appsv1 "k8s.io/api/apps/v1" 29 | autoscalingv1 "k8s.io/api/autoscaling/v1" 30 | batchv1 "k8s.io/api/batch/v1" 31 | corev1 "k8s.io/api/core/v1" 32 | policyv1 "k8s.io/api/policy/v1" 33 | "k8s.io/apimachinery/pkg/api/errors" 34 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 35 | "k8s.io/apimachinery/pkg/runtime" 36 | "k8s.io/apimachinery/pkg/types" 37 | clientretry "k8s.io/client-go/util/retry" 38 | ctrl "sigs.k8s.io/controller-runtime" 39 | "sigs.k8s.io/controller-runtime/pkg/client" 40 | "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" 41 | 42 | "github.com/go-logr/logr" 43 | valhallav1alpha1 "github.com/itayankri/valhalla-operator/api/v1alpha1" 44 | ) 45 | 46 | const finalizerName = "valhalla.itayankri/finalizer" 47 | 48 | // ValhallaReconciler reconciles a Valhalla object 49 | type ValhallaReconciler struct { 50 | client.Client 51 | Scheme *runtime.Scheme 52 | log logr.Logger 53 | } 54 | 55 | func NewValhallaReconciler(client client.Client, scheme *runtime.Scheme) *ValhallaReconciler { 56 | return &ValhallaReconciler{ 57 | Client: client, 58 | Scheme: scheme, 59 | log: ctrl.Log.WithName("controller").WithName("valhalla"), 60 | } 61 | } 62 | 63 | // +kubebuilder:rbac:groups=valhalla.itayankri,resources=valhallas,verbs=get;list;watch;create;update;patch;delete 64 | // +kubebuilder:rbac:groups="",resources=pods/exec,verbs=create 65 | // +kubebuilder:rbac:groups="",resources=pods,verbs=update;get;list;watch 66 | // +kubebuilder:rbac:groups="",resources=services,verbs=get;list;watch;create;update 67 | // +kubebuilder:rbac:groups="batch",resources=jobs,verbs=get;list;watch;create;update 68 | // +kubebuilder:rbac:groups="batch",resources=cronjobs,verbs=get;list;watch;create;update 69 | // +kubebuilder:rbac:groups="apps",resources=deployments,verbs=get;list;watch;create;update 70 | // +kubebuilder:rbac:groups="autoscaling",resources=horizontalpodautoscalers,verbs=get;list;watch;create;update 71 | // +kubebuilder:rbac:groups="policy",resources=poddisruptionbudgets,verbs=get;list;watch;create;update 72 | // +kubebuilder:rbac:groups=valhalla.itayankri,resources=valhallas,verbs=get;list;watch;create;update;patch;delete 73 | // +kubebuilder:rbac:groups=valhalla.itayankri,resources=valhallas/status,verbs=get;update;patch 74 | // +kubebuilder:rbac:groups=valhalla.itayankri,resources=valhallas/finalizers,verbs=update 75 | // +kubebuilder:rbac:groups="",resources=persistentvolumeclaims,verbs=get;list;watch;create;update 76 | // +kubebuilder:rbac:groups="rbac.authorization.k8s.io",resources=roles,verbs=get;list;watch;create;update 77 | // +kubebuilder:rbac:groups="rbac.authorization.k8s.io",resources=rolebindings,verbs=get;list;watch;create;update 78 | 79 | func (r *ValhallaReconciler) getValhallaInstance(ctx context.Context, namespacedName types.NamespacedName) (*valhallav1alpha1.Valhalla, error) { 80 | instance := &valhallav1alpha1.Valhalla{} 81 | err := r.Client.Get(ctx, namespacedName, instance) 82 | return instance, err 83 | } 84 | 85 | func (r *ValhallaReconciler) updateValhallaResource(ctx context.Context, instance *valhallav1alpha1.Valhalla) error { 86 | err := r.Client.Update(ctx, instance) 87 | if err != nil { 88 | return err 89 | } 90 | 91 | instance.Status.ObservedGeneration = instance.Generation 92 | return r.Client.Status().Update(ctx, instance) 93 | } 94 | 95 | func (r *ValhallaReconciler) getChildResources(ctx context.Context, instance *valhallav1alpha1.Valhalla) ([]runtime.Object, error) { 96 | pvc := &corev1.PersistentVolumeClaim{} 97 | if err := r.Client.Get(ctx, types.NamespacedName{ 98 | Name: instance.ChildResourceName(resource.PersistentVolumeClaimSuffix), 99 | Namespace: instance.Namespace, 100 | }, pvc); err != nil && !errors.IsNotFound(err) { 101 | return nil, err 102 | } else if errors.IsNotFound(err) { 103 | pvc = nil 104 | } 105 | 106 | job := &batchv1.Job{} 107 | if err := r.Client.Get(ctx, types.NamespacedName{ 108 | Name: instance.ChildResourceName(resource.JobSuffix), 109 | Namespace: instance.Namespace, 110 | }, job); err != nil && !errors.IsNotFound(err) { 111 | return nil, err 112 | } else if errors.IsNotFound(err) { 113 | job = nil 114 | } 115 | 116 | deployment := &appsv1.Deployment{} 117 | if err := r.Client.Get(ctx, types.NamespacedName{ 118 | Name: instance.ChildResourceName(resource.DeploymentSuffix), 119 | Namespace: instance.Namespace, 120 | }, deployment); err != nil && !errors.IsNotFound(err) { 121 | return nil, err 122 | } else if errors.IsNotFound(err) { 123 | deployment = nil 124 | } 125 | 126 | hpa := &autoscalingv1.HorizontalPodAutoscaler{} 127 | if err := r.Client.Get(ctx, types.NamespacedName{ 128 | Name: instance.ChildResourceName(resource.HorizontalPodAutoscalerSuffix), 129 | Namespace: instance.Namespace, 130 | }, hpa); err != nil && !errors.IsNotFound(err) { 131 | return nil, err 132 | } else if errors.IsNotFound(err) { 133 | hpa = nil 134 | } 135 | 136 | service := &corev1.Service{} 137 | if err := r.Client.Get(ctx, types.NamespacedName{ 138 | Name: instance.ChildResourceName(resource.ServiceSuffix), 139 | Namespace: instance.Namespace, 140 | }, service); err != nil && !errors.IsNotFound(err) { 141 | return nil, err 142 | } else if errors.IsNotFound(err) { 143 | service = nil 144 | } 145 | 146 | return []runtime.Object{pvc, job, deployment, hpa, service}, nil 147 | } 148 | 149 | func (r *ValhallaReconciler) initialize(ctx context.Context, instance *valhallav1alpha1.Valhalla) error { 150 | controllerutil.AddFinalizer(instance, finalizerName) 151 | return r.updateValhallaResource(ctx, instance) 152 | } 153 | 154 | func (r *ValhallaReconciler) updateValhallaStatusConditions( 155 | ctx context.Context, 156 | instance *valhallav1alpha1.Valhalla, 157 | childResources []runtime.Object, 158 | ) (time.Duration, error) { 159 | instance.Status.SetConditions(childResources) 160 | err := r.Client.Status().Update(ctx, instance) 161 | if err != nil { 162 | if errors.IsConflict(err) { 163 | r.log.Info("failed to update status because of conflict; requeueing...") 164 | return 2 * time.Second, nil 165 | } 166 | return 0, err 167 | } 168 | return 0, nil 169 | } 170 | 171 | func (r *ValhallaReconciler) setReconciliationSuccess( 172 | ctx context.Context, 173 | instance *valhallav1alpha1.Valhalla, 174 | conditionStatus metav1.ConditionStatus, 175 | reason, 176 | msg string, 177 | ) { 178 | instance.Status.SetCondition(metav1.Condition{ 179 | Type: status.ConditionReconciliationSuccess, 180 | Status: conditionStatus, 181 | Reason: reason, 182 | Message: msg, 183 | LastTransitionTime: metav1.Time{ 184 | Time: time.Now(), 185 | }, 186 | }) 187 | if writerErr := r.Status().Update(ctx, instance); writerErr != nil { 188 | ctrl.LoggerFrom(ctx).Error(writerErr, "Failed to update Custom Resource status", 189 | "namespace", instance.Namespace, 190 | "name", instance.Name) 191 | } 192 | } 193 | 194 | // logAndRecordOperationResult - helper function to log and record events with message and error 195 | // it logs and records 'updated' and 'created' OperationResult, and ignores OperationResult 'unchanged' 196 | func (r *ValhallaReconciler) logOperationResult( 197 | logger logr.Logger, 198 | ro runtime.Object, 199 | resource runtime.Object, 200 | operationResult controllerutil.OperationResult, 201 | err error, 202 | ) { 203 | if operationResult == controllerutil.OperationResultNone && err == nil { 204 | return 205 | } 206 | 207 | var operation string 208 | if operationResult == controllerutil.OperationResultCreated { 209 | operation = "create" 210 | } 211 | 212 | if operationResult == controllerutil.OperationResultUpdated { 213 | operation = "update" 214 | } 215 | 216 | if err == nil { 217 | msg := fmt.Sprintf("%sd resource %s of Type %T", operation, resource.(metav1.Object).GetName(), resource.(metav1.Object)) 218 | logger.Info(msg) 219 | } 220 | 221 | if err != nil { 222 | msg := fmt.Sprintf("failed to %s resource %s of Type %T", operation, resource.(metav1.Object).GetName(), resource.(metav1.Object)) 223 | logger.Error(err, msg) 224 | } 225 | } 226 | 227 | func (r *ValhallaReconciler) cleanup(ctx context.Context, instance *valhallav1alpha1.Valhalla) error { 228 | if controllerutil.ContainsFinalizer(instance, finalizerName) { 229 | instance.Status.ObservedGeneration = instance.Generation 230 | instance.Status.SetCondition(metav1.Condition{ 231 | Type: status.ConditionAvailable, 232 | Status: metav1.ConditionFalse, 233 | Reason: "Cleanup", 234 | Message: "Deleting Valhalla resources", 235 | }) 236 | 237 | err := r.Client.Status().Update(ctx, instance) 238 | if err != nil { 239 | return err 240 | } 241 | 242 | controllerutil.RemoveFinalizer(instance, finalizerName) 243 | 244 | err = r.Client.Update(ctx, instance) 245 | if err != nil { 246 | return err 247 | } 248 | } 249 | 250 | instance.Status.ObservedGeneration = instance.Generation 251 | err := r.Client.Status().Update(ctx, instance) 252 | if errors.IsConflict(err) || errors.IsNotFound(err) { 253 | // These errors are ignored. They can happen if the CR was removed 254 | // before the status update call is executed. 255 | return nil 256 | } 257 | return err 258 | } 259 | 260 | // Reconcile is part of the main kubernetes reconciliation loop which aims to 261 | // move the current state of the cluster closer to the desired state. 262 | // TODO(user): Modify the Reconcile function to compare the state specified by 263 | // the Valhalla object against the actual cluster state, and then 264 | // perform operations to make the cluster state reflect the state specified by 265 | // the user. 266 | // 267 | // For more details, check Reconcile and its Result here: 268 | // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.10.0/pkg/reconcile 269 | func (r *ValhallaReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { 270 | logger := r.log.WithValues("valhalla", req.NamespacedName) 271 | logger.Info("Starting reconciliation") 272 | 273 | instance, err := r.getValhallaInstance(ctx, req.NamespacedName) 274 | if err != nil { 275 | if errors.IsNotFound(err) { 276 | // Return and don't requeue 277 | logger.Info("Instance not found") 278 | return ctrl.Result{}, nil 279 | } 280 | 281 | // Error reading the object - requeue the request. 282 | logger.Error(err, "Failed to fetch Valhalla instance") 283 | return ctrl.Result{}, err 284 | } 285 | 286 | childResources, err := r.getChildResources(ctx, instance) 287 | if err != nil { 288 | logger.Error(err, "Failed to fetch child resources", instance.Namespace, instance.Name) 289 | r.setReconciliationSuccess(ctx, instance, metav1.ConditionFalse, "FailedToFetchChildResources", err.Error()) 290 | return ctrl.Result{}, err 291 | } 292 | 293 | if requeueAfter, err := r.updateValhallaStatusConditions(ctx, instance, childResources); err != nil || requeueAfter > 0 { 294 | return ctrl.Result{RequeueAfter: requeueAfter}, err 295 | } 296 | 297 | if !isInitialized(instance) { 298 | err := r.initialize(ctx, instance) 299 | // No need to requeue here, because 300 | // the update will trigger reconciliation again 301 | logger.Info("Valhalla Instance initialized") 302 | return ctrl.Result{}, err 303 | } 304 | 305 | if isBeingDeleted(instance) { 306 | err := r.cleanup(ctx, instance) 307 | if err != nil { 308 | logger.Error(err, "Cleanup failed for rerouce: %v/%v", instance.Namespace, instance.Name) 309 | return ctrl.Result{}, err 310 | } 311 | 312 | return ctrl.Result{}, nil 313 | } 314 | 315 | if isPaused(instance) { 316 | if instance.Status.Paused { 317 | logger.Info("Valhalla operator is paused on resource: %v/%v", instance.Namespace, instance.Name) 318 | return ctrl.Result{}, nil 319 | } 320 | logger.Info(fmt.Sprintf("Pausing Valhalla operator on resource: %v/%v", instance.Namespace, instance.Name)) 321 | instance.Status.Paused = true 322 | err := r.updateValhallaResource(ctx, instance) 323 | // instance.Status.ObservedGeneration = instance.Generation 324 | // err := r.Client.Status().Update(ctx, instance) 325 | return ctrl.Result{}, err 326 | } 327 | 328 | rawInstanceSpec, err := json.Marshal(instance.Spec) 329 | if err != nil { 330 | logger.Error(err, "Failed to marshal Valhalla instance spec") 331 | } 332 | 333 | logger.Info("Reconciling Valhalla instance", "spec", string(rawInstanceSpec)) 334 | 335 | resourceBuilder := resource.ValhallaResourceBuilder{ 336 | Instance: instance, 337 | Scheme: r.Scheme, 338 | } 339 | 340 | builders := resourceBuilder.ResourceBuilders() 341 | 342 | for _, builder := range builders { 343 | if builder.ShouldDeploy(childResources) { 344 | resource, err := builder.Build() 345 | if err != nil { 346 | logger.Error(err, "Failed to build resource %v for Valhalla Instance %v/%v", builder, instance.Namespace, instance.Name) 347 | r.setReconciliationSuccess(ctx, instance, metav1.ConditionFalse, "FailedToBuildChildResource", err.Error()) 348 | return ctrl.Result{}, err 349 | } 350 | 351 | var operationResult controllerutil.OperationResult 352 | err = clientretry.RetryOnConflict(clientretry.DefaultRetry, func() error { 353 | var apiError error 354 | operationResult, apiError = controllerutil.CreateOrUpdate(ctx, r.Client, resource, func() error { 355 | return builder.Update(resource) 356 | }) 357 | return apiError 358 | }) 359 | r.logOperationResult(logger, instance, resource, operationResult, err) 360 | if err != nil { 361 | r.setReconciliationSuccess(ctx, instance, metav1.ConditionFalse, "Error", err.Error()) 362 | return ctrl.Result{}, err 363 | } 364 | } 365 | } 366 | 367 | r.setReconciliationSuccess(ctx, instance, metav1.ConditionTrue, "Success", "Finished reconciling") 368 | logger.Info("Finished reconciling") 369 | return ctrl.Result{}, nil 370 | } 371 | 372 | func isInitialized(instance *valhallav1alpha1.Valhalla) bool { 373 | return controllerutil.ContainsFinalizer(instance, finalizerName) 374 | } 375 | 376 | func isBeingDeleted(object metav1.Object) bool { 377 | return !object.GetDeletionTimestamp().IsZero() 378 | } 379 | 380 | func isPaused(object metav1.Object) bool { 381 | if object.GetAnnotations() == nil { 382 | return false 383 | } 384 | pausedStr, ok := object.GetAnnotations()[valhallav1alpha1.OperatorPausedAnnotation] 385 | if !ok { 386 | return false 387 | } 388 | paused, err := strconv.ParseBool(pausedStr) 389 | if err != nil { 390 | return false 391 | } 392 | return paused 393 | } 394 | 395 | // SetupWithManager sets up the controller with the Manager. 396 | func (r *ValhallaReconciler) SetupWithManager(mgr ctrl.Manager) error { 397 | return ctrl.NewControllerManagedBy(mgr). 398 | For(&valhallav1alpha1.Valhalla{}). 399 | Owns(&corev1.PersistentVolumeClaim{}). 400 | Owns(&batchv1.Job{}). 401 | Owns(&batchv1.CronJob{}). 402 | Owns(&appsv1.Deployment{}). 403 | Owns(&corev1.Service{}). 404 | Owns(&autoscalingv1.HorizontalPodAutoscaler{}). 405 | Owns(&policyv1.PodDisruptionBudget{}). 406 | Complete(r) 407 | } 408 | -------------------------------------------------------------------------------- /controllers/valhalla_controller_test.go: -------------------------------------------------------------------------------- 1 | package controllers_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | valhallav1alpha1 "github.com/itayankri/valhalla-operator/api/v1alpha1" 9 | "github.com/itayankri/valhalla-operator/internal/status" 10 | . "github.com/onsi/ginkgo" 11 | . "github.com/onsi/gomega" 12 | appsv1 "k8s.io/api/apps/v1" 13 | autoscalingv1 "k8s.io/api/autoscaling/v1" 14 | corev1 "k8s.io/api/core/v1" 15 | "k8s.io/apimachinery/pkg/api/resource" 16 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 17 | "k8s.io/apimachinery/pkg/types" 18 | "k8s.io/utils/pointer" 19 | "sigs.k8s.io/controller-runtime/pkg/client" 20 | ) 21 | 22 | const ( 23 | ClusterDeletionTimeout = 5 * time.Second 24 | MapBuildingTimeout = 2 * 60 * time.Second 25 | ) 26 | 27 | var instance *valhallav1alpha1.Valhalla 28 | var defaultNamespace = "default" 29 | 30 | var _ = Describe("ValhallaController", func() { 31 | Context("Resource requirements configurations", func() { 32 | AfterEach(func() { 33 | Expect(k8sClient.Delete(ctx, instance)).To(Succeed()) 34 | }) 35 | 36 | It("uses resource requirements from instance spec when provided", func() { 37 | instance = generateValhallaCluster("resource-requirements-config") 38 | expectedResources := corev1.ResourceRequirements{ 39 | Limits: map[corev1.ResourceName]resource.Quantity{ 40 | corev1.ResourceMemory: resource.MustParse("4Gi"), 41 | }, 42 | Requests: map[corev1.ResourceName]resource.Quantity{ 43 | corev1.ResourceMemory: resource.MustParse("4Gi"), 44 | }, 45 | } 46 | instance.Spec.Resources = &expectedResources 47 | Expect(k8sClient.Create(ctx, instance)).To(Succeed()) 48 | waitForValhallaDeployment(ctx, instance, k8sClient) 49 | deployment := deployment(ctx, instance, "") 50 | actualResources := deployment.Spec.Template.Spec.Containers[0].Resources 51 | Expect(actualResources).To(Equal(expectedResources)) 52 | }) 53 | }) 54 | 55 | Context("Custom Resource updates", func() { 56 | BeforeEach(func() { 57 | instance = generateValhallaCluster("custom-resource-updates") 58 | Expect(k8sClient.Create(ctx, instance)).To(Succeed()) 59 | waitForValhallaDeployment(ctx, instance, k8sClient) 60 | }) 61 | 62 | AfterEach(func() { 63 | Expect(k8sClient.Delete(ctx, instance)).To(Succeed()) 64 | }) 65 | 66 | It("Should update deployment CPU and memory requests and limits", func() { 67 | var resourceRequirements corev1.ResourceRequirements 68 | expectedRequirements := &corev1.ResourceRequirements{ 69 | Requests: corev1.ResourceList{ 70 | corev1.ResourceCPU: resource.MustParse("200m"), 71 | corev1.ResourceMemory: resource.MustParse("200Mi"), 72 | }, 73 | Limits: corev1.ResourceList{ 74 | corev1.ResourceCPU: resource.MustParse("200m"), 75 | corev1.ResourceMemory: resource.MustParse("200Mi"), 76 | }, 77 | } 78 | 79 | Expect(updateWithRetry(instance, func(v *valhallav1alpha1.Valhalla) { 80 | v.Spec.Resources = expectedRequirements 81 | })).To(Succeed()) 82 | 83 | Eventually(func() corev1.ResourceList { 84 | deployment := deployment(ctx, instance, "") 85 | resourceRequirements = deployment.Spec.Template.Spec.Containers[0].Resources 86 | return resourceRequirements.Requests 87 | }, 3).Should(HaveKeyWithValue(corev1.ResourceCPU, expectedRequirements.Requests[corev1.ResourceCPU])) 88 | Expect(resourceRequirements.Limits).To(HaveKeyWithValue(corev1.ResourceCPU, expectedRequirements.Limits[corev1.ResourceCPU])) 89 | Expect(resourceRequirements.Requests).To(HaveKeyWithValue(corev1.ResourceMemory, expectedRequirements.Requests[corev1.ResourceMemory])) 90 | Expect(resourceRequirements.Limits).To(HaveKeyWithValue(corev1.ResourceMemory, expectedRequirements.Limits[corev1.ResourceMemory])) 91 | }) 92 | }) 93 | 94 | Context("Recreate child resources after deletion", func() { 95 | BeforeEach(func() { 96 | instance = generateValhallaCluster("recreate-children") 97 | Expect(k8sClient.Create(ctx, instance)).To(Succeed()) 98 | waitForValhallaDeployment(ctx, instance, k8sClient) 99 | }) 100 | 101 | AfterEach(func() { 102 | Expect(k8sClient.Delete(ctx, instance)).To(Succeed()) 103 | }) 104 | 105 | It("recreates child resources after deletion", func() { 106 | oldService := service(ctx, instance, "") 107 | oldDeployment := deployment(ctx, instance, "") 108 | oldHpa := hpa(ctx, instance, "") 109 | 110 | Expect(k8sClient.Delete(ctx, oldService)).NotTo(HaveOccurred()) 111 | Expect(k8sClient.Delete(ctx, oldHpa)).NotTo(HaveOccurred()) 112 | Expect(k8sClient.Delete(ctx, oldDeployment)).NotTo(HaveOccurred()) 113 | 114 | Eventually(func() bool { 115 | deployment := deployment(ctx, instance, "") 116 | return string(deployment.UID) != string(oldDeployment.UID) 117 | }, 5).Should(BeTrue()) 118 | 119 | Eventually(func() bool { 120 | svc := service(ctx, instance, "") 121 | return string(svc.UID) != string(oldService.UID) 122 | }, 5).Should(BeTrue()) 123 | 124 | Eventually(func() bool { 125 | hpa := hpa(ctx, instance, "") 126 | return string(hpa.UID) != string(oldHpa.UID) 127 | }, 5).Should(BeTrue()) 128 | }) 129 | }) 130 | 131 | Context("Valhalla CR ReconcileSuccess condition", func() { 132 | BeforeEach(func() { 133 | instance = generateValhallaCluster("reconcile-success-condition") 134 | }) 135 | 136 | AfterEach(func() { 137 | Expect(k8sClient.Delete(ctx, instance)).To(Succeed()) 138 | }) 139 | 140 | It("Should keep ReconcileSuccess condition updated", func() { 141 | By("setting to False when spec is not valid", func() { 142 | // It is impossible to create a deployment with -1 replicas. Thus we expect reconcilication to fail. 143 | instance.Spec.MinReplicas = pointer.Int32Ptr(-1) 144 | Expect(k8sClient.Create(ctx, instance)).To(Succeed()) 145 | waitForValhallaCreation(ctx, instance, k8sClient) 146 | 147 | Eventually(func() metav1.ConditionStatus { 148 | valhalla := &valhallav1alpha1.Valhalla{} 149 | Expect(k8sClient.Get(ctx, types.NamespacedName{ 150 | Name: instance.Name, 151 | Namespace: instance.Namespace, 152 | }, valhalla)).To(Succeed()) 153 | 154 | for _, condition := range valhalla.Status.Conditions { 155 | if condition.Type == status.ConditionReconciliationSuccess { 156 | return condition.Status 157 | } 158 | } 159 | return metav1.ConditionUnknown 160 | }, 60*time.Second).Should(Equal(metav1.ConditionFalse)) 161 | }) 162 | 163 | By("setting to True when spec is valid", func() { 164 | // It is impossible to create a deployment with -1 replicas. Thus we expect reconcilication to fail. 165 | Expect(updateWithRetry(instance, func(v *valhallav1alpha1.Valhalla) { 166 | v.Spec.MinReplicas = pointer.Int32Ptr(2) 167 | })).To(Succeed()) 168 | 169 | Eventually(func() metav1.ConditionStatus { 170 | valhalla := &valhallav1alpha1.Valhalla{} 171 | Expect(k8sClient.Get(ctx, types.NamespacedName{ 172 | Name: instance.Name, 173 | Namespace: instance.Namespace, 174 | }, valhalla)).To(Succeed()) 175 | 176 | for _, condition := range valhalla.Status.Conditions { 177 | if condition.Type == status.ConditionReconciliationSuccess { 178 | return condition.Status 179 | } 180 | } 181 | return metav1.ConditionUnknown 182 | }, 60*time.Second).Should(Equal(metav1.ConditionTrue)) 183 | }) 184 | }) 185 | }) 186 | 187 | Context("Pause reconciliation", func() { 188 | BeforeEach(func() { 189 | instance = generateValhallaCluster("pause-reconcile") 190 | Expect(k8sClient.Create(ctx, instance)).To(Succeed()) 191 | waitForValhallaDeployment(ctx, instance, k8sClient) 192 | }) 193 | 194 | AfterEach(func() { 195 | Expect(k8sClient.Delete(ctx, instance)).To(Succeed()) 196 | }) 197 | 198 | It("Should skip valhalla instance if pause reconciliation annotation is set to true", func() { 199 | minReplicas := int32(2) 200 | originalMinReplicas := *instance.Spec.MinReplicas 201 | Expect(updateWithRetry(instance, func(v *valhallav1alpha1.Valhalla) { 202 | v.SetAnnotations(map[string]string{"valhalla.itayankri/operator.paused": "true"}) 203 | v.Spec.MinReplicas = &minReplicas 204 | })).To(Succeed()) 205 | 206 | Eventually(func() int32 { 207 | return *hpa(ctx, instance, "").Spec.MinReplicas 208 | }, MapBuildingTimeout).Should(Equal(originalMinReplicas)) 209 | 210 | Expect(updateWithRetry(instance, func(v *valhallav1alpha1.Valhalla) { 211 | v.SetAnnotations(map[string]string{"valhalla.itayankri/operator.paused": "false"}) 212 | })).To(Succeed()) 213 | 214 | Eventually(func() int32 { 215 | return *hpa(ctx, instance, "").Spec.MinReplicas 216 | }, 10*time.Second).Should(Equal(minReplicas)) 217 | }) 218 | }) 219 | }) 220 | 221 | func generateValhallaCluster(name string) *valhallav1alpha1.Valhalla { 222 | storage := resource.MustParse("10Mi") 223 | image := "itayankri/valhalla:latest" 224 | minReplicas := int32(1) 225 | maxReplicas := int32(3) 226 | valhalla := &valhallav1alpha1.Valhalla{ 227 | ObjectMeta: metav1.ObjectMeta{ 228 | Name: name, 229 | Namespace: defaultNamespace, 230 | }, 231 | Spec: valhallav1alpha1.ValhallaSpec{ 232 | PBFURL: "https://download.geofabrik.de/australia-oceania/marshall-islands-latest.osm.pbf", 233 | Image: &image, 234 | MinReplicas: &minReplicas, 235 | MaxReplicas: &maxReplicas, 236 | Persistence: valhallav1alpha1.PersistenceSpec{ 237 | StorageClassName: "nfs-csi", 238 | Storage: &storage, 239 | }, 240 | Resources: &corev1.ResourceRequirements{ 241 | Limits: corev1.ResourceList{ 242 | corev1.ResourceCPU: resource.MustParse("100m"), 243 | corev1.ResourceMemory: resource.MustParse("100Mi"), 244 | }, 245 | Requests: corev1.ResourceList{ 246 | corev1.ResourceCPU: resource.MustParse("100m"), 247 | corev1.ResourceMemory: resource.MustParse("100Mi"), 248 | }, 249 | }, 250 | }, 251 | } 252 | return valhalla 253 | } 254 | 255 | func waitForValhallaCreation(ctx context.Context, instance *valhallav1alpha1.Valhalla, client client.Client) { 256 | EventuallyWithOffset(1, func() string { 257 | instanceCreated := valhallav1alpha1.Valhalla{} 258 | if err := k8sClient.Get( 259 | ctx, 260 | types.NamespacedName{Name: instance.Name, Namespace: instance.Namespace}, 261 | &instanceCreated, 262 | ); err != nil { 263 | return fmt.Sprintf("%v+", err) 264 | } 265 | 266 | if len(instanceCreated.Status.Conditions) == 0 { 267 | return "not ready" 268 | } 269 | 270 | return "ready" 271 | 272 | }, MapBuildingTimeout, 1*time.Second).Should(Equal("ready")) 273 | } 274 | 275 | func waitForValhallaDeployment(ctx context.Context, instance *valhallav1alpha1.Valhalla, client client.Client) { 276 | EventuallyWithOffset(1, func() string { 277 | instanceCreated := valhallav1alpha1.Valhalla{} 278 | if err := k8sClient.Get( 279 | ctx, 280 | types.NamespacedName{Name: instance.Name, Namespace: instance.Namespace}, 281 | &instanceCreated, 282 | ); err != nil { 283 | return fmt.Sprintf("%v+", err) 284 | } 285 | 286 | for _, condition := range instanceCreated.Status.Conditions { 287 | if condition.Type == status.ConditionAvailable && condition.Status == metav1.ConditionTrue { 288 | return "ready" 289 | } 290 | } 291 | 292 | return "not ready" 293 | 294 | }, MapBuildingTimeout, 1*time.Second).Should(Equal("ready")) 295 | } 296 | 297 | func hpa(ctx context.Context, v *valhallav1alpha1.Valhalla, hpaName string) *autoscalingv1.HorizontalPodAutoscaler { 298 | name := v.ChildResourceName(hpaName) 299 | hpa := &autoscalingv1.HorizontalPodAutoscaler{} 300 | EventuallyWithOffset(1, func() error { 301 | if err := k8sClient.Get( 302 | ctx, 303 | types.NamespacedName{Name: name, Namespace: v.Namespace}, 304 | hpa, 305 | ); err != nil { 306 | return err 307 | } 308 | return nil 309 | }, MapBuildingTimeout).Should(Succeed()) 310 | return hpa 311 | } 312 | 313 | func service(ctx context.Context, v *valhallav1alpha1.Valhalla, svcName string) *corev1.Service { 314 | name := v.ChildResourceName(svcName) 315 | svc := &corev1.Service{} 316 | EventuallyWithOffset(1, func() error { 317 | if err := k8sClient.Get( 318 | ctx, 319 | types.NamespacedName{Name: name, Namespace: v.Namespace}, 320 | svc, 321 | ); err != nil { 322 | return err 323 | } 324 | return nil 325 | }, MapBuildingTimeout).Should(Succeed()) 326 | return svc 327 | } 328 | 329 | func deployment(ctx context.Context, v *valhallav1alpha1.Valhalla, deploymentName string) *appsv1.Deployment { 330 | name := v.ChildResourceName(deploymentName) 331 | deployment := &appsv1.Deployment{} 332 | EventuallyWithOffset(1, func() error { 333 | if err := k8sClient.Get( 334 | ctx, 335 | types.NamespacedName{Name: name, Namespace: v.Namespace}, 336 | deployment, 337 | ); err != nil { 338 | return err 339 | } 340 | return nil 341 | }, MapBuildingTimeout).Should(Succeed()) 342 | return deployment 343 | } 344 | -------------------------------------------------------------------------------- /docker/builder/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM valhalla/valhalla:run-latest 2 | 3 | RUN apt update 4 | RUN apt --assume-yes install wget 5 | 6 | COPY build.sh build.sh 7 | 8 | RUN chmod +x build.sh 9 | 10 | ENTRYPOINT ["/build.sh"] -------------------------------------------------------------------------------- /docker/builder/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | TILES_DIR="valhalla_tiles" 4 | CONF_DIR="conf" 5 | 6 | echo "Evironment:" 7 | printenv 8 | 9 | if [[ -z "${ROOT_DIR}" ]]; then 10 | echo "ROOT_DIR environemnt variable must be provided" 11 | exit 1 12 | fi 13 | 14 | cd $ROOT_DIR 15 | mkdir $TILES_DIR $CONF_DIR 16 | 17 | if [[ -z "${PBF_URL}" ]]; then 18 | echo "PBF_URL environemnt variable must be provided" 19 | exit 1 20 | fi 21 | 22 | PBF_FILE_NAME=$(basename $PBF_URL) 23 | 24 | echo "Downloading PBF from $PBF_URL" 25 | wget $PBF_URL 26 | 27 | echo "Building configuration file..." 28 | valhalla_build_config --mjolnir-tile-dir $ROOT_DIR/$TILES_DIR \ 29 | --mjolnir-tile-extract $ROOT_DIR/valhalla_tiles.tar \ 30 | --mjolnir-timezone $ROOT_DIR/$TILES_DIR/timezones.sqlite \ 31 | --mjolnir-admin $ROOT_DIR/$TILES_DIR/admins.sqlite \ 32 | --mjolnir-traffic-extract $ROOT_DIR/traffic.tar > $ROOT_DIR/$CONF_DIR/valhalla.json 33 | 34 | echo "Building admins..." 35 | valhalla_build_admins --config ./$CONF_DIR/valhalla.json $PBF_FILE_NAME 36 | 37 | echo "Building timezones..." 38 | valhalla_build_timezones > ./$TILES_DIR/timezones.sqlite 39 | 40 | echo "Building tiles..." 41 | valhalla_build_tiles --config ./$CONF_DIR/valhalla.json $PBF_FILE_NAME 42 | 43 | echo "Packing files into tar file..." 44 | find $TILES_DIR | sort -n | tar -cf "valhalla_tiles.tar" --no-recursion -T - 45 | -------------------------------------------------------------------------------- /docker/predicted-traffic-fetcher/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM valhalla/valhalla:run-latest 2 | 3 | COPY fetch.sh fetch.sh 4 | 5 | RUN chmod +x fetch.sh 6 | 7 | ENTRYPOINT ["/fetch.sh"] -------------------------------------------------------------------------------- /docker/predicted-traffic-fetcher/fetch.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | TRAFFIC_DIR = "traffic" 4 | 5 | echo "Evironment:" 6 | printenv 7 | 8 | if [[ -z "${ROOT_DIR}" ]]; then 9 | echo "ROOT_DIR environemnt variable must be provided" 10 | exit 1 11 | fi 12 | 13 | cd $ROOT_DIR 14 | if [ ! -d "${TRAFFIC_DIR}" ]; then 15 | mkdir $TRAFFIC_DIR 16 | fi 17 | 18 | cd $TRAFFIC_DIR 19 | 20 | if [[ -z "${URL}" ]]; then 21 | echo "ROOT_DIR environemnt variable must be provided" 22 | exit 1 23 | fi 24 | 25 | echo "Downloading predicted traffic data from $URL" 26 | curl -O $URL predicted_traffic_data 27 | 28 | echo "Adding Predicted traffic data..." 29 | valhalla_add_predicted_traffic predicted_traffic_data -------------------------------------------------------------------------------- /docker/worker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM valhalla/valhalla:run-latest 2 | 3 | COPY run.sh run.sh 4 | 5 | RUN chmod +x run.sh 6 | 7 | ENTRYPOINT ["/run.sh"] -------------------------------------------------------------------------------- /docker/worker/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | CONF_DIR="conf" 4 | THREADS=${THREADS_PER_POD:=2} 5 | 6 | if [[ -z "${ROOT_DIR}" ]]; then 7 | echo "ROOT_DIR environemnt variable must be provided" 8 | exit 1 9 | fi 10 | 11 | cd $ROOT_DIR 12 | 13 | echo "Starting Valhalla server with $THREADS threads..." 14 | valhalla_service ./$CONF_DIR/valhalla.json $THREADS -------------------------------------------------------------------------------- /examples/Readme.md: -------------------------------------------------------------------------------- 1 | ## Environment Setup 2 | ### Setting up a kind cluster 3 | Kind is a tool for running a local version of kubernetes for development use. In case you already have an operational kubernetes cluster you can skip this step. If you don't, make sure you have [kind](https://kind.sigs.k8s.io/docs/user/quick-start/#installation) installed on your machine. 4 | Once you have it installed, run the following command in order to create a local kubernetes cluster. 5 | 6 | ```bash 7 | kind create cluster --name demo --config ./kind-config.yaml 8 | ``` 9 | 10 | ### Setting up an NFS server on the cluster 11 | In order to manage multiple Valhalla instances efficiently, it is required to have a volume that can be shared between multiple pods. In order to achieve that we are going to use an NFS server and a volume provisioner that will take advantage of this NFS server. Same as the previous step, you can skip this step if you already have a PersistentVolume provisioner installed on your cluster (if you are running in a cloud environment such as GCP for example). 12 | 13 | First, we will install the NFS server: 14 | 15 | ```bash 16 | kubectl create -f https://raw.githubusercontent.com/kubernetes-csi/csi-driver-nfs/master/deploy/example/nfs-provisioner/nfs-server.yaml 17 | ``` 18 | Now we need to make sure that the server is up and running. We will do that by looking in the pod's logs: 19 | 20 | ```bash 21 | kubectl logs -f 22 | ``` 23 | 24 | > **Note** 25 | > If your NFS server fails with the following error - "exportfs: /exports does not support NFS export", you will probably need to change your docker "storage-driver" setting. 26 | > Docker uses OverlayFs by default, we need to change it to vfs. In order to change this setting you need to edit a file called "deamon.json" and then restart the daemon. 27 | 28 | Once our NFS server is ready, we will install the provisioner using helm, so please make you have [helm](https://helm.sh/docs/intro/install/) installed on your machine. 29 | 30 | ```bash 31 | helm repo add csi-driver-nfs https://raw.githubusercontent.com/kubernetes-csi/csi-driver-nfs/master/charts 32 | 33 | helm install csi-driver-nfs csi-driver-nfs/csi-driver-nfs --namespace kube-system --version v3.1.0 34 | ``` 35 | 36 | Once you have a functional NFS server and an NFS volume provisioner you need create a new StorageClass on your cluster. 37 | 38 | ```bash 39 | kubectl create -f https://raw.githubusercontent.com/kubernetes-csi/csi-driver-nfs/master/deploy/example/storageclass-nfs.yaml 40 | ``` 41 | 42 | ## Installing the operator 43 | Just run the following command to install the operator, including the valhalla CRD and all the relevant resources. 44 | 45 | ```bash 46 | kubectl apply -f https://github.com/itayankri/valhalla-operator/releases/latest/download/valhalla-operator.yaml 47 | ``` 48 | 49 | ### Creating a new Valhalla Instance 50 | This directory contains an example Valhalla resource. You can create it using kubectl. 51 | 52 | ```bash 53 | kubectl apply -f example.yaml 54 | ``` 55 | -------------------------------------------------------------------------------- /examples/example.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: valhalla.itayankri/v1alpha1 2 | kind: Valhalla 3 | metadata: 4 | name: example 5 | spec: 6 | pbfUrl: https://download.geofabrik.de/australia-oceania/marshall-islands-latest.osm.pbf 7 | minReplicas: 2 8 | maxReplicas: 5 9 | resources: 10 | requests: 11 | cpu: "1000m" 12 | memory: "100Mi" 13 | limits: 14 | cpu: "1500m" 15 | memory: "150Mi" 16 | persistence: 17 | storage: "100Mi" 18 | storageClassName: standard-rwx 19 | accessMode: ReadWriteMany 20 | service: 21 | type: LoadBalancer 22 | annotations: 23 | example-annotation: "example-annotation" 24 | predictedTraffic: 25 | url: https://example.com 26 | schedule: "*/3 * * * *" 27 | -------------------------------------------------------------------------------- /examples/kind-config.yaml: -------------------------------------------------------------------------------- 1 | kind: Cluster 2 | apiVersion: kind.x-k8s.io/v1alpha4 3 | # 1 control plane node and 2 workers 4 | nodes: 5 | # the control plane node config 6 | - role: control-plane 7 | # the workers 8 | - role: worker -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/itayankri/valhalla-operator 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/go-logr/logr v1.2.4 7 | github.com/onsi/ginkgo v1.16.5 8 | github.com/onsi/gomega v1.27.10 9 | k8s.io/api v0.25.3 10 | k8s.io/apimachinery v0.27.3 11 | k8s.io/client-go v0.25.3 12 | k8s.io/utils v0.0.0-20230209194617-a36077c30491 13 | sigs.k8s.io/controller-runtime v0.13.1 14 | ) 15 | 16 | require ( 17 | cloud.google.com/go v0.99.0 // indirect 18 | github.com/Azure/go-autorest v14.2.0+incompatible // indirect 19 | github.com/Azure/go-autorest/autorest v0.11.27 // indirect 20 | github.com/Azure/go-autorest/autorest/adal v0.9.20 // indirect 21 | github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect 22 | github.com/Azure/go-autorest/logger v0.2.1 // indirect 23 | github.com/Azure/go-autorest/tracing v0.6.0 // indirect 24 | github.com/beorn7/perks v1.0.1 // indirect 25 | github.com/cespare/xxhash/v2 v2.1.2 // indirect 26 | github.com/davecgh/go-spew v1.1.1 // indirect 27 | github.com/emicklei/go-restful/v3 v3.8.0 // indirect 28 | github.com/evanphx/json-patch v5.6.0+incompatible // indirect 29 | github.com/evanphx/json-patch/v5 v5.6.0 // indirect 30 | github.com/fsnotify/fsnotify v1.5.4 // indirect 31 | github.com/go-logr/zapr v1.2.3 // indirect 32 | github.com/go-openapi/jsonpointer v0.19.6 // indirect 33 | github.com/go-openapi/jsonreference v0.20.1 // indirect 34 | github.com/go-openapi/swag v0.22.3 // indirect 35 | github.com/gogo/protobuf v1.3.2 // indirect 36 | github.com/golang-jwt/jwt/v4 v4.2.0 // indirect 37 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 38 | github.com/golang/protobuf v1.5.3 // indirect 39 | github.com/google/gnostic v0.5.7-v3refs // indirect 40 | github.com/google/go-cmp v0.5.9 // indirect 41 | github.com/google/gofuzz v1.2.0 // indirect 42 | github.com/google/uuid v1.3.0 // indirect 43 | github.com/imdario/mergo v0.3.12 // indirect 44 | github.com/josharian/intern v1.0.0 // indirect 45 | github.com/json-iterator/go v1.1.12 // indirect 46 | github.com/mailru/easyjson v0.7.7 // indirect 47 | github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect 48 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 49 | github.com/modern-go/reflect2 v1.0.2 // indirect 50 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 51 | github.com/nxadm/tail v1.4.8 // indirect 52 | github.com/pkg/errors v0.9.1 // indirect 53 | github.com/prometheus/client_golang v1.12.2 // indirect 54 | github.com/prometheus/client_model v0.2.0 // indirect 55 | github.com/prometheus/common v0.32.1 // indirect 56 | github.com/prometheus/procfs v0.7.3 // indirect 57 | github.com/rogpeppe/go-internal v1.10.0 // indirect 58 | github.com/spf13/pflag v1.0.5 // indirect 59 | go.uber.org/atomic v1.7.0 // indirect 60 | go.uber.org/multierr v1.6.0 // indirect 61 | go.uber.org/zap v1.21.0 // indirect 62 | golang.org/x/crypto v0.11.0 // indirect 63 | golang.org/x/net v0.12.0 // indirect 64 | golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect 65 | golang.org/x/sys v0.10.0 // indirect 66 | golang.org/x/term v0.10.0 // indirect 67 | golang.org/x/text v0.11.0 // indirect 68 | golang.org/x/time v0.0.0-20220609170525-579cf78fd858 // indirect 69 | gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect 70 | google.golang.org/appengine v1.6.7 // indirect 71 | google.golang.org/protobuf v1.28.1 // indirect 72 | gopkg.in/inf.v0 v0.9.1 // indirect 73 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect 74 | gopkg.in/yaml.v2 v2.4.0 // indirect 75 | gopkg.in/yaml.v3 v3.0.1 // indirect 76 | k8s.io/apiextensions-apiserver v0.25.2 // indirect 77 | k8s.io/component-base v0.25.2 // indirect 78 | k8s.io/klog/v2 v2.90.1 // indirect 79 | k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f // indirect 80 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect 81 | sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect 82 | sigs.k8s.io/yaml v1.3.0 // indirect 83 | ) 84 | -------------------------------------------------------------------------------- /hack/boilerplate.go.txt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ -------------------------------------------------------------------------------- /internal/metadata/annotations.go: -------------------------------------------------------------------------------- 1 | package metadata 2 | 3 | import "strings" 4 | 5 | func ReconcileAnnotations(existing map[string]string, defaults ...map[string]string) map[string]string { 6 | return merge(existing, defaults...) 7 | } 8 | 9 | func merge(baseAnnotations map[string]string, maps ...map[string]string) map[string]string { 10 | annotations := map[string]string{} 11 | if baseAnnotations != nil { 12 | annotations = baseAnnotations 13 | } 14 | 15 | for _, m := range maps { 16 | for k, v := range m { 17 | annotations[k] = v 18 | } 19 | } 20 | 21 | return annotations 22 | } 23 | 24 | func isKubernetesAnnotation(k string) bool { 25 | return strings.Contains(k, "kubernetes.io") || strings.Contains(k, "k8s.io") 26 | } 27 | -------------------------------------------------------------------------------- /internal/metadata/annotations_test.go: -------------------------------------------------------------------------------- 1 | package metadata_test 2 | 3 | import ( 4 | "github.com/itayankri/valhalla-operator/internal/metadata" 5 | . "github.com/onsi/ginkgo" 6 | . "github.com/onsi/gomega" 7 | ) 8 | 9 | const defaultAnnotationKey = "before" 10 | const defaultAnnotationValue = "each" 11 | 12 | var _ = Describe("Annotations", func() { 13 | Context("ReconcileAnnotations", func() { 14 | var baseAnnotations map[string]string 15 | BeforeEach(func() { 16 | baseAnnotations = map[string]string{defaultAnnotationKey: defaultAnnotationValue} 17 | }) 18 | 19 | It("Should return a merged map of annotations", func() { 20 | const testAnnotationKey = "valhalla" 21 | const testAnnotationValue = "operator" 22 | annotations := map[string]string{testAnnotationKey: testAnnotationValue} 23 | reconciledAnnotations := metadata.ReconcileAnnotations(baseAnnotations, annotations) 24 | 25 | beforeEachAnnotation, beforeEachAnnotationExists := reconciledAnnotations[defaultAnnotationKey] 26 | Expect(beforeEachAnnotationExists).To(Equal(true)) 27 | Expect(beforeEachAnnotation).To(Equal(defaultAnnotationValue)) 28 | 29 | testAnnotation, testAnnotationExists := reconciledAnnotations[testAnnotationKey] 30 | Expect(testAnnotationExists).To(Equal(true)) 31 | Expect(testAnnotation).To(Equal(testAnnotationValue)) 32 | }) 33 | 34 | It("Should prefer an annotation from maps array rather than existing annotations", func() { 35 | const testAnnotationKey = "before" 36 | const testAnnotationValue = "operator" 37 | annotations := map[string]string{testAnnotationKey: testAnnotationValue} 38 | reconciledAnnotations := metadata.ReconcileAnnotations(baseAnnotations, annotations) 39 | 40 | beforeEachAnnotation, beforeEachAnnotationExists := reconciledAnnotations[defaultAnnotationKey] 41 | Expect(beforeEachAnnotationExists).To(Equal(true)) 42 | Expect(beforeEachAnnotation).To(Equal(testAnnotationValue)) 43 | }) 44 | 45 | It("Should return an empty annotations map if existins is nil and no other maps supplied", func() { 46 | reconciledAnnotations := metadata.ReconcileAnnotations(nil) 47 | Expect(reconciledAnnotations).ToNot(Equal(nil)) 48 | Expect(len(reconciledAnnotations)).To(Equal(0)) 49 | }) 50 | }) 51 | }) 52 | -------------------------------------------------------------------------------- /internal/metadata/suite_test.go: -------------------------------------------------------------------------------- 1 | package metadata_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestStatus(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Metadata Suite") 13 | } 14 | -------------------------------------------------------------------------------- /internal/resource/constants.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | const valhallaDataPath = "/data" 4 | const workerImage = "itayankri/valhalla-worker:latest" 5 | const mapBuilderImage = "itayankri/valhalla-builder:latest" 6 | const hirtoricalTrafficDataFetcherImage = "itayankri/valhalla-predicted-traffic:latest" 7 | 8 | const DeploymentSuffix = "" 9 | const HorizontalPodAutoscalerSuffix = "" 10 | const JobSuffix = "builder" 11 | const CronJobSuffix = "predicted-traffic" 12 | const PersistentVolumeClaimSuffix = "" 13 | const PodDisruptionBudgetSuffix = "" 14 | const ServiceSuffix = "" 15 | const containerPort = 8002 16 | -------------------------------------------------------------------------------- /internal/resource/cron_job.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/itayankri/valhalla-operator/internal/status" 7 | batchv1 "k8s.io/api/batch/v1" 8 | corev1 "k8s.io/api/core/v1" 9 | "k8s.io/apimachinery/pkg/api/resource" 10 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 11 | "k8s.io/apimachinery/pkg/runtime" 12 | "sigs.k8s.io/controller-runtime/pkg/client" 13 | "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" 14 | ) 15 | 16 | type CronJobBuilder struct { 17 | *ValhallaResourceBuilder 18 | } 19 | 20 | func (builder *ValhallaResourceBuilder) CronJob() *CronJobBuilder { 21 | return &CronJobBuilder{builder} 22 | } 23 | 24 | func (builder *CronJobBuilder) Build() (client.Object, error) { 25 | return &batchv1.CronJob{ 26 | ObjectMeta: metav1.ObjectMeta{ 27 | Name: builder.Instance.ChildResourceName(CronJobSuffix), 28 | Namespace: builder.Instance.Namespace, 29 | }, 30 | }, nil 31 | } 32 | 33 | func (builder *CronJobBuilder) Update(object client.Object) error { 34 | cronJob := object.(*batchv1.CronJob) 35 | 36 | cronJob.Spec = batchv1.CronJobSpec{ 37 | Schedule: builder.Instance.Spec.PredictedTraffic.Schedule, 38 | JobTemplate: batchv1.JobTemplateSpec{ 39 | ObjectMeta: metav1.ObjectMeta{ 40 | Name: builder.Instance.ChildResourceName(CronJobSuffix), 41 | Namespace: builder.Instance.Namespace, 42 | }, 43 | Spec: batchv1.JobSpec{ 44 | Template: corev1.PodTemplateSpec{ 45 | Spec: corev1.PodSpec{ 46 | RestartPolicy: corev1.RestartPolicyOnFailure, 47 | Containers: []corev1.Container{ 48 | { 49 | Name: builder.Instance.ChildResourceName(CronJobSuffix), 50 | Image: hirtoricalTrafficDataFetcherImage, 51 | Resources: corev1.ResourceRequirements{ 52 | Requests: map[corev1.ResourceName]resource.Quantity{ 53 | "memory": resource.MustParse("100M"), 54 | "cpu": resource.MustParse("100m"), 55 | }, 56 | }, 57 | Env: []corev1.EnvVar{ 58 | { 59 | Name: "ROOT_DIR", 60 | Value: valhallaDataPath, 61 | }, 62 | { 63 | Name: "URL", 64 | Value: builder.Instance.Spec.PredictedTraffic.URL, 65 | }, 66 | }, 67 | VolumeMounts: []corev1.VolumeMount{ 68 | { 69 | Name: builder.Instance.Name, 70 | MountPath: valhallaDataPath, 71 | }, 72 | }, 73 | }, 74 | }, 75 | Volumes: []corev1.Volume{ 76 | { 77 | Name: builder.Instance.Name, 78 | VolumeSource: corev1.VolumeSource{ 79 | PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ 80 | ClaimName: builder.Instance.Name, 81 | ReadOnly: false, 82 | }, 83 | }, 84 | }, 85 | }, 86 | }, 87 | }, 88 | }, 89 | }, 90 | } 91 | 92 | if err := controllerutil.SetControllerReference(builder.Instance, cronJob, builder.Scheme); err != nil { 93 | return fmt.Errorf("failed setting controller reference: %v", err) 94 | } 95 | 96 | return nil 97 | } 98 | 99 | func (builder *CronJobBuilder) ShouldDeploy(resources []runtime.Object) bool { 100 | return builder.Instance.Spec.PredictedTraffic != nil && 101 | status.IsPersistentVolumeClaimBound(resources) && 102 | status.IsJobCompleted(resources) 103 | } 104 | -------------------------------------------------------------------------------- /internal/resource/deployment.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/itayankri/valhalla-operator/internal/status" 7 | appsv1 "k8s.io/api/apps/v1" 8 | corev1 "k8s.io/api/core/v1" 9 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 10 | "k8s.io/apimachinery/pkg/runtime" 11 | "sigs.k8s.io/controller-runtime/pkg/client" 12 | "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" 13 | ) 14 | 15 | type DeploymentBuilder struct { 16 | *ValhallaResourceBuilder 17 | } 18 | 19 | func (builder *ValhallaResourceBuilder) Deployment() *DeploymentBuilder { 20 | return &DeploymentBuilder{builder} 21 | } 22 | 23 | func (builder *DeploymentBuilder) Build() (client.Object, error) { 24 | return &appsv1.Deployment{ 25 | ObjectMeta: metav1.ObjectMeta{ 26 | Name: builder.Instance.ChildResourceName(DeploymentSuffix), 27 | Namespace: builder.Instance.Namespace, 28 | }, 29 | }, nil 30 | } 31 | 32 | func (builder *DeploymentBuilder) Update(object client.Object) error { 33 | name := builder.Instance.ChildResourceName(DeploymentSuffix) 34 | deployment := object.(*appsv1.Deployment) 35 | 36 | deployment.Spec = appsv1.DeploymentSpec{ 37 | Replicas: builder.Instance.Spec.MinReplicas, 38 | Selector: &metav1.LabelSelector{ 39 | MatchLabels: map[string]string{ 40 | "app": name, 41 | }, 42 | }, 43 | Template: corev1.PodTemplateSpec{ 44 | ObjectMeta: metav1.ObjectMeta{ 45 | Labels: map[string]string{ 46 | "app": name, 47 | }, 48 | }, 49 | Spec: corev1.PodSpec{ 50 | Containers: []corev1.Container{ 51 | { 52 | Name: name, 53 | Image: workerImage, 54 | Ports: []corev1.ContainerPort{ 55 | { 56 | ContainerPort: containerPort, 57 | }, 58 | }, 59 | Resources: *builder.Instance.Spec.GetResources(), 60 | Env: []corev1.EnvVar{ 61 | { 62 | Name: "ROOT_DIR", 63 | Value: valhallaDataPath, 64 | }, 65 | { 66 | Name: "THREADS_PER_POD", 67 | Value: fmt.Sprint(builder.Instance.Spec.GetThreadsPerPod()), 68 | }, 69 | }, 70 | VolumeMounts: []corev1.VolumeMount{ 71 | { 72 | Name: name, 73 | MountPath: valhallaDataPath, 74 | }, 75 | }, 76 | }, 77 | }, 78 | Volumes: []corev1.Volume{ 79 | { 80 | Name: name, 81 | VolumeSource: corev1.VolumeSource{ 82 | PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ 83 | ClaimName: name, 84 | ReadOnly: true, 85 | }, 86 | }, 87 | }, 88 | }, 89 | }, 90 | }, 91 | } 92 | 93 | if err := controllerutil.SetControllerReference(builder.Instance, deployment, builder.Scheme); err != nil { 94 | return fmt.Errorf("failed setting controller reference: %v", err) 95 | } 96 | 97 | return nil 98 | } 99 | 100 | func (*DeploymentBuilder) ShouldDeploy(resources []runtime.Object) bool { 101 | return status.IsPersistentVolumeClaimBound(resources) && status.IsJobCompleted(resources) 102 | } 103 | -------------------------------------------------------------------------------- /internal/resource/deployment_test.go: -------------------------------------------------------------------------------- 1 | package resource_test 2 | 3 | import ( 4 | "github.com/itayankri/valhalla-operator/internal/resource" 5 | . "github.com/onsi/ginkgo" 6 | . "github.com/onsi/gomega" 7 | ) 8 | 9 | var _ = Describe("Deployment builder", func() { 10 | Context("ShouldDeploy", func() { 11 | var builder resource.ResourceBuilder 12 | BeforeEach(func() { 13 | builder = valhallaResourceBuilder.Deployment() 14 | }) 15 | 16 | It("Should return 'false' when both PVC is bound and map builder Job is not completed yet", func() { 17 | resources := generateChildResources(false, false) 18 | Expect(builder.ShouldDeploy(resources)).To(Equal(false)) 19 | }) 20 | 21 | It("Should return 'false' when PVC is bound but map builder Job is not completed yet", func() { 22 | resources := generateChildResources(true, false) 23 | Expect(builder.ShouldDeploy(resources)).To(Equal(false)) 24 | }) 25 | 26 | It("Should return 'false' when PVC is not bound but map builder Job is completed", func() { 27 | resources := generateChildResources(false, true) 28 | Expect(builder.ShouldDeploy(resources)).To(Equal(false)) 29 | }) 30 | 31 | It("Should return 'true' when both PVC is bound and map builder Job is compoleted", func() { 32 | resources := generateChildResources(true, true) 33 | Expect(builder.ShouldDeploy(resources)).To(Equal(true)) 34 | }) 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /internal/resource/horizontal_pod_autoscaler.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/itayankri/valhalla-operator/internal/status" 7 | autoscalingv1 "k8s.io/api/autoscaling/v1" 8 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 | "k8s.io/apimachinery/pkg/runtime" 10 | "sigs.k8s.io/controller-runtime/pkg/client" 11 | "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" 12 | ) 13 | 14 | type HorizontalPodAutoscalerBuilder struct { 15 | *ValhallaResourceBuilder 16 | } 17 | 18 | func (builder *ValhallaResourceBuilder) HorizontalPodAutoscaler() *HorizontalPodAutoscalerBuilder { 19 | return &HorizontalPodAutoscalerBuilder{builder} 20 | } 21 | 22 | func (builder *HorizontalPodAutoscalerBuilder) Build() (client.Object, error) { 23 | return &autoscalingv1.HorizontalPodAutoscaler{ 24 | ObjectMeta: metav1.ObjectMeta{ 25 | Name: builder.Instance.ChildResourceName(HorizontalPodAutoscalerSuffix), 26 | Namespace: builder.Instance.Namespace, 27 | }, 28 | }, nil 29 | } 30 | 31 | func (builder *HorizontalPodAutoscalerBuilder) Update(object client.Object) error { 32 | name := builder.Instance.ChildResourceName(HorizontalPodAutoscalerSuffix) 33 | hpa := object.(*autoscalingv1.HorizontalPodAutoscaler) 34 | 35 | targetCPUUtilizationPercentage := int32(85) 36 | 37 | hpa.Spec.ScaleTargetRef = autoscalingv1.CrossVersionObjectReference{ 38 | Kind: "Deployment", 39 | Name: name, 40 | APIVersion: "apps/v1", 41 | } 42 | hpa.Spec.MinReplicas = builder.Instance.Spec.MinReplicas 43 | hpa.Spec.MaxReplicas = *builder.Instance.Spec.MaxReplicas 44 | hpa.Spec.TargetCPUUtilizationPercentage = &targetCPUUtilizationPercentage 45 | 46 | if err := controllerutil.SetControllerReference(builder.Instance, hpa, builder.Scheme); err != nil { 47 | return fmt.Errorf("failed setting controller reference: %v", err) 48 | } 49 | 50 | return nil 51 | } 52 | 53 | func (*HorizontalPodAutoscalerBuilder) ShouldDeploy(resources []runtime.Object) bool { 54 | return status.IsPersistentVolumeClaimBound(resources) && status.IsJobCompleted(resources) 55 | } 56 | -------------------------------------------------------------------------------- /internal/resource/horizontal_pod_autoscaler_test.go: -------------------------------------------------------------------------------- 1 | package resource_test 2 | 3 | import ( 4 | "github.com/itayankri/valhalla-operator/internal/resource" 5 | . "github.com/onsi/ginkgo" 6 | . "github.com/onsi/gomega" 7 | ) 8 | 9 | var _ = Describe("HorizontalPodAutoscaler builder", func() { 10 | Context("ShouldDeploy", func() { 11 | var builder resource.ResourceBuilder 12 | BeforeEach(func() { 13 | builder = valhallaResourceBuilder.HorizontalPodAutoscaler() 14 | }) 15 | 16 | It("Should return 'false' when both PVC is bound and map builder Job is not completed yet", func() { 17 | resources := generateChildResources(false, false) 18 | Expect(builder.ShouldDeploy(resources)).To(Equal(false)) 19 | }) 20 | 21 | It("Should return 'false' when PVC is bound but map builder Job is not completed yet", func() { 22 | resources := generateChildResources(true, false) 23 | Expect(builder.ShouldDeploy(resources)).To(Equal(false)) 24 | }) 25 | 26 | It("Should return 'false' when PVC is not bound but map builder Job is completed", func() { 27 | resources := generateChildResources(false, true) 28 | Expect(builder.ShouldDeploy(resources)).To(Equal(false)) 29 | }) 30 | 31 | It("Should return 'true' when both PVC is bound and map builder Job is compoleted", func() { 32 | resources := generateChildResources(true, true) 33 | Expect(builder.ShouldDeploy(resources)).To(Equal(true)) 34 | }) 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /internal/resource/job.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "fmt" 5 | 6 | batchv1 "k8s.io/api/batch/v1" 7 | corev1 "k8s.io/api/core/v1" 8 | "k8s.io/apimachinery/pkg/api/resource" 9 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 10 | "k8s.io/apimachinery/pkg/runtime" 11 | "sigs.k8s.io/controller-runtime/pkg/client" 12 | "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" 13 | ) 14 | 15 | type JobBuilder struct { 16 | *ValhallaResourceBuilder 17 | } 18 | 19 | func (builder *ValhallaResourceBuilder) Job() *JobBuilder { 20 | return &JobBuilder{builder} 21 | } 22 | 23 | func (builder *JobBuilder) Build() (client.Object, error) { 24 | return &batchv1.Job{ 25 | ObjectMeta: metav1.ObjectMeta{ 26 | Name: builder.Instance.ChildResourceName(JobSuffix), 27 | Namespace: builder.Instance.Namespace, 28 | }, 29 | }, nil 30 | } 31 | 32 | func (builder *JobBuilder) Update(object client.Object) error { 33 | job := object.(*batchv1.Job) 34 | 35 | job.Spec = batchv1.JobSpec{ 36 | Selector: job.Spec.Selector, 37 | Template: corev1.PodTemplateSpec{ 38 | ObjectMeta: metav1.ObjectMeta{ 39 | Labels: job.Spec.Template.ObjectMeta.Labels, 40 | }, 41 | Spec: corev1.PodSpec{ 42 | RestartPolicy: corev1.RestartPolicyOnFailure, 43 | Containers: []corev1.Container{ 44 | { 45 | Name: "map-builder", 46 | Image: mapBuilderImage, 47 | Resources: corev1.ResourceRequirements{ 48 | Requests: map[corev1.ResourceName]resource.Quantity{ 49 | "memory": resource.MustParse("1000M"), 50 | "cpu": resource.MustParse("1000m"), 51 | }, 52 | }, 53 | Env: []corev1.EnvVar{ 54 | { 55 | Name: "ROOT_DIR", 56 | Value: valhallaDataPath, 57 | }, 58 | { 59 | Name: "PBF_URL", 60 | Value: builder.Instance.Spec.PBFURL, 61 | }, 62 | }, 63 | VolumeMounts: []corev1.VolumeMount{ 64 | { 65 | Name: builder.Instance.Name, 66 | MountPath: valhallaDataPath, 67 | }, 68 | }, 69 | }, 70 | }, 71 | Volumes: []corev1.Volume{ 72 | { 73 | Name: builder.Instance.Name, 74 | VolumeSource: corev1.VolumeSource{ 75 | PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ 76 | ClaimName: builder.Instance.Name, 77 | ReadOnly: false, 78 | }, 79 | }, 80 | }, 81 | }, 82 | }, 83 | }, 84 | } 85 | 86 | if err := controllerutil.SetControllerReference(builder.Instance, job, builder.Scheme); err != nil { 87 | return fmt.Errorf("failed setting controller reference: %v", err) 88 | } 89 | 90 | return nil 91 | } 92 | 93 | func (*JobBuilder) ShouldDeploy(resources []runtime.Object) bool { 94 | return true 95 | } 96 | -------------------------------------------------------------------------------- /internal/resource/job_test.go: -------------------------------------------------------------------------------- 1 | package resource_test 2 | 3 | import ( 4 | "github.com/itayankri/valhalla-operator/internal/resource" 5 | . "github.com/onsi/ginkgo" 6 | . "github.com/onsi/gomega" 7 | "k8s.io/apimachinery/pkg/runtime" 8 | ) 9 | 10 | var _ = Describe("Job builder", func() { 11 | Context("ShouldDeploy", func() { 12 | var builder resource.ResourceBuilder 13 | BeforeEach(func() { 14 | builder = valhallaResourceBuilder.Job() 15 | }) 16 | 17 | It("Should always return 'true'", func() { 18 | resources := []runtime.Object{} 19 | Expect(builder.ShouldDeploy(resources)).To(Equal(true)) 20 | }) 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /internal/resource/persistent_volume_claim.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "fmt" 5 | 6 | corev1 "k8s.io/api/core/v1" 7 | "k8s.io/apimachinery/pkg/api/resource" 8 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 | "k8s.io/apimachinery/pkg/runtime" 10 | "sigs.k8s.io/controller-runtime/pkg/client" 11 | "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" 12 | ) 13 | 14 | type PersistentVolumeClaimBuilder struct { 15 | *ValhallaResourceBuilder 16 | } 17 | 18 | func (builder *ValhallaResourceBuilder) PersistentVolumeClaim() *PersistentVolumeClaimBuilder { 19 | return &PersistentVolumeClaimBuilder{builder} 20 | } 21 | 22 | func (builder *PersistentVolumeClaimBuilder) Build() (client.Object, error) { 23 | name := builder.Instance.ChildResourceName(PersistentVolumeClaimSuffix) 24 | return &corev1.PersistentVolumeClaim{ 25 | ObjectMeta: metav1.ObjectMeta{ 26 | Name: name, 27 | Namespace: builder.Instance.Namespace, 28 | }, 29 | Spec: corev1.PersistentVolumeClaimSpec{ 30 | AccessModes: []corev1.PersistentVolumeAccessMode{ 31 | builder.Instance.Spec.Persistence.GetAccessMode(), 32 | }, 33 | Resources: corev1.ResourceRequirements{ 34 | Requests: map[corev1.ResourceName]resource.Quantity{ 35 | corev1.ResourceStorage: *builder.Instance.Spec.Persistence.Storage, 36 | }, 37 | }, 38 | VolumeName: "", 39 | StorageClassName: &builder.Instance.Spec.Persistence.StorageClassName, 40 | }, 41 | }, nil 42 | } 43 | 44 | func (builder *PersistentVolumeClaimBuilder) Update(object client.Object) error { 45 | pvc := object.(*corev1.PersistentVolumeClaim) 46 | 47 | if err := controllerutil.SetControllerReference(builder.Instance, pvc, builder.Scheme); err != nil { 48 | return fmt.Errorf("failed setting controller reference: %v", err) 49 | } 50 | 51 | return nil 52 | } 53 | 54 | func (*PersistentVolumeClaimBuilder) ShouldDeploy(resources []runtime.Object) bool { 55 | return true 56 | } 57 | -------------------------------------------------------------------------------- /internal/resource/persistent_volume_claim_test.go: -------------------------------------------------------------------------------- 1 | package resource_test 2 | 3 | import ( 4 | "github.com/itayankri/valhalla-operator/internal/resource" 5 | . "github.com/onsi/ginkgo" 6 | . "github.com/onsi/gomega" 7 | "k8s.io/apimachinery/pkg/runtime" 8 | ) 9 | 10 | var _ = Describe("PersistentVolumeClaim builder", func() { 11 | Context("ShouldDeploy", func() { 12 | var builder resource.ResourceBuilder 13 | BeforeEach(func() { 14 | builder = valhallaResourceBuilder.PersistentVolumeClaim() 15 | }) 16 | 17 | It("Should always return 'true'", func() { 18 | resources := []runtime.Object{} 19 | Expect(builder.ShouldDeploy(resources)).To(Equal(true)) 20 | }) 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /internal/resource/pod_disruption_budget.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/itayankri/valhalla-operator/internal/status" 7 | policyv1 "k8s.io/api/policy/v1" 8 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 | "k8s.io/apimachinery/pkg/runtime" 10 | "sigs.k8s.io/controller-runtime/pkg/client" 11 | "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" 12 | ) 13 | 14 | type PodDisruptionBudgetBuilder struct { 15 | *ValhallaResourceBuilder 16 | } 17 | 18 | func (builder *ValhallaResourceBuilder) PodDisruptionBudget() *PodDisruptionBudgetBuilder { 19 | return &PodDisruptionBudgetBuilder{builder} 20 | } 21 | 22 | func (builder *PodDisruptionBudgetBuilder) Build() (client.Object, error) { 23 | return &policyv1.PodDisruptionBudget{ 24 | ObjectMeta: metav1.ObjectMeta{ 25 | Name: builder.Instance.ChildResourceName(HorizontalPodAutoscalerSuffix), 26 | Namespace: builder.Instance.Namespace, 27 | }, 28 | }, nil 29 | } 30 | 31 | func (builder *PodDisruptionBudgetBuilder) Update(object client.Object) error { 32 | name := builder.Instance.ChildResourceName(PodDisruptionBudgetSuffix) 33 | pdb := object.(*policyv1.PodDisruptionBudget) 34 | 35 | pdb.Spec.MinAvailable = builder.Instance.Spec.GetMinAvailable() 36 | pdb.Spec.Selector = &metav1.LabelSelector{ 37 | MatchLabels: map[string]string{ 38 | "app": name, 39 | }, 40 | } 41 | 42 | if err := controllerutil.SetControllerReference(builder.Instance, pdb, builder.Scheme); err != nil { 43 | return fmt.Errorf("failed setting controller reference: %v", err) 44 | } 45 | 46 | return nil 47 | } 48 | 49 | func (*PodDisruptionBudgetBuilder) ShouldDeploy(resources []runtime.Object) bool { 50 | return status.IsPersistentVolumeClaimBound(resources) && status.IsJobCompleted(resources) 51 | } 52 | -------------------------------------------------------------------------------- /internal/resource/pod_disruption_budget_test.go: -------------------------------------------------------------------------------- 1 | package resource_test 2 | 3 | import ( 4 | "github.com/itayankri/valhalla-operator/internal/resource" 5 | . "github.com/onsi/ginkgo" 6 | . "github.com/onsi/gomega" 7 | ) 8 | 9 | var _ = Describe("PodDisruptionBudget builder", func() { 10 | Context("ShouldDeploy", func() { 11 | var builder resource.ResourceBuilder 12 | BeforeEach(func() { 13 | builder = valhallaResourceBuilder.PodDisruptionBudget() 14 | }) 15 | 16 | It("Should return 'false' when both PVC is bound and map builder Job is not completed yet", func() { 17 | resources := generateChildResources(false, false) 18 | Expect(builder.ShouldDeploy(resources)).To(Equal(false)) 19 | }) 20 | 21 | It("Should return 'false' when PVC is bound but map builder Job is not completed yet", func() { 22 | resources := generateChildResources(true, false) 23 | Expect(builder.ShouldDeploy(resources)).To(Equal(false)) 24 | }) 25 | 26 | It("Should return 'false' when PVC is not bound but map builder Job is completed", func() { 27 | resources := generateChildResources(false, true) 28 | Expect(builder.ShouldDeploy(resources)).To(Equal(false)) 29 | }) 30 | 31 | It("Should return 'true' when both PVC is bound and map builder Job is compoleted", func() { 32 | resources := generateChildResources(true, true) 33 | Expect(builder.ShouldDeploy(resources)).To(Equal(true)) 34 | }) 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /internal/resource/resource_builder.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | valhallav1alpha1 "github.com/itayankri/valhalla-operator/api/v1alpha1" 5 | "k8s.io/apimachinery/pkg/runtime" 6 | "sigs.k8s.io/controller-runtime/pkg/client" 7 | ) 8 | 9 | type ResourceBuilder interface { 10 | Build() (client.Object, error) 11 | Update(client.Object) error 12 | ShouldDeploy(resources []runtime.Object) bool 13 | } 14 | 15 | type ValhallaResourceBuilder struct { 16 | Instance *valhallav1alpha1.Valhalla 17 | Scheme *runtime.Scheme 18 | } 19 | 20 | func (builder *ValhallaResourceBuilder) ResourceBuilders() []ResourceBuilder { 21 | builders := []ResourceBuilder{ 22 | builder.PersistentVolumeClaim(), 23 | builder.Job(), 24 | builder.CronJob(), 25 | builder.Deployment(), 26 | builder.Service(), 27 | builder.HorizontalPodAutoscaler(), 28 | builder.PodDisruptionBudget(), 29 | } 30 | return builders 31 | } 32 | -------------------------------------------------------------------------------- /internal/resource/service.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/itayankri/valhalla-operator/internal/metadata" 7 | "github.com/itayankri/valhalla-operator/internal/status" 8 | corev1 "k8s.io/api/core/v1" 9 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 10 | "k8s.io/apimachinery/pkg/runtime" 11 | "k8s.io/apimachinery/pkg/util/intstr" 12 | "sigs.k8s.io/controller-runtime/pkg/client" 13 | "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" 14 | ) 15 | 16 | type ServiceBuilder struct { 17 | *ValhallaResourceBuilder 18 | } 19 | 20 | func (builder *ValhallaResourceBuilder) Service() *ServiceBuilder { 21 | return &ServiceBuilder{builder} 22 | } 23 | 24 | func (builder *ServiceBuilder) Build() (client.Object, error) { 25 | return &corev1.Service{ 26 | ObjectMeta: metav1.ObjectMeta{ 27 | Name: builder.Instance.ChildResourceName(ServiceSuffix), 28 | Namespace: builder.Instance.Namespace, 29 | }, 30 | }, nil 31 | } 32 | 33 | func (builder *ServiceBuilder) Update(object client.Object) error { 34 | name := builder.Instance.ChildResourceName(ServiceSuffix) 35 | 36 | service := object.(*corev1.Service) 37 | 38 | service.Spec.Type = corev1.ServiceTypeClusterIP 39 | service.Spec.Ports = []corev1.ServicePort{ 40 | { 41 | Name: "default", 42 | Protocol: corev1.ProtocolTCP, 43 | Port: 80, 44 | TargetPort: intstr.IntOrString{ 45 | Type: intstr.Int, 46 | IntVal: containerPort, 47 | }, 48 | }, 49 | } 50 | service.Spec.Selector = map[string]string{ 51 | "app": name, 52 | } 53 | 54 | if builder.Instance.Spec.Service != nil { 55 | service.Spec.Type = builder.Instance.Spec.Service.Type 56 | if builder.Instance.Spec.Service.LoadBalancerIP != nil { 57 | service.Spec.LoadBalancerIP = *builder.Instance.Spec.Service.LoadBalancerIP 58 | } 59 | builder.setAnnotations(service) 60 | } 61 | 62 | if err := controllerutil.SetControllerReference(builder.Instance, service, builder.Scheme); err != nil { 63 | return fmt.Errorf("failed setting controller reference: %v", err) 64 | } 65 | 66 | return nil 67 | } 68 | 69 | func (*ServiceBuilder) ShouldDeploy(resources []runtime.Object) bool { 70 | return status.IsPersistentVolumeClaimBound(resources) && status.IsJobCompleted(resources) 71 | } 72 | 73 | func (builder *ServiceBuilder) setAnnotations(service *corev1.Service) { 74 | if builder.Instance.Spec.Service.Annotations != nil { 75 | service.Annotations = metadata.ReconcileAnnotations(service.Annotations, builder.Instance.Spec.Service.Annotations) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /internal/resource/service_test.go: -------------------------------------------------------------------------------- 1 | package resource_test 2 | 3 | import ( 4 | "github.com/itayankri/valhalla-operator/internal/resource" 5 | . "github.com/onsi/ginkgo" 6 | . "github.com/onsi/gomega" 7 | ) 8 | 9 | var _ = Describe("Service builder", func() { 10 | Context("ShouldDeploy", func() { 11 | var builder resource.ResourceBuilder 12 | BeforeEach(func() { 13 | builder = valhallaResourceBuilder.Service() 14 | }) 15 | 16 | It("Should return 'false' when both PVC is bound and map builder Job is not completed yet", func() { 17 | resources := generateChildResources(false, false) 18 | Expect(builder.ShouldDeploy(resources)).To(Equal(false)) 19 | }) 20 | 21 | It("Should return 'false' when PVC is bound but map builder Job is not completed yet", func() { 22 | resources := generateChildResources(true, false) 23 | Expect(builder.ShouldDeploy(resources)).To(Equal(false)) 24 | }) 25 | 26 | It("Should return 'false' when PVC is not bound but map builder Job is completed", func() { 27 | resources := generateChildResources(false, true) 28 | Expect(builder.ShouldDeploy(resources)).To(Equal(false)) 29 | }) 30 | 31 | It("Should return 'true' when both PVC is bound and map builder Job is compoleted", func() { 32 | resources := generateChildResources(true, true) 33 | Expect(builder.ShouldDeploy(resources)).To(Equal(true)) 34 | }) 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /internal/resource/suite_test.go: -------------------------------------------------------------------------------- 1 | package resource_test 2 | 3 | import ( 4 | "testing" 5 | 6 | valhallav1alpha1 "github.com/itayankri/valhalla-operator/api/v1alpha1" 7 | "github.com/itayankri/valhalla-operator/internal/resource" 8 | . "github.com/onsi/ginkgo" 9 | . "github.com/onsi/gomega" 10 | batchv1 "k8s.io/api/batch/v1" 11 | corev1 "k8s.io/api/core/v1" 12 | "k8s.io/apimachinery/pkg/runtime" 13 | ) 14 | 15 | var valhallaResourceBuilder *resource.ValhallaResourceBuilder 16 | 17 | func TestStatus(t *testing.T) { 18 | RegisterFailHandler(Fail) 19 | RunSpecs(t, "Resource Suite") 20 | } 21 | 22 | var _ = BeforeSuite(func() { 23 | valhallaResourceBuilder = &resource.ValhallaResourceBuilder{ 24 | Instance: &valhallav1alpha1.Valhalla{}, 25 | } 26 | }) 27 | 28 | func generateChildResources(pvcBound bool, jobCompleted bool) []runtime.Object { 29 | pvcPhase := corev1.ClaimPending 30 | if pvcBound { 31 | pvcPhase = corev1.ClaimBound 32 | } 33 | 34 | jobConditionStatus := corev1.ConditionFalse 35 | if jobCompleted { 36 | jobConditionStatus = corev1.ConditionTrue 37 | } 38 | 39 | childResources := []runtime.Object{ 40 | &batchv1.Job{ 41 | Status: batchv1.JobStatus{ 42 | Conditions: []batchv1.JobCondition{ 43 | { 44 | Type: batchv1.JobComplete, 45 | Status: jobConditionStatus, 46 | }, 47 | }, 48 | }, 49 | }, 50 | &corev1.PersistentVolumeClaim{ 51 | Status: corev1.PersistentVolumeClaimStatus{ 52 | Phase: pvcPhase, 53 | }, 54 | }, 55 | } 56 | 57 | return childResources 58 | } 59 | -------------------------------------------------------------------------------- /internal/status/status.go: -------------------------------------------------------------------------------- 1 | package status 2 | 3 | import ( 4 | "time" 5 | 6 | appsv1 "k8s.io/api/apps/v1" 7 | batchv1 "k8s.io/api/batch/v1" 8 | corev1 "k8s.io/api/core/v1" 9 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 10 | "k8s.io/apimachinery/pkg/runtime" 11 | ) 12 | 13 | const ( 14 | ConditionAvailable = "Available" 15 | ConditionReconciliationSuccess = "ReconciliationSuccess" 16 | ConditionAllReplicasReady = "AllReplicasReady" 17 | ) 18 | 19 | func AvailableCondition(resources []runtime.Object, old *metav1.Condition) metav1.Condition { 20 | condition := metav1.Condition{ 21 | Type: ConditionAvailable, 22 | Status: metav1.ConditionFalse, 23 | Reason: "DeploymentUnavailable", 24 | Message: "Deployment does not have minimum availability", 25 | } 26 | 27 | if old != nil { 28 | condition.LastTransitionTime = old.LastTransitionTime 29 | } 30 | 31 | for _, resource := range resources { 32 | if deployment, ok := resource.(*appsv1.Deployment); ok { 33 | if deployment != nil { 34 | for _, cond := range deployment.Status.Conditions { 35 | if cond.Type == appsv1.DeploymentAvailable && cond.Status == corev1.ConditionTrue { 36 | condition.Status = metav1.ConditionTrue 37 | condition.Message = cond.Message 38 | condition.Reason = "Available" 39 | } 40 | } 41 | } 42 | break 43 | } 44 | } 45 | 46 | if old == nil || old.Status != condition.Status { 47 | condition.LastTransitionTime = metav1.Time{ 48 | Time: time.Now(), 49 | } 50 | } 51 | 52 | return condition 53 | } 54 | 55 | func AllReplicasReadyCondition(resources []runtime.Object, old *metav1.Condition) metav1.Condition { 56 | condition := metav1.Condition{ 57 | Type: ConditionAllReplicasReady, 58 | Status: metav1.ConditionFalse, 59 | Reason: "NotAllReplicasReady", 60 | Message: "One or more pods are not ready", 61 | } 62 | 63 | if old != nil { 64 | condition.LastTransitionTime = old.LastTransitionTime 65 | } 66 | 67 | if DoAllReplicasReady(resources) { 68 | condition.Status = metav1.ConditionTrue 69 | condition.Message = "All pods are ready" 70 | condition.Reason = "AllReplicasReady" 71 | } 72 | 73 | if old == nil || old.Status != condition.Status { 74 | condition.LastTransitionTime = metav1.Time{ 75 | Time: time.Now(), 76 | } 77 | } 78 | 79 | return condition 80 | } 81 | 82 | func ReconcileSuccessCondition(status metav1.ConditionStatus, reason, message string) metav1.Condition { 83 | return metav1.Condition{ 84 | Type: ConditionReconciliationSuccess, 85 | Status: status, 86 | LastTransitionTime: metav1.Time{Time: time.Now()}, 87 | Reason: reason, 88 | Message: message, 89 | } 90 | } 91 | 92 | func IsPersistentVolumeClaimBound(resources []runtime.Object) bool { 93 | pvcBound := false 94 | for _, resource := range resources { 95 | if pvc, ok := resource.(*corev1.PersistentVolumeClaim); ok { 96 | if pvc != nil && pvc.Status.Phase == corev1.ClaimBound { 97 | pvcBound = true 98 | } 99 | break 100 | } 101 | } 102 | return pvcBound 103 | } 104 | 105 | func IsJobCompleted(resources []runtime.Object) bool { 106 | jobCompleted := false 107 | for _, resource := range resources { 108 | if job, ok := resource.(*batchv1.Job); ok { 109 | if job != nil { 110 | for _, condition := range job.Status.Conditions { 111 | if condition.Type == batchv1.JobComplete && condition.Status == corev1.ConditionTrue { 112 | jobCompleted = true 113 | } 114 | } 115 | break 116 | } 117 | } 118 | } 119 | return jobCompleted 120 | } 121 | 122 | func DoAllReplicasReady(resources []runtime.Object) bool { 123 | allReplicasReady := false 124 | for _, resource := range resources { 125 | if deployment, ok := resource.(*appsv1.Deployment); ok { 126 | if deployment != nil && deployment.Spec.Replicas != nil && deployment.Status.ReadyReplicas >= *deployment.Spec.Replicas { 127 | allReplicasReady = true 128 | } 129 | break 130 | } 131 | } 132 | return allReplicasReady 133 | } 134 | -------------------------------------------------------------------------------- /internal/status/status_test.go: -------------------------------------------------------------------------------- 1 | package status_test 2 | 3 | import ( 4 | "github.com/itayankri/valhalla-operator/internal/status" 5 | . "github.com/onsi/ginkgo" 6 | . "github.com/onsi/gomega" 7 | appsv1 "k8s.io/api/apps/v1" 8 | corev1 "k8s.io/api/core/v1" 9 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 10 | "k8s.io/apimachinery/pkg/runtime" 11 | "k8s.io/utils/pointer" 12 | "time" 13 | ) 14 | 15 | const valhallaName = "test" 16 | 17 | var _ = Describe("Status", func() { 18 | Context("Conditions", func() { 19 | Context("ConditionAvailable", func() { 20 | It("Should return a new condition with ConditionTrue status if child deployment is available", func() { 21 | oldCondition := &metav1.Condition{ 22 | Type: status.ConditionAvailable, 23 | Status: metav1.ConditionFalse, 24 | LastTransitionTime: metav1.Time{ 25 | Time: time.Now(), 26 | }, 27 | } 28 | 29 | childResources := []runtime.Object{ 30 | &appsv1.Deployment{ 31 | ObjectMeta: metav1.ObjectMeta{ 32 | Name: valhallaName, 33 | Namespace: "default", 34 | }, 35 | Spec: appsv1.DeploymentSpec{}, 36 | Status: appsv1.DeploymentStatus{ 37 | Conditions: []appsv1.DeploymentCondition{ 38 | appsv1.DeploymentCondition{ 39 | Type: appsv1.DeploymentAvailable, 40 | Status: corev1.ConditionTrue, 41 | LastTransitionTime: metav1.Time{ 42 | Time: time.Now(), 43 | }, 44 | LastUpdateTime: metav1.Time{ 45 | Time: time.Now(), 46 | }, 47 | }, 48 | }, 49 | }, 50 | }, 51 | } 52 | condition := status.AvailableCondition(childResources, oldCondition) 53 | Expect(condition.Type).To(Equal(status.ConditionAvailable)) 54 | Expect(condition.Status).To(Equal(metav1.ConditionTrue)) 55 | }) 56 | 57 | It("Should return a new condition with ConditionFalse status if child deployment is unavailable", func() { 58 | oldCondition := &metav1.Condition{ 59 | Type: status.ConditionAvailable, 60 | Status: metav1.ConditionFalse, 61 | LastTransitionTime: metav1.Time{ 62 | Time: time.Now(), 63 | }, 64 | } 65 | 66 | childResources := []runtime.Object{ 67 | &appsv1.Deployment{ 68 | ObjectMeta: metav1.ObjectMeta{ 69 | Name: valhallaName, 70 | Namespace: "default", 71 | }, 72 | Spec: appsv1.DeploymentSpec{}, 73 | Status: appsv1.DeploymentStatus{ 74 | Conditions: []appsv1.DeploymentCondition{ 75 | appsv1.DeploymentCondition{ 76 | Type: appsv1.DeploymentAvailable, 77 | Status: corev1.ConditionFalse, 78 | LastTransitionTime: metav1.Time{ 79 | Time: time.Now(), 80 | }, 81 | LastUpdateTime: metav1.Time{ 82 | Time: time.Now(), 83 | }, 84 | }, 85 | }, 86 | }, 87 | }, 88 | } 89 | condition := status.AvailableCondition(childResources, oldCondition) 90 | Expect(condition.Type).To(Equal(status.ConditionAvailable)) 91 | Expect(condition.Status).To(Equal(metav1.ConditionFalse)) 92 | }) 93 | 94 | It("Should return a new condition with ConditionFalse status if child deployment is not present in child resources slice", func() { 95 | oldCondition := &metav1.Condition{ 96 | Type: status.ConditionAvailable, 97 | Status: metav1.ConditionFalse, 98 | LastTransitionTime: metav1.Time{ 99 | Time: time.Now(), 100 | }, 101 | } 102 | 103 | childResources := []runtime.Object{} 104 | condition := status.AvailableCondition(childResources, oldCondition) 105 | Expect(condition.Type).To(Equal(status.ConditionAvailable)) 106 | Expect(condition.Status).To(Equal(metav1.ConditionFalse)) 107 | }) 108 | 109 | It("Should update LastTransitionTime if status changed", func() { 110 | oldCondition := &metav1.Condition{ 111 | Type: status.ConditionAvailable, 112 | Status: metav1.ConditionTrue, 113 | LastTransitionTime: metav1.Time{ 114 | Time: time.Now(), 115 | }, 116 | } 117 | 118 | childResources := []runtime.Object{} 119 | condition := status.AvailableCondition(childResources, oldCondition) 120 | Expect(condition.Type).To(Equal(status.ConditionAvailable)) 121 | Expect(condition.Status).To(Equal(metav1.ConditionFalse)) 122 | Expect(oldCondition.LastTransitionTime.Before(&condition.LastTransitionTime)).To(Equal(true)) 123 | }) 124 | 125 | It("Should not update LastTransitionTime if status has not changed", func() { 126 | oldCondition := &metav1.Condition{ 127 | Type: status.ConditionAvailable, 128 | Status: metav1.ConditionFalse, 129 | LastTransitionTime: metav1.Time{ 130 | Time: time.Now(), 131 | }, 132 | } 133 | 134 | childResources := []runtime.Object{} 135 | condition := status.AvailableCondition(childResources, oldCondition) 136 | Expect(condition.Type).To(Equal(status.ConditionAvailable)) 137 | Expect(condition.Status).To(Equal(metav1.ConditionFalse)) 138 | Expect(oldCondition.LastTransitionTime.Before(&condition.LastTransitionTime)).To(Equal(false)) 139 | }) 140 | }) 141 | 142 | Context("ConditionAllReplicasReady", func() { 143 | It("Should return a new condition with ConditionTrue status if all child deployment's pods are available", func() { 144 | oldCondition := &metav1.Condition{ 145 | Type: status.ConditionAllReplicasReady, 146 | Status: metav1.ConditionFalse, 147 | LastTransitionTime: metav1.Time{ 148 | Time: time.Now(), 149 | }, 150 | } 151 | 152 | childResources := []runtime.Object{ 153 | &appsv1.Deployment{ 154 | ObjectMeta: metav1.ObjectMeta{ 155 | Name: valhallaName, 156 | Namespace: "default", 157 | }, 158 | Spec: appsv1.DeploymentSpec{ 159 | Replicas: pointer.Int32Ptr(2), 160 | }, 161 | Status: appsv1.DeploymentStatus{ 162 | ReadyReplicas: 2, 163 | }, 164 | }, 165 | } 166 | 167 | condition := status.AllReplicasReadyCondition(childResources, oldCondition) 168 | Expect(condition.Type).To(Equal(status.ConditionAllReplicasReady)) 169 | Expect(condition.Status).To(Equal(metav1.ConditionTrue)) 170 | }) 171 | 172 | It("Should return a new condition with ConditionFalse status if not all child deployment's pods are available", func() { 173 | oldCondition := &metav1.Condition{ 174 | Type: status.ConditionAllReplicasReady, 175 | Status: metav1.ConditionFalse, 176 | LastTransitionTime: metav1.Time{ 177 | Time: time.Now(), 178 | }, 179 | } 180 | 181 | childResources := []runtime.Object{ 182 | &appsv1.Deployment{ 183 | ObjectMeta: metav1.ObjectMeta{ 184 | Name: valhallaName, 185 | Namespace: "default", 186 | }, 187 | Spec: appsv1.DeploymentSpec{ 188 | Replicas: pointer.Int32Ptr(2), 189 | }, 190 | Status: appsv1.DeploymentStatus{ 191 | ReadyReplicas: 1, 192 | }, 193 | }, 194 | } 195 | 196 | condition := status.AllReplicasReadyCondition(childResources, oldCondition) 197 | Expect(condition.Type).To(Equal(status.ConditionAllReplicasReady)) 198 | Expect(condition.Status).To(Equal(metav1.ConditionFalse)) 199 | }) 200 | 201 | It("Should return a new condition with ConditionFalse status if child deployment' is not present in child resources slice", func() { 202 | oldCondition := &metav1.Condition{ 203 | Type: status.ConditionAllReplicasReady, 204 | Status: metav1.ConditionFalse, 205 | LastTransitionTime: metav1.Time{ 206 | Time: time.Now(), 207 | }, 208 | } 209 | 210 | childResources := []runtime.Object{} 211 | condition := status.AllReplicasReadyCondition(childResources, oldCondition) 212 | Expect(condition.Type).To(Equal(status.ConditionAllReplicasReady)) 213 | Expect(condition.Status).To(Equal(metav1.ConditionFalse)) 214 | }) 215 | 216 | It("Should update LastTransitionTime if status changed", func() { 217 | oldCondition := &metav1.Condition{ 218 | Type: status.ConditionAllReplicasReady, 219 | Status: metav1.ConditionTrue, 220 | LastTransitionTime: metav1.Time{ 221 | Time: time.Now(), 222 | }, 223 | } 224 | 225 | childResources := []runtime.Object{} 226 | condition := status.AllReplicasReadyCondition(childResources, oldCondition) 227 | Expect(condition.Type).To(Equal(status.ConditionAllReplicasReady)) 228 | Expect(condition.Status).To(Equal(metav1.ConditionFalse)) 229 | Expect(oldCondition.LastTransitionTime.Before(&condition.LastTransitionTime)).To(Equal(true)) 230 | }) 231 | 232 | It("Should not update LastTransitionTime if status has not changed", func() { 233 | oldCondition := &metav1.Condition{ 234 | Type: status.ConditionAllReplicasReady, 235 | Status: metav1.ConditionFalse, 236 | LastTransitionTime: metav1.Time{ 237 | Time: time.Now(), 238 | }, 239 | } 240 | 241 | childResources := []runtime.Object{} 242 | condition := status.AllReplicasReadyCondition(childResources, oldCondition) 243 | Expect(condition.Type).To(Equal(status.ConditionAllReplicasReady)) 244 | Expect(condition.Status).To(Equal(metav1.ConditionFalse)) 245 | Expect(oldCondition.LastTransitionTime.Before(&condition.LastTransitionTime)).To(Equal(false)) 246 | }) 247 | }) 248 | }) 249 | }) 250 | -------------------------------------------------------------------------------- /internal/status/suite_test.go: -------------------------------------------------------------------------------- 1 | package status_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestStatus(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Status Suite") 13 | } 14 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "flag" 21 | "os" 22 | 23 | // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) 24 | // to ensure that exec-entrypoint and run can make use of them. 25 | "k8s.io/apimachinery/pkg/runtime" 26 | utilruntime "k8s.io/apimachinery/pkg/util/runtime" 27 | clientgoscheme "k8s.io/client-go/kubernetes/scheme" 28 | _ "k8s.io/client-go/plugin/pkg/client/auth" 29 | ctrl "sigs.k8s.io/controller-runtime" 30 | "sigs.k8s.io/controller-runtime/pkg/healthz" 31 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 32 | 33 | valhallav1alpha1 "github.com/itayankri/valhalla-operator/api/v1alpha1" 34 | "github.com/itayankri/valhalla-operator/controllers" 35 | //+kubebuilder:scaffold:imports 36 | ) 37 | 38 | var ( 39 | scheme = runtime.NewScheme() 40 | setupLog = ctrl.Log.WithName("setup") 41 | ) 42 | 43 | func init() { 44 | utilruntime.Must(clientgoscheme.AddToScheme(scheme)) 45 | 46 | utilruntime.Must(valhallav1alpha1.AddToScheme(scheme)) 47 | //+kubebuilder:scaffold:scheme 48 | } 49 | 50 | func main() { 51 | var metricsAddr string 52 | var enableLeaderElection bool 53 | var probeAddr string 54 | flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.") 55 | flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") 56 | flag.BoolVar(&enableLeaderElection, "leader-elect", false, 57 | "Enable leader election for controller manager. "+ 58 | "Enabling this will ensure there is only one active controller manager.") 59 | opts := zap.Options{ 60 | Development: true, 61 | } 62 | opts.BindFlags(flag.CommandLine) 63 | flag.Parse() 64 | 65 | ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) 66 | 67 | mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ 68 | Scheme: scheme, 69 | MetricsBindAddress: metricsAddr, 70 | Port: 9443, 71 | HealthProbeBindAddress: probeAddr, 72 | LeaderElection: enableLeaderElection, 73 | LeaderElectionID: "6593e2bd.itayankri", 74 | }) 75 | if err != nil { 76 | setupLog.Error(err, "unable to start manager") 77 | os.Exit(1) 78 | } 79 | 80 | if err = (controllers.NewValhallaReconciler(mgr.GetClient(), mgr.GetScheme())).SetupWithManager(mgr); err != nil { 81 | setupLog.Error(err, "unable to create controller", "controller", "Valhalla") 82 | os.Exit(1) 83 | } 84 | //+kubebuilder:scaffold:builder 85 | 86 | if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { 87 | setupLog.Error(err, "unable to set up health check") 88 | os.Exit(1) 89 | } 90 | if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil { 91 | setupLog.Error(err, "unable to set up ready check") 92 | os.Exit(1) 93 | } 94 | 95 | setupLog.Info("starting manager") 96 | if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { 97 | setupLog.Error(err, "problem running manager") 98 | os.Exit(1) 99 | } 100 | } 101 | --------------------------------------------------------------------------------