Repository: buildbot/buildbot

Issue:
SecretInVault secret provider integration tests no longer work
The test `buildbot.test.integration.test_integration_secrets_with_vault.SecretsConfig.test_secret` no longer works.

See https://travis-ci.org/buildbot/buildbot/jobs/464401540.

Looks like the default kv engine shipping with the `vault` engine is now v2 which we don't support yet.




Discussion:

Resolving patch:
diff --git a/master/buildbot/secrets/providers/vault.py b/master/buildbot/secrets/providers/vault.py
--- a/master/buildbot/secrets/providers/vault.py
+++ b/master/buildbot/secrets/providers/vault.py
@@ -28,25 +28,30 @@
 
 class HashiCorpVaultSecretProvider(SecretProviderBase):
     """
-    basic provider where each secret is stored in Vault
+    basic provider where each secret is stored in Vault KV secret engine
     """
 
     name = 'SecretInVault'
 
-    def checkConfig(self, vaultServer=None, vaultToken=None, secretsmount=None):
+    def checkConfig(self, vaultServer=None, vaultToken=None, secretsmount=None,
+                    apiVersion=1):
         if not isinstance(vaultServer, str):
             config.error("vaultServer must be a string while it is %s" % (type(vaultServer,)))
         if not isinstance(vaultToken, str):
             config.error("vaultToken must be a string while it is %s" % (type(vaultToken,)))
+        if apiVersion not in [1, 2]:
+            config.error("apiVersion %s is not supported" % apiVersion)
 
     @defer.inlineCallbacks
-    def reconfigService(self, vaultServer=None, vaultToken=None, secretsmount=None):
+    def reconfigService(self, vaultServer=None, vaultToken=None, secretsmount=None,
+                        apiVersion=1):
         if secretsmount is None:
             self.secretsmount = "secret"
         else:
             self.secretsmount = secretsmount
         self.vaultServer = vaultServer
         self.vaultToken = vaultToken
+        self.apiVersion = apiVersion
         if vaultServer.endswith('/'):
             vaultServer = vaultServer[:-1]
         self._http = yield httpclientservice.HTTPClientService.getService(
@@ -57,11 +62,23 @@ def get(self, entry):
         """
         get the value from vault secret backend
         """
-        path = self.secretsmount + '/' + entry
+        if self.apiVersion == 1:
+            path = self.secretsmount + '/' + entry
+        else:
+            path = self.secretsmount + '/data/' + entry
+
+        # note that the HTTP path contains v1 for both versions of the key-value
+        # secret engine. Different versions of the key-value engine are
+        # effectively separate secret engines in vault, with the same base HTTP
+        # API, but with different paths within it.
         proj = yield self._http.get('/v1/{0}'.format(path))
         code = yield proj.code
         if code != 200:
             raise KeyError("The key %s does not exist in Vault provider: request"
                            " return code:%d." % (entry, code))
         json = yield proj.json()
-        defer.returnValue(json.get(u'data', {}).get('value'))
+        if self.apiVersion == 1:
+            ret = json.get(u'data', {}).get('value')
+        else:
+            ret = json.get(u'data', {}).get(u'data', {}).get('value')
+        defer.returnValue(ret)


Test patch:
diff --git a/master/buildbot/test/integration/test_integration_secrets_with_vault.py b/master/buildbot/test/integration/test_integration_secrets_with_vault.py
--- a/master/buildbot/test/integration/test_integration_secrets_with_vault.py
+++ b/master/buildbot/test/integration/test_integration_secrets_with_vault.py
@@ -38,12 +38,15 @@ def setUp(self):
         if rv != 0:
             raise SkipTest(
                 "Vault integration need docker environment to be setup")
+
+        self.addCleanup(self.remove_container)
+
         rv = os.system("docker exec vault_for_buildbot /bin/sh -c "
                        "'export VAULT_ADDR=http://127.0.0.1:8200/\n"
-                       "vault write secret/key value=word'")
+                       "vault kv put secret/key value=word'")
         self.assertEqual(rv, 0)
 
-    def tearDown(self):
+    def remove_container(self):
         os.system("docker rm -f vault_for_buildbot")
 
     @defer.inlineCallbacks
