Skip to content

Commit 2fc1d02

Browse files
authored
kubernetesapi-report: Support authentication via kube config file (#174)
1 parent 47951ff commit 2fc1d02

File tree

5 files changed

+144
-22
lines changed

5 files changed

+144
-22
lines changed

plugins/kubernetesapi-report/README.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,13 @@ sauron.plugins:
1515
kubernetesapi-report:
1616
serviceLabel: "label/service.name" # The label that will used as a selector to find the resource by serviceName
1717
# When checks are needed in different clusters:
18-
# - deploy https://hub.docker.com/r/bitnami/kubectl/ as service in the desired cluster
19-
# - set below the url for the cluster
18+
# - Set up a kube config file, see https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/.
19+
# - Set up an association between the environment name and the name of a context in the kube config file.
20+
# - Optionally, use kubeConfigFile to set the location of the kube config file. Defaults to "$HOME/.kube/config" if not set.
2021
apiClientConfig:
2122
default: "default"
22-
clusterName: "cluster-url"
23+
environmentName: "kubeConfigContextName"
24+
kubeConfigFile: "/home/user/.kube/config"
2325
selectors:
2426
pod:
2527
- label
@@ -51,4 +53,4 @@ The possible selectors can be found in
5153

5254
- All the selector's value that can be found assigned to the specified resources
5355
- All the environment variables and its values that were found in the running pod
54-
- All the values found in the property files for the running pod
56+
- All the values found in the property files for the running pod

plugins/kubernetesapi-report/src/main/java/com/freenow/sauron/plugins/APIClientFactory.java

Lines changed: 77 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,23 @@
33
import com.freenow.sauron.model.DataSet;
44
import com.freenow.sauron.properties.PluginsConfigurationProperties;
55
import io.kubernetes.client.openapi.ApiClient;
6+
import io.kubernetes.client.util.ClientBuilder;
67
import io.kubernetes.client.util.Config;
8+
import io.kubernetes.client.util.KubeConfig;
9+
import java.io.File;
10+
import java.io.FileReader;
711
import java.util.HashMap;
812
import java.util.Map;
913
import java.util.Optional;
1014
import lombok.NoArgsConstructor;
1115
import lombok.extern.slf4j.Slf4j;
1216

1317
import static com.freenow.sauron.plugins.KubernetesApiReport.API_CLIENT_CONFIG_PROPERTY;
18+
import static com.freenow.sauron.plugins.KubernetesApiReport.KUBE_CONFIG_FILE_PROPERTY;
1419
import static com.freenow.sauron.plugins.KubernetesApiReport.PLUGIN_ID;
20+
import static io.kubernetes.client.util.KubeConfig.ENV_HOME;
21+
import static io.kubernetes.client.util.KubeConfig.KUBECONFIG;
22+
import static io.kubernetes.client.util.KubeConfig.KUBEDIR;
1523
import static org.apache.commons.lang3.StringUtils.EMPTY;
1624

1725
@Slf4j
@@ -31,28 +39,41 @@ public APIClientFactory(final Map<String, ApiClient> apiClients)
3139
}
3240

3341

