Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Git and IDE
.git
.gitignore
**/.idea
**/.vscode

# Build outputs (rebuilt in the image)
**/target
**/node_modules
www/node_modules
www/dist

# Docs and local tooling
*.md
.cursor
docker/cortex

# Keep project/build.properties, build.sbt, sources — needed for sbt
95 changes: 95 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# syntax=docker/dockerfile:1
#
# Multi-stage image: compile Scala/Play + AngularJS frontend, then run on the official Cortex base layer.
#
# Build:
# docker build -t cortex:local .
#
# Run (example; point Elasticsearch at your cluster):
# docker run --rm -p 9001:9001 cortex:local
#
# Requires network access during build (Maven/Ivy/npm, Debian/Corretto/Docker apt repos).
#
# Runtime matches project/DockerSettings.scala / builds/docker/Dockerfile (Debian + Corretto 11 + Docker).
# To use the prebuilt base image instead (if you can pull it): replace the runtime FROM below with
# FROM ghcr.io/strangebee/cortex-baselayer:rolling
# and remove the duplicate RUN that installs Java/Docker/user (keep COPY and chmod).

# -----------------------------------------------------------------------------
# Builder: JDK 11, sbt, Node (webpack), and bower (needed by www npm postinstall scripts)
# -----------------------------------------------------------------------------
FROM eclipse-temurin:11-jdk-jammy AS builder

ENV LANG=C.UTF-8 \
SBT_OPTS="-Xmx4096m -Xss2m"

