diff --git a/src/main/java/dev/dinauer/LogResource.java b/src/main/java/dev/dinauer/LogResource.java index 49b492e..37ed038 100644 --- a/src/main/java/dev/dinauer/LogResource.java +++ b/src/main/java/dev/dinauer/LogResource.java @@ -51,7 +51,7 @@ public class LogResource public List 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 result = new ArrayList<>(); List logs = processRunner.runToLines(command); for (String log : logs) diff --git a/src/main/java/dev/dinauer/PodResource.java b/src/main/java/dev/dinauer/PodResource.java index 3c192dd..2431be9 100644 --- a/src/main/java/dev/dinauer/PodResource.java +++ b/src/main/java/dev/dinauer/PodResource.java @@ -44,7 +44,7 @@ public class PodResource private List getVars(Pod pod) { List result = new ArrayList<>(); - List lines = processRunner.runToLines(String.format("kubectl --kubeconfig=%s exec -it %s -n %s -- env", clientProvider.pathToKubeconfig(), pod.getMetadata().getName(), pod.getMetadata().getNamespace())); + List 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("="); diff --git a/src/main/java/dev/dinauer/inspect/websocket/ResourceWebsocket.java b/src/main/java/dev/dinauer/inspect/websocket/ResourceWebsocket.java index 6632914..c8d0e06 100644 --- a/src/main/java/dev/dinauer/inspect/websocket/ResourceWebsocket.java +++ b/src/main/java/dev/dinauer/inspect/websocket/ResourceWebsocket.java @@ -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 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 Watcher 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 purpose = token.claim("purpose"); + if (purpose.isPresent()) + { + return purpose.get().equals("ws:connect"); + } + return false; + } } diff --git a/src/main/java/dev/dinauer/inspect/websocket/WebsocketSessionResource.java b/src/main/java/dev/dinauer/inspect/websocket/WebsocketSessionResource.java new file mode 100644 index 0000000..7869a88 --- /dev/null +++ b/src/main/java/dev/dinauer/inspect/websocket/WebsocketSessionResource.java @@ -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> namespaces = token.claim("namespaces"); + if (namespaces.isPresent()) + { + builder = builder.claim("namespaces", namespaces.get()); + } + Optional> resources = token.claim("resources"); + if (resources.isPresent()) + { + builder = builder.claim("resources", resources.get()); + } + return builder.claim("purpose", "ws:connect").sign(); + } + throw new UnauthorizedException(); + } +} diff --git a/src/main/java/dev/dinauer/login/LoginResource.java b/src/main/java/dev/dinauer/login/LoginResource.java index c1cf493..2c9b7d6 100644 --- a/src/main/java/dev/dinauer/login/LoginResource.java +++ b/src/main/java/dev/dinauer/login/LoginResource.java @@ -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(); diff --git a/src/main/java/dev/dinauer/monitoring/TopNodesService.java b/src/main/java/dev/dinauer/monitoring/TopNodesService.java index c98c8b4..b4c26eb 100644 --- a/src/main/java/dev/dinauer/monitoring/TopNodesService.java +++ b/src/main/java/dev/dinauer/monitoring/TopNodesService.java @@ -59,7 +59,7 @@ public class TopNodesService private List 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); } diff --git a/src/main/java/dev/dinauer/utils/ClientProvider.java b/src/main/java/dev/dinauer/utils/ClientProvider.java index 27fc724..308817b 100644 --- a/src/main/java/dev/dinauer/utils/ClientProvider.java +++ b/src/main/java/dev/dinauer/utils/ClientProvider.java @@ -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(); } } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 4d2195e..854b2ba 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -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