From 1f05b8b9fda04cd1189c2bc3bccf62e618526f5c Mon Sep 17 00:00:00 2001 From: Markus Meyer Date: Thu, 23 Jan 2025 10:34:44 +0100 Subject: [PATCH 1/3] support reading Kubernetes configuration from kube config file --- .../sauron/plugins/APIClientFactory.java | 74 +++++++++++++++---- .../sauron/plugins/APIClientFactoryTest.java | 40 +++++++++- 2 files changed, 97 insertions(+), 17 deletions(-) diff --git a/plugins/kubernetesapi-report/src/main/java/com/freenow/sauron/plugins/APIClientFactory.java b/plugins/kubernetesapi-report/src/main/java/com/freenow/sauron/plugins/APIClientFactory.java index 0c6ab9f..6b3841f 100644 --- a/plugins/kubernetesapi-report/src/main/java/com/freenow/sauron/plugins/APIClientFactory.java +++ b/plugins/kubernetesapi-report/src/main/java/com/freenow/sauron/plugins/APIClientFactory.java @@ -3,7 +3,11 @@ import com.freenow.sauron.model.DataSet; import com.freenow.sauron.properties.PluginsConfigurationProperties; import io.kubernetes.client.openapi.ApiClient; +import io.kubernetes.client.util.ClientBuilder; import io.kubernetes.client.util.Config; +import io.kubernetes.client.util.KubeConfig; +import java.io.File; +import java.io.FileReader; import java.util.HashMap; import java.util.Map; import java.util.Optional; @@ -12,6 +16,9 @@ import static com.freenow.sauron.plugins.KubernetesApiReport.API_CLIENT_CONFIG_PROPERTY; import static com.freenow.sauron.plugins.KubernetesApiReport.PLUGIN_ID; +import static io.kubernetes.client.util.KubeConfig.ENV_HOME; +import static io.kubernetes.client.util.KubeConfig.KUBECONFIG; +import static io.kubernetes.client.util.KubeConfig.KUBEDIR; import static org.apache.commons.lang3.StringUtils.EMPTY; @Slf4j @@ -31,28 +38,33 @@ public APIClientFactory(final Map apiClients) } + /** + * Creates the Kubernetes API client for an environment. + * It reads the environment from the field "environment" in the DataSet. + * If no client for the environment can be found, then it falls back to a default client. + *

+ * The configuration of this plugin supports multiple ways to create an API client: + *

+     *     kubernetesapi-report:
+     *       # ...
+     *       apiClientConfig:
+     *         default: default # Use the default client
+     *         clusterOne: "https://clusterOne.local" # Use a URL
+     *         clusterTwo: clusterTwo # Use the context "clusterTwo" from the kube config file at $HOME/.kube/config
+     *       # ...
+     * 
+ * + * @param input The current DataSet. + * @param properties Plugin configuration. + * @return Kubernetes API client. + */ public ApiClient get(final DataSet input, final PluginsConfigurationProperties properties) { if (apiClients.isEmpty()) { properties.getPluginConfigurationProperty(PLUGIN_ID, API_CLIENT_CONFIG_PROPERTY) .ifPresent(config -> ((Map) config).forEach((k, v) -> { - if (DEFAULT_CLIENT_CONFIG.equalsIgnoreCase(k)) - { - try - { - apiClients.put(DEFAULT_CLIENT_CONFIG, Config.defaultClient()); - } - catch (Exception e) - { - log.error("API Client not initialized. Error: {}", e.getMessage(), e); - throw new RuntimeException(e); - } - } - else - { - apiClients.put(k, Config.fromUrl(v)); - } + apiClients.put(k, createClient(k, v)); })); if (apiClients.isEmpty()) @@ -70,4 +82,34 @@ public ApiClient get(final DataSet input, final PluginsConfigurationProperties p } return Optional.ofNullable(apiClients.get(input.getStringAdditionalInformation(ENVIRONMENT).orElse(EMPTY))).orElse(apiClients.get(DEFAULT_CLIENT_CONFIG)); } + + private ApiClient createClient(String cluster, String value) + { + try + { + if (DEFAULT_CLIENT_CONFIG.equalsIgnoreCase(cluster)) + { + log.debug("Creating default Kubernetes client for cluster {}", cluster); + return Config.defaultClient(); + } + + if (value.startsWith("https://")) + { + log.debug("Creating Kubernetes client from URL {} for cluster {}", value, cluster); + return Config.fromUrl(value); + } + + log.debug("Creating Kubernetes client from config for cluster {}", cluster); + // Create KubeConfig here because it allows setting the context. + File configFile = new File(new File(System.getenv(ENV_HOME), KUBEDIR), KUBECONFIG); + KubeConfig kubeConfig = KubeConfig.loadKubeConfig(new FileReader(configFile)); + kubeConfig.setContext(value); + return ClientBuilder.kubeconfig(kubeConfig).build(); + } + catch (Exception e) + { + log.error("API Client for {} not initialized. Error: {}", cluster, e.getMessage(), e); + throw new RuntimeException(e); + } + } } diff --git a/plugins/kubernetesapi-report/src/test/java/com/freenow/sauron/plugins/APIClientFactoryTest.java b/plugins/kubernetesapi-report/src/test/java/com/freenow/sauron/plugins/APIClientFactoryTest.java index 1660c19..f9f797b 100644 --- a/plugins/kubernetesapi-report/src/test/java/com/freenow/sauron/plugins/APIClientFactoryTest.java +++ b/plugins/kubernetesapi-report/src/test/java/com/freenow/sauron/plugins/APIClientFactoryTest.java @@ -3,7 +3,10 @@ import com.freenow.sauron.model.DataSet; import com.freenow.sauron.properties.PluginsConfigurationProperties; import io.kubernetes.client.openapi.ApiClient; +import io.kubernetes.client.util.ClientBuilder; import io.kubernetes.client.util.Config; +import io.kubernetes.client.util.KubeConfig; +import java.io.File; import java.util.Map; import org.junit.Test; import org.mockito.MockedStatic; @@ -13,15 +16,20 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; public class APIClientFactoryTest { private static final String DEFAULT = "default"; private static final String CLUSTER_A = "cluster-a"; private static final String CLUSTER_B = "cluster-b"; + private static final String CLUSTER_C = "cluster-c"; private static final String KUBERNETES_CLUSTER_DEFAULT = "http://localhost"; public static final String KUBERNETES_CLUSTER_A_COM = "https://kubernetes.cluster-a.com"; public static final String KUBERNETES_CLUSTER_B_COM = "https://kubernetes.cluster-b.com"; + public static final String KUBERNETES_CLUSTER_C_COM = "https://kubernetes.cluster-c.com"; private APIClientFactory apiClientFactory = new APIClientFactory(); @@ -61,6 +69,36 @@ public void clusterBApiClient() } + @Test + public void configApiClient() + { + try (MockedStatic kubeConfigClass = Mockito.mockStatic(KubeConfig.class)) + { + KubeConfig kubeConfig = Mockito.mock(KubeConfig.class); + when(kubeConfig.getServer()).thenReturn(KUBERNETES_CLUSTER_C_COM); + kubeConfigClass.when(() -> KubeConfig.loadKubeConfig(any())).thenReturn(kubeConfig); + + PluginsConfigurationProperties properties = dummyPluginConfig(); + properties.put( + PLUGIN_ID, + Map.of( + "apiClientConfig", Map.of( + CLUSTER_C, CLUSTER_C + ) + ) + ); + + final var apiClient = apiClientFactory.get(dummyDataSet(CLUSTER_C), properties); + assertNotNull(apiClient); + assertTrue(apiClient.getBasePath().contains(CLUSTER_C)); + assertFalse(apiClient.getBasePath().contains(KUBERNETES_CLUSTER_DEFAULT)); + assertFalse(apiClient.getBasePath().contains(CLUSTER_A)); + assertFalse(apiClient.getBasePath().contains(CLUSTER_B)); + verify(kubeConfig).setContext(CLUSTER_C); + } + } + + private DataSet dummyDataSet(final String environment) { DataSet dataSet = new DataSet(); @@ -85,4 +123,4 @@ private PluginsConfigurationProperties dummyPluginConfig() ); return properties; } -} \ No newline at end of file +} From 9c5e53322d17588fe04c461994bcb8f32d1fb42a Mon Sep 17 00:00:00 2001 From: Markus Meyer Date: Thu, 23 Jan 2025 10:38:56 +0100 Subject: [PATCH 2/3] update readme --- plugins/kubernetesapi-report/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/kubernetesapi-report/README.md b/plugins/kubernetesapi-report/README.md index 10933a2..3401065 100644 --- a/plugins/kubernetesapi-report/README.md +++ b/plugins/kubernetesapi-report/README.md @@ -15,11 +15,11 @@ sauron.plugins: kubernetesapi-report: serviceLabel: "label/service.name" # The label that will used as a selector to find the resource by serviceName # When checks are needed in different clusters: - # - deploy https://hub.docker.com/r/bitnami/kubectl/ as service in the desired cluster - # - set below the url for the cluster + # - set up a kube config file, see https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/ + # - set up an association between the environment name and the name of a context in the kube config file apiClientConfig: default: "default" - clusterName: "cluster-url" + environmentName: "kubeConfigContextName" selectors: pod: - label @@ -51,4 +51,4 @@ The possible selectors can be found in - All the selector's value that can be found assigned to the specified resources - All the environment variables and its values that were found in the running pod -- All the values found in the property files for the running pod \ No newline at end of file +- All the values found in the property files for the running pod From c9e7067d87f78ec0bbbde4f4ce8c80b1a0bd9808 Mon Sep 17 00:00:00 2001 From: Markus Meyer Date: Thu, 23 Jan 2025 12:04:01 +0100 Subject: [PATCH 3/3] fix a failing test; allow setting location of kube config file --- plugins/kubernetesapi-report/README.md | 6 ++- .../sauron/plugins/APIClientFactory.java | 25 +++++++-- .../sauron/plugins/KubernetesApiReport.java | 3 +- .../sauron/plugins/APIClientFactoryTest.java | 52 ++++++++++--------- .../src/test/resources/kubeConfigFile.yaml | 16 ++++++ 5 files changed, 72 insertions(+), 30 deletions(-) create mode 100644 plugins/kubernetesapi-report/src/test/resources/kubeConfigFile.yaml diff --git a/plugins/kubernetesapi-report/README.md b/plugins/kubernetesapi-report/README.md index 3401065..d2ba4c3 100644 --- a/plugins/kubernetesapi-report/README.md +++ b/plugins/kubernetesapi-report/README.md @@ -15,11 +15,13 @@ sauron.plugins: kubernetesapi-report: serviceLabel: "label/service.name" # The label that will used as a selector to find the resource by serviceName # When checks are needed in different clusters: - # - set up a kube config file, see https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/ - # - set up an association between the environment name and the name of a context in the kube config file + # - Set up a kube config file, see https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/. + # - Set up an association between the environment name and the name of a context in the kube config file. + # - Optionally, use kubeConfigFile to set the location of the kube config file. Defaults to "$HOME/.kube/config" if not set. apiClientConfig: default: "default" environmentName: "kubeConfigContextName" + kubeConfigFile: "/home/user/.kube/config" selectors: pod: - label diff --git a/plugins/kubernetesapi-report/src/main/java/com/freenow/sauron/plugins/APIClientFactory.java b/plugins/kubernetesapi-report/src/main/java/com/freenow/sauron/plugins/APIClientFactory.java index 6b3841f..8d7aeae 100644 --- a/plugins/kubernetesapi-report/src/main/java/com/freenow/sauron/plugins/APIClientFactory.java +++ b/plugins/kubernetesapi-report/src/main/java/com/freenow/sauron/plugins/APIClientFactory.java @@ -15,6 +15,7 @@ import lombok.extern.slf4j.Slf4j; import static com.freenow.sauron.plugins.KubernetesApiReport.API_CLIENT_CONFIG_PROPERTY; +import static com.freenow.sauron.plugins.KubernetesApiReport.KUBE_CONFIG_FILE_PROPERTY; import static com.freenow.sauron.plugins.KubernetesApiReport.PLUGIN_ID; import static io.kubernetes.client.util.KubeConfig.ENV_HOME; import static io.kubernetes.client.util.KubeConfig.KUBECONFIG; @@ -62,9 +63,17 @@ public ApiClient get(final DataSet input, final PluginsConfigurationProperties p { if (apiClients.isEmpty()) { + String kubeConfigFile; + if (properties.getPluginConfigurationProperty(PLUGIN_ID, KUBE_CONFIG_FILE_PROPERTY).isPresent()) + { + kubeConfigFile = (String) properties.getPluginConfigurationProperty(PLUGIN_ID, KUBE_CONFIG_FILE_PROPERTY).get(); + } else { + kubeConfigFile = ""; + } + properties.getPluginConfigurationProperty(PLUGIN_ID, API_CLIENT_CONFIG_PROPERTY) .ifPresent(config -> ((Map) config).forEach((k, v) -> { - apiClients.put(k, createClient(k, v)); + apiClients.put(k, createClient(k, v, kubeConfigFile)); })); if (apiClients.isEmpty()) @@ -83,7 +92,7 @@ public ApiClient get(final DataSet input, final PluginsConfigurationProperties p return Optional.ofNullable(apiClients.get(input.getStringAdditionalInformation(ENVIRONMENT).orElse(EMPTY))).orElse(apiClients.get(DEFAULT_CLIENT_CONFIG)); } - private ApiClient createClient(String cluster, String value) + private ApiClient createClient(String cluster, String value, String kubeConfigFile) { try { @@ -101,7 +110,7 @@ private ApiClient createClient(String cluster, String value) log.debug("Creating Kubernetes client from config for cluster {}", cluster); // Create KubeConfig here because it allows setting the context. - File configFile = new File(new File(System.getenv(ENV_HOME), KUBEDIR), KUBECONFIG); + File configFile = getKubeConfig(kubeConfigFile); KubeConfig kubeConfig = KubeConfig.loadKubeConfig(new FileReader(configFile)); kubeConfig.setContext(value); return ClientBuilder.kubeconfig(kubeConfig).build(); @@ -112,4 +121,14 @@ private ApiClient createClient(String cluster, String value) throw new RuntimeException(e); } } + + private File getKubeConfig(String path) + { + if (path == null || path.isEmpty()) + { + return new File(new File(System.getenv(ENV_HOME), KUBEDIR), KUBECONFIG); + } + + return new File(path); + } } diff --git a/plugins/kubernetesapi-report/src/main/java/com/freenow/sauron/plugins/KubernetesApiReport.java b/plugins/kubernetesapi-report/src/main/java/com/freenow/sauron/plugins/KubernetesApiReport.java index 6a98938..57da322 100644 --- a/plugins/kubernetesapi-report/src/main/java/com/freenow/sauron/plugins/KubernetesApiReport.java +++ b/plugins/kubernetesapi-report/src/main/java/com/freenow/sauron/plugins/KubernetesApiReport.java @@ -21,6 +21,7 @@ public class KubernetesApiReport implements SauronExtension static final String SELECTORS_PROPERTY = "selectors"; static final String ENV_VARS_PROPERTY = "environmentVariablesCheck"; static final String PROPERTIES_FILES_CHECK = "propertiesFilesCheck"; + static final String KUBE_CONFIG_FILE_PROPERTY = "kubeConfigFile"; private APIClientFactory apiClientFactory = new APIClientFactory(); private KubernetesLabelAnnotationReader kubernetesLabelAnnotationReader = new KubernetesLabelAnnotationReader(); @@ -64,4 +65,4 @@ public DataSet apply(PluginsConfigurationProperties properties, DataSet input) }); return input; } -} \ No newline at end of file +} diff --git a/plugins/kubernetesapi-report/src/test/java/com/freenow/sauron/plugins/APIClientFactoryTest.java b/plugins/kubernetesapi-report/src/test/java/com/freenow/sauron/plugins/APIClientFactoryTest.java index f9f797b..9424689 100644 --- a/plugins/kubernetesapi-report/src/test/java/com/freenow/sauron/plugins/APIClientFactoryTest.java +++ b/plugins/kubernetesapi-report/src/test/java/com/freenow/sauron/plugins/APIClientFactoryTest.java @@ -7,12 +7,22 @@ import io.kubernetes.client.util.Config; import io.kubernetes.client.util.KubeConfig; import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.util.Map; +import java.util.Objects; +import org.apache.commons.io.IOUtils; import org.junit.Test; import org.mockito.MockedStatic; import org.mockito.Mockito; import static com.freenow.sauron.plugins.KubernetesApiReport.PLUGIN_ID; +import static io.kubernetes.client.util.KubeConfig.ENV_HOME; +import static io.kubernetes.client.util.KubeConfig.KUBECONFIG; +import static io.kubernetes.client.util.KubeConfig.KUBEDIR; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; @@ -29,7 +39,7 @@ public class APIClientFactoryTest private static final String KUBERNETES_CLUSTER_DEFAULT = "http://localhost"; public static final String KUBERNETES_CLUSTER_A_COM = "https://kubernetes.cluster-a.com"; public static final String KUBERNETES_CLUSTER_B_COM = "https://kubernetes.cluster-b.com"; - public static final String KUBERNETES_CLUSTER_C_COM = "https://kubernetes.cluster-c.com"; + public static final String KUBERNETES_CLUSTER_C_LOCAL = "https://kubernetes.cluster-c.local"; private APIClientFactory apiClientFactory = new APIClientFactory(); @@ -72,30 +82,24 @@ public void clusterBApiClient() @Test public void configApiClient() { - try (MockedStatic kubeConfigClass = Mockito.mockStatic(KubeConfig.class)) - { - KubeConfig kubeConfig = Mockito.mock(KubeConfig.class); - when(kubeConfig.getServer()).thenReturn(KUBERNETES_CLUSTER_C_COM); - kubeConfigClass.when(() -> KubeConfig.loadKubeConfig(any())).thenReturn(kubeConfig); - - PluginsConfigurationProperties properties = dummyPluginConfig(); - properties.put( - PLUGIN_ID, - Map.of( - "apiClientConfig", Map.of( - CLUSTER_C, CLUSTER_C - ) - ) - ); + URL kubeConfigFile = this.getClass().getClassLoader().getResource("kubeConfigFile.yaml"); + PluginsConfigurationProperties properties = dummyPluginConfig(); + properties.put( + PLUGIN_ID, + Map.of( + "apiClientConfig", Map.of( + CLUSTER_C, CLUSTER_C + ), + "kubeConfigFile", kubeConfigFile.getFile() + ) + ); - final var apiClient = apiClientFactory.get(dummyDataSet(CLUSTER_C), properties); - assertNotNull(apiClient); - assertTrue(apiClient.getBasePath().contains(CLUSTER_C)); - assertFalse(apiClient.getBasePath().contains(KUBERNETES_CLUSTER_DEFAULT)); - assertFalse(apiClient.getBasePath().contains(CLUSTER_A)); - assertFalse(apiClient.getBasePath().contains(CLUSTER_B)); - verify(kubeConfig).setContext(CLUSTER_C); - } + final var apiClient = apiClientFactory.get(dummyDataSet(CLUSTER_C), properties); + assertNotNull(apiClient); + assertEquals(KUBERNETES_CLUSTER_C_LOCAL, apiClient.getBasePath()); + assertFalse(apiClient.getBasePath().contains(KUBERNETES_CLUSTER_DEFAULT)); + assertFalse(apiClient.getBasePath().contains(CLUSTER_A)); + assertFalse(apiClient.getBasePath().contains(CLUSTER_B)); } diff --git a/plugins/kubernetesapi-report/src/test/resources/kubeConfigFile.yaml b/plugins/kubernetesapi-report/src/test/resources/kubeConfigFile.yaml new file mode 100644 index 0000000..8698aa0 --- /dev/null +++ b/plugins/kubernetesapi-report/src/test/resources/kubeConfigFile.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Config + +clusters: + - cluster: + server: https://kubernetes.cluster-c.local + name: cluster-c + +users: + - name: unittest + +contexts: + - context: + cluster: cluster-c + user: unittest + name: cluster-c