This repository has been archived by the owner on Jul 26, 2022. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 402
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement refresh secret on delete when polling is disabled
- Loading branch information
Maksym Kulish
committed
Aug 3, 2020
1 parent
a52987b
commit 983f1a3
Showing
10 changed files
with
346 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.