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

Commit

Permalink
Implement refresh secret on delete when polling is disabled
Browse files Browse the repository at this point in the history
  • Loading branch information
Maksym Kulish committed Jun 21, 2020
1 parent 7190120 commit c833f0f
Show file tree
Hide file tree
Showing 10 changed files with 346 additions and 7 deletions.
4 changes: 3 additions & 1 deletion bin/daemon.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ const {
pollerIntervalMilliseconds,
pollingDisabled,
rolePermittedAnnotation,
namingPermittedAnnotation
namingPermittedAnnotation,
pollInternalSecrets
} = require('../config')

async function main () {
Expand All @@ -51,6 +52,7 @@ async function main () {
namingPermittedAnnotation,
customResourceManifest,
pollingDisabled,
pollInternalSecrets,
logger
})

Expand Down
2 changes: 1 addition & 1 deletion charts/kubernetes-external-secrets/templates/rbac.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ metadata:
rules:
- apiGroups: [""]
resources: ["secrets"]
verbs: ["create", "update"]
verbs: ["create", "update"{{ with (index .Values.env "POLL_INTERNAL_SECRETS") }}, "list"{{ end}}]
- apiGroups: [""]
resources: ["namespaces"]
verbs: ["get", "watch", "list"]
Expand Down
1 change: 1 addition & 0 deletions charts/kubernetes-external-secrets/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ env:
LOG_LEVEL: info
METRICS_PORT: 3001
VAULT_ADDR: http:https://127.0.0.1:8200
# POLL_INTERNAL_SECRETS: "true"
# GOOGLE_APPLICATION_CREDENTIALS: /app/gcp-creds/gcp-creds.json

# Create environment variables from existing k8s secrets
Expand Down
5 changes: 4 additions & 1 deletion config/environment.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ const metricsPort = process.env.METRICS_PORT || 3001

const customResourceManagerDisabled = 'DISABLE_CUSTOM_RESOURCE_MANAGER' in process.env

const pollInternalSecrets = 'POLL_INTERNAL_SECRETS' in process.env

