🔥 Remove kubeconfig and rely on service account

This commit is contained in:
andreas.dinauer 2025-11-08 10:41:55 +01:00
parent a42768ca49
commit 33f95bad5a
8 changed files with 148 additions and 40 deletions

View File

@ -51,7 +51,7 @@ public class LogResource
public List<KubernetesLog> getLogs(Pod pod, LocalDateTime from)
{
String command = String.format("kubectl --kubeconfig=%s logs %s -n %s --timestamps --tail=1000", clientProvider.pathToKubeconfig(), pod.getMetadata().getName(), pod.getMetadata().getNamespace());
String command = String.format("kubectl logs %s -n %s --timestamps --tail=1000", pod.getMetadata().getName(), pod.getMetadata().getNamespace());
List<KubernetesLog> result = new ArrayList<>();
List<String> logs = processRunner.runToLines(command);
for (String log : logs)

View File

@ -44,7 +44,7 @@ public class PodResource
private List<EnvVar> getVars(Pod pod)
{
List<EnvVar> result = new ArrayList<>();
List<String> lines = processRunner.runToLines(String.format("kubectl --kubeconfig=%s exec -it %s -n %s -- env", clientProvider.pathToKubeconfig(), pod.getMetadata().getName(), pod.getMetadata().getNamespace()));
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("=");

View File

@ -8,19 +8,29 @@ import dev.dinauer.utils.ClientProvider;
import io.fabric8.kubernetes.client.Watch;
import io.fabric8.kubernetes.client.Watcher;
import io.fabric8.kubernetes.client.WatcherException;
import io.quarkus.security.UnauthorizedException;
import io.smallrye.jwt.auth.principal.JWTParser;
import io.smallrye.jwt.auth.principal.ParseException;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.websocket.CloseReason;
import jakarta.websocket.OnClose;
import jakarta.websocket.OnOpen;
import jakarta.websocket.Session;
import jakarta.websocket.server.PathParam;
import jakarta.websocket.server.ServerEndpoint;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.WebApplicationException;
import org.eclipse.microprofile.context.ManagedExecutor;
import org.eclipse.microprofile.jwt.JsonWebToken;
import org.jboss.logging.Logger;
import java.io.IOException;
import java.net.URL;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@ServerEndpoint("/watch/{resource-type}/{namespace}")
@ApplicationScoped
@ -28,44 +38,58 @@ public class ResourceWebsocket
{
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
@Inject
Logger LOG;
@Inject
ClientProvider clientProvider;
@Inject
ManagedExecutor executor;
@Inject
JWTParser parser;
private final Map<Session, Watch> sessions = new HashMap<>();
@OnOpen
public void onOpen(Session session, @PathParam("resource-type") String resourceType, @PathParam("namespace") String namespace)
public void onOpen(Session session, @PathParam("resource-type") String resourceType, @PathParam("namespace") String namespace) throws ParseException
{
executor.runAsync(() ->
JsonWebToken token = getToken(session.getQueryString());
if (isValid(token))
{
if (ResourceType.POD.equals(resourceType))
executor.runAsync(() ->
{
String version = clientProvider.getClient().pods().inAnyNamespace().list().getMetadata().getResourceVersion();
if (isGlobal(namespace))
if (ResourceType.POD.equals(resourceType))
{
send(session, EventType.INIT, clientProvider.getClient().pods().inAnyNamespace().list().getItems());
sessions.put(session, clientProvider.getClient().pods().inAnyNamespace().withResourceVersion(version).watch(getWatcher(session)));
String version = clientProvider.getClient().pods().inAnyNamespace().list().getMetadata().getResourceVersion();
if (isGlobal(namespace))
{
send(session, EventType.INIT, clientProvider.getClient().pods().inAnyNamespace().list().getItems());
sessions.put(session, clientProvider.getClient().pods().inAnyNamespace().withResourceVersion(version).watch(getWatcher(session)));
}
else
{
send(session, EventType.INIT, clientProvider.getClient().pods().inNamespace(namespace).list().getItems());
sessions.put(session, clientProvider.getClient().pods().inNamespace(namespace).withResourceVersion(version).watch(getWatcher(session)));
}
}
else
if (ResourceType.CONFIG_MAP.equals(resourceType))
{
sessions.put(session, clientProvider.getClient().pods().inNamespace(namespace).watch(getWatcher(session)));
String version = clientProvider.getClient().configMaps().inAnyNamespace().list().getMetadata().getResourceVersion();
if (isGlobal(namespace))
{
send(session, EventType.INIT, clientProvider.getClient().configMaps().inAnyNamespace().list().getItems());
sessions.put(session, clientProvider.getClient().configMaps().inAnyNamespace().withResourceVersion(version).watch(getWatcher(session)));
}
else
{
send(session, EventType.INIT, clientProvider.getClient().configMaps().inNamespace(namespace).list().getItems());
sessions.put(session, clientProvider.getClient().configMaps().inNamespace(namespace).withResourceVersion(version).watch(getWatcher(session)));
}
}
}
if (ResourceType.CONFIG_MAP.equals(resourceType))
{
if (isGlobal(namespace))
{
sessions.put(session, clientProvider.getClient().configMaps().inAnyNamespace().watch(getWatcher(session)));
}
else
{
sessions.put(session, clientProvider.getClient().configMaps().inNamespace(namespace).watch(getWatcher(session)));
}
}
});
});
}
}
@OnClose
@ -76,6 +100,14 @@ public class ResourceWebsocket
{
watch.close();
}
try
{
session.close();
}
catch (IOException e)
{
throw new RuntimeException(e);
}
}
private <T> Watcher<T> getWatcher(Session session)
@ -119,4 +151,31 @@ public class ResourceWebsocket
throw new RuntimeException(e);
}
}
private JsonWebToken getToken(String query) throws ParseException
{
for (String param : query.split("&"))
{
String[] sections = param.split("=", 2);
if (sections.length == 2)
{
if (sections[0].equals("token"))
{
return parser.parse(sections[1]);
}
}
}
LOG.error("Token cannot be null.");
throw new RuntimeException("Token cannot be null.");
}
private boolean isValid(JsonWebToken token)
{
Optional<String> purpose = token.claim("purpose");
if (purpose.isPresent())
{
return purpose.get().equals("ws:connect");
}
return false;
}
}

