Repository: godaddy/kubernetes-external-secrets
Pull request #13 (2019-02-18 19:35:07)

feat(json): support JSON objects in AWS Secret Manager
This is useful (required?) to safely rotate secrets atomically, like a client
certificate and private key.


Patch:
diff --git a/README.md b/README.md
index a935096..d807de2 100644
--- a/README.md
+++ b/README.md
@@ -35,7 +35,13 @@ This create all the necessary resources and a `Deployment` in the `kubernetes-ex
 
 ### Add a secret
 
-Add secret data in your external provider (e.g., `hello-service/password=1234` in AWS Secrets Manager), then create a `hello-service-external-secret.yml` file:
+Add your secret data to your backend. For example, AWS Secrets Manager:
+
+```
+aws secretsmanager create-secret --name hello-service/password --secret-string "1234"
+```
+
+and then create a `hello-service-external-secret.yml` file:
 
 ```yml
 apiVersion: 'kubernetes-client.io/v1'
@@ -73,7 +79,43 @@ data:
   password: MTIzNA==
 ```
 
-Currently we only support AWS Secrets Manager external provider.
+## Backends
+
+kubernetes-external-secrets support only AWS Secrets Manager.
+
+### AWS Secrets Manager
+
+kubernetes-external-secrets supports both JSON objects ("Secret
+key/value" in the AWS console) or strings ("Plaintext" in the AWS
+console). Using JSON objects is useful when you need to atomically
+update multiple values. For example, when rotating a client
+certificate and private key.
+
+When writing an ExternalSecret for a JSON object you must specify the
+properties to use. For example, if we add our hello-service
+credentials as a single JSON object:
+
+```
+aws secretsmanager create-secret --region us-west-2 --name hello-service/credentials --secret-string '{"username":"admin","password":"1234"}'
+```
+
+We can declare which properties we want from hello-service/credentials:
+
+```yml
+apiVersion: 'kubernetes-client.io/v1'
+kind: ExternalSecret
+metadata:
+  name: hello-service
+secretDescriptor:
+  backendType: secretManager
+  properties:
+    - key: hello-service/credentials
+      name: password
+      property: password
+    - key: hello-service/credentials
+      name: username
+      property: username
+```
 
 ## Development
 
diff --git a/lib/backends/kv-backend.js b/lib/backends/kv-backend.js
index 47761c6..10f8ef7 100644
--- a/lib/backends/kv-backend.js
+++ b/lib/backends/kv-backend.js
@@ -16,14 +16,21 @@ class KVBackend extends AbstractBackend {
   /**
    * Fetch Kubernetes secret property values.
    * @param {Object[]} secretProperties - Kubernetes secret properties.
-   * @param {string} secretProperties[].key - Kubernetes secret property key.
-   * @param {string} secretProperties[].name - Kubernetes secret property name.
+   * @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({ secretProperties }) {
-    return Promise.all(secretProperties.map(secretProperty => {
+    return Promise.all(secretProperties.map(async secretProperty => {
       this._logger.info(`fetching secret property ${secretProperty.name}`);
-      return this._get({ secretKey: secretProperty.key });
+      const value = await this._get({ secretKey: secretProperty.key });
+
+      if ('property' in secretProperty) {
+        return value[secretProperty.property];
+      }
+      return value;
     }));
   }
 
diff --git a/lib/backends/secrets-manager-backend.js b/lib/backends/secrets-manager-backend.js
index a76ef17..a1baa74 100644
--- a/lib/backends/secrets-manager-backend.js
+++ b/lib/backends/secrets-manager-backend.js
@@ -24,9 +24,12 @@ class SecretsManagerBackend extends KVBackend {
       .getSecretValue({ SecretId: secretKey })
       .promise();
 
-    // NOTE(jdaeli): data.SecretString can also be valid key/value serialized object
-    // but for compatibility with System Manager, we store a single string value
-    return data.SecretString;
+    const secretValue = data.SecretString;
+    try {
+      return JSON.parse(secretValue);
+    } catch (err) {
+      return secretValue;
+    }
   }
 }
 
diff --git a/lib/poller.js b/lib/poller.js
index e1f7937..1422c16 100644
--- a/lib/poller.js
+++ b/lib/poller.js
@@ -6,8 +6,10 @@
  * @param {string} backendType - Backend to use for fetching secret data.
  * @param {string} name - Kubernetes secret name.
  * @param {Object[]} properties - Kubernetes secret properties.
- * @param {string} properties[].key - Kubernetes secret property key.
- * @param {string} properties[].name - Kubernetes secret property name.
+ * @param {string} properties[].key - Secret key in the backend.
+ * @param {string} properties[].name - Kubernetes Secret property name.
+ * @param {string} properties[].property - If the backend secret is an
+ *   object, this is the property name of the value to use.
  */
 
 /** Poller class. */


Test patch:
diff --git a/lib/backends/kv-backend.test.js b/lib/backends/kv-backend.test.js
index e61094d..9534c01 100644
--- a/lib/backends/kv-backend.test.js
+++ b/lib/backends/kv-backend.test.js
@@ -24,6 +24,18 @@ describe('SecretsManagerBackend', () => {
       kvBackend._get = sinon.stub();
     });
 
+    it('handles secrets values that are objects', async () => {
+      kvBackend._get.onFirstCall().resolves({ foo: 'bar' });
+      const secretPropertyValues = await kvBackend._fetchSecretPropertyValues({
+        secretProperties: [{
+          key: 'mocked-key',
+          name: 'mocked-name',
+          property: 'foo'
+        }]
+      });
+      expect(secretPropertyValues).to.deep.equal(['bar']);
+    });
+
     it('fetches secret property values', async () => {
       kvBackend._get.onFirstCall().resolves('fakePropertyValue1');
       kvBackend._get.onSecondCall().resolves('fakePropertyValue2');