module.exports = {
vaultEndpoint,
environment,
Expand All @@ -39,5 +41,6 @@ module.exports = {
namingPermittedAnnotation,
pollingDisabled,
logLevel,
customResourceManagerDisabled
customResourceManagerDisabled,
pollInternalSecrets
}
3 changes: 1 addition & 2 deletions lib/daemon.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@ class Daemon {
/**
* Create daemon.
* @param {Object} backends - Backends for fetching secret properties.
* @param {Object} kubeClient - Client for interacting with kubernetes cluster.
* @param {Object} externalSecretEvents - Stream of external secret events.
* @param {Object} logger - Logger for logging stuff.
* @param {number} pollerIntervalMilliseconds - Interval time in milliseconds for polling secret properties.
* @param {PollerFactory} pollerFactory - A poller factory instance
*/
constructor ({
externalSecretEvents,
Expand Down
117 changes: 117 additions & 0 deletions lib/kubernetes-secrets-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/* eslint-env mocha */
'use strict'

const { expect } = require('chai')
const sinon = require('sinon')

const KubernetesSecrets = require('./kubernetes-secrets')

describe('KubernetesSecrets', () => {
let loggerMock
let metricsMock
const kubeNamespaceMock = sinon.mock()
const kubeClientMock = sinon.mock()

beforeEach(async () => {
loggerMock = sinon.mock()
loggerMock.info = sinon.stub()

metricsMock = sinon.mock()
metricsMock.observeSync = sinon.stub()

const fakeSecret1 = {
apiVersion: 'v1',
kind: 'Secret',
metadata: {
name: 'stub1'
}
}

const fakeSecret2 = {
apiVersion: 'v1',
kind: 'Secret',
metadata: {
name: 'stub2'
}
}

kubeClientMock.api = sinon.mock()
kubeClientMock.api.v1 = sinon.mock()
kubeClientMock.api.v1.namespaces = sinon.stub().returns(kubeNamespaceMock)

kubeNamespaceMock.get = sinon.stub().resolves(kubeNamespaceMock)
kubeNamespaceMock.secrets = sinon.mock()
kubeNamespaceMock.secrets.get = sinon.stub().resolves({ body: { items: [fakeSecret1, fakeSecret2] } })
})

afterEach(async () => {
sinon.restore()
})

it('caches secret after creating', async () => {
const os1 = KubernetesSecrets.getOrCreateSecretObserver(
{
namespace: 'ns1',
logger: loggerMock,
metrics: metricsMock,
intervalMilliseconds: 1000,
kubeClient: kubeClientMock
}
)

const os2 = KubernetesSecrets.getOrCreateSecretObserver(
{
namespace: 'ns1',
logger: loggerMock,
metrics: metricsMock,
intervalMilliseconds: 1000,
kubeClient: kubeClientMock
}
)

const os3 = KubernetesSecrets.getOrCreateSecretObserver(
{
namespace: 'ns2',
logger: loggerMock,
metrics: metricsMock,
intervalMilliseconds: 1000,
kubeClient: kubeClientMock
}
)

expect(os1).is.deep.equal(os2)
expect(os3).is.not.equal(os2)
expect(Object.keys(KubernetesSecrets.secretObservers)).is.deep.equal(['ns1', 'ns2'])
})

it('periodically refreshes internal secret state', async () => {
const os = KubernetesSecrets.getOrCreateSecretObserver(
{
namespace: kubeNamespaceMock,
logger: loggerMock,
metrics: metricsMock,
intervalMilliseconds: 2000,
kubeClient: kubeClientMock
}
)
await os._listAndRefreshSecrets()
clearTimeout(os._timeoutId)
expect(os.secretNames).is.deep.equal(['stub1', 'stub2'])
})

it('allows to testing of secret presence within state', async () => {
const os = KubernetesSecrets.getOrCreateSecretObserver(
{
namespace: kubeNamespaceMock,
logger: loggerMock,
metrics: metricsMock,
intervalMilliseconds: 2000,
kubeClient: kubeClientMock
}
)
await os._listAndRefreshSecrets()
clearTimeout(os._timeoutId)
expect(os.secretPresent('stub1')).equal(true)
expect(os.secretPresent('stub3')).equal(false)
})
})
107 changes: 107 additions & 0 deletions lib/kubernetes-secrets.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
'use strict'

/**
* Kubernetes secret observer.
* @param {string} timeoutId - An ID of setTimeout which schedules next poll of internal secrets.
* @param {Object} secrets - Object of secret names present in given namespace.
* Watch for Kubernetes secrets, and provide status promises.
*/

/** Kubernetes secret observer class. */
class KubernetesSecrets {
/**
* Create secrets observer.
* @param {string} namespace - A namespace to poll for internal secrets.
* @param {number} intervalMilliseconds - Interval time in milliseconds for polling secret properties.
* @param {Object} logger - Logger for logging stuff.
* @param {Object} metrics - Metrics client.
*/
constructor ({ namespace, kubeClient, intervalMilliseconds, logger, metrics }) {
this._intervalMilliseconds = intervalMilliseconds
this._logger = logger
this._metrics = metrics
this._kubeClient = kubeClient
this._timeoutId = null
this._secrets = {}
this._namespace = namespace
}

/**
* Return current set of present secret names
* @returns {Array} - secret names listing
*/
get secretNames () {
return Object.keys(this._secrets)
}

/**
* Refresh Kubernetes secrets.
* Set timeout for next refresh.
*/
async _listAndRefreshSecrets () {
const kubeNamespace = this._kubeClient.api.v1.namespaces(this._namespace)

const newSecrets = {}
const kubeSecrets = await kubeNamespace.secrets.get()

for (const kubeSecret of kubeSecrets.body.items) {
newSecrets[kubeSecret.metadata.name] = kubeSecret
}

this._secrets = newSecrets
this._metrics.observeSync({
name: 'all-internal-secrets-list',
namespace: this._namespace,
status: 'success'
})
this._timeoutId = setTimeout(this._listAndRefreshSecrets.bind(this), this._intervalMilliseconds)
}

/**
* Find out if given secret exists in a namespace.
* @param {string} secretName Name of secret
*
* @returns {boolean} A boolean status of k8s secret presence.
*/
secretPresent (secretName) {
return (secretName in this._secrets)
}

/**
* Start this secrets observer.
*/
start () {
this._logger.info(`starting kubernetes secrets observer for namespace ${this._namespace}.`)
this._timeoutId = setTimeout(this._listAndRefreshSecrets.bind(this), this._intervalMilliseconds)
return this
}

/**
* Stop this secrets observer.
*/
stop () {
if (this._timeoutId != null) {
clearTimeout(this._timeoutId)
}
}

/**
* Get or create secret observer in given namespace.
*/
static getOrCreateSecretObserver (props) {
const nsName = props.namespace

if (!(nsName in KubernetesSecrets.secretObservers)) {
KubernetesSecrets.secretObservers[nsName] =
new KubernetesSecrets(props)
}

return KubernetesSecrets.secretObservers[nsName]
}
}

// A static object containing namespace name as a key, and secret
// observer instance as a value.
KubernetesSecrets.secretObservers = {}

module.exports = KubernetesSecrets
5 changes: 5 additions & 0 deletions lib/poller-factory.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ class PollerFactory {
* @param {Object} customResourceManifest - CRD manifest
* @param {Object} logger - Logger for logging stuff.
* @param {number} pollerIntervalMilliseconds - Interval time in milliseconds for polling secret properties.
* @param {boolean} pollingDisabled - If set up, disables polling of all secrets.
* @param {boolean} pollInternalSecrets - If set up, starts polling of internal secrets.
* @param {String} rolePermittedAnnotation - namespace annotation that defines which roles can be assumed within this namespace
*/
constructor ({
Expand All @@ -22,6 +24,7 @@ class PollerFactory {
namingPermittedAnnotation,
customResourceManifest,
pollingDisabled,
pollInternalSecrets,
logger
}) {
this._logger = logger
Expand All @@ -33,6 +36,7 @@ class PollerFactory {
this._rolePermittedAnnotation = rolePermittedAnnotation
this._namingPermittedAnnotation = namingPermittedAnnotation
this._pollingDisabled = pollingDisabled
this._pollInternalSecrets = pollInternalSecrets
}

/**
Expand All @@ -50,6 +54,7 @@ class PollerFactory {
rolePermittedAnnotation: this._rolePermittedAnnotation,
namingPermittedAnnotation: this._namingPermittedAnnotation,
pollingDisabled: this._pollingDisabled,
pollInternalSecrets: this._pollInternalSecrets,
externalSecret
})

Expand Down
22 changes: 20 additions & 2 deletions lib/poller.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

const clonedeep = require('lodash.clonedeep')
const merge = require('lodash.merge')
const KubernetesSecrets = require('./kubernetes-secrets')

/**
* Kubernetes secret descriptor.
Expand All @@ -28,6 +29,7 @@ class Poller {
* @param {Object} customResourceManifest - CRD manifest
* @param {Object} externalSecret - ExternalSecret manifest.
* @param {string} rolePermittedAnnotation - namespace annotation that defines which roles can be assumed within this namespace
* @param {Boolean} pollInternalSecrets - Whether supports internal secrets listing
* @param {Object} metrics - Metrics client.
*/
constructor ({
Expand All @@ -40,6 +42,7 @@ class Poller {
rolePermittedAnnotation,
namingPermittedAnnotation,
pollingDisabled,
pollInternalSecrets,
externalSecret
}) {
this._backends = backends
Expand Down Expand Up @@ -72,6 +75,15 @@ class Poller {
this._status = this._kubeClient
.apis[this._customResourceManifest.spec.group]
.v1.namespaces(this._namespace)[this._customResourceManifest.spec.names.plural](this._name).status

this._internalSecrets = pollInternalSecrets
? KubernetesSecrets.getOrCreateSecretObserver({
namespace: this._namespace,
intervalMilliseconds: this._intervalMilliseconds,
kubeClient: this._kubeClient,
logger: this._logger,
metrics: this._metrics
}) : null
}

/**
Expand Down Expand Up @@ -283,7 +295,12 @@ class Poller {

// If polling is disabled we only react to changes in the ExternalSecret
if (this._pollingDisabled) {
return
// Check for internal secret being missing
if ((this._internalSecrets === null) ||
(this._internalSecrets.secretNames.length === 0) ||
(this._internalSecrets.secretPresent(this._name))) {
return
}
}

const now = Date.now()
Expand Down Expand Up @@ -325,7 +342,7 @@ class Poller {
*/
start () {
if (this._timeoutId) return this

if (this._internalSecrets) this._internalSecrets.start()
this._logger.info(`starting poller for ${this._namespace}/${this._name}`)
this._scheduleNextPoll()

Expand All @@ -338,6 +355,7 @@ class Poller {
*/
stop () {
if (!this._timeoutId) return this
if (this._internalSecrets) this._internalSecrets.stop()

this._logger.info(`stopping poller for ${this._namespace}/${this._name}`)

Expand Down
Loading

0 comments on commit c833f0f

Please sign in to comment.