Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

kubernetesapi-report: Support authentication via kube config file #174

Merged
merged 3 commits into from
Jan 27, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading