Add env, secrets and CRDs

This commit is contained in:
andreas.dinauer 2025-11-01 16:59:04 +01:00
parent 282a12111c
commit 855e48f6b2
12 changed files with 242 additions and 179 deletions

View File

@ -0,0 +1,6 @@
package dev.dinauer;
public record EnvVar(String key, String value)
{
}

View File

@ -0,0 +1,7 @@
package dev.dinauer;
import java.time.LocalDateTime;
public record KubernetesLog(LocalDateTime timestamp, String message)
{
}

View File

@ -12,7 +12,12 @@ import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import jakarta.ws.rs.*; import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.MediaType;
import org.jboss.resteasy.reactive.common.NotImplementedYet;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
@ -28,16 +33,44 @@ public class LogResource
@Inject @Inject
PodService podService; PodService podService;
@Inject
ProcessRunner processRunner;
@GET @GET
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
public List<String> getLogs(@PathParam("pod-id") String podId) public List<KubernetesLog> getLogs(@PathParam("pod-id") String podId)
{ {
Optional<Pod> podOptional = podService.findPodById(podId); Optional<Pod> podOptional = podService.findPodById(podId);
if(podOptional.isPresent()) if(podOptional.isPresent())
{ {
Pod pod = podOptional.get(); Pod pod = podOptional.get();
return List.of(clientProvider.getClient().pods().inNamespace(pod.getMetadata().getNamespace()).withName(pod.getMetadata().getName()).getLog().split("\\r?\\n")); return getLogs(pod, null);
} }
throw new NotFoundException(); throw new NotFoundException();
} }
public List<KubernetesLog> getLogs(Pod pod, LocalDateTime from)
{
String command = String.format("kubectl logs %s -n %s --timestamps", pod.getMetadata().getName(), pod.getMetadata().getNamespace());
List<KubernetesLog> result = new ArrayList<>();
List<String> logs = processRunner.runToLines(command);
for (String log : logs)
{
int indexFirstSpace = log.indexOf(" ");
if (indexFirstSpace != -1)
{
String timestampRaw = log.substring(0, indexFirstSpace);
String message = log.substring(indexFirstSpace).trim();
try
{
result.add(new KubernetesLog(LocalDateTime.parse(timestampRaw, DateTimeFormatter.ISO_DATE_TIME), message));
}
catch (Exception e)
{
}
}
}
return result;
}
} }

View File

@ -14,25 +14,42 @@ import jakarta.ws.rs.core.MediaType;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List; import java.util.List;
@Path("/pods") @Path("/pods")
@Startup @Startup
@ApplicationScoped @ApplicationScoped
@Blocking
@Authenticated
public class PodResource public class PodResource
{ {
@Inject @Inject
PodService podService; PodService podService;
@DELETE @Inject
@Produces ProcessRunner processRunner;
@Consumes
@Path("/{namespace}/{name}")
@RolesAllowed({"admin", "maintainer"})
public void getEnv()
{
@GET
@Produces(MediaType.APPLICATION_JSON)
@Consumes
@Blocking
@Path("/{namespace}/{name}/env")
public List<EnvVar> getEnv(@PathParam("namespace") String namespace, @PathParam("name") String name)
{
return getVars(podService.findByNameAndNamespace(name, namespace));
}
private List<EnvVar> getVars(Pod pod)
{
List<EnvVar> result = new ArrayList<>();
List<String> lines = processRunner.runToLines(String.format("kubectl exec -it %s -n %s -- env", pod.getMetadata().getName(), pod.getMetadata().getNamespace()));
for (String line : lines)
{
int indexOfFirstEquals = line.indexOf("=");
if (indexOfFirstEquals != -1)
{
result.add(new EnvVar(line.substring(0, indexOfFirstEquals), line.substring(indexOfFirstEquals + 1)));
}
}
return result;
} }
} }

View File

