Repository: godaddy/kubernetes-external-secrets
Pull request #211 (2019-11-08 21:48:29)

feat: allow disabling of interval polling
closes #156 


Patch:
diff --git a/README.md b/README.md
index 2393046..9a46428 100644
--- a/README.md
+++ b/README.md
@@ -40,6 +40,16 @@ $ helm install external-secrets/kubernetes-external-secrets
 
 For more details about configuration see the [helm chart docs](charts/kubernetes-external-secrets/README.md)
 
+### Install with kubectl
+
+If you don't want to install helm on your cluster and just want to use `kubectl` to install `kubernetes-external-secrets`, you could get the `helm` client cli first and then use the following sample command to generate kubernetes manifests:
+
+```bash
+$ helm template -f charts/kubernetes-external-secrets/values.yaml --output-dir ./output_dir ./charts/kubernetes-external-secrets/
+```
+
+The generated kubernetes manifests will be in `./output_dir` and can be applied to deploy `kubernetes-external-secrets` to the cluster.
+
 ### Use IAM credentials for Secrets Manager access
 
 If not running on EKS you will have to use an IAM user (in lieu of a role).
diff --git a/bin/daemon.js b/bin/daemon.js
index ec7986e..ba3ee28 100755
--- a/bin/daemon.js
+++ b/bin/daemon.js
@@ -22,6 +22,7 @@ const {
   logger,
   metricsPort,
   pollerIntervalMilliseconds,
+  pollingDisabled,
   rolePermittedAnnotation
 } = require('../config')
 
@@ -49,6 +50,7 @@ async function main () {
     pollerIntervalMilliseconds,
     rolePermittedAnnotation,
     customResourceManifest,
+    pollingDisabled,
     logger
   })
 
diff --git a/charts/kubernetes-external-secrets/README.md b/charts/kubernetes-external-secrets/README.md
index 948683e..afb95be 100644
--- a/charts/kubernetes-external-secrets/README.md
+++ b/charts/kubernetes-external-secrets/README.md
@@ -49,6 +49,7 @@ The following table lists the configurable parameters of the `kubernetes-externa
 | `env.ROLE_PERMITTED_ANNOTATION`           | Specify the annotation key where to lookup the role arn permission boundaries | `iam.amazonaws.com/permitted`          |
 | `env.POLLER_INTERVAL_MILLISECONDS`   | Set POLLER_INTERVAL_MILLISECONDS in Deployment Pod           | `10000`                                                 |
 | `env.VAULT_ADDR`                          | Endpoint for the Vault backend, if using Vault               | `http://127.0.0.1:8200                                  |
+| `env.DISABLE_POLLING`                          | Disables backend polling and only updates secrets when ExternalSecret is modified, setting this to any value will disable polling               | `nil`                                  |
 | `envVarsFromSecret.AWS_ACCESS_KEY_ID`     | Set AWS_ACCESS_KEY_ID (from a secret) in Deployment Pod      |                                                         |
 | `envVarsFromSecret.AWS_SECRET_ACCESS_KEY` | Set AWS_SECRET_ACCESS_KEY (from a secret) in Deployment Pod  |                                                         |
 | `image.repository`                   | kubernetes-external-secrets Image name                       | `godaddy/kubernetes-external-secrets`                   |
diff --git a/config/environment.js b/config/environment.js
index b2cf2dd..ae74552 100644
--- a/config/environment.js
+++ b/config/environment.js
@@ -21,6 +21,7 @@ const pollerIntervalMilliseconds = process.env.POLLER_INTERVAL_MILLISECONDS
   ? Number(process.env.POLLER_INTERVAL_MILLISECONDS) : 10000
 
 const logLevel = process.env.LOG_LEVEL || 'info'
+const pollingDisabled = 'DISABLE_POLLING' in process.env
 
 const rolePermittedAnnotation = process.env.ROLE_PERMITTED_ANNOTATION || 'iam.amazonaws.com/permitted'
 
@@ -32,5 +33,6 @@ module.exports = {
   pollerIntervalMilliseconds,
   metricsPort,
   rolePermittedAnnotation,
+  pollingDisabled,
   logLevel
 }
