├── .dockerignore
├── .github
├── dependabot.yaml
└── workflows
│ ├── dependabot.auto.yaml
│ ├── pipeline.yaml
│ └── stale.yaml
├── .gitignore
├── Directory.Build.props
├── Directory.Packages.props
├── ES.Kubernetes.Reflector.sln
├── ES.Kubernetes.Reflector.sln.DotSettings
├── GitVersion.yaml
├── LICENSE
├── NuGet.config
├── README.md
├── Shared.DotSettings
├── assets
└── helm_icon.png
├── src
├── ES.Kubernetes.Reflector
│ ├── Configuration
│ │ ├── ReflectorOptions.cs
│ │ └── WatcherOptions.cs
│ ├── Dockerfile
│ ├── ES.Kubernetes.Reflector.csproj
│ ├── Mirroring
│ │ ├── ConfigMapMirror.cs
│ │ ├── Core
│ │ │ ├── Annotations.cs
│ │ │ ├── MirroringProperties.cs
│ │ │ ├── MirroringPropertiesExtensions.cs
│ │ │ └── ResourceMirror.cs
│ │ └── SecretMirror.cs
│ ├── Program.cs
│ ├── Properties
│ │ └── launchSettings.json
│ ├── Watchers
│ │ ├── ConfigMapWatcher.cs
│ │ ├── Core
│ │ │ ├── Events
│ │ │ │ ├── IWatcherClosedHandler.cs
│ │ │ │ ├── IWatcherEventHandler.cs
│ │ │ │ ├── WatcherClosed.cs
│ │ │ │ └── WatcherEvent.cs
│ │ │ └── WatcherBackgroundService.cs
│ │ ├── NamespaceWatcher.cs
│ │ └── SecretWatcher.cs
│ ├── appsettings.Development.json
│ └── appsettings.json
└── helm
│ └── reflector
│ ├── .helmignore
│ ├── Chart.yaml
│ ├── templates
│ ├── NOTES.txt
│ ├── _helpers.tpl
│ ├── clusterRole.yaml
│ ├── clusterRoleBinding.yaml
│ ├── cron.yaml
│ ├── deployment.yaml
│ ├── hpa.yaml
│ └── serviceaccount.yaml
│ └── values.yaml
└── tests
└── ES.Kubernetes.Reflector.Tests
├── Additions
└── ReflectorAnnotationsBuilder.cs
├── ES.Kubernetes.Reflector.Tests.csproj
├── Fixtures
├── KubernetesFixture.cs
└── ReflectorFixture.cs
└── Integration
├── Base
└── BaseIntegrationTest.cs
├── Fixtures
└── ReflectorIntegrationFixture.cs
├── HealthCheckIntegrationTests.cs
└── MirroringIntegrationTests.cs
/.dockerignore:
--------------------------------------------------------------------------------
1 | **/.classpath
2 | **/.dockerignore
3 | **/.env
4 | **/.git
5 | **/.gitignore
6 | **/.project
7 | **/.settings
8 | **/.toolstarget
9 | **/.vs
10 | **/.vscode
11 | **/*.*proj.user
12 | **/*.dbmdl
13 | **/*.jfm
14 | **/azds.yaml
15 | **/bin
16 | **/charts
17 | **/docker-compose*
18 | **/Dockerfile*
19 | **/node_modules
20 | **/npm-debug.log
21 | **/obj
22 | **/secrets.dev.yaml
23 | **/values.dev.yaml
24 | LICENSE
25 | README.md
--------------------------------------------------------------------------------
/.github/dependabot.yaml:
--------------------------------------------------------------------------------
1 | version: 2
2 | registries:
3 | public-nuget:
4 | type: nuget-feed
5 | url: https://api.nuget.org/v3/index.json
6 | updates:
7 | - package-ecosystem: nuget
8 | directory: "/"
9 | registries:
10 | - public-nuget
11 | schedule:
12 | interval: daily
13 | open-pull-requests-limit: 15
14 | labels:
15 | - "area-dependencies"
16 | groups:
17 | all-dependencies:
18 | patterns:
19 | - "*"
20 | - package-ecosystem: "github-actions"
21 | directory: "/"
22 | schedule:
23 | interval: daily
24 | open-pull-requests-limit: 5
25 | labels:
26 | - "area-dependencies"
27 | groups:
28 | all-dependencies:
29 | patterns:
30 | - "*"
--------------------------------------------------------------------------------
/.github/workflows/dependabot.auto.yaml:
--------------------------------------------------------------------------------
1 | name: Dependabot AutoMerge
2 |
3 | on:
4 | pull_request:
5 | branches:
6 | - main
7 |
8 | permissions:
9 | contents: write
10 | pull-requests: write
11 |
12 | jobs:
13 | dependabot:
14 | runs-on: ubuntu-latest
15 | if: github.event.pull_request.user.login == 'dependabot[bot]'
16 | steps:
17 | - name: Fetch Dependabot metadata
18 | id: metadata
19 | uses: dependabot/fetch-metadata@v2
20 | with:
21 | github-token: "${{ secrets.GITHUB_TOKEN }}"
22 | skip-commit-verification: true
23 | skip-verification: true
24 |
25 | - name: Enable auto-merge
26 | run: gh pr merge --auto --squash "$PR_URL"
27 | env:
28 | PR_URL: ${{ github.event.pull_request.html_url }}
29 | GH_TOKEN: ${{ secrets.ES_GITHUB_PAT }}
30 |
31 | - name: Approve the PR
32 | run: gh pr review --approve "$PR_URL"
33 | env:
34 | PR_URL: ${{ github.event.pull_request.html_url }}
35 | GH_TOKEN: ${{ secrets.ES_GITHUB_PAT }}
36 |
--------------------------------------------------------------------------------
/.github/workflows/pipeline.yaml:
--------------------------------------------------------------------------------
1 | name: Pipeline
2 |
3 | on:
4 | push:
5 | branches:
6 | - "**" # Matches all branches
7 | pull_request:
8 | branches:
9 | - "**" # Matches all branches
10 |
11 | workflow_dispatch:
12 | inputs:
13 | force_build:
14 | description: "Forces a build even if no changes are detected"
15 | required: true
16 | default: "false"
17 | force_release:
18 | description: "Forces a release even if no changes are detected"
19 | required: true
20 | default: "false"
21 |
22 | concurrency:
23 | group: pipeline-${{ github.ref_name }}
24 | cancel-in-progress: true
25 |
26 | env:
27 | helm_chart: "reflector"
28 | helm_chart_dir: "src/helm/reflector"
29 | helm_chart_repository: "ghcr.io/emberstack/helm-charts"
30 | helm_chart_repository_protocol: "oci://"
31 |
32 | container_image: "kubernetes-reflector"
33 | container_image_build_context: "."
34 | container_image_build_platforms: "linux/amd64,linux/arm/v7,linux/arm64"
35 | container_image_build_dockerfile: "src/ES.Kubernetes.Reflector/Dockerfile"
36 | container_image_repository_dockerhub: "emberstack"
37 | container_image_repository_ghcr: "ghcr.io/emberstack"
38 |
39 | jobs:
40 | discovery:
41 | runs-on: ubuntu-latest
42 | permissions:
43 | contents: read
44 | pull-requests: read
45 | outputs:
46 | pathsFilter_src: ${{ steps.pathsFilter.outputs.src }}
47 | gitVersion_SemVer: ${{ steps.gitversion.outputs.GitVersion_SemVer }}
48 | gitVersion_AssemblySemFileVer: ${{ steps.gitversion.outputs.GitVersion_AssemblySemFileVer }}
49 | build: ${{ steps.evaluate_build.outputs.result }}
50 | build_push: ${{ steps.evaluate_build_push.outputs.result }}
51 | build_configuration: ${{ steps.evaluate_build_configuration.outputs.result }}
52 | release: ${{ steps.evaluate_release.outputs.result }}
53 | steps:
54 | - name: checkout
55 | uses: actions/checkout@v4
56 | with:
57 | fetch-depth: 0
58 |
59 | - name: tools - dotnet - install
60 | uses: actions/setup-dotnet@v4
61 | with:
62 | dotnet-version: "9.x"
63 |
64 | - name: tools - gitversion - install
65 | uses: gittools/actions/gitversion/setup@v3.2.1
66 | with:
67 | versionSpec: "5.x"
68 | preferLatestVersion: true
69 |
70 | - name: gitversion - execute
71 | id: gitversion
72 | uses: gittools/actions/gitversion/execute@v3.2.1
73 | with:
74 | useConfigFile: true
75 | configFilePath: GitVersion.yaml
76 |
77 | - name: tools - detect changes
78 | id: pathsFilter
79 | uses: dorny/paths-filter@v3
80 | with:
81 | base: ${{ github.ref }}
82 | filters: |
83 | src:
84 | - '*.sln'
85 | - '*.slnx'
86 | - '*.props'
87 | - 'src/**'
88 | build:
89 | - '*.sln'
90 | - '*.slnx'
91 | - '*.props'
92 | - 'src/**'
93 | - 'tests/**'
94 | - 'playground/**'
95 |
96 | - name: evaluate - build
97 | id: evaluate_build
98 | env:
99 | RESULT: ${{ steps.pathsFilter.outputs.build == 'true' || github.event.inputs.force_build == 'true' || github.event.inputs.force_release == 'true' }}
100 | run: echo "result=$RESULT" >> $GITHUB_OUTPUT
101 |
102 | - name: evaluate - build_push
103 | id: evaluate_build_push
104 | env:
105 | RESULT: ${{ github.actor != 'dependabot[bot]' && github.event_name != 'pull_request' && (steps.pathsFilter.outputs.src == 'true' || github.event.inputs.force_build == 'true') }}
106 | run: echo "result=$RESULT" >> $GITHUB_OUTPUT
107 |
108 | - name: evaluate - build_configuration
109 | id: evaluate_build_configuration
110 | env:
111 | RESULT: ${{ github.ref == 'refs/heads/main' && 'Release' || 'Debug' }}
112 | run: echo "result=$RESULT" >> $GITHUB_OUTPUT
113 |
114 | - name: evaluate - release
115 | id: evaluate_release
116 | env:
117 | RESULT: ${{ github.ref == 'refs/heads/main' || github.event.inputs.force_release == 'true' }}
118 | run: echo "result=$RESULT" >> $GITHUB_OUTPUT
119 |
120 |
121 | build:
122 | name: build
123 | if: ${{ needs.discovery.outputs.build == 'true' }}
124 | needs: [discovery]
125 | runs-on: ubuntu-latest
126 | env:
127 | build: ${{ needs.discovery.outputs.build }}
128 | build_push: ${{ needs.discovery.outputs.build_push }}
129 | build_configuration: ${{ needs.discovery.outputs.build_configuration }}
130 | gitVersion_SemVer: ${{ needs.discovery.outputs.gitVersion_SemVer }}
131 | gitVersion_AssemblySemFileVer: ${{ needs.discovery.outputs.gitVersion_AssemblySemFileVer }}
132 | steps:
133 | - name: checkout
134 | uses: actions/checkout@v4
135 |
136 | - name: artifacts - prepare directories
137 | run: |
138 | mkdir -p .artifacts/helm
139 | mkdir -p .artifacts/kubectl
140 |
141 | - name: tools - dotnet - install
142 | uses: actions/setup-dotnet@v4
143 | with:
144 | dotnet-version: "9.x"
145 |
146 | - name: dotnet - restore
147 | run: dotnet restore
148 |
149 | - name: dotnet - build
150 | run: dotnet build --no-restore --configuration ${{ env.build_configuration }} /p:Version=${{ env.gitVersion_SemVer }} /p:AssemblyVersion=${{env.gitVersion_AssemblySemFileVer}} /p:NuGetVersion=${{env.gitVersion_SemVer}}
151 |
152 | - name: dotnet - test
153 | run: dotnet test --no-build --configuration ${{ env.build_configuration }} --verbosity normal
154 |
155 | - name: tests - report
156 | uses: dorny/test-reporter@v2
157 | if: ${{ github.event.pull_request.head.repo.fork == false }}
158 | with:
159 | name: Test Results
160 | path: .artifacts/TestResults/*.trx
161 | reporter: dotnet-trx
162 | fail-on-empty: "false"
163 |
164 | - name: tools - helm - install
165 | uses: azure/setup-helm@v4
166 |
167 | - name: tools - helm - login - ghcr.io
168 | if: ${{ env.build_push == 'true' }}
169 | run: echo "${{ secrets.ES_GITHUB_PAT }}" | helm registry login ghcr.io -u ${{ github.actor }} --password-stdin
170 |
171 | - name: tools - docker - login ghcr.io
172 | if: ${{ env.build_push == 'true' }}
173 | uses: docker/login-action@v3
174 | with:
175 | registry: ghcr.io
176 | username: ${{ github.actor }}
177 | password: ${{ secrets.ES_GITHUB_PAT }}
178 |
179 | - name: tools - docker - login docker.io
180 | if: ${{ env.build_push == 'true' }}
181 | uses: docker/login-action@v3
182 | with:
183 | registry: docker.io
184 | username: ${{ secrets.ES_DOCKERHUB_USERNAME }}
185 | password: ${{ secrets.ES_DOCKERHUB_PAT }}
186 |
187 | - name: tools - docker - register QEMU
188 | run: |
189 | docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
190 |
191 | - name: tools - docker - setup buildx
192 | uses: docker/setup-buildx-action@v3
193 | with:
194 | driver: docker-container # REQUIRED for multi-platform builds
195 |
196 | - name: helm - import README
197 | run: cp README.md ${{ env.helm_chart_dir }}/README.md
198 |
199 | - name: helm - package chart
200 | run: helm package --destination .artifacts/helm --version ${{ env.gitVersion_SemVer }} --app-version ${{ env.gitVersion_SemVer }} ${{ env.helm_chart_dir }}
201 |
202 | - name: helm - template chart
203 | run: helm template --namespace kube-system ${{ env.helm_chart }} .artifacts/helm/${{ env.helm_chart }}-${{ env.gitVersion_SemVer }}.tgz > .artifacts/kubectl/${{ env.helm_chart }}.yaml
204 |
205 | - name: docker - build and push
206 | uses: docker/build-push-action@v6
207 | with:
208 | context: ${{ env.container_image_build_context }}
209 | file: ${{ env.container_image_build_dockerfile }}
210 | build-args: |
211 | BUILD_CONFIGURATION=${{ env.build_configuration }}
212 | push: ${{ env.build_push == 'true' }}
213 | provenance: false
214 | platforms: ${{ env.container_image_build_platforms }}
215 | labels: |
216 | org.opencontainers.image.source=https://github.com/${{ github.repository }}
217 | org.opencontainers.image.url=https://github.com/${{ github.repository }}
218 | org.opencontainers.image.vendor=https://github.com/${{ github.repository_owner }}
219 | org.opencontainers.image.version=${{ env.gitVersion_SemVer }}
220 | org.opencontainers.image.revision=${{ github.sha }}
221 | tags: |
222 | ${{ env.container_image_repository_dockerhub }}/${{ env.container_image }}:${{ env.gitVersion_SemVer }}
223 | ${{ env.container_image_repository_ghcr }}/${{ env.container_image }}:${{ env.gitVersion_SemVer }}
224 |
225 | - name: helm - push
226 | if: ${{ env.build_push == 'true' }}
227 | run: helm push .artifacts/helm/${{ env.helm_chart }}-${{ env.gitVersion_SemVer }}.tgz ${{ env.helm_chart_repository_protocol }}${{ env.helm_chart_repository }}
228 |
229 | - name: artifacts - helm - upload
230 | uses: actions/upload-artifact@v4
231 | with:
232 | name: artifacts-helm-${{env.gitVersion_SemVer}}
233 | path: .artifacts/helm
234 |
235 | - name: artifacts - kubectl - upload
236 | uses: actions/upload-artifact@v4
237 | with:
238 | name: artifacts-kubectl-${{env.gitVersion_SemVer}}
239 | path: .artifacts/kubectl
240 |
241 | release:
242 | name: release
243 | if: ${{ needs.discovery.outputs.release == 'true' && github.ref == 'refs/heads/main' }}
244 | needs: [discovery, build]
245 | runs-on: ubuntu-latest
246 | env:
247 | gitVersion_SemVer: ${{ needs.discovery.outputs.gitVersion_SemVer }}
248 | gitVersion_AssemblySemFileVer: ${{ needs.discovery.outputs.gitVersion_AssemblySemFileVer }}
249 | steps:
250 |
251 | - name: artifacts - helm - download
252 | uses: actions/download-artifact@v4
253 | with:
254 | name: artifacts-helm-${{env.gitVersion_SemVer}}
255 | path: .artifacts/helm
256 |
257 | - name: artifacts - kubectl - download
258 | uses: actions/download-artifact@v4
259 | with:
260 | name: artifacts-kubectl-${{env.gitVersion_SemVer}}
261 | path: .artifacts/kubectl
262 |
263 | - name: tools - helm - install
264 | uses: azure/setup-helm@v4
265 |
266 | - name: tools - helm - login - ghcr.io
267 | run: echo "${{ secrets.ES_GITHUB_PAT }}" | helm registry login ghcr.io -u ${{ github.actor }} --password-stdin
268 |
269 | - name: tools - oras - install
270 | uses: oras-project/setup-oras@v1
271 |
272 | - name: tools - oras - login - ghcr.io
273 | run: echo "${{ secrets.ES_GITHUB_PAT }}" | oras login ghcr.io -u ${{ github.actor }} --password-stdin
274 |
275 | - name: tools - docker - login ghcr.io
276 | uses: docker/login-action@v3
277 | with:
278 | registry: ghcr.io
279 | username: ${{ github.actor }}
280 | password: ${{ secrets.ES_GITHUB_PAT }}
281 |
282 | - name: tools - docker - login docker.io
283 | uses: docker/login-action@v3
284 | with:
285 | registry: docker.io
286 | username: ${{ secrets.ES_DOCKERHUB_USERNAME }}
287 | password: ${{ secrets.ES_DOCKERHUB_PAT }}
288 |
289 | - name: tools - docker - setup buildx
290 | uses: docker/setup-buildx-action@v3
291 |
292 | - name: docker - tag and push - latest
293 | run: |
294 | docker buildx imagetools create \
295 | --tag ${{ env.container_image_repository_dockerhub }}/${{ env.container_image }}:latest \
296 | --tag ${{ env.container_image_repository_ghcr }}/${{ env.container_image }}:latest \
297 | --tag ${{ env.container_image_repository_dockerhub }}/${{ env.container_image }}:${{ env.gitVersion_SemVer }} \
298 | --tag ${{ env.container_image_repository_ghcr }}/${{ env.container_image }}:${{ env.gitVersion_SemVer }} \
299 | ${{ env.container_image_repository_ghcr }}/${{ env.container_image }}:${{ env.gitVersion_SemVer }}
300 |
301 | - name: helm - push
302 | run: helm push .artifacts/helm/${{ env.helm_chart }}-${{ env.gitVersion_SemVer }}.tgz ${{ env.helm_chart_repository_protocol }}${{ env.helm_chart_repository }}
303 |
304 | - name: github - release - create
305 | uses: softprops/action-gh-release@v2
306 | with:
307 | repository: ${{ github.repository }}
308 | name: v${{ env.gitVersion_SemVer }}
309 | tag_name: v${{ env.gitVersion_SemVer }}
310 | body: The release process is automated.
311 | generate_release_notes: true
312 | token: ${{ secrets.ES_GITHUB_PAT }}
313 | files: |
314 | .artifacts/kubectl/${{ env.helm_chart }}.yaml
315 |
316 | - name: github - repository-dispatch - release
317 | uses: peter-evans/repository-dispatch@v3
318 | with:
319 | token: ${{ secrets.ES_GITHUB_PAT }}
320 | repository: emberstack/helm-charts
321 | event-type: release
322 | client-payload: '{"ref": "${{ github.ref }}", "sha": "${{ github.sha }}"}'
--------------------------------------------------------------------------------
/.github/workflows/stale.yaml:
--------------------------------------------------------------------------------
1 | name: Stale
2 |
3 | on:
4 | schedule:
5 | - cron: '0 0 * * *' # Runs daily at midnight UTC
6 | workflow_dispatch: # Allows manual triggering
7 |
8 | permissions:
9 | issues: write
10 | pull-requests: write
11 |
12 | jobs:
13 | stale:
14 | runs-on: ubuntu-latest
15 | steps:
16 | - uses: actions/stale@v9
17 | with:
18 | repo-token: ${{ secrets.GITHUB_TOKEN }}
19 |
20 | # General behavior
21 | stale-issue-message: >
22 | Automatically marked as stale due to no recent activity.
23 | It will be closed if no further activity occurs. Thank you for your contributions.
24 | close-issue-message: >
25 | Automatically closed stale item.
26 | stale-pr-message: >
27 | Automatically marked as stale due to no recent activity.
28 | It will be closed if no further activity occurs. Thank you for your contributions.
29 | close-pr-message: >
30 | Automatically closed stale item.
31 |
32 | days-before-stale: 14
33 | days-before-close: 14
34 |
35 | # Labels
36 | stale-issue-label: 'stale'
37 | stale-pr-label: 'stale'
38 |
39 | exempt-issue-labels: 'pinned,security,[Status] Maybe Later'
40 | exempt-pr-labels: 'pinned,security,[Status] Maybe Later'
41 |
42 | # Limits
43 | operations-per-run: 30
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 | ##
4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
5 |
6 | # User-specific files
7 | *.suo
8 | *.user
9 | *.userosscache
10 | *.sln.docstates
11 |
12 | # User-specific files (MonoDevelop/Xamarin Studio)
13 | *.userprefs
14 |
15 | # Build results
16 | [Dd]ebug/
17 | [Dd]ebugPublic/
18 | [Rr]elease/
19 | [Rr]eleases/
20 | x64/
21 | x86/
22 | bld/
23 | [Bb]in/
24 | [Oo]bj/
25 | [Ll]og/
26 |
27 | # Visual Studio 2015/2017 cache/options directory
28 | .vs/
29 | # Uncomment if you have tasks that create the project's static files in wwwroot
30 | #wwwroot/
31 |
32 | # Visual Studio 2017 auto generated files
33 | Generated\ Files/
34 |
35 | # MSTest test Results
36 | [Tt]est[Rr]esult*/
37 | [Bb]uild[Ll]og.*
38 |
39 | # NUNIT
40 | *.VisualState.xml
41 | TestResult.xml
42 |
43 | # Build Results of an ATL Project
44 | [Dd]ebugPS/
45 | [Rr]eleasePS/
46 | dlldata.c
47 |
48 | # Benchmark Results
49 | BenchmarkDotNet.Artifacts/
50 |
51 | # .NET Core
52 | project.lock.json
53 | project.fragment.lock.json
54 | artifacts/
55 | # **/Properties/launchSettings.json
56 |
57 | # StyleCop
58 | StyleCopReport.xml
59 |
60 | # Files built by Visual Studio
61 | *_i.c
62 | *_p.c
63 | *_i.h
64 | *.ilk
65 | *.meta
66 | *.obj
67 | *.iobj
68 | *.pch
69 | *.pdb
70 | *.ipdb
71 | *.pgc
72 | *.pgd
73 | *.rsp
74 | *.sbr
75 | *.tlb
76 | *.tli
77 | *.tlh
78 | *.tmp
79 | *.tmp_proj
80 | *.log
81 | *.vspscc
82 | *.vssscc
83 | .builds
84 | *.pidb
85 | *.svclog
86 | *.scc
87 |
88 | # Chutzpah Test files
89 | _Chutzpah*
90 |
91 | # Visual C++ cache files
92 | ipch/
93 | *.aps
94 | *.ncb
95 | *.opendb
96 | *.opensdf
97 | *.sdf
98 | *.cachefile
99 | *.VC.db
100 | *.VC.VC.opendb
101 |
102 | # Visual Studio profiler
103 | *.psess
104 | *.vsp
105 | *.vspx
106 | *.sap
107 |
108 | # Visual Studio Trace Files
109 | *.e2e
110 |
111 | # TFS 2012 Local Workspace
112 | $tf/
113 |
114 | # Guidance Automation Toolkit
115 | *.gpState
116 |
117 | # ReSharper is a .NET coding add-in
118 | _ReSharper*/
119 | *.[Rr]e[Ss]harper
120 | *.DotSettings.user
121 |
122 | # JustCode is a .NET coding add-in
123 | .JustCode
124 |
125 | # TeamCity is a build add-in
126 | _TeamCity*
127 |
128 | # DotCover is a Code Coverage Tool
129 | *.dotCover
130 |
131 | # AxoCover is a Code Coverage Tool
132 | .axoCover/*
133 | !.axoCover/settings.json
134 |
135 | # Visual Studio code coverage results
136 | *.coverage
137 | *.coveragexml
138 |
139 | # NCrunch
140 | _NCrunch_*
141 | .*crunch*.local.xml
142 | nCrunchTemp_*
143 |
144 | # MightyMoose
145 | *.mm.*
146 | AutoTest.Net/
147 |
148 | # Web workbench (sass)
149 | .sass-cache/
150 |
151 | # Installshield output folder
152 | [Ee]xpress/
153 |
154 | # DocProject is a documentation generator add-in
155 | DocProject/buildhelp/
156 | DocProject/Help/*.HxT
157 | DocProject/Help/*.HxC
158 | DocProject/Help/*.hhc
159 | DocProject/Help/*.hhk
160 | DocProject/Help/*.hhp
161 | DocProject/Help/Html2
162 | DocProject/Help/html
163 |
164 | # Click-Once directory
165 | publish/
166 |
167 | # Publish Web Output
168 | *.[Pp]ublish.xml
169 | *.azurePubxml
170 | # Note: Comment the next line if you want to checkin your web deploy settings,
171 | # but database connection strings (with potential passwords) will be unencrypted
172 | *.pubxml
173 | *.publishproj
174 |
175 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
176 | # checkin your Azure Web App publish settings, but sensitive information contained
177 | # in these scripts will be unencrypted
178 | PublishScripts/
179 |
180 | # NuGet Packages
181 | *.nupkg
182 | # The packages folder can be ignored because of Package Restore
183 | **/[Pp]ackages/*
184 | # except build/, which is used as an MSBuild target.
185 | !**/[Pp]ackages/build/
186 | # Uncomment if necessary however generally it will be regenerated when needed
187 | #!**/[Pp]ackages/repositories.config
188 | # NuGet v3's project.json files produces more ignorable files
189 | *.nuget.props
190 | *.nuget.targets
191 |
192 | # Microsoft Azure Build Output
193 | csx/
194 | *.build.csdef
195 |
196 | # Microsoft Azure Emulator
197 | ecf/
198 | rcf/
199 |
200 | # Windows Store app package directories and files
201 | AppPackages/
202 | BundleArtifacts/
203 | Package.StoreAssociation.xml
204 | _pkginfo.txt
205 | *.appx
206 |
207 | # Visual Studio cache files
208 | # files ending in .cache can be ignored
209 | *.[Cc]ache
210 | # but keep track of directories ending in .cache
211 | !*.[Cc]ache/
212 |
213 | # Others
214 | ClientBin/
215 | ~$*
216 | *~
217 | *.dbmdl
218 | *.dbproj.schemaview
219 | *.jfm
220 | *.pfx
221 | *.publishsettings
222 | orleans.codegen.cs
223 |
224 | # Including strong name files can present a security risk
225 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
226 | #*.snk
227 |
228 | # Since there are multiple workflows, uncomment next line to ignore bower_components
229 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
230 | #bower_components/
231 |
232 | # RIA/Silverlight projects
233 | Generated_Code/
234 |
235 | # Backup & report files from converting an old project file
236 | # to a newer Visual Studio version. Backup files are not needed,
237 | # because we have git ;-)
238 | _UpgradeReport_Files/
239 | Backup*/
240 | UpgradeLog*.XML
241 | UpgradeLog*.htm
242 | ServiceFabricBackup/
243 | *.rptproj.bak
244 |
245 | # SQL Server files
246 | *.mdf
247 | *.ldf
248 | *.ndf
249 |
250 | # Business Intelligence projects
251 | *.rdl.data
252 | *.bim.layout
253 | *.bim_*.settings
254 | *.rptproj.rsuser
255 |
256 | # Microsoft Fakes
257 | FakesAssemblies/
258 |
259 | # GhostDoc plugin setting file
260 | *.GhostDoc.xml
261 |
262 | # Node.js Tools for Visual Studio
263 | .ntvs_analysis.dat
264 | node_modules/
265 |
266 | # Visual Studio 6 build log
267 | *.plg
268 |
269 | # Visual Studio 6 workspace options file
270 | *.opt
271 |
272 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
273 | *.vbw
274 |
275 | # Visual Studio LightSwitch build output
276 | **/*.HTMLClient/GeneratedArtifacts
277 | **/*.DesktopClient/GeneratedArtifacts
278 | **/*.DesktopClient/ModelManifest.xml
279 | **/*.Server/GeneratedArtifacts
280 | **/*.Server/ModelManifest.xml
281 | _Pvt_Extensions
282 |
283 | # Paket dependency manager
284 | .paket/paket.exe
285 | paket-files/
286 |
287 | # FAKE - F# Make
288 | .fake/
289 |
290 | # JetBrains Rider
291 | .idea/
292 | *.iml
293 |
294 | # CodeRush
295 | .cr/
296 |
297 | # Python Tools for Visual Studio (PTVS)
298 | __pycache__/
299 | *.pyc
300 |
301 | # Cake - Uncomment if you are using it
302 | # tools/**
303 | # !tools/packages.config
304 |
305 | # Tabs Studio
306 | *.tss
307 |
308 | # Telerik's JustMock configuration file
309 | *.jmconfig
310 |
311 | # BizTalk build output
312 | *.btp.cs
313 | *.btm.cs
314 | *.odx.cs
315 | *.xsd.cs
316 |
317 | # OpenCover UI analysis results
318 | OpenCover/
319 |
320 | # Azure Stream Analytics local run output
321 | ASALocalRun/
322 |
323 | # MSBuild Binary and Structured Log
324 | *.binlog
325 |
326 | # NVidia Nsight GPU debugger configuration file
327 | *.nvuser
328 |
329 | # MFractors (Xamarin productivity tool) working folder
330 | .mfractor/
331 |
--------------------------------------------------------------------------------
/Directory.Build.props:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | false
5 | true
6 | enable
7 | enable
8 |
9 | embedded
10 | true
11 |
12 | $(MSBuildThisFileDirectory)..\
13 |
14 | true
15 |
16 |
17 | CS1591;CS1571;CS1573;CS1574;CS1723;NU1901;NU1902;NU1903;
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | false
31 | trx%3bLogFileName=$(MSBuildProjectName).trx
32 | $(MSBuildThisFileDirectory).artifacts/TestResults
33 |
34 |
--------------------------------------------------------------------------------
/Directory.Packages.props:
--------------------------------------------------------------------------------
1 |
2 |
3 | true
4 | false
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/ES.Kubernetes.Reflector.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 17
4 | VisualStudioVersion = 17.0.31710.8
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "....Solution Items", "....Solution Items", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}"
7 | ProjectSection(SolutionItems) = preProject
8 | Directory.Build.props = Directory.Build.props
9 | Directory.Packages.props = Directory.Packages.props
10 | NuGet.config = NuGet.config
11 | EndProjectSection
12 | EndProject
13 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ES.Kubernetes.Reflector", "src\ES.Kubernetes.Reflector\ES.Kubernetes.Reflector.csproj", "{D9295E0C-D3E8-1BE3-F512-1E2579A6882C}"
14 | EndProject
15 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".src", ".src", "{868DDC6B-AB77-4639-A95B-E182CC65AF60}"
16 | EndProject
17 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{755CA48A-4728-4A3F-8EAB-03E99B2F6087}"
18 | EndProject
19 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ES.Kubernetes.Reflector.Tests", "tests\ES.Kubernetes.Reflector.Tests\ES.Kubernetes.Reflector.Tests.csproj", "{4C67A712-2206-40F3-A08F-C2F9EF22B424}"
20 | EndProject
21 | Global
22 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
23 | Debug|Any CPU = Debug|Any CPU
24 | Release|Any CPU = Release|Any CPU
25 | EndGlobalSection
26 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
27 | {D9295E0C-D3E8-1BE3-F512-1E2579A6882C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
28 | {D9295E0C-D3E8-1BE3-F512-1E2579A6882C}.Debug|Any CPU.Build.0 = Debug|Any CPU
29 | {D9295E0C-D3E8-1BE3-F512-1E2579A6882C}.Release|Any CPU.ActiveCfg = Release|Any CPU
30 | {D9295E0C-D3E8-1BE3-F512-1E2579A6882C}.Release|Any CPU.Build.0 = Release|Any CPU
31 | {4C67A712-2206-40F3-A08F-C2F9EF22B424}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
32 | {4C67A712-2206-40F3-A08F-C2F9EF22B424}.Debug|Any CPU.Build.0 = Debug|Any CPU
33 | {4C67A712-2206-40F3-A08F-C2F9EF22B424}.Release|Any CPU.ActiveCfg = Release|Any CPU
34 | {4C67A712-2206-40F3-A08F-C2F9EF22B424}.Release|Any CPU.Build.0 = Release|Any CPU
35 | EndGlobalSection
36 | GlobalSection(SolutionProperties) = preSolution
37 | HideSolutionNode = FALSE
38 | EndGlobalSection
39 | GlobalSection(NestedProjects) = preSolution
40 | {D9295E0C-D3E8-1BE3-F512-1E2579A6882C} = {868DDC6B-AB77-4639-A95B-E182CC65AF60}
41 | {4C67A712-2206-40F3-A08F-C2F9EF22B424} = {755CA48A-4728-4A3F-8EAB-03E99B2F6087}
42 | EndGlobalSection
43 | GlobalSection(ExtensibilityGlobals) = postSolution
44 | SolutionGuid = {3D60B4E7-B886-4E0A-BD09-2AF8EC1BA851}
45 | EndGlobalSection
46 | EndGlobal
47 |
--------------------------------------------------------------------------------
/ES.Kubernetes.Reflector.sln.DotSettings:
--------------------------------------------------------------------------------
1 |
2 | E:\emberstack\kubernetes-reflector\src\Shared.DotSettings
3 | ..\Shared.DotSettings
4 | True
5 | True
6 | 1
--------------------------------------------------------------------------------
/GitVersion.yaml:
--------------------------------------------------------------------------------
1 | assembly-versioning-scheme: MajorMinorPatch
2 | assembly-file-versioning-scheme: MajorMinorPatch
3 | mode: ContinuousDelivery
4 | tag-prefix: '[vV]'
5 | continuous-delivery-fallback-tag: ci
6 | major-version-bump-message: '\+semver:\s?(breaking|major)'
7 | minor-version-bump-message: '\+semver:\s?(feature|minor)'
8 | patch-version-bump-message: '\+semver:\s?(fix|patch)'
9 | no-bump-message: '\+semver:\s?(none|skip)'
10 | legacy-semver-padding: 4
11 | build-metadata-padding: 4
12 | commits-since-version-source-padding: 4
13 | tag-pre-release-weight: 60000
14 | commit-message-incrementing: Enabled
15 | branches:
16 | develop:
17 | mode: ContinuousDeployment
18 | tag: develop
19 | increment: Minor
20 | prevent-increment-of-merged-branch-version: false
21 | track-merge-target: true
22 | regex: ^dev(elop)?(ment)?$
23 | source-branches: []
24 | tracks-release-branches: true
25 | is-release-branch: false
26 | is-mainline: false
27 | pre-release-weight: 0
28 | main:
29 | mode: ContinuousDelivery
30 | tag: ''
31 | increment: Patch
32 | prevent-increment-of-merged-branch-version: true
33 | track-merge-target: false
34 | regex: ^master$|^main$
35 | source-branches:
36 | - develop
37 | - release
38 | tracks-release-branches: false
39 | is-release-branch: false
40 | is-mainline: true
41 | pre-release-weight: 55000
42 | release:
43 | mode: ContinuousDelivery
44 | tag: rc
45 | increment: None
46 | prevent-increment-of-merged-branch-version: true
47 | track-merge-target: false
48 | regex: ^releases?[/-]
49 | source-branches:
50 | - develop
51 | - main
52 | - support
53 | - release
54 | tracks-release-branches: false
55 | is-release-branch: true
56 | is-mainline: false
57 | pre-release-weight: 30000
58 | feature:
59 | mode: ContinuousDelivery
60 | tag: '{BranchName}'
61 | increment: Inherit
62 | regex: ^features?[/-]
63 | source-branches:
64 | - develop
65 | - main
66 | - release
67 | - feature
68 | - support
69 | - hotfix
70 | pre-release-weight: 30000
71 | pull-request:
72 | mode: ContinuousDelivery
73 | tag: PullRequest
74 | increment: Inherit
75 | tag-number-pattern: '[/-](?\d+)'
76 | regex: ^(pull|pull\-requests|pr)[/-]
77 | source-branches:
78 | - develop
79 | - main
80 | - release
81 | - feature
82 | - support
83 | - hotfix
84 | pre-release-weight: 30000
85 | hotfix:
86 | mode: ContinuousDelivery
87 | tag: hotfix
88 | increment: Patch
89 | prevent-increment-of-merged-branch-version: false
90 | track-merge-target: false
91 | regex: ^hotfix(es)?[/-]
92 | source-branches:
93 | - release
94 | - main
95 | - support
96 | - hotfix
97 | tracks-release-branches: false
98 | is-release-branch: false
99 | is-mainline: false
100 | pre-release-weight: 30000
101 | support:
102 | mode: ContinuousDelivery
103 | tag: ''
104 | increment: Patch
105 | prevent-increment-of-merged-branch-version: true
106 | track-merge-target: false
107 | regex: ^support[/-]
108 | source-branches:
109 | - main
110 | tracks-release-branches: false
111 | is-release-branch: false
112 | is-mainline: true
113 | pre-release-weight: 55000
114 | ignore:
115 | sha: []
116 | increment: Inherit
117 | commit-date-format: yyyy-MM-dd
118 | merge-message-formats: {}
119 | update-build-number: true
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Romeo Dumitrescu
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/NuGet.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Reflector
2 | Reflector is a Kubernetes addon designed to monitor changes to resources (secrets and configmaps) and reflect changes to mirror resources in the same or other namespaces.
3 |
4 | [](https://github.com/emberstack/kubernetes-reflector/actions/workflows/pipeline.yaml)
5 | [](https://github.com/emberstack/kubernetes-reflector/releases/latest)
6 | [](https://hub.docker.com/r/emberstack/kubernetes-reflector)
7 | [](https://hub.docker.com/r/emberstack/kubernetes-reflector)
8 | [](LICENSE)
9 |
10 |
11 | > Supports `amd64`, `arm` and `arm64`
12 |
13 | ## Support
14 | If you need help or found a bug, please feel free to open an Issue on GitHub (https://github.com/emberstack/kubernetes-reflector/issues).
15 |
16 | ## Deployment
17 |
18 | Reflector can be deployed either manually or using Helm (recommended).
19 |
20 | ### Prerequisites
21 | - Kubernetes 1.22+
22 | - Helm 3.8+ (if deployed using Helm)
23 |
24 | #### Deployment using Helm
25 |
26 | Use Helm to install the latest released chart:
27 | ```shellsession
28 | $ helm upgrade --install reflector oci://ghcr.io/emberstack/helm-charts/reflector
29 | ```
30 | or
31 | ```shellsession
32 | $ helm repo add emberstack https://emberstack.github.io/helm-charts
33 | $ helm repo update
34 | $ helm upgrade --install reflector emberstack/reflector
35 | ```
36 |
37 | You can customize the values of the helm deployment by using the following Values:
38 |
39 | | Parameter | Description | Default |
40 | | ---------------------------------------- | ------------------------------------------------ | ------------------------------------------------------------------------------------------------ |
41 | | `nameOverride` | Overrides release name | `""` |
42 | | `namespaceOverride` | Overrides namespace | `""` |
43 | | `fullnameOverride` | Overrides release fullname | `""` |
44 | | `image.repository` | Container image repository | `emberstack/kubernetes-reflector` (also available: `ghcr.io/emberstack/kubernetes-reflector`) |
45 | | `image.tag` | Container image tag | `Same as chart version` |
46 | | `image.pullPolicy` | Container image pull policy | `IfNotPresent` |
47 | | `configuration.logging.minimumLevel` | Logging minimum level | `Information` |
48 | | `configuration.watcher.timeout` | Maximum watcher lifetime in seconds | `` |
49 | | `configuration.kubernetes.skipTlsVerify` | Skip TLS verify when connecting the the cluster | `false` |
50 | | `rbac.enabled` | Create and use RBAC resources | `true` |
51 | | `serviceAccount.create` | Create ServiceAccount | `true` |
52 | | `serviceAccount.name` | ServiceAccount name | _release name_ |
53 | | `livenessProbe.initialDelaySeconds` | `livenessProbe` initial delay | `5` |
54 | | `livenessProbe.periodSeconds` | `livenessProbe` period | `10` |
55 | | `readinessProbe.initialDelaySeconds` | `readinessProbe` initial delay | `5` |
56 | | `readinessProbe.periodSeconds` | `readinessProbe` period | `10` |
57 | | `startupProbe.failureThreshold` | `startupProbe` failure threshold | `10` |
58 | | `startupProbe.periodSeconds` | `startupProbe` period | `5` |
59 | | `resources` | Resource limits | `{}` |
60 | | `nodeSelector` | Node labels for pod assignment | `{}` |
61 | | `tolerations` | Toleration labels for pod assignment | `[]` |
62 | | `affinity` | Node affinity for pod assignment | `{}` |
63 | | `priorityClassName` | `priorityClassName` for pods | `""` |
64 |
65 | > Find us on [Artifact Hub](https://artifacthub.io/packages/search?org=emberstack)
66 |
67 |
68 | #### Manual deployment
69 | Each release (found on the [Releases](https://github.com/emberstack/kubernetes-reflector/releases) GitHub page) contains the manual deployment file (`reflector.yaml`).
70 |
71 | ```shellsession
72 | $ kubectl -n kube-system apply -f https://github.com/emberstack/kubernetes-reflector/releases/latest/download/reflector.yaml
73 | ```
74 |
75 |
76 | ## Usage
77 |
78 | ### 1. Annotate the source `secret` or `configmap`
79 |
80 | - Add `reflector.v1.k8s.emberstack.com/reflection-allowed: "true"` to the resource annotations to permit reflection to mirrors.
81 | - Add `reflector.v1.k8s.emberstack.com/reflection-allowed-namespaces: ""` to the resource annotations to permit reflection from only the list of comma separated namespaces or regular expressions. Note: If this annotation is omitted or is empty, all namespaces are allowed.
82 |
83 | #### Automatic mirror creation:
84 | Reflector can create mirrors with the same name in other namespaces automatically. The following annotations control if and how the mirrors are created:
85 | - Add `reflector.v1.k8s.emberstack.com/reflection-auto-enabled: "true"` to the resource annotations to automatically create mirrors in other namespaces. Note: Requires `reflector.v1.k8s.emberstack.com/reflection-allowed` to be `true` since mirrors need to able to reflect the source.
86 | - Add `reflector.v1.k8s.emberstack.com/reflection-auto-namespaces: ""` to the resource annotations specify in which namespaces to automatically create mirrors. Note: If this annotation is omitted or is empty, all namespaces are allowed. Namespaces in this list will also be checked by `reflector.v1.k8s.emberstack.com/reflection-allowed-namespaces` since mirrors need to be in namespaces from where reflection is permitted.
87 |
88 | > Important: If the `source` is deleted, automatic mirrors are deleted. Also if either reflection or automirroring is turned off or the automatic mirror's namespace is no longer a valid match for the allowed namespaces, the automatic mirror is deleted.
89 |
90 | > Important: Reflector will skip any conflicting resource when creating auto-mirrors. If there is already a resource with the source's name in a namespace where an automatic mirror is to be created, that namespace is skipped and logged as a warning.
91 |
92 | Example source secret:
93 | ```yaml
94 | apiVersion: v1
95 | kind: Secret
96 | metadata:
97 | name: source-secret
98 | annotations:
99 | reflector.v1.k8s.emberstack.com/reflection-allowed: "true"
100 | reflector.v1.k8s.emberstack.com/reflection-allowed-namespaces: "namespace-1,namespace-2,namespace-[0-9]*"
101 | data:
102 | ...
103 | ```
104 |
105 | Example source configmap:
106 | ```yaml
107 | apiVersion: v1
108 | kind: ConfigMap
109 | metadata:
110 | name: source-config-map
111 | annotations:
112 | reflector.v1.k8s.emberstack.com/reflection-allowed: "true"
113 | reflector.v1.k8s.emberstack.com/reflection-allowed-namespaces: "namespace-1,namespace-2,namespace-[0-9]*"
114 | data:
115 | ...
116 | ```
117 |
118 | ### 2. Annotate the mirror secret or configmap
119 |
120 | - Add `reflector.v1.k8s.emberstack.com/reflects: "/"` to the mirror object. The value of the annotation is the full name of the source object in `namespace/name` format.
121 |
122 | > Note: Add `reflector.v1.k8s.emberstack.com/reflected-version: ""` to the resource annotations when doing any manual changes to the mirror (for example when deploying with `helm` or re-applying the deployment script). This will reset the reflected version of the mirror.
123 |
124 | Example mirror secret:
125 | ```yaml
126 | apiVersion: v1
127 | kind: Secret
128 | metadata:
129 | name: mirror-secret
130 | annotations:
131 | reflector.v1.k8s.emberstack.com/reflects: "default/source-secret"
132 | data:
133 | ...
134 | ```
135 |
136 | Example mirror configmap:
137 | ```yaml
138 | apiVersion: v1
139 | kind: ConfigMap
140 | metadata:
141 | name: mirror-config-map
142 | annotations:
143 | reflector.v1.k8s.emberstack.com/reflects: "default/source-config-map"
144 | data:
145 | ...
146 | ```
147 |
148 | ### 3. Done!
149 | Reflector will monitor any changes done to the source objects and copy the following fields:
150 | - `data` for secrets
151 | - `data` and `binaryData` for configmaps
152 | Reflector keeps track of what was copied by annotating mirrors with the source object version.
153 |
154 | - - - -
155 |
156 |
157 |
158 | ## `cert-manager` support
159 |
160 | > Since version 1.5 of cert-manager you can annotate secrets created from certificates for mirroring using `secretTemplate` (see https://cert-manager.io/docs/usage/certificate/).
161 |
162 | ```yaml
163 | apiVersion: cert-manager.io/v1
164 | kind: Certificate
165 | ...
166 | spec:
167 | secretTemplate:
168 | annotations:
169 | reflector.v1.k8s.emberstack.com/reflection-allowed: "true"
170 | reflector.v1.k8s.emberstack.com/reflection-allowed-namespaces: ""
171 | ...
172 | ```
173 |
174 | =======
175 | > Since version 1.15 of cert-manager you can annotate `Ingress` to create secrets created from certificates for mirroring using `cert-manager.io/secret-template` annotation (see https://github.com/cert-manager/cert-manager/pull/6839).
176 | ```yaml
177 | apiVersion: networking.k8s.io/v1
178 | kind: Ingress
179 | ...
180 | metadata:
181 | annotations:
182 | cert-manager.io/cluster-issuer: letsencrypt-prod
183 | cert-manager.io/secret-template: |
184 | {"annotations": {"reflector.v1.k8s.emberstack.com/reflection-allowed": "true", "reflector.v1.k8s.emberstack.com/reflection-allowed-namespaces": ""}}
185 | ...
186 | ```
187 |
--------------------------------------------------------------------------------
/Shared.DotSettings:
--------------------------------------------------------------------------------
1 |
2 | HINT
3 | HINT
4 | HINT
5 | HINT
6 | Custom Full Cleanup
7 | ExpressionBody
8 | ExpressionBody
9 | ExpressionBody
10 | True
11 | True
12 | True
--------------------------------------------------------------------------------
/assets/helm_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/emberstack/kubernetes-reflector/e3bc6b3112bf2909ef2c74071b098413cf9d81d1/assets/helm_icon.png
--------------------------------------------------------------------------------
/src/ES.Kubernetes.Reflector/Configuration/ReflectorOptions.cs:
--------------------------------------------------------------------------------
1 | namespace ES.Kubernetes.Reflector.Configuration;
2 |
3 | public class ReflectorOptions
4 | {
5 | public WatcherOptions? Watcher { get; set; }
6 | }
--------------------------------------------------------------------------------
/src/ES.Kubernetes.Reflector/Configuration/WatcherOptions.cs:
--------------------------------------------------------------------------------
1 | namespace ES.Kubernetes.Reflector.Configuration;
2 |
3 | public class WatcherOptions
4 | {
5 | public int? Timeout { get; set; }
6 | }
--------------------------------------------------------------------------------
/src/ES.Kubernetes.Reflector/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM mcr.microsoft.com/dotnet/aspnet:9.0-bookworm-slim AS base
2 | USER app
3 | WORKDIR /app
4 | EXPOSE 8080
5 |
6 | FROM mcr.microsoft.com/dotnet/sdk:9.0-bookworm-slim-amd64 AS build
7 | ARG BUILD_CONFIGURATION=Release
8 | COPY . .
9 | RUN dotnet build "src/ES.Kubernetes.Reflector/ES.Kubernetes.Reflector.csproj" -c $BUILD_CONFIGURATION
10 |
11 | FROM build AS publish
12 | RUN dotnet publish "src/ES.Kubernetes.Reflector/ES.Kubernetes.Reflector.csproj" -c $BUILD_CONFIGURATION -o /app/publish --no-build /p:UseAppHost=false
13 |
14 | FROM base AS final
15 | WORKDIR /app
16 | COPY --from=publish /app/publish .
17 | ENTRYPOINT ["dotnet", "ES.Kubernetes.Reflector.dll"]
--------------------------------------------------------------------------------
/src/ES.Kubernetes.Reflector/ES.Kubernetes.Reflector.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net9.0
5 | enable
6 | enable
7 | Linux
8 | false
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/src/ES.Kubernetes.Reflector/Mirroring/ConfigMapMirror.cs:
--------------------------------------------------------------------------------
1 | using ES.FX.Additions.KubernetesClient.Models;
2 | using ES.Kubernetes.Reflector.Mirroring.Core;
3 | using k8s;
4 | using k8s.Models;
5 | using Microsoft.AspNetCore.JsonPatch;
6 |
7 | namespace ES.Kubernetes.Reflector.Mirroring;
8 |
9 | public class ConfigMapMirror(ILogger logger, IKubernetes kubernetes)
10 | : ResourceMirror(logger, kubernetes)
11 | {
12 | protected override async Task OnResourceWithNameList(string itemRefName) =>
13 | [
14 | .. (await Kubernetes.CoreV1.ListConfigMapForAllNamespacesAsync(
15 | fieldSelector: $"metadata.name={itemRefName}"))
16 | .Items
17 | ];
18 |
19 | protected override async Task OnResourceApplyPatch(V1Patch patch, NamespacedName refId)
20 | {
21 | await Kubernetes.CoreV1.PatchNamespacedConfigMapAsync(patch, refId.Name, refId.Namespace);
22 | }
23 |
24 | protected override Task OnResourceConfigurePatch(V1ConfigMap source, JsonPatchDocument patchDoc)
25 | {
26 | patchDoc.Replace(e => e.Data, source.Data);
27 | patchDoc.Replace(e => e.BinaryData, source.BinaryData);
28 | return Task.CompletedTask;
29 | }
30 |
31 | protected override async Task OnResourceCreate(V1ConfigMap item, string ns)
32 | {
33 | await Kubernetes.CoreV1.CreateNamespacedConfigMapAsync(item, ns);
34 | }
35 |
36 | protected override Task OnResourceClone(V1ConfigMap sourceResource) =>
37 | Task.FromResult(new V1ConfigMap
38 | {
39 | ApiVersion = sourceResource.ApiVersion,
40 | Kind = sourceResource.Kind,
41 | Data = sourceResource.Data,
42 | BinaryData = sourceResource.BinaryData
43 | });
44 |
45 | protected override async Task OnResourceDelete(NamespacedName resourceId)
46 | {
47 | await Kubernetes.CoreV1.DeleteNamespacedConfigMapAsync(resourceId.Name, resourceId.Namespace);
48 | }
49 |
50 | protected override async Task OnResourceGet(NamespacedName refId) =>
51 | await Kubernetes.CoreV1.ReadNamespacedConfigMapAsync(refId.Name, refId.Namespace);
52 | }
--------------------------------------------------------------------------------
/src/ES.Kubernetes.Reflector/Mirroring/Core/Annotations.cs:
--------------------------------------------------------------------------------
1 | namespace ES.Kubernetes.Reflector.Mirroring.Core;
2 |
3 | public static class Annotations
4 | {
5 | public const string Prefix = "reflector.v1.k8s.emberstack.com";
6 |
7 | public static class Reflection
8 | {
9 | public static string Allowed => $"{Prefix}/reflection-allowed";
10 | public static string AllowedNamespaces => $"{Prefix}/reflection-allowed-namespaces";
11 | public static string AutoEnabled => $"{Prefix}/reflection-auto-enabled";
12 | public static string AutoNamespaces => $"{Prefix}/reflection-auto-namespaces";
13 | public static string Reflects => $"{Prefix}/reflects";
14 |
15 |
16 | public static string MetaAutoReflects => $"{Prefix}/auto-reflects";
17 | public static string MetaReflectedVersion => $"{Prefix}/reflected-version";
18 | public static string MetaReflectedAt => $"{Prefix}/reflected-at";
19 | }
20 | }
--------------------------------------------------------------------------------
/src/ES.Kubernetes.Reflector/Mirroring/Core/MirroringProperties.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics.CodeAnalysis;
2 | using ES.FX.Additions.KubernetesClient.Models;
3 |
4 | namespace ES.Kubernetes.Reflector.Mirroring.Core;
5 |
6 | public class MirroringProperties
7 | {
8 | public bool Allowed { get; set; }
9 | public string AllowedNamespaces { get; set; } = string.Empty;
10 | public bool AutoEnabled { get; set; }
11 | public string AutoNamespaces { get; set; } = string.Empty;
12 | public NamespacedName? Reflects { get; set; }
13 |
14 | public string ResourceVersion { get; set; } = string.Empty;
15 |
16 |
17 | public bool IsAutoReflection { get; set; }
18 | public string ReflectedVersion { get; set; } = string.Empty;
19 | public DateTimeOffset? ReflectedAt { get; set; }
20 |
21 |
22 | [MemberNotNullWhen(true, nameof(Reflects))]
23 | public bool IsReflection => Reflects != null;
24 | }
--------------------------------------------------------------------------------
/src/ES.Kubernetes.Reflector/Mirroring/Core/MirroringPropertiesExtensions.cs:
--------------------------------------------------------------------------------
1 | using System.Text.RegularExpressions;
2 | using ES.FX.Additions.KubernetesClient.Models;
3 | using ES.FX.Additions.KubernetesClient.Models.Extensions;
4 | using k8s;
5 | using k8s.Models;
6 |
7 | namespace ES.Kubernetes.Reflector.Mirroring.Core;
8 |
9 | public static class MirroringPropertiesExtensions
10 | {
11 | public static MirroringProperties GetMirroringProperties(this IKubernetesObject resource) =>
12 | resource.EnsureMetadata().GetMirroringProperties();
13 |
14 | public static MirroringProperties GetMirroringProperties(this V1ObjectMeta metadata) =>
15 | new()
16 | {
17 | ResourceVersion = metadata.ResourceVersion,
18 |
19 | Allowed = metadata
20 | .TryGetAnnotationValue(Annotations.Reflection.Allowed, out bool allowed) && allowed,
21 |
22 | AllowedNamespaces = metadata
23 | .TryGetAnnotationValue(Annotations.Reflection.AllowedNamespaces, out string? allowedNamespaces)
24 | ? allowedNamespaces ?? string.Empty
25 | : string.Empty,
26 |
27 | AutoEnabled = metadata
28 | .TryGetAnnotationValue(Annotations.Reflection.AutoEnabled, out bool autoEnabled) && autoEnabled,
29 |
30 | AutoNamespaces = metadata
31 | .TryGetAnnotationValue(Annotations.Reflection.AutoNamespaces, out string? autoNamespaces)
32 | ? autoNamespaces ?? string.Empty
33 | : string.Empty,
34 |
35 | Reflects = metadata
36 | .TryGetAnnotationValue(Annotations.Reflection.Reflects, out string? metaReflects)
37 | ? NamespacedName.TryParse(metaReflects, out var id) ? id : null
38 | : null,
39 |
40 | IsAutoReflection = metadata
41 | .TryGetAnnotationValue(Annotations.Reflection.MetaAutoReflects,
42 | out bool metaAutoReflects) &&
43 | metaAutoReflects,
44 |
45 | ReflectedVersion = metadata
46 | .TryGetAnnotationValue(Annotations.Reflection.MetaReflectedVersion, out string? reflectedVersion)
47 | ? string.IsNullOrWhiteSpace(reflectedVersion) ? string.Empty : reflectedVersion
48 | : string.Empty,
49 |
50 | ReflectedAt = metadata
51 | .TryGetAnnotationValue(Annotations.Reflection.MetaReflectedAt, out string? reflectedAtString)
52 | ? string.IsNullOrWhiteSpace(reflectedAtString)
53 | ? null
54 | : DateTimeOffset.Parse(reflectedAtString.Replace("\"", string.Empty))
55 | : null
56 | };
57 |
58 | public static bool CanBeReflectedToNamespace(this MirroringProperties properties, string ns) =>
59 | properties.Allowed && PatternListMatch(properties.AllowedNamespaces, ns);
60 |
61 |
62 | public static bool CanBeAutoReflectedToNamespace(this MirroringProperties properties, string ns) =>
63 | properties.CanBeReflectedToNamespace(ns) && properties.AutoEnabled &&
64 | PatternListMatch(properties.AutoNamespaces, ns);
65 |
66 |
67 | private static bool PatternListMatch(string patternList, string value)
68 | {
69 | if (string.IsNullOrEmpty(patternList)) return true;
70 | var regexPatterns = patternList.Split([","], StringSplitOptions.RemoveEmptyEntries);
71 | return regexPatterns.Where(s => !string.IsNullOrWhiteSpace(s)).Select(s => s.Trim())
72 | .Select(pattern => Regex.Match(value, pattern))
73 | .Any(match => match.Success && match.Value.Length == value.Length);
74 | }
75 | }
--------------------------------------------------------------------------------
/src/ES.Kubernetes.Reflector/Mirroring/Core/ResourceMirror.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Concurrent;
2 | using System.Net;
3 | using ES.FX.Additions.KubernetesClient.Models;
4 | using ES.FX.Additions.KubernetesClient.Models.Extensions;
5 | using ES.FX.Additions.Newtonsoft.Json.Serialization;
6 | using ES.Kubernetes.Reflector.Watchers.Core.Events;
7 | using k8s;
8 | using k8s.Autorest;
9 | using k8s.Models;
10 | using Microsoft.AspNetCore.JsonPatch;
11 | using Newtonsoft.Json;
12 |
13 | namespace ES.Kubernetes.Reflector.Mirroring.Core;
14 |
15 | public abstract class ResourceMirror(ILogger logger, IKubernetes kubernetes) :
16 | IWatcherEventHandler, IWatcherClosedHandler
17 | where TResource : class, IKubernetesObject
18 | {
19 | private readonly ConcurrentDictionary> _autoReflectionCache = new();
20 | private readonly ConcurrentDictionary _autoSources = new();
21 | private readonly ConcurrentDictionary> _directReflectionCache = new();
22 |
23 | private readonly ConcurrentDictionary _notFoundCache = new();
24 | private readonly ConcurrentDictionary _propertiesCache = new();
25 | protected readonly IKubernetes Kubernetes = kubernetes;
26 | protected readonly ILogger Logger = logger;
27 |
28 |
29 | ///
30 | /// Handles notifications
31 | ///
32 | public Task Handle(WatcherClosed notification, CancellationToken cancellationToken)
33 | {
34 | //If not TResource or Namespace, not something this instance should handle
35 | if (notification.ResourceType != typeof(TResource) &&
36 | notification.ResourceType != typeof(V1Namespace)) return Task.CompletedTask;
37 |
38 | Logger.LogDebug("Cleared sources for {Type} resources", typeof(TResource).Name);
39 |
40 | _autoSources.Clear();
41 | _notFoundCache.Clear();
42 | _propertiesCache.Clear();
43 | _autoReflectionCache.Clear();
44 |
45 | return Task.CompletedTask;
46 | }
47 |
48 | ///
49 | /// Handles notifications
50 | ///
51 | public async Task Handle(WatcherEvent notification, CancellationToken cancellationToken)
52 | {
53 | switch (notification.Item)
54 | {
55 | case TResource obj:
56 | if (await OnResourceIgnoreCheck(obj)) return;
57 | var objNsName = obj.ObjectReference().NamespacedName();
58 |
59 | Logger.LogTrace("Handling {eventType} {resourceType} {resourceNsName}",
60 | notification.EventType, obj.Kind, obj.NamespacedName());
61 |
62 |
63 | //Remove from the not found, since it exists
64 | _notFoundCache.Remove(obj.NamespacedName(), out _);
65 |
66 | switch (notification.EventType)
67 | {
68 | case WatchEventType.Added:
69 | case WatchEventType.Modified:
70 | await HandleUpsert(obj, cancellationToken);
71 | break;
72 | case WatchEventType.Deleted:
73 | {
74 | _propertiesCache.Remove(objNsName, out _);
75 | var properties = obj.GetMirroringProperties();
76 |
77 | if (!properties.IsReflection)
78 | {
79 | if (properties is { Allowed: true, AutoEnabled: true } &&
80 | _autoReflectionCache.TryGetValue(objNsName, out var reflectionList))
81 | foreach (var reflectionNsName in reflectionList.ToArray())
82 | {
83 | Logger.LogDebug("Deleting {objNsName} - Source {sourceNsName} has been deleted",
84 | reflectionNsName, objNsName);
85 | await OnResourceDelete(reflectionNsName);
86 | }
87 |
88 | _autoSources.Remove(objNsName, out _);
89 | _directReflectionCache.Remove(objNsName, out _);
90 | _autoReflectionCache.Remove(objNsName, out _);
91 | }
92 | else
93 | {
94 | foreach (var item in _directReflectionCache) item.Value.Remove(objNsName);
95 | foreach (var item in _autoReflectionCache) item.Value.Remove(objNsName);
96 | }
97 | }
98 | break;
99 | case WatchEventType.Error:
100 | case WatchEventType.Bookmark:
101 | default:
102 | return;
103 | }
104 |
105 | break;
106 | case V1Namespace ns when notification.EventType == WatchEventType.Added:
107 | {
108 | Logger.LogTrace("Handling {eventType} {resourceType} {resourceRef}", notification.EventType, ns.Kind,
109 | ns.ObjectReference().NamespacedName());
110 |
111 | //Update all auto-sources
112 | foreach (var sourceNsName in _autoSources.Keys)
113 | {
114 | var properties = _propertiesCache[sourceNsName];
115 |
116 | //If it can't be reflected to this namespace, skip
117 | if (!properties.CanBeAutoReflectedToNamespace(ns.Name())) continue;
118 |
119 |
120 | //Get the list of auto-reflections
121 | var autoReflections = _autoReflectionCache.GetOrAdd(sourceNsName, []);
122 |
123 | var reflectionNsName = sourceNsName with { Namespace = ns.Name() };
124 |
125 | //Reflect the auto-source to the new namespace
126 | await ResourceReflect(
127 | sourceNsName,
128 | reflectionNsName,
129 | null,
130 | null,
131 | true);
132 |
133 | autoReflections.Add(reflectionNsName);
134 | }
135 | }
136 | break;
137 | }
138 | }
139 |
140 |
141 | private async Task HandleUpsert(TResource obj, CancellationToken cancellationToken)
142 | {
143 | var objNsName = obj.NamespacedName();
144 | var objProperties = obj.GetMirroringProperties();
145 |
146 | _propertiesCache.AddOrUpdate(objNsName, objProperties,
147 | (_, _) => objProperties);
148 |
149 | switch (objProperties)
150 | {
151 | //If the resource is not a reflection
152 | case { IsReflection: false }:
153 | {
154 | //Remove any direct reflections that are no longer valid
155 | if (_directReflectionCache.TryGetValue(objNsName, out var reflectionList))
156 | {
157 | var reflections = reflectionList
158 | .Where(s => !objProperties.CanBeReflectedToNamespace(s.Namespace))
159 | .ToHashSet();
160 |
161 | foreach (var reflectionNsName in reflections)
162 | {
163 | Logger.LogInformation(
164 | "Source {sourceNsName} no longer permits the direct reflection to {reflectionNsName}.",
165 | objNsName, reflectionNsName);
166 | reflectionList.Remove(reflectionNsName);
167 | }
168 | }
169 |
170 |
171 | //Delete any cached auto-reflections that are no longer valid
172 | if (_autoReflectionCache.TryGetValue(objNsName, out reflectionList))
173 | {
174 | var reflections = reflectionList
175 | .Where(s => !objProperties.CanBeReflectedToNamespace(s.Namespace))
176 | .ToHashSet();
177 | foreach (var reflectionNsName in reflections)
178 | {
179 | reflectionList.Remove(reflectionNsName);
180 |
181 | Logger.LogInformation(
182 | "Source {sourceNsName} no longer permits the auto reflection to {reflectionNsName}. " +
183 | "Deleting {reflectionNsName}.",
184 | objNsName, reflectionNsName, reflectionNsName);
185 | await OnResourceDelete(reflectionNsName);
186 | }
187 | }
188 |
189 |
190 | var isAutoSource = objProperties is { Allowed: true, AutoEnabled: true };
191 |
192 | //Update the status of an auto-source
193 | _autoSources.AddOrUpdate(objNsName, isAutoSource, (_, _) => isAutoSource);
194 |
195 | //If not allowed or auto is disabled, remove the cache for auto-reflections
196 | if (!isAutoSource) _autoReflectionCache.Remove(objNsName, out _);
197 |
198 | //If reflection is disabled, remove the reflections cache and stop reflecting
199 | if (!objProperties.Allowed)
200 | {
201 | _directReflectionCache.Remove(objNsName, out _);
202 | return;
203 | }
204 |
205 | //Update known permitted direct reflections
206 | if (_directReflectionCache.TryGetValue(objNsName, out reflectionList))
207 | foreach (var reflectionNsName in reflectionList.ToArray())
208 | {
209 | //Try to get the properties for the reflection. Otherwise, remove it
210 | if (!_propertiesCache.TryGetValue(reflectionNsName, out var reflectionProperties))
211 | {
212 | reflectionList.Remove(reflectionNsName);
213 | continue;
214 | }
215 |
216 | if (reflectionProperties.ReflectedVersion == objProperties.ResourceVersion)
217 | {
218 | Logger.LogDebug(
219 | "Skipping {reflectionNsName} - Source {sourceNsName} matches reflected version",
220 | reflectionNsName, objNsName);
221 | continue;
222 | }
223 |
224 | //Execute the reflection
225 | await ResourceReflect(objNsName,
226 | reflectionNsName,
227 | obj,
228 | null,
229 | false);
230 | }
231 |
232 | //Ensure updated auto-reflections
233 | if (isAutoSource) await AutoReflectionForSource(objNsName, obj, cancellationToken);
234 |
235 |
236 | return;
237 | }
238 | //If resource is a direct reflection
239 | case { IsReflection: true, IsAutoReflection: false }:
240 | {
241 | var sourceNsName = objProperties.Reflects;
242 | MirroringProperties sourceProperties;
243 | if (!_propertiesCache.TryGetValue(sourceNsName, out var sourceProps))
244 | {
245 | var sourceObj = await TryResourceGet(sourceNsName);
246 | if (sourceObj is null)
247 | {
248 | Logger.LogWarning(
249 | "Could not update {reflectionNsName} - Source {sourceNsName} could not be found.",
250 | objNsName, sourceNsName);
251 | return;
252 | }
253 |
254 | sourceProperties = sourceObj.GetMirroringProperties();
255 | }
256 | else
257 | {
258 | sourceProperties = sourceProps;
259 | }
260 |
261 | _propertiesCache.AddOrUpdate(sourceNsName,
262 | sourceProperties, (_, _) => sourceProperties);
263 | _directReflectionCache.TryAdd(sourceNsName, []);
264 | _directReflectionCache[sourceNsName].Add(objNsName);
265 |
266 | if (!sourceProperties.CanBeReflectedToNamespace(objNsName.Namespace))
267 | {
268 | Logger.LogWarning("Could not update {reflectionNsName} - Source {sourceNsName} does not permit it.",
269 | objNsName, sourceNsName);
270 |
271 | _directReflectionCache[sourceNsName]
272 | .Remove(objNsName);
273 | return;
274 | }
275 |
276 | if (sourceProperties.ResourceVersion == objProperties.ReflectedVersion)
277 | {
278 | Logger.LogDebug("Skipping {reflectionNsName} - Source {sourceNsName} matches reflected version",
279 | objNsName, sourceNsName);
280 | return;
281 | }
282 |
283 | await ResourceReflect(
284 | sourceNsName,
285 | objNsName,
286 | null,
287 | obj,
288 | false);
289 |
290 | return;
291 | }
292 | //If this is an auto-reflection, ensure it still has a source. reflection will be done when we hit the source
293 | case { IsReflection: true, IsAutoReflection: true }:
294 | {
295 | var sourceNsName = objProperties.Reflects;
296 |
297 | //If the source is known to not exist, drop the reflection
298 | if (_notFoundCache.ContainsKey(sourceNsName))
299 | {
300 | Logger.LogInformation("Source {sourceNsName} no longer exists. Deleting {reflectionNsName}.",
301 | sourceNsName, objNsName);
302 | await OnResourceDelete(objNsName);
303 | return;
304 | }
305 |
306 |
307 | //Find the source resource
308 | MirroringProperties sourceProperties;
309 | if (!_propertiesCache.TryGetValue(sourceNsName, out var props))
310 | {
311 | var sourceResource = await TryResourceGet(sourceNsName);
312 | if (sourceResource is null)
313 | {
314 | Logger.LogInformation("Source {sourceNsName} no longer exists. Deleting {reflectionNsName}.",
315 | sourceNsName, objNsName);
316 | await OnResourceDelete(objNsName);
317 | return;
318 | }
319 |
320 | sourceProperties = sourceResource.GetMirroringProperties();
321 | }
322 | else
323 | {
324 | sourceProperties = props;
325 | }
326 |
327 | _propertiesCache.AddOrUpdate(sourceNsName, sourceProperties,
328 | (_, _) => sourceProperties);
329 | if (!sourceProperties.CanBeAutoReflectedToNamespace(objNsName.Namespace))
330 | {
331 | Logger.LogInformation(
332 | "Source {sourceNsName} no longer permits the auto reflection to {reflectionNsName}. Deleting {reflectionNsName}.",
333 | sourceNsName, objNsName,
334 | objNsName);
335 | await OnResourceDelete(objNsName);
336 | }
337 |
338 | break;
339 | }
340 | }
341 | }
342 |
343 |
344 | private async Task AutoReflectionForSource(NamespacedName sourceNsName, TResource? sourceObj,
345 | CancellationToken cancellationToken)
346 | {
347 | Logger.LogDebug("Processing auto-reflection source {sourceNsName}", sourceNsName);
348 | var sourceProperties = _propertiesCache[sourceNsName];
349 |
350 | var autoReflectionList = _autoReflectionCache
351 | .GetOrAdd(sourceNsName, _ => []);
352 |
353 | var matches = await OnResourceWithNameList(sourceNsName.Name);
354 | var namespaces = (await Kubernetes.CoreV1
355 | .ListNamespaceAsync(cancellationToken: cancellationToken)).Items;
356 |
357 | foreach (var match in matches)
358 | {
359 | var matchProperties = match.GetMirroringProperties();
360 | _propertiesCache.AddOrUpdate(match.ObjectReference().NamespacedName(),
361 | _ => matchProperties, (_, _) => matchProperties);
362 | }
363 |
364 | var toDelete = matches
365 | .Where(s => s.Namespace() != sourceNsName.Namespace)
366 | .Where(m => !sourceProperties.CanBeAutoReflectedToNamespace(m.Namespace()))
367 | .Where(m => m.GetMirroringProperties().Reflects == sourceNsName)
368 | .Select(s => s.NamespacedName())
369 | .ToList();
370 |
371 | foreach (var reference in toDelete) await OnResourceDelete(reference);
372 |
373 | sourceObj ??= await TryResourceGet(sourceNsName);
374 | if (sourceObj is null) return;
375 |
376 | var toCreate = namespaces
377 | .Where(s => s.Name() != sourceNsName.Namespace)
378 | .Where(s =>
379 | matches.All(m => m.Namespace() != s.Name()) &&
380 | sourceProperties.CanBeAutoReflectedToNamespace(s.Name()))
381 | .Select(s => new NamespacedName(s.Name(), sourceNsName.Name)).ToList();
382 |
383 | var toUpdate = matches
384 | .Where(s => s.Namespace() != sourceNsName.Namespace)
385 | .Where(m => !toDelete.Contains(m.NamespacedName()) && !toCreate.Contains(m.NamespacedName()) &&
386 | m.GetMirroringProperties().ReflectedVersion != sourceProperties.ResourceVersion &&
387 | m.GetMirroringProperties().Reflects == sourceNsName)
388 | .Select(m => m.NamespacedName()).ToList();
389 |
390 | var toSkip = matches
391 | .Where(s => s.Namespace() != sourceNsName.Namespace)
392 | .Where(m =>
393 | !toDelete.Contains(m.NamespacedName()) &&
394 | !toCreate.Contains(m.NamespacedName()) &&
395 | m.GetMirroringProperties().ReflectedVersion == sourceProperties.ResourceVersion &&
396 | m.GetMirroringProperties().Reflects == sourceNsName)
397 | .Select(m => m.NamespacedName()).ToList();
398 |
399 |
400 | autoReflectionList.Clear();
401 | foreach (var item in toCreate
402 | .Concat(toSkip)
403 | .Concat(toUpdate)
404 | .ToHashSet())
405 | autoReflectionList.Add(item);
406 |
407 | foreach (var reflectionNsName in toCreate)
408 | await ResourceReflect(
409 | sourceNsName,
410 | reflectionNsName,
411 | sourceObj,
412 | null,
413 | true);
414 | foreach (var reflectionRef in toUpdate)
415 | {
416 | var reflectionObj = matches.Single(s => s.NamespacedName() == reflectionRef);
417 | await ResourceReflect(
418 | sourceNsName,
419 | reflectionRef,
420 | sourceObj,
421 | reflectionObj,
422 | true);
423 | }
424 |
425 | Logger.LogInformation(
426 | "Auto-reflected {sourceNsName} where permitted. " +
427 | "Created {createdCount} - Updated {updatedCount} - Deleted {deletedCount} - Validated {skippedCount}.",
428 | sourceNsName, toCreate.Count, toUpdate.Count, toDelete.Count, toSkip.Count);
429 | }
430 |
431 |
432 | private async Task ResourceReflect(NamespacedName sourceNsName, NamespacedName reflectionNsName,
433 | TResource? sourceObj,
434 | TResource? reflectionObj, bool autoReflection)
435 | {
436 | if (sourceNsName == reflectionNsName) return;
437 |
438 | Logger.LogDebug("Reflecting {sourceNsName} to {reflectionNsName}", sourceNsName, reflectionNsName);
439 |
440 | TResource source;
441 | if (sourceObj is null)
442 | {
443 | var lookup = await TryResourceGet(sourceNsName);
444 | if (lookup is not null)
445 | {
446 | source = lookup;
447 | }
448 | else
449 | {
450 | Logger.LogWarning("Could not update {reflectionNsName} - Source {sourceNsName} could not be found.",
451 | reflectionNsName, sourceNsName);
452 | return;
453 | }
454 | }
455 | else
456 | {
457 | source = sourceObj;
458 | }
459 |
460 |
461 | var patchAnnotations = new Dictionary
462 | {
463 | [Annotations.Reflection.MetaAutoReflects] = autoReflection.ToString(),
464 | [Annotations.Reflection.Reflects] = sourceNsName.ToString(),
465 | [Annotations.Reflection.MetaReflectedVersion] = source.Metadata.ResourceVersion,
466 | [Annotations.Reflection.MetaReflectedAt] =
467 | JsonConvert.SerializeObject(DateTimeOffset.UtcNow).Replace("\"", string.Empty)
468 | };
469 |
470 |
471 | try
472 | {
473 | if (reflectionObj is null)
474 | {
475 | var newResource = await OnResourceClone(source);
476 | newResource.Metadata ??= new V1ObjectMeta();
477 | newResource.Metadata.Name = reflectionNsName.Name;
478 | newResource.Metadata.NamespaceProperty = reflectionNsName.Namespace;
479 | newResource.Metadata.Annotations ??= new Dictionary();
480 | var newResourceAnnotations = newResource.Metadata.Annotations;
481 | foreach (var patchAnnotation in patchAnnotations)
482 | newResourceAnnotations[patchAnnotation.Key] = patchAnnotation.Value;
483 | newResourceAnnotations[Annotations.Reflection.MetaAutoReflects] = autoReflection.ToString();
484 | newResourceAnnotations[Annotations.Reflection.Reflects] = sourceNsName.ToString();
485 | newResourceAnnotations[Annotations.Reflection.MetaReflectedVersion] = source.Metadata.ResourceVersion;
486 | newResourceAnnotations[Annotations.Reflection.MetaReflectedAt] = DateTimeOffset.UtcNow.ToString("O");
487 |
488 | try
489 | {
490 | await OnResourceCreate(newResource, reflectionNsName.Namespace);
491 | Logger.LogInformation("Created {reflectionNsName} as a reflection of {sourceNsName}",
492 | reflectionNsName, sourceNsName);
493 | return;
494 | }
495 | catch (HttpOperationException ex) when (ex.Response.StatusCode == HttpStatusCode.Conflict)
496 | {
497 | //If resource already exists, set target and fallback to patch
498 | reflectionObj = await OnResourceGet(reflectionNsName);
499 | }
500 | }
501 |
502 | if (reflectionObj.GetMirroringProperties().ReflectedVersion == source.Metadata.ResourceVersion)
503 | {
504 | Logger.LogDebug("Skipping {reflectionNsName} - Source {sourceNsName} matches reflected version",
505 | reflectionNsName, sourceNsName);
506 | return;
507 | }
508 |
509 |
510 | var patchDoc = new JsonPatchDocument([], new JsonPropertyNameContractResolver());
511 | var annotations = new Dictionary(reflectionObj.Metadata.Annotations);
512 | foreach (var patchAnnotation in patchAnnotations)
513 | annotations[patchAnnotation.Key] = patchAnnotation.Value;
514 | patchDoc.Replace(e => e.Metadata.Annotations, annotations);
515 |
516 | await OnResourceConfigurePatch(source, patchDoc);
517 |
518 | var patch = JsonConvert.SerializeObject(patchDoc, Formatting.Indented);
519 | await OnResourceApplyPatch(new V1Patch(patch, V1Patch.PatchType.JsonPatch), reflectionNsName);
520 | Logger.LogInformation("Patched {reflectionNsName} as a reflection of {sourceNsName}",
521 | reflectionNsName, sourceNsName);
522 | }
523 | catch (Exception ex)
524 | {
525 | Logger.LogError(ex, "Could not reflect {sourceNsName} to {reflectionNsName} due to exception.",
526 | sourceNsName, reflectionNsName);
527 | }
528 | }
529 |
530 |
531 | protected abstract Task OnResourceApplyPatch(V1Patch source, NamespacedName refId);
532 | protected abstract Task OnResourceConfigurePatch(TResource source, JsonPatchDocument patchDoc);
533 | protected abstract Task OnResourceCreate(TResource item, string ns);
534 | protected abstract Task OnResourceClone(TResource sourceResource);
535 | protected abstract Task OnResourceDelete(NamespacedName resourceId);
536 |
537 |
538 | protected abstract Task OnResourceWithNameList(string itemRefName);
539 |
540 | private async Task TryResourceGet(NamespacedName resourceNsName)
541 | {
542 | try
543 | {
544 | Logger.LogDebug("Retrieving {id}", resourceNsName);
545 | var resource = await OnResourceGet(resourceNsName);
546 | _notFoundCache.TryRemove(resourceNsName, out _);
547 | return resource;
548 | }
549 | catch (HttpOperationException ex) when (ex.Response.StatusCode == HttpStatusCode.NotFound)
550 | {
551 | Logger.LogDebug("Could not find {nsName}", resourceNsName);
552 | _notFoundCache.TryAdd(resourceNsName, true);
553 | return null;
554 | }
555 | }
556 |
557 | protected abstract Task OnResourceGet(NamespacedName refId);
558 |
559 | protected virtual Task OnResourceIgnoreCheck(TResource item) => Task.FromResult(false);
560 | }
--------------------------------------------------------------------------------
/src/ES.Kubernetes.Reflector/Mirroring/SecretMirror.cs:
--------------------------------------------------------------------------------
1 | using ES.FX.Additions.KubernetesClient.Models;
2 | using ES.Kubernetes.Reflector.Mirroring.Core;
3 | using k8s;
4 | using k8s.Models;
5 | using Microsoft.AspNetCore.JsonPatch;
6 |
7 | namespace ES.Kubernetes.Reflector.Mirroring;
8 |
9 | public class SecretMirror(ILogger logger, IKubernetes kubernetesClient)
10 | : ResourceMirror(logger, kubernetesClient)
11 | {
12 | protected override async Task OnResourceWithNameList(string itemRefName) =>
13 | [
14 | .. (await Kubernetes.CoreV1.ListSecretForAllNamespacesAsync(
15 | fieldSelector: $"metadata.name={itemRefName}"))
16 | .Items
17 | ];
18 |
19 | protected override async Task OnResourceApplyPatch(V1Patch patch, NamespacedName refId)
20 | {
21 | await Kubernetes.CoreV1.PatchNamespacedSecretWithHttpMessagesAsync(patch, refId.Name, refId.Namespace);
22 | }
23 |
24 | protected override Task OnResourceConfigurePatch(V1Secret source, JsonPatchDocument patchDoc)
25 | {
26 | patchDoc.Replace(e => e.Data, source.Data);
27 | return Task.CompletedTask;
28 | }
29 |
30 | protected override async Task OnResourceCreate(V1Secret item, string ns)
31 | {
32 | await Kubernetes.CoreV1.CreateNamespacedSecretAsync(item, ns);
33 | }
34 |
35 | protected override Task OnResourceClone(V1Secret sourceResource) =>
36 | Task.FromResult(new V1Secret
37 | {
38 | ApiVersion = sourceResource.ApiVersion,
39 | Kind = sourceResource.Kind,
40 | Type = sourceResource.Type,
41 | Data = sourceResource.Data
42 | });
43 |
44 | protected override async Task OnResourceDelete(NamespacedName resourceId)
45 | {
46 | await Kubernetes.CoreV1.DeleteNamespacedSecretAsync(resourceId.Name, resourceId.Namespace);
47 | }
48 |
49 | protected override async Task OnResourceGet(NamespacedName refId) =>
50 | await Kubernetes.CoreV1.ReadNamespacedSecretAsync(refId.Name, refId.Namespace);
51 | }
--------------------------------------------------------------------------------
/src/ES.Kubernetes.Reflector/Program.cs:
--------------------------------------------------------------------------------
1 | using ES.FX.Additions.KubernetesClient.Models.Extensions;
2 | using ES.FX.Additions.Serilog.Lifetime;
3 | using ES.FX.Hosting.Lifetime;
4 | using ES.FX.Ignite.Hosting;
5 | using ES.FX.Ignite.KubernetesClient.Hosting;
6 | using ES.FX.Ignite.OpenTelemetry.Exporter.Seq.Hosting;
7 | using ES.FX.Ignite.Serilog.Hosting;
8 | using ES.Kubernetes.Reflector.Configuration;
9 | using ES.Kubernetes.Reflector.Mirroring;
10 | using ES.Kubernetes.Reflector.Watchers;
11 | using ES.Kubernetes.Reflector.Watchers.Core.Events;
12 | using k8s.Models;
13 |
14 | return await ProgramEntry.CreateBuilder(args).UseSerilog().Build().RunAsync(async _ =>
15 | {
16 | var builder = WebApplication.CreateBuilder(args);
17 |
18 | builder.Configuration.AddEnvironmentVariables("ES_");
19 |
20 | builder.Logging.ClearProviders();
21 | builder.Ignite();
22 | builder.IgniteSerilog(config =>
23 | config.Destructure.ByTransforming(v => v.NamespacedName()));
24 | builder.IgniteSeqOpenTelemetryExporter();
25 | builder.IgniteKubernetesClient();
26 |
27 | builder.Services.Configure(builder.Configuration.GetSection(nameof(ES.Kubernetes.Reflector)));
28 |
29 | builder.Services.AddHostedService();
30 | builder.Services.AddHostedService();
31 | builder.Services.AddHostedService();
32 |
33 | builder.Services.AddSingleton();
34 | builder.Services.AddSingleton();
35 |
36 | builder.Services.AddSingleton(sp => sp.GetRequiredService());
37 | builder.Services.AddSingleton(sp => sp.GetRequiredService());
38 |
39 | builder.Services.AddSingleton(sp => sp.GetRequiredService());
40 | builder.Services.AddSingleton(sp => sp.GetRequiredService());
41 |
42 | var app = builder.Build();
43 | app.Ignite();
44 | await app.RunAsync();
45 | return 0;
46 | });
47 |
48 | public partial class Program;
--------------------------------------------------------------------------------
/src/ES.Kubernetes.Reflector/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "profiles": {
3 | "HOST": {
4 | "commandName": "Project",
5 | "launchBrowser": false,
6 | "environmentVariables": {
7 | "ASPNETCORE_ENVIRONMENT": "Development"
8 | },
9 | "applicationUrl": "http://0.0.0.0:8080",
10 | "dotnetRunMessages": false
11 | },
12 | "Docker": {
13 | "commandName": "Docker",
14 | "launchBrowser": true,
15 | "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}",
16 | "publishAllPorts": true
17 | }
18 | }
19 | }
--------------------------------------------------------------------------------
/src/ES.Kubernetes.Reflector/Watchers/ConfigMapWatcher.cs:
--------------------------------------------------------------------------------
1 | using ES.Kubernetes.Reflector.Configuration;
2 | using ES.Kubernetes.Reflector.Watchers.Core;
3 | using ES.Kubernetes.Reflector.Watchers.Core.Events;
4 | using k8s;
5 | using k8s.Autorest;
6 | using k8s.Models;
7 | using Microsoft.Extensions.Options;
8 |
9 | namespace ES.Kubernetes.Reflector.Watchers;
10 |
11 | public class ConfigMapWatcher(
12 | ILogger logger,
13 | IKubernetes kubernetes,
14 | IOptionsMonitor options,
15 | IEnumerable watcherEventHandlers,
16 | IEnumerable watcherClosedHandlers)
17 | : WatcherBackgroundService(
18 | logger, options, watcherEventHandlers, watcherClosedHandlers)
19 | {
20 | protected override Task> OnGetWatcher(CancellationToken cancellationToken) =>
21 | kubernetes.CoreV1.ListConfigMapForAllNamespacesWithHttpMessagesAsync(watch: true,
22 | timeoutSeconds: WatcherTimeout,
23 | cancellationToken: cancellationToken);
24 | }
--------------------------------------------------------------------------------
/src/ES.Kubernetes.Reflector/Watchers/Core/Events/IWatcherClosedHandler.cs:
--------------------------------------------------------------------------------
1 | namespace ES.Kubernetes.Reflector.Watchers.Core.Events;
2 |
3 | public interface IWatcherClosedHandler
4 | {
5 | public Task Handle(WatcherClosed e, CancellationToken cancellationToken);
6 | }
--------------------------------------------------------------------------------
/src/ES.Kubernetes.Reflector/Watchers/Core/Events/IWatcherEventHandler.cs:
--------------------------------------------------------------------------------
1 | namespace ES.Kubernetes.Reflector.Watchers.Core.Events;
2 |
3 | public interface IWatcherEventHandler
4 | {
5 | public Task Handle(WatcherEvent e, CancellationToken cancellationToken);
6 | }
--------------------------------------------------------------------------------
/src/ES.Kubernetes.Reflector/Watchers/Core/Events/WatcherClosed.cs:
--------------------------------------------------------------------------------
1 | namespace ES.Kubernetes.Reflector.Watchers.Core.Events;
2 |
3 | public class WatcherClosed
4 | {
5 | public required Type ResourceType { get; set; }
6 | public bool Faulted { get; set; }
7 | }
--------------------------------------------------------------------------------
/src/ES.Kubernetes.Reflector/Watchers/Core/Events/WatcherEvent.cs:
--------------------------------------------------------------------------------
1 | using k8s;
2 | using k8s.Models;
3 |
4 | namespace ES.Kubernetes.Reflector.Watchers.Core.Events;
5 |
6 | public class WatcherEvent
7 | {
8 | public WatchEventType EventType { get; set; }
9 | public IKubernetesObject? Item { get; set; }
10 | }
--------------------------------------------------------------------------------
/src/ES.Kubernetes.Reflector/Watchers/Core/WatcherBackgroundService.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics;
2 | using System.Threading.Channels;
3 | using ES.Kubernetes.Reflector.Configuration;
4 | using ES.Kubernetes.Reflector.Watchers.Core.Events;
5 | using k8s;
6 | using k8s.Autorest;
7 | using k8s.Models;
8 | using Microsoft.Extensions.Options;
9 |
10 | namespace ES.Kubernetes.Reflector.Watchers.Core;
11 |
12 | public abstract class WatcherBackgroundService(
13 | ILogger logger,
14 | IOptionsMonitor options,
15 | IEnumerable watcherEventHandlers,
16 | IEnumerable watcherClosedHandlers)
17 | : BackgroundService
18 | where TResource : IKubernetesObject
19 | {
20 | protected int WatcherTimeout => options.CurrentValue.Watcher?.Timeout ?? 3600;
21 |
22 | protected override async Task ExecuteAsync(CancellationToken stoppingToken)
23 | {
24 | var sessionStopwatch = new Stopwatch();
25 | while (!stoppingToken.IsCancellationRequested)
26 | {
27 | var sessionFaulted = false;
28 | sessionStopwatch.Restart();
29 |
30 | using var absoluteTimeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(WatcherTimeout + 3));
31 | using var cancellationCts =
32 | CancellationTokenSource.CreateLinkedTokenSource(stoppingToken, absoluteTimeoutCts.Token);
33 | var cancellationToken = cancellationCts.Token;
34 |
35 | var eventChannel = Channel.CreateBounded(new BoundedChannelOptions(256)
36 | {
37 | FullMode = BoundedChannelFullMode.Wait
38 | });
39 |
40 | try
41 | {
42 | logger.LogInformation("Requesting {type} resources", typeof(TResource).Name);
43 |
44 | //Read using a separate task so the watcher doesn't get stuck waiting on subscribers to handle the event
45 | _ = Task.Run(async () =>
46 | {
47 | while (!cancellationToken.IsCancellationRequested)
48 | {
49 | var watcherEvent = await eventChannel.Reader.ReadAsync(cancellationToken)
50 | .ConfigureAwait(false);
51 | foreach (var watcherEventHandler in watcherEventHandlers)
52 | await watcherEventHandler.Handle(new WatcherEvent
53 | {
54 | Item = watcherEvent.Item,
55 | EventType = watcherEvent.EventType
56 | }, cancellationToken);
57 | }
58 | }, cancellationToken);
59 |
60 | using var watcher = OnGetWatcher(cancellationToken);
61 | var watchList = watcher.WatchAsync(cancellationToken: cancellationToken);
62 |
63 | try
64 | {
65 | await foreach (var (type, item) in watchList)
66 | {
67 | if (await OnResourceIgnoreCheck(item)) continue;
68 | await eventChannel.Writer.WriteAsync(new WatcherEvent
69 | {
70 | Item = item,
71 | EventType = type
72 | }, cancellationToken).ConfigureAwait(false);
73 | }
74 | }
75 | catch (OperationCanceledException)
76 | {
77 | logger.LogTrace("Event channel writing canceled.");
78 | }
79 | }
80 | catch (TaskCanceledException)
81 | {
82 | logger.LogTrace("Session canceled using token.");
83 | }
84 | catch (Exception exception)
85 | {
86 | logger.LogError(exception, "Faulted due to exception.");
87 | sessionFaulted = true;
88 | }
89 | finally
90 | {
91 | eventChannel.Writer.Complete();
92 | while (eventChannel.Reader.TryRead(out _)) ;
93 |
94 | var sessionElapsed = sessionStopwatch.Elapsed;
95 | sessionStopwatch.Stop();
96 | logger.LogInformation("Session closed. Duration: {duration}. Faulted: {faulted}.", sessionElapsed,
97 | sessionFaulted);
98 |
99 | foreach (var handler in watcherClosedHandlers)
100 | await handler.Handle(new WatcherClosed
101 | {
102 | ResourceType = typeof(TResource),
103 | Faulted = sessionFaulted
104 | }, stoppingToken);
105 | }
106 | }
107 | }
108 |
109 | protected abstract Task> OnGetWatcher(CancellationToken cancellationToken);
110 |
111 | protected virtual Task OnResourceIgnoreCheck(TResource item) => Task.FromResult(false);
112 | }
--------------------------------------------------------------------------------
/src/ES.Kubernetes.Reflector/Watchers/NamespaceWatcher.cs:
--------------------------------------------------------------------------------
1 | using ES.Kubernetes.Reflector.Configuration;
2 | using ES.Kubernetes.Reflector.Watchers.Core;
3 | using ES.Kubernetes.Reflector.Watchers.Core.Events;
4 | using k8s;
5 | using k8s.Autorest;
6 | using k8s.Models;
7 | using Microsoft.Extensions.Options;
8 |
9 | namespace ES.Kubernetes.Reflector.Watchers;
10 |
11 | public class NamespaceWatcher(
12 | ILogger logger,
13 | IKubernetes kubernetes,
14 | IOptionsMonitor options,
15 | IEnumerable watcherEventHandlers,
16 | IEnumerable watcherClosedHandlers)
17 | : WatcherBackgroundService(
18 | logger, options, watcherEventHandlers, watcherClosedHandlers)
19 | {
20 | protected override Task> OnGetWatcher(CancellationToken cancellationToken) =>
21 | kubernetes.CoreV1.ListNamespaceWithHttpMessagesAsync(watch: true, timeoutSeconds: WatcherTimeout,
22 | cancellationToken: cancellationToken);
23 | }
--------------------------------------------------------------------------------
/src/ES.Kubernetes.Reflector/Watchers/SecretWatcher.cs:
--------------------------------------------------------------------------------
1 | using ES.Kubernetes.Reflector.Configuration;
2 | using ES.Kubernetes.Reflector.Watchers.Core;
3 | using ES.Kubernetes.Reflector.Watchers.Core.Events;
4 | using k8s;
5 | using k8s.Autorest;
6 | using k8s.Models;
7 | using Microsoft.Extensions.Options;
8 |
9 | namespace ES.Kubernetes.Reflector.Watchers;
10 |
11 | public class SecretWatcher(
12 | ILogger logger,
13 | IKubernetes kubernetes,
14 | IOptionsMonitor options,
15 | IEnumerable watcherEventHandlers,
16 | IEnumerable watcherClosedHandlers)
17 | : WatcherBackgroundService(
18 | logger, options, watcherEventHandlers, watcherClosedHandlers)
19 | {
20 | protected override Task> OnGetWatcher(CancellationToken cancellationToken) =>
21 | kubernetes.CoreV1.ListSecretForAllNamespacesWithHttpMessagesAsync(watch: true,
22 | timeoutSeconds: WatcherTimeout,
23 | cancellationToken: cancellationToken);
24 |
25 | protected override Task OnResourceIgnoreCheck(V1Secret item)
26 | {
27 | //Skip helm version secrets. This can cause a terrible amount of traffic.
28 | var ignore = item.Type.StartsWith("helm.sh");
29 | return Task.FromResult(ignore);
30 | }
31 | }
--------------------------------------------------------------------------------
/src/ES.Kubernetes.Reflector/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "Serilog": {
3 | "MinimumLevel": {
4 | "Override": {
5 | "ES.Kubernetes.Reflector": "Verbose"
6 | }
7 | }
8 | },
9 |
10 | "Ignite": {
11 | "OpenTelemetry": {
12 | "Exporter": {
13 | "Seq": {
14 | "IngestionEndpoint": "http://seq.localenv.io:5341",
15 | "HealthUrl": "http://seq.localenv.io/health",
16 | "Settings": {
17 | "Enabled": true
18 | }
19 | }
20 | }
21 | },
22 | "KubernetesClient": {
23 | "SkipTlsVerify": true
24 | }
25 | },
26 | "Reflector": {
27 | "Watcher": {
28 | "Timeout": ""
29 | }
30 | }
31 | }
--------------------------------------------------------------------------------
/src/ES.Kubernetes.Reflector/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Serilog": {
3 | "Using": [ "Serilog.Sinks.Seq" ],
4 | "LevelSwitches": { "$consoleLevelSwitch": "Verbose" },
5 | "MinimumLevel": {
6 | "Default": "Verbose",
7 | "Override": {
8 | "Microsoft": "Information",
9 | "System.Net.Http": "Warning",
10 | "Polly": "Warning",
11 | "Microsoft.Hosting.Lifetime": "Information",
12 | "Microsoft.AspNetCore": "Warning",
13 | "Microsoft.AspNetCore.DataProtection": "Error",
14 | "ES.FX": "Information",
15 | "ES.Kubernetes.Reflector": "Information"
16 | }
17 | },
18 | "WriteTo": [
19 | {
20 | "Name": "Console",
21 | "Args": {
22 | "outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] ({SourceContext}) {Message:lj}{NewLine}{Exception}",
23 | "levelSwitch": "$consoleLevelSwitch",
24 | "formatter": "Serilog.Formatting.Compact.CompactJsonFormatter, Serilog.Formatting.Compact"
25 | }
26 | }
27 | ]
28 | },
29 | "Ignite": {
30 | "Settings": {
31 | "Configuration": {
32 | "AdditionalJsonSettingsFiles": [],
33 | "AdditionalJsonAppSettingsOverrides": [ "overrides" ]
34 | },
35 | "OpenTelemetry": {
36 | "AspNetCoreTracingHealthChecksRequestsFiltered": true
37 | }
38 | },
39 | "OpenTelemetry": {
40 | "Exporter": {
41 | "Seq": {
42 | "Settings": {
43 | "Enabled": false
44 | }
45 | }
46 | }
47 | },
48 | "KubernetesClient": {
49 | "SkipTlsVerify": false
50 | }
51 | },
52 | "Reflector": {
53 | "Watcher": {
54 | "Timeout": ""
55 | }
56 | }
57 | }
--------------------------------------------------------------------------------
/src/helm/reflector/.helmignore:
--------------------------------------------------------------------------------
1 | # Patterns to ignore when building packages.
2 | # This supports shell glob matching, relative path matching, and
3 | # negation (prefixed with !). Only one pattern per line.
4 | .DS_Store
5 | # Common VCS dirs
6 | .git/
7 | .gitignore
8 | .bzr/
9 | .bzrignore
10 | .hg/
11 | .hgignore
12 | .svn/
13 | # Common backup files
14 | *.swp
15 | *.bak
16 | *.tmp
17 | *.orig
18 | *~
19 | # Various IDEs
20 | .project
21 | .idea/
22 | *.tmproj
23 | .vscode/
24 |
--------------------------------------------------------------------------------
/src/helm/reflector/Chart.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v2
2 | name: reflector
3 | description: A Helm chart to deploy Reflector
4 |
5 | # A chart can be either an 'application' or a 'library' chart.
6 | #
7 | # Application charts are a collection of templates that can be packaged into versioned archives
8 | # to be deployed.
9 | #
10 | # Library charts provide useful utilities or functions for the chart developer. They're included as
11 | # a dependency of application charts to inject those utilities and functions into the rendering
12 | # pipeline. Library charts do not define any templates and therefore cannot be deployed.
13 | type: application
14 |
15 | # This is the chart version. This version number should be incremented each time you make changes
16 | # to the chart and its templates, including the app version.
17 | # Versions are expected to follow Semantic Versioning (https://semver.org/)
18 | version: 0.1.0
19 |
20 | # This is the version number of the application being deployed. This version number should be
21 | # incremented each time you make changes to the application. Versions are not expected to
22 | # follow Semantic Versioning. They should reflect the version the application is using.
23 | # It is recommended to use it with quotes.
24 | appVersion: 0.1.0
25 |
26 |
27 | icon: https://raw.githubusercontent.com/emberstack/kubernetes-reflector/main/assets/helm_icon.png
28 | keywords:
29 | - reflector
30 | - controller
31 | - copy
32 | - secrets
33 | - configmaps
34 | - cert-manager
35 | - certificates
36 | home: https://github.com/emberstack/kubernetes-reflector
37 | sources:
38 | - https://github.com/emberstack/kubernetes-reflector
39 | maintainers:
40 | - name: winromulus
41 | email: helm-charts@emberstack.com
42 |
--------------------------------------------------------------------------------
/src/helm/reflector/templates/NOTES.txt:
--------------------------------------------------------------------------------
1 | Reflector can now be used to perform automatic copy actions on secrets and configmaps.
--------------------------------------------------------------------------------
/src/helm/reflector/templates/_helpers.tpl:
--------------------------------------------------------------------------------
1 | {{/*
2 | Expand the name of the chart.
3 | */}}
4 | {{- define "reflector.name" -}}
5 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
6 | {{- end }}
7 |
8 | {{/*
9 | Create a default fully qualified app name.
10 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
11 | If release name contains chart name it will be used as a full name.
12 | */}}
13 | {{- define "reflector.fullname" -}}
14 | {{- if .Values.fullnameOverride }}
15 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
16 | {{- else }}
17 | {{- $name := default .Chart.Name .Values.nameOverride }}
18 | {{- if contains $name .Release.Name }}
19 | {{- .Release.Name | trunc 63 | trimSuffix "-" }}
20 | {{- else }}
21 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
22 | {{- end }}
23 | {{- end }}
24 | {{- end }}
25 |
26 | {{/*
27 | Create chart name and version as used by the chart label.
28 | */}}
29 | {{- define "reflector.chart" -}}
30 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
31 | {{- end }}
32 |
33 | {{/*
34 | Expand the namespace of the release.
35 | Allows overriding it for multi-namespace deployments in combined charts.
36 | */}}
37 | {{- define "reflector.namespace" -}}
38 | {{- default .Release.Namespace .Values.namespaceOverride | trunc 63 | trimSuffix "-" -}}
39 | {{- end -}}
40 |
41 | {{/*
42 | Common labels
43 | */}}
44 | {{- define "reflector.labels" -}}
45 | helm.sh/chart: {{ include "reflector.chart" . }}
46 | {{ include "reflector.selectorLabels" . }}
47 | {{- if .Chart.AppVersion }}
48 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
49 | {{- end }}
50 | app.kubernetes.io/managed-by: {{ .Release.Service }}
51 | {{- end }}
52 |
53 | {{/*
54 | Selector labels
55 | */}}
56 | {{- define "reflector.selectorLabels" -}}
57 | app.kubernetes.io/name: {{ include "reflector.name" . }}
58 | app.kubernetes.io/instance: {{ .Release.Name }}
59 | {{- end }}
60 |
61 | {{/*
62 | Create the name of the service account to use
63 | */}}
64 | {{- define "reflector.serviceAccountName" -}}
65 | {{- if .Values.serviceAccount.create }}
66 | {{- default (include "reflector.fullname" .) .Values.serviceAccount.name }}
67 | {{- else }}
68 | {{- default "default" .Values.serviceAccount.name }}
69 | {{- end }}
70 | {{- end }}
71 |
--------------------------------------------------------------------------------
/src/helm/reflector/templates/clusterRole.yaml:
--------------------------------------------------------------------------------
1 | {{- if .Values.rbac.enabled }}
2 | apiVersion: rbac.authorization.k8s.io/v1
3 | kind: ClusterRole
4 | metadata:
5 | name: {{ include "reflector.fullname" . }}
6 | namespace: {{ include "reflector.namespace" . }}
7 | labels:
8 | {{- include "reflector.labels" . | nindent 4 }}
9 | rules:
10 | - apiGroups: [""]
11 | resources: ["configmaps", "secrets"]
12 | verbs: ["*"]
13 | - apiGroups: [""]
14 | resources: ["namespaces"]
15 | verbs: ["watch", "list"]
16 | {{- end }}
17 |
--------------------------------------------------------------------------------
/src/helm/reflector/templates/clusterRoleBinding.yaml:
--------------------------------------------------------------------------------
1 | {{- if .Values.rbac.enabled }}
2 | apiVersion: rbac.authorization.k8s.io/v1
3 | kind: ClusterRoleBinding
4 | metadata:
5 | name: {{ include "reflector.fullname" . }}
6 | namespace: {{ include "reflector.namespace" . }}
7 | labels:
8 | {{- include "reflector.labels" . | nindent 4 }}
9 | roleRef:
10 | kind: ClusterRole
11 | name: {{ include "reflector.fullname" . }}
12 | apiGroup: rbac.authorization.k8s.io
13 | subjects:
14 | - kind: ServiceAccount
15 | name: {{ include "reflector.serviceAccountName" . }}
16 | namespace: {{ include "reflector.namespace" . }}
17 | {{- end }}
18 |
--------------------------------------------------------------------------------
/src/helm/reflector/templates/cron.yaml:
--------------------------------------------------------------------------------
1 | {{- if .Values.cron.enabled }}
2 | ---
3 | apiVersion: {{ .Values.cron.apiVersion | default "batch/v1" }}
4 | kind: CronJob
5 | metadata:
6 | name: {{ include "reflector.fullname" . }}
7 | namespace: {{ include "reflector.namespace" . }}
8 | labels:
9 | {{- include "reflector.labels" . | nindent 4 }}
10 |
11 | spec:
12 | schedule: {{ .Values.cron.schedule | quote }}
13 | suspend: {{ default false .Values.cron.suspend }}
14 | concurrencyPolicy: {{ default "Forbid" .Values.cron.concurrencyPolicy }}
15 | successfulJobsHistoryLimit: {{ default "5" .Values.cron.successfulJobsHistoryLimit }}
16 | failedJobsHistoryLimit: {{ default "5" .Values.cron.failedJobsHistoryLimit }}
17 | {{- if .Values.cron.startingDeadlineSeconds }}
18 | startingDeadlineSeconds: {{ .Values.cron.startingDeadlineSeconds }}
19 | {{- end }}
20 | jobTemplate:
21 | spec:
22 | {{- if .Values.cron.activeDeadlineSeconds }}
23 | activeDeadlineSeconds: {{ .Values.cron.activeDeadlineSeconds }}
24 | {{- end }}
25 | template:
26 | metadata:
27 | {{- with .Values.podAnnotations }}
28 | annotations:
29 | {{- toYaml . | nindent 12 }}
30 | {{- end }}
31 | labels:
32 | {{- include "reflector.selectorLabels" . | nindent 12 }}
33 | spec:
34 | serviceAccountName: {{ include "reflector.serviceAccountName" . }}
35 |
36 | {{- with .Values.imagePullSecrets }}
37 | imagePullSecrets:
38 | {{- toYaml . | nindent 12 }}
39 | {{- end }}
40 |
41 | {{- with .Values.affinity }}
42 | affinity:
43 | {{- toYaml . | nindent 12 }}
44 | {{- end }}
45 |
46 | {{- with .Values.nodeSelector }}
47 | nodeSelector:
48 | {{- toYaml . | nindent 12 }}
49 | {{- end }}
50 |
51 | {{- with .Values.tolerations }}
52 | tolerations:
53 | {{- toYaml . | nindent 12 }}
54 | {{- end }}
55 |
56 | {{- with .Values.cron.securityContext }}
57 | securityContext:
58 | {{- toYaml . | nindent 12 }}
59 | {{- end }}
60 |
61 | restartPolicy: {{ .Values.cron.restartPolicy | default "Never" }}
62 | containers:
63 | - name: {{ .Chart.Name }}
64 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
65 | imagePullPolicy: {{ .Values.image.pullPolicy }}
66 | env:
67 | - name: ES_Serilog__MinimumLevel__Default
68 | value: {{ .Values.configuration.logging.minimumLevel | quote }}
69 | - name: ES_Reflector__Watcher__Timeout
70 | value: {{ .Values.configuration.watcher.timeout | quote }}
71 | - name: ES_Ignite__KubernetesClient__SkipTlsVerify
72 | value: {{ .Values.configuration.kubernetes.skipTlsVerify | quote }}
73 | {{- with .Values.extraEnv }}
74 | {{- toYaml . | nindent 12 }}
75 | {{- end }}
76 | resources:
77 | {{- toYaml .Values.resources | nindent 16 }}
78 | {{- end }}
79 |
--------------------------------------------------------------------------------
/src/helm/reflector/templates/deployment.yaml:
--------------------------------------------------------------------------------
1 | {{- if not .Values.cron.enabled }}
2 | ---
3 | apiVersion: apps/v1
4 | kind: Deployment
5 | metadata:
6 | name: {{ include "reflector.fullname" . }}
7 | namespace: {{ include "reflector.namespace" . }}
8 | labels:
9 | {{- include "reflector.labels" . | nindent 4 }}
10 |
11 | spec:
12 | {{- if not .Values.autoscaling.enabled }}
13 | replicas: {{ .Values.replicaCount }}
14 | {{- end }}
15 | selector:
16 | matchLabels:
17 | {{- include "reflector.selectorLabels" . | nindent 6 }}
18 | template:
19 | metadata:
20 | {{- with .Values.podAnnotations }}
21 | annotations:
22 | {{- toYaml . | nindent 8 }}
23 | {{- end }}
24 | labels:
25 | {{- include "reflector.selectorLabels" . | nindent 8 }}
26 | {{- with .Values.podLabels }}
27 | {{- toYaml . | nindent 8 }}
28 | {{- end }}
29 |
30 | spec:
31 | {{- with .Values.imagePullSecrets }}
32 | imagePullSecrets:
33 | {{- toYaml . | nindent 8 }}
34 | {{- end }}
35 | serviceAccountName: {{ include "reflector.serviceAccountName" . }}
36 | securityContext:
37 | {{- toYaml .Values.podSecurityContext | nindent 8 }}
38 | {{- if .Values.priorityClassName }}
39 | priorityClassName: {{ .Values.priorityClassName }}
40 | {{- end }}
41 | containers:
42 | - name: {{ .Chart.Name }}
43 | securityContext:
44 | {{- toYaml .Values.securityContext | nindent 12 }}
45 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
46 | imagePullPolicy: {{ .Values.image.pullPolicy }}
47 | env:
48 | - name: ES_Serilog__MinimumLevel__Default
49 | value: {{ .Values.configuration.logging.minimumLevel | quote }}
50 | - name: ES_Reflector__Watcher__Timeout
51 | value: {{ .Values.configuration.watcher.timeout | quote }}
52 | - name: ES_Ignite__KubernetesClient__SkipTlsVerify
53 | value: {{ .Values.configuration.kubernetes.skipTlsVerify | quote }}
54 | {{- with .Values.extraEnv }}
55 | {{- toYaml . | nindent 12 }}
56 | {{- end }}
57 | ports:
58 | - name: http
59 | containerPort: 8080
60 | protocol: TCP
61 | livenessProbe:
62 | {{- toYaml .Values.livenessProbe | nindent 12 }}
63 | readinessProbe:
64 | {{- toYaml .Values.readinessProbe | nindent 12 }}
65 | {{- if semverCompare ">= 1.18-0" .Capabilities.KubeVersion.Version }}
66 | startupProbe:
67 | {{- toYaml .Values.startupProbe | nindent 12 }}
68 | {{- end }}
69 | resources:
70 | {{- toYaml .Values.resources | nindent 12 }}
71 | volumeMounts:
72 | {{- toYaml .Values.volumeMounts | nindent 12 }}
73 | {{- with .Values.nodeSelector }}
74 | nodeSelector:
75 | {{- toYaml . | nindent 8 }}
76 | {{- end }}
77 | {{- with .Values.affinity }}
78 | affinity:
79 | {{- toYaml . | nindent 8 }}
80 | {{- end }}
81 | {{- with .Values.tolerations }}
82 | tolerations:
83 | {{- toYaml . | nindent 8 }}
84 | {{- end }}
85 | {{- with .Values.volumes }}
86 | volumes:
87 | {{- toYaml . | nindent 8}}
88 | {{- end }}
89 | {{- with .Values.topologySpreadConstraints }}
90 | topologySpreadConstraints:
91 | {{- toYaml . | nindent 8 }}
92 | {{- end }}
93 | {{- end }}
94 |
--------------------------------------------------------------------------------
/src/helm/reflector/templates/hpa.yaml:
--------------------------------------------------------------------------------
1 | {{- if .Values.autoscaling.enabled }}
2 | apiVersion: autoscaling/v2
3 | kind: HorizontalPodAutoscaler
4 | metadata:
5 | name: {{ include "reflector.fullname" . }}
6 | namespace: {{ include "reflector.namespace" . }}
7 | labels:
8 | {{- include "reflector.labels" . | nindent 4 }}
9 | spec:
10 | scaleTargetRef:
11 | apiVersion: apps/v1
12 | kind: Deployment
13 | name: {{ include "reflector.fullname" . }}
14 | minReplicas: {{ .Values.autoscaling.minReplicas }}
15 | maxReplicas: {{ .Values.autoscaling.maxReplicas }}
16 | metrics:
17 | {{- if .Values.autoscaling.targetCPUUtilizationPercentage }}
18 | - type: Resource
19 | resource:
20 | name: cpu
21 | target:
22 | type: Utilization
23 | averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
24 | {{- end }}
25 | {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }}
26 | - type: Resource
27 | resource:
28 | name: memory
29 | target:
30 | type: Utilization
31 | averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }}
32 | {{- end }}
33 | {{- end }}
34 |
--------------------------------------------------------------------------------
/src/helm/reflector/templates/serviceaccount.yaml:
--------------------------------------------------------------------------------
1 | {{- if .Values.serviceAccount.create -}}
2 | apiVersion: v1
3 | kind: ServiceAccount
4 | metadata:
5 | name: {{ include "reflector.serviceAccountName" . }}
6 | namespace: {{ include "reflector.namespace" . }}
7 | labels:
8 | {{- include "reflector.labels" . | nindent 4 }}
9 | {{- with .Values.serviceAccount.annotations }}
10 | annotations:
11 | {{- toYaml . | nindent 4 }}
12 | {{- end }}
13 | {{- end }}
14 |
--------------------------------------------------------------------------------
/src/helm/reflector/values.yaml:
--------------------------------------------------------------------------------
1 | # Default values for reflector.
2 | # This is a YAML-formatted file.
3 | # Declare variables to be passed into your templates.
4 |
5 | replicaCount: 1
6 |
7 | image:
8 | repository: docker.io/emberstack/kubernetes-reflector
9 | pullPolicy: IfNotPresent
10 | # Overrides the image tag whose default is the chart appVersion.
11 | tag: ""
12 |
13 | imagePullSecrets: []
14 | nameOverride: ""
15 | namespaceOverride: ""
16 | fullnameOverride: ""
17 |
18 | cron:
19 | enabled: false
20 | schedule: "*/15 * * * *"
21 | activeDeadlineSeconds: 600
22 | securityContext:
23 | runAsNonRoot: true
24 | runAsUser: 1000
25 |
26 | configuration:
27 | logging:
28 | minimumLevel: Information
29 | watcher:
30 | timeout: ""
31 | kubernetes:
32 | skipTlsVerify: false
33 |
34 | rbac:
35 | enabled: true
36 |
37 | serviceAccount:
38 | # Specifies whether a service account should be created
39 | create: true
40 | # Annotations to add to the service account
41 | annotations: {}
42 | # The name of the service account to use.
43 | # If not set and create is true, a name is generated using the fullname template
44 | name: ""
45 |
46 | # additional annotations to set on the pod
47 | podAnnotations: {}
48 | # additional labels to set on the pod
49 | podLabels: {}
50 | # additional env vars to add to the pod
51 | extraEnv: []
52 |
53 | podSecurityContext:
54 | fsGroup: 2000
55 |
56 | securityContext:
57 | capabilities:
58 | drop:
59 | - ALL
60 | readOnlyRootFilesystem: true
61 | runAsNonRoot: true
62 | runAsUser: 1000
63 |
64 | livenessProbe:
65 | httpGet:
66 | path: /health/live
67 | port: http
68 | timeoutSeconds: 10
69 | initialDelaySeconds: 5
70 | periodSeconds: 10
71 | failureThreshold: 5
72 | readinessProbe:
73 | httpGet:
74 | path: /health/ready
75 | port: http
76 | timeoutSeconds: 10
77 | initialDelaySeconds: 5
78 | periodSeconds: 10
79 | failureThreshold: 5
80 | startupProbe:
81 | httpGet:
82 | path: /health/ready
83 | port: http
84 | timeoutSeconds: 10
85 | initialDelaySeconds: 5
86 | periodSeconds: 10
87 | failureThreshold: 5
88 |
89 |
90 | resources:
91 | {}
92 | # We usually recommend not to specify default resources and to leave this as a conscious
93 | # choice for the user. This also increases chances charts run on environments with little
94 | # resources, such as Minikube. If you do want to specify resources, uncomment the following
95 | # lines, adjust them as necessary, and remove the curly braces after 'resources:'.
96 | # limits:
97 | # cpu: 100m
98 | # memory: 128Mi
99 | # requests:
100 | # cpu: 100m
101 | # memory: 128Mi
102 |
103 | autoscaling:
104 | enabled: false
105 | minReplicas: 1
106 | maxReplicas: 100
107 | targetCPUUtilizationPercentage: 80
108 | # targetMemoryUtilizationPercentage: 80
109 |
110 | nodeSelector: {}
111 |
112 | tolerations: []
113 |
114 | affinity: {}
115 |
116 | topologySpreadConstraints: []
117 |
118 | priorityClassName: ""
119 |
120 | volumes: []
121 |
122 | volumeMounts: []
--------------------------------------------------------------------------------
/tests/ES.Kubernetes.Reflector.Tests/Additions/ReflectorAnnotationsBuilder.cs:
--------------------------------------------------------------------------------
1 | using ES.Kubernetes.Reflector.Mirroring.Core;
2 |
3 | namespace ES.Kubernetes.Reflector.Tests.Additions;
4 |
5 | public sealed class ReflectorAnnotationsBuilder
6 | {
7 | private readonly Dictionary _annotations = new();
8 |
9 | public ReflectorAnnotationsBuilder WithReflectionAllowed(bool allowed)
10 | {
11 | _annotations[Annotations.Reflection.Allowed] = allowed.ToString().ToLower();
12 | return this;
13 | }
14 |
15 | public ReflectorAnnotationsBuilder WithAllowedNamespaces(params string[] namespaces)
16 | {
17 | _annotations[Annotations.Reflection.AllowedNamespaces] = string.Join(",", namespaces);
18 | return this;
19 | }
20 |
21 | public ReflectorAnnotationsBuilder WithAutoEnabled(bool enabled)
22 | {
23 | _annotations[Annotations.Reflection.AutoEnabled] = enabled.ToString().ToLower();
24 | return this;
25 | }
26 |
27 | public ReflectorAnnotationsBuilder WithAutoNamespaces(bool enabled, params string[] namespaces)
28 | {
29 | _annotations[Annotations.Reflection.AutoNamespaces] = enabled ? string.Join(",", namespaces) : string.Empty;
30 | return this;
31 | }
32 |
33 | public Dictionary Build()
34 | {
35 | if (_annotations.Count != 0) return _annotations;
36 |
37 | _annotations[Annotations.Reflection.Allowed] = "true";
38 | _annotations[Annotations.Reflection.AllowedNamespaces] = string.Empty;
39 | _annotations[Annotations.Reflection.AutoEnabled] = "true";
40 | _annotations[Annotations.Reflection.AutoNamespaces] = string.Empty;
41 | return _annotations;
42 | }
43 | }
--------------------------------------------------------------------------------
/tests/ES.Kubernetes.Reflector.Tests/ES.Kubernetes.Reflector.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net9.0
5 | enable
6 | enable
7 | false
8 |
9 |
10 |
11 |
12 | all
13 | runtime; build; native; contentfiles; analyzers; buildtransitive
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | all
22 | runtime; build; native; contentfiles; analyzers; buildtransitive
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/tests/ES.Kubernetes.Reflector.Tests/Fixtures/KubernetesFixture.cs:
--------------------------------------------------------------------------------
1 | using System.Text;
2 | using k8s;
3 | using Testcontainers.K3s;
4 |
5 | namespace ES.Kubernetes.Reflector.Tests.Fixtures;
6 |
7 | public sealed class KubernetesFixture : IAsyncLifetime
8 | {
9 | public K3sContainer Container { get; } = new K3sBuilder()
10 | .WithName($"{nameof(KubernetesFixture)}-{Guid.CreateVersion7()}")
11 | .Build();
12 |
13 | public async ValueTask DisposeAsync() => await Container.DisposeAsync();
14 |
15 | public async ValueTask InitializeAsync() => await Container.StartAsync();
16 |
17 | public async Task GetKubernetesClientConfiguration() =>
18 | await KubernetesClientConfiguration
19 | .BuildConfigFromConfigFileAsync(
20 | new MemoryStream(Encoding.UTF8.GetBytes(
21 | await Container.GetKubeconfigAsync())));
22 |
23 | public async Task GetKubernetesClient() =>
24 | new k8s.Kubernetes(await GetKubernetesClientConfiguration());
25 | }
--------------------------------------------------------------------------------
/tests/ES.Kubernetes.Reflector.Tests/Fixtures/ReflectorFixture.cs:
--------------------------------------------------------------------------------
1 | using k8s;
2 | using Microsoft.AspNetCore.Hosting;
3 | using Microsoft.AspNetCore.Mvc.Testing;
4 | using Microsoft.Extensions.DependencyInjection;
5 | using Microsoft.Extensions.Hosting;
6 |
7 | namespace ES.Kubernetes.Reflector.Tests.Fixtures;
8 |
9 | public sealed class ReflectorFixture : WebApplicationFactory, IAsyncLifetime
10 | {
11 | private static readonly Lock Lock = new();
12 |
13 | public KubernetesClientConfiguration? KubernetesClientConfiguration { get; set; }
14 |
15 |
16 | public async ValueTask InitializeAsync() => await Task.CompletedTask;
17 |
18 | // https://github.com/serilog/serilog-aspnetcore/issues/289
19 | // https://github.com/dotnet/AspNetCore.Docs/issues/26609
20 | protected override IHost CreateHost(IHostBuilder builder)
21 | {
22 | lock (Lock)
23 | {
24 | return base.CreateHost(builder);
25 | }
26 | }
27 |
28 | protected override void ConfigureWebHost(IWebHostBuilder builder)
29 | {
30 | builder.UseEnvironment("tests");
31 | builder.ConfigureServices(services =>
32 | {
33 | var kubernetesClientConfiguration = services.SingleOrDefault(
34 | d => d.ServiceType == typeof(KubernetesClientConfiguration));
35 | if (kubernetesClientConfiguration is not null) services.Remove(kubernetesClientConfiguration);
36 |
37 | services.AddSingleton(s =>
38 | {
39 | var config = KubernetesClientConfiguration ??
40 | KubernetesClientConfiguration.BuildDefaultConfig();
41 | config.HttpClientTimeout = TimeSpan.FromMinutes(30);
42 |
43 | return config;
44 | });
45 | });
46 | }
47 | }
--------------------------------------------------------------------------------
/tests/ES.Kubernetes.Reflector.Tests/Integration/Base/BaseIntegrationTest.cs:
--------------------------------------------------------------------------------
1 | using System.Net;
2 | using ES.Kubernetes.Reflector.Tests.Integration.Fixtures;
3 | using k8s;
4 | using k8s.Autorest;
5 | using k8s.Models;
6 | using Polly;
7 | using Polly.Retry;
8 |
9 | namespace ES.Kubernetes.Reflector.Tests.Integration.Base;
10 |
11 | public class BaseIntegrationTest(ReflectorIntegrationFixture integrationFixture)
12 | {
13 | protected static readonly ResiliencePipeline ResourceExistsResiliencePipeline =
14 | new ResiliencePipelineBuilder()
15 | .AddRetry(new RetryStrategyOptions
16 | {
17 | ShouldHandle = new PredicateBuilder()
18 | .Handle(ex =>
19 | ex.Response.StatusCode == HttpStatusCode.NotFound)
20 | .HandleResult(false),
21 | MaxRetryAttempts = 10,
22 | Delay = TimeSpan.FromSeconds(1)
23 | })
24 | .AddTimeout(TimeSpan.FromSeconds(30))
25 | .Build();
26 |
27 | protected async Task GetKubernetesClient() =>
28 | await integrationFixture.Kubernetes.GetKubernetesClient();
29 |
30 | protected async Task CreateNamespaceAsync(string name)
31 | {
32 | var client = await GetKubernetesClient();
33 | var ns = new V1Namespace
34 | {
35 | ApiVersion = V1Namespace.KubeApiVersion,
36 | Kind = V1Namespace.KubeKind,
37 | Metadata = new V1ObjectMeta
38 | {
39 | Name = name
40 | }
41 | };
42 |
43 | return await client.CoreV1.CreateNamespaceAsync(ns);
44 | }
45 |
46 |
47 | protected async Task DelayForReflection() =>
48 | await Task.Delay(TimeSpan.FromSeconds(1));
49 | }
--------------------------------------------------------------------------------
/tests/ES.Kubernetes.Reflector.Tests/Integration/Fixtures/ReflectorIntegrationFixture.cs:
--------------------------------------------------------------------------------
1 | using ES.Kubernetes.Reflector.Tests.Fixtures;
2 |
3 | namespace ES.Kubernetes.Reflector.Tests.Integration.Fixtures;
4 |
5 | public class ReflectorIntegrationFixture : IAsyncLifetime
6 | {
7 | public KubernetesFixture Kubernetes { get; init; } = new();
8 | public ReflectorFixture Reflector { get; init; } = new();
9 |
10 | public async ValueTask InitializeAsync()
11 | {
12 | await Kubernetes.InitializeAsync();
13 | Reflector.KubernetesClientConfiguration =
14 | await Kubernetes.GetKubernetesClientConfiguration();
15 | await Reflector.InitializeAsync();
16 | Reflector.CreateClient();
17 | }
18 |
19 | public async ValueTask DisposeAsync() => await Task.CompletedTask;
20 | }
--------------------------------------------------------------------------------
/tests/ES.Kubernetes.Reflector.Tests/Integration/HealthCheckIntegrationTests.cs:
--------------------------------------------------------------------------------
1 | using System.Net;
2 | using System.Threading;
3 | using ES.FX.Ignite.Configuration;
4 | using ES.Kubernetes.Reflector.Tests.Integration.Base;
5 | using ES.Kubernetes.Reflector.Tests.Integration.Fixtures;
6 | using JetBrains.Annotations;
7 | using k8s;
8 | using Microsoft.Extensions.DependencyInjection;
9 | using Microsoft.Extensions.Diagnostics.HealthChecks;
10 | using Polly.Retry;
11 | using Polly;
12 | using k8s.Autorest;
13 |
14 | [assembly: AssemblyFixture(typeof(ReflectorIntegrationFixture))]
15 |
16 | namespace ES.Kubernetes.Reflector.Tests.Integration;
17 |
18 | [PublicAPI]
19 | public class HealthCheckIntegrationTests(ReflectorIntegrationFixture integrationFixture)
20 | : BaseIntegrationTest(integrationFixture)
21 | {
22 | private readonly ReflectorIntegrationFixture _integrationFixture = integrationFixture;
23 |
24 | [Fact]
25 | public async Task LivenessHealthCheck_Should_Return_Healthy()
26 | {
27 | var httpClient = _integrationFixture.Reflector.CreateClient();
28 | var settings = _integrationFixture.Reflector.Services.GetRequiredService();
29 |
30 | var response = await httpClient.GetAsync(settings.AspNetCore.HealthChecks.LivenessEndpointPath,
31 | TestContext.Current.CancellationToken);
32 | Assert.Equal(HttpStatusCode.OK,response.StatusCode);
33 | }
34 |
35 | [Fact]
36 | public async Task ReadinessHealthCheck_Should_Return_Healthy()
37 | {
38 | var settings = _integrationFixture.Reflector.Services.GetRequiredService();
39 | var httpClient = _integrationFixture.Reflector.CreateClient();
40 |
41 | var response = await httpClient.GetAsync(settings.AspNetCore.HealthChecks.ReadinessEndpointPath,
42 | TestContext.Current.CancellationToken);
43 | Assert.Equal(HttpStatusCode.OK, response.StatusCode);
44 | }
45 | }
--------------------------------------------------------------------------------
/tests/ES.Kubernetes.Reflector.Tests/Integration/MirroringIntegrationTests.cs:
--------------------------------------------------------------------------------
1 | using System.Net;
2 | using ES.Kubernetes.Reflector.Tests.Additions;
3 | using ES.Kubernetes.Reflector.Tests.Integration.Base;
4 | using ES.Kubernetes.Reflector.Tests.Integration.Fixtures;
5 | using JetBrains.Annotations;
6 | using k8s;
7 | using k8s.Autorest;
8 | using k8s.Models;
9 |
10 | [assembly: AssemblyFixture(typeof(ReflectorIntegrationFixture))]
11 |
12 | namespace ES.Kubernetes.Reflector.Tests.Integration;
13 |
14 | [PublicAPI]
15 | public class MirroringIntegrationTests(
16 | ReflectorIntegrationFixture integrationFixture)
17 | : BaseIntegrationTest(integrationFixture)
18 | {
19 | [Fact]
20 | public async Task AutoReflect_To_AllowedNamespaces()
21 | {
22 | var client = await GetKubernetesClient();
23 |
24 | var allowedNamespaces = new[]
25 | {
26 | $"allowed-{Guid.CreateVersion7()}",
27 | $"allowed-{Guid.CreateVersion7()}"
28 | };
29 | var notAllowedNamespaces = new[]
30 | {
31 | $"not-allowed-{Guid.CreateVersion7()}",
32 | $"not-allowed-{Guid.CreateVersion7()}"
33 | };
34 |
35 | foreach (var ns in allowedNamespaces.Concat(notAllowedNamespaces)) await CreateNamespaceAsync(ns);
36 |
37 | var sourceResource = await CreateResource(client,
38 | annotations: new ReflectorAnnotationsBuilder()
39 | .WithReflectionAllowed(true)
40 | .WithAllowedNamespaces("^allowed-.*")
41 | .WithAutoEnabled(true).Build());
42 |
43 | await DelayForReflection();
44 |
45 |
46 | foreach (var ns in allowedNamespaces)
47 | Assert.True(await WaitForResource(client, sourceResource.Name(), ns,
48 | TestContext.Current.CancellationToken));
49 |
50 | foreach (var ns in notAllowedNamespaces)
51 | Assert.False(await ResourceExists(client,
52 | sourceResource.Name(), ns,
53 | TestContext.Current.CancellationToken));
54 | }
55 |
56 |
57 | [Fact]
58 | public async Task AutoReflect_To_NewNamespaces()
59 | {
60 | var client = await GetKubernetesClient();
61 |
62 | var allowedNamespaces = new[]
63 | {
64 | $"allowed-{Guid.CreateVersion7()}",
65 | $"allowed-{Guid.CreateVersion7()}"
66 | };
67 |
68 | var sourceResource = await CreateResource(client,
69 | annotations: new ReflectorAnnotationsBuilder()
70 | .WithReflectionAllowed(true)
71 | .WithAllowedNamespaces("^allowed-.*")
72 | .WithAutoEnabled(true).Build());
73 |
74 | foreach (var ns in allowedNamespaces) await CreateNamespaceAsync(ns);
75 |
76 | await DelayForReflection();
77 |
78 | foreach (var ns in allowedNamespaces)
79 | Assert.True(await WaitForResource(client, sourceResource.Name(), ns,
80 | TestContext.Current.CancellationToken));
81 | }
82 |
83 |
84 | [Fact]
85 | public async Task AutoReflect_Remove_ReflectionsWhenResourceDeleted()
86 | {
87 | var client = await GetKubernetesClient();
88 |
89 | var allowedNamespaces = new[]
90 | {
91 | $"allowed-{Guid.CreateVersion7()}",
92 | $"allowed-{Guid.CreateVersion7()}"
93 | };
94 |
95 | var sourceResource = await CreateResource(client,
96 | annotations: new ReflectorAnnotationsBuilder()
97 | .WithReflectionAllowed(true)
98 | .WithAllowedNamespaces("^allowed-.*")
99 | .WithAutoEnabled(true).Build());
100 |
101 |
102 | foreach (var ns in allowedNamespaces) await CreateNamespaceAsync(ns);
103 |
104 | foreach (var ns in allowedNamespaces)
105 | Assert.True(await WaitForResource(client, sourceResource.Name(),
106 | ns, TestContext.Current.CancellationToken));
107 |
108 | await DeleteResource(client, sourceResource.Name(), sourceResource.Namespace(),
109 | TestContext.Current.CancellationToken);
110 |
111 |
112 | await DelayForReflection();
113 |
114 | foreach (var ns in allowedNamespaces)
115 | Assert.False(await ResourceExists(client,
116 | sourceResource.Name(), ns,
117 | TestContext.Current.CancellationToken));
118 | }
119 |
120 |
121 | private async Task CreateResource(IKubernetes client,
122 | string? name = null, string? namespaceName = null,
123 | Dictionary? annotations = null,
124 | Dictionary? data = null)
125 | {
126 | var sourceResource = new V1Secret
127 | {
128 | ApiVersion = V1Secret.KubeApiVersion,
129 | Kind = V1Secret.KubeKind,
130 | Metadata = new V1ObjectMeta
131 | {
132 | Name = name ?? Guid.CreateVersion7().ToString(),
133 | NamespaceProperty = namespaceName ?? Guid.CreateVersion7().ToString(),
134 | Annotations = annotations ?? new ReflectorAnnotationsBuilder().Build()
135 | },
136 | StringData = data ?? new Dictionary
137 | {
138 | { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() }
139 | },
140 | Type = "Opaque"
141 | };
142 |
143 | var namespaces = await client.CoreV1.ListNamespaceAsync();
144 | var nsExists = namespaces.Items.Any(s => s.Name() == namespaceName);
145 | if (!nsExists)
146 | await CreateNamespaceAsync(sourceResource.Metadata.NamespaceProperty);
147 | sourceResource = await client.CoreV1
148 | .CreateNamespacedSecretAsync(sourceResource, sourceResource.Metadata.NamespaceProperty);
149 | return sourceResource;
150 | }
151 |
152 | private async Task DeleteResource(IKubernetes client, string name, string namespaceName,
153 | CancellationToken cancellationToken = default)
154 | {
155 | await client.CoreV1.DeleteNamespacedSecretAsync(name, namespaceName, cancellationToken: cancellationToken);
156 | }
157 |
158 |
159 | private async Task WaitForResource(IKubernetes client, string name, string namespaceName,
160 | CancellationToken cancellationToken = default)
161 | {
162 | return await ResourceExistsResiliencePipeline.ExecuteAsync(async token =>
163 | {
164 | var resource = await client.CoreV1.ReadNamespacedSecretAsync(
165 | name, namespaceName, cancellationToken: token);
166 | return resource is not null;
167 | }, cancellationToken);
168 | }
169 |
170 |
171 | private async Task ResourceExists(IKubernetes client, string name, string namespaceName,
172 | CancellationToken cancellationToken = default)
173 | {
174 | bool exists;
175 | try
176 | {
177 | await client.CoreV1.ReadNamespacedSecretAsync(name, namespaceName,
178 | cancellationToken: TestContext.Current.CancellationToken);
179 | exists = true;
180 | }
181 | catch (HttpOperationException ex) when (ex.Response.StatusCode == HttpStatusCode.NotFound)
182 | {
183 | exists = false;
184 | }
185 |
186 | return exists;
187 | }
188 | }
--------------------------------------------------------------------------------