@ -10,7 +10,6 @@ import java.io.InputStreamReader;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@ApplicationScoped @ApplicationScoped
public class ProcessRunner public class ProcessRunner
@ -18,14 +17,30 @@ public class ProcessRunner
@Inject @Inject
Logger LOG; Logger LOG;
public String run(String command) throws IOException, InterruptedException public String runToText(String command)
{
return String.join("\n", runToLines(command));
}
public List<String> runToLines(String command)
{ {
LOG.infof("Running command: %s", command); LOG.infof("Running command: %s", command);
ProcessBuilder pb = new ProcessBuilder(command.split("\\s+")); ProcessBuilder pb = new ProcessBuilder(command.split("\\s+"));
pb.redirectErrorStream(true); pb.redirectErrorStream(true);
Process p = pb.start(); try
{
Process p = pb.start();
return runAndCollectLogs(p);
}
catch (IOException | InterruptedException e)
{
throw new RuntimeException("Failed to run command " + command);
}
}
private List<String> runAndCollectLogs(Process p) throws InterruptedException
{
List<String> text = new ArrayList<>(); List<String> text = new ArrayList<>();
try(BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream()))) try(BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream())))
{ {
@ -35,20 +50,19 @@ public class ProcessRunner
text.add(line); text.add(line);
} }
} }
catch (IOException e)
{
throw new RuntimeException(e);
}
boolean endedInTime = p.waitFor(10, TimeUnit.SECONDS); boolean endedInTime = p.waitFor(10, TimeUnit.SECONDS);
p.destroy();
if (endedInTime) if (endedInTime)
{ {
int exitCode = p.exitValue(); int exitCode = p.exitValue();
if(exitCode == 0) if(exitCode == 0)
{ {
return String.join("\n", text); return text;
} }
} }
else throw new InterruptedException();
{
LOG.error("Process did not end in time.");
}
throw new RuntimeException("Error executing command: " + command);
} }
} }

View File