RUN apt-get update \
&& apt-get install -y --no-install-recommends curl git gnupg ca-certificates \
&& rm -rf /var/lib/apt/lists/*

# sbt (version aligned with project/build.properties)
RUN curl -fsSL "https://github.com/sbt/sbt/releases/download/v1.11.7/sbt-1.11.7.tgz" \
| tar xz -C /usr/local

ENV PATH="/usr/local/sbt/bin:${PATH}"

# Node.js 20 (for www: npm install + webpack via sbt)
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
&& apt-get update \
&& apt-get install -y --no-install-recommends nodejs \
&& rm -rf /var/lib/apt/lists/*

# css-spaces and other legacy deps invoke `bower` in postinstall
RUN npm install -g bower

WORKDIR /build

COPY . .

RUN sbt -batch stage

# -----------------------------------------------------------------------------
# Runtime: Debian + Amazon Corretto 11 + Docker CLI (same idea as builds/docker/Dockerfile)
# -----------------------------------------------------------------------------
FROM debian:13-slim

LABEL org.opencontainers.image.source="https://github.com/TheHive-Project/Cortex"
LABEL org.opencontainers.image.description="Cortex built from source"

WORKDIR /opt/cortex

ENV JAVA_HOME=/usr/lib/jvm/java-11-amazon-corretto

RUN apt-get update && apt-get upgrade -y \
&& apt-get install -y --no-install-recommends ca-certificates curl gnupg \
&& curl -fL https://apt.corretto.aws/corretto.key | gpg --dearmor -o /usr/share/keyrings/corretto.gpg \
&& echo 'deb [signed-by=/usr/share/keyrings/corretto.gpg] https://apt.corretto.aws stable main' > /etc/apt/sources.list.d/corretto.list \
&& apt-get update \
&& apt-get install -y --no-install-recommends java-11-amazon-corretto-jdk \
&& curl -fsSL https://download.docker.com/linux/debian/gpg -o /usr/share/keyrings/docker.asc \
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker.asc] https://download.docker.com/linux/debian $(. /etc/os-release && echo "$VERSION_CODENAME") stable" > /etc/apt/sources.list.d/docker.list \
&& apt-get update \
&& apt-get install -y --no-install-recommends docker-ce docker-ce-cli containerd.io docker-ce-rootless-extras uidmap iproute2 fuse-overlayfs \
&& groupadd -g 1001 cortex \
&& useradd --system --uid 1001 --gid 1001 --groups docker cortex -d /opt/cortex \
&& mkdir -m 777 /var/log/cortex \
&& chmod 666 /etc/subuid /etc/subgid \
&& rm -rf /var/lib/apt/lists/* \
&& apt-get clean -y -q \
&& apt-get autoremove -y -q

COPY --from=builder --chown=root:root /build/target/universal/stage/ /opt/cortex/

COPY --from=builder /build/package/docker/entrypoint /opt/cortex/entrypoint
COPY --from=builder /build/conf/application.sample /etc/cortex/application.conf
COPY --from=builder /build/package/logback.xml /etc/cortex/logback.xml

RUN chmod +x /opt/cortex/bin/cortex /opt/cortex/entrypoint \
&& chown -R cortex:cortex /etc/cortex

VOLUME /var/lib/docker

EXPOSE 9001

ENTRYPOINT ["/opt/cortex/entrypoint"]
CMD []
37 changes: 30 additions & 7 deletions app/org/thp/cortex/services/K8sJobRunnerSrv.scala
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import play.api.{Configuration, Logger}
import java.nio.file._
import java.util
import javax.inject.{Inject, Singleton}
import scala.annotation.tailrec
import scala.concurrent.duration.{DurationInt, FiniteDuration}
import scala.jdk.CollectionConverters._
import scala.util.{Failure, Success, Try}
Expand All @@ -34,6 +35,27 @@ class K8sJobRunnerSrv(

lazy val logger: Logger = Logger(getClass)

/** Kubernetes label values must be ≤63 chars and start/end with [A-Za-z0-9] (see label value validation). */
private def isKubernetesLabelAlphanum(c: Char): Boolean =
(c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')

private def kubernetesLabelValue(raw: String): String = {
@tailrec
def trimEnds(s: String): String = {
if (s.isEmpty) s
else if (!isKubernetesLabelAlphanum(s.head)) trimEnds(s.tail)
else if (!isKubernetesLabelAlphanum(s.last)) trimEnds(s.init)
else s
}
val trimmed = trimEnds(raw)
if (trimmed.isEmpty) "0"
else if (trimmed.length <= 63) trimmed
else trimEnds(trimmed.take(63)) match {
case t if t.nonEmpty => t
case _ => "0"
}
}

lazy val isAvailable: Boolean =
Try {
val ver = client.getVersion
Expand All @@ -55,10 +77,11 @@ class K8sJobRunnerSrv(
// make the default longer than likely values, but still not infinite
val timeout_or_default = timeout.getOrElse(8.hours)
// https://kubernetes.io/docs/concepts/overview/working-with-objects/names/
// FIXME: this collapses case, jeopardizing the uniqueness of the identifier.
// LDH: lowercase, digits, hyphens.
val ldh_jobid = job.id.toLowerCase().replace('_', '-')
// RFC 1123 names: must start/end with alphanumeric; same trimming as label values.
val ldh_jobid = kubernetesLabelValue(job.id.toLowerCase().replace('_', '-'))
val kjobName = "neuron-job-" + ldh_jobid
val jobLabel = kubernetesLabelValue(job.id)
val workerLabel = kubernetesLabelValue(job.workerId())
val pvcvs = new PersistentVolumeClaimVolumeSourceBuilder()
.withClaimName(persistentVolumeClaimName.get)
.withReadOnly(false)
Expand All @@ -69,8 +92,8 @@ class K8sJobRunnerSrv(
.withNewMetadata()
.withName(kjobName)
.withLabels(Map(
"cortex-job-id" -> job.id,
"cortex-worker-id" -> job.workerId(),
"cortex-job-id" -> jobLabel,
"cortex-worker-id" -> workerLabel,
"cortex-neuron-job" -> "true").asJava)
.endMetadata()
.withNewSpec()
Expand Down Expand Up @@ -124,7 +147,7 @@ class K8sJobRunnerSrv(
s" image : $dockerImage\n" +
s" mount : pvc $persistentVolumeClaimName subdir $relativeJobDirectory as /job" +
created_env.map(ev => s"\n env : ${ev.getName} = ${ev.getValue}").mkString)
val ended_kjob = client.batch().v1().jobs().withLabel("cortex-job-id", job.id)
val ended_kjob = client.batch().v1().jobs().withLabel("cortex-job-id", jobLabel)
.waitUntilCondition(x => Option(x).flatMap(j =>
Option(j.getStatus).flatMap(s =>
Some(s.getConditions.asScala.map(_.getType).exists(t =>
Expand All @@ -140,7 +163,7 @@ class K8sJobRunnerSrv(
}
// let's find the job by the attribute we know is fundamentally
// unique, rather than one constructed from it
val deleted: util.List[StatusDetails] = client.batch().v1().jobs().withLabel("cortex-job-id", job.id).delete()
val deleted: util.List[StatusDetails] = client.batch().v1().jobs().withLabel("cortex-job-id", jobLabel).delete()
if(!deleted.isEmpty) {
logger.info(s"Deleted Kubernetes Job for job ${job.id}")
} else {
Expand Down