@@ -66,9 +69,12 @@ def masterConfig():
             name="force",
             builderNames=["testy"])]
 
+    # note that as of December 2018, the vault docker image default to kv
+    # version 2 to be enabled by default
     c['secretsProviders'] = [HashiCorpVaultSecretProvider(
         vaultToken='my_vaulttoken',
-        vaultServer="http://localhost:8200"
+        vaultServer="http://localhost:8200",
+        apiVersion=2
     )]
 
     f = BuildFactory()
diff --git a/master/buildbot/test/unit/test_secret_in_vault.py b/master/buildbot/test/unit/test_secret_in_vault.py
--- a/master/buildbot/test/unit/test_secret_in_vault.py
+++ b/master/buildbot/test/unit/test_secret_in_vault.py
@@ -25,11 +25,12 @@
 from buildbot.test.util.config import ConfigErrorsMixin
 
 
-class TestSecretInVaultHttpFake(ConfigErrorsMixin, unittest.TestCase):
+class TestSecretInVaultHttpFakeBase(ConfigErrorsMixin, unittest.TestCase):
 
-    def setUp(self):
+    def setUp(self, version):
         self.srvcVault = HashiCorpVaultSecretProvider(vaultServer="http://vaultServer",
-                                                      vaultToken="someToken")
+                                                      vaultToken="someToken",
+                                                      apiVersion=version)
         self.master = fakemaster.make_master(testcase=self, wantData=True)
         self._http = self.successResultOf(
             fakehttpclientservice.HTTPClientService.getFakeService(
@@ -41,6 +42,12 @@ def setUp(self):
     def tearDown(self):
         yield self.srvcVault.stopService()
 
+
+class TestSecretInVaultV1(TestSecretInVaultHttpFakeBase):
+
+    def setUp(self):
+        TestSecretInVaultHttpFakeBase.setUp(self, version=1)
+
     @defer.inlineCallbacks
     def testGetValue(self):
         self._http.expect(method='get', ep='/v1/secret/value', params=None,
@@ -79,6 +86,13 @@ def testCheckConfigErrorSecretInVaultServiceWrongServerAddress(self):
                 "vaultToken must be a string while it is"):
             self.srvcVault.checkConfig(vaultServer="serveraddr")
 
+    def test_check_config_error_apiVersion_unsupported(self):
+        with self.assertRaisesConfigError(
+                "apiVersion 0 is not supported"):
+            self.srvcVault.checkConfig(vaultServer="serveraddr",
+                                       vaultToken="vaultToken",
+                                       apiVersion=0)
+
     @defer.inlineCallbacks
     def testReconfigSecretInVaultService(self):
         self._http = self.successResultOf(
@@ -88,3 +102,32 @@ def testReconfigSecretInVaultService(self):
                                              vaultToken="someToken")
         self.assertEqual(self.srvcVault.vaultServer, "serveraddr")
         self.assertEqual(self.srvcVault.vaultToken, "someToken")
+
+
+class TestSecretInVaultV2(TestSecretInVaultHttpFakeBase):
+
+    def setUp(self):
+        TestSecretInVaultHttpFakeBase.setUp(self, version=2)
+
+    @defer.inlineCallbacks
+    def testGetValue(self):
+        self._http.expect(method='get', ep='/v1/secret/data/value', params=None,
+                          data=None, json=None, code=200,
+                          content_json={"data": {"data": {"value": "value1"}}})
+        value = yield self.srvcVault.get("value")
+        self.assertEqual(value, "value1")
+
+    @defer.inlineCallbacks
+    def testGetValueNotFound(self):
+        self._http.expect(method='get', ep='/v1/secret/data/value', params=None,
+                          data=None, json=None, code=200,
+                          content_json={"data": {"data": {"valueNotFound": "value1"}}})
+        value = yield self.srvcVault.get("value")
+        self.assertEqual(value, None)
+
+    @defer.inlineCallbacks
+    def testGetError(self):
+        self._http.expect(method='get', ep='/v1/secret/data/valueNotFound', params=None,
+                          data=None, json=None, code=404,
+                          content_json={"data": {"data": {"valueNotFound": "value1"}}})
+        yield self.assertFailure(self.srvcVault.get("valueNotFound"), KeyError)