Repository: kroxylicious/kroxylicious
Pull request #510 (2023-08-02 14:06:10)

Multi tenant kubernetes example

### Type of change

- Enhancement / new feature

### Description
(Revived from #481)
Simple kubernetes example showing multi-tenant in action. Kroxylicious exposed using NodePort.

The illustrative command used to demonstrate multitent use [kaf](https://github.com/birdayz/kaf).  You could use any client,`kaf` just has a nice CLI experience.





### Additional Context

_Why are you making this pull request?_

### Checklist

_Please go through this checklist and make sure all applicable tasks have been done_

- [ ] Write tests
- [ ] Make sure all tests pass
- [ ] Review performance test results. Ensure that any degradations to performance numbers are understood and justified.
- [x] Make sure all Sonar-Lint warnings are addressed or are justifiably ignored.
- [ ] Update documentation
- [ ] Reference relevant issue(s) and close them after merging
- [x] For user facing changes, update CHANGELOG.md (remember to include changes affecting the API of the test artefacts too).


sonarcloud[bot]: SonarCloud Quality Gate failed.&nbsp; &nbsp; [![Quality Gate failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/failed-16px.png 'Quality Gate failed')](https://sonarcloud.io/dashboard?id=kroxylicious_kroxylicious&pullRequest=510)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=510&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=510&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=510&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=510&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=510&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=510&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=kroxylicious_kroxylicious&pullRequest=510&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=kroxylicious_kroxylicious&pullRequest=510&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=kroxylicious_kroxylicious&pullRequest=510&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=510&resolved=false&types=CODE_SMELL) [![B](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/B-16px.png 'B')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=510&resolved=false&types=CODE_SMELL) [5 Code Smells](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=510&resolved=false&types=CODE_SMELL)

[![66.7%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/60-16px.png '66.7%')](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=510&metric=new_coverage&view=list) [66.7% Coverage](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=510&metric=new_coverage&view=list)  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=510&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=510&metric=new_duplicated_lines_density&view=list)



![idea](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/light_bulb-16px.png 'idea') Catch issues before they fail your Quality Gate with our IDE extension ![sonarlint](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/sonarlint-16px.png 'sonarlint') [SonarLint](https://www.sonarsource.com/products/sonarlint/features/connected-mode/?referrer=sonarcloud-welcome)
sonarcloud[bot]: SonarCloud Quality Gate failed.&nbsp; &nbsp; [![Quality Gate failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/failed-16px.png 'Quality Gate failed')](https://sonarcloud.io/dashboard?id=kroxylicious_kroxylicious&pullRequest=510)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=510&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=510&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=510&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=510&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=510&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=510&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=kroxylicious_kroxylicious&pullRequest=510&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=kroxylicious_kroxylicious&pullRequest=510&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=kroxylicious_kroxylicious&pullRequest=510&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=510&resolved=false&types=CODE_SMELL) [![B](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/B-16px.png 'B')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=510&resolved=false&types=CODE_SMELL) [5 Code Smells](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=510&resolved=false&types=CODE_SMELL)

[![66.7%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/60-16px.png '66.7%')](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=510&metric=new_coverage&view=list) [66.7% Coverage](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=510&metric=new_coverage&view=list)  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=510&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=510&metric=new_duplicated_lines_density&view=list)



![idea](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/light_bulb-16px.png 'idea') Catch issues before they fail your Quality Gate with our IDE extension ![sonarlint](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/sonarlint-16px.png 'sonarlint') [SonarLint](https://www.sonarsource.com/products/sonarlint/features/connected-mode/?referrer=sonarcloud-welcome)
sonarcloud[bot]: SonarCloud Quality Gate failed.&nbsp; &nbsp; [![Quality Gate failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/failed-16px.png 'Quality Gate failed')](https://sonarcloud.io/dashboard?id=kroxylicious_kroxylicious&pullRequest=510)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=510&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=510&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=510&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=510&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=510&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=510&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=kroxylicious_kroxylicious&pullRequest=510&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=kroxylicious_kroxylicious&pullRequest=510&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=kroxylicious_kroxylicious&pullRequest=510&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=510&resolved=false&types=CODE_SMELL) [![B](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/B-16px.png 'B')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=510&resolved=false&types=CODE_SMELL) [5 Code Smells](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=510&resolved=false&types=CODE_SMELL)

[![66.7%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/60-16px.png '66.7%')](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=510&metric=new_coverage&view=list) [66.7% Coverage](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=510&metric=new_coverage&view=list)  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=510&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=510&metric=new_duplicated_lines_density&view=list)



![idea](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/light_bulb-16px.png 'idea') Catch issues before they fail your Quality Gate with our IDE extension ![sonarlint](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/sonarlint-16px.png 'sonarlint') [SonarLint](https://www.sonarsource.com/products/sonarlint/features/connected-mode/?referrer=sonarcloud-welcome)
sonarcloud[bot]: SonarCloud Quality Gate failed.&nbsp; &nbsp; [![Quality Gate failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/failed-16px.png 'Quality Gate failed')](https://sonarcloud.io/dashboard?id=kroxylicious_kroxylicious&pullRequest=510)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=510&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=510&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=510&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=510&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=510&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=510&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=kroxylicious_kroxylicious&pullRequest=510&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=kroxylicious_kroxylicious&pullRequest=510&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=kroxylicious_kroxylicious&pullRequest=510&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=510&resolved=false&types=CODE_SMELL) [![B](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/B-16px.png 'B')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=510&resolved=false&types=CODE_SMELL) [5 Code Smells](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=510&resolved=false&types=CODE_SMELL)

[![66.7%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/60-16px.png '66.7%')](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=510&metric=new_coverage&view=list) [66.7% Coverage](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=510&metric=new_coverage&view=list)  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=510&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=510&metric=new_duplicated_lines_density&view=list)



![idea](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/light_bulb-16px.png 'idea') Catch issues before they fail your Quality Gate with our IDE extension ![sonarlint](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/sonarlint-16px.png 'sonarlint') [SonarLint](https://www.sonarsource.com/products/sonarlint/features/connected-mode/?referrer=sonarcloud-welcome)
sonarcloud[bot]: SonarCloud Quality Gate failed.&nbsp; &nbsp; [![Quality Gate failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/failed-16px.png 'Quality Gate failed')](https://sonarcloud.io/dashboard?id=kroxylicious_kroxylicious&pullRequest=510)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=510&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=510&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=510&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=510&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=510&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=510&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=kroxylicious_kroxylicious&pullRequest=510&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=kroxylicious_kroxylicious&pullRequest=510&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=kroxylicious_kroxylicious&pullRequest=510&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=510&resolved=false&types=CODE_SMELL) [![B](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/B-16px.png 'B')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=510&resolved=false&types=CODE_SMELL) [5 Code Smells](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=510&resolved=false&types=CODE_SMELL)

[![66.7%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/60-16px.png '66.7%')](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=510&metric=new_coverage&view=list) [66.7% Coverage](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=510&metric=new_coverage&view=list)  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=510&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=510&metric=new_duplicated_lines_density&view=list)



![idea](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/light_bulb-16px.png 'idea') Catch issues before they fail your Quality Gate with our IDE extension ![sonarlint](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/sonarlint-16px.png 'sonarlint') [SonarLint](https://www.sonarsource.com/products/sonarlint/features/connected-mode/?referrer=sonarcloud-welcome)
sonarcloud[bot]: SonarCloud Quality Gate failed.&nbsp; &nbsp; [![Quality Gate failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/failed-16px.png 'Quality Gate failed')](https://sonarcloud.io/dashboard?id=kroxylicious_kroxylicious&pullRequest=510)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=510&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=510&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=510&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=510&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=510&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=510&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=kroxylicious_kroxylicious&pullRequest=510&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=kroxylicious_kroxylicious&pullRequest=510&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=kroxylicious_kroxylicious&pullRequest=510&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=510&resolved=false&types=CODE_SMELL) [![B](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/B-16px.png 'B')](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=510&resolved=false&types=CODE_SMELL) [4 Code Smells](https://sonarcloud.io/project/issues?id=kroxylicious_kroxylicious&pullRequest=510&resolved=false&types=CODE_SMELL)

[![66.7%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/60-16px.png '66.7%')](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=510&metric=new_coverage&view=list) [66.7% Coverage](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=510&metric=new_coverage&view=list)  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=510&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=kroxylicious_kroxylicious&pullRequest=510&metric=new_duplicated_lines_density&view=list)



![idea](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/light_bulb-16px.png 'idea') Catch issues before they fail your Quality Gate with our IDE extension ![sonarlint](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/sonarlint-16px.png 'sonarlint') [SonarLint](https://www.sonarsource.com/products/sonarlint/features/connected-mode/?referrer=sonarcloud-welcome)

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 39441809..2f3fbc81 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,7 +6,7 @@ Please enumerate **all user-facing** changes using format `<githib issue/pr numb
 
 ## 0.3.0
 
-
+* [#510](https://github.com/kroxylicious/kroxylicious/pull/510 Add multi-tenant kubernetes example
 * [#519](https://github.com/kroxylicious/kroxylicious/pull/519): Fix Kafka Client leaks in the SampleFilterIntegrationTest.
 * [#494](https://github.com/kroxylicious/kroxylicious/issues/494): [Breaking] Make the Filter API fully asynchronous (filter methods must return a CompletionStage)
 * [#498](https://github.com/kroxylicious/kroxylicious/issues/498): Include the cluster name from the configuration node in the config model.
diff --git a/Dockerfile b/Dockerfile
index a9e5f516..d2616c3d 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -9,7 +9,7 @@ FROM registry.access.redhat.com/ubi9/openjdk-17:1.15 AS builder
 USER root
 WORKDIR /opt/kroxylicious
 COPY . .
-RUN ./mvnw -B clean verify -Pdist -Dquick -pl :kroxylicious-bom,:kroxylicious -am
+RUN ./mvnw -B clean verify -Pdist,withAdditionalFilters -Dquick -pl :kroxylicious-bom,:kroxylicious -am
 USER 185
 FROM registry.access.redhat.com/ubi9/ubi-minimal:9.2
 
diff --git a/kroxylicious-additional-filters/kroxylicious-multitenant/src/main/java/io/kroxylicious/proxy/filter/multitenant/MultiTenantTransformationFilter.java b/kroxylicious-additional-filters/kroxylicious-multitenant/src/main/java/io/kroxylicious/proxy/filter/multitenant/MultiTenantTransformationFilter.java
index 9df54175..6c20b4f0 100644
--- a/kroxylicious-additional-filters/kroxylicious-multitenant/src/main/java/io/kroxylicious/proxy/filter/multitenant/MultiTenantTransformationFilter.java
+++ b/kroxylicious-additional-filters/kroxylicious-multitenant/src/main/java/io/kroxylicious/proxy/filter/multitenant/MultiTenantTransformationFilter.java
@@ -488,16 +488,12 @@ public class MultiTenantTransformationFilter
     }
 
     private static String getTenantPrefix(KrpcFilterContext context) {
-        // TODO naive - POC implementation uses the first component of a FQDN as the multi-tenant prefix.
-        var sniHostname = context.sniHostname();
-        if (sniHostname == null) {
-            throw new IllegalStateException("This filter requires that the client provides a TLS SNI hostname.");
+        // TODO naive - POC implementation uses virtual cluster name as a tenant prefix
+        var virtualClusterName = context.getVirtualClusterName();
+        if (virtualClusterName == null) {
+            throw new IllegalStateException("This filter requires that the virtual cluster has a name");
         }
-        int dot = sniHostname.indexOf(".");
-        if (dot < 1) {
-            throw new IllegalStateException("Unexpected SNI hostname formation. SNI hostname : " + sniHostname);
-        }
-        return sniHostname.substring(0, dot) + "-";
+        return virtualClusterName + "-";
     }
 
     public MultiTenantTransformationFilter() {
diff --git a/kroxylicious-api/src/main/java/io/kroxylicious/proxy/filter/KrpcFilterContext.java b/kroxylicious-api/src/main/java/io/kroxylicious/proxy/filter/KrpcFilterContext.java
index 25e9077b..e77f4910 100644
--- a/kroxylicious-api/src/main/java/io/kroxylicious/proxy/filter/KrpcFilterContext.java
+++ b/kroxylicious-api/src/main/java/io/kroxylicious/proxy/filter/KrpcFilterContext.java
@@ -110,5 +110,10 @@ public interface KrpcFilterContext {
      */
     ResponseFilterResultBuilder responseFilterResultBuilder();
 
+    /**
+     * Allows the filter to identify which cluster it is processing a request for
+     * @return virtual cluster name
+     */
+    String getVirtualClusterName();
     // TODO an API to allow a filter to add/remove another filter from the pipeline
 }
diff --git a/kroxylicious/pom.xml b/kroxylicious/pom.xml
index 794b6e32..92bde857 100644
--- a/kroxylicious/pom.xml
+++ b/kroxylicious/pom.xml
@@ -327,6 +327,19 @@
                 </plugins>
             </build>
         </profile>
+        <profile>
+            <id>withAdditionalFilters</id>
+            <dependencies>
+                <dependency>
+                    <groupId>io.kroxylicious</groupId>
+                    <artifactId>kroxylicious-multitenant</artifactId>
+                </dependency>
+                <dependency>
+                    <groupId>io.kroxylicious</groupId>
+                    <artifactId>kroxylicious-schema-validation</artifactId>
+                </dependency>
+            </dependencies>
+        </profile>
         <profile>
             <id>architecture-check</id>
             <repositories>
diff --git a/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/DefaultFilterContext.java b/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/DefaultFilterContext.java
index 7ceb28f0..352000a2 100644
--- a/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/DefaultFilterContext.java
+++ b/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/DefaultFilterContext.java
@@ -36,6 +36,7 @@ import io.kroxylicious.proxy.future.InternalCompletionStage;
 import io.kroxylicious.proxy.internal.filter.RequestFilterResultBuilderImpl;
 import io.kroxylicious.proxy.internal.filter.ResponseFilterResultBuilderImpl;
 import io.kroxylicious.proxy.internal.util.ByteBufOutputStream;
+import io.kroxylicious.proxy.model.VirtualCluster;
 
 /**
  * Implementation of {@link KrpcFilterContext}.
@@ -50,19 +51,22 @@ class DefaultFilterContext implements KrpcFilterContext {
     private final KrpcFilter filter;
     private final long timeoutMs;
     private final String sniHostname;
+    private final VirtualCluster virtualCluster;
 
     DefaultFilterContext(KrpcFilter filter,
                          ChannelHandlerContext channelContext,
                          DecodedFrame<?, ?> decodedFrame,
                          ChannelPromise promise,
                          long timeoutMs,
-                         String sniHostname) {
+                         String sniHostname,
+                         VirtualCluster virtualCluster) {
         this.filter = filter;
         this.channelContext = channelContext;
         this.decodedFrame = decodedFrame;
         this.promise = promise;
         this.timeoutMs = timeoutMs;
         this.sniHostname = sniHostname;
+        this.virtualCluster = virtualCluster;
     }
 
     /**
@@ -229,6 +233,10 @@ class DefaultFilterContext implements KrpcFilterContext {
         });
     }
 
+    public String getVirtualClusterName() {
+        return virtualCluster.getClusterName();
+    }
+
     /**
      * In this case we are not forwarding to the proxied broker but responding immediately.
      * We want to check that the ApiMessage is the correct type for the request. Ie if the
diff --git a/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/FilterHandler.java b/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/FilterHandler.java
index bed0a1f1..67973459 100644
--- a/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/FilterHandler.java
+++ b/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/FilterHandler.java
@@ -27,6 +27,7 @@ import io.kroxylicious.proxy.frame.DecodedResponseFrame;
 import io.kroxylicious.proxy.frame.OpaqueRequestFrame;
 import io.kroxylicious.proxy.frame.OpaqueResponseFrame;
 import io.kroxylicious.proxy.internal.util.Assertions;
+import io.kroxylicious.proxy.model.VirtualCluster;
 
 /**
  * A {@code ChannelInboundHandler} (for handling requests from downstream)
@@ -38,13 +39,15 @@ public class FilterHandler extends ChannelDuplexHandler {
     private final KrpcFilter filter;
     private final long timeoutMs;
     private final String sniHostname;
+    private final VirtualCluster virtualCluster;
     private final FilterInvoker invoker;
 
-    public FilterHandler(FilterAndInvoker filterAndInvoker, long timeoutMs, String sniHostname) {
+    public FilterHandler(FilterAndInvoker filterAndInvoker, long timeoutMs, String sniHostname, VirtualCluster virtualCluster) {
         this.filter = Objects.requireNonNull(filterAndInvoker).filter();
         this.invoker = filterAndInvoker.invoker();
         this.timeoutMs = Assertions.requireStrictlyPositive(timeoutMs, "timeout");
         this.sniHostname = sniHostname;
+        this.virtualCluster = virtualCluster;
     }
 
     String filterDescriptor() {
@@ -54,7 +57,7 @@ public class FilterHandler extends ChannelDuplexHandler {
     @Override
     public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
         if (msg instanceof DecodedRequestFrame<?> decodedFrame) {
-            var filterContext = new DefaultFilterContext(filter, ctx, decodedFrame, promise, timeoutMs, sniHostname);
+            var filterContext = new DefaultFilterContext(filter, ctx, decodedFrame, promise, timeoutMs, sniHostname, virtualCluster);
             if (LOGGER.isDebugEnabled()) {
                 LOGGER.debug("{}: Dispatching downstream {} request to filter{}: {}",
                         ctx.channel(), decodedFrame.apiKey(), filterDescriptor(), msg);
@@ -127,7 +130,7 @@ public class FilterHandler extends ChannelDuplexHandler {
                 p.complete(decodedFrame.body());
             }
             else {
-                var filterContext = new DefaultFilterContext(filter, ctx, decodedFrame, null, timeoutMs, sniHostname);
+                var filterContext = new DefaultFilterContext(filter, ctx, decodedFrame, null, timeoutMs, sniHostname, virtualCluster);
                 if (LOGGER.isDebugEnabled()) {
                     LOGGER.debug("{}: Dispatching upstream {} response to filter {}: {}",
                             ctx.channel(), decodedFrame.apiKey(), filterDescriptor(), msg);
diff --git a/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/KafkaProxyFrontendHandler.java b/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/KafkaProxyFrontendHandler.java
index c1e88766..0b168503 100644
--- a/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/KafkaProxyFrontendHandler.java
+++ b/kroxylicious/src/main/java/io/kroxylicious/proxy/internal/KafkaProxyFrontendHandler.java
@@ -304,7 +304,7 @@ public class KafkaProxyFrontendHandler
     private void addFiltersToPipeline(List<FilterAndInvoker> filters, ChannelPipeline pipeline) {
         for (var filter : filters) {
             // TODO configurable timeout
-            pipeline.addFirst(filter.toString(), new FilterHandler(filter, 20000, sniHostname));
+            pipeline.addFirst(filter.toString(), new FilterHandler(filter, 20000, sniHostname, virtualCluster));
         }
     }
 
diff --git a/kubernetes-examples/multitenant/base/kafka-persistent.yaml b/kubernetes-examples/multitenant/base/kafka-persistent.yaml
new file mode 100644
index 00000000..4b279e7e
--- /dev/null
+++ b/kubernetes-examples/multitenant/base/kafka-persistent.yaml
@@ -0,0 +1,40 @@
+#
+# Copyright Kroxylicious Authors.
+#
+# Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+#
+
+---
+apiVersion: kafka.strimzi.io/v1beta2
+kind: Kafka
+metadata:
+  name: my-cluster
+spec:
+  kafka:
+    version: 3.4.0
+    replicas: 3
+    listeners:
+      - name: plain
+        port: 9092
+        type: internal
+        tls: false
+    config:
+      offsets.topic.replication.factor: 2
+      transaction.state.log.replication.factor: 2
+      transaction.state.log.min.isr: 2
+      default.replication.factor: 2
+      min.insync.replicas: 2
+      inter.broker.protocol.version: "3.4"
+    storage:
+      type: jbod
+      volumes:
+      - id: 0
+        type: persistent-claim
+        size: 100Gi
+        deleteClaim: false
+  zookeeper:
+    replicas: 1
+    storage:
+      type: persistent-claim
+      size: 100Gi
+      deleteClaim: false
diff --git a/kubernetes-examples/multitenant/base/kroxylicious-config.yaml b/kubernetes-examples/multitenant/base/kroxylicious-config.yaml
new file mode 100644
index 00000000..5bc08d85
--- /dev/null
+++ b/kubernetes-examples/multitenant/base/kroxylicious-config.yaml
@@ -0,0 +1,37 @@
+#
+# Copyright Kroxylicious Authors.
+#
+# Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+#
+
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: kroxylicious-config
+data:
+  config.yaml: |
+    filters:
+    - type: MultiTenant
+    adminHttp:
+      endpoints:
+        prometheus: {}
+    virtualClusters:
+      devenv1:
+        targetCluster:
+          bootstrap_servers: my-cluster-kafka-bootstrap:9092
+        clusterNetworkAddressConfigProvider:
+          type: PortPerBroker
+          config:
+            bootstrapAddress: minikube:30192
+        logNetwork: false
+        logFrames: false
+      devenv2:
+        targetCluster:
+          bootstrap_servers: my-cluster-kafka-bootstrap:9092
+        clusterNetworkAddressConfigProvider:
+          type: PortPerBroker
+          config:
+            bootstrapAddress: minikube:30292
+        logNetwork: false
+        logFrames: false
diff --git a/kubernetes-examples/multitenant/base/kroxylicious-deployment.yaml b/kubernetes-examples/multitenant/base/kroxylicious-deployment.yaml
new file mode 100644
index 00000000..68f34368
--- /dev/null
+++ b/kubernetes-examples/multitenant/base/kroxylicious-deployment.yaml
@@ -0,0 +1,47 @@
+#
+# Copyright Kroxylicious Authors.
+#
+# Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+#
+
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: kroxylicious-proxy
+  labels:
+    app: kroxylicious
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: kroxylicious
+  template:
+    metadata:
+      labels:
+        app: kroxylicious
+    spec:
+      containers:
+      - name: kroxylicious
+        image: quay.io/kroxylicious/kroxylicious-developer:0.3.0-SNAPSHOT
+        imagePullPolicy: Always
+        args: ["--config", "/opt/kroxylicious/config/config.yaml"]
+        ports:
+        # Tenant devenv1
+        - containerPort: 30192
+        - containerPort: 30193
+        - containerPort: 30194
+        - containerPort: 30195
+        # Tenant devenv2
+        - containerPort: 30292
+        - containerPort: 30293
+        - containerPort: 30294
+        - containerPort: 30295
+        volumeMounts:
+        - name: config-volume
+          mountPath: /opt/kroxylicious/config/config.yaml
+          subPath: config.yaml
+      volumes:
+      - name: config-volume
+        configMap:
+          name: kroxylicious-config
diff --git a/kubernetes-examples/multitenant/base/kroxylicious-service.yaml b/kubernetes-examples/multitenant/base/kroxylicious-service.yaml
new file mode 100644
index 00000000..087589d2
--- /dev/null
+++ b/kubernetes-examples/multitenant/base/kroxylicious-service.yaml
@@ -0,0 +1,63 @@
+#
+# Copyright Kroxylicious Authors.
+#
+# Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+#
+
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: kroxylicious-service
+spec:
+  type: NodePort
+  selector:
+    app: kroxylicious
+  ports:
+  - name: port-30192
+    protocol: TCP
+    port: 30192
+    targetPort: 30192
+    nodePort: 30192
+
+  - name: port-30193
+    protocol: TCP
+    port: 30193
+    targetPort: 30193
+    nodePort: 30193
+
+  - name: port-30194
+    protocol: TCP
+    port: 30194
+    targetPort: 30194
+    nodePort: 30194
+
+  - name: port-30195
+    protocol: TCP
+    port: 30195
+    targetPort: 30195
+    nodePort: 30195
+
+  - name: port-30292
+    protocol: TCP
+    port: 30292
+    targetPort: 30292
+    nodePort: 30292
+
+  - name: port-30293
+    protocol: TCP
+    port: 30293
+    targetPort: 30293
+    nodePort: 30293
+
+  - name: port-30294
+    protocol: TCP
+    port: 30294
+    targetPort: 30294
+    nodePort: 30294
+
+  - name: port-30295
+    protocol: TCP
+    port: 30295
+    targetPort: 30295
+    nodePort: 30295
diff --git a/kubernetes-examples/multitenant/base/kustomization.yaml b/kubernetes-examples/multitenant/base/kustomization.yaml
new file mode 100644
index 00000000..8eb257cf
--- /dev/null
+++ b/kubernetes-examples/multitenant/base/kustomization.yaml
@@ -0,0 +1,18 @@
+#
+# Copyright Kroxylicious Authors.
+#
+# Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+#
+
+---
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+
+namespace: kafka
+
+resources:
+- kroxylicious-config.yaml
+- kroxylicious-deployment.yaml
+- kroxylicious-service.yaml
+- kafka-persistent.yaml
+
diff --git a/kubernetes-examples/multitenant/overlays/minikube/kustomization.yaml b/kubernetes-examples/multitenant/overlays/minikube/kustomization.yaml
new file mode 100644
index 00000000..c05894d1
--- /dev/null
+++ b/kubernetes-examples/multitenant/overlays/minikube/kustomization.yaml
@@ -0,0 +1,11 @@
+#
+# Copyright Kroxylicious Authors.
+#
+# Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+#
+
+---
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+resources:
+- ../../base
diff --git a/kubernetes-examples/multitenant/postinstall.sh b/kubernetes-examples/multitenant/postinstall.sh
new file mode 100755
index 00000000..c54702cc
--- /dev/null
+++ b/kubernetes-examples/multitenant/postinstall.sh
@@ -0,0 +1,47 @@
+#!/bin/bash
+#
+# Copyright Kroxylicious Authors.
+#
+# Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+#
+
+set -eo pipefail
+
+STRIMZI_KAFKA=quay.io/strimzi/kafka:0.35.0-kafka-3.4.0
+BOOTSTRAP=my-cluster-kafka-bootstrap:9092
+
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+NOCOLOR='\033[0m'
+
+MINIKUBE_IP=$(minikube ip)
+echo -e  "${YELLOW}Please add/update following link to your '/etc/hosts'.${NOCOLOR}"
+echo -e  "${YELLOW}${MINIKUBE_IP} minikube${NOCOLOR}"
+echo
+
+echo "Then use commands like these to explore multi-tenancy."
+echo
+
+echo -e "${GREEN}Configure two tenants: ${NOCOLOR}"
+echo "kaf config add-cluster devenv1 -b minikube:30192"
+echo "kaf config add-cluster devenv2 -b minikube:30292"
+echo
+
+echo -e "${GREEN}Do some work in devenv1${NOCOLOR}"
+echo "kaf config use-cluster devenv1"
+echo "kaf topic create billingapp"
+echo "kaf topic create foo"
+echo "echo 'hello, world' | kaf produce billingapp"
+echo "kaf consume billingapp --group billinggroup --commit"
+echo "kaf topics"
+echo "kaf groups"
+echo
+
+echo -e "${GREEN}Now do some work in devenv2${NOCOLOR}"
+echo "kaf config use-cluster devenv2"
+echo "kaf topics"
+echo "etc.."
+
+echo -e "${GREEN}Finally let's lift the bonnet and take a look at the kafka underneath${NOCOLOR}"
+echo "kubectl -n kafka run listtopics -ti --image=${STRIMZI_KAFKA} --rm=true --restart=Never -- bin/kafka-topics.sh --bootstrap-server ${BOOTSTRAP} --list"
+echo "kubectl -n kafka run listgroups -ti --image=${STRIMZI_KAFKA} --rm=true --restart=Never -- bin/kafka-consumer-groups.sh --bootstrap-server ${BOOTSTRAP} --list"
diff --git a/kubernetes-examples/multitenant/script.txt b/kubernetes-examples/multitenant/script.txt
new file mode 100644
index 00000000..c65e683c
--- /dev/null
+++ b/kubernetes-examples/multitenant/script.txt
@@ -0,0 +1,151 @@
+====
+    Copyright Kroxylicious Authors.
+
+    Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+====
+
+This script was used to produce https://asciinema.org/a/kEG5mTwegb8yf9efwAKKT6iRW.
+I used the "Paste Special" feature of iTerm to inject the below text into a terminal session which was being recorded
+with asciinema. The "Paste Special" chunking options were tune to give an appearance of a human typing.
+
+# This demo will look at the Kroxylicious Multi-tenant Feature. It is an incubating feature, so it is not
+# ready for production use. But please try it and send us some feedback.
+
+# let us imagine that a team needs each developer to have their own Kafka Cluster for development work.
+# the team would like to avoid the costs associated with running a cluster per developer.
+
+# we will use Strimzi to deploy a Kafka Cluster to Kubernetes, then use Kroxylicious to expose that single
+# Kafka Cluster as if it were many clusters, with each tenant isolated from one and other.
+
+# the kafka cluster and kroxylicious will be deployed to Kubernetes.
+# on the client side, kaf (https://github.com/birdayz/kaf) will be used to demonstrate the isolation. It will run
+# off-cluster. it will connect to the kroxylicious via a nodeport kubernetes service.
+
+# we'll use a script to install strimzi, create a kafka cluster, and deployment kroxylicious.
+# let's do that now
+
+QUAY_ORG=k_wall ./scripts/run-with-strimzi.sh kubernetes-examples/multitenant
+
+# let's look at what we have running in kube.  It's all in the kubernetes namespace kafka.
+
+kubectl config set-context --current --namespace=kafka
+
+# let's start with kafka cluster.  this is the cluster we'll expose using kroxylicious.
+kubectl get kafka my-cluster
+
+# notice it's got three brokers (replicas)
+
+kubectl get kafka my-cluster -o yaml
+
+# notice also the bootstrap address is my-cluster-kafka-bootstrap.kafka.svc:9092.
+# We'll see the bootstrap address wired in kroxylicious in its configuration.
+
+# Let's see the kroxylicious configuration now.
+
+ kubectl get cm kroxylicious-config -o yaml
+
+# there are a couple of important bits:
+# - the filters with the MultiTenant - this enables the MultiTenant feature
+# - the two 'virtual' cluster definitions, devenv1 and devenv2
+#   - these provide the isolated kafka cluster environment to our developers.
+#   - each virtual cluster has its own bootstrap.
+# notice that both virtual cluster point to (target) our kafka cluster.
+
+# finally there is a nodeport kubernetes service that is exposing kroxylicious to the host
+
+kubectl get service kroxylicious-service
+
+# let's connect kafka clients up to the virtual clusters and do some
+# work to illustrate that the two environments are indeed separate from one and other.
+# later, we'll take a peep directly at the kafka cluster so you'll appreciate how naming is used to
+# achieve the isolation.
+
+
+# we are going to use kaf to create a topic called billingapp, and then publish and consume a message to the topic.
+
+# let's configure kaf to know about devenv1.
+# notice we are using the bootstrap of the first virtual cluster
+
+kaf config add-cluster devenv1 -b minikube:30192
+kaf config select-cluster
+
+# look at the broker topology
+kaf nodes
+# notice these are brokers presented by the virtual cluster, not the real cluster
+
+# now let's create that topic
+kaf topic create billingapp
+
+# confirm the topic exists
+kaf topics
+
+# publish a message to it
+echo 'hello, world' | kaf produce billingapp
+
+# consume it using a group, and commit the offsets
+kaf consume billingapp --group billinggroup --commit
+
+# finally, let's show the group exists
+kaf groups
+
+# Now let's switch to devenv2 and demonstrate devenv2 is independent of devenv1
+
+kaf config add-cluster devenv2 -b minikube:30292
+kaf config select-cluster
+
+# let's look at the broker topology
+kaf nodes
+# notice these are brokers presented by the virtual cluster too, again not the real cluster.
+# notice too, the port numbers are different from those used by devenv1
+
+# let's check the topics and groups. we expect this environment to have none.
+kaf topics
+kaf groups
+
+# now let's create a billingapp topic in this environment too.  like before
+# we'll produce and consume a message to it, but we'll use a different message.
+# like before we'll use the same group name.
+
+# the resources will be independent of those in devenv1 even though they have the
+# same names.
+
+# let's create that topic
+kaf topic create billingapp
+
+# confirm the topic exists
+kaf topics
+
+# publish a message to it, with a message in spanish rather than english
+echo 'hola, mundo' | kaf produce billingapp
+
+# consume it using a group
+kaf consume billingapp --group billinggroup --commit
+
+# okay, time to look beneath the hood at the kafka cluster.  how's the multi-tenancy
+# feature actually wired things up?  Let's look at the topics and groups present on the
+# cluster itself.
+
+# the kafka cluster actually isn't available off cluster, so we'll use an ephemeral pod
+# to query it using the Apache Kafka tooling.
+
+# first the topics
+kubectl -n kafka run listtopics -ti --image=quay.io/strimzi/kafka:0.35.0-kafka-3.4.0 --rm=true --restart=Never -- bin/kafka-topics.sh --bootstrap-server my-cluster-kafka-bootstrap:9092 --list
+# notice how the topics names are prefixed with the virtual cluster's name.
+
+# now, the groups
+kubectl -n kafka run listgroups -ti --image=quay.io/strimzi/kafka:0.35.0-kafka-3.4.0 --rm=true --restart=Never -- bin/kafka-consumer-groups.sh --bootstrap-server my-cluster-kafka-bootstrap:9092 --list
+# notice that the same prefixing has been done.
+
+# let's wrap up.
+# what have we seen?  we've seen a kafka cluster of three brokers presented to the world as if it
+# were two independent kafka clusters.  The MultiTenant filter dynamically rewrites the resource names to
+# create isolation between the tenants.  This will takes place transparently: no extra software on the client or broker
+# and no extra configuration, beyond bootstrap.
+
+# Hope you've found the demo interesting.  If you have questions get in touch via https://kroxylicious.slack.com/
+
+
+
+
+
+


Test patch:
diff --git a/kroxylicious-additional-filters/kroxylicious-multitenant/src/test/java/io/kroxylicious/proxy/filter/multitenant/MultiTenantTransformationFilterTest.java b/kroxylicious-additional-filters/kroxylicious-multitenant/src/test/java/io/kroxylicious/proxy/filter/multitenant/MultiTenantTransformationFilterTest.java
index d351cb72..773c27ea 100644
--- a/kroxylicious-additional-filters/kroxylicious-multitenant/src/test/java/io/kroxylicious/proxy/filter/multitenant/MultiTenantTransformationFilterTest.java
+++ b/kroxylicious-additional-filters/kroxylicious-multitenant/src/test/java/io/kroxylicious/proxy/filter/multitenant/MultiTenantTransformationFilterTest.java
@@ -17,6 +17,7 @@ import org.apache.kafka.common.message.RequestHeaderData;
 import org.apache.kafka.common.message.ResponseHeaderData;
 import org.apache.kafka.common.protocol.ApiKeys;
 import org.apache.kafka.common.protocol.ApiMessage;
+import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.extension.ExtendWith;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.Arguments;
@@ -51,7 +52,7 @@ import static org.mockito.Mockito.when;
 class MultiTenantTransformationFilterTest {
     private static final Pattern TEST_RESOURCE_FILTER = Pattern.compile(
             String.format("%s/.*\\.yaml", MultiTenantTransformationFilterTest.class.getPackageName().replace(".", "/")));
-    private static final String TENANT_1 = "tenant1.kafka.example.com";
+    private static final String TENANT_1 = "tenant1";
 
     private static List<ResourceInfo> getTestResources() throws IOException {
         var resources = ClassPath.from(MultiTenantTransformationFilterTest.class.getClassLoader()).getResources().stream()
@@ -85,6 +86,11 @@ class MultiTenantTransformationFilterTest {
     @Captor
     private ArgumentCaptor<ApiMessage> apiMessageCaptor;
 
+    @BeforeEach
+    public void beforeEach() {
+        when(context.getVirtualClusterName()).thenReturn(TENANT_1);
+    }
+
     public static Stream<Arguments> requests() throws Exception {
         return RequestResponseTestDef.requestResponseTestDefinitions(getTestResources()).filter(td -> td.request() != null)
                 .map(td -> Arguments.of(td.testName(), td.apiKey(), td.header(), td.request()));
@@ -99,7 +105,6 @@ class MultiTenantTransformationFilterTest {
         var requestWriter = requestConverterFor(apiMessageType).writer();
         var marshalled = requestWriter.apply(request, header.requestApiVersion());
 
-        when(context.sniHostname()).thenReturn(TENANT_1);
         when(requestFilterResult.message()).thenAnswer(invocation -> apiMessageCaptor.getValue());
         when(requestFilterResult.header()).thenAnswer(invocation -> requestHeaderDataCaptor.getValue());
         when(context.forwardRequest(requestHeaderDataCaptor.capture(), apiMessageCaptor.capture())).thenAnswer(
@@ -128,7 +133,6 @@ class MultiTenantTransformationFilterTest {
 
         var marshalled = responseWriter.apply(response, header.requestApiVersion());
 
-        when(context.sniHostname()).thenReturn(TENANT_1);
         when(responseFilterResult.message()).thenAnswer(invocation -> apiMessageCaptor.getValue());
         when(responseFilterResult.header()).thenAnswer(invocation -> responseHeaderDataArgumentCaptor.getValue());
         when(context.forwardResponse(responseHeaderDataArgumentCaptor.capture(), apiMessageCaptor.capture())).thenAnswer(
diff --git a/kroxylicious/src/test/java/io/kroxylicious/proxy/internal/FilterHarness.java b/kroxylicious/src/test/java/io/kroxylicious/proxy/internal/FilterHarness.java
index f117e2ac..39c5e951 100644
--- a/kroxylicious/src/test/java/io/kroxylicious/proxy/internal/FilterHarness.java
+++ b/kroxylicious/src/test/java/io/kroxylicious/proxy/internal/FilterHarness.java
@@ -5,6 +5,7 @@
  */
 package io.kroxylicious.proxy.internal;
 
+import java.util.Optional;
 import java.util.concurrent.CompletableFuture;
 
 import org.apache.kafka.common.message.RequestHeaderData;
@@ -15,13 +16,17 @@ import org.junit.jupiter.api.AfterEach;
 
 import io.netty.channel.embedded.EmbeddedChannel;
 
+import io.kroxylicious.proxy.config.TargetCluster;
 import io.kroxylicious.proxy.filter.FilterAndInvoker;
 import io.kroxylicious.proxy.filter.KrpcFilter;
 import io.kroxylicious.proxy.frame.DecodedRequestFrame;
 import io.kroxylicious.proxy.frame.DecodedResponseFrame;
+import io.kroxylicious.proxy.model.VirtualCluster;
+import io.kroxylicious.proxy.service.ClusterNetworkAddressConfigProvider;
 
 import static com.google.common.collect.Iterables.getOnlyElement;
 import static org.junit.jupiter.api.Assertions.fail;
+import static org.mockito.Mockito.mock;
 
 /**
  * A test harness for {@link KrpcFilter} implementations.
@@ -49,7 +54,8 @@ public abstract class FilterHarness {
      */
     protected void buildChannel(KrpcFilter filter, long timeoutMs) {
         this.filter = filter;
-        filterHandler = new FilterHandler(getOnlyElement(FilterAndInvoker.build(filter)), timeoutMs, null);
+        filterHandler = new FilterHandler(getOnlyElement(FilterAndInvoker.build(filter)), timeoutMs, null,
+                new VirtualCluster("TestVirtualCluster", mock(TargetCluster.class), mock(ClusterNetworkAddressConfigProvider.class), Optional.empty(), false, false));
         channel = new EmbeddedChannel(filterHandler);
     }
 
diff --git a/performance-tests/src/main/java/io/kroxylicious/benchmarks/InvokerDispatchBenchmark.java b/performance-tests/src/main/java/io/kroxylicious/benchmarks/InvokerDispatchBenchmark.java
index f8d4873e..962b52f4 100644
--- a/performance-tests/src/main/java/io/kroxylicious/benchmarks/InvokerDispatchBenchmark.java
+++ b/performance-tests/src/main/java/io/kroxylicious/benchmarks/InvokerDispatchBenchmark.java
@@ -173,6 +173,11 @@ public class InvokerDispatchBenchmark {
             return null;
         }
 
+        @Override
+        public String getVirtualClusterName() {
+            return null;
+        }
+
         @Override
         public <T extends ApiMessage> CompletionStage<T> sendRequest(short apiVersion, ApiMessage request) {
             return null;