View File

@ -0,0 +1,51 @@
package dev.dinauer.inspect.websocket;
import com.arjuna.ats.internal.arjuna.Header;
import io.quarkus.security.Authenticated;
import io.quarkus.security.UnauthorizedException;
import io.smallrye.jwt.auth.principal.JWTParser;
import io.smallrye.jwt.auth.principal.ParseException;
import io.smallrye.jwt.build.Jwt;
import io.smallrye.jwt.build.JwtClaimsBuilder;
import jakarta.inject.Inject;
import jakarta.ws.rs.HeaderParam;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.SecurityContext;
import org.eclipse.microprofile.jwt.JsonWebToken;
import java.time.ZonedDateTime;
import java.util.List;
import java.util.Optional;
@Path("/websocket-session")
public class WebsocketSessionResource
{
@Inject
JsonWebToken token;
@POST
@Authenticated
@Produces(MediaType.TEXT_PLAIN)
public String getSession()
{
if (token != null)
{
JwtClaimsBuilder builder = Jwt.upn(token.getName()).issuer(token.getIssuer()).expiresAt(ZonedDateTime.now().plusSeconds(15).toInstant());
Optional<List<String>> namespaces = token.claim("namespaces");
if (namespaces.isPresent())
{
builder = builder.claim("namespaces", namespaces.get());
}
Optional<List<String>> resources = token.claim("resources");
if (resources.isPresent())
{
builder = builder.claim("resources", resources.get());
}
return builder.claim("purpose", "ws:connect").sign();
}
throw new UnauthorizedException();
}
}

View File

@ -1,5 +1,8 @@
package dev.dinauer.login;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.fabric8.kubernetes.api.model.ObjectMeta;
import io.smallrye.jwt.build.Jwt;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
@ -10,12 +13,15 @@ import org.jboss.logging.Logger;
import java.io.IOException;
import java.time.ZonedDateTime;
import java.util.List;
import java.util.Optional;
@Path("/login")
@ApplicationScoped
public class LoginResource
{
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
@Inject
Logger LOG;
@ -33,7 +39,11 @@ public class LoginResource
UserEntity user = userOptional.get();
if(BcryptUtil.matches(login.password(), user.getPassword()))
{
return Jwt.upn(user.getId()).expiresAt(ZonedDateTime.now().plusDays(15).toInstant()).groups(user.getRoles()).sign();
return Jwt
.upn(user.getId())
.expiresAt(ZonedDateTime.now().plusDays(15).toInstant())
.groups(user.getRoles())
.sign();
}
LOG.info("Cannot access user. Forbidden");
throw new ForbiddenException();

View File

@ -59,7 +59,7 @@ public class TopNodesService
private List<String> runTopNodesCommand()
{
String command = String.format("kubectl --kubeconfig=%s top nodes --no-headers", clientProvider.pathToKubeconfig());
String command = String.format("kubectl top nodes --no-headers");
return processRunner.runToLines(command);
}

View File

@ -14,19 +14,11 @@ import java.io.File;
@ApplicationScoped
public class ClientProvider
{
@ConfigProperty(name = "dev.dinauer.kobooboo.kubeconfigs.dir")
String configFilePath;
@Inject
Vertx vertx;
public KubernetesClient getClient()
{
return new KubernetesClientBuilder().withConfig(Config.fromKubeconfig(new File(configFilePath))).withHttpClientFactory(new VertxHttpClientFactory(vertx.getDelegate())).build();
}
public String pathToKubeconfig()
{
return configFilePath;
return new KubernetesClientBuilder().withHttpClientFactory(new VertxHttpClientFactory(vertx.getDelegate())).build();
}
}

View File

@ -3,13 +3,9 @@ quarkus.http.root-path=/api
%dev.quarkus.http.cors.origins=/.*/
%dev.quarkus.http.port=9090
%dev,test.dev.dinauer.kobooboo.kubeconfigs.dir=/var/lib/kubooboo/config
dev.dinauer.kubooboo.work.dir=/var/lib/kubooboo/work
%dev.dev.dinauer.kobooboo.kubeconfigs.dir=/home/andreas/.kube/config
%dev.dev.dinauer.kubooboo.work.dir=/home/andreas/Documents/dev/kubooboo/backend/src/main/resources/dev
%prod.dev.dinauer.kobooboo.kubeconfigs.dir=${KUBECONFIG_LOCATION}
# Keys
%prod.smallrye.jwt.sign.key.location=${PRIVATE_KEY_LOCATION}
%prod.mp.jwt.verify.publickey.location=${PUBLIC_KEY_LOCATION}
@ -18,7 +14,7 @@ dev.dinauer.kubooboo.work.dir=/var/lib/kubooboo/work
%dev.mp.jwt.verify.publickey.location=publicKey.pem
# Postgres
%dev.quarkus.datasource.db-kind = postgresql
quarkus.datasource.db-kind = postgresql
%dev.quarkus.datasource.username = postgres
%dev.quarkus.datasource.password = postgres
%dev.quarkus.datasource.jdbc.url = jdbc:postgresql://localhost:6666/postgres