diff --git a/cdap-credential-ext-gcp-wi/src/main/java/io/cdap/cdap/security/spi/credential/GcpWorkloadIdentityCredentialProvider.java b/cdap-credential-ext-gcp-wi/src/main/java/io/cdap/cdap/security/spi/credential/GcpWorkloadIdentityCredentialProvider.java index ab380073b261..e03df737e920 100644 --- a/cdap-credential-ext-gcp-wi/src/main/java/io/cdap/cdap/security/spi/credential/GcpWorkloadIdentityCredentialProvider.java +++ b/cdap-credential-ext-gcp-wi/src/main/java/io/cdap/cdap/security/spi/credential/GcpWorkloadIdentityCredentialProvider.java @@ -19,6 +19,9 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Stopwatch; import com.google.common.base.Strings; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import io.cdap.cdap.api.retry.RetryableException; @@ -52,6 +55,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.Map; +import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.stream.Collectors; @@ -76,6 +80,8 @@ public class GcpWorkloadIdentityCredentialProvider implements CredentialProvider LoggerFactory.getLogger(GcpWorkloadIdentityCredentialProvider.class); private CredentialProviderContext credentialProviderContext; private ApiClient client; + private final LoadingCache credentialLoadingCache; static final String CONNECT_TIMEOUT_SECS = "k8s.api.client.connect.timeout.secs"; static final String CONNECT_TIMEOUT_SECS_DEFAULT = "120"; static final String READ_TIMEOUT_SECS = "k8s.api.client.read.timeout.secs"; @@ -95,6 +101,21 @@ public class GcpWorkloadIdentityCredentialProvider implements CredentialProvider private static final String PROVISIONING_FAILURE_ERROR_MESSAGE_FORMAT = "Failed to provision credential with identity '%s'"; + public GcpWorkloadIdentityCredentialProvider() { + this.credentialLoadingCache = CacheBuilder.newBuilder() + // Provisioned credential expire after 60mins, assuming 20% buffer in cache exp (0.8*60). + .expireAfterWrite(48, TimeUnit.MINUTES) + .build(new CacheLoader() { + @Override + public ProvisionedCredential load(ProvisionedCredentialCacheKey + provisionedCredentialCacheKey) throws Exception { + return getProvisionedCredential(provisionedCredentialCacheKey.getNamespaceMeta(), + provisionedCredentialCacheKey.getCredentialIdentity(), + provisionedCredentialCacheKey.getScopes()); + } + }); + } + @Override public String getName() { return NAME; @@ -155,8 +176,12 @@ public ProvisionedCredential provision(NamespaceMeta namespaceMeta, try { while (stopWatch.elapsed(TimeUnit.SECONDS) < timeout) { try { - return getProvisionedCredential(namespaceMeta, identity, scopes); - } catch (RetryableException e) { + return credentialLoadingCache.get(new ProvisionedCredentialCacheKey(namespaceMeta, + identity, scopes)); + } catch (ExecutionException e) { + if (!(e.getCause() instanceof RetryableException)) { + throw e; + } TimeUnit.MILLISECONDS.sleep(delay); delay = (long) (delay * (minMultiplier + Math.random() * (maxMultiplier - minMultiplier + 1))); @@ -171,7 +196,7 @@ public ProvisionedCredential provision(NamespaceMeta namespaceMeta, identity.getIdentity(), e.getMessage()), e); } } - } catch (InterruptedException e) { + } catch (InterruptedException | ExecutionException e) { throw new CredentialProvisioningException( String.format(PROVISIONING_FAILURE_ERROR_MESSAGE_FORMAT + ": %s", identity.getIdentity(), e.getMessage()), e); diff --git a/cdap-credential-ext-gcp-wi/src/main/java/io/cdap/cdap/security/spi/credential/ProvisionedCredentialCacheKey.java b/cdap-credential-ext-gcp-wi/src/main/java/io/cdap/cdap/security/spi/credential/ProvisionedCredentialCacheKey.java new file mode 100644 index 000000000000..65288c2ac5b7 --- /dev/null +++ b/cdap-credential-ext-gcp-wi/src/main/java/io/cdap/cdap/security/spi/credential/ProvisionedCredentialCacheKey.java @@ -0,0 +1,74 @@ +/* + * Copyright © 2023 Cask Data, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package io.cdap.cdap.security.spi.credential; + +import io.cdap.cdap.proto.NamespaceMeta; +import io.cdap.cdap.proto.credential.CredentialIdentity; +import java.util.Objects; + +/** + * Defines the contents of key used for + * caching {@link io.cdap.cdap.proto.credential.ProvisionedCredential}. + */ +public final class ProvisionedCredentialCacheKey { + private final NamespaceMeta namespaceMeta; + private final CredentialIdentity credentialIdentity; + private final String scopes; + private transient Integer hashCode; + + public ProvisionedCredentialCacheKey(NamespaceMeta namespaceMeta, + CredentialIdentity credentialIdentity, String scopes) { + this.namespaceMeta = namespaceMeta; + this.credentialIdentity = credentialIdentity; + this.scopes = scopes; + } + + public NamespaceMeta getNamespaceMeta() { + return namespaceMeta; + } + + public CredentialIdentity getCredentialIdentity() { + return credentialIdentity; + } + + public String getScopes() { + return scopes; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof ProvisionedCredentialCacheKey)) { + return false; + } + ProvisionedCredentialCacheKey that = (ProvisionedCredentialCacheKey) o; + return Objects.equals(namespaceMeta.getNamespaceId().getNamespace(), + that.namespaceMeta.getNamespaceId().getNamespace()) + && Objects.equals(credentialIdentity.getSecureValue(), + that.getCredentialIdentity().getSecureValue()) + && Objects.equals(scopes, that.scopes); + } + + @Override + public int hashCode() { + Integer hashCode = this.hashCode; + if (hashCode == null) { + this.hashCode = hashCode = Objects.hash(namespaceMeta.getNamespaceId().getNamespace(), + credentialIdentity.getSecureValue(), scopes); + } + return hashCode; + } +}