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

Commit 8db1749

Browse files
authored
feat: add status subresource with last sync and generation tracking (#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
1 parent 238ebd6 commit 8db1749

File tree

9 files changed

+488
-149
lines changed

9 files changed

+488
-149
lines changed

bin/daemon.js

+11-4
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const Daemon = require('../lib/daemon')
1212
const MetricsServer = require('../lib/metrics-server')
1313
const Metrics = require('../lib/metrics')
1414
const { getExternalSecretEvents } = require('../lib/external-secret')
15+
const PollerFactory = require('../lib/poller-factory')
1516

1617
const {
1718
backends,
@@ -40,13 +41,19 @@ async function main () {
4041
const registry = Prometheus.register
4142
const metrics = new Metrics({ registry })
4243

43-
const daemon = new Daemon({
44+
const pollerFactory = new PollerFactory({
4445
backends,
45-
externalSecretEvents,
4646
kubeClient,
47-
logger,
4847
metrics,
49-
pollerIntervalMilliseconds
48+
pollerIntervalMilliseconds,
49+
customResourceManifest,
50+
logger
51+
})
52+
53+
const daemon = new Daemon({
54+
externalSecretEvents,
55+
logger,
56+
pollerFactory
5057
})
5158

5259
const metricsServer = new MetricsServer({

charts/kubernetes-external-secrets/templates/rbac.yaml

+3
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ rules:
2525
- apiGroups: ["kubernetes-client.io"]
2626
resources: ["externalsecrets"]
2727
verbs: ["get", "watch", "list"]
28+
- apiGroups: ["kubernetes-client.io"]
29+
resources: ["externalsecrets/status"]
30+
verbs: ["get", "update"]
2831
---
2932
apiVersion: rbac.authorization.k8s.io/v1beta1
3033
kind: ClusterRoleBinding

custom-resource-manifest.json

+20
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,23 @@
22
"kind": "CustomResourceDefinition",
33
"spec": {
44
"scope": "Namespaced",
5+
"additionalPrinterColumns": [
6+
{
7+
"JSONPath": ".status.lastSync",
8+
"name": "Last Sync",
9+
"type": "date"
10+
},
11+
{
12+
"JSONPath": ".status.status",
13+
"name": "status",
14+
"type": "string"
15+
},
16+
{
17+
"JSONPath": ".metadata.creationTimestamp",
18+
"name": "Age",
19+
"type": "date"
20+
}
21+
],
522
"version": "v1",
623
"group": "kubernetes-client.io",
724
"names": {
@@ -11,6 +28,9 @@
1128
"kind": "ExternalSecret",
1229
"plural": "externalsecrets",
1330
"singular": "externalsecret"
31+
},
32+
"subresources": {
33+
"status": {}
1434
}
1535
},
1636
"apiVersion": "apiextensions.k8s.io/v1beta1",

external-secrets.yml

+3
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ rules:
2929
- apiGroups: ["kubernetes-client.io"]
3030
resources: ["externalsecrets"]
3131
verbs: ["get", "watch", "list"]
32+
- apiGroups: ["kubernetes-client.io"]
33+
resources: ["externalsecrets/status"]
34+
verbs: ["get", "update"]
3235
---
3336
apiVersion: v1
3437
kind: Namespace

lib/daemon.js

+11-43
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
11
'use strict'
22

3-
/* eslint-disable no-console */
4-
5-
const Poller = require('./poller')
6-
73
/** Daemon class. */
84
class Daemon {
95
/**
@@ -15,19 +11,13 @@ class Daemon {
1511
* @param {number} pollerIntervalMilliseconds - Interval time in milliseconds for polling secret properties.
1612
*/
1713
constructor ({
18-
backends,
1914
externalSecretEvents,
20-
kubeClient,
2115
logger,
22-
metrics,
23-
pollerIntervalMilliseconds
16+
pollerFactory
2417
}) {
25-
this._backends = backends
26-
this._kubeClient = kubeClient
2718
this._externalSecretEvents = externalSecretEvents
2819
this._logger = logger
29-
this._metrics = metrics
30-
this._pollerIntervalMilliseconds = pollerIntervalMilliseconds
20+
this._pollerFactory = pollerFactory
3121

3222
this._pollers = {}
3323
}
@@ -39,17 +29,8 @@ class Daemon {
3929
*/
4030
_createPollerDescriptor (externalSecret) {
4131
const { uid, name, namespace } = externalSecret.metadata
42-
// NOTE(jdaeli): hash this in case resource version becomes too long?
43-
const secretDescriptor = { ...externalSecret.secretDescriptor, name }
44-
const ownerReference = {
45-
apiVersion: externalSecret.apiVersion,
46-
controller: true,
47-
kind: externalSecret.kind,
48-
name,
49-
uid
50-
}
5132

52-
return { id: uid, namespace, secretDescriptor, ownerReference }
33+
return { id: uid, name, namespace, externalSecret }
5334
}
5435

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

71-
_addPoller (descriptor, forcePoll = false) {
72-
this._logger.info('spinning up poller', descriptor)
73-
74-
const poller = new Poller({
75-
backends: this._backends,
76-
intervalMilliseconds: this._pollerIntervalMilliseconds,
77-
kubeClient: this._kubeClient,
78-
logger: this._logger,
79-
metrics: this._metrics,
80-
namespace: descriptor.namespace,
81-
secretDescriptor: descriptor.secretDescriptor,
82-
ownerReference: descriptor.ownerReference
83-
})
84-
85-
this._pollers[descriptor.id] = poller.start({ forcePoll })
52+
_addPoller (descriptor) {
53+
this._logger.info('spinning up poller for', descriptor.name, 'in', descriptor.namespace)
54+
55+
const poller = this._pollerFactory.createPoller(descriptor)
56+
57+
this._pollers[descriptor.id] = poller.start()
8658
}
8759

8860
/**
@@ -98,14 +70,10 @@ class Daemon {
9870
break
9971
}
10072

101-
case 'ADDED': {
102-
this._addPoller(descriptor, true)
103-
break
104-
}
105-
73+
case 'ADDED':
10674
case 'MODIFIED': {
10775
this._removePoller(descriptor.id)
108-
this._addPoller(descriptor, true)
76+
this._addPoller(descriptor)
10977
break
11078
}
11179

lib/daemon.test.js

+62-9
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,27 @@ const { expect } = require('chai')
55
const sinon = require('sinon')
66

77
const Daemon = require('./daemon')
8-
const Poller = require('./poller')
98

109
describe('Daemon', () => {
1110
let daemon
1211
let loggerMock
12+
let pollerMock
13+
let pollerFactory
1314

1415
beforeEach(() => {
1516
loggerMock = sinon.mock()
1617
loggerMock.info = sinon.stub()
1718

19+
pollerMock = sinon.mock()
20+
pollerMock.start = sinon.stub().returns(pollerMock)
21+
pollerMock.stop = sinon.stub().returns(pollerMock)
22+
23+
pollerFactory = sinon.mock()
24+
pollerFactory.createPoller = sinon.stub().returns(pollerMock)
25+
1826
daemon = new Daemon({
1927
logger: loggerMock,
20-
pollerIntervalMilliseconds: 0
28+
pollerFactory
2129
})
2230
})
2331

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

2836
it('starts new pollers for external secrets', async () => {
29-
sinon.stub(Poller.prototype, 'start')
30-
.callsFake(function () { return this })
31-
sinon.stub(Poller.prototype, 'stop')
32-
.callsFake(function () { return this })
33-
3437
const fakeExternalSecretEvents = (async function * () {
3538
yield {
3639
type: 'ADDED',
@@ -49,7 +52,57 @@ describe('Daemon', () => {
4952
await daemon.start()
5053
daemon.stop()
5154

52-
expect(Poller.prototype.start.called).to.equal(true)
53-
expect(Poller.prototype.stop.called).to.equal(true)
55+
expect(pollerMock.start.called).to.equal(true)
56+
expect(pollerMock.stop.called).to.equal(true)
57+
})
58+
59+
it('tries to remove existing poller on ADDED events', async () => {
60+
const fakeExternalSecretEvents = (async function * () {
61+
yield {
62+
type: 'ADDED',
63+
object: {
64+
metadata: {
65+
name: 'foo',
66+
namespace: 'foo',
67+
uid: 'test-id'
68+
}
69+
}
70+
}
71+
}())
72+
73+
daemon._externalSecretEvents = fakeExternalSecretEvents
74+
daemon._addPoller = sinon.mock()
75+
daemon._removePoller = sinon.mock()
76+
77+
await daemon.start()
78+
daemon.stop()
79+
80+
expect(daemon._addPoller.called).to.equal(true)
81+
expect(daemon._removePoller.calledWith('test-id')).to.equal(true)
82+
})
83+
84+
it('tries to remove existing poller on MODIFIED event', async () => {
85+
const fakeExternalSecretEvents = (async function * () {
86+
yield {
87+
type: 'MODIFIED',
88+
object: {
89+
metadata: {
90+
name: 'foo',
91+
namespace: 'foo',
92+
uid: 'test-id'
93+
}
94+
}
95+
}
96+
}())
97+
98+
daemon._externalSecretEvents = fakeExternalSecretEvents
99+
daemon._addPoller = sinon.mock()
100+
daemon._removePoller = sinon.mock()
101+
102+
await daemon.start()
103+
daemon.stop()
104+
105+
expect(daemon._addPoller.called).to.equal(true)
106+
expect(daemon._removePoller.calledWith('test-id')).to.equal(true)
54107
})
55108
})

lib/poller-factory.js

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
'use strict'
2+
3+
const Poller = require('./poller')
4+
5+
class PollerFactory {
6+
/**
7+
* Create PollerFactory.
8+
* @param {Object} backends - Backends for fetching secret properties.
9+
* @param {Object} kubeClient - Client for interacting with kubernetes cluster.
10+
* @param {Object} metrics - Metrics client
11+
* @param {Object} customResourceManifest - CRD manifest
12+
* @param {Object} logger - Logger for logging stuff.
13+
* @param {number} pollerIntervalMilliseconds - Interval time in milliseconds for polling secret properties.
14+
*/
15+
constructor ({
16+
backends,
17+
kubeClient,
18+
metrics,
19+
pollerIntervalMilliseconds,
20+
customResourceManifest,
21+
logger
22+
}) {
23+
this._logger = logger
24+
this._metrics = metrics
25+
this._backends = backends
26+
this._kubeClient = kubeClient
27+
this._pollerIntervalMilliseconds = pollerIntervalMilliseconds
28+
this._customResourceManifest = customResourceManifest
29+
}
30+
31+
/**
32+
* Create poller
33+
* @param {Object} externalSecret - External Secret custom resource oject
34+
*/
35+
createPoller ({ externalSecret }) {
36+
const poller = new Poller({
37+
backends: this._backends,
38+
intervalMilliseconds: this._pollerIntervalMilliseconds,
39+
kubeClient: this._kubeClient,
40+
logger: this._logger,
41+
metrics: this._metrics,
42+
customResourceManifest: this._customResourceManifest,
43+
externalSecret
44+
})
45+
46+
return poller
47+
}
48+
}
49+
50+
module.exports = PollerFactory

0 commit comments

Comments
 (0)