From b435992adab82b5480b36ea64b5632e88b72e1de Mon Sep 17 00:00:00 2001 From: GaneshSPatil Date: Mon, 15 Jul 2019 15:10:08 +0530 Subject: [PATCH] Add CredentialValidator to validate whether specified secret configuration is valid * Try connecting to the specified kubernetes secret, if failed, raise a validation error. --- .../kubernetes/KubernetesClientFactory.java | 22 +++---- .../kubernetes/KubernetesSecretsPlugin.java | 16 ++++- .../SecretConfigLookupExecutor.java | 3 +- .../validators/CredentialValidator.java | 34 ++++++++++- .../KubernetesSecretsPluginTest.java | 61 ++++++++++++++++++- src/test/resources/invalid-secret-config.json | 10 +++ .../resources/non-existing-secret-config.json | 6 ++ 7 files changed, 135 insertions(+), 17 deletions(-) create mode 100644 src/test/resources/invalid-secret-config.json create mode 100644 src/test/resources/non-existing-secret-config.json diff --git a/src/main/java/cd/go/contrib/secrets/kubernetes/KubernetesClientFactory.java b/src/main/java/cd/go/contrib/secrets/kubernetes/KubernetesClientFactory.java index a824a8d..d1f39f9 100644 --- a/src/main/java/cd/go/contrib/secrets/kubernetes/KubernetesClientFactory.java +++ b/src/main/java/cd/go/contrib/secrets/kubernetes/KubernetesClientFactory.java @@ -27,31 +27,31 @@ public class KubernetesClientFactory { private static final KubernetesClientFactory KUBERNETES_CLIENT_FACTORY = new KubernetesClientFactory(); private KubernetesClient client; - private SecretConfig pluginSettings; + private SecretConfig secretConfig; public static KubernetesClientFactory instance() { return KUBERNETES_CLIENT_FACTORY; } - public synchronized KubernetesClient client(SecretConfig pluginSettings) { - if (pluginSettings.equals(this.pluginSettings) && this.client != null) { + public synchronized KubernetesClient client(SecretConfig secretConfig) { + if (secretConfig.equals(this.secretConfig) && this.client != null) { LOG.debug("Using previously created client."); return this.client; } - LOG.debug(format("Creating a new client because {0}.", (client == null) ? "client is null" : "plugin setting is changed")); - this.pluginSettings = pluginSettings; - this.client = createClientFor(pluginSettings); + LOG.debug(format("Creating a new client because {0}.", (client == null) ? "client is null" : "secret configuration has changed")); + this.secretConfig = secretConfig; + this.client = createClientFor(secretConfig); LOG.debug("New client is created."); return this.client; } - private KubernetesClient createClientFor(SecretConfig pluginSettings) { + private KubernetesClient createClientFor(SecretConfig secretConfig) { final ConfigBuilder configBuilder = new ConfigBuilder() - .withOauthToken(pluginSettings.getSecurityToken()) - .withMasterUrl(pluginSettings.getClusterUrl()) - .withCaCertData(pluginSettings.getClusterCACertData()) - .withNamespace(pluginSettings.getNamespace()); + .withOauthToken(secretConfig.getSecurityToken()) + .withMasterUrl(secretConfig.getClusterUrl()) + .withCaCertData(secretConfig.getClusterCACertData()) + .withNamespace(secretConfig.getNamespace()); return new DefaultKubernetesClient(configBuilder.build()); } diff --git a/src/main/java/cd/go/contrib/secrets/kubernetes/KubernetesSecretsPlugin.java b/src/main/java/cd/go/contrib/secrets/kubernetes/KubernetesSecretsPlugin.java index 5849811..3d7b426 100644 --- a/src/main/java/cd/go/contrib/secrets/kubernetes/KubernetesSecretsPlugin.java +++ b/src/main/java/cd/go/contrib/secrets/kubernetes/KubernetesSecretsPlugin.java @@ -1,5 +1,7 @@ package cd.go.contrib.secrets.kubernetes; +import cd.go.contrib.secrets.kubernetes.models.SecretConfig; +import cd.go.contrib.secrets.kubernetes.validators.CredentialValidator; import com.github.bdpiparva.plugin.base.dispatcher.BaseBuilder; import com.github.bdpiparva.plugin.base.dispatcher.RequestDispatcher; import com.thoughtworks.go.plugin.api.GoApplicationAccessor; @@ -10,8 +12,6 @@ import com.thoughtworks.go.plugin.api.logging.Logger; import com.thoughtworks.go.plugin.api.request.GoPluginApiRequest; import com.thoughtworks.go.plugin.api.response.GoPluginApiResponse; -import cd.go.contrib.secrets.kubernetes.models.SecretConfig; -import cd.go.contrib.secrets.kubernetes.validators.CredentialValidator; import static java.util.Collections.singletonList; @@ -19,6 +19,16 @@ public class KubernetesSecretsPlugin implements GoPlugin { public static final Logger LOG = Logger.getLoggerFor(KubernetesSecretsPlugin.class); private RequestDispatcher requestDispatcher; + private final KubernetesClientFactory kubernetesClientFactory; + + public KubernetesSecretsPlugin() { + kubernetesClientFactory = KubernetesClientFactory.instance(); + } + + //used for tests + public KubernetesSecretsPlugin(KubernetesClientFactory factory) { + kubernetesClientFactory = factory; + } @Override public void initializeGoApplicationAccessor(GoApplicationAccessor goApplicationAccessor) { @@ -28,7 +38,7 @@ public void initializeGoApplicationAccessor(GoApplicationAccessor goApplicationA .icon("/kubernetes_logo.svg", "image/svg+xml") .configMetadata(SecretConfig.class) .configView("/secrets.template.html") - .validateSecretConfig(new CredentialValidator()) + .validateSecretConfig(new CredentialValidator(kubernetesClientFactory)) .lookup(new SecretConfigLookupExecutor()) .build(); } diff --git a/src/main/java/cd/go/contrib/secrets/kubernetes/SecretConfigLookupExecutor.java b/src/main/java/cd/go/contrib/secrets/kubernetes/SecretConfigLookupExecutor.java index 478cbfe..c10e3fc 100644 --- a/src/main/java/cd/go/contrib/secrets/kubernetes/SecretConfigLookupExecutor.java +++ b/src/main/java/cd/go/contrib/secrets/kubernetes/SecretConfigLookupExecutor.java @@ -13,6 +13,7 @@ import java.util.Base64; import java.util.List; +import static cd.go.contrib.secrets.kubernetes.KubernetesSecretsPlugin.LOG; import static com.github.bdpiparva.plugin.base.GsonTransformer.fromJson; import static com.github.bdpiparva.plugin.base.GsonTransformer.toJson; import static java.util.Collections.singletonMap; @@ -50,7 +51,7 @@ protected GoPluginApiResponse execute(SecretConfigRequest request) { return DefaultGoPluginApiResponse.success(toJson(secrets)); } catch (Exception e) { - LOGGER.error("Failed to lookup secret from Kubernetes Secret.", e); + LOG.error("Failed to lookup secret from Kubernetes Secret.", e); return DefaultGoPluginApiResponse.error(toJson(singletonMap("message", "Failed to lookup secrets from Kubernetes Secret. See logs for more information."))); } finally { client.close(); diff --git a/src/main/java/cd/go/contrib/secrets/kubernetes/validators/CredentialValidator.java b/src/main/java/cd/go/contrib/secrets/kubernetes/validators/CredentialValidator.java index 198b52d..7b46668 100644 --- a/src/main/java/cd/go/contrib/secrets/kubernetes/validators/CredentialValidator.java +++ b/src/main/java/cd/go/contrib/secrets/kubernetes/validators/CredentialValidator.java @@ -1,13 +1,45 @@ package cd.go.contrib.secrets.kubernetes.validators; +import cd.go.contrib.secrets.kubernetes.KubernetesClientFactory; +import cd.go.contrib.secrets.kubernetes.models.SecretConfig; import com.github.bdpiparva.plugin.base.validation.ValidationResult; import com.github.bdpiparva.plugin.base.validation.Validator; +import io.fabric8.kubernetes.api.model.Secret; +import io.fabric8.kubernetes.client.KubernetesClient; import java.util.Map; +import static com.github.bdpiparva.plugin.base.executors.Executor.GSON; +import static com.github.bdpiparva.plugin.base.executors.Executor.LOGGER; + public class CredentialValidator implements Validator { + private KubernetesClientFactory kubernetesClientFactory; + + public CredentialValidator(KubernetesClientFactory kubernetesClientFactory) { + this.kubernetesClientFactory = kubernetesClientFactory; + } + @Override public ValidationResult validate(Map requestBody) { - return new ValidationResult(); + ValidationResult validationResult = new ValidationResult(); + + SecretConfig secretConfig = GSON.fromJson(GSON.toJson(requestBody), SecretConfig.class); + KubernetesClient client = kubernetesClientFactory.client(secretConfig); + + try { + Secret secret = client.secrets().inNamespace(secretConfig.getNamespace()).withName(secretConfig.getSecretName()).get(); + if (secret == null) { + validationResult.add("kubernetes_secret_name", "Specified Kubernetes secret does not exists."); + } + } catch (Exception e) { + LOGGER.error("Failed to verify connection.", e); + String errorMessage = "Could not read specified secret. Either the connection with kubernetes cluster could not be established or the kubernetes secret does not exists."; + validationResult.add("kubernetes_secret_name", errorMessage); + validationResult.add("kubernetes_cluster_url", errorMessage); + } finally { + client.close(); + } + + return validationResult; } } diff --git a/src/test/java/cd/go/contrib/secrets/kubernetes/KubernetesSecretsPluginTest.java b/src/test/java/cd/go/contrib/secrets/kubernetes/KubernetesSecretsPluginTest.java index 3d88fd2..23fa427 100644 --- a/src/test/java/cd/go/contrib/secrets/kubernetes/KubernetesSecretsPluginTest.java +++ b/src/test/java/cd/go/contrib/secrets/kubernetes/KubernetesSecretsPluginTest.java @@ -23,11 +23,16 @@ import com.thoughtworks.go.plugin.api.exceptions.UnhandledRequestTypeException; import com.thoughtworks.go.plugin.api.request.DefaultGoPluginApiRequest; import com.thoughtworks.go.plugin.api.response.GoPluginApiResponse; +import io.fabric8.kubernetes.api.model.Secret; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.dsl.MixedOperation; +import io.fabric8.kubernetes.client.dsl.Resource; import org.json.JSONException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; +import org.mockito.Mock; import java.util.Map; @@ -35,16 +40,27 @@ import static com.github.bdpiparva.plugin.base.ResourceReader.readResourceBytes; import static java.util.Base64.getDecoder; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.mockito.MockitoAnnotations.initMocks; import static org.skyscreamer.jsonassert.JSONAssert.assertEquals; class KubernetesSecretsPluginTest { private KubernetesSecretsPlugin kubernetesSecretsPlugin; + @Mock + KubernetesClientFactory kubernetesClientFactory; + + @Mock + KubernetesClient kubernetesClient; + @BeforeEach void setUp() { - kubernetesSecretsPlugin = new KubernetesSecretsPlugin(); + initMocks(this); + kubernetesSecretsPlugin = new KubernetesSecretsPlugin(kubernetesClientFactory); kubernetesSecretsPlugin.initializeGoApplicationAccessor(mock(GoApplicationAccessor.class)); + when(kubernetesClientFactory.client(any())).thenReturn(kubernetesClient); } @Test @@ -97,9 +113,20 @@ void shouldReturnSecretConfigView() throws UnhandledRequestTypeException { class ValidateSecretConfig { private String requestName; + @Mock + private MixedOperation secrets; + @Mock + private Resource resource; + @BeforeEach void setUp() { + initMocks(this); requestName = "go.cd.secrets.secrets-config.validate"; + + when(kubernetesClient.secrets()).thenReturn(secrets); + when(secrets.inNamespace(any())).thenReturn(secrets); + when(secrets.withName(any())).thenReturn(resource); + when(resource.get()).thenReturn(new Secret()); } @ParameterizedTest @@ -143,6 +170,38 @@ void shouldPassIfRequestIsValid(String requestBody) throws JSONException, Unhand assertThat(response.responseCode()).isEqualTo(200); assertEquals("[]", response.responseBody(), true); } + + @ParameterizedTest + @JsonSource(jsonFiles = { + "/secret-config.json", + "/non-existing-secret-config.json" + }) + void shouldFailWhenNoSecretExistsWithTheSpecifiedName(String requestBody, String expected) throws UnhandledRequestTypeException, JSONException { + when(resource.get()).thenReturn(null); + final DefaultGoPluginApiRequest request = request(requestName); + request.setRequestBody(requestBody); + + final GoPluginApiResponse response = kubernetesSecretsPlugin.handle(request); + + assertThat(response.responseCode()).isEqualTo(200); + assertEquals(expected, response.responseBody(), true); + } + + @ParameterizedTest + @JsonSource(jsonFiles = { + "/secret-config.json", + "/invalid-secret-config.json" + }) + void shouldFailWhenProvidedSecretConfigurationsAreInvalid(String requestBody, String expected) throws UnhandledRequestTypeException, JSONException { + when(kubernetesClient.secrets()).thenThrow(new RuntimeException("Boom!")); + final DefaultGoPluginApiRequest request = request(requestName); + request.setRequestBody(requestBody); + + final GoPluginApiResponse response = kubernetesSecretsPlugin.handle(request); + + assertThat(response.responseCode()).isEqualTo(200); + assertEquals(expected, response.responseBody(), true); + } } private Map toMap(String response) { diff --git a/src/test/resources/invalid-secret-config.json b/src/test/resources/invalid-secret-config.json new file mode 100644 index 0000000..2bbf654 --- /dev/null +++ b/src/test/resources/invalid-secret-config.json @@ -0,0 +1,10 @@ +[ + { + "key": "kubernetes_secret_name", + "message": "Could not read specified secret. Either the connection with kubernetes cluster could not be established or the kubernetes secret does not exists." + }, + { + "key": "kubernetes_cluster_url", + "message": "Could not read specified secret. Either the connection with kubernetes cluster could not be established or the kubernetes secret does not exists." + } +] diff --git a/src/test/resources/non-existing-secret-config.json b/src/test/resources/non-existing-secret-config.json new file mode 100644 index 0000000..6b410f7 --- /dev/null +++ b/src/test/resources/non-existing-secret-config.json @@ -0,0 +1,6 @@ +[ + { + "key": "kubernetes_secret_name", + "message": "Specified Kubernetes secret does not exists." + } +]