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

Commit 48db901

Browse files
feat: Upsert secrets only when needed (#782)
1 parent 2e00799 commit 48db901

File tree

3 files changed

+132
-12
lines changed

3 files changed

+132
-12
lines changed

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ metadata:
1515
rules:
1616
- apiGroups: [""]
1717
resources: ["secrets"]
18-
verbs: ["create", "update"]
18+
verbs: ["create", "update", "get"]
1919
- apiGroups: [""]
2020
resources: ["namespaces"]
2121
verbs: ["get", "watch", "list"]

lib/poller.js

+47-4
Original file line numberDiff line numberDiff line change
@@ -163,14 +163,57 @@ class Poller {
163163
}
164164

165165
const secretManifest = await this._createSecretManifest()
166-
this._logger.info(`upserting secret ${this._namespace}/${this._name}`)
166+
const kubeSecret = kubeNamespace.secrets(this._name)
167+
let existingSecret
167168

168169
try {
169-
return await kubeNamespace.secrets.post({ body: secretManifest })
170+
this._logger.info(`getting secret ${this._namespace}/${this._name}`)
171+
const secretResponse = await kubeSecret.get()
172+
existingSecret = secretResponse.body
170173
} catch (err) {
171-
if (err.statusCode !== 409) throw err
172-
return kubeNamespace.secrets(this._name).put({ body: secretManifest })
174+
if (err.statusCode !== 404) throw err
175+
// do nothing if the secret is not found
173176
}
177+
178+
if (existingSecret && this._equalSecretData(existingSecret, secretManifest)) {
179+
this._logger.info(`skipping secret ${this._namespace}/${this._name} upsert, objects are the same`)
180+
return Promise.resolve(true)
181+
} else if (existingSecret === undefined) {
182+
this._logger.info(`creating secret ${this._namespace}/${this._name}`)
183+
return await kubeNamespace.secrets.post({ body: secretManifest })
184+
} else {
185+
this._logger.info(`updating secret ${this._namespace}/${this._name}`)
186+
return kubeSecret.put({ body: secretManifest })
187+
}
188+
}
189+
190+
/**
191+
* Checks if a secret and the desired secret manifest are equal
192+
*
193+
* @param {Object} kubeSecret An actual kubernetes secret from the kube-client
194+
* @param {Object} secretManifest A hash representing the desired secret
195+
*
196+
* @return {Boolean} Boolean if they are the same or not
197+
*/
198+
_equalSecretData (kubeSecret, secretManifest) {
199+
let result = true
200+
const liveSecret = clonedeep(kubeSecret)
201+
const desiredSecret = clonedeep(secretManifest)
202+
203+
// Only use annotations and labels for metadata checking
204+
const secrets = [liveSecret, desiredSecret]
205+
secrets.forEach((s) => {
206+
s.metadata = {
207+
labels: s.metadata.labels,
208+
annotations: s.metadata.annotations
209+
}
210+
})
211+
212+
result = result ? JSON.stringify(liveSecret.metadata) === JSON.stringify(desiredSecret.metadata) : false
213+
214+
// check secret data
215+
result = result ? JSON.stringify(liveSecret.data) === JSON.stringify(desiredSecret.data) : false
216+
return result
174217
}
175218

176219
async _updateStatus (status) {

lib/poller.test.js

+84-7
Original file line numberDiff line numberDiff line change
@@ -677,6 +677,11 @@ describe('Poller', () => {
677677
})
678678

679679
it('creates new secret', async () => {
680+
const notFoundError = new Error('Not Found')
681+
notFoundError.statusCode = 404
682+
const kubeSecret = sinon.mock()
683+
kubeSecret.get = sinon.stub().throws(notFoundError)
684+
kubeNamespaceMock.secrets = sinon.stub().returns(kubeSecret)
680685
kubeNamespaceMock.secrets.post = sinon.stub().resolves()
681686

682687
await poller._upsertKubernetesSecret()
@@ -696,18 +701,88 @@ describe('Poller', () => {
696701
})).to.equal(true)
697702
})
698703

