Skip to content
This repository was archived by the owner on Jul 26, 2022. It is now read-only.

Commit 85739fd

Browse files
feat(multitenancy): Allow to watch ExternalSecrets in specific namespaces (#548)
* feat: allow to watch externalsecrets in specified namespaces * enable debug for e2e test * refactor: call startWatcher for each namespace instead of handling multi namespaces inside the watcher method * chore: update e2e/README.md * chore: remove DEBUG log for e2e workflow * chore: update readme, text + add env to chart readme * chore: update chart readme add format for watched namespaces env * fix: add WATCHED_NAMESPACES default value to helm chart values.yaml Co-authored-by: Markus Maga <[email protected]>
1 parent c9d7785 commit 85739fd

File tree

9 files changed

+99
-28
lines changed

9 files changed

+99
-28
lines changed

.github/workflows/workflow.yml

+1
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,4 @@ jobs:
3636
helm init --client-only
3737
if: matrix.helmVersion == 'V2'
3838
- run: ./e2e/run-e2e-suite.sh ${{ matrix.disableCustomResourceManager }} ${{ matrix.helmVersion }}
39+

README.md

+30-2
Original file line numberDiff line numberDiff line change
@@ -230,9 +230,12 @@ spec:
230230
name: .dockerconfigjson
231231
```
232232

233-
## Enforcing naming conventions for backend keys
233+
## Scoping access
234234

235-
by default an `ExternalSecret` may access arbitrary keys from the backend e.g.
235+
### Using Namespace annotation
236+
237+
Enforcing naming conventions for backend keys could be done by using namespace annotations.
238+
By default an `ExternalSecret` may access arbitrary keys from the backend e.g.
236239

237240
```yml
238241
data:
@@ -256,6 +259,31 @@ metadata:
256259
externalsecrets.kubernetes-client.io/permitted-key-name: "/dev/cluster1/core-namespace/.*"
257260
```
258261

262+
### Using ExternalSecret controller config
263+
264+
ExternalSecret config allows scoping the access of kubernetes-external-secrets controller.
265+
This allows to deploy multi kubernetes-external-secrets instances in the same cluster
266+
and each instance can access a set of predefined namespaces.
267+
268+
To enable this option, set the env var in the controller side with a list of namespaces:
269+
```yaml
270+
env:
271+
WATCHED_NAMESPACES: "default,qa,dev"
272+
```
273+
274+
Finally, in case more than one kubernetes-external-secrets is deployed,
275+
it's recommended to make only one deployment manage the CRDs.
276+
To disable CRD management in the other deployments,
277+
to avoid having them fighting over the CRD.
278+
279+
That can be done in the controller side by setting the env var:
280+
```yaml
281+
env:
282+
DISABLE_CUSTOM_RESOURCE_MANAGER: true
283+
```
284+
285+
Or in Helm, by setting `customResourceManagerDisabled=true`.
286+
259287
## Deprecations
260288

261289
A few properties has changed name overtime, we still maintain backwards compatbility with these but they will eventually be removed, and they are not validated using the CRD validation.

bin/daemon.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ const {
2626
rolePermittedAnnotation,
2727
namingPermittedAnnotation,
2828
enforceNamespaceAnnotation,
29-
watchTimeout
29+
watchTimeout,
30+
watchedNamespaces
3031
} = require('../config')
3132

3233
async function main () {
@@ -37,6 +38,7 @@ async function main () {
3738

3839
const externalSecretEvents = getExternalSecretEvents({
3940
kubeClient,
41+
watchedNamespaces,
4042
customResourceManifest,
4143
logger,
4244
watchTimeout

charts/kubernetes-external-secrets/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ The following table lists the configurable parameters of the `kubernetes-externa
8484
| `env.VAULT_ADDR` | Endpoint for the Vault backend, if using Vault | `http://127.0.0.1:8200` |
8585
| `env.DISABLE_POLLING` | Disables backend polling and only updates secrets when ExternalSecret is modified, setting this to any value will disable polling | `nil` |
8686
| `env.WATCH_TIMEOUT` | Restarts the external secrets resource watcher if no events have been seen in this time period (miliseconds) | `60000` |
87+
| `env.WATCHED_NAMESPACES` | Limits which namespaces the controller will watch, by default all namespaces will be watched. Comma separated list `qa,stage` | `''` |
8788
| `envVarsFromSecret.AWS_ACCESS_KEY_ID` | Set AWS_ACCESS_KEY_ID (from a secret) in Deployment Pod | |
8889
| `envVarsFromSecret.AWS_SECRET_ACCESS_KEY` | Set AWS_SECRET_ACCESS_KEY (from a secret) in Deployment Pod | |
8990
| `envVarsFromSecret.AZURE_TENANT_ID` | Set AZURE_TENANT_ID (from a secret) in Deployment Pod | |

charts/kubernetes-external-secrets/values.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ env:
1616
AWS_DEFAULT_REGION: us-west-2
1717
POLLER_INTERVAL_MILLISECONDS: 10000 # Caution, setting this frequency may incur additional charges on some platforms
1818
WATCH_TIMEOUT: 60000
19+
WATCHED_NAMESPACES: '' # Comma separated list of namespaces, empty or unset means ALL namespaces.
1920
LOG_LEVEL: info
2021
LOG_MESSAGE_KEY: 'msg'
2122
# Print logs level as string ("info") rather than integer (30)

config/environment.js

+13-1
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,17 @@ const metricsPort = process.env.METRICS_PORT || 3001
4141
const customResourceManagerDisabled = 'DISABLE_CUSTOM_RESOURCE_MANAGER' in process.env
4242
const watchTimeout = process.env.WATCH_TIMEOUT ? parseInt(process.env.WATCH_TIMEOUT) : 60000
4343

44+
// A comma-separated list of watched namespaces. If set, only ExternalSecrets in those namespaces will be handled.
45+
let watchedNamespaces = process.env.WATCHED_NAMESPACES || ''
46+
47+
// Return an array after splitting the watched namespaces string and clean up user input.
48+
watchedNamespaces = watchedNamespaces
49+
.split(',')
50+
// Remove any extra spaces.
51+
.map(namespace => { return namespace.trim() })
52+
// Remove empty values (in case there is a tailing comma).
53+
.filter(namespace => namespace)
54+
4455
module.exports = {
4556
vaultEndpoint,
4657
vaultNamespace,
@@ -58,5 +69,6 @@ module.exports = {
5869
customResourceManagerDisabled,
5970
useHumanReadableLogLevels,
6071
logMessageKey,
61-
watchTimeout
72+
watchTimeout,
73+
watchedNamespaces
6274
}

e2e/README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ kind load docker-image --name="es-dev-cluster" external-secrets:test
3333
kubectl apply -f ./localstack.deployment.yaml
3434
3535
# deploy external secrets
36-
helm template ../charts/kubernetes-external-secrets \
36+
helm template e2e ../charts/kubernetes-external-secrets \
3737
--set image.repository=external-secrets \
3838
--set image.tag=test \
3939
--set env.LOG_LEVEL=debug \

lib/external-secret.js

+26-12
Original file line numberDiff line numberDiff line change
@@ -24,20 +24,23 @@ function createEventQueue () {
2424

2525
async function startWatcher ({
2626
kubeClient,
27+
namespace,
2728
customResourceManifest,
2829
logger,
2930
eventQueue,
3031
watchTimeout
3132
}) {
3233
const deathQueue = createEventQueue()
34+
const loggedNamespaceName = namespace || '*'
3335

3436
try {
3537
while (true) {
36-
logger.debug('Starting watch stream')
38+
logger.debug('Starting watch stream for namespace %s', loggedNamespaceName)
3739

3840
const stream = kubeClient
3941
.apis[customResourceManifest.spec.group]
40-
.v1.watch[customResourceManifest.spec.names.plural]
42+
.v1.watch
43+
.namespaces(namespace)[customResourceManifest.spec.names.plural]
4144
.getStream()
4245

4346
const jsonStream = new JSONStream()
@@ -51,7 +54,7 @@ async function startWatcher ({
5154

5255
const timeMs = watchTimeout
5356
timeout = setTimeout(() => {
54-
logger.info(`No watch event for ${timeMs} ms, restarting watcher`)
57+
logger.info(`No watch event for ${timeMs} ms, restarting watcher for ${loggedNamespaceName}`)
5558
stream.abort()
5659
}, timeMs)
5760
timeout.unref()
@@ -63,7 +66,7 @@ async function startWatcher ({
6366
})
6467

6568
jsonStream.on('error', (err) => {
66-
logger.warn(err, 'Got error on stream')
69+
logger.warn(err, 'Got error on stream for namespace %s', loggedNamespaceName)
6770
deathQueue.put('ERROR')
6871
clearTimeout(timeout)
6972
})
@@ -75,38 +78,49 @@ async function startWatcher ({
7578

7679
const deathEvent = await deathQueue.take()
7780

78-
logger.info('Stopping watch stream due to event: %s', deathEvent)
81+
logger.info('Stopping watch stream for namespace %s due to event: %s', loggedNamespaceName, deathEvent)
7982
eventQueue.put({ type: 'DELETED_ALL' })
8083

8184
stream.abort()
8285
}
8386
} catch (err) {
84-
logger.error(err, 'Watcher crashed')
87+
logger.error(err, 'Watcher for namespace %s crashed', loggedNamespaceName)
88+
throw err
8589
}
8690
}
8791

8892
/**
8993
* Get a stream of external secret events. This implementation uses
9094
* watch and yields as a stream of events.
9195
* @param {Object} kubeClient - Client for interacting with kubernetes cluster.
96+
* @param {Array} watchedNamespaces - List of scoped namespaces.
9297
* @param {Object} customResourceManifest - Custom resource manifest.
9398
* @returns {Object} An async generator that yields externalsecret events.
9499
*/
95100
function getExternalSecretEvents ({
96101
kubeClient,
102+
watchedNamespaces,
97103
customResourceManifest,
98104
logger,
99105
watchTimeout
100106
}) {
101107
return (async function * () {
102108
const eventQueue = createEventQueue()
103109

104-
startWatcher({
105-
kubeClient,
106-
customResourceManifest,
107-
logger,
108-
eventQueue,
109-
watchTimeout
110+
// If the watchedNamespaces is an empty array (i.e. no scoped access),
111+
// add an empty element so all ExternalSecret resources in all namespaces will be watched.
112+
const namespaceToWatch = watchedNamespaces.length ? watchedNamespaces : ['']
113+
114+
// Create watcher for each namespace
115+
namespaceToWatch.forEach((namespace) => {
116+
startWatcher({
117+
namespace,
118+
kubeClient,
119+
customResourceManifest,
120+
logger,
121+
eventQueue,
122+
watchTimeout
123+
})
110124
})
111125

112126
while (true) {

lib/external-secret.test.js

+23-11
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const { getExternalSecretEvents } = require('./external-secret')
99

1010
describe('getExternalSecretEvents', () => {
1111
let kubeClientMock
12+
let watchedNamespaces
1213
let externalSecretsApiMock
1314
let fakeCustomResourceManifest
1415
let loggerMock
@@ -26,19 +27,29 @@ describe('getExternalSecretEvents', () => {
2627
externalSecretsApiMock = sinon.mock()
2728

2829
mockedStream = new Readable()
29-
mockedStream._read = () => {}
30-
mockedStream.abort = () => {}
30+
mockedStream._read = () => { }
3131

3232
externalSecretsApiMock.get = sinon.stub()
33-
kubeClientMock = sinon.mock()
34-
kubeClientMock.apis = sinon.mock()
35-
kubeClientMock.apis['kubernetes-client.io'] = sinon.mock()
36-
kubeClientMock.apis['kubernetes-client.io'].v1 = sinon.mock()
37-
kubeClientMock.apis['kubernetes-client.io'].v1.watch = sinon.mock()
38-
kubeClientMock.apis['kubernetes-client.io']
39-
.v1.watch.externalsecrets = sinon.mock()
40-
kubeClientMock.apis['kubernetes-client.io']
41-
.v1.watch.externalsecrets.getStream = () => mockedStream
33+
34+
kubeClientMock = {
35+
apis: {
36+
'kubernetes-client.io': {
37+
v1: {
38+
watch: {
39+
namespaces: () => {
40+
return {
41+
externalsecrets: {
42+
getStream: () => mockedStream
43+
}
44+
}
45+
}
46+
}
47+
}
48+
}
49+
}
50+
}
51+
52+
watchedNamespaces = []
4253

4354
loggerMock = sinon.mock()
4455
loggerMock.info = sinon.stub()
@@ -60,6 +71,7 @@ describe('getExternalSecretEvents', () => {
6071

6172
const events = getExternalSecretEvents({
6273
kubeClient: kubeClientMock,
74+
watchedNamespaces: watchedNamespaces,
6375
customResourceManifest: fakeCustomResourceManifest,
6476
logger: loggerMock,
6577
watchTimeout: 5000

0 commit comments

Comments
 (0)