Skip to content

Commit

Permalink
Merge pull request #47 from code-freak/feature/reverse-proxy
Browse files Browse the repository at this point in the history
Add reverse proxy support via Traefik
  • Loading branch information
erikhofer committed Apr 28, 2019
2 parents ee11906 + 2f71601 commit a6cd9ee
Show file tree
Hide file tree
Showing 7 changed files with 65 additions and 48 deletions.
9 changes: 9 additions & 0 deletions Vagrantfile
Expand Up @@ -12,6 +12,7 @@ Vagrant.configure("2") do |config|
config.vm.network "private_network", ip: "10.12.12.100"

config.vm.network "forwarded_port", guest: 2375, host: 2375
config.vm.network "forwarded_port", guest: 80, host: 8081

# Enable the automatic install of docker and make it available via TCP
# We bind to 0.0.0.0 because the VM and Host are on a private network
Expand All @@ -22,6 +23,14 @@ Vagrant.configure("2") do |config|
# $ docker ps -a
#
config.vm.provision "docker" do |d|
# Run Traefik as reverse proxy inside the VM
# It is available on port 8081 on the host
d.run "traefik",
cmd: "--loglevel=info --docker=true --docker.exposedbydefault=false",
args: "-p 80:80 -v /var/run/docker.sock:/var/run/docker.sock"
d.run "portainer/portainer", # credentials admin:admin
cmd: "-H unix:///var/run/docker.sock --admin-password='$2y$05$n8b3wSfBtMdMY1ei4FBx..qbvqlHx7Rpln7Wd61HQYcIJ7pWgGH7q'",
args: '-v /var/run/docker.sock:/var/run/docker.sock -l="traefik.enable=true" -l="traefik.frontend.rule=PathPrefixStrip: /portainer/" -l="traefik.port=9000" --name portainer'
# Make daemon accessible via tcp and restart to apply changes
d.post_install_provision "shell", inline: <<-eol
sed -i '/ExecStart=/c\ExecStart=/usr/bin/dockerd -H fd:// -H tcp://0.0.0.0:2375 --containerd=/run/containerd/containerd.sock' /lib/systemd/system/docker.service \
Expand Down
4 changes: 4 additions & 0 deletions build.gradle
Expand Up @@ -101,3 +101,7 @@ test {
exceptionFormat "full"
}
}