42+
/**
43+
* Creates the Kubernetes API client for an environment.
44+
* It reads the environment from the field "environment" in the DataSet.
45+
* If no client for the environment can be found, then it falls back to a default client.
46+
* <p>
47+
* The configuration of this plugin supports multiple ways to create an API client:
48+
* <pre>
49+
* kubernetesapi-report:
50+
* # ...
51+
* apiClientConfig:
52+
* default: default # Use the default client
53+
* clusterOne: "https://clusterOne.local" # Use a URL
54+
* clusterTwo: clusterTwo # Use the context "clusterTwo" from the kube config file at $HOME/.kube/config
55+
* # ...
56+
* </pre>
57+
*
58+
* @param input The current DataSet.
59+
* @param properties Plugin configuration.
60+
* @return Kubernetes API client.
61+
*/
3462
public ApiClient get(final DataSet input, final PluginsConfigurationProperties properties)
3563
{
3664
if (apiClients.isEmpty())
3765
{
66+
String kubeConfigFile;
67+
if (properties.getPluginConfigurationProperty(PLUGIN_ID, KUBE_CONFIG_FILE_PROPERTY).isPresent())
68+
{
69+
kubeConfigFile = (String) properties.getPluginConfigurationProperty(PLUGIN_ID, KUBE_CONFIG_FILE_PROPERTY).get();
70+
} else {
71+
kubeConfigFile = "";
72+
}
73+
3874
properties.getPluginConfigurationProperty(PLUGIN_ID, API_CLIENT_CONFIG_PROPERTY)
3975
.ifPresent(config -> ((Map<String, String>) config).forEach((k, v) -> {
40-
if (DEFAULT_CLIENT_CONFIG.equalsIgnoreCase(k))
41-
{
42-
try
43-
{
44-
apiClients.put(DEFAULT_CLIENT_CONFIG, Config.defaultClient());
45-
}
46-
catch (Exception e)
47-
{
48-
log.error("API Client not initialized. Error: {}", e.getMessage(), e);
49-
throw new RuntimeException(e);
50-
}
51-
}
52-
else
53-
{
54-
apiClients.put(k, Config.fromUrl(v));
55-
}
76+
apiClients.put(k, createClient(k, v, kubeConfigFile));
5677
}));
5778

5879
if (apiClients.isEmpty())
@@ -70,4 +91,44 @@ public ApiClient get(final DataSet input, final PluginsConfigurationProperties p
7091
}
7192
return Optional.ofNullable(apiClients.get(input.getStringAdditionalInformation(ENVIRONMENT).orElse(EMPTY))).orElse(apiClients.get(DEFAULT_CLIENT_CONFIG));
7293
}
94+
95+
private ApiClient createClient(String cluster, String value, String kubeConfigFile)
96+
{
97+
try
98+
{
99+
if (DEFAULT_CLIENT_CONFIG.equalsIgnoreCase(cluster))
100+
{
101+
log.debug("Creating default Kubernetes client for cluster {}", cluster);
102+
return Config.defaultClient();
103+
}
104+
105+
if (value.startsWith("https://"))
106+
{
107+
log.debug("Creating Kubernetes client from URL {} for cluster {}", value, cluster);
108+
return Config.fromUrl(value);
109+
}
110+
111+
log.debug("Creating Kubernetes client from config for cluster {}", cluster);
112+
// Create KubeConfig here because it allows setting the context.
113+
File configFile = getKubeConfig(kubeConfigFile);
114+
KubeConfig kubeConfig = KubeConfig.loadKubeConfig(new FileReader(configFile));
115+
kubeConfig.setContext(value);
116+
return ClientBuilder.kubeconfig(kubeConfig).build();
117+
}
118+
catch (Exception e)
119+
{
120+
log.error("API Client for {} not initialized. Error: {}", cluster, e.getMessage(), e);
121+
throw new RuntimeException(e);
122+
}
123+
}
124+
125+
private File getKubeConfig(String path)
126+
{
127+
if (path == null || path.isEmpty())
128+
{
129+
return new File(new File(System.getenv(ENV_HOME), KUBEDIR), KUBECONFIG);
130+
}
131+
132+
return new File(path);
133+
}
73134
}

plugins/kubernetesapi-report/src/main/java/com/freenow/sauron/plugins/KubernetesApiReport.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ public class KubernetesApiReport implements SauronExtension
2121
static final String SELECTORS_PROPERTY = "selectors";
2222
static final String ENV_VARS_PROPERTY = "environmentVariablesCheck";
2323
static final String PROPERTIES_FILES_CHECK = "propertiesFilesCheck";
24+
static final String KUBE_CONFIG_FILE_PROPERTY = "kubeConfigFile";
2425

2526
private APIClientFactory apiClientFactory = new APIClientFactory();
2627
private KubernetesLabelAnnotationReader kubernetesLabelAnnotationReader = new KubernetesLabelAnnotationReader();
@@ -64,4 +65,4 @@ public DataSet apply(PluginsConfigurationProperties properties, DataSet input)
6465
});
6566
return input;
6667
}
67-
}
68+
}

