├── .dockerignore ├── .github └── workflows │ └── test-coverage.yaml ├── .gitignore ├── Dockerfile ├── LICENSE ├── Makefile ├── PROJECT ├── README.md ├── api └── v1alpha1 │ ├── elastalert_types.go │ ├── freeform.go │ ├── freeform_test.go │ ├── groupversion_info.go │ ├── zz_generated.deepcopy.go │ └── zz_generated.deepcopy_test.go ├── config ├── crd │ ├── bases │ │ └── es.noah.domain_elastalerts.yaml │ ├── kustomization.yaml │ ├── kustomizeconfig.yaml │ └── patches │ │ ├── cainjection_in_elastalerts.yaml │ │ └── webhook_in_elastalerts.yaml ├── rbac │ ├── kustomization.yaml │ ├── leader_election_role.yaml │ ├── leader_election_role_binding.yaml │ ├── role.yaml │ ├── role_binding.yaml │ └── service_account.yaml └── samples │ ├── es_v1alpha1_elastalert.yaml │ └── kustomization.yaml ├── controllers ├── deployment_controller_test.go ├── deployment_controllers.go ├── elastalert_controller.go ├── elastalert_controller_test.go ├── event │ └── event.go ├── observer │ ├── observer.go │ └── observer_test.go ├── podspec │ ├── configmap.go │ ├── configmap_test.go │ ├── defaultattrs.go │ ├── defaulter.go │ ├── defaulter_test.go │ ├── deployment.go │ ├── deployment_test.go │ ├── maps.go │ ├── maps_test.go │ ├── podtemplate.go │ ├── secert_test.go │ └── secret.go └── test │ └── e2e │ ├── deploy_test.go │ └── suite_test.go ├── deploy ├── deployment.yaml ├── es.noah.domain_elastalerts.yaml ├── role.yaml ├── role_binding.yaml └── service_account.yaml ├── go.mod ├── go.sum ├── hack └── boilerplate.go.txt └── main.go /.dockerignore: -------------------------------------------------------------------------------- 1 | # More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file 2 | # Ignore all files which are not go type 3 | !**/*.go 4 | !**/*.mod 5 | !**/*.sum 6 | -------------------------------------------------------------------------------- /.github/workflows/test-coverage.yaml: -------------------------------------------------------------------------------- 1 | name: "CI Workflow" 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - name: Set up Go 17 | uses: actions/setup-go@v2 18 | with: 19 | go-version: 1.15 20 | 21 | - name: Test 22 | run: go test $(go list ./... | grep -v github.com/toughnoah/elastalert-operator/controllers/test/e2e) -race -coverprofile=coverage.txt -covermode=atomic -gcflags=-l 23 | - name: "upload test coverage report" 24 | env: 25 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 26 | run: bash <(curl -s https://codecov.io/bash) 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Binaries for programs and plugins 3 | *.exe 4 | *.exe~ 5 | *.dll 6 | *.so 7 | *.dylib 8 | bin 9 | testbin/* 10 | 11 | # Test binary, build with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Kubernetes Generated files - skip generated files, except for vendored files 18 | 19 | !vendor/**/zz_generated.* 20 | 21 | # editor and IDE paraphernalia 22 | .idea 23 | *.swp 24 | *.swo 25 | *.txt 26 | *.html 27 | *~ 28 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build the manager binary 2 | FROM golang:1.15 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 | 17 | # Build 18 | RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GO111MODULE=on go build -a -o manager main.go 19 | 20 | # Use distroless as minimal base image to package the manager binary 21 | # Refer to https://github.com/GoogleContainerTools/distroless for more details 22 | FROM gcr.io/distroless/static:nonroot 23 | WORKDIR / 24 | COPY --from=builder /workspace/manager . 25 | USER 65532:65532 26 | 27 | ENTRYPOINT ["/manager"] 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 toughnoah 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 ?= 1.0 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 = "preview,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=preview,fast,stable) 12 | # - use environment variables to overwrite this value (e.g export CHANNELS="preview,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 | # noah.domain/elastalert-operator-bundle:$VERSION and noah.domain/elastalert-operator-catalog:$VERSION. 32 | IMAGE_TAG_BASE ?= toughnoah/elastalert-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):v$(VERSION) 37 | 38 | # Image URL to use all building/pushing image targets 39 | IMG ?= toughnoah/elastalert-operator:v1.0 40 | # Produce CRDs that work back to Kubernetes 1.11 (no version conversion) 41 | CRD_OPTIONS ?= "crd:trivialVersions=true" 42 | 43 | # Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) 44 | ifeq (,$(shell go env GOBIN)) 45 | GOBIN=$(shell go env GOPATH)/bin 46 | else 47 | GOBIN=$(shell go env GOBIN) 48 | endif 49 | 50 | all: build 51 | 52 | ##@ General 53 | 54 | # The help target prints out all targets with their descriptions organized 55 | # beneath their categories. The categories are represented by '##@' and the 56 | # target descriptions by '##'. The awk commands is responsible for reading the 57 | # entire set of makefiles included in this invocation, looking for lines of the 58 | # file as xyz: ## something, and then pretty-format the target and help. Then, 59 | # if there's a line with ##@ something, that gets pretty-printed as a category. 60 | # More info on the usage of ANSI control characters for terminal formatting: 61 | # https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters 62 | # More info on the awk command: 63 | # http://linuxcommand.org/lc3_adv_awk.php 64 | 65 | help: ## Display this help. 66 | @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) 67 | 68 | ##@ Development 69 | 70 | manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. 71 | $(CONTROLLER_GEN) $(CRD_OPTIONS) rbac:roleName=manager-role webhook paths="./..." output:crd:artifacts:config=config/crd/bases 72 | 73 | generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. 74 | $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..." 75 | 76 | fmt: ## Run go fmt against code. 77 | go fmt ./... 78 | 79 | vet: ## Run go vet against code. 80 | go vet ./... 81 | 82 | ENVTEST_ASSETS_DIR=$(shell pwd)/testbin 83 | test: manifests generate fmt vet ## Run tests. 84 | mkdir -p ${ENVTEST_ASSETS_DIR} 85 | test -f ${ENVTEST_ASSETS_DIR}/setup-envtest.sh || curl -sSLo ${ENVTEST_ASSETS_DIR}/setup-envtest.sh https://raw.githubusercontent.com/kubernetes-sigs/controller-runtime/v0.7.2/hack/setup-envtest.sh 86 | source ${ENVTEST_ASSETS_DIR}/setup-envtest.sh; fetch_envtest_tools $(ENVTEST_ASSETS_DIR); setup_envtest_env $(ENVTEST_ASSETS_DIR); go test ./... -coverprofile cover.out 87 | 88 | ##@ Build 89 | 90 | build: generate fmt vet ## Build manager binary. 91 | go build -o bin/manager main.go 92 | 93 | run: manifests generate fmt vet ## Run a controller from your host. 94 | go run ./main.go 95 | 96 | docker-build: test ## Build docker image with the manager. 97 | docker build -t ${IMG} . 98 | 99 | docker-push: ## Push docker image with the manager. 100 | docker push ${IMG} 101 | 102 | ##@ Deployment 103 | 104 | install: manifests kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config. 105 | $(KUSTOMIZE) build config/crd | kubectl apply -f - 106 | 107 | uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified in ~/.kube/config. 108 | $(KUSTOMIZE) build config/crd | kubectl delete -f - 109 | 110 | deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config. 111 | cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} 112 | $(KUSTOMIZE) build config/default | kubectl apply -f - 113 | 114 | undeploy: ## Undeploy controller from the K8s cluster specified in ~/.kube/config. 115 | $(KUSTOMIZE) build config/default | kubectl delete -f - 116 | 117 | 118 | CONTROLLER_GEN = $(shell pwd)/bin/controller-gen 119 | controller-gen: ## Download controller-gen locally if necessary. 120 | $(call go-get-tool,$(CONTROLLER_GEN),sigs.k8s.io/controller-tools/cmd/controller-gen@v0.4.1) 121 | 122 | KUSTOMIZE = $(shell pwd)/bin/kustomize 123 | kustomize: ## Download kustomize locally if necessary. 124 | $(call go-get-tool,$(KUSTOMIZE),sigs.k8s.io/kustomize/kustomize/v3@v3.8.7) 125 | 126 | # go-get-tool will 'go get' any package $2 and install it to $1. 127 | PROJECT_DIR := $(shell dirname $(abspath $(lastword $(MAKEFILE_LIST)))) 128 | define go-get-tool 129 | @[ -f $(1) ] || { \ 130 | set -e ;\ 131 | TMP_DIR=$$(mktemp -d) ;\ 132 | cd $$TMP_DIR ;\ 133 | go mod init tmp ;\ 134 | echo "Downloading $(2)" ;\ 135 | GOBIN=$(PROJECT_DIR)/bin go get $(2) ;\ 136 | rm -rf $$TMP_DIR ;\ 137 | } 138 | endef 139 | 140 | .PHONY: bundle 141 | bundle: manifests kustomize ## Generate bundle manifests and metadata, then validate generated files. 142 | operator-sdk generate kustomize manifests -q 143 | cd config/manager && $(KUSTOMIZE) edit set image controller=$(IMG) 144 | $(KUSTOMIZE) build config/manifests | operator-sdk generate bundle -q --overwrite --version $(VERSION) $(BUNDLE_METADATA_OPTS) 145 | operator-sdk bundle validate ./bundle 146 | 147 | .PHONY: bundle-build 148 | bundle-build: ## Build the bundle image. 149 | docker build -f bundle.Dockerfile -t $(BUNDLE_IMG) . 150 | 151 | .PHONY: bundle-push 152 | bundle-push: ## Push the bundle image. 153 | $(MAKE) docker-push IMG=$(BUNDLE_IMG) 154 | 155 | .PHONY: opm 156 | OPM = ./bin/opm 157 | opm: ## Download opm locally if necessary. 158 | ifeq (,$(wildcard $(OPM))) 159 | ifeq (,$(shell which opm 2>/dev/null)) 160 | @{ \ 161 | set -e ;\ 162 | mkdir -p $(dir $(OPM)) ;\ 163 | OS=$(shell go env GOOS) && ARCH=$(shell go env GOARCH) && \ 164 | curl -sSLo $(OPM) https://github.com/operator-framework/operator-registry/releases/download/v1.15.1/$${OS}-$${ARCH}-opm ;\ 165 | chmod +x $(OPM) ;\ 166 | } 167 | else 168 | OPM = $(shell which opm) 169 | endif 170 | endif 171 | 172 | # 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). 173 | # These images MUST exist in a registry and be pull-able. 174 | BUNDLE_IMGS ?= $(BUNDLE_IMG) 175 | 176 | # The image tag given to the resulting catalog image (e.g. make catalog-build CATALOG_IMG=example.com/operator-catalog:v0.2.0). 177 | CATALOG_IMG ?= $(IMAGE_TAG_BASE)-catalog:v$(VERSION) 178 | 179 | # Set CATALOG_BASE_IMG to an existing catalog image tag to add $BUNDLE_IMGS to that image. 180 | ifneq ($(origin CATALOG_BASE_IMG), undefined) 181 | FROM_INDEX_OPT := --from-index $(CATALOG_BASE_IMG) 182 | endif 183 | 184 | # Build a catalog image by adding bundle images to an empty catalog using the operator package manager tool, 'opm'. 185 | # This recipe invokes 'opm' in 'semver' bundle add mode. For more information on add modes, see: 186 | # https://github.com/operator-framework/community-operators/blob/7f1438c/docs/packaging-operator.md#updating-your-existing-operator 187 | .PHONY: catalog-build 188 | catalog-build: opm ## Build a catalog image. 189 | $(OPM) index add --container-tool docker --mode semver --tag $(CATALOG_IMG) --bundles $(BUNDLE_IMGS) $(FROM_INDEX_OPT) 190 | 191 | # Push the catalog image. 192 | .PHONY: catalog-push 193 | catalog-push: ## Push a catalog image. 194 | $(MAKE) docker-push IMG=$(CATALOG_IMG) 195 | -------------------------------------------------------------------------------- /PROJECT: -------------------------------------------------------------------------------- 1 | domain: noah.domain 2 | layout: 3 | - go.kubebuilder.io/v3 4 | plugins: 5 | manifests.sdk.operatorframework.io/v2: {} 6 | scorecard.sdk.operatorframework.io/v2: {} 7 | projectName: elastalert-operator 8 | repo: elastalert 9 | resources: 10 | - api: 11 | crdVersion: v1 12 | namespaced: true 13 | controller: true 14 | domain: noah.domain 15 | group: es 16 | kind: Elastalert 17 | path: github.com/toughnoah/elastalert-operator/api/v1alpha1 18 | version: v1alpha1 19 | version: "3" 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Go 1.16](https://img.shields.io/badge/Go-v1.16-blue) 2 | [![codecov](https://codecov.io/gh/toughnoah/elastalert-operator/branch/master/graph/badge.svg?token=5B1DBTNIDN)](https://codecov.io/gh/toughnoah/elastalert-operator) [![CI Workflow](https://github.com/toughnoah/elastalert-operator/actions/workflows/test-coverage.yaml/badge.svg)](https://github.com/toughnoah/elastalert-operator/actions/workflows/test-coverage.yaml) 3 | [![Go Report Card](https://goreportcard.com/badge/github.com/toughnoah/elastalert-operator)](https://goreportcard.com/report/github.com/toughnoah/elastalert-operator) 4 | 5 | * 1. [Getting started](#Gettingstarted) 6 | * 2. [What's more](#Whatsmore) 7 | * 2.1. [Elasticsearch Cert](#ElasticsearchCert) 8 | * 2.2. [Overall](#Overall) 9 | * 2.3. [Pod Template](#PodTemplate) 10 | * 2.4. [Build Your Own Elastalert Dockerfile.](#BuildYourOwnElastalertDockerfile.) 11 | * 2.5. [Notice](#Notice) 12 | * 3. [Contact Me](#ContactMe) 13 | 14 | 18 | 19 | # Elastalert Operator for Kubernetes 20 | 21 | The Elastalert Operator is an implementation of a [Kubernetes Operator](https://kubernetes.io/docs/concepts/extend-kubernetes/operator/). 22 | 23 | ## 1. Getting started 24 | 25 | Firstly, learn [How to use elastalert](https://elastalert.readthedocs.io/en/latest/), exactly how to setup a `config.yaml` and `rule`. 26 | The default command to start elastalert container is `elastalert --config /etc/elastalert/config.yaml --verbose`. 27 | 28 | To install the operator, run: 29 | ``` 30 | kubectl create namespace alert 31 | kubectl create -n alert -f https://raw.githubusercontent.com/toughnoah/elastalert-operator/master/deploy/es.noah.domain_elastalerts.yaml 32 | kubectl create -n alert -f https://raw.githubusercontent.com/toughnoah/elastalert-operator/master/deploy/role.yaml 33 | kubectl create -n alert -f https://raw.githubusercontent.com/toughnoah/elastalert-operator/master/deploy/role_binding.yaml 34 | kubectl create -n alert -f https://raw.githubusercontent.com/toughnoah/elastalert-operator/master/deploy/service_account.yaml 35 | kubectl create -n alert -f https://raw.githubusercontent.com/toughnoah/elastalert-operator/master/deploy/deployment.yaml 36 | ``` 37 | 38 | Args for Operator: 39 | ```console 40 | # /manager -h 41 | -health-probe-bind-address string 42 | The address the probe endpoint binds to. (default ":8081") 43 | 44 | -leader-elect bool 45 | Enable leader election for controller manager. Enabling this will ensure there is only one active controller manager. (default false) 46 | 47 | -metrics-bind-address string 48 | The address the metric endpoint binds to. (default ":8080") 49 | 50 | -zap-log-level string 51 | Zap Level to configure the verbosity of logging. Can be one of debug, info, error. (default info) 52 | ``` 53 | 54 | 55 | 56 | 57 | Once the `elastalert-operator` deployment in the namespace `alert` is ready, create an Elastalert instance in any namespace, like: 58 | 59 | ``` 60 | kubectl apply -n alert -f - <What's more 137 | ### 2.1. Elasticsearch Cert 138 | ``` 139 | kubectl apply -n alert -f - <Overall 187 | `overall` is used to config global alert settings. If you defined `alert` in a rule, it will override `overall` settings. 188 | ``` 189 | kubectl apply -n alert -f - <Pod Template 216 | Define customized podTemplate 217 | ``` 218 | kubectl apply -n alert -f - <Build Your Own Elastalert Dockerfile. 249 | 250 | ~~~ 251 | FROM docker.io/library/python:3.7.4-alpine 252 | 253 | ENV TZ=Asia/Shanghai 254 | 255 | ENV CRYPTOGRAPHY_DONT_BUILD_RUST=1 256 | 257 | RUN apk add gcc g++ make libffi-dev openssl-dev 258 | 259 | RUN apk add --no-cache tzdata 260 | 261 | RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone 262 | 263 | RUN pip install --upgrade pip 264 | 265 | RUN pip install elastalert 266 | 267 | ENTRYPOINT ["elastalert", "--config", "/etc/elastalert/config.yaml", "--verbose"] 268 | 269 | ~~~ 270 | 271 | ### 2.5. Notice 272 | You don't have to specify `rules_folder` in config section, because operator will auto patch `rules_folder: /etc/elastalert/rules/..data/` for your config. 273 | The reason why have to be `..data/` is the workaround when the configmap is mounted as file(such as `/etc/elastalert/rules/test.yaml`) in a pod, it will create a soft-link to `/etc/elastalert/rules/..data/test.yaml`. 274 | That is to say, you will receive duplicated rules name error that both files in `rules` and `..data` would be loaded if you specify merely `rules_folder: /etc/elastalert/rules` 275 | 276 | ## 3. Contact Me 277 | Any advice is welcome! Please email to toughnoah@163.com 278 | -------------------------------------------------------------------------------- /api/v1alpha1/elastalert_types.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright 2021. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | 18 | package v1alpha1 19 | 20 | import ( 21 | v1 "k8s.io/api/core/v1" 22 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 23 | "time" 24 | ) 25 | 26 | const ( 27 | // +k8s:openapi-gen=true 28 | ElastAlertPhraseFailed = "FAILED" 29 | 30 | ElastAlertInitializing = "INITIALIZING" 31 | // +k8s:openapi-gen=true 32 | ElastAlertPhraseSucceeded = "RUNNING" 33 | 34 | ElastAlertAvailableReason = "NewElastAlertAvailable" 35 | 36 | ElastAlertAvailableType = "Progressing" 37 | 38 | ElastAlertAvailableStatus = "True" 39 | 40 | ElastAlertUnAvailableReason = "ElastAlertUnAvailable" 41 | 42 | ElastAlertUnKnowReason = "ResourcesCreating" 43 | 44 | ElastAlertUnAvailableType = "Stopped" 45 | 46 | ElastAlertUnAvailableStatus = "False" 47 | 48 | ElastAlertUnKnownStatus = "Unknown" 49 | 50 | ResourcesCreating = "starting" 51 | 52 | ActionSuccess = "success" 53 | 54 | ActionFailed = "failed" 55 | 56 | ElastAlertVersion = "v1.0" 57 | 58 | ConfigSuffx = "-config" 59 | 60 | RuleSuffx = "-rule" 61 | 62 | RuleMountPath = "/etc/elastalert/rules" 63 | 64 | ConfigMountPath = "/etc/elastalert" 65 | 66 | ElastAlertObserveInterval = time.Minute 67 | 68 | ElastAlertPollInterval = time.Second * 5 69 | ) 70 | 71 | // EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! 72 | // NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. 73 | 74 | // ElastalertSpec defines the desired state of Elastalert 75 | // +k8s:openapi-gen=true 76 | type ElastalertSpec struct { 77 | // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster 78 | // Important: Run "make" to regenerate code after modifying this file 79 | PodTemplateSpec v1.PodTemplateSpec `json:"podTemplate,omitempty"` 80 | Image string `json:"image,omitempty"` 81 | Cert string `json:"cert,omitempty"` 82 | 83 | ConfigSetting FreeForm `json:"config"` 84 | Rule []FreeForm `json:"rule"` 85 | // +optional 86 | Alert FreeForm `json:"overall,omitempty"` 87 | } 88 | 89 | // +k8s:openapi-gen=true 90 | // ElastalertStatus defines the observed state of Elastalert 91 | type ElastalertStatus struct { 92 | Version string `json:"version,omitempty"` 93 | Phase string `json:"phase,omitempty"` 94 | Condictions []metav1.Condition `json:"conditions,omitempty"` 95 | // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster 96 | // Important: Run "make" to regenerate code after modifying this file 97 | } 98 | 99 | // +k8s:openapi-gen=true 100 | // +operator-sdk:gen-csv:customresourcedefinitions.displayName="Elastalert" 101 | // +kubebuilder:object:root=true 102 | // +kubebuilder:subresource:status 103 | // +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.phase",description="Elastalert instance's status" 104 | // +kubebuilder:printcolumn:name="Version",type="string",JSONPath=".status.version",description="Elastalert Version" 105 | // +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" 106 | // Elastalert is the Schema for the elastalerts API 107 | type Elastalert struct { 108 | metav1.TypeMeta `json:",inline"` 109 | metav1.ObjectMeta `json:"metadata,omitempty"` 110 | 111 | Spec ElastalertSpec `json:"spec,omitempty"` 112 | Status ElastalertStatus `json:"status,omitempty"` 113 | } 114 | 115 | //+kubebuilder:object:root=true 116 | 117 | // ElastalertList contains a list of Elastalert 118 | type ElastalertList struct { 119 | metav1.TypeMeta `json:",inline"` 120 | metav1.ListMeta `json:"metadata,omitempty"` 121 | Items []Elastalert `json:"items"` 122 | } 123 | 124 | func init() { 125 | SchemeBuilder.Register(&Elastalert{}, &ElastalertList{}) 126 | } 127 | -------------------------------------------------------------------------------- /api/v1alpha1/freeform.go: -------------------------------------------------------------------------------- 1 | package v1alpha1 2 | 3 | import ( 4 | "encoding/json" 5 | ) 6 | 7 | // FreeForm defines a common options parameter that maintains the hierarchical structure of the data, unlike Options which flattens the hierarchy into a key/value map where the hierarchy is converted to '.' separated items in the key. 8 | //+kubebuilder:pruning:PreserveUnknownFields 9 | type FreeForm struct { 10 | json *[]byte `json:"-"` 11 | } 12 | 13 | // NewFreeForm build a new FreeForm object based on the given map 14 | func NewFreeForm(o map[string]interface{}) FreeForm { 15 | freeForm := FreeForm{} 16 | if o != nil { 17 | j, _ := json.Marshal(o) 18 | freeForm.json = &j 19 | } 20 | return freeForm 21 | } 22 | 23 | // UnmarshalJSON implements an alternative parser for this field 24 | func (o *FreeForm) UnmarshalJSON(b []byte) error { 25 | o.json = &b 26 | return nil 27 | } 28 | 29 | // MarshalJSON specifies how to convert this object into JSON 30 | func (o FreeForm) MarshalJSON() ([]byte, error) { 31 | if nil == o.json { 32 | return []byte("{}"), nil 33 | } 34 | if len(*o.json) == 0 { 35 | return []byte("{}"), nil 36 | } 37 | return *o.json, nil 38 | } 39 | 40 | // IsEmpty determines if the freeform options are empty 41 | func (o FreeForm) IsEmpty() bool { 42 | if nil == o.json { 43 | return true 44 | } 45 | return len(*o.json) == 0 || string(*o.json) == "{}" 46 | } 47 | 48 | // GetMap returns a map created from json 49 | func (o FreeForm) GetMap() (map[string]interface{}, error) { 50 | m := map[string]interface{}{} 51 | if nil == o.json { 52 | return m, nil 53 | } 54 | 55 | if err := json.Unmarshal(*o.json, &m); err != nil { 56 | return nil, err 57 | } 58 | return m, nil 59 | } 60 | 61 | func (o FreeForm) GetStringMap() (map[string]string, error) { 62 | m := map[string]string{} 63 | if nil == o.json { 64 | return m, nil 65 | } 66 | 67 | if err := json.Unmarshal(*o.json, &m); err != nil { 68 | return nil, err 69 | } 70 | return m, nil 71 | } 72 | -------------------------------------------------------------------------------- /api/v1alpha1/freeform_test.go: -------------------------------------------------------------------------------- 1 | package v1alpha1 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestFreeForm(t *testing.T) { 10 | uiconfig := `{"es":{"password":"changeme","server-urls":"http://elasticsearch:9200","username":"elastic"}}` 11 | o := NewFreeForm(map[string]interface{}{ 12 | "es": map[string]interface{}{ 13 | "server-urls": "http://elasticsearch:9200", 14 | "username": "elastic", 15 | "password": "changeme", 16 | }, 17 | }) 18 | json, err := o.MarshalJSON() 19 | assert.NoError(t, err) 20 | assert.NotNil(t, json) 21 | assert.Equal(t, uiconfig, string(*o.json)) 22 | } 23 | 24 | func TestFreeFormUnmarshalMarshal(t *testing.T) { 25 | uiconfig := `{"es":{"password":"changeme","server-urls":"http://elasticsearch:9200","username":"elastic"}}` 26 | o := NewFreeForm(nil) 27 | _ = o.UnmarshalJSON([]byte(uiconfig)) 28 | json, err := o.MarshalJSON() 29 | assert.NoError(t, err) 30 | assert.NotNil(t, json) 31 | assert.Equal(t, uiconfig, string(*o.json)) 32 | } 33 | 34 | func TestFreeFormListUnmarshalMarshal(t *testing.T) { 35 | testconfig := `[{"es":{"password":"changeme","server-urls":"http://elasticsearch:9200","username":"elastic"}},{"es2":{"password":"changeme","server-urls":"http://elasticsearch:9200","username":"elastic"}}]` 36 | o := NewFreeForm(nil) 37 | _ = o.UnmarshalJSON([]byte(testconfig)) 38 | json, err := o.MarshalJSON() 39 | assert.NoError(t, err) 40 | assert.NotNil(t, json) 41 | assert.Equal(t, testconfig, string(*o.json)) 42 | } 43 | 44 | func TestFreeFormNilUnmarshalMarshal(t *testing.T) { 45 | testconfig := `` 46 | o := NewFreeForm(nil) 47 | _ = o.UnmarshalJSON([]byte(testconfig)) 48 | json, err := o.MarshalJSON() 49 | assert.NoError(t, err) 50 | assert.NotNil(t, json) 51 | assert.Equal(t, testconfig, string(*o.json)) 52 | } 53 | 54 | func TestFreeFormIsEmptyFalse(t *testing.T) { 55 | o := NewFreeForm(map[string]interface{}{ 56 | "es": map[string]interface{}{ 57 | "server-urls": "http://elasticsearch:9200", 58 | "username": "elastic", 59 | "password": "changeme", 60 | }, 61 | }) 62 | assert.False(t, o.IsEmpty()) 63 | } 64 | 65 | func TestFreeFormIsEmptyTrue(t *testing.T) { 66 | o := NewFreeForm(map[string]interface{}{}) 67 | assert.True(t, o.IsEmpty()) 68 | } 69 | 70 | func TestFreeFormIsEmptyNilTrue(t *testing.T) { 71 | o := NewFreeForm(nil) 72 | assert.True(t, o.IsEmpty()) 73 | } 74 | 75 | func TestToMap(t *testing.T) { 76 | tests := []struct { 77 | m map[string]interface{} 78 | expected map[string]interface{} 79 | err string 80 | }{ 81 | {expected: map[string]interface{}{}}, 82 | {m: map[string]interface{}{"foo": "bar$"}, expected: map[string]interface{}{"foo": "bar$"}}, 83 | {m: map[string]interface{}{"foo": true}, expected: map[string]interface{}{"foo": true}}, 84 | } 85 | for _, test := range tests { 86 | f := NewFreeForm(test.m) 87 | got, err := f.GetMap() 88 | if test.err != "" { 89 | assert.EqualError(t, err, test.err) 90 | } else { 91 | assert.NoError(t, err) 92 | assert.Equal(t, test.expected, got) 93 | } 94 | } 95 | } 96 | 97 | func TestFreeForm_GetStringMap(t *testing.T) { 98 | tests := []struct { 99 | m map[string]interface{} 100 | expected map[string]string 101 | err string 102 | }{ 103 | {expected: map[string]string{}}, 104 | {m: map[string]interface{}{"foo": "bar$"}, expected: map[string]string{"foo": "bar$"}}, 105 | {m: map[string]interface{}{"foo": "true"}, expected: map[string]string{"foo": "true"}}, 106 | } 107 | for _, test := range tests { 108 | f := NewFreeForm(test.m) 109 | got, err := f.GetStringMap() 110 | if test.err != "" { 111 | assert.EqualError(t, err, test.err) 112 | } else { 113 | assert.NoError(t, err) 114 | assert.Equal(t, test.expected, got) 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /api/v1alpha1/groupversion_info.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021. 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 es v1alpha1 API group 18 | //+kubebuilder:object:generate=true 19 | //+groupName=es.noah.domain 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: "es.noah.domain", 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/zz_generated.deepcopy.go: -------------------------------------------------------------------------------- 1 | // +build !ignore_autogenerated 2 | 3 | /* 4 | Copyright 2021. 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | */ 18 | 19 | // Code generated by controller-gen. DO NOT EDIT. 20 | 21 | package v1alpha1 22 | 23 | import ( 24 | "k8s.io/apimachinery/pkg/apis/meta/v1" 25 | runtime "k8s.io/apimachinery/pkg/runtime" 26 | ) 27 | 28 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 29 | func (in *Elastalert) DeepCopyInto(out *Elastalert) { 30 | *out = *in 31 | out.TypeMeta = in.TypeMeta 32 | in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) 33 | in.Spec.DeepCopyInto(&out.Spec) 34 | in.Status.DeepCopyInto(&out.Status) 35 | } 36 | 37 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Elastalert. 38 | func (in *Elastalert) DeepCopy() *Elastalert { 39 | if in == nil { 40 | return nil 41 | } 42 | out := new(Elastalert) 43 | in.DeepCopyInto(out) 44 | return out 45 | } 46 | 47 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 48 | func (in *Elastalert) DeepCopyObject() runtime.Object { 49 | if c := in.DeepCopy(); c != nil { 50 | return c 51 | } 52 | return nil 53 | } 54 | 55 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 56 | func (in *ElastalertList) DeepCopyInto(out *ElastalertList) { 57 | *out = *in 58 | out.TypeMeta = in.TypeMeta 59 | in.ListMeta.DeepCopyInto(&out.ListMeta) 60 | if in.Items != nil { 61 | in, out := &in.Items, &out.Items 62 | *out = make([]Elastalert, len(*in)) 63 | for i := range *in { 64 | (*in)[i].DeepCopyInto(&(*out)[i]) 65 | } 66 | } 67 | } 68 | 69 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ElastalertList. 70 | func (in *ElastalertList) DeepCopy() *ElastalertList { 71 | if in == nil { 72 | return nil 73 | } 74 | out := new(ElastalertList) 75 | in.DeepCopyInto(out) 76 | return out 77 | } 78 | 79 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 80 | func (in *ElastalertList) DeepCopyObject() runtime.Object { 81 | if c := in.DeepCopy(); c != nil { 82 | return c 83 | } 84 | return nil 85 | } 86 | 87 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 88 | func (in *ElastalertSpec) DeepCopyInto(out *ElastalertSpec) { 89 | *out = *in 90 | in.PodTemplateSpec.DeepCopyInto(&out.PodTemplateSpec) 91 | in.ConfigSetting.DeepCopyInto(&out.ConfigSetting) 92 | if in.Rule != nil { 93 | in, out := &in.Rule, &out.Rule 94 | *out = make([]FreeForm, len(*in)) 95 | for i := range *in { 96 | (*in)[i].DeepCopyInto(&(*out)[i]) 97 | } 98 | } 99 | in.Alert.DeepCopyInto(&out.Alert) 100 | } 101 | 102 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ElastalertSpec. 103 | func (in *ElastalertSpec) DeepCopy() *ElastalertSpec { 104 | if in == nil { 105 | return nil 106 | } 107 | out := new(ElastalertSpec) 108 | in.DeepCopyInto(out) 109 | return out 110 | } 111 | 112 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 113 | func (in *ElastalertStatus) DeepCopyInto(out *ElastalertStatus) { 114 | *out = *in 115 | if in.Condictions != nil { 116 | in, out := &in.Condictions, &out.Condictions 117 | *out = make([]v1.Condition, len(*in)) 118 | for i := range *in { 119 | (*in)[i].DeepCopyInto(&(*out)[i]) 120 | } 121 | } 122 | } 123 | 124 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ElastalertStatus. 125 | func (in *ElastalertStatus) DeepCopy() *ElastalertStatus { 126 | if in == nil { 127 | return nil 128 | } 129 | out := new(ElastalertStatus) 130 | in.DeepCopyInto(out) 131 | return out 132 | } 133 | 134 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 135 | func (in *FreeForm) DeepCopyInto(out *FreeForm) { 136 | *out = *in 137 | if in.json != nil { 138 | in, out := &in.json, &out.json 139 | *out = new([]byte) 140 | if **in != nil { 141 | in, out := *in, *out 142 | *out = make([]byte, len(*in)) 143 | copy(*out, *in) 144 | } 145 | } 146 | } 147 | 148 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FreeForm. 149 | func (in *FreeForm) DeepCopy() *FreeForm { 150 | if in == nil { 151 | return nil 152 | } 153 | out := new(FreeForm) 154 | in.DeepCopyInto(out) 155 | return out 156 | } 157 | -------------------------------------------------------------------------------- /api/v1alpha1/zz_generated.deepcopy_test.go: -------------------------------------------------------------------------------- 1 | package v1alpha1 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | v1 "k8s.io/api/core/v1" 6 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 7 | "testing" 8 | ) 9 | 10 | func TestFreeForm_DeepCopy(t *testing.T) { 11 | nf := NewFreeForm(map[string]interface{}{ 12 | "test": 1, 13 | }) 14 | newnf := nf.DeepCopy() 15 | assert.Equal(t, *newnf, nf) 16 | } 17 | 18 | func TestElastalert_DeepCopy(t *testing.T) { 19 | ea := &Elastalert{ 20 | ObjectMeta: metav1.ObjectMeta{ 21 | Name: "test", 22 | }, 23 | } 24 | newea := ea.DeepCopy() 25 | assert.Equal(t, newea, ea) 26 | } 27 | 28 | func TestElastalert_DeepCopyObject(t *testing.T) { 29 | ea := &Elastalert{ 30 | ObjectMeta: metav1.ObjectMeta{ 31 | Name: "test", 32 | }, 33 | } 34 | newea := ea.DeepCopyObject() 35 | assert.Equal(t, newea, ea) 36 | } 37 | 38 | func TestElastalertSpec_DeepCopy(t *testing.T) { 39 | ea := &ElastalertSpec{ 40 | PodTemplateSpec: v1.PodTemplateSpec{ 41 | ObjectMeta: metav1.ObjectMeta{ 42 | Name: "test", 43 | }, 44 | }, 45 | Rule: []FreeForm{ 46 | NewFreeForm(map[string]interface{}{ 47 | "test": true, 48 | }), 49 | }, 50 | } 51 | newea := ea.DeepCopy() 52 | assert.Equal(t, newea, ea) 53 | } 54 | 55 | func TestElastalertStatus_DeepCopy(t *testing.T) { 56 | ea := &ElastalertStatus{ 57 | Version: "v1.0", 58 | Phase: "RUNNIG", 59 | } 60 | newea := ea.DeepCopy() 61 | assert.Equal(t, newea, ea) 62 | } 63 | 64 | func TestElastalertList_DeepCopy(t *testing.T) { 65 | ea := &ElastalertList{ 66 | Items: []Elastalert{ 67 | { 68 | ObjectMeta: metav1.ObjectMeta{ 69 | Name: "test", 70 | }, 71 | }, 72 | }, 73 | } 74 | newea := ea.DeepCopy() 75 | assert.Equal(t, newea, ea) 76 | } 77 | func TestElastalertList_DeepCopyObject(t *testing.T) { 78 | ea := &ElastalertList{ 79 | Items: []Elastalert{ 80 | { 81 | ObjectMeta: metav1.ObjectMeta{ 82 | Name: "test", 83 | }, 84 | }, 85 | }, 86 | } 87 | newea := ea.DeepCopyObject() 88 | assert.Equal(t, newea, ea) 89 | } 90 | 91 | func TestFreeForm_DeepCopyInTo(t *testing.T) { 92 | nf := NewFreeForm(map[string]interface{}{ 93 | "test": 1, 94 | }) 95 | newnf := new(FreeForm) 96 | nf.DeepCopyInto(newnf) 97 | assert.Equal(t, *newnf, nf) 98 | } 99 | func TestElastalert_DeepCopyInTo(t *testing.T) { 100 | ea := &Elastalert{ 101 | ObjectMeta: metav1.ObjectMeta{ 102 | Name: "test", 103 | }, 104 | Status: ElastalertStatus{ 105 | Condictions: []metav1.Condition{ 106 | { 107 | Type: "test", 108 | }, 109 | }, 110 | }, 111 | } 112 | newea := new(Elastalert) 113 | ea.DeepCopyInto(newea) 114 | assert.Equal(t, newea, ea) 115 | } 116 | -------------------------------------------------------------------------------- /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/es.noah.domain_elastalerts.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_elastalerts.yaml 12 | #+kubebuilder:scaffold:crdkustomizewebhookpatch 13 | 14 | # [CERTMANAGER] To enable webhook, uncomment all the sections with [CERTMANAGER] prefix. 15 | # patches here are for enabling the CA injection for each CRD 16 | #- patches/cainjection_in_elastalerts.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_elastalerts.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: elastalerts.es.noah.domain 8 | -------------------------------------------------------------------------------- /config/crd/patches/webhook_in_elastalerts.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: elastalerts.es.noah.domain 6 | spec: 7 | conversion: 8 | strategy: Webhook 9 | webhook: 10 | clientConfig: 11 | service: 12 | namespace: system 13 | name: webhook-service 14 | path: /convert 15 | -------------------------------------------------------------------------------- /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 | 13 | -------------------------------------------------------------------------------- /config/rbac/leader_election_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions to do leader election. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: Role 4 | metadata: 5 | name: leader-election-role 6 | rules: 7 | - apiGroups: 8 | - "" 9 | - coordination.k8s.io 10 | resources: 11 | - configmaps 12 | - leases 13 | verbs: 14 | - get 15 | - list 16 | - watch 17 | - create 18 | - update 19 | - patch 20 | - delete 21 | - apiGroups: 22 | - "" 23 | resources: 24 | - events 25 | verbs: 26 | - create 27 | - patch 28 | -------------------------------------------------------------------------------- /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 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: elastalert-operator 5 | rules: 6 | - apiGroups: 7 | - es.noah.domain 8 | resources: 9 | - elastalerts 10 | verbs: 11 | - create 12 | - delete 13 | - get 14 | - list 15 | - patch 16 | - update 17 | - watch 18 | - apiGroups: 19 | - es.noah.domain 20 | resources: 21 | - elastalerts/finalizers 22 | verbs: 23 | - update 24 | - apiGroups: 25 | - es.noah.domain 26 | resources: 27 | - elastalerts/status 28 | verbs: 29 | - get 30 | - patch 31 | - update 32 | - apiGroups: 33 | - "" 34 | resources: 35 | - pods 36 | - secrets 37 | - configmaps 38 | verbs: 39 | - get 40 | - list 41 | - watch 42 | - create 43 | - update 44 | - patch 45 | - delete 46 | - apiGroups: 47 | - apps 48 | resources: 49 | - deployments 50 | verbs: 51 | - get 52 | - list 53 | - watch 54 | - create 55 | - update 56 | - patch 57 | - delete 58 | -------------------------------------------------------------------------------- /config/rbac/role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | name: elastalert-operator 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: ClusterRole 8 | name: elastalert-operator 9 | subjects: 10 | - kind: ServiceAccount 11 | name: elastalert-operator 12 | namespace: qa-paas-sre 13 | -------------------------------------------------------------------------------- /config/rbac/service_account.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: elastalert-operator 5 | namespace: qa-paas-sre 6 | -------------------------------------------------------------------------------- /config/samples/es_v1alpha1_elastalert.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: es.noah.domain/v1alpha1 2 | kind: Elastalert 3 | metadata: 4 | name: elastalert-sample 5 | spec: 6 | # Add fields here 7 | foo: bar 8 | -------------------------------------------------------------------------------- /config/samples/kustomization.yaml: -------------------------------------------------------------------------------- 1 | ## Append samples you want in your CSV to this file as resources ## 2 | resources: 3 | - es_v1alpha1_elastalert.yaml 4 | #+kubebuilder:scaffold:manifestskustomizesamples 5 | -------------------------------------------------------------------------------- /controllers/deployment_controller_test.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "github.com/bouk/monkey" 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | "github.com/toughnoah/elastalert-operator/api/v1alpha1" 10 | "github.com/toughnoah/elastalert-operator/controllers/observer" 11 | "github.com/toughnoah/elastalert-operator/controllers/podspec" 12 | appsv1 "k8s.io/api/apps/v1" 13 | corev1 "k8s.io/api/core/v1" 14 | k8serrors "k8s.io/apimachinery/pkg/api/errors" 15 | "k8s.io/apimachinery/pkg/api/meta" 16 | "k8s.io/apimachinery/pkg/api/resource" 17 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 18 | "k8s.io/apimachinery/pkg/runtime" 19 | "k8s.io/apimachinery/pkg/types" 20 | "k8s.io/client-go/kubernetes/scheme" 21 | "sigs.k8s.io/controller-runtime/pkg/client" 22 | "sigs.k8s.io/controller-runtime/pkg/client/fake" 23 | "sigs.k8s.io/controller-runtime/pkg/reconcile" 24 | "testing" 25 | ) 26 | 27 | func TestReCreateDeployment(t *testing.T) { 28 | testCases := []struct { 29 | desc string 30 | elastalert v1alpha1.Elastalert 31 | }{ 32 | { 33 | desc: "test recreate deployment", 34 | elastalert: v1alpha1.Elastalert{ 35 | ObjectMeta: metav1.ObjectMeta{ 36 | Namespace: "esa1", 37 | Name: "my-esa", 38 | }, 39 | Spec: v1alpha1.ElastalertSpec{ 40 | Cert: "abc", 41 | }, 42 | }, 43 | }, 44 | } 45 | for _, tc := range testCases { 46 | t.Run(tc.desc, func(t *testing.T) { 47 | s := scheme.Scheme 48 | cl := fake.NewClientBuilder().Build() 49 | r := &ElastalertReconciler{ 50 | Client: cl, 51 | Scheme: s, 52 | } 53 | monkey.Patch(podspec.GetUtcTimeString, func() string { 54 | return "2021-05-17T01:38:44+08:00" 55 | }) 56 | dep := appsv1.Deployment{} 57 | r.Scheme.AddKnownTypes(corev1.SchemeGroupVersion, &v1alpha1.Elastalert{}) 58 | r.Scheme.AddKnownTypes(appsv1.SchemeGroupVersion, &dep) 59 | _, err := recreateDeployment(cl, r.Scheme, context.Background(), &tc.elastalert) 60 | assert.NoError(t, err) 61 | err = cl.Get(context.Background(), types.NamespacedName{ 62 | Namespace: tc.elastalert.Namespace, 63 | Name: tc.elastalert.Name, 64 | }, &dep) 65 | require.NoError(t, err) 66 | }) 67 | } 68 | 69 | } 70 | 71 | func TestDeploymentReconcile(t *testing.T) { 72 | s := scheme.Scheme 73 | s.AddKnownTypes(corev1.SchemeGroupVersion, &v1alpha1.Elastalert{}) 74 | testCases := []struct { 75 | desc string 76 | c client.Client 77 | testNotfound bool 78 | want appsv1.Deployment 79 | }{ 80 | { 81 | desc: "test deployment reconcile", 82 | c: fake.NewClientBuilder().WithRuntimeObjects( 83 | &v1alpha1.Elastalert{ 84 | ObjectMeta: metav1.ObjectMeta{ 85 | Namespace: "test", 86 | Name: "test-elastalert", 87 | }, 88 | Spec: v1alpha1.ElastalertSpec{ 89 | PodTemplateSpec: corev1.PodTemplateSpec{ 90 | Spec: corev1.PodSpec{ 91 | Containers: []corev1.Container{ 92 | { 93 | Name: "elastalert", 94 | }, 95 | }, 96 | }, 97 | }, 98 | }, 99 | }).Build(), 100 | want: appsv1.Deployment{ 101 | TypeMeta: metav1.TypeMeta{ 102 | Kind: "Deployment", 103 | APIVersion: "apps/v1", 104 | }, 105 | ObjectMeta: metav1.ObjectMeta{ 106 | ResourceVersion: "1", 107 | Namespace: "test", 108 | Name: "test-elastalert", 109 | OwnerReferences: []metav1.OwnerReference{ 110 | { 111 | APIVersion: "v1", 112 | Kind: "Elastalert", 113 | Name: "test-elastalert", 114 | UID: "", 115 | Controller: &varTrue, 116 | BlockOwnerDeletion: &varTrue, 117 | }, 118 | }, 119 | }, 120 | Spec: appsv1.DeploymentSpec{ 121 | Replicas: &Replicas, 122 | Selector: &metav1.LabelSelector{ 123 | MatchLabels: map[string]string{"app": "elastalert"}, 124 | }, 125 | Template: corev1.PodTemplateSpec{ 126 | ObjectMeta: metav1.ObjectMeta{ 127 | Labels: map[string]string{ 128 | "app": "elastalert", 129 | }, 130 | Annotations: map[string]string{ 131 | "kubectl.kubernetes.io/restartedAt": "2021-05-17T01:38:44+08:00", 132 | }, 133 | }, 134 | 135 | Spec: corev1.PodSpec{ 136 | AutomountServiceAccountToken: &varTrue, 137 | TerminationGracePeriodSeconds: &TerminationGracePeriodSeconds, 138 | Containers: []corev1.Container{ 139 | { 140 | Name: "elastalert", 141 | Image: "toughnoah/elastalert:v1.0", 142 | VolumeMounts: []corev1.VolumeMount{ 143 | // have to keep sequence 144 | { 145 | Name: "elasticsearch-cert", 146 | MountPath: "/ssl", 147 | }, 148 | { 149 | Name: "test-elastalert-config", 150 | MountPath: "/etc/elastalert", 151 | }, 152 | { 153 | Name: "test-elastalert-rule", 154 | MountPath: "/etc/elastalert/rules", 155 | }, 156 | }, 157 | Command: []string{"elastalert", "--config", "/etc/elastalert/config.yaml", "--verbose"}, 158 | Resources: corev1.ResourceRequirements{ 159 | Requests: map[corev1.ResourceName]resource.Quantity{ 160 | corev1.ResourceMemory: resource.MustParse("2Gi"), 161 | }, 162 | Limits: map[corev1.ResourceName]resource.Quantity{ 163 | corev1.ResourceMemory: resource.MustParse("2Gi"), 164 | }, 165 | }, 166 | Ports: []corev1.ContainerPort{ 167 | {Name: "http", ContainerPort: 8080, Protocol: corev1.ProtocolTCP}, 168 | }, 169 | ReadinessProbe: &corev1.Probe{ 170 | Handler: corev1.Handler{ 171 | Exec: &corev1.ExecAction{ 172 | Command: []string{ 173 | "cat", 174 | "/etc/elastalert/config.yaml", 175 | }, 176 | }, 177 | }, 178 | InitialDelaySeconds: 20, 179 | TimeoutSeconds: 3, 180 | PeriodSeconds: 2, 181 | SuccessThreshold: 5, 182 | FailureThreshold: 3, 183 | }, 184 | LivenessProbe: &corev1.Probe{ 185 | Handler: corev1.Handler{ 186 | Exec: &corev1.ExecAction{ 187 | Command: []string{ 188 | "sh", 189 | "-c", 190 | "ps -ef|grep -v grep|grep elastalert", 191 | }, 192 | }, 193 | }, 194 | InitialDelaySeconds: 50, 195 | TimeoutSeconds: 3, 196 | PeriodSeconds: 2, 197 | SuccessThreshold: 1, 198 | FailureThreshold: 3, 199 | }, 200 | }, 201 | }, 202 | Volumes: []corev1.Volume{ 203 | // have to keep sequence 204 | { 205 | Name: "elasticsearch-cert", 206 | VolumeSource: corev1.VolumeSource{ 207 | Secret: &corev1.SecretVolumeSource{ 208 | SecretName: "test-elastalert-es-cert", 209 | }, 210 | }, 211 | }, 212 | { 213 | Name: "test-elastalert-config", 214 | VolumeSource: corev1.VolumeSource{ 215 | ConfigMap: &corev1.ConfigMapVolumeSource{ 216 | LocalObjectReference: corev1.LocalObjectReference{ 217 | Name: "test-elastalert-config", 218 | }, 219 | }, 220 | }, 221 | }, 222 | { 223 | Name: "test-elastalert-rule", 224 | VolumeSource: corev1.VolumeSource{ 225 | ConfigMap: &corev1.ConfigMapVolumeSource{ 226 | LocalObjectReference: corev1.LocalObjectReference{ 227 | Name: "test-elastalert-rule", 228 | }, 229 | }, 230 | }, 231 | }, 232 | }, 233 | Affinity: &corev1.Affinity{ 234 | PodAntiAffinity: &corev1.PodAntiAffinity{}, 235 | }, 236 | }, 237 | }, 238 | }, 239 | }, 240 | }, 241 | { 242 | desc: "test deployment reconcile 1", 243 | c: fake.NewClientBuilder().Build(), 244 | testNotfound: true, 245 | }, 246 | } 247 | for _, tc := range testCases { 248 | t.Run(tc.desc, func(t *testing.T) { 249 | r := &DeploymentReconciler{ 250 | Client: tc.c, 251 | Scheme: s, 252 | } 253 | ctx := context.Background() 254 | nsn := types.NamespacedName{Name: "test-elastalert", Namespace: "test"} 255 | req := reconcile.Request{NamespacedName: nsn} 256 | _, err := r.Reconcile(ctx, req) 257 | assert.NoError(t, err) 258 | if !tc.testNotfound { 259 | dep := appsv1.Deployment{} 260 | err = r.Client.Get(ctx, req.NamespacedName, &dep) 261 | assert.NoError(t, err) 262 | dep.Spec.Template.Annotations["kubectl.kubernetes.io/restartedAt"] = "2021-05-17T01:38:44+08:00" 263 | assert.Equal(t, tc.want, dep) 264 | } 265 | }) 266 | } 267 | } 268 | 269 | func TestDeploymentReconcileFailed(t *testing.T) { 270 | s := scheme.Scheme 271 | s.AddKnownTypes(corev1.SchemeGroupVersion, &v1alpha1.Elastalert{}) 272 | testCases := []struct { 273 | desc string 274 | c client.Client 275 | isToWait bool 276 | }{ 277 | { 278 | desc: "test deployment reconcile failed", 279 | c: fake.NewClientBuilder().WithRuntimeObjects( 280 | &v1alpha1.Elastalert{ 281 | ObjectMeta: metav1.ObjectMeta{ 282 | Namespace: "test", 283 | Name: "test-elastalert", 284 | }, 285 | Spec: v1alpha1.ElastalertSpec{ 286 | PodTemplateSpec: corev1.PodTemplateSpec{ 287 | Spec: corev1.PodSpec{ 288 | Containers: []corev1.Container{ 289 | { 290 | Name: "elastalert", 291 | }, 292 | }, 293 | }, 294 | }, 295 | }, 296 | }).Build(), 297 | isToWait: false, 298 | }, 299 | { 300 | desc: "test deployment reconcile failed", 301 | c: fake.NewClientBuilder().WithRuntimeObjects( 302 | &v1alpha1.Elastalert{ 303 | ObjectMeta: metav1.ObjectMeta{ 304 | Namespace: "test", 305 | Name: "test-elastalert", 306 | }, 307 | Spec: v1alpha1.ElastalertSpec{ 308 | PodTemplateSpec: corev1.PodTemplateSpec{ 309 | Spec: corev1.PodSpec{ 310 | Containers: []corev1.Container{ 311 | { 312 | Name: "elastalert", 313 | }, 314 | }, 315 | }, 316 | }, 317 | }, 318 | }).Build(), 319 | isToWait: true, 320 | }, 321 | } 322 | for _, tc := range testCases { 323 | defer monkey.Unpatch(recreateDeployment) 324 | defer monkey.Unpatch(observer.UpdateElastalertStatus) 325 | t.Run(tc.desc, func(t *testing.T) { 326 | r := &DeploymentReconciler{ 327 | Client: tc.c, 328 | Scheme: s, 329 | } 330 | if !tc.isToWait { 331 | monkey.Patch(recreateDeployment, func(c client.Client, Scheme *runtime.Scheme, ctx context.Context, e *v1alpha1.Elastalert) (*appsv1.Deployment, error) { 332 | return nil, errors.New("test") 333 | }) 334 | monkey.Patch(observer.UpdateElastalertStatus, func(c client.Client, ctx context.Context, e *v1alpha1.Elastalert, flag string) error { 335 | return errors.New("test update failed") 336 | }) 337 | } else { 338 | monkey.Patch(recreateDeployment, func(c client.Client, Scheme *runtime.Scheme, ctx context.Context, e *v1alpha1.Elastalert) (*appsv1.Deployment, error) { 339 | return nil, errors.New("test") 340 | }) 341 | monkey.Patch(observer.UpdateElastalertStatus, func(c client.Client, ctx context.Context, e *v1alpha1.Elastalert, flag string) error { 342 | return errors.New("test update failed") 343 | }) 344 | } 345 | ctx := context.Background() 346 | nsn := types.NamespacedName{Name: "test-elastalert", Namespace: "test"} 347 | req := reconcile.Request{NamespacedName: nsn} 348 | _, err := r.Reconcile(ctx, req) 349 | assert.Error(t, err) 350 | }) 351 | } 352 | } 353 | 354 | func TestDeploymentReconcile_SetupWithManager(t *testing.T) { 355 | s := scheme.Scheme 356 | s.AddKnownTypes(appsv1.SchemeGroupVersion) 357 | r := &DeploymentReconciler{ 358 | Client: fake.NewClientBuilder().WithRuntimeObjects().Build(), 359 | Scheme: s, 360 | } 361 | assert.Error(t, r.SetupWithManager(nil)) 362 | } 363 | 364 | func TestRecreateGetError(t *testing.T) { 365 | defer monkey.Unpatch(k8serrors.IsNotFound) 366 | s := scheme.Scheme 367 | s.AddKnownTypes(corev1.SchemeGroupVersion, &v1alpha1.Elastalert{}) 368 | c := fake.NewClientBuilder().Build() 369 | _, err := recreateDeployment(c, scheme.Scheme, context.Background(), &v1alpha1.Elastalert{}) 370 | require.Error(t, err) 371 | 372 | monkey.Patch(k8serrors.IsNotFound, func(err error) bool { 373 | return false 374 | }) 375 | _, err = recreateDeployment(c, s, context.Background(), &v1alpha1.Elastalert{}) 376 | require.Error(t, err) 377 | } 378 | 379 | func TestDeploymentReconcile_ReconcileError(t *testing.T) { 380 | defer monkey.Unpatch(k8serrors.IsNotFound) 381 | nsn := types.NamespacedName{Name: "test-elastalert", Namespace: "test"} 382 | req := reconcile.Request{NamespacedName: nsn} 383 | r := &DeploymentReconciler{ 384 | Client: fake.NewClientBuilder().Build(), 385 | Scheme: scheme.Scheme, 386 | } 387 | monkey.Patch(k8serrors.IsNotFound, func(err error) bool { 388 | return false 389 | }) 390 | _, err := r.Reconcile(context.Background(), req) 391 | require.Error(t, err) 392 | } 393 | 394 | func TestClientFailed(t *testing.T) { 395 | 396 | s := scheme.Scheme 397 | s.AddKnownTypes(corev1.SchemeGroupVersion, &v1alpha1.Elastalert{}) 398 | c := &ErrorClient{} 399 | _, err := recreateDeployment(c, scheme.Scheme, context.Background(), &v1alpha1.Elastalert{}) 400 | require.Error(t, err) 401 | } 402 | 403 | var _ client.Client = &ErrorClient{} 404 | 405 | type ErrorClient struct { 406 | } 407 | 408 | func (e *ErrorClient) Get(ctx context.Context, key client.ObjectKey, obj client.Object) error { 409 | return errors.New("for test") 410 | } 411 | 412 | func (e *ErrorClient) List(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error { 413 | return errors.New("for test") 414 | } 415 | 416 | func (e *ErrorClient) Create(ctx context.Context, obj client.Object, opts ...client.CreateOption) error { 417 | return errors.New("for test") 418 | } 419 | 420 | // Delete deletes the given obj from Kubernetes cluster. 421 | func (e *ErrorClient) Delete(ctx context.Context, obj client.Object, opts ...client.DeleteOption) error { 422 | return errors.New("for test") 423 | } 424 | 425 | // Update updates the given obj in the Kubernetes cluster. obj must be a 426 | // struct pointer so that obj can be updated with the content returned by the Server. 427 | func (e *ErrorClient) Update(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error { 428 | return errors.New("for test") 429 | } 430 | 431 | // Patch patches the given obj in the Kubernetes cluster. obj must be a 432 | // struct pointer so that obj can be updated with the content returned by the Server. 433 | func (e *ErrorClient) Patch(ctx context.Context, obj client.Object, patch client.Patch, opts ...client.PatchOption) error { 434 | return errors.New("for test") 435 | } 436 | 437 | // DeleteAllOf deletes all objects of the given type matching the given options. 438 | func (e *ErrorClient) DeleteAllOf(ctx context.Context, obj client.Object, opts ...client.DeleteAllOfOption) error { 439 | return errors.New("for test") 440 | } 441 | 442 | func (e *ErrorClient) Scheme() *runtime.Scheme { 443 | return nil 444 | } 445 | 446 | // RESTMapper returns the rest this client is using. 447 | func (e *ErrorClient) RESTMapper() meta.RESTMapper { 448 | return nil 449 | } 450 | 451 | func (e *ErrorClient) Status() client.StatusWriter { 452 | return &ErrorStatusWriter{} 453 | } 454 | 455 | type ErrorStatusWriter struct { 456 | } 457 | 458 | func (e *ErrorStatusWriter) Update(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error { 459 | return errors.New("for test") 460 | } 461 | 462 | // Patch patches the given object's subresource. obj must be a struct 463 | // pointer so that obj can be updated with the content returned by the 464 | // Server. 465 | func (e *ErrorStatusWriter) Patch(ctx context.Context, obj client.Object, patch client.Patch, opts ...client.PatchOption) error { 466 | return errors.New("for test") 467 | } 468 | -------------------------------------------------------------------------------- /controllers/deployment_controllers.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "context" 5 | esv1alpha1 "github.com/toughnoah/elastalert-operator/api/v1alpha1" 6 | ob "github.com/toughnoah/elastalert-operator/controllers/observer" 7 | "github.com/toughnoah/elastalert-operator/controllers/podspec" 8 | appsv1 "k8s.io/api/apps/v1" 9 | k8serrors "k8s.io/apimachinery/pkg/api/errors" 10 | "k8s.io/apimachinery/pkg/runtime" 11 | "k8s.io/apimachinery/pkg/types" 12 | ctrl "sigs.k8s.io/controller-runtime" 13 | "sigs.k8s.io/controller-runtime/pkg/client" 14 | "sigs.k8s.io/controller-runtime/pkg/controller" 15 | "sigs.k8s.io/controller-runtime/pkg/reconcile" 16 | ) 17 | 18 | type DeploymentReconciler struct { 19 | client.Client 20 | Scheme *runtime.Scheme 21 | } 22 | 23 | func (r *DeploymentReconciler) Reconcile(ctx context.Context, req reconcile.Request) (ctrl.Result, error) { 24 | elastalert := &esv1alpha1.Elastalert{} 25 | err := r.Get(ctx, req.NamespacedName, elastalert) 26 | if err != nil { 27 | if k8serrors.IsNotFound(err) { 28 | return ctrl.Result{}, nil 29 | } 30 | // Error reading the object - requeue the request. 31 | log.Error(err, "Failed to get deployment from server") 32 | return ctrl.Result{}, err 33 | } 34 | if _, err = recreateDeployment(r.Client, r.Scheme, ctx, elastalert); err != nil { 35 | if statusError := ob.UpdateElastalertStatus(r.Client, ctx, elastalert, esv1alpha1.ActionFailed); statusError != nil { 36 | return ctrl.Result{}, statusError 37 | } 38 | return ctrl.Result{}, err 39 | } 40 | return ctrl.Result{}, nil 41 | } 42 | 43 | func (r *DeploymentReconciler) SetupWithManager(mgr ctrl.Manager) error { 44 | return ctrl.NewControllerManagedBy(mgr). 45 | For(&appsv1.Deployment{}). 46 | WithOptions(controller.Options{MaxConcurrentReconciles: 5}). 47 | Complete(r) 48 | } 49 | 50 | func recreateDeployment(c client.Client, Scheme *runtime.Scheme, ctx context.Context, e *esv1alpha1.Elastalert) (*appsv1.Deployment, error) { 51 | deploy := &appsv1.Deployment{} 52 | err := c.Get(ctx, 53 | types.NamespacedName{ 54 | Namespace: e.Namespace, 55 | Name: e.Name, 56 | }, 57 | deploy) 58 | if err != nil { 59 | if k8serrors.IsNotFound(err) { 60 | newDeploy, err := podspec.GenerateNewDeployment(Scheme, e) 61 | if err != nil { 62 | return nil, err 63 | } 64 | if err = applySecret(c, Scheme, ctx, e); err != nil { 65 | return nil, err 66 | } 67 | if err = applyConfigMaps(c, Scheme, ctx, e); err != nil { 68 | return nil, err 69 | } 70 | if err = c.Create(ctx, newDeploy); err != nil { 71 | return nil, err 72 | } 73 | log.V(1).Info( 74 | "Deployment reconcile success.", 75 | "Elastalert.Name", e.Name, 76 | "Deployment.Name", e.Name, 77 | ) 78 | return newDeploy, nil 79 | } 80 | log.Error(err, "Failed to get deployment from server", "Elastalert.Name", e.Name, "Deployment.Name", e.Name) 81 | return nil, err 82 | } 83 | // if err is nil, means that event is about about other deployment in same namespace. so just return nil 84 | return nil, nil 85 | } 86 | -------------------------------------------------------------------------------- /controllers/elastalert_controller.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021. 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 | esv1alpha1 "github.com/toughnoah/elastalert-operator/api/v1alpha1" 22 | "github.com/toughnoah/elastalert-operator/controllers/event" 23 | ob "github.com/toughnoah/elastalert-operator/controllers/observer" 24 | "github.com/toughnoah/elastalert-operator/controllers/podspec" 25 | appsv1 "k8s.io/api/apps/v1" 26 | corev1 "k8s.io/api/core/v1" 27 | k8serrors "k8s.io/apimachinery/pkg/api/errors" 28 | "k8s.io/apimachinery/pkg/api/meta" 29 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 30 | "k8s.io/apimachinery/pkg/runtime" 31 | "k8s.io/apimachinery/pkg/types" 32 | "k8s.io/client-go/tools/record" 33 | ctrl "sigs.k8s.io/controller-runtime" 34 | "sigs.k8s.io/controller-runtime/pkg/client" 35 | "sigs.k8s.io/controller-runtime/pkg/controller" 36 | "sigs.k8s.io/controller-runtime/pkg/reconcile" 37 | ) 38 | 39 | const name = "elastalert-controller" 40 | 41 | var log = ctrl.Log.WithName(name) 42 | 43 | // ElastalertReconciler reconciles a Elastalert object 44 | type ElastalertReconciler struct { 45 | client.Client 46 | Scheme *runtime.Scheme 47 | Recorder record.EventRecorder 48 | Observer ob.Manager 49 | } 50 | 51 | //+kubebuilder:rbac:groups=es.noah.domain,resources=elastalerts,verbs=get;list;watch;create;update;patch;delete 52 | //+kubebuilder:rbac:groups=es.noah.domain,resources=elastalerts/status,verbs=get;update;patch 53 | //+kubebuilder:rbac:groups=es.noah.domain,resources=elastalerts/finalizers,verbs=update 54 | func (r *ElastalertReconciler) Reconcile(ctx context.Context, req reconcile.Request) (ctrl.Result, error) { 55 | elastalert := &esv1alpha1.Elastalert{} 56 | err := r.Get(ctx, req.NamespacedName, elastalert) 57 | if err != nil { 58 | if k8serrors.IsNotFound(err) { 59 | r.Observer.StopObserving(req.NamespacedName) 60 | return ctrl.Result{}, nil 61 | } 62 | // Error reading the object - requeue the request. 63 | log.Error(err, "Failed to get Elastalert from server") 64 | return ctrl.Result{}, err 65 | } 66 | cond := r.findSuccessCondition(elastalert) 67 | if cond == nil || cond.ObservedGeneration != elastalert.Generation { 68 | if statusError := ob.UpdateElastalertStatus(r.Client, ctx, elastalert, esv1alpha1.ResourcesCreating); statusError != nil { 69 | return ctrl.Result{}, statusError 70 | } 71 | if err = applySecret(r.Client, r.Scheme, ctx, elastalert); err != nil { 72 | ob.EmitK8sEvent(r.Recorder, elastalert, corev1.EventTypeWarning, event.EventReasonError, "Failed to apply Secret.") 73 | if statusError := ob.UpdateElastalertStatus(r.Client, ctx, elastalert, esv1alpha1.ActionFailed); statusError != nil { 74 | return ctrl.Result{}, statusError 75 | } 76 | return ctrl.Result{}, err 77 | } 78 | ob.EmitK8sEvent(r.Recorder, elastalert, corev1.EventTypeNormal, event.EventReasonCreated, "Apply cert secret successfully.") 79 | if err = applyConfigMaps(r.Client, r.Scheme, ctx, elastalert); err != nil { 80 | ob.EmitK8sEvent(r.Recorder, elastalert, corev1.EventTypeWarning, event.EventReasonError, "Failed to apply configmaps") 81 | if statusError := ob.UpdateElastalertStatus(r.Client, ctx, elastalert, esv1alpha1.ActionFailed); statusError != nil { 82 | return ctrl.Result{}, statusError 83 | } 84 | return ctrl.Result{}, err 85 | } 86 | ob.EmitK8sEvent(r.Recorder, elastalert, corev1.EventTypeNormal, event.EventReasonCreated, "Apply configmaps successfully.") 87 | if _, err = applyDeployment(r.Client, r.Scheme, ctx, elastalert); err != nil { 88 | ob.EmitK8sEvent(r.Recorder, elastalert, corev1.EventTypeWarning, event.EventReasonError, "Failed to apply deployment.") 89 | if statusError := ob.UpdateElastalertStatus(r.Client, ctx, elastalert, esv1alpha1.ActionFailed); statusError != nil { 90 | return ctrl.Result{}, statusError 91 | } 92 | return ctrl.Result{}, err 93 | } 94 | ob.EmitK8sEvent(r.Recorder, elastalert, corev1.EventTypeNormal, event.EventReasonSuccess, "Apply deployment done, reconcile Elastalert resources successfully.") 95 | } 96 | r.startObservingHealth(elastalert) 97 | return ctrl.Result{}, nil 98 | } 99 | 100 | // SetupWithManager sets up the controller with the Manager. 101 | func (r *ElastalertReconciler) SetupWithManager(mgr ctrl.Manager) error { 102 | return ctrl.NewControllerManagedBy(mgr). 103 | For(&esv1alpha1.Elastalert{}). 104 | WithOptions(controller.Options{MaxConcurrentReconciles: 5}). 105 | Complete(r) 106 | } 107 | 108 | func (r *ElastalertReconciler) startObservingHealth(e *esv1alpha1.Elastalert) { 109 | r.Observer.Observe(e, r.Client, r.Recorder) 110 | } 111 | 112 | func (r *ElastalertReconciler) findSuccessCondition(e *esv1alpha1.Elastalert) *metav1.Condition { 113 | return meta.FindStatusCondition(e.Status.Condictions, esv1alpha1.ElastAlertAvailableType) 114 | } 115 | 116 | func applyConfigMaps(c client.Client, Scheme *runtime.Scheme, ctx context.Context, e *esv1alpha1.Elastalert) error { 117 | stringCert := e.Spec.Cert 118 | err := podspec.PatchConfigSettings(e, stringCert) 119 | if err != nil { 120 | log.Error(err, "Failed to patch config.yaml configmaps", "Elastalert.Namespace", e.Namespace, "Configmaps.Namespace", e.Namespace) 121 | return err 122 | } 123 | err = podspec.PatchAlertSettings(e) 124 | if err != nil { 125 | log.Error(err, "Failed to patch alert for rules configmaps", "Elastalert.Namespace", e.Namespace, "Configmaps.Namespace", e.Namespace) 126 | return err 127 | } 128 | list := &corev1.ConfigMapList{} 129 | opts := client.InNamespace(e.Namespace) 130 | if err = c.List(ctx, list, opts); err != nil { 131 | return err 132 | } 133 | config, err := podspec.GenerateNewConfigmap(Scheme, e, esv1alpha1.ConfigSuffx) 134 | if err != nil { 135 | return err 136 | } 137 | rule, err := podspec.GenerateNewConfigmap(Scheme, e, esv1alpha1.RuleSuffx) 138 | if err != nil { 139 | return err 140 | } 141 | configMapsMaps := podspec.ConfigMapsToMap(list.Items) 142 | var configMapsList []corev1.ConfigMap 143 | configMapsList = append(configMapsList, *rule, *config) 144 | if len(list.Items) != 0 { 145 | for _, cm := range configMapsList { 146 | if _, ok := configMapsMaps[cm.Name]; ok { 147 | if err = c.Update(ctx, &cm); err != nil { 148 | log.Error(err, "Failed to update configmaps", "Elastalert.Namespace", e.Namespace, "Configmaps.Namespace", e.Namespace) 149 | return err 150 | } 151 | } else { 152 | if err = c.Create(ctx, &cm); err != nil { 153 | log.Error(err, "Failed to create configmaps", "Elastalert.Namespace", e.Namespace, "Configmaps.Namespace", e.Namespace) 154 | return err 155 | } 156 | } 157 | } 158 | return nil 159 | } else { 160 | for _, cm := range configMapsList { 161 | if err = c.Create(ctx, &cm); err != nil { 162 | log.Error(err, "Failed to create configmaps", "Elastalert.Namespace", e.Namespace, "Configmaps.Namespace", e.Namespace) 163 | return err 164 | } 165 | } 166 | 167 | } 168 | log.V(1).Info( 169 | "Apply configmaps successfully", 170 | "Elastalert.Namespace", e.Namespace, 171 | "Configmaps.Namespace", e.Namespace, 172 | ) 173 | return nil 174 | } 175 | 176 | func applySecret(c client.Client, Scheme *runtime.Scheme, ctx context.Context, e *esv1alpha1.Elastalert) error { 177 | secret := &corev1.Secret{} 178 | newSecret, err := podspec.GenerateCertSecret(Scheme, e) 179 | if err != nil { 180 | return err 181 | } 182 | if err = c.Get(ctx, types.NamespacedName{ 183 | Namespace: e.Namespace, 184 | Name: e.Name + podspec.DefaultCertSuffix, 185 | }, 186 | secret); err != nil { 187 | if k8serrors.IsNotFound(err) { 188 | if err = c.Create(ctx, newSecret); err != nil { 189 | log.Error(err, "Failed to create Secret", "Elastalert.Namespace", e.Namespace, "Secret.Name", secret.Name) 190 | return err 191 | } 192 | } 193 | return err 194 | } else { 195 | if err = c.Update(ctx, newSecret); err != nil { 196 | log.Error(err, "Failed to update Secret", "Elastalert.Namespace", e.Namespace) 197 | return err 198 | } 199 | } 200 | log.V(1).Info( 201 | "Apply cert secret successfully", 202 | "Elastalert.Namespace", e.Namespace, 203 | "Secret.Name", secret.Name, 204 | ) 205 | return nil 206 | } 207 | 208 | func applyDeployment(c client.Client, Scheme *runtime.Scheme, ctx context.Context, e *esv1alpha1.Elastalert) (*appsv1.Deployment, error) { 209 | deploy := &appsv1.Deployment{} 210 | err := c.Get(ctx, 211 | types.NamespacedName{ 212 | Namespace: e.Namespace, 213 | Name: e.Name, 214 | }, deploy) 215 | if err != nil { 216 | if k8serrors.IsNotFound(err) { 217 | deploy, err = podspec.GenerateNewDeployment(Scheme, e) 218 | if err != nil { 219 | return nil, err 220 | } 221 | if err = c.Create(ctx, deploy); err != nil { 222 | log.Error(err, "Failed to create Deployment", "Elastalert.Name", e.Name, "Deployment.Name", e.Name) 223 | return nil, err 224 | } 225 | return deploy, nil 226 | } 227 | return nil, err 228 | } else { 229 | deploy, err = podspec.GenerateNewDeployment(Scheme, e) 230 | if err != nil { 231 | return nil, err 232 | } 233 | if err = c.Update(ctx, deploy); err != nil { 234 | log.Error(err, "Failed to update Deployment", "Elastalert.Name", e.Name, "Deployment.Name", e.Name) 235 | return nil, err 236 | } 237 | 238 | log.V(1).Info( 239 | "Apply deployment successfully", 240 | "Elastalert.Name", e.Name, 241 | "Deployment.Name", e.Name, 242 | ) 243 | return deploy, nil 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /controllers/event/event.go: -------------------------------------------------------------------------------- 1 | package event 2 | 3 | // Event reasons for the Elastalert 4 | const ( 5 | // EventReasonCreated describes events where resources were created. 6 | EventReasonCreated = "Created" 7 | // EventReasonDeleted describes events where resources were deleted. 8 | EventReasonDeleted = "Deleted" 9 | // EventReasonError describes events where resources were an error occurs. 10 | EventReasonError = "Error" 11 | // EventReasonSuccess describes events where resources were successfully reconciled. 12 | EventReasonSuccess = "Success" 13 | ) 14 | -------------------------------------------------------------------------------- /controllers/observer/observer.go: -------------------------------------------------------------------------------- 1 | package observer 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | esv1alpha1 "github.com/toughnoah/elastalert-operator/api/v1alpha1" 7 | "github.com/toughnoah/elastalert-operator/controllers/event" 8 | "github.com/toughnoah/elastalert-operator/controllers/podspec" 9 | appsv1 "k8s.io/api/apps/v1" 10 | corev1 "k8s.io/api/core/v1" 11 | "k8s.io/apimachinery/pkg/api/meta" 12 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 13 | "k8s.io/apimachinery/pkg/runtime" 14 | "k8s.io/apimachinery/pkg/types" 15 | "k8s.io/client-go/tools/record" 16 | ctrl "sigs.k8s.io/controller-runtime" 17 | "sigs.k8s.io/controller-runtime/pkg/client" 18 | "sync" 19 | "time" 20 | ) 21 | 22 | const name = "observation" 23 | 24 | var log = ctrl.Log.WithName(name) 25 | 26 | // Observer regularly check the health of elastalert deployment 27 | // in a thread-safe way 28 | type Observer struct { 29 | elastalert types.NamespacedName 30 | creationTime time.Time 31 | stopChan chan struct{} 32 | stopOnce sync.Once 33 | mutex sync.RWMutex 34 | ObservationInterval time.Duration 35 | client client.Client 36 | recorder record.EventRecorder 37 | } 38 | 39 | // NewObserver creates and starts an Observer 40 | func NewObserver(c client.Client, elastalert types.NamespacedName, interval time.Duration, recorder record.EventRecorder) *Observer { 41 | observer := Observer{ 42 | elastalert: elastalert, 43 | client: c, 44 | creationTime: time.Now(), 45 | stopChan: make(chan struct{}), 46 | stopOnce: sync.Once{}, 47 | ObservationInterval: interval, 48 | recorder: recorder, 49 | } 50 | return &observer 51 | } 52 | 53 | // Start the observer in a separate goroutine 54 | func (o *Observer) Start() { 55 | log.Info( 56 | "Starting observer for elastalert instance.", 57 | "namespace", o.elastalert.Namespace, 58 | "elastalert", o.elastalert.Name, 59 | ) 60 | go o.runPeriodically() 61 | } 62 | 63 | // Stop the observer loop 64 | func (o *Observer) Stop() { 65 | log.Info( 66 | "Stopping observer for deleted elastalert instance.", 67 | "namespace", o.elastalert.Namespace, 68 | "elastalert", o.elastalert.Name, 69 | ) 70 | o.stopOnce.Do(func() { 71 | close(o.stopChan) 72 | }) 73 | } 74 | 75 | func (o *Observer) runPeriodically() { 76 | ticker := time.NewTicker(o.ObservationInterval) 77 | defer ticker.Stop() 78 | 79 | for { 80 | select { 81 | case <-ticker.C: 82 | o.checkDeploymentHeath() 83 | case <-o.stopChan: 84 | return 85 | } 86 | } 87 | } 88 | func (o *Observer) checkDeploymentHeath() error { 89 | ea := &esv1alpha1.Elastalert{} 90 | err := o.client.Get(context.Background(), o.elastalert, ea) 91 | if err != nil { 92 | log.Error(err, "Failed to get elastalert instance while observing.", "namespace", o.elastalert.Namespace, "elastalert", o.elastalert.Name) 93 | return err 94 | } 95 | dep := &appsv1.Deployment{} 96 | err = o.client.Get(context.Background(), o.elastalert, dep) 97 | if err != nil { 98 | log.Error(err, "Failed to get deployment instance while observing.", "namespace", o.elastalert.Namespace, "elastalert", o.elastalert.Name) 99 | EmitK8sEvent(o.recorder, ea, corev1.EventTypeWarning, event.EventReasonError, "Get deployment instance failed while observing.") 100 | return UpdateElastalertStatus(o.client, context.Background(), ea, esv1alpha1.ActionFailed) 101 | } 102 | if dep.Status.AvailableReplicas != *dep.Spec.Replicas { 103 | log.Error(err, "AvailableReplicas of deployment instance is 0 .", "namespace", o.elastalert.Namespace, "elastalert", o.elastalert.Name) 104 | EmitK8sEvent(o.recorder, ea, corev1.EventTypeWarning, event.EventReasonError, "AvailableReplicas of deployment instance is 0.") 105 | return UpdateElastalertStatus(o.client, context.Background(), ea, esv1alpha1.ActionFailed) 106 | } 107 | if dep.Status.AvailableReplicas == *dep.Spec.Replicas { 108 | log.V(1).Info( 109 | "Updating Elastalert resources phase to SUCCESS.", 110 | "Elastalert.Namespace", o.elastalert.Namespace, 111 | "elastalert", o.elastalert.Name, 112 | ) 113 | EmitK8sEvent(o.recorder, ea, corev1.EventTypeNormal, event.EventReasonSuccess, "Deployment has been stabilized.") 114 | return UpdateElastalertStatus(o.client, context.Background(), ea, esv1alpha1.ActionSuccess) 115 | 116 | } 117 | 118 | return nil 119 | } 120 | 121 | type Manager struct { 122 | observerLock sync.RWMutex 123 | observers map[types.NamespacedName]*Observer 124 | } 125 | 126 | func NewManager() *Manager { 127 | return &Manager{ 128 | observers: make(map[types.NamespacedName]*Observer), 129 | } 130 | } 131 | 132 | func (m *Manager) getObserver(key types.NamespacedName) (*Observer, bool) { 133 | m.observerLock.RLock() 134 | defer m.observerLock.RUnlock() 135 | 136 | observer, ok := m.observers[key] 137 | return observer, ok 138 | } 139 | 140 | func (m *Manager) Observe(elastalert *esv1alpha1.Elastalert, c client.Client, recorder record.EventRecorder) *Observer { 141 | nsName := types.NamespacedName{ 142 | Namespace: elastalert.Namespace, 143 | Name: elastalert.Name, 144 | } 145 | 146 | observer, exists := m.getObserver(nsName) 147 | if !exists { 148 | return m.createOrReplaceObserver(nsName, c, recorder) 149 | } 150 | return observer 151 | } 152 | 153 | // createOrReplaceObserver creates a new observer and adds it to the observers map, replacing existing observers if necessary. 154 | func (m *Manager) createOrReplaceObserver(elastalert types.NamespacedName, c client.Client, recorder record.EventRecorder) *Observer { 155 | m.observerLock.Lock() 156 | defer m.observerLock.Unlock() 157 | 158 | observer := NewObserver(c, elastalert, esv1alpha1.ElastAlertObserveInterval, recorder) 159 | observer.Start() 160 | 161 | m.observers[elastalert] = observer 162 | return observer 163 | } 164 | 165 | func (m *Manager) StopObserving(key types.NamespacedName) { 166 | m.observerLock.Lock() 167 | defer m.observerLock.Unlock() 168 | 169 | if observer, ok := m.observers[key]; ok { 170 | observer.Stop() 171 | delete(m.observers, key) 172 | } 173 | } 174 | 175 | func UpdateElastalertStatus(c client.Client, ctx context.Context, e *esv1alpha1.Elastalert, flag string) error { 176 | condition := NewCondition(e, flag) 177 | if err := UpdateStatus(c, ctx, e, condition); err != nil { 178 | return err 179 | } 180 | return nil 181 | } 182 | 183 | func UpdateStatus(c client.Client, ctx context.Context, e *esv1alpha1.Elastalert, condition *metav1.Condition) error { 184 | patch := client.MergeFrom(e.DeepCopy()) 185 | e.Status.Version = esv1alpha1.ElastAlertVersion 186 | 187 | if condition != nil { 188 | switch condition.Type { 189 | case esv1alpha1.ElastAlertAvailableType: 190 | e.Status.Phase = esv1alpha1.ElastAlertPhraseSucceeded 191 | meta.SetStatusCondition(&e.Status.Condictions, *condition) 192 | meta.RemoveStatusCondition(&e.Status.Condictions, esv1alpha1.ElastAlertUnAvailableType) 193 | case esv1alpha1.ElastAlertUnAvailableType: 194 | e.Status.Phase = esv1alpha1.ElastAlertPhraseFailed 195 | meta.SetStatusCondition(&e.Status.Condictions, *condition) 196 | meta.RemoveStatusCondition(&e.Status.Condictions, esv1alpha1.ElastAlertAvailableType) 197 | } 198 | if err := c.Status().Patch(ctx, e, patch); err != nil { 199 | log.Error(err, "Failed to update elastalert failed status", "Elastalert.Name", e.Name, "Status", e.Status.Phase) 200 | return err 201 | } 202 | } 203 | if len(e.Status.Condictions) == 0 && condition == nil || e.Status.Condictions[0].ObservedGeneration != e.Generation && condition == nil { 204 | e.Status.Phase = esv1alpha1.ElastAlertInitializing 205 | if err := c.Status().Patch(ctx, e, patch); err != nil { 206 | log.Error(err, "Failed to update elastalert failed status", "Elastalert.Name", e.Name, "Status", e.Status.Phase) 207 | return err 208 | } 209 | } 210 | log.V(1).Info( 211 | "Update Elastalert resources status success.", 212 | "Elastalert.Namespace", e.Name, 213 | "Status", e.Status.Phase, 214 | ) 215 | return nil 216 | } 217 | 218 | func NewCondition(e *esv1alpha1.Elastalert, flag string) *metav1.Condition { 219 | var condition *metav1.Condition 220 | switch flag { 221 | case esv1alpha1.ActionSuccess: 222 | condition = &metav1.Condition{ 223 | Type: esv1alpha1.ElastAlertAvailableType, 224 | Status: esv1alpha1.ElastAlertAvailableStatus, 225 | ObservedGeneration: e.Generation, 226 | LastTransitionTime: metav1.NewTime(podspec.GetUtcTime()), 227 | Reason: esv1alpha1.ElastAlertAvailableReason, 228 | Message: fmt.Sprintf("ElastAlert %s has successfully progressed.", e.Name), 229 | } 230 | case esv1alpha1.ActionFailed: 231 | condition = &metav1.Condition{ 232 | Type: esv1alpha1.ElastAlertUnAvailableType, 233 | Status: esv1alpha1.ElastAlertUnAvailableStatus, 234 | ObservedGeneration: e.Generation, 235 | LastTransitionTime: metav1.NewTime(podspec.GetUtcTime()), 236 | Reason: esv1alpha1.ElastAlertUnAvailableReason, 237 | Message: fmt.Sprintf("Failed to apply ElastAlert %s resources.", e.Name), 238 | } 239 | case esv1alpha1.ResourcesCreating: 240 | return nil 241 | } 242 | return condition 243 | } 244 | 245 | func EmitK8sEvent(recorder record.EventRecorder, object runtime.Object, eventtype, reason, messageFmt string) { 246 | recorder.Eventf(object, eventtype, reason, messageFmt) 247 | } 248 | -------------------------------------------------------------------------------- /controllers/observer/observer_test.go: -------------------------------------------------------------------------------- 1 | package observer 2 | 3 | import ( 4 | "context" 5 | . "github.com/onsi/ginkgo" 6 | . "github.com/onsi/gomega" 7 | "github.com/toughnoah/elastalert-operator/api/v1alpha1" 8 | appsv1 "k8s.io/api/apps/v1" 9 | corev1 "k8s.io/api/core/v1" 10 | "k8s.io/apimachinery/pkg/api/resource" 11 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 12 | "k8s.io/apimachinery/pkg/types" 13 | "k8s.io/client-go/kubernetes/scheme" 14 | "k8s.io/client-go/tools/record" 15 | "sigs.k8s.io/controller-runtime/pkg/client" 16 | "sigs.k8s.io/controller-runtime/pkg/client/fake" 17 | "testing" 18 | "time" 19 | ) 20 | 21 | var ( 22 | varTrue = true 23 | Replicas int32 = 1 24 | TerminationGracePeriodSeconds int64 = 10 25 | ) 26 | 27 | func TestObserver(t *testing.T) { 28 | RegisterFailHandler(Fail) 29 | RunSpecs(t, "Observer Suite") 30 | } 31 | 32 | var _ = Describe("Test Observer", func() { 33 | s := scheme.Scheme 34 | s.AddKnownTypes(corev1.SchemeGroupVersion, &v1alpha1.Elastalert{}) 35 | recoder := record.NewBroadcaster().NewRecorder(s, corev1.EventSource{}) 36 | ea := types.NamespacedName{ 37 | Name: "elastalert", 38 | Namespace: "ns", 39 | } 40 | testCases := []struct { 41 | client client.Client 42 | eaPhase string 43 | }{ 44 | { 45 | client: fake.NewClientBuilder().WithRuntimeObjects( 46 | &v1alpha1.Elastalert{ 47 | ObjectMeta: metav1.ObjectMeta{ 48 | Name: "elastalert", 49 | Namespace: "ns", 50 | }, 51 | Status: v1alpha1.ElastalertStatus{Phase: v1alpha1.ElastAlertPhraseSucceeded}, 52 | }, 53 | &appsv1.Deployment{ 54 | TypeMeta: metav1.TypeMeta{ 55 | Kind: "Deployment", 56 | APIVersion: "apps/v1", 57 | }, 58 | ObjectMeta: metav1.ObjectMeta{ 59 | ResourceVersion: "1", 60 | Namespace: "ns", 61 | Name: "elastalert", 62 | OwnerReferences: []metav1.OwnerReference{ 63 | { 64 | APIVersion: "v1", 65 | Kind: "Elastalert", 66 | Name: "elastalert", 67 | UID: "", 68 | Controller: &varTrue, 69 | BlockOwnerDeletion: &varTrue, 70 | }, 71 | }, 72 | }, 73 | Spec: appsv1.DeploymentSpec{ 74 | Replicas: &Replicas, 75 | Selector: &metav1.LabelSelector{ 76 | MatchLabels: map[string]string{"app": "elastalert"}, 77 | }, 78 | Template: corev1.PodTemplateSpec{ 79 | ObjectMeta: metav1.ObjectMeta{ 80 | Labels: map[string]string{ 81 | "app": "elastalert", 82 | }, 83 | Annotations: map[string]string{ 84 | "kubectl.kubernetes.io/restartedAt": "2021-05-17T01:38:44+08:00", 85 | }, 86 | }, 87 | 88 | Spec: corev1.PodSpec{ 89 | AutomountServiceAccountToken: &varTrue, 90 | TerminationGracePeriodSeconds: &TerminationGracePeriodSeconds, 91 | Containers: []corev1.Container{ 92 | { 93 | Name: "elastalert", 94 | Image: "toughnoah/elastalert:v1.0", 95 | VolumeMounts: []corev1.VolumeMount{ 96 | // have to keep sequence 97 | { 98 | Name: "elasticsearch-cert", 99 | MountPath: "/ssl", 100 | }, 101 | { 102 | Name: "test-elastalert-config", 103 | MountPath: "/etc/elastalert", 104 | }, 105 | { 106 | Name: "test-elastalert-rule", 107 | MountPath: "/etc/elastalert/rules", 108 | }, 109 | }, 110 | Command: []string{"elastalert", "--config", "/etc/elastalert/config.yaml", "--verbose"}, 111 | Resources: corev1.ResourceRequirements{ 112 | Requests: map[corev1.ResourceName]resource.Quantity{ 113 | corev1.ResourceMemory: resource.MustParse("2Gi"), 114 | }, 115 | Limits: map[corev1.ResourceName]resource.Quantity{ 116 | corev1.ResourceMemory: resource.MustParse("2Gi"), 117 | }, 118 | }, 119 | Ports: []corev1.ContainerPort{ 120 | {Name: "http", ContainerPort: 8080, Protocol: corev1.ProtocolTCP}, 121 | }, 122 | ReadinessProbe: &corev1.Probe{ 123 | Handler: corev1.Handler{ 124 | Exec: &corev1.ExecAction{ 125 | Command: []string{ 126 | "cat", 127 | "/etc/elastalert/config.yaml", 128 | }, 129 | }, 130 | }, 131 | InitialDelaySeconds: 20, 132 | TimeoutSeconds: 3, 133 | PeriodSeconds: 2, 134 | SuccessThreshold: 5, 135 | FailureThreshold: 3, 136 | }, 137 | LivenessProbe: &corev1.Probe{ 138 | Handler: corev1.Handler{ 139 | Exec: &corev1.ExecAction{ 140 | Command: []string{ 141 | "sh", 142 | "-c", 143 | "ps -ef|grep -v grep|grep elastalert", 144 | }, 145 | }, 146 | }, 147 | InitialDelaySeconds: 50, 148 | TimeoutSeconds: 3, 149 | PeriodSeconds: 2, 150 | SuccessThreshold: 1, 151 | FailureThreshold: 3, 152 | }, 153 | }, 154 | }, 155 | Volumes: []corev1.Volume{ 156 | // have to keep sequence 157 | { 158 | Name: "elasticsearch-cert", 159 | VolumeSource: corev1.VolumeSource{ 160 | Secret: &corev1.SecretVolumeSource{ 161 | SecretName: "test-elastalert-es-cert", 162 | }, 163 | }, 164 | }, 165 | { 166 | Name: "test-elastalert-config", 167 | VolumeSource: corev1.VolumeSource{ 168 | ConfigMap: &corev1.ConfigMapVolumeSource{ 169 | LocalObjectReference: corev1.LocalObjectReference{ 170 | Name: "test-elastalert-config", 171 | }, 172 | }, 173 | }, 174 | }, 175 | { 176 | Name: "test-elastalert-rule", 177 | VolumeSource: corev1.VolumeSource{ 178 | ConfigMap: &corev1.ConfigMapVolumeSource{ 179 | LocalObjectReference: corev1.LocalObjectReference{ 180 | Name: "test-elastalert-rule", 181 | }, 182 | }, 183 | }, 184 | }, 185 | }, 186 | Affinity: &corev1.Affinity{ 187 | PodAntiAffinity: &corev1.PodAntiAffinity{}, 188 | }, 189 | }, 190 | }, 191 | }, 192 | Status: appsv1.DeploymentStatus{ 193 | AvailableReplicas: int32(0), 194 | }, 195 | }, 196 | ).Build(), 197 | eaPhase: v1alpha1.ElastAlertPhraseFailed, 198 | }, 199 | { 200 | client: fake.NewClientBuilder().WithRuntimeObjects( 201 | &v1alpha1.Elastalert{ 202 | ObjectMeta: metav1.ObjectMeta{ 203 | Name: "elastalert", 204 | Namespace: "ns", 205 | }, 206 | Status: v1alpha1.ElastalertStatus{Phase: v1alpha1.ElastAlertPhraseSucceeded}, 207 | }, 208 | &appsv1.Deployment{ 209 | TypeMeta: metav1.TypeMeta{ 210 | Kind: "Deployment", 211 | APIVersion: "apps/v1", 212 | }, 213 | ObjectMeta: metav1.ObjectMeta{ 214 | ResourceVersion: "1", 215 | Namespace: "ns", 216 | Name: "elastalert", 217 | OwnerReferences: []metav1.OwnerReference{ 218 | { 219 | APIVersion: "v1", 220 | Kind: "Elastalert", 221 | Name: "elastalert", 222 | UID: "", 223 | Controller: &varTrue, 224 | BlockOwnerDeletion: &varTrue, 225 | }, 226 | }, 227 | }, 228 | Spec: appsv1.DeploymentSpec{ 229 | Replicas: &Replicas, 230 | Selector: &metav1.LabelSelector{ 231 | MatchLabels: map[string]string{"app": "elastalert"}, 232 | }, 233 | Template: corev1.PodTemplateSpec{ 234 | ObjectMeta: metav1.ObjectMeta{ 235 | Labels: map[string]string{ 236 | "app": "elastalert", 237 | }, 238 | Annotations: map[string]string{ 239 | "kubectl.kubernetes.io/restartedAt": "2021-05-17T01:38:44+08:00", 240 | }, 241 | }, 242 | 243 | Spec: corev1.PodSpec{ 244 | AutomountServiceAccountToken: &varTrue, 245 | TerminationGracePeriodSeconds: &TerminationGracePeriodSeconds, 246 | Containers: []corev1.Container{ 247 | { 248 | Name: "elastalert", 249 | Image: "toughnoah/elastalert:v1.0", 250 | VolumeMounts: []corev1.VolumeMount{ 251 | // have to keep sequence 252 | { 253 | Name: "elasticsearch-cert", 254 | MountPath: "/ssl", 255 | }, 256 | { 257 | Name: "test-elastalert-config", 258 | MountPath: "/etc/elastalert", 259 | }, 260 | { 261 | Name: "test-elastalert-rule", 262 | MountPath: "/etc/elastalert/rules", 263 | }, 264 | }, 265 | Command: []string{"elastalert", "--config", "/etc/elastalert/config.yaml", "--verbose"}, 266 | Resources: corev1.ResourceRequirements{ 267 | Requests: map[corev1.ResourceName]resource.Quantity{ 268 | corev1.ResourceMemory: resource.MustParse("2Gi"), 269 | }, 270 | Limits: map[corev1.ResourceName]resource.Quantity{ 271 | corev1.ResourceMemory: resource.MustParse("2Gi"), 272 | }, 273 | }, 274 | Ports: []corev1.ContainerPort{ 275 | {Name: "http", ContainerPort: 8080, Protocol: corev1.ProtocolTCP}, 276 | }, 277 | ReadinessProbe: &corev1.Probe{ 278 | Handler: corev1.Handler{ 279 | Exec: &corev1.ExecAction{ 280 | Command: []string{ 281 | "cat", 282 | "/etc/elastalert/config.yaml", 283 | }, 284 | }, 285 | }, 286 | InitialDelaySeconds: 20, 287 | TimeoutSeconds: 3, 288 | PeriodSeconds: 2, 289 | SuccessThreshold: 5, 290 | FailureThreshold: 3, 291 | }, 292 | LivenessProbe: &corev1.Probe{ 293 | Handler: corev1.Handler{ 294 | Exec: &corev1.ExecAction{ 295 | Command: []string{ 296 | "sh", 297 | "-c", 298 | "ps -ef|grep -v grep|grep elastalert", 299 | }, 300 | }, 301 | }, 302 | InitialDelaySeconds: 50, 303 | TimeoutSeconds: 3, 304 | PeriodSeconds: 2, 305 | SuccessThreshold: 1, 306 | FailureThreshold: 3, 307 | }, 308 | }, 309 | }, 310 | Volumes: []corev1.Volume{ 311 | // have to keep sequence 312 | { 313 | Name: "elasticsearch-cert", 314 | VolumeSource: corev1.VolumeSource{ 315 | Secret: &corev1.SecretVolumeSource{ 316 | SecretName: "test-elastalert-es-cert", 317 | }, 318 | }, 319 | }, 320 | { 321 | Name: "test-elastalert-config", 322 | VolumeSource: corev1.VolumeSource{ 323 | ConfigMap: &corev1.ConfigMapVolumeSource{ 324 | LocalObjectReference: corev1.LocalObjectReference{ 325 | Name: "test-elastalert-config", 326 | }, 327 | }, 328 | }, 329 | }, 330 | { 331 | Name: "test-elastalert-rule", 332 | VolumeSource: corev1.VolumeSource{ 333 | ConfigMap: &corev1.ConfigMapVolumeSource{ 334 | LocalObjectReference: corev1.LocalObjectReference{ 335 | Name: "test-elastalert-rule", 336 | }, 337 | }, 338 | }, 339 | }, 340 | }, 341 | Affinity: &corev1.Affinity{ 342 | PodAntiAffinity: &corev1.PodAntiAffinity{}, 343 | }, 344 | }, 345 | }, 346 | }, 347 | Status: appsv1.DeploymentStatus{ 348 | AvailableReplicas: int32(1), 349 | }, 350 | }, 351 | ).Build(), 352 | eaPhase: v1alpha1.ElastAlertPhraseSucceeded, 353 | }, 354 | { 355 | client: fake.NewClientBuilder().WithRuntimeObjects( 356 | &v1alpha1.Elastalert{ 357 | ObjectMeta: metav1.ObjectMeta{ 358 | Name: "elastalert", 359 | Namespace: "ns", 360 | }, 361 | Status: v1alpha1.ElastalertStatus{Phase: v1alpha1.ElastAlertPhraseFailed}, 362 | }, 363 | &appsv1.Deployment{ 364 | TypeMeta: metav1.TypeMeta{ 365 | Kind: "Deployment", 366 | APIVersion: "apps/v1", 367 | }, 368 | ObjectMeta: metav1.ObjectMeta{ 369 | ResourceVersion: "1", 370 | Namespace: "ns", 371 | Name: "elastalert", 372 | OwnerReferences: []metav1.OwnerReference{ 373 | { 374 | APIVersion: "v1", 375 | Kind: "Elastalert", 376 | Name: "elastalert", 377 | UID: "", 378 | Controller: &varTrue, 379 | BlockOwnerDeletion: &varTrue, 380 | }, 381 | }, 382 | }, 383 | Spec: appsv1.DeploymentSpec{ 384 | Replicas: &Replicas, 385 | Selector: &metav1.LabelSelector{ 386 | MatchLabels: map[string]string{"app": "elastalert"}, 387 | }, 388 | Template: corev1.PodTemplateSpec{ 389 | ObjectMeta: metav1.ObjectMeta{ 390 | Labels: map[string]string{ 391 | "app": "elastalert", 392 | }, 393 | Annotations: map[string]string{ 394 | "kubectl.kubernetes.io/restartedAt": "2021-05-17T01:38:44+08:00", 395 | }, 396 | }, 397 | 398 | Spec: corev1.PodSpec{ 399 | AutomountServiceAccountToken: &varTrue, 400 | TerminationGracePeriodSeconds: &TerminationGracePeriodSeconds, 401 | Containers: []corev1.Container{ 402 | { 403 | Name: "elastalert", 404 | Image: "toughnoah/elastalert:v1.0", 405 | VolumeMounts: []corev1.VolumeMount{ 406 | // have to keep sequence 407 | { 408 | Name: "elasticsearch-cert", 409 | MountPath: "/ssl", 410 | }, 411 | { 412 | Name: "test-elastalert-config", 413 | MountPath: "/etc/elastalert", 414 | }, 415 | { 416 | Name: "test-elastalert-rule", 417 | MountPath: "/etc/elastalert/rules", 418 | }, 419 | }, 420 | Command: []string{"elastalert", "--config", "/etc/elastalert/config.yaml", "--verbose"}, 421 | Resources: corev1.ResourceRequirements{ 422 | Requests: map[corev1.ResourceName]resource.Quantity{ 423 | corev1.ResourceMemory: resource.MustParse("2Gi"), 424 | }, 425 | Limits: map[corev1.ResourceName]resource.Quantity{ 426 | corev1.ResourceMemory: resource.MustParse("2Gi"), 427 | }, 428 | }, 429 | Ports: []corev1.ContainerPort{ 430 | {Name: "http", ContainerPort: 8080, Protocol: corev1.ProtocolTCP}, 431 | }, 432 | ReadinessProbe: &corev1.Probe{ 433 | Handler: corev1.Handler{ 434 | Exec: &corev1.ExecAction{ 435 | Command: []string{ 436 | "cat", 437 | "/etc/elastalert/config.yaml", 438 | }, 439 | }, 440 | }, 441 | InitialDelaySeconds: 20, 442 | TimeoutSeconds: 3, 443 | PeriodSeconds: 2, 444 | SuccessThreshold: 5, 445 | FailureThreshold: 3, 446 | }, 447 | LivenessProbe: &corev1.Probe{ 448 | Handler: corev1.Handler{ 449 | Exec: &corev1.ExecAction{ 450 | Command: []string{ 451 | "sh", 452 | "-c", 453 | "ps -ef|grep -v grep|grep elastalert", 454 | }, 455 | }, 456 | }, 457 | InitialDelaySeconds: 50, 458 | TimeoutSeconds: 3, 459 | PeriodSeconds: 2, 460 | SuccessThreshold: 1, 461 | FailureThreshold: 3, 462 | }, 463 | }, 464 | }, 465 | Volumes: []corev1.Volume{ 466 | // have to keep sequence 467 | { 468 | Name: "elasticsearch-cert", 469 | VolumeSource: corev1.VolumeSource{ 470 | Secret: &corev1.SecretVolumeSource{ 471 | SecretName: "test-elastalert-es-cert", 472 | }, 473 | }, 474 | }, 475 | { 476 | Name: "test-elastalert-config", 477 | VolumeSource: corev1.VolumeSource{ 478 | ConfigMap: &corev1.ConfigMapVolumeSource{ 479 | LocalObjectReference: corev1.LocalObjectReference{ 480 | Name: "test-elastalert-config", 481 | }, 482 | }, 483 | }, 484 | }, 485 | { 486 | Name: "test-elastalert-rule", 487 | VolumeSource: corev1.VolumeSource{ 488 | ConfigMap: &corev1.ConfigMapVolumeSource{ 489 | LocalObjectReference: corev1.LocalObjectReference{ 490 | Name: "test-elastalert-rule", 491 | }, 492 | }, 493 | }, 494 | }, 495 | }, 496 | Affinity: &corev1.Affinity{ 497 | PodAntiAffinity: &corev1.PodAntiAffinity{}, 498 | }, 499 | }, 500 | }, 501 | }, 502 | Status: appsv1.DeploymentStatus{ 503 | AvailableReplicas: int32(1), 504 | }, 505 | }, 506 | ).Build(), 507 | eaPhase: v1alpha1.ElastAlertPhraseSucceeded, 508 | }, 509 | { 510 | client: fake.NewClientBuilder().WithRuntimeObjects( 511 | &v1alpha1.Elastalert{ 512 | ObjectMeta: metav1.ObjectMeta{ 513 | Name: "elastalert", 514 | Namespace: "ns", 515 | }, 516 | Status: v1alpha1.ElastalertStatus{Phase: v1alpha1.ElastAlertPhraseSucceeded}, 517 | }, 518 | ).Build(), 519 | eaPhase: v1alpha1.ElastAlertPhraseFailed, 520 | }, 521 | } 522 | 523 | Context("test observing", func() { 524 | It("test checkDeploymentHeath", func() { 525 | for _, tc := range testCases { 526 | 527 | ob := NewObserver(tc.client, ea, time.Second*2, recoder) 528 | ob.Start() 529 | defer ob.Stop() 530 | Eventually(func() bool { 531 | elastalert := &v1alpha1.Elastalert{} 532 | _ = tc.client.Get(context.Background(), ea, elastalert) 533 | return elastalert.Status.Phase == tc.eaPhase 534 | }, 2*time.Minute, time.Second).Should(Equal(true)) 535 | } 536 | }) 537 | }) 538 | Context("test manager", func() { 539 | It("test manager observes", func() { 540 | elastalert := &v1alpha1.Elastalert{ 541 | ObjectMeta: metav1.ObjectMeta{ 542 | Name: "elastalert", 543 | Namespace: "ns", 544 | }, 545 | Status: v1alpha1.ElastalertStatus{Phase: v1alpha1.ElastAlertPhraseSucceeded}, 546 | } 547 | client := fake.NewClientBuilder().Build() 548 | manager := NewManager() 549 | manager.Observe(elastalert, client, recoder) 550 | defer manager.StopObserving(ea) 551 | Eventually(func() bool { 552 | _, ok := manager.getObserver(ea) 553 | return ok 554 | }, time.Second*10, time.Second).Should(Equal(true)) 555 | }) 556 | }) 557 | }) 558 | -------------------------------------------------------------------------------- /controllers/podspec/configmap.go: -------------------------------------------------------------------------------- 1 | package podspec 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | esv1alpha1 "github.com/toughnoah/elastalert-operator/api/v1alpha1" 7 | "gopkg.in/yaml.v2" 8 | corev1 "k8s.io/api/core/v1" 9 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 10 | "k8s.io/apimachinery/pkg/runtime" 11 | ctrl "sigs.k8s.io/controller-runtime" 12 | ) 13 | 14 | func GenerateNewConfigmap(Scheme *runtime.Scheme, e *esv1alpha1.Elastalert, suffix string) (*corev1.ConfigMap, error) { 15 | var data = make(map[string]string) 16 | var err error 17 | switch suffix { 18 | case esv1alpha1.RuleSuffx: 19 | data, err = GenerateYamlMap(e.Spec.Rule) 20 | if err != nil { 21 | log.Error( 22 | err, 23 | "Failed to generate rules configmaps", 24 | "Elastalert.Namespace", e.Namespace, 25 | "Configmaps.Namespace", e.Namespace, 26 | ) 27 | return nil, err 28 | } 29 | case esv1alpha1.ConfigSuffx: 30 | rawMap, err := e.Spec.ConfigSetting.GetMap() 31 | out, err := yaml.Marshal(rawMap) 32 | if err != nil { 33 | log.Error( 34 | err, 35 | "Failed to generate config.yaml configmaps", 36 | "Elastalert.Namespace", e.Namespace, 37 | "Configmaps.Namespace", e.Namespace, 38 | ) 39 | return nil, err 40 | } 41 | data["config.yaml"] = string(out) 42 | } 43 | cm := &corev1.ConfigMap{ 44 | ObjectMeta: metav1.ObjectMeta{ 45 | Name: e.Name + suffix, 46 | Namespace: e.Namespace, 47 | }, 48 | Data: data, 49 | } 50 | err = ctrl.SetControllerReference(e, cm, Scheme) 51 | if err != nil { 52 | log.Error( 53 | err, 54 | "Failed to generate configmaps", 55 | "Elastalert.Namespace", e.Namespace, 56 | "Configmaps.Namespace", e.Namespace, 57 | ) 58 | return nil, err 59 | } 60 | return cm, nil 61 | } 62 | 63 | // PatchConfigSettings TODO should change to Chain Of Responsibility 64 | func PatchConfigSettings(e *esv1alpha1.Elastalert, stringCert string) error { 65 | config, err := e.Spec.ConfigSetting.GetMap() 66 | if err != nil { 67 | return errors.New("get config failed") 68 | } 69 | rawConfig := &RawConfig{ 70 | config: config, 71 | cert: stringCert, 72 | } 73 | drHandler := &DefaultRulesFolderHandler{} 74 | useSSLHandler := &UseSSLHandler{} 75 | drHandler.setNext(useSSLHandler) 76 | 77 | addCertHandler := &AddCertHandler{} 78 | useSSLHandler.setNext(addCertHandler) 79 | 80 | verifyCertHandler := &VerifyCertHandler{} 81 | addCertHandler.setNext(verifyCertHandler) 82 | 83 | drHandler.handle(rawConfig) 84 | if rawConfig.err != nil { 85 | return rawConfig.err 86 | } 87 | e.Spec.ConfigSetting = esv1alpha1.NewFreeForm(rawConfig.config) 88 | return nil 89 | } 90 | 91 | func ConfigMapsToMap(cms []corev1.ConfigMap) map[string]corev1.ConfigMap { 92 | m := map[string]corev1.ConfigMap{} 93 | for _, d := range cms { 94 | m[d.Name] = d 95 | } 96 | return m 97 | } 98 | 99 | func GenerateYamlMap(ruleArray []esv1alpha1.FreeForm) (map[string]string, error) { 100 | var data = map[string]string{} 101 | for _, v := range ruleArray { 102 | m, err := v.GetMap() 103 | if err != nil { 104 | return nil, err 105 | } 106 | key := fmt.Sprintf("%s.yaml", m["name"]) 107 | out, err := yaml.Marshal(m) 108 | if err != nil { 109 | return nil, err 110 | } 111 | data[key] = string(out) 112 | 113 | } 114 | return data, nil 115 | } 116 | 117 | func PatchAlertSettings(e *esv1alpha1.Elastalert) error { 118 | var ruleArray []esv1alpha1.FreeForm 119 | alert, err := e.Spec.Alert.GetMap() 120 | if err != nil { 121 | return err 122 | } 123 | if alert == nil { 124 | return nil 125 | } 126 | for _, v := range e.Spec.Rule { 127 | rule, err := v.GetMap() 128 | if err != nil { 129 | return err 130 | } 131 | if rule["alert"] == nil { 132 | MergeInterfaceMap(rule, alert) 133 | } 134 | ruleArray = append(ruleArray, esv1alpha1.NewFreeForm(rule)) 135 | } 136 | e.Spec.Rule = ruleArray 137 | 138 | return nil 139 | } 140 | 141 | type RawConfig struct { 142 | config map[string]interface{} 143 | cert string 144 | err error 145 | useSSL bool 146 | } 147 | 148 | type handler interface { 149 | handle(config *RawConfig) 150 | setNext(handler handler) 151 | } 152 | 153 | type DefaultRulesFolderHandler struct { 154 | next handler 155 | } 156 | 157 | func (d *DefaultRulesFolderHandler) handle(raw *RawConfig) { 158 | if raw.err != nil { 159 | return 160 | } 161 | if raw.config == nil { 162 | raw.err = errors.New("get config map failed") 163 | } else { 164 | raw.config["rules_folder"] = DefaultRulesFolder 165 | } 166 | d.next.handle(raw) 167 | } 168 | 169 | func (d *DefaultRulesFolderHandler) setNext(next handler) { 170 | d.next = next 171 | } 172 | 173 | type UseSSLHandler struct { 174 | next handler 175 | } 176 | 177 | func (u *UseSSLHandler) handle(raw *RawConfig) { 178 | if raw.err != nil { 179 | return 180 | } 181 | if raw.config["use_ssl"] != nil { 182 | useSSL, ok := raw.config["use_ssl"].(bool) 183 | if !ok { 184 | raw.err = errors.New("error type for 'use_ssl', want bool") 185 | } else { 186 | raw.useSSL = useSSL 187 | } 188 | } else { 189 | raw.useSSL = false 190 | } 191 | u.next.handle(raw) 192 | } 193 | func (u *UseSSLHandler) setNext(next handler) { 194 | u.next = next 195 | } 196 | 197 | type AddCertHandler struct { 198 | next handler 199 | } 200 | 201 | func (a *AddCertHandler) handle(raw *RawConfig) { 202 | if raw.err != nil { 203 | return 204 | } 205 | if raw.useSSL { 206 | if raw.cert != "" { 207 | raw.config["verify_certs"] = true 208 | raw.config["ca_certs"] = DefaultElasticCertPath 209 | } else { 210 | raw.config["verify_certs"] = false 211 | } 212 | } else { 213 | delete(raw.config, "verify_certs") 214 | delete(raw.config, "ca_certs") 215 | } 216 | a.next.handle(raw) 217 | } 218 | func (a *AddCertHandler) setNext(next handler) { 219 | a.next = next 220 | } 221 | 222 | type VerifyCertHandler struct { 223 | next handler 224 | } 225 | 226 | func (v *VerifyCertHandler) handle(raw *RawConfig) { 227 | if raw.err != nil { 228 | return 229 | } 230 | if raw.config["verify_certs"] != nil { 231 | vc, ok := raw.config["verify_certs"].(bool) 232 | if !ok { 233 | raw.err = errors.New("error type for 'verify_certs', want bool") 234 | } else if vc == false && raw.cert != "" { 235 | delete(raw.config, "ca_certs") 236 | } 237 | } 238 | } 239 | func (v *VerifyCertHandler) setNext(next handler) { 240 | v.next = next 241 | } 242 | -------------------------------------------------------------------------------- /controllers/podspec/configmap_test.go: -------------------------------------------------------------------------------- 1 | package podspec 2 | 3 | import ( 4 | "github.com/stretchr/testify/require" 5 | esv1alpha1 "github.com/toughnoah/elastalert-operator/api/v1alpha1" 6 | "gopkg.in/yaml.v2" 7 | corev1 "k8s.io/api/core/v1" 8 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 | "k8s.io/client-go/kubernetes/scheme" 10 | "reflect" 11 | "testing" 12 | ) 13 | 14 | var ( 15 | wantUseSSLAndCertUndefined = ` 16 | use_ssl: True 17 | rules_folder: /etc/elastalert/rules/..data/ 18 | verify_certs: False 19 | ` 20 | 21 | wantNotUseSSL = ` 22 | use_ssl: False 23 | rules_folder: /etc/elastalert/rules/..data/ 24 | ` 25 | 26 | wantUseSSLAndCertDndefined = ` 27 | use_ssl: True 28 | rules_folder: /etc/elastalert/rules/..data/ 29 | verify_certs: True 30 | ca_certs: /ssl/elasticCA.crt 31 | 32 | ` 33 | 34 | wantRulesFolder = ` 35 | rules_folder: /etc/elastalert/rules/..data/ 36 | ` 37 | ) 38 | 39 | func TestPatchConfigSettings(t *testing.T) { 40 | testCases := []struct { 41 | name string 42 | certString string 43 | elastalert *esv1alpha1.Elastalert 44 | want string 45 | }{ 46 | { 47 | name: "test use ssl and cert undefined", 48 | certString: "", 49 | elastalert: &esv1alpha1.Elastalert{ 50 | Spec: esv1alpha1.ElastalertSpec{ 51 | ConfigSetting: esv1alpha1.NewFreeForm(map[string]interface{}{ 52 | "use_ssl": true, 53 | }), 54 | }, 55 | }, 56 | want: wantUseSSLAndCertUndefined, 57 | }, 58 | { 59 | name: "test not use ssl", 60 | certString: "", 61 | elastalert: &esv1alpha1.Elastalert{ 62 | Spec: esv1alpha1.ElastalertSpec{ 63 | ConfigSetting: esv1alpha1.NewFreeForm(map[string]interface{}{ 64 | "use_ssl": false, 65 | }), 66 | }, 67 | }, 68 | want: wantNotUseSSL, 69 | }, 70 | { 71 | name: "test use ssl and cert defined", 72 | certString: "abc", 73 | elastalert: &esv1alpha1.Elastalert{ 74 | Spec: esv1alpha1.ElastalertSpec{ 75 | ConfigSetting: esv1alpha1.NewFreeForm(map[string]interface{}{ 76 | "use_ssl": true, 77 | "verify_certs": true, 78 | }), 79 | }, 80 | }, 81 | want: wantUseSSLAndCertDndefined, 82 | }, 83 | { 84 | name: "test add rules folder", 85 | certString: "abc", 86 | elastalert: &esv1alpha1.Elastalert{ 87 | Spec: esv1alpha1.ElastalertSpec{ 88 | ConfigSetting: esv1alpha1.NewFreeForm(map[string]interface{}{}), 89 | }, 90 | }, 91 | want: wantRulesFolder, 92 | }, 93 | } 94 | for _, tc := range testCases { 95 | t.Run(tc.name, func(t *testing.T) { 96 | var want map[string]interface{} 97 | err := PatchConfigSettings(tc.elastalert, tc.certString) 98 | require.NoError(t, err) 99 | err = yaml.Unmarshal([]byte(tc.want), &want) 100 | require.NoError(t, err) 101 | have, err := tc.elastalert.Spec.ConfigSetting.GetMap() 102 | require.Equal(t, want, have) 103 | }) 104 | } 105 | } 106 | 107 | func TestConfigMapsToMap(t *testing.T) { 108 | testCases := []struct { 109 | name string 110 | confimaps []corev1.ConfigMap 111 | want map[string]corev1.ConfigMap 112 | }{ 113 | { 114 | name: "test configmaps to map", 115 | confimaps: []corev1.ConfigMap{ 116 | { 117 | ObjectMeta: metav1.ObjectMeta{ 118 | Name: "myconfigmap1", 119 | }, 120 | }, 121 | { 122 | ObjectMeta: metav1.ObjectMeta{ 123 | Name: "myconfigmap2", 124 | }, 125 | }, 126 | }, 127 | want: map[string]corev1.ConfigMap{ 128 | "myconfigmap1": { 129 | ObjectMeta: metav1.ObjectMeta{ 130 | Name: "myconfigmap1", 131 | }, 132 | }, 133 | "myconfigmap2": { 134 | ObjectMeta: metav1.ObjectMeta{ 135 | Name: "myconfigmap2", 136 | }, 137 | }, 138 | }, 139 | }, 140 | } 141 | for _, tc := range testCases { 142 | t.Run(tc.name, func(t *testing.T) { 143 | have := ConfigMapsToMap(tc.confimaps) 144 | if !reflect.DeepEqual(have, tc.want) { 145 | t.Errorf("podspec.ConfigMapsToMap() = %v, want %v", have, tc.want) 146 | } 147 | }) 148 | } 149 | } 150 | 151 | func TestGenerateNewConfigmap(t *testing.T) { 152 | testCases := []struct { 153 | name string 154 | elastalert esv1alpha1.Elastalert 155 | suffx string 156 | want corev1.ConfigMap 157 | }{ 158 | { 159 | name: "test generate default config", 160 | suffx: "-config", 161 | elastalert: esv1alpha1.Elastalert{ 162 | ObjectMeta: metav1.ObjectMeta{ 163 | Name: "test-elastalert", 164 | }, 165 | Spec: esv1alpha1.ElastalertSpec{ 166 | ConfigSetting: esv1alpha1.NewFreeForm(map[string]interface{}{ 167 | "config": "test", 168 | }), 169 | }, 170 | }, 171 | want: corev1.ConfigMap{ 172 | ObjectMeta: metav1.ObjectMeta{ 173 | Name: "test-elastalert-config", 174 | OwnerReferences: []metav1.OwnerReference{ 175 | { 176 | APIVersion: "v1", 177 | Kind: "Elastalert", 178 | Name: "test-elastalert", 179 | UID: "", 180 | Controller: &varTrue, 181 | BlockOwnerDeletion: &varTrue, 182 | }, 183 | }, 184 | }, 185 | Data: map[string]string{ 186 | "config.yaml": "config: test\n", 187 | }, 188 | }, 189 | }, 190 | { 191 | name: "test generate default rule", 192 | suffx: "-rule", 193 | elastalert: esv1alpha1.Elastalert{ 194 | ObjectMeta: metav1.ObjectMeta{ 195 | Name: "test-elastalert", 196 | }, 197 | Spec: esv1alpha1.ElastalertSpec{ 198 | ConfigSetting: esv1alpha1.NewFreeForm(map[string]interface{}{ 199 | "config": "test", 200 | }), 201 | Rule: []esv1alpha1.FreeForm{ 202 | esv1alpha1.NewFreeForm(map[string]interface{}{ 203 | "name": "test-elastalert", "type": "any", 204 | }), 205 | }, 206 | }, 207 | }, 208 | want: corev1.ConfigMap{ 209 | ObjectMeta: metav1.ObjectMeta{ 210 | Name: "test-elastalert-rule", 211 | OwnerReferences: []metav1.OwnerReference{ 212 | { 213 | APIVersion: "v1", 214 | Kind: "Elastalert", 215 | Name: "test-elastalert", 216 | UID: "", 217 | Controller: &varTrue, 218 | BlockOwnerDeletion: &varTrue, 219 | }, 220 | }, 221 | }, 222 | Data: map[string]string{ 223 | "test-elastalert.yaml": "name: test-elastalert\ntype: any\n", 224 | }, 225 | }, 226 | }, 227 | } 228 | for _, tc := range testCases { 229 | t.Run(tc.name, func(t *testing.T) { 230 | s := scheme.Scheme 231 | s.AddKnownTypes(corev1.SchemeGroupVersion, &esv1alpha1.Elastalert{}) 232 | have, err := GenerateNewConfigmap(s, &tc.elastalert, tc.suffx) 233 | require.NoError(t, err) 234 | require.Equal(t, tc.want, *have) 235 | }) 236 | } 237 | } 238 | 239 | func TestGenerateYamlMap(t *testing.T) { 240 | testCases := []struct { 241 | name string 242 | maparray []esv1alpha1.FreeForm 243 | want map[string]string 244 | }{ 245 | { 246 | name: "test generate yaml map", 247 | maparray: []esv1alpha1.FreeForm{ 248 | esv1alpha1.NewFreeForm(map[string]interface{}{ 249 | "name": "test-elastalert", "type": "any", 250 | }), 251 | esv1alpha1.NewFreeForm(map[string]interface{}{ 252 | "name": "test-elastalert2", "type": "aggs", 253 | }), 254 | }, 255 | want: map[string]string{ 256 | "test-elastalert.yaml": "name: test-elastalert\ntype: any\n", 257 | "test-elastalert2.yaml": "name: test-elastalert2\ntype: aggs\n", 258 | }, 259 | }, 260 | } 261 | for _, tc := range testCases { 262 | have, err := GenerateYamlMap(tc.maparray) 263 | require.NoError(t, err) 264 | require.Equal(t, tc.want, have) 265 | } 266 | } 267 | 268 | func TestPatchAlertSettings(t *testing.T) { 269 | testCases := []struct { 270 | name string 271 | certString string 272 | elastalert *esv1alpha1.Elastalert 273 | want esv1alpha1.Elastalert 274 | }{ 275 | { 276 | name: "test use global alert", 277 | certString: "", 278 | elastalert: &esv1alpha1.Elastalert{ 279 | Spec: esv1alpha1.ElastalertSpec{ 280 | ConfigSetting: esv1alpha1.NewFreeForm(map[string]interface{}{ 281 | "use_ssl": true, 282 | }), 283 | Rule: []esv1alpha1.FreeForm{ 284 | esv1alpha1.NewFreeForm(map[string]interface{}{ 285 | "name": "test-elastalert1", "type": "any", 286 | }), 287 | esv1alpha1.NewFreeForm(map[string]interface{}{ 288 | "name": "test-elastalert2", "type": "any", 289 | }), 290 | }, 291 | Alert: esv1alpha1.NewFreeForm(map[string]interface{}{ 292 | "alert": []string{"post"}, "http_post_url": "https://test.com", 293 | }), 294 | }, 295 | }, 296 | want: esv1alpha1.Elastalert{ 297 | Spec: esv1alpha1.ElastalertSpec{ 298 | ConfigSetting: esv1alpha1.NewFreeForm(map[string]interface{}{ 299 | "use_ssl": true, 300 | }), 301 | Rule: []esv1alpha1.FreeForm{ 302 | esv1alpha1.NewFreeForm(map[string]interface{}{ 303 | "name": "test-elastalert1", "type": "any", "alert": []string{"post"}, "http_post_url": "https://test.com", 304 | }), 305 | esv1alpha1.NewFreeForm(map[string]interface{}{ 306 | "name": "test-elastalert2", "type": "any", "alert": []string{"post"}, "http_post_url": "https://test.com", 307 | }), 308 | }, 309 | Alert: esv1alpha1.NewFreeForm(map[string]interface{}{ 310 | "alert": []string{"post"}, "http_post_url": "https://test.com", 311 | }), 312 | }, 313 | }, 314 | }, 315 | { 316 | name: "test not global alert", 317 | certString: "", 318 | elastalert: &esv1alpha1.Elastalert{ 319 | Spec: esv1alpha1.ElastalertSpec{ 320 | ConfigSetting: esv1alpha1.NewFreeForm(map[string]interface{}{ 321 | "use_ssl": true, 322 | }), 323 | Rule: []esv1alpha1.FreeForm{ 324 | esv1alpha1.NewFreeForm(map[string]interface{}{ 325 | "name": "test-elastalert1", "type": "any", "alert": []string{"get"}, "http_post_url": "https://elatalert.com", 326 | }), 327 | esv1alpha1.NewFreeForm(map[string]interface{}{ 328 | "name": "test-elastalert2", "type": "any", 329 | }), 330 | }, 331 | Alert: esv1alpha1.NewFreeForm(map[string]interface{}{ 332 | "alert": []string{"post"}, "http_post_url": "https://test.com", 333 | }), 334 | }, 335 | }, 336 | want: esv1alpha1.Elastalert{ 337 | Spec: esv1alpha1.ElastalertSpec{ 338 | ConfigSetting: esv1alpha1.NewFreeForm(map[string]interface{}{ 339 | "use_ssl": true, 340 | }), 341 | Rule: []esv1alpha1.FreeForm{ 342 | esv1alpha1.NewFreeForm(map[string]interface{}{ 343 | "name": "test-elastalert1", "type": "any", "alert": []string{"get"}, "http_post_url": "https://elatalert.com", 344 | }), 345 | esv1alpha1.NewFreeForm(map[string]interface{}{ 346 | "name": "test-elastalert2", "type": "any", "alert": []string{"post"}, "http_post_url": "https://test.com", 347 | }), 348 | }, 349 | Alert: esv1alpha1.NewFreeForm(map[string]interface{}{ 350 | "alert": []string{"post"}, "http_post_url": "https://test.com", 351 | }), 352 | }, 353 | }, 354 | }, 355 | } 356 | for _, tc := range testCases { 357 | t.Run(tc.name, func(t *testing.T) { 358 | err := PatchAlertSettings(tc.elastalert) 359 | require.NoError(t, err) 360 | require.Equal(t, tc.want, *tc.elastalert) 361 | }) 362 | } 363 | } 364 | -------------------------------------------------------------------------------- /controllers/podspec/defaultattrs.go: -------------------------------------------------------------------------------- 1 | // Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one 2 | // or more contributor license agreements. Licensed under the Elastic License; 3 | // you may not use this file except in compliance with the Elastic License. 4 | 5 | package podspec 6 | 7 | import ( 8 | corev1 "k8s.io/api/core/v1" 9 | "k8s.io/apimachinery/pkg/api/resource" 10 | ) 11 | 12 | const ( 13 | // DefaultTerminationGracePeriodSeconds is the termination grace period for the Elasalert containers 14 | DefaultTerminationGracePeriodSeconds int64 = 10 15 | DefaultElastAlertName = "elastalert" 16 | DefautlImage = "toughnoah/elastalert:v1.0" 17 | DefaultCertVolumeName = "elasticsearch-cert" 18 | DefaultCertSuffix = "-es-cert" 19 | DefaultCertMountPath = "/ssl" 20 | DefaultElasticCertName = "elasticCA.crt" 21 | DefaultRulesFolder = "/etc/elastalert/rules/..data/" 22 | DefaultElasticCertPath = "/ssl/elasticCA.crt" 23 | ) 24 | 25 | var ( 26 | DefaultMemoryLimits = resource.MustParse("2Gi") 27 | // DefaultResources for the Elasalert container. The JVM default heap size is 1Gi, so we 28 | // request at least 2Gi for the container to make sure ES can work properly. 29 | // Not applying this minimum default would make Elasalert randomly crash (OOM) on small machines. 30 | // Similarly, we apply a default memory limit of 2Gi, to ensure the Pod isn't the first one to get evicted. 31 | // No CPU requirement is set by default. 32 | DefaultResources = corev1.ResourceRequirements{ 33 | Requests: map[corev1.ResourceName]resource.Quantity{ 34 | corev1.ResourceMemory: DefaultMemoryLimits, 35 | }, 36 | Limits: map[corev1.ResourceName]resource.Quantity{ 37 | corev1.ResourceMemory: DefaultMemoryLimits, 38 | }, 39 | } 40 | ) 41 | 42 | // DefaultAffinity returns the default affinity for pods in a cluster. 43 | func DefaultAffinity(esName string) *corev1.Affinity { 44 | return &corev1.Affinity{ 45 | // prefer to avoid two pods in the same cluster being co-located on a single node 46 | PodAntiAffinity: &corev1.PodAntiAffinity{ 47 | PreferredDuringSchedulingIgnoredDuringExecution: []corev1.WeightedPodAffinityTerm{}, 48 | }, 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /controllers/podspec/defaulter.go: -------------------------------------------------------------------------------- 1 | // Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one 2 | // or more contributor license agreements. Licensed under the Elastic License; 3 | // you may not use this file except in compliance with the Elastic License. 4 | 5 | package podspec 6 | 7 | import ( 8 | "sort" 9 | 10 | corev1 "k8s.io/api/core/v1" 11 | ) 12 | 13 | // Defaulter ensures that values are set if none exists in the base container. 14 | type Defaulter struct { 15 | base *corev1.Container 16 | } 17 | 18 | // Container returns a copy of the resulting container. 19 | func (d Defaulter) Container() corev1.Container { 20 | return *d.base.DeepCopy() 21 | } 22 | 23 | func NewDefaulter(base *corev1.Container) Defaulter { 24 | return Defaulter{ 25 | base: base, 26 | } 27 | } 28 | 29 | // From inherits default values from an other container. 30 | func (d Defaulter) From(other corev1.Container) Defaulter { 31 | if other.Lifecycle != nil { 32 | d.WithPreStopHook(other.Lifecycle.PreStop) 33 | } 34 | 35 | return d. 36 | WithImage(other.Image). 37 | WithCommand(other.Command). 38 | WithArgs(other.Args). 39 | WithPorts(other.Ports). 40 | WithEnv(other.Env). 41 | WithResources(other.Resources). 42 | WithVolumeMounts(other.VolumeMounts). 43 | WithReadinessProbe(other.ReadinessProbe) 44 | } 45 | 46 | func (d Defaulter) WithCommand(command []string) Defaulter { 47 | if len(d.base.Command) == 0 { 48 | d.base.Command = command 49 | } 50 | return d 51 | } 52 | 53 | func (d Defaulter) WithArgs(args []string) Defaulter { 54 | if len(d.base.Args) == 0 { 55 | d.base.Args = args 56 | } 57 | return d 58 | } 59 | 60 | func (d Defaulter) WithPorts(ports []corev1.ContainerPort) Defaulter { 61 | for _, p := range ports { 62 | if !d.portExists(p.Name) { 63 | d.base.Ports = append(d.base.Ports, p) 64 | } 65 | } 66 | // order ports by name to ensure stable pod spec comparison 67 | sort.SliceStable(d.base.Ports, func(i, j int) bool { 68 | return d.base.Ports[i].Name < d.base.Ports[j].Name 69 | }) 70 | return d 71 | } 72 | 73 | // portExists checks if a port with the given name already exists in the Container. 74 | func (d Defaulter) portExists(name string) bool { 75 | for _, p := range d.base.Ports { 76 | if p.Name == name { 77 | return true 78 | } 79 | } 80 | return false 81 | } 82 | 83 | // WithImage sets up the Container Docker image, unless already provided. 84 | // The default image will be used unless customImage is not empty. 85 | func (d Defaulter) WithImage(image string) Defaulter { 86 | if d.base.Image == "" { 87 | d.base.Image = image 88 | } 89 | return d 90 | } 91 | 92 | func (d Defaulter) WithReadinessProbe(readinessProbe *corev1.Probe) Defaulter { 93 | if d.base.ReadinessProbe == nil { 94 | d.base.ReadinessProbe = readinessProbe 95 | } 96 | return d 97 | } 98 | 99 | func (d Defaulter) WithLivenessProbe(LivenessProbe *corev1.Probe) Defaulter { 100 | if d.base.LivenessProbe == nil { 101 | d.base.LivenessProbe = LivenessProbe 102 | } 103 | return d 104 | } 105 | 106 | // envExists checks if an env var with the given name already exists in the provided slice. 107 | func (d Defaulter) envExists(name string) bool { 108 | for _, v := range d.base.Env { 109 | if v.Name == name { 110 | return true 111 | } 112 | } 113 | return false 114 | } 115 | 116 | func (d Defaulter) WithEnv(vars []corev1.EnvVar) Defaulter { 117 | for _, v := range vars { 118 | if !d.envExists(v.Name) { 119 | d.base.Env = append(d.base.Env, v) 120 | } 121 | } 122 | return d 123 | } 124 | 125 | // WithResources ensures that resource requirements are set in the container. 126 | func (d Defaulter) WithResources(resources corev1.ResourceRequirements) Defaulter { 127 | if d.base.Resources.Requests == nil && d.base.Resources.Limits == nil { 128 | d.base.Resources = resources 129 | } 130 | return d 131 | } 132 | 133 | // volumeExists checks if a volume mount with the given name already exists in the Container. 134 | func (d Defaulter) volumeMountExists(volumeMount corev1.VolumeMount) bool { 135 | for _, v := range d.base.VolumeMounts { 136 | if v.Name == volumeMount.Name || v.MountPath == volumeMount.MountPath { 137 | return true 138 | } 139 | } 140 | return false 141 | } 142 | 143 | func (d Defaulter) WithVolumeMounts(volumeMounts []corev1.VolumeMount) Defaulter { 144 | for _, v := range volumeMounts { 145 | if !d.volumeMountExists(v) { 146 | d.base.VolumeMounts = append(d.base.VolumeMounts, v) 147 | } 148 | } 149 | // order volume mounts by name to ensure stable pod spec comparison 150 | sort.SliceStable(d.base.VolumeMounts, func(i, j int) bool { 151 | return d.base.VolumeMounts[i].Name < d.base.VolumeMounts[j].Name 152 | }) 153 | return d 154 | } 155 | 156 | func (d Defaulter) WithPreStopHook(handler *corev1.Handler) Defaulter { 157 | if d.base.Lifecycle == nil { 158 | d.base.Lifecycle = &corev1.Lifecycle{} 159 | } 160 | 161 | if d.base.Lifecycle.PreStop == nil { 162 | // no user-provided hook, we can use our own 163 | d.base.Lifecycle.PreStop = handler 164 | } 165 | 166 | return d 167 | } 168 | 169 | // WithLabels sets the given labels, but does not override those that already exist. 170 | func (b *PodTemplateBuilder) WithLabels(labels map[string]string) *PodTemplateBuilder { 171 | b.PodTemplate.Labels = MergePreservingExistingKeys(b.PodTemplate.Labels, labels) 172 | return b 173 | } 174 | 175 | // WithAnnotations sets the given annotations, but does not override those that already exist. 176 | func (b *PodTemplateBuilder) WithAnnotations(annotations map[string]string) *PodTemplateBuilder { 177 | b.PodTemplate.Annotations = MergePreservingExistingKeys(b.PodTemplate.Annotations, annotations) 178 | return b 179 | } 180 | 181 | // WithDockerImage sets up the Container Docker image, unless already provided. 182 | // The default image will be used unless customImage is not empty. 183 | func (b *PodTemplateBuilder) WithDockerImage(customImage string, defaultImage string) *PodTemplateBuilder { 184 | if customImage != "" { 185 | b.containerDefaulter.WithImage(customImage) 186 | } else { 187 | b.containerDefaulter.WithImage(defaultImage) 188 | } 189 | return b 190 | } 191 | 192 | // WithReadinessProbe sets up the given readiness probe, unless already provided in the template. 193 | func (b *PodTemplateBuilder) WithReadinessProbe(readinessProbe corev1.Probe) *PodTemplateBuilder { 194 | b.containerDefaulter.WithReadinessProbe(&readinessProbe) 195 | return b 196 | } 197 | 198 | // WithLivenessProbe sets up the given readiness probe, unless already provided in the template. 199 | func (b *PodTemplateBuilder) WithLivenessProbe(LivenessProbe corev1.Probe) *PodTemplateBuilder { 200 | b.containerDefaulter.WithLivenessProbe(&LivenessProbe) 201 | return b 202 | } 203 | 204 | // WithAffinity sets a default affinity, unless already provided in the template. 205 | // An empty affinity in the spec is not overridden. 206 | func (b *PodTemplateBuilder) WithAffinity(affinity *corev1.Affinity) *PodTemplateBuilder { 207 | if b.PodTemplate.Spec.Affinity == nil { 208 | b.PodTemplate.Spec.Affinity = affinity 209 | } 210 | return b 211 | } 212 | 213 | // WithPorts appends the given ports to the Container ports, unless already provided in the template. 214 | func (b *PodTemplateBuilder) WithPorts(ports []corev1.ContainerPort) *PodTemplateBuilder { 215 | b.containerDefaulter.WithPorts(ports) 216 | return b 217 | } 218 | 219 | // WithCommand sets the given command to the Container, unless already provided in the template. 220 | func (b *PodTemplateBuilder) WithCommand(command []string) *PodTemplateBuilder { 221 | b.containerDefaulter.WithCommand(command) 222 | return b 223 | } 224 | 225 | // volumeExists checks if a volume with the given name already exists in the Container. 226 | func (b *PodTemplateBuilder) volumeExists(name string) bool { 227 | for _, v := range b.PodTemplate.Spec.Volumes { 228 | if v.Name == name { 229 | return true 230 | } 231 | } 232 | return false 233 | } 234 | 235 | // WithVolumes appends the given volumes to the Container, unless already provided in the template. 236 | func (b *PodTemplateBuilder) WithVolumes(volumes ...corev1.Volume) *PodTemplateBuilder { 237 | for _, v := range volumes { 238 | if !b.volumeExists(v.Name) { 239 | b.PodTemplate.Spec.Volumes = append(b.PodTemplate.Spec.Volumes, v) 240 | } 241 | } 242 | // order volumes by name to ensure stable pod spec comparison 243 | sort.SliceStable(b.PodTemplate.Spec.Volumes, func(i, j int) bool { 244 | return b.PodTemplate.Spec.Volumes[i].Name < b.PodTemplate.Spec.Volumes[j].Name 245 | }) 246 | return b 247 | } 248 | 249 | // WithVolumeMounts appends the given volume mounts to the Container, unless already provided in the template. 250 | func (b *PodTemplateBuilder) WithVolumeMounts(volumeMounts ...corev1.VolumeMount) *PodTemplateBuilder { 251 | b.containerDefaulter.WithVolumeMounts(volumeMounts) 252 | return b 253 | } 254 | 255 | // WithEnv appends the given env vars to the Container, unless already provided in the template. 256 | func (b *PodTemplateBuilder) WithEnv(vars ...corev1.EnvVar) *PodTemplateBuilder { 257 | b.containerDefaulter.WithEnv(vars) 258 | return b 259 | } 260 | 261 | // WithTerminationGracePeriod sets the given termination grace period if not already specified in the template. 262 | func (b *PodTemplateBuilder) WithTerminationGracePeriod(period int64) *PodTemplateBuilder { 263 | if b.PodTemplate.Spec.TerminationGracePeriodSeconds == nil { 264 | b.PodTemplate.Spec.TerminationGracePeriodSeconds = &period 265 | } 266 | return b 267 | } 268 | 269 | // WithInitContainerDefaults sets default values for the current init containers. 270 | // 271 | // Defaults: 272 | // - If the init container contains an empty image field, it's inherited from the elastalert container. 273 | // - VolumeMounts from the elastalert container are added to the init container VolumeMounts, unless they would conflict 274 | // with a specified VolumeMount (by having the same VolumeMount.Name or VolumeMount.MountPath) 275 | // - default environment variables 276 | // 277 | // This method can also be used to set some additional environment variables. 278 | func (b *PodTemplateBuilder) WithInitContainerDefaults(additionalEnvVars ...corev1.EnvVar) *PodTemplateBuilder { 279 | elastalertContainer := b.containerDefaulter.Container() 280 | for i := range b.PodTemplate.Spec.InitContainers { 281 | b.PodTemplate.Spec.InitContainers[i] = 282 | NewDefaulter(&b.PodTemplate.Spec.InitContainers[i]). 283 | // Inherit image and volume mounts from elastalert container in the Pod 284 | WithImage(elastalertContainer.Image). 285 | WithVolumeMounts(elastalertContainer.VolumeMounts). 286 | Container() 287 | } 288 | return b 289 | } 290 | 291 | // findInitContainerByName attempts to find an init container with the given name in the template 292 | // Returns the index of the container or -1 if no init container by that name was found. 293 | func (b *PodTemplateBuilder) findInitContainerByName(name string) int { 294 | for i, c := range b.PodTemplate.Spec.InitContainers { 295 | if c.Name == name { 296 | return i 297 | } 298 | } 299 | return -1 300 | } 301 | 302 | // WithInitContainers includes the given init containers to the pod template. 303 | // 304 | // Ordering: 305 | // - Provided init containers are prepended to the existing ones in the template. 306 | // - If an init container by the same name already exists in the template, the two PodTemplates are merged, the values 307 | // provided by the user take precedence. 308 | func (b *PodTemplateBuilder) WithInitContainers( 309 | initContainers ...corev1.Container, 310 | ) *PodTemplateBuilder { 311 | var containers []corev1.Container 312 | 313 | for _, c := range initContainers { 314 | if index := b.findInitContainerByName(c.Name); index != -1 { 315 | userContainer := b.PodTemplate.Spec.InitContainers[index] 316 | 317 | // remove it from the podTemplate 318 | b.PodTemplate.Spec.InitContainers = append( 319 | b.PodTemplate.Spec.InitContainers[:index], 320 | b.PodTemplate.Spec.InitContainers[index+1:]..., 321 | ) 322 | 323 | // Create a container based on what the user specified but ensure that values 324 | // are set if none are provided. 325 | containers = append(containers, 326 | // Set the container provided by the user as the base. 327 | NewDefaulter(userContainer.DeepCopy()). 328 | // Inherit all other values from the container built by the controller. 329 | From(c). 330 | Container()) 331 | } else { 332 | containers = append(containers, c) 333 | } 334 | } 335 | b.PodTemplate.Spec.InitContainers = append(containers, b.PodTemplate.Spec.InitContainers...) 336 | return b 337 | } 338 | 339 | // WithResources sets up the given resource requirements if both resources limits and requests 340 | // are nil in the main container. 341 | // If a zero-value (empty map) for at least one of limits or request is provided, the given resource requirements 342 | // are not applied: the user may want to use a LimitRange. 343 | func (b *PodTemplateBuilder) WithResources(resources corev1.ResourceRequirements) *PodTemplateBuilder { 344 | b.containerDefaulter.WithResources(resources) 345 | return b 346 | } 347 | 348 | func (b *PodTemplateBuilder) WithPreStopHook(handler corev1.Handler) *PodTemplateBuilder { 349 | b.containerDefaulter.WithPreStopHook(&handler) 350 | return b 351 | } 352 | 353 | //func (b *PodTemplateBuilder) WithArgs(args ...string) *PodTemplateBuilder { 354 | // b.containerDefaulter.WithArgs(args) 355 | // return b 356 | //} 357 | 358 | //func (b *PodTemplateBuilder) WithServiceAccount(serviceAccount string) *PodTemplateBuilder { 359 | // if b.PodTemplate.Spec.ServiceAccountName == "" { 360 | // b.PodTemplate.Spec.ServiceAccountName = serviceAccount 361 | // } 362 | // return b 363 | //} 364 | 365 | //func (b *PodTemplateBuilder) WithHostNetwork() *PodTemplateBuilder { 366 | // b.PodTemplate.Spec.HostNetwork = true 367 | // return b 368 | //} 369 | // 370 | //func (b *PodTemplateBuilder) WithDNSPolicy(dnsPolicy corev1.DNSPolicy) *PodTemplateBuilder { 371 | // if b.PodTemplate.Spec.DNSPolicy == "" { 372 | // b.PodTemplate.Spec.DNSPolicy = dnsPolicy 373 | // } 374 | // return b 375 | //} 376 | // 377 | //func (b *PodTemplateBuilder) WithPodSecurityContext(securityContext corev1.PodSecurityContext) *PodTemplateBuilder { 378 | // if b.PodTemplate.Spec.SecurityContext == nil { 379 | // b.PodTemplate.Spec.SecurityContext = &securityContext 380 | // } 381 | // return b 382 | //} 383 | // 384 | //func (b *PodTemplateBuilder) WithAutomountServiceAccountToken() *PodTemplateBuilder { 385 | // if b.PodTemplate.Spec.AutomountServiceAccountToken == nil { 386 | // t := true 387 | // b.PodTemplate.Spec.AutomountServiceAccountToken = &t 388 | // } 389 | // return b 390 | //} 391 | 392 | //func NewPreStopHook() *corev1.Handler { 393 | // return &corev1.Handler{} 394 | //} 395 | -------------------------------------------------------------------------------- /controllers/podspec/defaulter_test.go: -------------------------------------------------------------------------------- 1 | // Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one 2 | // or more contributor license agreements. Licensed under the Elastic License; 3 | // you may not use this file except in compliance with the Elastic License. 4 | 5 | package podspec 6 | 7 | import ( 8 | "reflect" 9 | "testing" 10 | 11 | "github.com/stretchr/testify/require" 12 | corev1 "k8s.io/api/core/v1" 13 | "k8s.io/apimachinery/pkg/api/resource" 14 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 15 | ) 16 | 17 | var varFalse = false 18 | var varTrue = true 19 | 20 | func TestPodTemplateBuilder_setDefaults(t *testing.T) { 21 | tests := []struct { 22 | name string 23 | PodTemplate corev1.PodTemplateSpec 24 | containerName string 25 | container *corev1.Container 26 | want corev1.PodTemplateSpec 27 | }{ 28 | { 29 | name: "set defaults on empty pod template", 30 | PodTemplate: corev1.PodTemplateSpec{}, 31 | containerName: "mycontainer", 32 | want: corev1.PodTemplateSpec{ 33 | Spec: corev1.PodSpec{ 34 | AutomountServiceAccountToken: &varFalse, 35 | Containers: []corev1.Container{ 36 | { 37 | Name: "mycontainer", 38 | }, 39 | }, 40 | }, 41 | }, 42 | }, 43 | { 44 | name: "don't override user automount SA token", 45 | PodTemplate: corev1.PodTemplateSpec{ 46 | Spec: corev1.PodSpec{ 47 | AutomountServiceAccountToken: &varTrue, 48 | }, 49 | }, 50 | containerName: "mycontainer", 51 | want: corev1.PodTemplateSpec{ 52 | Spec: corev1.PodSpec{ 53 | AutomountServiceAccountToken: &varTrue, 54 | Containers: []corev1.Container{ 55 | { 56 | Name: "mycontainer", 57 | }, 58 | }, 59 | }, 60 | }, 61 | }, 62 | { 63 | name: "append Container on after user-provided ones", 64 | PodTemplate: corev1.PodTemplateSpec{ 65 | Spec: corev1.PodSpec{ 66 | Containers: []corev1.Container{ 67 | { 68 | Name: "usercontainer1", 69 | }, 70 | { 71 | Name: "usercontainer2", 72 | }, 73 | }, 74 | }, 75 | }, 76 | containerName: "mycontainer", 77 | want: corev1.PodTemplateSpec{ 78 | Spec: corev1.PodSpec{ 79 | AutomountServiceAccountToken: &varFalse, 80 | Containers: []corev1.Container{ 81 | { 82 | Name: "usercontainer1", 83 | }, 84 | { 85 | Name: "usercontainer2", 86 | }, 87 | { 88 | Name: "mycontainer", 89 | }, 90 | }, 91 | }, 92 | }, 93 | }, 94 | } 95 | for _, tt := range tests { 96 | t.Run(tt.name, func(t *testing.T) { 97 | b := &PodTemplateBuilder{ 98 | PodTemplate: tt.PodTemplate, 99 | containerName: tt.containerName, 100 | } 101 | if got := b.setDefaults().PodTemplate; !reflect.DeepEqual(got, tt.want) { 102 | t.Errorf("PodTemplateBuilder.setDefaults() = %v, want %v", got, tt.want) 103 | } 104 | }) 105 | } 106 | } 107 | 108 | func TestPodTemplateBuilder_WithLabels(t *testing.T) { 109 | tests := []struct { 110 | name string 111 | PodTemplate corev1.PodTemplateSpec 112 | labels map[string]string 113 | want map[string]string 114 | }{ 115 | { 116 | name: "append to but don't override user provided pod template labels", 117 | PodTemplate: corev1.PodTemplateSpec{ 118 | ObjectMeta: metav1.ObjectMeta{ 119 | Labels: map[string]string{ 120 | "a": "b", 121 | "c": "d", 122 | }, 123 | }, 124 | }, 125 | labels: map[string]string{ 126 | "a": "anothervalue", 127 | "e": "f", 128 | }, 129 | want: map[string]string{ 130 | "a": "b", 131 | "c": "d", 132 | "e": "f", 133 | }, 134 | }, 135 | } 136 | for _, tt := range tests { 137 | t.Run(tt.name, func(t *testing.T) { 138 | b := &PodTemplateBuilder{ 139 | PodTemplate: tt.PodTemplate, 140 | } 141 | if got := b.WithLabels(tt.labels).PodTemplate.Labels; !reflect.DeepEqual(got, tt.want) { 142 | t.Errorf("PodTemplateBuilder.WithLabels() = %v, want %v", got, tt.want) 143 | } 144 | }) 145 | } 146 | } 147 | 148 | func TestPodTemplateBuilder_WithDockerImage(t *testing.T) { 149 | containerName := "mycontainer" 150 | type args struct { 151 | customImage string 152 | defaultImage string 153 | } 154 | tests := []struct { 155 | name string 156 | podTemplate corev1.PodTemplateSpec 157 | args args 158 | want string 159 | }{ 160 | { 161 | name: "use default image if none provided", 162 | podTemplate: corev1.PodTemplateSpec{}, 163 | args: args{ 164 | customImage: "", 165 | defaultImage: "default-image", 166 | }, 167 | want: "default-image", 168 | }, 169 | { 170 | name: "use custom image if provided", 171 | podTemplate: corev1.PodTemplateSpec{}, 172 | args: args{ 173 | customImage: "custom-image", 174 | defaultImage: "default-image", 175 | }, 176 | want: "custom-image", 177 | }, 178 | { 179 | name: "use podTemplate Container image if provided", 180 | podTemplate: corev1.PodTemplateSpec{ 181 | Spec: corev1.PodSpec{ 182 | Containers: []corev1.Container{ 183 | { 184 | Name: containerName, 185 | Image: "Container-image", 186 | }, 187 | }, 188 | }, 189 | }, 190 | args: args{ 191 | customImage: "custom-image", 192 | defaultImage: "default-image", 193 | }, 194 | want: "Container-image", 195 | }, 196 | } 197 | for _, tt := range tests { 198 | t.Run(tt.name, func(t *testing.T) { 199 | b := NewPodTemplateBuilder(tt.podTemplate, containerName) 200 | if got := b.WithDockerImage(tt.args.customImage, tt.args.defaultImage).containerDefaulter.Container().Image; !reflect.DeepEqual(got, tt.want) { 201 | t.Errorf("PodTemplateBuilder.WithImage() = %v, want %v", got, tt.want) 202 | } 203 | }) 204 | } 205 | } 206 | 207 | func TestPodTemplateBuilder_WithReadinessProbe(t *testing.T) { 208 | containerName := "mycontainer" 209 | tests := []struct { 210 | name string 211 | PodTemplate corev1.PodTemplateSpec 212 | readinessProbe corev1.Probe 213 | want *corev1.Probe 214 | }{ 215 | { 216 | name: "no readiness probe in pod template: use default one", 217 | PodTemplate: corev1.PodTemplateSpec{}, 218 | readinessProbe: corev1.Probe{ 219 | Handler: corev1.Handler{ 220 | HTTPGet: &corev1.HTTPGetAction{ 221 | Path: "/probe", 222 | }, 223 | }, 224 | }, 225 | want: &corev1.Probe{ 226 | Handler: corev1.Handler{ 227 | HTTPGet: &corev1.HTTPGetAction{ 228 | Path: "/probe", 229 | }, 230 | }, 231 | }, 232 | }, 233 | { 234 | name: "don't override pod template readiness probe", 235 | PodTemplate: corev1.PodTemplateSpec{ 236 | Spec: corev1.PodSpec{ 237 | Containers: []corev1.Container{ 238 | { 239 | Name: containerName, 240 | ReadinessProbe: &corev1.Probe{ 241 | Handler: corev1.Handler{ 242 | HTTPGet: &corev1.HTTPGetAction{ 243 | Path: "/user-provided", 244 | }, 245 | }, 246 | }, 247 | }, 248 | }, 249 | }, 250 | }, 251 | readinessProbe: corev1.Probe{ 252 | Handler: corev1.Handler{ 253 | HTTPGet: &corev1.HTTPGetAction{ 254 | Path: "/probe", 255 | }, 256 | }, 257 | }, 258 | want: &corev1.Probe{ 259 | Handler: corev1.Handler{ 260 | HTTPGet: &corev1.HTTPGetAction{ 261 | Path: "/user-provided", 262 | }, 263 | }, 264 | }, 265 | }, 266 | } 267 | for _, tt := range tests { 268 | t.Run(tt.name, func(t *testing.T) { 269 | b := NewPodTemplateBuilder(tt.PodTemplate, containerName) 270 | if got := b.WithReadinessProbe(tt.readinessProbe).containerDefaulter.Container().ReadinessProbe; !reflect.DeepEqual(got, tt.want) { 271 | t.Errorf("PodTemplateBuilder.WithReadinessProbe() = %v, want %v", got, tt.want) 272 | } 273 | }) 274 | } 275 | } 276 | 277 | func TestPodTemplateBuilder_WithAffinity(t *testing.T) { 278 | defaultAffinity := &corev1.Affinity{ 279 | NodeAffinity: &corev1.NodeAffinity{}, 280 | } 281 | 282 | containerName := "mycontainer" 283 | tests := []struct { 284 | name string 285 | PodTemplate corev1.PodTemplateSpec 286 | affinity *corev1.Affinity 287 | want *corev1.Affinity 288 | }{ 289 | { 290 | name: "set default affinity", 291 | PodTemplate: corev1.PodTemplateSpec{}, 292 | affinity: defaultAffinity, 293 | want: defaultAffinity, 294 | }, 295 | { 296 | name: "don't override user-provided affinity", 297 | PodTemplate: corev1.PodTemplateSpec{ 298 | Spec: corev1.PodSpec{ 299 | Affinity: &corev1.Affinity{}, 300 | }, 301 | }, 302 | affinity: defaultAffinity, 303 | want: &corev1.Affinity{}, 304 | }, 305 | } 306 | for _, tt := range tests { 307 | t.Run(tt.name, func(t *testing.T) { 308 | b := NewPodTemplateBuilder(tt.PodTemplate, containerName) 309 | if got := b.WithAffinity(tt.affinity).PodTemplate.Spec.Affinity; !reflect.DeepEqual(got, tt.want) { 310 | t.Errorf("PodTemplateBuilder.WithAffinity() = %v, want %v", got, tt.want) 311 | } 312 | }) 313 | } 314 | } 315 | 316 | func TestPodTemplateBuilder_WithPorts(t *testing.T) { 317 | containerName := "mycontainer" 318 | tests := []struct { 319 | name string 320 | PodTemplate corev1.PodTemplateSpec 321 | ports []corev1.ContainerPort 322 | want []corev1.ContainerPort 323 | }{ 324 | { 325 | name: "set default ports", 326 | PodTemplate: corev1.PodTemplateSpec{}, 327 | ports: []corev1.ContainerPort{ 328 | {Name: "http", ContainerPort: int32(8080), Protocol: corev1.ProtocolTCP}, 329 | }, 330 | want: []corev1.ContainerPort{ 331 | {Name: "http", ContainerPort: int32(8080), Protocol: corev1.ProtocolTCP}, 332 | }, 333 | }, 334 | { 335 | name: "ports should be sorted", 336 | PodTemplate: corev1.PodTemplateSpec{ 337 | Spec: corev1.PodSpec{ 338 | Containers: []corev1.Container{ 339 | { 340 | Name: containerName, 341 | Ports: []corev1.ContainerPort{ 342 | {Name: "b", ContainerPort: int32(8080), Protocol: corev1.ProtocolTCP}, 343 | {Name: "d", ContainerPort: int32(8081), Protocol: corev1.ProtocolTCP}, 344 | {Name: "c", ContainerPort: int32(8082), Protocol: corev1.ProtocolTCP}, 345 | }, 346 | }, 347 | }, 348 | }, 349 | }, 350 | ports: []corev1.ContainerPort{ 351 | {Name: "a", ContainerPort: int32(9999), Protocol: corev1.ProtocolTCP}, 352 | {Name: "e", ContainerPort: int32(7777), Protocol: corev1.ProtocolTCP}, 353 | {Name: "b", ContainerPort: int32(8083), Protocol: corev1.ProtocolTCP}, 354 | }, 355 | want: []corev1.ContainerPort{ 356 | {Name: "a", ContainerPort: int32(9999), Protocol: corev1.ProtocolTCP}, 357 | {Name: "b", ContainerPort: int32(8080), Protocol: corev1.ProtocolTCP}, 358 | {Name: "c", ContainerPort: int32(8082), Protocol: corev1.ProtocolTCP}, 359 | {Name: "d", ContainerPort: int32(8081), Protocol: corev1.ProtocolTCP}, 360 | {Name: "e", ContainerPort: int32(7777), Protocol: corev1.ProtocolTCP}, 361 | }, 362 | }, 363 | { 364 | name: "append to but don't override user provided ports", 365 | PodTemplate: corev1.PodTemplateSpec{ 366 | Spec: corev1.PodSpec{ 367 | Containers: []corev1.Container{ 368 | { 369 | Name: containerName, 370 | Ports: []corev1.ContainerPort{ 371 | {Name: "a", ContainerPort: int32(8080), Protocol: corev1.ProtocolTCP}, 372 | {Name: "b", ContainerPort: int32(8081), Protocol: corev1.ProtocolTCP}, 373 | {Name: "c", ContainerPort: int32(8082), Protocol: corev1.ProtocolTCP}, 374 | }, 375 | }, 376 | }, 377 | }, 378 | }, 379 | ports: []corev1.ContainerPort{ 380 | {Name: "a", ContainerPort: int32(9999), Protocol: corev1.ProtocolTCP}, 381 | {Name: "b", ContainerPort: int32(7777), Protocol: corev1.ProtocolTCP}, 382 | {Name: "d", ContainerPort: int32(8083), Protocol: corev1.ProtocolTCP}, 383 | }, 384 | want: []corev1.ContainerPort{ 385 | {Name: "a", ContainerPort: int32(8080), Protocol: corev1.ProtocolTCP}, 386 | {Name: "b", ContainerPort: int32(8081), Protocol: corev1.ProtocolTCP}, 387 | {Name: "c", ContainerPort: int32(8082), Protocol: corev1.ProtocolTCP}, 388 | {Name: "d", ContainerPort: int32(8083), Protocol: corev1.ProtocolTCP}, 389 | }, 390 | }, 391 | } 392 | for _, tt := range tests { 393 | t.Run(tt.name, func(t *testing.T) { 394 | b := NewPodTemplateBuilder(tt.PodTemplate, containerName) 395 | if got := b.WithPorts(tt.ports).containerDefaulter.Container().Ports; !reflect.DeepEqual(got, tt.want) { 396 | t.Errorf("PodTemplateBuilder.WithPorts() = %v, want %v", got, tt.want) 397 | } 398 | }) 399 | } 400 | } 401 | 402 | func TestPodTemplateBuilder_WithCommand(t *testing.T) { 403 | containerName := "mycontainer" 404 | tests := []struct { 405 | name string 406 | PodTemplate corev1.PodTemplateSpec 407 | command []string 408 | want []string 409 | }{ 410 | { 411 | name: "set default command", 412 | PodTemplate: corev1.PodTemplateSpec{}, 413 | command: []string{"my", "command"}, 414 | want: []string{"my", "command"}, 415 | }, 416 | { 417 | name: "don't override user-provided command", 418 | PodTemplate: corev1.PodTemplateSpec{ 419 | Spec: corev1.PodSpec{ 420 | Containers: []corev1.Container{ 421 | { 422 | Name: containerName, 423 | Command: []string{"user", "provided"}, 424 | }, 425 | }, 426 | }}, 427 | command: []string{"my", "command"}, 428 | want: []string{"user", "provided"}, 429 | }, 430 | } 431 | for _, tt := range tests { 432 | t.Run(tt.name, func(t *testing.T) { 433 | b := NewPodTemplateBuilder(tt.PodTemplate, containerName) 434 | if got := b.WithCommand(tt.command).containerDefaulter.Container().Command; !reflect.DeepEqual(got, tt.want) { 435 | t.Errorf("PodTemplateBuilder.WithCommand() = %v, want %v", got, tt.want) 436 | } 437 | }) 438 | } 439 | } 440 | 441 | func TestPodTemplateBuilder_WithVolumes(t *testing.T) { 442 | containerName := "mycontainer" 443 | tests := []struct { 444 | name string 445 | PodTemplate corev1.PodTemplateSpec 446 | volumes []corev1.Volume 447 | want []corev1.Volume 448 | }{ 449 | { 450 | name: "set default volumes", 451 | PodTemplate: corev1.PodTemplateSpec{}, 452 | volumes: []corev1.Volume{{Name: "vol1"}, {Name: "vol2"}}, 453 | want: []corev1.Volume{{Name: "vol1"}, {Name: "vol2"}}, 454 | }, 455 | { 456 | name: "volumes should be sorted", 457 | PodTemplate: corev1.PodTemplateSpec{}, 458 | volumes: []corev1.Volume{{Name: "cc"}, {Name: "aa"}, {Name: "bb"}}, 459 | want: []corev1.Volume{{Name: "aa"}, {Name: "bb"}, {Name: "cc"}}, 460 | }, 461 | { 462 | name: "append to but don't override user-provided volumes", 463 | PodTemplate: corev1.PodTemplateSpec{ 464 | Spec: corev1.PodSpec{ 465 | Volumes: []corev1.Volume{ 466 | { 467 | Name: "vol1", 468 | VolumeSource: corev1.VolumeSource{ 469 | Secret: &corev1.SecretVolumeSource{SecretName: "secret1"}, 470 | }, 471 | }, 472 | { 473 | Name: "vol2", 474 | VolumeSource: corev1.VolumeSource{ 475 | Secret: &corev1.SecretVolumeSource{SecretName: "secret2"}, 476 | }, 477 | }, 478 | }, 479 | }, 480 | }, 481 | volumes: []corev1.Volume{ 482 | { 483 | Name: "vol1", 484 | VolumeSource: corev1.VolumeSource{ 485 | Secret: &corev1.SecretVolumeSource{SecretName: "dont-override"}, 486 | }, 487 | }, 488 | { 489 | Name: "vol2", 490 | VolumeSource: corev1.VolumeSource{ 491 | Secret: &corev1.SecretVolumeSource{SecretName: "dont-override"}, 492 | }, 493 | }, 494 | { 495 | Name: "vol3", 496 | VolumeSource: corev1.VolumeSource{ 497 | Secret: &corev1.SecretVolumeSource{SecretName: "secret3"}, 498 | }, 499 | }, 500 | }, 501 | want: []corev1.Volume{ 502 | { 503 | Name: "vol1", 504 | VolumeSource: corev1.VolumeSource{ 505 | Secret: &corev1.SecretVolumeSource{SecretName: "secret1"}, 506 | }, 507 | }, 508 | { 509 | Name: "vol2", 510 | VolumeSource: corev1.VolumeSource{ 511 | Secret: &corev1.SecretVolumeSource{SecretName: "secret2"}, 512 | }, 513 | }, 514 | { 515 | Name: "vol3", 516 | VolumeSource: corev1.VolumeSource{ 517 | Secret: &corev1.SecretVolumeSource{SecretName: "secret3"}, 518 | }, 519 | }}, 520 | }, 521 | } 522 | for _, tt := range tests { 523 | t.Run(tt.name, func(t *testing.T) { 524 | b := NewPodTemplateBuilder(tt.PodTemplate, containerName) 525 | if got := b.WithVolumes(tt.volumes...).PodTemplate.Spec.Volumes; !reflect.DeepEqual(got, tt.want) { 526 | t.Errorf("PodTemplateBuilder.WithVolumes() = %v, want %v", got, tt.want) 527 | } 528 | }) 529 | } 530 | } 531 | 532 | func TestPodTemplateBuilder_WithVolumeMounts(t *testing.T) { 533 | containerName := "mycontainer" 534 | tests := []struct { 535 | name string 536 | PodTemplate corev1.PodTemplateSpec 537 | volumeMounts []corev1.VolumeMount 538 | want []corev1.VolumeMount 539 | }{ 540 | { 541 | name: "set default volume mounts", 542 | PodTemplate: corev1.PodTemplateSpec{}, 543 | volumeMounts: []corev1.VolumeMount{{Name: "vm1", MountPath: "/vm1"}, {Name: "vm2", MountPath: "/vm2"}}, 544 | want: []corev1.VolumeMount{{Name: "vm1", MountPath: "/vm1"}, {Name: "vm2", MountPath: "/vm2"}}, 545 | }, 546 | { 547 | name: "volume mounts should be sorted alphabetically", 548 | PodTemplate: corev1.PodTemplateSpec{}, 549 | volumeMounts: []corev1.VolumeMount{{Name: "cc", MountPath: "/cc"}, {Name: "aa", MountPath: "/aa"}, {Name: "bb", MountPath: "/bb"}}, 550 | want: []corev1.VolumeMount{{Name: "aa", MountPath: "/aa"}, {Name: "bb", MountPath: "/bb"}, {Name: "cc", MountPath: "/cc"}}, 551 | }, 552 | { 553 | name: "append to but don't override user-provided volume mounts", 554 | PodTemplate: corev1.PodTemplateSpec{ 555 | Spec: corev1.PodSpec{ 556 | Containers: []corev1.Container{ 557 | { 558 | Name: containerName, 559 | VolumeMounts: []corev1.VolumeMount{ 560 | { 561 | Name: "vm1", 562 | MountPath: "path1", 563 | }, 564 | { 565 | Name: "vm2", 566 | MountPath: "path2", 567 | }, 568 | }, 569 | }, 570 | }, 571 | }, 572 | }, 573 | volumeMounts: []corev1.VolumeMount{ 574 | { 575 | Name: "vm1", 576 | MountPath: "/dont/override", 577 | }, 578 | { 579 | Name: "vm2", 580 | MountPath: "/dont/override", 581 | }, 582 | { 583 | Name: "vm3", 584 | MountPath: "path3", 585 | }, 586 | }, 587 | want: []corev1.VolumeMount{ 588 | { 589 | Name: "vm1", 590 | MountPath: "path1", 591 | }, 592 | { 593 | Name: "vm2", 594 | MountPath: "path2", 595 | }, 596 | { 597 | Name: "vm3", 598 | MountPath: "path3", 599 | }, 600 | }, 601 | }, 602 | } 603 | for _, tt := range tests { 604 | t.Run(tt.name, func(t *testing.T) { 605 | b := NewPodTemplateBuilder(tt.PodTemplate, containerName) 606 | if got := b.WithVolumeMounts(tt.volumeMounts...).containerDefaulter.Container().VolumeMounts; !reflect.DeepEqual(got, tt.want) { 607 | t.Errorf("PodTemplateBuilder.WithVolumeMounts() = %v, want %v", got, tt.want) 608 | } 609 | }) 610 | } 611 | } 612 | 613 | func TestPodTemplateBuilder_WithEnv(t *testing.T) { 614 | containerName := "mycontainer" 615 | tests := []struct { 616 | name string 617 | PodTemplate corev1.PodTemplateSpec 618 | vars []corev1.EnvVar 619 | want []corev1.EnvVar 620 | }{ 621 | { 622 | name: "set defaults", 623 | PodTemplate: corev1.PodTemplateSpec{}, 624 | vars: []corev1.EnvVar{{Name: "var1"}, {Name: "var2"}}, 625 | want: []corev1.EnvVar{{Name: "var1"}, {Name: "var2"}}, 626 | }, 627 | { 628 | name: "env var order should be preserved", 629 | PodTemplate: corev1.PodTemplateSpec{}, 630 | vars: []corev1.EnvVar{{Name: "cc"}, {Name: "aa"}, {Name: "bb"}}, 631 | want: []corev1.EnvVar{{Name: "cc"}, {Name: "aa"}, {Name: "bb"}}, 632 | }, 633 | { 634 | name: "append to but don't override user provided env vars", 635 | PodTemplate: corev1.PodTemplateSpec{ 636 | Spec: corev1.PodSpec{ 637 | Containers: []corev1.Container{ 638 | { 639 | Name: containerName, 640 | Env: []corev1.EnvVar{ 641 | { 642 | Name: "var1", 643 | Value: "value1", 644 | }, 645 | { 646 | Name: "var2", 647 | Value: "value2", 648 | }, 649 | }, 650 | }, 651 | }, 652 | }, 653 | }, 654 | vars: []corev1.EnvVar{ 655 | { 656 | Name: "var1", 657 | Value: "dont override", 658 | }, 659 | { 660 | Name: "var2", 661 | Value: "dont override", 662 | }, 663 | { 664 | Name: "var3", 665 | Value: "value3", 666 | }, 667 | }, 668 | want: []corev1.EnvVar{ 669 | { 670 | Name: "var1", 671 | Value: "value1", 672 | }, 673 | { 674 | Name: "var2", 675 | Value: "value2", 676 | }, 677 | { 678 | Name: "var3", 679 | Value: "value3", 680 | }, 681 | }, 682 | }, 683 | } 684 | for _, tt := range tests { 685 | t.Run(tt.name, func(t *testing.T) { 686 | b := NewPodTemplateBuilder(tt.PodTemplate, containerName) 687 | if got := b.WithEnv(tt.vars...).containerDefaulter.Container().Env; !reflect.DeepEqual(got, tt.want) { 688 | t.Errorf("PodTemplateBuilder.WithEnv() = %v, want %v", got, tt.want) 689 | } 690 | }) 691 | } 692 | } 693 | 694 | func TestPodTemplateBuilder_WithTerminationGracePeriod(t *testing.T) { 695 | period := int64(12) 696 | userPeriod := int64(13) 697 | tests := []struct { 698 | name string 699 | PodTemplate corev1.PodTemplateSpec 700 | period int64 701 | want *int64 702 | }{ 703 | { 704 | name: "set default", 705 | PodTemplate: corev1.PodTemplateSpec{}, 706 | period: period, 707 | want: &period, 708 | }, 709 | { 710 | name: "don't override user-specified value", 711 | PodTemplate: corev1.PodTemplateSpec{ 712 | Spec: corev1.PodSpec{ 713 | TerminationGracePeriodSeconds: &userPeriod, 714 | }, 715 | }, 716 | period: period, 717 | want: &userPeriod, 718 | }, 719 | } 720 | for _, tt := range tests { 721 | t.Run(tt.name, func(t *testing.T) { 722 | b := NewPodTemplateBuilder(tt.PodTemplate, "") 723 | if got := b.WithTerminationGracePeriod(tt.period).PodTemplate.Spec.TerminationGracePeriodSeconds; !reflect.DeepEqual(got, tt.want) { 724 | t.Errorf("PodTemplateBuilder.WithTerminationGracePeriod() = %v, want %v", got, tt.want) 725 | } 726 | }) 727 | } 728 | } 729 | 730 | func TestPodTemplateBuilder_WithInitContainers(t *testing.T) { 731 | tests := []struct { 732 | name string 733 | PodTemplate corev1.PodTemplateSpec 734 | initContainers []corev1.Container 735 | want []corev1.Container 736 | }{ 737 | { 738 | name: "set defaults", 739 | PodTemplate: corev1.PodTemplateSpec{}, 740 | initContainers: []corev1.Container{{Name: "init-container1"}, {Name: "init-container2"}}, 741 | want: []corev1.Container{{Name: "init-container1"}, {Name: "init-container2"}}, 742 | }, 743 | { 744 | name: "merge operator and user-provided init containers", 745 | PodTemplate: corev1.PodTemplateSpec{ 746 | Spec: corev1.PodSpec{ 747 | InitContainers: []corev1.Container{ 748 | { 749 | Name: "init-container1", 750 | Image: "image1", 751 | }, 752 | { 753 | Name: "init-container2", 754 | Image: "image2", 755 | }, 756 | }, 757 | }, 758 | }, 759 | initContainers: []corev1.Container{ 760 | { 761 | Name: "init-container1", 762 | Image: "dont-override", 763 | }, 764 | { 765 | Name: "init-container2", 766 | Image: "dont-override", 767 | }, 768 | { 769 | Name: "init-container3", 770 | Image: "image3", 771 | }, 772 | }, 773 | want: []corev1.Container{ 774 | { 775 | Name: "init-container1", 776 | Image: "image1", 777 | }, 778 | { 779 | Name: "init-container2", 780 | Image: "image2", 781 | }, 782 | { 783 | Name: "init-container3", 784 | Image: "image3", 785 | }, 786 | }, 787 | }, 788 | { 789 | name: "prepend provided init containers", 790 | PodTemplate: corev1.PodTemplateSpec{ 791 | Spec: corev1.PodSpec{ 792 | InitContainers: []corev1.Container{ 793 | { 794 | Name: "user-init-container1", 795 | }, 796 | { 797 | Name: "user-init-container2", 798 | }, 799 | }, 800 | }, 801 | }, 802 | initContainers: []corev1.Container{ 803 | { 804 | Name: "init-container1", 805 | Image: "init-image", 806 | }, 807 | }, 808 | want: []corev1.Container{ 809 | { 810 | Name: "init-container1", 811 | Image: "init-image", 812 | }, 813 | { 814 | Name: "user-init-container1", 815 | }, 816 | { 817 | Name: "user-init-container2", 818 | }, 819 | }, 820 | }, 821 | } 822 | 823 | for _, tt := range tests { 824 | t.Run(tt.name, func(t *testing.T) { 825 | b := NewPodTemplateBuilder(tt.PodTemplate, "main") 826 | 827 | got := b.WithInitContainers(tt.initContainers...).PodTemplate.Spec.InitContainers 828 | 829 | require.Equal(t, tt.want, got) 830 | }) 831 | } 832 | } 833 | 834 | func TestPodTemplateBuilder_WithDefaultResources(t *testing.T) { 835 | containerName := "default-container" 836 | tests := []struct { 837 | name string 838 | PodTemplate corev1.PodTemplateSpec 839 | defaultResources corev1.ResourceRequirements 840 | want corev1.ResourceRequirements 841 | }{ 842 | { 843 | name: "no resource set (nil values): use defaults", 844 | PodTemplate: corev1.PodTemplateSpec{ 845 | Spec: corev1.PodSpec{ 846 | Containers: []corev1.Container{ 847 | { 848 | Name: containerName, 849 | }, 850 | }, 851 | }, 852 | }, 853 | defaultResources: corev1.ResourceRequirements{ 854 | Requests: map[corev1.ResourceName]resource.Quantity{ 855 | corev1.ResourceMemory: resource.MustParse("2Gi"), 856 | }, 857 | }, 858 | want: corev1.ResourceRequirements{ 859 | Requests: map[corev1.ResourceName]resource.Quantity{ 860 | corev1.ResourceMemory: resource.MustParse("2Gi"), 861 | }, 862 | }, 863 | }, 864 | { 865 | name: "resource limits set: don't use defaults", 866 | PodTemplate: corev1.PodTemplateSpec{ 867 | Spec: corev1.PodSpec{ 868 | Containers: []corev1.Container{ 869 | { 870 | Name: containerName, 871 | Resources: corev1.ResourceRequirements{ 872 | Limits: map[corev1.ResourceName]resource.Quantity{ 873 | corev1.ResourceMemory: resource.MustParse("4Gi"), 874 | }, 875 | }, 876 | }, 877 | }, 878 | }, 879 | }, 880 | defaultResources: corev1.ResourceRequirements{ 881 | Requests: map[corev1.ResourceName]resource.Quantity{ 882 | corev1.ResourceMemory: resource.MustParse("2Gi"), 883 | }, 884 | }, 885 | want: corev1.ResourceRequirements{ 886 | Limits: map[corev1.ResourceName]resource.Quantity{ 887 | corev1.ResourceMemory: resource.MustParse("4Gi"), 888 | }, 889 | }, 890 | }, 891 | { 892 | name: "resource requests set: don't use defaults", 893 | PodTemplate: corev1.PodTemplateSpec{ 894 | Spec: corev1.PodSpec{ 895 | Containers: []corev1.Container{ 896 | { 897 | Name: containerName, 898 | Resources: corev1.ResourceRequirements{ 899 | Requests: map[corev1.ResourceName]resource.Quantity{ 900 | corev1.ResourceMemory: resource.MustParse("4Gi"), 901 | }, 902 | }, 903 | }, 904 | }, 905 | }, 906 | }, 907 | defaultResources: corev1.ResourceRequirements{ 908 | Requests: map[corev1.ResourceName]resource.Quantity{ 909 | corev1.ResourceMemory: resource.MustParse("2Gi"), 910 | }, 911 | }, 912 | want: corev1.ResourceRequirements{ 913 | Requests: map[corev1.ResourceName]resource.Quantity{ 914 | corev1.ResourceMemory: resource.MustParse("4Gi"), 915 | }, 916 | }, 917 | }, 918 | { 919 | name: "resource requests explicitly empty (not nil): don't use defaults", 920 | PodTemplate: corev1.PodTemplateSpec{ 921 | Spec: corev1.PodSpec{ 922 | Containers: []corev1.Container{ 923 | { 924 | Name: containerName, 925 | Resources: corev1.ResourceRequirements{ 926 | Requests: map[corev1.ResourceName]resource.Quantity{}, 927 | }, 928 | }, 929 | }, 930 | }, 931 | }, 932 | defaultResources: corev1.ResourceRequirements{ 933 | Requests: map[corev1.ResourceName]resource.Quantity{ 934 | corev1.ResourceMemory: resource.MustParse("2Gi"), 935 | }, 936 | }, 937 | want: corev1.ResourceRequirements{ 938 | Requests: map[corev1.ResourceName]resource.Quantity{}, 939 | }, 940 | }, 941 | { 942 | name: "resource limits explicitly empty (not nil): don't use defaults", 943 | PodTemplate: corev1.PodTemplateSpec{ 944 | Spec: corev1.PodSpec{ 945 | Containers: []corev1.Container{ 946 | { 947 | Name: containerName, 948 | Resources: corev1.ResourceRequirements{ 949 | Limits: map[corev1.ResourceName]resource.Quantity{}, 950 | }, 951 | }, 952 | }, 953 | }, 954 | }, 955 | defaultResources: corev1.ResourceRequirements{ 956 | Requests: map[corev1.ResourceName]resource.Quantity{ 957 | corev1.ResourceMemory: resource.MustParse("2Gi"), 958 | }, 959 | }, 960 | want: corev1.ResourceRequirements{ 961 | Limits: map[corev1.ResourceName]resource.Quantity{}, 962 | }, 963 | }, 964 | } 965 | for _, tt := range tests { 966 | t.Run(tt.name, func(t *testing.T) { 967 | b := NewPodTemplateBuilder(tt.PodTemplate, containerName) 968 | if got := b.WithResources(tt.defaultResources).containerDefaulter.Container().Resources; !reflect.DeepEqual(got, tt.want) { 969 | t.Errorf("PodTemplateBuilder.WithResources() = %v, want %v", got, tt.want) 970 | } 971 | }) 972 | } 973 | } 974 | 975 | func TestPodTemplateBuilder_WithPreStopHook(t *testing.T) { 976 | containerName := "mycontainer" 977 | defaultHook := corev1.Handler{Exec: &corev1.ExecAction{Command: []string{"default", "command"}}} 978 | userHook := &corev1.Handler{} 979 | tests := []struct { 980 | name string 981 | podTemplate corev1.PodTemplateSpec 982 | preStopHook corev1.Handler 983 | wantPreStop corev1.Handler 984 | wantPostStart *corev1.Handler 985 | }{ 986 | { 987 | name: "no pre stop hook in pod template: use default one", 988 | podTemplate: corev1.PodTemplateSpec{}, 989 | preStopHook: defaultHook, 990 | wantPreStop: defaultHook, 991 | wantPostStart: nil, 992 | }, 993 | { 994 | name: "user provided post start hook, but no pre stop hook in pod template: use default one", 995 | podTemplate: corev1.PodTemplateSpec{ 996 | Spec: corev1.PodSpec{ 997 | Containers: []corev1.Container{ 998 | { 999 | Name: containerName, 1000 | Lifecycle: &corev1.Lifecycle{ 1001 | PostStart: userHook, 1002 | }, 1003 | }, 1004 | }, 1005 | }, 1006 | }, 1007 | preStopHook: defaultHook, 1008 | wantPreStop: defaultHook, 1009 | wantPostStart: userHook, 1010 | }, 1011 | { 1012 | name: "pre stop hook in pod template: use provided one", 1013 | podTemplate: corev1.PodTemplateSpec{ 1014 | Spec: corev1.PodSpec{ 1015 | Containers: []corev1.Container{ 1016 | { 1017 | Name: containerName, 1018 | Lifecycle: &corev1.Lifecycle{ 1019 | PreStop: userHook, 1020 | }, 1021 | }, 1022 | }, 1023 | }}, 1024 | preStopHook: *userHook, 1025 | wantPostStart: nil, 1026 | }, 1027 | { 1028 | name: "user provided post start hook and pre stop hook in pod template: use provided one", 1029 | podTemplate: corev1.PodTemplateSpec{ 1030 | Spec: corev1.PodSpec{ 1031 | Containers: []corev1.Container{ 1032 | { 1033 | Name: containerName, 1034 | Lifecycle: &corev1.Lifecycle{ 1035 | PostStart: &corev1.Handler{}, 1036 | PreStop: userHook, 1037 | }, 1038 | }, 1039 | }, 1040 | }, 1041 | }, 1042 | preStopHook: *userHook, 1043 | wantPostStart: userHook, 1044 | }, 1045 | } 1046 | for _, tt := range tests { 1047 | t.Run(tt.name, func(t *testing.T) { 1048 | b := NewPodTemplateBuilder(tt.podTemplate, "mycontainer") 1049 | got := b.WithPreStopHook(tt.preStopHook).containerDefaulter.Container().Lifecycle 1050 | if !reflect.DeepEqual(got.PreStop, &tt.wantPreStop) { 1051 | t.Errorf("PreStop after PodTemplateBuilder.WithPreStopHook() = %v, want %v", got.PreStop, tt.wantPreStop) 1052 | } 1053 | if !reflect.DeepEqual(got.PostStart, tt.wantPostStart) { 1054 | t.Errorf("PostStart after PodTemplateBuilder.WithPreStopHook() = %v, want %v", got.PostStart, tt.wantPostStart) 1055 | } 1056 | }) 1057 | } 1058 | } 1059 | -------------------------------------------------------------------------------- /controllers/podspec/deployment.go: -------------------------------------------------------------------------------- 1 | package podspec 2 | 3 | import ( 4 | "github.com/toughnoah/elastalert-operator/api/v1alpha1" 5 | appsv1 "k8s.io/api/apps/v1" 6 | corev1 "k8s.io/api/core/v1" 7 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 8 | "k8s.io/apimachinery/pkg/runtime" 9 | ctrl "sigs.k8s.io/controller-runtime" 10 | ) 11 | 12 | var log = ctrl.Log.WithName("deployment") 13 | 14 | type PodTemplateBuilder struct { 15 | PodTemplate corev1.PodTemplateSpec 16 | containerName string 17 | containerDefaulter Defaulter 18 | } 19 | 20 | func GenerateNewDeployment(Scheme *runtime.Scheme, e *v1alpha1.Elastalert) (*appsv1.Deployment, error) { 21 | deploy := BuildDeployment(*e) 22 | if err := ctrl.SetControllerReference(e, deploy, Scheme); err != nil { 23 | log.Error(err, "Failed to generate Deployment", "Elastalert.Name", e.Name, "Deployment.Name", e.Name) 24 | return nil, err 25 | } 26 | return deploy, nil 27 | } 28 | 29 | func BuildDeployment(elastalert v1alpha1.Elastalert) *appsv1.Deployment { 30 | var replicas = new(int32) 31 | *replicas = 1 32 | podTemplate := BuildPodTemplateSpec(elastalert) 33 | varTrue := true 34 | //deliberate action to enable 35 | podTemplate.Spec.AutomountServiceAccountToken = &varTrue 36 | 37 | deploy := &appsv1.Deployment{ 38 | ObjectMeta: metav1.ObjectMeta{ 39 | Name: elastalert.Name, 40 | Namespace: elastalert.Namespace, 41 | }, 42 | Spec: appsv1.DeploymentSpec{ 43 | Replicas: replicas, 44 | Selector: &metav1.LabelSelector{ 45 | MatchLabels: map[string]string{"app": "elastalert"}, 46 | }, 47 | Template: podTemplate, 48 | }, 49 | } 50 | return deploy 51 | } 52 | -------------------------------------------------------------------------------- /controllers/podspec/maps.go: -------------------------------------------------------------------------------- 1 | // Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one 2 | // or more contributor license agreements. Licensed under the Elastic License; 3 | // you may not use this file except in compliance with the Elastic License. 4 | 5 | package podspec 6 | 7 | // IsSubset compares two maps to determine if one of them is fully contained in the other. 8 | func IsSubset(toCheck, fullSet map[string]string) bool { 9 | if len(toCheck) > len(fullSet) { 10 | return false 11 | } 12 | 13 | for k, v := range toCheck { 14 | if currValue, ok := fullSet[k]; !ok || currValue != v { 15 | return false 16 | } 17 | } 18 | 19 | return true 20 | } 21 | 22 | // Merge merges source into destination, overwriting existing values if necessary. 23 | func Merge(dest, src map[string]string) map[string]string { 24 | if dest == nil { 25 | if src == nil { 26 | return nil 27 | } 28 | dest = make(map[string]string, len(src)) 29 | } 30 | 31 | for k, v := range src { 32 | dest[k] = v 33 | } 34 | return dest 35 | } 36 | func MergeInterfaceMap(dest, src map[string]interface{}) map[string]interface{} { 37 | if dest == nil { 38 | if src == nil { 39 | return nil 40 | } 41 | dest = make(map[string]interface{}, len(src)) 42 | } 43 | for k, v := range src { 44 | dest[k] = v 45 | } 46 | return dest 47 | } 48 | 49 | // MergePreservingExistingKeys merges source into destination while skipping any keys that exist in the destination. 50 | func MergePreservingExistingKeys(dest, src map[string]string) map[string]string { 51 | if dest == nil { 52 | if src == nil { 53 | return nil 54 | } 55 | dest = make(map[string]string, len(src)) 56 | } 57 | 58 | for k, v := range src { 59 | if _, exists := dest[k]; !exists { 60 | dest[k] = v 61 | } 62 | } 63 | 64 | return dest 65 | } 66 | 67 | // ContainsKeys determines if a set of label (keys) are present in a map of labels (keys and values). 68 | func ContainsKeys(m map[string]string, labels ...string) bool { 69 | for _, label := range labels { 70 | if _, exists := m[label]; !exists { 71 | return false 72 | } 73 | } 74 | return true 75 | } 76 | -------------------------------------------------------------------------------- /controllers/podspec/maps_test.go: -------------------------------------------------------------------------------- 1 | // Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one 2 | // or more contributor license agreements. Licensed under the Elastic License; 3 | // you may not use this file except in compliance with the Elastic License. 4 | 5 | package podspec 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestIsSubset(t *testing.T) { 14 | tests := []struct { 15 | name string 16 | map1 map[string]string 17 | map2 map[string]string 18 | want bool 19 | }{ 20 | { 21 | name: "when map1 is nil", 22 | map2: map[string]string{"x": "y"}, 23 | want: true, 24 | }, 25 | { 26 | name: "when map2 is nil", 27 | map1: map[string]string{"x": "y"}, 28 | want: false, 29 | }, 30 | { 31 | name: "when both maps are nil", 32 | want: true, 33 | }, 34 | { 35 | name: "when map1 is empty", 36 | map1: map[string]string{}, 37 | map2: map[string]string{"x": "y"}, 38 | want: true, 39 | }, 40 | { 41 | name: "when map2 is empty", 42 | map1: map[string]string{"x": "y"}, 43 | map2: map[string]string{}, 44 | want: false, 45 | }, 46 | { 47 | name: "when both maps are empty", 48 | map1: map[string]string{}, 49 | map2: map[string]string{}, 50 | want: true, 51 | }, 52 | { 53 | name: "when both maps contain the same items", 54 | map1: map[string]string{"x": "y", "a": "b"}, 55 | map2: map[string]string{"x": "y", "a": "b"}, 56 | want: true, 57 | }, 58 | { 59 | name: "when keys are the same but value are different", 60 | map1: map[string]string{"x": "p", "a": "q"}, 61 | map2: map[string]string{"x": "y", "a": "b"}, 62 | want: false, 63 | }, 64 | 65 | { 66 | name: "when map1 has fewer items than map2", 67 | map1: map[string]string{"x": "y"}, 68 | map2: map[string]string{"x": "y", "a": "b"}, 69 | want: true, 70 | }, 71 | { 72 | name: "when map1 has more items than map2", 73 | map1: map[string]string{"x": "y", "a": "b"}, 74 | map2: map[string]string{"x": "y"}, 75 | want: false, 76 | }, 77 | } 78 | 79 | for _, tt := range tests { 80 | t.Run(tt.name, func(t *testing.T) { 81 | have := IsSubset(tt.map1, tt.map2) 82 | require.Equal(t, tt.want, have) 83 | }) 84 | } 85 | } 86 | 87 | func TestMerge(t *testing.T) { 88 | tests := []struct { 89 | name string 90 | dest map[string]string 91 | src map[string]string 92 | want map[string]string 93 | }{ 94 | { 95 | name: "when dest is nil", 96 | src: map[string]string{"x": "y"}, 97 | want: map[string]string{"x": "y"}, 98 | }, 99 | { 100 | name: "when src is nil", 101 | dest: map[string]string{"x": "y"}, 102 | want: map[string]string{"x": "y"}, 103 | }, 104 | { 105 | name: "when both maps are nil", 106 | }, 107 | { 108 | name: "when dest is empty", 109 | dest: map[string]string{}, 110 | src: map[string]string{"x": "y"}, 111 | want: map[string]string{"x": "y"}, 112 | }, 113 | { 114 | name: "when src is empty", 115 | dest: map[string]string{"x": "y"}, 116 | src: map[string]string{}, 117 | want: map[string]string{"x": "y"}, 118 | }, 119 | { 120 | name: "when both maps are empty", 121 | dest: map[string]string{}, 122 | src: map[string]string{}, 123 | want: map[string]string{}, 124 | }, 125 | { 126 | name: "when both maps contain the same items", 127 | dest: map[string]string{"x": "y", "a": "b"}, 128 | src: map[string]string{"x": "y", "a": "b"}, 129 | want: map[string]string{"x": "y", "a": "b"}, 130 | }, 131 | { 132 | name: "when keys are the same but value are different", 133 | dest: map[string]string{"x": "p", "a": "q"}, 134 | src: map[string]string{"x": "y", "a": "b"}, 135 | want: map[string]string{"x": "y", "a": "b"}, 136 | }, 137 | 138 | { 139 | name: "when dest has fewer items than src", 140 | dest: map[string]string{"x": "y"}, 141 | src: map[string]string{"x": "y", "a": "b"}, 142 | want: map[string]string{"x": "y", "a": "b"}, 143 | }, 144 | { 145 | name: "when dest has more items than src", 146 | dest: map[string]string{"x": "y", "a": "b"}, 147 | src: map[string]string{"x": "y"}, 148 | want: map[string]string{"x": "y", "a": "b"}, 149 | }, 150 | } 151 | 152 | for _, tt := range tests { 153 | t.Run(tt.name, func(t *testing.T) { 154 | have := Merge(tt.dest, tt.src) 155 | require.Equal(t, tt.want, have) 156 | }) 157 | } 158 | } 159 | 160 | func TestMergePreservingExistingKeys(t *testing.T) { 161 | tests := []struct { 162 | name string 163 | dest map[string]string 164 | src map[string]string 165 | want map[string]string 166 | }{ 167 | { 168 | name: "when dest is nil", 169 | src: map[string]string{"x": "y"}, 170 | want: map[string]string{"x": "y"}, 171 | }, 172 | { 173 | name: "when src is nil", 174 | dest: map[string]string{"x": "y"}, 175 | want: map[string]string{"x": "y"}, 176 | }, 177 | { 178 | name: "when both maps are nil", 179 | }, 180 | { 181 | name: "when dest is empty", 182 | dest: map[string]string{}, 183 | src: map[string]string{"x": "y"}, 184 | want: map[string]string{"x": "y"}, 185 | }, 186 | { 187 | name: "when src is empty", 188 | dest: map[string]string{"x": "y"}, 189 | src: map[string]string{}, 190 | want: map[string]string{"x": "y"}, 191 | }, 192 | { 193 | name: "when both maps are empty", 194 | dest: map[string]string{}, 195 | src: map[string]string{}, 196 | want: map[string]string{}, 197 | }, 198 | { 199 | name: "when both maps contain the same items", 200 | dest: map[string]string{"x": "y", "a": "b"}, 201 | src: map[string]string{"x": "y", "a": "b"}, 202 | want: map[string]string{"x": "y", "a": "b"}, 203 | }, 204 | { 205 | name: "when keys are the same but value are different", 206 | dest: map[string]string{"x": "p", "a": "q"}, 207 | src: map[string]string{"x": "y", "a": "b"}, 208 | want: map[string]string{"x": "p", "a": "q"}, 209 | }, 210 | 211 | { 212 | name: "when dest has fewer items than src", 213 | dest: map[string]string{"x": "y"}, 214 | src: map[string]string{"x": "z", "a": "b"}, 215 | want: map[string]string{"x": "y", "a": "b"}, 216 | }, 217 | { 218 | name: "when dest has more items than src", 219 | dest: map[string]string{"x": "y", "a": "b"}, 220 | src: map[string]string{"x": "z"}, 221 | want: map[string]string{"x": "y", "a": "b"}, 222 | }, 223 | } 224 | 225 | for _, tt := range tests { 226 | t.Run(tt.name, func(t *testing.T) { 227 | have := MergePreservingExistingKeys(tt.dest, tt.src) 228 | require.Equal(t, tt.want, have) 229 | }) 230 | } 231 | } 232 | 233 | func TestContainsKeys(t *testing.T) { 234 | tests := []struct { 235 | name string 236 | m map[string]string 237 | labels []string 238 | want bool 239 | }{ 240 | { 241 | name: "when no labels on object", 242 | m: map[string]string{}, 243 | labels: []string{"x", "y"}, 244 | want: false, 245 | }, 246 | { 247 | // empty label set is a subset of every non-empty set 248 | name: "when empty label set provided", 249 | m: map[string]string{"x": "y"}, 250 | labels: []string{}, 251 | want: true, 252 | }, 253 | { 254 | name: "when labels that match", 255 | m: map[string]string{"x": "y", "a": "b"}, 256 | labels: []string{"x", "a"}, 257 | want: true, 258 | }, 259 | { 260 | name: "when labels that don't match", 261 | m: map[string]string{"x": "y", "a": "b"}, 262 | 263 | labels: []string{"c", "d"}, 264 | want: false, 265 | }, 266 | { 267 | name: "when labels that are nil", 268 | m: nil, 269 | labels: []string{"c", "d"}, 270 | want: false, 271 | }, 272 | } 273 | 274 | for _, tc := range tests { 275 | t.Run(tc.name, func(t *testing.T) { 276 | have := ContainsKeys(tc.m, tc.labels...) 277 | require.Equal(t, tc.want, have) 278 | }) 279 | } 280 | } 281 | 282 | func TestMergeInterfaceMap(t *testing.T) { 283 | tests := []struct { 284 | name string 285 | dest map[string]interface{} 286 | src map[string]interface{} 287 | want map[string]interface{} 288 | }{ 289 | { 290 | name: "when dest is nil", 291 | src: map[string]interface{}{"x": "y"}, 292 | want: map[string]interface{}{"x": "y"}, 293 | }, 294 | { 295 | name: "when src is nil", 296 | dest: map[string]interface{}{"x": "y"}, 297 | want: map[string]interface{}{"x": "y"}, 298 | }, 299 | { 300 | name: "when both maps are nil", 301 | }, 302 | { 303 | name: "when dest is empty", 304 | dest: map[string]interface{}{}, 305 | src: map[string]interface{}{"x": "y"}, 306 | want: map[string]interface{}{"x": "y"}, 307 | }, 308 | { 309 | name: "when src is empty", 310 | dest: map[string]interface{}{"x": "y"}, 311 | src: map[string]interface{}{}, 312 | want: map[string]interface{}{"x": "y"}, 313 | }, 314 | { 315 | name: "when both maps are empty", 316 | dest: map[string]interface{}{}, 317 | src: map[string]interface{}{}, 318 | want: map[string]interface{}{}, 319 | }, 320 | { 321 | name: "when both maps contain the same items", 322 | dest: map[string]interface{}{"x": "y", "a": "b"}, 323 | src: map[string]interface{}{"x": "y", "a": "b"}, 324 | want: map[string]interface{}{"x": "y", "a": "b"}, 325 | }, 326 | { 327 | name: "when keys are the same but value are different", 328 | dest: map[string]interface{}{"x": "p", "a": "q"}, 329 | src: map[string]interface{}{"x": "y", "a": "b"}, 330 | want: map[string]interface{}{"x": "y", "a": "b"}, 331 | }, 332 | 333 | { 334 | name: "when dest has fewer items than src", 335 | dest: map[string]interface{}{"x": "y"}, 336 | src: map[string]interface{}{"x": "y", "a": "b"}, 337 | want: map[string]interface{}{"x": "y", "a": "b"}, 338 | }, 339 | { 340 | name: "when dest has more items than src", 341 | dest: map[string]interface{}{"x": "y", "a": "b"}, 342 | src: map[string]interface{}{"x": "y"}, 343 | want: map[string]interface{}{"x": "y", "a": "b"}, 344 | }, 345 | } 346 | 347 | for _, tt := range tests { 348 | t.Run(tt.name, func(t *testing.T) { 349 | have := MergeInterfaceMap(tt.dest, tt.src) 350 | require.Equal(t, tt.want, have) 351 | }) 352 | } 353 | } 354 | -------------------------------------------------------------------------------- /controllers/podspec/podtemplate.go: -------------------------------------------------------------------------------- 1 | package podspec 2 | 3 | import ( 4 | "github.com/toughnoah/elastalert-operator/api/v1alpha1" 5 | corev1 "k8s.io/api/core/v1" 6 | "time" 7 | ) 8 | 9 | func BuildPodTemplateSpec(elastalert v1alpha1.Elastalert) corev1.PodTemplateSpec { 10 | DefaultAnnotations := map[string]string{ 11 | "kubectl.kubernetes.io/restartedAt": GetUtcTimeString(), 12 | } 13 | DefaultAnnotations = Merge(DefaultAnnotations, elastalert.Annotations) 14 | var DefaultCommand = []string{"elastalert", "--config", "/etc/elastalert/config.yaml", "--verbose"} 15 | volumes, volumeMounts := buildVolumes(elastalert.Name) 16 | labelselector := buildLabels() 17 | builder := NewPodTemplateBuilder(elastalert.Spec.PodTemplateSpec, DefaultElastAlertName) 18 | builder = builder. 19 | WithLabels(labelselector). 20 | WithAnnotations(DefaultAnnotations). 21 | WithDockerImage(elastalert.Spec.Image, DefautlImage). 22 | WithResources(DefaultResources). 23 | WithTerminationGracePeriod(DefaultTerminationGracePeriodSeconds). 24 | WithPorts(GetDefaultContainerPorts()). 25 | WithAffinity(DefaultAffinity(elastalert.Name)). 26 | WithCommand(DefaultCommand). 27 | WithInitContainers(). 28 | WithVolumes(volumes...). 29 | WithVolumeMounts(volumeMounts...). 30 | WithInitContainerDefaults(). 31 | WithReadinessProbe(corev1.Probe{ 32 | Handler: corev1.Handler{ 33 | Exec: &corev1.ExecAction{ 34 | Command: []string{ 35 | "cat", 36 | "/etc/elastalert/config.yaml", 37 | }, 38 | }, 39 | }, 40 | InitialDelaySeconds: 20, 41 | TimeoutSeconds: 3, 42 | PeriodSeconds: 2, 43 | SuccessThreshold: 5, 44 | FailureThreshold: 3, 45 | }).WithLivenessProbe( 46 | corev1.Probe{ 47 | Handler: corev1.Handler{ 48 | Exec: &corev1.ExecAction{ 49 | Command: []string{ 50 | "sh", 51 | "-c", 52 | "ps -ef|grep -v grep|grep elastalert", 53 | }, 54 | }, 55 | }, 56 | InitialDelaySeconds: 50, 57 | TimeoutSeconds: 3, 58 | PeriodSeconds: 2, 59 | SuccessThreshold: 1, 60 | FailureThreshold: 3, 61 | }) 62 | return builder.PodTemplate 63 | } 64 | 65 | func buildVolumes(eaName string) ([]corev1.Volume, []corev1.VolumeMount) { 66 | var elastAlertVolumes []corev1.Volume 67 | var elastAlertVolumesMounts []corev1.VolumeMount 68 | 69 | var volumesTypeMap = map[string]string{ 70 | v1alpha1.RuleSuffx: v1alpha1.RuleMountPath, 71 | v1alpha1.ConfigSuffx: v1alpha1.ConfigMountPath, 72 | } 73 | for typeSuffix, Path := range volumesTypeMap { 74 | elastalertVolume := corev1.Volume{ 75 | Name: eaName + typeSuffix, 76 | VolumeSource: corev1.VolumeSource{ 77 | ConfigMap: &corev1.ConfigMapVolumeSource{ 78 | LocalObjectReference: corev1.LocalObjectReference{ 79 | Name: eaName + typeSuffix, 80 | }, 81 | }, 82 | }, 83 | } 84 | elastalertVolumeMount := corev1.VolumeMount{ 85 | Name: eaName + typeSuffix, 86 | MountPath: Path, 87 | } 88 | elastAlertVolumes = append(elastAlertVolumes, elastalertVolume) 89 | elastAlertVolumesMounts = append(elastAlertVolumesMounts, elastalertVolumeMount) 90 | } 91 | 92 | certVolume := &corev1.Volume{ 93 | Name: DefaultCertVolumeName, 94 | VolumeSource: corev1.VolumeSource{ 95 | Secret: &corev1.SecretVolumeSource{ 96 | SecretName: eaName + DefaultCertSuffix, 97 | }, 98 | }, 99 | } 100 | certVolumeMount := &corev1.VolumeMount{ 101 | Name: DefaultCertVolumeName, 102 | MountPath: DefaultCertMountPath, 103 | } 104 | 105 | elastAlertVolumes = append(elastAlertVolumes, *certVolume) 106 | elastAlertVolumesMounts = append(elastAlertVolumesMounts, *certVolumeMount) 107 | return elastAlertVolumes, elastAlertVolumesMounts 108 | } 109 | 110 | // PodTemplateBuilder helps with building a pod template inheriting values 111 | // from a user-provided pod template. It focuses on building a pod with 112 | // one main Container. 113 | 114 | func NewPodTemplateBuilder(base corev1.PodTemplateSpec, containerName string) *PodTemplateBuilder { 115 | builder := &PodTemplateBuilder{ 116 | PodTemplate: *base.DeepCopy(), 117 | containerName: containerName, 118 | } 119 | return builder.setDefaults() 120 | } 121 | 122 | func (b *PodTemplateBuilder) setDefaults() *PodTemplateBuilder { 123 | // retrieve the existing Container from the pod template 124 | getContainer := func() *corev1.Container { 125 | for i, c := range b.PodTemplate.Spec.Containers { 126 | if c.Name == b.containerName { 127 | return &b.PodTemplate.Spec.Containers[i] 128 | } 129 | } 130 | return nil 131 | } 132 | userContainer := getContainer() 133 | if userContainer == nil { 134 | // create the default Container if not provided by the user 135 | b.PodTemplate.Spec.Containers = append(b.PodTemplate.Spec.Containers, corev1.Container{Name: b.containerName}) 136 | b.containerDefaulter = NewDefaulter(getContainer()) 137 | } else { 138 | b.containerDefaulter = NewDefaulter(userContainer) 139 | } 140 | 141 | //disable service account token auto mount, unless explicitly enabled by the user 142 | varFalse := false 143 | if b.PodTemplate.Spec.AutomountServiceAccountToken == nil { 144 | b.PodTemplate.Spec.AutomountServiceAccountToken = &varFalse 145 | } 146 | return b 147 | } 148 | 149 | func GetDefaultContainerPorts() []corev1.ContainerPort { 150 | return []corev1.ContainerPort{ 151 | {Name: "http", ContainerPort: 8080, Protocol: corev1.ProtocolTCP}, 152 | } 153 | } 154 | 155 | func buildLabels() map[string]string { 156 | return map[string]string{"app": "elastalert"} 157 | } 158 | 159 | func GetUtcTimeString() string { 160 | return time.Now().UTC().Format("2006-01-02T15:04:05+08:00") 161 | } 162 | func GetUtcTime() time.Time { 163 | return time.Now().UTC() 164 | } 165 | -------------------------------------------------------------------------------- /controllers/podspec/secert_test.go: -------------------------------------------------------------------------------- 1 | package podspec 2 | 3 | import ( 4 | "github.com/stretchr/testify/require" 5 | "github.com/toughnoah/elastalert-operator/api/v1alpha1" 6 | v1 "k8s.io/api/core/v1" 7 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 8 | "k8s.io/client-go/kubernetes/scheme" 9 | "testing" 10 | "time" 11 | ) 12 | 13 | func TestGenerateCertSecret(t *testing.T) { 14 | testCases := []struct { 15 | name string 16 | elastalert v1alpha1.Elastalert 17 | want v1.Secret 18 | }{ 19 | { 20 | name: "test generate default secret", 21 | elastalert: v1alpha1.Elastalert{ 22 | ObjectMeta: metav1.ObjectMeta{ 23 | Name: "test", 24 | }, 25 | Spec: v1alpha1.ElastalertSpec{ 26 | Cert: "abc", 27 | }, 28 | }, 29 | want: v1.Secret{ 30 | ObjectMeta: metav1.ObjectMeta{ 31 | Name: "test" + DefaultCertSuffix, 32 | OwnerReferences: []metav1.OwnerReference{ 33 | { 34 | APIVersion: "v1", 35 | Kind: "Elastalert", 36 | Name: "test", 37 | UID: "", 38 | Controller: &varTrue, 39 | BlockOwnerDeletion: &varTrue, 40 | }, 41 | }, 42 | }, 43 | Data: map[string][]byte{ 44 | DefaultElasticCertName: []byte("abc"), 45 | }, 46 | }, 47 | }, 48 | } 49 | for _, tc := range testCases { 50 | t.Run(tc.name, func(t *testing.T) { 51 | s := scheme.Scheme 52 | s.AddKnownTypes(v1.SchemeGroupVersion, &v1alpha1.Elastalert{}) 53 | have, err := GenerateCertSecret(s, &tc.elastalert) 54 | require.NoError(t, err) 55 | require.Equal(t, tc.want, *have) 56 | }) 57 | } 58 | } 59 | 60 | func TestGetUtcTime(t *testing.T) { 61 | require.NotEqual(t, GetUtcTime(), time.Time{}) 62 | } 63 | func TestGetUtcTimeString(t *testing.T) { 64 | require.NotEqual(t, GetUtcTimeString(), "2006-01-02T15:04:05+08:00") 65 | } 66 | -------------------------------------------------------------------------------- /controllers/podspec/secret.go: -------------------------------------------------------------------------------- 1 | package podspec 2 | 3 | import ( 4 | esv1alpha1 "github.com/toughnoah/elastalert-operator/api/v1alpha1" 5 | corev1 "k8s.io/api/core/v1" 6 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 7 | "k8s.io/apimachinery/pkg/runtime" 8 | ctrl "sigs.k8s.io/controller-runtime" 9 | ) 10 | 11 | func GenerateCertSecret(Scheme *runtime.Scheme, e *esv1alpha1.Elastalert) (*corev1.Secret, error) { 12 | se := BuildCertSecret(e) 13 | if err := ctrl.SetControllerReference(e, se, Scheme); err != nil { 14 | log.Error( 15 | err, 16 | "Failed to generate Secret", 17 | "Elastalert.Namespace", e.Namespace, 18 | ) 19 | return nil, err 20 | } 21 | return se, nil 22 | } 23 | 24 | func BuildCertSecret(e *esv1alpha1.Elastalert) *corev1.Secret { 25 | var data = map[string][]byte{} 26 | stringCert := e.Spec.Cert 27 | data[DefaultElasticCertName] = []byte(stringCert) 28 | secret := &corev1.Secret{ 29 | ObjectMeta: metav1.ObjectMeta{ 30 | Name: e.Name + DefaultCertSuffix, 31 | Namespace: e.Namespace, 32 | }, 33 | Data: data, 34 | } 35 | return secret 36 | } 37 | -------------------------------------------------------------------------------- /controllers/test/e2e/deploy_test.go: -------------------------------------------------------------------------------- 1 | package e2e 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | . "github.com/onsi/ginkgo" 7 | . "github.com/onsi/gomega" 8 | "github.com/toughnoah/elastalert-operator/api/v1alpha1" 9 | "gopkg.in/yaml.v2" 10 | appsv1 "k8s.io/api/apps/v1" 11 | v1 "k8s.io/api/core/v1" 12 | "k8s.io/apimachinery/pkg/api/resource" 13 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 14 | "k8s.io/apimachinery/pkg/types" 15 | "reflect" 16 | "time" 17 | ) 18 | 19 | const interval = time.Second * 1 20 | const timeout = time.Second * 30 21 | 22 | var ( 23 | ConfigSample = map[string]interface{}{ 24 | "es_host": "es.com.cn", 25 | "es_port": 9200, 26 | "use_ssl": true, 27 | "es_username": "elastic", 28 | "es_password": "changeme", 29 | "verify_certs": false, 30 | "writeback_index": "elastalert", 31 | "rules_folder": "/etc/elastalert/rules/..data/", 32 | "run_every": map[string]interface{}{ 33 | "minutes": 1, 34 | }, 35 | "buffer_time": map[string]interface{}{ 36 | "minutes": 15, 37 | }, 38 | } 39 | RuleSample1 = map[string]interface{}{ 40 | "name": "test-elastalert", 41 | "type": "any", 42 | "index": "api-*", 43 | "filter": []map[string]interface{}{ 44 | { 45 | "query": map[string]interface{}{ 46 | "query_string": map[string]interface{}{ 47 | "query": "http_status_code: 503", 48 | }, 49 | }, 50 | }, 51 | }, 52 | "alert": []string{ 53 | "post", 54 | }, 55 | "http_post_url": "https://test.com/alerts", 56 | "http_post_timeout": 60, 57 | } 58 | RuleSample2 = map[string]interface{}{ 59 | "name": "check-elastalert", 60 | "type": "any", 61 | "index": "kpi-*", 62 | "filter": []map[string]interface{}{ 63 | { 64 | "query": map[string]interface{}{ 65 | "query_string": map[string]interface{}{ 66 | "query": "http_status_code: 600", 67 | }, 68 | }, 69 | }, 70 | }, 71 | "alert": []string{ 72 | "post", 73 | }, 74 | "http_post_url": "https://test.com/alerts", 75 | "http_post_timeout": 60, 76 | } 77 | Key = types.NamespacedName{ 78 | Name: "e2e-elastalert", 79 | Namespace: "default", 80 | } 81 | ) 82 | 83 | var _ = Describe("Elastalert Controller", func() { 84 | BeforeEach(func() { 85 | // Add any setup steps that needs to be executed before each test 86 | }) 87 | 88 | AfterEach(func() { 89 | ea := &v1alpha1.Elastalert{ 90 | ObjectMeta: metav1.ObjectMeta{ 91 | Namespace: Key.Namespace, 92 | Name: Key.Name, 93 | }, 94 | } 95 | _ = k8sClient.Delete(context.Background(), ea) 96 | }) 97 | 98 | Context("Deploy Elastalert", func() { 99 | It("Test create Elastalert with wrong config", func() { 100 | elastalert := &v1alpha1.Elastalert{ 101 | ObjectMeta: metav1.ObjectMeta{ 102 | Namespace: Key.Namespace, 103 | Name: Key.Name, 104 | }, 105 | Spec: v1alpha1.ElastalertSpec{ 106 | ConfigSetting: v1alpha1.NewFreeForm(map[string]interface{}{ 107 | "config": "test", 108 | }), 109 | Rule: []v1alpha1.FreeForm{ 110 | v1alpha1.NewFreeForm(map[string]interface{}{ 111 | "name": "test-elastalert", 112 | "type": "any", 113 | }), 114 | }, 115 | PodTemplateSpec: v1.PodTemplateSpec{ 116 | Spec: v1.PodSpec{ 117 | Containers: []v1.Container{}, 118 | }, 119 | }, 120 | }, 121 | } 122 | Expect(k8sClient.Create(context.Background(), elastalert)).Should(Succeed()) 123 | 124 | By("Check the cert secret exists.") 125 | Eventually(func() error { 126 | err := k8sClient.Get(context.Background(), types.NamespacedName{ 127 | Name: "e2e-elastalert-es-cert", 128 | Namespace: "default", 129 | }, &v1.Secret{}) 130 | return err 131 | }, timeout, interval).Should(Succeed()) 132 | 133 | elastalert = &v1alpha1.Elastalert{} 134 | 135 | By("Start checking for INITIALIZING status") 136 | Eventually(func() string { 137 | _ = k8sClient.Get(context.Background(), Key, elastalert) 138 | return elastalert.Status.Phase 139 | }, timeout*8, interval).Should(Equal("INITIALIZING")) 140 | 141 | By("Start waiting for FAILED status with wrong config") 142 | Eventually(func() string { 143 | _ = k8sClient.Get(context.Background(), Key, elastalert) 144 | return elastalert.Status.Phase 145 | }, timeout*8, interval).Should(Equal("FAILED")) 146 | 147 | By("Update elastalert config.yaml then check restart.") 148 | Expect(k8sClient.Get(context.Background(), Key, elastalert)).To(Succeed()) 149 | 150 | By("Start update elastalert pod template and fix the wrong config") 151 | elastalert.ObjectMeta.Annotations = map[string]string{ 152 | "sidecar.istio.io/inject": "false", 153 | } 154 | elastalert.Spec.PodTemplateSpec.Spec.Containers = append(elastalert.Spec.PodTemplateSpec.Spec.Containers, v1.Container{ 155 | Name: "elastalert", 156 | Resources: v1.ResourceRequirements{ 157 | Limits: map[v1.ResourceName]resource.Quantity{ 158 | v1.ResourceMemory: resource.MustParse("4Gi"), 159 | v1.ResourceCPU: resource.MustParse("2"), 160 | }, 161 | Requests: map[v1.ResourceName]resource.Quantity{ 162 | v1.ResourceMemory: resource.MustParse("1Gi"), 163 | v1.ResourceCPU: resource.MustParse("1"), 164 | }, 165 | }, 166 | }) 167 | elastalert.Spec.ConfigSetting = v1alpha1.NewFreeForm(ConfigSample) 168 | elastalert.Spec.Rule = []v1alpha1.FreeForm{ 169 | v1alpha1.NewFreeForm(RuleSample1), 170 | v1alpha1.NewFreeForm(RuleSample2), 171 | } 172 | Expect(k8sClient.Update(context.Background(), elastalert)).To(Succeed()) 173 | 174 | By("Start checking for INITIALIZING status again") 175 | Eventually(func() string { 176 | _ = k8sClient.Get(context.Background(), Key, elastalert) 177 | return elastalert.Status.Phase 178 | }, timeout*8, interval).Should(Equal("INITIALIZING")) 179 | 180 | By("Check RUNNING status") 181 | Eventually(func() string { 182 | _ = k8sClient.Get(context.Background(), Key, elastalert) 183 | return elastalert.Status.Phase 184 | }, timeout*8, interval).Should(Equal("RUNNING")) 185 | 186 | By("Start waiting deployment to be stable.") 187 | dep := &appsv1.Deployment{} 188 | Eventually(func() int { 189 | _ = k8sClient.Get(context.Background(), Key, dep) 190 | return int(dep.Status.AvailableReplicas) 191 | }, timeout*4, interval).Should(Equal(1)) 192 | 193 | By("Check pod resources") 194 | Eventually(func() bool { 195 | _ = k8sClient.Get(context.Background(), Key, dep) 196 | if len(dep.Spec.Template.Spec.Containers) == 0 { 197 | return false 198 | } 199 | return reflect.DeepEqual(dep.Spec.Template.Spec.Containers[0].Resources, elastalert.Spec.PodTemplateSpec.Spec.Containers[0].Resources) 200 | }, timeout, interval).Should(Equal(true)) 201 | 202 | By("Check pod annotations") 203 | Expect(dep.Spec.Template.Annotations["sidecar.istio.io/inject"]).Should(Equal("false")) 204 | 205 | By("check elastalert rules.") 206 | Eventually(func() bool { 207 | RuleConfigMap := &v1.ConfigMap{} 208 | _ = k8sClient.Get(context.Background(), types.NamespacedName{ 209 | Name: "e2e-elastalert-rule", 210 | Namespace: "default", 211 | }, RuleConfigMap) 212 | return compare(RuleConfigMap.Data["test-elastalert.yaml"], RuleSample1) && compare(RuleConfigMap.Data["check-elastalert.yaml"], RuleSample2) 213 | }, timeout, interval).Should(Equal(true)) 214 | 215 | By("Check config.yaml configmap.") 216 | Eventually(func() bool { 217 | configConfigMap := &v1.ConfigMap{} 218 | _ = k8sClient.Get(context.Background(), types.NamespacedName{ 219 | Name: "e2e-elastalert-config", 220 | Namespace: "default", 221 | }, configConfigMap) 222 | return compare(configConfigMap.Data["config.yaml"], ConfigSample) 223 | }, timeout, interval).Should(Equal(true)) 224 | 225 | By("Start to test deployment reconcile") 226 | Eventually(func() error { 227 | dep = &appsv1.Deployment{ 228 | ObjectMeta: metav1.ObjectMeta{ 229 | Name: Key.Name, 230 | Namespace: Key.Namespace, 231 | }, 232 | } 233 | return k8sClient.Delete(context.Background(), dep) 234 | }, timeout, interval).Should(Succeed()) 235 | 236 | By("Start waiting deployment to be stable.") 237 | Eventually(func() int { 238 | dep = &appsv1.Deployment{} 239 | _ = k8sClient.Get(context.Background(), Key, dep) 240 | return int(dep.Status.AvailableReplicas) 241 | }, timeout*4, interval).Should(Equal(1)) 242 | 243 | }) 244 | }) 245 | }) 246 | 247 | func compare(source string, dest map[string]interface{}) bool { 248 | out, _ := yaml.Marshal(dest) 249 | return bytes.Compare([]byte(source), out) == 0 250 | } 251 | -------------------------------------------------------------------------------- /controllers/test/e2e/suite_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021. 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 e2e 18 | 19 | import ( 20 | "context" 21 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 22 | "path/filepath" 23 | "testing" 24 | 25 | . "github.com/onsi/ginkgo" 26 | . "github.com/onsi/gomega" 27 | "k8s.io/client-go/kubernetes/scheme" 28 | "k8s.io/client-go/rest" 29 | "sigs.k8s.io/controller-runtime/pkg/client" 30 | "sigs.k8s.io/controller-runtime/pkg/envtest" 31 | "sigs.k8s.io/controller-runtime/pkg/envtest/printer" 32 | logf "sigs.k8s.io/controller-runtime/pkg/log" 33 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 34 | 35 | esv1alpha1 "github.com/toughnoah/elastalert-operator/api/v1alpha1" 36 | //+kubebuilder:scaffold:imports 37 | ) 38 | 39 | // These tests use Ginkgo (BDD-style Go testing framework). Refer to 40 | // http://onsi.github.io/ginkgo/ to learn more about Ginkgo. 41 | 42 | var cfg *rest.Config 43 | var k8sClient client.Client 44 | var testEnv *envtest.Environment 45 | 46 | func TestAPIs(t *testing.T) { 47 | RegisterFailHandler(Fail) 48 | RunSpecsWithDefaultAndCustomReporters(t, 49 | "Controller Suite", 50 | []Reporter{printer.NewlineReporter{}}) 51 | } 52 | 53 | var _ = BeforeSuite(func() { 54 | var t = true 55 | logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) 56 | By("bootstrapping test environment") 57 | testEnv = &envtest.Environment{ 58 | CRDDirectoryPaths: []string{filepath.Join("../../", "config", "crd", "bases")}, 59 | ErrorIfCRDPathMissing: true, 60 | UseExistingCluster: &t, 61 | } 62 | 63 | cfg, err := testEnv.Start() 64 | Expect(err).NotTo(HaveOccurred()) 65 | Expect(cfg).NotTo(BeNil()) 66 | 67 | err = esv1alpha1.AddToScheme(scheme.Scheme) 68 | Expect(err).NotTo(HaveOccurred()) 69 | 70 | //+kubebuilder:scaffold:scheme 71 | 72 | k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) 73 | Expect(err).NotTo(HaveOccurred()) 74 | Expect(k8sClient).NotTo(BeNil()) 75 | 76 | }, 60) 77 | 78 | var _ = AfterSuite(func() { 79 | By("tearing down the test environment") 80 | err := testEnv.Stop() 81 | Expect(err).NotTo(HaveOccurred()) 82 | _ = k8sClient.Delete(context.Background(), 83 | &esv1alpha1.Elastalert{ 84 | ObjectMeta: metav1.ObjectMeta{ 85 | Namespace: Key.Namespace, 86 | Name: Key.Name, 87 | }, 88 | }) 89 | }) 90 | -------------------------------------------------------------------------------- /deploy/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: elastalert-operator 5 | namespace: alert 6 | labels: 7 | app: elastalert-operator 8 | version: v1 9 | spec: 10 | replicas: 1 11 | selector: 12 | matchLabels: 13 | app: elastalert-operator 14 | version: v1 15 | template: 16 | metadata: 17 | labels: 18 | app: elastalert-operator 19 | version: v1 20 | spec: 21 | serviceAccountName: elastalert-operator 22 | containers: 23 | - name: elastalert-operator 24 | image: toughnoah/elastalert-operator:v1.0 25 | imagePullPolicy: Always 26 | ports: 27 | - containerPort: 8080 28 | 29 | -------------------------------------------------------------------------------- /deploy/role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: elastalert-operator 5 | rules: 6 | - apiGroups: 7 | - es.noah.domain 8 | resources: 9 | - elastalerts 10 | verbs: 11 | - create 12 | - delete 13 | - get 14 | - list 15 | - patch 16 | - update 17 | - watch 18 | - apiGroups: 19 | - es.noah.domain 20 | resources: 21 | - elastalerts/finalizers 22 | verbs: 23 | - update 24 | - apiGroups: 25 | - es.noah.domain 26 | resources: 27 | - elastalerts/status 28 | verbs: 29 | - get 30 | - patch 31 | - update 32 | - apiGroups: 33 | - "" 34 | resources: 35 | - pods 36 | - secrets 37 | - configmaps 38 | - events 39 | verbs: 40 | - get 41 | - list 42 | - watch 43 | - create 44 | - update 45 | - patch 46 | - delete 47 | - apiGroups: 48 | - apps 49 | resources: 50 | - deployments 51 | verbs: 52 | - get 53 | - list 54 | - watch 55 | - create 56 | - update 57 | - patch 58 | - delete 59 | -------------------------------------------------------------------------------- /deploy/role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | name: elastalert-operator 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: ClusterRole 8 | name: elastalert-operator 9 | subjects: 10 | - kind: ServiceAccount 11 | name: elastalert-operator 12 | namespace: alert 13 | -------------------------------------------------------------------------------- /deploy/service_account.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: elastalert-operator 5 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/toughnoah/elastalert-operator 2 | 3 | go 1.16 4 | 5 | replace github.com/bouk/monkey v1.0.2 => bou.ke/monkey v1.0.0 6 | 7 | require ( 8 | github.com/bouk/monkey v1.0.2 9 | github.com/onsi/ginkgo v1.16.4 10 | github.com/onsi/gomega v1.14.0 11 | github.com/stretchr/testify v1.7.0 12 | gopkg.in/yaml.v2 v2.4.0 13 | k8s.io/api v0.21.3 14 | k8s.io/apimachinery v0.21.3 15 | k8s.io/client-go v0.21.3 16 | sigs.k8s.io/controller-runtime v0.9.5 17 | ) 18 | -------------------------------------------------------------------------------- /hack/boilerplate.go.txt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021. 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 | */ -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021. 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 | "github.com/toughnoah/elastalert-operator/controllers/observer" 22 | "os" 23 | 24 | // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) 25 | // to ensure that exec-entrypoint and run can make use of them. 26 | _ "k8s.io/client-go/plugin/pkg/client/auth" 27 | 28 | "k8s.io/apimachinery/pkg/runtime" 29 | utilruntime "k8s.io/apimachinery/pkg/util/runtime" 30 | clientgoscheme "k8s.io/client-go/kubernetes/scheme" 31 | ctrl "sigs.k8s.io/controller-runtime" 32 | "sigs.k8s.io/controller-runtime/pkg/healthz" 33 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 34 | 35 | esv1alpha1 "github.com/toughnoah/elastalert-operator/api/v1alpha1" 36 | "github.com/toughnoah/elastalert-operator/controllers" 37 | //+kubebuilder:scaffold:imports 38 | ) 39 | 40 | var ( 41 | scheme = runtime.NewScheme() 42 | setupLog = ctrl.Log.WithName("setup") 43 | ) 44 | 45 | func init() { 46 | utilruntime.Must(clientgoscheme.AddToScheme(scheme)) 47 | 48 | utilruntime.Must(esv1alpha1.AddToScheme(scheme)) 49 | //+kubebuilder:scaffold:scheme 50 | } 51 | 52 | func main() { 53 | var metricsAddr string 54 | var enableLeaderElection bool 55 | var probeAddr string 56 | flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.") 57 | flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") 58 | flag.BoolVar(&enableLeaderElection, "leader-elect", false, 59 | "Enable leader election for controller manager. "+ 60 | "Enabling this will ensure there is only one active controller manager.") 61 | 62 | opts := zap.Options{} 63 | opts.BindFlags(flag.CommandLine) 64 | flag.Parse() 65 | 66 | ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) 67 | 68 | mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ 69 | Scheme: scheme, 70 | MetricsBindAddress: metricsAddr, 71 | Port: 9443, 72 | HealthProbeBindAddress: probeAddr, 73 | LeaderElection: enableLeaderElection, 74 | LeaderElectionID: "a81002ee.noah.domain", 75 | }) 76 | if err != nil { 77 | setupLog.Error(err, "unable to start manager") 78 | os.Exit(1) 79 | } 80 | 81 | if err = (&controllers.ElastalertReconciler{ 82 | Client: mgr.GetClient(), 83 | Scheme: mgr.GetScheme(), 84 | Recorder: mgr.GetEventRecorderFor("elastalert"), 85 | Observer: *observer.NewManager(), 86 | }).SetupWithManager(mgr); err != nil { 87 | setupLog.Error(err, "unable to create controller", "controller", "Elastalert") 88 | os.Exit(1) 89 | } 90 | //+kubebuilder:scaffold:builder 91 | 92 | if err = (&controllers.DeploymentReconciler{ 93 | Client: mgr.GetClient(), 94 | Scheme: mgr.GetScheme(), 95 | }).SetupWithManager(mgr); err != nil { 96 | setupLog.Error(err, "unable to create controller", "controller", "Deployment") 97 | os.Exit(1) 98 | } 99 | 100 | if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { 101 | setupLog.Error(err, "unable to set up health check") 102 | os.Exit(1) 103 | } 104 | if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil { 105 | setupLog.Error(err, "unable to set up ready check") 106 | os.Exit(1) 107 | } 108 | 109 | setupLog.Info("starting manager") 110 | if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { 111 | setupLog.Error(err, "problem running manager") 112 | os.Exit(1) 113 | } 114 | } 115 | --------------------------------------------------------------------------------