diff --git a/external-secrets.yml b/external-secrets.yml
deleted file mode 100644
index 38477cc..0000000
--- a/external-secrets.yml
+++ /dev/null
@@ -1,88 +0,0 @@
-apiVersion: rbac.authorization.k8s.io/v1
-kind: ClusterRoleBinding
-metadata:
-  name: kubernetes-external-secrets-cluster-role-binding
-subjects:
-- kind: ServiceAccount
-  name: kubernetes-external-secrets-service-account
-  namespace: kubernetes-external-secrets
-roleRef:
-  kind: ClusterRole
-  name: kubernetes-external-secrets-cluster-role
-  apiGroup: rbac.authorization.k8s.io
----
-apiVersion: rbac.authorization.k8s.io/v1
-kind: ClusterRole
-metadata:
-  name: kubernetes-external-secrets-cluster-role
-rules:
-- apiGroups: [""]
-  resources: ["secrets"]
-  verbs: ["create", "update"]
-- apiGroups: [""]
-  resources: ["namespaces"]
-  verbs: ["get", "watch", "list"]
-- apiGroups: ["apiextensions.k8s.io"]
-  resources: ["customresourcedefinitions"]
-  verbs: ["create"]
-- apiGroups: ["apiextensions.k8s.io"]
-  resources: ["customresourcedefinitions"]
-  resourceNames: ["externalsecrets.kubernetes-client.io"]
-  verbs: ["get", "update"]
-- 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
-metadata:
-  name: kubernetes-external-secrets-cluster-role-binding-auth
-roleRef:
-  apiGroup: rbac.authorization.k8s.io
-  kind: ClusterRole
-  name: system:auth-delegator
-subjects:
-- kind: ServiceAccount
-  name: kubernetes-external-secrets-service-account
-  namespace: kubernetes-external-secrets
----
-apiVersion: v1
-kind: Namespace
-metadata:
-  name: kubernetes-external-secrets
----
-apiVersion: v1
-kind: ServiceAccount
-metadata:
-  name: kubernetes-external-secrets-service-account
-  namespace: kubernetes-external-secrets
----
-apiVersion: apps/v1
-kind: Deployment
-metadata:
-  labels:
-    name: kubernetes-external-secrets
-  name: kubernetes-external-secrets
-  namespace: kubernetes-external-secrets
-spec:
-  replicas: 1
-  selector:
-    matchLabels:
-      name: kubernetes-external-secrets
-  template:
-    metadata:
-      labels:
-        name: kubernetes-external-secrets
-        service: kubernetes-external-secrets
-    spec:
-      serviceAccountName: kubernetes-external-secrets-service-account
-      containers:
-        - image: godaddy/kubernetes-external-secrets:2.1.0
-          imagePullPolicy: Always
-          name: kubernetes-external-secrets
-          env:
-            - name: AWS_REGION
-              value: us-west-2
diff --git a/lib/backends/kv-backend.js b/lib/backends/kv-backend.js
index e4e5a41..e7820ca 100644
--- a/lib/backends/kv-backend.js
+++ b/lib/backends/kv-backend.js
@@ -20,14 +20,14 @@ class KVBackend extends AbstractBackend {
    * @param {string} data[].name - Kubernetes Secret property name.
    * @param {string} data[].property - If the backend secret is an
    *   object, this is the property name of the value to use.
-   * @param {string} roleArn - If the client should assume a role before fetching the secret
+   * @param {Object} specOptions - Options set on spec level.
    * @returns {Promise} Promise object representing secret property values.
    */
-  _fetchDataValues ({ data, roleArn }) {
-    return Promise.all(data.map(async secretProperty => {
-      this._logger.info(`fetching secret property ${secretProperty.name} with role: ${roleArn || 'no role set'}`)
-      const plainOrObjValue = await this._get({ secretKey: secretProperty.key, roleArn })
-      const shouldParseValue = 'property' in secretProperty
+  _fetchDataValues ({ data, specOptions }) {
+    return Promise.all(data.map(async dataItem => {
+      const { name, property = null, key, ...keyOptions } = dataItem
+      const plainOrObjValue = await this._get({ key, keyOptions, specOptions })
+      const shouldParseValue = 'property' in dataItem
 
       let value = plainOrObjValue
       if (shouldParseValue) {
@@ -35,38 +35,37 @@ class KVBackend extends AbstractBackend {
         try {
           parsedValue = JSON.parse(value)
         } catch (err) {
-          this._logger.warn(`Failed to JSON.parse value for '${secretProperty.key}',` +
+          this._logger.warn(`Failed to JSON.parse value for '${key}',` +
            ` please verify that your secret value is correctly formatted as JSON.` +
-           ` To use plain text secret remove the 'property: ${secretProperty.property}'`)
+           ` To use plain text secret remove the 'property: ${property}'`)
           return
         }
 
-        if (!(secretProperty.property in parsedValue)) {
-          throw new Error(`Could not find property ${secretProperty.property} in ${secretProperty.key}`)
+        if (!(property in parsedValue)) {
+          throw new Error(`Could not find property ${property} in ${key}`)
         }
 
-        value = parsedValue[secretProperty.property]
+        value = parsedValue[property]
       }
 
-      return { [secretProperty.name]: value }
+      return { [name]: value }
     }))
   }
 
   /**
    * Fetch Kubernetes secret property values.
    * @param {string[]} dataFrom - Array of secret keys in the backend
-   * @param {string} roleArn - If the client should assume a role before fetching the secret
+   * @param {string} specOptions - Options set on spec level that might be interesting for the backend
    * @returns {Promise} Promise object representing secret property values.
    */
-  _fetchDataFromValues ({ dataFrom, roleArn }) {
-    return Promise.all(dataFrom.map(async secretKey => {
-      this._logger.info(`fetching secret ${secretKey} with role: ${roleArn || 'no role set'}`)
-      const value = await this._get({ secretKey, roleArn })
+  _fetchDataFromValues ({ dataFrom, specOptions }) {
+    return Promise.all(dataFrom.map(async key => {
+      const value = await this._get({ key, specOptions })
 
       try {
         return JSON.parse(value)
       } catch (err) {
-        this._logger.warn(`Failed to JSON.parse value for '${secretKey}',` +
+        this._logger.warn(`Failed to JSON.parse value for '${key}',` +
            ` please verify that your secret value is correctly formatted as JSON.`)
       }
     }))
@@ -74,8 +73,12 @@ class KVBackend extends AbstractBackend {
 
   /**
    * Get a secret property value from Key Value backend.
+   * @param {string} key - Secret key in the backend.
+   * @param {string} keyOptions - Options for this specific key, eg version etc.
+   * @param {string} specOptions - Options for this external secret, eg role
+   * @returns {Promise} Promise object representing secret property values.
    */
-  _get () {
+  _get ({ key, keyOptions, specOptions }) {
     throw new Error('_get not implemented')
   }
 
@@ -90,12 +93,12 @@ class KVBackend extends AbstractBackend {
       properties = [],
       data = properties,
       dataFrom = [],
-      roleArn
+      ...specOptions
     }
   }) {
     const [dataFromValues, dataValues] = await Promise.all([
-      this._fetchDataFromValues({ dataFrom, roleArn }),
-      this._fetchDataValues({ data, roleArn })
+      this._fetchDataFromValues({ dataFrom, specOptions }),
+      this._fetchDataValues({ data, specOptions })
     ])
 
     const plainValues = dataFromValues.concat(dataValues)
diff --git a/lib/backends/secrets-manager-backend.js b/lib/backends/secrets-manager-backend.js
index 99fdcbf..0cc86c1 100644
--- a/lib/backends/secrets-manager-backend.js
+++ b/lib/backends/secrets-manager-backend.js
@@ -18,10 +18,15 @@ class SecretsManagerBackend extends KVBackend {
 
   /**
    * Get secret property value from Secrets Manager.
-   * @param {string} secretKey - Key used to store secret property value in Secrets Manager.
+   * @param {string} key - Key used to store secret property value in Secrets Manager.
+   * @param {object} keyOptions - Options for this specific key, eg version etc.
+   * @param {object} specOptions - Options for this external secret, eg role
+   * @param {string} specOptions.roleArn - IAM role arn to assume
    * @returns {Promise} Promise object representing secret property value.
    */
-  async _get ({ secretKey, roleArn }) {
+  async _get ({ key, specOptions: { roleArn } }) {
+    this._logger.info(`fetching secret property ${key} with role: ${roleArn || 'pods role'}`)
+
     let client = this._client
     if (roleArn) {
       const res = await this._assumeRole({
@@ -36,7 +41,7 @@ class SecretsManagerBackend extends KVBackend {
     }
 
     const data = await client
-      .getSecretValue({ SecretId: secretKey })
+      .getSecretValue({ SecretId: key })
       .promise()
 
     if ('SecretBinary' in data) {
@@ -45,7 +50,7 @@ class SecretsManagerBackend extends KVBackend {
       return data.SecretString
     }
 
-    this._logger.error(`Unexpected data from Secrets Manager secret ${secretKey}`)
+    this._logger.error(`Unexpected data from Secrets Manager secret ${key}`)
     return null
   }
 }
diff --git a/lib/backends/system-manager-backend.js b/lib/backends/system-manager-backend.js
index 9f30a82..24255bf 100644
--- a/lib/backends/system-manager-backend.js
+++ b/lib/backends/system-manager-backend.js
@@ -18,10 +18,15 @@ class SystemManagerBackend extends KVBackend {
 
   /**
    * Get secret property value from System Manager.
-   * @param {string} secretKey - Key used to store secret property value in System Manager.
+   * @param {string} key - Key used to store secret property value in System Manager.
+   * @param {object} keyOptions - Options for this specific key, eg version etc.
+   * @param {object} specOptions - Options for this external secret, eg role
+   * @param {string} specOptions.roleArn - IAM role arn to assume
    * @returns {Promise} Promise object representing secret property value.
    */
-  async _get ({ secretKey, roleArn }) {
+  async _get ({ key, specOptions: { roleArn } }) {
+    this._logger.info(`fetching secret property ${key} with role: ${roleArn || 'pods role'}`)
+
     let client = this._client
     if (roleArn) {
       const res = await this._assumeRole({
@@ -36,7 +41,7 @@ class SystemManagerBackend extends KVBackend {
     }
     const data = await client
       .getParameter({
-        Name: secretKey,
+        Name: key,
         WithDecryption: true
       })
       .promise()
diff --git a/lib/backends/vault-backend.js b/lib/backends/vault-backend.js
index 987d664..68687d2 100644
--- a/lib/backends/vault-backend.js
+++ b/lib/backends/vault-backend.js
@@ -26,30 +26,16 @@ class VaultBackend extends KVBackend {
     return this._serviceAccountToken
   }
 
-  /**
-   * Fetch Kubernetes secret property values.
-   * @param {Object[]} secretProperties - Kubernetes secret properties.
-   * @param {string} secretProperties[].key - Secret key in the backend.
-   * @param {string} secretProperties[].name - Kubernetes Secret property name.
-   * @param {string} secretProperties[].property - If the backend secret is an
-   *   object, this is the property name of the value to use.
-   * @returns {Promise} Promise object representing secret property values.
-   */
-  _fetchSecretPropertyValues ({ vaultMountPoint, vaultRole, jwt, externalData }) {
-    return Promise.all(externalData.map(async secretProperty => {
-      this._logger.info(`fetching secret property ${secretProperty.key}`)
-      const value = await this._get({ vaultMountPoint: vaultMountPoint, vaultRole: vaultRole, jwt: jwt, secretKey: secretProperty.key })
-
-      return value[secretProperty.property]
-    }))
-  }
-
   /**
    * Get secret property value from Vault.
-   * @param {string} secretKey - Key used to store secret property value in Vault.
-   * @returns {Promise} Promise object representing secret property value.
+   * @param {string} key - Secret key in the backend.
+   * @param {object} keyOptions - Options for this specific key, eg version etc.
+   * @param {object} specOptions - Options for this external secret, eg role
+   * @param {string} specOptions.vaultMountPoint - mount point
+   * @param {string} specOptions.vaultRole - role
+   * @returns {Promise} Promise object representing secret property values.
    */
-  async _get ({ vaultMountPoint, vaultRole, secretKey }) {
+  async _get ({ key, specOptions: { vaultMountPoint, vaultRole } }) {
     if (!this._client.token) {
       const jwt = this._fetchServiceAccountToken()
       this._logger.debug(`fetching new token from vault`)
@@ -64,34 +50,11 @@ class VaultBackend extends KVBackend {
       this._client.tokenRenewSelf()
     }
 
-    this._logger.debug(`reading secret key ${secretKey} from vault`)
-    const secretResponse = await this._client.read(secretKey)
+    this._logger.debug(`reading secret key ${key} from vault`)
+    const secretResponse = await this._client.read(key)
 
     return secretResponse.data.data
   }
-
-  /**
-   * Fetch Kubernetes secret manifest data.
-   * @param {ExternalSecretSpec} spec - Kubernetes ExternalSecret spec.
-   * @returns {Promise} Promise object representing Kubernetes secret manifest data.
-   */
-  async getSecretManifestData ({ spec }) {
-    const data = {}
-    const vaultMountPoint = spec.vaultMountPoint
-    const vaultRole = spec.vaultRole
-
-    // Also support spec.properties to be backwards compatible.
-    const externalData = spec.data || spec.properties
-    const secretPropertyValues = await this._fetchSecretPropertyValues({
-      vaultMountPoint,
-      vaultRole,
-      externalData
-    })
-    externalData.forEach((secret, index) => {
-      data[secret.name] = (Buffer.from(secretPropertyValues[index], 'utf8')).toString('base64')
-    })
-    return data
-  }
 }
 
 module.exports = VaultBackend
diff --git a/lib/poller-factory.js b/lib/poller-factory.js
index d683a8f..d5c71b9 100644
--- a/lib/poller-factory.js
+++ b/lib/poller-factory.js
@@ -20,6 +20,7 @@ class PollerFactory {
     pollerIntervalMilliseconds,
     rolePermittedAnnotation,
     customResourceManifest,
+    pollingDisabled,
     logger
   }) {
     this._logger = logger
@@ -29,6 +30,7 @@ class PollerFactory {
     this._pollerIntervalMilliseconds = pollerIntervalMilliseconds
     this._customResourceManifest = customResourceManifest
     this._rolePermittedAnnotation = rolePermittedAnnotation
+    this._pollingDisabled = pollingDisabled
   }
 
   /**
@@ -44,6 +46,7 @@ class PollerFactory {
       metrics: this._metrics,
       customResourceManifest: this._customResourceManifest,
       rolePermittedAnnotation: this._rolePermittedAnnotation,
+      pollingDisabled: this._pollingDisabled,
       externalSecret
     })
 
diff --git a/lib/poller.js b/lib/poller.js
index 123f723..d64819b 100644
--- a/lib/poller.js
+++ b/lib/poller.js
@@ -37,6 +37,7 @@ class Poller {
     metrics,
     customResourceManifest,
     rolePermittedAnnotation,
+    pollingDisabled,
     externalSecret
   }) {
     this._backends = backends
@@ -45,6 +46,7 @@ class Poller {
     this._logger = logger
     this._timeoutId = null
     this._metrics = metrics
+    this._pollingDisabled = pollingDisabled
     this._rolePermittedAnnotation = rolePermittedAnnotation
     this._customResourceManifest = customResourceManifest
 
@@ -221,6 +223,11 @@ class Poller {
         return this._setNextPoll(0)
       }
 
+      // If polling is disabled we only react to changes in the ExternalSecret
+      if (this._pollingDisabled) {
+        return
+      }
+
       const now = Date.now()
       const lastPollTime = Date.parse(lastSync) || 0
 


Test patch:
diff --git a/lib/backends/kv-backend.test.js b/lib/backends/kv-backend.test.js
index 4098ee3..200ac7b 100644
--- a/lib/backends/kv-backend.test.js
+++ b/lib/backends/kv-backend.test.js
@@ -34,12 +34,19 @@ describe('kv-backend', () => {
             key: 'mocked-key',
             name: 'mocked-name',
             property: 'oops'
-          }]
+          }],
+          specOptions: {}
         })
         expect.fail('Should not reach')
       } catch (err) {
         expect(err).to.be.an('error')
       }
+      expect(kvBackend._get.calledOnce).to.equal(true)
+      expect(kvBackend._get.getCall(0).args[0]).to.deep.equal({
+        key: 'mocked-key',
+        keyOptions: {},
+        specOptions: {}
+      })
     })
 
     it('handles secrets values that are objects', async () => {
@@ -77,19 +84,22 @@ describe('kv-backend', () => {
         }, {
           key: 'fakePropertyKey2',
           name: 'fakePropertyName2'
-        }]
+        }],
+        specOptions: {
+          passMeAlong: true
+        }
       })
 
-      expect(loggerMock.info.calledWith('fetching secret property fakePropertyName1 with role: no role set')).to.equal(true)
-      expect(loggerMock.info.calledWith('fetching secret property fakePropertyName2 with role: no role set')).to.equal(true)
-      expect(kvBackend._get.calledWith({
-        secretKey: 'fakePropertyKey1',
-        roleArn: undefined
-      })).to.equal(true)
-      expect(kvBackend._get.calledWith({
-        secretKey: 'fakePropertyKey2',
-        roleArn: undefined
-      })).to.equal(true)
+      expect(kvBackend._get.getCall(0).args[0]).to.deep.equal({
+        key: 'fakePropertyKey1',
+        keyOptions: {},
+        specOptions: { passMeAlong: true }
+      })
+      expect(kvBackend._get.getCall(1).args[0]).to.deep.equal({
+        key: 'fakePropertyKey2',
+        keyOptions: {},
+        specOptions: { passMeAlong: true }
+      })
       expect(secretPropertyValues).deep.equals([{ fakePropertyName1: 'fakePropertyValue1' }, { fakePropertyName2: 'fakePropertyValue2' }])
     })
 
@@ -105,19 +115,19 @@ describe('kv-backend', () => {
           key: 'fakePropertyKey2',
           name: 'fakePropertyName2'
         }],
-        roleArn: 'secretDescriptiorRole'
+        specOptions: { roleArn: 'secretDescriptiorRole' }
       })
 
-      expect(loggerMock.info.calledWith('fetching secret property fakePropertyName1 with role: secretDescriptiorRole')).to.equal(true)
-      expect(loggerMock.info.calledWith('fetching secret property fakePropertyName2 with role: secretDescriptiorRole')).to.equal(true)
-      expect(kvBackend._get.calledWith({
-        secretKey: 'fakePropertyKey1',
-        roleArn: 'secretDescriptiorRole'
-      })).to.equal(true)
-      expect(kvBackend._get.calledWith({
-        secretKey: 'fakePropertyKey2',
-        roleArn: 'secretDescriptiorRole'
-      })).to.equal(true)
+      expect(kvBackend._get.getCall(0).args[0]).to.deep.equal({
+        key: 'fakePropertyKey1',
+        keyOptions: {},
+        specOptions: { roleArn: 'secretDescriptiorRole' }
+      })
+      expect(kvBackend._get.getCall(1).args[0]).to.deep.equal({
+        key: 'fakePropertyKey2',
+        keyOptions: {},
+        specOptions: { roleArn: 'secretDescriptiorRole' }
+      })
       expect(secretPropertyValues).deep.equals([{ fakePropertyName1: 'fakePropertyValue1' }, { fakePropertyName2: 'fakePropertyValue2' }])
     })
   })
@@ -147,33 +157,34 @@ describe('kv-backend', () => {
       kvBackend._get.onFirstCall().resolves('fakePropertyValue1')
 
       const dataFromValues = await kvBackend._fetchDataFromValues({
-        dataFrom: ['fakePropertyKey1']
+        dataFrom: ['fakePropertyKey1'],
+        specOptions: { passMeAlong: true }
       })
 
-      expect(kvBackend._get.calledWith({
-        secretKey: 'fakePropertyKey1',
-        roleArn: undefined
-      })).to.equal(true)
+      expect(kvBackend._get.getCall(0).args[0]).to.deep.equal({
+        key: 'fakePropertyKey1',
+        specOptions: { passMeAlong: true }
+      })
       expect(dataFromValues).deep.equals([undefined])
     })
 
-    it('fetches secret property values using the specified role', async () => {
+    it('fetches secret property values using the specified options', async () => {
       kvBackend._get.onFirstCall().resolves('{"fakePropertyName1":"fakePropertyValue1"}')
       kvBackend._get.onSecondCall().resolves('{"fakePropertyName2":"fakePropertyValue2"}')
 
       const dataFromValues = await kvBackend._fetchDataFromValues({
         dataFrom: ['fakePropertyKey1', 'fakePropertyKey2'],
-        roleArn: 'secretDescriptiorRole'
+        specOptions: { roleArn: 'secretDescriptiorRole' }
       })
 
-      expect(kvBackend._get.calledWith({
-        secretKey: 'fakePropertyKey1',
-        roleArn: 'secretDescriptiorRole'
-      })).to.equal(true)
-      expect(kvBackend._get.calledWith({
-        secretKey: 'fakePropertyKey2',
-        roleArn: 'secretDescriptiorRole'
-      })).to.equal(true)
+      expect(kvBackend._get.getCall(0).args[0]).to.deep.equal({
+        key: 'fakePropertyKey1',
+        specOptions: { roleArn: 'secretDescriptiorRole' }
+      })
+      expect(kvBackend._get.getCall(1).args[0]).to.deep.equal({
+        key: 'fakePropertyKey2',
+        specOptions: { roleArn: 'secretDescriptiorRole' }
+      })
       expect(dataFromValues).deep.equals([{ fakePropertyName1: 'fakePropertyValue1' }, { fakePropertyName2: 'fakePropertyValue2' }])
     })
   })
@@ -183,7 +194,7 @@ describe('kv-backend', () => {
       let error
 
       try {
-        kvBackend._get()
+        kvBackend._get({})
       } catch (err) {
         error = err
       }
@@ -267,12 +278,12 @@ describe('kv-backend', () => {
           key: 'fakePropertyKey2',
           name: 'fakePropertyName2'
         }],
-        roleArn: 'my-role'
+        specOptions: { roleArn: 'my-role' }
       })).to.equal(true)
 
       expect(kvBackend._fetchDataFromValues.calledWith({
         dataFrom: [],
-        roleArn: 'my-role'
+        specOptions: { roleArn: 'my-role' }
       })).to.equal(true)
     })
 
@@ -290,11 +301,13 @@ describe('kv-backend', () => {
           ],
           dataFrom: [
             'fakeDataFromKey1'
-          ]
+          ],
+          roleArn: 'this-should-be-passed-along',
+          magicSetting: 'this as well'
         }
       })
 
-      expect(kvBackend._fetchDataValues.calledWith({
+      expect(kvBackend._fetchDataValues.getCall(0).args[0]).to.deep.equal({
         data: [{
           key: 'fakePropertyKey1',
           name: 'fakePropertyName1'
@@ -302,12 +315,18 @@ describe('kv-backend', () => {
           key: 'fakePropertyKey2',
           name: 'fakePropertyName2'
         }],
-        roleArn: undefined
-      })).to.equal(true)
+        specOptions: {
+          roleArn: 'this-should-be-passed-along',
+          magicSetting: 'this as well'
+        }
+      })
 
       expect(kvBackend._fetchDataFromValues.calledWith({
         dataFrom: ['fakeDataFromKey1'],
-        roleArn: undefined
+        specOptions: {
+          roleArn: 'this-should-be-passed-along',
+          magicSetting: 'this as well'
+        }
       })).to.equal(true)
     })
 
@@ -321,15 +340,15 @@ describe('kv-backend', () => {
         }
       })
 
-      expect(kvBackend._fetchDataValues.calledWith({
+      expect(kvBackend._fetchDataValues.getCall(0).args[0]).to.deep.equal({
         data: [],
-        roleArn: undefined
-      })).to.equal(true)
+        specOptions: { }
+      })
 
-      expect(kvBackend._fetchDataFromValues.calledWith({
+      expect(kvBackend._fetchDataFromValues.getCall(0).args[0]).to.deep.equal({
         dataFrom: ['fakeDataFromKey1', 'fakeDataFromKey2'],
-        roleArn: undefined
-      })).to.equal(true)
+        specOptions: { }
+      })
     })
   })
 })
diff --git a/lib/backends/secrets-manager-backend.test.js b/lib/backends/secrets-manager-backend.test.js
index 7816d9e..2e51792 100644
--- a/lib/backends/secrets-manager-backend.test.js
+++ b/lib/backends/secrets-manager-backend.test.js
@@ -8,9 +8,12 @@ const SecretsManagerBackend = require('./secrets-manager-backend')
 
 describe('SecretsManagerBackend', () => {
   let clientMock
+  let loggerMock
   let clientFactoryMock
   let assumeRoleMock
   let secretsManagerBackend
+  const specOptions = {}
+
   const assumeRoleCredentials = {
     Credentials: {
       AccessKeyId: '1234',
@@ -21,11 +24,14 @@ describe('SecretsManagerBackend', () => {
 
   beforeEach(() => {
     clientMock = sinon.mock()
+    loggerMock = sinon.mock()
+    loggerMock.info = sinon.stub()
     clientFactoryMock = sinon.fake.returns(clientMock)
     assumeRoleMock = sinon.fake.returns(Promise.resolve(assumeRoleCredentials))
     secretsManagerBackend = new SecretsManagerBackend({
       clientFactory: clientFactoryMock,
-      assumeRole: assumeRoleMock
+      assumeRole: assumeRoleMock,
+      logger: loggerMock
     })
   })
 
@@ -44,7 +50,8 @@ describe('SecretsManagerBackend', () => {
       })
 
       const secretPropertyValue = await secretsManagerBackend._get({
-        secretKey: 'fakeSecretKey'
+        key: 'fakeSecretKey',
+        specOptions
       })
 
       expect(clientMock.getSecretValue.calledWith({
@@ -61,7 +68,8 @@ describe('SecretsManagerBackend', () => {
       })
 
       const secretPropertyValue = await secretsManagerBackend._get({
-        secretKey: 'fakeSecretKey'
+        key: 'fakeSecretKey',
+        specOptions
       })
 
       expect(clientMock.getSecretValue.calledWith({
@@ -78,8 +86,8 @@ describe('SecretsManagerBackend', () => {
       })
 
       const secretPropertyValue = await secretsManagerBackend._get({
-        secretKey: 'fakeSecretKey',
-        roleArn: 'my-role'
+        key: 'fakeSecretKey',
+        specOptions: { roleArn: 'my-role' }
       })
 
       expect(clientFactoryMock.lastArg).deep.equals({
diff --git a/lib/backends/system-manager-backend.test.js b/lib/backends/system-manager-backend.test.js
index 652d57e..5446617 100644
--- a/lib/backends/system-manager-backend.test.js
+++ b/lib/backends/system-manager-backend.test.js
@@ -8,9 +8,12 @@ const SystemManagerBackend = require('./system-manager-backend')
 
 describe('SystemManagerBackend', () => {
   let clientMock
+  let loggerMock
   let clientFactoryMock
   let assumeRoleMock
   let systemManagerBackend
+  const specOptions = {}
+
   const assumeRoleCredentials = {
     Credentials: {
       AccessKeyId: '1234',
@@ -21,13 +24,16 @@ describe('SystemManagerBackend', () => {
 
   beforeEach(() => {
     clientMock = sinon.mock()
+    loggerMock = sinon.mock()
+    loggerMock.info = sinon.stub()
     clientFactoryMock = sinon.fake.returns(clientMock)
     assumeRoleMock = sinon.fake.returns(Promise.resolve(assumeRoleCredentials))
 
     systemManagerBackend = new SystemManagerBackend({
       client: clientMock,
       clientFactory: clientFactoryMock,
-      assumeRole: assumeRoleMock
+      assumeRole: assumeRoleMock,
+      logger: loggerMock
     })
   })
 
@@ -48,7 +54,8 @@ describe('SystemManagerBackend', () => {
       })
 
       const secretPropertyValue = await systemManagerBackend._get({
-        secretKey: 'fakeSecretKey'
+        key: 'fakeSecretKey',
+        specOptions
       })
 
       expect(clientMock.getParameter.calledWith({
@@ -68,8 +75,8 @@ describe('SystemManagerBackend', () => {
       })
 
       const secretPropertyValue = await systemManagerBackend._get({
-        secretKey: 'fakeSecretKey',
-        roleArn: 'my-role'
+        key: 'fakeSecretKey',
+        specOptions: { roleArn: 'my-role' }
       })
       expect(clientFactoryMock.lastArg).deep.equals({
         accessKeyId: assumeRoleCredentials.Credentials.AccessKeyId,
diff --git a/lib/backends/vault-backend.test.js b/lib/backends/vault-backend.test.js
index 8c82e11..8ef5dd2 100644
--- a/lib/backends/vault-backend.test.js
+++ b/lib/backends/vault-backend.test.js
@@ -51,9 +51,11 @@ describe('VaultBackend', () => {
 
     it('logs in and returns secret property value', async () => {
       const secretPropertyValue = await vaultBackend._get({
-        vaultMountPoint: mountPoint,
-        vaultRole: role,
-        secretKey: secretKey
+        specOptions: {
+          vaultMountPoint: mountPoint,
+          vaultRole: role
+        },
+        key: secretKey
       })
 
       // First, we log into Vault...
@@ -74,9 +76,11 @@ describe('VaultBackend', () => {
       clientMock.token = 'an-existing-token'
 
       const secretPropertyValue = await vaultBackend._get({
-        vaultMountPoint: mountPoint,
-        vaultRole: role,
-        secretKey: secretKey
+        specOptions: {
+          vaultMountPoint: mountPoint,
+          vaultRole: role
+        },
+        key: secretKey
       })
 
       // No logging into Vault...
diff --git a/lib/poller.test.js b/lib/poller.test.js
index 6e6db67..d9abd73 100644
--- a/lib/poller.test.js
+++ b/lib/poller.test.js
@@ -479,6 +479,25 @@ describe('Poller', () => {
       })
     })
 
+    it('disable interval polling', async () => {
+      poller = new Poller({
+        intervalMilliseconds: 5000,
+        kubeClient: kubeClientMock,
+        logger: loggerMock,
+        externalSecret: fakeExternalSecret,
+        customResourceManifest: fakeCustomResourceManifest,
+        // Disable polling!
+        pollingDisabled: true
+      })
+
+      poller._setNextPoll = sinon.stub()
+
+      await poller._scheduleNextPoll()
+
+      expect(externalSecretsApiMock.status.get.calledWith()).to.equal(true)
+      sinon.assert.notCalled(poller._setNextPoll)
+    })
+
     it('logs error if it fails', async () => {
       const error = new Error('something boom')
       externalSecretsApiMock.status.get = sinon.stub().throws(error)