Repository: godaddy/kubernetes-external-secrets
Pull request #475 (2020-09-01 07:28:36)

feat(aws): add region support to ssm and sm
This PR adds multi-region support for ssm and sm #354

I feel like we should (somehow) cache & reuse the ssm/sm `client` object so we don't assume a role in each `_get` call. E.g. by using the hash of the `ExternalSecret` or `${namespace}/${name}/${metadata.generation}` as cache key for the client object.

moolen: @silasbw do you have some time to look into this? 
Flydiverny: Yo! I merged #454 which caused a bunch of conflicts here.
I think caching client is probably good, we just merged a PR for vault doing something similar. 
moolen: Thanks for the update @Flydiverny, i rebased my changes.

Patch:
diff --git a/README.md b/README.md
index e21f6d2..7d0b0a5 100644
--- a/README.md
+++ b/README.md
@@ -285,6 +285,8 @@ spec:
   backendType: secretsManager
   # optional: specify role to assume when retrieving the data
   roleArn: arn:aws:iam::123456789012:role/test-role
+  # optional: specify region
+  region: us-east-1
   data:
     - key: hello-service/credentials
       name: password
@@ -315,6 +317,8 @@ spec:
   backendType: secretsManager
   # optional: specify role to assume when retrieving the data
   roleArn: arn:aws:iam::123456789012:role/test-role
