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

Commit

Permalink
feat: add status subresource with last sync and generation tracking (#…
Browse files Browse the repository at this point in the history
…133)

* feat: add status subresource with last sync and generation tracking

- Remove excessive polls by tracking last sync on external secrets resource
- Track generation to poll when resource is modified
- Show status when using `kubectl get externalsecrets`

NOTE: This requires additional RBAC privileges!

Squashed:
- fix: move metrics success call so failures doesn't emit double metrics
- refactor: use factory for poller in daemon to reduce dependency passthru
  • Loading branch information
Flydiverny authored Nov 4, 2019
1 parent 238ebd6 commit 8db1749
Show file tree
Hide file tree
Showing 9 changed files with 488 additions and 149 deletions.
15 changes: 11 additions & 4 deletions bin/daemon.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const Daemon = require('../lib/daemon')
const MetricsServer = require('../lib/metrics-server')
const Metrics = require('../lib/metrics')
const { getExternalSecretEvents } = require('../lib/external-secret')
const PollerFactory = require('../lib/poller-factory')

const {
backends,
Expand Down Expand Up @@ -40,13 +41,19 @@ async function main () {
const registry = Prometheus.register
const metrics = new Metrics({ registry })

const daemon = new Daemon({
const pollerFactory = new PollerFactory({
backends,
externalSecretEvents,
kubeClient,
logger,
metrics,
pollerIntervalMilliseconds
pollerIntervalMilliseconds,
customResourceManifest,
logger
})

const daemon = new Daemon({
externalSecretEvents,
logger,
pollerFactory
})

const metricsServer = new MetricsServer({
Expand Down
3 changes: 3 additions & 0 deletions charts/kubernetes-external-secrets/templates/rbac.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ rules:
- apiGroups: ["kubernetes-client.io"]
resources: ["externalsecrets"]
verbs: ["get", "watch", "list"]
- apiGroups: ["kubernetes-client.io"]
resources: ["externalsecrets/status"]
verbs: ["get", "update"]
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
Expand Down
20 changes: 20 additions & 0 deletions custom-resource-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,23 @@
"kind": "CustomResourceDefinition",
"spec": {
"scope": "Namespaced",
"additionalPrinterColumns": [
{
"JSONPath": ".status.lastSync",
"name": "Last Sync",
"type": "date"
},
{
"JSONPath": ".status.status",
"name": "status",
"type": "string"
},
{
"JSONPath": ".metadata.creationTimestamp",
"name": "Age",
"type": "date"
}
],
"version": "v1",
"group": "kubernetes-client.io",
"names": {
Expand All @@ -11,6 +28,9 @@
"kind": "ExternalSecret",
"plural": "externalsecrets",
"singular": "externalsecret"
},
"subresources": {
"status": {}
}
},
"apiVersion": "apiextensions.k8s.io/v1beta1",
Expand Down
3 changes: 3 additions & 0 deletions external-secrets.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ rules:
- apiGroups: ["kubernetes-client.io"]
resources: ["externalsecrets"]
verbs: ["get", "watch", "list"]
- apiGroups: ["kubernetes-client.io"]
resources: ["externalsecrets/status"]
verbs: ["get", "update"]
---
apiVersion: v1
kind: Namespace
Expand Down
54 changes: 11 additions & 43 deletions lib/daemon.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
'use strict'

/* eslint-disable no-console */

const Poller = require('./poller')

/** Daemon class. */
class Daemon {
/**
Expand All @@ -15,19 +11,13 @@ class Daemon {
* @param {number} pollerIntervalMilliseconds - Interval time in milliseconds for polling secret properties.
*/
constructor ({
backends,
externalSecretEvents,
kubeClient,
logger,
metrics,
pollerIntervalMilliseconds
pollerFactory
}) {
this._backends = backends
this._kubeClient = kubeClient
this._externalSecretEvents = externalSecretEvents
this._logger = logger
this._metrics = metrics
this._pollerIntervalMilliseconds = pollerIntervalMilliseconds
this._pollerFactory = pollerFactory

this._pollers = {}
}
Expand All @@ -39,17 +29,8 @@ class Daemon {
*/
_createPollerDescriptor (externalSecret) {
const { uid, name, namespace } = externalSecret.metadata
// NOTE(jdaeli): hash this in case resource version becomes too long?
const secretDescriptor = { ...externalSecret.secretDescriptor, name }
const ownerReference = {
apiVersion: externalSecret.apiVersion,
controller: true,
kind: externalSecret.kind,
name,
uid
}

return { id: uid, namespace, secretDescriptor, ownerReference }
return { id: uid, name, namespace, externalSecret }
}

/**
Expand All @@ -68,21 +49,12 @@ class Daemon {
Object.keys(this._pollers).forEach(pollerId => this._removePoller(pollerId))
}

_addPoller (descriptor, forcePoll = false) {
this._logger.info('spinning up poller', descriptor)

const poller = new Poller({
backends: this._backends,
intervalMilliseconds: this._pollerIntervalMilliseconds,
kubeClient: this._kubeClient,
logger: this._logger,
metrics: this._metrics,
namespace: descriptor.namespace,
secretDescriptor: descriptor.secretDescriptor,
ownerReference: descriptor.ownerReference
})

this._pollers[descriptor.id] = poller.start({ forcePoll })
_addPoller (descriptor) {
this._logger.info('spinning up poller for', descriptor.name, 'in', descriptor.namespace)

const poller = this._pollerFactory.createPoller(descriptor)

this._pollers[descriptor.id] = poller.start()
}

/**
Expand All @@ -98,14 +70,10 @@ class Daemon {
break
}

case 'ADDED': {
this._addPoller(descriptor, true)
break
}

case 'ADDED':
case 'MODIFIED': {
this._removePoller(descriptor.id)
this._addPoller(descriptor, true)
this._addPoller(descriptor)
break
}

Expand Down
71 changes: 62 additions & 9 deletions lib/daemon.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,27 @@ const { expect } = require('chai')
const sinon = require('sinon')

const Daemon = require('./daemon')
const Poller = require('./poller')

describe('Daemon', () => {
let daemon
let loggerMock
let pollerMock
let pollerFactory

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

pollerMock = sinon.mock()
pollerMock.start = sinon.stub().returns(pollerMock)
pollerMock.stop = sinon.stub().returns(pollerMock)

pollerFactory = sinon.mock()
pollerFactory.createPoller = sinon.stub().returns(pollerMock)

daemon = new Daemon({
logger: loggerMock,
pollerIntervalMilliseconds: 0
pollerFactory
})
})

Expand All @@ -26,11 +34,6 @@ describe('Daemon', () => {
})

it('starts new pollers for external secrets', async () => {
sinon.stub(Poller.prototype, 'start')
.callsFake(function () { return this })
sinon.stub(Poller.prototype, 'stop')
.callsFake(function () { return this })

const fakeExternalSecretEvents = (async function * () {
yield {
type: 'ADDED',
Expand All @@ -49,7 +52,57 @@ describe('Daemon', () => {
await daemon.start()
daemon.stop()

expect(Poller.prototype.start.called).to.equal(true)
expect(Poller.prototype.stop.called).to.equal(true)
expect(pollerMock.start.called).to.equal(true)
expect(pollerMock.stop.called).to.equal(true)
})

it('tries to remove existing poller on ADDED events', async () => {
const fakeExternalSecretEvents = (async function * () {
yield {
type: 'ADDED',
object: {
metadata: {
name: 'foo',
namespace: 'foo',
uid: 'test-id'
}
}
}
}())

daemon._externalSecretEvents = fakeExternalSecretEvents
daemon._addPoller = sinon.mock()
daemon._removePoller = sinon.mock()

await daemon.start()
daemon.stop()

expect(daemon._addPoller.called).to.equal(true)
expect(daemon._removePoller.calledWith('test-id')).to.equal(true)
})

it('tries to remove existing poller on MODIFIED event', async () => {
const fakeExternalSecretEvents = (async function * () {
yield {
type: 'MODIFIED',
object: {
metadata: {
name: 'foo',
namespace: 'foo',
uid: 'test-id'
}
}
}
}())

daemon._externalSecretEvents = fakeExternalSecretEvents
daemon._addPoller = sinon.mock()
daemon._removePoller = sinon.mock()

await daemon.start()
daemon.stop()

expect(daemon._addPoller.called).to.equal(true)
expect(daemon._removePoller.calledWith('test-id')).to.equal(true)
})
})
50 changes: 50 additions & 0 deletions lib/poller-factory.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
'use strict'

const Poller = require('./poller')

class PollerFactory {
/**
* Create PollerFactory.
* @param {Object} backends - Backends for fetching secret properties.
* @param {Object} kubeClient - Client for interacting with kubernetes cluster.
* @param {Object} metrics - Metrics client
* @param {Object} customResourceManifest - CRD manifest
* @param {Object} logger - Logger for logging stuff.
* @param {number} pollerIntervalMilliseconds - Interval time in milliseconds for polling secret properties.
*/
constructor ({
backends,
kubeClient,
metrics,
pollerIntervalMilliseconds,
customResourceManifest,
logger
}) {
this._logger = logger
this._metrics = metrics
this._backends = backends
this._kubeClient = kubeClient
this._pollerIntervalMilliseconds = pollerIntervalMilliseconds
this._customResourceManifest = customResourceManifest
}

/**
* Create poller
* @param {Object} externalSecret - External Secret custom resource oject
*/
createPoller ({ externalSecret }) {
const poller = new Poller({
backends: this._backends,
intervalMilliseconds: this._pollerIntervalMilliseconds,
kubeClient: this._kubeClient,
logger: this._logger,
metrics: this._metrics,
customResourceManifest: this._customResourceManifest,
externalSecret
})

return poller
}
}

module.exports = PollerFactory
Loading

0 comments on commit 8db1749

Please sign in to comment.