Skip to content

Commit 05d44d6

Browse files
authored
production readiness additional information defined in the plugin (#186)
cover kubernetes health probes
1 parent 9fbf193 commit 05d44d6

File tree

11 files changed

+462
-7
lines changed

11 files changed

+462
-7
lines changed

plugins/kubernetesapi-report/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ sauron.plugins:
4040
"THE_OUTPUT_KEY": "the.prop.key.in.the.file"
4141
"[/path/to/file_b.env]":
4242
"ANOTHER_OUTPUT_KEY": "the.prop.key.in.the.file"
43+
containersCheck:
44+
- liveness
45+
- readiness
4346
```
4447
4548
The possible selectors can be found in

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import com.freenow.sauron.plugins.readers.KubernetesEnvironmentVariablesReader;
55
import com.freenow.sauron.plugins.readers.KubernetesLabelAnnotationReader;
66
import com.freenow.sauron.plugins.readers.KubernetesPropertiesFilesReader;
7+
import com.freenow.sauron.plugins.readers.KubernetesContainersReader;
78
import com.freenow.sauron.properties.PluginsConfigurationProperties;
89
import java.util.Map;
910
import lombok.NoArgsConstructor;
@@ -21,24 +22,28 @@ public class KubernetesApiReport implements SauronExtension
2122
static final String SELECTORS_PROPERTY = "selectors";
2223
static final String ENV_VARS_PROPERTY = "environmentVariablesCheck";
2324
static final String PROPERTIES_FILES_CHECK = "propertiesFilesCheck";
25+
static final String CONTAINERS_CHECK = "containersCheck";
2426
static final String KUBE_CONFIG_FILE_PROPERTY = "kubeConfigFile";
2527

2628
private APIClientFactory apiClientFactory = new APIClientFactory();
2729
private KubernetesLabelAnnotationReader kubernetesLabelAnnotationReader = new KubernetesLabelAnnotationReader();
2830
private KubernetesEnvironmentVariablesReader kubernetesEnvironmentVariablesReader = new KubernetesEnvironmentVariablesReader();
2931
private KubernetesPropertiesFilesReader kubernetesPropertiesFilesReader = new KubernetesPropertiesFilesReader();
32+
private KubernetesContainersReader kubernetesContainersReader = new KubernetesContainersReader();
3033

3134

3235
public KubernetesApiReport(
3336
final APIClientFactory apiClientFactory,
3437
final KubernetesLabelAnnotationReader kubernetesLabelAnnotationReader,
3538
final KubernetesEnvironmentVariablesReader kubernetesEnvironmentVariablesReader,
36-
final KubernetesPropertiesFilesReader kubernetesPropertiesFilesReader)
39+
final KubernetesPropertiesFilesReader kubernetesPropertiesFilesReader,
40+
final KubernetesContainersReader kubernetesContainersReader)
3741
{
3842
this.apiClientFactory = apiClientFactory;
3943
this.kubernetesLabelAnnotationReader = kubernetesLabelAnnotationReader;
4044
this.kubernetesEnvironmentVariablesReader = kubernetesEnvironmentVariablesReader;
4145
this.kubernetesPropertiesFilesReader = kubernetesPropertiesFilesReader;
46+
this.kubernetesContainersReader = kubernetesContainersReader;
4247
}
4348

4449

@@ -62,6 +67,11 @@ public DataSet apply(PluginsConfigurationProperties properties, DataSet input)
6267
.map(Map.class::cast)
6368
.map(Map.class::cast)
6469
.ifPresent(propFilesCheck -> kubernetesPropertiesFilesReader.read(input, serviceLabel, propFilesCheck, apiClient));
70+
71+
properties.getPluginConfigurationProperty(PLUGIN_ID, CONTAINERS_CHECK).filter(Map.class::isInstance)
72+
.map(Map.class::cast)
73+
.map(Map::values)
74+
.ifPresent(containersCheck -> kubernetesContainersReader.read(input, serviceLabel, containersCheck, apiClient));
6575
});
6676
return input;
6777
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package com.freenow.sauron.plugins.commands;
2+
3+
import com.freenow.sauron.plugins.utils.KubernetesResources;
4+
import io.kubernetes.client.openapi.ApiClient;
5+
import io.kubernetes.client.openapi.ApiException;
6+
import io.kubernetes.client.openapi.apis.*;
7+
import io.kubernetes.client.openapi.models.*;
8+
import lombok.extern.slf4j.Slf4j;
9+
10+
import java.util.Comparator;
11+
import java.util.Optional;
12+
13+
import static com.freenow.sauron.plugins.utils.KubernetesConstants.K8S_API_TIMEOUT_SECONDS;
14+
import static com.freenow.sauron.plugins.utils.KubernetesConstants.K8S_DEFAULT_NAMESPACE;
15+
import static com.freenow.sauron.plugins.utils.KubernetesConstants.K8S_PRETTY_OUTPUT;
16+
17+
@Slf4j
18+
public class KubernetesGetDeploymentSpecCommand
19+
{
20+
//defined to let us override it in the tests and inject a mock
21+
protected AppsV1Api createAppsV1Api(ApiClient client)
22+
{
23+
return new AppsV1Api(client);
24+
}
25+
26+
27+
public Optional<V1DeploymentSpec> getDeploymentSpec(String serviceLabel, KubernetesResources resource, String service, ApiClient client)
28+
{
29+
try
30+
{
31+
String labelSelector = String.format("%s=%s", serviceLabel, service);
32+
log.debug("Filtering deployment {} using selector {}", resource, labelSelector);
33+
return createAppsV1Api(client).listNamespacedDeployment(
34+
K8S_DEFAULT_NAMESPACE,
35+
K8S_PRETTY_OUTPUT,
36+
false,
37+
null,
38+
null,
39+
labelSelector,
40+
null,
41+
null,
42+
null,
43+
K8S_API_TIMEOUT_SECONDS,
44+
false
45+
)
46+
.getItems().stream()
47+
.max(Comparator.comparing(
48+
d -> {
49+
if (d.getMetadata() == null)
50+
{
51+
return null;
52+
}
53+
return d.getMetadata().getCreationTimestamp();
54+
}, Comparator.nullsLast(Comparator.naturalOrder())
55+
))
56+
.map(V1Deployment::getSpec);
57+
}
58+
catch (ApiException ex)
59+
{
60+
log.error(ex.getMessage(), ex);
61+
}
62+
63+
return Optional.empty();
64+
}
65+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package com.freenow.sauron.plugins.readers;
2+
3+
import com.freenow.sauron.model.DataSet;
4+
import com.freenow.sauron.plugins.commands.KubernetesGetDeploymentSpecCommand;
5+
import com.freenow.sauron.plugins.utils.ContainerCheckStrategy;
6+
import com.freenow.sauron.plugins.utils.LivenessCheckStrategy;
7+
import com.freenow.sauron.plugins.utils.ReadinessCheckStrategy;
8+
import com.freenow.sauron.plugins.utils.RetryCommand;
9+
import com.freenow.sauron.plugins.utils.RetryConfig;
10+
import io.kubernetes.client.openapi.ApiClient;
11+
import io.kubernetes.client.openapi.models.*;
12+
import java.util.Collection;
13+
import java.util.List;
14+
import java.util.Map;
15+
import java.util.stream.Collectors;
16+
import lombok.RequiredArgsConstructor;
17+
import lombok.extern.slf4j.Slf4j;
18+
19+
import java.util.Objects;
20+
import java.util.Optional;
21+
22+
import static com.freenow.sauron.plugins.utils.KubernetesResources.DEPLOYMENT;
23+
24+
@Slf4j
25+
@RequiredArgsConstructor
26+
public class KubernetesContainersReader
27+
{
28+
private final KubernetesGetDeploymentSpecCommand kubernetesGetDeploymentSpecCommand;
29+
private final RetryConfig retryConfig;
30+
public static final String LIVENESS = "liveness";
31+
public static final String READINESS = "readiness";
32+
33+
public static final Map<String, ContainerCheckStrategy> strategies = Map.of(
34+
LIVENESS, new LivenessCheckStrategy(),
35+
READINESS, new ReadinessCheckStrategy()
36+
);
37+
38+
39+
public KubernetesContainersReader()
40+
{
41+
this.kubernetesGetDeploymentSpecCommand = new KubernetesGetDeploymentSpecCommand();
42+
this.retryConfig = new RetryConfig();
43+
}
44+
45+
46+
public void read(DataSet input, String serviceLabel, Collection<String> containersCheck, ApiClient apiClient)
47+
{
48+
List<ContainerCheckStrategy> strategiesToApply = containersCheck.stream()
49+
.map(strategies::get)
50+
.filter(Objects::nonNull)
51+
.collect(Collectors.toList());
52+
53+
new RetryCommand<Void>(retryConfig).run(() ->
54+
{
55+
Optional<V1DeploymentSpec> deploymentSpecOpt = kubernetesGetDeploymentSpecCommand.getDeploymentSpec(
56+
String.valueOf(serviceLabel),
57+
DEPLOYMENT,
58+
input.getServiceName(),
59+
apiClient
60+
);
61+
62+
if (deploymentSpecOpt.isPresent())
63+
{
64+
final String deploymentName = Objects.requireNonNull(deploymentSpecOpt.get().getTemplate().getMetadata()).getName();
65+
try
66+
{
67+
deploymentSpecOpt.ifPresent(deploymentSpec -> {
68+
V1PodSpec podSpec = deploymentSpec.getTemplate().getSpec();
69+
if (podSpec == null)
70+
{
71+
log.warn("deployment by name: {} doesn't have spec", deploymentName);
72+
return;
73+
}
74+
for (V1Container container : podSpec.getContainers())
75+
{
76+
for (ContainerCheckStrategy strategy : strategiesToApply)
77+
{
78+
strategy.check(container, input);
79+
}
80+
}
81+
});
82+
}
83+
catch (Exception e)
84+
{
85+
log.warn("Failed to fetch deployment by name: {}", deploymentName, e);
86+
}
87+
}
88+
else
89+
{
90+
log.warn("Deployment not found for service: {}", input.getServiceName());
91+
}
92+
return null;
93+
});
94+
}
95+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.freenow.sauron.plugins.utils;
2+
3+
import com.freenow.sauron.model.DataSet;
4+
import io.kubernetes.client.openapi.models.V1Container;
5+
6+
public interface ContainerCheckStrategy {
7+
String getName();
8+
void check(V1Container container, DataSet input);
9+
}
10+
11+
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.freenow.sauron.plugins.utils;
2+
3+
import com.freenow.sauron.model.DataSet;
4+
import com.freenow.sauron.plugins.readers.KubernetesContainersReader;
5+
import io.kubernetes.client.openapi.models.V1Container;
6+
7+
public class LivenessCheckStrategy implements ContainerCheckStrategy
8+
{
9+
@Override
10+
public String getName() { return KubernetesContainersReader.LIVENESS; }
11+
12+
@Override
13+
public void check(V1Container container, DataSet input) {
14+
boolean hasLiveness = container.getLivenessProbe() != null;
15+
input.setAdditionalInformation(getName(), String.valueOf(hasLiveness));
16+
}
17+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.freenow.sauron.plugins.utils;
2+
3+
import com.freenow.sauron.model.DataSet;
4+
import com.freenow.sauron.plugins.readers.KubernetesContainersReader;
5+
import io.kubernetes.client.openapi.models.V1Container;
6+
7+
public class ReadinessCheckStrategy implements ContainerCheckStrategy
8+
{
9+
@Override
10+
public String getName() { return KubernetesContainersReader.READINESS; }
11+
12+
@Override
13+
public void check(V1Container container, DataSet input) {
14+
boolean hasReadiness = container.getReadinessProbe() != null;
15+
input.setAdditionalInformation(getName(), String.valueOf(hasReadiness));
16+
}
17+
}

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

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -139,11 +139,12 @@ private PluginsConfigurationProperties pluginConfig()
139139
props.put("serviceLabel", SERVICE_LABEL);
140140
props.put("selectors", dummySelectors());
141141
props.put("environmentVariablesCheck", Map.of("0", "ENV_ENABLED", "1", "ENV_VERSION"));
142-
props.put("apiClientConfig", Map.of(
143-
"default", "",
144-
"cluster-a", "https://kubernetes.cluster-a.com",
145-
"cluster-b", "https://kubernetes.cluster-b.com"
146-
));
142+
props.put(
143+
"apiClientConfig", Map.of(
144+
"default", "",
145+
"cluster-a", "https://kubernetes.cluster-a.com",
146+
"cluster-b", "https://kubernetes.cluster-b.com"
147+
));
147148

148149
properties.put(PLUGIN_ID, props);
149150
return properties;

0 commit comments

Comments
 (0)