Skip to content

Commit

Permalink
kubernetesapi-report: Support authentication via kube config file (#174)
Browse files Browse the repository at this point in the history
  • Loading branch information
wndhydrnt authored Jan 27, 2025
1 parent 47951ff commit 2fc1d02
Show file tree
Hide file tree
Showing 5 changed files with 144 additions and 22 deletions.
10 changes: 6 additions & 4 deletions plugins/kubernetesapi-report/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
# - 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.
# - Optionally, use kubeConfigFile to set the location of the kube config file. Defaults to "$HOME/.kube/config" if not set.
apiClientConfig:
default: "default"
clusterName: "cluster-url"
environmentName: "kubeConfigContextName"
kubeConfigFile: "/home/user/.kube/config"
selectors:
pod:
- label
Expand Down Expand Up @@ -51,4 +53,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
- All the values found in the property files for the running pod
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,23 @@
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;
import lombok.NoArgsConstructor;
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;
import static io.kubernetes.client.util.KubeConfig.KUBEDIR;
import static org.apache.commons.lang3.StringUtils.EMPTY;

@Slf4j
Expand All @@ -31,28 +39,41 @@ public APIClientFactory(final Map<String, ApiClient> 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.
* <p>
* The configuration of this plugin supports multiple ways to create an API client:
* <pre>
* 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
* # ...
* </pre>
*
* @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())
{
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<String, String>) 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, kubeConfigFile));
}));

if (apiClients.isEmpty())
Expand All @@ -70,4 +91,44 @@ 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, String kubeConfigFile)
{
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 = getKubeConfig(kubeConfigFile);
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);
}
}

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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -64,4 +65,4 @@ public DataSet apply(PluginsConfigurationProperties properties, DataSet input)
});
return input;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,43 @@
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.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;
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_LOCAL = "https://kubernetes.cluster-c.local";
private APIClientFactory apiClientFactory = new APIClientFactory();


Expand Down Expand Up @@ -61,6 +79,30 @@ public void clusterBApiClient()
}


@Test
public void configApiClient()
{
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);
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));
}


private DataSet dummyDataSet(final String environment)
{
DataSet dataSet = new DataSet();
Expand All @@ -85,4 +127,4 @@ private PluginsConfigurationProperties dummyPluginConfig()
);
return properties;
}
}
}
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 2fc1d02

Please sign in to comment.