├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── dependabot.yml └── workflows │ ├── e2e.yaml │ ├── go.yml │ ├── publish.yml │ ├── stale.yml │ └── upload-helm-chart.yml ├── .gitignore ├── .golangci.yaml ├── .goreleaser.yml ├── .toolbox.mk ├── Dockerfile ├── LICENSE ├── Makefile ├── PROJECT ├── README.md ├── api └── v1 │ ├── eventlogger_types.go │ ├── eventlogger_types_test.go │ ├── eventlogger_validation.go │ ├── eventlogger_validation_test.go │ ├── eventlogger_webhook.go │ ├── eventlogger_webhook_test.go │ ├── groupversion_info.go │ ├── v1_suite_test.go │ └── zz_generated.deepcopy.go ├── controllers ├── config │ ├── config_controller.go │ ├── config_controller_test.go │ └── config_suite_test.go ├── logging │ ├── event_controller.go │ ├── event_controller_test.go │ ├── logging_suite_test.go │ ├── types.go │ └── types_test.go └── setup │ ├── eventlogger_controller.go │ ├── eventlogger_controller_pod.go │ ├── eventlogger_controller_rbac.go │ ├── eventlogger_controller_test.go │ └── setup_suite_test.go ├── docs └── chart-logo.png ├── go.mod ├── go.sum ├── hack ├── .gitignore ├── boilerplate.go.txt ├── build-images.sh ├── build.sh ├── generate-mocks.sh ├── push-to-quay.sh └── release.sh ├── helm ├── .helmignore ├── Chart.yaml ├── README.md ├── README.md.gotmpl ├── crds │ └── eventlogger.bakito.ch_eventloggers.yaml ├── templates │ ├── _helpers.tpl │ ├── configmap.yaml │ ├── deployment.yaml │ ├── rbac │ │ ├── clusterrole.yaml │ │ ├── clusterrole_aggregation.yaml │ │ ├── clusterrole_binding.yaml │ │ ├── role.yaml │ │ └── role_binding.yaml │ ├── service.yaml │ ├── serviceaccount.yaml │ └── webhook │ │ ├── certificate.yaml │ │ └── validating.yaml └── values.yaml ├── main.go ├── pkg ├── constants │ └── constants.go ├── filter │ ├── filter.go │ ├── filter_suite_test.go │ └── filter_test.go └── mocks │ ├── client │ └── mock.go │ └── logr │ └── mock.go ├── testdata └── e2e │ ├── buildImage.sh │ ├── e2e-values.yaml │ ├── installChart.sh │ ├── old-eventlogger-pod.yaml │ ├── test-eventlogger.yaml │ └── test-pod.yaml ├── tools.go └── version └── version.go /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [bakito] 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gomod" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | groups: 8 | k8s: 9 | patterns: 10 | - "k8s.io/*" 11 | update-types: 12 | - "minor" 13 | - "patch" 14 | onsi: 15 | patterns: 16 | - "github.com/onsi/*" 17 | update-types: 18 | - "minor" 19 | - "patch" 20 | commit-message: 21 | prefix: "chore" 22 | include: "scope" 23 | 24 | - package-ecosystem: "github-actions" 25 | directory: "/" 26 | schedule: 27 | interval: "weekly" 28 | 29 | - package-ecosystem: "docker" 30 | directory: "/" 31 | schedule: 32 | interval: "weekly" 33 | -------------------------------------------------------------------------------- /.github/workflows/e2e.yaml: -------------------------------------------------------------------------------- 1 | name: e2e tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | concurrency: 11 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 12 | cancel-in-progress: true 13 | jobs: 14 | e2e: 15 | name: Run end-2-end Tests 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v4 20 | 21 | - name: Install kind with registry 22 | uses: bakito/kind-with-registry-action@main 23 | 24 | - name: Build image 25 | run: ./testdata/e2e/buildImage.sh 26 | 27 | - name: Install helm chart 28 | run: | 29 | ./testdata/e2e/installChart.sh 30 | kubectl create ns e2e-test 31 | 32 | - name: Start 'old' eventlogger pod 33 | run: | 34 | kubectl apply -f testdata/e2e/old-eventlogger-pod.yaml 35 | kubectl wait --namespace e2e-test --for=condition=ready pod event-logger-example-eventlogger-old-version --timeout=90s 36 | 37 | - name: Setup an EventLogger 38 | run: | 39 | kubectl apply -f testdata/e2e/test-eventlogger.yaml 40 | kubectl wait --namespace e2e-test \ 41 | --for=condition=ready pod \ 42 | --selector=app.kubernetes.io/component=event-logger-example-eventlogger \ 43 | --timeout=90s 44 | 45 | - name: Check 'old' eventlogger pod is deleted 46 | run: | 47 | test $(kubectl get pod event-logger-example-eventlogger-old-version --no-headers | wc -l) -eq 0 48 | 49 | - name: Start/Stop test pod 50 | run: | 51 | kubectl apply -f testdata/e2e/test-pod.yaml 52 | kubectl wait --namespace e2e-test --for=condition=ready pod nginx --timeout=90s 53 | kubectl delete -f testdata/e2e/test-pod.yaml 54 | 55 | - name: Check logs 56 | run: | 57 | kubectl get pods -n e2e-test 58 | kubectl logs -n e2e-test -l app.kubernetes.io/component=event-logger-example-eventlogger 59 | kubectl logs -n e2e-test -l app.kubernetes.io/component=event-logger-example-eventlogger | grep '"logger":"event"' 60 | 61 | - name: Print logs 62 | timeout-minutes: 2 63 | run: | 64 | echo 'Pod logs' >> $GITHUB_STEP_SUMMARY 65 | echo '```' >> $GITHUB_STEP_SUMMARY 66 | kubectl logs -n e2e-test -l app.kubernetes.io/component=event-logger-example-eventlogger >> $GITHUB_STEP_SUMMARY 67 | echo '```' >> $GITHUB_STEP_SUMMARY 68 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | concurrency: 9 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 10 | cancel-in-progress: true 11 | jobs: 12 | lint: 13 | name: Lint 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | 18 | - name: Set up Go 19 | uses: actions/setup-go@v5 20 | with: 21 | go-version-file: "go.mod" 22 | 23 | - name: Lint 24 | run: make lint 25 | 26 | test: 27 | name: Test Code 28 | runs-on: ubuntu-latest 29 | steps: 30 | - uses: actions/checkout@v4 31 | 32 | - name: Set up Go 33 | uses: actions/setup-go@v5 34 | with: 35 | go-version-file: "go.mod" 36 | 37 | - name: Test 38 | run: make test-ci 39 | 40 | - name: Send coverage 41 | uses: shogo82148/actions-goveralls@v1 42 | with: 43 | path-to-profile: cover.out 44 | 45 | helm: 46 | name: Test Helm Chart 47 | runs-on: ubuntu-latest 48 | steps: 49 | - uses: actions/checkout@v4 50 | with: 51 | fetch-depth: 0 52 | 53 | - name: Set up Go 54 | uses: actions/setup-go@v5 55 | with: 56 | go-version-file: "go.mod" 57 | 58 | - name: Install Helm 59 | uses: azure/setup-helm@v4 60 | 61 | - name: Lint 62 | run: | 63 | cp helm/README.md helm/README.md.old 64 | make helm-lint 65 | 66 | - name: Check chart/README.md is correctly generated with 'make docs' 67 | run: | 68 | # ignore version as it is updated during build 69 | sed -i '/!\[Version\:/d' helm/README.md 70 | sed -i '/!\[Version\:/d' helm/README.md.old 71 | diff helm/README.md.old helm/README.md 72 | 73 | test-release: 74 | name: Test Release 75 | runs-on: ubuntu-latest 76 | steps: 77 | 78 | - name: Check out code into the Go module directory 79 | uses: actions/checkout@v4 80 | 81 | - name: Set up Go 82 | uses: actions/setup-go@v5 83 | with: 84 | go-version-file: "go.mod" 85 | 86 | - name: Run GoReleaser 87 | run: make test-release 88 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Build images 2 | 3 | on: 4 | push: 5 | branches: main 6 | release: 7 | types: 8 | - published 9 | 10 | jobs: 11 | main: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Set up QEMU 15 | uses: docker/setup-qemu-action@v3 16 | - name: Set up Docker Buildx 17 | uses: docker/setup-buildx-action@v3 18 | - name: Login to ghcr.io 19 | uses: docker/login-action@v3 20 | with: 21 | registry: ghcr.io 22 | username: ${{ github.repository_owner }} 23 | password: ${{ secrets.GITHUB_TOKEN }} 24 | - name: Login to Quay 25 | uses: docker/login-action@v3 26 | with: 27 | registry: ${{ secrets.REGISTRY_SERVER }} 28 | username: ${{ secrets.REGISTRY_USERNAME }} 29 | password: ${{ secrets.REGISTRY_PASSWORD }} 30 | - name: Build and push ${{github.event.release.tag_name }} 31 | id: docker_build_release 32 | uses: docker/build-push-action@v6 33 | if: ${{ github.event.release.tag_name != '' }} 34 | with: 35 | push: true 36 | tags: ghcr.io/bakito/k8s-event-logger:latest,ghcr.io/bakito/k8s-event-logger:${{ github.event.release.tag_name }},quay.io/bakito/k8s-event-logger:latest,quay.io/bakito/k8s-event-logger:${{ github.event.release.tag_name }} 37 | platforms: linux/amd64 38 | provenance: false 39 | build-args: VERSION=${{ github.event.release.tag_name }} 40 | 41 | - name: Build and push main 42 | id: docker_build_main 43 | uses: docker/build-push-action@v6 44 | if: ${{ github.event.release.tag_name == '' }} 45 | with: 46 | push: true 47 | tags: ghcr.io/bakito/k8s-event-logger:main,quay.io/bakito/k8s-event-logger:main 48 | platforms: linux/amd64 49 | provenance: false 50 | build-args: VERSION=main 51 | - name: Image digest 52 | run: echo ${{ steps.docker_build.outputs.digest }} 53 | 54 | # - name: Prune ghcr images 55 | # uses: vlaurin/action-ghcr-prune@main 56 | # with: 57 | # token: ${{ secrets.PRUNE_IMAGE_TOKEN }} 58 | # container: k8s-event-logger 59 | # dry-run: false # Dry-run first, then change to `false` 60 | # older-than: 7 # days 61 | # untagged: true 62 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: Mark stale issues and pull requests 2 | 3 | on: 4 | schedule: 5 | - cron: "0 0 * * *" 6 | 7 | jobs: 8 | stale: 9 | 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/stale@v9 14 | with: 15 | repo-token: ${{ secrets.GITHUB_TOKEN }} 16 | stale-issue-message: 'This issue has been inactive for 60 days. If the issue is still relevant please comment to re-activate the issue. If no action is taken within 7 days, the issue will be marked closed.' 17 | stale-pr-message: 'This pull request has been inactive for 60 days. If the pull request is still relevant please comment to re-activate the pull request. If no action is taken within 7 days, the pull request will be marked closed.' 18 | -------------------------------------------------------------------------------- /.github/workflows/upload-helm-chart.yml: -------------------------------------------------------------------------------- 1 | name: Upload Helm Chart 2 | 3 | on: 4 | release: 5 | types: 6 | - published 7 | 8 | # Allows you to run this workflow manually from the Actions tab 9 | workflow_dispatch: 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 17 | - uses: actions/checkout@v4 18 | 19 | - name: Set up Go 20 | uses: actions/setup-go@v5 21 | with: 22 | go-version-file: "go.mod" 23 | 24 | - name: Configure Git 25 | run: | 26 | git config user.name "$GITHUB_ACTOR" 27 | git config user.email "$GITHUB_ACTOR@users.noreply.github.com" 28 | 29 | - name: Install Helm 30 | uses: azure/setup-helm@v4 31 | 32 | - name: Login to ghcr.io 33 | uses: docker/login-action@v3 34 | with: 35 | registry: ghcr.io 36 | username: ${{ github.repository_owner }} 37 | password: ${{ secrets.GITHUB_TOKEN }} 38 | 39 | - name: Build chart 40 | run: | 41 | VERSION=$(echo "${{github.event.release.tag_name }}" | sed 's/v//g') 42 | sed -i "s/^version:.*$/version: ${VERSION}/" ./helm/Chart.yaml 43 | sed -i "s/^appVersion:.*$/appVersion: v${VERSION}/" ./helm/Chart.yaml 44 | make helm-docs 45 | ./bin/helm-docs 46 | helm package ./helm/ --version $VERSION --app-version v$VERSION -d dist 47 | helm push dist/k8s-event-logger-operator-${VERSION}.tgz oci://ghcr.io/bakito/helm-charts 48 | 49 | - name: Install chart-releaser 50 | uses: engineerd/configurator@v0.0.10 51 | with: 52 | name: cr 53 | url: https://github.com/helm/chart-releaser/releases/download/v1.6.1/chart-releaser_1.6.1_linux_amd64.tar.gz 54 | pathInArchive: cr 55 | 56 | - name: Upload Chart 57 | run: cr upload 58 | env: 59 | CR_OWNER: "${{github.repository_owner}}" 60 | CR_TOKEN: "${{secrets.CR_TOKEN}}" 61 | CR_GIT_REPO: "helm-charts" 62 | CR_PACKAGE_PATH: dist 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /config 2 | 3 | # Binaries for programs and plugins 4 | *.exe 5 | *.exe~ 6 | *.dll 7 | *.so 8 | *.dylib 9 | bin 10 | *.tgz 11 | 12 | # Test binary, build with `go test -c` 13 | *.test 14 | 15 | # Output of the go coverage tool, specifically when used with LiteIDE 16 | *.out 17 | *.out.tmp 18 | 19 | # Kubernetes Generated files - skip generated files, except for vendored files 20 | 21 | !vendor/**/zz_generated.* 22 | 23 | # editor and IDE paraphernalia 24 | .idea 25 | *.swp 26 | *.swo 27 | *~ 28 | /helm/*-values.yaml 29 | dist 30 | gomock* 31 | certs 32 | -------------------------------------------------------------------------------- /.golangci.yaml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | linters: 3 | enable: 4 | - asciicheck 5 | - bodyclose 6 | - dogsled 7 | - durationcheck 8 | - errorlint 9 | - gosec 10 | - importas 11 | - misspell 12 | - nakedret 13 | - nolintlint 14 | - unconvert 15 | - unparam 16 | exclusions: 17 | generated: lax 18 | presets: 19 | - comments 20 | - common-false-positives 21 | - legacy 22 | - std-error-handling 23 | paths: 24 | - third_party$ 25 | - builtin$ 26 | - examples$ 27 | formatters: 28 | enable: 29 | - gci 30 | - gofmt 31 | - gofumpt 32 | - goimports 33 | settings: 34 | gofmt: 35 | simplify: true 36 | exclusions: 37 | generated: lax 38 | paths: 39 | - third_party$ 40 | - builtin$ 41 | - examples$ 42 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | builds: 2 | - skip: true 3 | main: ./main.go 4 | ldflags: 5 | - -s -w -X github.com/bakito/k8s-event-logger-operator/version.Version={{.Version}} 6 | checksum: 7 | name_template: 'checksums.txt' 8 | snapshot: 9 | version_template: "{{ .Tag }}-next" 10 | changelog: 11 | sort: asc 12 | filters: 13 | exclude: 14 | - '^docs:' 15 | - '^test:' 16 | -------------------------------------------------------------------------------- /.toolbox.mk: -------------------------------------------------------------------------------- 1 | ## toolbox - start 2 | ## Generated with https://github.com/bakito/toolbox 3 | 4 | ## Current working directory 5 | TB_LOCALDIR ?= $(shell which cygpath > /dev/null 2>&1 && cygpath -m $$(pwd) || pwd) 6 | ## Location to install dependencies to 7 | TB_LOCALBIN ?= $(TB_LOCALDIR)/bin 8 | $(TB_LOCALBIN): 9 | if [ ! -e $(TB_LOCALBIN) ]; then mkdir -p $(TB_LOCALBIN); fi 10 | 11 | ## Tool Binaries 12 | TB_CONTROLLER_GEN ?= $(TB_LOCALBIN)/controller-gen 13 | TB_DEEPCOPY_GEN ?= $(TB_LOCALBIN)/deepcopy-gen 14 | TB_GINKGO ?= $(TB_LOCALBIN)/ginkgo 15 | TB_GOFUMPT ?= $(TB_LOCALBIN)/gofumpt 16 | TB_GOLANGCI_LINT ?= $(TB_LOCALBIN)/golangci-lint 17 | TB_GOLINES ?= $(TB_LOCALBIN)/golines 18 | TB_GORELEASER ?= $(TB_LOCALBIN)/goreleaser 19 | TB_HELM_DOCS ?= $(TB_LOCALBIN)/helm-docs 20 | TB_MOCKGEN ?= $(TB_LOCALBIN)/mockgen 21 | TB_SEMVER ?= $(TB_LOCALBIN)/semver 22 | 23 | ## Tool Versions 24 | TB_CONTROLLER_GEN_VERSION ?= v0.18.0 25 | TB_DEEPCOPY_GEN_VERSION ?= v0.33.1 26 | TB_GOFUMPT_VERSION ?= v0.8.0 27 | TB_GOLANGCI_LINT_VERSION ?= v2.1.6 28 | TB_GOLINES_VERSION ?= v0.12.2 29 | TB_GORELEASER_VERSION ?= v2.9.0 30 | TB_HELM_DOCS_VERSION ?= v1.14.2 31 | TB_SEMVER_VERSION ?= v1.1.3 32 | 33 | ## Tool Installer 34 | .PHONY: tb.controller-gen 35 | tb.controller-gen: $(TB_CONTROLLER_GEN) ## Download controller-gen locally if necessary. 36 | $(TB_CONTROLLER_GEN): $(TB_LOCALBIN) 37 | test -s $(TB_LOCALBIN)/controller-gen || GOBIN=$(TB_LOCALBIN) go install sigs.k8s.io/controller-tools/cmd/controller-gen@$(TB_CONTROLLER_GEN_VERSION) 38 | .PHONY: tb.deepcopy-gen 39 | tb.deepcopy-gen: $(TB_DEEPCOPY_GEN) ## Download deepcopy-gen locally if necessary. 40 | $(TB_DEEPCOPY_GEN): $(TB_LOCALBIN) 41 | test -s $(TB_LOCALBIN)/deepcopy-gen || GOBIN=$(TB_LOCALBIN) go install k8s.io/code-generator/cmd/deepcopy-gen@$(TB_DEEPCOPY_GEN_VERSION) 42 | .PHONY: tb.ginkgo 43 | tb.ginkgo: $(TB_GINKGO) ## Download ginkgo locally if necessary. 44 | $(TB_GINKGO): $(TB_LOCALBIN) 45 | test -s $(TB_LOCALBIN)/ginkgo || GOBIN=$(TB_LOCALBIN) go install github.com/onsi/ginkgo/v2/ginkgo 46 | .PHONY: tb.gofumpt 47 | tb.gofumpt: $(TB_GOFUMPT) ## Download gofumpt locally if necessary. 48 | $(TB_GOFUMPT): $(TB_LOCALBIN) 49 | test -s $(TB_LOCALBIN)/gofumpt || GOBIN=$(TB_LOCALBIN) go install mvdan.cc/gofumpt@$(TB_GOFUMPT_VERSION) 50 | .PHONY: tb.golangci-lint 51 | tb.golangci-lint: $(TB_GOLANGCI_LINT) ## Download golangci-lint locally if necessary. 52 | $(TB_GOLANGCI_LINT): $(TB_LOCALBIN) 53 | test -s $(TB_LOCALBIN)/golangci-lint || GOBIN=$(TB_LOCALBIN) go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@$(TB_GOLANGCI_LINT_VERSION) 54 | .PHONY: tb.golines 55 | tb.golines: $(TB_GOLINES) ## Download golines locally if necessary. 56 | $(TB_GOLINES): $(TB_LOCALBIN) 57 | test -s $(TB_LOCALBIN)/golines || GOBIN=$(TB_LOCALBIN) go install github.com/segmentio/golines@$(TB_GOLINES_VERSION) 58 | .PHONY: tb.goreleaser 59 | tb.goreleaser: $(TB_GORELEASER) ## Download goreleaser locally if necessary. 60 | $(TB_GORELEASER): $(TB_LOCALBIN) 61 | test -s $(TB_LOCALBIN)/goreleaser || GOBIN=$(TB_LOCALBIN) go install github.com/goreleaser/goreleaser/v2@$(TB_GORELEASER_VERSION) 62 | .PHONY: tb.helm-docs 63 | tb.helm-docs: $(TB_HELM_DOCS) ## Download helm-docs locally if necessary. 64 | $(TB_HELM_DOCS): $(TB_LOCALBIN) 65 | test -s $(TB_LOCALBIN)/helm-docs || GOBIN=$(TB_LOCALBIN) go install github.com/norwoodj/helm-docs/cmd/helm-docs@$(TB_HELM_DOCS_VERSION) 66 | .PHONY: tb.mockgen 67 | tb.mockgen: $(TB_MOCKGEN) ## Download mockgen locally if necessary. 68 | $(TB_MOCKGEN): $(TB_LOCALBIN) 69 | test -s $(TB_LOCALBIN)/mockgen || GOBIN=$(TB_LOCALBIN) go install go.uber.org/mock/mockgen 70 | .PHONY: tb.semver 71 | tb.semver: $(TB_SEMVER) ## Download semver locally if necessary. 72 | $(TB_SEMVER): $(TB_LOCALBIN) 73 | test -s $(TB_LOCALBIN)/semver || GOBIN=$(TB_LOCALBIN) go install github.com/bakito/semver@$(TB_SEMVER_VERSION) 74 | 75 | ## Reset Tools 76 | .PHONY: tb.reset 77 | tb.reset: 78 | @rm -f \ 79 | $(TB_LOCALBIN)/controller-gen \ 80 | $(TB_LOCALBIN)/deepcopy-gen \ 81 | $(TB_LOCALBIN)/ginkgo \ 82 | $(TB_LOCALBIN)/gofumpt \ 83 | $(TB_LOCALBIN)/golangci-lint \ 84 | $(TB_LOCALBIN)/golines \ 85 | $(TB_LOCALBIN)/goreleaser \ 86 | $(TB_LOCALBIN)/helm-docs \ 87 | $(TB_LOCALBIN)/mockgen \ 88 | $(TB_LOCALBIN)/semver 89 | 90 | ## Update Tools 91 | .PHONY: tb.update 92 | tb.update: tb.reset 93 | toolbox makefile -f $(TB_LOCALDIR)/Makefile \ 94 | sigs.k8s.io/controller-tools/cmd/controller-gen@github.com/kubernetes-sigs/controller-tools \ 95 | k8s.io/code-generator/cmd/deepcopy-gen@github.com/kubernetes/code-generator \ 96 | mvdan.cc/gofumpt@github.com/mvdan/gofumpt \ 97 | github.com/golangci/golangci-lint/v2/cmd/golangci-lint \ 98 | github.com/segmentio/golines \ 99 | github.com/goreleaser/goreleaser/v2 \ 100 | github.com/norwoodj/helm-docs/cmd/helm-docs \ 101 | github.com/bakito/semver 102 | ## toolbox - end -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.24-alpine AS builder 2 | 3 | WORKDIR /build 4 | 5 | RUN apk update && apk add upx 6 | 7 | ARG VERSION=main 8 | ENV GO111MODULE=on \ 9 | CGO_ENABLED=0 \ 10 | GOOS=linux 11 | COPY . . 12 | 13 | RUN go build -a -installsuffix cgo -ldflags="-w -s -X github.com/bakito/k8s-event-logger-operator/version.Version=${VERSION}" -o k8s-event-logger && \ 14 | upx -q k8s-event-logger 15 | 16 | # application image 17 | FROM scratch 18 | WORKDIR /opt/go 19 | 20 | LABEL maintainer="bakito " 21 | EXPOSE 8080 22 | USER 1001 23 | ENTRYPOINT ["/opt/go/k8s-event-logger"] 24 | 25 | COPY --from=builder /build/k8s-event-logger /opt/go/k8s-event-logger 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Include toolbox tasks 2 | include ./.toolbox.mk 3 | 4 | # Image URL to use all building/pushing image targets 5 | IMG ?= controller:latest 6 | # Produce CRDs that work back to Kubernetes 1.11 (no version conversion) 7 | CRD_OPTIONS ?= "crd" 8 | 9 | all: manager 10 | 11 | fmt: tb.golines tb.gofumpt 12 | $(TB_GOLINES) --base-formatter="$(TB_GOFUMPT)" --max-len=120 --write-output . 13 | 14 | # Run tests 15 | test: tidy lint generate mocks manifests test-ci 16 | 17 | # Run tests 18 | test-ci: 19 | go test ./... -coverprofile cover.out.tmp 20 | @cat cover.out.tmp | grep -v "zz_generated.deepcopy.go" > cover.out # filter coverage of generated code 21 | @rm -f cover.out.tmp 22 | 23 | # Build manager binary 24 | manager: generate lint 25 | go build -o bin/manager main.go 26 | 27 | manifests: tb.controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. 28 | $(TB_CONTROLLER_GEN) $(CRD_OPTIONS) rbac:roleName=manager-role webhook paths="./pkg/..." paths="./api/..." paths="./controllers/..." output:crd:artifacts:config=config/crd/bases 29 | cp config/crd/bases/*.yaml helm/crds/ 30 | yq -i '.spec.versions[0].schema.openAPIV3Schema.properties.spec.properties.imagePullSecrets.items.properties.name.description="Name of the referent."' helm/crds/eventlogger.bakito.ch_eventloggers.yaml 31 | 32 | generate: tb.controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. 33 | $(TB_CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..." 34 | 35 | lint: tb.golangci-lint 36 | $(TB_GOLANGCI_LINT) run --fix 37 | 38 | # go mod tidy 39 | tidy: 40 | go mod tidy 41 | 42 | # Build the docker image 43 | docker-build: test 44 | docker build . -t ${IMG} 45 | 46 | # Push the docker image 47 | docker-push: 48 | docker push ${IMG} 49 | 50 | .PHONY: release 51 | release: tb.goreleaser 52 | @version=$$($(TB_SEMVER)); \ 53 | git tag -s $$version -m"Release $$version" 54 | $(TB_GORELEASER) --clean 55 | 56 | .PHONY: test-release 57 | test-release: tb.goreleaser 58 | $(TB_GORELEASER) --skip=publish --snapshot --clean 59 | 60 | # generate mocks 61 | .PHONY: mocks 62 | mocks: tb.mockgen 63 | $(TB_MOCKGEN) -destination pkg/mocks/client/mock.go sigs.k8s.io/controller-runtime/pkg/client Client 64 | 65 | $(TB_MOCKGEN) -destination pkg/mocks/logr/mock.go github.com/go-logr/logr LogSink 66 | 67 | docs: tb.helm-docs update-docs 68 | @$(TB_HELM_DOCS) 69 | 70 | # Detect OS 71 | OS := $(shell uname) 72 | # Define the sed command based on OS 73 | SED := $(if $(filter Darwin, $(OS)), sed -i "", sed -i) 74 | update-docs: tb.semver 75 | @version=$$($(TB_SEMVER) -next); \ 76 | versionNum=$$($(TB_SEMVER) -next -numeric); \ 77 | $(SED) "s/^version:.*$$/version: $${versionNum}/" ./helm/Chart.yaml; \ 78 | $(SED) "s/^appVersion:.*$$/appVersion: $${version}/" ./helm/Chart.yaml 79 | 80 | helm-lint: docs 81 | helm lint ./helm --set webhook.enabled=true --set webhook.certManager.enabled=true -------------------------------------------------------------------------------- /PROJECT: -------------------------------------------------------------------------------- 1 | domain: bakito.ch 2 | layout: 3 | - go.kubebuilder.io/v3 4 | plugins: 5 | manifests.sdk.operatorframework.io/v2: {} 6 | scorecard.sdk.operatorframework.io/v2: {} 7 | projectName: k8s-event-logger-operator 8 | repo: github.com/bakito/k8s-event-logger-operator 9 | resources: 10 | - group: eventlogger 11 | kind: EventLogger 12 | version: v1 13 | webhooks: 14 | defaulting: false 15 | validation: true 16 | webhookVersion: v1 17 | version: "3" 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Go](https://github.com/bakito/k8s-event-logger-operator/actions/workflows/go.yml/badge.svg)](https://github.com/bakito/k8s-event-logger-operator/actions/workflows/go.yml) 2 | [![e2e](https://github.com/bakito/k8s-event-logger-operator/actions/workflows/e2e.yaml/badge.svg)](https://github.com/bakito/k8s-event-logger-operator/actions/workflows/e2e.yaml) 3 | [![Go Report Card](https://goreportcard.com/badge/github.com/bakito/k8s-event-logger-operator)](https://goreportcard.com/report/github.com/bakito/k8s-event-logger-operator) 4 | 5 | [![GitHub Release](https://img.shields.io/github/release/bakito/k8s-event-logger-operator.svg?style=flat)](https://github.com/bakito/k8s-event-logger-operator/releases) [![Coverage Status](https://coveralls.io/repos/github/bakito/k8s-event-logger-operator/badge.svg?branch=main)](https://coveralls.io/github/bakito/k8s-event-logger-operator?branch=main) 6 | 7 | # k8s event logger operator 8 | 9 | This operator creates a logging pod that logs corev1.Event information as structured json log. 10 | The crd allows to configure the events to be logged. 11 | 12 | ## Installation 13 | 14 | ### Operator 15 | The operator is insalled with helm. 16 | 17 | ```bash 18 | helm upgrade --install eventlogger ./helm/ 19 | ``` 20 | 21 | ### Custom Resource Definition (CRD) 22 | 23 | ```yaml 24 | apiVersion: eventlogger.bakito.ch/v1 25 | kind: EventLogger 26 | metadata: 27 | name: example-eventlogger 28 | spec: 29 | kinds: 30 | - name: DeploymentConfig # the kind of the event source to be logged 31 | apiGroup: apps.openshift.io # optional 32 | eventTypes: # optional 33 | - Normal 34 | - Warning 35 | reasons: # optional 36 | - DeploymentCreated 37 | - ReplicationControllerScaled 38 | skipReasons: # optional 39 | - Unhealthy 40 | matchingPatterns: # optional - regexp pattern to match event messages 41 | - .* 42 | skipOnMatch: false # optional - skip events where messages match the pattern. Default false 43 | 44 | 45 | eventTypes: # optional - define the event types to log. If no types are defined, all events are logged 46 | - Normal 47 | - Warning 48 | 49 | labels: # optional - additional labels for the pod 50 | name: value 51 | 52 | annotations: # optional - additional annotations for the pod 53 | name: value 54 | 55 | scrapeMetrics: false # optional att prometheus scrape metrics annotation to the pod. Default false 56 | 57 | namespace: "ns" # optional - the namespace to listen the events on. Default the current namespace 58 | 59 | nodeSelector: # optional - a node selector for the logging pod. 60 | key: value 61 | 62 | serviceAccount: "sa" # optional - if a custom ServiceAccount should be used for the pod. Default ServiceAccount is automatically created 63 | 64 | ImagePullSecrets: # optional - list of references to secrets to use for pulling the image. 65 | - name: name 66 | 67 | logFields: # optional - map if custom log field names. Key then log field name / Value: the reflection fields to the value within the struct corev1.Event https://github.com/kubernetes/api/blob/master/core/v1/types.go 68 | - name: name 69 | path: 70 | - InvolvedObject 71 | - Name 72 | - name: kind 73 | path: 74 | - InvolvedObject 75 | - Kind 76 | - name: type 77 | path: 78 | - Type 79 | - name: some-static-value 80 | value: "" 81 | ``` 82 | -------------------------------------------------------------------------------- /api/v1/eventlogger_types.go: -------------------------------------------------------------------------------- 1 | /* 2 | 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 v1 18 | 19 | import ( 20 | "github.com/bakito/k8s-event-logger-operator/version" 21 | corev1 "k8s.io/api/core/v1" 22 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 23 | ) 24 | 25 | // EventLoggerSpec defines the desired state of EventLogger 26 | type EventLoggerSpec struct { 27 | // Kinds the kinds to log the events for 28 | // +kubebuilder:validation:MinItems=1 29 | Kinds []Kind `json:"kinds,omitempty"` 30 | 31 | // EventTypes the event types to log. If empty all events are logged. 32 | // +kubebuilder:validation:MinItems=0 33 | EventTypes []string `json:"eventTypes,omitempty"` 34 | 35 | // Labels additional labels for the logger pod 36 | Labels map[string]string `json:"labels,omitempty" validate:"k8s-label-annotation-keys,k8s-label-values"` 37 | 38 | // Labels additional annotations for the logger pod 39 | Annotations map[string]string `json:"annotations,omitempty" validate:"k8s-label-annotation-keys"` 40 | 41 | // ScrapeMetrics if true, prometheus scrape annotations are added to the pod 42 | ScrapeMetrics *bool `json:"scrapeMetrics,omitempty"` 43 | 44 | // namespace the namespace to watch on, may be an empty string 45 | // +nullable 46 | // +optional 47 | Namespace *string `json:"namespace,omitempty"` 48 | 49 | // ServiceAccount the service account to use for the logger pod 50 | ServiceAccount string `json:"serviceAccount,omitempty"` 51 | 52 | // ImagePullSecrets is an optional list of references to secrets in the same namespace to use for pulling any of the images used by this EventLoggerSpec. 53 | // If specified, these secrets will be passed to individual puller implementations for them to use. 54 | // +optional 55 | ImagePullSecrets []corev1.LocalObjectReference `json:"imagePullSecrets,omitempty"` 56 | 57 | // NodeSelector is a selector which must be true for the pod to fit on a node. 58 | // Selector which must match a node's labels for the pod to be scheduled on that node. 59 | // More info: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/ 60 | // +optional 61 | NodeSelector map[string]string `json:"nodeSelector,omitempty" validate:"k8s-label-annotation-keys,k8s-label-values"` 62 | 63 | // LogFields fields ot the event to be logged. 64 | LogFields []LogField `json:"logFields,omitempty"` 65 | } 66 | 67 | // Kind defines a kind to log events for 68 | type Kind struct { 69 | // +kubebuilder:validation:MinLength=3 70 | Name string `json:"name"` 71 | 72 | // +optional 73 | // +nullable 74 | APIGroup *string `json:"apiGroup,omitempty"` 75 | 76 | // EventTypes the event types to log. If empty events are logged as defined in spec. 77 | // +kubebuilder:validation:MinItems=0 78 | EventTypes []string `json:"eventTypes,omitempty"` 79 | 80 | // Reasons the event reasons to log. If empty events with any reasons are logged. 81 | // +kubebuilder:validation:MinItems=0 82 | Reasons []string `json:"reasons,omitempty"` 83 | 84 | // SkipReasons event reasons to log to skip. If empty events with any reasons are logged. 85 | // +kubebuilder:validation:MinItems=0 86 | SkipReasons []string `json:"skipReasons,omitempty"` 87 | 88 | // MatchingPatterns optional regex pattern that must be contained in the message to be logged 89 | // +kubebuilder:validation:MinItems=0 90 | MatchingPatterns []string `json:"matchingPatterns,omitempty"` 91 | 92 | // SkipOnMatch skip the entry if matched 93 | SkipOnMatch *bool `json:"skipOnMatch,omitempty"` 94 | } 95 | 96 | // LogField defines a log field 97 | type LogField struct { 98 | // name of the log field 99 | Name string `json:"name"` 100 | // Path within the corev1.Event struct https://github.com/kubernetes/api/blob/master/core/v1/types.go 101 | // +kubebuilder:validation:MinItems=1 102 | Path []string `json:"path,omitempty"` 103 | 104 | // Value a static value of the log field. Can be uses to add static log fields 105 | // +optional 106 | // +nullable 107 | Value *string `json:"value,omitempty"` 108 | } 109 | 110 | // EventLoggerStatus defines the observed state of EventLogger 111 | type EventLoggerStatus struct { 112 | // OperatorVersion the version of the operator that processed the cr 113 | OperatorVersion string `json:"operatorVersion"` 114 | // LastProcessed the timestamp the cr was last processed 115 | LastProcessed metav1.Time `json:"lastProcessed"` 116 | // Hash 117 | Hash string `json:"hash,omitempty"` 118 | // Error 119 | Error string `json:"error,omitempty"` 120 | } 121 | 122 | // +kubebuilder:object:root=true 123 | 124 | // EventLogger is the Schema for the eventloggers API 125 | type EventLogger struct { 126 | metav1.TypeMeta `json:",inline"` 127 | metav1.ObjectMeta `json:"metadata,omitempty"` 128 | 129 | Spec EventLoggerSpec `json:"spec,omitempty"` 130 | Status EventLoggerStatus `json:"status,omitempty"` 131 | } 132 | 133 | // +kubebuilder:object:root=true 134 | 135 | // EventLoggerList contains a list of EventLogger 136 | type EventLoggerList struct { 137 | metav1.TypeMeta ` json:",inline"` 138 | metav1.ListMeta ` json:"metadata,omitempty"` 139 | Items []EventLogger `json:"items"` 140 | } 141 | 142 | func init() { 143 | SchemeBuilder.Register(&EventLogger{}, &EventLoggerList{}) 144 | } 145 | 146 | // Apply update the status of the current event logger 147 | func (in *EventLogger) Apply(err error) { 148 | if err != nil { 149 | in.Status.Error = err.Error() 150 | } else { 151 | in.Status.Error = "" 152 | } 153 | in.Status.LastProcessed = metav1.Now() 154 | in.Status.OperatorVersion = version.Version 155 | } 156 | -------------------------------------------------------------------------------- /api/v1/eventlogger_types_test.go: -------------------------------------------------------------------------------- 1 | package v1_test 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | 7 | v1 "github.com/bakito/k8s-event-logger-operator/api/v1" 8 | "github.com/bakito/k8s-event-logger-operator/version" 9 | . "github.com/onsi/ginkgo/v2" 10 | . "github.com/onsi/gomega" 11 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 12 | "k8s.io/utils/ptr" 13 | ) 14 | 15 | var _ = Describe("V1", func() { 16 | Context("APIGroup serialisation", func() { 17 | It("should serialize an empty string", func() { 18 | k := &v1.Kind{ 19 | Name: "a", 20 | APIGroup: ptr.To(""), 21 | } 22 | b, err := json.Marshal(k) 23 | Ω(err).ShouldNot(HaveOccurred()) 24 | Ω(string(b)).Should(Equal(`{"name":"a","apiGroup":""}`)) 25 | }) 26 | It("not add apiGroups if nil", func() { 27 | k := &v1.Kind{ 28 | Name: "a", 29 | } 30 | b, err := json.Marshal(k) 31 | Ω(err).ShouldNot(HaveOccurred()) 32 | Ω(string(b)).Should(Equal(`{"name":"a"}`)) 33 | }) 34 | It("should serialize an the apiGroup value", func() { 35 | k := &v1.Kind{ 36 | Name: "a", 37 | APIGroup: ptr.To("b"), 38 | } 39 | b, err := json.Marshal(k) 40 | Ω(err).ShouldNot(HaveOccurred()) 41 | Ω(string(b)).Should(Equal(`{"name":"a","apiGroup":"b"}`)) 42 | }) 43 | }) 44 | Context("Apply", func() { 45 | var el *v1.EventLogger 46 | BeforeEach(func() { 47 | el = &v1.EventLogger{} 48 | }) 49 | It("should not set an error message", func() { 50 | el.Apply(nil) 51 | Ω(el.Status.Error).Should(BeEmpty()) 52 | Ω(el.Status.OperatorVersion).Should(Equal(version.Version)) 53 | Ω(el.Status.LastProcessed).ShouldNot(Equal(Equal(metav1.Time{}))) 54 | }) 55 | It("should set an error message", func() { 56 | err := errors.New("this is an error") 57 | el.Apply(err) 58 | Ω(el.Status.Error).Should(Equal(err.Error())) 59 | Ω(el.Status.OperatorVersion).Should(Equal(version.Version)) 60 | Ω(el.Status.LastProcessed).ShouldNot(Equal(Equal(metav1.Time{}))) 61 | }) 62 | }) 63 | }) 64 | -------------------------------------------------------------------------------- /api/v1/eventlogger_validation.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "context" 5 | "crypto/sha256" 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | "reflect" 10 | "strings" 11 | 12 | "github.com/bakito/k8s-event-logger-operator/version" 13 | english "github.com/go-playground/locales/en" 14 | ut "github.com/go-playground/universal-translator" 15 | "github.com/go-playground/validator/v10" 16 | "github.com/go-playground/validator/v10/translations/en" 17 | "k8s.io/apimachinery/pkg/util/validation" 18 | ) 19 | 20 | // Custom type for context key, so we don't have to use 'string' directly 21 | type contextKey string 22 | 23 | var ( 24 | specKey = contextKey("spec") 25 | errorsKey = contextKey("errors") 26 | ) 27 | 28 | // HasChanged check if the spec or operator version has changed 29 | func (in *EventLogger) HasChanged() bool { 30 | return in.Status.Hash != in.Spec.Hash() || in.Status.OperatorVersion != version.Version 31 | } 32 | 33 | // Hash the event 34 | func (in *EventLoggerSpec) Hash() string { 35 | h := sha256.New() 36 | bytes, _ := json.Marshal(in) 37 | _, _ = h.Write(bytes) 38 | sum := h.Sum(nil) 39 | return fmt.Sprintf("%x", sum) 40 | } 41 | 42 | // Validate the event 43 | func (in *EventLoggerSpec) Validate() error { 44 | return newEventLoggerValidator(in).Validate() 45 | } 46 | 47 | func k8sLabelValues(_ context.Context, fl validator.FieldLevel) bool { 48 | if labels, ok := fl.Field().Interface().(map[string]string); ok { 49 | for _, v := range labels { 50 | if errs := validation.IsValidLabelValue(v); len(errs) > 0 { 51 | return false 52 | } 53 | } 54 | } 55 | return true 56 | } 57 | 58 | func k8sLabelAnnotationKeys(_ context.Context, fl validator.FieldLevel) bool { 59 | if annotations, ok := fl.Field().Interface().(map[string]string); ok { 60 | for k := range annotations { 61 | if errs := validation.IsQualifiedName(k); len(errs) > 0 { 62 | return false 63 | } 64 | } 65 | } 66 | return true 67 | } 68 | 69 | // eventLoggerValidator is a custom validator for the event logger 70 | type eventLoggerValidator struct { 71 | val *validator.Validate 72 | ctx context.Context 73 | spec *EventLoggerSpec 74 | trans ut.Translator 75 | } 76 | 77 | // Custom error for event logger validation 78 | type eventLoggerValidatorError struct { 79 | errList []string 80 | } 81 | 82 | func (err eventLoggerValidatorError) Error() string { 83 | return strings.Join(err.errList, "\n") 84 | } 85 | 86 | func (err *eventLoggerValidatorError) addError(errStr string) { 87 | err.errList = append(err.errList, errStr) 88 | } 89 | 90 | // newEventLoggerValidator creates a new EventLoggerValidator 91 | func newEventLoggerValidator(spec *EventLoggerSpec) *eventLoggerValidator { 92 | result := validator.New() 93 | 94 | _ = result.RegisterValidationCtx("k8s-label-annotation-keys", k8sLabelAnnotationKeys) 95 | _ = result.RegisterValidationCtx("k8s-label-values", k8sLabelValues) 96 | 97 | errKey := strings.Join(validation.IsQualifiedName("a@a"), " ") 98 | errLabelVal := strings.Join(validation.IsValidLabelValue("a:/a"), " ") 99 | 100 | // context 101 | ctx := context.WithValue(context.Background(), specKey, spec) 102 | ctx = context.WithValue(ctx, errorsKey, &eventLoggerValidatorError{}) 103 | 104 | // default translations 105 | eng := english.New() 106 | uni := ut.New(eng, eng) 107 | trans, _ := uni.GetTranslator("en") 108 | _ = en.RegisterDefaultTranslations(result, trans) 109 | 110 | // additional translations 111 | translations := []struct { 112 | tag string 113 | translation string 114 | }{ 115 | { 116 | tag: "k8s-label-annotation-keys", 117 | translation: fmt.Sprintf("'key in {0}' must match the pattern %s", errKey), 118 | }, 119 | { 120 | tag: "k8s-label-values", 121 | translation: fmt.Sprintf("'values in {0}' must match the pattern %s", errLabelVal), 122 | }, 123 | } 124 | for _, t := range translations { 125 | _ = result.RegisterTranslation(t.tag, trans, registrationFunc(t.tag, t.translation), translateFunc) 126 | } 127 | 128 | return &eventLoggerValidator{ 129 | val: result, 130 | ctx: ctx, 131 | spec: spec, 132 | trans: trans, 133 | } 134 | } 135 | 136 | func registrationFunc(tag string, translation string) validator.RegisterTranslationsFunc { 137 | return func(ut ut.Translator) (err error) { 138 | if err = ut.Add(tag, translation, true); err != nil { 139 | return 140 | } 141 | return 142 | } 143 | } 144 | 145 | func translateFunc(ut ut.Translator, fe validator.FieldError) string { 146 | t, err := ut.T(fe.Tag(), reflect.ValueOf(fe.Value()).String(), fe.Param()) 147 | if err != nil { 148 | return fe.(error).Error() 149 | } 150 | return t 151 | } 152 | 153 | // Validate validates the entire event logger spec for errors and returns an error (it can be casted to 154 | // eventLoggerValidatorError, containing a list of errors inside). When error is printed as string, it will 155 | // automatically contain the full list of validation errors. 156 | func (v *eventLoggerValidator) Validate() error { 157 | // validate spec 158 | err := v.val.StructCtx(v.ctx, v.spec) 159 | if err == nil { 160 | return nil 161 | } 162 | 163 | // collect human-readable errors 164 | result := eventLoggerValidatorError{} 165 | var vErrors validator.ValidationErrors 166 | errors.As(err, &vErrors) 167 | for _, vErr := range vErrors { 168 | errStr := fmt.Sprintf("%s: %s", vErr.Namespace(), vErr.Translate(v.trans)) 169 | result.addError(errStr) 170 | } 171 | 172 | // collect additional errors stored in context 173 | for _, errStr := range v.ctx.Value(errorsKey).(*eventLoggerValidatorError).errList { 174 | result.addError(errStr) 175 | } 176 | 177 | return result 178 | } 179 | -------------------------------------------------------------------------------- /api/v1/eventlogger_validation_test.go: -------------------------------------------------------------------------------- 1 | package v1_test 2 | 3 | import ( 4 | v1 "github.com/bakito/k8s-event-logger-operator/api/v1" 5 | . "github.com/onsi/ginkgo/v2" 6 | . "github.com/onsi/gomega" 7 | ) 8 | 9 | var _ = Describe("V1", func() { 10 | Context("Validate", func() { 11 | It("should succeed", func() { 12 | s := &v1.EventLoggerSpec{ 13 | Annotations: map[string]string{"valid/valid": "valid", "valid": "valid"}, 14 | Labels: map[string]string{"valid": "valid"}, 15 | } 16 | Ω(s.Validate()).ShouldNot(HaveOccurred()) 17 | }) 18 | It("should have invalid label key", func() { 19 | s := &v1.EventLoggerSpec{ 20 | Labels: map[string]string{"in valid": "valid"}, 21 | } 22 | Ω(s.Validate()).Should(HaveOccurred()) 23 | s = &v1.EventLoggerSpec{ 24 | Labels: map[string]string{"in:valid": "valid"}, 25 | } 26 | Ω(s.Validate()).Should(HaveOccurred()) 27 | }) 28 | It("should have invalid label value", func() { 29 | s := &v1.EventLoggerSpec{ 30 | Labels: map[string]string{"valid": "in valid"}, 31 | } 32 | Ω(s.Validate()).Should(HaveOccurred()) 33 | s = &v1.EventLoggerSpec{ 34 | Labels: map[string]string{"valid": "in:valid"}, 35 | } 36 | Ω(s.Validate()).Should(HaveOccurred()) 37 | }) 38 | It("should have invalid annotation key", func() { 39 | s := &v1.EventLoggerSpec{ 40 | Annotations: map[string]string{"in valid": "valid"}, 41 | } 42 | Ω(s.Validate()).Should(HaveOccurred()) 43 | s = &v1.EventLoggerSpec{ 44 | Annotations: map[string]string{"in:valid:": "valid"}, 45 | } 46 | Ω(s.Validate()).Should(HaveOccurred()) 47 | }) 48 | }) 49 | }) 50 | -------------------------------------------------------------------------------- /api/v1/eventlogger_webhook.go: -------------------------------------------------------------------------------- 1 | /* 2 | 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 v1 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | 23 | "k8s.io/apimachinery/pkg/runtime" 24 | ctrl "sigs.k8s.io/controller-runtime" 25 | "sigs.k8s.io/controller-runtime/pkg/webhook" 26 | "sigs.k8s.io/controller-runtime/pkg/webhook/admission" 27 | ) 28 | 29 | // SetupWebhookWithManager setup with manager 30 | func (in *EventLogger) SetupWebhookWithManager(mgr ctrl.Manager) error { 31 | return ctrl.NewWebhookManagedBy(mgr). 32 | WithValidator(&validateEl{}). 33 | For(in). 34 | Complete() 35 | } 36 | 37 | // +kubebuilder:webhook:verbs=create;update,path=/validate-eventlogger-bakito-ch-v1-eventlogger,mutating=false,failurePolicy=fail,sideEffects=None,groups=eventlogger.bakito.ch,resources=eventloggers,versions=v1,name=veventlogger.bakito.ch,admissionReviewVersions={v1,v1beta1} 38 | 39 | var _ webhook.CustomValidator = &validateEl{} 40 | 41 | type validateEl struct{} 42 | 43 | // ValidateCreate implements webhook.Validator so a webhook will be registered for the type 44 | func (v *validateEl) ValidateCreate(_ context.Context, o runtime.Object) (warnings admission.Warnings, err error) { 45 | return v.validate(o) 46 | } 47 | 48 | // ValidateUpdate implements webhook.Validator so a webhook will be registered for the type 49 | func (v *validateEl) ValidateUpdate(_ context.Context, o, _ runtime.Object) (warnings admission.Warnings, err error) { 50 | return v.validate(o) 51 | } 52 | 53 | // ValidateDelete implements webhook.Validator so a webhook will be registered for the type 54 | func (v *validateEl) ValidateDelete(_ context.Context, _ runtime.Object) (warnings admission.Warnings, err error) { 55 | return nil, nil 56 | } 57 | 58 | func (v *validateEl) validate(obj runtime.Object) (admission.Warnings, error) { 59 | el, ok := obj.(*EventLogger) 60 | if !ok { 61 | return nil, fmt.Errorf("expected a EventLogger but got a %T", obj) 62 | } 63 | 64 | return nil, el.Spec.Validate() 65 | } 66 | -------------------------------------------------------------------------------- /api/v1/eventlogger_webhook_test.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "context" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | var _ = Describe("V1", func() { 11 | var el *EventLogger 12 | var val *validateEl 13 | BeforeEach(func() { 14 | val = &validateEl{} 15 | el = &EventLogger{ 16 | Spec: EventLoggerSpec{}, 17 | } 18 | }) 19 | Context("Valid", func() { 20 | Context("ValidateCreate", func() { 21 | It("should be valid", func() { 22 | w, err := val.ValidateCreate(context.TODO(), el) 23 | Ω(w).Should(BeNil()) 24 | Ω(err).ShouldNot(HaveOccurred()) 25 | }) 26 | }) 27 | Context("ValidateUpdate", func() { 28 | It("should be valid", func() { 29 | w, err := val.ValidateUpdate(context.TODO(), el, nil) 30 | Ω(w).Should(BeNil()) 31 | Ω(err).ShouldNot(HaveOccurred()) 32 | }) 33 | }) 34 | Context("ValidateUpdate", func() { 35 | It("should be nil", func() { 36 | w, err := val.ValidateDelete(context.TODO(), el) 37 | Ω(w).Should(BeNil()) 38 | Ω(err).ShouldNot(HaveOccurred()) 39 | }) 40 | }) 41 | }) 42 | Context("Invalid", func() { 43 | BeforeEach(func() { 44 | el.Spec.Labels = map[string]string{"in valid": "valid"} 45 | }) 46 | Context("ValidateCreate", func() { 47 | It("should be invalid", func() { 48 | w, err := val.ValidateCreate(context.TODO(), el) 49 | Ω(w).Should(BeNil()) 50 | Ω(err).Should(HaveOccurred()) 51 | }) 52 | }) 53 | Context("ValidateUpdate", func() { 54 | It("should be invalid", func() { 55 | w, err := val.ValidateUpdate(context.TODO(), el, nil) 56 | Ω(w).Should(BeNil()) 57 | Ω(err).Should(HaveOccurred()) 58 | }) 59 | }) 60 | Context("ValidateUpdate", func() { 61 | It("should be nil", func() { 62 | w, err := val.ValidateDelete(context.TODO(), el) 63 | Ω(w).Should(BeNil()) 64 | Ω(err).ShouldNot(HaveOccurred()) 65 | }) 66 | }) 67 | }) 68 | }) 69 | -------------------------------------------------------------------------------- /api/v1/groupversion_info.go: -------------------------------------------------------------------------------- 1 | /* 2 | 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 v1 contains API Schema definitions for the eventlogger v1 API group 18 | // +kubebuilder:object:generate=true 19 | // +groupName=eventlogger.bakito.ch 20 | package v1 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: "eventlogger.bakito.ch", Version: "v1"} 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/v1/v1_suite_test.go: -------------------------------------------------------------------------------- 1 | package v1_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestV1(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "V1 Suite") 13 | } 14 | -------------------------------------------------------------------------------- /api/v1/zz_generated.deepcopy.go: -------------------------------------------------------------------------------- 1 | //go:build !ignore_autogenerated 2 | 3 | /* 4 | 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 v1 22 | 23 | import ( 24 | corev1 "k8s.io/api/core/v1" 25 | "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 *EventLogger) DeepCopyInto(out *EventLogger) { 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 EventLogger. 38 | func (in *EventLogger) DeepCopy() *EventLogger { 39 | if in == nil { 40 | return nil 41 | } 42 | out := new(EventLogger) 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 *EventLogger) 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 *EventLoggerList) DeepCopyInto(out *EventLoggerList) { 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([]EventLogger, 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 EventLoggerList. 70 | func (in *EventLoggerList) DeepCopy() *EventLoggerList { 71 | if in == nil { 72 | return nil 73 | } 74 | out := new(EventLoggerList) 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 *EventLoggerList) 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 *EventLoggerSpec) DeepCopyInto(out *EventLoggerSpec) { 89 | *out = *in 90 | if in.Kinds != nil { 91 | in, out := &in.Kinds, &out.Kinds 92 | *out = make([]Kind, len(*in)) 93 | for i := range *in { 94 | (*in)[i].DeepCopyInto(&(*out)[i]) 95 | } 96 | } 97 | if in.EventTypes != nil { 98 | in, out := &in.EventTypes, &out.EventTypes 99 | *out = make([]string, len(*in)) 100 | copy(*out, *in) 101 | } 102 | if in.Labels != nil { 103 | in, out := &in.Labels, &out.Labels 104 | *out = make(map[string]string, len(*in)) 105 | for key, val := range *in { 106 | (*out)[key] = val 107 | } 108 | } 109 | if in.Annotations != nil { 110 | in, out := &in.Annotations, &out.Annotations 111 | *out = make(map[string]string, len(*in)) 112 | for key, val := range *in { 113 | (*out)[key] = val 114 | } 115 | } 116 | if in.ScrapeMetrics != nil { 117 | in, out := &in.ScrapeMetrics, &out.ScrapeMetrics 118 | *out = new(bool) 119 | **out = **in 120 | } 121 | if in.Namespace != nil { 122 | in, out := &in.Namespace, &out.Namespace 123 | *out = new(string) 124 | **out = **in 125 | } 126 | if in.ImagePullSecrets != nil { 127 | in, out := &in.ImagePullSecrets, &out.ImagePullSecrets 128 | *out = make([]corev1.LocalObjectReference, len(*in)) 129 | copy(*out, *in) 130 | } 131 | if in.NodeSelector != nil { 132 | in, out := &in.NodeSelector, &out.NodeSelector 133 | *out = make(map[string]string, len(*in)) 134 | for key, val := range *in { 135 | (*out)[key] = val 136 | } 137 | } 138 | if in.LogFields != nil { 139 | in, out := &in.LogFields, &out.LogFields 140 | *out = make([]LogField, len(*in)) 141 | for i := range *in { 142 | (*in)[i].DeepCopyInto(&(*out)[i]) 143 | } 144 | } 145 | } 146 | 147 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EventLoggerSpec. 148 | func (in *EventLoggerSpec) DeepCopy() *EventLoggerSpec { 149 | if in == nil { 150 | return nil 151 | } 152 | out := new(EventLoggerSpec) 153 | in.DeepCopyInto(out) 154 | return out 155 | } 156 | 157 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 158 | func (in *EventLoggerStatus) DeepCopyInto(out *EventLoggerStatus) { 159 | *out = *in 160 | in.LastProcessed.DeepCopyInto(&out.LastProcessed) 161 | } 162 | 163 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EventLoggerStatus. 164 | func (in *EventLoggerStatus) DeepCopy() *EventLoggerStatus { 165 | if in == nil { 166 | return nil 167 | } 168 | out := new(EventLoggerStatus) 169 | in.DeepCopyInto(out) 170 | return out 171 | } 172 | 173 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 174 | func (in *Kind) DeepCopyInto(out *Kind) { 175 | *out = *in 176 | if in.APIGroup != nil { 177 | in, out := &in.APIGroup, &out.APIGroup 178 | *out = new(string) 179 | **out = **in 180 | } 181 | if in.EventTypes != nil { 182 | in, out := &in.EventTypes, &out.EventTypes 183 | *out = make([]string, len(*in)) 184 | copy(*out, *in) 185 | } 186 | if in.Reasons != nil { 187 | in, out := &in.Reasons, &out.Reasons 188 | *out = make([]string, len(*in)) 189 | copy(*out, *in) 190 | } 191 | if in.SkipReasons != nil { 192 | in, out := &in.SkipReasons, &out.SkipReasons 193 | *out = make([]string, len(*in)) 194 | copy(*out, *in) 195 | } 196 | if in.MatchingPatterns != nil { 197 | in, out := &in.MatchingPatterns, &out.MatchingPatterns 198 | *out = make([]string, len(*in)) 199 | copy(*out, *in) 200 | } 201 | if in.SkipOnMatch != nil { 202 | in, out := &in.SkipOnMatch, &out.SkipOnMatch 203 | *out = new(bool) 204 | **out = **in 205 | } 206 | } 207 | 208 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Kind. 209 | func (in *Kind) DeepCopy() *Kind { 210 | if in == nil { 211 | return nil 212 | } 213 | out := new(Kind) 214 | in.DeepCopyInto(out) 215 | return out 216 | } 217 | 218 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 219 | func (in *LogField) DeepCopyInto(out *LogField) { 220 | *out = *in 221 | if in.Path != nil { 222 | in, out := &in.Path, &out.Path 223 | *out = make([]string, len(*in)) 224 | copy(*out, *in) 225 | } 226 | if in.Value != nil { 227 | in, out := &in.Value, &out.Value 228 | *out = new(string) 229 | **out = **in 230 | } 231 | } 232 | 233 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LogField. 234 | func (in *LogField) DeepCopy() *LogField { 235 | if in == nil { 236 | return nil 237 | } 238 | out := new(LogField) 239 | in.DeepCopyInto(out) 240 | return out 241 | } 242 | -------------------------------------------------------------------------------- /controllers/config/config_controller.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "sync" 8 | 9 | cnst "github.com/bakito/k8s-event-logger-operator/pkg/constants" 10 | "github.com/bakito/operator-utils/pkg/filter" 11 | "github.com/go-logr/logr" 12 | corev1 "k8s.io/api/core/v1" 13 | "k8s.io/apimachinery/pkg/api/resource" 14 | "k8s.io/apimachinery/pkg/runtime" 15 | "k8s.io/apimachinery/pkg/types" 16 | "k8s.io/apimachinery/pkg/util/yaml" 17 | ctrl "sigs.k8s.io/controller-runtime" 18 | "sigs.k8s.io/controller-runtime/pkg/client" 19 | "sigs.k8s.io/controller-runtime/pkg/reconcile" 20 | ) 21 | 22 | type contextKey string 23 | 24 | const ( 25 | defaultContainerName = "k8s-event-logger-operator" 26 | configKey contextKey = "config" 27 | ) 28 | 29 | var ( 30 | defaultPodReqCPU = resource.MustParse("100m") 31 | defaultPodReqMem = resource.MustParse("64Mi") 32 | defaultPodMaxCPU = resource.MustParse("200m") 33 | defaultPodMaxMem = resource.MustParse("128Mi") 34 | ) 35 | 36 | // Reconciler reconciles a Pod object 37 | type Reconciler struct { 38 | client.Reader 39 | Log logr.Logger 40 | Scheme *runtime.Scheme 41 | 42 | cfg *Cfg 43 | once sync.Once 44 | eventLoggerImage string 45 | } 46 | 47 | // +kubebuilder:rbac:groups=,resources=configmaps,verbs=get;list;watch 48 | 49 | // Reconcile EventLogger to setup event logger pods 50 | func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { 51 | reqLogger := r.Log.WithValues("namespace", req.Namespace, "name", req.Name) 52 | return reconcile.Result{}, r.readConfig(ctx, reqLogger, req.NamespacedName) 53 | } 54 | 55 | func (r *Reconciler) readConfig(ctx context.Context, reqLogger logr.Logger, nn types.NamespacedName) error { 56 | // Fetch the EventLogger cr 57 | cm := &corev1.ConfigMap{} 58 | err := r.Get(ctx, nn, cm) 59 | if err != nil { 60 | return err 61 | } 62 | 63 | reqLogger.Info("Reconciling config") 64 | 65 | noPodTemplate := fmt.Errorf( 66 | `configmap %q must contain the container template %q`, 67 | nn.String(), 68 | cnst.ConfigKeyContainerTemplate, 69 | ) 70 | if len(cm.Data) == 0 { 71 | return noPodTemplate 72 | } 73 | pt, ok := cm.Data[cnst.ConfigKeyContainerTemplate] 74 | if !ok { 75 | return noPodTemplate 76 | } 77 | container := corev1.Container{} 78 | if err := yaml.Unmarshal([]byte(pt), &container); err != nil { 79 | return err 80 | } 81 | 82 | if container.Resources.Requests == nil { 83 | container.Resources.Requests = map[corev1.ResourceName]resource.Quantity{} 84 | } 85 | if container.Resources.Limits == nil { 86 | container.Resources.Limits = map[corev1.ResourceName]resource.Quantity{} 87 | } 88 | 89 | if _, ok := container.Resources.Requests[corev1.ResourceCPU]; !ok { 90 | container.Resources.Requests[corev1.ResourceCPU] = defaultPodReqCPU 91 | } 92 | if _, ok := container.Resources.Requests[corev1.ResourceMemory]; !ok { 93 | container.Resources.Requests[corev1.ResourceMemory] = defaultPodReqMem 94 | } 95 | if _, ok := container.Resources.Limits[corev1.ResourceCPU]; !ok { 96 | container.Resources.Limits[corev1.ResourceCPU] = defaultPodMaxCPU 97 | } 98 | if _, ok := container.Resources.Limits[corev1.ResourceMemory]; !ok { 99 | container.Resources.Limits[corev1.ResourceMemory] = defaultPodMaxMem 100 | } 101 | 102 | if container.Image == "" { 103 | container.Image = r.eventLoggerImage 104 | } 105 | if container.ImagePullPolicy == "" { 106 | container.ImagePullPolicy = corev1.PullAlways 107 | } 108 | 109 | r.cfg.ContainerTemplate = container 110 | 111 | return nil 112 | } 113 | 114 | func (r *Reconciler) setupEventLoggerImage(nn types.NamespacedName) error { 115 | if podImage, ok := os.LookupEnv(cnst.EnvEventLoggerImage); ok && podImage != "" { 116 | r.eventLoggerImage = podImage 117 | return nil 118 | } 119 | p := &corev1.Pod{} 120 | err := r.Get(context.TODO(), nn, p) 121 | if err != nil { 122 | return err 123 | } 124 | 125 | if len(p.Spec.Containers) == 1 { 126 | r.eventLoggerImage = p.Spec.Containers[0].Image 127 | return nil 128 | 129 | } 130 | for _, c := range p.Spec.Containers { 131 | if c.Name == defaultContainerName { 132 | r.eventLoggerImage = c.Image 133 | return nil 134 | } 135 | } 136 | return fmt.Errorf("could not evaluate the event logger image to use") 137 | } 138 | 139 | func (r *Reconciler) Ctx() context.Context { 140 | r.once.Do(func() { 141 | r.cfg = &Cfg{} 142 | }) 143 | return context.WithValue(context.Background(), configKey, r.cfg) 144 | } 145 | 146 | func GetCfg(ctx context.Context) *Cfg { 147 | c, ok := ctx.Value(configKey).(*Cfg) 148 | if !ok { 149 | return nil 150 | } 151 | clone := *c 152 | return &clone 153 | } 154 | 155 | // SetupWithManager setup with manager 156 | func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error { 157 | namespace := os.Getenv(cnst.EnvPodNamespace) 158 | cmName := os.Getenv(cnst.EnvConfigMapName) 159 | podName := os.Getenv(cnst.EnvPodName) 160 | 161 | if err := r.setupEventLoggerImage(types.NamespacedName{ 162 | Namespace: namespace, 163 | Name: podName, 164 | }); err != nil { 165 | return err 166 | } 167 | 168 | if err := r.readConfig(r.Ctx(), mgr.GetLogger(), types.NamespacedName{ 169 | Namespace: namespace, 170 | Name: cmName, 171 | }); err != nil { 172 | return err 173 | } 174 | 175 | return ctrl.NewControllerManagedBy(mgr). 176 | For(&corev1.ConfigMap{}).WithEventFilter(filter.NamePredicate{ 177 | Namespace: namespace, 178 | Names: []string{cmName}, 179 | }, 180 | ).Complete(r) 181 | } 182 | 183 | type Cfg struct { 184 | ContainerTemplate corev1.Container 185 | } 186 | -------------------------------------------------------------------------------- /controllers/config/config_controller_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "os" 5 | "time" 6 | 7 | v1 "github.com/bakito/k8s-event-logger-operator/api/v1" 8 | cnst "github.com/bakito/k8s-event-logger-operator/pkg/constants" 9 | "github.com/google/uuid" 10 | . "github.com/onsi/ginkgo/v2" 11 | . "github.com/onsi/gomega" 12 | corev1 "k8s.io/api/core/v1" 13 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 14 | "k8s.io/apimachinery/pkg/runtime" 15 | "k8s.io/apimachinery/pkg/types" 16 | "k8s.io/client-go/kubernetes/scheme" 17 | ctrl "sigs.k8s.io/controller-runtime" 18 | "sigs.k8s.io/controller-runtime/pkg/client/fake" 19 | "sigs.k8s.io/controller-runtime/pkg/reconcile" 20 | ) 21 | 22 | var _ = Describe("Config", func() { 23 | var ( 24 | s *runtime.Scheme 25 | cr *Reconciler 26 | ) 27 | 28 | BeforeEach(func() { 29 | s = scheme.Scheme 30 | Ω(v1.SchemeBuilder.AddToScheme(s)).ShouldNot(HaveOccurred()) 31 | 32 | cr = &Reconciler{ 33 | Log: ctrl.Log.WithName("controllers").WithName("Pod"), 34 | Scheme: s, 35 | } 36 | }) 37 | 38 | Context("Reconcile", func() { 39 | var configMap *corev1.ConfigMap 40 | BeforeEach(func() { 41 | configMap = &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{ 42 | Name: uuid.NewString(), 43 | Namespace: uuid.NewString(), 44 | }} 45 | }) 46 | 47 | It("should fail if the data is empty", func() { 48 | cr.Reader = fake.NewClientBuilder().WithScheme(s).WithObjects(configMap).Build() 49 | res, err := cr.Reconcile(cr.Ctx(), reconcile.Request{ 50 | NamespacedName: types.NamespacedName{ 51 | Name: configMap.Name, 52 | Namespace: configMap.Namespace, 53 | }, 54 | }) 55 | Ω(err).Should(HaveOccurred()) 56 | Ω(res.RequeueAfter).Should(Equal(time.Duration(0))) 57 | }) 58 | It("should fail if the container template does not exist", func() { 59 | configMap.Data = map[string]string{"foo": "bar"} 60 | cr.Reader = fake.NewClientBuilder().WithScheme(s).WithObjects(configMap).Build() 61 | res, err := cr.Reconcile(cr.Ctx(), reconcile.Request{ 62 | NamespacedName: types.NamespacedName{ 63 | Name: configMap.Name, 64 | Namespace: configMap.Namespace, 65 | }, 66 | }) 67 | Ω(err).Should(HaveOccurred()) 68 | Ω(res.RequeueAfter).Should(Equal(time.Duration(0))) 69 | }) 70 | It("should read the default config", func() { 71 | configMap.Data = map[string]string{cnst.ConfigKeyContainerTemplate: ""} 72 | cr.Reader = fake.NewClientBuilder().WithScheme(s).WithObjects(configMap).Build() 73 | res, err := cr.Reconcile(cr.Ctx(), reconcile.Request{ 74 | NamespacedName: types.NamespacedName{ 75 | Name: configMap.Name, 76 | Namespace: configMap.Namespace, 77 | }, 78 | }) 79 | Ω(err).ShouldNot(HaveOccurred()) 80 | Ω(res.RequeueAfter).Should(Equal(time.Duration(0))) 81 | cfg := GetCfg(cr.Ctx()) 82 | 83 | Ω(cfg.ContainerTemplate.Resources.Requests.Cpu().String()).Should(Equal(defaultPodReqCPU.String())) 84 | Ω(cfg.ContainerTemplate.Resources.Requests.Memory().String()).Should(Equal(defaultPodReqMem.String())) 85 | Ω(cfg.ContainerTemplate.Resources.Limits.Cpu().String()).Should(Equal(defaultPodMaxCPU.String())) 86 | Ω(cfg.ContainerTemplate.Resources.Limits.Memory().String()).Should(Equal(defaultPodMaxMem.String())) 87 | }) 88 | It("should fail if the container template can not be parsed", func() { 89 | configMap.Data = map[string]string{cnst.ConfigKeyContainerTemplate: ` 90 | image: quay.io/bakito/k8s-event-logger 91 | resources: 92 | requests: 93 | cpu: 111m 94 | memory: 222Mi 95 | limits: 96 | cpu: 333m 97 | memory: 444Mi 98 | `} 99 | cr.Reader = fake.NewClientBuilder().WithScheme(s).WithObjects(configMap).Build() 100 | res, err := cr.Reconcile(cr.Ctx(), reconcile.Request{ 101 | NamespacedName: types.NamespacedName{ 102 | Name: configMap.Name, 103 | Namespace: configMap.Namespace, 104 | }, 105 | }) 106 | Ω(err).ShouldNot(HaveOccurred()) 107 | Ω(res.RequeueAfter).Should(Equal(time.Duration(0))) 108 | 109 | cfg := GetCfg(cr.Ctx()) 110 | Ω(cfg.ContainerTemplate.Resources.Requests.Cpu().String()).Should(Equal("111m")) 111 | Ω(cfg.ContainerTemplate.Resources.Requests.Memory().String()).Should(Equal("222Mi")) 112 | Ω(cfg.ContainerTemplate.Resources.Limits.Cpu().String()).Should(Equal("333m")) 113 | Ω(cfg.ContainerTemplate.Resources.Limits.Memory().String()).Should(Equal("444Mi")) 114 | }) 115 | }) 116 | 117 | Context("setupEventLoggerImage", func() { 118 | var ( 119 | pod *corev1.Pod 120 | nn types.NamespacedName 121 | ) 122 | BeforeEach(func() { 123 | pod = &corev1.Pod{ObjectMeta: metav1.ObjectMeta{ 124 | Name: uuid.NewString(), 125 | Namespace: uuid.NewString(), 126 | }} 127 | nn = types.NamespacedName{ 128 | Namespace: pod.Namespace, 129 | Name: pod.Name, 130 | } 131 | }) 132 | AfterEach(func() { 133 | _ = os.Setenv(cnst.EnvEventLoggerImage, "") 134 | }) 135 | 136 | It("should evaluate the image from the env variable", func() { 137 | _ = os.Setenv(cnst.EnvEventLoggerImage, "my:image") 138 | err := cr.setupEventLoggerImage(nn) 139 | Ω(err).ShouldNot(HaveOccurred()) 140 | Ω(cr.eventLoggerImage).Should(Equal("my:image")) 141 | }) 142 | It("should fail if no pod is found", func() { 143 | cr.Reader = fake.NewClientBuilder().WithScheme(s).Build() 144 | err := cr.setupEventLoggerImage(nn) 145 | Ω(err).Should(HaveOccurred()) 146 | Ω(cr.eventLoggerImage).Should(BeEmpty()) 147 | }) 148 | It("should evaluate the image from the first operator pod container", func() { 149 | pod.Spec = corev1.PodSpec{Containers: []corev1.Container{ 150 | {Image: "my-container:image"}, 151 | }} 152 | cr.Reader = fake.NewClientBuilder().WithScheme(s).WithObjects(pod).Build() 153 | err := cr.setupEventLoggerImage(nn) 154 | Ω(err).ShouldNot(HaveOccurred()) 155 | Ω(cr.eventLoggerImage).Should(Equal("my-container:image")) 156 | }) 157 | 158 | It("should evaluate the image from the operator pod container", func() { 159 | pod.Spec = corev1.PodSpec{Containers: []corev1.Container{ 160 | {Image: "my-container:image1", Name: "something-else"}, 161 | {Image: "my-container:image2", Name: defaultContainerName}, 162 | }} 163 | cr.Reader = fake.NewClientBuilder().WithScheme(s).WithObjects(pod).Build() 164 | err := cr.setupEventLoggerImage(nn) 165 | Ω(err).ShouldNot(HaveOccurred()) 166 | Ω(cr.eventLoggerImage).Should(Equal("my-container:image2")) 167 | }) 168 | It("should fail if the container is not found", func() { 169 | pod.Spec = corev1.PodSpec{Containers: []corev1.Container{ 170 | {Image: "my-container:image1", Name: "something-else"}, 171 | {Image: "my-container:image2", Name: "not-my-container"}, 172 | }} 173 | cr.Reader = fake.NewClientBuilder().WithScheme(s).WithObjects(pod).Build() 174 | err := cr.setupEventLoggerImage(nn) 175 | Ω(err).Should(HaveOccurred()) 176 | Ω(cr.eventLoggerImage).Should(BeEmpty()) 177 | }) 178 | }) 179 | }) 180 | -------------------------------------------------------------------------------- /controllers/config/config_suite_test.go: -------------------------------------------------------------------------------- 1 | package config_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestSetup(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Config Suite") 13 | } 14 | -------------------------------------------------------------------------------- /controllers/logging/event_controller.go: -------------------------------------------------------------------------------- 1 | /* 2 | 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 logging 18 | 19 | import ( 20 | "context" 21 | "reflect" 22 | 23 | eventloggerv1 "github.com/bakito/k8s-event-logger-operator/api/v1" 24 | "github.com/fatih/structs" 25 | "github.com/go-logr/logr" 26 | corev1 "k8s.io/api/core/v1" 27 | "k8s.io/apimachinery/pkg/api/errors" 28 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 29 | "k8s.io/apimachinery/pkg/runtime" 30 | ctrl "sigs.k8s.io/controller-runtime" 31 | "sigs.k8s.io/controller-runtime/pkg/client" 32 | "sigs.k8s.io/controller-runtime/pkg/event" 33 | "sigs.k8s.io/controller-runtime/pkg/handler" 34 | "sigs.k8s.io/controller-runtime/pkg/predicate" 35 | "sigs.k8s.io/controller-runtime/pkg/reconcile" 36 | ) 37 | 38 | var eventLog = ctrl.Log.WithName("event") 39 | 40 | // Reconciler reconciles a Event object 41 | type Reconciler struct { 42 | client.Client 43 | Log logr.Logger 44 | Scheme *runtime.Scheme 45 | Config *Config 46 | // LoggerMode if enabled, the controller does only logging and no update on the custom resource 47 | LoggerMode bool 48 | } 49 | 50 | // +kubebuilder:rbac:groups=eventlogger.bakito.ch,resources=eventloggers,verbs=get;list;watch;create;update;patch;delete 51 | 52 | // Reconcile EventLogger to update the current config 53 | func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { 54 | reqLogger := r.Log.WithValues("namespace", req.Namespace, "name", req.Name) 55 | if r.Config.name == "" { 56 | r.Config.name = req.Name 57 | } 58 | 59 | reqLogger.V(2).Info("Reconciling event logger") 60 | 61 | // Fetch the EventLogger cr 62 | cr := &eventloggerv1.EventLogger{} 63 | err := r.Get(ctx, req.NamespacedName, cr) 64 | if err != nil { 65 | if errors.IsNotFound(err) { 66 | // Request object not found, could have been deleted after reconcile request. 67 | // Owned objects are automatically garbage collected. For additional cleanup logic use finalizers. 68 | // Return and don't requeue 69 | r.Config.filter = nil 70 | reqLogger.Info("cr was deleted, removing filter") 71 | return reconcile.Result{}, nil 72 | } 73 | // Error reading the object - requeue the request. 74 | return r.updateCR(ctx, cr, reqLogger, err) 75 | } 76 | 77 | needUpdate := false 78 | if !reflect.DeepEqual(r.Config.logFields, cr.Spec.LogFields) { 79 | r.Config.logFields = cr.Spec.LogFields 80 | reqLogger.WithValues("logFields", r.Config.logFields).Info("apply new log fields") 81 | needUpdate = true 82 | } 83 | 84 | newFilter := newFilter(cr.Spec) 85 | if r.Config.filter == nil || !r.Config.filter.Equals(newFilter) { 86 | r.Config.filter = newFilter 87 | reqLogger.WithValues("filter", r.Config.filter.String()).Info("apply new filter") 88 | needUpdate = true 89 | } 90 | 91 | if needUpdate { 92 | return r.updateCR(ctx, cr, reqLogger, nil) 93 | } 94 | 95 | return reconcile.Result{}, nil 96 | } 97 | 98 | func (r *Reconciler) updateCR( 99 | ctx context.Context, 100 | cr *eventloggerv1.EventLogger, 101 | logger logr.Logger, 102 | err error, 103 | ) (reconcile.Result, error) { 104 | if err != nil { 105 | logger.Error(err, "") 106 | } 107 | if r.LoggerMode { 108 | // return only, no update 109 | return reconcile.Result{}, err 110 | } 111 | cr.Apply(err) 112 | err = r.Update(ctx, cr) 113 | return reconcile.Result{}, err 114 | } 115 | 116 | type loggingPredicate struct { 117 | predicate.Funcs 118 | lastVersion string 119 | Config *Config 120 | } 121 | 122 | // Create implements Predicate 123 | func (p *loggingPredicate) Create(e event.CreateEvent) bool { 124 | if _, ok := e.Object.(*eventloggerv1.EventLogger); ok { 125 | return p.Config.matches(e.Object) 126 | } 127 | return p.logEvent(e.Object) 128 | } 129 | 130 | // Update implements Predicate 131 | func (p *loggingPredicate) Update(e event.UpdateEvent) bool { 132 | if _, ok := e.ObjectNew.(*eventloggerv1.EventLogger); ok { 133 | return p.Config.matches(e.ObjectNew) 134 | } 135 | return p.logEvent(e.ObjectNew) 136 | } 137 | 138 | // Delete implements Predicate 139 | func (p *loggingPredicate) Delete(e event.DeleteEvent) bool { 140 | if _, ok := e.Object.(*eventloggerv1.EventLogger); ok { 141 | return p.Config.matches(e.Object) 142 | } 143 | return false 144 | } 145 | 146 | func (p *loggingPredicate) logEvent(e runtime.Object) bool { 147 | if p.Config == nil || p.Config.filter == nil { 148 | return false 149 | } 150 | 151 | evt, ok := e.(*corev1.Event) 152 | if !ok { 153 | return false 154 | } 155 | if evt.ResourceVersion <= p.lastVersion { 156 | return false 157 | } 158 | p.lastVersion = evt.ResourceVersion // SA4005: 159 | 160 | if p.Config.filter.Match(evt) { 161 | var eventLogger logr.Logger 162 | if len(p.Config.logFields) == 0 { 163 | eventLogger = eventLog.WithValues( 164 | "namespace", evt.Namespace, 165 | "name", evt.Name, 166 | "reason", evt.Reason, 167 | "timestamp", evt.LastTimestamp, 168 | "type", evt.Type, 169 | "involvedObject", evt.InvolvedObject, 170 | "source", evt.Source, 171 | ) 172 | } else { 173 | m := structs.Map(evt) 174 | eventLogger = eventLog 175 | for _, lf := range p.Config.logFields { 176 | if len(lf.Path) > 0 { 177 | val, ok, err := unstructured.NestedFieldNoCopy(m, lf.Path...) 178 | if ok && err == nil { 179 | eventLogger = eventLogger.WithValues(lf.Name, val) 180 | } 181 | } else if lf.Value != nil { 182 | eventLogger = eventLogger.WithValues(lf.Name, *lf.Value) 183 | } 184 | } 185 | } 186 | 187 | eventLogger.Info(evt.Message) 188 | } 189 | return false 190 | } 191 | 192 | func getLatestRevision(ctx context.Context, cl client.Client, namespace string) (string, error) { 193 | eventList := &corev1.EventList{} 194 | opts := []client.ListOption{ 195 | client.Limit(0), 196 | client.InNamespace(namespace), 197 | } 198 | 199 | err := cl.List(ctx, eventList, opts...) 200 | if err != nil { 201 | return "", err 202 | } 203 | return eventList.ResourceVersion, nil 204 | } 205 | 206 | // SetupWithManager setup with manager 207 | func (r *Reconciler) SetupWithManager(mgr ctrl.Manager, namespace string) error { 208 | cl, err := client.New(mgr.GetConfig(), client.Options{}) 209 | if err != nil { 210 | return err 211 | } 212 | 213 | lv, err := getLatestRevision(context.Background(), cl, namespace) 214 | if err != nil { 215 | return err 216 | } 217 | return ctrl.NewControllerManagedBy(mgr). 218 | For(&eventloggerv1.EventLogger{}). 219 | Watches(&corev1.Event{}, &handler.Funcs{}). 220 | WithEventFilter(&loggingPredicate{Config: r.Config, lastVersion: lv}). 221 | Complete(r) 222 | } 223 | -------------------------------------------------------------------------------- /controllers/logging/event_controller_test.go: -------------------------------------------------------------------------------- 1 | package logging 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | 7 | v1 "github.com/bakito/k8s-event-logger-operator/api/v1" 8 | "github.com/bakito/k8s-event-logger-operator/pkg/filter" 9 | mc "github.com/bakito/k8s-event-logger-operator/pkg/mocks/client" 10 | ml "github.com/bakito/k8s-event-logger-operator/pkg/mocks/logr" 11 | "github.com/go-logr/logr" 12 | . "github.com/onsi/ginkgo/v2" 13 | . "github.com/onsi/gomega" 14 | gm "go.uber.org/mock/gomock" 15 | corev1 "k8s.io/api/core/v1" 16 | "k8s.io/apimachinery/pkg/api/errors" 17 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 18 | "k8s.io/apimachinery/pkg/runtime" 19 | "k8s.io/apimachinery/pkg/runtime/schema" 20 | "k8s.io/apimachinery/pkg/types" 21 | "k8s.io/client-go/kubernetes/scheme" 22 | "k8s.io/utils/ptr" 23 | ctrl "sigs.k8s.io/controller-runtime" 24 | "sigs.k8s.io/controller-runtime/pkg/client" 25 | "sigs.k8s.io/controller-runtime/pkg/event" 26 | "sigs.k8s.io/controller-runtime/pkg/reconcile" 27 | ) 28 | 29 | const ( 30 | testNamespace = "eventlogger-operator" 31 | testName = "eventlogger-operator-name" 32 | ) 33 | 34 | var _ = Describe("Logging", func() { 35 | var ( 36 | mockCtrl *gm.Controller 37 | ctx context.Context 38 | cl *mc.MockClient 39 | ) 40 | 41 | BeforeEach(func() { 42 | mockCtrl = gm.NewController(GinkgoT()) 43 | cl = mc.NewMockClient(mockCtrl) 44 | ctx = context.Background() 45 | }) 46 | AfterEach(func() { 47 | defer mockCtrl.Finish() 48 | }) 49 | 50 | Context("Reconcile", func() { 51 | var ( 52 | s *runtime.Scheme 53 | r *Reconciler 54 | req reconcile.Request 55 | ) 56 | 57 | BeforeEach(func() { 58 | s = scheme.Scheme 59 | Ω(v1.SchemeBuilder.AddToScheme(s)).ShouldNot(HaveOccurred()) 60 | r = &Reconciler{ 61 | Client: cl, 62 | Log: ctrl.Log.WithName("controllers").WithName("Event"), 63 | Scheme: s, 64 | Config: &Config{}, 65 | LoggerMode: false, 66 | } 67 | req = reconcile.Request{ 68 | NamespacedName: types.NamespacedName{ 69 | Name: "foo", 70 | Namespace: testNamespace, 71 | }, 72 | } 73 | }) 74 | 75 | Context("Update", func() { 76 | It("should update an existing if LoggerMode is disabled", func() { 77 | r.LoggerMode = false 78 | cl.EXPECT().Get(gm.Any(), gm.Any(), gm.Any()) 79 | cl.EXPECT().Update(gm.Any(), gm.Any(), gm.Any()) 80 | _, err := r.Reconcile(ctx, req) 81 | Ω(err).ShouldNot(HaveOccurred()) 82 | Ω(r.Config.filter).ShouldNot(BeNil()) 83 | }) 84 | It("should not update an existing if LoggerMode is enabled", func() { 85 | r.LoggerMode = true 86 | cl.EXPECT().Get(gm.Any(), gm.Any(), gm.Any()) 87 | _, err := r.Reconcile(ctx, req) 88 | Ω(err).ShouldNot(HaveOccurred()) 89 | Ω(r.Config.filter).ShouldNot(BeNil()) 90 | }) 91 | }) 92 | 93 | It("should do noting if not found", func() { 94 | cl.EXPECT(). 95 | Get(gm.Any(), gm.Any(), gm.Any()). 96 | Return(errors.NewNotFound(v1.GroupVersion.WithResource("").GroupResource(), "")) 97 | _, err := r.Reconcile(ctx, req) 98 | Ω(err).ShouldNot(HaveOccurred()) 99 | }) 100 | }) 101 | 102 | Context("logEvent", func() { 103 | var mockSink *ml.MockLogSink 104 | 105 | BeforeEach(func() { 106 | mockSink = ml.NewMockLogSink(mockCtrl) 107 | mockSink.EXPECT().Init(gm.Any()) 108 | mockSink.EXPECT().Enabled(gm.Any()).AnyTimes().Return(true) 109 | eventLog = logr.New(mockSink) 110 | }) 111 | 112 | It("should log nothing", func() { 113 | mockSink.EXPECT().WithValues().Times(0) 114 | 115 | lp := &loggingPredicate{} 116 | lp.logEvent(&corev1.Event{}) 117 | }) 118 | It("should log nothing if object is not an event", func() { 119 | mockSink.EXPECT().WithValues().Times(0) 120 | 121 | lp := &loggingPredicate{ 122 | lastVersion: "2", 123 | Config: &Config{filter: filter.Always}, 124 | } 125 | 126 | lp.logEvent(&corev1.Pod{}) 127 | }) 128 | It("should log nothing if resource version does not match", func() { 129 | mockSink.EXPECT().WithValues().Times(0) 130 | 131 | lp := &loggingPredicate{ 132 | lastVersion: "2", 133 | Config: &Config{filter: filter.Always}, 134 | } 135 | 136 | lp.logEvent(&corev1.Event{ 137 | ObjectMeta: metav1.ObjectMeta{ 138 | ResourceVersion: "1", 139 | }, 140 | }) 141 | }) 142 | It("should log one message with 14 fields", func() { 143 | childSink := ml.NewMockLogSink(mockCtrl) 144 | childSink.EXPECT().Init(gm.Any()).AnyTimes() 145 | childSink.EXPECT().Enabled(gm.Any()).AnyTimes().Return(true) 146 | mockSink.EXPECT().WithValues(repeat(gm.Any(), 14)...).Times(1).Return(childSink) 147 | childSink.EXPECT().Info(gm.Any(), gm.Any()).Times(1) 148 | 149 | lp := &loggingPredicate{ 150 | lastVersion: "2", 151 | Config: &Config{filter: filter.Always}, 152 | } 153 | 154 | lp.logEvent(&corev1.Event{ 155 | ObjectMeta: metav1.ObjectMeta{ 156 | ResourceVersion: "3", 157 | }, 158 | }) 159 | }) 160 | It("should log one message with custom fields", func() { 161 | childSink := ml.NewMockLogSink(mockCtrl) 162 | childSink.EXPECT().Init(gm.Any()).AnyTimes() 163 | childSink.EXPECT().Enabled(gm.Any()).AnyTimes().Return(true) 164 | mockSink.EXPECT().WithValues("type", "test-type").Times(1).Return(childSink) 165 | childSink.EXPECT().WithValues("name", "test-io-name").Times(1).Return(childSink) 166 | childSink.EXPECT().WithValues("kind", "test-kind").Times(1).Return(childSink) 167 | childSink.EXPECT().WithValues("reason", "").Times(1).Return(childSink) 168 | childSink.EXPECT().Info(gm.Any(), gm.Any()).Times(1) 169 | 170 | lp := &loggingPredicate{ 171 | Config: &Config{ 172 | filter: filter.Always, 173 | logFields: []v1.LogField{ 174 | {Name: "type", Path: []string{"Type"}}, 175 | {Name: "name", Path: []string{"InvolvedObject", "Name"}}, 176 | {Name: "kind", Path: []string{"InvolvedObject", "Kind"}}, 177 | {Name: "reason", Path: []string{"Reason"}}, 178 | }, 179 | }, 180 | } 181 | 182 | lp.logEvent(&corev1.Event{ 183 | ObjectMeta: metav1.ObjectMeta{ 184 | ResourceVersion: "3", 185 | Name: "test-event-name", 186 | }, 187 | Type: "test-type", 188 | InvolvedObject: corev1.ObjectReference{ 189 | Kind: "test-kind", 190 | Name: "test-io-name", 191 | }, 192 | Reason: "", 193 | }) 194 | }) 195 | 196 | DescribeTable("the > inequality", 197 | func(config v1.EventLoggerSpec, event corev1.Event, expected bool, description string) { 198 | data := &sld{config, event, expected, description} 199 | lp := &loggingPredicate{Config: &Config{filter: newFilter(data.Config)}} 200 | 201 | _, err := json.Marshal(&data) 202 | Ω(err).ShouldNot(HaveOccurred()) 203 | Ω(lp.Config.filter.Match(&data.Event)).Should(Equal(expected)) 204 | Ω(lp.Config.filter.String()).Should(Equal(data.Description)) 205 | }, 206 | Entry("1", 207 | v1.EventLoggerSpec{}, 208 | corev1.Event{InvolvedObject: corev1.ObjectReference{Kind: "Pod"}}, 209 | true, 210 | "true", 211 | ), 212 | Entry("2", 213 | v1.EventLoggerSpec{EventTypes: []string{}}, 214 | corev1.Event{InvolvedObject: corev1.ObjectReference{Kind: "Pod"}, Type: "Normal"}, 215 | true, 216 | "true", 217 | ), 218 | Entry("3", 219 | v1.EventLoggerSpec{EventTypes: []string{"Normal"}}, 220 | corev1.Event{InvolvedObject: corev1.ObjectReference{Kind: "Pod"}, Type: "Normal"}, 221 | true, 222 | "( EventType in [Normal] )", 223 | ), 224 | Entry("4", 225 | v1.EventLoggerSpec{EventTypes: []string{"Normal"}}, 226 | corev1.Event{InvolvedObject: corev1.ObjectReference{Kind: "Pod"}, Type: "Warning"}, 227 | false, 228 | "( EventType in [Normal] )", 229 | ), 230 | Entry("5", 231 | 232 | v1.EventLoggerSpec{Kinds: []v1.Kind{{Name: "Pod"}}}, 233 | corev1.Event{InvolvedObject: corev1.ObjectReference{Kind: "Pod"}}, 234 | true, 235 | "( ( ( Kind == 'Pod' ) ) )", 236 | ), 237 | Entry("6", 238 | 239 | v1.EventLoggerSpec{Kinds: []v1.Kind{{Name: "ConfigMap"}}}, 240 | corev1.Event{InvolvedObject: corev1.ObjectReference{Kind: "Pod"}}, 241 | false, 242 | "( ( ( Kind == 'ConfigMap' ) ) )", 243 | ), 244 | Entry("7", 245 | 246 | v1.EventLoggerSpec{Kinds: []v1.Kind{{Name: "Pod", EventTypes: []string{}}}}, 247 | corev1.Event{InvolvedObject: corev1.ObjectReference{Kind: "Pod"}}, 248 | true, 249 | "( ( ( Kind == 'Pod' ) ) )", 250 | ), 251 | Entry( 252 | "8", 253 | 254 | v1.EventLoggerSpec{ 255 | Kinds: []v1.Kind{{Name: "Pod", EventTypes: []string{}, Reasons: []string{"Created", "Started"}}}, 256 | }, 257 | corev1.Event{InvolvedObject: corev1.ObjectReference{Kind: "Pod"}, Reason: "Created"}, 258 | true, 259 | "( ( ( Kind == 'Pod' AND Reason in [Created, Started] ) ) )", 260 | ), 261 | Entry( 262 | "9", 263 | 264 | v1.EventLoggerSpec{ 265 | Kinds: []v1.Kind{{Name: "Pod", EventTypes: []string{}, Reasons: []string{"Created", "Started"}}}, 266 | }, 267 | corev1.Event{InvolvedObject: corev1.ObjectReference{Kind: "Pod"}, Reason: "Killing"}, 268 | false, 269 | "( ( ( Kind == 'Pod' AND Reason in [Created, Started] ) ) )", 270 | ), 271 | Entry( 272 | "10", 273 | v1.EventLoggerSpec{ 274 | Kinds: []v1.Kind{{Name: "Application", APIGroup: ptr.To("argoproj.io"), EventTypes: []string{}}}, 275 | }, 276 | corev1.Event{ 277 | InvolvedObject: corev1.ObjectReference{ 278 | Kind: "Application", 279 | APIVersion: schema.GroupVersion{Group: "argoproj.io", Version: "v1alpha1"}.String(), 280 | }, 281 | }, 282 | true, 283 | "( ( ( Kind == 'Application' AND APIGroup == 'argoproj.io' ) ) )", 284 | ), 285 | Entry( 286 | "11", 287 | v1.EventLoggerSpec{ 288 | Kinds: []v1.Kind{{Name: "Application", APIGroup: ptr.To("argoproj.io"), EventTypes: []string{}}}, 289 | }, 290 | corev1.Event{ 291 | InvolvedObject: corev1.ObjectReference{ 292 | Kind: "Application", 293 | APIVersion: schema.GroupVersion{Group: "app.k8s.io", Version: "v1beta1"}.String(), 294 | }, 295 | }, 296 | false, 297 | "( ( ( Kind == 'Application' AND APIGroup == 'argoproj.io' ) ) )", 298 | ), 299 | Entry("12", 300 | 301 | v1.EventLoggerSpec{Kinds: []v1.Kind{{Name: "Pod"}}, EventTypes: []string{"Normal"}}, 302 | corev1.Event{InvolvedObject: corev1.ObjectReference{Kind: "Pod"}, Type: "Normal"}, 303 | true, 304 | "( EventType in [Normal] OR ( ( Kind == 'Pod' AND EventType in [Normal] ) ) )", 305 | ), 306 | Entry("13", 307 | 308 | v1.EventLoggerSpec{Kinds: []v1.Kind{{Name: "Pod"}}, EventTypes: []string{"Warning"}}, 309 | corev1.Event{InvolvedObject: corev1.ObjectReference{Kind: "Pod"}, Type: "Normal"}, 310 | false, 311 | "( EventType in [Warning] OR ( ( Kind == 'Pod' AND EventType in [Warning] ) ) )", 312 | ), 313 | Entry("14", 314 | 315 | v1.EventLoggerSpec{Kinds: []v1.Kind{{Name: "Pod", EventTypes: []string{"Normal"}}}}, 316 | corev1.Event{InvolvedObject: corev1.ObjectReference{Kind: "Pod"}, Type: "Normal"}, 317 | true, 318 | "( ( ( Kind == 'Pod' AND EventType in [Normal] ) ) )", 319 | ), 320 | Entry("15", 321 | 322 | v1.EventLoggerSpec{Kinds: []v1.Kind{{Name: "Pod", EventTypes: []string{"Warning"}}}}, 323 | corev1.Event{InvolvedObject: corev1.ObjectReference{Kind: "Pod"}, Type: "Normal"}, 324 | false, 325 | "( ( ( Kind == 'Pod' AND EventType in [Warning] ) ) )", 326 | ), 327 | Entry( 328 | "16", 329 | 330 | v1.EventLoggerSpec{ 331 | Kinds: []v1.Kind{{Name: "Pod", EventTypes: []string{"Normal"}}}, 332 | EventTypes: []string{"Warning"}, 333 | }, 334 | corev1.Event{InvolvedObject: corev1.ObjectReference{Kind: "Pod"}, Type: "Normal"}, 335 | true, 336 | "( EventType in [Warning] OR ( ( Kind == 'Pod' AND EventType in [Normal] ) ) )", 337 | ), 338 | Entry("17", 339 | v1.EventLoggerSpec{Kinds: []v1.Kind{{Name: "Pod", MatchingPatterns: []string{".*message.*"}}}}, 340 | corev1.Event{InvolvedObject: corev1.ObjectReference{Kind: "Pod"}, Message: "This is a test message"}, 341 | true, 342 | "( ( ( Kind == 'Pod' AND ( false XOR ( Message matches /.*message.*/ ) ) ) ) )", 343 | ), 344 | Entry("18", 345 | v1.EventLoggerSpec{Kinds: []v1.Kind{{Name: "Pod", MatchingPatterns: []string{".*Message.*"}}}}, 346 | corev1.Event{InvolvedObject: corev1.ObjectReference{Kind: "Pod"}, Message: "This is a test message"}, 347 | false, 348 | "( ( ( Kind == 'Pod' AND ( false XOR ( Message matches /.*Message.*/ ) ) ) ) )", 349 | ), 350 | Entry( 351 | "19", 352 | v1.EventLoggerSpec{ 353 | Kinds: []v1.Kind{ 354 | {Name: "Pod", MatchingPatterns: []string{".*message.*"}, SkipOnMatch: ptr.To(false)}, 355 | }, 356 | }, 357 | corev1.Event{InvolvedObject: corev1.ObjectReference{Kind: "Pod"}, Message: "This is a test message"}, 358 | true, 359 | "( ( ( Kind == 'Pod' AND ( false XOR ( Message matches /.*message.*/ ) ) ) ) )", 360 | ), 361 | Entry( 362 | "20", 363 | v1.EventLoggerSpec{ 364 | Kinds: []v1.Kind{ 365 | {Name: "Pod", MatchingPatterns: []string{".*Message.*"}, SkipOnMatch: ptr.To(false)}, 366 | }, 367 | }, 368 | corev1.Event{InvolvedObject: corev1.ObjectReference{Kind: "Pod"}, Message: "This is a test message"}, 369 | false, 370 | "( ( ( Kind == 'Pod' AND ( false XOR ( Message matches /.*Message.*/ ) ) ) ) )", 371 | ), 372 | Entry( 373 | "21", 374 | v1.EventLoggerSpec{ 375 | Kinds: []v1.Kind{ 376 | {Name: "Pod", MatchingPatterns: []string{".*message.*"}, SkipOnMatch: ptr.To(true)}, 377 | }, 378 | }, 379 | corev1.Event{InvolvedObject: corev1.ObjectReference{Kind: "Pod"}, Message: "This is a test message"}, 380 | false, 381 | "( ( ( Kind == 'Pod' AND ( true XOR ( Message matches /.*message.*/ ) ) ) ) )", 382 | ), 383 | Entry( 384 | "22", 385 | v1.EventLoggerSpec{ 386 | Kinds: []v1.Kind{ 387 | {Name: "Pod", MatchingPatterns: []string{".*Message.*"}, SkipOnMatch: ptr.To(true)}, 388 | }, 389 | }, 390 | corev1.Event{InvolvedObject: corev1.ObjectReference{Kind: "Pod"}, Message: "This is a test message"}, 391 | true, 392 | "( ( ( Kind == 'Pod' AND ( true XOR ( Message matches /.*Message.*/ ) ) ) ) )", 393 | ), 394 | Entry( 395 | "23", 396 | 397 | v1.EventLoggerSpec{ 398 | Kinds: []v1.Kind{ 399 | {Name: "Pod", EventTypes: []string{}, SkipReasons: []string{"Created", "Started"}}, 400 | }, 401 | }, 402 | corev1.Event{InvolvedObject: corev1.ObjectReference{Kind: "Pod"}, Reason: "Created"}, 403 | false, 404 | "( ( ( Kind == 'Pod' AND Reason NOT in [Created, Started] ) ) )", 405 | ), 406 | Entry( 407 | "24", 408 | 409 | v1.EventLoggerSpec{ 410 | Kinds: []v1.Kind{ 411 | {Name: "Pod", EventTypes: []string{}, SkipReasons: []string{"Created", "Started"}}, 412 | }, 413 | }, 414 | corev1.Event{InvolvedObject: corev1.ObjectReference{Kind: "Pod"}, Reason: "Killing"}, 415 | true, 416 | "( ( ( Kind == 'Pod' AND Reason NOT in [Created, Started] ) ) )", 417 | ), 418 | Entry("25", 419 | 420 | v1.EventLoggerSpec{Kinds: []v1.Kind{{ 421 | Name: "Pod", EventTypes: []string{}, 422 | SkipReasons: []string{"Created"}, 423 | Reasons: []string{"Created"}, 424 | }}}, 425 | corev1.Event{InvolvedObject: corev1.ObjectReference{Kind: "Pod"}, Reason: "Created"}, 426 | false, 427 | "( ( ( Kind == 'Pod' AND Reason NOT in [Created] AND Reason in [Created] ) ) )", 428 | ), 429 | ) 430 | }) 431 | 432 | Context("contains", func() { 433 | It("should contain the value", func() { 434 | Ω(contains([]string{"abc", "xyz"}, "abc")).Should(BeTrue()) 435 | Ω(contains([]string{"abc", "xyz"}, "xyz")).Should(BeTrue()) 436 | }) 437 | It("should not contain the value", func() { 438 | Ω(contains([]string{"abc", "xyz"}, "xxx")).Should(BeFalse()) 439 | }) 440 | }) 441 | 442 | Context("loggingPredicate", func() { 443 | var ( 444 | lp *loggingPredicate 445 | el *v1.EventLogger 446 | ) 447 | BeforeEach(func() { 448 | lp = &loggingPredicate{ 449 | Config: &Config{ 450 | watchNamespace: testNamespace, 451 | podNamespace: "", 452 | name: testName, 453 | }, 454 | } 455 | el = &v1.EventLogger{ 456 | ObjectMeta: metav1.ObjectMeta{ 457 | Namespace: testNamespace, 458 | Name: testName, 459 | }, 460 | } 461 | }) 462 | Context("Create", func() { 463 | It("should match for reconciling with watchNamespace", func() { 464 | Ω(lp.Create(event.CreateEvent{Object: el})).Should(BeTrue()) 465 | }) 466 | It("should not match for reconciling with watchNamespace", func() { 467 | el.Name = "foo" 468 | Ω(lp.Create(event.CreateEvent{Object: el})).Should(BeFalse()) 469 | }) 470 | It("should match for reconciling with podNamespace", func() { 471 | lp.Config.watchNamespace = "" 472 | lp.Config.podNamespace = testNamespace 473 | Ω(lp.Create(event.CreateEvent{Object: el})).Should(BeTrue()) 474 | }) 475 | It("should match for reconciling with podNamespace", func() { 476 | el.Name = "foo" 477 | lp.Config.watchNamespace = "" 478 | lp.Config.podNamespace = testNamespace 479 | Ω(lp.Create(event.CreateEvent{Object: el})).Should(BeFalse()) 480 | }) 481 | It("should not reconcile or log for another object", func() { 482 | pod := &corev1.Pod{} 483 | Ω(lp.Create(event.CreateEvent{Object: pod})).Should(BeFalse()) 484 | }) 485 | }) 486 | Context("Update", func() { 487 | It("should match for reconciling with watchNamespace", func() { 488 | Ω(lp.Update(event.UpdateEvent{ObjectNew: el})).Should(BeTrue()) 489 | }) 490 | It("should not match for reconciling with watchNamespace", func() { 491 | el.Name = "foo" 492 | Ω(lp.Update(event.UpdateEvent{ObjectNew: el})).Should(BeFalse()) 493 | }) 494 | It("should match for reconciling with podNamespace", func() { 495 | lp.Config.watchNamespace = "" 496 | lp.Config.podNamespace = testNamespace 497 | Ω(lp.Update(event.UpdateEvent{ObjectNew: el})).Should(BeTrue()) 498 | }) 499 | It("should match for reconciling with podNamespace", func() { 500 | el.Name = "foo" 501 | lp.Config.watchNamespace = "" 502 | lp.Config.podNamespace = testNamespace 503 | Ω(lp.Update(event.UpdateEvent{ObjectNew: el})).Should(BeFalse()) 504 | }) 505 | It("should not reconcile or log for another object", func() { 506 | pod := &corev1.Pod{} 507 | Ω(lp.Update(event.UpdateEvent{ObjectNew: pod})).Should(BeFalse()) 508 | }) 509 | }) 510 | Context("Delete", func() { 511 | It("should match for reconciling with watchNamespace", func() { 512 | Ω(lp.Delete(event.DeleteEvent{Object: el})).Should(BeTrue()) 513 | }) 514 | It("should not match for reconciling with watchNamespace", func() { 515 | el.Name = "foo" 516 | Ω(lp.Delete(event.DeleteEvent{Object: el})).Should(BeFalse()) 517 | }) 518 | It("should match for reconciling with podNamespace", func() { 519 | lp.Config.watchNamespace = "" 520 | lp.Config.podNamespace = testNamespace 521 | Ω(lp.Delete(event.DeleteEvent{Object: el})).Should(BeTrue()) 522 | }) 523 | It("should match for reconciling with podNamespace", func() { 524 | el.Name = "foo" 525 | lp.Config.watchNamespace = "" 526 | lp.Config.podNamespace = testNamespace 527 | Ω(lp.Delete(event.DeleteEvent{Object: el})).Should(BeFalse()) 528 | }) 529 | It("should not reconcile or log for another object", func() { 530 | pod := &corev1.Pod{} 531 | Ω(lp.Delete(event.DeleteEvent{Object: pod})).Should(BeFalse()) 532 | }) 533 | }) 534 | }) 535 | Context("getLatestRevision", func() { 536 | It("should find the last revision", func() { 537 | cl.EXPECT(). 538 | List(gm.Any(), gm.Any(), gm.Any()). 539 | Do(func(_ context.Context, el *corev1.EventList, _ ...client.ListOption) { 540 | el.ResourceVersion = "3" 541 | }) 542 | rev, err := getLatestRevision(ctx, cl, "") 543 | Ω(err).ShouldNot(HaveOccurred()) 544 | Ω(rev).Should(Equal("3")) 545 | }) 546 | }) 547 | }) 548 | 549 | type sld struct { 550 | Config v1.EventLoggerSpec `json:"config"` 551 | Event corev1.Event `json:"event"` 552 | Expected bool `json:"expected"` 553 | Description string `json:"description"` 554 | } 555 | 556 | func repeat(m gm.Matcher, times int) []interface{} { 557 | var list []interface{} 558 | for i := 0; i < times; i++ { 559 | list = append(list, m) 560 | } 561 | return list 562 | } 563 | -------------------------------------------------------------------------------- /controllers/logging/logging_suite_test.go: -------------------------------------------------------------------------------- 1 | package logging_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestLogging(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Logging Suite") 13 | } 14 | -------------------------------------------------------------------------------- /controllers/logging/types.go: -------------------------------------------------------------------------------- 1 | package logging 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strings" 7 | 8 | eventloggerv1 "github.com/bakito/k8s-event-logger-operator/api/v1" 9 | "github.com/bakito/k8s-event-logger-operator/pkg/filter" 10 | corev1 "k8s.io/api/core/v1" 11 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 12 | "k8s.io/utils/ptr" 13 | ) 14 | 15 | func newFilter(c eventloggerv1.EventLoggerSpec) filter.Filter { 16 | filters := filter.Slice{} 17 | 18 | if len(c.EventTypes) > 0 { 19 | filters = append(filters, filter.New(func(e *corev1.Event) bool { 20 | return contains(c.EventTypes, e.Type) 21 | }, fmt.Sprintf("EventType in [%s]", strings.Join(c.EventTypes, ", ")))) 22 | } 23 | 24 | if len(c.Kinds) > 0 { 25 | filterForKinds := filter.Slice{} 26 | for _, k := range c.Kinds { 27 | if len(k.EventTypes) == 0 { 28 | k.EventTypes = c.EventTypes 29 | } 30 | 31 | filterForKinds = append(filterForKinds, newFilterForKind(k)) 32 | } 33 | 34 | filters = append(filters, filterForKinds.Any()) 35 | } 36 | 37 | if len(filters) == 0 { 38 | return filter.Always 39 | } 40 | 41 | return filters.Any() 42 | } 43 | 44 | func newFilterForKind(k eventloggerv1.Kind) filter.Filter { 45 | filters := filter.Slice{} 46 | 47 | filters = append(filters, filter.New(func(e *corev1.Event) bool { 48 | return k.Name == e.InvolvedObject.Kind 49 | }, fmt.Sprintf("Kind == '%s'", k.Name))) 50 | 51 | if k.APIGroup != nil { 52 | filters = append(filters, filter.New(func(e *corev1.Event) bool { 53 | return *k.APIGroup == e.InvolvedObject.GroupVersionKind().Group 54 | }, fmt.Sprintf("APIGroup == '%s'", *k.APIGroup))) 55 | } 56 | 57 | if len(k.EventTypes) > 0 { 58 | filters = append(filters, filter.New(func(e *corev1.Event) bool { 59 | return contains(k.EventTypes, e.Type) 60 | }, fmt.Sprintf("EventType in [%s]", strings.Join(k.EventTypes, ", ")))) 61 | } 62 | 63 | if len(k.SkipReasons) > 0 { 64 | filters = append(filters, filter.New(func(e *corev1.Event) bool { 65 | return !contains(k.SkipReasons, e.Reason) 66 | }, fmt.Sprintf("Reason NOT in [%s]", strings.Join(k.SkipReasons, ", ")))) 67 | } 68 | 69 | if len(k.Reasons) > 0 { 70 | filters = append(filters, filter.New(func(e *corev1.Event) bool { 71 | return contains(k.Reasons, e.Reason) 72 | }, fmt.Sprintf("Reason in [%s]", strings.Join(k.Reasons, ", ")))) 73 | } 74 | 75 | if k.MatchingPatterns != nil { 76 | filters = append(filters, newFilterForMatchingPatterns(k.MatchingPatterns, ptr.Deref(k.SkipOnMatch, false))) 77 | } 78 | 79 | return filters.All() 80 | } 81 | 82 | func newFilterForMatchingPatterns(patterns []string, skipOnMatch bool) filter.Filter { 83 | filters := filter.Slice{} 84 | for _, mp := range patterns { 85 | matcher := regexp.MustCompile(mp) 86 | filters = append(filters, filter.New(func(e *corev1.Event) bool { 87 | return matcher.Match([]byte(e.Message)) 88 | }, fmt.Sprintf("Message matches /%s/", mp))) 89 | } 90 | 91 | f := filters.Any() 92 | return filter.New(func(e *corev1.Event) bool { 93 | return skipOnMatch != f.Match(e) 94 | }, fmt.Sprintf("( %v XOR %s )", skipOnMatch, f.String())) 95 | } 96 | 97 | // ConfigFor get config for namespace and name 98 | func ConfigFor(name, podNamespace, watchNamespace string) *Config { 99 | return &Config{ 100 | name: name, 101 | podNamespace: podNamespace, 102 | watchNamespace: watchNamespace, 103 | } 104 | } 105 | 106 | // Config event config 107 | type Config struct { 108 | podNamespace string 109 | watchNamespace string 110 | name string 111 | logFields []eventloggerv1.LogField 112 | filter filter.Filter 113 | } 114 | 115 | func (c Config) matches(meta metav1.Object) bool { 116 | if c.watchNamespace == "" { 117 | return c.podNamespace == meta.GetNamespace() && (c.name == meta.GetName()) 118 | } 119 | return c.watchNamespace == meta.GetNamespace() && (c.name == meta.GetName()) 120 | } 121 | 122 | // contains check if a string in a []string exists 123 | func contains(slice []string, str string) bool { 124 | for _, v := range slice { 125 | if v == str { 126 | return true 127 | } 128 | } 129 | return false 130 | } 131 | -------------------------------------------------------------------------------- /controllers/logging/types_test.go: -------------------------------------------------------------------------------- 1 | package logging 2 | 3 | import ( 4 | "github.com/google/uuid" 5 | . "github.com/onsi/ginkgo/v2" 6 | . "github.com/onsi/gomega" 7 | ) 8 | 9 | var _ = Describe("Logging", func() { 10 | Context("ConfigFor", func() { 11 | It("should create a correct config", func() { 12 | name := uuid.NewString() 13 | podNs := uuid.NewString() 14 | watchNs := uuid.NewString() 15 | cfg := ConfigFor(name, podNs, watchNs) 16 | Ω(cfg.name).Should(Equal(name)) 17 | Ω(cfg.podNamespace).Should(Equal(podNs)) 18 | Ω(cfg.watchNamespace).Should(Equal(watchNs)) 19 | }) 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /controllers/setup/eventlogger_controller.go: -------------------------------------------------------------------------------- 1 | /* 2 | 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 setup 18 | 19 | import ( 20 | "context" 21 | 22 | eventloggerv1 "github.com/bakito/k8s-event-logger-operator/api/v1" 23 | "github.com/bakito/k8s-event-logger-operator/version" 24 | "github.com/go-logr/logr" 25 | corev1 "k8s.io/api/core/v1" 26 | rbacv1 "k8s.io/api/rbac/v1" 27 | "k8s.io/apimachinery/pkg/api/errors" 28 | "k8s.io/apimachinery/pkg/runtime" 29 | ctrl "sigs.k8s.io/controller-runtime" 30 | "sigs.k8s.io/controller-runtime/pkg/client" 31 | "sigs.k8s.io/controller-runtime/pkg/reconcile" 32 | ) 33 | 34 | var gracePeriod int64 35 | 36 | // Reconciler reconciles a Pod object 37 | type Reconciler struct { 38 | client.Client 39 | Log logr.Logger 40 | Scheme *runtime.Scheme 41 | 42 | Config context.Context 43 | } 44 | 45 | // +kubebuilder:rbac:groups=eventlogger.bakito.ch,resources=eventloggers,verbs=get;list;watch;create;update;patch;delete 46 | 47 | // Reconcile EventLogger to setup event logger pods 48 | func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { 49 | reqLogger := r.Log.WithValues("namespace", req.Namespace, "name", req.Name) 50 | 51 | // Fetch the EventLogger cr 52 | cr := &eventloggerv1.EventLogger{} 53 | err := r.Get(ctx, req.NamespacedName, cr) 54 | if err != nil { 55 | if errors.IsNotFound(err) { 56 | // Request object not found, could have been deleted after reconcile req. 57 | // Owned objects are automatically garbage collected. For additional cleanup logic use finalizers. 58 | // Return and don't requeue 59 | return reconcile.Result{}, nil 60 | } 61 | // Error reading the object - requeue the req. 62 | return r.updateCR(ctx, cr, reqLogger, err) 63 | } 64 | 65 | if err = cr.Spec.Validate(); err != nil { 66 | return r.updateCR(ctx, cr, reqLogger, err) 67 | } 68 | 69 | saccChanged, roleChanged, rbChanged, err := r.setupRbac(ctx, cr) 70 | if err != nil { 71 | return r.updateCR(ctx, cr, reqLogger, err) 72 | } 73 | 74 | // Define a new Pod object 75 | pod := r.podForCR(cr) 76 | 77 | // set owner reference for pod 78 | if err := ctrl.SetControllerReference(cr, pod, r.Scheme); err != nil { 79 | return r.updateCR(ctx, cr, reqLogger, err) 80 | } 81 | 82 | // Check if this Pod already exists 83 | podChanged, err := r.createOrReplacePod(ctx, cr, pod, reqLogger) 84 | if err != nil { 85 | return r.updateCR(ctx, cr, reqLogger, err) 86 | } 87 | 88 | if cr.HasChanged() || saccChanged || roleChanged || rbChanged || podChanged { 89 | reqLogger.Info("Reconciling event logger") 90 | return r.updateCR(ctx, cr, reqLogger, nil) 91 | } 92 | 93 | return reconcile.Result{}, nil 94 | } 95 | 96 | func (r *Reconciler) updateCR( 97 | ctx context.Context, 98 | cr *eventloggerv1.EventLogger, 99 | logger logr.Logger, 100 | err error, 101 | ) (reconcile.Result, error) { 102 | if err != nil { 103 | logger.Error(err, "") 104 | } 105 | cr.Apply(err) 106 | cr.Status.Hash = cr.Spec.Hash() 107 | cr.Status.OperatorVersion = version.Version 108 | err = r.Update(ctx, cr) 109 | return reconcile.Result{}, err 110 | } 111 | 112 | func (r *Reconciler) saveDelete(ctx context.Context, obj client.Object) error { 113 | err := r.Delete(ctx, obj) 114 | if err != nil { 115 | if !errors.IsNotFound(err) { 116 | return err 117 | } 118 | } 119 | return nil 120 | } 121 | 122 | func loggerName(cr *eventloggerv1.EventLogger) string { 123 | return "event-logger-" + cr.Name 124 | } 125 | 126 | func podChanged(old, new *corev1.Pod) bool { 127 | if old.Spec.ServiceAccountName != new.Spec.ServiceAccountName { 128 | return true 129 | } 130 | if len(old.Spec.Containers) > 0 && len(new.Spec.Containers) > 0 && 131 | old.Spec.Containers[0].Image != new.Spec.Containers[0].Image { 132 | return true 133 | } 134 | 135 | return podEnv(old, "WATCH_NAMESPACE") != podEnv(new, "WATCH_NAMESPACE") 136 | } 137 | 138 | func podEnv(pod *corev1.Pod, name string) string { 139 | for _, env := range pod.Spec.Containers[0].Env { 140 | if env.Name == name { 141 | return env.Value 142 | } 143 | } 144 | return "N/A" 145 | } 146 | 147 | // SetupWithManager setup with manager 148 | func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error { 149 | return ctrl.NewControllerManagedBy(mgr). 150 | For(&eventloggerv1.EventLogger{}). 151 | Owns(&corev1.Pod{}). 152 | Owns(&corev1.ServiceAccount{}). 153 | Owns(&rbacv1.Role{}). 154 | Owns(&rbacv1.RoleBinding{}). 155 | Complete(r) 156 | } 157 | -------------------------------------------------------------------------------- /controllers/setup/eventlogger_controller_pod.go: -------------------------------------------------------------------------------- 1 | /* 2 | 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 setup 18 | 19 | import ( 20 | "context" 21 | "flag" 22 | "fmt" 23 | 24 | eventloggerv1 "github.com/bakito/k8s-event-logger-operator/api/v1" 25 | "github.com/bakito/k8s-event-logger-operator/controllers/config" 26 | cnst "github.com/bakito/k8s-event-logger-operator/pkg/constants" 27 | "github.com/go-logr/logr" 28 | corev1 "k8s.io/api/core/v1" 29 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 30 | "sigs.k8s.io/controller-runtime/pkg/client" 31 | "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" 32 | ) 33 | 34 | const ( 35 | labelComponent = "app.kubernetes.io/component" 36 | labelManagedBy = "app.kubernetes.io/managed-by" 37 | ) 38 | 39 | func (r *Reconciler) createOrReplacePod(ctx context.Context, cr *eventloggerv1.EventLogger, pod *corev1.Pod, 40 | reqLogger logr.Logger) (bool, error, 41 | ) { 42 | // current labels 43 | labels := make(map[string]string) 44 | applyDefaultLabels(cr, labels) 45 | podList, err := r.findPods(ctx, cr, labels) 46 | if err != nil { 47 | return false, err 48 | } 49 | 50 | if len(podList.Items) == 0 { 51 | // old labels 52 | oldPods, err := r.findPods(ctx, cr, map[string]string{ 53 | "app": loggerName(cr), 54 | "created-by": "eventlogger", 55 | }) 56 | if err != nil { 57 | return false, err 58 | } 59 | podList.Items = oldPods.Items 60 | } 61 | 62 | replacePod := false 63 | if len(podList.Items) == 1 { 64 | op := podList.Items[0] 65 | replacePod = podChanged(&op, pod) 66 | } 67 | 68 | if replacePod || len(podList.Items) > 1 { 69 | for i := range podList.Items { 70 | p := podList.Items[i] 71 | reqLogger.Info(fmt.Sprintf("Deleting %s", pod.Kind), "namespace", pod.GetNamespace(), "name", pod.GetName()) 72 | err = r.Delete(ctx, &p, &client.DeleteOptions{GracePeriodSeconds: &gracePeriod}) 73 | if err != nil { 74 | return false, err 75 | } 76 | } 77 | podList = &corev1.PodList{} 78 | } 79 | 80 | if len(podList.Items) == 0 { 81 | // Set EventLogger cr as the owner and controller 82 | if err := controllerutil.SetControllerReference(cr, pod, r.Scheme); err != nil { 83 | return false, err 84 | } 85 | reqLogger.Info( 86 | fmt.Sprintf("Creating a new %s", pod.Kind), 87 | "namespace", 88 | pod.GetNamespace(), 89 | "name", 90 | pod.GetName(), 91 | ) 92 | err = r.Create(ctx, pod) 93 | if err != nil { 94 | return false, err 95 | } 96 | return true, nil 97 | } 98 | 99 | return false, nil 100 | } 101 | 102 | func (r *Reconciler) findPods( 103 | ctx context.Context, 104 | cr *eventloggerv1.EventLogger, 105 | matchLabels map[string]string, 106 | ) (*corev1.PodList, error) { 107 | podList := &corev1.PodList{} 108 | opts := []client.ListOption{ 109 | client.InNamespace(cr.Namespace), 110 | client.MatchingLabels(matchLabels), 111 | } 112 | return podList, r.List(ctx, podList, opts...) 113 | } 114 | 115 | // podForCR returns a pod with the same name/namespace as the cr 116 | func (r *Reconciler) podForCR(cr *eventloggerv1.EventLogger) *corev1.Pod { 117 | metricsAddrFlag := flag.Lookup(cnst.ArgMetricsAddr) 118 | var metricsAddr string 119 | if metricsAddrFlag != nil { 120 | metricsAddr = metricsAddrFlag.Value.String() 121 | } 122 | if metricsAddr == "" { 123 | metricsAddr = cnst.DefaultMetricsAddr 124 | } 125 | metricsPort := metricsAddr[:1] 126 | 127 | annotations := make(map[string]string) 128 | for k, v := range cr.Spec.Annotations { 129 | annotations[k] = v 130 | } 131 | if cr.Spec.ScrapeMetrics != nil && *cr.Spec.ScrapeMetrics { 132 | annotations["prometheus.io/port"] = metricsPort 133 | annotations["prometheus.io/scrape"] = "true" 134 | } 135 | 136 | watchNamespace := cr.GetNamespace() 137 | if cr.Spec.Namespace != nil { 138 | watchNamespace = *cr.Spec.Namespace 139 | } 140 | 141 | saccName := loggerName(cr) 142 | if cr.Spec.ServiceAccount != "" { 143 | saccName = cr.Spec.ServiceAccount 144 | } 145 | 146 | container := config.GetCfg(r.Config).ContainerTemplate 147 | 148 | container.Name = "event-logger" 149 | container.Command = []string{"/opt/go/k8s-event-logger"} 150 | container.Args = []string{ 151 | "--" + cnst.ArgConfigName, cr.Name, 152 | "--" + cnst.ArgMetricsAddr, metricsAddr, 153 | "--" + cnst.ArgEnableLoggerMode, "true", 154 | } 155 | container.Env = []corev1.EnvVar{ 156 | {Name: cnst.EnvWatchNamespace, Value: watchNamespace}, 157 | {Name: cnst.EnvPodNamespace, ValueFrom: &corev1.EnvVarSource{ 158 | FieldRef: &corev1.ObjectFieldSelector{ 159 | APIVersion: "v1", 160 | FieldPath: "metadata.namespace", 161 | }, 162 | }}, 163 | } 164 | 165 | pod := &corev1.Pod{ 166 | TypeMeta: metav1.TypeMeta{ 167 | Kind: "Pod", 168 | }, 169 | ObjectMeta: metav1.ObjectMeta{ 170 | GenerateName: loggerName(cr) + "-", 171 | Namespace: cr.Namespace, 172 | Labels: copyLabels(cr), 173 | Annotations: annotations, 174 | }, 175 | Spec: corev1.PodSpec{ 176 | Containers: []corev1.Container{ 177 | container, 178 | }, 179 | ServiceAccountName: saccName, 180 | ImagePullSecrets: cr.Spec.ImagePullSecrets, 181 | NodeSelector: cr.Spec.NodeSelector, 182 | }, 183 | } 184 | 185 | return pod 186 | } 187 | 188 | func copyLabels(cr *eventloggerv1.EventLogger) map[string]string { 189 | labels := make(map[string]string) 190 | for k, v := range cr.Spec.Labels { 191 | labels[k] = v 192 | } 193 | applyDefaultLabels(cr, labels) 194 | return labels 195 | } 196 | 197 | func applyDefaultLabels(cr *eventloggerv1.EventLogger, labels map[string]string) { 198 | labels[labelComponent] = loggerName(cr) 199 | labels[labelManagedBy] = "eventlogger" 200 | } 201 | -------------------------------------------------------------------------------- /controllers/setup/eventlogger_controller_rbac.go: -------------------------------------------------------------------------------- 1 | /* 2 | 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 setup 18 | 19 | import ( 20 | "context" 21 | 22 | eventloggerv1 "github.com/bakito/k8s-event-logger-operator/api/v1" 23 | corev1 "k8s.io/api/core/v1" 24 | rbacv1 "k8s.io/api/rbac/v1" 25 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 | ctrl "sigs.k8s.io/controller-runtime" 27 | "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" 28 | ) 29 | 30 | func (r *Reconciler) setupRbac(ctx context.Context, cr *eventloggerv1.EventLogger) (bool, bool, bool, error) { 31 | var err error 32 | sacc, role, rb := rbacForCR(cr) 33 | 34 | if cr.Spec.ServiceAccount == "" { 35 | saccRes, err := controllerutil.CreateOrUpdate(ctx, r.Client, sacc, r.mutateServiceAccount(sacc, cr)) 36 | if err != nil { 37 | return false, false, false, err 38 | } 39 | 40 | roleRes, err := controllerutil.CreateOrUpdate(ctx, r.Client, role, r.mutateRole(role, cr)) 41 | if err != nil { 42 | return false, false, false, err 43 | } 44 | 45 | rbRes, err := controllerutil.CreateOrUpdate(ctx, r.Client, rb, r.mutateRolebinding(rb, cr)) 46 | if err != nil { 47 | return false, false, false, err 48 | } 49 | return saccRes != controllerutil.OperationResultNone, roleRes != controllerutil.OperationResultNone, rbRes != controllerutil.OperationResultNone, nil 50 | } 51 | 52 | // Only delete sa if the name is different from the configured 53 | if cr.Spec.ServiceAccount != sacc.GetName() { 54 | err = r.saveDelete(ctx, sacc) 55 | if err != nil { 56 | return false, false, false, err 57 | } 58 | } 59 | err = r.saveDelete(ctx, role) 60 | if err != nil { 61 | return false, false, false, err 62 | } 63 | err = r.saveDelete(ctx, rb) 64 | if err != nil { 65 | return false, false, false, err 66 | } 67 | return false, false, false, nil 68 | } 69 | 70 | func (r *Reconciler) mutateServiceAccount(sacc *corev1.ServiceAccount, cr *eventloggerv1.EventLogger) func() error { 71 | return func() error { 72 | sacc.Labels = copyLabels(cr) 73 | return ctrl.SetControllerReference(cr, sacc, r.Scheme) 74 | } 75 | } 76 | 77 | func (r *Reconciler) mutateRole(role *rbacv1.Role, cr *eventloggerv1.EventLogger) func() error { 78 | return func() error { 79 | role.Labels = copyLabels(cr) 80 | role.Rules = []rbacv1.PolicyRule{ 81 | { 82 | APIGroups: []string{""}, 83 | Resources: []string{"events", "pods"}, 84 | Verbs: []string{"watch", "get", "list"}, 85 | }, 86 | { 87 | APIGroups: []string{"eventlogger.bakito.ch"}, 88 | Resources: []string{"eventloggers"}, 89 | Verbs: []string{"get", "list", "patch", "update", "watch"}, 90 | }, 91 | } 92 | return ctrl.SetControllerReference(cr, role, r.Scheme) 93 | } 94 | } 95 | 96 | func (r *Reconciler) mutateRolebinding(rb *rbacv1.RoleBinding, cr *eventloggerv1.EventLogger) func() error { 97 | return func() error { 98 | rb.Labels = copyLabels(cr) 99 | 100 | rb.Subjects = []rbacv1.Subject{ 101 | { 102 | Kind: "ServiceAccount", 103 | Name: loggerName(cr), 104 | Namespace: cr.Namespace, 105 | }, 106 | } 107 | rb.RoleRef = rbacv1.RoleRef{ 108 | Kind: "Role", 109 | APIGroup: "rbac.authorization.k8s.io", 110 | Name: loggerName(cr), 111 | } 112 | return ctrl.SetControllerReference(cr, rb, r.Scheme) 113 | } 114 | } 115 | 116 | func rbacForCR(cr *eventloggerv1.EventLogger) (*corev1.ServiceAccount, *rbacv1.Role, *rbacv1.RoleBinding) { 117 | sacc := &corev1.ServiceAccount{ 118 | ObjectMeta: metav1.ObjectMeta{ 119 | Name: loggerName(cr), 120 | Namespace: cr.Namespace, 121 | }, 122 | } 123 | 124 | role := &rbacv1.Role{ 125 | ObjectMeta: metav1.ObjectMeta{ 126 | Name: loggerName(cr), 127 | Namespace: cr.Namespace, 128 | }, 129 | } 130 | rb := &rbacv1.RoleBinding{ 131 | ObjectMeta: metav1.ObjectMeta{ 132 | Name: loggerName(cr), 133 | Namespace: cr.Namespace, 134 | }, 135 | } 136 | 137 | return sacc, role, rb 138 | } 139 | -------------------------------------------------------------------------------- /controllers/setup/eventlogger_controller_test.go: -------------------------------------------------------------------------------- 1 | package setup 2 | 3 | import ( 4 | "context" 5 | "reflect" 6 | "time" 7 | 8 | v1 "github.com/bakito/k8s-event-logger-operator/api/v1" 9 | "github.com/bakito/k8s-event-logger-operator/controllers/config" 10 | c "github.com/bakito/k8s-event-logger-operator/pkg/constants" 11 | "github.com/bakito/k8s-event-logger-operator/version" 12 | "github.com/google/uuid" 13 | . "github.com/onsi/ginkgo/v2" 14 | . "github.com/onsi/gomega" 15 | gm "go.uber.org/mock/gomock" 16 | corev1 "k8s.io/api/core/v1" 17 | rbacv1 "k8s.io/api/rbac/v1" 18 | "k8s.io/apimachinery/pkg/api/resource" 19 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 20 | "k8s.io/apimachinery/pkg/types" 21 | "k8s.io/client-go/kubernetes/scheme" 22 | "k8s.io/utils/ptr" 23 | ctrl "sigs.k8s.io/controller-runtime" 24 | "sigs.k8s.io/controller-runtime/pkg/client" 25 | "sigs.k8s.io/controller-runtime/pkg/client/fake" 26 | "sigs.k8s.io/controller-runtime/pkg/reconcile" 27 | ) 28 | 29 | const ( 30 | testNamespace = "eventlogger-operator" 31 | testImage = "quay.io/bakito/k8s-event-logger" 32 | ) 33 | 34 | var _ = Describe("Logging", func() { 35 | var ( 36 | mockCtrl *gm.Controller 37 | ns2 string 38 | el *v1.EventLogger 39 | ) 40 | 41 | BeforeEach(func() { 42 | mockCtrl = gm.NewController(GinkgoT()) 43 | ns2 = "eventlogger-operators" 44 | 45 | el = &v1.EventLogger{ 46 | ObjectMeta: metav1.ObjectMeta{ 47 | Name: "eventlogger", 48 | Namespace: testNamespace, 49 | }, 50 | Spec: v1.EventLoggerSpec{ 51 | Labels: map[string]string{"test-label": "foo"}, 52 | Annotations: map[string]string{"test-annotation": "bar"}, 53 | ScrapeMetrics: ptr.To(true), 54 | Namespace: &ns2, 55 | NodeSelector: map[string]string{"ns-key": "ns-value"}, 56 | }, 57 | Status: v1.EventLoggerStatus{ 58 | OperatorVersion: "0", 59 | Hash: "", 60 | LastProcessed: metav1.Date(2020, 1, 1, 1, 1, 1, 1, time.Local), 61 | }, 62 | } 63 | }) 64 | AfterEach(func() { 65 | defer mockCtrl.Finish() 66 | }) 67 | 68 | Context("Reconcile", func() { 69 | Context("EventLogger", func() { 70 | It("update the eventlogger", func() { 71 | cl, res := testReconcile(el) 72 | Ω(res.RequeueAfter).Should(Equal(time.Duration(0))) 73 | 74 | // check updated status 75 | updated := &v1.EventLogger{} 76 | err := cl.Get(context.TODO(), types.NamespacedName{ 77 | Name: "eventlogger", 78 | Namespace: testNamespace, 79 | }, updated) 80 | Ω(err).ShouldNot(HaveOccurred()) 81 | Ω(updated.Status.LastProcessed.String()).ShouldNot(BeEmpty()) 82 | Ω(updated.Status.Hash).ShouldNot(BeEmpty()) 83 | Ω(updated.Status.OperatorVersion).Should(Equal(version.Version)) 84 | }) 85 | }) 86 | Context("Pod", func() { 87 | It("create a correct pod", func() { 88 | cl, res := testReconcile(el) 89 | Ω(res.RequeueAfter).Should(Equal(time.Duration(0))) 90 | 91 | // check created pod 92 | pods := &corev1.PodList{} 93 | assertEntrySize(cl, el, pods, 1) 94 | pod := pods.Items[0] 95 | 96 | Ω(pod.ObjectMeta.Labels).Should(HaveKey(labelComponent)) 97 | Ω(pod.ObjectMeta.Labels).Should(HaveKey(labelManagedBy)) 98 | Ω(pod.ObjectMeta.Labels["test-label"]).Should(Equal("foo")) 99 | Ω(pod.ObjectMeta.Annotations["test-annotation"]).Should(Equal("bar")) 100 | Ω(pod.ObjectMeta.Annotations["prometheus.io/port"]).Should(Equal(c.DefaultMetricsAddr[:1])) 101 | Ω(pod.ObjectMeta.Annotations["prometheus.io/scrape"]).Should(Equal("true")) 102 | Ω(pod.ObjectMeta.Namespace).Should(Equal(el.GetNamespace())) 103 | Ω(pod.ObjectMeta.OwnerReferences).Should(HaveLen(1)) 104 | 105 | Ω(pod.Spec.NodeSelector).Should(HaveLen(1)) 106 | Ω(pod.Spec.NodeSelector["ns-key"]).Should(Equal("ns-value")) 107 | 108 | Ω(pod.Spec.Containers).Should(HaveLen(1)) 109 | container := pod.Spec.Containers[0] 110 | Ω(*container.Resources.Requests.Cpu()).Should(Equal(resource.MustParse("111m"))) 111 | Ω(*container.Resources.Requests.Memory()).Should(Equal(resource.MustParse("222Mi"))) 112 | Ω(*container.Resources.Limits.Cpu()).Should(Equal(resource.MustParse("333m"))) 113 | Ω(*container.Resources.Limits.Memory()).Should(Equal(resource.MustParse("444Mi"))) 114 | 115 | evars := make(map[string]corev1.EnvVar) 116 | for _, e := range container.Env { 117 | evars[e.Name] = e 118 | } 119 | Ω(evars[c.EnvWatchNamespace].Value).Should(Equal(ns2)) 120 | }) 121 | 122 | It("should update the pod image", func() { 123 | pod := newPod() 124 | pod.Spec.Containers[0].Image = "foo" 125 | 126 | cl, _ := testReconcile(el, pod) 127 | 128 | pods := &corev1.PodList{} 129 | assertEntrySize(cl, el, pods, 1) 130 | pod2 := pods.Items[0] 131 | Ω(pod2.Spec.Containers[0].Image).Should(Equal(testImage)) 132 | }) 133 | 134 | It("should update the imagePullSecrets", func() { 135 | el.Spec.ImagePullSecrets = []corev1.LocalObjectReference{{Name: "secret1"}, {Name: "secret2"}} 136 | 137 | cl, res := testReconcile(el) 138 | Ω(res.RequeueAfter).Should(Equal(time.Duration(0))) 139 | 140 | pods := &corev1.PodList{} 141 | assertEntrySize(cl, el, pods, 1) 142 | pod2 := pods.Items[0] 143 | 144 | Ω(len(pod2.Spec.ImagePullSecrets)).Should(Equal(2)) 145 | Ω(pod2.Spec.ImagePullSecrets[0].Name).Should(Equal("secret1")) 146 | Ω(pod2.Spec.ImagePullSecrets[1].Name).Should(Equal("secret2")) 147 | }) 148 | 149 | It("should use an external service account", func() { 150 | el.Spec.ServiceAccount = "foo" 151 | 152 | sacc, role, rb := rbacForCR(el) 153 | cl, _ := testReconcile(el, sacc, role, rb) 154 | 155 | pods := &corev1.PodList{} 156 | assertEntrySize(cl, el, pods, 1) 157 | pod2 := pods.Items[0] 158 | 159 | Ω(pod2.Spec.Containers[0].Image).Should(Equal(testImage)) 160 | 161 | assertEntrySize(cl, el, &corev1.ServiceAccountList{}, 0) 162 | assertEntrySize(cl, el, &rbacv1.RoleList{}, 0) 163 | assertEntrySize(cl, el, &rbacv1.RoleBindingList{}, 0) 164 | }) 165 | }) 166 | Context("ServiceAccount", func() { 167 | It("create a correct service account", func() { 168 | cl, res := testReconcile(el) 169 | Ω(res.RequeueAfter).Should(Equal(time.Duration(0))) 170 | 171 | // service account 172 | saccList := &corev1.ServiceAccountList{} 173 | assertEntrySize(cl, el, saccList, 1) 174 | sacc := saccList.Items[0] 175 | Ω(sacc.ObjectMeta.Name).Should(Equal(loggerName(el))) 176 | Ω(sacc.ObjectMeta.Labels).Should(HaveKey(labelComponent)) 177 | Ω(sacc.ObjectMeta.Labels).Should(HaveKey(labelManagedBy)) 178 | Ω(sacc.ObjectMeta.OwnerReferences).Should(HaveLen(1)) 179 | }) 180 | }) 181 | Context("Role", func() { 182 | It("create a correct role", func() { 183 | cl, res := testReconcile(el) 184 | Ω(res.RequeueAfter).Should(Equal(time.Duration(0))) 185 | 186 | // role 187 | roleList := &rbacv1.RoleList{} 188 | assertEntrySize(cl, el, roleList, 1) 189 | role := roleList.Items[0] 190 | Ω(role.ObjectMeta.Name).Should(Equal(loggerName(el))) 191 | Ω(role.ObjectMeta.Labels).Should(HaveKey(labelComponent)) 192 | Ω(role.ObjectMeta.Labels).Should(HaveKey(labelManagedBy)) 193 | Ω(role.ObjectMeta.OwnerReferences).Should(HaveLen(1)) 194 | 195 | Ω(role.Rules).Should(HaveLen(2)) 196 | Ω(role.Rules[0].APIGroups).Should(Equal([]string{""})) 197 | Ω(role.Rules[0].Resources).Should(Equal([]string{"events", "pods"})) 198 | Ω(role.Rules[0].Verbs).Should(Equal([]string{"watch", "get", "list"})) 199 | 200 | Ω(role.Rules[1].APIGroups).Should(Equal([]string{"eventlogger.bakito.ch"})) 201 | Ω(role.Rules[1].Resources).Should(Equal([]string{"eventloggers"})) 202 | Ω(role.Rules[1].Verbs).Should(Equal([]string{"get", "list", "patch", "update", "watch"})) 203 | }) 204 | }) 205 | Context("Rolebinding", func() { 206 | It("create a correct role binding", func() { 207 | cl, res := testReconcile(el) 208 | Ω(res.RequeueAfter).Should(Equal(time.Duration(0))) 209 | 210 | // rolebinding 211 | rbList := &rbacv1.RoleBindingList{} 212 | assertEntrySize(cl, el, rbList, 1) 213 | rb := rbList.Items[0] 214 | Ω(rb.ObjectMeta.Name).Should(Equal(loggerName(el))) 215 | Ω(rb.ObjectMeta.Labels).Should(HaveKey(labelComponent)) 216 | Ω(rb.ObjectMeta.Labels).Should(HaveKey(labelManagedBy)) 217 | Ω(rb.ObjectMeta.OwnerReferences).Should(HaveLen(1)) 218 | 219 | Ω(rb.Subjects).Should(HaveLen(1)) 220 | Ω(rb.Subjects[0].Kind).Should(Equal("ServiceAccount")) 221 | Ω(rb.Subjects[0].Name).Should(Equal(loggerName(el))) 222 | Ω(rb.Subjects[0].Namespace).Should(Equal(el.GetNamespace())) 223 | Ω(rb.RoleRef.Kind).Should(Equal("Role")) 224 | Ω(rb.RoleRef.Name).Should(Equal(loggerName(el))) 225 | }) 226 | }) 227 | }) 228 | }) 229 | 230 | func testReconcile(initialObjects ...client.Object) (client.Client, reconcile.Result) { 231 | s := scheme.Scheme 232 | 233 | Ω(v1.SchemeBuilder.AddToScheme(s)).ShouldNot(HaveOccurred()) 234 | 235 | nn := types.NamespacedName{ 236 | Namespace: uuid.New().String(), 237 | Name: uuid.New().String(), 238 | } 239 | operatorPod := &corev1.Pod{ 240 | ObjectMeta: metav1.ObjectMeta{ 241 | Namespace: nn.Namespace, 242 | Name: nn.Name, 243 | }, 244 | Spec: corev1.PodSpec{ 245 | Containers: []corev1.Container{{Image: testImage}}, 246 | }, 247 | } 248 | cfg := &corev1.ConfigMap{ 249 | ObjectMeta: metav1.ObjectMeta{ 250 | Namespace: nn.Namespace, 251 | Name: nn.Name, 252 | }, 253 | Data: map[string]string{c.ConfigKeyContainerTemplate: ` 254 | image: quay.io/bakito/k8s-event-logger 255 | resources: 256 | limits: 257 | cpu: 333m 258 | memory: 444Mi 259 | requests: 260 | cpu: 111m 261 | memory: 222Mi 262 | `}, 263 | } 264 | 265 | initialObjects = append(initialObjects, operatorPod, cfg) 266 | 267 | cl := fake.NewClientBuilder().WithScheme(s).WithObjects(initialObjects...).Build() 268 | 269 | cr := config.Reconciler{ 270 | Reader: cl, 271 | Log: ctrl.Log.WithName("controllers").WithName("Pod"), 272 | Scheme: s, 273 | } 274 | 275 | _, err := cr.Reconcile(cr.Ctx(), reconcile.Request{ 276 | NamespacedName: types.NamespacedName{ 277 | Name: cfg.Name, 278 | Namespace: cfg.Namespace, 279 | }, 280 | }) 281 | Ω(err).ShouldNot(HaveOccurred()) 282 | 283 | r := &Reconciler{ 284 | Client: cl, 285 | Log: ctrl.Log.WithName("controllers").WithName("Pod"), 286 | Scheme: s, 287 | Config: cr.Ctx(), 288 | } 289 | 290 | req := reconcile.Request{ 291 | NamespacedName: types.NamespacedName{ 292 | Name: "eventlogger", 293 | Namespace: testNamespace, 294 | }, 295 | } 296 | res, err := r.Reconcile(context.Background(), req) 297 | Ω(err).ShouldNot(HaveOccurred()) 298 | 299 | return cl, res 300 | } 301 | 302 | func assertEntrySize(cl client.Client, el *v1.EventLogger, list client.ObjectList, expected int) { 303 | option := client.MatchingLabels{} 304 | applyDefaultLabels(el, option) 305 | err := cl.List(context.TODO(), list, option) 306 | 307 | Ω(err).ShouldNot(HaveOccurred()) 308 | r := reflect.ValueOf(list) 309 | f := reflect.Indirect(r).FieldByName("Items") 310 | Ω(f.Len()).Should(Equal(expected)) 311 | } 312 | 313 | func newPod() *corev1.Pod { 314 | return &corev1.Pod{ 315 | TypeMeta: metav1.TypeMeta{ 316 | Kind: "Pod", 317 | }, 318 | ObjectMeta: metav1.ObjectMeta{ 319 | Namespace: testNamespace, 320 | Labels: map[string]string{ 321 | "app": "event-logger-eventlogger", 322 | "created-by": "eventlogger", 323 | }, 324 | }, 325 | Spec: corev1.PodSpec{ 326 | Containers: []corev1.Container{ 327 | {}, 328 | }, 329 | }, 330 | } 331 | } 332 | -------------------------------------------------------------------------------- /controllers/setup/setup_suite_test.go: -------------------------------------------------------------------------------- 1 | package setup_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestSetup(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Setup Suite") 13 | } 14 | -------------------------------------------------------------------------------- /docs/chart-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakito/k8s-event-logger-operator/453f41830c25d482b8ea4ec96b25345ec503a9d1/docs/chart-logo.png -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/bakito/k8s-event-logger-operator 2 | 3 | go 1.24.0 4 | 5 | require ( 6 | github.com/bakito/operator-utils v1.3.3 7 | github.com/fatih/structs v1.1.0 8 | github.com/go-logr/logr v1.4.3 9 | github.com/go-logr/zapr v1.3.0 10 | github.com/go-playground/locales v0.14.1 11 | github.com/go-playground/universal-translator v0.18.1 12 | github.com/go-playground/validator/v10 v10.26.0 13 | github.com/google/uuid v1.6.0 14 | github.com/onsi/ginkgo/v2 v2.23.4 15 | github.com/onsi/gomega v1.37.0 16 | go.uber.org/mock v0.5.2 17 | go.uber.org/zap v1.27.0 18 | k8s.io/api v0.33.1 19 | k8s.io/apimachinery v0.33.1 20 | k8s.io/client-go v0.33.1 21 | k8s.io/klog/v2 v2.130.1 22 | k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 23 | sigs.k8s.io/controller-runtime v0.21.0 24 | ) 25 | 26 | require ( 27 | github.com/beorn7/perks v1.0.1 // indirect 28 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 29 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 30 | github.com/emicklei/go-restful/v3 v3.11.0 // indirect 31 | github.com/evanphx/json-patch/v5 v5.9.11 // indirect 32 | github.com/fsnotify/fsnotify v1.8.0 // indirect 33 | github.com/fxamacker/cbor/v2 v2.7.0 // indirect 34 | github.com/gabriel-vasile/mimetype v1.4.8 // indirect 35 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 36 | github.com/go-openapi/jsonreference v0.20.2 // indirect 37 | github.com/go-openapi/swag v0.23.0 // indirect 38 | github.com/go-task/slim-sprig/v3 v3.0.0 // indirect 39 | github.com/gogo/protobuf v1.3.2 // indirect 40 | github.com/google/btree v1.1.3 // indirect 41 | github.com/google/gnostic-models v0.6.9 // indirect 42 | github.com/google/go-cmp v0.7.0 // indirect 43 | github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect 44 | github.com/josharian/intern v1.0.0 // indirect 45 | github.com/json-iterator/go v1.1.12 // indirect 46 | github.com/leodido/go-urn v1.4.0 // indirect 47 | github.com/mailru/easyjson v0.7.7 // indirect 48 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 49 | github.com/modern-go/reflect2 v1.0.2 // indirect 50 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 51 | github.com/pkg/errors v0.9.1 // indirect 52 | github.com/prometheus/client_golang v1.22.0 // indirect 53 | github.com/prometheus/client_model v0.6.1 // indirect 54 | github.com/prometheus/common v0.62.0 // indirect 55 | github.com/prometheus/procfs v0.15.1 // indirect 56 | github.com/spf13/pflag v1.0.5 // indirect 57 | github.com/x448/float16 v0.8.4 // indirect 58 | go.uber.org/automaxprocs v1.6.0 // indirect 59 | go.uber.org/multierr v1.11.0 // indirect 60 | golang.org/x/crypto v0.36.0 // indirect 61 | golang.org/x/mod v0.24.0 // indirect 62 | golang.org/x/net v0.38.0 // indirect 63 | golang.org/x/oauth2 v0.27.0 // indirect 64 | golang.org/x/sync v0.12.0 // indirect 65 | golang.org/x/sys v0.32.0 // indirect 66 | golang.org/x/term v0.30.0 // indirect 67 | golang.org/x/text v0.23.0 // indirect 68 | golang.org/x/time v0.9.0 // indirect 69 | golang.org/x/tools v0.31.0 // indirect 70 | gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect 71 | google.golang.org/protobuf v1.36.5 // indirect 72 | gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect 73 | gopkg.in/inf.v0 v0.9.1 // indirect 74 | gopkg.in/yaml.v3 v3.0.1 // indirect 75 | k8s.io/apiextensions-apiserver v0.33.0 // indirect 76 | k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect 77 | sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect 78 | sigs.k8s.io/randfill v1.0.0 // indirect 79 | sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect 80 | sigs.k8s.io/yaml v1.4.0 // indirect 81 | ) 82 | -------------------------------------------------------------------------------- /hack/.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | -------------------------------------------------------------------------------- /hack/boilerplate.go.txt: -------------------------------------------------------------------------------- 1 | /* 2 | 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 | */ -------------------------------------------------------------------------------- /hack/build-images.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | set -e 4 | 5 | if [[ $# -ne 1 ]] ; then 6 | echo 'please use version as argument' 7 | exit 1 8 | fi 9 | 10 | RELEASE=v${1} 11 | git pull 12 | git checkout tags/${RELEASE} -b ${RELEASE} 13 | 14 | podman rmi -f golang:1.15 || true 15 | podman rmi -f registry.access.redhat.com/ubi8/ubi-minimal:latest | true 16 | 17 | podman build -t quay.io/bakito/k8s-event-logger:${RELEASE} --no-cache -f ./Dockerfile . 18 | podman push quay.io/bakito/k8s-event-logger:${RELEASE} 19 | 20 | git checkout main 21 | git branch -d ${RELEASE} -f 22 | -------------------------------------------------------------------------------- /hack/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | set -e 4 | 5 | 6 | if GIT_TAG=$(git describe --tags --abbrev=0 --exact-match 2>/dev/null); then 7 | VERSION=${GIT_TAG} 8 | else 9 | VERSION=$(git rev-parse --short HEAD) 10 | fi 11 | 12 | echo "Building with version ${VERSION}" 13 | 14 | go build -a -installsuffix cgo -ldflags="-w -s -X github.com/bakito/k8s-event-logger-operator/version.Version=${VERSION}" -o ${1} ${2} 15 | 16 | upx -q ${1} 17 | -------------------------------------------------------------------------------- /hack/generate-mocks.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | go get -u go.uber.org/mock/mockgen 3 | mockgen -package logr github.com/go-logr/logr Logger > pkg/mock/logr/logr_mock.go 4 | -------------------------------------------------------------------------------- /hack/push-to-quay.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | echo "$QUAY_BOT_PASSWORD" | docker login -u "$QUAY_BOT_USERNAME" --password-stdin quay.io 4 | 5 | for img in "$@"; do 6 | if [ -z "${TRAVIS_TAG}" ]; then 7 | docker push ${img}:${TRAVIS_BRANCH} 8 | else 9 | docker tag ${img}:${TRAVIS_BRANCH} ${img}:${TRAVIS_TAG} 10 | docker tag ${img}:${TRAVIS_BRANCH} ${img}:latest 11 | docker push ${img}:${TRAVIS_TAG} 12 | docker push ${img}:latest 13 | fi 14 | done 15 | 16 | docker logout quay.io 17 | -------------------------------------------------------------------------------- /hack/release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | set -e 4 | 5 | if [[ $# -ne 1 ]] ; then 6 | echo 'please use version as argument' 7 | exit 1 8 | fi 9 | 10 | CHANGED=$(git diff-index --name-only HEAD --) 11 | if [[ ! -z $CHANGED ]]; then 12 | echo "Please commit your local changes first" 13 | exit 1 14 | fi 15 | 16 | RELEASE=${1} 17 | 18 | sed -i "s/Version = \".*\"/Version = \"v${RELEASE}\"/" version/version.go 19 | sed -i "s/version: .*/version: ${RELEASE}/" helm/Chart.yaml 20 | sed -i "s/appVersion: .*/appVersion: v${RELEASE}/" helm/Chart.yaml 21 | 22 | make test 23 | 24 | GO_VERSION=$(cat go.mod | grep -a "^go.*" | awk '{print $2}') 25 | 26 | sed -i "s/golang:.*/golang:${GO_VERSION} as builder/" Dockerfile 27 | sed -i "s/golang:.*/golang:${GO_VERSION} || true/" hack/build-images.sh 28 | 29 | git add . 30 | git diff-index --quiet HEAD || git commit -m "prepare release ${RELEASE}" 31 | git push 32 | 33 | echo "Create Release" 34 | RELEASE_ID=$(curl --header "Content-Type: application/json" \ 35 | --header "Authorization: token ${GITHUB_TOKEN}" \ 36 | --request POST \ 37 | --data "{ 38 | \"tag_name\": \"v${RELEASE}\", 39 | \"name\": \"v${RELEASE}\", 40 | \"body\": \"Release v${RELEASE}\n\nHelm Chart: [k8s-event-logger-operator-${RELEASE}.tgz](https://github.com/bakito/k8s-event-logger-operator/releases/download/v${RELEASE}/k8s-event-logger-operator-${RELEASE}.tgz) \", 41 | \"draft\": false, 42 | \"prerelease\": false 43 | }" https://api.github.com/repos/bakito/k8s-event-logger-operator/releases | jq '.id') 44 | 45 | echo "Release Id: ${RELEASE_ID}" 46 | 47 | helm package ./helm/ --version ${RELEASE} --app-version v${RELEASE} 48 | 49 | FILE=k8s-event-logger-operator-${RELEASE}.tgz 50 | 51 | echo "Upload Helm Chart: ${FILE}" 52 | curl \ 53 | -H "Authorization: token $GITHUB_TOKEN" \ 54 | -H "Content-Type: $(file -b --mime-type ${FILE})" \ 55 | --data-binary @${FILE} \ 56 | "https://uploads.github.com/repos/bakito/k8s-event-logger-operator/releases/${RELEASE_ID}/assets?name=${FILE}" 57 | 58 | rm -Rf ${FILE} -------------------------------------------------------------------------------- /helm/.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 | *~ 18 | # Various IDEs 19 | .project 20 | .idea/ 21 | *.tmproj 22 | .vscode/ 23 | README.md.gotmpl 24 | -------------------------------------------------------------------------------- /helm/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: k8s-event-logger-operator 3 | description: This operator creates a logging pod that logs corev1.Event information as structured json log. The crd allows to configure the events to be logged. 4 | type: application 5 | version: 1.13.12 6 | appVersion: v1.13.12 7 | sources: 8 | - https://github.com/bakito/k8s-event-logger-operator 9 | annotations: 10 | artifacthub.io/changes: | 11 | - add image pull secrets to the EventLogger CRD 12 | - CRD reformatted in post-processing 13 | icon: https://raw.githubusercontent.com/bakito/k8s-event-logger-operator/main/docs/chart-logo.png 14 | -------------------------------------------------------------------------------- /helm/README.md: -------------------------------------------------------------------------------- 1 | # k8s-event-logger-operator 2 | 3 | ![Version: 1.13.12](https://img.shields.io/badge/Version-1.13.12-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: v1.13.12](https://img.shields.io/badge/AppVersion-v1.13.12-informational?style=flat-square) 4 | 5 | This operator creates a logging pod that logs corev1.Event information as structured json log. The crd allows to configure the events to be logged. 6 | 7 | ## Installation 8 | 9 | ### oci 10 | 11 | ```console 12 | helm install my-k8s-event-logger-operator oci://ghcr.io/bakito/helm-charts/k8s-event-logger-operator --version 1.13.12 13 | ``` 14 | 15 | ### helm repository 16 | 17 | ```console 18 | helm repo add bakito https://charts.bakito.net 19 | helm install my-k8s-event-logger-operator bakito/k8s-event-logger-operator --version 1.13.12 20 | ``` 21 | 22 | ## Values 23 | 24 | | Key | Type | Default | Description | 25 | |-----|------|---------|-------------| 26 | | affinity | object | `{}` | Assign custom [affinity] rules to the deployment | 27 | | eventLogger.leaderElection | bool | `true` | Enable leader election for the controller | 28 | | eventLogger.leaderElectionResourceLock | string | `nil` | Leader election lock type | 29 | | eventLogger.resources | object | `{"limits":{"cpu":"200m","memory":"256Mi"},"requests":{"cpu":"100m","memory":"64Mi"}}` | Resource limits and requests for the logger pods. | 30 | | eventLogger.securityContext | object | `{}` | Security Context for the logger pods. | 31 | | extraPodAnnotations | object | `{}` | Add additional pod [annotations] | 32 | | extraPodLabels | object | `{}` | Add additional pod [labels] | 33 | | fullnameOverride | string | `""` | String to fully override "argo-rollouts.fullname" template | 34 | | image.imagePullSecrets | object | `{}` | Image pull secrets | 35 | | image.pullPolicy | string | `"IfNotPresent"` | Image pull policy | 36 | | image.repository | string | `"ghcr.io/bakito/k8s-event-logger"` | Repository to use | 37 | | image.tag | string | `nil` | Overrides the image tag (default is the chart appVersion) | 38 | | logging.level | string | `"info"` | Log level | 39 | | logging.timeEncoding | string | `"iso8601"` | Log time encoding | 40 | | nameOverride | string | `""` | String to partially override "argo-rollouts.fullname" template | 41 | | nodeSelector | object | `{}` | [Node selector] | 42 | | rbac.create | bool | `true` | Specifies whether rbac should be created | 43 | | rbac.roleName | string | `nil` | The name of the role and clusterrole to use. If not set and create is true, a name is generated using the fullname template | 44 | | resources | object | `{"limits":{"cpu":"200m","memory":"512Mi"},"requests":{"cpu":"100m","memory":"128Mi"}}` | Resource limits and requests for the operator pods. | 45 | | revisionHistoryLimit | int | `3` | Number of old deployment ReplicaSets to retain. The rest will be garbage collected. | 46 | | securityContext | object | `{}` | Security Context for the operator pods. | 47 | | serviceAccount.create | bool | `true` | Specifies whether a service account should be created | 48 | | serviceAccount.name | string | `nil` | The name of the service account to use. If not set and create is true, a name is generated using the fullname template | 49 | | tolerations | list | `[]` | [Tolerations] for use with node taints | 50 | | webhook.caBundle | string | `"Cg=="` | certificate ca bundle | 51 | | webhook.certManager.enabled | bool | `false` | Enable cert manager setup | 52 | | webhook.certsSecret.name | string | `nil` | Certificate secret name | 53 | | webhook.enabled | bool | `false` | Specifies whether validation webhook should be created. | 54 | | webhook.openShiftServiceCert.enabled | bool | `false` | Enable OpenShift service certificate | 55 | 56 | ---------------------------------------------- 57 | Autogenerated from chart metadata using [helm-docs](https://github.com/norwoodj/helm-docs) 58 | -------------------------------------------------------------------------------- /helm/README.md.gotmpl: -------------------------------------------------------------------------------- 1 | {{ template "chart.header" . }} 2 | 3 | {{ template "chart.badgesSection" . }} 4 | 5 | {{ template "chart.description" . }} 6 | 7 | ## Installation 8 | 9 | ### oci 10 | 11 | ```console 12 | helm install my-{{ template "chart.name" . }} oci://ghcr.io/bakito/helm-charts/{{ template "chart.name" . }} --version {{ template "chart.version" . }} 13 | ``` 14 | 15 | ### helm repository 16 | 17 | ```console 18 | helm repo add bakito https://charts.bakito.net 19 | helm install my-{{ template "chart.name" . }} bakito/{{ template "chart.name" . }} --version {{ template "chart.version" . }} 20 | ``` 21 | 22 | {{ template "chart.valuesSection" . }} 23 | 24 | ---------------------------------------------- 25 | Autogenerated from chart metadata using [helm-docs](https://github.com/norwoodj/helm-docs) 26 | -------------------------------------------------------------------------------- /helm/crds/eventlogger.bakito.ch_eventloggers.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | controller-gen.kubebuilder.io/version: v0.17.2 7 | name: eventloggers.eventlogger.bakito.ch 8 | spec: 9 | group: eventlogger.bakito.ch 10 | names: 11 | kind: EventLogger 12 | listKind: EventLoggerList 13 | plural: eventloggers 14 | singular: eventlogger 15 | scope: Namespaced 16 | versions: 17 | - name: v1 18 | schema: 19 | openAPIV3Schema: 20 | description: EventLogger is the Schema for the eventloggers API 21 | properties: 22 | apiVersion: 23 | description: |- 24 | APIVersion defines the versioned schema of this representation of an object. 25 | Servers should convert recognized schemas to the latest internal value, and 26 | may reject unrecognized values. 27 | More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources 28 | type: string 29 | kind: 30 | description: |- 31 | Kind is a string value representing the REST resource this object represents. 32 | Servers may infer this from the endpoint the client submits requests to. 33 | Cannot be updated. 34 | In CamelCase. 35 | More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds 36 | type: string 37 | metadata: 38 | type: object 39 | spec: 40 | description: EventLoggerSpec defines the desired state of EventLogger 41 | properties: 42 | annotations: 43 | additionalProperties: 44 | type: string 45 | description: Labels additional annotations for the logger pod 46 | type: object 47 | eventTypes: 48 | description: EventTypes the event types to log. If empty all events are logged. 49 | items: 50 | type: string 51 | minItems: 0 52 | type: array 53 | imagePullSecrets: 54 | description: |- 55 | ImagePullSecrets is an optional list of references to secrets in the same namespace to use for pulling any of the images used by this EventLoggerSpec. 56 | If specified, these secrets will be passed to individual puller implementations for them to use. 57 | items: 58 | description: |- 59 | LocalObjectReference contains enough information to let you locate the 60 | referenced object inside the same namespace. 61 | properties: 62 | name: 63 | default: "" 64 | description: |- 65 | Name of the referent. 66 | type: string 67 | type: object 68 | x-kubernetes-map-type: atomic 69 | type: array 70 | kinds: 71 | description: Kinds the kinds to log the events for 72 | items: 73 | description: Kind defines a kind to log events for 74 | properties: 75 | apiGroup: 76 | nullable: true 77 | type: string 78 | eventTypes: 79 | description: EventTypes the event types to log. If empty events are logged as defined in spec. 80 | items: 81 | type: string 82 | minItems: 0 83 | type: array 84 | matchingPatterns: 85 | description: MatchingPatterns optional regex pattern that must be contained in the message to be logged 86 | items: 87 | type: string 88 | minItems: 0 89 | type: array 90 | name: 91 | minLength: 3 92 | type: string 93 | reasons: 94 | description: Reasons the event reasons to log. If empty events with any reasons are logged. 95 | items: 96 | type: string 97 | minItems: 0 98 | type: array 99 | skipOnMatch: 100 | description: SkipOnMatch skip the entry if matched 101 | type: boolean 102 | skipReasons: 103 | description: SkipReasons event reasons to log to skip. If empty events with any reasons are logged. 104 | items: 105 | type: string 106 | minItems: 0 107 | type: array 108 | required: 109 | - name 110 | type: object 111 | minItems: 1 112 | type: array 113 | labels: 114 | additionalProperties: 115 | type: string 116 | description: Labels additional labels for the logger pod 117 | type: object 118 | logFields: 119 | description: LogFields fields ot the event to be logged. 120 | items: 121 | description: LogField defines a log field 122 | properties: 123 | name: 124 | description: name of the log field 125 | type: string 126 | path: 127 | description: Path within the corev1.Event struct https://github.com/kubernetes/api/blob/master/core/v1/types.go 128 | items: 129 | type: string 130 | minItems: 1 131 | type: array 132 | value: 133 | description: Value a static value of the log field. Can be uses to add static log fields 134 | nullable: true 135 | type: string 136 | required: 137 | - name 138 | type: object 139 | type: array 140 | namespace: 141 | description: namespace the namespace to watch on, may be an empty string 142 | nullable: true 143 | type: string 144 | nodeSelector: 145 | additionalProperties: 146 | type: string 147 | description: |- 148 | NodeSelector is a selector which must be true for the pod to fit on a node. 149 | Selector which must match a node's labels for the pod to be scheduled on that node. 150 | More info: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/ 151 | type: object 152 | scrapeMetrics: 153 | description: ScrapeMetrics if true, prometheus scrape annotations are added to the pod 154 | type: boolean 155 | serviceAccount: 156 | description: ServiceAccount the service account to use for the logger pod 157 | type: string 158 | type: object 159 | status: 160 | description: EventLoggerStatus defines the observed state of EventLogger 161 | properties: 162 | error: 163 | description: Error 164 | type: string 165 | hash: 166 | description: Hash 167 | type: string 168 | lastProcessed: 169 | description: LastProcessed the timestamp the cr was last processed 170 | format: date-time 171 | type: string 172 | operatorVersion: 173 | description: OperatorVersion the version of the operator that processed the cr 174 | type: string 175 | required: 176 | - lastProcessed 177 | - operatorVersion 178 | type: object 179 | type: object 180 | served: true 181 | storage: true 182 | -------------------------------------------------------------------------------- /helm/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* vim: set filetype=mustache: */}} 2 | {{/* 3 | Expand the name of the chart. 4 | */}} 5 | {{- define "k8s-event-logger-operator.name" -}} 6 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} 7 | {{- end -}} 8 | 9 | {{/* 10 | Create a default fully qualified app name. 11 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 12 | If release name contains chart name it will be used as a full name. 13 | */}} 14 | {{- define "k8s-event-logger-operator.fullname" -}} 15 | {{- if .Values.fullnameOverride -}} 16 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} 17 | {{- else -}} 18 | {{- $name := default .Chart.Name .Values.nameOverride -}} 19 | {{- if contains $name .Release.Name -}} 20 | {{- .Release.Name | trunc 63 | trimSuffix "-" -}} 21 | {{- else -}} 22 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} 23 | {{- end -}} 24 | {{- end -}} 25 | {{- end -}} 26 | 27 | {{/* 28 | Create chart name and version as used by the chart label. 29 | */}} 30 | {{- define "k8s-event-logger-operator.chart" -}} 31 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} 32 | {{- end -}} 33 | 34 | {{/* 35 | Common labels 36 | */}} 37 | {{- define "k8s-event-logger-operator.labels" -}} 38 | helm.sh/chart: {{ include "k8s-event-logger-operator.chart" . }} 39 | helm.sh/namespace: {{ .Release.Namespace }} 40 | {{ include "k8s-event-logger-operator.selectorLabels" . }} 41 | {{- if .Chart.AppVersion }} 42 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 43 | {{- end }} 44 | app.kubernetes.io/managed-by: {{ .Release.Service }} 45 | {{- end -}} 46 | 47 | {{/* 48 | Selector labels 49 | */}} 50 | {{- define "k8s-event-logger-operator.selectorLabels" -}} 51 | app.kubernetes.io/name: {{ include "k8s-event-logger-operator.name" . }} 52 | app.kubernetes.io/instance: {{ .Release.Name }} 53 | {{- end -}} 54 | 55 | {{/* 56 | Create the name of the service account to use 57 | */}} 58 | {{- define "k8s-event-logger-operator.serviceAccountName" -}} 59 | {{- if .Values.serviceAccount.create -}} 60 | {{ default (include "k8s-event-logger-operator.fullname" .) .Values.serviceAccount.name }} 61 | {{- else -}} 62 | {{ default "default" .Values.serviceAccount.name }} 63 | {{- end -}} 64 | {{- end -}} 65 | 66 | {{/* 67 | Create the name of the role to use 68 | */}} 69 | {{- define "k8s-event-logger-operator.roleName" -}} 70 | {{- if .Values.rbac.create -}} 71 | {{ default (include "k8s-event-logger-operator.fullname" .) .Values.rbac.roleName }} 72 | {{- else -}} 73 | {{ default "default" .Values.rbac.roleName }} 74 | {{- end -}} 75 | {{- end -}} 76 | 77 | {{/* 78 | Get the webhook cert secret name 79 | */}} 80 | {{- define "k8s-event-logger-operator.webhookCertSecretName" -}} 81 | {{- default (printf "%s-webhook" (include "k8s-event-logger-operator.fullname" .)) .Values.webhook.certsSecret.name | trunc 63 | trimSuffix "-" -}} 82 | {{- end -}} 83 | -------------------------------------------------------------------------------- /helm/templates/configmap.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: {{ include "k8s-event-logger-operator.fullname" . }} 5 | namespace: {{ .Release.Namespace }} 6 | labels: 7 | {{- include "k8s-event-logger-operator.labels" . | nindent 4 }} 8 | data: 9 | container_template.yaml: | 10 | resources: 11 | {{- toYaml .Values.eventLogger.resources | nindent 6 }} 12 | livenessProbe: 13 | httpGet: 14 | path: /healthz 15 | port: 8081 16 | initialDelaySeconds: 15 17 | periodSeconds: 20 18 | readinessProbe: 19 | httpGet: 20 | path: /readyz 21 | port: 8081 22 | initialDelaySeconds: 5 23 | periodSeconds: 10 24 | {{- with .Values.eventLogger.securityContext }} 25 | securityContext: 26 | {{- toYaml . | nindent 6 }} 27 | {{- end }} 28 | -------------------------------------------------------------------------------- /helm/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ include "k8s-event-logger-operator.fullname" . }} 5 | namespace: {{ .Release.Namespace }} 6 | labels: 7 | {{- include "k8s-event-logger-operator.labels" . | nindent 4 }} 8 | spec: 9 | replicas: 1 10 | revisionHistoryLimit: {{ .Values.revisionHistoryLimit }} 11 | selector: 12 | matchLabels: 13 | {{- include "k8s-event-logger-operator.selectorLabels" . | nindent 6 }} 14 | template: 15 | metadata: 16 | labels: 17 | {{- include "k8s-event-logger-operator.selectorLabels" . | nindent 8 }} 18 | {{- with .Values.extraPodLabels }} 19 | {{- toYaml . | nindent 8 }} 20 | {{- end }} 21 | {{- with .Values.extraPodAnnotations }} 22 | annotations: 23 | {{- toYaml . | nindent 8 }} 24 | {{- end }} 25 | spec: 26 | {{- with .Values.image.imagePullSecrets }} 27 | imagePullSecrets: 28 | {{- toYaml . | nindent 8 }} 29 | {{- end }} 30 | serviceAccountName: {{ include "k8s-event-logger-operator.serviceAccountName" . }} 31 | containers: 32 | - name: {{ .Chart.Name }} 33 | image: '{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}' 34 | imagePullPolicy: {{ .Values.image.pullPolicy }} 35 | command: 36 | - /opt/go/k8s-event-logger 37 | args: 38 | - '--enable-leader-election={{ .Values.eventLogger.leaderElection }}' 39 | env: 40 | - name: OPERATOR_NAME 41 | value: {{ include "k8s-event-logger-operator.fullname" . }} 42 | - name: 'WATCH_NAMESPACE' 43 | value: '' 44 | - name: 'POD_NAME' 45 | valueFrom: 46 | fieldRef: 47 | apiVersion: v1 48 | fieldPath: metadata.name 49 | - name: 'POD_NAMESPACE' 50 | valueFrom: 51 | fieldRef: 52 | apiVersion: v1 53 | fieldPath: metadata.namespace 54 | - name: 'CONFIG_MAP_NAME' 55 | value: '{{ include "k8s-event-logger-operator.fullname" . }}' 56 | - name: 'ENABLE_WEBHOOKS' 57 | value: '{{ .Values.webhook.enabled }}' 58 | {{- if .Values.eventLogger.leaderElectionResourceLock }} 59 | - name: LEADER_ELECTION_RESOURCE_LOCK 60 | value: {{ .Values.eventLogger.leaderElectionResourceLock }} 61 | {{- end }} 62 | resources: 63 | {{- toYaml .Values.resources | nindent 12 }} 64 | livenessProbe: 65 | httpGet: 66 | path: /healthz 67 | port: 8081 68 | initialDelaySeconds: 15 69 | periodSeconds: 20 70 | readinessProbe: 71 | httpGet: 72 | path: /readyz 73 | port: 8081 74 | initialDelaySeconds: 5 75 | periodSeconds: 10 76 | ports: 77 | - containerPort: 8080 78 | name: metrics 79 | {{- if .Values.webhook.enabled }} 80 | - containerPort: 9443 81 | name: webhook 82 | {{- end }} 83 | {{- if .Values.webhook.enabled }} 84 | volumeMounts: 85 | - mountPath: /opt/go/certs 86 | name: 'webhook-certs' 87 | readOnly: true 88 | {{- end }} 89 | {{- with .Values.securityContext }} 90 | securityContext: 91 | {{- toYaml . | nindent 12 }} 92 | {{- end }} 93 | {{- with .Values.nodeSelector }} 94 | nodeSelector: 95 | {{- toYaml . | nindent 8 }} 96 | {{- end }} 97 | {{- with .Values.affinity }} 98 | affinity: 99 | {{- toYaml . | nindent 8 }} 100 | {{- end }} 101 | {{- with .Values.tolerations }} 102 | tolerations: 103 | {{- toYaml . | nindent 8 }} 104 | {{- end }} 105 | {{- if .Values.webhook.enabled }} 106 | volumes: 107 | - name: 'webhook-certs' 108 | secret: 109 | defaultMode: 420 110 | secretName: {{ include "k8s-event-logger-operator.webhookCertSecretName" . }} 111 | {{- end }} 112 | -------------------------------------------------------------------------------- /helm/templates/rbac/clusterrole.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.rbac.create -}} 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: {{ include "k8s-event-logger-operator.roleName" . }} 6 | labels: 7 | {{ include "k8s-event-logger-operator.labels" . | nindent 4 }} 8 | rules: 9 | - apiGroups: 10 | - "" 11 | resources: 12 | - pods 13 | - secrets 14 | - serviceaccounts 15 | verbs: 16 | - '*' 17 | - apiGroups: 18 | - "" 19 | resources: 20 | - events 21 | verbs: 22 | - get 23 | - list 24 | - watch 25 | - apiGroups: 26 | - rbac.authorization.k8s.io 27 | resources: 28 | - roles 29 | - rolebindings 30 | verbs: 31 | - '*' 32 | - apiGroups: 33 | - eventlogger.bakito.ch 34 | resources: 35 | - '*' 36 | verbs: 37 | - '*' 38 | - apiGroups: 39 | - "" 40 | resources: 41 | - configmaps 42 | verbs: 43 | - get 44 | - list 45 | - watch 46 | {{- end -}} 47 | -------------------------------------------------------------------------------- /helm/templates/rbac/clusterrole_aggregation.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.rbac.create -}} 2 | # extend edit and view cluster roles 3 | # see documentation: https://kubernetes.io/docs/reference/access-authn-authz/rbac/#user-facing-roles 4 | 5 | apiVersion: rbac.authorization.k8s.io/v1 6 | kind: ClusterRole 7 | metadata: 8 | name: eventlogger-bakito-ch-edit 9 | labels: 10 | rbac.authorization.k8s.io/aggregate-to-edit: "true" 11 | {{ include "k8s-event-logger-operator.labels" . | nindent 4 }} 12 | rules: 13 | - apiGroups: 14 | - eventlogger.bakito.ch 15 | resources: 16 | - eventloggers 17 | verbs: 18 | - create 19 | - delete 20 | - get 21 | - list 22 | - patch 23 | - update 24 | - watch 25 | 26 | --- 27 | 28 | apiVersion: rbac.authorization.k8s.io/v1 29 | kind: ClusterRole 30 | metadata: 31 | name: eventlogger-bakito-ch-view 32 | labels: 33 | rbac.authorization.k8s.io/aggregate-to-view: "true" 34 | {{ include "k8s-event-logger-operator.labels" . | nindent 4 }} 35 | rules: 36 | - apiGroups: 37 | - eventlogger.bakito.ch 38 | resources: 39 | - eventloggers 40 | verbs: 41 | - get 42 | - list 43 | - watch 44 | {{- end -}} -------------------------------------------------------------------------------- /helm/templates/rbac/clusterrole_binding.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.rbac.create -}} 2 | kind: ClusterRoleBinding 3 | apiVersion: rbac.authorization.k8s.io/v1 4 | metadata: 5 | name: {{ include "k8s-event-logger-operator.roleName" . }} 6 | labels: 7 | {{ include "k8s-event-logger-operator.labels" . | nindent 4 }} 8 | subjects: 9 | - kind: ServiceAccount 10 | name: {{ include "k8s-event-logger-operator.serviceAccountName" . }} 11 | namespace: {{ .Release.Namespace }} 12 | roleRef: 13 | kind: ClusterRole 14 | name: {{ include "k8s-event-logger-operator.roleName" . }} 15 | apiGroup: rbac.authorization.k8s.io 16 | {{- end -}} 17 | -------------------------------------------------------------------------------- /helm/templates/rbac/role.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.rbac.create -}} 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: Role 4 | metadata: 5 | name: {{ include "k8s-event-logger-operator.roleName" . }} 6 | labels: 7 | {{ include "k8s-event-logger-operator.labels" . | nindent 4 }} 8 | rules: 9 | - apiGroups: 10 | - "" 11 | resources: 12 | - configmaps/status 13 | verbs: 14 | - get 15 | - update 16 | - patch 17 | - apiGroups: 18 | - apps 19 | resources: 20 | - deployments 21 | - replicasets 22 | verbs: 23 | - '*' 24 | - apiGroups: 25 | - apps 26 | resourceNames: 27 | - {{ include "k8s-event-logger-operator.fullname" . }} 28 | resources: 29 | - deployments/finalizers 30 | verbs: 31 | - update 32 | - apiGroups: 33 | - rbac.authorization.k8s.io 34 | resources: 35 | - roles 36 | verbs: 37 | - list 38 | - apiGroups: 39 | - monitoring.coreos.com 40 | resources: 41 | - servicemonitors 42 | verbs: 43 | - get 44 | - create 45 | - apiGroups: 46 | - eventlogger.bakito.ch 47 | resources: 48 | - '*' 49 | verbs: 50 | - '*' 51 | # start leader election 52 | - apiGroups: 53 | - "" 54 | resources: 55 | - configmaps 56 | verbs: 57 | - get 58 | - list 59 | - watch 60 | - create 61 | - update 62 | - patch 63 | - delete 64 | - apiGroups: 65 | - coordination.k8s.io 66 | resources: 67 | - leases 68 | verbs: 69 | - get 70 | - list 71 | - watch 72 | - create 73 | - update 74 | - patch 75 | - delete 76 | - apiGroups: 77 | - "" 78 | resources: 79 | - events 80 | verbs: 81 | - create 82 | - patch 83 | # end leader election 84 | {{- end -}} -------------------------------------------------------------------------------- /helm/templates/rbac/role_binding.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.rbac.create -}} 2 | kind: RoleBinding 3 | apiVersion: rbac.authorization.k8s.io/v1 4 | metadata: 5 | name: {{ include "k8s-event-logger-operator.roleName" . }} 6 | labels: 7 | {{ include "k8s-event-logger-operator.labels" . | nindent 4 }} 8 | subjects: 9 | - kind: ServiceAccount 10 | name: {{ include "k8s-event-logger-operator.serviceAccountName" . }} 11 | roleRef: 12 | kind: Role 13 | name: {{ include "k8s-event-logger-operator.roleName" . }} 14 | apiGroup: rbac.authorization.k8s.io 15 | {{- end -}} 16 | -------------------------------------------------------------------------------- /helm/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "k8s-event-logger-operator.fullname" . }} 5 | labels: 6 | {{- include "k8s-event-logger-operator.labels" . | nindent 4 }} 7 | {{- if .Values.webhook.openShiftServiceCert.enabled }} 8 | annotations: 9 | service.beta.openshift.io/serving-cert-secret-name: {{ include "k8s-event-logger-operator.webhookCertSecretName" . }} 10 | {{- end }} 11 | namespace: {{ .Release.Namespace }} 12 | spec: 13 | ports: 14 | {{- if .Values.webhook.enabled }} 15 | - name: webhook 16 | port: 443 17 | targetPort: webhook 18 | {{- end }} 19 | - name: metrics 20 | protocol: TCP 21 | port: 8080 22 | targetPort: metrics 23 | selector: 24 | {{- include "k8s-event-logger-operator.selectorLabels" . | nindent 6 }} 25 | -------------------------------------------------------------------------------- /helm/templates/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.serviceAccount.create -}} 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: {{ include "k8s-event-logger-operator.serviceAccountName" . }} 6 | labels: 7 | {{ include "k8s-event-logger-operator.labels" . | nindent 4 }} 8 | {{- end -}} 9 | -------------------------------------------------------------------------------- /helm/templates/webhook/certificate.yaml: -------------------------------------------------------------------------------- 1 | {{- if and (.Values.webhook.enabled) (.Values.webhook.certManager.enabled) -}} 2 | apiVersion: cert-manager.io/v1 3 | kind: Issuer 4 | metadata: 5 | name: {{ include "k8s-event-logger-operator.fullname" . }} 6 | namespace: {{ .Release.Namespace }} 7 | labels: 8 | {{- include "k8s-event-logger-operator.labels" . | nindent 4 }} 9 | spec: 10 | selfSigned: {} 11 | --- 12 | apiVersion: cert-manager.io/v1 13 | kind: Certificate 14 | metadata: 15 | name: {{ include "k8s-event-logger-operator.fullname" . }} 16 | namespace: {{ .Release.Namespace }} 17 | labels: 18 | {{- include "k8s-event-logger-operator.labels" . | nindent 4 }} 19 | spec: 20 | dnsNames: 21 | - {{ include "k8s-event-logger-operator.fullname" . }}.{{ .Release.Namespace }}.svc 22 | - {{ include "k8s-event-logger-operator.fullname" . }}.{{ .Release.Namespace }}.svc.cluster.local 23 | issuerRef: 24 | kind: Issuer 25 | name: {{ include "k8s-event-logger-operator.fullname" . }} 26 | secretName: {{ include "k8s-event-logger-operator.webhookCertSecretName" . }} 27 | {{- end -}} -------------------------------------------------------------------------------- /helm/templates/webhook/validating.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.webhook.enabled -}} 2 | apiVersion: admissionregistration.k8s.io/v1 3 | kind: ValidatingWebhookConfiguration 4 | metadata: 5 | name: eventlogger.bakito.ch 6 | {{- if .Values.webhook.openShiftServiceCert.enabled }} 7 | annotations: 8 | service.beta.openshift.io/inject-cabundle: "true" 9 | {{- end }} 10 | webhooks: 11 | - admissionReviewVersions: 12 | - v1 13 | - v1beta1 14 | clientConfig: 15 | caBundle: {{ .Values.webhook.caBundle }} 16 | service: 17 | name: {{ include "k8s-event-logger-operator.fullname" . }} 18 | namespace: {{ .Release.Namespace }} 19 | path: /validate-eventlogger-bakito-ch-v1-eventlogger 20 | failurePolicy: Fail 21 | name: veventlogger.bakito.ch 22 | rules: 23 | - apiGroups: 24 | - eventlogger.bakito.ch 25 | apiVersions: 26 | - v1 27 | operations: 28 | - CREATE 29 | - UPDATE 30 | resources: 31 | - eventloggers 32 | sideEffects: None 33 | {{- end -}} -------------------------------------------------------------------------------- /helm/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values for k8s-event-logger-operator. 2 | # This is a YAML-formatted file. 3 | # Declare variables to be passed into your templates. 4 | 5 | image: 6 | # -- Repository to use 7 | repository: ghcr.io/bakito/k8s-event-logger 8 | # -- Overrides the image tag (default is the chart appVersion) 9 | tag: 10 | # -- Image pull policy 11 | pullPolicy: IfNotPresent 12 | # -- Image pull secrets 13 | imagePullSecrets: { } 14 | # -- Additional command args 15 | 16 | eventLogger: 17 | # -- Enable leader election for the controller 18 | leaderElection: true 19 | # -- Leader election lock type 20 | leaderElectionResourceLock: 21 | # -- Resource limits and requests for the logger pods. 22 | resources: 23 | requests: 24 | cpu: 100m 25 | memory: 64Mi 26 | limits: 27 | cpu: 200m 28 | memory: 256Mi 29 | # -- Security Context for the logger pods. 30 | securityContext: {} 31 | 32 | logging: # see https://github.com/operator-framework/operator-sdk/blob/master/doc/user/logging.md 33 | # -- Log level 34 | level: info 35 | # -- Log time encoding 36 | timeEncoding: iso8601 37 | 38 | webhook: 39 | # -- Specifies whether validation webhook should be created. 40 | enabled: false 41 | 42 | certManager: 43 | # -- Enable cert manager setup 44 | enabled: false 45 | 46 | openShiftServiceCert: 47 | # -- Enable OpenShift service certificate 48 | enabled: false 49 | certsSecret: 50 | # -- Certificate secret name 51 | name: 52 | # -- certificate ca bundle 53 | caBundle: Cg== 54 | 55 | # -- String to partially override "argo-rollouts.fullname" template 56 | nameOverride: "" 57 | 58 | # -- String to fully override "argo-rollouts.fullname" template 59 | fullnameOverride: "" 60 | 61 | serviceAccount: 62 | # -- Specifies whether a service account should be created 63 | create: true 64 | # -- The name of the service account to use. 65 | # If not set and create is true, a name is generated using the fullname template 66 | name: 67 | 68 | rbac: 69 | # -- Specifies whether rbac should be created 70 | create: true 71 | # -- The name of the role and clusterrole to use. 72 | # If not set and create is true, a name is generated using the fullname template 73 | roleName: 74 | 75 | # -- Number of old deployment ReplicaSets to retain. The rest will be garbage collected. 76 | revisionHistoryLimit: 3 77 | 78 | # -- Resource limits and requests for the operator pods. 79 | resources: 80 | requests: 81 | cpu: 100m 82 | memory: 128Mi 83 | limits: 84 | cpu: 200m 85 | memory: 512Mi 86 | 87 | # -- Security Context for the operator pods. 88 | securityContext: { } 89 | 90 | # -- [Node selector] 91 | nodeSelector: { } 92 | 93 | # -- [Tolerations] for use with node taints 94 | tolerations: [ ] 95 | 96 | # -- Assign custom [affinity] rules to the deployment 97 | affinity: { } 98 | 99 | # -- Add additional pod [annotations] 100 | extraPodAnnotations: { } 101 | 102 | # -- Add additional pod [labels] 103 | extraPodLabels: { } 104 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /* 2 | 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 | "fmt" 22 | "os" 23 | gr "runtime" 24 | 25 | eventloggerv1 "github.com/bakito/k8s-event-logger-operator/api/v1" 26 | "github.com/bakito/k8s-event-logger-operator/controllers/config" 27 | "github.com/bakito/k8s-event-logger-operator/controllers/logging" 28 | "github.com/bakito/k8s-event-logger-operator/controllers/setup" 29 | cnst "github.com/bakito/k8s-event-logger-operator/pkg/constants" 30 | "github.com/bakito/k8s-event-logger-operator/version" 31 | "github.com/bakito/operator-utils/pkg/pprof" 32 | "github.com/go-logr/zapr" 33 | zap2 "go.uber.org/zap" 34 | "go.uber.org/zap/zapcore" 35 | corev1 "k8s.io/api/core/v1" 36 | "k8s.io/apimachinery/pkg/runtime" 37 | utilruntime "k8s.io/apimachinery/pkg/util/runtime" 38 | clientgoscheme "k8s.io/client-go/kubernetes/scheme" 39 | _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" 40 | "k8s.io/klog/v2" 41 | ctrl "sigs.k8s.io/controller-runtime" 42 | crtlcache "sigs.k8s.io/controller-runtime/pkg/cache" 43 | "sigs.k8s.io/controller-runtime/pkg/healthz" 44 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 45 | "sigs.k8s.io/controller-runtime/pkg/metrics/server" 46 | "sigs.k8s.io/controller-runtime/pkg/webhook" 47 | ) 48 | 49 | var ( 50 | scheme = runtime.NewScheme() 51 | setupLog = ctrl.Log.WithName("setup") 52 | ) 53 | 54 | func init() { 55 | utilruntime.Must(clientgoscheme.AddToScheme(scheme)) 56 | utilruntime.Must(eventloggerv1.AddToScheme(scheme)) 57 | utilruntime.Must(corev1.AddToScheme(scheme)) 58 | // +kubebuilder:scaffold:scheme 59 | } 60 | 61 | func main() { 62 | var metricsAddr string 63 | var healthAddr string 64 | var profilingAddr string 65 | var configName string 66 | var enableLeaderElection bool 67 | var enableLoggerMode bool 68 | var enableProfiling bool 69 | flag.StringVar( 70 | &metricsAddr, 71 | cnst.ArgMetricsAddr, 72 | cnst.DefaultMetricsAddr, 73 | "The address the metric endpoint binds to.", 74 | ) 75 | flag.StringVar(&healthAddr, cnst.ArgHealthAddr, cnst.DefaultHealthAddr, "The address the health endpoint binds to.") 76 | flag.StringVar( 77 | &profilingAddr, 78 | cnst.ArgProfilingAddr, 79 | cnst.DefaultProfilingAddr, 80 | "The address the profiling endpoint binds to.", 81 | ) 82 | flag.BoolVar( 83 | &enableLeaderElection, 84 | cnst.ArgEnableLeaderElection, 85 | false, 86 | "Enable leader election for controller manager. Enabling this will ensure there is only one active controller manager.", 87 | ) 88 | flag.BoolVar(&enableLoggerMode, cnst.ArgEnableLoggerMode, false, 89 | "Enable logger mode. Enabling this will only log events of the current namespace.") 90 | flag.BoolVar(&enableProfiling, cnst.ArgEnableProfiling, false, "Enable profiling endpoint.") 91 | 92 | flag.StringVar(&configName, cnst.ArgConfigName, "", 93 | "The name of the eventlogger config to work with.") 94 | flag.Parse() 95 | 96 | o := func(o *zap.Options) { 97 | o.DestWriter = os.Stderr 98 | o.Development = false 99 | encCfg := zap2.NewProductionEncoderConfig() 100 | encCfg.EncodeTime = zapcore.ISO8601TimeEncoder 101 | o.Encoder = zapcore.NewJSONEncoder(encCfg) 102 | } 103 | ctrl.SetLogger(zapr.NewLogger(zap.NewRaw(o))) 104 | klog.SetLogger(ctrl.Log) 105 | 106 | printVersion() 107 | 108 | watchNamespace := os.Getenv(cnst.EnvWatchNamespace) 109 | podNamespace := os.Getenv(cnst.EnvPodNamespace) 110 | 111 | defaultNamespaces := map[string]crtlcache.Config{ 112 | watchNamespace: {}, 113 | } 114 | if watchNamespace == "" { 115 | defaultNamespaces = nil 116 | } 117 | 118 | mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ 119 | Scheme: scheme, 120 | Metrics: server.Options{ 121 | BindAddress: metricsAddr, 122 | }, 123 | WebhookServer: webhook.NewServer(webhook.Options{ 124 | Port: 9443, 125 | CertDir: "certs", 126 | }), 127 | LeaderElection: enableLeaderElection && !enableLoggerMode, 128 | LeaderElectionID: "leader.eventlogger.bakito.ch", 129 | LeaderElectionResourceLock: os.Getenv(cnst.EnvLeaderElectionResourceLock), 130 | HealthProbeBindAddress: healthAddr, 131 | LeaderElectionReleaseOnCancel: true, 132 | 133 | Cache: crtlcache.Options{ 134 | DefaultNamespaces: defaultNamespaces, 135 | }, 136 | }) 137 | if err != nil { 138 | setupLog.Error(err, "unable to start manager") 139 | os.Exit(1) 140 | } 141 | 142 | if enableLoggerMode { 143 | setupLog.WithValues("configName", configName).Info("Current configuration") 144 | if err = (&logging.Reconciler{ 145 | Client: mgr.GetClient(), 146 | Log: ctrl.Log.WithName("controllers").WithName("Event"), 147 | Scheme: mgr.GetScheme(), 148 | Config: logging.ConfigFor(configName, podNamespace, watchNamespace), 149 | LoggerMode: true, 150 | }).SetupWithManager(mgr, watchNamespace); err != nil { 151 | setupLog.Error(err, "unable to create controller", "controller", "Event") 152 | os.Exit(1) 153 | } 154 | } else { 155 | // Setup all Controllers 156 | if watchNamespace == "" { 157 | cr := &config.Reconciler{ 158 | Reader: mgr.GetAPIReader(), 159 | Log: ctrl.Log.WithName("controllers").WithName("Config"), 160 | Scheme: mgr.GetScheme(), 161 | } 162 | if err = (cr).SetupWithManager(mgr); err != nil { 163 | setupLog.Error(err, "unable to create controller", "controller", "Config") 164 | os.Exit(1) 165 | } 166 | if err = (&setup.Reconciler{ 167 | Client: mgr.GetClient(), 168 | Log: ctrl.Log.WithName("controllers").WithName("EventLogger"), 169 | Scheme: mgr.GetScheme(), 170 | Config: cr.Ctx(), 171 | }).SetupWithManager(mgr); err != nil { 172 | setupLog.Error(err, "unable to create controller", "controller", "EventLogger") 173 | os.Exit(1) 174 | } 175 | setupLog.Info("Running in global mode.") 176 | 177 | if os.Getenv(cnst.EnvEnableWebhook) != "false" { 178 | if err = (&eventloggerv1.EventLogger{}).SetupWebhookWithManager(mgr); err != nil { 179 | setupLog.Error(err, "unable to create webhook", "webhook", "EventLogger") 180 | os.Exit(1) 181 | } 182 | } 183 | } else { 184 | if err = (&logging.Reconciler{ 185 | Client: mgr.GetClient(), 186 | Log: ctrl.Log.WithName("controllers").WithName("Event"), 187 | Scheme: mgr.GetScheme(), 188 | Config: logging.ConfigFor(configName, podNamespace, watchNamespace), 189 | LoggerMode: false, 190 | }).SetupWithManager(mgr, watchNamespace); err != nil { 191 | setupLog.Error(err, "unable to create controller", "controller", "Event") 192 | os.Exit(1) 193 | } 194 | setupLog.WithValues("namespace", watchNamespace).Info("Running in single namespace mode.") 195 | } 196 | } 197 | // +kubebuilder:scaffold:builder 198 | 199 | if enableProfiling { 200 | if err = mgr.Add(pprof.New(profilingAddr)); err != nil { 201 | setupLog.Error(err, "unable to create pprof service") 202 | os.Exit(1) 203 | } 204 | } 205 | 206 | if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { 207 | setupLog.Error(err, "unable to set up health check") 208 | os.Exit(1) 209 | } 210 | if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil { 211 | setupLog.Error(err, "unable to set up ready check") 212 | os.Exit(1) 213 | } 214 | 215 | setupLog.Info("starting manager") 216 | if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { 217 | setupLog.Error(err, "problem running manager") 218 | os.Exit(1) 219 | } 220 | } 221 | 222 | func printVersion() { 223 | setupLog.Info(fmt.Sprintf("Operator Version: %s", version.Version)) 224 | setupLog.Info(fmt.Sprintf("Go Version: %s", gr.Version())) 225 | setupLog.Info(fmt.Sprintf("Go OS/Arch: %s/%s", gr.GOOS, gr.GOARCH)) 226 | } 227 | -------------------------------------------------------------------------------- /pkg/constants/constants.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | const ( 4 | 5 | // EnvLeaderElectionResourceLock leader election release lock mode 6 | EnvLeaderElectionResourceLock = "LEADER_ELECTION_RESOURCE_LOCK" 7 | 8 | // ArgEnableLoggerMode enable logger mode 9 | ArgEnableLoggerMode = "enable-logger-mode" 10 | 11 | // ArgConfigName name of the config 12 | ArgConfigName = "config-name" 13 | 14 | // ArgMetricsAddr metrics address 15 | ArgMetricsAddr = "metrics-addr" 16 | 17 | // DefaultMetricsAddr default metrics address 18 | DefaultMetricsAddr = ":8080" 19 | 20 | // ArgHealthAddr health address 21 | ArgHealthAddr = "health-addr" 22 | 23 | // DefaultHealthAddr default health address 24 | DefaultHealthAddr = ":8081" 25 | 26 | // ArgProfilingAddr profiling address 27 | ArgProfilingAddr = "profiling-addr" 28 | 29 | // DefaultProfilingAddr default profiling address 30 | DefaultProfilingAddr = ":8082" 31 | 32 | // ArgEnableLeaderElection enable leader election 33 | ArgEnableLeaderElection = "enable-leader-election" 34 | 35 | // ArgEnableProfiling enable profiling 36 | ArgEnableProfiling = "enable-profiling" 37 | 38 | // EnvWatchNamespace watch namespace env variable 39 | EnvWatchNamespace = "WATCH_NAMESPACE" 40 | 41 | // EnvEventLoggerImage env variable name for the image if the event logger 42 | EnvEventLoggerImage = "EVENT_LOGGER_IMAGE" 43 | 44 | // EnvEnableWebhook enable webhooks 45 | EnvEnableWebhook = "ENABLE_WEBHOOKS" 46 | 47 | // EnvPodName the name the pod 48 | EnvPodName = "POD_NAME" 49 | 50 | // EnvPodNamespace the namespace the pod 51 | EnvPodNamespace = "POD_NAMESPACE" 52 | 53 | // EnvConfigMapName the name of the configmap 54 | EnvConfigMapName = "CONFIG_MAP_NAME" 55 | 56 | // ConfigKeyContainerTemplate pod template config key 57 | ConfigKeyContainerTemplate = "container_template.yaml" 58 | ) 59 | -------------------------------------------------------------------------------- /pkg/filter/filter.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | import ( 4 | "strings" 5 | 6 | corev1 "k8s.io/api/core/v1" 7 | ) 8 | 9 | // Filter is an event filters 10 | type Filter interface { 11 | // Match checks if a Event matches the filter 12 | Match(*corev1.Event) bool 13 | // Equals compares the Filter with another 14 | Equals(Filter) bool 15 | // String returns the description of the Filter 16 | String() string 17 | } 18 | 19 | // New creates a new Filter from a filter function: func(*corev1.Event) bool 20 | func New(f func(*corev1.Event) bool, description string) Filter { 21 | return &Func{Func: f, Description: description} 22 | } 23 | 24 | // Func is a generic Filter 25 | type Func struct { 26 | Func func(*corev1.Event) bool 27 | Description string 28 | } 29 | 30 | // Match implements Filter interface 31 | func (f *Func) Match(e *corev1.Event) bool { 32 | return f.Func(e) 33 | } 34 | 35 | // Equals implements Filter interface 36 | func (f *Func) Equals(o Filter) bool { 37 | return f.String() == o.String() 38 | } 39 | 40 | func (f *Func) String() string { 41 | return f.Description 42 | } 43 | 44 | // Never is a filter that never matches 45 | var Never = &Func{ 46 | Func: func(_ *corev1.Event) bool { 47 | return false 48 | }, 49 | Description: "false", 50 | } 51 | 52 | // Always is a filter that always matches 53 | var Always = &Func{ 54 | Func: func(_ *corev1.Event) bool { 55 | return true 56 | }, 57 | Description: "true", 58 | } 59 | 60 | // Slice is a slice of Filter 61 | type Slice []Filter 62 | 63 | // Any creates a new Filter which checks if least one Filter in the Slice matches (if the Slice is empty this is equivalent to Never) 64 | func (s Slice) Any() Filter { 65 | return &Func{ 66 | Func: func(e *corev1.Event) bool { 67 | for _, filter := range s { 68 | if filter.Match(e) { 69 | return true 70 | } 71 | } 72 | return false 73 | }, 74 | Description: "( " + strings.Join(s.toStringSlice(), " OR ") + " )", 75 | } 76 | } 77 | 78 | // All creates a new Filter which checks if all Filter in the Slice matches (if the Slice is empty this is equivalent to Always) 79 | func (s Slice) All() Filter { 80 | return &Func{ 81 | Func: func(e *corev1.Event) bool { 82 | for _, filter := range s { 83 | if !filter.Match(e) { 84 | return false 85 | } 86 | } 87 | return true 88 | }, 89 | Description: "( " + strings.Join(s.toStringSlice(), " AND ") + " )", 90 | } 91 | } 92 | 93 | // toStringSlice creates a slice with the descriptions of all the Filter in Slice 94 | func (s Slice) toStringSlice() []string { 95 | var descriptions []string 96 | 97 | for _, filter := range s { 98 | descriptions = append(descriptions, filter.String()) 99 | } 100 | 101 | return descriptions 102 | } 103 | -------------------------------------------------------------------------------- /pkg/filter/filter_suite_test.go: -------------------------------------------------------------------------------- 1 | package filter_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestFilter(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Filter Suite") 13 | } 14 | -------------------------------------------------------------------------------- /pkg/filter/filter_test.go: -------------------------------------------------------------------------------- 1 | package filter_test 2 | 3 | import ( 4 | . "github.com/bakito/k8s-event-logger-operator/pkg/filter" 5 | . "github.com/onsi/ginkgo/v2" 6 | . "github.com/onsi/gomega" 7 | corev1 "k8s.io/api/core/v1" 8 | ) 9 | 10 | var _ = Describe("V1", func() { 11 | Context("Filter", func() { 12 | It("should match always", func() { 13 | Ω(Always.Match(&corev1.Event{})).Should(BeTrue()) 14 | Ω(Always.Match(nil)).Should(BeTrue()) 15 | Ω(Always.String()).Should(Equal("true")) 16 | }) 17 | It("should match never", func() { 18 | Ω(Never.Match(&corev1.Event{})).Should(BeFalse()) 19 | Ω(Never.Match(nil)).Should(BeFalse()) 20 | Ω(Never.String()).Should(Equal("false")) 21 | }) 22 | It("should match with func", func() { 23 | description := "type =='Bar'" 24 | filter := New(func(event *corev1.Event) bool { 25 | return event.Type == "Bar" 26 | }, description) 27 | 28 | Ω(filter.Match(&corev1.Event{Type: "Foo"})).Should(BeFalse()) 29 | Ω(filter.Match(&corev1.Event{Type: "Bar"})).Should(BeTrue()) 30 | Ω(filter.String()).Should(Equal(description)) 31 | }) 32 | It("should match all", func() { 33 | Ω(Slice{Always, Always, Always}.All().Match(&corev1.Event{})).Should(BeTrue()) 34 | Ω(Slice{Always}.All().Match(&corev1.Event{})).Should(BeTrue()) 35 | Ω(Slice{}.All().Match(&corev1.Event{})).Should(BeTrue()) 36 | Ω(Slice{Never, Always}.All().Match(&corev1.Event{})).Should(BeFalse()) 37 | Ω(Slice{Never, Always, Never}.All().String()).Should(Equal("( false AND true AND false )")) 38 | }) 39 | It("should match any", func() { 40 | Ω(Slice{Never, Always, Never}.Any().Match(&corev1.Event{})).Should(BeTrue()) 41 | Ω(Slice{Always}.Any().Match(&corev1.Event{})).Should(BeTrue()) 42 | Ω(Slice{}.Any().Match(&corev1.Event{})).Should(BeFalse()) 43 | Ω(Slice{Never}.Any().Match(&corev1.Event{})).Should(BeFalse()) 44 | Ω(Slice{Never, Never}.Any().Match(&corev1.Event{})).Should(BeFalse()) 45 | Ω(Slice{Never, Always, Never}.Any().String()).Should(Equal("( false OR true OR false )")) 46 | }) 47 | It("should match nested slice", func() { 48 | filter := Slice{Slice{Never, Always}.Any(), Slice{Always, Always}.All()}.All() 49 | Ω(filter.Match(&corev1.Event{})).Should(BeTrue()) 50 | Ω(filter.String()).Should(Equal("( ( false OR true ) AND ( true AND true ) )")) 51 | }) 52 | It("same filters should be equal", func() { 53 | filter1 := Slice{Slice{Never, Always}.Any(), Slice{Always, Always}.All()}.All() 54 | filter2 := Slice{Slice{Never, Always}.Any(), Slice{Always, Always}.All()}.All() 55 | Ω(filter1.Equals(filter2)).Should(BeTrue()) 56 | }) 57 | It("different filters should be equal", func() { 58 | filter1 := Slice{Slice{Never, Always}.Any(), Slice{Always, Never}.All()}.All() 59 | filter2 := Slice{Slice{Never, Always}.Any(), Slice{Always, Always}.All()}.All() 60 | Ω(filter1.Equals(filter2)).Should(BeFalse()) 61 | }) 62 | }) 63 | }) 64 | -------------------------------------------------------------------------------- /pkg/mocks/client/mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: sigs.k8s.io/controller-runtime/pkg/client (interfaces: Client) 3 | // 4 | // Generated by this command: 5 | // 6 | // mockgen -destination pkg/mocks/client/mock.go sigs.k8s.io/controller-runtime/pkg/client Client 7 | // 8 | 9 | // Package mock_client is a generated GoMock package. 10 | package mock_client 11 | 12 | import ( 13 | context "context" 14 | reflect "reflect" 15 | 16 | gomock "go.uber.org/mock/gomock" 17 | meta "k8s.io/apimachinery/pkg/api/meta" 18 | runtime "k8s.io/apimachinery/pkg/runtime" 19 | schema "k8s.io/apimachinery/pkg/runtime/schema" 20 | client "sigs.k8s.io/controller-runtime/pkg/client" 21 | ) 22 | 23 | // MockClient is a mock of Client interface. 24 | type MockClient struct { 25 | ctrl *gomock.Controller 26 | recorder *MockClientMockRecorder 27 | isgomock struct{} 28 | } 29 | 30 | // MockClientMockRecorder is the mock recorder for MockClient. 31 | type MockClientMockRecorder struct { 32 | mock *MockClient 33 | } 34 | 35 | // NewMockClient creates a new mock instance. 36 | func NewMockClient(ctrl *gomock.Controller) *MockClient { 37 | mock := &MockClient{ctrl: ctrl} 38 | mock.recorder = &MockClientMockRecorder{mock} 39 | return mock 40 | } 41 | 42 | // EXPECT returns an object that allows the caller to indicate expected use. 43 | func (m *MockClient) EXPECT() *MockClientMockRecorder { 44 | return m.recorder 45 | } 46 | 47 | // Create mocks base method. 48 | func (m *MockClient) Create(ctx context.Context, obj client.Object, opts ...client.CreateOption) error { 49 | m.ctrl.T.Helper() 50 | varargs := []any{ctx, obj} 51 | for _, a := range opts { 52 | varargs = append(varargs, a) 53 | } 54 | ret := m.ctrl.Call(m, "Create", varargs...) 55 | ret0, _ := ret[0].(error) 56 | return ret0 57 | } 58 | 59 | // Create indicates an expected call of Create. 60 | func (mr *MockClientMockRecorder) Create(ctx, obj any, opts ...any) *gomock.Call { 61 | mr.mock.ctrl.T.Helper() 62 | varargs := append([]any{ctx, obj}, opts...) 63 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockClient)(nil).Create), varargs...) 64 | } 65 | 66 | // Delete mocks base method. 67 | func (m *MockClient) Delete(ctx context.Context, obj client.Object, opts ...client.DeleteOption) error { 68 | m.ctrl.T.Helper() 69 | varargs := []any{ctx, obj} 70 | for _, a := range opts { 71 | varargs = append(varargs, a) 72 | } 73 | ret := m.ctrl.Call(m, "Delete", varargs...) 74 | ret0, _ := ret[0].(error) 75 | return ret0 76 | } 77 | 78 | // Delete indicates an expected call of Delete. 79 | func (mr *MockClientMockRecorder) Delete(ctx, obj any, opts ...any) *gomock.Call { 80 | mr.mock.ctrl.T.Helper() 81 | varargs := append([]any{ctx, obj}, opts...) 82 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockClient)(nil).Delete), varargs...) 83 | } 84 | 85 | // DeleteAllOf mocks base method. 86 | func (m *MockClient) DeleteAllOf(ctx context.Context, obj client.Object, opts ...client.DeleteAllOfOption) error { 87 | m.ctrl.T.Helper() 88 | varargs := []any{ctx, obj} 89 | for _, a := range opts { 90 | varargs = append(varargs, a) 91 | } 92 | ret := m.ctrl.Call(m, "DeleteAllOf", varargs...) 93 | ret0, _ := ret[0].(error) 94 | return ret0 95 | } 96 | 97 | // DeleteAllOf indicates an expected call of DeleteAllOf. 98 | func (mr *MockClientMockRecorder) DeleteAllOf(ctx, obj any, opts ...any) *gomock.Call { 99 | mr.mock.ctrl.T.Helper() 100 | varargs := append([]any{ctx, obj}, opts...) 101 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAllOf", reflect.TypeOf((*MockClient)(nil).DeleteAllOf), varargs...) 102 | } 103 | 104 | // Get mocks base method. 105 | func (m *MockClient) Get(ctx context.Context, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error { 106 | m.ctrl.T.Helper() 107 | varargs := []any{ctx, key, obj} 108 | for _, a := range opts { 109 | varargs = append(varargs, a) 110 | } 111 | ret := m.ctrl.Call(m, "Get", varargs...) 112 | ret0, _ := ret[0].(error) 113 | return ret0 114 | } 115 | 116 | // Get indicates an expected call of Get. 117 | func (mr *MockClientMockRecorder) Get(ctx, key, obj any, opts ...any) *gomock.Call { 118 | mr.mock.ctrl.T.Helper() 119 | varargs := append([]any{ctx, key, obj}, opts...) 120 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockClient)(nil).Get), varargs...) 121 | } 122 | 123 | // GroupVersionKindFor mocks base method. 124 | func (m *MockClient) GroupVersionKindFor(obj runtime.Object) (schema.GroupVersionKind, error) { 125 | m.ctrl.T.Helper() 126 | ret := m.ctrl.Call(m, "GroupVersionKindFor", obj) 127 | ret0, _ := ret[0].(schema.GroupVersionKind) 128 | ret1, _ := ret[1].(error) 129 | return ret0, ret1 130 | } 131 | 132 | // GroupVersionKindFor indicates an expected call of GroupVersionKindFor. 133 | func (mr *MockClientMockRecorder) GroupVersionKindFor(obj any) *gomock.Call { 134 | mr.mock.ctrl.T.Helper() 135 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GroupVersionKindFor", reflect.TypeOf((*MockClient)(nil).GroupVersionKindFor), obj) 136 | } 137 | 138 | // IsObjectNamespaced mocks base method. 139 | func (m *MockClient) IsObjectNamespaced(obj runtime.Object) (bool, error) { 140 | m.ctrl.T.Helper() 141 | ret := m.ctrl.Call(m, "IsObjectNamespaced", obj) 142 | ret0, _ := ret[0].(bool) 143 | ret1, _ := ret[1].(error) 144 | return ret0, ret1 145 | } 146 | 147 | // IsObjectNamespaced indicates an expected call of IsObjectNamespaced. 148 | func (mr *MockClientMockRecorder) IsObjectNamespaced(obj any) *gomock.Call { 149 | mr.mock.ctrl.T.Helper() 150 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsObjectNamespaced", reflect.TypeOf((*MockClient)(nil).IsObjectNamespaced), obj) 151 | } 152 | 153 | // List mocks base method. 154 | func (m *MockClient) List(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error { 155 | m.ctrl.T.Helper() 156 | varargs := []any{ctx, list} 157 | for _, a := range opts { 158 | varargs = append(varargs, a) 159 | } 160 | ret := m.ctrl.Call(m, "List", varargs...) 161 | ret0, _ := ret[0].(error) 162 | return ret0 163 | } 164 | 165 | // List indicates an expected call of List. 166 | func (mr *MockClientMockRecorder) List(ctx, list any, opts ...any) *gomock.Call { 167 | mr.mock.ctrl.T.Helper() 168 | varargs := append([]any{ctx, list}, opts...) 169 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockClient)(nil).List), varargs...) 170 | } 171 | 172 | // Patch mocks base method. 173 | func (m *MockClient) Patch(ctx context.Context, obj client.Object, patch client.Patch, opts ...client.PatchOption) error { 174 | m.ctrl.T.Helper() 175 | varargs := []any{ctx, obj, patch} 176 | for _, a := range opts { 177 | varargs = append(varargs, a) 178 | } 179 | ret := m.ctrl.Call(m, "Patch", varargs...) 180 | ret0, _ := ret[0].(error) 181 | return ret0 182 | } 183 | 184 | // Patch indicates an expected call of Patch. 185 | func (mr *MockClientMockRecorder) Patch(ctx, obj, patch any, opts ...any) *gomock.Call { 186 | mr.mock.ctrl.T.Helper() 187 | varargs := append([]any{ctx, obj, patch}, opts...) 188 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Patch", reflect.TypeOf((*MockClient)(nil).Patch), varargs...) 189 | } 190 | 191 | // RESTMapper mocks base method. 192 | func (m *MockClient) RESTMapper() meta.RESTMapper { 193 | m.ctrl.T.Helper() 194 | ret := m.ctrl.Call(m, "RESTMapper") 195 | ret0, _ := ret[0].(meta.RESTMapper) 196 | return ret0 197 | } 198 | 199 | // RESTMapper indicates an expected call of RESTMapper. 200 | func (mr *MockClientMockRecorder) RESTMapper() *gomock.Call { 201 | mr.mock.ctrl.T.Helper() 202 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RESTMapper", reflect.TypeOf((*MockClient)(nil).RESTMapper)) 203 | } 204 | 205 | // Scheme mocks base method. 206 | func (m *MockClient) Scheme() *runtime.Scheme { 207 | m.ctrl.T.Helper() 208 | ret := m.ctrl.Call(m, "Scheme") 209 | ret0, _ := ret[0].(*runtime.Scheme) 210 | return ret0 211 | } 212 | 213 | // Scheme indicates an expected call of Scheme. 214 | func (mr *MockClientMockRecorder) Scheme() *gomock.Call { 215 | mr.mock.ctrl.T.Helper() 216 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Scheme", reflect.TypeOf((*MockClient)(nil).Scheme)) 217 | } 218 | 219 | // Status mocks base method. 220 | func (m *MockClient) Status() client.SubResourceWriter { 221 | m.ctrl.T.Helper() 222 | ret := m.ctrl.Call(m, "Status") 223 | ret0, _ := ret[0].(client.SubResourceWriter) 224 | return ret0 225 | } 226 | 227 | // Status indicates an expected call of Status. 228 | func (mr *MockClientMockRecorder) Status() *gomock.Call { 229 | mr.mock.ctrl.T.Helper() 230 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Status", reflect.TypeOf((*MockClient)(nil).Status)) 231 | } 232 | 233 | // SubResource mocks base method. 234 | func (m *MockClient) SubResource(subResource string) client.SubResourceClient { 235 | m.ctrl.T.Helper() 236 | ret := m.ctrl.Call(m, "SubResource", subResource) 237 | ret0, _ := ret[0].(client.SubResourceClient) 238 | return ret0 239 | } 240 | 241 | // SubResource indicates an expected call of SubResource. 242 | func (mr *MockClientMockRecorder) SubResource(subResource any) *gomock.Call { 243 | mr.mock.ctrl.T.Helper() 244 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SubResource", reflect.TypeOf((*MockClient)(nil).SubResource), subResource) 245 | } 246 | 247 | // Update mocks base method. 248 | func (m *MockClient) Update(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error { 249 | m.ctrl.T.Helper() 250 | varargs := []any{ctx, obj} 251 | for _, a := range opts { 252 | varargs = append(varargs, a) 253 | } 254 | ret := m.ctrl.Call(m, "Update", varargs...) 255 | ret0, _ := ret[0].(error) 256 | return ret0 257 | } 258 | 259 | // Update indicates an expected call of Update. 260 | func (mr *MockClientMockRecorder) Update(ctx, obj any, opts ...any) *gomock.Call { 261 | mr.mock.ctrl.T.Helper() 262 | varargs := append([]any{ctx, obj}, opts...) 263 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockClient)(nil).Update), varargs...) 264 | } 265 | -------------------------------------------------------------------------------- /pkg/mocks/logr/mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: github.com/go-logr/logr (interfaces: LogSink) 3 | // 4 | // Generated by this command: 5 | // 6 | // mockgen -destination pkg/mocks/logr/mock.go github.com/go-logr/logr LogSink 7 | // 8 | 9 | // Package mock_logr is a generated GoMock package. 10 | package mock_logr 11 | 12 | import ( 13 | reflect "reflect" 14 | 15 | logr "github.com/go-logr/logr" 16 | gomock "go.uber.org/mock/gomock" 17 | ) 18 | 19 | // MockLogSink is a mock of LogSink interface. 20 | type MockLogSink struct { 21 | ctrl *gomock.Controller 22 | recorder *MockLogSinkMockRecorder 23 | isgomock struct{} 24 | } 25 | 26 | // MockLogSinkMockRecorder is the mock recorder for MockLogSink. 27 | type MockLogSinkMockRecorder struct { 28 | mock *MockLogSink 29 | } 30 | 31 | // NewMockLogSink creates a new mock instance. 32 | func NewMockLogSink(ctrl *gomock.Controller) *MockLogSink { 33 | mock := &MockLogSink{ctrl: ctrl} 34 | mock.recorder = &MockLogSinkMockRecorder{mock} 35 | return mock 36 | } 37 | 38 | // EXPECT returns an object that allows the caller to indicate expected use. 39 | func (m *MockLogSink) EXPECT() *MockLogSinkMockRecorder { 40 | return m.recorder 41 | } 42 | 43 | // Enabled mocks base method. 44 | func (m *MockLogSink) Enabled(level int) bool { 45 | m.ctrl.T.Helper() 46 | ret := m.ctrl.Call(m, "Enabled", level) 47 | ret0, _ := ret[0].(bool) 48 | return ret0 49 | } 50 | 51 | // Enabled indicates an expected call of Enabled. 52 | func (mr *MockLogSinkMockRecorder) Enabled(level any) *gomock.Call { 53 | mr.mock.ctrl.T.Helper() 54 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Enabled", reflect.TypeOf((*MockLogSink)(nil).Enabled), level) 55 | } 56 | 57 | // Error mocks base method. 58 | func (m *MockLogSink) Error(err error, msg string, keysAndValues ...any) { 59 | m.ctrl.T.Helper() 60 | varargs := []any{err, msg} 61 | for _, a := range keysAndValues { 62 | varargs = append(varargs, a) 63 | } 64 | m.ctrl.Call(m, "Error", varargs...) 65 | } 66 | 67 | // Error indicates an expected call of Error. 68 | func (mr *MockLogSinkMockRecorder) Error(err, msg any, keysAndValues ...any) *gomock.Call { 69 | mr.mock.ctrl.T.Helper() 70 | varargs := append([]any{err, msg}, keysAndValues...) 71 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Error", reflect.TypeOf((*MockLogSink)(nil).Error), varargs...) 72 | } 73 | 74 | // Info mocks base method. 75 | func (m *MockLogSink) Info(level int, msg string, keysAndValues ...any) { 76 | m.ctrl.T.Helper() 77 | varargs := []any{level, msg} 78 | for _, a := range keysAndValues { 79 | varargs = append(varargs, a) 80 | } 81 | m.ctrl.Call(m, "Info", varargs...) 82 | } 83 | 84 | // Info indicates an expected call of Info. 85 | func (mr *MockLogSinkMockRecorder) Info(level, msg any, keysAndValues ...any) *gomock.Call { 86 | mr.mock.ctrl.T.Helper() 87 | varargs := append([]any{level, msg}, keysAndValues...) 88 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Info", reflect.TypeOf((*MockLogSink)(nil).Info), varargs...) 89 | } 90 | 91 | // Init mocks base method. 92 | func (m *MockLogSink) Init(info logr.RuntimeInfo) { 93 | m.ctrl.T.Helper() 94 | m.ctrl.Call(m, "Init", info) 95 | } 96 | 97 | // Init indicates an expected call of Init. 98 | func (mr *MockLogSinkMockRecorder) Init(info any) *gomock.Call { 99 | mr.mock.ctrl.T.Helper() 100 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Init", reflect.TypeOf((*MockLogSink)(nil).Init), info) 101 | } 102 | 103 | // WithName mocks base method. 104 | func (m *MockLogSink) WithName(name string) logr.LogSink { 105 | m.ctrl.T.Helper() 106 | ret := m.ctrl.Call(m, "WithName", name) 107 | ret0, _ := ret[0].(logr.LogSink) 108 | return ret0 109 | } 110 | 111 | // WithName indicates an expected call of WithName. 112 | func (mr *MockLogSinkMockRecorder) WithName(name any) *gomock.Call { 113 | mr.mock.ctrl.T.Helper() 114 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WithName", reflect.TypeOf((*MockLogSink)(nil).WithName), name) 115 | } 116 | 117 | // WithValues mocks base method. 118 | func (m *MockLogSink) WithValues(keysAndValues ...any) logr.LogSink { 119 | m.ctrl.T.Helper() 120 | varargs := []any{} 121 | for _, a := range keysAndValues { 122 | varargs = append(varargs, a) 123 | } 124 | ret := m.ctrl.Call(m, "WithValues", varargs...) 125 | ret0, _ := ret[0].(logr.LogSink) 126 | return ret0 127 | } 128 | 129 | // WithValues indicates an expected call of WithValues. 130 | func (mr *MockLogSinkMockRecorder) WithValues(keysAndValues ...any) *gomock.Call { 131 | mr.mock.ctrl.T.Helper() 132 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WithValues", reflect.TypeOf((*MockLogSink)(nil).WithValues), keysAndValues...) 133 | } 134 | -------------------------------------------------------------------------------- /testdata/e2e/buildImage.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | docker build -f Dockerfile --build-arg VERSION=e2e-tests -t localhost:5001/k8s-event-logger:e2e . 4 | docker push localhost:5001/k8s-event-logger:e2e 5 | -------------------------------------------------------------------------------- /testdata/e2e/e2e-values.yaml: -------------------------------------------------------------------------------- 1 | image: 2 | repository: localhost:5001/k8s-event-logger 3 | tag: e2e 4 | pullPolicy: Always 5 | 6 | securityContext: &securityContext 7 | allowPrivilegeEscalation: false 8 | runAsNonRoot: true 9 | seccompProfile: 10 | type: RuntimeDefault 11 | capabilities: 12 | drop: [ "ALL" ] 13 | 14 | eventLogger: 15 | securityContext: *securityContext 16 | -------------------------------------------------------------------------------- /testdata/e2e/installChart.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | helm upgrade --install k8s-event-logger-operator helm \ 5 | --namespace k8s-event-logger-operator \ 6 | --create-namespace \ 7 | -f testdata/e2e/e2e-values.yaml \ 8 | --atomic 9 | 10 | -------------------------------------------------------------------------------- /testdata/e2e/old-eventlogger-pod.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Pod 4 | metadata: 5 | name: event-logger-example-eventlogger-old-version 6 | namespace: e2e-test 7 | labels: 8 | app: event-logger-example-eventlogger 9 | created-by: eventlogger 10 | 11 | spec: 12 | containers: 13 | - name: nginx 14 | image: nginx:latest 15 | -------------------------------------------------------------------------------- /testdata/e2e/test-eventlogger.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: eventlogger.bakito.ch/v1 2 | kind: EventLogger 3 | metadata: 4 | name: example-eventlogger 5 | namespace: e2e-test 6 | spec: 7 | kinds: 8 | - name: Pod 9 | skipReasons: 10 | - Pulled 11 | -------------------------------------------------------------------------------- /testdata/e2e/test-pod.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Pod 4 | metadata: 5 | name: nginx 6 | namespace: e2e-test 7 | spec: 8 | containers: 9 | - name: nginx 10 | image: nginx:latest 11 | -------------------------------------------------------------------------------- /tools.go: -------------------------------------------------------------------------------- 1 | //go:build tools 2 | // +build tools 3 | 4 | package tools 5 | 6 | import ( 7 | _ "github.com/onsi/ginkgo/v2/ginkgo" 8 | _ "go.uber.org/mock/mockgen" 9 | ) 10 | -------------------------------------------------------------------------------- /version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | // Version the module version 4 | var Version = "v1.7" 5 | --------------------------------------------------------------------------------