diff --git a/.gitignore b/.gitignore
index 74257689..60e2c583 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,3 +4,28 @@ logs
.idea
*.iml
+
+.gradle
+**/build/
+!src/**/build/
+
+# Ignore Gradle GUI config
+gradle-app.setting
+
+# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
+!gradle-wrapper.jar
+
+# Avoid ignore Gradle wrappper properties
+!gradle-wrapper.properties
+
+# Cache of project
+.gradletasknamecache
+
+# Eclipse Gradle plugin generated files
+# Eclipse Core
+.project
+# JDT-specific (Eclipse Java Development Tools)
+.classpath
+
+*.log
+*.gz
\ No newline at end of file
diff --git a/build.gradle.kts b/build.gradle.kts
new file mode 100644
index 00000000..94e2a595
--- /dev/null
+++ b/build.gradle.kts
@@ -0,0 +1,173 @@
+//import com.ewerk.gradle.plugins.tasks.QuerydslCompile
+
+plugins {
+ application
+ idea
+ id("com.google.cloud.tools.jib") version "3.2.0"
+ id("io.freefair.lombok") version "6.4.1"
+ id("io.spring.dependency-management") version "1.0.11.RELEASE"
+ id("org.springframework.boot") version "2.6.6"
+}
+
+group = "hk.edu.polyu.comp.vlabcontroller"
+version = "1.0.3"
+description = "VLabController"
+java.sourceCompatibility = JavaVersion.VERSION_11
+
+val springCloudVersion by extra("2021.0.1")
+
+configurations {
+ implementation.configure {
+ exclude(module = "spring-boot-starter-tomcat")
+ exclude("org.apache.tomcat")
+ }
+ compileOnly {
+ extendsFrom(configurations.annotationProcessor.get())
+ }
+}
+
+springBoot {
+ buildInfo()
+}
+
+repositories {
+ maven(url = "https://repo.spring.io/release")
+ mavenCentral()
+}
+
+dependencyManagement {
+ imports {
+ mavenBom("org.springframework.cloud:spring-cloud-dependencies:$springCloudVersion")
+ }
+}
+
+dependencies {
+ var springBoot = run {
+ compileOnly("org.springframework.boot", "spring-boot-devtools")
+ implementation("org.springframework.boot", "spring-boot-configuration-processor")
+ implementation("org.springframework.boot", "spring-boot-starter-actuator")
+ implementation("org.springframework.boot", "spring-boot-starter-data-mongodb")
+ implementation("org.springframework.boot", "spring-boot-starter-data-redis")
+ implementation("org.springframework.boot", "spring-boot-starter-jdbc")
+ implementation("org.springframework.boot", "spring-boot-starter-mail")
+ implementation("org.springframework.boot", "spring-boot-starter-security")
+ implementation("org.springframework.boot", "spring-boot-starter-thymeleaf")
+ implementation("org.springframework.boot", "spring-boot-starter-undertow")
+ implementation("org.springframework.boot", "spring-boot-starter-web")
+ implementation("org.springframework.boot", "spring-boot-starter-websocket")
+ implementation("org.springframework.cloud", "spring-cloud-context")
+ implementation("org.springframework.data", "spring-data-commons")
+ implementation("org.springframework.security", "spring-security-oauth2-client")
+ implementation("org.springframework.security", "spring-security-oauth2-jose")
+ implementation("org.springframework.security.oauth.boot", "spring-security-oauth2-autoconfigure")
+ implementation("org.springframework.session", "spring-session-data-redis")
+
+// compile("org.springframework.data:spring-data-mongodb")
+
+ testImplementation("org.springframework.boot", "spring-boot-starter-test")
+ testImplementation("org.springframework.boot", "spring-boot-starter-webflux")
+ testImplementation("org.springframework.security", "spring-security-test")
+ }
+
+ var database = run {
+ implementation("mysql", "mysql-connector-java", "8.0.27")
+ implementation("org.postgresql", "postgresql", "42.2.24")
+ implementation("org.xerial", "sqlite-jdbc", "3.36.0.3")
+ implementation("org.mongodb:mongodb-driver-sync:4.4.2")
+ implementation("org.mongodb:bson:4.4.2")
+ }
+
+ var javax = run {
+ implementation("javax.inject", "javax.inject", "1")
+ implementation("javax.json", "javax.json-api", "1.1.4")
+ implementation("javax.xml.bind", "jaxb-api", "2.3.1")
+ }
+
+ var queryDsl = run {
+ annotationProcessor("com.querydsl:querydsl-apt:5.0.0:general")
+ implementation("com.querydsl:querydsl-mongodb")
+ }
+
+ implementation("com.amazonaws", "aws-java-sdk-s3", "1.12.90")
+ implementation("com.fasterxml.jackson.datatype", "jackson-datatype-jsr353", "2.13.0")
+ implementation("com.google.guava", "guava", "31.1-jre")
+ implementation("io.fabric8", "kubernetes-client", "5.9.0")
+ implementation("io.micrometer", "micrometer-registry-influx", "1.7.5")
+ implementation("io.micrometer", "micrometer-registry-prometheus", "1.7.5")
+ implementation("io.vavr", "vavr", "0.10.4")
+ implementation("org.apache.commons", "commons-lang3", "3.12.0")
+ implementation("org.glassfish", "javax.json", "1.1.4")
+ implementation("org.jboss.xnio", "xnio-api", "3.8.4.Final")
+ implementation("org.keycloak", "keycloak-spring-security-adapter", "15.0.2")
+ implementation("org.thymeleaf.extras", "thymeleaf-extras-springsecurity5", "3.0.4.RELEASE")
+ implementation("com.ea.async:ea-async:1.2.3")
+
+ testImplementation("junit", "junit", "4.13.2")
+}
+
+jib {
+ from {
+ image = "ghcr.io/stevefan1999/vlab-controller-base"
+ }
+ to {
+ image = "ghcr.io/endangeredf1sh/vlab-controller:$version"
+ auth {
+ username = System.getenv("REGISTRY_USERNAME")
+ password = System.getenv("REGISTRY_PASSWORD")
+ }
+ }
+ container {
+ appRoot = "/opt/vlab-controller"
+ workingDirectory = "/opt/vlab-controller"
+ environment = mapOf(
+ "VLAB_USER" to "vlab",
+ "PROXY_TEMPLATEPATH" to "/opt/vlab-controller/resources/templates",
+ "SERVER_ERROR_WHITELABEL_ENABLED" to "false",
+ "TZ" to "Asia/Hong_Kong"
+ )
+ labels.put("maintainer", mapOf(
+ "Aiden ZHANG Wenyi" to "im.endangeredfish@gmail.com",
+ "Fan Chun Yin" to "stevefan1999@gmail.com"
+ ).map { "${it.key} <${it.value}>" }.joinToString { "," })
+ user = "vlab:vlab"
+ args = listOf(
+ "--spring.jmx.enabled=false",
+ "--spring.config.location=/etc/vlab-controller/config/application.yml"
+ )
+ jvmFlags = listOf(
+ "-server",
+ "-Djava.awt.headless=true",
+ "-XX:+UseStringDeduplication"
+ )
+ }
+ extraDirectories {
+ paths {
+ path {
+ setFrom("resources/templates")
+ into = "/opt/vlab-controller/resources/templates"
+ }
+ }
+ }
+}
+
+tasks.withType {
+ options.encoding = "UTF-8"
+}
+
+tasks.withType {
+ useJUnitPlatform()
+}
+
+tasks.getByName("jar") {
+ enabled = false
+}
+
+val runEaAsyncInstrumentation by tasks.registering(JavaExec::class) {
+ mainClass.set("com.ea.async.instrumentation.Main")
+ classpath = sourceSets.main.get().compileClasspath
+ args = listOf(buildDir.path)
+}
+
+val compileJava by tasks.existing(JavaCompile::class) {
+ finalizedBy(runEaAsyncInstrumentation)
+}
\ No newline at end of file
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 00000000..41d9927a
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 00000000..41dfb879
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
new file mode 100755
index 00000000..7ff1072a
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,234 @@
+#!/bin/sh
+
+#
+# Copyright © 2015-2021 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
+done
+
+APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
+
+APP_NAME="Gradle"
+APP_BASE_NAME=${0##*/}
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=''
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+ echo "$*"
+} >&2
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD=$JAVA_HOME/jre/sh/java
+ else
+ JAVACMD=$JAVA_HOME/bin/java
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD=java
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+ CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
+ fi
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
+ done
+fi
+
+# Collect all arguments for the java command;
+# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
+# shell script including quotes and variable substitutions, so put them in
+# double quotes to make sure that they get re-expanded; and
+# * put everything else in single quotes, so that it's not re-expanded.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -classpath "$CLASSPATH" \
+ org.gradle.wrapper.GradleWrapperMain \
+ "$@"
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 00000000..107acd32
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,89 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/pom.xml b/pom.xml
deleted file mode 100644
index c3a7e022..00000000
--- a/pom.xml
+++ /dev/null
@@ -1,441 +0,0 @@
-
- 4.0.0
-
- hk.edu.polyu.comp.vl
- vlabcontroller
- 1.0.3
-
- VLabController
- jar
-
-
- org.springframework.boot
- spring-boot-starter-parent
- 2.5.6
-
-
-
-
- UTF-8
- 1.18.2
- 11
-
- 2021.0.0-M3
-
-
-
-
- repository.spring.milestone
- Spring Milestone Repository
- https://repo.spring.io/milestone
-
-
-
-
-
-
- org.springframework.cloud
- spring-cloud-dependencies
- ${spring-cloud.version}
- pom
- import
-
-
-
-
-
-
- javax.json
- javax.json-api
- 1.1.4
-
-
- org.glassfish
- javax.json
- 1.1.4
-
-
- com.fasterxml.jackson.datatype
- jackson-datatype-jsr353
- 2.13.0
-
-
- org.springframework.boot
- spring-boot-starter-mail
-
-
-
- org.springframework.boot
- spring-boot-configuration-processor
- true
-
-
-
-
- org.apache.commons
- commons-collections4
- 4.4
-
-
- org.apache.commons
- commons-compress
- 1.21
-
-
- commons-beanutils
- commons-beanutils
- 1.9.4
-
-
- com.google.guava
- guava
- 31.0.1-jre
-
-
- junit
- junit
- test
-
-
- org.springframework.data
- spring-data-commons
- ${project.parent.version}
- compile
-
-
- org.jboss.xnio
- xnio-api
- 3.8.4.Final
- compile
-
-
-
-
- org.springframework.boot
- spring-boot-starter-web
-
-
- org.springframework.boot
- spring-boot-starter-tomcat
-
-
-
-
- org.springframework.boot
- spring-boot-starter-websocket
-
-
- org.springframework.boot
- spring-boot-starter-security
-
-
- org.springframework.boot
- spring-boot-starter-undertow
-
-
- org.springframework.boot
- spring-boot-starter-thymeleaf
-
-
- org.springframework.boot
- spring-boot-starter-test
- test
-
-
- org.springframework.boot
- spring-boot-starter-webflux
- test
-
-
- org.springframework.boot
- spring-boot-starter-data-redis
-
-
-
-
- org.springframework.security.oauth.boot
- spring-security-oauth2-autoconfigure
-
-
- org.springframework.security
- spring-security-oauth2-client
-
-
- org.springframework.security
- spring-security-oauth2-jose
-
-
- org.springframework.security
- spring-security-test
- test
-
-
- org.springframework.boot
- spring-boot-starter-actuator
-
-
- org.springframework.session
- spring-session-data-redis
-
-
- org.springframework.cloud
- spring-cloud-context
-
-
-
-
- org.springframework.boot
- spring-boot-starter-jdbc
-
-
- com.h2database
- h2
-
-
-
-
- org.keycloak
- keycloak-spring-security-adapter
- 15.0.2
-
-
-
-
- com.spotify
- docker-client
- 8.16.0
-
-
-
- org.glassfish.jersey.inject
- jersey-hk2
- 3.0.3
-
-
-
-
-
-
-
-
- org.postgresql
- postgresql
-
-
- mysql
- mysql-connector-java
-
-
-
- io.micrometer
- micrometer-registry-prometheus
-
-
- io.micrometer
- micrometer-registry-influx
-
-
-
-
- io.fabric8
- kubernetes-client
- 5.9.0
-
-
-
-
- org.thymeleaf.extras
- thymeleaf-extras-springsecurity5
-
-
-
-
- com.amazonaws
- aws-java-sdk-s3
- 1.12.90
-
-
-
-
- org.projectlombok
- lombok
- 1.18.22
- provided
-
-
-
- com.pivovarit
- throwing-function
- 1.5.1
-
-
-
- org.springframework.boot
- spring-boot-devtools
- provided
-
-
-
-
-
-
-
-
- maven-clean-plugin
- 3.1.0
-
-
-
- maven-compiler-plugin
- 3.8.0
-
-
- maven-surefire-plugin
- 2.22.1
-
-
- maven-jar-plugin
- 3.0.2
-
-
- maven-install-plugin
- 2.5.2
-
-
- maven-deploy-plugin
- 2.8.2
-
-
-
- maven-site-plugin
- 3.7.1
-
-
- maven-project-info-reports-plugin
- 3.0.0
-
-
-
-
-
- com.google.cloud.tools
- jib-maven-plugin
- 3.1.4
-
-
- ghcr.io/stevefan1999/vlab-controller-base
-
-
- ghcr.io/endangeredf1sh/vlab-controller:${project.version}
-
- ${env.REGISTRY_USERNAME}
- ${env.REGISTRY_PASSWORD}
-
-
-
- /opt/vlab-controller
- /opt/vlab-controller
-
- vlab
- /opt/vlab-controller/resources/templates
- false
- Asia/Hong_Kong
-
-
-
- Aiden ZHANG Wenyi <im.endangeredfish@gmail.com>, Fan Chun Yin <stevefan1999@gmail.com>
-
-
- vlab:vlab
-
- --spring.jmx.enabled=false
- --spring.config.location=/etc/vlab-controller/config/application.yml
-
-
-
-
-
- resources/templates
- /opt/vlab-controller/resources/templates
-
-
-
-
-
-
-
- org.apache.maven.plugins
- maven-compiler-plugin
-
- 11
- 11
-
-
- org.projectlombok
- lombok
- 1.18.22
-
-
-
-
-
-
-
- org.springframework.boot
- spring-boot-maven-plugin
- ${project.parent.version}
-
- ${repackage.classifier}
-
-
-
-
- build-info
-
-
-
-
-
-
- org.codehaus.mojo
- versions-maven-plugin
- 2.8.1
-
-
- org.apache.commons:commons-collections4
-
-
-
-
-
- org.apache.maven.plugins
- maven-dependency-plugin
- 3.2.0
-
-
- net.nicoulaj.maven.plugins
- checksum-maven-plugin
- 1.5
-
-
-
- attach-artifact-checksums
-
- artifacts
-
-
-
-
-
- true
-
- SHA-256
- MD5
-
-
-
-
-
-
diff --git a/settings.gradle.kts b/settings.gradle.kts
new file mode 100644
index 00000000..845d7bca
--- /dev/null
+++ b/settings.gradle.kts
@@ -0,0 +1,14 @@
+/*
+ * This file was generated by the Gradle 'init' task.
+ *
+ * This project uses @Incubating APIs which are subject to change.
+ */
+
+rootProject.name = "vlabcontroller"
+
+pluginManagement {
+ repositories {
+ maven { url = uri("https://repo.spring.io/release") }
+ gradlePluginPortal()
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/hk/edu/polyu/comp/vlabcontroller/VLabControllerApplication.java b/src/main/java/hk/edu/polyu/comp/vlabcontroller/VLabControllerApplication.java
index 4db12fc5..39c4500e 100644
--- a/src/main/java/hk/edu/polyu/comp/vlabcontroller/VLabControllerApplication.java
+++ b/src/main/java/hk/edu/polyu/comp/vlabcontroller/VLabControllerApplication.java
@@ -1,17 +1,21 @@
package hk.edu.polyu.comp.vlabcontroller;
import com.fasterxml.jackson.datatype.jsr353.JSR353Module;
+import hk.edu.polyu.comp.vlabcontroller.config.ProxyProperties;
+import hk.edu.polyu.comp.vlabcontroller.config.ServerProperties;
import hk.edu.polyu.comp.vlabcontroller.util.ProxyMappingManager;
import io.undertow.Handlers;
import io.undertow.servlet.api.ServletSessionConfig;
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.Logger;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.boot.actuate.redis.RedisHealthIndicator;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.embedded.undertow.UndertowServletWebServerFactory;
import org.springframework.boot.web.server.PortInUseException;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
@@ -32,38 +36,37 @@
import java.net.UnknownHostException;
import java.nio.file.Files;
import java.nio.file.Paths;
+import java.util.Arrays;
import java.util.Objects;
+import java.util.Optional;
import java.util.Properties;
+import java.util.function.Predicate;
+@Slf4j
@SpringBootApplication
+@ConfigurationPropertiesScan
+@EnableConfigurationProperties
@ComponentScan("hk.edu.polyu.comp")
+@RequiredArgsConstructor
public class VLabControllerApplication {
public static final String CONFIG_FILENAME = "application.yml";
public static final String CONFIG_DEMO_PROFILE = "demo";
- private final Logger log = LogManager.getLogger(getClass());
private final Environment environment;
private final ProxyMappingManager mappingManager;
private final DefaultCookieSerializer defaultCookieSerializer;
-
- public VLabControllerApplication(Environment environment, ProxyMappingManager mappingManager, DefaultCookieSerializer defaultCookieSerializer) {
- this.environment = environment;
- this.mappingManager = mappingManager;
- this.defaultCookieSerializer = defaultCookieSerializer;
- }
+ private final ServerProperties serverProps;
+ private final ProxyProperties proxyProperties;
public static void main(String[] args) {
- SpringApplication app = new SpringApplication(VLabControllerApplication.class);
-
- String configFilename = System.getenv("SPRING_CONFIG_LOCATION");
- for (String arg : args) {
- String pattern = "spring.config.location=";
- int idx = arg.indexOf(pattern);
- if (idx > -1) configFilename = arg.substring(idx + pattern.length());
- break;
- }
- if (configFilename == null) configFilename = CONFIG_FILENAME;
- boolean hasExternalConfig = Files.exists(Paths.get(configFilename));
- if (!hasExternalConfig) app.setAdditionalProfiles(CONFIG_DEMO_PROFILE);
+ var app = new SpringApplication(VLabControllerApplication.class);
+
+ var configFilename = Optional.ofNullable(System.getenv("SPRING_CONFIG_LOCATION"))
+ .filter(Predicate.not(String::isBlank))
+ .or(() -> Arrays.stream(args)
+ .filter(x -> x.contains("spring.config.location"))
+ .map(x -> x.split("=")[1]).findFirst())
+ .orElse(CONFIG_FILENAME);
+ if (!Files.exists(Paths.get(configFilename))) app.setAdditionalProfiles(CONFIG_DEMO_PROFILE);
setDefaultProperties(app);
@@ -79,67 +82,63 @@ public static void main(String[] args) {
}
private static void setDefaultProperties(SpringApplication app) {
- Properties properties = new Properties();
-
- // use in-memory session storage by default. Can be overwritten in application.yml
- properties.put("spring.session.store-type", "none");
- // required for proper working of the SP_USER_INITIATED_LOGOUT session attribute in the UserService
- properties.put("spring.session.redis.flush-mode", "IMMEDIATE");
-
- // disable multi-part handling by Spring. We don't need this anywhere in the application.
- // When enabled this will cause problems when proxying file-uploads to apps.
- properties.put("spring.servlet.multipart.enabled", "false");
-
- // disable logging of requests, since this reads part of the requests and therefore undertow is unable to correctly handle those requests
- properties.put("logging.level.org.springframework.web.servlet.DispatcherServlet", "INFO");
-
- properties.put("spring.application.name", "VLabController");
-
- // Metrics configuration
- // ====================
-
- // disable all supported exporters by default
- // Note: if we upgrade to Spring Boot 2.4.0 we can use properties.put("management.metrics.export.defaults.enabled", "false");
- properties.put("management.metrics.export.prometheus.enabled", "false");
- properties.put("management.metrics.export.influx.enabled", "false");
- // set actuator to port 9090 (can be overwritten)
- properties.put("management.server.port", "9090");
- // enable prometheus endpoint by default (but not the exporter)
- properties.put("management.endpoint.prometheus.enabled", "true");
- // include prometheus and health endpoint in exposure
- properties.put("management.endpoints.web.exposure.include", "health,prometheus");
-
- // ====================
-
- // Health configuration
- // ====================
-
- // enable redisSession check for the readiness probe
- properties.put("management.endpoint.health.group.readiness.include", "readinessProbe,redisSession");
- // disable ldap health endpoint
- properties.put("management.health.ldap.enabled", false);
- // disable default redis health endpoint since it's managed by redisSession
- properties.put("management.health.redis.enabled", "false");
- // enable Kubernetes probes
- properties.put("management.endpoint.health.probes.enabled", true);
-
- // ====================
-
- app.setDefaultProperties(properties);
+ app.setDefaultProperties(new Properties() {{
+ // use in-memory session storage by default. Can be overwritten in application.yml
+ put("spring.session.store-type", "none");
+ // required for proper working of the SP_USER_INITIATED_LOGOUT session attribute in the UserService
+ put("spring.session.redis.flush-mode", "IMMEDIATE");
+
+ // disable multi-part handling by Spring. We don't need this anywhere in the application.
+ // When enabled this will cause problems when proxying file-uploads to apps.
+ put("spring.servlet.multipart.enabled", "false");
+
+ // disable logging of requests, since this reads part of the requests and therefore undertow is unable to correctly handle those requests
+ put("logging.level.org.springframework.web.servlet.DispatcherServlet", "INFO");
+
+ put("spring.application.name", "VLabController");
+
+ // ====================
+ // Metrics configuration
+ // ====================
+
+ // disable all supported exporters by default
+ // Note: if we upgrade to Spring Boot 2.4.0 we can use put("management.metrics.export.defaults.enabled", "false");
+ put("management.metrics.export.prometheus.enabled", "false");
+ put("management.metrics.export.influx.enabled", "false");
+ // set actuator to port 9090 (can be overwritten)
+ put("management.server.port", "9090");
+ // enable prometheus endpoint by default (but not the exporter)
+ put("management.endpoint.prometheus.enabled", "true");
+ // include prometheus and health endpoint in exposure
+ put("management.endpoints.web.exposure.include", "health,prometheus");
+
+ // ====================
+ // Health configuration
+ // ====================
+
+ // enable redisSession check for the readiness probe
+ put("management.endpoint.health.group.readiness.include", "readinessProbe,redisSession");
+ // disable ldap health endpoint
+ put("management.health.ldap.enabled", false);
+ // disable default redis health endpoint since it's managed by redisSession
+ put("management.health.redis.enabled", "false");
+ // enable Kubernetes probes
+ put("management.endpoint.health.probes.enabled", true);
+ }});
// See: https://github.com/keycloak/keycloak/pull/7053
System.setProperty("jdk.serialSetFilterAfterRead", "true");
}
@PostConstruct
public void init() {
- if (environment.getProperty("server.use-forward-headers") != null) {
+ if (serverProps.isUseForwardHeaders()) {
log.warn("WARNING: Using server.use-forward-headers will not work in this VLabController release, you need to change your configuration to use another property. See https://shinyproxy.io/documentation/security/#forward-headers on how to change your configuration.");
}
- String sameSiteCookie = environment.getProperty("proxy.same-site-cookie", "Lax");
+ var sameSiteCookie = proxyProperties.getSameSiteCookie();
log.debug("Setting sameSiteCookie policy to {}", sameSiteCookie);
defaultCookieSerializer.setSameSite(sameSiteCookie);
- String proxyIdentifier = environment.getProperty("proxy.identifier-value");
+ var proxyIdentifier = proxyProperties.getIdentifierValue();
if (proxyIdentifier != null && !proxyIdentifier.isEmpty()) {
defaultCookieSerializer.setCookieName("SESSION_" + proxyIdentifier.toUpperCase());
}
@@ -149,32 +148,30 @@ public void init() {
@Bean
public UndertowServletWebServerFactory servletContainer() {
- UndertowServletWebServerFactory factory = new UndertowServletWebServerFactory();
+ var factory = new UndertowServletWebServerFactory();
factory.addDeploymentInfoCustomizers(info -> {
info.setPreservePathOnForward(false); // required for the /api/route/{id}/ endpoint to work properly
- if (Boolean.valueOf(environment.getProperty("logging.requestdump", "false"))) {
- info.addOuterHandlerChainWrapper(defaultHandler -> Handlers.requestDump(defaultHandler));
+ if (Boolean.parseBoolean(environment.getProperty("logging.requestdump", "false"))) {
+ info.addOuterHandlerChainWrapper(Handlers::requestDump);
}
- info.addInnerHandlerChainWrapper(defaultHandler -> {
- return mappingManager.createHttpHandler(defaultHandler);
- });
- ServletSessionConfig sessionConfig = new ServletSessionConfig();
+ info.addInnerHandlerChainWrapper(mappingManager::createHttpHandler);
+ var sessionConfig = new ServletSessionConfig();
sessionConfig.setHttpOnly(true);
- sessionConfig.setSecure(Boolean.valueOf(environment.getProperty("server.secureCookies", "false")));
+ sessionConfig.setSecure(serverProps.isSecureCookies());
info.setServletSessionConfig(sessionConfig);
});
try {
- factory.setAddress(InetAddress.getByName(environment.getProperty("proxy.bind-address", "0.0.0.0")));
+ factory.setAddress(InetAddress.getByName(proxyProperties.getBindAddress()));
} catch (UnknownHostException e) {
throw new IllegalArgumentException("Invalid bind address specified", e);
}
- factory.setPort(Integer.parseInt(environment.getProperty("proxy.port", "8080")));
+ factory.setPort(proxyProperties.getPort());
return factory;
}
@Bean
public FilterRegistrationBean registration2(FormContentFilter filter) {
- FilterRegistrationBean registration = new FilterRegistrationBean<>(filter);
+ var registration = new FilterRegistrationBean<>(filter);
registration.setEnabled(false);
return registration;
}
@@ -217,7 +214,7 @@ public Health health() {
@Bean
@ConditionalOnProperty(name = "spring.session.store-type", havingValue = "redis")
public SessionRegistry sessionRegistry(FindByIndexNameSessionRepository sessionRepository) {
- return new SpringSessionBackedSessionRegistry(sessionRepository);
+ return new SpringSessionBackedSessionRegistry<>(sessionRepository);
}
@Bean
diff --git a/src/main/java/hk/edu/polyu/comp/vlabcontroller/VLabControllerConfiguration.java b/src/main/java/hk/edu/polyu/comp/vlabcontroller/VLabControllerConfiguration.java
index 1ab0dceb..59a19ce9 100644
--- a/src/main/java/hk/edu/polyu/comp/vlabcontroller/VLabControllerConfiguration.java
+++ b/src/main/java/hk/edu/polyu/comp/vlabcontroller/VLabControllerConfiguration.java
@@ -1,27 +1,31 @@
package hk.edu.polyu.comp.vlabcontroller;
+import hk.edu.polyu.comp.vlabcontroller.config.ProxyProperties;
import hk.edu.polyu.comp.vlabcontroller.service.HeartbeatService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
-import org.springframework.core.env.Environment;
+import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;
+import org.springframework.scheduling.annotation.EnableScheduling;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import javax.annotation.PostConstruct;
@Configuration
+@RequiredArgsConstructor(onConstructor_ = {@Lazy})
+@RefreshScope
+@EnableMongoRepositories
+@EnableScheduling
public class VLabControllerConfiguration {
-
private final HeartbeatService heartbeatService;
- private final Environment environment;
-
- public VLabControllerConfiguration(@Lazy HeartbeatService heartbeatService, Environment environment) {
- this.heartbeatService = heartbeatService;
- this.environment = environment;
- }
+ private final ProxyProperties proxyProperties;
+ private final ThreadPoolTaskScheduler threadPoolTaskScheduler;
@PostConstruct
public void init() {
+ threadPoolTaskScheduler.setPoolSize(2048);
// Enable heartbeat unless explicitly disabled.
- boolean enabled = Boolean.valueOf(environment.getProperty("proxy.heartbeat-enabled", "true"));
- heartbeatService.setEnabled(enabled);
+ heartbeatService.setEnabled(proxyProperties.isHeartbeatEnabled());
}
}
diff --git a/src/main/java/hk/edu/polyu/comp/vlabcontroller/api/BaseController.java b/src/main/java/hk/edu/polyu/comp/vlabcontroller/api/BaseController.java
index 96124bde..ff6e9afe 100644
--- a/src/main/java/hk/edu/polyu/comp/vlabcontroller/api/BaseController.java
+++ b/src/main/java/hk/edu/polyu/comp/vlabcontroller/api/BaseController.java
@@ -1,7 +1,9 @@
package hk.edu.polyu.comp.vlabcontroller.api;
+import hk.edu.polyu.comp.vlabcontroller.config.ProxyProperties;
+import lombok.Setter;
import lombok.experimental.StandardException;
-import org.springframework.core.env.Environment;
+import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.http.HttpStatus;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.ControllerAdvice;
@@ -11,13 +13,13 @@
import javax.inject.Inject;
+@RefreshScope
public class BaseController {
-
- @Inject
- private Environment environment;
+ @Setter(onMethod_ = {@Inject})
+ protected ProxyProperties proxyProperties;
protected void prepareMap(ModelMap map) {
- map.put("title", environment.getProperty("proxy.title", "VLabController"));
+ map.put("title", proxyProperties.getTitle());
}
@StandardException
diff --git a/src/main/java/hk/edu/polyu/comp/vlabcontroller/api/ConfigController.java b/src/main/java/hk/edu/polyu/comp/vlabcontroller/api/ConfigController.java
index f18f9f5e..56152932 100644
--- a/src/main/java/hk/edu/polyu/comp/vlabcontroller/api/ConfigController.java
+++ b/src/main/java/hk/edu/polyu/comp/vlabcontroller/api/ConfigController.java
@@ -2,6 +2,7 @@
import hk.edu.polyu.comp.vlabcontroller.event.ConfigUpdateEvent;
import hk.edu.polyu.comp.vlabcontroller.util.ConfigFileHelper;
+import lombok.RequiredArgsConstructor;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.http.HttpStatus;
@@ -13,18 +14,14 @@
@ConditionalOnExpression("${proxy.config.enable-refresh-api:false}")
@RestController
+@RequiredArgsConstructor
public class ConfigController {
private final ApplicationEventPublisher publisher;
private final ConfigFileHelper configFileHelper;
- public ConfigController(ApplicationEventPublisher publisher, ConfigFileHelper configFileHelper) {
- this.publisher = publisher;
- this.configFileHelper = configFileHelper;
- }
-
@PostMapping(value = "/api/config/refresh")
public ResponseEntity refresh() throws NoSuchAlgorithmException {
- String hash = configFileHelper.getConfigHash();
+ var hash = configFileHelper.getConfigHash();
publisher.publishEvent(new ConfigUpdateEvent(this));
return new ResponseEntity<>(hash, HttpStatus.OK);
}
diff --git a/src/main/java/hk/edu/polyu/comp/vlabcontroller/api/ProxyController.java b/src/main/java/hk/edu/polyu/comp/vlabcontroller/api/ProxyController.java
index 87bcbaea..9d86496c 100644
--- a/src/main/java/hk/edu/polyu/comp/vlabcontroller/api/ProxyController.java
+++ b/src/main/java/hk/edu/polyu/comp/vlabcontroller/api/ProxyController.java
@@ -1,25 +1,28 @@
package hk.edu.polyu.comp.vlabcontroller.api;
+import com.google.common.collect.Sets;
import hk.edu.polyu.comp.vlabcontroller.model.runtime.Proxy;
import hk.edu.polyu.comp.vlabcontroller.model.runtime.RuntimeSetting;
import hk.edu.polyu.comp.vlabcontroller.model.spec.ProxySpec;
import hk.edu.polyu.comp.vlabcontroller.service.ProxyService;
+import io.vavr.Function1;
+import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
+import java.time.Duration;
import java.util.List;
+import java.util.Map;
+import java.util.Optional;
import java.util.Set;
@RestController
+@RequiredArgsConstructor
public class ProxyController extends BaseController {
private final ProxyService proxyService;
- public ProxyController(ProxyService proxyService) {
- this.proxyService = proxyService;
- }
-
@GetMapping(value = "/api/proxyspec", produces = MediaType.APPLICATION_JSON_VALUE)
public List listProxySpecs() {
return proxyService.getProxySpecs(null, false);
@@ -27,9 +30,8 @@ public List listProxySpecs() {
@GetMapping(value = "/api/proxyspec/{proxySpecId}", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity getProxySpec(@PathVariable String proxySpecId) {
- ProxySpec spec = proxyService.findProxySpec(s -> s.getId().equals(proxySpecId), false);
- if (spec == null) return new ResponseEntity<>(HttpStatus.NOT_FOUND);
- return new ResponseEntity<>(spec, HttpStatus.OK);
+ return findProxySpecByIdAndACL(proxySpecId)
+ .map(ResponseEntity::ok).orElse(ResponseEntity.notFound().build());
}
@GetMapping(value = "/api/proxy", produces = MediaType.APPLICATION_JSON_VALUE)
@@ -39,34 +41,101 @@ public List listProxies() {
@GetMapping(value = "/api/proxy/{proxyId}", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity getProxy(@PathVariable String proxyId) {
- Proxy proxy = proxyService.findProxy(p -> p.getId().equals(proxyId), false);
- if (proxy == null) return new ResponseEntity<>(HttpStatus.NOT_FOUND);
- return new ResponseEntity<>(proxy, HttpStatus.OK);
+ return findProxyByIdAndACL(proxyId, false)
+ .map(ResponseEntity::ok).orElse(ResponseEntity.notFound().build());
+ }
+
+ @PostMapping(value = "/api/proxy/{proxyId}/metadata", produces = MediaType.APPLICATION_JSON_VALUE)
+ public ResponseEntity
*/
+@Slf4j
@Service
+@RequiredArgsConstructor(onConstructor_ = {@Lazy})
public class ProxyService {
-
- private final Logger log = LogManager.getLogger(ProxyService.class);
private final List activeProxies = Collections.synchronizedList(new ArrayList<>());
- private final ExecutorService containerKiller = Executors.newSingleThreadExecutor();
+ private final ThreadPoolTaskScheduler taskScheduler;
private final IProxySpecProvider baseSpecProvider;
private final IProxySpecMergeStrategy specMergeStrategy;
@@ -61,29 +58,17 @@ public class ProxyService {
private final UserService userService;
private final LogService logService;
private final ApplicationEventPublisher applicationEventPublisher;
-
- @Lazy
- public ProxyService(IProxySpecProvider baseSpecProvider, IProxySpecMergeStrategy specMergeStrategy, IContainerBackend backend, ProxyMappingManager mappingManager, UserService userService, LogService logService, ApplicationEventPublisher applicationEventPublisher) {
- this.baseSpecProvider = baseSpecProvider;
- this.specMergeStrategy = specMergeStrategy;
- this.backend = backend;
- this.mappingManager = mappingManager;
- this.userService = userService;
- this.logService = logService;
- this.applicationEventPublisher = applicationEventPublisher;
- }
+ private List> containerKillerFutures = new ArrayList<>();
@PreDestroy
public void shutdown() {
- try {
- containerKiller.shutdown();
- } finally {
- for (Proxy proxy : activeProxies) {
- try {
- backend.stopProxy(proxy);
- } catch (Exception exception) {
- exception.printStackTrace();
- }
+ containerKillerFutures.forEach(x -> x.cancel(true));
+
+ for (var proxy : activeProxies) {
+ try {
+ backend.stopProxy(proxy);
+ } catch (Exception exception) {
+ exception.printStackTrace();
}
}
}
@@ -119,9 +104,9 @@ public ProxySpec findProxySpec(Predicate filter, boolean ignoreAccess
*/
public List getProxySpecs(Predicate filter, boolean ignoreAccessControl) {
return baseSpecProvider.getSpecs().stream()
- .filter(spec -> ignoreAccessControl || userService.canAccess(spec))
- .filter(spec -> filter == null || filter.test(spec))
- .collect(Collectors.toList());
+ .filter(spec -> ignoreAccessControl || userService.canAccess(spec))
+ .filter(spec -> filter == null || filter.test(spec))
+ .collect(Collectors.toList());
}
/**
@@ -168,11 +153,11 @@ public Proxy findProxy(Predicate filter, boolean ignoreAccessControl) {
* @return A List of matching proxies, may be empty.
*/
public List getProxies(Predicate filter, boolean ignoreAccessControl) {
- boolean isAdmin = userService.isAdmin();
+ var isAdmin = userService.isAdmin();
List matches = new ArrayList<>();
synchronized (activeProxies) {
- for (Proxy proxy : activeProxies) {
- boolean hasAccess = ignoreAccessControl || isAdmin || userService.isOwner(proxy);
+ for (var proxy : activeProxies) {
+ var hasAccess = ignoreAccessControl || isAdmin || userService.isOwner(proxy);
if (hasAccess && (filter == null || filter.test(proxy))) matches.add(proxy);
}
}
@@ -192,11 +177,12 @@ public Proxy startProxy(ProxySpec spec, boolean ignoreAccessControl) throws VLab
throw new AccessDeniedException(String.format("Cannot start proxy %s: access denied", spec.getId()));
}
- Proxy proxy = new Proxy();
- proxy.setStatus(ProxyStatus.New);
- proxy.setUserId(userService.getCurrentUserId());
- proxy.setSpec(spec);
- proxy.setAdmin(userService.isAdmin());
+ var proxy = Proxy.builder()
+ .status(ProxyStatus.New)
+ .userId(userService.getCurrentUserId())
+ .spec(spec.copy())
+ .admin(userService.isAdmin())
+ .build();
activeProxies.add(proxy);
try {
@@ -204,16 +190,17 @@ public Proxy startProxy(ProxySpec spec, boolean ignoreAccessControl) throws VLab
} finally {
if (proxy.getStatus() != ProxyStatus.Up) {
activeProxies.remove(proxy);
- applicationEventPublisher.publishEvent(new ProxyStartFailedEvent(this, proxy.getUserId(), spec.getId()));
+ var event = ProxyStartFailedEvent.builder().source(this).specId(spec.getId()).userId(proxy.getUserId()).build();
+ applicationEventPublisher.publishEvent(event);
}
}
- for (Entry target : proxy.getTargets().entrySet()) {
+ for (var target : proxy.getTargets().entrySet()) {
mappingManager.addMapping(proxy.getId(), target.getKey(), target.getValue());
}
if (logService.isLoggingEnabled()) {
- BiConsumer outputAttacher = backend.getOutputAttacher(proxy);
+ var outputAttacher = backend.getOutputAttacher(proxy);
if (outputAttacher == null) {
log.warn("Cannot log proxy output: " + backend.getClass() + " does not support output attaching.");
} else {
@@ -222,7 +209,11 @@ public Proxy startProxy(ProxySpec spec, boolean ignoreAccessControl) throws VLab
}
log.info(String.format("Proxy activated [user: %s] [spec: %s] [id: %s]", proxy.getUserId(), spec.getId(), proxy.getId()));
- applicationEventPublisher.publishEvent(new ProxyStartEvent(this, proxy.getUserId(), spec.getId(), Duration.ofMillis(proxy.getStartupTimestamp() - proxy.getCreatedTimestamp())));
+ var event = ProxyStartEvent.builder()
+ .source(this).proxyId(proxy.getId()).specId(spec.getId()).userId(proxy.getUserId())
+ .startupTime(proxy.getStartupTimestamp().minus(proxy.getCreatedTimestamp()))
+ .build();
+ applicationEventPublisher.publishEvent(event);
return proxy;
}
@@ -235,7 +226,7 @@ public Proxy startProxy(ProxySpec spec, boolean ignoreAccessControl) throws VLab
* @param ignoreAccessControl True to allow access to any proxy, regardless of the current security context.
* @param silenceOffset Milliseconds to subtract idle silence period, report accurate usage time.
*/
- public void stopProxy(Proxy proxy, boolean async, boolean ignoreAccessControl, long silenceOffset) {
+ public void stopProxy(Proxy proxy, boolean async, boolean ignoreAccessControl, Duration silenceOffset) {
if (!ignoreAccessControl && !userService.isAdmin() && !userService.isOwner(proxy)) {
throw new AccessDeniedException(String.format("Cannot stop proxy %s: access denied", proxy.getId()));
}
@@ -247,16 +238,18 @@ public void stopProxy(Proxy proxy, boolean async, boolean ignoreAccessControl, l
backend.stopProxy(proxy);
logService.detach(proxy);
log.info(String.format("Proxy released [user: %s] [spec: %s] [id: %s]", proxy.getUserId(), proxy.getSpec().getId(), proxy.getId()));
- if (proxy.getStartupTimestamp() > 0) {
- applicationEventPublisher.publishEvent(new ProxyStopEvent(this, proxy.getUserId(),
- proxy.getSpec().getId(),
- Duration.ofMillis(System.currentTimeMillis() - proxy.getStartupTimestamp() - silenceOffset)));
+ if (DurationUtils.isPositive(proxy.getStartupTimestamp())) {
+ var event = ProxyStopEvent.builder()
+ .usageTime(Duration.ofMillis(System.currentTimeMillis()).minus(proxy.getStartupTimestamp()).minus(silenceOffset))
+ .source(this).proxyId(proxy.getId()).userId(proxy.getUserId()).specId(proxy.getSpec().getId())
+ .build();
+ applicationEventPublisher.publishEvent(event);
}
} catch (Exception e) {
log.error("Failed to release proxy " + proxy.getId(), e);
}
};
- if (async) containerKiller.submit(releaser);
+ if (async) containerKillerFutures.add(taskScheduler.submit(releaser));
else releaser.run();
mappingManager.removeProxyMapping(proxy.getId());
diff --git a/src/main/java/hk/edu/polyu/comp/vlabcontroller/service/UserActionEventsListener.java b/src/main/java/hk/edu/polyu/comp/vlabcontroller/service/UserActionEventsListener.java
new file mode 100644
index 00000000..61f276fd
--- /dev/null
+++ b/src/main/java/hk/edu/polyu/comp/vlabcontroller/service/UserActionEventsListener.java
@@ -0,0 +1,76 @@
+package hk.edu.polyu.comp.vlabcontroller.service;
+
+import hk.edu.polyu.comp.vlabcontroller.entity.LabInstance;
+import hk.edu.polyu.comp.vlabcontroller.entity.SessionData;
+import hk.edu.polyu.comp.vlabcontroller.entity.User;
+import hk.edu.polyu.comp.vlabcontroller.event.ProxyStartEvent;
+import hk.edu.polyu.comp.vlabcontroller.event.ProxyStopEvent;
+import hk.edu.polyu.comp.vlabcontroller.event.UserLoginEvent;
+import hk.edu.polyu.comp.vlabcontroller.event.UserLogoutEvent;
+import hk.edu.polyu.comp.vlabcontroller.repository.UserRepository;
+import lombok.RequiredArgsConstructor;
+import org.joda.time.DateTime;
+import org.springframework.cloud.context.config.annotation.RefreshScope;
+import org.springframework.context.event.EventListener;
+import org.springframework.stereotype.Component;
+
+import java.util.Optional;
+
+@RefreshScope
+@Component
+@RequiredArgsConstructor
+public class UserActionEventsListener {
+ private final UserRepository repository;
+
+ @EventListener
+ public void onProxyStart(ProxyStartEvent event) {
+ var time = new DateTime(event.getTimestamp());
+ var user = this.repository.findUserByIdOrCreate(event.getUserId());
+ var labs = user.getLabs();
+ labs.stream().filter(x -> x.getId().equals(event.getProxyId())).findAny()
+ .ifPresentOrElse(
+ lab -> lab.setStartedAt(time),
+ () -> labs.addFirst(LabInstance.builder().id(event.getProxyId()).startedAt(time).build())
+ );
+ this.repository.save(user);
+ }
+
+ @EventListener
+ public void onProxyStop(ProxyStopEvent event) {
+ var time = new DateTime(event.getTimestamp());
+ User user = this.repository.findUserByIdOrCreate(event.getUserId());
+ var labs = user.getLabs();
+ labs.stream().filter(x -> x.getId().equals(event.getProxyId())).findAny()
+ .ifPresentOrElse(
+ lab -> lab.setStartedAt(time),
+ () -> labs.addFirst(LabInstance.builder().id(event.getProxyId()).completedAt(time).build())
+ );
+ this.repository.save(user);
+ }
+
+ @EventListener
+ public void onUserLogin(UserLoginEvent event) {
+ var time = new DateTime(event.getTimestamp());
+ var user = this.repository.findUserByIdOrCreate(event.getUserId());
+ var sessions = user.getSession();
+ Optional.ofNullable(sessions.get(event.getSessionId()))
+ .ifPresentOrElse(
+ session -> session.setLoggedInAt(time),
+ () -> sessions.put(event.getSessionId(), SessionData.builder().loggedInAt(time).build())
+ );
+ this.repository.save(user);
+ }
+
+ @EventListener
+ public void onUserLogout(UserLogoutEvent event) {
+ var time = new DateTime(event.getTimestamp());
+ var user = this.repository.findUserByIdOrCreate(event.getUserId());
+ var sessions = user.getSession();
+ Optional.ofNullable(sessions.get(event.getSessionId()))
+ .ifPresentOrElse(
+ session -> session.setLoggedOutAt(time),
+ () -> sessions.put(event.getSessionId(), SessionData.builder().loggedOutAt(time).build())
+ );
+ this.repository.save(user);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/hk/edu/polyu/comp/vlabcontroller/service/UserService.java b/src/main/java/hk/edu/polyu/comp/vlabcontroller/service/UserService.java
index f97acef7..88616535 100644
--- a/src/main/java/hk/edu/polyu/comp/vlabcontroller/service/UserService.java
+++ b/src/main/java/hk/edu/polyu/comp/vlabcontroller/service/UserService.java
@@ -2,24 +2,24 @@
import hk.edu.polyu.comp.vlabcontroller.auth.IAuthenticationBackend;
import hk.edu.polyu.comp.vlabcontroller.backend.strategy.IProxyLogoutStrategy;
+import hk.edu.polyu.comp.vlabcontroller.config.ProxyProperties;
import hk.edu.polyu.comp.vlabcontroller.event.AuthFailedEvent;
import hk.edu.polyu.comp.vlabcontroller.event.UserLoginEvent;
import hk.edu.polyu.comp.vlabcontroller.event.UserLogoutEvent;
import hk.edu.polyu.comp.vlabcontroller.model.runtime.Proxy;
import hk.edu.polyu.comp.vlabcontroller.model.spec.ProxySpec;
import hk.edu.polyu.comp.vlabcontroller.util.SessionHelper;
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.Logger;
+import io.vavr.control.Option;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.event.EventListener;
-import org.springframework.core.env.Environment;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.authentication.event.AbstractAuthenticationFailureEvent;
import org.springframework.security.authentication.event.AuthenticationSuccessEvent;
import org.springframework.security.core.Authentication;
-import org.springframework.security.core.GrantedAuthority;
-import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.session.HttpSessionCreatedEvent;
import org.springframework.security.web.session.HttpSessionDestroyedEvent;
@@ -27,29 +27,24 @@
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
-import javax.servlet.http.HttpSession;
-import java.util.*;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+@Slf4j
@Service
+@RequiredArgsConstructor(onConstructor_ = {@Lazy})
+@RefreshScope
public class UserService {
- private final static String ATTRIBUTE_USER_INITIATED_LOGOUT = "SP_USER_INITIATED_LOGOUT";
-
- private final Logger log = LogManager.getLogger(UserService.class);
private final Map userInitiatedLogoutMap = new HashMap<>();
- private final Environment environment;
+ private final ProxyProperties proxyProperties;
private final IAuthenticationBackend authBackend;
private final IProxyLogoutStrategy logoutStrategy;
private final ApplicationEventPublisher applicationEventPublisher;
- @Lazy
- public UserService(Environment environment, IAuthenticationBackend authBackend, IProxyLogoutStrategy logoutStrategy, ApplicationEventPublisher applicationEventPublisher) {
- this.environment = environment;
- this.authBackend = authBackend;
- this.logoutStrategy = logoutStrategy;
- this.applicationEventPublisher = applicationEventPublisher;
- }
-
public Authentication getCurrentAuth() {
return SecurityContextHolder.getContext().getAuthentication();
}
@@ -58,36 +53,23 @@ public String getCurrentUserId() {
return getUserId(getCurrentAuth());
}
- public String[] getAdminGroups() {
- Set adminGroups = new HashSet<>();
-
- // Support for old, non-array notation
- String singleGroup = environment.getProperty("proxy.admin-groups");
- if (singleGroup != null && !singleGroup.isEmpty()) adminGroups.add(singleGroup.toUpperCase());
-
- for (int i = 0; ; i++) {
- String groupName = environment.getProperty(String.format("proxy.admin-groups[%s]", i));
- if (groupName == null || groupName.isEmpty()) break;
- adminGroups.add(groupName.toUpperCase());
- }
-
- return adminGroups.toArray(new String[adminGroups.size()]);
+ public Collection getAdminGroups() {
+ return proxyProperties.getAdminGroups().stream()
+ .filter(Predicate.not(String::isBlank))
+ .map(String::toUpperCase)
+ .collect(Collectors.toSet());
}
- public String[] getGroups() {
+ public Collection getGroups() {
return getGroups(getCurrentAuth());
}
- public String[] getGroups(Authentication auth) {
- List groups = new ArrayList<>();
- if (auth != null) {
- for (GrantedAuthority grantedAuth : auth.getAuthorities()) {
- String authName = grantedAuth.getAuthority().toUpperCase();
- if (authName.startsWith("ROLE_")) authName = authName.substring(5);
- groups.add(authName);
- }
- }
- return groups.toArray(new String[groups.size()]);
+ public Collection getGroups(Authentication auth) {
+ return auth.getAuthorities().stream().map(grantedAuth -> {
+ var authName = grantedAuth.getAuthority().toUpperCase();
+ if (authName.startsWith("ROLE_")) authName = authName.substring(5);
+ return authName;
+ }).collect(Collectors.toList());
}
public boolean isAdmin() {
@@ -95,10 +77,7 @@ public boolean isAdmin() {
}
public boolean isAdmin(Authentication auth) {
- for (String adminGroup : getAdminGroups()) {
- if (isMember(auth, adminGroup)) return true;
- }
- return false;
+ return getAdminGroups().stream().anyMatch(adminGroup -> isMember(auth, adminGroup));
}
public boolean canAccess(ProxySpec spec) {
@@ -108,12 +87,8 @@ public boolean canAccess(ProxySpec spec) {
public boolean canAccess(Authentication auth, ProxySpec spec) {
if (auth == null || spec == null) return false;
if (auth instanceof AnonymousAuthenticationToken) return !authBackend.hasAuthorization();
- List groups = spec.getAccessGroups();
- if (groups.isEmpty()) return true;
- for (String group : groups) {
- if (isMember(auth, group)) return true;
- }
- return false;
+ var groups = spec.getAccessGroups();
+ return groups.isEmpty() || groups.stream().anyMatch(group -> isMember(auth, group));
}
public boolean isOwner(Proxy proxy) {
@@ -127,10 +102,7 @@ public boolean isOwner(Authentication auth, Proxy proxy) {
private boolean isMember(Authentication auth, String groupName) {
if (auth == null || auth instanceof AnonymousAuthenticationToken || groupName == null) return false;
- for (String group : getGroups(auth)) {
- if (group.equalsIgnoreCase(groupName)) return true;
- }
- return false;
+ return getGroups(auth).stream().anyMatch(group -> group.equalsIgnoreCase(groupName));
}
private String getUserId(Authentication auth) {
@@ -144,10 +116,10 @@ private String getUserId(Authentication auth) {
@EventListener
public void onAbstractAuthenticationFailureEvent(AbstractAuthenticationFailureEvent event) {
- Authentication source = event.getAuthentication();
+ var source = event.getAuthentication();
Exception e = event.getException();
log.info(String.format("Authentication failure [user: %s] [error: %s]", source.getName(), e.getMessage()));
- String userId = getUserId(source);
+ var userId = getUserId(source);
applicationEventPublisher.publishEvent(new AuthFailedEvent(
this,
@@ -156,14 +128,14 @@ public void onAbstractAuthenticationFailureEvent(AbstractAuthenticationFailureEv
}
public void logout(Authentication auth) {
- String userId = getUserId(auth);
+ var userId = getUserId(auth);
if (userId == null) return;
if (logoutStrategy != null) logoutStrategy.onLogout(userId, false);
log.info(String.format("User logged out [user: %s]", userId));
- HttpSession session = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest().getSession();
- String sessionId = session.getId();
+ var session = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest().getSession();
+ var sessionId = session.getId();
userInitiatedLogoutMap.put(sessionId, "true");
applicationEventPublisher.publishEvent(new UserLogoutEvent(
this,
@@ -174,11 +146,11 @@ public void logout(Authentication auth) {
@EventListener
public void onAuthenticationSuccessEvent(AuthenticationSuccessEvent event) {
- Authentication auth = event.getAuthentication();
- String userName = auth.getName();
+ var auth = event.getAuthentication();
+ var userName = auth.getName();
- HttpSession session = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest().getSession();
- boolean firstLogin = session.getAttribute("firstLogin") == null || (Boolean) session.getAttribute("firstLogin");
+ var session = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest().getSession();
+ var firstLogin = session.getAttribute("firstLogin") == null || (Boolean) session.getAttribute("firstLogin");
if (firstLogin) {
session.setAttribute("firstLogin", false);
} else {
@@ -187,11 +159,8 @@ public void onAuthenticationSuccessEvent(AuthenticationSuccessEvent event) {
log.info(String.format("User logged in [user: %s]", userName));
- String userId = getUserId(auth);
- applicationEventPublisher.publishEvent(new UserLoginEvent(
- this,
- userId,
- RequestContextHolder.currentRequestAttributes().getSessionId()));
+ var userId = getUserId(auth);
+ applicationEventPublisher.publishEvent(UserLoginEvent.builder().source(this).userId(userId).sessionId(RequestContextHolder.currentRequestAttributes().getSessionId()).build());
}
@EventListener
@@ -203,47 +172,42 @@ public void onHttpSessionDestroyedEvent(HttpSessionDestroyedEvent event) {
Session Attributes set in logout() cannot be fetched here
but these two session instances have same sessionId, an additional Map can be used as workaround
*/
- String userInitiatedLogout = userInitiatedLogoutMap.remove(event.getId());
+ var userInitiatedLogout = userInitiatedLogoutMap.remove(event.getId());
if (userInitiatedLogout != null && userInitiatedLogout.equals("true")) {
// user initiated the logout
// event already handled by the logout() function above -> ignore it
} else {
// user did not initiated the logout -> session expired
// not already handled by any other handler
+ var eventBuilder = UserLogoutEvent.builder().source(this);
+
+ var sid = Option.none();
+ var uid = Option.none();
+
if (!event.getSecurityContexts().isEmpty()) {
- SecurityContext securityContext = event.getSecurityContexts().get(0);
+ var securityContext = event.getSecurityContexts().get(0);
if (securityContext == null) return;
- String userId = securityContext.getAuthentication().getName();
-
+ var userId = securityContext.getAuthentication().getName();
logoutStrategy.onLogout(userId, true);
log.info(String.format("HTTP session expired [user: %s]", userId));
- applicationEventPublisher.publishEvent(new UserLogoutEvent(
- this,
- userId,
- event.getSession().getId(),
- true
- ));
+ uid = Option.some(userId);
+ sid = Option.some(RequestContextHolder.currentRequestAttributes().getSessionId());
} else if (authBackend.getName().equals("none")) {
- log.info(String.format("Anonymous user logged out [user: %s]", event.getSession().getId()));
- applicationEventPublisher.publishEvent(new UserLogoutEvent(
- this,
- event.getSession().getId(),
- event.getSession().getId(),
- true
- ));
+ var id = event.getSession().getId();
+ log.info(String.format("Anonymous user logged out [user: %s]", id));
+ sid = uid = Option.some(id);
}
+ applicationEventPublisher.publishEvent(eventBuilder.userId(uid.get()).sessionId(sid.get()).wasExpired(true).build());
}
}
@EventListener
public void onHttpSessionCreated(HttpSessionCreatedEvent event) {
if (authBackend.getName().equals("none")) {
- log.info(String.format("Anonymous user logged in [user: %s]", event.getSession().getId()));
- applicationEventPublisher.publishEvent(new UserLoginEvent(
- this,
- event.getSession().getId(),
- event.getSession().getId()));
+ var id = event.getSession().getId();
+ log.info(String.format("Anonymous user logged in [user: %s]", id));
+ applicationEventPublisher.publishEvent(UserLoginEvent.builder().source(this).userId(id).sessionId(id).build());
}
}
diff --git a/src/main/java/hk/edu/polyu/comp/vlabcontroller/spec/EngagementProperties.java b/src/main/java/hk/edu/polyu/comp/vlabcontroller/spec/EngagementProperties.java
deleted file mode 100644
index 8101ba32..00000000
--- a/src/main/java/hk/edu/polyu/comp/vlabcontroller/spec/EngagementProperties.java
+++ /dev/null
@@ -1,38 +0,0 @@
-package hk.edu.polyu.comp.vlabcontroller.spec;
-
-
-import lombok.Getter;
-import lombok.Setter;
-import org.springframework.boot.context.properties.ConfigurationProperties;
-import org.springframework.boot.context.properties.EnableConfigurationProperties;
-import org.springframework.cloud.context.config.annotation.RefreshScope;
-import org.springframework.context.annotation.Configuration;
-
-import java.time.Duration;
-import java.util.ArrayList;
-import java.util.List;
-
-@RefreshScope
-@EnableConfigurationProperties
-@Configuration
-@ConfigurationProperties(prefix = "proxy.engagement")
-public class EngagementProperties {
- @Getter
- @Setter
- private boolean enabled = true;
- @Getter
- @Setter
- private List filterPath = new ArrayList<>();
- @Getter
- @Setter
- private int idleRetry = 3;
- @Getter
- @Setter
- private int threshold = 230;
- @Getter
- private Duration maxAge = Duration.ofHours(4);
-
- public void setMaxAge(String duration) {
- this.maxAge = Duration.parse(duration);
- }
-}
diff --git a/src/main/java/hk/edu/polyu/comp/vlabcontroller/spec/FileBrowserProperties.java b/src/main/java/hk/edu/polyu/comp/vlabcontroller/spec/FileBrowserProperties.java
deleted file mode 100644
index 8e998735..00000000
--- a/src/main/java/hk/edu/polyu/comp/vlabcontroller/spec/FileBrowserProperties.java
+++ /dev/null
@@ -1,15 +0,0 @@
-package hk.edu.polyu.comp.vlabcontroller.spec;
-
-import hk.edu.polyu.comp.vlabcontroller.model.spec.ProxySpec;
-import org.springframework.boot.context.properties.ConfigurationProperties;
-import org.springframework.boot.context.properties.EnableConfigurationProperties;
-import org.springframework.cloud.context.config.annotation.RefreshScope;
-import org.springframework.context.annotation.Configuration;
-
-@RefreshScope
-@EnableConfigurationProperties
-@Configuration
-@ConfigurationProperties(prefix = "proxy.filebrowser")
-public class FileBrowserProperties extends ProxySpec {
-
-}
\ No newline at end of file
diff --git a/src/main/java/hk/edu/polyu/comp/vlabcontroller/spec/StatCollectorProperties.java b/src/main/java/hk/edu/polyu/comp/vlabcontroller/spec/StatCollectorProperties.java
deleted file mode 100644
index a9f1bec1..00000000
--- a/src/main/java/hk/edu/polyu/comp/vlabcontroller/spec/StatCollectorProperties.java
+++ /dev/null
@@ -1,28 +0,0 @@
-package hk.edu.polyu.comp.vlabcontroller.spec;
-
-import lombok.Getter;
-import lombok.Setter;
-import org.springframework.boot.context.properties.ConfigurationProperties;
-import org.springframework.boot.context.properties.EnableConfigurationProperties;
-import org.springframework.cloud.context.config.annotation.RefreshScope;
-import org.springframework.context.annotation.Configuration;
-
-@RefreshScope
-@EnableConfigurationProperties
-@Configuration
-@ConfigurationProperties(prefix = "proxy.usage-stats-url")
-public class StatCollectorProperties {
- @Getter
- @Setter
- private String influxURL = "";
- @Getter
- @Setter
- private String jdbcURL = "";
- @Getter
- @Setter
- private String micrometerURL = "";
-
- public boolean backendExists() {
- return !influxURL.isEmpty() || !jdbcURL.isEmpty() || !micrometerURL.isEmpty();
- }
-}
diff --git a/src/main/java/hk/edu/polyu/comp/vlabcontroller/spec/expression/ExpressionAwareContainerSpec.java b/src/main/java/hk/edu/polyu/comp/vlabcontroller/spec/expression/ExpressionAwareContainerSpec.java
index 7c5c009c..d3fd3369 100644
--- a/src/main/java/hk/edu/polyu/comp/vlabcontroller/spec/expression/ExpressionAwareContainerSpec.java
+++ b/src/main/java/hk/edu/polyu/comp/vlabcontroller/spec/expression/ExpressionAwareContainerSpec.java
@@ -1,14 +1,13 @@
package hk.edu.polyu.comp.vlabcontroller.spec.expression;
import hk.edu.polyu.comp.vlabcontroller.model.runtime.Proxy;
-import hk.edu.polyu.comp.vlabcontroller.model.spec.EntryPointSpec;
import hk.edu.polyu.comp.vlabcontroller.model.spec.ContainerSpec;
+import hk.edu.polyu.comp.vlabcontroller.model.spec.EntryPointSpec;
import hk.edu.polyu.comp.vlabcontroller.model.spec.ResourceSpec;
import io.fabric8.kubernetes.api.model.VolumeMount;
import io.fabric8.kubernetes.api.model.VolumeMountBuilder;
import org.springframework.data.util.Pair;
-import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@@ -43,9 +42,8 @@ public List getCmd() {
}
public Map getEnv() {
- Map env = new HashMap<>();
- source.getEnv().entrySet().stream().forEach(e -> env.put(e.getKey(), resolve(e.getValue())));
- return env;
+ return source.getEnv().entrySet().stream()
+ .collect(Collectors.toMap(Map.Entry::getKey, x -> resolve(x.getValue())));
}
public String getEnvFile() {
@@ -74,10 +72,8 @@ public Map getPortMapping() {
@Override
public ResourceSpec getResources() {
- ResourceSpec resourceSpec = new ResourceSpec();
- source.getResources().getLimits().forEach((key, value) -> resourceSpec.getLimits().put(key, resolve(value)));
- source.getResources().getRequests().forEach((key, value) -> resourceSpec.getRequests().put(key, resolve(value)));
- return resourceSpec;
+ var resources = source.getResources();
+ return ResourceSpec.builder().limits(resources.getLimits()).requests(resources.getRequests()).build();
}
public boolean isPrivileged() {
@@ -91,9 +87,8 @@ public Map> getRuntimeLabels() {
}
public Map getSettings() {
- Map settings = new HashMap<>();
- source.getSettings().entrySet().stream().forEach(e -> settings.put(e.getKey(), resolve(e.getValue())));
- return settings;
+ return source.getSettings().entrySet().stream()
+ .collect(Collectors.toMap(Map.Entry::getKey, x -> resolve(x.getValue())));
}
public List getVolumeMounts() {
diff --git a/src/main/java/hk/edu/polyu/comp/vlabcontroller/spec/expression/SpecExpressionContext.java b/src/main/java/hk/edu/polyu/comp/vlabcontroller/spec/expression/SpecExpressionContext.java
index 2bfa0853..0e081d07 100644
--- a/src/main/java/hk/edu/polyu/comp/vlabcontroller/spec/expression/SpecExpressionContext.java
+++ b/src/main/java/hk/edu/polyu/comp/vlabcontroller/spec/expression/SpecExpressionContext.java
@@ -3,16 +3,19 @@
import hk.edu.polyu.comp.vlabcontroller.model.runtime.Proxy;
import hk.edu.polyu.comp.vlabcontroller.model.spec.ContainerSpec;
import hk.edu.polyu.comp.vlabcontroller.model.spec.ProxySpec;
+import lombok.Getter;
public class SpecExpressionContext {
-
+ @Getter
private ContainerSpec containerSpec;
+ @Getter
private ProxySpec proxySpec;
+ @Getter
private Proxy proxy;
public static SpecExpressionContext create(Object... objects) {
- SpecExpressionContext ctx = new SpecExpressionContext();
- for (Object o : objects) {
+ var ctx = new SpecExpressionContext();
+ for (var o : objects) {
if (o instanceof ContainerSpec) {
ctx.containerSpec = (ContainerSpec) o;
} else if (o instanceof ProxySpec) {
@@ -24,15 +27,4 @@ public static SpecExpressionContext create(Object... objects) {
return ctx;
}
- public ContainerSpec getContainerSpec() {
- return containerSpec;
- }
-
- public ProxySpec getProxySpec() {
- return proxySpec;
- }
-
- public Proxy getProxy() {
- return proxy;
- }
}
diff --git a/src/main/java/hk/edu/polyu/comp/vlabcontroller/spec/expression/SpecExpressionResolver.java b/src/main/java/hk/edu/polyu/comp/vlabcontroller/spec/expression/SpecExpressionResolver.java
index a00d0665..719277af 100644
--- a/src/main/java/hk/edu/polyu/comp/vlabcontroller/spec/expression/SpecExpressionResolver.java
+++ b/src/main/java/hk/edu/polyu/comp/vlabcontroller/spec/expression/SpecExpressionResolver.java
@@ -1,11 +1,10 @@
package hk.edu.polyu.comp.vlabcontroller.spec.expression;
+import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.expression.*;
-import org.springframework.core.convert.ConversionService;
-import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.ParserContext;
import org.springframework.expression.spel.standard.SpelExpressionParser;
@@ -21,6 +20,7 @@
* Note: inspired by org.springframework.context.expression.StandardBeanExpressionResolver
*/
@Component
+@RequiredArgsConstructor
public class SpecExpressionResolver {
private final Map evaluationCache = new ConcurrentHashMap<>(8);
@@ -40,33 +40,29 @@ public String getExpressionSuffix() {
return StandardBeanExpressionResolver.DEFAULT_EXPRESSION_SUFFIX;
}
};
- private final ExpressionParser expressionParser;
+ private final ExpressionParser expressionParser = new SpelExpressionParser();
private final ApplicationContext appContext;
- public SpecExpressionResolver(ApplicationContext appContext) {
- this.expressionParser = new SpelExpressionParser();
- this.appContext = appContext;
- }
-
public Object evaluate(String expression, SpecExpressionContext context) {
if (expression == null) return null;
if (expression.isEmpty()) return "";
- Expression expr = this.expressionParser.parseExpression(expression, this.beanExpressionParserContext);
+ var expr = this.expressionParser.parseExpression(expression, this.beanExpressionParserContext);
ConfigurableBeanFactory beanFactory = ((ConfigurableApplicationContext) appContext).getBeanFactory();
- StandardEvaluationContext sec = evaluationCache.get(context);
+ var sec = evaluationCache.get(context);
if (sec == null) {
- sec = new StandardEvaluationContext();
- sec.setRootObject(context);
- sec.addPropertyAccessor(new BeanExpressionContextAccessor());
- sec.addPropertyAccessor(new BeanFactoryAccessor());
- sec.addPropertyAccessor(new MapAccessor());
- sec.addPropertyAccessor(new EnvironmentAccessor());
- sec.setBeanResolver(new BeanFactoryResolver(appContext));
- sec.setTypeLocator(new StandardTypeLocator(beanFactory.getBeanClassLoader()));
- ConversionService conversionService = beanFactory.getConversionService();
+ sec = new StandardEvaluationContext() {{
+ setRootObject(context);
+ addPropertyAccessor(new BeanExpressionContextAccessor());
+ addPropertyAccessor(new BeanFactoryAccessor());
+ addPropertyAccessor(new MapAccessor());
+ addPropertyAccessor(new EnvironmentAccessor());
+ setBeanResolver(new BeanFactoryResolver(appContext));
+ setTypeLocator(new StandardTypeLocator(beanFactory.getBeanClassLoader()));
+ }};
+ var conversionService = beanFactory.getConversionService();
if (conversionService != null) sec.setTypeConverter(new StandardTypeConverter(conversionService));
evaluationCache.put(context, sec);
}
diff --git a/src/main/java/hk/edu/polyu/comp/vlabcontroller/spec/impl/DefaultSpecMergeStrategy.java b/src/main/java/hk/edu/polyu/comp/vlabcontroller/spec/impl/DefaultSpecMergeStrategy.java
index e85bff7e..b31c4138 100644
--- a/src/main/java/hk/edu/polyu/comp/vlabcontroller/spec/impl/DefaultSpecMergeStrategy.java
+++ b/src/main/java/hk/edu/polyu/comp/vlabcontroller/spec/impl/DefaultSpecMergeStrategy.java
@@ -5,8 +5,11 @@
import hk.edu.polyu.comp.vlabcontroller.spec.IProxySpecMergeStrategy;
import hk.edu.polyu.comp.vlabcontroller.spec.ProxySpecException;
import hk.edu.polyu.comp.vlabcontroller.spec.setting.SettingTypeRegistry;
+import lombok.RequiredArgsConstructor;
+import lombok.val;
import org.springframework.stereotype.Component;
+import java.util.Optional;
import java.util.Set;
import java.util.UUID;
@@ -14,40 +17,29 @@
* This default merge strategy allows any combination of base spec, runtime spec and runtime settings.
*/
@Component
+@RequiredArgsConstructor
public class DefaultSpecMergeStrategy implements IProxySpecMergeStrategy {
private final SettingTypeRegistry settingTypeRegistry;
- public DefaultSpecMergeStrategy(SettingTypeRegistry settingTypeRegistry) {
- this.settingTypeRegistry = settingTypeRegistry;
- }
-
@Override
public ProxySpec merge(ProxySpec baseSpec, ProxySpec runtimeSpec, Set runtimeSettings) throws ProxySpecException {
- if (baseSpec == null && runtimeSpec == null)
+ val hasBase = baseSpec != null;
+ val hasRuntime = runtimeSpec != null;
+ if (!(hasBase || hasRuntime))
throw new ProxySpecException("No base or runtime proxy spec provided");
- ProxySpec finalSpec = new ProxySpec();
- copySpec(baseSpec, finalSpec);
- copySpec(runtimeSpec, finalSpec);
+ var finalSpec = (hasBase && hasRuntime)
+ ? runtimeSpec.copyToBuilder(baseSpec.copyBuilder()).build()
+ : (hasBase ? baseSpec : runtimeSpec);
- if (runtimeSettings != null) {
- for (RuntimeSetting setting : runtimeSettings) {
- settingTypeRegistry.applySetting(setting, finalSpec);
- }
+ for (var setting : Optional.ofNullable(runtimeSettings).orElse(Set.of())) {
+ settingTypeRegistry.applySetting(setting, finalSpec);
}
if (finalSpec.getId() == null) {
- var id = UUID.randomUUID().toString();
- finalSpec.setId(id);
- for (var containerSpec : finalSpec.getContainerSpecs()) {
- containerSpec.getEnv().put("PUBLIC_PATH", DefaultSpecProvider.getPublicPath(id));
- }
+ finalSpec.setId(UUID.randomUUID().toString());
+ finalSpec.populateContainerSpecPublicPathById();
}
return finalSpec;
}
-
- protected void copySpec(ProxySpec from, ProxySpec to) {
- if (from == null || to == null) return;
- from.copy(to);
- }
}
diff --git a/src/main/java/hk/edu/polyu/comp/vlabcontroller/spec/impl/DefaultSpecProvider.java b/src/main/java/hk/edu/polyu/comp/vlabcontroller/spec/impl/DefaultSpecProvider.java
deleted file mode 100644
index 84a75efd..00000000
--- a/src/main/java/hk/edu/polyu/comp/vlabcontroller/spec/impl/DefaultSpecProvider.java
+++ /dev/null
@@ -1,51 +0,0 @@
-package hk.edu.polyu.comp.vlabcontroller.spec.impl;
-
-import hk.edu.polyu.comp.vlabcontroller.model.spec.ProxySpec;
-import hk.edu.polyu.comp.vlabcontroller.spec.IProxySpecProvider;
-import hk.edu.polyu.comp.vlabcontroller.util.SessionHelper;
-import lombok.Getter;
-import lombok.Setter;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.context.properties.ConfigurationProperties;
-import org.springframework.context.annotation.Primary;
-import org.springframework.core.env.Environment;
-import org.springframework.stereotype.Component;
-
-import javax.annotation.PostConstruct;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.stream.Collectors;
-
-@Component
-@Primary
-@ConfigurationProperties(prefix = "proxy")
-public class DefaultSpecProvider implements IProxySpecProvider {
- @Getter
- @Setter
- private List specs = new ArrayList<>();
-
- public ProxySpec getSpec(String id) {
- if (id == null || id.isEmpty()) return null;
- return specs.stream().filter(s -> id.equals(s.getId())).findAny().orElse(null);
- }
-
- @PostConstruct
- public void afterPropertiesSet() {
- this.specs.stream().collect(Collectors.groupingBy(ProxySpec::getId)).forEach((id, duplicateSpecs) -> {
- if (duplicateSpecs.size() > 1)
- throw new IllegalArgumentException(String.format("Configuration error: spec with id '%s' is defined multiple times", id));
- });
- }
-
- private static Environment environment;
-
- @Autowired
- public void setEnvironment(Environment env) {
- DefaultSpecProvider.environment = env;
- }
-
- public static String getPublicPath(String appName) {
- String contextPath = SessionHelper.getContextPath(environment, true);
- return contextPath + "app_direct/" + appName + "/";
- }
-}
diff --git a/src/main/java/hk/edu/polyu/comp/vlabcontroller/spec/impl/VLabControllerSpecMergeStrategy.java b/src/main/java/hk/edu/polyu/comp/vlabcontroller/spec/impl/VLabControllerSpecMergeStrategy.java
index 2f0636b1..fe5cc806 100644
--- a/src/main/java/hk/edu/polyu/comp/vlabcontroller/spec/impl/VLabControllerSpecMergeStrategy.java
+++ b/src/main/java/hk/edu/polyu/comp/vlabcontroller/spec/impl/VLabControllerSpecMergeStrategy.java
@@ -19,10 +19,7 @@ public ProxySpec merge(ProxySpec baseSpec, ProxySpec runtimeSpec, Set= target.getContainerSpecs().size()) doFail(spec, "container index too high");
targetObject = target.getContainerSpecs().get(index);
if (nameParts.length < 2) doFail(spec, "no container field specified");
diff --git a/src/main/java/hk/edu/polyu/comp/vlabcontroller/spec/setting/SettingTypeRegistry.java b/src/main/java/hk/edu/polyu/comp/vlabcontroller/spec/setting/SettingTypeRegistry.java
index 2bc88ed9..ea949b74 100644
--- a/src/main/java/hk/edu/polyu/comp/vlabcontroller/spec/setting/SettingTypeRegistry.java
+++ b/src/main/java/hk/edu/polyu/comp/vlabcontroller/spec/setting/SettingTypeRegistry.java
@@ -4,6 +4,7 @@
import hk.edu.polyu.comp.vlabcontroller.model.spec.ProxySpec;
import hk.edu.polyu.comp.vlabcontroller.model.spec.RuntimeSettingSpec;
import hk.edu.polyu.comp.vlabcontroller.spec.ProxySpecException;
+import lombok.Setter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@@ -22,8 +23,7 @@
*/
@Component
public class SettingTypeRegistry {
-
- @Autowired(required = false)
+ @Setter(onMethod_ = {@Autowired(required = false)})
private Map typeMap = new HashMap<>();
public RuntimeSettingSpec resolveSpec(RuntimeSetting setting, ProxySpec proxySpec) {
@@ -31,7 +31,7 @@ public RuntimeSettingSpec resolveSpec(RuntimeSetting setting, ProxySpec proxySpe
}
public IRuntimeSettingType resolveSpecType(RuntimeSettingSpec settingSpec) {
- String type = settingSpec.getType();
+ var type = settingSpec.getType();
if (type == null || type.isEmpty()) {
//TODO try to determine the type via the spec config
type = "setting.type.string";
@@ -40,10 +40,10 @@ public IRuntimeSettingType resolveSpecType(RuntimeSettingSpec settingSpec) {
}
public void applySetting(RuntimeSetting setting, ProxySpec targetSpec) throws ProxySpecException {
- RuntimeSettingSpec settingSpec = resolveSpec(setting, targetSpec);
+ var settingSpec = resolveSpec(setting, targetSpec);
if (settingSpec == null) return;
- IRuntimeSettingType type = resolveSpecType(settingSpec);
+ var type = resolveSpecType(settingSpec);
if (type == null) throw new ProxySpecException("Unknown setting type: " + settingSpec.getType());
type.apply(setting, settingSpec, targetSpec);
diff --git a/src/main/java/hk/edu/polyu/comp/vlabcontroller/spec/setting/type/AbstractSettingType.java b/src/main/java/hk/edu/polyu/comp/vlabcontroller/spec/setting/type/AbstractSettingType.java
index 3007c473..fba7377b 100644
--- a/src/main/java/hk/edu/polyu/comp/vlabcontroller/spec/setting/type/AbstractSettingType.java
+++ b/src/main/java/hk/edu/polyu/comp/vlabcontroller/spec/setting/type/AbstractSettingType.java
@@ -8,6 +8,7 @@
import hk.edu.polyu.comp.vlabcontroller.spec.ProxySpecException;
import hk.edu.polyu.comp.vlabcontroller.spec.setting.IRuntimeSettingType;
import hk.edu.polyu.comp.vlabcontroller.spec.setting.SettingSpecMapper;
+import lombok.Setter;
/**
* Example runtime settings:
@@ -34,13 +35,12 @@
* Each class translates into several settings, e.g. cpu & memory
*/
public abstract class AbstractSettingType implements IRuntimeSettingType {
-
- @Inject
+ @Setter(onMethod_ = {@Inject})
protected SettingSpecMapper mapper;
@Override
public void apply(RuntimeSetting setting, RuntimeSettingSpec settingSpec, ProxySpec targetSpec) throws ProxySpecException {
- Object value = getValue(setting, settingSpec);
+ var value = getValue(setting, settingSpec);
if (value == null) return;
mapper.mapValue(value, settingSpec, targetSpec);
}
diff --git a/src/main/java/hk/edu/polyu/comp/vlabcontroller/stat/StatCollectorRegistry.java b/src/main/java/hk/edu/polyu/comp/vlabcontroller/stat/StatCollectorRegistry.java
index 946473a3..94243161 100644
--- a/src/main/java/hk/edu/polyu/comp/vlabcontroller/stat/StatCollectorRegistry.java
+++ b/src/main/java/hk/edu/polyu/comp/vlabcontroller/stat/StatCollectorRegistry.java
@@ -1,62 +1,56 @@
package hk.edu.polyu.comp.vlabcontroller.stat;
-import hk.edu.polyu.comp.vlabcontroller.spec.StatCollectorProperties;
+import hk.edu.polyu.comp.vlabcontroller.config.ProxyProperties;
import hk.edu.polyu.comp.vlabcontroller.stat.impl.InfluxDBCollector;
import hk.edu.polyu.comp.vlabcontroller.stat.impl.JDBCCollector;
import hk.edu.polyu.comp.vlabcontroller.stat.impl.Micrometer;
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.Logger;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
+import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
-import org.springframework.core.env.Environment;
import java.util.function.Consumer;
+@Slf4j
@Configuration
+@RequiredArgsConstructor
+@RefreshScope
class StatCollectorFactory {
-
- private final Logger log = LogManager.getLogger(StatCollectorFactory.class);
-
- private final Environment environment;
private final ApplicationContext applicationContext;
- private final StatCollectorProperties statCollectorProperties;
-
- public StatCollectorFactory(Environment environment, ApplicationContext applicationContext, StatCollectorProperties statCollectorProperties) {
- this.environment = environment;
- this.applicationContext = applicationContext;
- this.statCollectorProperties = statCollectorProperties;
- }
+ private final ProxyProperties proxyProperties;
@Bean
public IStatCollector statsCollector() {
// create beans manually, spring will not create beans automatically when null returned
- if (!statCollectorProperties.backendExists()) {
+ var url = proxyProperties.getUsageStats().getUrl();
+ if (!url.backendExists()) {
log.info("Disabled. Usage statistics will not be processed.");
return null;
}
- ConfigurableApplicationContext configurableApplicationContext = (ConfigurableApplicationContext) applicationContext;
- DefaultListableBeanFactory defaultListableBeanFactory = (DefaultListableBeanFactory) configurableApplicationContext.getAutowireCapableBeanFactory();
+ var configurableApplicationContext = (ConfigurableApplicationContext) applicationContext;
+ var defaultListableBeanFactory = (DefaultListableBeanFactory) configurableApplicationContext.getAutowireCapableBeanFactory();
- Consumer> createBean = (Class> klass) -> {
- BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(klass);
+ var createBean = (Consumer>) (Class> klass) -> {
+ var beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(klass);
defaultListableBeanFactory.registerBeanDefinition(klass.getName() + "Bean", beanDefinitionBuilder.getBeanDefinition());
};
- if (statCollectorProperties.getInfluxURL().contains("/write?db=")) {
+ if (url.getInflux().contains("/write?db=")) {
createBean.accept(InfluxDBCollector.class);
- log.info("Influx DB backend enabled, sending usage statics to {}", statCollectorProperties.getInfluxURL());
+ log.info("Influx DB backend enabled, sending usage statics to {}", url.getInflux());
}
- if (statCollectorProperties.getJdbcURL().contains("jdbc")) {
+ if (url.getJdbc().contains("jdbc")) {
createBean.accept(JDBCCollector.class);
- log.info("JDBC backend enabled, sending usage statistics to {}", statCollectorProperties.getJdbcURL());
+ log.info("JDBC backend enabled, sending usage statistics to {}", url.getJdbc());
}
- if (statCollectorProperties.getMicrometerURL().contains("micrometer")) {
+ if (url.getMicrometer().contains("micrometer")) {
createBean.accept(Micrometer.class);
log.info("Prometheus (Micrometer) backend enabled");
}
diff --git a/src/main/java/hk/edu/polyu/comp/vlabcontroller/stat/impl/AbstractDbCollector.java b/src/main/java/hk/edu/polyu/comp/vlabcontroller/stat/impl/AbstractDbCollector.java
index 913b05bf..879ae448 100644
--- a/src/main/java/hk/edu/polyu/comp/vlabcontroller/stat/impl/AbstractDbCollector.java
+++ b/src/main/java/hk/edu/polyu/comp/vlabcontroller/stat/impl/AbstractDbCollector.java
@@ -1,12 +1,20 @@
package hk.edu.polyu.comp.vlabcontroller.stat.impl;
-import hk.edu.polyu.comp.vlabcontroller.stat.IStatCollector;
+import hk.edu.polyu.comp.vlabcontroller.config.ProxyProperties;
import hk.edu.polyu.comp.vlabcontroller.event.*;
+import hk.edu.polyu.comp.vlabcontroller.stat.IStatCollector;
+import lombok.Setter;
+import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.context.event.EventListener;
+import javax.inject.Inject;
import java.io.IOException;
+@RefreshScope
public abstract class AbstractDbCollector implements IStatCollector {
+ @Setter(onMethod_ = {@Inject})
+ protected ProxyProperties proxyProperties;
+
@EventListener
public void onUserLogoutEvent(UserLogoutEvent event) throws IOException {
writeToDb(event.getTimestamp(), event.getUserId(), "Logout", null, String.valueOf(event.getWasExpired()));
diff --git a/src/main/java/hk/edu/polyu/comp/vlabcontroller/stat/impl/InfluxDBCollector.java b/src/main/java/hk/edu/polyu/comp/vlabcontroller/stat/impl/InfluxDBCollector.java
index c13114e1..b33dc5a6 100644
--- a/src/main/java/hk/edu/polyu/comp/vlabcontroller/stat/impl/InfluxDBCollector.java
+++ b/src/main/java/hk/edu/polyu/comp/vlabcontroller/stat/impl/InfluxDBCollector.java
@@ -1,10 +1,5 @@
package hk.edu.polyu.comp.vlabcontroller.stat.impl;
-import org.apache.commons.io.IOUtils;
-import org.springframework.core.env.Environment;
-
-import javax.annotation.PostConstruct;
-import javax.inject.Inject;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
@@ -19,39 +14,33 @@
* usage-stats-url: http://localhost:8086/write?db=usagestats
*/
public class InfluxDBCollector extends AbstractDbCollector {
-
- private String destination;
- @Inject
- private Environment environment;
-
- @PostConstruct
- public void init() {
- destination = environment.getProperty("proxy.usage-stats-url.influx-url");
+ public String getDestination() {
+ return proxyProperties.getUsageStats().getUrl().getInflux();
}
@Override
protected void writeToDb(long timestamp, String userId, String type, String specId, String info) throws IOException {
- String identifier = environment.getProperty("proxy.identifier-value", "default-identifier");
- String body = String.format("event,username=%s,type=%s,identifier=%s specid=\"%s\",info=\"%s\"",
+ var identifier = proxyProperties.getIdentifierValue();
+ var body = String.format("event,username=%s,type=%s,identifier=%s specid=\"%s\",info=\"%s\"",
userId.replace(" ", "\\ "),
type.replace(" ", "\\ "),
identifier.replace(" ", "\\ "),
Optional.ofNullable(specId).orElse(""),
Optional.ofNullable(info).orElse(""));
- HttpURLConnection conn = (HttpURLConnection) new URL(destination).openConnection();
+ var conn = (HttpURLConnection) new URL(getDestination()).openConnection();
conn.setRequestMethod("POST");
conn.setDoOutput(true);
- try (DataOutputStream dos = new DataOutputStream(conn.getOutputStream())) {
+ try (var dos = new DataOutputStream(conn.getOutputStream())) {
dos.write(body.getBytes(StandardCharsets.UTF_8));
dos.flush();
}
- int responseCode = conn.getResponseCode();
+ var responseCode = conn.getResponseCode();
if (responseCode == 204) {
// All is well.
} else {
- ByteArrayOutputStream bos = new ByteArrayOutputStream();
- IOUtils.copy(conn.getErrorStream(), bos);
+ var bos = new ByteArrayOutputStream();
+ conn.getErrorStream().transferTo(bos);
throw new IOException(bos.toString());
}
}
diff --git a/src/main/java/hk/edu/polyu/comp/vlabcontroller/stat/impl/JDBCCollector.java b/src/main/java/hk/edu/polyu/comp/vlabcontroller/stat/impl/JDBCCollector.java
index e7bf5107..d3ed37ce 100644
--- a/src/main/java/hk/edu/polyu/comp/vlabcontroller/stat/impl/JDBCCollector.java
+++ b/src/main/java/hk/edu/polyu/comp/vlabcontroller/stat/impl/JDBCCollector.java
@@ -1,13 +1,9 @@
package hk.edu.polyu.comp.vlabcontroller.stat.impl;
import com.zaxxer.hikari.HikariDataSource;
-import org.springframework.core.env.Environment;
import javax.annotation.PostConstruct;
-import javax.inject.Inject;
import java.io.IOException;
-import java.sql.Connection;
-import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Timestamp;
@@ -32,57 +28,55 @@
* varchar(128), data text );
*/
public class JDBCCollector extends AbstractDbCollector {
-
private HikariDataSource ds;
- @Inject
- private Environment environment;
-
@PostConstruct
public void init() {
- String baseURL = environment.getProperty("proxy.usage-stats-url.jdbc-url");
- String username = environment.getProperty("proxy.usage-stats-username", "monetdb");
- String password = environment.getProperty("proxy.usage-stats-password", "monetdb");
- ds = new HikariDataSource();
- ds.setJdbcUrl(baseURL);
- ds.setUsername(username);
- ds.setPassword(password);
- ds.addDataSourceProperty("useJDBCCompliantTimezoneShift", "true");
- ds.addDataSourceProperty("serverTimezone", "UTC");
+ var usageStats = proxyProperties.getUsageStats();
+ var baseURL = usageStats.getUrl().getJdbc();
+ var username = usageStats.getUsername();
+ var password = usageStats.getPassword();
+ ds = new HikariDataSource() {{
+ setJdbcUrl(baseURL);
+ setUsername(username);
+ setPassword(password);
+ addDataSourceProperty("useJDBCCompliantTimezoneShift", "true");
+ addDataSourceProperty("serverTimezone", "UTC");
+ }};
- Long connectionTimeout = environment.getProperty("proxy.usage-stats-hikari.connection-timeout", Long.class);
- if (connectionTimeout != null) {
- ds.setConnectionTimeout(connectionTimeout);
+ var hikari = usageStats.getHikari();
+ var connectionTimeout = hikari.getConnectionTimeout();
+ if (!connectionTimeout.isNegative()) {
+ ds.setConnectionTimeout(connectionTimeout.toMillis());
}
- Long idleTimeout = environment.getProperty("proxy.usage-stats-hikari.idle-timeout", Long.class);
- if (idleTimeout != null) {
- ds.setIdleTimeout(idleTimeout);
+ var idleTimeout = hikari.getIdleTimeout();
+ if (!idleTimeout.isNegative()) {
+ ds.setIdleTimeout(idleTimeout.toMillis());
}
- Long maxLifetime = environment.getProperty("proxy.usage-stats-hikari.max-lifetime", Long.class);
- if (maxLifetime != null) {
- ds.setMaxLifetime(maxLifetime);
+ var maxLifetime = hikari.getMaxLifetime();
+ if (!maxLifetime.isNegative()) {
+ ds.setMaxLifetime(maxLifetime.toMillis());
}
- Integer minimumIdle = environment.getProperty("proxy.usage-stats-hikari.minimum-idle", Integer.class);
- if (minimumIdle != null) {
+ var minimumIdle = hikari.getMinimumIdle();
+ if (minimumIdle >= 0) {
ds.setMinimumIdle(minimumIdle);
}
- Integer maximumPoolSize = environment.getProperty("proxy.usage-stats-hikari.maximum-pool-size", Integer.class);
- if (maximumPoolSize != null) {
+ var maximumPoolSize = hikari.getMaximumPoolSize();
+ if (maximumPoolSize >= 0) {
ds.setMaximumPoolSize(maximumPoolSize);
}
-
}
@Override
protected void writeToDb(long timestamp, String userId, String type, String specId, String info) throws IOException {
- String identifier = environment.getProperty("proxy.identifier-value", "default-identifier");
- String sql = "INSERT INTO event(event_time, username, type, specid, identifier, info) VALUES (?,?,?,?,?,?)";
- try (Connection con = ds.getConnection()) {
- try (PreparedStatement stmt = con.prepareStatement(sql)) {
+ var identifier = proxyProperties.getIdentifierValue();
+ var sql = "INSERT INTO event(event_time, username, type, specid, identifier, info) VALUES (?,?,?,?,?,?)";
+ try (var con = ds.getConnection()) {
+ try (var stmt = con.prepareStatement(sql)) {
stmt.setTimestamp(1, new Timestamp(timestamp));
stmt.setString(2, userId);
stmt.setString(3, type);
diff --git a/src/main/java/hk/edu/polyu/comp/vlabcontroller/stat/impl/Micrometer.java b/src/main/java/hk/edu/polyu/comp/vlabcontroller/stat/impl/Micrometer.java
index 92095ecb..b702bc87 100644
--- a/src/main/java/hk/edu/polyu/comp/vlabcontroller/stat/impl/Micrometer.java
+++ b/src/main/java/hk/edu/polyu/comp/vlabcontroller/stat/impl/Micrometer.java
@@ -1,30 +1,27 @@
package hk.edu.polyu.comp.vlabcontroller.stat.impl;
+import hk.edu.polyu.comp.vlabcontroller.event.*;
import hk.edu.polyu.comp.vlabcontroller.service.ProxyService;
import hk.edu.polyu.comp.vlabcontroller.stat.IStatCollector;
-import hk.edu.polyu.comp.vlabcontroller.event.*;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.Logger;
+import lombok.Setter;
+import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.EventListener;
import javax.annotation.PostConstruct;
import javax.inject.Inject;
+@Slf4j
public class Micrometer implements IStatCollector {
-
- private final Logger logger = LogManager.getLogger(getClass());
- @Inject
+ @Setter(onMethod_ = {@Inject})
private MeterRegistry registry;
- @Inject
+ @Setter(onMethod_ = {@Inject})
private ProxyService proxyService;
- private Counter appStartFailedCounter;
+ private Counter appStartFailedCounter;
private Counter authFailedCounter;
-
private Counter userLogins;
-
private Counter userLogouts;
@PostConstruct
@@ -39,14 +36,14 @@ public void init() {
@EventListener
public void onUserLogoutEvent(UserLogoutEvent event) {
- logger.debug("UserLogoutEvent [user: {}, sessionId: {}, expired: {}]", event.getUserId(), event.getSessionId(), event.getWasExpired());
+ log.debug("UserLogoutEvent [user: {}, sessionId: {}, expired: {}]", event.getUserId(), event.getSessionId(), event.getWasExpired());
userLogouts.increment();
registry.counter("userIdLogouts", "user.id", event.getUserId()).increment();
}
@EventListener
public void onUserLoginEvent(UserLoginEvent event) {
- logger.debug("UserLoginEvent [user: {}, sessionId: {}]", event.getUserId(), event.getSessionId());
+ log.debug("UserLoginEvent [user: {}, sessionId: {}]", event.getUserId(), event.getSessionId());
userLogins.increment();
registry.counter("userIdLogins", "user.id", event.getUserId()).increment();
registry.counter("userIdLogouts", "user.id", event.getUserId()).increment(0);
@@ -54,27 +51,27 @@ public void onUserLoginEvent(UserLoginEvent event) {
@EventListener
public void onProxyStartEvent(ProxyStartEvent event) {
- logger.debug("ProxyStartEvent [user: {}, startupTime: {}]", event.getUserId(), event.getStartupTime());
+ log.debug("ProxyStartEvent [user: {}, startupTime: {}]", event.getUserId(), event.getStartupTime());
registry.counter("appStarts", "spec.id", event.getSpecId(), "user.id", event.getUserId()).increment();
registry.timer("startupTime", "spec.id", event.getSpecId(), "user.id", event.getUserId()).record(event.getStartupTime());
}
@EventListener
public void onProxyStopEvent(ProxyStopEvent event) {
- logger.debug("ProxyStopEvent [user: {}, usageTime: {}]", event.getUserId(), event.getUsageTime());
+ log.debug("ProxyStopEvent [user: {}, usageTime: {}]", event.getUserId(), event.getUsageTime());
registry.counter("appStops", "spec.id", event.getSpecId(), "user.id", event.getUserId()).increment();
registry.timer("usageTime", "spec.id", event.getSpecId(), "user.id", event.getUserId()).record(event.getUsageTime());
}
@EventListener
public void onProxyStartFailedEvent(ProxyStartFailedEvent event) {
- logger.debug("ProxyStartFailedEvent [user: {}, specId: {}]", event.getUserId(), event.getSpecId());
+ log.debug("ProxyStartFailedEvent [user: {}, specId: {}]", event.getUserId(), event.getSpecId());
appStartFailedCounter.increment();
}
@EventListener
public void onAuthFailedEvent(AuthFailedEvent event) {
- logger.debug("AuthFailedEvent [user: {}, sessionId: {}]", event.getUserId(), event.getSessionId());
+ log.debug("AuthFailedEvent [user: {}, sessionId: {}]", event.getUserId(), event.getSessionId());
authFailedCounter.increment();
}
diff --git a/src/main/java/hk/edu/polyu/comp/vlabcontroller/ui/AuthController.java b/src/main/java/hk/edu/polyu/comp/vlabcontroller/ui/AuthController.java
index 4eac19af..d8fc5536 100644
--- a/src/main/java/hk/edu/polyu/comp/vlabcontroller/ui/AuthController.java
+++ b/src/main/java/hk/edu/polyu/comp/vlabcontroller/ui/AuthController.java
@@ -3,6 +3,7 @@
import hk.edu.polyu.comp.vlabcontroller.api.BaseController;
import hk.edu.polyu.comp.vlabcontroller.auth.IAuthenticationBackend;
import hk.edu.polyu.comp.vlabcontroller.auth.impl.OpenIDAuthenticationBackend;
+import lombok.RequiredArgsConstructor;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
@@ -13,16 +14,10 @@
import java.util.Optional;
@Controller
+@RequiredArgsConstructor
public class AuthController extends BaseController {
-
- private final Environment environment;
-
private final IAuthenticationBackend auth;
-
- public AuthController(Environment environment, IAuthenticationBackend auth) {
- this.environment = environment;
- this.auth = auth;
- }
+ private final Environment environment;
@GetMapping(value = "/login")
public Object getLoginPage(@RequestParam Optional error, ModelMap map) {
diff --git a/src/main/java/hk/edu/polyu/comp/vlabcontroller/ui/ErrorController.java b/src/main/java/hk/edu/polyu/comp/vlabcontroller/ui/ErrorController.java
index 3d49c413..1ce1dd53 100644
--- a/src/main/java/hk/edu/polyu/comp/vlabcontroller/ui/ErrorController.java
+++ b/src/main/java/hk/edu/polyu/comp/vlabcontroller/ui/ErrorController.java
@@ -2,7 +2,7 @@
import hk.edu.polyu.comp.vlabcontroller.api.BaseController;
import hk.edu.polyu.comp.vlabcontroller.auth.impl.keycloak.AuthenticationFailureHandler;
-import lombok.extern.log4j.Log4j2;
+import lombok.extern.slf4j.Slf4j;
import org.keycloak.adapters.OIDCAuthenticationError;
import org.keycloak.adapters.springsecurity.authentication.KeycloakCookieBasedRedirect;
import org.springframework.http.HttpStatus;
@@ -21,37 +21,36 @@
import java.util.HashMap;
import java.util.Map;
-@Log4j2
+@Slf4j
@Controller
@RequestMapping("/error")
public class ErrorController extends BaseController implements org.springframework.boot.web.servlet.error.ErrorController {
-
@RequestMapping(produces = "text/html")
public String handleError(ModelMap map, HttpServletRequest request, HttpServletResponse response) {
// handle keycloak errors
- Object obj = request.getSession().getAttribute(AuthenticationFailureHandler.SP_KEYCLOAK_ERROR_REASON);
+ var obj = request.getSession().getAttribute(AuthenticationFailureHandler.SP_KEYCLOAK_ERROR_REASON);
if (obj instanceof OIDCAuthenticationError.Reason) {
request.getSession().removeAttribute(AuthenticationFailureHandler.SP_KEYCLOAK_ERROR_REASON);
- OIDCAuthenticationError.Reason reason = (OIDCAuthenticationError.Reason) obj;
+ var reason = (OIDCAuthenticationError.Reason) obj;
if (reason == OIDCAuthenticationError.Reason.INVALID_STATE_COOKIE ||
reason == OIDCAuthenticationError.Reason.STALE_TOKEN) {
// These errors are typically caused by users using wrong bookmarks (e.g. bookmarks with states in)
// or when some cookies got stale. However, the user is logged into the IDP, therefore it's enough to
// send the user to the main page, and they will get logged in automatically.
- response.addCookie(KeycloakCookieBasedRedirect.createCookieFromRedirectUrl((String) null));
+ response.addCookie(KeycloakCookieBasedRedirect.createCookieFromRedirectUrl(null));
return "redirect:/";
} else {
return "redirect:/auth-error";
}
}
- Throwable exception = (Throwable) request.getAttribute("javax.servlet.error.exception");
+ var exception = (Throwable) request.getAttribute("javax.servlet.error.exception");
if (exception == null) {
exception = (Throwable) request.getAttribute("SPRING_SECURITY_LAST_EXCEPTION");
}
- String[] msg = createMsgStack(exception);
+ var msg = createMsgStack(exception);
if (exception == null) {
msg[0] = HttpStatus.valueOf(response.getStatus()).getReasonPhrase();
}
@@ -62,7 +61,7 @@ public String handleError(ModelMap map, HttpServletRequest request, HttpServletR
if (isIllegalStateException(exception)) {
log.warn("No state cookie on login attempt, force redirect to homepage");
- response.addCookie(KeycloakCookieBasedRedirect.createCookieFromRedirectUrl((String) null));
+ response.addCookie(KeycloakCookieBasedRedirect.createCookieFromRedirectUrl(null));
return "redirect:/";
}
@@ -77,8 +76,8 @@ public String handleError(ModelMap map, HttpServletRequest request, HttpServletR
@RequestMapping(consumes = "application/json", produces = "application/json")
@ResponseBody
public ResponseEntity> error(HttpServletRequest request, HttpServletResponse response) {
- Throwable exception = (Throwable) request.getAttribute("javax.servlet.error.exception");
- String[] msg = createMsgStack(exception);
+ var exception = (Throwable) request.getAttribute("javax.servlet.error.exception");
+ var msg = createMsgStack(exception);
Map map = new HashMap<>();
map.put("message", msg[0]);
@@ -92,16 +91,16 @@ public String getErrorPath() {
}
private String[] createMsgStack(Throwable exception) {
- String message = "";
- String stackTrace = "";
+ var message = "";
+ var stackTrace = "";
if (exception instanceof NestedServletException && exception.getCause() instanceof Exception) {
exception = exception.getCause();
}
if (exception != null) {
if (exception.getMessage() != null) message = exception.getMessage();
- ByteArrayOutputStream bos = new ByteArrayOutputStream();
- try (PrintWriter writer = new PrintWriter(bos)) {
+ var bos = new ByteArrayOutputStream();
+ try (var writer = new PrintWriter(bos)) {
exception.printStackTrace(writer);
}
stackTrace = bos.toString();
diff --git a/src/main/java/hk/edu/polyu/comp/vlabcontroller/ui/FaviconConfig.java b/src/main/java/hk/edu/polyu/comp/vlabcontroller/ui/FaviconConfig.java
index 849a6c95..2b13e505 100644
--- a/src/main/java/hk/edu/polyu/comp/vlabcontroller/ui/FaviconConfig.java
+++ b/src/main/java/hk/edu/polyu/comp/vlabcontroller/ui/FaviconConfig.java
@@ -1,11 +1,13 @@
package hk.edu.polyu.comp.vlabcontroller.ui;
-import org.apache.logging.log4j.LogManager;
+import hk.edu.polyu.comp.vlabcontroller.config.ProxyProperties;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
-import org.springframework.core.env.Environment;
import org.springframework.http.MediaType;
import org.springframework.http.MediaTypeFactory;
import org.springframework.util.FileCopyUtils;
@@ -16,40 +18,36 @@
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
-import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collections;
+@Slf4j
@Configuration
+@RequiredArgsConstructor
+@RefreshScope
public class FaviconConfig {
-
private static final String CONTENT_TYPE_ICO = "image/x-icon";
-
- private final Environment environment;
-
- public FaviconConfig(Environment environment) {
- this.environment = environment;
- }
+ private final ProxyProperties proxyProperties;
@Bean
@ConditionalOnProperty(name = "proxy.favicon-path")
public SimpleUrlHandlerMapping customFaviconHandlerMapping() {
byte[] cachedIcon = null;
- Path iconPath = Paths.get(environment.getProperty("proxy.favicon-path"));
+ var iconPath = Paths.get(proxyProperties.getFaviconPath());
if (Files.isRegularFile(iconPath)) {
- try (InputStream input = Files.newInputStream(iconPath)) {
+ try (var input = Files.newInputStream(iconPath)) {
cachedIcon = FileCopyUtils.copyToByteArray(input);
} catch (IOException e) {
throw new IllegalArgumentException("Cannot read favicon: " + iconPath, e);
}
} else {
- LogManager.getLogger(FaviconConfig.class).error("Invalid favicon path: " + iconPath);
+ log.error("Invalid favicon path: " + iconPath);
}
- SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
+ var mapping = new SimpleUrlHandlerMapping();
mapping.setOrder(Ordered.HIGHEST_PRECEDENCE);
mapping.setUrlMap(Collections.singletonMap("**/favicon.???", new CachedFaviconHttpRequestHandler(cachedIcon, iconPath)));
return mapping;
@@ -75,10 +73,10 @@ public void handleRequest(HttpServletRequest request, HttpServletResponse respon
}
private String getContentType() {
- String fileName = iconPath.getFileName().toString().toLowerCase();
+ var fileName = iconPath.getFileName().toString().toLowerCase();
if (fileName.endsWith(".ico")) return CONTENT_TYPE_ICO;
- MediaType mediaType = MediaTypeFactory.getMediaType(fileName).orElse(MediaType.APPLICATION_OCTET_STREAM);
+ var mediaType = MediaTypeFactory.getMediaType(fileName).orElse(MediaType.APPLICATION_OCTET_STREAM);
return mediaType.toString();
}
}
diff --git a/src/main/java/hk/edu/polyu/comp/vlabcontroller/ui/TemplateResolverConfig.java b/src/main/java/hk/edu/polyu/comp/vlabcontroller/ui/TemplateResolverConfig.java
index cd38720b..003d0f1f 100644
--- a/src/main/java/hk/edu/polyu/comp/vlabcontroller/ui/TemplateResolverConfig.java
+++ b/src/main/java/hk/edu/polyu/comp/vlabcontroller/ui/TemplateResolverConfig.java
@@ -1,38 +1,35 @@
package hk.edu.polyu.comp.vlabcontroller.ui;
+import hk.edu.polyu.comp.vlabcontroller.config.ProxyProperties;
+import lombok.RequiredArgsConstructor;
+import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
-import org.springframework.core.env.Environment;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.thymeleaf.templateresolver.FileTemplateResolver;
-import javax.inject.Inject;
-
@Configuration
+@RequiredArgsConstructor
+@RefreshScope
public class TemplateResolverConfig implements WebMvcConfigurer {
- private final Environment environment;
-
- public TemplateResolverConfig(Environment environment) {
- this.environment = environment;
- }
+ private final ProxyProperties proxyProperties;
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/assets/**")
- .addResourceLocations("file:" + environment.getProperty("proxy.template-path") + "/assets/");
+ .addResourceLocations("file:" + proxyProperties.getTemplatePath() + "/assets/");
}
@Bean
public FileTemplateResolver templateResolver() {
- FileTemplateResolver resolver = new FileTemplateResolver();
- resolver.setPrefix(environment.getProperty("proxy.template-path") + "/");
-
- resolver.setSuffix(".html");
- resolver.setTemplateMode("HTML5");
- resolver.setCacheable(false);
- resolver.setCheckExistence(true);
- resolver.setOrder(1);
- return resolver;
+ return new FileTemplateResolver() {{
+ setPrefix(proxyProperties.getTemplatePath() + "/");
+ setSuffix(".html");
+ setTemplateMode("HTML5");
+ setCacheable(false);
+ setCheckExistence(true);
+ setOrder(1);
+ }};
}
}
diff --git a/src/main/java/hk/edu/polyu/comp/vlabcontroller/util/ChannelActiveListener.java b/src/main/java/hk/edu/polyu/comp/vlabcontroller/util/ChannelActiveListener.java
index ca3a3f57..898b8131 100644
--- a/src/main/java/hk/edu/polyu/comp/vlabcontroller/util/ChannelActiveListener.java
+++ b/src/main/java/hk/edu/polyu/comp/vlabcontroller/util/ChannelActiveListener.java
@@ -1,30 +1,28 @@
package hk.edu.polyu.comp.vlabcontroller.util;
+import java.time.Duration;
+
/**
* A listener that keeps track of whether a channel is active.
*/
public class ChannelActiveListener implements Runnable {
- private long lastWrite = 0;
+ private Duration lastWrite = Duration.ZERO;
@Override
public void run() {
- lastWrite = System.currentTimeMillis();
+ lastWrite = Duration.ofMillis(System.currentTimeMillis());
}
/**
* Checks whether the channel was active in the provided period.
*/
- public boolean isActive(long period) {
- long diff = System.currentTimeMillis() - lastWrite;
+ public boolean isActive(Duration period) {
+ var diff = Duration.ofMillis(System.currentTimeMillis()).minus(lastWrite);
// make sure the period is at least 5 seconds
// this ensures that when the socket is active, the ping is delayed for at least 5 seconds
- if (period < 5000) {
- period = 5000;
- }
-
- return diff <= period;
+ return diff.compareTo(DurationUtil.atLeast(Duration.ofSeconds(5)).apply(period)) <= 0;
}
}
diff --git a/src/main/java/hk/edu/polyu/comp/vlabcontroller/util/ConfigFileHelper.java b/src/main/java/hk/edu/polyu/comp/vlabcontroller/util/ConfigFileHelper.java
index ca122add..bb384230 100644
--- a/src/main/java/hk/edu/polyu/comp/vlabcontroller/util/ConfigFileHelper.java
+++ b/src/main/java/hk/edu/polyu/comp/vlabcontroller/util/ConfigFileHelper.java
@@ -4,56 +4,47 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
-import com.google.common.base.Charsets;
import hk.edu.polyu.comp.vlabcontroller.VLabControllerApplication;
+import io.vavr.CheckedFunction1;
+import lombok.RequiredArgsConstructor;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;
import java.io.File;
-import java.io.IOException;
import java.math.BigInteger;
+import java.nio.charset.StandardCharsets;
import java.nio.file.Paths;
import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
+import java.util.Optional;
@Component
+@RequiredArgsConstructor
public class ConfigFileHelper {
private final Environment environment;
- public ConfigFileHelper(Environment environment) {
- this.environment = environment;
- }
-
private File getConfigFile() {
- String path = environment.getProperty("spring.config.location");
- path = path == null ? VLabControllerApplication.CONFIG_FILENAME : path;
- File file = Paths.get(path).toFile();
- if (file.exists()) {
- return file;
- }
- return null;
+ return Optional.ofNullable(environment.getProperty("spring.config.location"))
+ .or(() -> Optional.of(VLabControllerApplication.CONFIG_FILENAME))
+ .map(path -> Paths.get(path).toFile())
+ .filter(File::exists)
+ .orElse(null);
}
- public String getConfigHash() throws NoSuchAlgorithmException {
- ObjectMapper objectMapper = new ObjectMapper(new YAMLFactory());
- objectMapper.configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true);
- objectMapper.configure(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY, true);
- File file = getConfigFile();
- String configHash;
- if (file == null) {
- configHash = "unknown";
- return configHash;
- }
- try {
- Object parsedConfig = objectMapper.readValue(file, Object.class);
- String canonicalConfigFile = objectMapper.writeValueAsString(parsedConfig);
- MessageDigest digest = MessageDigest.getInstance("SHA-1");
- digest.reset();
- digest.update(canonicalConfigFile.getBytes(Charsets.UTF_8));
- configHash = String.format("%040x", new BigInteger(1, digest.digest()));
- return configHash;
- } catch (IOException e) {
- return "illegal";
- }
+ public String getConfigHash() {
+ var objectMapper = new ObjectMapper(new YAMLFactory()) {{
+ configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true);
+ configure(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY, true);
+ }};
+ return Optional.ofNullable(getConfigFile())
+ .map(CheckedFunction1.lift(file -> {
+ var parsedConfig = objectMapper.readValue(file, Object.class);
+ var canonicalConfigFile = objectMapper.writeValueAsString(parsedConfig);
+ var digest = MessageDigest.getInstance("SHA-1");
+ digest.reset();
+ digest.update(canonicalConfigFile.getBytes(StandardCharsets.UTF_8));
+ return String.format("%040x", new BigInteger(1, digest.digest()));
+ }))
+ .map(x -> x.getOrElse("illegal"))
+ .orElse("unknown");
}
}
diff --git a/src/main/java/hk/edu/polyu/comp/vlabcontroller/util/ConfigUpdateListener.java b/src/main/java/hk/edu/polyu/comp/vlabcontroller/util/ConfigUpdateListener.java
index 2cafb623..d3fbcb19 100644
--- a/src/main/java/hk/edu/polyu/comp/vlabcontroller/util/ConfigUpdateListener.java
+++ b/src/main/java/hk/edu/polyu/comp/vlabcontroller/util/ConfigUpdateListener.java
@@ -1,8 +1,8 @@
package hk.edu.polyu.comp.vlabcontroller.util;
import hk.edu.polyu.comp.vlabcontroller.event.ConfigUpdateEvent;
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.Logger;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.context.environment.EnvironmentChangeEvent;
import org.springframework.cloud.context.refresh.ContextRefresher;
import org.springframework.context.annotation.Configuration;
@@ -10,28 +10,23 @@
import java.security.NoSuchAlgorithmException;
+@Slf4j
@Configuration
+@RequiredArgsConstructor
public class ConfigUpdateListener {
- protected final Logger log = LogManager.getLogger(getClass());
-
private final ConfigFileHelper configFileHelper;
private final ContextRefresher contextRefresher;
- public ConfigUpdateListener(ConfigFileHelper configFileHelper, ContextRefresher contextRefresher) {
- this.configFileHelper = configFileHelper;
- this.contextRefresher = contextRefresher;
- }
-
@EventListener
public void onUpdate(ConfigUpdateEvent event) throws NoSuchAlgorithmException {
- String hash = configFileHelper.getConfigHash();
+ var hash = configFileHelper.getConfigHash();
if (hash.equals("unknown")) {
log.info("No active application.yml set");
} else if (hash.equals("illegal")) {
log.error("application.yml syntax error");
} else {
log.info("Config changed, new hash = " + hash);
- new Thread(() -> contextRefresher.refresh()).start();
+ new Thread(contextRefresher::refresh).start();
}
}
diff --git a/src/main/java/hk/edu/polyu/comp/vlabcontroller/util/DelegatingStreamSinkConduit.java b/src/main/java/hk/edu/polyu/comp/vlabcontroller/util/DelegatingStreamSinkConduit.java
index 02a264aa..be139851 100644
--- a/src/main/java/hk/edu/polyu/comp/vlabcontroller/util/DelegatingStreamSinkConduit.java
+++ b/src/main/java/hk/edu/polyu/comp/vlabcontroller/util/DelegatingStreamSinkConduit.java
@@ -1,102 +1,23 @@
package hk.edu.polyu.comp.vlabcontroller.util;
-import org.xnio.XnioIoThread;
-import org.xnio.XnioWorker;
-import org.xnio.channels.StreamSourceChannel;
+import lombok.RequiredArgsConstructor;
+import lombok.experimental.Delegate;
import org.xnio.conduits.StreamSinkConduit;
-import org.xnio.conduits.WriteReadyHandler;
import java.io.IOException;
import java.nio.ByteBuffer;
-import java.nio.channels.FileChannel;
-import java.util.concurrent.TimeUnit;
+@RequiredArgsConstructor
public class DelegatingStreamSinkConduit implements StreamSinkConduit {
+ @SuppressWarnings("unused")
+ private interface Write {
+ int write(ByteBuffer src) throws IOException;
+ }
+ @Delegate(excludes=Write.class)
private final StreamSinkConduit delegate;
private final Runnable writeListener;
-
- public DelegatingStreamSinkConduit(StreamSinkConduit delegate, Runnable writeListener) {
- this.delegate = delegate;
- this.writeListener = writeListener;
- }
-
- @Override
- public void terminateWrites() throws IOException {
- delegate.terminateWrites();
- }
-
- @Override
- public boolean isWriteShutdown() {
- return delegate.isWriteShutdown();
- }
-
- @Override
- public void resumeWrites() {
- delegate.resumeWrites();
- }
-
- @Override
- public void suspendWrites() {
- delegate.suspendWrites();
- }
-
- @Override
- public void wakeupWrites() {
- delegate.wakeupWrites();
- }
-
- @Override
- public boolean isWriteResumed() {
- return delegate.isWriteResumed();
- }
-
- @Override
- public void awaitWritable() throws IOException {
- delegate.awaitWritable();
- }
-
- @Override
- public void awaitWritable(long time, TimeUnit timeUnit) throws IOException {
- delegate.awaitWritable(time, timeUnit);
- }
-
- @Override
- public XnioIoThread getWriteThread() {
- return delegate.getWriteThread();
- }
-
- @Override
- public void setWriteReadyHandler(WriteReadyHandler handler) {
- delegate.setWriteReadyHandler(handler);
- }
-
- @Override
- public void truncateWrites() throws IOException {
- delegate.truncateWrites();
- }
-
- @Override
- public boolean flush() throws IOException {
- return delegate.flush();
- }
-
- @Override
- public XnioWorker getWorker() {
- return delegate.getWorker();
- }
-
- @Override
- public long transferFrom(FileChannel src, long position, long count) throws IOException {
- return delegate.transferFrom(src, position, count);
- }
-
- @Override
- public long transferFrom(StreamSourceChannel source, long count, ByteBuffer throughBuffer) throws IOException {
- return delegate.transferFrom(source, count, throughBuffer);
- }
-
@Override
public int write(ByteBuffer src) throws IOException {
if (writeListener != null) {
@@ -108,20 +29,4 @@ public int write(ByteBuffer src) throws IOException {
public int writeWithoutNotifying(ByteBuffer src) throws IOException {
return delegate.write(src);
}
-
- @Override
- public long write(ByteBuffer[] srcs, int offs, int len) throws IOException {
- return delegate.write(srcs, offs, len);
- }
-
- @Override
- public int writeFinal(ByteBuffer src) throws IOException {
- return delegate.writeFinal(src);
- }
-
- @Override
- public long writeFinal(ByteBuffer[] srcs, int offset, int length) throws IOException {
- return delegate.writeFinal(srcs, offset, length);
- }
-
}
diff --git a/src/main/java/hk/edu/polyu/comp/vlabcontroller/util/DelegatingStreamSourceConduit.java b/src/main/java/hk/edu/polyu/comp/vlabcontroller/util/DelegatingStreamSourceConduit.java
index 5af5e989..3f11317a 100644
--- a/src/main/java/hk/edu/polyu/comp/vlabcontroller/util/DelegatingStreamSourceConduit.java
+++ b/src/main/java/hk/edu/polyu/comp/vlabcontroller/util/DelegatingStreamSourceConduit.java
@@ -1,111 +1,34 @@
package hk.edu.polyu.comp.vlabcontroller.util;
-import org.xnio.XnioIoThread;
-import org.xnio.XnioWorker;
-import org.xnio.channels.StreamSinkChannel;
-import org.xnio.conduits.ReadReadyHandler;
+import lombok.RequiredArgsConstructor;
+import lombok.experimental.Delegate;
import org.xnio.conduits.StreamSourceConduit;
import java.io.IOException;
import java.nio.ByteBuffer;
-import java.nio.channels.FileChannel;
-import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
+@RequiredArgsConstructor
public class DelegatingStreamSourceConduit implements StreamSourceConduit {
+ @SuppressWarnings("unused")
+ private interface Read {
+ int read(ByteBuffer dst) throws IOException;
+ }
+ @Delegate(excludes=Read.class)
private final StreamSourceConduit delegate;
private final Consumer readListener;
- public DelegatingStreamSourceConduit(StreamSourceConduit delegate, Consumer readListener) {
- this.delegate = delegate;
- this.readListener = readListener;
- }
-
- @Override
- public void terminateReads() throws IOException {
- delegate.terminateReads();
- }
-
- @Override
- public boolean isReadShutdown() {
- return delegate.isReadShutdown();
- }
-
- @Override
- public void resumeReads() {
- delegate.resumeReads();
- }
-
- @Override
- public void suspendReads() {
- delegate.suspendReads();
- }
-
- @Override
- public void wakeupReads() {
- delegate.wakeupReads();
- }
-
- @Override
- public boolean isReadResumed() {
- return delegate.isReadResumed();
- }
-
- @Override
- public void awaitReadable() throws IOException {
- delegate.awaitReadable();
- }
-
- @Override
- public void awaitReadable(long time, TimeUnit timeUnit) throws IOException {
- delegate.awaitReadable(time, timeUnit);
- }
-
- @Override
- public XnioIoThread getReadThread() {
- return delegate.getReadThread();
- }
-
- @Override
- public void setReadReadyHandler(ReadReadyHandler handler) {
- delegate.setReadReadyHandler(handler);
- }
-
- @Override
- public XnioWorker getWorker() {
- return delegate.getWorker();
- }
-
- @Override
- public long transferTo(long position, long count, FileChannel target) throws IOException {
- return delegate.transferTo(position, count, target);
- }
-
- @Override
- public long transferTo(long count, ByteBuffer throughBuffer, StreamSinkChannel target) throws IOException {
- return delegate.transferTo(count, throughBuffer, target);
- }
-
@Override
public int read(ByteBuffer dst) throws IOException {
- if (readListener == null) {
- return delegate.read(dst);
- } else {
- int read = delegate.read(dst);
- ByteBuffer copy = dst.duplicate();
+ var read = delegate.read(dst);
+ if (readListener != null) {
+ var copy = dst.duplicate();
copy.flip();
- byte[] data = new byte[copy.remaining()];
+ var data = new byte[copy.remaining()];
copy.get(data);
readListener.accept(data);
- return read;
}
+ return read;
}
-
- @Override
- public long read(ByteBuffer[] dsts, int offs, int len) throws IOException {
- return delegate.read(dsts, offs, len);
- }
-
-
}
diff --git a/src/main/java/hk/edu/polyu/comp/vlabcontroller/util/DurationUtil.java b/src/main/java/hk/edu/polyu/comp/vlabcontroller/util/DurationUtil.java
new file mode 100644
index 00000000..35a9b7fd
--- /dev/null
+++ b/src/main/java/hk/edu/polyu/comp/vlabcontroller/util/DurationUtil.java
@@ -0,0 +1,20 @@
+package hk.edu.polyu.comp.vlabcontroller.util;
+
+import io.vavr.Function1;
+
+import java.time.Duration;
+
+public class DurationUtil {
+ public static Duration max(Duration a, Duration b) {
+ return a.compareTo(b) > 0 ? a : b;
+ }
+ public static Duration min(Duration a, Duration b) {
+ return a.compareTo(b) < 0 ? a : b;
+ }
+ public static Function1 atLeast(Duration least) {
+ return x -> min(x, least).equals(x) ? least : x;
+ }
+ public static Function1 atMost(Duration most) {
+ return x -> max(x, most).equals(x) ? most : x;
+ }
+}
diff --git a/src/main/java/hk/edu/polyu/comp/vlabcontroller/util/PortAllocator.java b/src/main/java/hk/edu/polyu/comp/vlabcontroller/util/PortAllocator.java
index d04f8900..8e10fac7 100644
--- a/src/main/java/hk/edu/polyu/comp/vlabcontroller/util/PortAllocator.java
+++ b/src/main/java/hk/edu/polyu/comp/vlabcontroller/util/PortAllocator.java
@@ -1,9 +1,9 @@
package hk.edu.polyu.comp.vlabcontroller.util;
import hk.edu.polyu.comp.vlabcontroller.VLabControllerException;
+import lombok.Synchronized;
import java.util.*;
-import java.util.stream.Collectors;
public class PortAllocator {
@@ -18,12 +18,12 @@ public PortAllocator(int from, int to) {
}
public int allocate(String ownerId) {
- int nextPort = range[0];
+ var nextPort = range[0];
while (occupiedPorts.contains(nextPort)) nextPort++;
if (range[1] > 0 && nextPort > range[1]) {
throw new VLabControllerException("Cannot create container: all allocated ports are currently in use."
- + " Please try again later or contact an administrator.");
+ + " Please try again later or contact an administrator.");
}
occupiedPorts.add(nextPort);
@@ -36,16 +36,10 @@ public void release(int port) {
occupiedPortOwners.remove(port);
}
+ @Synchronized("occupiedPortOwners")
public void release(String ownerId) {
- synchronized (occupiedPortOwners) {
- Set portsToRelease = occupiedPortOwners.entrySet().stream()
- .filter(e -> e.getValue().equals(ownerId))
- .map(e -> e.getKey())
- .collect(Collectors.toSet());
- for (Integer port : portsToRelease) {
- occupiedPorts.remove(port);
- occupiedPortOwners.remove(port);
- }
- }
+ occupiedPortOwners.entrySet().stream()
+ .filter(e -> e.getValue().equals(ownerId))
+ .map(Map.Entry::getKey).distinct().forEach(this::release);
}
}
diff --git a/src/main/java/hk/edu/polyu/comp/vlabcontroller/util/ProxyMappingManager.java b/src/main/java/hk/edu/polyu/comp/vlabcontroller/util/ProxyMappingManager.java
index fb64a9bb..fe199e3c 100644
--- a/src/main/java/hk/edu/polyu/comp/vlabcontroller/util/ProxyMappingManager.java
+++ b/src/main/java/hk/edu/polyu/comp/vlabcontroller/util/ProxyMappingManager.java
@@ -15,43 +15,42 @@
import io.undertow.servlet.handlers.ServletRequestContext;
import io.undertow.util.AttachmentKey;
import io.undertow.util.PathMatcher;
-import lombok.extern.log4j.Log4j2;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
-import java.lang.reflect.Field;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.ByteBuffer;
+import java.time.Duration;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
-import java.util.Map.Entry;
+import java.util.Optional;
+import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
/**
* This component keeps track of which proxy mappings (i.e. URL endpoints) are currently registered,
* and tells Undertow where they should proxy to.
*/
-@Log4j2
+@Slf4j
@Component
+@RequiredArgsConstructor
public class ProxyMappingManager {
-
private static final String PROXY_INTERNAL_ENDPOINT = "/proxy_endpoint";
private static final String PROXY_PORT_MAPPINGS_ENDPOINT = "/port_mappings";
private static final AttachmentKey ATTACHMENT_KEY_DISPATCHER = AttachmentKey.create(ProxyMappingManager.class);
private final Map proxyMappings = new HashMap<>(); // proxyId -> metadata
private PathHandler pathHandler;
private final HeartbeatService heartbeatService;
-
- public ProxyMappingManager(HeartbeatService heartbeatService) {
- this.heartbeatService = heartbeatService;
- }
+ private final Retrying retrying;
public synchronized HttpHandler createHttpHandler(HttpHandler defaultHandler) {
if (pathHandler == null) {
@@ -67,27 +66,32 @@ public synchronized void addMapping(String proxyId, String mapping, URI target)
if (proxyMappings.containsKey(proxyId)) {
if (proxyMappings.get(proxyId).containsExactMappingPath(mapping)) return;
}
- ProxyMappingMetadata proxyMappingMetadata = proxyMappings.computeIfAbsent(proxyId, value -> new ProxyMappingMetadata());
+ var proxyMappingMetadata = proxyMappings.computeIfAbsent(proxyId, __ -> ProxyMappingMetadata.builder().build());
+ proxyMappingMetadata.setDefaultTarget(target);
- LoadBalancingProxyClient proxyClient = new LoadBalancingProxyClient() {
+ var proxyClient = new LoadBalancingProxyClient() {
@Override
public void getConnection(ProxyTarget target, HttpServerExchange exchange, ProxyCallback callback, long timeout, TimeUnit timeUnit) {
try {
exchange.addResponseCommitListener(ex -> heartbeatService.attachHeartbeatChecker(ex, proxyId));
} catch (Exception e) {
- log.error(e);
+ log.error("an error occured: {}", e);
}
super.getConnection(target, exchange, callback, timeout, timeUnit);
}
};
proxyClient.setMaxQueueSize(100);
proxyClient.addHost(target);
-
- String path = PROXY_INTERNAL_ENDPOINT + "/" + mapping;
+ proxyMappingMetadata.getPortMappingMetadataList().add(
+ PortMappingMetadata.builder()
+ .portMapping(mapping)
+ .target(target)
+ .loadBalancingProxyClient(proxyClient)
+ .build()
+ );
+
+ var path = PROXY_INTERNAL_ENDPOINT + "/" + mapping;
pathHandler.addPrefixPath(path, new ProxyHandler(proxyClient, ResponseCodeHandler.HANDLE_404));
-
- proxyMappingMetadata.setDefaultTarget(target);
- proxyMappingMetadata.getPortMappingMetadataList().add(new PortMappingMetadata(mapping, target, proxyClient));
log.debug("mapping {} was added, current mappings: {}", mapping, proxyMappings);
}
@@ -95,10 +99,11 @@ public synchronized void removeProxyMapping(String proxyId) {
if (pathHandler == null)
throw new IllegalStateException("Cannot change mappings: web server is not yet running.");
if (proxyMappings.containsKey(proxyId)) {
- ProxyMappingMetadata metadata = proxyMappings.get(proxyId);
+ var metadata = proxyMappings.get(proxyId);
metadata.getPortMappingMetadataList().forEach(e -> {
- e.getLoadBalancingProxyClient().closeCurrentConnections();
- e.getLoadBalancingProxyClient().removeHost(e.getTarget());
+ var loadBalancingProxyClient = e.getLoadBalancingProxyClient();
+ loadBalancingProxyClient.closeCurrentConnections();
+ loadBalancingProxyClient.removeHost(e.getTarget());
pathHandler.removePrefixPath(PROXY_INTERNAL_ENDPOINT + "/" + e.getPortMapping());
});
proxyMappings.remove(proxyId);
@@ -107,11 +112,9 @@ public synchronized void removeProxyMapping(String proxyId) {
}
public String getProxyId(String mapping) {
- for (Entry e : proxyMappings.entrySet()) {
- ProxyMappingMetadata metadata = e.getValue();
- if (metadata.containsMappingPathPrefix(mapping)) return e.getKey();
- }
- return null;
+ return proxyMappings.entrySet().stream()
+ .filter(e -> e.getValue().containsMappingPathPrefix(mapping))
+ .map(Map.Entry::getKey).findFirst().orElse(null);
}
public String getProxyPortMappingsEndpoint() {
@@ -134,12 +137,12 @@ public String getProxyPortMappingsEndpoint() {
* @throws ServletException If the dispatch fails for any other reason.
*/
public void dispatchAsync(String mapping, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
- HttpServerExchange exchange = ServletRequestContext.current().getExchange();
+ var exchange = ServletRequestContext.current().getExchange();
exchange.putAttachment(ATTACHMENT_KEY_DISPATCHER, this);
- String queryString = request.getQueryString();
+ var queryString = request.getQueryString();
queryString = (queryString == null) ? "" : "?" + queryString;
- String targetPath = PROXY_INTERNAL_ENDPOINT + "/" + mapping + queryString;
+ var targetPath = PROXY_INTERNAL_ENDPOINT + "/" + mapping + queryString;
request.startAsync();
request.getRequestDispatcher(targetPath).forward(request, response);
@@ -163,58 +166,56 @@ public void dispatchAsync(String mapping, HttpServletRequest request, HttpServle
* @throws ServletException If the dispatch fails for any other reason.
* @throws URISyntaxException If URI syntax is not allowed.
*/
- public void dispatchAsync(Proxy proxy, String mapping, int port, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException, URISyntaxException {
- HttpServerExchange exchange = ServletRequestContext.current().getExchange();
+ public void dispatchAsync(Proxy proxy, String mapping, int port, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException, URISyntaxException, ExecutionException, InterruptedException {
+ var exchange = ServletRequestContext.current().getExchange();
exchange.putAttachment(ATTACHMENT_KEY_DISPATCHER, this);
- String proxyId = proxy.getId();
- URI defaultTarget = proxyMappings.get(proxyId).getDefaultTarget();
- String port_mapping = proxyId + PROXY_PORT_MAPPINGS_ENDPOINT + "/" + port;
- URI newTarget = new URI(defaultTarget.getScheme() + "://" + defaultTarget.getHost() + ":" + port);
- int[] failedResponseCode = new int[1];
- boolean targetConnected = Retrying.retry(i -> {
+ var proxyId = proxy.getId();
+ var defaultTarget = proxyMappings.get(proxyId).getDefaultTarget();
+ var port_mapping = proxyId + PROXY_PORT_MAPPINGS_ENDPOINT + "/" + port;
+ var newTarget = new URI(defaultTarget.getScheme() + "://" + defaultTarget.getHost() + ":" + port);
+ var failedResponseCode = new int[1];
+ var query = Optional.ofNullable(request.getQueryString()).map(x -> "?" + x).orElse("");
+ var targetConnected = retrying.retry(i -> {
try {
- String query = request.getQueryString() == null ? "" : "?" + request.getQueryString();
log.debug("request protocol: {}, scheme: {}, headers: {}", request.getProtocol(), request.getScheme(), Collections.list(request.getHeaderNames()));
// Handle websocket case
if (request.getHeaders("Upgrade").hasMoreElements()) {
return true;
}
- URL testURL = new URL(newTarget + mapping + query);
+ var testURL = new URL(newTarget + mapping + query);
log.debug("Testing url of {}", testURL);
- HttpURLConnection connection = (HttpURLConnection) testURL.openConnection();
+ var connection = (HttpURLConnection) testURL.openConnection();
connection.setConnectTimeout(5000);
connection.setInstanceFollowRedirects(false);
- int responseCode = connection.getResponseCode();
+ var responseCode = connection.getResponseCode();
log.debug("received connection from {}, status code: {}", testURL, responseCode);
if (responseCode < 500) {
log.debug("successfully connected to target {}", testURL);
- }else{
+ } else {
failedResponseCode[0] = responseCode;
}
return true;
- }catch (IOException ioe) {
+ } catch (IOException ioe) {
failedResponseCode[0] = 404;
log.debug("Trying to connect target URL ({}/{})", i, 5);
} catch (Exception e) {
failedResponseCode[0] = 500;
- log.debug(e);
+ log.debug("an error occured: {}", e);
log.debug("Trying to connect target URL ({}/{})", i, 5);
}
return false;
- }, 5, 2000, true);
+ }, 5, Duration.ofSeconds(2), true);
- if (!targetConnected) {
+ if (!targetConnected.get()) {
response.sendError(failedResponseCode[0]);
return;
}
addMapping(proxyId, port_mapping, newTarget);
proxy.getTargets().put(port_mapping, newTarget);
- String queryString = request.getQueryString();
- queryString = (queryString == null) ? "" : "?" + queryString;
- String targetPath = PROXY_INTERNAL_ENDPOINT + "/" + port_mapping + mapping + queryString;
+ var targetPath = PROXY_INTERNAL_ENDPOINT + "/" + port_mapping + mapping + query;
request.startAsync();
request.getRequestDispatcher(targetPath).forward(request, response);
}
@@ -228,10 +229,10 @@ public ProxyPathHandler(HttpHandler defaultHandler) {
@SuppressWarnings("unchecked")
@Override
public void handleRequest(HttpServerExchange exchange) throws Exception {
- Field field = PathHandler.class.getDeclaredField("pathMatcher");
+ var field = PathHandler.class.getDeclaredField("pathMatcher");
field.setAccessible(true);
- PathMatcher pathMatcher = (PathMatcher) field.get(this);
- PathMatcher.PathMatch match = pathMatcher.match(exchange.getRelativePath());
+ var pathMatcher = (PathMatcher) field.get(this);
+ var match = pathMatcher.match(exchange.getRelativePath());
// Note: this handler may never be accessed directly (because it bypasses Spring security).
// Only allowed if the request was dispatched via this class.
diff --git a/src/main/java/hk/edu/polyu/comp/vlabcontroller/util/RFC6335Validator.java b/src/main/java/hk/edu/polyu/comp/vlabcontroller/util/RFC6335Validator.java
new file mode 100644
index 00000000..4ce7e027
--- /dev/null
+++ b/src/main/java/hk/edu/polyu/comp/vlabcontroller/util/RFC6335Validator.java
@@ -0,0 +1,7 @@
+package hk.edu.polyu.comp.vlabcontroller.util;
+
+public class RFC6335Validator {
+ public static boolean valid(String input) {
+ return input.matches("^(?!.*--.*)[^\\W_]([^\\W_]|-)*(? sessionRepository;
- public RedisSessionHelper(FindByIndexNameSessionRepository extends Session> sessionRepository) {
- this.sessionRepository = sessionRepository;
- }
-
- public Map getSessionByUsername(String username) {
+ public Map getSessionByUsername(String username) {
return sessionRepository.findByIndexNameAndIndexValue(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, username);
}
}
diff --git a/src/main/java/hk/edu/polyu/comp/vlabcontroller/util/Retrying.java b/src/main/java/hk/edu/polyu/comp/vlabcontroller/util/Retrying.java
index 1832342c..4539c296 100644
--- a/src/main/java/hk/edu/polyu/comp/vlabcontroller/util/Retrying.java
+++ b/src/main/java/hk/edu/polyu/comp/vlabcontroller/util/Retrying.java
@@ -1,34 +1,35 @@
package hk.edu.polyu.comp.vlabcontroller.util;
+import io.vavr.control.Try;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Component;
+
+import java.time.Duration;
+import java.util.concurrent.CompletableFuture;
import java.util.function.IntPredicate;
+import java.util.stream.IntStream;
+@Component
public class Retrying {
-
- public static boolean retry(IntPredicate job, int tries, int waitTime) {
+ @Async
+ public CompletableFuture retry(IntPredicate job, int tries, Duration waitTime) {
return retry(job, tries, waitTime, false);
}
- public static boolean retry(IntPredicate job, int tries, int waitTime, boolean retryOnException) {
- boolean retVal = false;
- RuntimeException exception = null;
- for (int currentTry = 1; currentTry <= tries; currentTry++) {
+ @Async
+ public CompletableFuture retry(IntPredicate job, int tries, Duration waitTime, boolean retryOnException) {
+ var result = Try.success(false);
+ for (var currentTry : (Iterable) () -> IntStream.rangeClosed(1, tries).iterator()) {
+ result = Try.of(() -> job.test(currentTry))
+ .recoverWith(e -> retryOnException ? Try.success(false) : Try.failure(e));
+ if (result.isFailure()) return CompletableFuture.failedFuture(result.getCause());
+ if (result.get()) return CompletableFuture.completedFuture(true);
try {
- if (job.test(currentTry)) {
- retVal = true;
- exception = null;
- break;
- }
- } catch (RuntimeException e) {
- if (retryOnException) exception = e;
- else throw e;
- }
- try {
- Thread.sleep(waitTime);
- } catch (InterruptedException ignore) {
+ Thread.sleep(waitTime.toMillis());
+ } catch (InterruptedException ignored) {
}
}
- if (exception == null) return retVal;
- else throw exception;
-
+ if (result.isFailure()) return CompletableFuture.failedFuture(result.getCause());
+ return CompletableFuture.completedFuture(result.get());
}
}
diff --git a/src/main/java/hk/edu/polyu/comp/vlabcontroller/util/SessionHelper.java b/src/main/java/hk/edu/polyu/comp/vlabcontroller/util/SessionHelper.java
index 9b490f63..c9950f44 100644
--- a/src/main/java/hk/edu/polyu/comp/vlabcontroller/util/SessionHelper.java
+++ b/src/main/java/hk/edu/polyu/comp/vlabcontroller/util/SessionHelper.java
@@ -1,12 +1,11 @@
package hk.edu.polyu.comp.vlabcontroller.util;
+import hk.edu.polyu.comp.vlabcontroller.config.ServerProperties;
import io.undertow.server.HttpServerExchange;
-import io.undertow.server.handlers.Cookie;
import io.undertow.servlet.handlers.ServletRequestContext;
-import io.undertow.util.HeaderValues;
-import org.springframework.core.env.Environment;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.core.AuthenticatedPrincipal;
+import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import javax.servlet.http.HttpSession;
@@ -21,13 +20,13 @@ public class SessionHelper {
* @return The current session ID, or null if no session is active.
*/
public static String getCurrentSessionId(boolean createIfMissing) {
- ServletRequestContext context = ServletRequestContext.current();
+ var context = ServletRequestContext.current();
if (context == null) return null;
HttpSession session = context.getSession();
if (session != null) return session.getId();
- Cookie jSessionIdCookie = context.getExchange().getRequestCookies().get("JSESSIONID");
+ var jSessionIdCookie = context.getExchange().getRequestCookie("JSESSIONID");
if (jSessionIdCookie != null) return jSessionIdCookie.getValue();
if (createIfMissing) return context.getCurrentServletContext().getSession(context.getExchange(), true).getId();
@@ -37,13 +36,13 @@ public static String getCurrentSessionId(boolean createIfMissing) {
/**
* Get the context path that has been configured for this instance.
*
- * @param environment The Spring environment containing the context-path setting.
+ * @param serverProperties The Spring configuration properties that resolves context-path
* @param endWithSlash True to always end the context path with a slash.
* @return The instance's context path, may be empty, never null.
*/
- public static String getContextPath(Environment environment, boolean endWithSlash) {
- String contextPath = environment.getProperty("server.servlet.context-path");
- if (contextPath == null || contextPath.trim().equals("/") || contextPath.trim().isEmpty())
+ public static String getContextPath(ServerProperties serverProperties, boolean endWithSlash) {
+ var contextPath = serverProperties.getServletContextPath();
+ if (contextPath == null || contextPath.isBlank() || contextPath.trim().equals("/"))
return endWithSlash ? "/" : "";
if (!contextPath.startsWith("/")) contextPath = "/" + contextPath;
@@ -65,23 +64,23 @@ public static String getContextPath(Environment environment, boolean endWithSlas
* @return An object containing information about the current user.
*/
public static SessionOwnerInfo createOwnerInfo(HttpServerExchange exchange) {
- SessionOwnerInfo info = new SessionOwnerInfo();
+ var info = new SessionOwnerInfo();
// Ideally, use the HTTP session information.
info.principal = Optional.ofNullable(ServletRequestContext.current())
- .map(ctx -> ctx.getSession())
+ .map(ServletRequestContext::getSession)
.map(session -> (SecurityContext) session.getAttribute("SPRING_SECURITY_CONTEXT"))
- .map(ctx -> ctx.getAuthentication())
+ .map(SecurityContext::getAuthentication)
.filter(auth -> !(auth instanceof AnonymousAuthenticationToken))
- .map(auth -> auth.getPrincipal())
+ .map(Authentication::getPrincipal)
.orElse(null);
// Fallback: use the Authorization header, if present.
- HeaderValues authHeader = exchange.getRequestHeaders().get("Authorization");
+ var authHeader = exchange.getRequestHeaders().get("Authorization");
if (authHeader != null) info.authHeader = authHeader.getFirst();
// Fallback: use the JSESSIONID cookie, if present.
- Cookie jSessionIdCookie = exchange.getRequestCookies().get("JSESSIONID");
+ var jSessionIdCookie = exchange.getRequestCookie("JSESSIONID");
if (jSessionIdCookie != null) info.jSessionId = jSessionIdCookie.getValue();
// Final fallback: generate a JSESSIONID for this exchange.
diff --git a/src/main/java/hk/edu/polyu/comp/vlabcontroller/util/StartupEventListener.java b/src/main/java/hk/edu/polyu/comp/vlabcontroller/util/StartupEventListener.java
index 078aafe0..b65791c5 100644
--- a/src/main/java/hk/edu/polyu/comp/vlabcontroller/util/StartupEventListener.java
+++ b/src/main/java/hk/edu/polyu/comp/vlabcontroller/util/StartupEventListener.java
@@ -1,16 +1,14 @@
package hk.edu.polyu.comp.vlabcontroller.util;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
+import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.boot.info.BuildProperties;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
+@Slf4j
@Component
public class StartupEventListener {
- private static final Logger LOGGER = LoggerFactory.getLogger(StartupEventListener.class);
-
private final BuildProperties buildProperties;
public StartupEventListener(BuildProperties buildProperties) {
@@ -19,9 +17,6 @@ public StartupEventListener(BuildProperties buildProperties) {
@EventListener
public void onStartup(ApplicationReadyEvent event) {
- StringBuilder startupMsg = new StringBuilder("Started ");
- startupMsg.append(buildProperties.getName()).append(" ");
- startupMsg.append(buildProperties.getVersion());
- LOGGER.info(startupMsg.toString());
+ log.info(String.format("Started %s %s", buildProperties.getName(), buildProperties.getVersion()));
}
}
diff --git a/src/test/java/hk/edu/polyu/comp/vlabcontroller/model/runtime/MongoTest.java b/src/test/java/hk/edu/polyu/comp/vlabcontroller/model/runtime/MongoTest.java
new file mode 100644
index 00000000..9d2d7b94
--- /dev/null
+++ b/src/test/java/hk/edu/polyu/comp/vlabcontroller/model/runtime/MongoTest.java
@@ -0,0 +1,22 @@
+package hk.edu.polyu.comp.vlabcontroller.model.runtime;
+
+import hk.edu.polyu.comp.vlabcontroller.entity.QUser;
+import hk.edu.polyu.comp.vlabcontroller.entity.User;
+import hk.edu.polyu.comp.vlabcontroller.repository.UserRepository;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+
+import static org.springframework.test.util.AssertionErrors.assertNotNull;
+
+@SpringBootTest
+public class MongoTest {
+ @Autowired
+ UserRepository repository;
+
+ @Test
+ public void testUserRepo() {
+ this.repository.insert(User.builder().id("test").build());
+ assertNotNull("entity is null", this.repository.findOne(QUser.user.id.eq("test")));
+ }
+}
diff --git a/src/test/java/hk/edu/polyu/comp/vlabcontroller/model/runtime/ProxyMappingMetadataTest.java b/src/test/java/hk/edu/polyu/comp/vlabcontroller/model/runtime/ProxyMappingMetadataTest.java
index 96375881..e4c6e527 100644
--- a/src/test/java/hk/edu/polyu/comp/vlabcontroller/model/runtime/ProxyMappingMetadataTest.java
+++ b/src/test/java/hk/edu/polyu/comp/vlabcontroller/model/runtime/ProxyMappingMetadataTest.java
@@ -1,56 +1,57 @@
package hk.edu.polyu.comp.vlabcontroller.model.runtime;
-import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
import java.net.URI;
import java.net.URISyntaxException;
+import java.util.List;
-class ProxyMappingMetadataTest {
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+@SpringBootTest
+public class ProxyMappingMetadataTest {
@Test
- void containsExactTargetPath() throws URISyntaxException {
- var metadata = new ProxyMappingMetadata();
- Assertions.assertFalse(metadata.containsExactMappingPath("test"));
- metadata.getPortMappingMetadataList().add(
- new PortMappingMetadata(
- "1ca3dde2-8fdf-4fe4-8327-6849e4d77fcd/port_mappings/8000",
- new URI("http://10.42.61.11:8000"),
- null
- ));
- metadata.getPortMappingMetadataList().add(
- new PortMappingMetadata(
- "1ca3dde2-8fdf-4fe4-8327-6849e4d77fcd/port_mappings/8080",
- new URI("http://10.42.61.11:8080"),
- null
- ));
- Assertions.assertFalse(metadata.containsExactMappingPath("test"));
- Assertions.assertTrue(metadata.containsExactMappingPath("1ca3dde2-8fdf-4fe4-8327-6849e4d77fcd/port_mappings/8000"));
- Assertions.assertTrue(metadata.containsExactMappingPath("1ca3dde2-8fdf-4fe4-8327-6849e4d77fcd/port_mappings/8080"));
+ public void testContainsExactTargetPath() throws URISyntaxException {
+ var metadata = ProxyMappingMetadata.builder()
+ .portMappingMetadataList(List.of(
+ PortMappingMetadata.builder()
+ .portMapping("1ca3dde2-8fdf-4fe4-8327-6849e4d77fcd/port_mappings/8000")
+ .target(new URI("http://10.42.61.11:8000"))
+ .build(),
+ PortMappingMetadata.builder()
+ .portMapping("1ca3dde2-8fdf-4fe4-8327-6849e4d77fcd/port_mappings/8080")
+ .target(new URI("http://10.42.61.11:8080"))
+ .build()
+ ))
+ .build();
+ assertFalse(metadata.containsExactMappingPath("test"));
+ assertTrue(metadata.containsExactMappingPath("1ca3dde2-8fdf-4fe4-8327-6849e4d77fcd/port_mappings/8000"));
+ assertTrue(metadata.containsExactMappingPath("1ca3dde2-8fdf-4fe4-8327-6849e4d77fcd/port_mappings/8080"));
}
@Test
- void containsMappingPathPrefix() throws URISyntaxException {
- var metadata = new ProxyMappingMetadata();
- Assertions.assertFalse(metadata.containsMappingPathPrefix("test"));
- metadata.getPortMappingMetadataList().add(
- new PortMappingMetadata(
- "1ca3dde2-8fdf-4fe4-8327-6849e4d77fcd/port_mappings/8000",
- new URI("http://10.42.61.11:8000"),
- null
- ));
- metadata.getPortMappingMetadataList().add(
- new PortMappingMetadata(
- "1ca3dde2-8fdf-4fe4-8327-6849e4d77fcd/port_mappings/8080",
- new URI("http://10.42.61.11:8080"),
- null
- ));
- Assertions.assertFalse(metadata.containsMappingPathPrefix("test"));
- Assertions.assertFalse(metadata.containsMappingPathPrefix("1ca3dde2-8fdf-4fe4-8327-6849e4d77fcd/port_mappings/8087"));
- Assertions.assertFalse(metadata.containsMappingPathPrefix("1ca3dde2-8fdf-4fe4-8327-6849e4d77fcd/port8087"));
- Assertions.assertTrue(metadata.containsMappingPathPrefix("1ca3dde2-8fdf-4fe4-8327-6849e4d77fcd"));
- Assertions.assertTrue(metadata.containsMappingPathPrefix("1ca3dde2-8fdf-4fe4-8327-6849e4d77fcd/port_mappings"));
- Assertions.assertTrue(metadata.containsMappingPathPrefix("1ca3dde2-8fdf-4fe4-8327-6849e4d77fcd/port_mappings/8080"));
- Assertions.assertTrue(metadata.containsMappingPathPrefix("1ca3dde2-8fdf-4fe4-8327-6849e4d77fcd/port_mappings/8080/"));
+ public void testContainsMappingPathPrefix() throws URISyntaxException {
+ var metadata = ProxyMappingMetadata.builder()
+ .portMappingMetadataList(List.of(
+ PortMappingMetadata.builder()
+ .portMapping("1ca3dde2-8fdf-4fe4-8327-6849e4d77fcd/port_mappings/8000")
+ .target(new URI("http://10.42.61.11:8000"))
+ .build(),
+ PortMappingMetadata.builder()
+ .portMapping("1ca3dde2-8fdf-4fe4-8327-6849e4d77fcd/port_mappings/8080")
+ .target(new URI("http://10.42.61.11:8080"))
+ .build()
+ ))
+ .build();
+ assertFalse(metadata.containsMappingPathPrefix("test"));
+ assertFalse(metadata.containsMappingPathPrefix("1ca3dde2-8fdf-4fe4-8327-6849e4d77fcd/port_mappings/8087"));
+ assertFalse(metadata.containsMappingPathPrefix("1ca3dde2-8fdf-4fe4-8327-6849e4d77fcd/port8087"));
+ assertTrue(metadata.containsMappingPathPrefix("1ca3dde2-8fdf-4fe4-8327-6849e4d77fcd"));
+ assertTrue(metadata.containsMappingPathPrefix("1ca3dde2-8fdf-4fe4-8327-6849e4d77fcd/port_mappings"));
+ assertTrue(metadata.containsMappingPathPrefix("1ca3dde2-8fdf-4fe4-8327-6849e4d77fcd/port_mappings/8080"));
+ assertTrue(metadata.containsMappingPathPrefix("1ca3dde2-8fdf-4fe4-8327-6849e4d77fcd/port_mappings/8080/"));
}
}
\ No newline at end of file