@ -6,7 +6,6 @@ import jakarta.inject.Inject;
import jakarta.ws.rs.*; import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.MediaType;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.jboss.resteasy.reactive.common.NotImplementedYet;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
@ -38,6 +37,9 @@ public class ResourceResource
@Inject @Inject
NodeService nodeService; NodeService nodeService;
@Inject
SecretService secretService;
@GET @GET
public List<?> get(@PathParam("resource") String resourceType) public List<?> get(@PathParam("resource") String resourceType)
{ {
@ -122,6 +124,10 @@ public class ResourceResource
{ {
return nodeService; return nodeService;
} }
case ResourceType.SECRET ->
{
return secretService;
}
default -> default ->
{ {
LOG.errorf("Invalid resource type %s.", resourceType); LOG.errorf("Invalid resource type %s.", resourceType);

View File

@ -9,4 +9,5 @@ public class ResourceType
public static final String POD = "pods"; public static final String POD = "pods";
public static final String CUSTOM_RESOURCE_DEFINITION = "custom-resource-definitions"; public static final String CUSTOM_RESOURCE_DEFINITION = "custom-resource-definitions";
public static final String NODE = "nodes"; public static final String NODE = "nodes";
public static final String SECRET = "secrets";
} }

View File

@ -0,0 +1,73 @@
package dev.dinauer.monitoring;
import dev.dinauer.ProcessRunner;
import dev.dinauer.monitoring.nodes.NodeStats;
import dev.dinauer.utils.ClientProvider;
import io.fabric8.kubernetes.api.model.Node;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import java.util.ArrayList;
import java.util.List;
@ApplicationScoped
public class TopNodesService
{
@Inject
ClientProvider clientProvider;
@Inject
ProcessRunner processRunner;
public List<NodeStats> findAll()
{
List<NodeStats> result = new ArrayList<>();
List<String> stats = runTopNodesCommand();
for(String nodeName : stats)
{
String[] parts = nodeName.split("\\s+");
if(parts.length == 5)
{
String name = parts[0];
Node node = clientProvider.getClient().nodes().withName(name).get();
Integer absoluteCpu = extractInteger(parts[1]);
Integer relativeCpu = extractInteger(parts[2]);
Integer absoluteMemory = extractMemory(parts[3]);
Integer relativeMemory = extractInteger(parts[4]);
result.add(new NodeStats(node, absoluteCpu, relativeCpu, Integer.parseInt(node.getStatus().getAllocatable().get("cpu").getAmount()) * 1000, absoluteMemory, relativeMemory, extractMemory(node.getStatus().getAllocatable().get("memory").getAmount())));
}
}
return result;
}
private List<String> runTopNodesCommand()
{
String command = String.format("kubectl --kubeconfig=%s top nodes --no-headers", clientProvider.pathToKubeconfig());
return processRunner.runToLines(command);
}
private Integer extractInteger(String input)
{
return Integer.valueOf(input.replace("m", "").replace("%", ""));
}
private Integer extractMemory(String input)
{
if(input.contains("Ki"))
{
return Integer.parseInt(input.replace("Ki", ""));
}
if(input.contains("Mi"))
{
return Integer.parseInt(input.replace("Mi", "")) * 1024;
}
if(input.contains("Gi"))
{
return Integer.parseInt(input.replace("Gi", "")) * 1024 * 1024;
}
return Integer.parseInt(input);
}
}

View File

@ -1,7 +1,9 @@
package dev.dinauer.monitoring.nodes; package dev.dinauer.monitoring.nodes;
import dev.dinauer.ProcessRunner; import dev.dinauer.ProcessRunner;
import dev.dinauer.monitoring.TopNodesService;
import dev.dinauer.monitoring.indexing.IndexingService; import dev.dinauer.monitoring.indexing.IndexingService;
import dev.dinauer.service.NodeService;
import dev.dinauer.utils.ClientProvider; import dev.dinauer.utils.ClientProvider;
import io.smallrye.common.annotation.Blocking; import io.smallrye.common.annotation.Blocking;
import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.context.ApplicationScoped;
@ -15,65 +17,22 @@ import java.util.Map;
public class NodeMonitoringService public class NodeMonitoringService
{ {
@Inject @Inject
ClientProvider clientProvider; TopNodesService topNodesService;
@Inject
ProcessRunner processRunner;
@Inject @Inject
IndexingService indexingService; IndexingService indexingService;
public void run() throws IOException, InterruptedException public void run() throws IOException, InterruptedException
{ {
List<NodeStats> result = new ArrayList<>(); List<NodeStats> nodes = topNodesService.findAll();
for (NodeStats node : nodes)
String[] stats = getTopNodes().split("\n");
for(String nodeName : stats)
{ {
String[] parts = nodeName.split("\\s+"); Map<String, Long> metrics = Map.ofEntries(
if(parts.length == 5) Map.entry("RELATIVE_CPU", (long) node.relativeCpuUsage()),
{ Map.entry("RELATIVE_MEMORY", (long) node.relativeMemory()),
String name = parts[0]; Map.entry("ABSOLUTE_MEMORY", (long) node.absoluteMemory()),
String node = clientProvider.getClient().nodes().withName(name).get().getMetadata().getUid(); Map.entry("ABSOLUTE_CPU", (long) node.absoluteCpuUsage()));
int absoluteCpu = extractInteger(parts[1]); indexingService.index(String.format("NODE-%s", node), "NODE_METRICS", metrics);
int relativeCpu = extractInteger(parts[2]);
int absoluteMemory = extractMemory(parts[3]);
int relativeMemory = extractInteger(parts[4]);
Map<String, Long> metrics = Map.ofEntries(
Map.entry("RELATIVE_CPU", (long) relativeCpu),
Map.entry("RELATIVE_MEMORY", (long) relativeMemory),
Map.entry("ABSOLUTE_MEMORY", (long) absoluteMemory));
indexingService.index(String.format("NODE-%s", node), "NODE_METRICS", metrics);
}
} }
} }
private String getTopNodes() throws IOException, InterruptedException
{
String command = String.format("kubectl --kubeconfig=%s top nodes --no-headers", clientProvider.pathToKubeconfig());
return processRunner.run(command);
}
private Integer extractInteger(String input)
{
return Integer.valueOf(input.replace("m", "").replace("%", ""));
}
private Integer extractMemory(String input)
{
if(input.contains("Ki"))
{
return Integer.parseInt(input.replace("Ki", ""));
}
if(input.contains("Mi"))
{
return Integer.parseInt(input.replace("Mi", "")) * 1024;
}
if(input.contains("Gi"))
{
return Integer.parseInt(input.replace("Gi", "")) * 1024 * 1024;
}
return Integer.parseInt(input);
}
} }

View File

@ -31,7 +31,7 @@ public class CustomResourceDefinitionService implements ResourceService<CustomRe
@Override @Override
public List<CustomResourceDefinition> findByNamespace(String namespace) public List<CustomResourceDefinition> findByNamespace(String namespace)
{ {
throw new UnsupportedOperationException(); return findAll();
} }
@Override @Override

View File

@ -1,122 +1,22 @@
package dev.dinauer.service; package dev.dinauer.service;
import dev.dinauer.monitoring.TopNodesService;
import dev.dinauer.monitoring.nodes.NodeStats; import dev.dinauer.monitoring.nodes.NodeStats;
import dev.dinauer.utils.ClientProvider;
import io.fabric8.kubernetes.api.model.Node;
import io.smallrye.common.annotation.Blocking;
import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import org.jboss.resteasy.reactive.common.NotImplementedYet; import org.jboss.resteasy.reactive.common.NotImplementedYet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.TimeUnit;
@ApplicationScoped @ApplicationScoped
public class NodeService implements ResourceService<NodeStats> public class NodeService implements ResourceService<NodeStats>
{ {
private static final Logger LOG = LoggerFactory.getLogger(NodeService.class);
@Inject @Inject
ClientProvider clientProvider; TopNodesService topNodesService;
@GET
@Blocking
@Produces(MediaType.APPLICATION_JSON)
public List<NodeStats> findAll() public List<NodeStats> findAll()
{ {
List<NodeStats> result = new ArrayList<>(); return topNodesService.findAll();
List<String> stats = null;
try
{
stats = getTopNodes();
}
catch (IOException e)
{
throw new RuntimeException(e);
}
catch (InterruptedException e)
{
throw new RuntimeException(e);
}
for(String nodeName : stats)
{
String[] parts = nodeName.split("\\s+");
if(parts.length == 5)
{
String name = parts[0];
Node node = clientProvider.getClient().nodes().withName(name).get();
Integer absoluteCpu = extractInteger(parts[1]);
Integer relativeCpu = extractInteger(parts[2]);
Integer absoluteMemory = extractMemory(parts[3]);
Integer relativeMemory = extractInteger(parts[4]);
result.add(new NodeStats(node, absoluteCpu, relativeCpu, Integer.parseInt(node.getStatus().getAllocatable().get("cpu").getAmount()) * 1000, absoluteMemory, relativeMemory, extractMemory(node.getStatus().getAllocatable().get("memory").getAmount())));
}
}
return result;
}
private List<String> getTopNodes() throws IOException, InterruptedException
{
List<String> commands = List.of("kubectl", String.format("--kubeconfig=%s", clientProvider.pathToKubeconfig()), "top", "nodes", "--no-headers");
LOG.info("Executing command: {}", String.join(" ", commands));
ProcessBuilder pb = new ProcessBuilder(commands);
Process p = pb.start();
List<String> text = new ArrayList<>();
try(BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream())))
{
String line;
while((line = br.readLine()) != null)
{
text.add(line);
}
}
boolean hasEndedInTime = p.waitFor(10, TimeUnit.SECONDS);
p.destroy();
if (hasEndedInTime)
{
int exitCode = p.waitFor();
if(exitCode == 0)
{
LOG.info("Found {} nodes", text.size());
return text;
}
}
throw new RuntimeException("Failed to retrieve top nodes.");
}
private Integer extractInteger(String input)
{
return Integer.valueOf(input.replace("m", "").replace("%", ""));
}
private Integer extractMemory(String input)
{
if(input.contains("Ki"))
{
return Integer.parseInt(input.replace("Ki", ""));
}
if(input.contains("Mi"))
{
return Integer.parseInt(input.replace("Mi", "")) * 1024;
}
if(input.contains("Gi"))
{
return Integer.parseInt(input.replace("Gi", "")) * 1024 * 1024;
}
return Integer.parseInt(input);
} }
@Override @Override
@ -134,7 +34,7 @@ public class NodeService implements ResourceService<NodeStats>
@Override @Override
public List<NodeStats> findByNamespace(String namespace) public List<NodeStats> findByNamespace(String namespace)
{ {
throw new NotImplementedYet(); return findAll();
} }
@Override @Override

View File

@ -0,0 +1,47 @@
package dev.dinauer.service;
import dev.dinauer.utils.ClientProvider;
import io.fabric8.kubernetes.api.model.Secret;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import org.jboss.resteasy.reactive.common.NotImplementedYet;
import java.util.List;
import java.util.Optional;
@ApplicationScoped
public class SecretService implements ResourceService<Secret>
{
@Inject
ClientProvider clientProvider;
@Override
public void delete(String name, String namespace)
{
throw new NotImplementedYet();
}
@Override
public Secret findByNameAndNamespace(String name, String namespace)
{
throw new NotImplementedYet();
}
@Override
public List<Secret> findByNamespace(String namespace)
{
return clientProvider.getClient().secrets().inNamespace(namespace).list().getItems();
}
@Override
public List<Secret> findAll()
{
return clientProvider.getClient().secrets().inAnyNamespace().list().getItems();
}
@Override
public Optional<Secret> findOptionalByNameAndNamespace(String name, String namespace)
{
throw new NotImplementedYet();
}
}