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

Commit

Permalink
feat: allow to watch externalsecrets in specified namespaces
Browse files Browse the repository at this point in the history
  • Loading branch information
aabouzaid committed Dec 22, 2020
1 parent 972f8a9 commit 5ad8e6a
Show file tree
Hide file tree
Showing 5 changed files with 93 additions and 23 deletions.
32 changes: 30 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -230,9 +230,12 @@ spec:
name: .dockerconfigjson
```

## Enforcing naming conventions for backend keys
## Scoping access

by default an `ExternalSecret` may access arbitrary keys from the backend e.g.
### Using Namespace annotation

Enforcing naming conventions for backend keys could be done by using namespace annotations.
By default an `ExternalSecret` may access arbitrary keys from the backend e.g.

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

### Using ExternalSecret controller config

ExternalSecret config allows scoping the access of kubernetes-external-secrets controller.
This allows to deploy multi kubernetes-external-secrets instances at the same cluster
and each instance can access a set of predefined namespaces.

To enable this option, set the env var in the controller side with a list of namespaces:
```yaml
env:
WATCHED_NAMESPACES: "default,qa,dev"
```

Finally, in case more than a kubernetes-external-secrets is deployment,
it's recommended to make only one deployment is the CRD manager,
and disable CRD management in the rest of the deployment
to avoid having multiple deployments fighting over the CRD.

That's could be done in the controller side by setting the env var:
```yaml
env:
DISABLE_CUSTOM_RESOURCE_MANAGER: true
```

Or in Helm, by setting `customResourceManagerDisabled=true`.

## Deprecations

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.
Expand Down
4 changes: 3 additions & 1 deletion bin/daemon.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ const {
rolePermittedAnnotation,
namingPermittedAnnotation,
enforceNamespaceAnnotation,
watchTimeout
watchTimeout,
watchedNamespaces
} = require('../config')

async function main () {
Expand All @@ -37,6 +38,7 @@ async function main () {

const externalSecretEvents = getExternalSecretEvents({
kubeClient,
watchedNamespaces,
customResourceManifest,
logger,
watchTimeout
Expand Down
14 changes: 13 additions & 1 deletion config/environment.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,17 @@ const metricsPort = process.env.METRICS_PORT || 3001
const customResourceManagerDisabled = 'DISABLE_CUSTOM_RESOURCE_MANAGER' in process.env
const watchTimeout = process.env.WATCH_TIMEOUT ? parseInt(process.env.WATCH_TIMEOUT) : 60000

// A comma-separated list of watched namespaces. If set, only ExternalSecrets in those namespaces will be handled.
let watchedNamespaces = process.env.WATCHED_NAMESPACES || ''

// Return an array after splitting the watched namespaces string and clean up user input.
watchedNamespaces = watchedNamespaces
.split(',')
// Remove any extra spaces.
.map(namespace => { return namespace.trim() })
// Remove empty values (in case there is a tailing comma).
.filter(namespace => namespace)

module.exports = {
vaultEndpoint,
vaultNamespace,
Expand All @@ -58,5 +69,6 @@ module.exports = {
customResourceManagerDisabled,
useHumanReadableLogLevels,
logMessageKey,
watchTimeout
watchTimeout,
watchedNamespaces
}
32 changes: 24 additions & 8 deletions lib/external-secret.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ function createEventQueue () {

async function startWatcher ({
kubeClient,
watchedNamespaces,
customResourceManifest,
logger,
eventQueue,
Expand All @@ -35,13 +36,21 @@ async function startWatcher ({
while (true) {
logger.debug('Starting watch stream')

const stream = kubeClient
.apis[customResourceManifest.spec.group]
.v1.watch[customResourceManifest.spec.names.plural]
.getStream()

const jsonStream = new JSONStream()
stream.pipe(jsonStream)

// If the watchedNamespaces is an empty array (i.e. no scoped access),
// add an empty element so all ExternalSecret resources in all namespaces will be watched.
const namespacesStreams = watchedNamespaces.length ? watchedNamespaces : ['']

// Create a namespace stream per namespace and add it to the JSON stream.
namespacesStreams.map(namespace => {
return kubeClient
.apis[customResourceManifest.spec.group]
.v1.watch
.namespaces(namespace)[customResourceManifest.spec.names.plural]
.getStream()
.pipe(jsonStream)
})

let timeout
const restartTimeout = () => {
Expand All @@ -52,7 +61,9 @@ async function startWatcher ({
const timeMs = watchTimeout
timeout = setTimeout(() => {
logger.info(`No watch event for ${timeMs} ms, restarting watcher`)
stream.abort()
namespacesStreams.forEach(namespaceStream => {
namespaceStream.abort();
})
}, timeMs)
timeout.unref()
}
Expand All @@ -78,7 +89,9 @@ async function startWatcher ({
logger.info('Stopping watch stream due to event: %s', deathEvent)
eventQueue.put({ type: 'DELETED_ALL' })

stream.abort()
namespacesStreams.forEach(namespaceStream => {
namespaceStream.abort()
})
}
} catch (err) {
logger.error(err, 'Watcher crashed')
Expand All @@ -89,11 +102,13 @@ async function startWatcher ({
* Get a stream of external secret events. This implementation uses
* watch and yields as a stream of events.
* @param {Object} kubeClient - Client for interacting with kubernetes cluster.
* @param {Array} watchedNamespaces - List of scoped namespaces.
* @param {Object} customResourceManifest - Custom resource manifest.
* @returns {Object} An async generator that yields externalsecret events.
*/
function getExternalSecretEvents ({
kubeClient,
watchedNamespaces,
customResourceManifest,
logger,
watchTimeout
Expand All @@ -103,6 +118,7 @@ function getExternalSecretEvents ({

startWatcher({
kubeClient,
watchedNamespaces,
customResourceManifest,
logger,
eventQueue,
Expand Down
34 changes: 23 additions & 11 deletions lib/external-secret.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const { getExternalSecretEvents } = require('./external-secret')

describe('getExternalSecretEvents', () => {
let kubeClientMock
let watchedNamespaces
let externalSecretsApiMock
let fakeCustomResourceManifest
let loggerMock
Expand All @@ -26,19 +27,29 @@ describe('getExternalSecretEvents', () => {
externalSecretsApiMock = sinon.mock()

mockedStream = new Readable()
mockedStream._read = () => {}
mockedStream.abort = () => {}
mockedStream._read = () => { }

externalSecretsApiMock.get = sinon.stub()
kubeClientMock = sinon.mock()
kubeClientMock.apis = sinon.mock()
kubeClientMock.apis['kubernetes-client.io'] = sinon.mock()
kubeClientMock.apis['kubernetes-client.io'].v1 = sinon.mock()
kubeClientMock.apis['kubernetes-client.io'].v1.watch = sinon.mock()
kubeClientMock.apis['kubernetes-client.io']
.v1.watch.externalsecrets = sinon.mock()
kubeClientMock.apis['kubernetes-client.io']
.v1.watch.externalsecrets.getStream = () => mockedStream

kubeClientMock = {
apis: {
'kubernetes-client.io': {
v1: {
watch: {
namespaces: () => {
return {
externalsecrets: {
getStream: () => mockedStream
}
}
}
}
}
}
}
}

watchedNamespaces = []

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

const events = getExternalSecretEvents({
kubeClient: kubeClientMock,
watchedNamespaces: watchedNamespaces,
customResourceManifest: fakeCustomResourceManifest,
logger: loggerMock,
watchTimeout: 5000
Expand Down

0 comments on commit 5ad8e6a

Please sign in to comment.