Skip to content

Commit

Permalink
Add CredentialValidator to validate whether specified secret configur…
Browse files Browse the repository at this point in the history
…ation is valid

* Try connecting to the specified kubernetes secret, if failed, raise a validation error.
  • Loading branch information
GaneshSPatil committed Jul 15, 2019
1 parent 1042d1b commit b435992
Show file tree
Hide file tree
Showing 7 changed files with 135 additions and 17 deletions.
Expand Up @@ -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());
}
Expand Down
@@ -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;
Expand All @@ -10,15 +12,23 @@
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;

@Extension
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) {
Expand All @@ -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();
}
Expand Down
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand Down
@@ -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<String, String> 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;
}
}
Expand Up @@ -23,28 +23,44 @@
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;

import static com.github.bdpiparva.plugin.base.ResourceReader.readResource;
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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<String, String> toMap(String response) {
Expand Down
10 changes: 10 additions & 0 deletions 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."
}
]
6 changes: 6 additions & 0 deletions src/test/resources/non-existing-secret-config.json
@@ -0,0 +1,6 @@
[
{
"key": "kubernetes_secret_name",
"message": "Specified Kubernetes secret does not exists."
}
]

0 comments on commit b435992

Please sign in to comment.