+  # optional: specify region
+  region: us-east-1
   dataFrom:
     - hello-service/credentials
 ```
@@ -330,6 +334,8 @@ spec:
   backendType: secretsManager
   # optional: specify role to assume when retrieving the data
   roleArn: arn:aws:iam::123456789012:role/test-role
+  # optional: specify region
+  region: us-east-1
   dataFrom:
     - hello-service/credentials
   data:
diff --git a/config/index.js b/config/index.js
index c9f5d7b..d047095 100644
--- a/config/index.js
+++ b/config/index.js
@@ -83,6 +83,7 @@ const vaultFactory = () => vault(vaultOptions)
 // expires and with at least one remaining poll opportunty to retry renewal if it fails.
 const vaultTokenRenewThreshold = envConfig.vaultTokenRenewThreshold
   ? Number(envConfig.vaultTokenRenewThreshold) : 3 * envConfig.pollerIntervalMilliseconds / 1000
+
 const vaultBackend = new VaultBackend({
   vaultFactory: vaultFactory,
   tokenRenewThreshold: vaultTokenRenewThreshold,
diff --git a/examples/secretsmanager-example.yaml b/examples/secretsmanager-example.yaml
index 46f1b05..b79ef39 100644
--- a/examples/secretsmanager-example.yaml
+++ b/examples/secretsmanager-example.yaml
@@ -6,6 +6,8 @@ spec:
   backendType: secretsManager
   # optional: specify role to assume when retrieving the data
   roleArn: arn:aws:iam::123412341234:role/let-other-account-access-secrets
+  # optional: specify region of the secret
+  region: eu-west-1
   data:
     - key: demo-service/credentials
       name: password
diff --git a/examples/ssm-example.yaml b/examples/ssm-example.yaml
index be62b90..112bab0 100644
--- a/examples/ssm-example.yaml
+++ b/examples/ssm-example.yaml
@@ -6,6 +6,8 @@ spec:
   backendType: systemManager
   # optional: specify role to assume when retrieving the data
   roleArn: arn:aws:iam::123456789012:role/test-role
+  # optional: specify region
+  region: eu-west-1
   data:
     - key: /foo/name1
       name: variable-name
diff --git a/lib/backends/secrets-manager-backend.js b/lib/backends/secrets-manager-backend.js
index 61f75a3..4c1d88e 100644
--- a/lib/backends/secrets-manager-backend.js
+++ b/lib/backends/secrets-manager-backend.js
@@ -26,20 +26,30 @@ class SecretsManagerBackend extends KVBackend {
    * @param {string} specOptions.roleArn - IAM role arn to assume
    * @returns {Promise} Promise object representing secret property value.
    */
-  async _get ({ key, specOptions: { roleArn }, keyOptions: { versionStage = 'AWSCURRENT', versionId = null } }) {
-    this._logger.info(`fetching secret property ${key} with role: ${roleArn || 'pods role'}`)
+  async _get ({ key, specOptions: { roleArn, region }, keyOptions: { versionStage = 'AWSCURRENT', versionId = null } }) {
+    this._logger.info(`fetching secret property ${key} with role: ${roleArn || 'pods role'} in region ${region}`)
 
     let client = this._client
+    let factoryArgs = null
     if (roleArn) {
       const credentials = this._assumeRole({
         RoleArn: roleArn,
         RoleSessionName: 'k8s-external-secrets'
       })
-      client = this._clientFactory({
+      factoryArgs = {
+        ...factoryArgs,
         credentials
-      })
+      }
+    }
+    if (region) {
+      factoryArgs = {
+        ...factoryArgs,
+        region
+      }
+    }
+    if (factoryArgs) {
+      client = this._clientFactory(factoryArgs)
     }
-
     let params
     if (versionId) {
       params = { SecretId: key, VersionId: versionId }
diff --git a/lib/backends/system-manager-backend.js b/lib/backends/system-manager-backend.js
index 778cb58..63b8b80 100644
--- a/lib/backends/system-manager-backend.js
+++ b/lib/backends/system-manager-backend.js
@@ -19,23 +19,33 @@ class SystemManagerBackend extends KVBackend {
   /**
    * Get secret property value from 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 ({ key, specOptions: { roleArn } }) {
-    this._logger.info(`fetching secret property ${key} with role: ${roleArn || 'pods role'}`)
+  async _get ({ key, specOptions: { roleArn, region } }) {
+    this._logger.info(`fetching secret property ${key} with role: ${roleArn || 'pods role'} in region ${region}`)
 
     let client = this._client
+    let factoryArgs = null
     if (roleArn) {
       const credentials = this._assumeRole({
         RoleArn: roleArn,
         RoleSessionName: 'k8s-external-secrets'
       })
-      client = this._clientFactory({
+      factoryArgs = {
+        ...factoryArgs,
         credentials
-      })
+      }
+    }
+    if (region) {
+      factoryArgs = {
+        ...factoryArgs,
+        region
+      }
+    }
+    if (factoryArgs) {
+      client = this._clientFactory(factoryArgs)
     }
     try {
       const data = await client
diff --git a/lib/backends/vault-backend.js b/lib/backends/vault-backend.js
index 4674bc3..73cff2c 100644
--- a/lib/backends/vault-backend.js
+++ b/lib/backends/vault-backend.js
@@ -53,6 +53,29 @@ class VaultBackend extends KVBackend {
       this._clients.set(clientCacheKey, client)
     }
 
+    // If we already have a cached token then inspect it...
+    if (client.token) {
+      try {
+        this._logger.debug(`checking vault token expiry for role ${vaultRoleGet} on ${vaultMountPointGet}`)
+        const tokenStatus = await client.tokenLookupSelf()
+        this._logger.debug(`vault token (role ${vaultRoleGet} on ${vaultMountPointGet}) valid for ${tokenStatus.data.ttl} seconds, renews at ${this._tokenRenewThreshold}`)
+
+        // If it needs renewing, renew it.
+        if (Number(tokenStatus.data.ttl) <= this._tokenRenewThreshold) {
+          this._logger.debug(`renewing role ${vaultRoleGet} on ${vaultMountPointGet} vault token`)
+          if (!(await client.tokenRenewSelf())) {
+            this._logger.debug(`cached token renewal failed.  Clearing cached token for role ${vaultRoleGet} on ${vaultMountPointGet}`)
+            client.token = null
+          }
+        }
+      } catch {
+        // If it can't be inspected/renewed, we clear the token.
+        this._logger.debug(`cached token operation failed.  Clearing cached token for role ${vaultRoleGet} on ${vaultMountPointGet}`)
+        client.token = null
+      }
+    }
+
+    // If we don't have a token here we either never had one or we just failed to renew it, so get a new one by logging-in
     if (!client.token) {
       const jwt = this._fetchServiceAccountToken()
       this._logger.debug(`fetching new token from vault for role ${vaultRoleGet} on ${vaultMountPointGet}`)
@@ -61,15 +84,6 @@ class VaultBackend extends KVBackend {
         role: vaultRoleGet,
         jwt: jwt
       })
-    } else {
-      this._logger.debug(`checking vault token expiry for role ${vaultRoleGet} on ${vaultMountPointGet}`)
-      const tokenStatus = await client.tokenLookupSelf()
-      this._logger.debug(`vault token (role ${vaultRoleGet} on ${vaultMountPointGet}) valid for ${tokenStatus.data.ttl} seconds, renews at ${this._tokenRenewThreshold}`)
-
-      if (Number(tokenStatus.data.ttl) <= this._tokenRenewThreshold) {
-        this._logger.debug(`renewing role ${vaultRoleGet} on ${vaultMountPointGet} vault token`)
-        await client.tokenRenewSelf()
-      }
     }
 
     this._logger.debug(`reading secret key ${key} from vault`)


Test patch:
diff --git a/e2e/tests/secrets-manager.test.js b/e2e/tests/secrets-manager.test.js
index 043ee63..684bfb0 100644
--- a/e2e/tests/secrets-manager.test.js
+++ b/e2e/tests/secrets-manager.test.js
@@ -124,6 +124,70 @@ describe('secretsmanager', async () => {
     expect(secret.body.type).to.equal('kubernetes.io/tls')
   })
 
+  it('should pull existing secret from secretsmanager in the correct region', async () => {
+    const smEU = awsConfig.secretsManagerFactory({
+      region: 'eu-west-1'
+    })
+    const createSecret = util.promisify(smEU.createSecret).bind(smEU)
+    const putSecretValue = util.promisify(smEU.putSecretValue).bind(smEU)
+
+    let result = await createSecret({
+      Name: `e2e/${uuid}/x-region-credentials`,
+      SecretString: '{"username":"foo","password":"bar"}'
+    }).catch(err => {
+      expect(err).to.equal(null)
+    })
+
+    result = await kubeClient
+      .apis[customResourceManifest.spec.group]
+      .v1.namespaces('default')[customResourceManifest.spec.names.plural]
+      .post({
+        body: {
+          apiVersion: 'kubernetes-client.io/v1',
+          kind: 'ExternalSecret',
+          metadata: {
+            name: `e2e-secretmanager-x-region-${uuid}`
+          },
+          spec: {
+            backendType: 'secretsManager',
+            region: 'eu-west-1',
+            data: [
+              {
+                key: `e2e/${uuid}/x-region-credentials`,
+                property: 'password',
+                name: 'password'
+              },
+              {
+                key: `e2e/${uuid}/x-region-credentials`,
+                property: 'username',
+                name: 'username'
+              }
+            ]
+          }
+        }
+      })
+
+    expect(result).to.not.equal(undefined)
+    expect(result.statusCode).to.equal(201)
+
+    let secret = await waitForSecret('default', `e2e-secretmanager-x-region-${uuid}`)
+    expect(secret).to.not.equal(undefined)
+    expect(secret.body.data.username).to.equal('Zm9v')
+    expect(secret.body.data.password).to.equal('YmFy')
+
+    // update the secret value
+    result = await putSecretValue({
+      SecretId: `e2e/${uuid}/x-region-credentials`,
+      SecretString: '{"username":"your mom","password":"1234"}'
+    }).catch(err => {
+      expect(err).to.equal(null)
+    })
+    await delay(2000)
+    secret = await waitForSecret('default', `e2e-secretmanager-x-region-${uuid}`)
+    expect(secret.body.data.username).to.equal('eW91ciBtb20=')
+    expect(secret.body.data.password).to.equal('MTIzNA==')
+  })
+
   describe('permitted annotation', async () => {
     beforeEach(async () => {
       await kubeClient.api.v1.namespaces('default').patch({
diff --git a/e2e/tests/ssm.test.js b/e2e/tests/ssm.test.js
index a2bc137..23b76f7 100644
--- a/e2e/tests/ssm.test.js
+++ b/e2e/tests/ssm.test.js
@@ -53,6 +53,50 @@ describe('ssm', async () => {
     expect(secret.body.data.name).to.equal('Zm9v')
   })
 
+  it('should pull existing secret from ssm in a different region', async () => {
+    const ssmEU = awsConfig.systemManagerFactory({
+      region: 'eu-west-1'
+    })
+    const putParameter = util.promisify(ssmEU.putParameter).bind(ssmEU)
+
+    let result = await putParameter({
+      Name: `/e2e/${uuid}/x-region`,
+      Type: 'String',
+      Value: 'foo'
+    }).catch(err => {
+      expect(err).to.equal(null)
+    })
+
+    result = await kubeClient
+      .apis[customResourceManifest.spec.group]
+      .v1.namespaces('default')[customResourceManifest.spec.names.plural]
+      .post({
+        body: {
+          apiVersion: 'kubernetes-client.io/v1',
+          kind: 'ExternalSecret',
+          metadata: {
+            name: `e2e-ssm-xregion-${uuid}`
+          },
+          spec: {
+            backendType: 'systemManager',
+            region: 'eu-west-1',
+            data: [
+              {
+                key: `/e2e/${uuid}/x-region`,
+                name: 'name'
+              }
+            ]
+          }
+        }
+      })
+
+    expect(result).to.not.equal(undefined)
+    expect(result.statusCode).to.equal(201)
+
+    const secret = await waitForSecret('default', `e2e-ssm-xregion-${uuid}`)
+    expect(secret.body.data.name).to.equal('Zm9v')
+  })
+
   describe('permitted annotation', async () => {
     beforeEach(async () => {
       await kubeClient.api.v1.namespaces('default').patch({
diff --git a/lib/backends/kv-backend.test.js b/lib/backends/kv-backend.test.js
index 39b01ed..6cb0843 100644
--- a/lib/backends/kv-backend.test.js
+++ b/lib/backends/kv-backend.test.js
@@ -296,7 +296,7 @@ describe('kv-backend', () => {
       expect(manifestData).deep.equals({})
     })
 
-    it('makes correct calls - with data and role', async () => {
+    it('makes correct calls - with data, role and region', async () => {
       await kvBackend.getSecretManifestData({
         spec: {
           data: [
@@ -308,7 +308,8 @@ describe('kv-backend', () => {
               name: 'fakePropertyName2'
             }
           ],
-          roleArn: 'my-role'
+          roleArn: 'my-role',
+          region: 'my-region'
         }
       })
 
@@ -320,12 +321,18 @@ describe('kv-backend', () => {
           key: 'fakePropertyKey2',
           name: 'fakePropertyName2'
         }],
-        specOptions: { roleArn: 'my-role' }
+        specOptions: {
+          roleArn: 'my-role',
+          region: 'my-region'
+        }
       })).to.equal(true)
 
       expect(kvBackend._fetchDataFromValues.calledWith({
         dataFrom: [],
-        specOptions: { roleArn: 'my-role' }
+        specOptions: {
+          roleArn: 'my-role',
+          region: 'my-region'
+        }
       })).to.equal(true)
     })
 
diff --git a/lib/backends/secrets-manager-backend.test.js b/lib/backends/secrets-manager-backend.test.js
index 2bd0279..023b0b3 100644
--- a/lib/backends/secrets-manager-backend.test.js
+++ b/lib/backends/secrets-manager-backend.test.js
@@ -81,19 +81,23 @@ describe('SecretsManagerBackend', () => {
       expect(secretPropertyValue.toString()).equals('fakeSecretPropertyValue')
     })
 
-    it('returns secret property value assuming a role', async () => {
+    it('returns secret property value assuming a role with region', async () => {
       getSecretValuePromise.promise.resolves({
         SecretString: 'fakeAssumeRoleSecretValue'
       })
 
       const secretPropertyValue = await secretsManagerBackend._get({
         key: 'fakeSecretKey',
-        specOptions: { roleArn: 'my-role' },
+        specOptions: {
+          roleArn: 'my-role',
+          region: 'foo-bar-baz'
+        },
         keyOptions
       })
 
       expect(clientFactoryMock.lastArg).deep.equals({
-        credentials: assumeRoleCredentials
+        credentials: assumeRoleCredentials,
+        region: 'foo-bar-baz'
       })
       expect(clientMock.getSecretValue.calledWith({
         SecretId: 'fakeSecretKey',
@@ -101,12 +105,39 @@ describe('SecretsManagerBackend', () => {
       })).to.equal(true)
       expect(clientFactoryMock.getCall(0).args).deep.equals([])
       expect(clientFactoryMock.getCall(1).args).deep.equals([{
-        credentials: assumeRoleCredentials
+        credentials: assumeRoleCredentials,
+        region: 'foo-bar-baz'
       }])
       expect(assumeRoleMock.callCount).equals(1)
       expect(secretPropertyValue).equals('fakeAssumeRoleSecretValue')
     })
 
+    it('returns secret property value from specific region', async () => {
+      getSecretValuePromise.promise.resolves({
+        SecretString: 'fakeAssumeRoleSecretValue'
+      })
+
+      const secretPropertyValue = await secretsManagerBackend._get({
+        key: 'fakeSecretKey',
+        specOptions: { region: 'my-region' },
+        keyOptions
+      })
+
+      expect(clientFactoryMock.lastArg).deep.equals({
+        region: 'my-region'
+      })
+      expect(clientMock.getSecretValue.calledWith({
+        SecretId: 'fakeSecretKey',
+        VersionStage: 'AWSCURRENT'
+      })).to.equal(true)
+      expect(clientFactoryMock.getCall(0).args).deep.equals([])
+      expect(clientFactoryMock.getCall(1).args).deep.equals([{
+        region: 'my-region'
+      }])
+      expect(assumeRoleMock.callCount).equals(0)
+      expect(secretPropertyValue).equals('fakeAssumeRoleSecretValue')
+    })
+
     it('returns secret property value with versionStage', async () => {
       getSecretValuePromise.promise.resolves({
         SecretString: 'fakeSecretPropertyValuePreviousVersion'
diff --git a/lib/backends/system-manager-backend.test.js b/lib/backends/system-manager-backend.test.js
index aa86b84..500e3b1 100644
--- a/lib/backends/system-manager-backend.test.js
+++ b/lib/backends/system-manager-backend.test.js
@@ -63,7 +63,7 @@ describe('SystemManagerBackend', () => {
       expect(secretPropertyValue).equals('fakeSecretPropertyValue')
     })
 
-    it('returns secret property value assuming a role', async () => {
+    it('returns secret property value assuming a role with region', async () => {
       getParameterPromise.promise.resolves({
         Parameter: {
           Value: 'fakeAssumeRoleSecretValue'
@@ -72,10 +72,43 @@ describe('SystemManagerBackend', () => {
 
       const secretPropertyValue = await systemManagerBackend._get({
         key: 'fakeSecretKey',
-        specOptions: { roleArn: 'my-role' }
+        specOptions: {
+          roleArn: 'my-role',
+          region: 'my-region'
+        }
+      })
+      expect(clientFactoryMock.lastArg).deep.equals({
+        credentials: assumeRoleCredentials,
+        region: 'my-region'
+      })
+      expect(clientMock.getParameter.calledWith({
+        Name: 'fakeSecretKey',
+        WithDecryption: true
+      })).to.equal(true)
+      expect(clientFactoryMock.getCall(0).args).deep.equals([])
+      expect(clientFactoryMock.getCall(1).args).deep.equals([{
+        credentials: assumeRoleCredentials,
+        region: 'my-region'
+      }])
+      expect(assumeRoleMock.callCount).equals(1)
+      expect(secretPropertyValue).equals('fakeAssumeRoleSecretValue')
+    })
+
+    it('returns secret property value from specific region', async () => {
+      getParameterPromise.promise.resolves({
+        Parameter: {
+          Value: 'fakeAssumeRoleSecretValue'
+        }
+      })
+
+      const secretPropertyValue = await systemManagerBackend._get({
+        key: 'fakeSecretKey',
+        specOptions: {
+          region: 'my-region'
+        }
       })
       expect(clientFactoryMock.lastArg).deep.equals({
-        credentials: assumeRoleCredentials
+        region: 'my-region'
       })
       expect(clientMock.getParameter.calledWith({
         Name: 'fakeSecretKey',
@@ -83,9 +116,10 @@ describe('SystemManagerBackend', () => {
       })).to.equal(true)
       expect(clientFactoryMock.getCall(0).args).deep.equals([])
       expect(clientFactoryMock.getCall(1).args).deep.equals([{
-        credentials: assumeRoleCredentials
+        region: 'my-region'
       }])
       expect(secretPropertyValue).equals('fakeAssumeRoleSecretValue')
+      expect(assumeRoleMock.callCount).equals(0)
     })
 
     it('throws a meaningful message when the parameter does not exist', async () => {
diff --git a/lib/backends/vault-backend.test.js b/lib/backends/vault-backend.test.js
index 319fa7b..106a878 100644
--- a/lib/backends/vault-backend.test.js
+++ b/lib/backends/vault-backend.test.js
@@ -40,6 +40,7 @@ describe('VaultBackend', () => {
   }
 
   const vaultTokenRenewThreshold = 30
+
   const mockTokenLookupResultMustRenew = {
     data: {
       ttl: 15
@@ -213,6 +214,93 @@ describe('VaultBackend', () => {
       expect(secretPropertyValue).equals(quotedSecretValue)
     })
 
+    it('returns secret property value after failed renew token and then relogin', async () => {
+      clientMock.token = 'an-existing-token'
+      clientMock.tokenLookupSelf = sinon.stub().returns(mockTokenLookupResultMustRenew)
+      clientMock.tokenRenewSelf = sinon.stub().returns(false)
+
+      const secretPropertyValue = await vaultBackend._get({
+        specOptions: {
+          vaultMountPoint: mountPoint,
+          vaultRole: role
+        },
+        key: secretKey
+      })
+
+      // check the token happened ...
+      sinon.assert.calledOnce(clientMock.tokenLookupSelf)
+
+      // ... then try to renew the token ...
+      sinon.assert.calledOnce(clientMock.tokenRenewSelf)
+
+      // ... now relogin due to failure to renew ...
+      sinon.assert.calledOnce(clientMock.kubernetesLogin)
+
+      // ... then we fetch the secret ...
+      sinon.assert.calledWith(clientMock.read, secretKey)
+
+      // ... and expect to get its proper value
+      expect(secretPropertyValue).equals(quotedSecretValue)
+    })
+
+    it('returns secret property value after errored token inspection and then relogin', async () => {
+      clientMock.token = 'an-existing-token'
+      clientMock.tokenLookupSelf = sinon.stub().throws(new Error())
+      clientMock.tokenRenewSelf = sinon.stub().returns(false)
+
+      const secretPropertyValue = await vaultBackend._get({
+        specOptions: {
+          vaultMountPoint: mountPoint,
+          vaultRole: role
+        },
+        key: secretKey
+      })
+
+      // check the token happened ...
+      sinon.assert.calledOnce(clientMock.tokenLookupSelf)
+
+      // ... didn't try to renew the token ...
+      sinon.assert.notCalled(clientMock.tokenRenewSelf)
+
+      // ... but instead just relogin due to error upon inspection ...
+      sinon.assert.calledOnce(clientMock.kubernetesLogin)
+
+      // ... then we fetch the secret ...
+      sinon.assert.calledWith(clientMock.read, secretKey)
+
+      // ... and expect to get its proper value
+      expect(secretPropertyValue).equals(quotedSecretValue)
+    })
+
+    it('returns secret property value after errored token renewal and then relogin', async () => {
+      clientMock.token = 'an-existing-token'
+      clientMock.tokenLookupSelf = sinon.stub().returns(mockTokenLookupResultMustRenew)
+      clientMock.tokenRenewSelf = sinon.stub().throws(new Error())
+
+      const secretPropertyValue = await vaultBackend._get({
+        specOptions: {
+          vaultMountPoint: mountPoint,
+          vaultRole: role
+        },
+        key: secretKey
+      })
+
+      // check the token happened ...
+      sinon.assert.calledOnce(clientMock.tokenLookupSelf)
+
+      // ... didn't try to renew the token ...
+      sinon.assert.calledOnce(clientMock.tokenRenewSelf)
+
+      // ... but instead just relogin due to error upon renewal ...
+      sinon.assert.calledOnce(clientMock.kubernetesLogin)
+
+      // ... then we fetch the secret ...
+      sinon.assert.calledWith(clientMock.read, secretKey)
+
+      // ... and expect to get its proper value
+      expect(secretPropertyValue).equals(quotedSecretValue)
+    })
+
     it('returns secret property value if a token exists that does not need renewal', async () => {
       clientMock.token = 'an-existing-token'
       clientMock.tokenLookupSelf = sinon.stub().returns(mockTokenLookupResultNoRenew)