704+
it("doesn't update a secret if it hasn't changed", async () => {
705+
const kubeSecret = sinon.mock()
706+
kubeSecret.put = sinon.stub()
707+
kubeSecret.get = sinon.stub().returns({
708+
body: {
709+
metadata: {
710+
name: 'fakeSecretName'
711+
},
712+
data: {
713+
fakePropertyName: 'ZmFrZVByb3BlcnR5VmFsdWU='
714+
}
715+
}
716+
})
717+
kubeNamespaceMock.secrets = sinon.stub().returns(kubeSecret)
718+
kubeNamespaceMock.secrets.post = sinon.stub()
719+
720+
const result = await poller._upsertKubernetesSecret()
721+
expect(result).to.equal(true)
722+
expect(kubeSecret.put.called).to.equal(false)
723+
expect(kubeNamespaceMock.secrets.post.called).to.equal(false)
724+
})
725+
699726
it('updates secret', async () => {
700-
const conflictError = new Error('Conflict')
701-
conflictError.statusCode = 409
702-
kubeNamespaceMock.secrets.post = sinon.stub().throws(conflictError)
703-
kubeNamespaceMock.put = sinon.stub().resolves()
727+
const kubeSecret = sinon.mock()
728+
kubeSecret.get = sinon.stub().returns({
729+
body: {
730+
metadata: {
731+
name: 'fakeSecretName'
732+
},
733+
data: {
734+
fakePropertyName: 'differentValue'
735+
}
736+
}
737+
})
738+
kubeNamespaceMock.secrets = sinon.stub().returns(kubeSecret)
739+
kubeSecret.put = sinon.stub().resolves()
704740
kubeNamespaceMock.get = sinon.stub().resolves(fakeNamespace)
705741

706742
await poller._upsertKubernetesSecret()
707743

708744
expect(kubeNamespaceMock.secrets.calledWith('fakeSecretName')).to.equal(true)
709745

710-
expect(kubeNamespaceMock.put.calledWith({
746+
expect(kubeSecret.put.calledWith({
747+
body: {
748+
apiVersion: 'v1',
749+
kind: 'Secret',
750+
metadata: {
751+
name: 'fakeSecretName'
752+
},
753+
type: 'some-type',
754+
data: {
755+
fakePropertyName: 'ZmFrZVByb3BlcnR5VmFsdWU='
756+
}
757+
}
758+
})).to.equal(true)
759+
})
760+
761+
it('updates secret if the custom metadata has changed', async () => {
762+
const kubeSecret = sinon.mock()
763+
kubeSecret.get = sinon.stub().returns({
764+
body: {
765+
metadata: {
766+
creationTimestamp: new Date().toDateString(),
767+
name: 'fakeSecretName',
768+
labels: {
769+
myFakeLabel: 'test'
770+
}
771+
},
772+
data: {
773+
fakePropertyName: 'ZmFrZVByb3BlcnR5VmFsdWU='
774+
}
775+
}
776+
})
777+
kubeNamespaceMock.secrets = sinon.stub().returns(kubeSecret)
778+
kubeSecret.put = sinon.stub().resolves()
779+
kubeNamespaceMock.get = sinon.stub().resolves(fakeNamespace)
780+
781+
await poller._upsertKubernetesSecret()
782+
783+
expect(kubeNamespaceMock.secrets.calledWith('fakeSecretName')).to.equal(true)
784+
785+
expect(kubeSecret.put.calledWith({
711786
body: {
712787
apiVersion: 'v1',
713788
kind: 'Secret',
@@ -746,7 +821,9 @@ describe('Poller', () => {
746821
it('fails storing secret', async () => {
747822
const internalErrorServer = new Error('Internal Error Server')
748823
internalErrorServer.statusCode = 500
749-
824+
const kubeSecret = sinon.mock()
825+
kubeNamespaceMock.secrets = sinon.stub().returns(kubeSecret)
826+
kubeSecret.get = sinon.stub().throws({ statusCode: 404 })
750827
kubeNamespaceMock.secrets.post = sinon.stub().throws(internalErrorServer)
751828

752829
let error
@@ -856,7 +933,7 @@ describe('Poller', () => {
856933
}
857934
})
858935
})
859-
describe('nameing conventions', () => {
936+
describe('naming conventions', () => {
860937
let poller
861938
beforeEach(() => {
862939
poller = pollerFactory()

0 commit comments

Comments
 (0)