plugins/kubernetesapi-report/src/test/java/com/freenow/sauron/plugins/APIClientFactoryTest.java

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,43 @@
33
import com.freenow.sauron.model.DataSet;
44
import com.freenow.sauron.properties.PluginsConfigurationProperties;
55
import io.kubernetes.client.openapi.ApiClient;
6+
import io.kubernetes.client.util.ClientBuilder;
67
import io.kubernetes.client.util.Config;
8+
import io.kubernetes.client.util.KubeConfig;
9+
import java.io.File;
10+
import java.io.IOException;
11+
import java.net.URL;
12+
import java.nio.charset.StandardCharsets;
13+
import java.nio.file.Files;
714
import java.util.Map;
15+
import java.util.Objects;
16+
import org.apache.commons.io.IOUtils;
817
import org.junit.Test;
918
import org.mockito.MockedStatic;
1019
import org.mockito.Mockito;
1120

1221
import static com.freenow.sauron.plugins.KubernetesApiReport.PLUGIN_ID;
22+
import static io.kubernetes.client.util.KubeConfig.ENV_HOME;
23+
import static io.kubernetes.client.util.KubeConfig.KUBECONFIG;
24+
import static io.kubernetes.client.util.KubeConfig.KUBEDIR;
25+
import static org.junit.Assert.assertEquals;
1326
import static org.junit.Assert.assertFalse;
1427
import static org.junit.Assert.assertNotNull;
1528
import static org.junit.Assert.assertTrue;
29+
import static org.mockito.ArgumentMatchers.any;
30+
import static org.mockito.Mockito.verify;
31+
import static org.mockito.Mockito.when;
1632

1733
public class APIClientFactoryTest
1834
{
1935
private static final String DEFAULT = "default";
2036
private static final String CLUSTER_A = "cluster-a";
2137
private static final String CLUSTER_B = "cluster-b";
38+
private static final String CLUSTER_C = "cluster-c";
2239
private static final String KUBERNETES_CLUSTER_DEFAULT = "http://localhost";
2340
public static final String KUBERNETES_CLUSTER_A_COM = "https://kubernetes.cluster-a.com";
2441
public static final String KUBERNETES_CLUSTER_B_COM = "https://kubernetes.cluster-b.com";
42+
public static final String KUBERNETES_CLUSTER_C_LOCAL = "https://kubernetes.cluster-c.local";
2543
private APIClientFactory apiClientFactory = new APIClientFactory();
2644

2745

@@ -61,6 +79,30 @@ public void clusterBApiClient()
6179
}
6280

6381

82+
@Test
83+
public void configApiClient()
84+
{
85+
URL kubeConfigFile = this.getClass().getClassLoader().getResource("kubeConfigFile.yaml");
86+
PluginsConfigurationProperties properties = dummyPluginConfig();
87+
properties.put(
88+
PLUGIN_ID,
89+
Map.of(
90+
"apiClientConfig", Map.of(
91+
CLUSTER_C, CLUSTER_C
92+
),
93+
"kubeConfigFile", kubeConfigFile.getFile()
94+
)
95+
);
96+
97+
final var apiClient = apiClientFactory.get(dummyDataSet(CLUSTER_C), properties);
98+
assertNotNull(apiClient);
99+
assertEquals(KUBERNETES_CLUSTER_C_LOCAL, apiClient.getBasePath());
100+
assertFalse(apiClient.getBasePath().contains(KUBERNETES_CLUSTER_DEFAULT));
101+
assertFalse(apiClient.getBasePath().contains(CLUSTER_A));
102+
assertFalse(apiClient.getBasePath().contains(CLUSTER_B));
103+
}
104+
105+
64106
private DataSet dummyDataSet(final String environment)
65107
{
66108
DataSet dataSet = new DataSet();
@@ -85,4 +127,4 @@ private PluginsConfigurationProperties dummyPluginConfig()
85127
);
86128
return properties;
87129
}
88-
}
130+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
apiVersion: v1
2+
kind: Config
3+
4+
clusters:
5+
- cluster:
6+
server: https://kubernetes.cluster-c.local
7+
name: cluster-c
8+
9+
users:
10+
- name: unittest
11+
12+
contexts:
13+
- context:
14+
cluster: cluster-c
15+
user: unittest
16+
name: cluster-c

0 commit comments

Comments
 (0)