├── .commitlintrc.yml ├── .dockerignore ├── .editorconfig ├── .github └── workflows │ ├── build.yaml │ └── release.yaml ├── .gitignore ├── .helmignore ├── .releaserc.yaml ├── CONTRIBUTING.md ├── Chart.yaml ├── LICENSE ├── README.md ├── arch.puml ├── artifacthub-repo.yml ├── cspell.json ├── justfile ├── package-lock.json ├── package.json ├── skaffold.yaml ├── templates ├── _utils.tpl ├── configmap.yaml ├── cronjob-reconcile.yaml ├── deployment.yaml ├── ingress.yaml ├── ingressroute.yaml ├── job-setup.yaml ├── secret.yaml └── service.yaml ├── test ├── e2e │ ├── conftest.py │ ├── default │ │ ├── test.py │ │ └── values.yaml │ ├── k3d.yaml │ ├── kafka.yaml │ ├── requirements.txt │ └── traefik.py ├── lint │ ├── common.yaml │ ├── subchart │ │ ├── .gitignore │ │ ├── .helmignore │ │ ├── Chart.yaml │ │ ├── templates │ │ │ └── test.yaml │ │ ├── values.schema.json │ │ └── values.yaml │ └── ui.yaml └── unit │ ├── deploy-placement.yaml │ ├── deployment.yaml │ ├── ingress.yaml │ ├── job-annotations.yaml │ ├── reconcile-placement.yaml │ └── setup-placement.yaml ├── values.schema.json └── values.yaml /.commitlintrc.yml: -------------------------------------------------------------------------------- 1 | extends: 2 | - '@commitlint/config-conventional' 3 | 4 | rules: 5 | type-enum: 6 | - 2 7 | - always 8 | - 9 | - chore 10 | - feat 11 | - fix 12 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | venv 2 | .venv 3 | *.json 4 | *.yaml 5 | .github 6 | .git 7 | *.md 8 | .editorconfig 9 | .gitignore 10 | .helmignore 11 | justfile 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | charset = utf-8 6 | insert_final_newline = true 7 | trim_trailing_whitespace = true 8 | indent_style = space 9 | indent_size = 2 10 | 11 | [*.py] 12 | indent_size = 4 13 | 14 | [*.md] 15 | max_line_length = 120 16 | 17 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: 3 | pull_request: 4 | branches: 5 | - master 6 | push: 7 | branches: 8 | - master 9 | paths-ignore: 10 | - '*.md' 11 | - '*.puml' 12 | - '.github/workflows/release.yaml' 13 | jobs: 14 | build_job: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v3 18 | with: 19 | fetch-depth: 20 20 | - uses: wagoid/commitlint-github-action@v5 21 | with: 22 | configFile: .commitlintrc.yml 23 | token: ${{ secrets.GITHUB_TOKEN }} 24 | - uses: jaxxstorm/action-install-gh-release@v1 25 | with: 26 | repo: GoogleContainerTools/skaffold 27 | tag: v1.38.0 28 | cache: enable 29 | extension-matching: disable 30 | rename-to: skaffold 31 | chmod: 0755 32 | - uses: extractions/setup-just@v1 33 | env: 34 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 35 | - run: just test 36 | - uses: rinx/setup-k3d@v0.0.4 37 | with: 38 | skipClusterCreation: true 39 | - run: just k3d 40 | - run: just test-e2e 41 | 42 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Publish release 2 | on: 3 | workflow_dispatch 4 | jobs: 5 | rel_job: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v3 9 | with: 10 | fetch-depth: 0 11 | - uses: bahmutov/npm-install@v1 12 | - run: npx semantic-release 13 | env: 14 | REGISTRY_USERNAME: ${{ github.actor }} 15 | REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} 16 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | venv 2 | CHANGELOG.md 3 | *.tgz 4 | node_modules 5 | .idea 6 | values-dev.yaml 7 | __pycache__ 8 | .venv 9 | 10 | -------------------------------------------------------------------------------- /.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | 25 | venv 26 | .venv 27 | *.tgz 28 | .github 29 | *.py 30 | *.txt 31 | .release-it.json 32 | CONTRIBUTING.md 33 | skaffold.yaml 34 | values-lint.yaml 35 | node_modules 36 | *.sh 37 | package-lock.json 38 | package.json 39 | *.puml 40 | .editorconfig 41 | .dockerignore 42 | test/ 43 | justfile 44 | 45 | -------------------------------------------------------------------------------- /.releaserc.yaml: -------------------------------------------------------------------------------- 1 | branches: 2 | - master 3 | 4 | tagFormat: "${version}" 5 | 6 | preset: "conventionalcommits" 7 | 8 | plugins: 9 | - "@semantic-release/commit-analyzer" 10 | - "semantic-release-commitlint" 11 | - "@semantic-release/release-notes-generator" 12 | - 13 | - "@semantic-release/git" 14 | - assets: [] 15 | messsage: false 16 | - 17 | - "@semantic-release/github" 18 | - successComment: false 19 | failComment: false 20 | - 21 | - '@eshepelyuk/semantic-release-helm-oci' 22 | - registry: oci://ghcr.io/eshepelyuk/helm 23 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing guide 2 | 3 | ## Requirements 4 | 5 | ### Commit message format 6 | 7 | This repository uses [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/). 8 | 9 | Use only following _types_ to create valid commit messages: 10 | 11 | * `feat`, for a new functionality. 12 | * `fix`, for a bug fixes. 13 | * `chore`, for everything else (CI/CD, docs, etc) 14 | 15 | Also, we would like PRs to contain a single commit and be rebased onto project `master` branch before submitting. 16 | 17 | ### Test coverage 18 | 19 | The project is covered by following types of tests 20 | 21 | * linter tests - `test/linter` 22 | * unit tests - `test/unit` 23 | * e2e tests - `test/e2e` 24 | 25 | Every PR should provide appropriate amount of testing, corresponding to its scope. 26 | 27 | Use [just](https://github.com/casey/just) to run test locally: 28 | 29 | * `just test-lint` - execute linter tests 30 | * `just test-unit` - execute unit tests 31 | 32 | ## Development flow. 33 | 34 | Although, you could choose your own approach to create branches, develop code 35 | and provide new PR, we recommend following. 36 | 37 | 1. Fork this project and clone it to local PC. 38 | 39 | 1. Add this project as a new remote named `upstream`. 40 | 41 | ```sh 42 | git remote add upstream https://github.com/eshepelyuk/cmak-operator.git 43 | git fetch upstream 44 | ``` 45 | 46 | 1. Create a new branch, based on `upstream` master and push it to your repository. 47 | 48 | ```sh 49 | git checkout --no-track -b upstream/master 50 | git push -u origin 51 | ``` 52 | 53 | 1. Develop your code locally, test and commit. 54 | 55 | ```sh 56 | git commit -am '' 57 | ``` 58 | 59 | 1. Before pushing code to `origin` ensure your working branch is rebased onto `upstream/master`. 60 | 61 | ```sh 62 | git fetch upstream 63 | git rebase -i upstream/master 64 | ``` 65 | During rebase, make your PR to be comprised of a single commit, 66 | unless, you _really_ want to provide multiple commits via single PR. 67 | 68 | 1. Push your branch and create a PR via GitHub UI. 69 | 70 | ```sh 71 | git push -u -f origin 72 | ``` 73 | 74 | -------------------------------------------------------------------------------- /Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: cmak-operator 3 | description: Manage and vizualize Kafka clusters with CMAK (prev. Kafka Manager). 4 | icon: https://www.vectorlogo.zone/logos/kubernetes/kubernetes-icon.svg 5 | type: application 6 | version: 0.0.0 # actual chart version is managed by git tags 7 | appVersion: 0.0.0 8 | 9 | home: https://github.com/eshepelyuk/cmak-operator 10 | 11 | sources: 12 | - https://github.com/yahoo/CMAK 13 | - https://github.com/eshepelyuk/cmak-docker 14 | 15 | maintainers: 16 | - name: Ievgenii Shepeliuk 17 | email: eshepelyuk@gmail.com 18 | url: https://github.com/eshepelyuk 19 | 20 | keywords: 21 | - cmak 22 | - operator 23 | - kafka-manager 24 | - kafka 25 | - kubernetes 26 | - ui 27 | - monitoring 28 | - streaming 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Ievgenii Shepeliuk 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CMAK (prev. Kafka Manager) for Kubernetes 2 | 3 | [![Current](https://img.shields.io/github/v/tag/eshepelyuk/cmak-operator?logo=github&sort=semver&style=for-the-badge&label=current)](https://github.com/eshepelyuk/cmak-operator/releases/latest) 4 | [![Artifact HUB](https://img.shields.io/endpoint?style=for-the-badge&url=https://artifacthub.io/badge/repository/cmak-operator)](https://artifacthub.io/packages/helm/cmak-operator/cmak-operator) 5 | [![MIT License](https://img.shields.io/github/license/eshepelyuk/cmak-operator?logo=mit&style=for-the-badge)](https://opensource.org/licenses/MIT) 6 | 7 | [CMAK](https://github.com/yahoo/CMAK) (prev. Kafka Manager) 8 | is a tool for monitoring and managing [Apache Kafka](https://kafka.apache.org/) clusters. 9 | 10 | CMAK operator is a Helm chart combining set of utilities, 11 | that allows to install and configure CMAK in K8s cluster. 12 | 13 | ![Component diagram](https://www.plantuml.com/plantuml/proxy?cache=no&src=https://raw.githubusercontent.com/eshepelyuk/cmak-operator/master/arch.puml) 14 | 15 | CMAK operator comprises following components: 16 | 17 | * [CMAK](https://github.com/yahoo/CMAK/), 18 | powered by [CMAK docker](https://github.com/eshepelyuk/cmak-docker). 19 | * [Apache ZooKeeper](https://zookeeper.apache.org/), 20 | powered by [official Docker image](https://hub.docker.com/_/zookeeper/). 21 | * Custom [cmak2zk tool](https://github.com/users/eshepelyuk/packages/container/package/dckr%2Fcmak2zk), 22 | for configuring Kafka clusters in CMAK from YAML files. 23 | 24 | ## Installation 25 | 26 | `cmak-operator` chart is published into OCI compatible registry 27 | and requires Helm version >= 3.8. 28 | 29 | It's recommended to install CMAK operator into a dedicated namespace. 30 | To install specific `VERSION` use following command. 31 | 32 | ```sh 33 | helm upgrade -i --wait --create-namespace -n cmak cmak-operator \ 34 | oci://ghcr.io/eshepelyuk/helm/cmak-operator --version 35 | ``` 36 | 37 | To install the latest version - omit `--version` flag from previous command. 38 | 39 | ### Verify installation 40 | 41 | By default, CMAK operator doesn't create neither `Ingress` 42 | nor any other K8s resources to expose UI via HTTP. 43 | 44 | The simpliest test is to port forward CMAK UI HTTP port and access it from browser. 45 | 46 | ```sh 47 | kubectl port-forward -n cmak service/cmak 9000 48 | ``` 49 | 50 | Then, open http://localhost:9000 in a browser. 51 | 52 | ## Configuration 53 | 54 | Configuration should be passed to helm via command line during installation or upgrade. 55 | 56 | ```sh 57 | helm upgrade -i --wait --create-namespace -n cmak cmak-operator \ 58 | oci://ghcr.io/eshepelyuk/helm/cmak-operator --version \ 59 | -f cmak-values.yaml 60 | ``` 61 | 62 | ### CMAK application settings 63 | 64 | CMAK uses configuration file 65 | [/cmak/conf/application.conf](https://github.com/yahoo/CMAK/blob/master/conf/application.conf). 66 | Every parameter could be overriden via JVM system property, i.e. `-DmyProp=myVal`. 67 | Properties are passed to CMAK container via `values.yaml`. 68 | 69 | For example, to enable basic auth, add following to `values.yaml`. 70 | 71 | ```yaml 72 | ui: 73 | extraArgs: 74 | - "-DbasicAuthentication.enabled=true" 75 | - "-DbasicAuthentication.username=admin" 76 | - "-DbasicAuthentication.password=password" 77 | ``` 78 | 79 | ### Kafka clusters 80 | 81 | It's extremely easy to configure multiple clusters in CMAK, 82 | starting from cluster setup, connection settings and ending with authorization, 83 | using [Helm values files](https://helm.sh/docs/chart_template_guide/values_files/). 84 | 85 | Check [CMAK operator values](https://artifacthub.io/packages/helm/cmak-operator/cmak-operator?modal=values-schema) 86 | for all available options and their description. 87 | 88 | Minimal values.yaml configuration for adding a several Kafka clusters to CMAK. 89 | 90 | ```yaml 91 | cmak: 92 | clusters: 93 | - name: "cluster-stage" 94 | curatorConfig: 95 | zkConnect: "kafka01.stage:2181,kafka02.stage:2181" 96 | - name: "cluster-prod" 97 | curatorConfig: 98 | zkConnect: "kafka01.prod:2181,kafka02.prod:2181,kafka03.prod:2181" 99 | ``` 100 | 101 | Connection settings could be configured for all clusters at once or per selected cluster. 102 | 103 | ```yaml 104 | cmak: 105 | clustersCommon: 106 | curatorConfig: 107 | zkMaxRetry: 100 # <1> 108 | clusters: 109 | - name: "cluster-stage" 110 | kafkaVersion: "2.4.0" # <2> 111 | curatorConfig: 112 | zkConnect: "kafka01.stage:2181,kafka02.stage:2181" 113 | - name: "cluster-prod" 114 | kafkaVersion: "2.1.0" # <3> 115 | enabled: false 116 | curatorConfig: 117 | zkConnect: "kafka01.prod:2181,kafka02.prod:2181,kafka03.prod:2181" 118 | ``` 119 | 120 | 1. this setting is applied to both clusters. 121 | 1. applied only to `cluster-stage`. 122 | 1. applied only to `cluster-prod`. 123 | 124 | ## Alternatives 125 | 126 | [AKHQ](https://akhq.io/) project seems to be the most active open source tool 127 | for managing and monitoring Kafka clusters. 128 | It could be missing some functionality from CMAK, 129 | but their developers are open for feature requests and contributions. 130 | 131 | ## How to contribute 132 | 133 | Your contributions like feature suggesstions, bug reports and pull requests are always welcomed. 134 | 135 | Please check [CONTRIBUTING](./CONTRIBUTING.md) guide for details. 136 | 137 | -------------------------------------------------------------------------------- /arch.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | skinparam componentStyle uml2 3 | 4 | cloud "Kubernetes cluster" { 5 | component "CronJob\ncontroller" as cron <> 6 | () "CMAK UI" as ui <> #White 7 | package "CMAK namespace" as ns { 8 | frame "CMAK" as pod <> { 9 | component "CMAK UI" as cmak <> 10 | component "Zookeeper" as zk <> 11 | cmak -r(0- zk : CMAK clusters settings\nread from ZK 12 | } 13 | frame "Reconciliation" as cronPod <> { 14 | component cmak2zk <> 15 | component "Kafka settings" as cm <> 16 | cmak2zk .r.> cm : 1. compares with ZK 17 | } 18 | } 19 | 20 | cron .l.> cronPod : periodically\nschedules 21 | cmak2zk .d.> zk : <>\n 2. upserts to ZK 22 | 23 | ui -r- cmak 24 | } 25 | 26 | @enduml 27 | -------------------------------------------------------------------------------- /artifacthub-repo.yml: -------------------------------------------------------------------------------- 1 | # Artifact Hub repository metadata file 2 | repositoryID: 18a109e9-bb1c-4ed3-810d-2af879cb87ff 3 | owners: 4 | - name: Ievgenii Shepeliuk 5 | email: eshepelyuk@gmail.com 6 | -------------------------------------------------------------------------------- /cspell.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eshepelyuk/cmak-operator/b3269e6e45aed460f253c16cf13cf82dd4e76e94/cspell.json -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | export E2E_TEST := "default" 2 | 3 | default: 4 | @just --list 5 | 6 | _helm-unittest: 7 | helm plugin ls | grep unittest || helm plugin install https://github.com/helm-unittest/helm-unittest.git 8 | 9 | # run helm unit tests 10 | test-helm filter="*": _helm-unittest 11 | helm unittest -f 'test/unit/{{filter}}.yaml' . 12 | 13 | test: lint test-helm 14 | 15 | # helm linter 16 | lint-helm filter="*": _helm-unittest 17 | helm unittest -f 'test/lint/{{filter}}.yaml' . 18 | 19 | lint: lint-helm 20 | 21 | _skaffold-ctx: 22 | skaffold config set default-repo localhost:5000 23 | 24 | # (re) create local k8s cluster using k3d 25 | k3d: _chk-py && _skaffold-ctx 26 | #!/usr/bin/env bash 27 | set -euxo pipefail 28 | 29 | k3d cluster rm cmak-operator || true 30 | k3d cluster create --config ./test/e2e/k3d.yaml 31 | 32 | source .venv/bin/activate 33 | pytest --capture=tee-sys -p no:warnings test/e2e/traefik.py 34 | 35 | # install into local k8s 36 | up: _skaffold-ctx down 37 | skaffold run 38 | 39 | # remove from local k8s 40 | down: 41 | skaffold delete || true 42 | 43 | _chk-py: 44 | #!/usr/bin/env bash 45 | set -euxo pipefail 46 | if [ ! -d .venv ]; then 47 | python3 -mvenv .venv 48 | pip3 install -r test/e2e/requirements.txt 49 | fi 50 | 51 | # run only e2e test script 52 | test-e2e-sh: _chk-py 53 | #!/usr/bin/env bash 54 | set -euxo pipefail 55 | 56 | source .venv/bin/activate 57 | pytest --capture=tee-sys -p no:warnings test/e2e/{{E2E_TEST}}/test.py 58 | 59 | # run single e2e test 60 | test-e2e: up test-e2e-sh 61 | 62 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "@semantic-release/exec": "^6.0.3", 4 | "@semantic-release/git": "^10.0.1", 5 | "semantic-release": "^19.0.2", 6 | "semantic-release-commitlint": "^1.2.2", 7 | "@commitlint/config-conventional": "^16.2.4", 8 | "@eshepelyuk/semantic-release-helm-oci": "^1.4.0" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /skaffold.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: skaffold/v2beta28 2 | kind: Config 3 | metadata: 4 | name: cmak-operator 5 | deploy: 6 | helm: 7 | releases: 8 | - name: cmak-operator 9 | chartPath: . 10 | wait: true 11 | setValues: 12 | e2e: true 13 | valuesFiles: 14 | - "test/e2e/{{.E2E_TEST}}/values.yaml" 15 | upgradeOnChange: false 16 | skipBuildDependencies: true 17 | -------------------------------------------------------------------------------- /templates/_utils.tpl: -------------------------------------------------------------------------------- 1 | {{- define "cmak.name" -}} 2 | cmak 3 | {{- end -}} 4 | 5 | {{- define "cmak.selectorLabels" -}} 6 | app.kubernetes.io/name: {{ include "cmak.name" . | quote }} 7 | app.kubernetes.io/instance: {{ .Release.Name | quote }} 8 | {{- end -}} 9 | 10 | {{- define "cmak.labels" -}} 11 | app.kubernetes.io/managed-by: {{ .Release.Service | quote }} 12 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 13 | helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version | quote }} 14 | {{ include "cmak.selectorLabels" . }} 15 | {{- end -}} 16 | 17 | {{ define "cmak.consumerProperties" }} 18 | {{ $default_props := dict "key.deserializer" "org.apache.kafka.common.serialization.ByteArrayDeserializer" "value.deserializer" "org.apache.kafka.common.serialization.ByteArrayDeserializer" -}} 19 | {{ $consumer_propes := merge .Values.ui.consumerProperties $default_props -}} 20 | {{- range $key, $val := $consumer_propes }} 21 | {{ $key }}={{ $val -}} 22 | {{- end }} 23 | {{ if .Values.ui.consumerPropertiesSsl }} 24 | {{- with .Values.ui.consumerPropertiesSsl -}} 25 | ssl.truststore.location=/conf/ssl/truststore 26 | ssl.truststore.type={{ .truststore.type }} 27 | ssl.truststore.password= {{ .truststore.password }} 28 | ssl.keystore.location=/conf/ssl/keystore 29 | ssl.keystore.type={{ .keystore.type }} 30 | ssl.keystore.password={{ .keystore.password }} 31 | {{- end -}} 32 | {{- end -}} 33 | {{- end -}} 34 | 35 | {{- define "cmak.healthUi" -}} 36 | {{ $httpCtx := "" }} 37 | {{- range .Values.ui.extraArgs -}} 38 | {{- if hasPrefix "-Dplay.http.context=" . -}} 39 | {{- $httpCtx = trimPrefix "-Dplay.http.context=" . -}} 40 | {{- end -}} 41 | {{- end -}} 42 | {{ $httpCtx }}/api/health 43 | {{- end -}} 44 | -------------------------------------------------------------------------------- /templates/configmap.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: {{ include "cmak.name" . }}-clusters 5 | labels: 6 | {{- include "cmak.labels" . | nindent 4 }} 7 | data: 8 | clusters.yaml: |- 9 | {{- pick .Values "cmak" | toYaml | nindent 4 }} 10 | 11 | {{ if .Values.ui.consumerProperties }} 12 | --- 13 | apiVersion: v1 14 | kind: ConfigMap 15 | metadata: 16 | name: {{ include "cmak.name" . }}-consumer-properties 17 | labels: {{ include "cmak.labels" . | nindent 4 }} 18 | data: 19 | consumer.properties: |- 20 | {{- include "cmak.consumerProperties" . | nindent 4 }} 21 | {{- end }} -------------------------------------------------------------------------------- /templates/cronjob-reconcile.yaml: -------------------------------------------------------------------------------- 1 | {{- $nm := include "cmak.name" . -}} 2 | apiVersion: batch/v1 3 | kind: CronJob 4 | metadata: 5 | name: {{ $nm }}-reconcile 6 | labels: 7 | {{- include "cmak.labels" . | nindent 4 }} 8 | spec: 9 | {{- if .Values.reconcile.successfulJobsHistoryLimit }} 10 | successfulJobsHistoryLimit: {{ .Values.reconcile.successfulJobsHistoryLimit }} 11 | {{- end }} 12 | {{- if .Values.reconcile.failedJobsHistoryLimit }} 13 | failedJobsHistoryLimit: {{ .Values.reconcile.failedJobsHistoryLimit }} 14 | {{- end }} 15 | schedule: {{ .Values.reconcile.schedule | quote }} 16 | concurrencyPolicy: "Forbid" 17 | jobTemplate: 18 | spec: 19 | template: 20 | metadata: 21 | {{- with .Values.reconcile.annotations }} 22 | annotations: 23 | {{- toYaml . | nindent 12 }} 24 | {{- end }} 25 | spec: 26 | restartPolicy: Never 27 | volumes: 28 | - name: {{ $nm }}-clusters 29 | configMap: 30 | name: {{ $nm }}-clusters 31 | {{- with .Values.nodeSelector }} 32 | nodeSelector: {{ toYaml . | nindent 12 }} 33 | {{- end }} 34 | {{- with .Values.affinity }} 35 | affinity: {{ toYaml . | nindent 12 }} 36 | {{- end }} 37 | {{- with .Values.tolerations }} 38 | tolerations: {{ toYaml . | nindent 12 }} 39 | {{- end }} 40 | containers: 41 | - name: setup 42 | image: "{{ .Values.reconcile.image.repository }}:{{ .Values.reconcile.image.tag }}" 43 | imagePullPolicy: "{{ .Values.reconcile.image.pullPolicy }}" 44 | args: 45 | - '{{ .Values.reconcile.overwriteZk | ternary "--" "--no-" }}overwrite-zk' 46 | - '{{ $nm }}.{{ $.Release.Namespace }}:2181' 47 | - '/app/etc/clusters.yaml' 48 | volumeMounts: 49 | - name: {{ $nm }}-clusters 50 | mountPath: /app/etc 51 | {{- with .Values.reconcile.resources }} 52 | resources: {{ . | toYaml | nindent 16 }} 53 | {{- end }} 54 | -------------------------------------------------------------------------------- /templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | {{- $nm := include "cmak.name" . -}} 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: {{ include "cmak.name" . }} 6 | labels: 7 | {{- include "cmak.labels" . | nindent 4 }} 8 | spec: 9 | selector: 10 | matchLabels: 11 | {{- include "cmak.selectorLabels" . | nindent 6 }} 12 | template: 13 | metadata: 14 | labels: 15 | {{- include "cmak.labels" . | nindent 8 }} 16 | spec: 17 | {{- if or .Values.ui.consumerProperties .Values.ui.consumerPropertiesSsl }} 18 | volumes: 19 | {{- if .Values.ui.consumerProperties }} 20 | - name: {{ $nm }}-consumer-properties 21 | configMap: 22 | name: {{ $nm }}-consumer-properties 23 | {{- end -}} 24 | {{ if .Values.ui.consumerPropertiesSsl }} 25 | - name: {{ $nm }}-ssl 26 | secret: 27 | secretName: {{ include "cmak.name" . }}-ssl 28 | {{- end -}} 29 | {{- end }} 30 | {{- with .Values.nodeSelector }} 31 | nodeSelector: {{ toYaml . | nindent 8 }} 32 | {{- end }} 33 | {{- with .Values.affinity }} 34 | affinity: {{ toYaml . | nindent 8 }} 35 | {{- end }} 36 | {{- with .Values.tolerations }} 37 | tolerations: {{ toYaml . | nindent 8 }} 38 | {{- end }} 39 | containers: 40 | - name: zk 41 | image: "{{ .Values.zk.image.repository }}:{{ .Values.zk.image.tag }}" 42 | imagePullPolicy: "{{ .Values.zk.image.pullPolicy }}" 43 | ports: 44 | - name: zk 45 | containerPort: 2181 46 | protocol: TCP 47 | env: 48 | - name: ZOO_SERVERS 49 | value: "server.1={{ include "cmak.name" . }}.{{ .Release.Namespace }}:2888:3888;2181" 50 | - name: ZOO_4LW_COMMANDS_WHITELIST 51 | value: "ruok,stat" 52 | livenessProbe: 53 | exec: 54 | command: ['sh', '-c', '[ "$(echo ruok | nc 127.0.0.1 2181)" = "imok" ]'] 55 | initialDelaySeconds: 20 56 | periodSeconds: 30 57 | timeoutSeconds: 15 58 | {{- if or (.Values.zk.resources.limits | empty | not) (.Values.zk.resources.requests | empty | not) }} 59 | resources: 60 | {{- with .Values.zk.resources.requests }} 61 | requests: {{ . | toYaml | nindent 14 }} 62 | {{- end }} 63 | {{- with .Values.zk.resources.limits }} 64 | limits: {{ . | toYaml | nindent 14 }} 65 | {{- end }} 66 | {{- end }} 67 | - name: ui 68 | image: "{{ .Values.ui.image.repository }}:{{ .Values.ui.image.tag }}" 69 | imagePullPolicy: "{{ .Values.ui.image.pullPolicy }}" 70 | ports: 71 | - name: http 72 | containerPort: {{ .Values.ui.port }} 73 | protocol: TCP 74 | args: 75 | - "-Dcmak.zkhosts=127.0.0.1:2181" 76 | - "-Dhttp.port={{ .Values.ui.port }}" 77 | {{- range .Values.ui.extraArgs }} 78 | - {{ . | quote }} 79 | {{- end }} 80 | env: 81 | {{- if .Values.ui.consumerProperties }} 82 | - name: CONSUMER_PROPERTIES_FILE 83 | value: /conf/consumer.properties 84 | {{- end }} 85 | {{- with .Values.ui.extraEnv -}} 86 | {{ toYaml . | nindent 10 }} 87 | {{- end }} 88 | readinessProbe: 89 | httpGet: 90 | path: {{ include "cmak.healthUi" . }} 91 | port: http 92 | failureThreshold: 7 93 | initialDelaySeconds: 15 94 | timeoutSeconds: 15 95 | periodSeconds: 15 96 | livenessProbe: 97 | httpGet: 98 | path: {{ include "cmak.healthUi" . }} 99 | port: http 100 | failureThreshold: 7 101 | initialDelaySeconds: 45 102 | timeoutSeconds: 15 103 | periodSeconds: 30 104 | {{- if or .Values.ui.consumerProperties .Values.ui.consumerPropertiesSsl }} 105 | volumeMounts: 106 | {{- if .Values.ui.consumerProperties }} 107 | - name: {{ $nm }}-consumer-properties 108 | mountPath: /conf/consumer.properties 109 | subPath: consumer.properties 110 | {{- end -}} 111 | {{ if .Values.ui.consumerPropertiesSsl }} 112 | - name: {{ $nm }}-ssl 113 | mountPath: "/conf/ssl" 114 | {{- end -}} 115 | {{- end }} 116 | {{- if or (.Values.ui.resources.limits | empty | not) (.Values.ui.resources.requests | empty | not) }} 117 | resources: 118 | {{- with .Values.ui.resources.requests }} 119 | requests: {{ . | toYaml | nindent 14 }} 120 | {{- end }} 121 | {{- with .Values.ui.resources.limits }} 122 | limits: {{ . | toYaml | nindent 14 }} 123 | {{- end }} 124 | {{- end }} 125 | -------------------------------------------------------------------------------- /templates/ingress.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.ingress -}} 2 | apiVersion: networking.k8s.io/v1 3 | kind: Ingress 4 | metadata: 5 | name: {{ include "cmak.name" . | quote }} 6 | labels: 7 | {{- include "cmak.labels" . | nindent 4 }} 8 | {{- with .Values.ingress.labels }} 9 | {{- . | toYaml | nindent 4 }} 10 | {{- end }} 11 | {{- with .Values.ingress.annotations }} 12 | annotations: 13 | {{- . | toYaml | nindent 4 }} 14 | {{- end }} 15 | spec: 16 | {{- if .Values.ingress.className }} 17 | ingressClassName: {{ .Values.ingress.className | quote }} 18 | {{- end }} 19 | {{- if .Values.ingress.tls }} 20 | tls: 21 | - hosts: 22 | - {{ .Values.ingress.host | quote }} 23 | secretName: {{ .Values.ingress.tls.secret | default (.Values.ingress.host | replace "." "-") | quote }} 24 | {{- end }} 25 | rules: 26 | - host: {{ .Values.ingress.host | quote }} 27 | http: 28 | paths: 29 | - path: {{ .Values.ingress.path | quote }} 30 | pathType: {{ .Values.ingress.pathType | default "ImplementationSpecific" | quote }} 31 | backend: 32 | service: 33 | name: {{ include "cmak.name" . | quote }} 34 | port: 35 | number: {{ .Values.ui.port }} 36 | {{- end }} 37 | -------------------------------------------------------------------------------- /templates/ingressroute.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.e2e }} 2 | apiVersion: traefik.containo.us/v1alpha1 3 | kind: IngressRouteTCP 4 | metadata: 5 | name: {{ include "cmak.name" . }} 6 | spec: 7 | entryPoints: 8 | - web 9 | routes: 10 | - match: HostSNI(`*`) 11 | services: 12 | - name: {{ include "cmak.name" . }} 13 | port: 2181 14 | {{- end }} 15 | -------------------------------------------------------------------------------- /templates/job-setup.yaml: -------------------------------------------------------------------------------- 1 | {{- $nm := include "cmak.name" . -}} 2 | apiVersion: batch/v1 3 | kind: Job 4 | metadata: 5 | name: {{ $nm }}-setup 6 | labels: 7 | {{- include "cmak.labels" . | nindent 4 }} 8 | annotations: 9 | "helm.sh/hook": post-install,post-upgrade 10 | "helm.sh/hook-delete-policy": hook-succeeded 11 | spec: 12 | backoffLimit: 3 13 | template: 14 | metadata: 15 | {{- with .Values.reconcile.annotations }} 16 | annotations: 17 | {{- toYaml . | nindent 8 }} 18 | {{- end }} 19 | spec: 20 | restartPolicy: Never 21 | volumes: 22 | - name: {{ $nm }}-clusters 23 | configMap: 24 | name: {{ $nm }}-clusters 25 | {{- with .Values.nodeSelector }} 26 | nodeSelector: {{ toYaml . | nindent 8 }} 27 | {{- end }} 28 | {{- with .Values.affinity }} 29 | affinity: {{ toYaml . | nindent 8 }} 30 | {{- end }} 31 | {{- with .Values.tolerations }} 32 | tolerations: {{ toYaml . | nindent 8 }} 33 | {{- end }} 34 | containers: 35 | - name: setup 36 | image: "{{ .Values.reconcile.image.repository }}:{{ .Values.reconcile.image.tag }}" 37 | imagePullPolicy: "{{ .Values.reconcile.image.pullPolicy }}" 38 | args: 39 | - '{{ .Values.reconcile.overwriteZk | ternary "--" "--no-" }}overwrite-zk' 40 | - '{{ $nm }}.{{ $.Release.Namespace }}:2181' 41 | - '/app/etc/clusters.yaml' 42 | volumeMounts: 43 | - name: {{ $nm }}-clusters 44 | mountPath: /app/etc 45 | -------------------------------------------------------------------------------- /templates/secret.yaml: -------------------------------------------------------------------------------- 1 | {{ if .Values.ui.consumerPropertiesSsl }} 2 | apiVersion: v1 3 | kind: Secret 4 | metadata: 5 | name: {{ include "cmak.name" . }}-ssl 6 | labels: 7 | {{- include "cmak.labels" . | nindent 4 }} 8 | type: Opaque 9 | data: 10 | {{- with .Values.ui.consumerPropertiesSsl }} 11 | truststore: {{ .truststore.value }} 12 | keystore: {{ .keystore.value }} 13 | {{- end -}} 14 | {{- end -}} 15 | -------------------------------------------------------------------------------- /templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "cmak.name" . }} 5 | labels: 6 | {{- include "cmak.labels" . | nindent 4 }} 7 | spec: 8 | type: ClusterIP 9 | ports: 10 | - name: zk 11 | port: 2181 12 | targetPort: zk 13 | protocol: TCP 14 | - name: http 15 | port: {{ .Values.ui.port }} 16 | targetPort: http 17 | protocol: TCP 18 | selector: 19 | {{- include "cmak.selectorLabels" . | nindent 4 }} 20 | -------------------------------------------------------------------------------- /test/e2e/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from requests import ConnectionError, get 3 | from time import sleep 4 | 5 | 6 | @pytest.fixture(scope="package") 7 | def api_url(request): 8 | return "https://localhost:18443" 9 | 10 | 11 | @pytest.fixture(scope="package") 12 | def zk_url(request): 13 | return "localhost:18080" 14 | 15 | 16 | @pytest.fixture(scope="package") 17 | def traefik(api_url, zk_url): 18 | for i in range(10): 19 | try: 20 | get(f"{api_url}", verify=False) 21 | return True 22 | except ConnectionError: 23 | print(f"Waiting for Traefik #{i}, {i * 20} sec") 24 | sleep(20) 25 | return False 26 | 27 | 28 | -------------------------------------------------------------------------------- /test/e2e/default/test.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from kazoo.client import KazooClient 3 | from time import sleep 4 | 5 | def waitUntilClusters(api_url): 6 | for i in range(12): 7 | response = requests.get(f"{api_url}/api/status/clusters", verify=False) 8 | assert response.status_code == 200 9 | if len(response.json()['clusters']['active']) == 2: 10 | return True 11 | else: 12 | print(f"Waiting CronJob #{i}, {i * 20} sec") 13 | sleep(20) 14 | 15 | return False 16 | 17 | 18 | def test_clusters(api_url, zk_url): 19 | response = requests.get(f"{api_url}/api/status/clusters", verify=False) 20 | assert response.status_code == 200 21 | assert len(response.json()['clusters']['active']) == 2 22 | 23 | response = requests.get(f"{api_url}/api/status/cluster/topics", verify=False) 24 | assert response.status_code == 200 25 | assert response.json()["topics"] == ["__consumer_offsets", "test"] 26 | 27 | zk = KazooClient(hosts=zk_url) 28 | zk.start() 29 | zk.delete("/kafka-manager/configs/cluster-disabled", recursive=False) 30 | zk.stop() 31 | 32 | response = requests.get(f"{api_url}/api/status/clusters", verify=False) 33 | assert response.status_code == 200 34 | assert len(response.json()['clusters']['active']) == 1 35 | 36 | assert waitUntilClusters(api_url) == True 37 | -------------------------------------------------------------------------------- /test/e2e/default/values.yaml: -------------------------------------------------------------------------------- 1 | reconcile: 2 | schedule: "*/1 * * * *" 3 | 4 | cmak: 5 | clustersCommon: 6 | kafkaVersion: "2.4.0" 7 | clusters: 8 | - name: cluster 9 | curatorConfig: 10 | zkConnect: kafka-test:2181 11 | - name: cluster-disabled 12 | enabled: false 13 | curatorConfig: 14 | zkConnect: kafka-test:2181 15 | 16 | ingress: 17 | host: localhost 18 | path: / 19 | pathType: ImplementationSpecific 20 | annotations: 21 | traefik.ingress.kubernetes.io/router.entrypoints: websecure 22 | -------------------------------------------------------------------------------- /test/e2e/k3d.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: k3d.io/v1alpha4 2 | kind: Simple 3 | metadata: 4 | name: cmak-operator 5 | servers: 1 6 | agents: 0 7 | volumes: 8 | - volume: ${PWD}/test/e2e/kafka.yaml:/var/lib/rancher/k3s/server/manifests/kafka.yaml 9 | nodeFilters: ["server:*"] 10 | registries: 11 | create: 12 | name: cmak-operator-registry 13 | host: "0.0.0.0" 14 | hostPort: "5000" 15 | config: | 16 | mirrors: 17 | "localhost:5000": 18 | endpoint: 19 | - http://cmak-operator-registry:5000 20 | ports: 21 | - port: 18080:80 22 | nodeFilters: ["server:*"] 23 | - port: 18443:443 24 | nodeFilters: ["server:*"] 25 | options: 26 | k3s: 27 | extraArgs: 28 | - arg: "--disable=local-storage,metrics-server" 29 | nodeFilters: ["server:*"] 30 | -------------------------------------------------------------------------------- /test/e2e/kafka.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: kafka-test 5 | spec: 6 | ports: 7 | - port: 2181 8 | name: zk 9 | protocol: TCP 10 | - port: 9092 11 | name: kafka 12 | protocol: TCP 13 | selector: 14 | app: kafka-test 15 | --- 16 | apiVersion: apps/v1 17 | kind: Deployment 18 | metadata: 19 | name: kafka-test 20 | spec: 21 | selector: 22 | matchLabels: 23 | app: kafka-test 24 | template: 25 | metadata: 26 | labels: 27 | app: kafka-test 28 | spec: 29 | containers: 30 | - name: zk 31 | image: zookeeper:latest 32 | ports: 33 | - containerPort: 2181 34 | env: 35 | - name: ZOO_SERVERS 36 | value: server.1=0.0.0.0:2888:3888;2181 37 | - name: kafka 38 | image: wurstmeister/kafka:2.12-2.4.1 39 | ports: 40 | - containerPort: 9092 41 | env: 42 | - name: KAFKA_PORT 43 | value: "9092" 44 | - name: KAFKA_ADVERTISED_PORT 45 | value: "9092" 46 | - name: KAFKA_ZOOKEEPER_CONNECT 47 | value: "localhost:2181" 48 | - name: KAFKA_CREATE_TOPICS 49 | value: "test:1:1" 50 | - name: KAFKA_LISTENERS 51 | value: PLAINTEXT://0.0.0.0:9092 52 | - name: KAFKA_ADVERTISED_LISTENERS 53 | value: PLAINTEXT://kafka-test:9092 54 | -------------------------------------------------------------------------------- /test/e2e/requirements.txt: -------------------------------------------------------------------------------- 1 | kazoo~=2.0 2 | pytest~=7.0 3 | requests~=2.0 4 | 5 | -------------------------------------------------------------------------------- /test/e2e/traefik.py: -------------------------------------------------------------------------------- 1 | 2 | def test_clusters(traefik): 3 | assert True 4 | -------------------------------------------------------------------------------- /test/lint/common.yaml: -------------------------------------------------------------------------------- 1 | suite: lint common values 2 | templates: 3 | - fake.yaml 4 | tests: 5 | - it: affinity is not object 6 | set: 7 | affinity: false 8 | asserts: 9 | - failedTemplate: 10 | errorMessage: | 11 | values don't meet the specifications of the schema(s) in the following chart(s): 12 | cmak-operator: 13 | - affinity: Invalid type. Expected: object, given: boolean 14 | - it: ingress is broken 15 | set: 16 | ingress: 17 | pathType: false 18 | asserts: 19 | - failedTemplate: 20 | errorMessage: | 21 | values don't meet the specifications of the schema(s) in the following chart(s): 22 | cmak-operator: 23 | - ingress: host is required 24 | - ingress: path is required 25 | - ingress.pathType: Invalid type. Expected: string, given: boolean 26 | -------------------------------------------------------------------------------- /test/lint/subchart/.gitignore: -------------------------------------------------------------------------------- 1 | charts 2 | Chart.lock 3 | 4 | -------------------------------------------------------------------------------- /test/lint/subchart/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /test/lint/subchart/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: test 3 | type: application 4 | version: 0.0.0 5 | appVersion: 0.0.0 6 | 7 | dependencies: 8 | - name: cmak-operator 9 | version: 0.0.0 10 | repository: file://../../.. 11 | -------------------------------------------------------------------------------- /test/lint/subchart/templates/test.yaml: -------------------------------------------------------------------------------- 1 | metadata: 2 | name: test 3 | -------------------------------------------------------------------------------- /test/lint/subchart/values.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "$id": "https://github.com/eshepelyuk/test/", 4 | "title": "test helm values", 5 | 6 | "type": "object", "additionalProperties": true, 7 | "required": ["propStr"], 8 | 9 | "properties": { 10 | "propStr": { "type": "string" }, 11 | "propBool": { "type": "boolean" } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/lint/subchart/values.yaml: -------------------------------------------------------------------------------- 1 | cmak-operator: 2 | cmak: 3 | clusters: [] 4 | 5 | propStr: "true" 6 | 7 | global: 8 | globalStr: "false" 9 | 10 | image: 11 | tag: xxx 12 | repository: yyy 13 | -------------------------------------------------------------------------------- /test/lint/ui.yaml: -------------------------------------------------------------------------------- 1 | suite: lint ui 2 | templates: 3 | - fake.yaml 4 | tests: 5 | - it: ui.extraEnv is not list 6 | set: 7 | ui: 8 | extraEnv: false 9 | asserts: 10 | - failedTemplate: 11 | errorMessage: | 12 | values don't meet the specifications of the schema(s) in the following chart(s): 13 | cmak-operator: 14 | - ui.extraEnv: Invalid type. Expected: array, given: boolean 15 | -------------------------------------------------------------------------------- /test/unit/deploy-placement.yaml: -------------------------------------------------------------------------------- 1 | suite: deployment placement 2 | templates: 3 | - deployment.yaml 4 | tests: 5 | - it: should not populate placement by default 6 | asserts: 7 | - isNull: 8 | path: spec.template.spec.nodeSelector 9 | - isNull: 10 | path: spec.template.spec.affinity 11 | - isNull: 12 | path: spec.template.spec.tolerations 13 | - it: should populate nodeSelector 14 | set: 15 | nodeSelector: 16 | myLbl: myVal 17 | asserts: 18 | - isNotNull: 19 | path: spec.template.spec.nodeSelector 20 | - isNotEmpty: 21 | path: spec.template.spec.nodeSelector.myLbl 22 | - equal: 23 | path: spec.template.spec.nodeSelector.myLbl 24 | value: myVal 25 | - it: should populate affinity 26 | set: 27 | affinity: 28 | someProp: someVal 29 | asserts: 30 | - isNotEmpty: 31 | path: spec.template.spec.affinity 32 | - equal: 33 | path: spec.template.spec.affinity.someProp 34 | value: someVal 35 | - it: should populate tolerations 36 | set: 37 | tolerations: 38 | - name: val 39 | someProp: someVal 40 | asserts: 41 | - isNotEmpty: 42 | path: spec.template.spec.tolerations 43 | - contains: 44 | path: spec.template.spec.tolerations 45 | content: 46 | name: val 47 | someProp: someVal 48 | count: 1 49 | any: false 50 | -------------------------------------------------------------------------------- /test/unit/deployment.yaml: -------------------------------------------------------------------------------- 1 | suite: deployment 2 | templates: 3 | - deployment.yaml 4 | tests: 5 | - it: should emit empty env by default 6 | asserts: 7 | - isEmpty: 8 | path: spec.template.spec.containers[1].env 9 | - it: should populate env when passed 10 | set: 11 | ui: 12 | extraEnv: 13 | - name: myvar 14 | value: myvalue 15 | asserts: 16 | - contains: 17 | path: spec.template.spec.containers[1].env 18 | content: 19 | name: myvar 20 | value: myvalue 21 | count: 1 22 | any: false 23 | -------------------------------------------------------------------------------- /test/unit/ingress.yaml: -------------------------------------------------------------------------------- 1 | suite: ingress 2 | templates: 3 | - ingress.yaml 4 | tests: 5 | - it: should not render by defaul 6 | asserts: 7 | - hasDocuments: 8 | count: 0 9 | - it: should use pathType ImplementaionSpecific by default 10 | set: 11 | ingress: 12 | host: myhost 13 | path: / 14 | asserts: 15 | - hasDocuments: 16 | count: 1 17 | - equal: 18 | path: spec.rules[0].http.paths[0].pathType 19 | value: ImplementationSpecific 20 | - it: should use pathType ImplementaionSpecific by default 21 | set: 22 | ingress: 23 | host: myhost 24 | path: / 25 | pathType: Prefix 26 | asserts: 27 | - hasDocuments: 28 | count: 1 29 | - equal: 30 | path: spec.rules[0].http.paths[0].pathType 31 | value: Prefix 32 | - it: should not render spec.ingressClassName by default 33 | set: 34 | ingress: 35 | host: myhost 36 | path: / 37 | pathType: Prefix 38 | asserts: 39 | - notExists: 40 | path: spec.ingressClassName 41 | - it: should render spec.ingressClassName if specified 42 | set: 43 | ingress: 44 | host: myhost 45 | path: / 46 | pathType: Prefix 47 | className: nginx 48 | asserts: 49 | - hasDocuments: 50 | count: 1 51 | - equal: 52 | path: spec.ingressClassName 53 | value: nginx 54 | -------------------------------------------------------------------------------- /test/unit/job-annotations.yaml: -------------------------------------------------------------------------------- 1 | suite: reconcile cronjob annotations 2 | templates: 3 | - cronjob-reconcile.yaml 4 | - job-setup.yaml 5 | tests: 6 | - it: should populate job annotations 7 | set: 8 | reconcile: 9 | annotations: 10 | istest: yes 11 | asserts: 12 | - isNotNull: 13 | path: spec.jobTemplate.spec.template.metadata.annotations.istest 14 | template: cronjob-reconcile.yaml 15 | - isNotNull: 16 | path: spec.template.metadata.annotations.istest 17 | template: job-setup.yaml 18 | - isNotEmpty: 19 | path: spec.jobTemplate.spec.template.metadata.annotations.istest 20 | template: cronjob-reconcile.yaml 21 | - isNotEmpty: 22 | path: spec.template.metadata.annotations.istest 23 | template: job-setup.yaml 24 | - equal: 25 | path: spec.jobTemplate.spec.template.metadata.annotations.istest 26 | value: yes 27 | template: cronjob-reconcile.yaml 28 | - equal: 29 | path: spec.template.metadata.annotations.istest 30 | value: yes 31 | template: job-setup.yaml 32 | 33 | - it: should not have annotations by default 34 | asserts: 35 | - isNullOrEmpty: 36 | path: spec.jobTemplate.spec.template.metadata 37 | template: cronjob-reconcile.yaml 38 | - isNullOrEmpty: 39 | path: spec.template.metadata 40 | template: job-setup.yaml 41 | -------------------------------------------------------------------------------- /test/unit/reconcile-placement.yaml: -------------------------------------------------------------------------------- 1 | suite: reconcile cronjob placement 2 | templates: 3 | - cronjob-reconcile.yaml 4 | tests: 5 | - it: should not populate placement by default 6 | asserts: 7 | - isNull: 8 | path: spec.jobTemplate.spec.template.spec.nodeSelector 9 | - isNull: 10 | path: spec.jobTemplate.spec.template.spec.affinity 11 | - isNull: 12 | path: spec.jobTemplate.spec.template.spec.tolerations 13 | - it: should populate nodeSelector 14 | set: 15 | nodeSelector: 16 | myLbl: myVal 17 | asserts: 18 | - isNotNull: 19 | path: spec.jobTemplate.spec.template.spec.nodeSelector 20 | - isNotEmpty: 21 | path: spec.jobTemplate.spec.template.spec.nodeSelector.myLbl 22 | - equal: 23 | path: spec.jobTemplate.spec.template.spec.nodeSelector.myLbl 24 | value: myVal 25 | - it: should populate affinity 26 | set: 27 | affinity: 28 | someProp: someVal 29 | asserts: 30 | - isNotEmpty: 31 | path: spec.jobTemplate.spec.template.spec.affinity 32 | - equal: 33 | path: spec.jobTemplate.spec.template.spec.affinity.someProp 34 | value: someVal 35 | - it: should populate tolerations 36 | set: 37 | tolerations: 38 | - name: val 39 | someProp: someVal 40 | asserts: 41 | - isNotEmpty: 42 | path: spec.jobTemplate.spec.template.spec.tolerations 43 | - contains: 44 | path: spec.jobTemplate.spec.template.spec.tolerations 45 | content: 46 | name: val 47 | someProp: someVal 48 | count: 1 49 | any: false 50 | -------------------------------------------------------------------------------- /test/unit/setup-placement.yaml: -------------------------------------------------------------------------------- 1 | suite: setup job placement 2 | templates: 3 | - job-setup.yaml 4 | tests: 5 | - it: should not populate placement by default 6 | asserts: 7 | - isNull: 8 | path: spec.template.spec.nodeSelector 9 | - isNull: 10 | path: spec.template.spec.affinity 11 | - isNull: 12 | path: spec.template.spec.tolerations 13 | - it: should populate nodeSelector 14 | set: 15 | nodeSelector: 16 | myLbl: myVal 17 | asserts: 18 | - isNotNull: 19 | path: spec.template.spec.nodeSelector 20 | - isNotEmpty: 21 | path: spec.template.spec.nodeSelector.myLbl 22 | - equal: 23 | path: spec.template.spec.nodeSelector.myLbl 24 | value: myVal 25 | - it: should populate affinity 26 | set: 27 | affinity: 28 | someProp: someVal 29 | asserts: 30 | - isNotEmpty: 31 | path: spec.template.spec.affinity 32 | - equal: 33 | path: spec.template.spec.affinity.someProp 34 | value: someVal 35 | - it: should populate tolerations 36 | set: 37 | tolerations: 38 | - name: val 39 | someProp: someVal 40 | asserts: 41 | - isNotEmpty: 42 | path: spec.template.spec.tolerations 43 | - contains: 44 | path: spec.template.spec.tolerations 45 | content: 46 | name: val 47 | someProp: someVal 48 | count: 1 49 | any: false 50 | -------------------------------------------------------------------------------- /values.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "$id": "https://github.com/eshepelyuk/cmak-operator/", 4 | "title": "CMAK operator Helm values", 5 | 6 | "definitions": { 7 | "image": { 8 | "type": "object", "title": "docker image configuration", "required": ["repository", "tag"], 9 | "properties": { 10 | "repository": { "type": "string" }, 11 | "tag": { "type": "string" }, 12 | "pullPolicy": { "type": "string", "default": "IfNotPresent" } 13 | } 14 | }, 15 | "curatorConfigCommon": { 16 | "type": "object", "title": "curator framework settings for zookeeper", 17 | "properties": { 18 | "zkMaxRetry": { "type": "integer", "default": 100 }, 19 | "baseSleepTimeMs": { "type": "integer", "default": 100 }, 20 | "maxSleepTimeMs": { "type": "integer", "default": 1000 } 21 | } 22 | }, 23 | "curatorConfig": { 24 | "type": "object", "required": ["zkConnect"], 25 | "allOf": [ 26 | { "$ref": "#/definitions/curatorConfigCommon" }, 27 | { "properties": { 28 | "zkConnect": { 29 | "type": "string", "title": "zookeeper connection string", 30 | "description": "Zookeeper addresses joined by , host1:port,host2:port,host3:port" 31 | } 32 | } 33 | } 34 | ] 35 | }, 36 | "clusterConfigShared": { 37 | "type": "object", 38 | "properties": { 39 | "enabled": { "type": "boolean", "default": true, "title": "either cluster enabled" }, 40 | "kafkaVersion": { "type": "string", "default": "2.2.0" }, 41 | "jmxEnabled": { "type": "boolean", "default": false }, 42 | "jmxUser": { "type": ["null", "string"], "default": null }, 43 | "jmxPass": { "type": ["null", "string"], "default": null }, 44 | "jmxSsl": { "type": "boolean", "default": false }, 45 | "pollConsumers": { "type": "boolean", "default": true }, 46 | "filterConsumers": { "type": "boolean", "default": false }, 47 | "logkafkaEnabled": { "type": "boolean", "default": false }, 48 | "activeOffsetCacheEnabled": { "type": "boolean", "default": true }, 49 | "displaySizeEnabled": { "type": "boolean", "default": false } 50 | } 51 | }, 52 | "clusterConfigCommon": { 53 | "type": "object", "required": ["curatorConfig"], "title": "common config for all declared clusters", 54 | "allOf": [ 55 | { "$ref": "#/definitions/clusterConfigShared" }, 56 | { "properties": { 57 | "curatorConfig": { "$ref": "#/definitions/curatorConfigCommon" } 58 | } 59 | } 60 | ] 61 | }, 62 | "clusterConfig": { 63 | "type": "object", "required": ["name", "curatorConfig"], "title": "config for particular cluster", 64 | "allOf": [ 65 | { "$ref": "#/definitions/clusterConfigShared" }, 66 | { "properties": { 67 | "name": { "type": "string", "title": "display name for the cluster" }, 68 | "curatorConfig": { "$ref": "#/definitions/curatorConfig" } 69 | } 70 | } 71 | ] 72 | }, 73 | "resources": { 74 | "type": "object", "title": "resource requests and limits", "additionalProperties": false, 75 | "required": ["limits", "requests"], 76 | "description": "See https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/", 77 | "properties": { 78 | "limits": { "type": "object", "title": "resource limits", "default": {} }, 79 | "requests": { "type": "object", "title": "resource requests", "default": {} } 80 | } 81 | }, 82 | "tls": { 83 | "type": ["null", "object"], "title": "use TLS secret", "default": null, 84 | "properties": { 85 | "secret": { "type": "string", "title": "Secret name to attach to the ingress object" } 86 | } 87 | }, 88 | "consumerSsl": { 89 | "type": "object", "title": "Consumer SSL configuration", "default": null, 90 | "properties": { 91 | "keystore": { 92 | "type": "object", "title": "keystore configuration", 93 | "properties": { 94 | "type": { "type": "string" }, 95 | "password": { "type": "string" }, 96 | "value": { "type": "string", "description": "base64 encoded keystore" } 97 | } 98 | }, 99 | "truststore": { 100 | "type": "object", "title": "truststore configuration", 101 | "properties": { 102 | "type": { "type": "string" }, 103 | "password": { "type": "string" }, 104 | "value": { "type": "string", "description": "base64 encoded truststore" } 105 | } 106 | } 107 | } 108 | } 109 | }, 110 | "type": "object", "required": ["cmak", "reconcile", "ui", "zk"], 111 | "properties": { 112 | "affinity": { 113 | "type": "object", "default": {}, "title": "affinity", 114 | "description": "See https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#affinity-and-anti-affinity" 115 | }, 116 | "nodeSelector": { 117 | "type": "object", "default": {}, "title": "node selector", 118 | "description": "See https://kubernetes.io/docs/tasks/configure-pod-container/assign-pods-nodes/" 119 | }, 120 | "tolerations": { 121 | "type": "array", "default": [], "title": "tolerations", 122 | "description": "See https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/", 123 | "items": {"type": "object"} 124 | }, 125 | "reconcile": { 126 | "type": "object", "additionalProperties": false, 127 | "required": ["image", "schedule"], "title": "reconciliation job config", 128 | "properties": { 129 | "image": { "$ref": "#/definitions/image" }, 130 | "schedule": { 131 | "type": "string", "default": "*/3 * * * *", "title": "cron expression for periodic reconciliation" 132 | }, 133 | "overwriteZk": { 134 | "type": "boolean", "default": true, "title": "allow overwrite Zookeeper settings of CMAK" 135 | }, 136 | "successfulJobsHistoryLimit": { 137 | "type": ["null", "integer"], "default": null, "title": "number of completed jobs to keep" 138 | }, 139 | "failedJobsHistoryLimit": { 140 | "type": ["null", "integer"], "default": null, "title": "number of failed jobs to keep" 141 | }, 142 | "annotations": { 143 | "type": "object", "title": "optional annotations to apply to the pods spun up via the job", "default": {} 144 | }, 145 | "resources": { "$ref": "#/definitions/resources" } 146 | } 147 | }, 148 | "cmak": { 149 | "type": "object", "additionalProperties": false, "title": "cmak instance settings", 150 | "description": "Those settings are mirroring CMAK UI preferences.", 151 | "required": ["clustersCommon", "clusters"], 152 | "properties": { 153 | "clustersCommon": { "$ref": "#/definitions/clusterConfigCommon" }, 154 | "clusters": { 155 | "type": "array", "title": "list of configured clusters", 156 | "items": { "$ref": "#/definitions/clusterConfig" } 157 | } 158 | } 159 | }, 160 | "ui": { 161 | "type": "object", "title": "ui container k8s settings", "additionalProperties": false, 162 | "required": ["image"], 163 | "properties": { 164 | "image": {"$ref": "#/definitions/image"}, 165 | "port": {"type": "integer", "default": 9000}, 166 | "extraArgs": { "type": "array", "title": "extra cmd line arguments", "items": {"type": "string"}, "default": [] }, 167 | "resources": { "$ref": "#/definitions/resources" }, 168 | "consumerProperties": { "type": "object", "title": "provide key value base pairs for consumer properties according to java docs", "default": {} }, 169 | "consumerPropertiesSsl": { "$ref": "#/definitions/consumerSsl" }, 170 | "extraEnv": { "type": "array", "title": "optional environment variables", "items": {"type": "object"}, "default": [] } 171 | } 172 | }, 173 | "zk": { 174 | "type": "object", "title": "zk container k8s settings", "additionalProperties": false, 175 | "required": ["image"], 176 | "properties": { 177 | "image": { "$ref": "#/definitions/image" }, 178 | "resources": { "$ref": "#/definitions/resources" } 179 | } 180 | }, 181 | "ingress": { 182 | "type": ["null", "object"], "title": "ingress configuration", "default": null, "additionalProperties": false, 183 | "description": "If object not null, then Ingress resources will be created.", 184 | "required": ["host", "path"], 185 | "properties": { 186 | "host": { "type": "string", "title": "ingress host" }, 187 | "path": { "type": "string", "title": "ingress path" }, 188 | "pathType": { "type": "string", "title": "ingress pathType", "default": "ImplementationSpecific" }, 189 | "tls": { "$ref": "#/definitions/tls" }, 190 | "className": { "type": ["null", "string"], "title": "ingress class name", "default": null }, 191 | "labels": { "type": "object", "title": "optional ingress labels", "default": {} }, 192 | "annotations": { "type": "object", "title": "optional ingress annotations", "default": {} } 193 | } 194 | } 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /values.yaml: -------------------------------------------------------------------------------- 1 | # mirroring CMAK UI preferences for Kafka clusters 2 | cmak: 3 | clustersCommon: 4 | curatorConfig: 5 | zkMaxRetry: 100 6 | baseSleepTimeMs: 100 7 | maxSleepTimeMs: 1000 8 | enabled: true 9 | kafkaVersion: "2.2.0" 10 | jmxEnabled: false 11 | jmxUser: null 12 | jmxPass: null 13 | jmxSsl: false 14 | pollConsumers: true 15 | filterConsumers: false 16 | logkafkaEnabled: false 17 | activeOffsetCacheEnabled: true 18 | displaySizeEnabled: false 19 | tuning: 20 | brokerViewUpdatePeriodSeconds: 30 21 | clusterManagerThreadPoolSize: 10 22 | clusterManagerThreadPoolQueueSize: 100 23 | kafkaCommandThreadPoolSize: 10 24 | kafkaCommandThreadPoolQueueSize: 100 25 | logkafkaCommandThreadPoolSize: 10 26 | logkafkaCommandThreadPoolQueueSize: 100 27 | logkafkaUpdatePeriodSeconds: 30 28 | partitionOffsetCacheTimeoutSecs: 5 29 | brokerViewThreadPoolSize: 10 30 | brokerViewThreadPoolQueueSize: 1000 31 | offsetCacheThreadPoolSize: 10 32 | offsetCacheThreadPoolQueueSize: 1000 33 | kafkaAdminClientThreadPoolSize: 10 34 | kafkaAdminClientThreadPoolQueueSize: 1000 35 | kafkaManagedOffsetMetadataCheckMillis: 30000 36 | kafkaManagedOffsetGroupCacheSize: 1000000 37 | kafkaManagedOffsetGroupExpireDays: 7 38 | securityProtocol: PLAINTEXT 39 | saslMechanism: null 40 | jaasConfig: null 41 | 42 | clusters: [] 43 | 44 | reconcile: 45 | image: 46 | repository: "ghcr.io/eshepelyuk/dckr/cmak2zk" 47 | tag: "1.2.0" 48 | pullPolicy: IfNotPresent 49 | schedule: "*/3 * * * *" 50 | overwriteZk: true 51 | successfulJobsHistoryLimit: null 52 | failedJobsHistoryLimit: null 53 | annotations: {} 54 | # resource configuration 55 | # resources: 56 | # limits: 57 | # cpu: 100m 58 | # memory: 128Mi 59 | # requests: 60 | # cpu: 100m 61 | # memory: 128Mi 62 | 63 | # various settings for CMAK UI container and CMAK application 64 | ui: 65 | image: 66 | repository: "ghcr.io/eshepelyuk/dckr/cmak-3.0.0.6" 67 | tag: "1.2.0" 68 | pullPolicy: IfNotPresent 69 | port: 9000 70 | # additional command line arguments 71 | extraArgs: [] 72 | #- "-DbasicAuthentication.enabled=false" 73 | 74 | ## resource requests and limits 75 | ## http://kubernetes.io/docs/user-guide/compute-resources/ 76 | resources: 77 | limits: {} 78 | # cpu: 100m 79 | # memory: 128Mi 80 | requests: {} 81 | # cpu: 100m 82 | # memory: 128Mi 83 | 84 | # Cosumer Properties 85 | consumerProperties: {} 86 | # Key value pairs of values to add into consumer properties. 87 | # default values which will be added if not specified here: 88 | # key.deserializer: "org.apache.kafka.common.serialization.ByteArrayDeserializer" 89 | # value.deserializer: "org.apache.kafka.common.serialization.ByteArrayDeserializer" 90 | 91 | # Consumer SSL configurations 92 | consumerPropertiesSsl: {} 93 | # keystore: 94 | # type: PKCS 95 | # password: test123 96 | # value: 97 | # truststore: 98 | # type: JKS 99 | # password: test123 100 | # value: 101 | 102 | # Additional environment variables (declared as k8s style) 103 | # ref: https://kubernetes.io/docs/tasks/inject-data-application/define-environment-variable-container/ 104 | extraEnv: [] 105 | # - name: GREETING 106 | # value: "Warm greetings to" 107 | # - name: SECRET_USERNAME 108 | # valueFrom: 109 | # secretKeyRef: 110 | # name: mysecret 111 | # key: username 112 | # - name: SPECIAL_LEVEL_KEY 113 | # valueFrom: 114 | # configMapKeyRef: 115 | # name: special-config 116 | # key: SPECIAL_LEVEL 117 | 118 | # various settings for Zookeeper container 119 | zk: 120 | image: 121 | repository: zookeeper 122 | tag: "3.6.1" 123 | pullPolicy: IfNotPresent 124 | ## resource requests and limits 125 | ## http://kubernetes.io/docs/user-guide/compute-resources/ 126 | resources: 127 | limits: {} 128 | # cpu: 100m 129 | # memory: 128Mi 130 | requests: {} 131 | # cpu: 100m 132 | # memory: 128Mi 133 | 134 | # uncomment section and remove null to create Ingress resource 135 | ingress: null 136 | # # ingress host 137 | # host: www.cmak.mycompany 138 | # 139 | # # ingress 140 | # path: / 141 | # pathType: ImplementationSpecific 142 | # className: nginx 143 | # 144 | # # Enable TLS configuration for the hostname defined at ingress.host 145 | # # secret name will be "${ingress.host.replace(".", "-")}" 146 | # tls: null 147 | # # secret: null 148 | # 149 | # # optional ingress annotations 150 | # annotations: {} 151 | # 152 | # # optional ingress labels 153 | # labels: {} 154 | 155 | affinity: {} 156 | 157 | nodeSelector: {} 158 | 159 | tolerations: [] 160 | --------------------------------------------------------------------------------