task vagrantTest(type: Test) {
systemProperty "code-freak.docker.host", "http://localhost:2375"
}
Expand Up @@ -45,8 +45,9 @@ class AssignmentController : BaseController() {
val submission = getSubmission(request, assignmentId)

// start a container based on the submission for the current task
val containerId = containerService.startIdeContainer(submission.getAnswerForTask(taskId)!!)
val containerUrl = containerService.getIdeUrl(containerId)
val answer = submission.getAnswerForTask(taskId)!!
containerService.startIdeContainer(answer)
val containerUrl = containerService.getIdeUrl(answer.id)

model.addAttribute("ide_url", containerUrl)
return "ide-redirect"
Expand Down
49 changes: 21 additions & 28 deletions src/main/kotlin/de/code_freak/codefreak/service/ContainerService.kt
Expand Up @@ -3,17 +3,17 @@ package de.code_freak.codefreak.service
import com.spotify.docker.client.DockerClient
import com.spotify.docker.client.messages.ContainerConfig
import com.spotify.docker.client.messages.HostConfig
import com.spotify.docker.client.messages.PortBinding
import de.code_freak.codefreak.entity.Answer
import org.apache.commons.io.IOUtils
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.beans.factory.annotation.Value
import org.springframework.boot.context.event.ApplicationReadyEvent
import org.springframework.context.event.EventListener
import org.springframework.stereotype.Service
import java.lang.IllegalArgumentException
import java.util.UUID
import javax.transaction.Transactional
import kotlin.random.Random

@Service
class ContainerService(
Expand All @@ -25,13 +25,15 @@ class ContainerService(
val DOCKER_IMAGES = listOf(
IDE_DOCKER_IMAGE
)
const val LABEL_PREFIX = "de.code-freak."
const val LABEL_TASK_SUBMISSION_ID = LABEL_PREFIX + "task-submission-id"
const val LABEL_THEIA_PORT = LABEL_PREFIX + "theia-port"
private const val LABEL_PREFIX = "de.code-freak."
const val LABEL_ANSWER_ID = LABEL_PREFIX + "answer-id"
}

private val log = LoggerFactory.getLogger(this::class.java)

@Value("\${code-freak.traefik.url}")
private lateinit var traefikUrl: String

/**
* Pull all required docker images on startup
*/
Expand All @@ -48,7 +50,7 @@ class ContainerService(
* Start an IDE container for the given submission and returns the container ID
* If there is already a container for the submission it will be used instead
*/
fun startIdeContainer(answer: Answer): String {
fun startIdeContainer(answer: Answer) {
// either take existing container or create a new one
val containerId = this.getIdeContainer(answer) ?: this.createIdeContainer(answer)
// make sure the container is running. Also existing ones could have been stopped
Expand All @@ -57,8 +59,6 @@ class ContainerService(
}
// prepare the environment after the container has started
this.prepareIdeContainer(containerId, answer)

return containerId
}

/**
Expand All @@ -80,19 +80,16 @@ class ContainerService(
* Get the URL for an IDE container
* TODO: make this configurable for different types of hosting/reverse proxies/etc
*/
fun getIdeUrl(containerId: String): String {
val containerInfo = docker.inspectContainer(containerId)
val port = containerInfo.config().labels()!![LABEL_THEIA_PORT]

return "http://localhost:$port"
fun getIdeUrl(answerId: UUID): String {
return "$traefikUrl/ide/$answerId/"
}

/**
* Try to find an existing container for the given submission
*/
protected fun getIdeContainer(answer: Answer): String? {
return docker.listContainers(
DockerClient.ListContainersParam.withLabel(LABEL_TASK_SUBMISSION_ID, answer.id.toString()),
DockerClient.ListContainersParam.withLabel(LABEL_ANSWER_ID, answer.id.toString()),
DockerClient.ListContainersParam.limitContainers(1)
).firstOrNull()?.id()
}
Expand All @@ -111,26 +108,22 @@ class ContainerService(
* Returns the ID of the created container
*/
protected fun createIdeContainer(answer: Answer): String {
val id = answer.id.toString()

// 49152-65535 is the private port range
val theiaPort = Random.nextInt(49152, 65535).toString()

val labelMap = mapOf(
LABEL_TASK_SUBMISSION_ID to id,
LABEL_THEIA_PORT to theiaPort
val answerId = answer.id.toString()

val labels = mapOf(
LABEL_ANSWER_ID to answerId,
"traefik.enable" to "true",
"traefik.frontend.rule" to "PathPrefixStrip: /ide/$answerId/",
"traefik.port" to "3000",
"traefik.frontend.headers.customResponseHeaders" to "Access-Control-Allow-Origin:*"
)

val publishedPorts = mapOf(
"3000" to listOf(PortBinding.of("0.0.0.0", theiaPort))
)
val hostConfig = HostConfig.builder().portBindings(publishedPorts).build()
val hostConfig = HostConfig.builder().build()

val containerConfig = ContainerConfig.builder()
.image(IDE_DOCKER_IMAGE)
.labels(labelMap)
.labels(labels)
.hostConfig(hostConfig)
.exposedPorts(publishedPorts.keys)
.build()

val container = docker.createContainer(containerConfig)
Expand Down
2 changes: 2 additions & 0 deletions src/main/resources/application.properties
Expand Up @@ -28,6 +28,8 @@

spring.profiles.active=dev

code-freak.traefik.url=http://localhost:8081

# Hides HHH000424: Disabling contextual LOB creation as createClob() method threw error
logging.level.org.hibernate.engine.jdbc.env.internal.LobCreatorBuilderImpl = WARN

Expand Down
8 changes: 6 additions & 2 deletions src/main/resources/templates/ide-redirect.html
Expand Up @@ -11,8 +11,12 @@
let ide_url = /*[[${ide_url}]]*/

function tryRedirect() {
fetch(ide_url, { mode: "no-cors" }).then(function() {
window.location.href = ide_url
fetch(ide_url).then(function(res) {
if (res.ok) {
window.location.href = ide_url
} else {
throw new Error()
}
}).catch(function() {
setTimeout(tryRedirect, 1000)
});
Expand Down
Expand Up @@ -7,10 +7,10 @@ import de.code_freak.codefreak.entity.Answer
import de.code_freak.codefreak.util.TarUtil
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.containsString
import org.hamcrest.Matchers.equalTo
import org.hamcrest.Matchers.hasSize
import org.hamcrest.Matchers.not
import org.junit.After
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.mockito.Mockito.`when`
Expand All @@ -27,7 +27,7 @@ internal class ContainerServiceTest : SpringTest() {
@Autowired
lateinit var containerService: ContainerService

val taskSubmission by lazy {
val answer by lazy {
val mock = mock(Answer::class.java)
`when`(mock.id).thenReturn(UUID(0, 0))
mock
Expand All @@ -42,40 +42,44 @@ internal class ContainerServiceTest : SpringTest() {
@After
fun tearDown() {
// delete all containers before and after each run
listIdeContainer().parallelStream().forEach {
getAllIdeContainers().parallelStream().forEach {
docker.killContainer(it.id())
docker.removeContainer(it.id())
}
}

@Test
fun `New IDE container is started`() {
val containerId = containerService.startIdeContainer(taskSubmission)
val containers = listIdeContainer()
assertThat(containers, hasSize(1))
assertThat(containers[0].id(), equalTo(containerId))
containerService.startIdeContainer(answer)
val container = getIdeContainer(answer) // throws if container is not present
assertTrue(docker.inspectContainer(container.id()).state().running())
}

@Test
fun `Existing IDE container is used`() {
val containerId1 = containerService.startIdeContainer(taskSubmission)
containerService.startIdeContainer(taskSubmission)
val containers = listIdeContainer()
assertThat(containers, hasSize(1))
assertThat(containers[0].id(), equalTo(containerId1))
containerService.startIdeContainer(answer)
containerService.startIdeContainer(answer) // start twice for the same answer
assertThat(getIdeContainers(answer), hasSize(1))
}

@Test
fun `files are extracted to project directory`() {
`when`(taskSubmission.files).thenReturn(TarUtil.createTarFromDirectory(ClassPathResource("tasks/c-simple").file))
val containerId = containerService.startIdeContainer(taskSubmission)
`when`(answer.files).thenReturn(TarUtil.createTarFromDirectory(ClassPathResource("tasks/c-simple").file))
containerService.startIdeContainer(answer)
val containerId = getIdeContainer(answer).id()
// assert that file is existing and nothing is owned by root
val dirContent = containerService.exec(containerId, arrayOf("ls", "-l", "/home/project"))
assertThat(dirContent, containsString("main.c"))
assertThat(dirContent, not(containsString("root")))
}

private fun listIdeContainer() = docker.listContainers(
ListContainersParam.withLabel(ContainerService.LABEL_TASK_SUBMISSION_ID)
private fun getAllIdeContainers() = docker.listContainers(
ListContainersParam.withLabel(ContainerService.LABEL_ANSWER_ID)
)

private fun getIdeContainers(answer: Answer) = docker.listContainers(
ListContainersParam.withLabel(ContainerService.LABEL_ANSWER_ID, answer.id.toString())
)

private fun getIdeContainer(answer: Answer) = getIdeContainers(answer).first()
}

0 comments on commit a6cd9ee

Please sign in to comment.