Merge branch 'atlantis' into 'main'
Merge Atlantis See merge request htwkmooc/poseidon!1
This commit is contained in:
@ -1,21 +0,0 @@
|
||||
version: "2"
|
||||
|
||||
checks:
|
||||
similar-code:
|
||||
config:
|
||||
threshold: 110 # Golang default: 100
|
||||
identical-code:
|
||||
config:
|
||||
threshold: 110 # Golang default: 100
|
||||
|
||||
plugins:
|
||||
golint:
|
||||
enabled: false
|
||||
govet:
|
||||
enabled: false
|
||||
gofmt:
|
||||
enabled: false
|
||||
|
||||
exclude_patterns:
|
||||
- "**/*_mock.go"
|
||||
- "**/*_test.go"
|
3
.gitmodules
vendored
3
.gitmodules
vendored
@ -1,3 +0,0 @@
|
||||
[submodule "deploy/codeocean-terraform"]
|
||||
path = deploy/codeocean-terraform
|
||||
url = git@lab.xikolo.de:codeocean/codeocean-terraform.git
|
@ -10,11 +10,11 @@ RUN apt-get update
|
||||
#apt-get install -y git && \
|
||||
#git clone https://github.com/openHPI/poseidon.git .
|
||||
|
||||
COPY . .
|
||||
|
||||
# Install make (required for building)
|
||||
RUN apt-get install -y make
|
||||
|
||||
COPY . .
|
||||
|
||||
# Install required project libraries
|
||||
RUN make bootstrap
|
||||
|
||||
|
13
Makefile
13
Makefile
@ -1,4 +1,4 @@
|
||||
PROJECT_NAME = poseidon
|
||||
ROJECT_NAME = poseidon
|
||||
REPOSITORY_OWNER = openHPI
|
||||
PKG = github.com/$(REPOSITORY_OWNER)/$(PROJECT_NAME)/cmd/$(PROJECT_NAME)
|
||||
UNIT_TESTS = $(shell go list ./... | grep -v /e2e | grep -v /recovery)
|
||||
@ -43,10 +43,13 @@ tidy-deps: ## Remove unused dependencies
|
||||
|
||||
|
||||
.PHONY: git-hooks
|
||||
git-hooks: .git/hooks/pre-commit ## Install the git-hooks
|
||||
.git/hooks/%: git_hooks/%
|
||||
cp $^ $@
|
||||
chmod 755 $@
|
||||
GIT_HOOKS_DIR := $(shell if [ -f .git ]; then echo $$(cat .git | sed 's/gitdir: //')/hooks; else echo .git/hooks; fi)
|
||||
git-hooks: $(GIT_HOOKS_DIR)/pre-commit ## Install the git-hooks
|
||||
$(GIT_HOOKS_DIR)/%: git_hooks/%
|
||||
@if [ -d "$(GIT_HOOKS_DIR)" ]; then \
|
||||
cp $^ $@; \
|
||||
chmod 755 $@; \
|
||||
fi
|
||||
|
||||
.PHONY: build
|
||||
build: deps ## Build the binary
|
||||
|
@ -5,6 +5,7 @@ import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"github.com/coreos/go-systemd/v22/activation"
|
||||
"github.com/coreos/go-systemd/v22/daemon"
|
||||
@ -14,12 +15,17 @@ import (
|
||||
"github.com/openHPI/poseidon/internal/api"
|
||||
"github.com/openHPI/poseidon/internal/config"
|
||||
"github.com/openHPI/poseidon/internal/environment"
|
||||
kubernetes2 "github.com/openHPI/poseidon/internal/kubernetes"
|
||||
"github.com/openHPI/poseidon/internal/nomad"
|
||||
"github.com/openHPI/poseidon/internal/runner"
|
||||
"github.com/openHPI/poseidon/pkg/dto"
|
||||
"github.com/openHPI/poseidon/pkg/logging"
|
||||
"github.com/openHPI/poseidon/pkg/monitoring"
|
||||
"golang.org/x/sys/unix"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
"k8s.io/client-go/util/homedir"
|
||||
"path/filepath"
|
||||
// Import the unix package for Unix signals like SIGUSR1
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
@ -31,6 +37,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
@ -343,13 +350,48 @@ func createNomadManager(ctx context.Context) (runner.Manager, environment.Manage
|
||||
environmentManager, err := environment.
|
||||
NewNomadEnvironmentManager(runnerManager, nomadAPIClient, config.Config.Server.TemplateJobFile)
|
||||
if err != nil {
|
||||
log.WithError(err).Fatal("Error initializing environment manager")
|
||||
log.WithError(err).Fatal("Error initializing nomad environment manager")
|
||||
}
|
||||
|
||||
synchronizeNomad(ctx, environmentManager, runnerManager)
|
||||
return runnerManager, environmentManager
|
||||
}
|
||||
|
||||
func createKubernetesManager(ctx context.Context) (runner.Manager, environment.ManagerHandler) {
|
||||
kubernetesConfig := k8sConfig()
|
||||
|
||||
executorAPI, err := kubernetes2.NewExecutorAPI(kubernetesConfig)
|
||||
if err != nil {
|
||||
log.WithError(err).Fatal("Error creating Kubernetes API client configuration")
|
||||
}
|
||||
|
||||
runnerManager := runner.NewKubernetesRunnerManager(&executorAPI, ctx)
|
||||
environmentManager, err := environment.NewKubernetesEnvironmentManager(runnerManager, &executorAPI, config.Config.Server.TemplateJobFile)
|
||||
if err != nil {
|
||||
log.WithError(err).Fatal("Error initializing kubernetes environment manager")
|
||||
}
|
||||
return runnerManager, environmentManager
|
||||
|
||||
}
|
||||
|
||||
func k8sConfig() *rest.Config {
|
||||
var kubeconfig *string
|
||||
if home := homedir.HomeDir(); home != "" {
|
||||
kubeconfig = flag.String("kubeconfig", filepath.Join(home, ".kube", "config"), "(optional) absolute path to the kubeconfig file")
|
||||
} else {
|
||||
kubeconfig = flag.String("kubeconfig", "", "absolute path to the kubeconfig file")
|
||||
}
|
||||
flag.Parse()
|
||||
|
||||
// use the current context in kubeconfig
|
||||
configFromFlags, err := clientcmd.BuildConfigFromFlags("", *kubeconfig)
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
|
||||
return configFromFlags
|
||||
}
|
||||
|
||||
// synchronizeNomad starts the asynchronous synchronization background task and waits for the first environment and runner recovery.
|
||||
func synchronizeNomad(ctx context.Context, environmentManager *environment.NomadEnvironmentManager, runnerManager *runner.NomadRunnerManager) {
|
||||
firstRecoveryDone := make(chan struct{})
|
||||
@ -382,18 +424,13 @@ func synchronizeNomad(ctx context.Context, environmentManager *environment.Nomad
|
||||
}
|
||||
}
|
||||
|
||||
func createAWSManager(ctx context.Context) (
|
||||
runnerManager runner.Manager, environmentManager environment.ManagerHandler) {
|
||||
runnerManager = runner.NewAWSRunnerManager(ctx)
|
||||
return runnerManager, environment.NewAWSEnvironmentManager(runnerManager)
|
||||
}
|
||||
|
||||
// initRouter builds a router that serves the API with the chain of responsibility for multiple managers.
|
||||
func initRouter(ctx context.Context) *mux.Router {
|
||||
runnerManager, environmentManager := createManagerHandler(createNomadManager, config.Config.Nomad.Enabled,
|
||||
nil, nil, ctx)
|
||||
runnerManager, environmentManager = createManagerHandler(createAWSManager, config.Config.AWS.Enabled,
|
||||
runnerManager, environmentManager, ctx)
|
||||
|
||||
runnerManager, environmentManager = createManagerHandler(createKubernetesManager, config.Config.Kubernetes.Enabled,
|
||||
nil, nil, ctx)
|
||||
|
||||
return api.NewRouter(runnerManager, environmentManager)
|
||||
}
|
||||
@ -421,11 +458,11 @@ func initServer(router *mux.Router) *http.Server {
|
||||
func shutdownOnOSSignal(server *http.Server, ctx context.Context, stopProfiling func()) {
|
||||
// wait for SIGINT
|
||||
shutdownSignals := make(chan os.Signal, 1)
|
||||
signal.Notify(shutdownSignals, unix.SIGINT, unix.SIGTERM, unix.SIGABRT)
|
||||
signal.Notify(shutdownSignals, syscall.SIGINT, syscall.SIGTERM, syscall.SIGABRT)
|
||||
|
||||
// wait for SIGUSR1
|
||||
writeProfileSignal := make(chan os.Signal, 1)
|
||||
signal.Notify(writeProfileSignal, unix.SIGUSR1)
|
||||
signal.Notify(writeProfileSignal, syscall.Signal(0xa)) // syscall.SIGUSR1
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
|
@ -1,83 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/hashicorp/nomad/api"
|
||||
"github.com/openHPI/poseidon/internal/environment"
|
||||
"github.com/openHPI/poseidon/internal/nomad"
|
||||
"github.com/openHPI/poseidon/internal/runner"
|
||||
"github.com/openHPI/poseidon/tests"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"golang.org/x/sys/unix"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type MainTestSuite struct {
|
||||
tests.MemoryLeakTestSuite
|
||||
}
|
||||
|
||||
func TestMainTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(MainTestSuite))
|
||||
}
|
||||
|
||||
func (s *MainTestSuite) TestAWSDisabledUsesNomadManager() {
|
||||
disableRecovery, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
runnerManager, environmentManager := createManagerHandler(createNomadManager, true,
|
||||
runner.NewAbstractManager(s.TestCtx), &environment.AbstractManager{}, disableRecovery)
|
||||
awsRunnerManager, awsEnvironmentManager := createManagerHandler(createAWSManager, false,
|
||||
runnerManager, environmentManager, s.TestCtx)
|
||||
s.Equal(runnerManager, awsRunnerManager)
|
||||
s.Equal(environmentManager, awsEnvironmentManager)
|
||||
}
|
||||
|
||||
func (s *MainTestSuite) TestAWSEnabledWrappesNomadManager() {
|
||||
disableRecovery, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
runnerManager, environmentManager := createManagerHandler(createNomadManager, true,
|
||||
runner.NewAbstractManager(s.TestCtx), &environment.AbstractManager{}, disableRecovery)
|
||||
awsRunnerManager, awsEnvironmentManager := createManagerHandler(createAWSManager,
|
||||
true, runnerManager, environmentManager, s.TestCtx)
|
||||
s.NotEqual(runnerManager, awsRunnerManager)
|
||||
s.NotEqual(environmentManager, awsEnvironmentManager)
|
||||
}
|
||||
|
||||
func (s *MainTestSuite) TestShutdownOnOSSignal_Profiling() {
|
||||
called := false
|
||||
disableRecovery, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
s.ExpectedGoroutineIncrease++ // The shutdownOnOSSignal waits for an exit after stopping the profiling.
|
||||
s.ExpectedGoroutineIncrease++ // The shutdownOnOSSignal triggers a os.Signal Goroutine.
|
||||
|
||||
server := initServer(initRouter(disableRecovery))
|
||||
go shutdownOnOSSignal(server, context.Background(), func() {
|
||||
called = true
|
||||
})
|
||||
|
||||
<-time.After(tests.ShortTimeout)
|
||||
err := unix.Kill(unix.Getpid(), unix.SIGUSR1)
|
||||
s.Require().NoError(err)
|
||||
<-time.After(tests.ShortTimeout)
|
||||
|
||||
s.True(called)
|
||||
}
|
||||
|
||||
func (s *MainTestSuite) TestLoadNomadEnvironmentsBeforeStartingWebserver() {
|
||||
apiMock := &nomad.ExecutorAPIMock{}
|
||||
apiMock.On("LoadEnvironmentJobs").Return([]*api.Job{}, nil)
|
||||
apiMock.On("WatchEventStream", mock.Anything, mock.Anything).Run(func(_ mock.Arguments) {
|
||||
<-s.TestCtx.Done()
|
||||
}).Return(nil).Maybe()
|
||||
|
||||
runnerManager := runner.NewNomadRunnerManager(apiMock, s.TestCtx)
|
||||
environmentManager, err := environment.NewNomadEnvironmentManager(runnerManager, apiMock, "")
|
||||
s.Require().NoError(err)
|
||||
|
||||
synchronizeNomad(s.TestCtx, environmentManager, runnerManager)
|
||||
apiMock.AssertExpectations(s.T())
|
||||
}
|
@ -1,90 +0,0 @@
|
||||
job "${NOMAD_SLUG}" {
|
||||
datacenters = ["dc1"]
|
||||
namespace = "${NOMAD_NAMESPACE}"
|
||||
|
||||
group "api" {
|
||||
count = 1
|
||||
|
||||
update {
|
||||
// https://learn.hashicorp.com/tutorials/nomad/job-rolling-update
|
||||
max_parallel = 1
|
||||
min_healthy_time = "30s"
|
||||
healthy_deadline = "5m"
|
||||
progress_deadline = "10m"
|
||||
auto_revert = true
|
||||
}
|
||||
|
||||
// Don't allow rescheduling to fail deployment and pipeline if task fails
|
||||
reschedule {
|
||||
attempts = 0
|
||||
unlimited = false
|
||||
}
|
||||
|
||||
// No restarts to immediately fail the deployment and pipeline on first task fail
|
||||
restart {
|
||||
attempts = 0
|
||||
}
|
||||
|
||||
network {
|
||||
mode = "cni/secure-bridge"
|
||||
|
||||
port "http" {
|
||||
to = 7200
|
||||
}
|
||||
}
|
||||
|
||||
service {
|
||||
# urlprefix- tag allows Fabio to discover this service and proxy traffic correctly
|
||||
tags = ["urlprefix-${HOSTNAME}:80/"]
|
||||
name = "${NOMAD_SLUG}"
|
||||
port = "http"
|
||||
|
||||
// Health check to let Consul know we are alive
|
||||
check {
|
||||
name = "health-check"
|
||||
type = "http"
|
||||
port = "http"
|
||||
path = "/api/v1/health"
|
||||
interval = "10s"
|
||||
timeout = "2s"
|
||||
check_restart {
|
||||
limit = 3 // auto-restart task when health check fails 3x in a row
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
task "api" {
|
||||
driver = "docker"
|
||||
|
||||
config {
|
||||
image = "${IMAGE_NAME_ENV}"
|
||||
}
|
||||
|
||||
template {
|
||||
data = <<EOH
|
||||
${NOMAD_CACERT_DATA}
|
||||
EOH
|
||||
// Note that only some destinations are allowed here
|
||||
// (see https://www.nomadproject.io/docs/job-specification/template#destination).
|
||||
// The secrets folder (NOMAD_SECRETS_DIR) is one of them.
|
||||
destination = "secrets/nomad-ca.crt"
|
||||
change_mode = "noop"
|
||||
}
|
||||
|
||||
env {
|
||||
POSEIDON_SERVER_ADDRESS = "${POSEIDON_LISTEN_ADDRESS}"
|
||||
POSEIDON_NOMAD_ADDRESS = "${NOMAD_SERVER_HOST}"
|
||||
POSEIDON_NOMAD_NAMESPACE = "${NOMAD_NAMESPACE}"
|
||||
POSEIDON_NOMAD_TOKEN = "${DEPLOY_POSEIDON_NOMAD_TOKEN}"
|
||||
POSEIDON_NOMAD_TLS_ACTIVE = "${DEPLOY_POSEIDON_NOMAD_TLS_ACTIVE}"
|
||||
POSEIDON_NOMAD_TLS_CAFILE = "/secrets/nomad-ca.crt"
|
||||
}
|
||||
|
||||
resources {
|
||||
memory = "100" // 100 MB RAM
|
||||
cpu = "100" // 100 MHz
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
1
deploy/aws/.gitignore
vendored
1
deploy/aws/.gitignore
vendored
@ -1 +0,0 @@
|
||||
java11Exec/target/
|
@ -1,44 +0,0 @@
|
||||
# Poseidon AWS Executors
|
||||
|
||||
This project contains source code and supporting files for a serverless application that you can deploy with the SAM CLI. It includes the following functions.
|
||||
|
||||
- java11ExecFunction - Code for the application's Lambda function. It can execute Java files with JDK 11.
|
||||
- events - Invocation events that you can use to invoke the function.
|
||||
- template.yaml - A template that defines the application's AWS resources.
|
||||
|
||||
The application uses several AWS resources, including Lambda functions and an API Gateway API. These resources are defined in the `template.yaml` file in this project. You can update the template to add AWS resources through the same deployment process that updates your application code.
|
||||
|
||||
See the [AWS SAM developer guide](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/what-is-sam.html) for deployment, usage and an introduction to SAM specification, the SAM CLI, and serverless application concepts.
|
||||
|
||||
## Interface
|
||||
|
||||
You can establish a WebSocket connection to the WebSocketURI generated by the deployment. With this connection you can send requests to the lambda functions following this interface:
|
||||
|
||||
```
|
||||
action:
|
||||
description: The name of the requested function.
|
||||
type: string
|
||||
cmd:
|
||||
description: The command that should be executed.
|
||||
type: []string
|
||||
files:
|
||||
description: The files that will be copied before the execution.
|
||||
type: map[string]string
|
||||
```
|
||||
|
||||
So for example:
|
||||
```
|
||||
{
|
||||
"action": "java11Exec",
|
||||
"cmd": [
|
||||
"sh",
|
||||
"-c",
|
||||
"javac org/example/RecursiveMath.java && java org/example/RecursiveMath"
|
||||
],
|
||||
"files": {
|
||||
"org/example/RecursiveMath.java":"cGFja2FnZSBvcmcuZXhhbXBsZTsKCnB1YmxpYyBjbGFzcyBSZWN1cnNpdmVNYXRoIHsKCiAgICBwdWJsaWMgc3RhdGljIHZvaWQgbWFpbihTdHJpbmdbXSBhcmdzKSB7CiAgICAgICAgU3lzdGVtLm91dC5wcmludGxuKCJNZWluIFRleHQiKTsKICAgIH0KCiAgICBwdWJsaWMgc3RhdGljIGRvdWJsZSBwb3dlcihpbnQgYmFzZSwgaW50IGV4cG9uZW50KSB7CiAgICAgICAgcmV0dXJuIDQyOwogICAgfQp9Cgo="
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The messages sent by the function use the [WebSocket Schema](../../api/websocket.schema.json).
|
@ -1,22 +0,0 @@
|
||||
{
|
||||
"headers": {
|
||||
"disableOutput": "True"
|
||||
},
|
||||
"requestContext": {
|
||||
"stage": "production",
|
||||
"requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef",
|
||||
"apiId": "1234567890",
|
||||
"connectedAt": 1641993862426,
|
||||
"connectionId": "L1Z1Cc7iFiACEKQ=",
|
||||
"domainName": "abcdef1234.execute-api.eu-central-1.amazonaws.com",
|
||||
"eventType": "MESSAGE",
|
||||
"extendedRequestId": "L1Z1CH3rliAFRhg=",
|
||||
"messageDirection": "IN",
|
||||
"messageId": "MUBfpelRFiACF9g=",
|
||||
"requestTime": "12/Jan/2022:13:24:22 +0000",
|
||||
"requestTimeEpoch": 1641993862426,
|
||||
"routeKey": "java11Exec"
|
||||
},
|
||||
"body": "{\n \"action\": \"java11Exec\",\n \"cmd\": [\n \"sh\",\n \"-c\",\n \"javac org/example/RecursiveMath.java && java org/example/RecursiveMath\"\n ],\n \"files\": {\n \"org/example/RecursiveMath.java\": \"cGFja2FnZSBvcmcuZXhhbXBsZTsKCnB1YmxpYyBjbGFzcyBSZWN1cnNpdmVNYXRoIHsKCiAgICBwdWJsaWMgc3RhdGljIHZvaWQgbWFpbihTdHJpbmdbXSBhcmdzKSB7CiAgICAgICAgU3lzdGVtLm91dC5wcmludGxuKCJNZWluIFRleHQiKTsKICAgIH0KCiAgICBwdWJsaWMgc3RhdGljIGRvdWJsZSBwb3dlcihpbnQgYmFzZSwgaW50IGV4cG9uZW50KSB7CiAgICAgICAgcmV0dXJuIDQyOwogICAgfQp9Cgo=\"\n }\n}",
|
||||
"isBase64Encoded": false
|
||||
}
|
@ -1,68 +0,0 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>poseidon</groupId>
|
||||
<artifactId>java11Exec</artifactId>
|
||||
<version>1.0</version>
|
||||
<packaging>jar</packaging>
|
||||
<name>A Java executor created for openHPI/Poseidon.</name>
|
||||
<properties>
|
||||
<maven.compiler.source>11</maven.compiler.source>
|
||||
<maven.compiler.target>11</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.amazonaws</groupId>
|
||||
<artifactId>aws-lambda-java-core</artifactId>
|
||||
<version>1.2.3</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.amazonaws</groupId>
|
||||
<artifactId>aws-java-sdk-apigatewaymanagementapi</artifactId>
|
||||
<version>1.12.753</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.amazonaws</groupId>
|
||||
<artifactId>aws-lambda-java-events</artifactId>
|
||||
<version>3.11.6</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.code.gson</groupId>
|
||||
<artifactId>gson</artifactId>
|
||||
<version>2.11.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<version>4.13.2</version>
|
||||
</dependency>
|
||||
<!-- https://mvnrepository.com/artifact/org.hamcrest/hamcrest-core -->
|
||||
<dependency>
|
||||
<groupId>org.hamcrest</groupId>
|
||||
<artifactId>hamcrest-core</artifactId>
|
||||
<version>1.3</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<version>3.6.0</version>
|
||||
<configuration>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>shade</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
@ -1,191 +0,0 @@
|
||||
package poseidon;
|
||||
|
||||
import com.amazonaws.client.builder.AwsClientBuilder;
|
||||
import com.amazonaws.services.apigatewaymanagementapi.AmazonApiGatewayManagementApi;
|
||||
import com.amazonaws.services.apigatewaymanagementapi.AmazonApiGatewayManagementApiClientBuilder;
|
||||
import com.amazonaws.services.apigatewaymanagementapi.model.PostToConnectionRequest;
|
||||
import com.amazonaws.services.lambda.runtime.Context;
|
||||
import com.amazonaws.services.lambda.runtime.RequestHandler;
|
||||
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;
|
||||
import com.amazonaws.services.lambda.runtime.events.APIGatewayV2WebSocketEvent;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonObject;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.util.Base64;
|
||||
import java.util.Map;
|
||||
|
||||
// AwsFunctionRequest contains the java files that needs to be executed.
|
||||
class AwsFunctionRequest {
|
||||
String[] cmd;
|
||||
Map<String, String> files;
|
||||
}
|
||||
|
||||
// WebSocketMessageType are the types of messages that are being sent back over the WebSocket connection.
|
||||
enum WebSocketMessageType {
|
||||
WebSocketOutputStdout("stdout"),
|
||||
WebSocketOutputStderr("stderr"),
|
||||
WebSocketOutputError("error"),
|
||||
WebSocketExit("exit");
|
||||
|
||||
private final String typeName;
|
||||
|
||||
WebSocketMessageType(String name) {
|
||||
this.typeName = name;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return typeName;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for requests to Lambda function.
|
||||
* This Lambda function executes the passed command with the provided files in an isolated Java environment.
|
||||
*/
|
||||
public class App implements RequestHandler<APIGatewayV2WebSocketEvent, APIGatewayProxyResponseEvent> {
|
||||
|
||||
// gson helps parse the json objects.
|
||||
private static final Gson gson = new Gson();
|
||||
|
||||
// gwClient is used to send messages back via the WebSocket connection.
|
||||
private AmazonApiGatewayManagementApi gwClient;
|
||||
|
||||
// connectionID helps to identify the WebSocket connection that has called this function.
|
||||
private String connectionID;
|
||||
|
||||
public static final String disableOutputHeaderKey = "disableOutput";
|
||||
|
||||
// disableOutput: If set to true, no output will be sent over the WebSocket connection.
|
||||
private boolean disableOutput = false;
|
||||
|
||||
// Unwrapps the passed command. We expect a "sh -c" wrapped command.
|
||||
public static String unwrapCommand(String[] cmd) {
|
||||
return cmd[cmd.length - 1];
|
||||
}
|
||||
|
||||
// Replaces the last element of the command with the new shell command.
|
||||
public static String[] wrapCommand(String[] originalCommand, String shellCommand) {
|
||||
originalCommand[originalCommand.length - 1] = shellCommand;
|
||||
return originalCommand;
|
||||
}
|
||||
|
||||
public APIGatewayProxyResponseEvent handleRequest(final APIGatewayV2WebSocketEvent input, final Context context) {
|
||||
APIGatewayV2WebSocketEvent.RequestContext ctx = input.getRequestContext();
|
||||
String[] domains = ctx.getDomainName().split("\\.");
|
||||
String region = domains[domains.length-3];
|
||||
this.gwClient = AmazonApiGatewayManagementApiClientBuilder.standard()
|
||||
.withEndpointConfiguration(new AwsClientBuilder.EndpointConfiguration("https://" + ctx.getDomainName() + "/" + ctx.getStage(), region))
|
||||
.build();
|
||||
this.connectionID = ctx.getConnectionId();
|
||||
this.disableOutput = input.getHeaders() != null && input.getHeaders().containsKey(disableOutputHeaderKey) && Boolean.parseBoolean(input.getHeaders().get(disableOutputHeaderKey));
|
||||
AwsFunctionRequest execution = gson.fromJson(input.getBody(), AwsFunctionRequest.class);
|
||||
|
||||
try {
|
||||
File workingDirectory = this.writeFS(execution.files);
|
||||
|
||||
String[] cmd = execution.cmd;
|
||||
try {
|
||||
SimpleMakefile make = new SimpleMakefile(execution.files);
|
||||
cmd = wrapCommand(cmd, make.parseCommand(unwrapCommand(execution.cmd)));
|
||||
} catch (NoMakefileFoundException | NoMakeCommandException | InvalidMakefileException ignored) {}
|
||||
|
||||
ProcessBuilder pb = new ProcessBuilder(cmd);
|
||||
pb.directory(workingDirectory);
|
||||
Process p = pb.start();
|
||||
InputStream stdout = p.getInputStream(), stderr = p.getErrorStream();
|
||||
this.forwardOutput(p, stdout, stderr);
|
||||
p.destroy();
|
||||
return new APIGatewayProxyResponseEvent().withStatusCode(200);
|
||||
} catch (Exception e) {
|
||||
this.sendMessage(WebSocketMessageType.WebSocketOutputError, e.toString(), null);
|
||||
return new APIGatewayProxyResponseEvent().withBody(e.toString()).withStatusCode(500);
|
||||
}
|
||||
}
|
||||
|
||||
// writeFS writes the files to the local filesystem.
|
||||
private File writeFS(Map<String, String> files) throws IOException {
|
||||
File workspace = Files.createTempDirectory("workspace").toFile();
|
||||
for (Map.Entry<String, String> entry : files.entrySet()) {
|
||||
try {
|
||||
File f = new File(entry.getKey());
|
||||
|
||||
if (!f.isAbsolute()) {
|
||||
f = new File(workspace, entry.getKey());
|
||||
}
|
||||
|
||||
f.getParentFile().mkdirs();
|
||||
if (!f.getParentFile().exists()) {
|
||||
throw new IOException("Cannot create parent directories.");
|
||||
}
|
||||
|
||||
f.createNewFile();
|
||||
if (!f.exists()) {
|
||||
throw new IOException("Cannot create file.");
|
||||
}
|
||||
|
||||
Files.write(f.toPath(), Base64.getDecoder().decode(entry.getValue()));
|
||||
} catch (IOException e) {
|
||||
this.sendMessage(WebSocketMessageType.WebSocketOutputError, e.toString(), null);
|
||||
}
|
||||
}
|
||||
return workspace;
|
||||
}
|
||||
|
||||
// forwardOutput sends the output of the process to the WebSocket connection.
|
||||
private void forwardOutput(Process p, InputStream stdout, InputStream stderr) throws InterruptedException {
|
||||
Thread output = new Thread(() -> scanForOutput(p, stdout, WebSocketMessageType.WebSocketOutputStdout));
|
||||
Thread error = new Thread(() -> scanForOutput(p, stderr, WebSocketMessageType.WebSocketOutputStderr));
|
||||
output.start();
|
||||
error.start();
|
||||
|
||||
output.join();
|
||||
error.join();
|
||||
this.sendMessage(WebSocketMessageType.WebSocketExit, null, p.waitFor());
|
||||
}
|
||||
|
||||
// scanForOutput reads the passed stream and forwards it via the WebSocket connection.
|
||||
private void scanForOutput(Process p, InputStream stream, WebSocketMessageType type) {
|
||||
BufferedReader br = new BufferedReader(new InputStreamReader(stream));
|
||||
StringBuilder s = new StringBuilder();
|
||||
int nextByte;
|
||||
|
||||
try {
|
||||
while ((nextByte = br.read()) != -1) {
|
||||
char c = (char) nextByte;
|
||||
s.append(c);
|
||||
|
||||
if (c == '\n') {
|
||||
this.sendMessage(type, s.toString(), null);
|
||||
s = new StringBuilder();
|
||||
}
|
||||
}
|
||||
} catch (IOException ignored) {}
|
||||
|
||||
if (!s.toString().isEmpty()) {
|
||||
this.sendMessage(type, s.toString(), null);
|
||||
}
|
||||
}
|
||||
|
||||
// sendMessage sends WebSocketMessage objects back to the requester of this Lambda function.
|
||||
private void sendMessage(WebSocketMessageType type, String data, Integer exitCode) {
|
||||
JsonObject msg = new JsonObject();
|
||||
msg.addProperty("type", type.toString());
|
||||
if (type == WebSocketMessageType.WebSocketExit) {
|
||||
msg.addProperty("data", exitCode);
|
||||
} else {
|
||||
msg.addProperty("data", data);
|
||||
}
|
||||
|
||||
if (this.disableOutput) {
|
||||
System.out.println(gson.toJson(msg));
|
||||
} else {
|
||||
this.gwClient.postToConnection(new PostToConnectionRequest()
|
||||
.withConnectionId(this.connectionID)
|
||||
.withData(ByteBuffer.wrap(gson.toJson(msg).getBytes(StandardCharsets.UTF_8))));
|
||||
}
|
||||
}
|
||||
}
|
@ -1,152 +0,0 @@
|
||||
package poseidon;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
// NoMakefileFoundException is thrown if no makefile could be found.
|
||||
class NoMakefileFoundException extends Exception {}
|
||||
|
||||
// NoMakeCommandException is thrown if no make command is called.
|
||||
class NoMakeCommandException extends Exception {}
|
||||
|
||||
// InvalidMakefileException is thrown if there is no valid rule to be executed.
|
||||
class InvalidMakefileException extends Exception {}
|
||||
|
||||
// SimpleMakefile adds limited support for the execution of a makefile as passed command. The default Java image does not contain make.
|
||||
class SimpleMakefile {
|
||||
|
||||
// This pattern validates if a command is a make command.
|
||||
private static final Pattern isMakeCommand = Pattern.compile("^(?<before>.* && )?make(?:\\s+(?<startRule>\\w*))?(?<assignments>(?:.*?=.*?)+)?(?<after> && .*)?$");
|
||||
|
||||
// This pattern identifies the rules in a makefile.
|
||||
private static final Pattern makeRules = Pattern.compile("(?<name>.*):\\r?\\n(?<commands>(?:\\t.+\\r?\\n?)*)");
|
||||
|
||||
// The first rule of the makefile.
|
||||
private String firstRule = null;
|
||||
|
||||
// The rules included in the makefile.
|
||||
private final Map<String, String[]> rules = new HashMap<>();
|
||||
|
||||
|
||||
private static String concatCommands(String[] commands) {
|
||||
return String.join(" && ", commands);
|
||||
}
|
||||
|
||||
// getMakefile returns the makefile out of the passed files map.
|
||||
private static String getMakefile(Map<String, String> files) throws NoMakefileFoundException {
|
||||
String makefileB64;
|
||||
if (files.containsKey("Makefile")) {
|
||||
makefileB64 = files.get("Makefile");
|
||||
} else if (files.containsKey("makefile")) {
|
||||
makefileB64 = files.get("makefile");
|
||||
} else {
|
||||
throw new NoMakefileFoundException();
|
||||
}
|
||||
|
||||
return new String(Base64.getDecoder().decode(makefileB64), StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
public SimpleMakefile(Map<String, String> files) throws NoMakefileFoundException {
|
||||
this.parseRules(getMakefile(files));
|
||||
}
|
||||
|
||||
// parseRules uses the passed makefile to parse rules into the objet's map "rules".
|
||||
private void parseRules(String makefile) {
|
||||
Matcher makeRuleMatcher = makeRules.matcher(makefile);
|
||||
while (makeRuleMatcher.find()) {
|
||||
String ruleName = makeRuleMatcher.group("name");
|
||||
if (firstRule == null) {
|
||||
firstRule = ruleName;
|
||||
}
|
||||
|
||||
String[] ruleCommands = makeRuleMatcher.group("commands").split("\n");
|
||||
String[] trimmedCommands = Arrays.stream(ruleCommands)
|
||||
.map(String::trim)
|
||||
.map(s -> s.startsWith("@") ? s.substring(1) : s)
|
||||
.map(s -> s.contains("#") ? s.substring(0, s.indexOf("#")) : s)
|
||||
.filter(s -> !s.isEmpty())
|
||||
.map(s -> s.replaceAll("/usr/java/lib/hamcrest-core-1\\.3\\.jar", "/var/task/lib/org.hamcrest.hamcrest-core-1.3.jar"))
|
||||
.map(s -> s.replaceAll("/usr/java/lib/junit-4\\.13\\.jar", "/var/task/lib/junit.junit-4.13.2.jar"))
|
||||
.toArray(String[]::new);
|
||||
|
||||
rules.put(ruleName, trimmedCommands);
|
||||
}
|
||||
}
|
||||
|
||||
// getCommand returns a bash line of commands that would be executed by the passed rule.
|
||||
public String getCommand(String rule) {
|
||||
if (rule == null || rule.isEmpty()) {
|
||||
rule = this.firstRule;
|
||||
}
|
||||
|
||||
return concatCommands(rules.get(rule));
|
||||
}
|
||||
|
||||
// updateCommand injects the passed make assignments and before and after statements to the command.
|
||||
private String updateCommand(Matcher makeCommandMatcher, String command) {
|
||||
String assignments = makeCommandMatcher.group("assignments");
|
||||
if (assignments != null) {
|
||||
command = injectAssignments(command, assignments);
|
||||
}
|
||||
|
||||
if (makeCommandMatcher.group("before") != null) {
|
||||
command = makeCommandMatcher.group("before") + command;
|
||||
}
|
||||
if (makeCommandMatcher.group("after") != null) {
|
||||
command = command + makeCommandMatcher.group("after");
|
||||
}
|
||||
|
||||
return command;
|
||||
}
|
||||
|
||||
// getAssignmentPart returns the key or value of the passed assignment depending on the flag firstPart.
|
||||
private String getAssignmentPart(String assignment, boolean firstPart) {
|
||||
String[] parts = assignment.split("=");
|
||||
|
||||
if (firstPart) {
|
||||
return parts[0];
|
||||
} else {
|
||||
return parts[1].replaceAll("^\\\"|\\\"$", "");
|
||||
}
|
||||
}
|
||||
|
||||
// injectAssignments applies all set assignments in the command string.
|
||||
private String injectAssignments(String command, String assignments) {
|
||||
String result = command;
|
||||
Map<String, String> map = Arrays.stream(assignments.split(" "))
|
||||
.filter(s -> !s.isEmpty())
|
||||
.collect(Collectors.toMap(s -> getAssignmentPart(s, true), s -> getAssignmentPart(s, false)));
|
||||
|
||||
for (Map.Entry<String, String> entry : map.entrySet()) {
|
||||
result = result.replaceAll("\\$\\{" + entry.getKey() + "\\}", entry.getValue());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// parseCommand returns a bash line of commands that would be executed by the passed command.
|
||||
public String parseCommand(String shellCommand) throws InvalidMakefileException, NoMakeCommandException {
|
||||
Matcher makeCommandMatcher = isMakeCommand.matcher(shellCommand);
|
||||
if (!makeCommandMatcher.find()) {
|
||||
throw new NoMakeCommandException();
|
||||
}
|
||||
|
||||
String ruleArgument = makeCommandMatcher.group("startRule");
|
||||
if (ruleArgument.isEmpty()) {
|
||||
ruleArgument = this.firstRule;
|
||||
}
|
||||
|
||||
if ((this.firstRule == null) || !rules.containsKey(ruleArgument)) {
|
||||
throw new InvalidMakefileException();
|
||||
}
|
||||
|
||||
String command = getCommand(ruleArgument);
|
||||
return updateCommand(makeCommandMatcher, command);
|
||||
}
|
||||
}
|
@ -1,134 +0,0 @@
|
||||
package poseidon;
|
||||
|
||||
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;
|
||||
import com.amazonaws.services.lambda.runtime.events.APIGatewayV2WebSocketEvent;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.PrintStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
|
||||
public class AppTest {
|
||||
|
||||
static final String RecursiveMathContent = Base64.getEncoder().encodeToString(
|
||||
("package org.example;\n" +
|
||||
"\n" +
|
||||
"public class RecursiveMath {\n" +
|
||||
"\n" +
|
||||
" public static void main(String[] args) {\n" +
|
||||
" System.out.println(\"Mein Text\");\n" +
|
||||
" }\n" +
|
||||
"\n" +
|
||||
" public static double power(int base, int exponent) {\n" +
|
||||
" return 42;\n" +
|
||||
" }\n" +
|
||||
"}").getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
static final String MultilineMathContent = Base64.getEncoder().encodeToString(
|
||||
("package org.example;\n" +
|
||||
"\n" +
|
||||
"public class RecursiveMath {\n" +
|
||||
"\n" +
|
||||
" public static void main(String[] args) {\n" +
|
||||
" System.out.println(\"Mein Text\");\n" +
|
||||
" System.out.println(\"Mein Text\");\n" +
|
||||
" }\n" +
|
||||
"}").getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
static final String MathContentWithoutTrailingNewline = Base64.getEncoder().encodeToString(
|
||||
("package org.example;\n" +
|
||||
"\n" +
|
||||
"public class RecursiveMath {\n" +
|
||||
"\n" +
|
||||
" public static void main(String[] args) {\n" +
|
||||
" System.out.print(\"Mein Text\");\n" +
|
||||
" }\n" +
|
||||
"}").getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
@Test
|
||||
public void successfulResponse() {
|
||||
APIGatewayProxyResponseEvent result = getApiGatewayProxyResponseRecursiveMath(RecursiveMathContent);
|
||||
assertEquals(200, result.getStatusCode().intValue());
|
||||
}
|
||||
|
||||
// Scanner.nextLine() consumes the new line. Therefore, we need to add it again.
|
||||
@Test
|
||||
public void successfulMultilineResponse() {
|
||||
ByteArrayOutputStream out = setupStdOutLogs();
|
||||
APIGatewayProxyResponseEvent result = getApiGatewayProxyResponseRecursiveMath(MultilineMathContent);
|
||||
restoreStdOutLogs();
|
||||
|
||||
assertEquals(200, result.getStatusCode().intValue());
|
||||
String expectedOutput =
|
||||
"{\"type\":\"stdout\",\"data\":\"Mein Text\\n\"}\n" +
|
||||
"{\"type\":\"stdout\",\"data\":\"Mein Text\\n\"}\n" +
|
||||
"{\"type\":\"exit\",\"data\":0}\n";
|
||||
assertEquals(expectedOutput, out.toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void outputWithoutTrailingNewline() {
|
||||
ByteArrayOutputStream out = setupStdOutLogs();
|
||||
APIGatewayProxyResponseEvent result = getApiGatewayProxyResponseRecursiveMath(MathContentWithoutTrailingNewline);
|
||||
restoreStdOutLogs();
|
||||
|
||||
assertEquals(200, result.getStatusCode().intValue());
|
||||
String expectedOutput =
|
||||
"{\"type\":\"stdout\",\"data\":\"Mein Text\"}\n" +
|
||||
"{\"type\":\"exit\",\"data\":0}\n";
|
||||
assertEquals(expectedOutput, out.toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void makefileJustReplacesShellCommand() {
|
||||
ByteArrayOutputStream out = setupStdOutLogs();
|
||||
APIGatewayProxyResponseEvent result = getApiGatewayProxyResponse("{\"action\":\"java11Exec\"," +
|
||||
"\"cmd\":[\"env\", \"TEST_VAR=42\", \"sh\",\"-c\",\"make run\"]," +
|
||||
"\"files\":{\"Makefile\":\"" + Base64.getEncoder().encodeToString(("run:\n\t@echo $TEST_VAR\n").getBytes(StandardCharsets.UTF_8)) + "\"}}");
|
||||
restoreStdOutLogs();
|
||||
|
||||
assertEquals(200, result.getStatusCode().intValue());
|
||||
String expectedOutput =
|
||||
"{\"type\":\"stdout\",\"data\":\"42\\n\"}\n" +
|
||||
"{\"type\":\"exit\",\"data\":0}\n";
|
||||
assertEquals(expectedOutput, out.toString());
|
||||
}
|
||||
|
||||
|
||||
private PrintStream originalOut;
|
||||
private ByteArrayOutputStream setupStdOutLogs() {
|
||||
originalOut = System.out;
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
System.setOut(new PrintStream(out));
|
||||
return out;
|
||||
}
|
||||
|
||||
private void restoreStdOutLogs() {
|
||||
System.setOut(originalOut);
|
||||
}
|
||||
|
||||
private APIGatewayProxyResponseEvent getApiGatewayProxyResponse(String body) {
|
||||
App app = new App();
|
||||
APIGatewayV2WebSocketEvent input = new APIGatewayV2WebSocketEvent();
|
||||
APIGatewayV2WebSocketEvent.RequestContext ctx = new APIGatewayV2WebSocketEvent.RequestContext();
|
||||
ctx.setDomainName("abcdef1234.execute-api.eu-central-1.amazonaws.com");
|
||||
ctx.setConnectionId("myUUID");
|
||||
input.setRequestContext(ctx);
|
||||
Map<String, String> headers = new HashMap<>();
|
||||
headers.put(App.disableOutputHeaderKey, "True");
|
||||
input.setHeaders(headers);
|
||||
input.setBody(body);
|
||||
return app.handleRequest(input, null);
|
||||
}
|
||||
|
||||
private APIGatewayProxyResponseEvent getApiGatewayProxyResponseRecursiveMath(String content) {
|
||||
return getApiGatewayProxyResponse("{\"action\":\"java11Exec\",\"cmd\":[\"sh\",\"-c\",\"javac org/example/RecursiveMath.java && java org/example/RecursiveMath\"]," +
|
||||
"\"files\":{\"org/example/RecursiveMath.java\":\"" + content + "\"}}");
|
||||
}
|
||||
}
|
@ -1,167 +0,0 @@
|
||||
package poseidon;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.fail;
|
||||
import static poseidon.AppTest.RecursiveMathContent;
|
||||
|
||||
|
||||
public class SimpleMakefileTest {
|
||||
static final String SuccessfulMakefile = Base64.getEncoder().encodeToString(
|
||||
("run:\n" +
|
||||
"\tjavac org/example/RecursiveMath.java\n" +
|
||||
"\tjava org/example/RecursiveMath\n" +
|
||||
"\n" +
|
||||
"test:\n" +
|
||||
"\techo Hi\n"
|
||||
).getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
static final String SuccessfulWindowsMakefile = Base64.getEncoder().encodeToString(
|
||||
("run:\r\n" +
|
||||
"\tjavac org/example/RecursiveMath.java\r\n" +
|
||||
"\tjava org/example/RecursiveMath\r\n" +
|
||||
"\r\n" +
|
||||
"test:\r\n" +
|
||||
"\techo Hi\r\n"
|
||||
).getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
static final String SuccessfulMakefileWithAtSymbol = Base64.getEncoder().encodeToString(
|
||||
("run:\r\n" +
|
||||
"\t@javac org/example/RecursiveMath.java\r\n" +
|
||||
"\t@java org/example/RecursiveMath\r\n"
|
||||
).getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
static final String SuccessfulMakefileWithAssignments = Base64.getEncoder().encodeToString(
|
||||
("test:\n" +
|
||||
"\tjavac -encoding utf8 -cp .:/usr/java/lib/hamcrest-core-1.3.jar:/usr/java/lib/junit-4.13.jar ${FILENAME}\n" +
|
||||
"\tjava -Dfile.encoding=UTF8 -cp .:/usr/java/lib/hamcrest-core-1.3.jar:/usr/java/lib/junit-4.13.jar org.junit.runner.JUnitCore ${CLASS_NAME}\n"
|
||||
).getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
static final String SuccessfulMakefileWithComment = Base64.getEncoder().encodeToString(
|
||||
("run:\r\n" +
|
||||
"\t@javac org/example/RecursiveMath.java\r\n" +
|
||||
"\t@java org/example/RecursiveMath\r\n" +
|
||||
"\t#exit\r\n"
|
||||
).getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
static final String NotSupportedMakefile = Base64.getEncoder().encodeToString(
|
||||
("run: test\n" +
|
||||
"\tjavac org/example/RecursiveMath.java\n" +
|
||||
"\tjava org/example/RecursiveMath\n" +
|
||||
"\n" +
|
||||
"test:\n" +
|
||||
"\techo Hi\n"
|
||||
).getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
@Test
|
||||
public void sucessfullMake() {
|
||||
parseRunCommandOfMakefile(SuccessfulMakefile);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sucessfullMakeWithCR() {
|
||||
parseRunCommandOfMakefile(SuccessfulWindowsMakefile);
|
||||
}
|
||||
|
||||
// We remove [the @ Symbol](https://www.gnu.org/software/make/manual/make.html#Echoing)
|
||||
// as the command itself is never written to stdout with this implementation.
|
||||
@Test
|
||||
public void sucessfullMakeWithAtSymbol() {
|
||||
parseRunCommandOfMakefile(SuccessfulMakefileWithAtSymbol);
|
||||
}
|
||||
|
||||
// We remove [any comments with #](https://www.gnu.org/software/make/manual/make.html#Recipe-Syntax)
|
||||
// as they are normally ignored / echoed with most shells.
|
||||
@Test
|
||||
public void sucessfullMakeWithComment() {
|
||||
parseRunCommandOfMakefile(SuccessfulMakefileWithComment);
|
||||
}
|
||||
|
||||
private void parseRunCommandOfMakefile(String makefileB64) {
|
||||
Map<String, String> files = new HashMap<>();
|
||||
files.put("Makefile", makefileB64);
|
||||
files.put("org/example/RecursiveMath.java", RecursiveMathContent);
|
||||
|
||||
try {
|
||||
String command = "make run";
|
||||
SimpleMakefile makefile = new SimpleMakefile(files);
|
||||
String cmd = makefile.parseCommand(command);
|
||||
|
||||
assertEquals("javac org/example/RecursiveMath.java && java org/example/RecursiveMath", cmd);
|
||||
} catch (NoMakefileFoundException | InvalidMakefileException | NoMakeCommandException ignored) {
|
||||
fail();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void withoutMake() {
|
||||
Map<String, String> files = new HashMap<>();
|
||||
files.put("Makefile", SuccessfulMakefile);
|
||||
files.put("org/example/RecursiveMath.java", RecursiveMathContent);
|
||||
|
||||
try {
|
||||
String command = "javac org/example/RecursiveMath.java";
|
||||
SimpleMakefile make = new SimpleMakefile(files);
|
||||
make.parseCommand(command);
|
||||
fail();
|
||||
} catch (NoMakefileFoundException | InvalidMakefileException ignored) {
|
||||
fail();
|
||||
} catch (NoMakeCommandException ignored) {}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sucessfullMakeWithAssignments() {
|
||||
Map<String, String> files = new HashMap<>();
|
||||
files.put("Makefile", SuccessfulMakefileWithAssignments);
|
||||
files.put("org/example/RecursiveMath.java", RecursiveMathContent);
|
||||
|
||||
try {
|
||||
String command = "make test CLASS_NAME=\"RecursiveMath\" FILENAME=\"RecursiveMath-Test.java\"";
|
||||
SimpleMakefile make = new SimpleMakefile(files);
|
||||
String cmd = make.parseCommand(command);
|
||||
|
||||
assertEquals("javac -encoding utf8 -cp .:/var/task/lib/org.hamcrest.hamcrest-core-1.3.jar:/var/task/lib/junit.junit-4.13.2.jar RecursiveMath-Test.java && " +
|
||||
"java -Dfile.encoding=UTF8 -cp .:/var/task/lib/org.hamcrest.hamcrest-core-1.3.jar:/var/task/lib/junit.junit-4.13.2.jar org.junit.runner.JUnitCore RecursiveMath", cmd);
|
||||
} catch (NoMakefileFoundException | InvalidMakefileException | NoMakeCommandException ignored) {
|
||||
fail();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void withNotSupportedMakefile() {
|
||||
Map<String, String> files = new HashMap<>();
|
||||
files.put("Makefile", NotSupportedMakefile);
|
||||
files.put("org/example/RecursiveMath.java", RecursiveMathContent);
|
||||
|
||||
try {
|
||||
String command = "make run";
|
||||
SimpleMakefile makefile = new SimpleMakefile(files);
|
||||
makefile.parseCommand(command);
|
||||
fail();
|
||||
} catch (NoMakefileFoundException | NoMakeCommandException ignored) {
|
||||
fail();
|
||||
} catch (InvalidMakefileException ignored) {}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void withBeforeAndAfterStatements() {
|
||||
Map<String, String> files = new HashMap<>();
|
||||
files.put("Makefile", Base64.getEncoder().encodeToString(("run:\n\t@echo TRAAAIIN\n").getBytes(StandardCharsets.UTF_8)));
|
||||
|
||||
try {
|
||||
String command = "echo \"Look it's a\" && sl && make run && echo WOW";
|
||||
SimpleMakefile makefile = new SimpleMakefile(files);
|
||||
String cmd = makefile.parseCommand(command);
|
||||
|
||||
assertEquals("echo \"Look it's a\" && sl && echo TRAAAIIN && echo WOW", cmd);
|
||||
} catch (NoMakefileFoundException | InvalidMakefileException | NoMakeCommandException ignored) {
|
||||
fail();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,105 +0,0 @@
|
||||
AWSTemplateFormatVersion: '2010-09-09'
|
||||
Transform: AWS::Serverless-2016-10-31
|
||||
Description: >
|
||||
PoseidonExecutors
|
||||
|
||||
Execute untrusted code in AWS functions.
|
||||
|
||||
# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst
|
||||
Globals:
|
||||
Function:
|
||||
Timeout: 15
|
||||
|
||||
Resources:
|
||||
PoseidonExecWebSocket:
|
||||
Type: AWS::ApiGatewayV2::Api
|
||||
Properties:
|
||||
Name: PoseidonExecWebSocket
|
||||
ProtocolType: WEBSOCKET
|
||||
RouteSelectionExpression: "$request.body.action"
|
||||
|
||||
Deployment:
|
||||
Type: AWS::ApiGatewayV2::Deployment
|
||||
DependsOn:
|
||||
- java11ExecRoute
|
||||
Properties:
|
||||
ApiId: !Ref PoseidonExecWebSocket
|
||||
|
||||
Stage:
|
||||
Type: AWS::ApiGatewayV2::Stage
|
||||
Properties:
|
||||
StageName: production
|
||||
Description: Production Stage
|
||||
DeploymentId: !Ref Deployment
|
||||
ApiId: !Ref PoseidonExecWebSocket
|
||||
|
||||
java11ExecRoute: # More info about Routes: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigatewayv2-route.html
|
||||
Type: AWS::ApiGatewayV2::Route
|
||||
Properties:
|
||||
ApiId: !Ref PoseidonExecWebSocket
|
||||
RouteKey: java11Exec
|
||||
AuthorizationType: NONE
|
||||
OperationName: java11ExecRoute
|
||||
Target: !Join
|
||||
- '/'
|
||||
- - 'integrations'
|
||||
- !Ref java11ExecInteg
|
||||
|
||||
java11ExecInteg: # More info about Integrations: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html
|
||||
Type: AWS::ApiGatewayV2::Integration
|
||||
Properties:
|
||||
ApiId: !Ref PoseidonExecWebSocket
|
||||
Description: Java 11 Exec Integration
|
||||
IntegrationType: AWS_PROXY
|
||||
IntegrationUri:
|
||||
Fn::Sub:
|
||||
arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${java11ExecFunction.Arn}/invocations
|
||||
|
||||
java11ExecFunction:
|
||||
Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
|
||||
Properties:
|
||||
CodeUri: java11Exec/
|
||||
Handler: poseidon.App::handleRequest
|
||||
Runtime: java11
|
||||
Architectures:
|
||||
- arm64
|
||||
MemorySize: 2048
|
||||
Policies:
|
||||
- Statement:
|
||||
- Effect: Allow
|
||||
Action:
|
||||
- 'execute-api:*'
|
||||
Resource: "*"
|
||||
- Effect: Allow
|
||||
Action:
|
||||
- 'logs:CreateLogGroup'
|
||||
Resource:
|
||||
- !Sub 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:*'
|
||||
- Effect: Allow
|
||||
Action:
|
||||
- 'logs:CreateLogStream'
|
||||
- 'logs:PutLogEvents'
|
||||
Resource:
|
||||
- !Sub 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:${PoseidonExecWebSocket}:*'
|
||||
|
||||
java11ExecPermission:
|
||||
Type: AWS::Lambda::Permission
|
||||
DependsOn:
|
||||
- PoseidonExecWebSocket
|
||||
Properties:
|
||||
Action: lambda:InvokeFunction
|
||||
FunctionName: !Ref java11ExecFunction
|
||||
Principal: apigateway.amazonaws.com
|
||||
|
||||
Outputs:
|
||||
WebSocketURI:
|
||||
Description: "The WSS Protocol URI to connect to"
|
||||
Value: !Join [ '', [ 'wss://', !Ref PoseidonExecWebSocket, '.execute-api.',!Ref 'AWS::Region','.amazonaws.com/',!Ref 'Stage' ] ]
|
||||
|
||||
java11ExecFunctionArn:
|
||||
Description: "Java 11 Execution Lambda Function ARN"
|
||||
Value: !GetAtt java11ExecFunction.Arn
|
||||
|
||||
java11ExecFunctionIamRole:
|
||||
Description: "Implicit IAM Role created for the Java 11 Execution function"
|
||||
Value: !GetAtt java11ExecFunctionRole.Arn
|
Submodule deploy/codeocean-terraform deleted from 2717dd9ad6
@ -1,3 +0,0 @@
|
||||
FROM docker:latest
|
||||
|
||||
RUN apk update && apk add make
|
8
deploy/grafana-dashboard/.gitignore
vendored
8
deploy/grafana-dashboard/.gitignore
vendored
@ -1,8 +0,0 @@
|
||||
# Ignore the generated json encoded dashboard.
|
||||
main.json
|
||||
|
||||
# Ignore the current environments required for generating the dashboard.
|
||||
environments.json
|
||||
|
||||
# Ignore the Grafana dashboard deployment files.
|
||||
dashboards/
|
@ -1,9 +0,0 @@
|
||||
[[source]]
|
||||
url = "https://pypi.org/simple"
|
||||
verify_ssl = true
|
||||
name = "pypi"
|
||||
|
||||
[packages]
|
||||
grafanalib = "*"
|
||||
|
||||
[dev-packages]
|
35
deploy/grafana-dashboard/Pipfile.lock
generated
35
deploy/grafana-dashboard/Pipfile.lock
generated
@ -1,35 +0,0 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "6b655eb9342c7315ed456ce5fe58b653a36f3726736f0cecf6252e9a2247d9ee"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {},
|
||||
"sources": [
|
||||
{
|
||||
"name": "pypi",
|
||||
"url": "https://pypi.org/simple",
|
||||
"verify_ssl": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"default": {
|
||||
"attrs": {
|
||||
"hashes": [
|
||||
"sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30",
|
||||
"sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==23.2.0"
|
||||
},
|
||||
"grafanalib": {
|
||||
"hashes": [
|
||||
"sha256:3d92bb4e92ae78fe4e21c5b252ab51f4fdcacd8523ba5a44545b897b2a375b83",
|
||||
"sha256:6fab5d7b837a1f2d1322ef762cd52e565ec0422707a7512765c59f668bdceb58"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.7.1"
|
||||
}
|
||||
},
|
||||
"develop": {}
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
# Grafana Dashboard Deployment
|
||||
|
||||
## Grafanalib
|
||||
|
||||
We use [Grafanalib](https://github.com/weaveworks/grafanalib) for the definition of our dashboard.
|
||||
You need to install the python package: `pip install grafanalib`.
|
||||
|
||||
### Generation
|
||||
|
||||
Generate the Grafana dashboard by running `main.py`.
|
||||
The generated Json definition can be imported in the Grafana UI.
|
||||
|
||||
### Deployment
|
||||
|
||||
You can copy the generated json into the field under the dashboards setting -> "JSON Model".
|
||||
The version number needs to match!
|
@ -1,15 +0,0 @@
|
||||
{
|
||||
"executionEnvironments": [
|
||||
{"image": "openhpi/co_execenv_java:8-antlr", "id": 10},
|
||||
{"image": "openhpi/co_execenv_r:4", "id": 28},
|
||||
{"image": "openhpi/co_execenv_python:3.8", "id": 29},
|
||||
{"image": "openhpi/co_execenv_java:17", "id": 31},
|
||||
{"image": "openhpi/docker_exec_phusion", "id": 33},
|
||||
{"image": "openhpi/co_execenv_java:8-antlr", "id": 11},
|
||||
{"image": "openhpi/co_execenv_python:3.4", "id": 14},
|
||||
{"image": "openhpi/co_execenv_node:0.12", "id": 18},
|
||||
{"image": "openhpi/co_execenv_python:3.4-rpi-web", "id": 22},
|
||||
{"image": "openhpi/co_execenv_ruby:2.5", "id": 25},
|
||||
{"image": "openhpi/co_execenv_python:3.7-ml", "id": 30}
|
||||
]
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
#!/usr/bin/python3
|
||||
from grafanalib._gen import generate_dashboard
|
||||
|
||||
if __name__ == "__main__":
|
||||
generate_dashboard(args=["-o", "main.json", "panels/poseidon.dashboard.py"])
|
@ -1,62 +0,0 @@
|
||||
from grafanalib.core import RowPanel, BarGauge, GridPos, TimeSeries, ORIENTATION_VERTICAL, \
|
||||
GAUGE_DISPLAY_MODE_BASIC
|
||||
from grafanalib.influxdb import InfluxDBTarget
|
||||
|
||||
from utils.color_mapping import color_mapping_environments
|
||||
from utils.utils import read_query
|
||||
|
||||
prewarming_pool_size = BarGauge(
|
||||
title="Prewarming Pool Size",
|
||||
dataSource="Flux",
|
||||
targets=[InfluxDBTarget(query=read_query("prewarming-pool-size", "environment-mapping"))],
|
||||
gridPos=GridPos(h=10, w=11, x=0, y=1),
|
||||
allValues=True,
|
||||
orientation=ORIENTATION_VERTICAL,
|
||||
displayMode=GAUGE_DISPLAY_MODE_BASIC,
|
||||
max=None,
|
||||
extraJson=color_mapping_environments,
|
||||
)
|
||||
|
||||
idle_runner = TimeSeries(
|
||||
title="Idle Runner",
|
||||
dataSource="Flux",
|
||||
targets=[InfluxDBTarget(query=read_query("idle-runner", "environment-mapping"))],
|
||||
gridPos=GridPos(h=10, w=13, x=11, y=1),
|
||||
lineInterpolation="stepAfter",
|
||||
maxDataPoints=None,
|
||||
extraJson=color_mapping_environments,
|
||||
)
|
||||
|
||||
runner_startup_duration = TimeSeries(
|
||||
title="Runner startup duration",
|
||||
dataSource="Flux",
|
||||
targets=[InfluxDBTarget(query=read_query("runner-startup-duration", "environment-mapping"))],
|
||||
gridPos=GridPos(h=10, w=12, x=0, y=11),
|
||||
unit="ns",
|
||||
maxDataPoints=None,
|
||||
lineInterpolation="smooth",
|
||||
extraJson=color_mapping_environments,
|
||||
)
|
||||
|
||||
used_runner = TimeSeries(
|
||||
title="Used Runner",
|
||||
dataSource="Flux",
|
||||
targets=[InfluxDBTarget(query=read_query("used-runner"))],
|
||||
gridPos=GridPos(h=10, w=12, x=12, y=11),
|
||||
maxDataPoints=None,
|
||||
lineInterpolation="smooth",
|
||||
)
|
||||
|
||||
availability_row = RowPanel(
|
||||
title="Availability",
|
||||
collapsed=False,
|
||||
gridPos=GridPos(h=1, w=24, x=0, y=0),
|
||||
)
|
||||
|
||||
availability_panels = [
|
||||
availability_row,
|
||||
prewarming_pool_size,
|
||||
idle_runner,
|
||||
runner_startup_duration,
|
||||
used_runner,
|
||||
]
|
@ -1,127 +0,0 @@
|
||||
from grafanalib.core import RowPanel, GridPos, Stat, TimeSeries, Heatmap, BarGauge, GAUGE_DISPLAY_MODE_GRADIENT, \
|
||||
ORIENTATION_VERTICAL, GAUGE_DISPLAY_MODE_BASIC
|
||||
from grafanalib.influxdb import InfluxDBTarget
|
||||
|
||||
from utils.color_mapping import grey_all_mapping, color_mapping_environments
|
||||
from utils.utils import read_query
|
||||
|
||||
requests_per_minute = TimeSeries(
|
||||
title="Requests per minute",
|
||||
dataSource="Flux",
|
||||
targets=[InfluxDBTarget(query=read_query("requests-per-minute"))],
|
||||
gridPos=GridPos(h=9, w=8, x=0, y=22),
|
||||
scaleDistributionType="log",
|
||||
extraJson=grey_all_mapping,
|
||||
lineInterpolation="smooth",
|
||||
)
|
||||
|
||||
request_latency = Heatmap(
|
||||
title="Request Latency",
|
||||
dataSource="Flux",
|
||||
dataFormat="timeseries",
|
||||
targets=[InfluxDBTarget(query=read_query("request-latency"))],
|
||||
gridPos=GridPos(h=9, w=8, x=8, y=22),
|
||||
maxDataPoints=None,
|
||||
extraJson={
|
||||
"options": {},
|
||||
"yAxis": {
|
||||
"format": "ns"
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
service_time = TimeSeries(
|
||||
title="Service time (99.9%)",
|
||||
dataSource="Flux",
|
||||
targets=[InfluxDBTarget(query=read_query("service-time"))],
|
||||
gridPos=GridPos(h=9, w=8, x=16, y=22),
|
||||
scaleDistributionType="log",
|
||||
scaleDistributionLog=10,
|
||||
unit="ns",
|
||||
maxDataPoints=None,
|
||||
lineInterpolation="smooth",
|
||||
)
|
||||
|
||||
current_environment_count = Stat(
|
||||
title="Current environment count",
|
||||
dataSource="Flux",
|
||||
targets=[InfluxDBTarget(query=read_query("current-environment-count"))],
|
||||
gridPos=GridPos(h=6, w=8, x=0, y=31),
|
||||
alignment="center",
|
||||
)
|
||||
|
||||
currently_used_runners = Stat(
|
||||
title="Currently used runners",
|
||||
dataSource="Flux",
|
||||
targets=[InfluxDBTarget(query=read_query("currently-used-runners"))],
|
||||
gridPos=GridPos(h=6, w=8, x=8, y=31),
|
||||
alignment="center",
|
||||
)
|
||||
|
||||
number_of_executions = BarGauge(
|
||||
title="Number of Executions",
|
||||
dataSource="Flux",
|
||||
targets=[InfluxDBTarget(query=read_query("number-of-executions", "environment-mapping"))],
|
||||
gridPos=GridPos(h=6, w=8, x=16, y=31),
|
||||
allValues=True,
|
||||
orientation=ORIENTATION_VERTICAL,
|
||||
displayMode=GAUGE_DISPLAY_MODE_BASIC,
|
||||
max=None,
|
||||
extraJson=color_mapping_environments,
|
||||
)
|
||||
|
||||
execution_duration = BarGauge(
|
||||
title="Execution duration",
|
||||
dataSource="Flux",
|
||||
targets=[InfluxDBTarget(query=read_query("execution-duration", "environment-mapping"))],
|
||||
gridPos=GridPos(h=11, w=8, x=0, y=37),
|
||||
allValues=True,
|
||||
displayMode=GAUGE_DISPLAY_MODE_GRADIENT,
|
||||
format="ns",
|
||||
max=None,
|
||||
decimals=2,
|
||||
extraJson=color_mapping_environments,
|
||||
)
|
||||
|
||||
executions_per_runner = BarGauge(
|
||||
title="Executions per runner",
|
||||
dataSource="Flux",
|
||||
targets=[InfluxDBTarget(query=read_query("executions-per-runner", "environment-mapping"))],
|
||||
gridPos=GridPos(h=11, w=8, x=8, y=37),
|
||||
allValues=True,
|
||||
displayMode=GAUGE_DISPLAY_MODE_GRADIENT,
|
||||
max=None,
|
||||
decimals=2,
|
||||
extraJson=color_mapping_environments,
|
||||
)
|
||||
|
||||
executions_per_minute = BarGauge(
|
||||
title="Executions per minute",
|
||||
dataSource="Flux",
|
||||
targets=[InfluxDBTarget(query=read_query("executions-per-minute", "environment-mapping"))],
|
||||
gridPos=GridPos(h=11, w=8, x=16, y=37),
|
||||
allValues=True,
|
||||
displayMode=GAUGE_DISPLAY_MODE_GRADIENT,
|
||||
max=None,
|
||||
decimals=2,
|
||||
extraJson=color_mapping_environments,
|
||||
)
|
||||
|
||||
general_row = RowPanel(
|
||||
title="General",
|
||||
collapsed=False,
|
||||
gridPos=GridPos(h=1, w=24, x=0, y=21),
|
||||
)
|
||||
|
||||
general_panels = [
|
||||
general_row,
|
||||
requests_per_minute,
|
||||
request_latency,
|
||||
service_time,
|
||||
current_environment_count,
|
||||
currently_used_runners,
|
||||
number_of_executions,
|
||||
execution_duration,
|
||||
executions_per_runner,
|
||||
executions_per_minute,
|
||||
]
|
@ -1,18 +0,0 @@
|
||||
from grafanalib.core import Dashboard, Templating, Time
|
||||
|
||||
from panels.availability_row import availability_panels
|
||||
from panels.general_row import general_panels
|
||||
from panels.runner_insights_row import runner_insights_panels
|
||||
from utils.variables import environment_variable
|
||||
|
||||
dashboard = Dashboard(
|
||||
title="Poseidon autogen",
|
||||
timezone="browser",
|
||||
panels=availability_panels + general_panels + runner_insights_panels,
|
||||
templating=Templating(list=[ environment_variable ]),
|
||||
editable=True,
|
||||
refresh="30s",
|
||||
time=Time("now-6h", "now"),
|
||||
uid="P21Bh1SVk",
|
||||
version=1,
|
||||
).auto_panel_ids()
|
@ -1,115 +0,0 @@
|
||||
from grafanalib.core import RowPanel, GridPos, Histogram, TimeSeries, BarGauge, ORIENTATION_VERTICAL, \
|
||||
GAUGE_DISPLAY_MODE_BASIC, PERCENT_UNIT_FORMAT, GAUGE_CALC_MEAN
|
||||
from grafanalib.influxdb import InfluxDBTarget
|
||||
|
||||
from utils.color_mapping import color_mapping_environments
|
||||
from utils.utils import read_query, deep_update_dict
|
||||
|
||||
execution_duration_extra_json = {
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "ns"
|
||||
}
|
||||
}
|
||||
}
|
||||
deep_update_dict(execution_duration_extra_json, color_mapping_environments)
|
||||
execution_duration = Histogram(
|
||||
title="Execution duration",
|
||||
dataSource="Flux",
|
||||
targets=[InfluxDBTarget(query=read_query("execution-duration-hist", "environment-mapping"))],
|
||||
gridPos=GridPos(h=8, w=24, x=0, y=49),
|
||||
bucketSize=100000000,
|
||||
colorMode="palette-classic",
|
||||
fillOpacity=50,
|
||||
lineWidth=1,
|
||||
maxDataPoints=None,
|
||||
extraJson=execution_duration_extra_json,
|
||||
)
|
||||
|
||||
executions_per_runner = Histogram(
|
||||
title="Executions per runner",
|
||||
dataSource="Flux",
|
||||
targets=[InfluxDBTarget(query=read_query("executions-per-runner-hist", "environment-mapping"))],
|
||||
gridPos=GridPos(h=10, w=11, x=0, y=57),
|
||||
bucketSize=1,
|
||||
colorMode="palette-classic",
|
||||
fillOpacity=50,
|
||||
lineWidth=1,
|
||||
maxDataPoints=None,
|
||||
extraJson=color_mapping_environments,
|
||||
)
|
||||
|
||||
executions_per_minute = TimeSeries(
|
||||
title="Executions per minute",
|
||||
dataSource="Flux",
|
||||
targets=[InfluxDBTarget(query=read_query("executions-per-minute-time", "environment-mapping"))],
|
||||
gridPos=GridPos(h=10, w=13, x=11, y=57),
|
||||
maxDataPoints=None,
|
||||
lineInterpolation="smooth",
|
||||
extraJson=color_mapping_environments,
|
||||
)
|
||||
|
||||
file_upload = TimeSeries(
|
||||
title="File Upload",
|
||||
dataSource="Flux",
|
||||
targets=[InfluxDBTarget(query=read_query("file-upload", "environment-mapping"))],
|
||||
gridPos=GridPos(h=10, w=11, x=0, y=67),
|
||||
scaleDistributionType="log",
|
||||
unit="bytes",
|
||||
maxDataPoints=None,
|
||||
lineInterpolation="smooth",
|
||||
extraJson=color_mapping_environments,
|
||||
)
|
||||
|
||||
runner_per_minute = TimeSeries(
|
||||
title="Runner per minute",
|
||||
dataSource="Flux",
|
||||
targets=[InfluxDBTarget(query=read_query("runner-per-minute", "environment-mapping"))],
|
||||
gridPos=GridPos(h=10, w=13, x=11, y=67),
|
||||
maxDataPoints=None,
|
||||
lineInterpolation="smooth",
|
||||
extraJson=color_mapping_environments,
|
||||
)
|
||||
|
||||
file_download = TimeSeries(
|
||||
title="File Download",
|
||||
dataSource="Flux",
|
||||
targets=[InfluxDBTarget(query=read_query("file-download", "environment-mapping"))],
|
||||
gridPos=GridPos(h=10, w=11, x=0, y=77),
|
||||
scaleDistributionType="log",
|
||||
unit="bytes",
|
||||
maxDataPoints=None,
|
||||
lineInterpolation="smooth",
|
||||
extraJson=color_mapping_environments,
|
||||
)
|
||||
|
||||
file_download_ratio = BarGauge(
|
||||
title="File Download Ratio",
|
||||
dataSource="Flux",
|
||||
targets=[InfluxDBTarget(query=read_query("file-download-ratio", "environment-mapping"))],
|
||||
gridPos=GridPos(h=10, w=13, x=11, y=77),
|
||||
max=1,
|
||||
allValues=False,
|
||||
calc=GAUGE_CALC_MEAN,
|
||||
orientation=ORIENTATION_VERTICAL,
|
||||
displayMode=GAUGE_DISPLAY_MODE_BASIC,
|
||||
format=PERCENT_UNIT_FORMAT,
|
||||
extraJson=color_mapping_environments,
|
||||
)
|
||||
|
||||
runner_insights_row = RowPanel(
|
||||
title="Runner Insights",
|
||||
collapsed=False,
|
||||
gridPos=GridPos(h=1, w=24, x=0, y=48),
|
||||
)
|
||||
|
||||
runner_insights_panels = [
|
||||
runner_insights_row,
|
||||
execution_duration,
|
||||
executions_per_runner,
|
||||
executions_per_minute,
|
||||
file_upload,
|
||||
runner_per_minute,
|
||||
file_download,
|
||||
file_download_ratio,
|
||||
]
|
@ -1,19 +0,0 @@
|
||||
import "date"
|
||||
|
||||
// The need for the date truncation is caused by Poseidon sending all influx events at the same time when starting up. This way not the last but a random value is displayed.
|
||||
// Since in this startup process the highest value is the correct one, we choose the highest value of the last events.
|
||||
|
||||
data = from(bucket: "poseidon")
|
||||
|> range(start: -1y)
|
||||
|> filter(fn: (r) => r["_measurement"] == "poseidon_environments")
|
||||
|> group(columns: ["stage"], mode:"by")
|
||||
|> map(fn: (r) => ({ r with _time: date.truncate(t: r._time, unit: 1m) }))
|
||||
|
||||
deploy_times = data
|
||||
|> last()
|
||||
|> keep(columns: ["stage", "_time"])
|
||||
|
||||
join(tables: {key1: data, key2: deploy_times}, on: ["stage", "_time"], method: "inner")
|
||||
|> max()
|
||||
|> keep(columns: ["stage", "_value"])
|
||||
|> rename(columns: {_value: ""})
|
@ -1,8 +0,0 @@
|
||||
from(bucket: "poseidon")
|
||||
|> range(start: v.timeRangeStart, stop: v.timeRangeStop)
|
||||
|> filter(fn: (r) => r["_measurement"] == "poseidon_used_runners")
|
||||
|> filter(fn: (r) => r["_field"] == "count")
|
||||
|> group(columns: ["stage"], mode:"by")
|
||||
|> last()
|
||||
|> keep(columns: ["_value", "stage"])
|
||||
|> rename(columns: {_value: ""})
|
@ -1,6 +0,0 @@
|
||||
from(bucket: "poseidon")
|
||||
|> range(start: -1y)
|
||||
|> filter(fn: (r) => r["_measurement"] == "poseidon_environments")
|
||||
|> keep(columns: ["id"])
|
||||
|> distinct(column: "id")
|
||||
|> keep(columns: ["_value"])
|
@ -1,14 +0,0 @@
|
||||
envMapping = from(bucket: "poseidon")
|
||||
|> range(start: -1y)
|
||||
|> filter(fn: (r) => r["_measurement"] == "poseidon_environments")
|
||||
|> filter(fn: (r) => r["event_type"] == "creation")
|
||||
|> group(columns: ["id", "stage"], mode:"by")
|
||||
|> last()
|
||||
|> keep(columns: ["id", "image", "stage"])
|
||||
|> rename(columns: {id: "environment_id"})
|
||||
|> map(fn: (r) => ({ r with image: r.environment_id + "/" + strings.trimPrefix(v: r.image, prefix: "openhpi/co_execenv_")}))
|
||||
|
||||
join(tables: {key1: result, key2: envMapping}, on: ["environment_id", "stage"], method: "inner")
|
||||
|> keep(columns: ["_value", "image", "_time"])
|
||||
|> group(columns: ["image"], mode: "by")
|
||||
|> rename(columns: {_value: ""})
|
@ -1,11 +0,0 @@
|
||||
import "strings"
|
||||
|
||||
result = from(bucket: "poseidon")
|
||||
|> range(start: v.timeRangeStart, stop: v.timeRangeStop)
|
||||
|> filter(fn: (r) => r["_field"] == "duration")
|
||||
|> filter(fn: (r) => contains(value: r["environment_id"], set: ${environment_ids:json}))
|
||||
|> filter(fn: (r) => r["_measurement"] == "poseidon_/execute" or r["_measurement"] == "poseidon_/files" or r["_measurement"] == "poseidon_/websocket")
|
||||
|> filter(fn: (r) => exists r.environment_id)
|
||||
|> keep(columns: ["_time", "_value", "environment_id", "stage"])
|
||||
|> aggregateWindow(every: v.windowPeriod, fn: mean)
|
||||
|> map(fn: (r) => ({r with _value: r._value * 3.0})) // Each execution has three requests
|
@ -1,12 +0,0 @@
|
||||
import "strings"
|
||||
|
||||
result = from(bucket: "poseidon")
|
||||
|> range(start: v.timeRangeStart, stop: v.timeRangeStop)
|
||||
|> filter(fn: (r) => r["_field"] == "duration")
|
||||
|> filter(fn: (r) => r["_measurement"] == "poseidon_/execute" or r["_measurement"] == "poseidon_/files" or r["_measurement"] == "poseidon_/websocket")
|
||||
|> filter(fn: (r) => contains(value: r["environment_id"], set: ${environment_ids:json}))
|
||||
|> filter(fn: (r) => exists r.environment_id)
|
||||
|> keep(columns: ["_value", "runner_id", "environment_id", "stage"])
|
||||
|> group(columns: ["environment_id", "stage"])
|
||||
|> mean()
|
||||
|> map(fn: (r) => ({r with _value: r._value * 3.0})) // Each execution has three requests
|
@ -1,11 +0,0 @@
|
||||
import "strings"
|
||||
import "date"
|
||||
|
||||
result = from(bucket: "poseidon")
|
||||
|> range(start: date.truncate(t: v.timeRangeStart, unit: 1m), stop: date.truncate(t: v.timeRangeStop, unit: 1m))
|
||||
|> filter(fn: (r) => r["_measurement"] == "poseidon_aws_executions" or r["_measurement"] == "poseidon_nomad_executions")
|
||||
|> filter(fn: (r) => contains(value: r["environment_id"], set: ${environment_ids:json}))
|
||||
|> filter(fn: (r) => r["event_type"] == "creation")
|
||||
|> group(columns: ["environment_id", "stage"], mode:"by")
|
||||
|> aggregateWindow(every: 1m, fn: count, createEmpty: true)
|
||||
|> aggregateWindow(every: duration(v: int(v: v.windowPeriod) * 8), fn: mean, createEmpty: true)
|
@ -1,12 +0,0 @@
|
||||
import "date"
|
||||
import "strings"
|
||||
|
||||
result = from(bucket: "poseidon")
|
||||
|> range(start: date.truncate(t: v.timeRangeStart, unit: 1m), stop: date.truncate(t: v.timeRangeStop, unit: 1m))
|
||||
|> filter(fn: (r) => r["_measurement"] == "poseidon_aws_executions" or r["_measurement"] == "poseidon_nomad_executions")
|
||||
|> filter(fn: (r) => contains(value: r["environment_id"], set: ${environment_ids:json}))
|
||||
|> filter(fn: (r) => r["event_type"] == "creation")
|
||||
|> group(columns: ["environment_id", "stage"], mode:"by")
|
||||
|> aggregateWindow(every: 1m, fn: count, createEmpty: true)
|
||||
|> keep(columns: ["_value", "environment_id", "stage"])
|
||||
|> mean()
|
@ -1,21 +0,0 @@
|
||||
import "strings"
|
||||
|
||||
data = from(bucket: "poseidon")
|
||||
|> range(start: v.timeRangeStart, stop: v.timeRangeStop)
|
||||
|
||||
runner_deletions = data
|
||||
|> filter(fn: (r) => r["_measurement"] == "poseidon_used_runners")
|
||||
|> filter(fn: (r) => r["event_type"] == "deletion")
|
||||
|> keep(columns: ["_time", "id", "stage"])
|
||||
|> rename(columns: {id: "runner_id"})
|
||||
|
||||
executions = data
|
||||
|> filter(fn: (r) => r["_measurement"] == "poseidon_nomad_executions" or r["_measurement"] == "poseidon_aws_executions")
|
||||
|> filter(fn: (r) => r["event_type"] == "creation")
|
||||
|> filter(fn: (r) => contains(value: r["environment_id"], set: ${environment_ids:json}))
|
||||
|> keep(columns: ["_value", "environment_id", "runner_id"])
|
||||
|> count()
|
||||
|
||||
result = join(tables: {key1: executions, key2: runner_deletions}, on: ["runner_id"], method: "inner")
|
||||
|> keep(columns: ["_value", "_time", "environment_id", "stage"])
|
||||
|> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)
|
@ -1,21 +0,0 @@
|
||||
import "strings"
|
||||
|
||||
data = from(bucket: "poseidon")
|
||||
|> range(start: v.timeRangeStart, stop: v.timeRangeStop)
|
||||
|
||||
runner_deletions = data
|
||||
|> filter(fn: (r) => r["_measurement"] == "poseidon_used_runners")
|
||||
|> filter(fn: (r) => r["event_type"] == "deletion")
|
||||
|> keep(columns: ["id", "stage"])
|
||||
|> rename(columns: {id: "runner_id"})
|
||||
|
||||
executions = data
|
||||
|> filter(fn: (r) => r["_measurement"] == "poseidon_nomad_executions" or r["_measurement"] == "poseidon_aws_executions")
|
||||
|> filter(fn: (r) => r["event_type"] == "creation")
|
||||
|> filter(fn: (r) => contains(value: r["environment_id"], set: ${environment_ids:json}))
|
||||
|> keep(columns: ["_value", "environment_id", "runner_id"])
|
||||
|> count()
|
||||
|
||||
result = join(tables: {key1: executions, key2: runner_deletions}, on: ["runner_id"], method: "inner")
|
||||
|> keep(columns: ["_value", "environment_id", "stage"])
|
||||
|> mean()
|
@ -1,12 +0,0 @@
|
||||
import "strings"
|
||||
|
||||
myWindowPeriod = if int(v: v.windowPeriod) > int(v: 1m) then duration(v: int(v: v.windowPeriod) * 10) else duration(v: int(v: v.windowPeriod) * 5)
|
||||
data = from(bucket: "poseidon")
|
||||
|> range(start: v.timeRangeStart, stop: v.timeRangeStop)
|
||||
|> filter(fn: (r) => r["_measurement"] == "poseidon_file_download")
|
||||
|> filter(fn: (r) => contains(value: r["environment_id"], set: ${environment_ids:json}))
|
||||
|
||||
actual = data |> filter(fn: (r) => r["_field"] == "actual_length")
|
||||
expected = data |> filter(fn: (r) => r["_field"] == "expected_length")
|
||||
result = join(tables: {key1: actual, key2: expected}, on: ["_time", "environment_id", "runner_id", "stage"], method: "inner")
|
||||
|> map(fn: (r) => ({ _value: if r._value_key2 == 0 then 1.0 else float(v: r._value_key1) / float(v: r._value_key2), environment_id: r.environment_id, runner_id: r.runner_id, stage: r.stage }))
|
@ -1,9 +0,0 @@
|
||||
import "strings"
|
||||
|
||||
myWindowPeriod = if int(v: v.windowPeriod) > int(v: 1m) then duration(v: int(v: v.windowPeriod) * 100) else duration(v: int(v: v.windowPeriod) * 5)
|
||||
result = from(bucket: "poseidon")
|
||||
|> range(start: v.timeRangeStart, stop: v.timeRangeStop)
|
||||
|> filter(fn: (r) => r["_measurement"] == "poseidon_file_download")
|
||||
|> filter(fn: (r) => r["_field"] == "actual_length")
|
||||
|> filter(fn: (r) => contains(value: r["environment_id"], set: ${environment_ids:json}))
|
||||
|> keep(columns: ["_time", "_value", "environment_id", "stage"])
|
@ -1,9 +0,0 @@
|
||||
import "strings"
|
||||
|
||||
myWindowPeriod = if int(v: v.windowPeriod) > int(v: 1m) then duration(v: int(v: v.windowPeriod) * 10) else duration(v: int(v: v.windowPeriod) * 5)
|
||||
result = from(bucket: "poseidon")
|
||||
|> range(start: v.timeRangeStart, stop: v.timeRangeStop)
|
||||
|> filter(fn: (r) => r["_field"] == "request_size")
|
||||
|> filter(fn: (r) => contains(value: r["environment_id"], set: ${environment_ids:json}))
|
||||
|> keep(columns: ["_time", "_value", "environment_id", "stage"])
|
||||
|> aggregateWindow(every: myWindowPeriod, fn: mean, createEmpty: false)
|
@ -1,9 +0,0 @@
|
||||
import "strings"
|
||||
|
||||
myWindowPeriod = if int(v: v.windowPeriod) >= int(v: 30s) then duration(v: int(v: v.windowPeriod) * 5) else v.windowPeriod
|
||||
result = from(bucket: "poseidon")
|
||||
|> range(start: v.timeRangeStart, stop: v.timeRangeStop)
|
||||
|> filter(fn: (r) => r["_measurement"] == "poseidon_nomad_idle_runners" and r["_field"] == "count")
|
||||
|> filter(fn: (r) => contains(value: r["environment_id"], set: ${environment_ids:json}))
|
||||
|> keep(columns: ["_value", "_time", "environment_id", "stage"])
|
||||
|> aggregateWindow(every: myWindowPeriod, fn: min, createEmpty: false)
|
@ -1,10 +0,0 @@
|
||||
import "strings"
|
||||
|
||||
result = from(bucket: "poseidon")
|
||||
|> range(start: v.timeRangeStart, stop: v.timeRangeStop)
|
||||
|> filter(fn: (r) => r["_measurement"] == "poseidon_aws_executions" or r["_measurement"] == "poseidon_nomad_executions")
|
||||
|> filter(fn: (r) => contains(value: r["environment_id"], set: ${environment_ids:json}))
|
||||
|> filter(fn: (r) => r["event_type"] == "creation")
|
||||
|> group(columns: ["environment_id", "stage"], mode:"by")
|
||||
|> count()
|
||||
|> keep(columns: ["_value", "environment_id", "stage"])
|
@ -1,9 +0,0 @@
|
||||
import "strings"
|
||||
|
||||
result = from(bucket: "poseidon")
|
||||
|> range(start: -1y)
|
||||
|> filter(fn: (r) => r["_measurement"] == "poseidon_poolsize")
|
||||
|> filter(fn: (r) => contains(value: r["environment_id"], set: ${environment_ids:json}))
|
||||
|> group(columns: ["environment_id", "stage"], mode:"by")
|
||||
|> last()
|
||||
|> keep(columns: ["_value", "environment_id", "stage"])
|
@ -1,6 +0,0 @@
|
||||
from(bucket: "poseidon")
|
||||
|> range(start: v.timeRangeStart, stop: v.timeRangeStop)
|
||||
|> filter(fn: (r) => r["_field"] == "duration")
|
||||
|> filter(fn: (r) => (not exists r.environment_id) or contains(value: r["environment_id"], set: ${environment_ids:json}))
|
||||
|> keep(columns: ["_time", "_value"])
|
||||
|> aggregateWindow(every: v.windowPeriod, fn: mean)
|
@ -1,16 +0,0 @@
|
||||
import "date"
|
||||
|
||||
data = from(bucket: "poseidon")
|
||||
|> range(start: date.truncate(t: v.timeRangeStart, unit: 1m), stop: date.truncate(t: v.timeRangeStop, unit: 1m))
|
||||
|> filter(fn: (r) => r._field == "duration")
|
||||
|> filter(fn: (r) => (not exists r.environment_id) or contains(value: r["environment_id"], set: ${environment_ids:json}))
|
||||
|> keep(columns: ["_time", "_value", "status"])
|
||||
|
||||
all = data |> set(key: "status", value: "all")
|
||||
|
||||
result = union(tables: [data, all])
|
||||
|> aggregateWindow(every: 1m, fn: count, createEmpty: true)
|
||||
|
||||
if int(v: v.windowPeriod) > int(v: 1m)
|
||||
then result |> aggregateWindow(every: duration(v: int(v: v.windowPeriod) * 2), fn: mean, createEmpty: true)
|
||||
else result |> aggregateWindow(every: duration(v: int(v: v.windowPeriod) * 5), fn: mean, createEmpty: false)
|
@ -1,13 +0,0 @@
|
||||
import "strings"
|
||||
import "date"
|
||||
|
||||
myWindowPeriod = if int(v: v.windowPeriod) > int(v: 2m) then duration(v: int(v: v.windowPeriod) * 30) else duration(v: int(v: v.windowPeriod) * 15)
|
||||
result = from(bucket: "poseidon")
|
||||
|> range(start: date.truncate(t: v.timeRangeStart, unit: 1m), stop: date.truncate(t: v.timeRangeStop, unit: 1m))
|
||||
|> filter(fn: (r) => r["_measurement"] == "poseidon_used_runners")
|
||||
|> filter(fn: (r) => contains(value: r["environment_id"], set: ${environment_ids:json}))
|
||||
|> filter(fn: (r) => r["event_type"] == "creation")
|
||||
|> group(columns: ["environment_id", "stage"], mode:"by")
|
||||
|> aggregateWindow(every: 1m, fn: count, createEmpty: true)
|
||||
|> keep(columns: ["_value", "_time", "environment_id", "stage"])
|
||||
|> aggregateWindow(every: myWindowPeriod, fn: mean, createEmpty: true)
|
@ -1,9 +0,0 @@
|
||||
import "strings"
|
||||
|
||||
result = from(bucket: "poseidon")
|
||||
|> range(start: v.timeRangeStart, stop: v.timeRangeStop)
|
||||
|> filter(fn: (r) => r["_measurement"] == "poseidon_nomad_idle_runners")
|
||||
|> filter(fn: (r) => r["_field"] == "startup_duration")
|
||||
|> filter(fn: (r) => contains(value: r["environment_id"], set: ${environment_ids:json}))
|
||||
|> keep(columns: ["_value", "_time", "environment_id", "stage"])
|
||||
|> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)
|
@ -1,6 +0,0 @@
|
||||
from(bucket: "poseidon")
|
||||
|> range(start: v.timeRangeStart, stop: v.timeRangeStop)
|
||||
|> filter(fn: (r) => r["_field"] == "duration")
|
||||
|> filter(fn: (r) => (not exists r.environment_id) or contains(value: r["environment_id"], set: ${environment_ids:json}))
|
||||
|> keep(columns: ["_time", "_value", "_measurement"])
|
||||
|> aggregateWindow(every: duration(v: int(v: v.windowPeriod) * 10), fn: (tables=<-, column) => tables |> quantile(q: 0.999))
|
@ -1,7 +0,0 @@
|
||||
from(bucket: "poseidon")
|
||||
|> range(start: v.timeRangeStart, stop: v.timeRangeStop)
|
||||
|> filter(fn: (r) => r["_measurement"] == "poseidon_used_runners")
|
||||
|> filter(fn: (r) => r["_field"] == "count")
|
||||
|> group(columns: ["stage"], mode:"by")
|
||||
|> keep(columns: ["_value", "_time", "stage"])
|
||||
|> aggregateWindow(every: duration(v: int(v: v.windowPeriod) * 5), fn: mean, createEmpty: false)
|
@ -1,41 +0,0 @@
|
||||
from utils.utils import deep_update_dict
|
||||
import json
|
||||
|
||||
|
||||
def color_mapping(name, color):
|
||||
return {
|
||||
"fieldConfig": {
|
||||
"overrides": [{
|
||||
"matcher": {
|
||||
"id": "byName",
|
||||
"options": name
|
||||
},
|
||||
"properties": [{
|
||||
"id": "color",
|
||||
"value": {
|
||||
"fixedColor": color,
|
||||
"mode": "fixed"
|
||||
}
|
||||
}]
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
grey_all_mapping = color_mapping("all", "#4c4b5a")
|
||||
color_mapping_environments = {}
|
||||
colours = [
|
||||
"#E41A1C", "#377EB8", "#4DAF4A", "#984EA3", "#FF7F00", "#FFFF33", "#A65628", "#F781BF", "#999999", # R ColorBrewer Set1
|
||||
"#8DD3C7", "#FFFFB3", "#BEBADA", "#FB8072", "#80B1D3", "#FDB462", "#B3DE69", "#FCCDE5", "#D9D9D9", "#BC80BD", "#CCEBC5", "#FFED6F" # R ColorBrewer Set3
|
||||
]
|
||||
|
||||
with open("environments.json") as f:
|
||||
environments = json.load(f)
|
||||
|
||||
environment_identifier = []
|
||||
for environment in environments["executionEnvironments"]:
|
||||
environment_identifier.append(str(environment["id"]) + "/" + environment["image"].removeprefix("openhpi/co_execenv_"))
|
||||
|
||||
environment_identifier.sort()
|
||||
for environment in environment_identifier:
|
||||
deep_update_dict(color_mapping_environments, color_mapping(environment, colours.pop(0)))
|
@ -1,26 +0,0 @@
|
||||
def read_query(*names):
|
||||
result = ""
|
||||
for name in names:
|
||||
with open("queries/" + name + ".flux", "r") as file:
|
||||
result += file.read()
|
||||
result += "\n"
|
||||
return result
|
||||
|
||||
|
||||
def deep_update_dict(base_dict, extra_dict):
|
||||
if extra_dict is None:
|
||||
return base_dict
|
||||
|
||||
for k, v in extra_dict.items():
|
||||
update_dict_entry(base_dict, k, v)
|
||||
|
||||
|
||||
def update_dict_entry(base_dict, k, v):
|
||||
if k in base_dict and hasattr(base_dict[k], "to_json_data"):
|
||||
base_dict[k] = base_dict[k].to_json_data()
|
||||
if k in base_dict and isinstance(base_dict[k], dict):
|
||||
deep_update_dict(base_dict[k], v)
|
||||
elif k in base_dict and isinstance(base_dict[k], list):
|
||||
base_dict[k].extend(v)
|
||||
else:
|
||||
base_dict[k] = v
|
@ -1,14 +0,0 @@
|
||||
from grafanalib.core import Template
|
||||
|
||||
from utils.utils import read_query
|
||||
|
||||
environment_variable = Template(
|
||||
dataSource="Flux",
|
||||
label="Environment IDs",
|
||||
name="environment_ids",
|
||||
query=read_query("environment-ids"),
|
||||
refresh=1,
|
||||
includeAll=True,
|
||||
multi=True,
|
||||
default="$__all",
|
||||
)
|
@ -1,36 +0,0 @@
|
||||
# Simple image containing the Nomad binary to deploy Nomad jobs
|
||||
|
||||
FROM golang:latest
|
||||
|
||||
# Install prerequisites, gettext contains envsubst used in the CI
|
||||
RUN apt-get update && \
|
||||
apt install -y \
|
||||
unzip \
|
||||
wget \
|
||||
gettext \
|
||||
apt-transport-https \
|
||||
ca-certificates \
|
||||
curl \
|
||||
gnupg \
|
||||
lsb-release && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists
|
||||
|
||||
RUN curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
|
||||
RUN echo \
|
||||
"deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian \
|
||||
$(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null
|
||||
RUN apt-get update && \
|
||||
apt-get install -y docker-ce docker-ce-cli containerd.io && \
|
||||
rm -rf /var/lib/apt/lists
|
||||
|
||||
ENV NOMAD_VERSION="1.1.2"
|
||||
|
||||
# Download Nomad
|
||||
RUN wget "https://releases.hashicorp.com/nomad/${NOMAD_VERSION}/nomad_${NOMAD_VERSION}_linux_amd64.zip" && \
|
||||
wget "https://releases.hashicorp.com/nomad/${NOMAD_VERSION}/nomad_${NOMAD_VERSION}_SHA256SUMS" && \
|
||||
grep "nomad_${NOMAD_VERSION}_linux_amd64.zip" nomad_${NOMAD_VERSION}_SHA256SUMS | sha256sum -c - && \
|
||||
unzip nomad_${NOMAD_VERSION}_linux_amd64.zip
|
||||
|
||||
# Install Nomad
|
||||
RUN mv nomad /usr/sbin/ && nomad -version
|
@ -1,21 +0,0 @@
|
||||
# Nomad-in-Docker Image
|
||||
|
||||
The [`Dockerfile`](Dockerfile) in this folder creates a Docker image that contains Docker and Nomad.
|
||||
|
||||
Running the image requires the following Docker options:
|
||||
|
||||
- Allow Nomad to use mount: `--cap-add=SYS_ADMIN`
|
||||
- Allow Nomad to use bind mounts: `--security-opt apparmor=unconfined`
|
||||
- Add access to Docker daemon: `-v /var/run/docker.sock:/var/run/docker.sock`
|
||||
- Map port to host: `-p 4646:4646`
|
||||
|
||||
A complete command to run the container is as follows.
|
||||
|
||||
```shell
|
||||
docker run --rm --name nomad \
|
||||
--cap-add=SYS_ADMIN \
|
||||
--security-opt apparmor=unconfined \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-p 4646:4646 \
|
||||
nomad-ci
|
||||
```
|
@ -1,14 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# This script substitutes environment variables in the given Nomad job and runs it afterwards.
|
||||
|
||||
if [[ "$#" -ne 2 ]]; then
|
||||
echo "Usage: $0 path/to/infile path/to/outfile"
|
||||
fi
|
||||
|
||||
envsubst "$(env | sed -e 's/=.*//' -e 's/^/\$/g')" < $1 > $2
|
||||
nomad validate $2
|
||||
# nomad plan returns 1 if allocations are created or destroyed which is what we want here
|
||||
# https://www.nomadproject.io/docs/commands/job/plan#usage
|
||||
nomad plan $2 || [ $? == 1 ]
|
||||
nomad run $2
|
@ -1,8 +0,0 @@
|
||||
FROM alpine:latest
|
||||
|
||||
RUN adduser --disabled-password api
|
||||
USER api
|
||||
COPY poseidon /home/api/
|
||||
|
||||
EXPOSE 7200
|
||||
CMD ["/home/api/poseidon"]
|
@ -5,7 +5,8 @@ services:
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "7200:7200"
|
||||
network_mode: host
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
volumes:
|
||||
- ./configuration.yaml:/go/src/app/configuration.yaml
|
||||
restart: unless-stopped
|
||||
|
35
go.mod
35
go.mod
@ -15,7 +15,7 @@ require (
|
||||
github.com/shirou/gopsutil/v3 v3.24.5
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/stretchr/testify v1.9.0
|
||||
golang.org/x/sys v0.22.0
|
||||
golang.org/x/sys v0.25.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
@ -35,14 +35,23 @@ require (
|
||||
github.com/bmatcuk/doublestar v1.3.4 // indirect
|
||||
github.com/cenkalti/backoff/v3 v3.2.2 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
|
||||
github.com/fatih/color v1.17.0 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
|
||||
github.com/go-jose/go-jose/v3 v3.0.3 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.0.2 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.19.6 // indirect
|
||||
github.com/go-openapi/jsonreference v0.20.2 // indirect
|
||||
github.com/go-openapi/swag v0.22.4 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/gojuno/minimock/v3 v3.3.13 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/google/btree v1.1.2 // indirect
|
||||
github.com/google/gnostic-models v0.6.8 // indirect
|
||||
github.com/google/go-cmp v0.6.0 // indirect
|
||||
github.com/google/gofuzz v1.2.0 // indirect
|
||||
github.com/hashicorp/consul/api v1.29.1 // indirect
|
||||
github.com/hashicorp/cronexpr v1.1.2 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
@ -73,8 +82,12 @@ require (
|
||||
github.com/hashicorp/serf v0.10.2-0.20240320153621-5d32001edfaa // indirect
|
||||
github.com/hashicorp/vault/api v1.14.0 // indirect
|
||||
github.com/hashicorp/yamux v0.1.1 // indirect
|
||||
github.com/imdario/mergo v0.3.13 // indirect
|
||||
github.com/influxdata/line-protocol v0.0.0-20210922203350-b1ad95c89adf // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/miekg/dns v1.1.61 // indirect
|
||||
@ -85,16 +98,22 @@ require (
|
||||
github.com/mitchellh/hashstructure v1.1.0 // indirect
|
||||
github.com/mitchellh/pointerstructure v1.2.1 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/oapi-codegen/runtime v1.1.1 // indirect
|
||||
github.com/oklog/run v1.1.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||
github.com/ryanuber/go-glob v1.0.0 // indirect
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 // indirect
|
||||
github.com/shoenig/go-m1cpu v0.1.6 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.14 // indirect
|
||||
github.com/tklauser/numcpus v0.8.0 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
github.com/zclconf/go-cty v1.14.4 // indirect
|
||||
github.com/zclconf/go-cty-yaml v1.0.3 // indirect
|
||||
@ -102,12 +121,26 @@ require (
|
||||
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect
|
||||
golang.org/x/mod v0.18.0 // indirect
|
||||
golang.org/x/net v0.26.0 // indirect
|
||||
golang.org/x/oauth2 v0.21.0 // indirect
|
||||
golang.org/x/sync v0.7.0 // indirect
|
||||
golang.org/x/term v0.21.0 // indirect
|
||||
golang.org/x/text v0.16.0 // indirect
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
golang.org/x/tools v0.22.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240624140628-dc46fd24d27d // indirect
|
||||
google.golang.org/grpc v1.64.1 // indirect
|
||||
google.golang.org/protobuf v1.34.2 // indirect
|
||||
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
k8s.io/api v0.31.1 // indirect
|
||||
k8s.io/apimachinery v0.31.1 // indirect
|
||||
k8s.io/client-go v0.31.1 // indirect
|
||||
k8s.io/klog/v2 v2.130.1 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect
|
||||
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 // indirect
|
||||
oss.indeed.com/go/libtime v1.6.0 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
|
||||
sigs.k8s.io/yaml v1.4.0 // indirect
|
||||
)
|
||||
|
91
go.sum
91
go.sum
@ -31,18 +31,23 @@ github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6D
|
||||
github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
|
||||
github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
||||
github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
|
||||
github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
|
||||
github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk=
|
||||
github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
|
||||
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
|
||||
github.com/getsentry/sentry-go v0.28.1 h1:zzaSm/vHmGllRM6Tpx1492r0YDzauArdBfkJRtY6P5k=
|
||||
github.com/getsentry/sentry-go v0.28.1/go.mod h1:1fQZ+7l7eeJ3wYi82q5Hg8GqAPgefRq+FP/QhafYVgg=
|
||||
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
|
||||
@ -55,14 +60,25 @@ github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2
|
||||
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||
github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE=
|
||||
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
|
||||
github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
|
||||
github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
|
||||
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
|
||||
github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU=
|
||||
github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
|
||||
github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/gojuno/minimock/v3 v3.0.4/go.mod h1:HqeqnwV8mAABn3pO5hqF+RE7gjA0jsN8cbbSogoGrzI=
|
||||
github.com/gojuno/minimock/v3 v3.0.6/go.mod h1:v61ZjAKHr+WnEkND63nQPCZ/DTfQgJdvbCi3IuoMblY=
|
||||
github.com/gojuno/minimock/v3 v3.3.13 h1:sXFO7RbB4JnZiKhgMO4BU4RLYcfhcOSepfiv4wPgGNY=
|
||||
@ -75,12 +91,16 @@ github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
|
||||
github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
|
||||
github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I=
|
||||
github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
@ -170,19 +190,28 @@ github.com/hashicorp/vault/api v1.14.0/go.mod h1:pV9YLxBGSz+cItFDd8Ii4G17waWOQ32
|
||||
github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE=
|
||||
github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
|
||||
github.com/hexdigest/gowrap v1.1.7/go.mod h1:Z+nBFUDLa01iaNM+/jzoOA1JJ7sm51rnYFauKFUB5fs=
|
||||
github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk=
|
||||
github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg=
|
||||
github.com/influxdata/influxdb-client-go/v2 v2.13.0 h1:ioBbLmR5NMbAjP4UVA5r9b5xGjpABD7j65pI8kFphDM=
|
||||
github.com/influxdata/influxdb-client-go/v2 v2.13.0/go.mod h1:k+spCbt9hcvqvUiz0sr5D8LolXHqAAOfPw9v/RIRHl4=
|
||||
github.com/influxdata/line-protocol v0.0.0-20210922203350-b1ad95c89adf h1:7JTmneyiNEwVBOHSjoMxiWAqB992atOeepeFYegn5RU=
|
||||
github.com/influxdata/line-protocol v0.0.0-20210922203350-b1ad95c89adf/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo=
|
||||
github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c=
|
||||
github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
@ -191,6 +220,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae h1:dIZY4ULFcto4tAFlj1FYZl8ztUZ13bdq+PLY+NOfbyI=
|
||||
github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
@ -224,9 +255,14 @@ github.com/mitchellh/pointerstructure v1.2.1/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8oh
|
||||
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
|
||||
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/oapi-codegen/runtime v1.1.1 h1:EXLHh0DXIJnWhdRPN2w4MXAzFyE4CskzhNLUmtpMYro=
|
||||
github.com/oapi-codegen/runtime v1.1.1/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg=
|
||||
@ -273,9 +309,13 @@ github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPx
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
@ -283,7 +323,10 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU=
|
||||
@ -292,6 +335,10 @@ github.com/tklauser/numcpus v0.8.0 h1:Mx4Wwe/FjZLeQsK/6kt2EOepwwSl7SmJrK5bV/dXYg
|
||||
github.com/tklauser/numcpus v0.8.0/go.mod h1:ZJZlAY+dmR4eut8epnzf0u/VwodKmryxR8txiloSqBE=
|
||||
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
|
||||
github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
@ -307,7 +354,9 @@ go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
|
||||
go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200422194213-44a606286825/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
|
||||
@ -315,6 +364,8 @@ golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5D
|
||||
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY=
|
||||
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
|
||||
@ -324,17 +375,22 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
|
||||
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
|
||||
golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs=
|
||||
golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||
@ -351,6 +407,7 @@ golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@ -368,11 +425,15 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
||||
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
|
||||
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA=
|
||||
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
@ -389,12 +450,16 @@ golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3
|
||||
golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
|
||||
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240624140628-dc46fd24d27d h1:k3zyW3BYYR30e8v3x0bTDdE9vpYFjZHK+HcyqkrppWk=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240624140628-dc46fd24d27d/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY=
|
||||
@ -408,12 +473,38 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4=
|
||||
gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
|
||||
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
k8s.io/api v0.31.1 h1:Xe1hX/fPW3PXYYv8BlozYqw63ytA92snr96zMW9gWTU=
|
||||
k8s.io/api v0.31.1/go.mod h1:sbN1g6eY6XVLeqNsZGLnI5FwVseTrZX7Fv3O26rhAaI=
|
||||
k8s.io/apimachinery v0.31.1 h1:mhcUBbj7KUjaVhyXILglcVjuS4nYXiwC+KKFBgIVy7U=
|
||||
k8s.io/apimachinery v0.31.1/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo=
|
||||
k8s.io/client-go v0.31.1 h1:f0ugtWSbWpxHR7sjVpQwuvw9a3ZKLXX0u0itkFXufb0=
|
||||
k8s.io/client-go v0.31.1/go.mod h1:sKI8871MJN2OyeqRlmA4W4KM9KBdBUpDLu/43eGemCg=
|
||||
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
|
||||
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
|
||||
k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag=
|
||||
k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98=
|
||||
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A=
|
||||
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||
oss.indeed.com/go/libtime v1.6.0 h1:XQyczJihse/wQGo59OfPF3f4f+Sywv4R8vdGB3S9BfU=
|
||||
oss.indeed.com/go/libtime v1.6.0/go.mod h1:B2sdEcuzB0zhTKkAuHy4JInKRc7Al3tME4qWam6R7mA=
|
||||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=
|
||||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4=
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08=
|
||||
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
|
||||
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
|
||||
|
@ -1,85 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/openHPI/poseidon/internal/config"
|
||||
"github.com/openHPI/poseidon/internal/environment"
|
||||
"github.com/openHPI/poseidon/pkg/dto"
|
||||
"github.com/openHPI/poseidon/tests"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func mockHTTPHandler(writer http.ResponseWriter, _ *http.Request) {
|
||||
writer.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
type MainTestSuite struct {
|
||||
tests.MemoryLeakTestSuite
|
||||
}
|
||||
|
||||
func TestMainTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(MainTestSuite))
|
||||
}
|
||||
|
||||
func (s *MainTestSuite) TestNewRouterV1WithAuthenticationDisabled() {
|
||||
config.Config.Server.Token = ""
|
||||
router := mux.NewRouter()
|
||||
m := &environment.ManagerHandlerMock{}
|
||||
m.On("Statistics").Return(make(map[dto.EnvironmentID]*dto.StatisticalExecutionEnvironmentData))
|
||||
configureV1Router(router, nil, m)
|
||||
|
||||
s.Run("health route is accessible", func() {
|
||||
request, err := http.NewRequest(http.MethodGet, "/api/v1/health", http.NoBody)
|
||||
if err != nil {
|
||||
s.T().Fatal(err)
|
||||
}
|
||||
recorder := httptest.NewRecorder()
|
||||
router.ServeHTTP(recorder, request)
|
||||
s.Equal(http.StatusNoContent, recorder.Code)
|
||||
})
|
||||
|
||||
s.Run("added route is accessible", func() {
|
||||
router.HandleFunc("/api/v1/test", mockHTTPHandler)
|
||||
request, err := http.NewRequest(http.MethodGet, "/api/v1/test", http.NoBody)
|
||||
if err != nil {
|
||||
s.T().Fatal(err)
|
||||
}
|
||||
recorder := httptest.NewRecorder()
|
||||
router.ServeHTTP(recorder, request)
|
||||
s.Equal(http.StatusOK, recorder.Code)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MainTestSuite) TestNewRouterV1WithAuthenticationEnabled() {
|
||||
config.Config.Server.Token = "TestToken"
|
||||
router := mux.NewRouter()
|
||||
m := &environment.ManagerHandlerMock{}
|
||||
m.On("Statistics").Return(make(map[dto.EnvironmentID]*dto.StatisticalExecutionEnvironmentData))
|
||||
configureV1Router(router, nil, m)
|
||||
|
||||
s.Run("health route is accessible", func() {
|
||||
request, err := http.NewRequest(http.MethodGet, "/api/v1/health", http.NoBody)
|
||||
if err != nil {
|
||||
s.T().Fatal(err)
|
||||
}
|
||||
recorder := httptest.NewRecorder()
|
||||
router.ServeHTTP(recorder, request)
|
||||
s.Equal(http.StatusNoContent, recorder.Code)
|
||||
})
|
||||
|
||||
s.Run("protected route is not accessible", func() {
|
||||
// request an available API route that should be guarded by authentication.
|
||||
// (which one, in particular, does not matter here)
|
||||
request, err := http.NewRequest(http.MethodPost, "/api/v1/runners", http.NoBody)
|
||||
if err != nil {
|
||||
s.T().Fatal(err)
|
||||
}
|
||||
recorder := httptest.NewRecorder()
|
||||
router.ServeHTTP(recorder, request)
|
||||
s.Equal(http.StatusUnauthorized, recorder.Code)
|
||||
})
|
||||
config.Config.Server.Token = ""
|
||||
}
|
@ -1,94 +0,0 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"github.com/openHPI/poseidon/internal/config"
|
||||
"github.com/openHPI/poseidon/tests"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/sirupsen/logrus/hooks/test"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const testToken = "C0rr3ctT0k3n"
|
||||
|
||||
type AuthenticationMiddlewareTestSuite struct {
|
||||
tests.MemoryLeakTestSuite
|
||||
request *http.Request
|
||||
recorder *httptest.ResponseRecorder
|
||||
httpAuthenticationMiddleware http.Handler
|
||||
}
|
||||
|
||||
func (s *AuthenticationMiddlewareTestSuite) SetupTest() {
|
||||
s.MemoryLeakTestSuite.SetupTest()
|
||||
correctAuthenticationToken = []byte(testToken)
|
||||
s.recorder = httptest.NewRecorder()
|
||||
request, err := http.NewRequest(http.MethodGet, "/api/v1/test", http.NoBody)
|
||||
if err != nil {
|
||||
s.T().Fatal(err)
|
||||
}
|
||||
s.request = request
|
||||
s.httpAuthenticationMiddleware = HTTPAuthenticationMiddleware(
|
||||
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
}
|
||||
|
||||
func (s *AuthenticationMiddlewareTestSuite) TearDownTest() {
|
||||
defer s.MemoryLeakTestSuite.TearDownTest()
|
||||
correctAuthenticationToken = []byte(nil)
|
||||
}
|
||||
|
||||
func (s *AuthenticationMiddlewareTestSuite) TestReturns401WhenHeaderUnset() {
|
||||
s.httpAuthenticationMiddleware.ServeHTTP(s.recorder, s.request)
|
||||
assert.Equal(s.T(), http.StatusUnauthorized, s.recorder.Code)
|
||||
}
|
||||
|
||||
func (s *AuthenticationMiddlewareTestSuite) TestReturns401WhenTokenWrong() {
|
||||
s.request.Header.Set(TokenHeader, "Wr0ngT0k3n")
|
||||
s.httpAuthenticationMiddleware.ServeHTTP(s.recorder, s.request)
|
||||
assert.Equal(s.T(), http.StatusUnauthorized, s.recorder.Code)
|
||||
}
|
||||
|
||||
func (s *AuthenticationMiddlewareTestSuite) TestWarnsWhenUnauthorized() {
|
||||
var hook *test.Hook
|
||||
logger, hook := test.NewNullLogger()
|
||||
log = logger.WithField("pkg", "api/auth")
|
||||
|
||||
s.request.Header.Set(TokenHeader, "Wr0ngT0k3n")
|
||||
s.httpAuthenticationMiddleware.ServeHTTP(s.recorder, s.request)
|
||||
|
||||
assert.Equal(s.T(), http.StatusUnauthorized, s.recorder.Code)
|
||||
assert.Equal(s.T(), logrus.WarnLevel, hook.LastEntry().Level)
|
||||
assert.Equal(s.T(), hook.LastEntry().Data["token"], "Wr0ngT0k3n")
|
||||
}
|
||||
|
||||
func (s *AuthenticationMiddlewareTestSuite) TestPassesWhenTokenCorrect() {
|
||||
s.request.Header.Set(TokenHeader, testToken)
|
||||
s.httpAuthenticationMiddleware.ServeHTTP(s.recorder, s.request)
|
||||
|
||||
assert.Equal(s.T(), http.StatusOK, s.recorder.Code)
|
||||
}
|
||||
|
||||
func TestHTTPAuthenticationMiddleware(t *testing.T) {
|
||||
suite.Run(t, new(AuthenticationMiddlewareTestSuite))
|
||||
}
|
||||
|
||||
func TestInitializeAuthentication(t *testing.T) {
|
||||
t.Run("if token unset", func(t *testing.T) {
|
||||
config.Config.Server.Token = ""
|
||||
initialized := InitializeAuthentication()
|
||||
assert.Equal(t, false, initialized)
|
||||
assert.Equal(t, []byte(nil), correctAuthenticationToken, "it should not set correctAuthenticationToken")
|
||||
})
|
||||
t.Run("if token set", func(t *testing.T) {
|
||||
config.Config.Server.Token = testToken
|
||||
initialized := InitializeAuthentication()
|
||||
assert.Equal(t, true, initialized)
|
||||
assert.Equal(t, []byte(testToken), correctAuthenticationToken, "it should set correctAuthenticationToken")
|
||||
config.Config.Server.Token = ""
|
||||
correctAuthenticationToken = []byte(nil)
|
||||
})
|
||||
}
|
@ -1,308 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/openHPI/poseidon/internal/environment"
|
||||
"github.com/openHPI/poseidon/internal/nomad"
|
||||
"github.com/openHPI/poseidon/internal/runner"
|
||||
"github.com/openHPI/poseidon/pkg/dto"
|
||||
"github.com/openHPI/poseidon/tests"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"math"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const jobHCLBasicFormat = "job \"%s\" {}"
|
||||
|
||||
type EnvironmentControllerTestSuite struct {
|
||||
tests.MemoryLeakTestSuite
|
||||
manager *environment.ManagerHandlerMock
|
||||
router *mux.Router
|
||||
}
|
||||
|
||||
func TestEnvironmentControllerTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(EnvironmentControllerTestSuite))
|
||||
}
|
||||
|
||||
func (s *EnvironmentControllerTestSuite) SetupTest() {
|
||||
s.MemoryLeakTestSuite.SetupTest()
|
||||
s.manager = &environment.ManagerHandlerMock{}
|
||||
s.router = NewRouter(nil, s.manager)
|
||||
}
|
||||
|
||||
func (s *EnvironmentControllerTestSuite) TestList() {
|
||||
call := s.manager.On("List", mock.AnythingOfType("bool"))
|
||||
call.Run(func(args mock.Arguments) {
|
||||
call.ReturnArguments = mock.Arguments{[]runner.ExecutionEnvironment{}, nil}
|
||||
})
|
||||
path, err := s.router.Get(listRouteName).URL()
|
||||
s.Require().NoError(err)
|
||||
request, err := http.NewRequest(http.MethodGet, path.String(), http.NoBody)
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.Run("with no Environments", func() {
|
||||
recorder := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(recorder, request)
|
||||
s.Equal(http.StatusOK, recorder.Code)
|
||||
|
||||
var environmentsResponse ExecutionEnvironmentsResponse
|
||||
err = json.NewDecoder(recorder.Result().Body).Decode(&environmentsResponse)
|
||||
s.Require().NoError(err)
|
||||
_ = recorder.Result().Body.Close()
|
||||
|
||||
s.Empty(environmentsResponse.ExecutionEnvironments)
|
||||
})
|
||||
s.manager.Calls = []mock.Call{}
|
||||
|
||||
s.Run("with fetch", func() {
|
||||
recorder := httptest.NewRecorder()
|
||||
query := path.Query()
|
||||
query.Set("fetch", "true")
|
||||
path.RawQuery = query.Encode()
|
||||
request, err := http.NewRequest(http.MethodGet, path.String(), http.NoBody)
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.router.ServeHTTP(recorder, request)
|
||||
s.Equal(http.StatusOK, recorder.Code)
|
||||
s.manager.AssertCalled(s.T(), "List", true)
|
||||
})
|
||||
s.manager.Calls = []mock.Call{}
|
||||
|
||||
s.Run("with bad fetch", func() {
|
||||
recorder := httptest.NewRecorder()
|
||||
query := path.Query()
|
||||
query.Set("fetch", "YouDecide")
|
||||
path.RawQuery = query.Encode()
|
||||
request, err := http.NewRequest(http.MethodGet, path.String(), http.NoBody)
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.router.ServeHTTP(recorder, request)
|
||||
s.Equal(http.StatusBadRequest, recorder.Code)
|
||||
s.manager.AssertNotCalled(s.T(), "List")
|
||||
})
|
||||
|
||||
s.Run("returns multiple environments", func() {
|
||||
apiMock := &nomad.ExecutorAPIMock{}
|
||||
apiMock.On("LoadRunnerIDs", mock.AnythingOfType("string")).Return([]string{}, nil)
|
||||
apiMock.On("DeleteJob", mock.AnythingOfType("string")).Return(nil)
|
||||
|
||||
var firstEnvironment, secondEnvironment *environment.NomadEnvironment
|
||||
call.Run(func(args mock.Arguments) {
|
||||
firstEnvironment, err = environment.NewNomadEnvironment(tests.DefaultEnvironmentIDAsInteger, apiMock,
|
||||
fmt.Sprintf(jobHCLBasicFormat, nomad.TemplateJobID(tests.DefaultEnvironmentIDAsInteger)))
|
||||
s.Require().NoError(err)
|
||||
secondEnvironment, err = environment.NewNomadEnvironment(tests.DefaultEnvironmentIDAsInteger, apiMock,
|
||||
fmt.Sprintf(jobHCLBasicFormat, nomad.TemplateJobID(tests.DefaultEnvironmentIDAsInteger)))
|
||||
s.Require().NoError(err)
|
||||
call.ReturnArguments = mock.Arguments{[]runner.ExecutionEnvironment{firstEnvironment, secondEnvironment}, nil}
|
||||
})
|
||||
recorder := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(recorder, request)
|
||||
s.Equal(http.StatusOK, recorder.Code)
|
||||
|
||||
paramMap := make(map[string]interface{})
|
||||
err := json.NewDecoder(recorder.Result().Body).Decode(¶mMap)
|
||||
s.Require().NoError(err)
|
||||
environmentsInterface, ok := paramMap["executionEnvironments"]
|
||||
s.Require().True(ok)
|
||||
environments, ok := environmentsInterface.([]interface{})
|
||||
s.Require().True(ok)
|
||||
s.Equal(2, len(environments))
|
||||
|
||||
err = firstEnvironment.Delete(tests.ErrCleanupDestroyReason)
|
||||
s.NoError(err)
|
||||
err = secondEnvironment.Delete(tests.ErrCleanupDestroyReason)
|
||||
s.NoError(err)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *EnvironmentControllerTestSuite) TestGet() {
|
||||
call := s.manager.On("Get", mock.AnythingOfType("dto.EnvironmentID"), mock.AnythingOfType("bool"))
|
||||
path, err := s.router.Get(getRouteName).URL(executionEnvironmentIDKey, tests.DefaultEnvironmentIDAsString)
|
||||
s.Require().NoError(err)
|
||||
request, err := http.NewRequest(http.MethodGet, path.String(), http.NoBody)
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.Run("with unknown environment", func() {
|
||||
call.Run(func(args mock.Arguments) {
|
||||
call.ReturnArguments = mock.Arguments{nil, runner.ErrUnknownExecutionEnvironment}
|
||||
})
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(recorder, request)
|
||||
s.Equal(http.StatusNotFound, recorder.Code)
|
||||
s.manager.AssertCalled(s.T(), "Get", dto.EnvironmentID(0), false)
|
||||
})
|
||||
s.manager.Calls = []mock.Call{}
|
||||
|
||||
s.Run("not found with fetch", func() {
|
||||
recorder := httptest.NewRecorder()
|
||||
query := path.Query()
|
||||
query.Set("fetch", "true")
|
||||
path.RawQuery = query.Encode()
|
||||
request, err := http.NewRequest(http.MethodGet, path.String(), http.NoBody)
|
||||
s.Require().NoError(err)
|
||||
|
||||
call.Run(func(args mock.Arguments) {
|
||||
call.ReturnArguments = mock.Arguments{nil, runner.ErrUnknownExecutionEnvironment}
|
||||
})
|
||||
|
||||
s.router.ServeHTTP(recorder, request)
|
||||
s.Equal(http.StatusNotFound, recorder.Code)
|
||||
s.manager.AssertCalled(s.T(), "Get", dto.EnvironmentID(0), true)
|
||||
})
|
||||
s.manager.Calls = []mock.Call{}
|
||||
|
||||
s.Run("returns environment", func() {
|
||||
apiMock := &nomad.ExecutorAPIMock{}
|
||||
apiMock.On("LoadRunnerIDs", mock.AnythingOfType("string")).Return([]string{}, nil)
|
||||
apiMock.On("DeleteJob", mock.AnythingOfType("string")).Return(nil)
|
||||
|
||||
var testEnvironment *environment.NomadEnvironment
|
||||
call.Run(func(args mock.Arguments) {
|
||||
testEnvironment, err = environment.NewNomadEnvironment(tests.DefaultEnvironmentIDAsInteger, apiMock,
|
||||
fmt.Sprintf(jobHCLBasicFormat, nomad.TemplateJobID(tests.DefaultEnvironmentIDAsInteger)))
|
||||
s.Require().NoError(err)
|
||||
call.ReturnArguments = mock.Arguments{testEnvironment, nil}
|
||||
})
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(recorder, request)
|
||||
s.Equal(http.StatusOK, recorder.Code)
|
||||
|
||||
var environmentParams map[string]interface{}
|
||||
err := json.NewDecoder(recorder.Result().Body).Decode(&environmentParams)
|
||||
s.Require().NoError(err)
|
||||
idInterface, ok := environmentParams["id"]
|
||||
s.Require().True(ok)
|
||||
idFloat, ok := idInterface.(float64)
|
||||
s.Require().True(ok)
|
||||
s.Equal(tests.DefaultEnvironmentIDAsInteger, int(idFloat))
|
||||
|
||||
err = testEnvironment.Delete(tests.ErrCleanupDestroyReason)
|
||||
s.NoError(err)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *EnvironmentControllerTestSuite) TestDelete() {
|
||||
call := s.manager.On("Delete", mock.AnythingOfType("dto.EnvironmentID"))
|
||||
path, err := s.router.Get(deleteRouteName).URL(executionEnvironmentIDKey, tests.DefaultEnvironmentIDAsString)
|
||||
s.Require().NoError(err)
|
||||
request, err := http.NewRequest(http.MethodDelete, path.String(), http.NoBody)
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.Run("environment not found", func() {
|
||||
call.Run(func(args mock.Arguments) {
|
||||
call.ReturnArguments = mock.Arguments{false, nil}
|
||||
})
|
||||
recorder := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(recorder, request)
|
||||
s.Equal(http.StatusNotFound, recorder.Code)
|
||||
})
|
||||
|
||||
s.Run("environment deleted", func() {
|
||||
call.Run(func(args mock.Arguments) {
|
||||
call.ReturnArguments = mock.Arguments{true, nil}
|
||||
})
|
||||
recorder := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(recorder, request)
|
||||
s.Equal(http.StatusNoContent, recorder.Code)
|
||||
})
|
||||
|
||||
s.manager.Calls = []mock.Call{}
|
||||
s.Run("with bad environment id", func() {
|
||||
_, err := s.router.Get(deleteRouteName).URL(executionEnvironmentIDKey, "MagicNonNumberID")
|
||||
s.Error(err)
|
||||
})
|
||||
}
|
||||
|
||||
type CreateOrUpdateEnvironmentTestSuite struct {
|
||||
EnvironmentControllerTestSuite
|
||||
path string
|
||||
id dto.EnvironmentID
|
||||
body []byte
|
||||
}
|
||||
|
||||
func TestCreateOrUpdateEnvironmentTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(CreateOrUpdateEnvironmentTestSuite))
|
||||
}
|
||||
|
||||
func (s *CreateOrUpdateEnvironmentTestSuite) SetupTest() {
|
||||
s.EnvironmentControllerTestSuite.SetupTest()
|
||||
s.id = tests.DefaultEnvironmentIDAsInteger
|
||||
testURL, err := s.router.Get(createOrUpdateRouteName).URL(executionEnvironmentIDKey, strconv.Itoa(int(s.id)))
|
||||
if err != nil {
|
||||
s.T().Fatal(err)
|
||||
}
|
||||
s.path = testURL.String()
|
||||
s.body, err = json.Marshal(dto.ExecutionEnvironmentRequest{})
|
||||
if err != nil {
|
||||
s.T().Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *CreateOrUpdateEnvironmentTestSuite) recordRequest() *httptest.ResponseRecorder {
|
||||
recorder := httptest.NewRecorder()
|
||||
request, err := http.NewRequest(http.MethodPut, s.path, bytes.NewReader(s.body))
|
||||
if err != nil {
|
||||
s.T().Fatal(err)
|
||||
}
|
||||
s.router.ServeHTTP(recorder, request)
|
||||
return recorder
|
||||
}
|
||||
|
||||
func (s *CreateOrUpdateEnvironmentTestSuite) TestReturnsBadRequestWhenBadBody() {
|
||||
s.body = []byte{}
|
||||
recorder := s.recordRequest()
|
||||
s.Equal(http.StatusBadRequest, recorder.Code)
|
||||
}
|
||||
|
||||
func (s *CreateOrUpdateEnvironmentTestSuite) TestReturnsInternalServerErrorWhenManagerReturnsError() {
|
||||
testError := tests.ErrDefault
|
||||
s.manager.
|
||||
On("CreateOrUpdate", s.id, mock.AnythingOfType("dto.ExecutionEnvironmentRequest"), mock.Anything).
|
||||
Return(false, testError)
|
||||
|
||||
recorder := s.recordRequest()
|
||||
s.Equal(http.StatusInternalServerError, recorder.Code)
|
||||
s.Contains(recorder.Body.String(), testError.Error())
|
||||
}
|
||||
|
||||
func (s *CreateOrUpdateEnvironmentTestSuite) TestReturnsCreatedIfNewEnvironment() {
|
||||
s.manager.
|
||||
On("CreateOrUpdate", s.id, mock.AnythingOfType("dto.ExecutionEnvironmentRequest"), mock.Anything).
|
||||
Return(true, nil)
|
||||
|
||||
recorder := s.recordRequest()
|
||||
s.Equal(http.StatusCreated, recorder.Code)
|
||||
}
|
||||
|
||||
func (s *CreateOrUpdateEnvironmentTestSuite) TestReturnsNoContentIfNotNewEnvironment() {
|
||||
s.manager.
|
||||
On("CreateOrUpdate", s.id, mock.AnythingOfType("dto.ExecutionEnvironmentRequest"), mock.Anything).
|
||||
Return(false, nil)
|
||||
|
||||
recorder := s.recordRequest()
|
||||
s.Equal(http.StatusNoContent, recorder.Code)
|
||||
}
|
||||
|
||||
func (s *CreateOrUpdateEnvironmentTestSuite) TestReturnsNotFoundOnNonIntegerID() {
|
||||
s.path = strings.Join([]string{BasePath, EnvironmentsPath, "/", "invalid-id"}, "")
|
||||
recorder := s.recordRequest()
|
||||
s.Equal(http.StatusNotFound, recorder.Code)
|
||||
}
|
||||
|
||||
func (s *CreateOrUpdateEnvironmentTestSuite) TestFailsOnTooLargeID() {
|
||||
tooLargeIntStr := strconv.Itoa(math.MaxInt64) + "0"
|
||||
s.path = strings.Join([]string{BasePath, EnvironmentsPath, "/", tooLargeIntStr}, "")
|
||||
recorder := s.recordRequest()
|
||||
s.Equal(http.StatusBadRequest, recorder.Code)
|
||||
}
|
@ -1,55 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/openHPI/poseidon/internal/config"
|
||||
"github.com/openHPI/poseidon/internal/environment"
|
||||
"github.com/openHPI/poseidon/pkg/dto"
|
||||
"github.com/openHPI/poseidon/tests"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
)
|
||||
|
||||
func (s *MainTestSuite) TestHealth() {
|
||||
s.Run("returns StatusNoContent as default", func() {
|
||||
request, err := http.NewRequest(http.MethodGet, "/health", http.NoBody)
|
||||
if err != nil {
|
||||
s.T().Fatal(err)
|
||||
}
|
||||
recorder := httptest.NewRecorder()
|
||||
manager := &environment.ManagerHandlerMock{}
|
||||
manager.On("Statistics").Return(map[dto.EnvironmentID]*dto.StatisticalExecutionEnvironmentData{})
|
||||
|
||||
Health(manager).ServeHTTP(recorder, request)
|
||||
s.Equal(http.StatusNoContent, recorder.Code)
|
||||
})
|
||||
s.Run("returns InternalServerError for warnings and errors", func() {
|
||||
s.Run("Prewarming Pool Alert", func() {
|
||||
request, err := http.NewRequest(http.MethodGet, "/health", http.NoBody)
|
||||
if err != nil {
|
||||
s.T().Fatal(err)
|
||||
}
|
||||
recorder := httptest.NewRecorder()
|
||||
manager := &environment.ManagerHandlerMock{}
|
||||
manager.On("Statistics").Return(map[dto.EnvironmentID]*dto.StatisticalExecutionEnvironmentData{
|
||||
tests.DefaultEnvironmentIDAsInteger: {
|
||||
ID: tests.DefaultEnvironmentIDAsInteger,
|
||||
PrewarmingPoolSize: 3,
|
||||
IdleRunners: 1,
|
||||
},
|
||||
})
|
||||
config.Config.Server.Alert.PrewarmingPoolThreshold = 0.5
|
||||
|
||||
Health(manager).ServeHTTP(recorder, request)
|
||||
s.Equal(http.StatusServiceUnavailable, recorder.Code)
|
||||
|
||||
b, err := io.ReadAll(recorder.Body)
|
||||
s.Require().NoError(err)
|
||||
var details dto.InternalServerError
|
||||
err = json.Unmarshal(b, &details)
|
||||
s.Require().NoError(err)
|
||||
s.Contains(details.Message, ErrorPrewarmingPoolDepleting.Error())
|
||||
})
|
||||
})
|
||||
}
|
@ -1,501 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/openHPI/poseidon/internal/nomad"
|
||||
"github.com/openHPI/poseidon/internal/runner"
|
||||
"github.com/openHPI/poseidon/pkg/dto"
|
||||
"github.com/openHPI/poseidon/pkg/monitoring"
|
||||
"github.com/openHPI/poseidon/tests"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const invalidID = "some-invalid-runner-id"
|
||||
|
||||
type MiddlewareTestSuite struct {
|
||||
tests.MemoryLeakTestSuite
|
||||
manager *runner.ManagerMock
|
||||
router *mux.Router
|
||||
runner runner.Runner
|
||||
capturedRunner runner.Runner
|
||||
runnerRequest func(string) *http.Request
|
||||
}
|
||||
|
||||
func (s *MiddlewareTestSuite) SetupTest() {
|
||||
s.MemoryLeakTestSuite.SetupTest()
|
||||
s.manager = &runner.ManagerMock{}
|
||||
apiMock := &nomad.ExecutorAPIMock{}
|
||||
apiMock.On("DeleteJob", mock.AnythingOfType("string")).Return(nil)
|
||||
s.runner = runner.NewNomadJob(tests.DefaultRunnerID, nil, apiMock, nil)
|
||||
s.capturedRunner = nil
|
||||
s.runnerRequest = func(runnerId string) *http.Request {
|
||||
path, err := s.router.Get("test-runner-id").URL(RunnerIDKey, runnerId)
|
||||
s.Require().NoError(err)
|
||||
request, err := http.NewRequest(http.MethodPost, path.String(), http.NoBody)
|
||||
s.Require().NoError(err)
|
||||
return request
|
||||
}
|
||||
runnerRouteHandler := func(writer http.ResponseWriter, request *http.Request) {
|
||||
var ok bool
|
||||
s.capturedRunner, ok = runner.FromContext(request.Context())
|
||||
if ok {
|
||||
writer.WriteHeader(http.StatusOK)
|
||||
} else {
|
||||
writer.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
s.router = mux.NewRouter()
|
||||
runnerController := &RunnerController{s.manager, s.router}
|
||||
s.router.Use(monitoring.InfluxDB2Middleware)
|
||||
s.router.Use(runnerController.findRunnerMiddleware)
|
||||
s.router.HandleFunc(fmt.Sprintf("/test/{%s}", RunnerIDKey), runnerRouteHandler).Name("test-runner-id")
|
||||
}
|
||||
|
||||
func (s *MiddlewareTestSuite) TearDownTest() {
|
||||
defer s.MemoryLeakTestSuite.TearDownTest()
|
||||
err := s.runner.Destroy(nil)
|
||||
s.Require().NoError(err)
|
||||
}
|
||||
|
||||
func TestMiddlewareTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(MiddlewareTestSuite))
|
||||
}
|
||||
|
||||
func (s *MiddlewareTestSuite) TestFindRunnerMiddlewareIfRunnerExists() {
|
||||
s.manager.On("Get", s.runner.ID()).Return(s.runner, nil)
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(recorder, s.runnerRequest(s.runner.ID()))
|
||||
|
||||
s.Equal(http.StatusOK, recorder.Code)
|
||||
s.Equal(s.runner, s.capturedRunner)
|
||||
}
|
||||
|
||||
func (s *MiddlewareTestSuite) TestFindRunnerMiddlewareIfRunnerDoesNotExist() {
|
||||
s.manager.On("Get", invalidID).Return(nil, runner.ErrRunnerNotFound)
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(recorder, s.runnerRequest(invalidID))
|
||||
|
||||
s.Equal(http.StatusGone, recorder.Code)
|
||||
}
|
||||
|
||||
func (s *MiddlewareTestSuite) TestFindRunnerMiddlewareDoesNotEarlyRespond() {
|
||||
body := strings.NewReader(strings.Repeat("A", 798968))
|
||||
|
||||
path, err := s.router.Get("test-runner-id").URL(RunnerIDKey, invalidID)
|
||||
s.Require().NoError(err)
|
||||
request, err := http.NewRequest(http.MethodPost, path.String(), body)
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.manager.On("Get", mock.AnythingOfType("string")).Return(nil, runner.ErrRunnerNotFound)
|
||||
recorder := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(recorder, request)
|
||||
|
||||
s.Equal(http.StatusGone, recorder.Code)
|
||||
s.Equal(0, body.Len()) // No data should be unread
|
||||
}
|
||||
|
||||
func TestRunnerRouteTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(RunnerRouteTestSuite))
|
||||
}
|
||||
|
||||
type RunnerRouteTestSuite struct {
|
||||
tests.MemoryLeakTestSuite
|
||||
runnerManager *runner.ManagerMock
|
||||
router *mux.Router
|
||||
runner runner.Runner
|
||||
executionID string
|
||||
}
|
||||
|
||||
func (s *RunnerRouteTestSuite) SetupTest() {
|
||||
s.MemoryLeakTestSuite.SetupTest()
|
||||
s.runnerManager = &runner.ManagerMock{}
|
||||
s.router = NewRouter(s.runnerManager, nil)
|
||||
apiMock := &nomad.ExecutorAPIMock{}
|
||||
apiMock.On("DeleteJob", mock.AnythingOfType("string")).Return(nil)
|
||||
s.runner = runner.NewNomadJob("some-id", nil, apiMock, func(_ runner.Runner) error { return nil })
|
||||
s.executionID = "execution"
|
||||
s.runner.StoreExecution(s.executionID, &dto.ExecutionRequest{})
|
||||
s.runnerManager.On("Get", s.runner.ID()).Return(s.runner, nil)
|
||||
}
|
||||
|
||||
func (s *RunnerRouteTestSuite) TearDownTest() {
|
||||
defer s.MemoryLeakTestSuite.TearDownTest()
|
||||
s.Require().NoError(s.runner.Destroy(nil))
|
||||
}
|
||||
|
||||
func TestProvideRunnerTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(ProvideRunnerTestSuite))
|
||||
}
|
||||
|
||||
type ProvideRunnerTestSuite struct {
|
||||
RunnerRouteTestSuite
|
||||
defaultRequest *http.Request
|
||||
path string
|
||||
}
|
||||
|
||||
func (s *ProvideRunnerTestSuite) SetupTest() {
|
||||
s.RunnerRouteTestSuite.SetupTest()
|
||||
|
||||
path, err := s.router.Get(ProvideRoute).URL()
|
||||
s.Require().NoError(err)
|
||||
s.path = path.String()
|
||||
|
||||
runnerRequest := dto.RunnerRequest{ExecutionEnvironmentID: tests.DefaultEnvironmentIDAsInteger}
|
||||
body, err := json.Marshal(runnerRequest)
|
||||
s.Require().NoError(err)
|
||||
s.defaultRequest, err = http.NewRequest(http.MethodPost, s.path, bytes.NewReader(body))
|
||||
s.Require().NoError(err)
|
||||
}
|
||||
|
||||
func (s *ProvideRunnerTestSuite) TestValidRequestReturnsRunner() {
|
||||
s.runnerManager.
|
||||
On("Claim", mock.AnythingOfType("dto.EnvironmentID"), mock.AnythingOfType("int")).
|
||||
Return(s.runner, nil)
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
s.router.ServeHTTP(recorder, s.defaultRequest)
|
||||
s.Equal(http.StatusOK, recorder.Code)
|
||||
|
||||
s.Run("response contains runnerId", func() {
|
||||
var runnerResponse dto.RunnerResponse
|
||||
err := json.NewDecoder(recorder.Result().Body).Decode(&runnerResponse)
|
||||
s.Require().NoError(err)
|
||||
_ = recorder.Result().Body.Close()
|
||||
s.Equal(s.runner.ID(), runnerResponse.ID)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *ProvideRunnerTestSuite) TestInvalidRequestReturnsBadRequest() {
|
||||
badRequest, err := http.NewRequest(http.MethodPost, s.path, strings.NewReader(""))
|
||||
s.Require().NoError(err)
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
s.router.ServeHTTP(recorder, badRequest)
|
||||
s.Equal(http.StatusBadRequest, recorder.Code)
|
||||
}
|
||||
|
||||
func (s *ProvideRunnerTestSuite) TestWhenExecutionEnvironmentDoesNotExistReturnsNotFound() {
|
||||
s.runnerManager.
|
||||
On("Claim", mock.AnythingOfType("dto.EnvironmentID"), mock.AnythingOfType("int")).
|
||||
Return(nil, runner.ErrUnknownExecutionEnvironment)
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
s.router.ServeHTTP(recorder, s.defaultRequest)
|
||||
s.Equal(http.StatusNotFound, recorder.Code)
|
||||
}
|
||||
|
||||
func (s *ProvideRunnerTestSuite) TestWhenNoRunnerAvailableReturnsNomadOverload() {
|
||||
s.runnerManager.
|
||||
On("Claim", mock.AnythingOfType("dto.EnvironmentID"), mock.AnythingOfType("int")).
|
||||
Return(nil, runner.ErrNoRunnersAvailable)
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
s.router.ServeHTTP(recorder, s.defaultRequest)
|
||||
s.Equal(http.StatusInternalServerError, recorder.Code)
|
||||
|
||||
var internalServerError dto.InternalServerError
|
||||
err := json.NewDecoder(recorder.Result().Body).Decode(&internalServerError)
|
||||
s.Require().NoError(err)
|
||||
_ = recorder.Result().Body.Close()
|
||||
s.Equal(dto.ErrorNomadOverload, internalServerError.ErrorCode)
|
||||
}
|
||||
|
||||
func (s *RunnerRouteTestSuite) TestExecuteRoute() {
|
||||
path, err := s.router.Get(ExecutePath).URL(RunnerIDKey, s.runner.ID())
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.Run("valid request", func() {
|
||||
recorder := httptest.NewRecorder()
|
||||
executionRequest := dto.ExecutionRequest{
|
||||
Command: "command",
|
||||
TimeLimit: 10,
|
||||
Environment: nil,
|
||||
}
|
||||
body, err := json.Marshal(executionRequest)
|
||||
s.Require().NoError(err)
|
||||
request, err := http.NewRequest(http.MethodPost, path.String(), bytes.NewReader(body))
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.router.ServeHTTP(recorder, request)
|
||||
|
||||
var webSocketResponse dto.ExecutionResponse
|
||||
err = json.NewDecoder(recorder.Result().Body).Decode(&webSocketResponse)
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.Equal(http.StatusOK, recorder.Code)
|
||||
|
||||
s.Run("creates an execution request for the runner", func() {
|
||||
webSocketURL, err := url.Parse(webSocketResponse.WebSocketURL)
|
||||
s.Require().NoError(err)
|
||||
executionID := webSocketURL.Query().Get(ExecutionIDKey)
|
||||
ok := s.runner.ExecutionExists(executionID)
|
||||
|
||||
s.True(ok, "No execution request with this id: ", executionID)
|
||||
})
|
||||
})
|
||||
|
||||
s.Run("invalid request", func() {
|
||||
recorder := httptest.NewRecorder()
|
||||
body := ""
|
||||
request, err := http.NewRequest(http.MethodPost, path.String(), strings.NewReader(body))
|
||||
s.Require().NoError(err)
|
||||
s.router.ServeHTTP(recorder, request)
|
||||
|
||||
s.Equal(http.StatusBadRequest, recorder.Code)
|
||||
})
|
||||
|
||||
s.Run("forbidden characters in command", func() {
|
||||
recorder := httptest.NewRecorder()
|
||||
executionRequest := dto.ExecutionRequest{
|
||||
Command: "echo 'forbidden'",
|
||||
TimeLimit: 10,
|
||||
}
|
||||
body, err := json.Marshal(executionRequest)
|
||||
s.Require().NoError(err)
|
||||
request, err := http.NewRequest(http.MethodPost, path.String(), bytes.NewReader(body))
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.router.ServeHTTP(recorder, request)
|
||||
s.Equal(http.StatusBadRequest, recorder.Code)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUpdateFileSystemRouteTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(UpdateFileSystemRouteTestSuite))
|
||||
}
|
||||
|
||||
type UpdateFileSystemRouteTestSuite struct {
|
||||
RunnerRouteTestSuite
|
||||
path string
|
||||
recorder *httptest.ResponseRecorder
|
||||
runnerMock *runner.RunnerMock
|
||||
}
|
||||
|
||||
func (s *UpdateFileSystemRouteTestSuite) SetupTest() {
|
||||
s.RunnerRouteTestSuite.SetupTest()
|
||||
routeURL, err := s.router.Get(UpdateFileSystemPath).URL(RunnerIDKey, tests.DefaultMockID)
|
||||
s.Require().NoError(err)
|
||||
s.path = routeURL.String()
|
||||
s.runnerMock = &runner.RunnerMock{}
|
||||
s.runnerMock.On("ID").Return(tests.DefaultMockID)
|
||||
s.runnerMock.On("Environment").Return(dto.EnvironmentID(tests.DefaultEnvironmentIDAsInteger))
|
||||
s.runnerManager.On("Get", tests.DefaultMockID).Return(s.runnerMock, nil)
|
||||
s.recorder = httptest.NewRecorder()
|
||||
}
|
||||
|
||||
func (s *UpdateFileSystemRouteTestSuite) TestUpdateFileSystemReturnsNoContentOnValidRequest() {
|
||||
s.runnerMock.On("UpdateFileSystem", mock.AnythingOfType("*dto.UpdateFileSystemRequest"), mock.Anything).
|
||||
Return(nil)
|
||||
|
||||
copyRequest := dto.UpdateFileSystemRequest{}
|
||||
body, err := json.Marshal(copyRequest)
|
||||
s.Require().NoError(err)
|
||||
request, err := http.NewRequest(http.MethodPatch, s.path, bytes.NewReader(body))
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.router.ServeHTTP(s.recorder, request)
|
||||
s.Equal(http.StatusNoContent, s.recorder.Code)
|
||||
s.runnerMock.AssertCalled(s.T(), "UpdateFileSystem",
|
||||
mock.AnythingOfType("*dto.UpdateFileSystemRequest"), mock.Anything)
|
||||
}
|
||||
|
||||
func (s *UpdateFileSystemRouteTestSuite) TestUpdateFileSystemReturnsBadRequestOnInvalidRequestBody() {
|
||||
request, err := http.NewRequest(http.MethodPatch, s.path, strings.NewReader(""))
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.router.ServeHTTP(s.recorder, request)
|
||||
s.Equal(http.StatusBadRequest, s.recorder.Code)
|
||||
}
|
||||
|
||||
func (s *UpdateFileSystemRouteTestSuite) TestUpdateFileSystemToNonExistingRunnerReturnsGone() {
|
||||
s.runnerManager.On("Get", invalidID).Return(nil, runner.ErrRunnerNotFound)
|
||||
path, err := s.router.Get(UpdateFileSystemPath).URL(RunnerIDKey, invalidID)
|
||||
s.Require().NoError(err)
|
||||
copyRequest := dto.UpdateFileSystemRequest{}
|
||||
body, err := json.Marshal(copyRequest)
|
||||
s.Require().NoError(err)
|
||||
request, err := http.NewRequest(http.MethodPatch, path.String(), bytes.NewReader(body))
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.router.ServeHTTP(s.recorder, request)
|
||||
s.Equal(http.StatusGone, s.recorder.Code)
|
||||
}
|
||||
|
||||
func (s *UpdateFileSystemRouteTestSuite) TestUpdateFileSystemReturnsInternalServerErrorWhenCopyFailed() {
|
||||
s.runnerMock.
|
||||
On("UpdateFileSystem", mock.AnythingOfType("*dto.UpdateFileSystemRequest"), mock.Anything).
|
||||
Return(runner.ErrorFileCopyFailed)
|
||||
|
||||
copyRequest := dto.UpdateFileSystemRequest{}
|
||||
body, err := json.Marshal(copyRequest)
|
||||
s.Require().NoError(err)
|
||||
request, err := http.NewRequest(http.MethodPatch, s.path, bytes.NewReader(body))
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.router.ServeHTTP(s.recorder, request)
|
||||
s.Equal(http.StatusInternalServerError, s.recorder.Code)
|
||||
}
|
||||
|
||||
func (s *UpdateFileSystemRouteTestSuite) TestListFileSystem() {
|
||||
routeURL, err := s.router.Get(UpdateFileSystemPath).URL(RunnerIDKey, tests.DefaultMockID)
|
||||
s.Require().NoError(err)
|
||||
mockCall := s.runnerMock.On("ListFileSystem", mock.AnythingOfType("string"),
|
||||
mock.AnythingOfType("bool"), mock.Anything, mock.AnythingOfType("bool"), mock.Anything)
|
||||
|
||||
s.Run("default parameters", func() {
|
||||
mockCall.Run(func(args mock.Arguments) {
|
||||
path, ok := args.Get(0).(string)
|
||||
s.True(ok)
|
||||
s.Equal("./", path)
|
||||
recursive, ok := args.Get(1).(bool)
|
||||
s.True(ok)
|
||||
s.True(recursive)
|
||||
mockCall.ReturnArguments = mock.Arguments{nil}
|
||||
})
|
||||
request, err := http.NewRequest(http.MethodGet, routeURL.String(), strings.NewReader(""))
|
||||
s.Require().NoError(err)
|
||||
s.router.ServeHTTP(s.recorder, request)
|
||||
s.Equal(http.StatusOK, s.recorder.Code)
|
||||
})
|
||||
|
||||
s.recorder = httptest.NewRecorder()
|
||||
s.Run("passed parameters", func() {
|
||||
expectedPath := "/flag"
|
||||
|
||||
mockCall.Run(func(args mock.Arguments) {
|
||||
path, ok := args.Get(0).(string)
|
||||
s.True(ok)
|
||||
s.Equal(expectedPath, path)
|
||||
recursive, ok := args.Get(1).(bool)
|
||||
s.True(ok)
|
||||
s.False(recursive)
|
||||
mockCall.ReturnArguments = mock.Arguments{nil}
|
||||
})
|
||||
|
||||
query := routeURL.Query()
|
||||
query.Set(PathKey, expectedPath)
|
||||
query.Set(RecursiveKey, strconv.FormatBool(false))
|
||||
routeURL.RawQuery = query.Encode()
|
||||
|
||||
request, err := http.NewRequest(http.MethodGet, routeURL.String(), strings.NewReader(""))
|
||||
s.Require().NoError(err)
|
||||
s.router.ServeHTTP(s.recorder, request)
|
||||
s.Equal(http.StatusOK, s.recorder.Code)
|
||||
})
|
||||
|
||||
s.recorder = httptest.NewRecorder()
|
||||
s.Run("Internal Server Error on failure", func() {
|
||||
mockCall.Run(func(args mock.Arguments) {
|
||||
mockCall.ReturnArguments = mock.Arguments{runner.ErrRunnerNotFound}
|
||||
})
|
||||
|
||||
request, err := http.NewRequest(http.MethodGet, routeURL.String(), strings.NewReader(""))
|
||||
s.Require().NoError(err)
|
||||
s.router.ServeHTTP(s.recorder, request)
|
||||
s.Equal(http.StatusInternalServerError, s.recorder.Code)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *UpdateFileSystemRouteTestSuite) TestFileContent() {
|
||||
routeURL, err := s.router.Get(FileContentRawPath).URL(RunnerIDKey, tests.DefaultMockID)
|
||||
s.Require().NoError(err)
|
||||
mockCall := s.runnerMock.On("GetFileContent",
|
||||
mock.AnythingOfType("string"), mock.Anything, mock.AnythingOfType("bool"), mock.Anything)
|
||||
|
||||
s.Run("Not Found", func() {
|
||||
mockCall.Return(runner.ErrFileNotFound)
|
||||
request, err := http.NewRequest(http.MethodGet, routeURL.String(), strings.NewReader(""))
|
||||
s.Require().NoError(err)
|
||||
s.router.ServeHTTP(s.recorder, request)
|
||||
s.Equal(http.StatusFailedDependency, s.recorder.Code)
|
||||
})
|
||||
|
||||
s.recorder = httptest.NewRecorder()
|
||||
s.Run("Unknown Error", func() {
|
||||
mockCall.Return(nomad.ErrorExecutorCommunicationFailed)
|
||||
request, err := http.NewRequest(http.MethodGet, routeURL.String(), strings.NewReader(""))
|
||||
s.Require().NoError(err)
|
||||
s.router.ServeHTTP(s.recorder, request)
|
||||
s.Equal(http.StatusInternalServerError, s.recorder.Code)
|
||||
})
|
||||
|
||||
s.recorder = httptest.NewRecorder()
|
||||
s.Run("No Error", func() {
|
||||
mockCall.Return(nil)
|
||||
request, err := http.NewRequest(http.MethodGet, routeURL.String(), strings.NewReader(""))
|
||||
s.Require().NoError(err)
|
||||
s.router.ServeHTTP(s.recorder, request)
|
||||
s.Equal(http.StatusOK, s.recorder.Code)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDeleteRunnerRouteTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(DeleteRunnerRouteTestSuite))
|
||||
}
|
||||
|
||||
type DeleteRunnerRouteTestSuite struct {
|
||||
RunnerRouteTestSuite
|
||||
path string
|
||||
}
|
||||
|
||||
func (s *DeleteRunnerRouteTestSuite) SetupTest() {
|
||||
s.RunnerRouteTestSuite.SetupTest()
|
||||
deleteURL, err := s.router.Get(DeleteRoute).URL(RunnerIDKey, s.runner.ID())
|
||||
s.Require().NoError(err)
|
||||
s.path = deleteURL.String()
|
||||
}
|
||||
|
||||
func (s *DeleteRunnerRouteTestSuite) TestValidRequestReturnsNoContent() {
|
||||
s.runnerManager.On("Return", s.runner).Return(nil)
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
request, err := http.NewRequest(http.MethodDelete, s.path, http.NoBody)
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.router.ServeHTTP(recorder, request)
|
||||
|
||||
s.Equal(http.StatusNoContent, recorder.Code)
|
||||
|
||||
s.Run("runner was returned to runner manager", func() {
|
||||
s.runnerManager.AssertCalled(s.T(), "Return", s.runner)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *DeleteRunnerRouteTestSuite) TestReturnInternalServerErrorWhenApiCallToNomadFailed() {
|
||||
s.runnerManager.On("Return", s.runner).Return(tests.ErrDefault)
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
request, err := http.NewRequest(http.MethodDelete, s.path, http.NoBody)
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.router.ServeHTTP(recorder, request)
|
||||
|
||||
s.Equal(http.StatusInternalServerError, recorder.Code)
|
||||
}
|
||||
|
||||
func (s *DeleteRunnerRouteTestSuite) TestDeleteInvalidRunnerIdReturnsGone() {
|
||||
s.runnerManager.On("Get", mock.AnythingOfType("string")).Return(nil, tests.ErrDefault)
|
||||
deleteURL, err := s.router.Get(DeleteRoute).URL(RunnerIDKey, "1nv4l1dID")
|
||||
s.Require().NoError(err)
|
||||
deletePath := deleteURL.String()
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
request, err := http.NewRequest(http.MethodDelete, deletePath, http.NoBody)
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.router.ServeHTTP(recorder, request)
|
||||
|
||||
s.Equal(http.StatusGone, recorder.Code)
|
||||
}
|
@ -1,494 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/openHPI/poseidon/internal/environment"
|
||||
"github.com/openHPI/poseidon/internal/nomad"
|
||||
"github.com/openHPI/poseidon/internal/runner"
|
||||
"github.com/openHPI/poseidon/pkg/dto"
|
||||
"github.com/openHPI/poseidon/tests"
|
||||
"github.com/openHPI/poseidon/tests/helpers"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/sirupsen/logrus/hooks/test"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestWebSocketTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(WebSocketTestSuite))
|
||||
}
|
||||
|
||||
type WebSocketTestSuite struct {
|
||||
tests.MemoryLeakTestSuite
|
||||
router *mux.Router
|
||||
executionID string
|
||||
runner runner.Runner
|
||||
apiMock *nomad.ExecutorAPIMock
|
||||
server *httptest.Server
|
||||
}
|
||||
|
||||
func (s *WebSocketTestSuite) SetupTest() {
|
||||
s.MemoryLeakTestSuite.SetupTest()
|
||||
runnerID := "runner-id"
|
||||
s.runner, s.apiMock = newNomadAllocationWithMockedAPIClient(runnerID)
|
||||
|
||||
// default execution
|
||||
s.executionID = tests.DefaultExecutionID
|
||||
s.runner.StoreExecution(s.executionID, &executionRequestLs)
|
||||
mockAPIExecuteLs(s.apiMock)
|
||||
|
||||
runnerManager := &runner.ManagerMock{}
|
||||
runnerManager.On("Get", s.runner.ID()).Return(s.runner, nil)
|
||||
s.router = NewRouter(runnerManager, nil)
|
||||
s.server = httptest.NewServer(s.router)
|
||||
}
|
||||
|
||||
func (s *WebSocketTestSuite) TearDownTest() {
|
||||
defer s.MemoryLeakTestSuite.TearDownTest()
|
||||
s.server.Close()
|
||||
err := s.runner.Destroy(nil)
|
||||
s.Require().NoError(err)
|
||||
}
|
||||
|
||||
func (s *WebSocketTestSuite) TestWebsocketConnectionCanBeEstablished() {
|
||||
wsURL, err := s.webSocketURL("ws", s.runner.ID(), s.executionID)
|
||||
s.Require().NoError(err)
|
||||
conn, _, err := websocket.DefaultDialer.Dial(wsURL.String(), nil)
|
||||
s.Require().NoError(err)
|
||||
|
||||
<-time.After(tests.ShortTimeout)
|
||||
err = conn.Close()
|
||||
s.NoError(err)
|
||||
}
|
||||
|
||||
func (s *WebSocketTestSuite) TestWebsocketReturns404IfExecutionDoesNotExist() {
|
||||
wsURL, err := s.webSocketURL("ws", s.runner.ID(), "invalid-execution-id")
|
||||
s.Require().NoError(err)
|
||||
_, response, err := websocket.DefaultDialer.Dial(wsURL.String(), nil)
|
||||
s.Require().ErrorIs(err, websocket.ErrBadHandshake)
|
||||
s.Equal(http.StatusNotFound, response.StatusCode)
|
||||
}
|
||||
|
||||
func (s *WebSocketTestSuite) TestWebsocketReturns400IfRequestedViaHttp() {
|
||||
wsURL, err := s.webSocketURL("http", s.runner.ID(), s.executionID)
|
||||
s.Require().NoError(err)
|
||||
response, err := http.Get(wsURL.String())
|
||||
s.Require().NoError(err)
|
||||
// This functionality is implemented by the WebSocket library.
|
||||
s.Equal(http.StatusBadRequest, response.StatusCode)
|
||||
_, err = io.ReadAll(response.Body)
|
||||
s.NoError(err)
|
||||
}
|
||||
|
||||
func (s *WebSocketTestSuite) TestWebsocketConnection() {
|
||||
s.runner.StoreExecution(s.executionID, &executionRequestHead)
|
||||
mockAPIExecuteHead(s.apiMock)
|
||||
wsURL, err := s.webSocketURL("ws", s.runner.ID(), s.executionID)
|
||||
s.Require().NoError(err)
|
||||
connection, _, err := websocket.DefaultDialer.Dial(wsURL.String(), nil)
|
||||
s.Require().NoError(err)
|
||||
err = connection.SetReadDeadline(time.Now().Add(5 * time.Second))
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.Run("Receives start message", func() {
|
||||
message, err := helpers.ReceiveNextWebSocketMessage(connection)
|
||||
s.Require().NoError(err)
|
||||
s.Equal(dto.WebSocketMetaStart, message.Type)
|
||||
})
|
||||
|
||||
s.Run("Executes the request in the runner", func() {
|
||||
<-time.After(tests.ShortTimeout)
|
||||
s.apiMock.AssertCalled(s.T(), "ExecuteCommand",
|
||||
mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.AnythingOfType("bool"),
|
||||
mock.Anything, mock.Anything, mock.Anything)
|
||||
})
|
||||
|
||||
s.Run("Can send input", func() {
|
||||
err = connection.WriteMessage(websocket.TextMessage, []byte("Hello World\n"))
|
||||
s.Require().NoError(err)
|
||||
})
|
||||
|
||||
messages, err := helpers.ReceiveAllWebSocketMessages(connection)
|
||||
s.Require().Error(err)
|
||||
s.True(websocket.IsCloseError(err, websocket.CloseNormalClosure))
|
||||
|
||||
s.Run("Receives output message", func() {
|
||||
stdout, _, _ := helpers.WebSocketOutputMessages(messages)
|
||||
s.Equal("Hello World", stdout)
|
||||
})
|
||||
|
||||
s.Run("Receives exit message", func() {
|
||||
controlMessages := helpers.WebSocketControlMessages(messages)
|
||||
s.Require().Equal(1, len(controlMessages))
|
||||
s.Equal(dto.WebSocketExit, controlMessages[0].Type)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *WebSocketTestSuite) TestCancelWebSocketConnection() {
|
||||
executionID := "sleeping-execution"
|
||||
s.runner.StoreExecution(executionID, &executionRequestSleep)
|
||||
canceled := mockAPIExecuteSleep(s.apiMock)
|
||||
|
||||
wsURL, err := webSocketURL("ws", s.server, s.router, s.runner.ID(), executionID)
|
||||
s.Require().NoError(err)
|
||||
connection, _, err := websocket.DefaultDialer.Dial(wsURL.String(), nil)
|
||||
s.Require().NoError(err)
|
||||
|
||||
message, err := helpers.ReceiveNextWebSocketMessage(connection)
|
||||
s.Require().NoError(err)
|
||||
s.Equal(dto.WebSocketMetaStart, message.Type)
|
||||
|
||||
select {
|
||||
case <-canceled:
|
||||
s.Fail("ExecuteInteractively canceled unexpected")
|
||||
default:
|
||||
}
|
||||
|
||||
err = connection.WriteControl(websocket.CloseMessage,
|
||||
websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""), time.Now().Add(time.Second))
|
||||
s.Require().NoError(err)
|
||||
|
||||
select {
|
||||
case <-canceled:
|
||||
case <-time.After(time.Second):
|
||||
s.Fail("ExecuteInteractively not canceled")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *WebSocketTestSuite) TestWebSocketConnectionTimeout() {
|
||||
executionID := "time-out-execution"
|
||||
limitExecution := executionRequestSleep
|
||||
limitExecution.TimeLimit = 2
|
||||
s.runner.StoreExecution(executionID, &limitExecution)
|
||||
canceled := mockAPIExecuteSleep(s.apiMock)
|
||||
|
||||
wsURL, err := webSocketURL("ws", s.server, s.router, s.runner.ID(), executionID)
|
||||
s.Require().NoError(err)
|
||||
connection, _, err := websocket.DefaultDialer.Dial(wsURL.String(), nil)
|
||||
s.Require().NoError(err)
|
||||
|
||||
message, err := helpers.ReceiveNextWebSocketMessage(connection)
|
||||
s.Require().NoError(err)
|
||||
s.Equal(dto.WebSocketMetaStart, message.Type)
|
||||
|
||||
select {
|
||||
case <-canceled:
|
||||
s.Fail("ExecuteInteractively canceled unexpected")
|
||||
case <-time.After(time.Duration(limitExecution.TimeLimit-1) * time.Second):
|
||||
<-time.After(time.Second)
|
||||
}
|
||||
|
||||
select {
|
||||
case <-canceled:
|
||||
case <-time.After(time.Second):
|
||||
s.Fail("ExecuteInteractively not canceled")
|
||||
}
|
||||
|
||||
message, err = helpers.ReceiveNextWebSocketMessage(connection)
|
||||
s.Require().NoError(err)
|
||||
s.Equal(dto.WebSocketMetaTimeout, message.Type)
|
||||
}
|
||||
|
||||
func (s *WebSocketTestSuite) TestWebsocketStdoutAndStderr() {
|
||||
executionID := "ls-execution"
|
||||
s.runner.StoreExecution(executionID, &executionRequestLs)
|
||||
mockAPIExecuteLs(s.apiMock)
|
||||
|
||||
wsURL, err := webSocketURL("ws", s.server, s.router, s.runner.ID(), executionID)
|
||||
s.Require().NoError(err)
|
||||
connection, _, err := websocket.DefaultDialer.Dial(wsURL.String(), nil)
|
||||
s.Require().NoError(err)
|
||||
|
||||
messages, err := helpers.ReceiveAllWebSocketMessages(connection)
|
||||
s.Require().Error(err)
|
||||
s.True(websocket.IsCloseError(err, websocket.CloseNormalClosure))
|
||||
stdout, stderr, _ := helpers.WebSocketOutputMessages(messages)
|
||||
|
||||
s.Contains(stdout, "existing-file")
|
||||
|
||||
s.Contains(stderr, "non-existing-file")
|
||||
}
|
||||
|
||||
func (s *WebSocketTestSuite) TestWebsocketError() {
|
||||
executionID := "error-execution"
|
||||
s.runner.StoreExecution(executionID, &executionRequestError)
|
||||
mockAPIExecuteError(s.apiMock)
|
||||
|
||||
wsURL, err := webSocketURL("ws", s.server, s.router, s.runner.ID(), executionID)
|
||||
s.Require().NoError(err)
|
||||
connection, _, err := websocket.DefaultDialer.Dial(wsURL.String(), nil)
|
||||
s.Require().NoError(err)
|
||||
|
||||
messages, err := helpers.ReceiveAllWebSocketMessages(connection)
|
||||
s.Require().Error(err)
|
||||
s.True(websocket.IsCloseError(err, websocket.CloseNormalClosure))
|
||||
|
||||
_, _, errMessages := helpers.WebSocketOutputMessages(messages)
|
||||
s.Require().Equal(1, len(errMessages))
|
||||
s.Equal("Error executing the request", errMessages[0])
|
||||
}
|
||||
|
||||
func (s *WebSocketTestSuite) TestWebsocketNonZeroExit() {
|
||||
executionID := "exit-execution"
|
||||
s.runner.StoreExecution(executionID, &executionRequestExitNonZero)
|
||||
mockAPIExecuteExitNonZero(s.apiMock)
|
||||
|
||||
wsURL, err := webSocketURL("ws", s.server, s.router, s.runner.ID(), executionID)
|
||||
s.Require().NoError(err)
|
||||
connection, _, err := websocket.DefaultDialer.Dial(wsURL.String(), nil)
|
||||
s.Require().NoError(err)
|
||||
|
||||
messages, err := helpers.ReceiveAllWebSocketMessages(connection)
|
||||
s.Require().Error(err)
|
||||
s.True(websocket.IsCloseError(err, websocket.CloseNormalClosure))
|
||||
|
||||
controlMessages := helpers.WebSocketControlMessages(messages)
|
||||
s.Equal(2, len(controlMessages))
|
||||
s.Equal(&dto.WebSocketMessage{Type: dto.WebSocketExit, ExitCode: 42}, controlMessages[1])
|
||||
}
|
||||
|
||||
func (s *MainTestSuite) TestWebsocketTLS() {
|
||||
runnerID := "runner-id"
|
||||
r, apiMock := newNomadAllocationWithMockedAPIClient(runnerID)
|
||||
|
||||
executionID := tests.DefaultExecutionID
|
||||
r.StoreExecution(executionID, &executionRequestLs)
|
||||
mockAPIExecuteLs(apiMock)
|
||||
|
||||
runnerManager := &runner.ManagerMock{}
|
||||
runnerManager.On("Get", r.ID()).Return(r, nil)
|
||||
router := NewRouter(runnerManager, nil)
|
||||
|
||||
server, err := helpers.StartTLSServer(s.T(), router)
|
||||
s.Require().NoError(err)
|
||||
defer server.Close()
|
||||
|
||||
wsURL, err := webSocketURL("wss", server, router, runnerID, executionID)
|
||||
s.Require().NoError(err)
|
||||
|
||||
config := &tls.Config{RootCAs: nil, InsecureSkipVerify: true} //nolint:gosec // test needs self-signed cert
|
||||
d := websocket.Dialer{TLSClientConfig: config}
|
||||
connection, _, err := d.Dial(wsURL.String(), nil)
|
||||
s.Require().NoError(err)
|
||||
|
||||
message, err := helpers.ReceiveNextWebSocketMessage(connection)
|
||||
s.Require().NoError(err)
|
||||
s.Equal(dto.WebSocketMetaStart, message.Type)
|
||||
_, err = helpers.ReceiveAllWebSocketMessages(connection)
|
||||
s.Require().Error(err)
|
||||
s.True(websocket.IsCloseError(err, websocket.CloseNormalClosure))
|
||||
s.NoError(r.Destroy(nil))
|
||||
}
|
||||
|
||||
func (s *MainTestSuite) TestWebSocketProxyStopsReadingTheWebSocketAfterClosingIt() {
|
||||
apiMock := &nomad.ExecutorAPIMock{}
|
||||
executionID := tests.DefaultExecutionID
|
||||
r, wsURL, cleanup := newRunnerWithNotMockedRunnerManager(s, apiMock, executionID)
|
||||
defer cleanup()
|
||||
|
||||
logger, hook := test.NewNullLogger()
|
||||
log = logger.WithField("pkg", "api")
|
||||
|
||||
r.StoreExecution(executionID, &executionRequestHead)
|
||||
mockAPIExecute(apiMock, &executionRequestHead,
|
||||
func(_ string, ctx context.Context, _ string, _ bool, _ io.Reader, _, _ io.Writer) (int, error) {
|
||||
return 0, nil
|
||||
})
|
||||
connection, _, err := websocket.DefaultDialer.Dial(wsURL.String(), nil)
|
||||
s.Require().NoError(err)
|
||||
|
||||
_, err = helpers.ReceiveAllWebSocketMessages(connection)
|
||||
s.Require().Error(err)
|
||||
s.True(websocket.IsCloseError(err, websocket.CloseNormalClosure))
|
||||
for _, logMsg := range hook.Entries {
|
||||
if logMsg.Level < logrus.InfoLevel {
|
||||
s.Fail(logMsg.Message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Test suite specific test helpers ---
|
||||
|
||||
func newNomadAllocationWithMockedAPIClient(runnerID string) (runner.Runner, *nomad.ExecutorAPIMock) {
|
||||
executorAPIMock := &nomad.ExecutorAPIMock{}
|
||||
executorAPIMock.On("DeleteJob", mock.AnythingOfType("string")).Return(nil)
|
||||
manager := &runner.ManagerMock{}
|
||||
manager.On("Return", mock.Anything).Return(nil)
|
||||
r := runner.NewNomadJob(runnerID, nil, executorAPIMock, nil)
|
||||
return r, executorAPIMock
|
||||
}
|
||||
|
||||
func newRunnerWithNotMockedRunnerManager(s *MainTestSuite, apiMock *nomad.ExecutorAPIMock, executionID string) (
|
||||
r runner.Runner, wsURL *url.URL, cleanup func()) {
|
||||
s.T().Helper()
|
||||
apiMock.On("MarkRunnerAsUsed", mock.AnythingOfType("string"), mock.AnythingOfType("int")).Return(nil)
|
||||
apiMock.On("LoadRunnerIDs", mock.AnythingOfType("string")).Return([]string{}, nil)
|
||||
apiMock.On("DeleteJob", mock.AnythingOfType("string")).Return(nil)
|
||||
apiMock.On("RegisterRunnerJob", mock.AnythingOfType("*api.Job")).Return(nil)
|
||||
call := apiMock.On("WatchEventStream", mock.Anything, mock.Anything, mock.Anything)
|
||||
call.Run(func(args mock.Arguments) {
|
||||
<-s.TestCtx.Done()
|
||||
call.ReturnArguments = mock.Arguments{nil}
|
||||
})
|
||||
|
||||
runnerManager := runner.NewNomadRunnerManager(apiMock, s.TestCtx)
|
||||
router := NewRouter(runnerManager, nil)
|
||||
s.ExpectedGoroutineIncrease++ // The server is not closing properly. Therefore, we don't even try.
|
||||
server := httptest.NewServer(router)
|
||||
|
||||
runnerID := tests.DefaultRunnerID
|
||||
runnerJob := runner.NewNomadJob(runnerID, nil, apiMock, nil)
|
||||
e, err := environment.NewNomadEnvironment(0, apiMock, "job \"template-0\" {}")
|
||||
s.Require().NoError(err)
|
||||
eID, err := nomad.EnvironmentIDFromRunnerID(runnerID)
|
||||
s.Require().NoError(err)
|
||||
e.SetID(eID)
|
||||
e.SetPrewarmingPoolSize(0)
|
||||
runnerManager.StoreEnvironment(e)
|
||||
e.AddRunner(runnerJob)
|
||||
|
||||
r, err = runnerManager.Claim(e.ID(), int(tests.DefaultTestTimeout.Seconds()))
|
||||
s.Require().NoError(err)
|
||||
wsURL, err = webSocketURL("ws", server, router, r.ID(), executionID)
|
||||
s.Require().NoError(err)
|
||||
|
||||
return r, wsURL, func() {
|
||||
err = r.Destroy(tests.ErrCleanupDestroyReason)
|
||||
s.NoError(err)
|
||||
err = e.Delete(tests.ErrCleanupDestroyReason)
|
||||
s.NoError(err)
|
||||
}
|
||||
}
|
||||
|
||||
func webSocketURL(scheme string, server *httptest.Server, router *mux.Router,
|
||||
runnerID string, executionID string,
|
||||
) (*url.URL, error) {
|
||||
websocketURL, err := url.Parse(server.URL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
path, err := router.Get(WebsocketPath).URL(RunnerIDKey, runnerID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
websocketURL.Scheme = scheme
|
||||
websocketURL.Path = path.Path
|
||||
websocketURL.RawQuery = fmt.Sprintf("executionID=%s", executionID)
|
||||
return websocketURL, nil
|
||||
}
|
||||
|
||||
func (s *WebSocketTestSuite) webSocketURL(scheme, runnerID, executionID string) (*url.URL, error) {
|
||||
return webSocketURL(scheme, s.server, s.router, runnerID, executionID)
|
||||
}
|
||||
|
||||
var executionRequestLs = dto.ExecutionRequest{Command: "ls"}
|
||||
|
||||
// mockAPIExecuteLs mocks the ExecuteCommand of an ExecutorApi to act as if
|
||||
// 'ls existing-file non-existing-file' was executed.
|
||||
func mockAPIExecuteLs(api *nomad.ExecutorAPIMock) {
|
||||
mockAPIExecute(api, &executionRequestLs,
|
||||
func(_ string, _ context.Context, _ string, _ bool, _ io.Reader, stdout, stderr io.Writer) (int, error) {
|
||||
_, _ = stdout.Write([]byte("existing-file\n"))
|
||||
_, _ = stderr.Write([]byte("ls: cannot access 'non-existing-file': No such file or directory\n"))
|
||||
return 0, nil
|
||||
})
|
||||
}
|
||||
|
||||
var executionRequestHead = dto.ExecutionRequest{Command: "head -n 1"}
|
||||
|
||||
// mockAPIExecuteHead mocks the ExecuteCommand of an ExecutorApi to act as if 'head -n 1' was executed.
|
||||
func mockAPIExecuteHead(api *nomad.ExecutorAPIMock) {
|
||||
mockAPIExecute(api, &executionRequestHead,
|
||||
func(_ string, _ context.Context, _ string, _ bool,
|
||||
stdin io.Reader, stdout io.Writer, stderr io.Writer,
|
||||
) (int, error) {
|
||||
scanner := bufio.NewScanner(stdin)
|
||||
for !scanner.Scan() {
|
||||
scanner = bufio.NewScanner(stdin)
|
||||
}
|
||||
_, _ = stdout.Write(scanner.Bytes())
|
||||
return 0, nil
|
||||
})
|
||||
}
|
||||
|
||||
var executionRequestSleep = dto.ExecutionRequest{Command: "sleep infinity"}
|
||||
|
||||
// mockAPIExecuteSleep mocks the ExecuteCommand method of an ExecutorAPI to sleep
|
||||
// until the execution receives a SIGQUIT.
|
||||
func mockAPIExecuteSleep(api *nomad.ExecutorAPIMock) <-chan bool {
|
||||
canceled := make(chan bool, 1)
|
||||
|
||||
mockAPIExecute(api, &executionRequestSleep,
|
||||
func(_ string, ctx context.Context, _ string, _ bool,
|
||||
stdin io.Reader, stdout io.Writer, stderr io.Writer,
|
||||
) (int, error) {
|
||||
var err error
|
||||
buffer := make([]byte, 1) //nolint:makezero // if the length is zero, the Read call never reads anything
|
||||
for n := 0; !(n == 1 && buffer[0] == runner.SIGQUIT); n, err = stdin.Read(buffer) {
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("error while reading stdin: %w", err)
|
||||
}
|
||||
}
|
||||
close(canceled)
|
||||
return 0, ctx.Err()
|
||||
})
|
||||
return canceled
|
||||
}
|
||||
|
||||
var executionRequestError = dto.ExecutionRequest{Command: "error"}
|
||||
|
||||
// mockAPIExecuteError mocks the ExecuteCommand method of an ExecutorApi to return an error.
|
||||
func mockAPIExecuteError(api *nomad.ExecutorAPIMock) {
|
||||
mockAPIExecute(api, &executionRequestError,
|
||||
func(_ string, _ context.Context, _ string, _ bool, _ io.Reader, _, _ io.Writer) (int, error) {
|
||||
return 0, tests.ErrDefault
|
||||
})
|
||||
}
|
||||
|
||||
var executionRequestExitNonZero = dto.ExecutionRequest{Command: "exit 42"}
|
||||
|
||||
// mockAPIExecuteExitNonZero mocks the ExecuteCommand method of an ExecutorApi to exit with exit status 42.
|
||||
func mockAPIExecuteExitNonZero(api *nomad.ExecutorAPIMock) {
|
||||
mockAPIExecute(api, &executionRequestExitNonZero,
|
||||
func(_ string, _ context.Context, _ string, _ bool, _ io.Reader, _, _ io.Writer) (int, error) {
|
||||
return 42, nil
|
||||
})
|
||||
}
|
||||
|
||||
// mockAPIExecute mocks the ExecuteCommand method of an ExecutorApi to call the given method run when the command
|
||||
// corresponding to the given ExecutionRequest is called.
|
||||
func mockAPIExecute(api *nomad.ExecutorAPIMock, request *dto.ExecutionRequest,
|
||||
run func(runnerId string, ctx context.Context, command string, tty bool,
|
||||
stdin io.Reader, stdout, stderr io.Writer) (int, error)) {
|
||||
tests.RemoveMethodFromMock(&api.Mock, "ExecuteCommand")
|
||||
call := api.On("ExecuteCommand",
|
||||
mock.AnythingOfType("string"),
|
||||
mock.Anything,
|
||||
request.FullCommand(),
|
||||
mock.AnythingOfType("bool"),
|
||||
mock.AnythingOfType("bool"),
|
||||
mock.Anything,
|
||||
mock.Anything,
|
||||
mock.Anything)
|
||||
call.Run(func(args mock.Arguments) {
|
||||
exit, err := run(args.Get(0).(string),
|
||||
args.Get(1).(context.Context),
|
||||
args.Get(2).(string),
|
||||
args.Get(3).(bool),
|
||||
args.Get(5).(io.Reader),
|
||||
args.Get(6).(io.Writer),
|
||||
args.Get(7).(io.Writer))
|
||||
call.ReturnArguments = mock.Arguments{exit, err}
|
||||
})
|
||||
}
|
@ -1,83 +0,0 @@
|
||||
package ws
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/openHPI/poseidon/tests"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type MainTestSuite struct {
|
||||
tests.MemoryLeakTestSuite
|
||||
}
|
||||
|
||||
func TestMainTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(MainTestSuite))
|
||||
}
|
||||
|
||||
func (s *MainTestSuite) TestCodeOceanToRawReaderReturnsOnlyAfterOneByteWasRead() {
|
||||
readingCtx, cancel := context.WithCancel(context.Background())
|
||||
forwardingCtx := readingCtx
|
||||
defer cancel()
|
||||
reader := NewCodeOceanToRawReader(nil, readingCtx, forwardingCtx)
|
||||
|
||||
read := make(chan bool)
|
||||
go func() {
|
||||
//nolint:makezero // we can't make zero initial length here as the reader otherwise doesn't block
|
||||
p := make([]byte, 10)
|
||||
_, err := reader.Read(p)
|
||||
s.Require().NoError(err)
|
||||
read <- true
|
||||
}()
|
||||
|
||||
s.Run("Does not return immediately when there is no data", func() {
|
||||
s.False(tests.ChannelReceivesSomething(read, tests.ShortTimeout))
|
||||
})
|
||||
|
||||
s.Run("Returns when there is data available", func() {
|
||||
reader.buffer <- byte(42)
|
||||
s.True(tests.ChannelReceivesSomething(read, tests.ShortTimeout))
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MainTestSuite) TestCodeOceanToRawReaderReturnsOnlyAfterOneByteWasReadFromConnection() {
|
||||
messages := make(chan io.Reader)
|
||||
defer close(messages)
|
||||
|
||||
connection := &ConnectionMock{}
|
||||
connection.On("WriteMessage", mock.AnythingOfType("int"), mock.AnythingOfType("[]uint8")).Return(nil)
|
||||
connection.On("CloseHandler").Return(nil)
|
||||
connection.On("SetCloseHandler", mock.Anything).Return()
|
||||
call := connection.On("NextReader")
|
||||
call.Run(func(_ mock.Arguments) {
|
||||
call.Return(websocket.TextMessage, <-messages, nil)
|
||||
})
|
||||
|
||||
readingCtx, cancel := context.WithCancel(context.Background())
|
||||
forwardingCtx := readingCtx
|
||||
defer cancel()
|
||||
reader := NewCodeOceanToRawReader(connection, readingCtx, forwardingCtx)
|
||||
reader.Start()
|
||||
|
||||
read := make(chan bool)
|
||||
//nolint:makezero // this is required here to make the Read call blocking
|
||||
message := make([]byte, 10)
|
||||
go func() {
|
||||
_, err := reader.Read(message)
|
||||
s.Require().NoError(err)
|
||||
read <- true
|
||||
}()
|
||||
|
||||
s.Run("Does not return immediately when there is no data", func() {
|
||||
s.False(tests.ChannelReceivesSomething(read, tests.ShortTimeout))
|
||||
})
|
||||
|
||||
s.Run("Returns when there is data available", func() {
|
||||
messages <- strings.NewReader("Hello")
|
||||
s.True(tests.ChannelReceivesSomething(read, tests.ShortTimeout))
|
||||
})
|
||||
}
|
@ -1,107 +0,0 @@
|
||||
package ws
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/openHPI/poseidon/internal/runner"
|
||||
"github.com/openHPI/poseidon/pkg/dto"
|
||||
"github.com/openHPI/poseidon/tests"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
func (s *MainTestSuite) TestRawToCodeOceanWriter() {
|
||||
connectionMock, messages := buildConnectionMock(&s.MemoryLeakTestSuite)
|
||||
proxyCtx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
output := NewCodeOceanOutputWriter(connectionMock, proxyCtx, cancel)
|
||||
defer output.Close(nil)
|
||||
<-messages // start messages
|
||||
|
||||
s.Run("StdOut", func() {
|
||||
testMessage := "testStdOut"
|
||||
_, err := output.StdOut().Write([]byte(testMessage))
|
||||
s.Require().NoError(err)
|
||||
|
||||
expected, err := json.Marshal(struct {
|
||||
Type string `json:"type"`
|
||||
Data string `json:"data"`
|
||||
}{string(dto.WebSocketOutputStdout), testMessage})
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.Equal(expected, <-messages)
|
||||
})
|
||||
|
||||
s.Run("StdErr", func() {
|
||||
testMessage := "testStdErr"
|
||||
_, err := output.StdErr().Write([]byte(testMessage))
|
||||
s.Require().NoError(err)
|
||||
|
||||
expected, err := json.Marshal(struct {
|
||||
Type string `json:"type"`
|
||||
Data string `json:"data"`
|
||||
}{string(dto.WebSocketOutputStderr), testMessage})
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.Equal(expected, <-messages)
|
||||
})
|
||||
}
|
||||
|
||||
type sendExitInfoTestCase struct {
|
||||
name string
|
||||
info *runner.ExitInfo
|
||||
message dto.WebSocketMessage
|
||||
}
|
||||
|
||||
func (s *MainTestSuite) TestCodeOceanOutputWriter_SendExitInfo() {
|
||||
testCases := []sendExitInfoTestCase{
|
||||
{"Timeout", &runner.ExitInfo{Err: runner.ErrorRunnerInactivityTimeout},
|
||||
dto.WebSocketMessage{Type: dto.WebSocketMetaTimeout}},
|
||||
{"Error", &runner.ExitInfo{Err: websocket.ErrCloseSent},
|
||||
dto.WebSocketMessage{Type: dto.WebSocketOutputError, Data: "Error executing the request"}},
|
||||
// CodeOcean expects this exact string in case of a OOM Killed runner.
|
||||
{"Specific data for OOM Killed runner", &runner.ExitInfo{Err: runner.ErrOOMKilled},
|
||||
dto.WebSocketMessage{Type: dto.WebSocketOutputError, Data: "the allocation was OOM Killed"}},
|
||||
{"Exit", &runner.ExitInfo{Code: 21},
|
||||
dto.WebSocketMessage{Type: dto.WebSocketExit, ExitCode: 21}},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
s.Run(test.name, func() {
|
||||
connectionMock, messages := buildConnectionMock(&s.MemoryLeakTestSuite)
|
||||
proxyCtx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
output := NewCodeOceanOutputWriter(connectionMock, proxyCtx, cancel)
|
||||
<-messages // start messages
|
||||
|
||||
output.Close(test.info)
|
||||
expected, err := json.Marshal(test.message)
|
||||
s.Require().NoError(err)
|
||||
|
||||
msg := <-messages
|
||||
s.Equal(expected, msg)
|
||||
|
||||
<-messages // close message
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func buildConnectionMock(s *tests.MemoryLeakTestSuite) (conn *ConnectionMock, messages <-chan []byte) {
|
||||
s.T().Helper()
|
||||
message := make(chan []byte)
|
||||
connectionMock := &ConnectionMock{}
|
||||
connectionMock.On("WriteMessage", mock.AnythingOfType("int"), mock.AnythingOfType("[]uint8")).
|
||||
Run(func(args mock.Arguments) {
|
||||
m, ok := args.Get(1).([]byte)
|
||||
s.Require().True(ok)
|
||||
select {
|
||||
case <-s.TestCtx.Done():
|
||||
case message <- m:
|
||||
}
|
||||
}).
|
||||
Return(nil)
|
||||
connectionMock.On("CloseHandler").Return(nil)
|
||||
connectionMock.On("SetCloseHandler", mock.Anything).Return()
|
||||
connectionMock.On("Close").Return(nil)
|
||||
return connectionMock, message
|
||||
}
|
@ -1,108 +0,0 @@
|
||||
// Code generated by mockery v2.13.1. DO NOT EDIT.
|
||||
|
||||
package ws
|
||||
|
||||
import (
|
||||
io "io"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// ConnectionMock is an autogenerated mock type for the Connection type
|
||||
type ConnectionMock struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// Close provides a mock function with given fields:
|
||||
func (_m *ConnectionMock) Close() error {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func() error); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// CloseHandler provides a mock function with given fields:
|
||||
func (_m *ConnectionMock) CloseHandler() func(int, string) error {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 func(int, string) error
|
||||
if rf, ok := ret.Get(0).(func() func(int, string) error); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(func(int, string) error)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// NextReader provides a mock function with given fields:
|
||||
func (_m *ConnectionMock) NextReader() (int, io.Reader, error) {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 int
|
||||
if rf, ok := ret.Get(0).(func() int); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Get(0).(int)
|
||||
}
|
||||
|
||||
var r1 io.Reader
|
||||
if rf, ok := ret.Get(1).(func() io.Reader); ok {
|
||||
r1 = rf()
|
||||
} else {
|
||||
if ret.Get(1) != nil {
|
||||
r1 = ret.Get(1).(io.Reader)
|
||||
}
|
||||
}
|
||||
|
||||
var r2 error
|
||||
if rf, ok := ret.Get(2).(func() error); ok {
|
||||
r2 = rf()
|
||||
} else {
|
||||
r2 = ret.Error(2)
|
||||
}
|
||||
|
||||
return r0, r1, r2
|
||||
}
|
||||
|
||||
// SetCloseHandler provides a mock function with given fields: handler
|
||||
func (_m *ConnectionMock) SetCloseHandler(handler func(int, string) error) {
|
||||
_m.Called(handler)
|
||||
}
|
||||
|
||||
// WriteMessage provides a mock function with given fields: messageType, data
|
||||
func (_m *ConnectionMock) WriteMessage(messageType int, data []byte) error {
|
||||
ret := _m.Called(messageType, data)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(int, []byte) error); ok {
|
||||
r0 = rf(messageType, data)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
type mockConstructorTestingTNewConnectionMock interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}
|
||||
|
||||
// NewConnectionMock creates a new instance of ConnectionMock. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
func NewConnectionMock(t mockConstructorTestingTNewConnectionMock) *ConnectionMock {
|
||||
mock := &ConnectionMock{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
@ -13,6 +13,7 @@ import (
|
||||
"github.com/openHPI/poseidon/pkg/logging"
|
||||
"github.com/sirupsen/logrus"
|
||||
"gopkg.in/yaml.v3"
|
||||
"k8s.io/client-go/rest"
|
||||
"net/url"
|
||||
"os"
|
||||
"reflect"
|
||||
@ -66,6 +67,17 @@ var (
|
||||
DNS: nil,
|
||||
},
|
||||
},
|
||||
Kubernetes: Kubernetes{
|
||||
Enabled: false,
|
||||
KubeConfig: rest.Config{
|
||||
Host: "",
|
||||
TLSClientConfig: rest.TLSClientConfig{
|
||||
Insecure: false,
|
||||
ServerName: "",
|
||||
},
|
||||
BearerToken: "",
|
||||
},
|
||||
},
|
||||
AWS: AWS{
|
||||
Enabled: false,
|
||||
Endpoint: "",
|
||||
@ -134,6 +146,11 @@ type Nomad struct {
|
||||
Network nomadApi.NetworkResource
|
||||
}
|
||||
|
||||
type Kubernetes struct {
|
||||
Enabled bool
|
||||
KubeConfig rest.Config
|
||||
}
|
||||
|
||||
// URL returns the URL for the configured Nomad cluster.
|
||||
func (n *Nomad) URL() *url.URL {
|
||||
return parseURL(n.Address, n.Port, n.TLS.Active)
|
||||
@ -181,6 +198,7 @@ type InfluxDB struct {
|
||||
type configuration struct {
|
||||
Server server
|
||||
Nomad Nomad
|
||||
Kubernetes Kubernetes
|
||||
AWS AWS
|
||||
Logger Logger
|
||||
Profiling Profiling
|
||||
|
@ -1,223 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/openHPI/poseidon/tests"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/sirupsen/logrus/hooks/test"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var (
|
||||
getServerPort = func(c *configuration) interface{} { return c.Server.Port }
|
||||
getNomadToken = func(c *configuration) interface{} { return c.Nomad.Token }
|
||||
getNomadTLSActive = func(c *configuration) interface{} { return c.Nomad.TLS.Active }
|
||||
getAWSFunctions = func(c *configuration) interface{} { return c.AWS.Functions }
|
||||
)
|
||||
|
||||
func newTestConfiguration() *configuration {
|
||||
return &configuration{
|
||||
Server: server{
|
||||
Address: "127.0.0.1",
|
||||
Port: 3000,
|
||||
},
|
||||
Nomad: Nomad{
|
||||
Address: "127.0.0.2",
|
||||
Port: 4646,
|
||||
Token: "SECRET",
|
||||
TLS: TLS{
|
||||
Active: false,
|
||||
},
|
||||
},
|
||||
Logger: Logger{
|
||||
Level: "INFO",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *configuration) getReflectValue() reflect.Value {
|
||||
return reflect.ValueOf(c).Elem()
|
||||
}
|
||||
|
||||
// writeConfigurationFile creates a file on disk and returns the path to it.
|
||||
func writeConfigurationFile(t *testing.T, name string, content []byte) string {
|
||||
t.Helper()
|
||||
directory := t.TempDir()
|
||||
filePath := filepath.Join(directory, name)
|
||||
file, err := os.Create(filePath)
|
||||
if err != nil {
|
||||
t.Fatal("Could not create config file")
|
||||
}
|
||||
defer file.Close()
|
||||
_, err = file.Write(content)
|
||||
require.NoError(t, err)
|
||||
return filePath
|
||||
}
|
||||
|
||||
type MainTestSuite struct {
|
||||
tests.MemoryLeakTestSuite
|
||||
}
|
||||
|
||||
func TestMainTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(MainTestSuite))
|
||||
}
|
||||
|
||||
func (s *MainTestSuite) TestCallingInitConfigTwiceReturnsError() {
|
||||
configurationInitialized = false
|
||||
err := InitConfig()
|
||||
s.NoError(err)
|
||||
err = InitConfig()
|
||||
s.Error(err)
|
||||
}
|
||||
|
||||
func (s *MainTestSuite) TestCallingInitConfigTwiceDoesNotChangeConfig() {
|
||||
configurationInitialized = false
|
||||
err := InitConfig()
|
||||
s.Require().NoError(err)
|
||||
Config = newTestConfiguration()
|
||||
filePath := writeConfigurationFile(s.T(), "test.yaml", []byte("server:\n port: 5000\n"))
|
||||
oldArgs := os.Args
|
||||
defer func() { os.Args = oldArgs }()
|
||||
os.Args = append(os.Args, "-config", filePath)
|
||||
err = InitConfig()
|
||||
s.Require().Error(err)
|
||||
s.Equal(3000, Config.Server.Port)
|
||||
}
|
||||
|
||||
func (s *MainTestSuite) TestReadEnvironmentVariables() {
|
||||
var environmentTests = []struct {
|
||||
variableSuffix string
|
||||
valueToSet string
|
||||
expectedValue interface{}
|
||||
getTargetField func(*configuration) interface{}
|
||||
}{
|
||||
{"SERVER_PORT", "4000", 4000, getServerPort},
|
||||
{"SERVER_PORT", "hello", 3000, getServerPort},
|
||||
{"NOMAD_TOKEN", "ACCESS", "ACCESS", getNomadToken},
|
||||
{"NOMAD_TLS_ACTIVE", "true", true, getNomadTLSActive},
|
||||
{"NOMAD_TLS_ACTIVE", "hello", false, getNomadTLSActive},
|
||||
{"AWS_FUNCTIONS", "java11Exec go118Exec", []string{"java11Exec", "go118Exec"}, getAWSFunctions},
|
||||
}
|
||||
prefix := "POSEIDON_TEST"
|
||||
for _, testCase := range environmentTests {
|
||||
config := newTestConfiguration()
|
||||
environmentVariable := fmt.Sprintf("%s_%s", prefix, testCase.variableSuffix)
|
||||
_ = os.Setenv(environmentVariable, testCase.valueToSet)
|
||||
readFromEnvironment(prefix, config.getReflectValue())
|
||||
_ = os.Unsetenv(environmentVariable)
|
||||
s.Equal(testCase.expectedValue, testCase.getTargetField(config))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *MainTestSuite) TestReadEnvironmentIgnoresNonPointerValue() {
|
||||
config := newTestConfiguration()
|
||||
_ = os.Setenv("POSEIDON_TEST_SERVER_PORT", "4000")
|
||||
readFromEnvironment("POSEIDON_TEST", reflect.ValueOf(config))
|
||||
_ = os.Unsetenv("POSEIDON_TEST_SERVER_PORT")
|
||||
s.Equal(3000, config.Server.Port)
|
||||
}
|
||||
|
||||
func (s *MainTestSuite) TestReadEnvironmentIgnoresNotSupportedType() {
|
||||
config := &struct{ Timeout float64 }{1.0}
|
||||
_ = os.Setenv("POSEIDON_TEST_TIMEOUT", "2.5")
|
||||
readFromEnvironment("POSEIDON_TEST", reflect.ValueOf(config).Elem())
|
||||
_ = os.Unsetenv("POSEIDON_TEST_TIMEOUT")
|
||||
s.Equal(1.0, config.Timeout)
|
||||
}
|
||||
|
||||
func (s *MainTestSuite) TestUnsetEnvironmentVariableDoesNotChangeConfig() {
|
||||
config := newTestConfiguration()
|
||||
readFromEnvironment("POSEIDON_TEST", config.getReflectValue())
|
||||
s.Equal("INFO", config.Logger.Level)
|
||||
}
|
||||
|
||||
func (s *MainTestSuite) TestReadYamlConfigFile() {
|
||||
var yamlTests = []struct {
|
||||
content []byte
|
||||
expectedValue interface{}
|
||||
getTargetField func(*configuration) interface{}
|
||||
}{
|
||||
{[]byte("server:\n port: 5000\n"), 5000, getServerPort},
|
||||
{[]byte("nomad:\n token: ACCESS\n"), "ACCESS", getNomadToken},
|
||||
{[]byte("nomad:\n tls:\n active: true\n"), true, getNomadTLSActive},
|
||||
{[]byte(""), false, getNomadTLSActive},
|
||||
{[]byte("nomad:\n token:\n"), "SECRET", getNomadToken},
|
||||
{[]byte("aws:\n functions:\n - java11Exec\n - go118Exec\n"),
|
||||
[]string{"java11Exec", "go118Exec"}, getAWSFunctions},
|
||||
}
|
||||
for _, testCase := range yamlTests {
|
||||
config := newTestConfiguration()
|
||||
config.mergeYaml(testCase.content)
|
||||
s.Equal(testCase.expectedValue, testCase.getTargetField(config))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *MainTestSuite) TestInvalidYamlExitsProgram() {
|
||||
logger, hook := test.NewNullLogger()
|
||||
// this function is used when calling log.Fatal() and
|
||||
// prevents the program from exiting during this test
|
||||
logger.ExitFunc = func(code int) {}
|
||||
log = logger.WithField("package", "config_test")
|
||||
config := newTestConfiguration()
|
||||
config.mergeYaml([]byte("logger: level: DEBUG"))
|
||||
s.Equal(1, len(hook.Entries))
|
||||
s.Equal(logrus.FatalLevel, hook.LastEntry().Level)
|
||||
}
|
||||
|
||||
func (s *MainTestSuite) TestReadConfigFileOverwritesConfig() {
|
||||
Config = newTestConfiguration()
|
||||
filePath := writeConfigurationFile(s.T(), "test.yaml", []byte("server:\n port: 5000\n"))
|
||||
oldArgs := os.Args
|
||||
defer func() { os.Args = oldArgs }()
|
||||
os.Args = append(os.Args, "-config", filePath)
|
||||
configurationInitialized = false
|
||||
err := InitConfig()
|
||||
s.Require().NoError(err)
|
||||
s.Equal(5000, Config.Server.Port)
|
||||
}
|
||||
|
||||
func (s *MainTestSuite) TestReadNonExistingConfigFileDoesNotOverwriteConfig() {
|
||||
Config = newTestConfiguration()
|
||||
oldArgs := os.Args
|
||||
defer func() { os.Args = oldArgs }()
|
||||
os.Args = append(os.Args, "-config", "file_does_not_exist.yaml")
|
||||
configurationInitialized = false
|
||||
err := InitConfig()
|
||||
s.Require().NoError(err)
|
||||
s.Equal(3000, Config.Server.Port)
|
||||
}
|
||||
|
||||
func (s *MainTestSuite) TestURLParsing() {
|
||||
var urlTests = []struct {
|
||||
address string
|
||||
port int
|
||||
tls bool
|
||||
expectedScheme string
|
||||
expectedHost string
|
||||
}{
|
||||
{"localhost", 3000, false, "http", "localhost:3000"},
|
||||
{"127.0.0.1", 4000, true, "https", "127.0.0.1:4000"},
|
||||
}
|
||||
for _, testCase := range urlTests {
|
||||
url := parseURL(testCase.address, testCase.port, testCase.tls)
|
||||
s.Equal(testCase.expectedScheme, url.Scheme)
|
||||
s.Equal(testCase.expectedHost, url.Host)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *MainTestSuite) TestNomadAPIURL() {
|
||||
config := newTestConfiguration()
|
||||
s.Equal("http", config.Nomad.URL().Scheme)
|
||||
s.Equal("127.0.0.2:4646", config.Nomad.URL().Host)
|
||||
}
|
||||
|
||||
func (s *MainTestSuite) TestPoseidonAPIURL() {
|
||||
config := newTestConfiguration()
|
||||
s.Equal("http", config.Server.URL().Scheme)
|
||||
s.Equal("127.0.0.1:3000", config.Server.URL().Host)
|
||||
}
|
@ -1,121 +0,0 @@
|
||||
package environment
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/openHPI/poseidon/internal/runner"
|
||||
"github.com/openHPI/poseidon/pkg/dto"
|
||||
)
|
||||
|
||||
type AWSEnvironment struct {
|
||||
id dto.EnvironmentID
|
||||
awsEndpoint string
|
||||
onDestroyRunner runner.DestroyRunnerHandler
|
||||
}
|
||||
|
||||
func NewAWSEnvironment(onDestroyRunner runner.DestroyRunnerHandler) *AWSEnvironment {
|
||||
return &AWSEnvironment{onDestroyRunner: onDestroyRunner}
|
||||
}
|
||||
|
||||
func (a *AWSEnvironment) MarshalJSON() ([]byte, error) {
|
||||
res, err := json.Marshal(dto.ExecutionEnvironmentData{
|
||||
ID: int(a.ID()),
|
||||
ExecutionEnvironmentRequest: dto.ExecutionEnvironmentRequest{Image: a.Image()},
|
||||
})
|
||||
if err != nil {
|
||||
return res, fmt.Errorf("couldn't marshal aws execution environment: %w", err)
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (a *AWSEnvironment) ID() dto.EnvironmentID {
|
||||
return a.id
|
||||
}
|
||||
|
||||
func (a *AWSEnvironment) SetID(id dto.EnvironmentID) {
|
||||
a.id = id
|
||||
}
|
||||
|
||||
// Image is used to specify the AWS Endpoint Poseidon is connecting to.
|
||||
func (a *AWSEnvironment) Image() string {
|
||||
return a.awsEndpoint
|
||||
}
|
||||
|
||||
func (a *AWSEnvironment) SetImage(awsEndpoint string) {
|
||||
a.awsEndpoint = awsEndpoint
|
||||
}
|
||||
|
||||
func (a *AWSEnvironment) Delete(_ runner.DestroyReason) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *AWSEnvironment) Sample() (r runner.Runner, ok bool) {
|
||||
workload, err := runner.NewAWSFunctionWorkload(a, a.onDestroyRunner)
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
return workload, true
|
||||
}
|
||||
|
||||
// The following methods are not supported at this moment.
|
||||
|
||||
// IdleRunnerCount is not supported as we have no information about the AWS managed prewarming pool.
|
||||
// For the Poseidon Health check we default to 1.
|
||||
func (a *AWSEnvironment) IdleRunnerCount() uint {
|
||||
return 1
|
||||
}
|
||||
|
||||
// PrewarmingPoolSize is neither supported nor required. It is handled transparently by AWS.
|
||||
// For easy compatibility with CodeOcean, 1 is the static value.
|
||||
func (a *AWSEnvironment) PrewarmingPoolSize() uint {
|
||||
return 1
|
||||
}
|
||||
|
||||
// SetPrewarmingPoolSize is neither supported nor required. It is handled transparently by AWS.
|
||||
func (a *AWSEnvironment) SetPrewarmingPoolSize(_ uint) {}
|
||||
|
||||
// ApplyPrewarmingPoolSize is neither supported nor required. It is handled transparently by AWS.
|
||||
func (a *AWSEnvironment) ApplyPrewarmingPoolSize() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// CPULimit is disabled as one can only set the memory limit with AWS Lambda.
|
||||
func (a *AWSEnvironment) CPULimit() uint {
|
||||
return 0
|
||||
}
|
||||
|
||||
// SetCPULimit is disabled as one can only set the memory limit with AWS Lambda.
|
||||
func (a *AWSEnvironment) SetCPULimit(_ uint) {}
|
||||
|
||||
func (a *AWSEnvironment) MemoryLimit() uint {
|
||||
const memorySizeOfDeployedLambdaFunction = 2048 // configured /deploy/aws/template.yaml
|
||||
return memorySizeOfDeployedLambdaFunction
|
||||
}
|
||||
|
||||
func (a *AWSEnvironment) SetMemoryLimit(_ uint) {
|
||||
panic("not supported")
|
||||
}
|
||||
|
||||
func (a *AWSEnvironment) NetworkAccess() (enabled bool, mappedPorts []uint16) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (a *AWSEnvironment) SetNetworkAccess(_ bool, _ []uint16) {
|
||||
panic("not supported")
|
||||
}
|
||||
|
||||
func (a *AWSEnvironment) SetConfigFrom(_ runner.ExecutionEnvironment) {
|
||||
panic("not supported")
|
||||
}
|
||||
|
||||
func (a *AWSEnvironment) Register() error {
|
||||
panic("not supported")
|
||||
}
|
||||
|
||||
func (a *AWSEnvironment) AddRunner(_ runner.Runner) {
|
||||
panic("not supported")
|
||||
}
|
||||
|
||||
func (a *AWSEnvironment) DeleteRunner(_ string) (r runner.Runner, ok bool) {
|
||||
panic("not supported")
|
||||
}
|
@ -1,67 +0,0 @@
|
||||
package environment
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/openHPI/poseidon/internal/config"
|
||||
"github.com/openHPI/poseidon/internal/runner"
|
||||
"github.com/openHPI/poseidon/pkg/dto"
|
||||
)
|
||||
|
||||
// AWSEnvironmentManager contains no functionality at the moment.
|
||||
// IMPROVE: Create Lambda functions dynamically.
|
||||
type AWSEnvironmentManager struct {
|
||||
*AbstractManager
|
||||
}
|
||||
|
||||
func NewAWSEnvironmentManager(runnerManager runner.Manager) *AWSEnvironmentManager {
|
||||
return &AWSEnvironmentManager{&AbstractManager{nil, runnerManager}}
|
||||
}
|
||||
|
||||
func (a *AWSEnvironmentManager) List(fetch bool) ([]runner.ExecutionEnvironment, error) {
|
||||
list, err := a.NextHandler().List(fetch)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("aws wrapped: %w", err)
|
||||
}
|
||||
return append(list, a.runnerManager.ListEnvironments()...), nil
|
||||
}
|
||||
|
||||
func (a *AWSEnvironmentManager) Get(id dto.EnvironmentID, fetch bool) (runner.ExecutionEnvironment, error) {
|
||||
e, ok := a.runnerManager.GetEnvironment(id)
|
||||
if ok {
|
||||
return e, nil
|
||||
} else {
|
||||
e, err := a.NextHandler().Get(id, fetch)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("aws wrapped: %w", err)
|
||||
}
|
||||
return e, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (a *AWSEnvironmentManager) CreateOrUpdate(
|
||||
id dto.EnvironmentID, request dto.ExecutionEnvironmentRequest, ctx context.Context) (bool, error) {
|
||||
if !isAWSEnvironment(request) {
|
||||
isCreated, err := a.NextHandler().CreateOrUpdate(id, request, ctx)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("aws wrapped: %w", err)
|
||||
}
|
||||
return isCreated, nil
|
||||
}
|
||||
|
||||
_, ok := a.runnerManager.GetEnvironment(id)
|
||||
e := NewAWSEnvironment(a.runnerManager.Return)
|
||||
e.SetID(id)
|
||||
e.SetImage(request.Image)
|
||||
a.runnerManager.StoreEnvironment(e)
|
||||
return !ok, nil
|
||||
}
|
||||
|
||||
func isAWSEnvironment(request dto.ExecutionEnvironmentRequest) bool {
|
||||
for _, function := range config.Config.AWS.Functions {
|
||||
if request.Image == function {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
@ -1,122 +0,0 @@
|
||||
package environment
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/openHPI/poseidon/internal/config"
|
||||
"github.com/openHPI/poseidon/internal/runner"
|
||||
"github.com/openHPI/poseidon/pkg/dto"
|
||||
"github.com/openHPI/poseidon/tests"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type MainTestSuite struct {
|
||||
tests.MemoryLeakTestSuite
|
||||
}
|
||||
|
||||
func TestMainTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(MainTestSuite))
|
||||
}
|
||||
|
||||
func (s *MainTestSuite) TestAWSEnvironmentManager_CreateOrUpdate() {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
runnerManager := runner.NewAWSRunnerManager(ctx)
|
||||
m := NewAWSEnvironmentManager(runnerManager)
|
||||
uniqueImage := "java11Exec"
|
||||
|
||||
s.Run("can create default Java environment", func() {
|
||||
config.Config.AWS.Functions = []string{uniqueImage}
|
||||
_, err := m.CreateOrUpdate(
|
||||
tests.AnotherEnvironmentIDAsInteger, dto.ExecutionEnvironmentRequest{Image: uniqueImage}, context.Background())
|
||||
s.NoError(err)
|
||||
})
|
||||
|
||||
s.Run("can retrieve added environment", func() {
|
||||
environment, err := m.Get(tests.AnotherEnvironmentIDAsInteger, false)
|
||||
s.NoError(err)
|
||||
s.Equal(environment.Image(), uniqueImage)
|
||||
})
|
||||
|
||||
s.Run("non-handleable requests are forwarded to the next manager", func() {
|
||||
nextHandler := &ManagerHandlerMock{}
|
||||
nextHandler.On("CreateOrUpdate", mock.AnythingOfType("dto.EnvironmentID"),
|
||||
mock.AnythingOfType("dto.ExecutionEnvironmentRequest"), mock.Anything).Return(true, nil)
|
||||
m.SetNextHandler(nextHandler)
|
||||
|
||||
request := dto.ExecutionEnvironmentRequest{}
|
||||
_, err := m.CreateOrUpdate(tests.DefaultEnvironmentIDAsInteger, request, context.Background())
|
||||
s.NoError(err)
|
||||
nextHandler.AssertCalled(s.T(), "CreateOrUpdate",
|
||||
dto.EnvironmentID(tests.DefaultEnvironmentIDAsInteger), request, mock.Anything)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MainTestSuite) TestAWSEnvironmentManager_Get() {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
runnerManager := runner.NewAWSRunnerManager(ctx)
|
||||
m := NewAWSEnvironmentManager(runnerManager)
|
||||
|
||||
s.Run("Calls next handler when not found", func() {
|
||||
nextHandler := &ManagerHandlerMock{}
|
||||
nextHandler.On("Get", mock.AnythingOfType("dto.EnvironmentID"), mock.AnythingOfType("bool")).
|
||||
Return(nil, nil)
|
||||
m.SetNextHandler(nextHandler)
|
||||
|
||||
_, err := m.Get(tests.DefaultEnvironmentIDAsInteger, false)
|
||||
s.NoError(err)
|
||||
nextHandler.AssertCalled(s.T(), "Get", dto.EnvironmentID(tests.DefaultEnvironmentIDAsInteger), false)
|
||||
})
|
||||
|
||||
s.Run("Returns error when not found", func() {
|
||||
nextHandler := &AbstractManager{nil, nil}
|
||||
m.SetNextHandler(nextHandler)
|
||||
|
||||
_, err := m.Get(tests.DefaultEnvironmentIDAsInteger, false)
|
||||
s.ErrorIs(err, runner.ErrRunnerNotFound)
|
||||
})
|
||||
|
||||
s.Run("Returns environment when it was added before", func() {
|
||||
expectedEnvironment := NewAWSEnvironment(nil)
|
||||
expectedEnvironment.SetID(tests.DefaultEnvironmentIDAsInteger)
|
||||
runnerManager.StoreEnvironment(expectedEnvironment)
|
||||
|
||||
environment, err := m.Get(tests.DefaultEnvironmentIDAsInteger, false)
|
||||
s.NoError(err)
|
||||
s.Equal(expectedEnvironment, environment)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MainTestSuite) TestAWSEnvironmentManager_List() {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
runnerManager := runner.NewAWSRunnerManager(ctx)
|
||||
m := NewAWSEnvironmentManager(runnerManager)
|
||||
|
||||
s.Run("also returns environments of the rest of the manager chain", func() {
|
||||
nextHandler := &ManagerHandlerMock{}
|
||||
existingEnvironment := NewAWSEnvironment(nil)
|
||||
nextHandler.On("List", mock.AnythingOfType("bool")).
|
||||
Return([]runner.ExecutionEnvironment{existingEnvironment}, nil)
|
||||
m.SetNextHandler(nextHandler)
|
||||
|
||||
environments, err := m.List(false)
|
||||
s.NoError(err)
|
||||
s.Require().Len(environments, 1)
|
||||
s.Contains(environments, existingEnvironment)
|
||||
})
|
||||
m.SetNextHandler(nil)
|
||||
|
||||
s.Run("Returns added environment", func() {
|
||||
localEnvironment := NewAWSEnvironment(nil)
|
||||
localEnvironment.SetID(tests.DefaultEnvironmentIDAsInteger)
|
||||
runnerManager.StoreEnvironment(localEnvironment)
|
||||
|
||||
environments, err := m.List(false)
|
||||
s.NoError(err)
|
||||
s.Len(environments, 1)
|
||||
s.Contains(environments, localEnvironment)
|
||||
})
|
||||
}
|
175
internal/environment/kubernetes_environment.go
Normal file
175
internal/environment/kubernetes_environment.go
Normal file
@ -0,0 +1,175 @@
|
||||
package environment
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
poseidonK8s "github.com/openHPI/poseidon/internal/kubernetes"
|
||||
"github.com/openHPI/poseidon/internal/runner"
|
||||
"github.com/openHPI/poseidon/pkg/dto"
|
||||
"github.com/openHPI/poseidon/pkg/monitoring"
|
||||
"github.com/openHPI/poseidon/pkg/storage"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
"time"
|
||||
)
|
||||
|
||||
type KubernetesEnvironment struct {
|
||||
apiClient *poseidonK8s.ExecutorAPI
|
||||
jobHCL string
|
||||
deployment *appsv1.Deployment
|
||||
idleRunners storage.Storage[runner.Runner]
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
func (k KubernetesEnvironment) MarshalJSON() ([]byte, error) {
|
||||
//TODO implement me
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (k KubernetesEnvironment) ID() dto.EnvironmentID {
|
||||
//TODO implement me
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (k KubernetesEnvironment) SetID(id dto.EnvironmentID) {
|
||||
//TODO implement me
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (k KubernetesEnvironment) PrewarmingPoolSize() uint {
|
||||
//TODO implement me
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (k KubernetesEnvironment) SetPrewarmingPoolSize(count uint) {
|
||||
//TODO implement me
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (k KubernetesEnvironment) ApplyPrewarmingPoolSize() error {
|
||||
//TODO implement me
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (k KubernetesEnvironment) CPULimit() uint {
|
||||
//TODO implement me
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (k KubernetesEnvironment) SetCPULimit(limit uint) {
|
||||
//TODO implement me
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (k KubernetesEnvironment) MemoryLimit() uint {
|
||||
//TODO implement me
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (k KubernetesEnvironment) SetMemoryLimit(limit uint) {
|
||||
//TODO implement me
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (k KubernetesEnvironment) Image() string {
|
||||
//TODO implement me
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (k KubernetesEnvironment) SetImage(image string) {
|
||||
//TODO implement me
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (k KubernetesEnvironment) NetworkAccess() (bool, []uint16) {
|
||||
//TODO implement me
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (k KubernetesEnvironment) SetNetworkAccess(allow bool, ports []uint16) {
|
||||
//TODO implement me
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (k KubernetesEnvironment) SetConfigFrom(environment runner.ExecutionEnvironment) {
|
||||
//TODO implement me
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (k KubernetesEnvironment) Register() error {
|
||||
//TODO implement me
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (k KubernetesEnvironment) Delete(reason runner.DestroyReason) error {
|
||||
//TODO implement me
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (k KubernetesEnvironment) Sample() (r runner.Runner, ok bool) {
|
||||
//TODO implement me
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (k KubernetesEnvironment) AddRunner(r runner.Runner) {
|
||||
//TODO implement me
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (k KubernetesEnvironment) DeleteRunner(id string) (r runner.Runner, ok bool) {
|
||||
//TODO implement me
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (k KubernetesEnvironment) IdleRunnerCount() uint {
|
||||
//TODO implement me
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func NewKubernetesEnvironmentFromRequest(
|
||||
apiClient poseidonK8s.ExecutorAPI, jobHCL string, id dto.EnvironmentID, request dto.ExecutionEnvironmentRequest) (
|
||||
*KubernetesEnvironment, error) {
|
||||
environment, err := NewKubernetesEnvironment(id, apiClient, jobHCL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
environment.SetID(id)
|
||||
|
||||
// Set options according to request
|
||||
environment.SetPrewarmingPoolSize(request.PrewarmingPoolSize)
|
||||
environment.SetCPULimit(request.CPULimit)
|
||||
environment.SetMemoryLimit(request.MemoryLimit)
|
||||
environment.SetImage(request.Image)
|
||||
environment.SetNetworkAccess(request.NetworkAccess, request.ExposedPorts)
|
||||
return environment, nil
|
||||
}
|
||||
|
||||
func NewKubernetesEnvironment(id dto.EnvironmentID, apiClient poseidonK8s.ExecutorAPI, jobHCL string) (*KubernetesEnvironment, error) {
|
||||
job, err := parseDeployment(jobHCL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing Nomad job: %w", err)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
e := &KubernetesEnvironment{&apiClient, jobHCL, job, nil, ctx, cancel}
|
||||
e.idleRunners = storage.NewMonitoredLocalStorage[runner.Runner](monitoring.MeasurementIdleRunnerNomad,
|
||||
runner.MonitorEnvironmentID[runner.Runner](id), time.Minute, ctx)
|
||||
return e, nil
|
||||
}
|
||||
|
||||
// TODO MISSING IMPLEMENTATION
|
||||
func parseDeployment(jobHCL string) (*appsv1.Deployment, error) {
|
||||
|
||||
deployment := appsv1.Deployment{}
|
||||
|
||||
// jobConfig := jobspec2.ParseConfig{
|
||||
// Body: []byte(jobHCL),
|
||||
// AllowFS: false,
|
||||
// Strict: true,
|
||||
// }
|
||||
// job, err := jobspec2.ParseWithConfig(&jobConfig)
|
||||
// if err != nil {
|
||||
// return job, fmt.Errorf("couldn't parse job HCL: %w", err)
|
||||
// }
|
||||
return &deployment, nil
|
||||
}
|
297
internal/environment/kubernetes_manager.go
Normal file
297
internal/environment/kubernetes_manager.go
Normal file
@ -0,0 +1,297 @@
|
||||
package environment
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
poseidonK8s "github.com/openHPI/poseidon/internal/kubernetes"
|
||||
"github.com/openHPI/poseidon/internal/nomad"
|
||||
"github.com/openHPI/poseidon/internal/runner"
|
||||
"github.com/openHPI/poseidon/pkg/dto"
|
||||
"github.com/openHPI/poseidon/pkg/logging"
|
||||
"github.com/openHPI/poseidon/pkg/monitoring"
|
||||
"github.com/openHPI/poseidon/pkg/storage"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type KubernetesEnvironmentManager struct {
|
||||
*AbstractManager
|
||||
api poseidonK8s.ExecutorAPI
|
||||
templateEnvironmentHCL string
|
||||
}
|
||||
|
||||
func NewKubernetesEnvironmentManager(
|
||||
runnerManager runner.Manager,
|
||||
apiClient *poseidonK8s.ExecutorAPI,
|
||||
templateJobFile string,
|
||||
) (*KubernetesEnvironmentManager, error) {
|
||||
if err := loadTemplateEnvironmentJobHCL(templateJobFile); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m := &KubernetesEnvironmentManager{
|
||||
AbstractManager: &AbstractManager{nil, runnerManager},
|
||||
api: *apiClient,
|
||||
templateEnvironmentHCL: templateEnvironmentJobHCL,
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (k *KubernetesEnvironmentManager) SetNextHandler(next ManagerHandler) {
|
||||
k.nextHandler = next
|
||||
}
|
||||
|
||||
func (k *KubernetesEnvironmentManager) NextHandler() ManagerHandler {
|
||||
if k.HasNextHandler() {
|
||||
return k.nextHandler
|
||||
} else {
|
||||
return &AbstractManager{}
|
||||
}
|
||||
}
|
||||
|
||||
func (k *KubernetesEnvironmentManager) HasNextHandler() bool {
|
||||
return k.nextHandler != nil
|
||||
}
|
||||
|
||||
// List all Kubernetes-based environments
|
||||
func (k *KubernetesEnvironmentManager) List(fetch bool) ([]runner.ExecutionEnvironment, error) {
|
||||
if fetch {
|
||||
if err := k.fetchEnvironments(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return k.runnerManager.ListEnvironments(), nil
|
||||
}
|
||||
|
||||
func (k *KubernetesEnvironmentManager) fetchEnvironments() error {
|
||||
remoteDeploymentResponse, err := k.api.LoadEnvironmentJobs()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed fetching environments: %w", err)
|
||||
}
|
||||
|
||||
remoteDeployments := make(map[string]appsv1.Deployment)
|
||||
|
||||
// Update local environments from remote environments.
|
||||
for _, deployment := range remoteDeploymentResponse {
|
||||
remoteDeployments[deployment.Name] = *deployment
|
||||
|
||||
// Job Id to Environment Id Integer
|
||||
intIdentifier, err := strconv.Atoi(deployment.Name)
|
||||
if err != nil {
|
||||
log.WithError(err).Warn("Failed to convert job name to int")
|
||||
continue
|
||||
}
|
||||
id := dto.EnvironmentID(intIdentifier)
|
||||
|
||||
if localEnvironment, ok := k.runnerManager.GetEnvironment(id); ok {
|
||||
fetchedEnvironment := newKubernetesEnvironmentFromJob(deployment, &k.api)
|
||||
localEnvironment.SetConfigFrom(fetchedEnvironment)
|
||||
// We destroy only this (second) local reference to the environment.
|
||||
if err = fetchedEnvironment.Delete(runner.ErrDestroyedAndReplaced); err != nil {
|
||||
log.WithError(err).Warn("Failed to remove environment locally")
|
||||
}
|
||||
} else {
|
||||
k.runnerManager.StoreEnvironment(newKubernetesEnvironmentFromJob(deployment, &k.api))
|
||||
}
|
||||
}
|
||||
|
||||
// Remove local environments that are not remote environments.
|
||||
for _, localEnvironment := range k.runnerManager.ListEnvironments() {
|
||||
if _, ok := remoteDeployments[localEnvironment.ID().ToString()]; !ok {
|
||||
err := localEnvironment.Delete(runner.ErrLocalDestruction)
|
||||
log.WithError(err).Warn("Failed to remove environment locally")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// newNomadEnvironmentFromJob creates a Nomad environment from the passed Nomad job definition.
|
||||
func newKubernetesEnvironmentFromJob(deployment *appsv1.Deployment, apiClient *poseidonK8s.ExecutorAPI) *KubernetesEnvironment {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
e := &KubernetesEnvironment{
|
||||
apiClient: apiClient,
|
||||
jobHCL: templateEnvironmentJobHCL,
|
||||
deployment: deployment,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
}
|
||||
e.idleRunners = storage.NewMonitoredLocalStorage[runner.Runner](monitoring.MeasurementIdleRunnerNomad,
|
||||
runner.MonitorEnvironmentID[runner.Runner](e.ID()), time.Minute, ctx)
|
||||
return e
|
||||
}
|
||||
|
||||
// Get retrieves a specific Kubernetes environment
|
||||
func (k *KubernetesEnvironmentManager) Get(id dto.EnvironmentID, fetch bool) (executionEnvironment runner.ExecutionEnvironment, err error) {
|
||||
executionEnvironment, ok := k.runnerManager.GetEnvironment(id)
|
||||
|
||||
if fetch {
|
||||
fetchedEnvironment, err := fetchK8sEnvironment(id, k.api)
|
||||
switch {
|
||||
case err != nil:
|
||||
return nil, err
|
||||
case fetchedEnvironment == nil:
|
||||
_, err = k.Delete(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ok = false
|
||||
case !ok:
|
||||
k.runnerManager.StoreEnvironment(fetchedEnvironment)
|
||||
executionEnvironment = fetchedEnvironment
|
||||
ok = true
|
||||
default:
|
||||
executionEnvironment.SetConfigFrom(fetchedEnvironment)
|
||||
// We destroy only this (second) local reference to the environment.
|
||||
err = fetchedEnvironment.Delete(runner.ErrDestroyedAndReplaced)
|
||||
if err != nil {
|
||||
log.WithError(err).Warn("Failed to remove environment locally")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !ok {
|
||||
err = runner.ErrUnknownExecutionEnvironment
|
||||
}
|
||||
return executionEnvironment, err
|
||||
}
|
||||
|
||||
// CreateOrUpdate creates or updates an environment in Kubernetes
|
||||
func (k *KubernetesEnvironmentManager) CreateOrUpdate(
|
||||
id dto.EnvironmentID, request dto.ExecutionEnvironmentRequest, ctx context.Context) (created bool, err error) {
|
||||
|
||||
// Check if execution environment is already existing (in the local memory).
|
||||
environment, isExistingEnvironment := k.runnerManager.GetEnvironment(id)
|
||||
if isExistingEnvironment {
|
||||
// Remove existing environment to force downloading the newest Docker image.
|
||||
// See https://github.com/openHPI/poseidon/issues/69
|
||||
err = environment.Delete(runner.ErrEnvironmentUpdated)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to remove the environment: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Create a new environment with the given request options.
|
||||
environment, err = NewKubernetesEnvironmentFromRequest(k.api, k.templateEnvironmentHCL, id, request)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("error creating Nomad environment: %w", err)
|
||||
}
|
||||
|
||||
// Keep a copy of environment specification in memory.
|
||||
k.runnerManager.StoreEnvironment(environment)
|
||||
|
||||
// Register template Job with Nomad.
|
||||
logging.StartSpan("env.update.register", "Register Environment", ctx, func(_ context.Context) {
|
||||
err = environment.Register()
|
||||
})
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("error registering template job in API: %w", err)
|
||||
}
|
||||
|
||||
// Launch idle runners based on the template job.
|
||||
logging.StartSpan("env.update.poolsize", "Apply Prewarming Pool Size", ctx, func(_ context.Context) {
|
||||
err = environment.ApplyPrewarmingPoolSize()
|
||||
})
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("error scaling template job in API: %w", err)
|
||||
}
|
||||
|
||||
return !isExistingEnvironment, nil
|
||||
|
||||
}
|
||||
|
||||
// Statistics fetches statistics from Kubernetes
|
||||
func (k *KubernetesEnvironmentManager) Statistics() map[dto.EnvironmentID]*dto.StatisticalExecutionEnvironmentData {
|
||||
// Collect and return statistics for Kubernetes environments
|
||||
return map[dto.EnvironmentID]*dto.StatisticalExecutionEnvironmentData{}
|
||||
}
|
||||
|
||||
// MapExecutionEnvironmentRequestToDeployment maps ExecutionEnvironmentRequest to a Kubernetes Deployment
|
||||
func MapExecutionEnvironmentRequestToDeployment(req dto.ExecutionEnvironmentRequest, environmentID string) *appsv1.Deployment {
|
||||
// Create the Deployment object
|
||||
deployment := &appsv1.Deployment{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "Deployment",
|
||||
APIVersion: "apps/v1",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: environmentID, // Set the environment ID as the name of the deployment
|
||||
Labels: map[string]string{
|
||||
"environment-id": environmentID,
|
||||
},
|
||||
},
|
||||
Spec: appsv1.DeploymentSpec{
|
||||
Replicas: int32Ptr(int32(req.PrewarmingPoolSize)), // Use PrewarmingPoolSize to set the number of replicas
|
||||
Selector: &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{
|
||||
"environment-id": environmentID,
|
||||
},
|
||||
},
|
||||
Template: v1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{
|
||||
"environment-id": environmentID,
|
||||
},
|
||||
},
|
||||
Spec: v1.PodSpec{
|
||||
Containers: []v1.Container{
|
||||
{
|
||||
Name: "runner-container",
|
||||
Image: req.Image, // Map the image to the container
|
||||
Resources: v1.ResourceRequirements{
|
||||
Requests: v1.ResourceList{
|
||||
"cpu": resource.MustParse(strconv.Itoa(int(req.CPULimit))), // Map CPU request
|
||||
"memory": resource.MustParse(strconv.Itoa(int(req.MemoryLimit)) + "Mi"), // Map Memory request
|
||||
},
|
||||
Limits: v1.ResourceList{
|
||||
"cpu": resource.MustParse(strconv.Itoa(int(req.CPULimit))), // Map CPU limit
|
||||
"memory": resource.MustParse(strconv.Itoa(int(req.MemoryLimit)) + "Mi"), // Map Memory limit
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Handle network access and exposed ports
|
||||
if req.NetworkAccess {
|
||||
var containerPorts []v1.ContainerPort
|
||||
for _, port := range req.ExposedPorts {
|
||||
containerPorts = append(containerPorts, v1.ContainerPort{
|
||||
ContainerPort: int32(port),
|
||||
})
|
||||
}
|
||||
deployment.Spec.Template.Spec.Containers[0].Ports = containerPorts
|
||||
}
|
||||
|
||||
return deployment
|
||||
}
|
||||
|
||||
// Helper function to return a pointer to an int32
|
||||
func int32Ptr(i int32) *int32 {
|
||||
return &i
|
||||
}
|
||||
|
||||
func fetchK8sEnvironment(id dto.EnvironmentID, apiClient poseidonK8s.ExecutorAPI) (runner.ExecutionEnvironment, error) {
|
||||
environments, err := apiClient.LoadEnvironmentJobs()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error fetching the environment jobs: %w", err)
|
||||
}
|
||||
var fetchedEnvironment runner.ExecutionEnvironment
|
||||
for _, deployment := range environments {
|
||||
environmentID, err := nomad.EnvironmentIDFromTemplateJobID(deployment.Name)
|
||||
if err != nil {
|
||||
log.WithError(err).Warn("Cannot parse environment id of loaded environment")
|
||||
continue
|
||||
}
|
||||
if id == environmentID {
|
||||
fetchedEnvironment = newKubernetesEnvironmentFromJob(deployment, &apiClient)
|
||||
}
|
||||
}
|
||||
return fetchedEnvironment, nil
|
||||
}
|
@ -1,171 +0,0 @@
|
||||
// Code generated by mockery v2.16.0. DO NOT EDIT.
|
||||
|
||||
package environment
|
||||
|
||||
import (
|
||||
context "context"
|
||||
|
||||
dto "github.com/openHPI/poseidon/pkg/dto"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
|
||||
runner "github.com/openHPI/poseidon/internal/runner"
|
||||
)
|
||||
|
||||
// ManagerHandlerMock is an autogenerated mock type for the ManagerHandler type
|
||||
type ManagerHandlerMock struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// CreateOrUpdate provides a mock function with given fields: id, request, ctx
|
||||
func (_m *ManagerHandlerMock) CreateOrUpdate(id dto.EnvironmentID, request dto.ExecutionEnvironmentRequest, ctx context.Context) (bool, error) {
|
||||
ret := _m.Called(id, request, ctx)
|
||||
|
||||
var r0 bool
|
||||
if rf, ok := ret.Get(0).(func(dto.EnvironmentID, dto.ExecutionEnvironmentRequest, context.Context) bool); ok {
|
||||
r0 = rf(id, request, ctx)
|
||||
} else {
|
||||
r0 = ret.Get(0).(bool)
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(dto.EnvironmentID, dto.ExecutionEnvironmentRequest, context.Context) error); ok {
|
||||
r1 = rf(id, request, ctx)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Delete provides a mock function with given fields: id
|
||||
func (_m *ManagerHandlerMock) Delete(id dto.EnvironmentID) (bool, error) {
|
||||
ret := _m.Called(id)
|
||||
|
||||
var r0 bool
|
||||
if rf, ok := ret.Get(0).(func(dto.EnvironmentID) bool); ok {
|
||||
r0 = rf(id)
|
||||
} else {
|
||||
r0 = ret.Get(0).(bool)
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(dto.EnvironmentID) error); ok {
|
||||
r1 = rf(id)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Get provides a mock function with given fields: id, fetch
|
||||
func (_m *ManagerHandlerMock) Get(id dto.EnvironmentID, fetch bool) (runner.ExecutionEnvironment, error) {
|
||||
ret := _m.Called(id, fetch)
|
||||
|
||||
var r0 runner.ExecutionEnvironment
|
||||
if rf, ok := ret.Get(0).(func(dto.EnvironmentID, bool) runner.ExecutionEnvironment); ok {
|
||||
r0 = rf(id, fetch)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(runner.ExecutionEnvironment)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(dto.EnvironmentID, bool) error); ok {
|
||||
r1 = rf(id, fetch)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// HasNextHandler provides a mock function with given fields:
|
||||
func (_m *ManagerHandlerMock) HasNextHandler() bool {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 bool
|
||||
if rf, ok := ret.Get(0).(func() bool); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Get(0).(bool)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// List provides a mock function with given fields: fetch
|
||||
func (_m *ManagerHandlerMock) List(fetch bool) ([]runner.ExecutionEnvironment, error) {
|
||||
ret := _m.Called(fetch)
|
||||
|
||||
var r0 []runner.ExecutionEnvironment
|
||||
if rf, ok := ret.Get(0).(func(bool) []runner.ExecutionEnvironment); ok {
|
||||
r0 = rf(fetch)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]runner.ExecutionEnvironment)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(bool) error); ok {
|
||||
r1 = rf(fetch)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// NextHandler provides a mock function with given fields:
|
||||
func (_m *ManagerHandlerMock) NextHandler() ManagerHandler {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 ManagerHandler
|
||||
if rf, ok := ret.Get(0).(func() ManagerHandler); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(ManagerHandler)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// SetNextHandler provides a mock function with given fields: next
|
||||
func (_m *ManagerHandlerMock) SetNextHandler(next ManagerHandler) {
|
||||
_m.Called(next)
|
||||
}
|
||||
|
||||
// Statistics provides a mock function with given fields:
|
||||
func (_m *ManagerHandlerMock) Statistics() map[dto.EnvironmentID]*dto.StatisticalExecutionEnvironmentData {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 map[dto.EnvironmentID]*dto.StatisticalExecutionEnvironmentData
|
||||
if rf, ok := ret.Get(0).(func() map[dto.EnvironmentID]*dto.StatisticalExecutionEnvironmentData); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(map[dto.EnvironmentID]*dto.StatisticalExecutionEnvironmentData)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
type mockConstructorTestingTNewManagerHandlerMock interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}
|
||||
|
||||
// NewManagerHandlerMock creates a new instance of ManagerHandlerMock. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
func NewManagerHandlerMock(t mockConstructorTestingTNewManagerHandlerMock) *ManagerHandlerMock {
|
||||
mock := &ManagerHandlerMock{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
@ -1,265 +0,0 @@
|
||||
package environment
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
nomadApi "github.com/hashicorp/nomad/api"
|
||||
"github.com/openHPI/poseidon/internal/config"
|
||||
"github.com/openHPI/poseidon/internal/nomad"
|
||||
"github.com/openHPI/poseidon/internal/runner"
|
||||
"github.com/openHPI/poseidon/pkg/storage"
|
||||
"github.com/openHPI/poseidon/tests"
|
||||
"github.com/openHPI/poseidon/tests/helpers"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (s *MainTestSuite) TestConfigureNetworkCreatesNewNetworkWhenNoNetworkExists() {
|
||||
_, job := helpers.CreateTemplateJob()
|
||||
defaultTaskGroup := nomad.FindAndValidateDefaultTaskGroup(job)
|
||||
environment := &NomadEnvironment{nil, "", job, nil, context.Background(), nil}
|
||||
|
||||
if s.Equal(0, len(defaultTaskGroup.Networks)) {
|
||||
environment.SetNetworkAccess(true, []uint16{})
|
||||
|
||||
s.Equal(1, len(defaultTaskGroup.Networks))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *MainTestSuite) TestConfigureNetworkDoesNotCreateNewNetworkWhenNetworkExists() {
|
||||
_, job := helpers.CreateTemplateJob()
|
||||
defaultTaskGroup := nomad.FindAndValidateDefaultTaskGroup(job)
|
||||
environment := &NomadEnvironment{nil, "", job, nil, context.Background(), nil}
|
||||
|
||||
networkResource := config.Config.Nomad.Network
|
||||
defaultTaskGroup.Networks = []*nomadApi.NetworkResource{&networkResource}
|
||||
|
||||
if s.Equal(1, len(defaultTaskGroup.Networks)) {
|
||||
environment.SetNetworkAccess(true, []uint16{})
|
||||
|
||||
s.Equal(1, len(defaultTaskGroup.Networks))
|
||||
s.Equal(&networkResource, defaultTaskGroup.Networks[0])
|
||||
}
|
||||
}
|
||||
|
||||
func (s *MainTestSuite) TestConfigureNetworkSetsCorrectValues() {
|
||||
_, job := helpers.CreateTemplateJob()
|
||||
defaultTaskGroup := nomad.FindAndValidateDefaultTaskGroup(job)
|
||||
defaultTask := nomad.FindAndValidateDefaultTask(defaultTaskGroup)
|
||||
|
||||
mode, ok := defaultTask.Config["network_mode"]
|
||||
s.True(ok)
|
||||
s.Equal("none", mode)
|
||||
s.Equal(0, len(defaultTaskGroup.Networks))
|
||||
|
||||
exposedPortsTests := [][]uint16{{}, {1337}, {42, 1337}}
|
||||
s.Run("with no network access", func() {
|
||||
for _, ports := range exposedPortsTests {
|
||||
_, testJob := helpers.CreateTemplateJob()
|
||||
testTaskGroup := nomad.FindAndValidateDefaultTaskGroup(testJob)
|
||||
testTask := nomad.FindAndValidateDefaultTask(testTaskGroup)
|
||||
testEnvironment := &NomadEnvironment{nil, "", job, nil, context.Background(), nil}
|
||||
|
||||
testEnvironment.SetNetworkAccess(false, ports)
|
||||
mode, ok := testTask.Config["network_mode"]
|
||||
s.True(ok)
|
||||
s.Equal("none", mode)
|
||||
s.Equal(0, len(testTaskGroup.Networks))
|
||||
}
|
||||
})
|
||||
|
||||
s.Run("with network access", func() {
|
||||
for _, ports := range exposedPortsTests {
|
||||
_, testJob := helpers.CreateTemplateJob()
|
||||
testTaskGroup := nomad.FindAndValidateDefaultTaskGroup(testJob)
|
||||
testTask := nomad.FindAndValidateDefaultTask(testTaskGroup)
|
||||
testEnvironment := &NomadEnvironment{nil, "", testJob, nil, context.Background(), nil}
|
||||
|
||||
testEnvironment.SetNetworkAccess(true, ports)
|
||||
s.Require().Equal(1, len(testTaskGroup.Networks))
|
||||
|
||||
networkResource := testTaskGroup.Networks[0]
|
||||
s.Equal(config.Config.Nomad.Network.Mode, networkResource.Mode)
|
||||
s.Require().Equal(len(ports), len(networkResource.DynamicPorts))
|
||||
|
||||
assertExpectedPorts(s.T(), ports, networkResource)
|
||||
|
||||
mode, ok := testTask.Config["network_mode"]
|
||||
s.True(ok)
|
||||
s.Equal(mode, "")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func assertExpectedPorts(t *testing.T, expectedPorts []uint16, networkResource *nomadApi.NetworkResource) {
|
||||
t.Helper()
|
||||
for _, expectedPort := range expectedPorts {
|
||||
found := false
|
||||
for _, actualPort := range networkResource.DynamicPorts {
|
||||
if actualPort.To == int(expectedPort) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.True(t, found, fmt.Sprintf("port list should contain %v", expectedPort))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *MainTestSuite) TestRegisterFailsWhenNomadJobRegistrationFails() {
|
||||
apiClientMock := &nomad.ExecutorAPIMock{}
|
||||
expectedErr := tests.ErrDefault
|
||||
|
||||
apiClientMock.On("RegisterNomadJob", mock.AnythingOfType("*api.Job")).Return("", expectedErr)
|
||||
apiClientMock.On("LoadRunnerIDs", mock.AnythingOfType("string")).Return([]string{}, nil)
|
||||
apiClientMock.On("DeleteJob", mock.AnythingOfType("string")).Return(nil)
|
||||
|
||||
environment := &NomadEnvironment{apiClientMock, "", &nomadApi.Job{},
|
||||
storage.NewLocalStorage[runner.Runner](), nil, nil}
|
||||
environment.SetID(tests.DefaultEnvironmentIDAsInteger)
|
||||
err := environment.Register()
|
||||
|
||||
s.ErrorIs(err, expectedErr)
|
||||
apiClientMock.AssertNotCalled(s.T(), "MonitorEvaluation")
|
||||
}
|
||||
|
||||
func (s *MainTestSuite) TestRegisterTemplateJobSucceedsWhenMonitoringEvaluationSucceeds() {
|
||||
apiClientMock := &nomad.ExecutorAPIMock{}
|
||||
evaluationID := "id"
|
||||
|
||||
apiClientMock.On("RegisterNomadJob", mock.AnythingOfType("*api.Job")).Return(evaluationID, nil)
|
||||
apiClientMock.On("MonitorEvaluation", mock.AnythingOfType("string"), mock.Anything).Return(nil)
|
||||
apiClientMock.On("LoadRunnerIDs", mock.AnythingOfType("string")).Return([]string{}, nil)
|
||||
apiClientMock.On("DeleteJob", mock.AnythingOfType("string")).Return(nil)
|
||||
|
||||
environment := &NomadEnvironment{apiClientMock, "", &nomadApi.Job{},
|
||||
storage.NewLocalStorage[runner.Runner](), context.Background(), nil}
|
||||
environment.SetID(tests.DefaultEnvironmentIDAsInteger)
|
||||
err := environment.Register()
|
||||
|
||||
s.NoError(err)
|
||||
}
|
||||
|
||||
func (s *MainTestSuite) TestRegisterTemplateJobReturnsErrorWhenMonitoringEvaluationFails() {
|
||||
apiClientMock := &nomad.ExecutorAPIMock{}
|
||||
evaluationID := "id"
|
||||
|
||||
apiClientMock.On("RegisterNomadJob", mock.AnythingOfType("*api.Job")).Return(evaluationID, nil)
|
||||
apiClientMock.On("MonitorEvaluation", mock.AnythingOfType("string"), mock.Anything).Return(tests.ErrDefault)
|
||||
apiClientMock.On("LoadRunnerIDs", mock.AnythingOfType("string")).Return([]string{}, nil)
|
||||
apiClientMock.On("DeleteJob", mock.AnythingOfType("string")).Return(nil)
|
||||
|
||||
environment := &NomadEnvironment{apiClientMock, "", &nomadApi.Job{},
|
||||
storage.NewLocalStorage[runner.Runner](), context.Background(), nil}
|
||||
environment.SetID(tests.DefaultEnvironmentIDAsInteger)
|
||||
err := environment.Register()
|
||||
|
||||
s.ErrorIs(err, tests.ErrDefault)
|
||||
}
|
||||
|
||||
func (s *MainTestSuite) TestParseJob() {
|
||||
apiMock := &nomad.ExecutorAPIMock{}
|
||||
apiMock.On("LoadRunnerIDs", mock.AnythingOfType("string")).Return([]string{}, nil)
|
||||
apiMock.On("DeleteJob", mock.AnythingOfType("string")).Return(nil)
|
||||
s.Run("parses the given default job", func() {
|
||||
environment, err := NewNomadEnvironment(tests.DefaultEnvironmentIDAsInteger, apiMock, templateEnvironmentJobHCL)
|
||||
s.NoError(err)
|
||||
s.NotNil(environment.job)
|
||||
s.NoError(environment.Delete(tests.ErrCleanupDestroyReason))
|
||||
})
|
||||
|
||||
s.Run("returns error when given wrong job", func() {
|
||||
environment, err := NewNomadEnvironment(tests.DefaultEnvironmentIDAsInteger, nil, "")
|
||||
s.Error(err)
|
||||
s.Nil(environment)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MainTestSuite) TestTwoSampleAddExactlyTwoRunners() {
|
||||
apiMock := &nomad.ExecutorAPIMock{}
|
||||
apiMock.On("RegisterRunnerJob", mock.AnythingOfType("*api.Job")).Return(nil)
|
||||
|
||||
_, job := helpers.CreateTemplateJob()
|
||||
environment := &NomadEnvironment{apiMock, templateEnvironmentJobHCL, job,
|
||||
storage.NewLocalStorage[runner.Runner](), context.Background(), nil}
|
||||
environment.SetPrewarmingPoolSize(2)
|
||||
runner1 := &runner.RunnerMock{}
|
||||
runner1.On("ID").Return(tests.DefaultRunnerID)
|
||||
runner2 := &runner.RunnerMock{}
|
||||
runner2.On("ID").Return(tests.AnotherRunnerID)
|
||||
|
||||
environment.AddRunner(runner1)
|
||||
environment.AddRunner(runner2)
|
||||
|
||||
_, ok := environment.Sample()
|
||||
s.Require().True(ok)
|
||||
_, ok = environment.Sample()
|
||||
s.Require().True(ok)
|
||||
|
||||
<-time.After(tests.ShortTimeout) // New Runners are requested asynchronously
|
||||
apiMock.AssertNumberOfCalls(s.T(), "RegisterRunnerJob", 2)
|
||||
}
|
||||
|
||||
func (s *MainTestSuite) TestSampleDoesNotSetForcePullFlag() {
|
||||
apiMock := &nomad.ExecutorAPIMock{}
|
||||
call := apiMock.On("RegisterRunnerJob", mock.AnythingOfType("*api.Job"))
|
||||
call.Run(func(args mock.Arguments) {
|
||||
job, ok := args.Get(0).(*nomadApi.Job)
|
||||
s.True(ok)
|
||||
|
||||
taskGroup := nomad.FindAndValidateDefaultTaskGroup(job)
|
||||
task := nomad.FindAndValidateDefaultTask(taskGroup)
|
||||
s.False(task.Config["force_pull"].(bool))
|
||||
|
||||
call.ReturnArguments = mock.Arguments{nil}
|
||||
})
|
||||
|
||||
_, job := helpers.CreateTemplateJob()
|
||||
environment := &NomadEnvironment{apiMock, templateEnvironmentJobHCL, job,
|
||||
storage.NewLocalStorage[runner.Runner](), s.TestCtx, nil}
|
||||
runner1 := &runner.RunnerMock{}
|
||||
runner1.On("ID").Return(tests.DefaultRunnerID)
|
||||
environment.AddRunner(runner1)
|
||||
|
||||
_, ok := environment.Sample()
|
||||
s.Require().True(ok)
|
||||
<-time.After(tests.ShortTimeout) // New Runners are requested asynchronously
|
||||
}
|
||||
|
||||
func (s *MainTestSuite) TestNomadEnvironment_DeleteLocally() {
|
||||
apiMock := &nomad.ExecutorAPIMock{}
|
||||
environment, err := NewNomadEnvironment(tests.DefaultEnvironmentIDAsInteger, apiMock, templateEnvironmentJobHCL)
|
||||
s.Require().NoError(err)
|
||||
|
||||
err = environment.Delete(runner.ErrLocalDestruction)
|
||||
s.NoError(err)
|
||||
apiMock.AssertExpectations(s.T())
|
||||
}
|
||||
|
||||
func (s *MainTestSuite) TestNomadEnvironment_AddRunner() {
|
||||
s.Run("Destroys runner before replacing it", func() {
|
||||
apiMock := &nomad.ExecutorAPIMock{}
|
||||
environment, err := NewNomadEnvironment(tests.DefaultEnvironmentIDAsInteger, apiMock, templateEnvironmentJobHCL)
|
||||
s.Require().NoError(err)
|
||||
r := &runner.RunnerMock{}
|
||||
r.On("ID").Return(tests.DefaultRunnerID)
|
||||
r.On("Destroy", mock.Anything).Run(func(args mock.Arguments) {
|
||||
err, ok := args[0].(error)
|
||||
s.Require().True(ok)
|
||||
s.ErrorIs(err, runner.ErrLocalDestruction)
|
||||
}).Return(nil).Once()
|
||||
r2 := &runner.RunnerMock{}
|
||||
r2.On("ID").Return(tests.DefaultRunnerID)
|
||||
|
||||
environment.AddRunner(r)
|
||||
environment.AddRunner(r2)
|
||||
r.AssertExpectations(s.T())
|
||||
|
||||
// Teardown test case
|
||||
r2.On("Destroy", mock.Anything).Return(nil)
|
||||
apiMock.On("LoadRunnerIDs", mock.Anything).Return([]string{}, nil)
|
||||
apiMock.On("DeleteJob", mock.Anything).Return(nil)
|
||||
s.NoError(environment.Delete(tests.ErrCleanupDestroyReason))
|
||||
})
|
||||
}
|
@ -1,455 +0,0 @@
|
||||
package environment
|
||||
|
||||
import (
|
||||
"context"
|
||||
nomadApi "github.com/hashicorp/nomad/api"
|
||||
"github.com/hashicorp/nomad/nomad/structs"
|
||||
"github.com/openHPI/poseidon/internal/nomad"
|
||||
"github.com/openHPI/poseidon/internal/runner"
|
||||
"github.com/openHPI/poseidon/pkg/dto"
|
||||
"github.com/openHPI/poseidon/tests"
|
||||
"github.com/openHPI/poseidon/tests/helpers"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type CreateOrUpdateTestSuite struct {
|
||||
tests.MemoryLeakTestSuite
|
||||
runnerManagerMock runner.ManagerMock
|
||||
apiMock nomad.ExecutorAPIMock
|
||||
request dto.ExecutionEnvironmentRequest
|
||||
manager *NomadEnvironmentManager
|
||||
environmentID dto.EnvironmentID
|
||||
}
|
||||
|
||||
func TestCreateOrUpdateTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(CreateOrUpdateTestSuite))
|
||||
}
|
||||
|
||||
func (s *CreateOrUpdateTestSuite) SetupTest() {
|
||||
s.MemoryLeakTestSuite.SetupTest()
|
||||
s.runnerManagerMock = runner.ManagerMock{}
|
||||
|
||||
s.apiMock = nomad.ExecutorAPIMock{}
|
||||
s.request = dto.ExecutionEnvironmentRequest{
|
||||
PrewarmingPoolSize: 10,
|
||||
CPULimit: 20,
|
||||
MemoryLimit: 30,
|
||||
Image: "my-image",
|
||||
NetworkAccess: false,
|
||||
ExposedPorts: nil,
|
||||
}
|
||||
|
||||
s.manager = &NomadEnvironmentManager{
|
||||
AbstractManager: &AbstractManager{runnerManager: &s.runnerManagerMock},
|
||||
api: &s.apiMock,
|
||||
templateEnvironmentHCL: templateEnvironmentJobHCL,
|
||||
}
|
||||
|
||||
s.environmentID = dto.EnvironmentID(tests.DefaultEnvironmentIDAsInteger)
|
||||
}
|
||||
|
||||
func (s *CreateOrUpdateTestSuite) TestReturnsErrorIfCreatesOrUpdateEnvironmentReturnsError() {
|
||||
s.apiMock.On("RegisterNomadJob", mock.AnythingOfType("*api.Job")).Return("", tests.ErrDefault)
|
||||
s.apiMock.On("LoadRunnerIDs", mock.AnythingOfType("string")).Return([]string{}, nil)
|
||||
s.apiMock.On("DeleteJob", mock.AnythingOfType("string")).Return(nil)
|
||||
s.runnerManagerMock.On("GetEnvironment", mock.AnythingOfType("dto.EnvironmentID")).Return(nil, false)
|
||||
s.runnerManagerMock.On("StoreEnvironment", mock.AnythingOfType("*environment.NomadEnvironment")).Return(true)
|
||||
s.ExpectedGoroutineIncrease++ // We don't care about removing the created environment.
|
||||
_, err := s.manager.CreateOrUpdate(
|
||||
dto.EnvironmentID(tests.DefaultEnvironmentIDAsInteger), s.request, context.Background())
|
||||
s.ErrorIs(err, tests.ErrDefault)
|
||||
}
|
||||
|
||||
func (s *CreateOrUpdateTestSuite) TestCreateOrUpdatesSetsForcePullFlag() {
|
||||
s.apiMock.On("RegisterNomadJob", mock.AnythingOfType("*api.Job")).Return("", nil)
|
||||
s.apiMock.On("LoadRunnerIDs", mock.AnythingOfType("string")).Return([]string{}, nil)
|
||||
s.apiMock.On("DeleteJob", mock.AnythingOfType("string")).Return(nil)
|
||||
s.runnerManagerMock.On("GetEnvironment", mock.AnythingOfType("dto.EnvironmentID")).Return(nil, false)
|
||||
s.runnerManagerMock.On("StoreEnvironment", mock.AnythingOfType("*environment.NomadEnvironment")).Return(true)
|
||||
s.apiMock.On("MonitorEvaluation", mock.AnythingOfType("string"), mock.Anything).Return(nil)
|
||||
s.apiMock.On("LoadRunnerIDs", mock.AnythingOfType("string")).Return([]string{}, nil)
|
||||
call := s.apiMock.On("RegisterRunnerJob", mock.AnythingOfType("*api.Job"))
|
||||
count := 0
|
||||
call.Run(func(args mock.Arguments) {
|
||||
count++
|
||||
job, ok := args.Get(0).(*nomadApi.Job)
|
||||
s.True(ok)
|
||||
|
||||
// The environment job itself has not the force_pull flag
|
||||
if count > 1 {
|
||||
taskGroup := nomad.FindAndValidateDefaultTaskGroup(job)
|
||||
task := nomad.FindAndValidateDefaultTask(taskGroup)
|
||||
s.True(task.Config["force_pull"].(bool))
|
||||
}
|
||||
|
||||
call.ReturnArguments = mock.Arguments{nil}
|
||||
})
|
||||
s.ExpectedGoroutineIncrease++ // We dont care about removing the created environment at this point.
|
||||
_, err := s.manager.CreateOrUpdate(
|
||||
dto.EnvironmentID(tests.DefaultEnvironmentIDAsInteger), s.request, context.Background())
|
||||
s.NoError(err)
|
||||
s.True(count > 1)
|
||||
}
|
||||
|
||||
func (s *MainTestSuite) TestNewNomadEnvironmentManager() {
|
||||
executorAPIMock := &nomad.ExecutorAPIMock{}
|
||||
executorAPIMock.On("LoadEnvironmentJobs").Return([]*nomadApi.Job{}, nil)
|
||||
executorAPIMock.On("LoadRunnerIDs", mock.AnythingOfType("string")).Return([]string{}, nil)
|
||||
executorAPIMock.On("DeleteJob", mock.AnythingOfType("string")).Return(nil)
|
||||
|
||||
runnerManagerMock := &runner.ManagerMock{}
|
||||
runnerManagerMock.On("Load").Return()
|
||||
|
||||
previousTemplateEnvironmentJobHCL := templateEnvironmentJobHCL
|
||||
|
||||
s.Run("returns error if template file does not exist", func() {
|
||||
_, err := NewNomadEnvironmentManager(runnerManagerMock, executorAPIMock, "/non-existent/file")
|
||||
s.Error(err)
|
||||
})
|
||||
|
||||
s.Run("loads template environment job from file", func() {
|
||||
templateJobHCL := "job \"" + tests.DefaultTemplateJobID + "\" {}"
|
||||
|
||||
environment, err := NewNomadEnvironment(tests.DefaultEnvironmentIDAsInteger, executorAPIMock, templateJobHCL)
|
||||
s.Require().NoError(err)
|
||||
f := createTempFile(s.T(), templateJobHCL)
|
||||
defer os.Remove(f.Name())
|
||||
|
||||
m, err := NewNomadEnvironmentManager(runnerManagerMock, executorAPIMock, f.Name())
|
||||
s.NoError(err)
|
||||
s.NotNil(m)
|
||||
s.Equal(templateJobHCL, m.templateEnvironmentHCL)
|
||||
|
||||
s.NoError(environment.Delete(tests.ErrCleanupDestroyReason))
|
||||
})
|
||||
|
||||
s.Run("returns error if template file is invalid", func() {
|
||||
templateJobHCL := "invalid hcl file"
|
||||
f := createTempFile(s.T(), templateJobHCL)
|
||||
defer os.Remove(f.Name())
|
||||
|
||||
m, err := NewNomadEnvironmentManager(runnerManagerMock, executorAPIMock, f.Name())
|
||||
s.Require().NoError(err)
|
||||
_, err = NewNomadEnvironment(tests.DefaultEnvironmentIDAsInteger, nil, m.templateEnvironmentHCL)
|
||||
s.Error(err)
|
||||
})
|
||||
|
||||
templateEnvironmentJobHCL = previousTemplateEnvironmentJobHCL
|
||||
}
|
||||
|
||||
func (s *MainTestSuite) TestNomadEnvironmentManager_Get() {
|
||||
apiMock := &nomad.ExecutorAPIMock{}
|
||||
mockWatchAllocations(s.TestCtx, apiMock)
|
||||
apiMock.On("LoadRunnerIDs", mock.AnythingOfType("string")).Return([]string{}, nil)
|
||||
apiMock.On("DeleteJob", mock.AnythingOfType("string")).Return(nil)
|
||||
call := apiMock.On("LoadEnvironmentJobs")
|
||||
call.Run(func(args mock.Arguments) {
|
||||
call.ReturnArguments = mock.Arguments{[]*nomadApi.Job{}, nil}
|
||||
})
|
||||
|
||||
runnerManager := runner.NewNomadRunnerManager(apiMock, s.TestCtx)
|
||||
m, err := NewNomadEnvironmentManager(runnerManager, apiMock, "")
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.Run("Returns error when not found", func() {
|
||||
_, err := m.Get(tests.DefaultEnvironmentIDAsInteger, false)
|
||||
s.Error(err)
|
||||
})
|
||||
|
||||
s.Run("Returns environment when it was added before", func() {
|
||||
expectedEnvironment, err :=
|
||||
NewNomadEnvironment(tests.DefaultEnvironmentIDAsInteger, apiMock, templateEnvironmentJobHCL)
|
||||
expectedEnvironment.SetID(tests.DefaultEnvironmentIDAsInteger)
|
||||
s.Require().NoError(err)
|
||||
runnerManager.StoreEnvironment(expectedEnvironment)
|
||||
|
||||
environment, err := m.Get(tests.DefaultEnvironmentIDAsInteger, false)
|
||||
s.NoError(err)
|
||||
s.Equal(expectedEnvironment, environment)
|
||||
|
||||
err = environment.Delete(tests.ErrCleanupDestroyReason)
|
||||
s.Require().NoError(err)
|
||||
})
|
||||
|
||||
s.Run("Fetch", func() {
|
||||
apiMock.On("DeleteJob", mock.AnythingOfType("string")).Return(nil)
|
||||
s.Run("Returns error when not found", func() {
|
||||
_, err := m.Get(tests.DefaultEnvironmentIDAsInteger, true)
|
||||
s.Error(err)
|
||||
})
|
||||
|
||||
s.Run("Updates values when environment already known by Poseidon", func() {
|
||||
fetchedEnvironment, err := NewNomadEnvironment(
|
||||
tests.DefaultEnvironmentIDAsInteger, apiMock, templateEnvironmentJobHCL)
|
||||
s.Require().NoError(err)
|
||||
fetchedEnvironment.SetID(tests.DefaultEnvironmentIDAsInteger)
|
||||
fetchedEnvironment.SetImage("random docker image")
|
||||
call.Run(func(args mock.Arguments) {
|
||||
call.ReturnArguments = mock.Arguments{[]*nomadApi.Job{fetchedEnvironment.job}, nil}
|
||||
})
|
||||
|
||||
localEnvironment, err := NewNomadEnvironment(tests.DefaultEnvironmentIDAsInteger, apiMock, templateEnvironmentJobHCL)
|
||||
s.Require().NoError(err)
|
||||
localEnvironment.SetID(tests.DefaultEnvironmentIDAsInteger)
|
||||
runnerManager.StoreEnvironment(localEnvironment)
|
||||
|
||||
environment, err := m.Get(tests.DefaultEnvironmentIDAsInteger, false)
|
||||
s.NoError(err)
|
||||
s.NotEqual(fetchedEnvironment.Image(), environment.Image())
|
||||
|
||||
environment, err = m.Get(tests.DefaultEnvironmentIDAsInteger, true)
|
||||
s.NoError(err)
|
||||
s.Equal(fetchedEnvironment.Image(), environment.Image())
|
||||
|
||||
err = fetchedEnvironment.Delete(tests.ErrCleanupDestroyReason)
|
||||
s.Require().NoError(err)
|
||||
err = environment.Delete(tests.ErrCleanupDestroyReason)
|
||||
s.Require().NoError(err)
|
||||
err = localEnvironment.Delete(tests.ErrCleanupDestroyReason)
|
||||
s.Require().NoError(err)
|
||||
})
|
||||
runnerManager.DeleteEnvironment(tests.DefaultEnvironmentIDAsInteger)
|
||||
|
||||
s.Run("Adds environment when not already known by Poseidon", func() {
|
||||
fetchedEnvironment, err := NewNomadEnvironment(
|
||||
tests.DefaultEnvironmentIDAsInteger, apiMock, templateEnvironmentJobHCL)
|
||||
s.Require().NoError(err)
|
||||
fetchedEnvironment.SetID(tests.DefaultEnvironmentIDAsInteger)
|
||||
fetchedEnvironment.SetImage("random docker image")
|
||||
call.Run(func(args mock.Arguments) {
|
||||
call.ReturnArguments = mock.Arguments{[]*nomadApi.Job{fetchedEnvironment.job}, nil}
|
||||
})
|
||||
|
||||
_, err = m.Get(tests.DefaultEnvironmentIDAsInteger, false)
|
||||
s.Error(err)
|
||||
|
||||
environment, err := m.Get(tests.DefaultEnvironmentIDAsInteger, true)
|
||||
s.NoError(err)
|
||||
s.Equal(fetchedEnvironment.Image(), environment.Image())
|
||||
|
||||
err = fetchedEnvironment.Delete(tests.ErrCleanupDestroyReason)
|
||||
s.Require().NoError(err)
|
||||
err = environment.Delete(tests.ErrCleanupDestroyReason)
|
||||
s.Require().NoError(err)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MainTestSuite) TestNomadEnvironmentManager_List() {
|
||||
apiMock := &nomad.ExecutorAPIMock{}
|
||||
apiMock.On("LoadRunnerIDs", mock.AnythingOfType("string")).Return([]string{}, nil)
|
||||
apiMock.On("DeleteJob", mock.AnythingOfType("string")).Return(nil)
|
||||
mockWatchAllocations(s.TestCtx, apiMock)
|
||||
call := apiMock.On("LoadEnvironmentJobs")
|
||||
call.Run(func(args mock.Arguments) {
|
||||
call.ReturnArguments = mock.Arguments{[]*nomadApi.Job{}, nil}
|
||||
})
|
||||
|
||||
runnerManager := runner.NewNomadRunnerManager(apiMock, s.TestCtx)
|
||||
m, err := NewNomadEnvironmentManager(runnerManager, apiMock, "")
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.Run("with no environments", func() {
|
||||
environments, err := m.List(true)
|
||||
s.NoError(err)
|
||||
s.Empty(environments)
|
||||
})
|
||||
|
||||
s.Run("Returns added environment", func() {
|
||||
localEnvironment, err := NewNomadEnvironment(tests.DefaultEnvironmentIDAsInteger, apiMock, templateEnvironmentJobHCL)
|
||||
s.Require().NoError(err)
|
||||
localEnvironment.SetID(tests.DefaultEnvironmentIDAsInteger)
|
||||
runnerManager.StoreEnvironment(localEnvironment)
|
||||
|
||||
environments, err := m.List(false)
|
||||
s.NoError(err)
|
||||
s.Equal(1, len(environments))
|
||||
s.Equal(localEnvironment, environments[0])
|
||||
|
||||
err = localEnvironment.Delete(tests.ErrCleanupDestroyReason)
|
||||
s.Require().NoError(err)
|
||||
})
|
||||
runnerManager.DeleteEnvironment(tests.DefaultEnvironmentIDAsInteger)
|
||||
|
||||
s.Run("Fetches new Runners via the api client", func() {
|
||||
fetchedEnvironment, err :=
|
||||
NewNomadEnvironment(tests.DefaultEnvironmentIDAsInteger, apiMock, templateEnvironmentJobHCL)
|
||||
s.Require().NoError(err)
|
||||
fetchedEnvironment.SetID(tests.DefaultEnvironmentIDAsInteger)
|
||||
status := structs.JobStatusRunning
|
||||
fetchedEnvironment.job.Status = &status
|
||||
call.Run(func(args mock.Arguments) {
|
||||
call.ReturnArguments = mock.Arguments{[]*nomadApi.Job{fetchedEnvironment.job}, nil}
|
||||
})
|
||||
|
||||
environments, err := m.List(false)
|
||||
s.NoError(err)
|
||||
s.Empty(environments)
|
||||
|
||||
environments, err = m.List(true)
|
||||
s.NoError(err)
|
||||
s.Equal(1, len(environments))
|
||||
nomadEnvironment, ok := environments[0].(*NomadEnvironment)
|
||||
s.True(ok)
|
||||
s.Equal(fetchedEnvironment.job, nomadEnvironment.job)
|
||||
|
||||
err = fetchedEnvironment.Delete(tests.ErrCleanupDestroyReason)
|
||||
s.Require().NoError(err)
|
||||
err = nomadEnvironment.Delete(tests.ErrCleanupDestroyReason)
|
||||
s.Require().NoError(err)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MainTestSuite) TestNomadEnvironmentManager_Load() {
|
||||
apiMock := &nomad.ExecutorAPIMock{}
|
||||
apiMock.On("LoadRunnerIDs", mock.AnythingOfType("string")).Return([]string{}, nil)
|
||||
apiMock.On("DeleteJob", mock.AnythingOfType("string")).Return(nil)
|
||||
mockWatchAllocations(s.TestCtx, apiMock)
|
||||
call := apiMock.On("LoadEnvironmentJobs")
|
||||
apiMock.On("LoadRunnerJobs", mock.AnythingOfType("dto.EnvironmentID")).
|
||||
Return([]*nomadApi.Job{}, nil)
|
||||
|
||||
runnerManager := runner.NewNomadRunnerManager(apiMock, s.TestCtx)
|
||||
|
||||
s.Run("deletes local environments before loading Nomad environments", func() {
|
||||
call.Return([]*nomadApi.Job{}, nil)
|
||||
environment := &runner.ExecutionEnvironmentMock{}
|
||||
environment.On("ID").Return(dto.EnvironmentID(tests.DefaultEnvironmentIDAsInteger))
|
||||
environment.On("Image").Return("")
|
||||
environment.On("CPULimit").Return(uint(0))
|
||||
environment.On("MemoryLimit").Return(uint(0))
|
||||
environment.On("NetworkAccess").Return(false, nil)
|
||||
environment.On("Delete", mock.Anything).Return(nil)
|
||||
runnerManager.StoreEnvironment(environment)
|
||||
|
||||
m, err := NewNomadEnvironmentManager(runnerManager, apiMock, "")
|
||||
s.Require().NoError(err)
|
||||
|
||||
err = m.load()
|
||||
s.Require().NoError(err)
|
||||
environment.AssertExpectations(s.T())
|
||||
})
|
||||
runnerManager.DeleteEnvironment(tests.DefaultEnvironmentIDAsInteger)
|
||||
|
||||
s.Run("Stores fetched environments", func() {
|
||||
_, job := helpers.CreateTemplateJob()
|
||||
call.Return([]*nomadApi.Job{job}, nil)
|
||||
|
||||
_, ok := runnerManager.GetEnvironment(tests.DefaultEnvironmentIDAsInteger)
|
||||
s.Require().False(ok)
|
||||
|
||||
m, err := NewNomadEnvironmentManager(runnerManager, apiMock, "")
|
||||
s.Require().NoError(err)
|
||||
|
||||
err = m.load()
|
||||
s.Require().NoError(err)
|
||||
|
||||
environment, ok := runnerManager.GetEnvironment(tests.DefaultEnvironmentIDAsInteger)
|
||||
s.Require().True(ok)
|
||||
s.Equal("python:latest", environment.Image())
|
||||
|
||||
err = environment.Delete(tests.ErrCleanupDestroyReason)
|
||||
s.Require().NoError(err)
|
||||
})
|
||||
|
||||
runnerManager.DeleteEnvironment(tests.DefaultEnvironmentIDAsInteger)
|
||||
s.Run("Processes only running environments", func() {
|
||||
_, job := helpers.CreateTemplateJob()
|
||||
jobStatus := structs.JobStatusDead
|
||||
job.Status = &jobStatus
|
||||
call.Return([]*nomadApi.Job{job}, nil)
|
||||
|
||||
_, ok := runnerManager.GetEnvironment(tests.DefaultEnvironmentIDAsInteger)
|
||||
s.Require().False(ok)
|
||||
|
||||
_, err := NewNomadEnvironmentManager(runnerManager, apiMock, "")
|
||||
s.Require().NoError(err)
|
||||
|
||||
_, ok = runnerManager.GetEnvironment(tests.DefaultEnvironmentIDAsInteger)
|
||||
s.Require().False(ok)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MainTestSuite) TestNomadEnvironmentManager_KeepEnvironmentsSynced() {
|
||||
apiMock := &nomad.ExecutorAPIMock{}
|
||||
runnerManager := runner.NewNomadRunnerManager(apiMock, s.TestCtx)
|
||||
m, err := NewNomadEnvironmentManager(runnerManager, apiMock, "")
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.Run("stops when context is done", func() {
|
||||
apiMock.On("LoadEnvironmentJobs").Return([]*nomadApi.Job{}, context.DeadlineExceeded)
|
||||
ctx, cancel := context.WithCancel(s.TestCtx)
|
||||
cancel()
|
||||
|
||||
var done bool
|
||||
go func() {
|
||||
<-time.After(tests.ShortTimeout)
|
||||
if !done {
|
||||
s.FailNow("KeepEnvironmentsSynced is ignoring the context")
|
||||
}
|
||||
}()
|
||||
|
||||
m.KeepEnvironmentsSynced(func(_ context.Context) error { return nil }, ctx)
|
||||
done = true
|
||||
})
|
||||
apiMock.ExpectedCalls = []*mock.Call{}
|
||||
apiMock.Calls = []mock.Call{}
|
||||
|
||||
s.Run("retries loading environments", func() {
|
||||
ctx, cancel := context.WithCancel(s.TestCtx)
|
||||
|
||||
apiMock.On("LoadEnvironmentJobs").Return([]*nomadApi.Job{}, context.DeadlineExceeded).Once()
|
||||
apiMock.On("LoadEnvironmentJobs").Return([]*nomadApi.Job{}, nil).Run(func(_ mock.Arguments) {
|
||||
cancel()
|
||||
}).Once()
|
||||
|
||||
m.KeepEnvironmentsSynced(func(_ context.Context) error { return nil }, ctx)
|
||||
apiMock.AssertExpectations(s.T())
|
||||
})
|
||||
apiMock.ExpectedCalls = []*mock.Call{}
|
||||
apiMock.Calls = []mock.Call{}
|
||||
|
||||
s.Run("retries synchronizing runners", func() {
|
||||
apiMock.On("LoadEnvironmentJobs").Return([]*nomadApi.Job{}, nil)
|
||||
ctx, cancel := context.WithCancel(s.TestCtx)
|
||||
|
||||
count := 0
|
||||
synchronizeRunners := func(ctx context.Context) error {
|
||||
count++
|
||||
if count >= 2 {
|
||||
cancel()
|
||||
return nil
|
||||
}
|
||||
return context.DeadlineExceeded
|
||||
}
|
||||
m.KeepEnvironmentsSynced(synchronizeRunners, ctx)
|
||||
|
||||
if count < 2 {
|
||||
s.Fail("KeepEnvironmentsSynced is not retrying to synchronize the runners")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func mockWatchAllocations(ctx context.Context, apiMock *nomad.ExecutorAPIMock) {
|
||||
call := apiMock.On("WatchEventStream", mock.Anything, mock.Anything, mock.Anything)
|
||||
call.Run(func(args mock.Arguments) {
|
||||
<-ctx.Done()
|
||||
call.ReturnArguments = mock.Arguments{nil}
|
||||
})
|
||||
}
|
||||
|
||||
func createTempFile(t *testing.T, content string) *os.File {
|
||||
t.Helper()
|
||||
f, err := os.CreateTemp("", "test")
|
||||
require.NoError(t, err)
|
||||
n, err := f.WriteString(content)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, len(content), n)
|
||||
|
||||
return f
|
||||
}
|
122
internal/kubernetes/api_querier.go
Normal file
122
internal/kubernetes/api_querier.go
Normal file
@ -0,0 +1,122 @@
|
||||
package kubernetes
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
nomadApi "github.com/hashicorp/nomad/api"
|
||||
"io"
|
||||
appv1 "k8s.io/api/apps/v1"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/rest"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrorNoAllocationFound = errors.New("no allocation found")
|
||||
)
|
||||
|
||||
// apiQuerier provides access to the Nomad functionality.
|
||||
type apiQuerier interface {
|
||||
// init prepares an apiClient to be able to communicate to a provided Nomad API.
|
||||
init(nomadConfig *rest.Config) (err error)
|
||||
|
||||
// LoadJobList loads the list of jobs from the Nomad API.
|
||||
LoadJobList() (list []*appv1.DeploymentList, err error)
|
||||
|
||||
// JobScale returns the scale of the passed job.
|
||||
JobScale(jobID string) (jobScale uint, err error)
|
||||
|
||||
// SetJobScale sets the scaling count of the passed job to Nomad.
|
||||
SetJobScale(jobID string, count uint, reason string) (err error)
|
||||
|
||||
// DeleteJob deletes the Job with the given ID.
|
||||
DeleteDeployment(name string) (err error)
|
||||
|
||||
// Execute runs a command in the passed job.
|
||||
Execute(jobID string, ctx context.Context, command string, tty bool,
|
||||
stdin io.Reader, stdout, stderr io.Writer) (int, error)
|
||||
|
||||
// listJobs loads all jobs with the specified prefix.
|
||||
listDeployments(namespace string) (jobListStub []*appv1.DeploymentList, err error)
|
||||
|
||||
// job returns the job of the given jobID.
|
||||
deployment(name string) (deployment appv1.Deployment, err error)
|
||||
|
||||
// listAllocations loads all allocations.
|
||||
listAllocations() (allocationListStub []*nomadApi.AllocationListStub, err error)
|
||||
|
||||
// allocation returns the first allocation of the given job.
|
||||
allocation(jobID string) (*nomadApi.Allocation, error)
|
||||
|
||||
// RegisterKubernetesDeployment registers a deployment with Kubernetes.
|
||||
// It returns the deployment ID that can be used when listening to the Kubernetes event stream.
|
||||
RegisterKubernetesDeployment(deployment appv1.Deployment) (string, error)
|
||||
|
||||
// EventStream returns a Nomad event stream filtered to return only allocation and evaluation events.
|
||||
EventStream(ctx context.Context) (<-chan *nomadApi.Events, error)
|
||||
}
|
||||
|
||||
// nomadAPIClient implements the nomadApiQuerier interface and provides access to a real Nomad API.
|
||||
type kubernetesAPIClient struct {
|
||||
client *kubernetes.Clientset
|
||||
namespace string
|
||||
}
|
||||
|
||||
func (k kubernetesAPIClient) init(nomadConfig *rest.Config) (err error) {
|
||||
//TODO implement me
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (k kubernetesAPIClient) LoadJobList() (list []*appv1.DeploymentList, err error) {
|
||||
//TODO implement me
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (k kubernetesAPIClient) JobScale(jobID string) (jobScale uint, err error) {
|
||||
//TODO implement me
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (k kubernetesAPIClient) SetJobScale(jobID string, count uint, reason string) (err error) {
|
||||
//TODO implement me
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (k kubernetesAPIClient) DeleteDeployment(name string) (err error) {
|
||||
//TODO implement me
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (k kubernetesAPIClient) Execute(jobID string, ctx context.Context, command string, tty bool, stdin io.Reader, stdout, stderr io.Writer) (int, error) {
|
||||
//TODO implement me
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (k kubernetesAPIClient) listDeployments(namespace string) (jobListStub []*appv1.DeploymentList, err error) {
|
||||
//TODO implement me
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (k kubernetesAPIClient) deployment(name string) (deployment appv1.Deployment, err error) {
|
||||
//TODO implement me
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (k kubernetesAPIClient) listAllocations() (allocationListStub []*nomadApi.AllocationListStub, err error) {
|
||||
//TODO implement me
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (k kubernetesAPIClient) allocation(jobID string) (*nomadApi.Allocation, error) {
|
||||
//TODO implement me
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (k kubernetesAPIClient) RegisterKubernetesDeployment(deployment appv1.Deployment) (string, error) {
|
||||
//TODO implement me
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (k kubernetesAPIClient) EventStream(ctx context.Context) (<-chan *nomadApi.Events, error) {
|
||||
//TODO implement me
|
||||
panic("implement me")
|
||||
}
|
125
internal/kubernetes/deployment.go
Normal file
125
internal/kubernetes/deployment.go
Normal file
@ -0,0 +1,125 @@
|
||||
package kubernetes
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
nomadApi "github.com/hashicorp/nomad/api"
|
||||
"github.com/openHPI/poseidon/pkg/dto"
|
||||
appv1 "k8s.io/api/apps/v1"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
TemplateJobPrefix = "template"
|
||||
TaskGroupName = "default-group"
|
||||
TaskName = "default-task"
|
||||
TaskCount = 1
|
||||
TaskDriver = "docker"
|
||||
TaskCommand = "sleep"
|
||||
ConfigTaskGroupName = "config"
|
||||
ConfigTaskName = "config"
|
||||
ConfigTaskDriver = "exec"
|
||||
ConfigTaskCommand = "true"
|
||||
ConfigMetaUsedKey = "used"
|
||||
ConfigMetaUsedValue = "true"
|
||||
ConfigMetaUnusedValue = "false"
|
||||
ConfigMetaTimeoutKey = "timeout"
|
||||
ConfigMetaPoolSizeKey = "prewarmingPoolSize"
|
||||
TemplateJobNameParts = 2
|
||||
RegisterTimeout = 10 * time.Second
|
||||
RunnerTimeoutFallback = 60 * time.Second
|
||||
)
|
||||
|
||||
var (
|
||||
ErrorInvalidJobID = errors.New("invalid job id")
|
||||
ErrorMissingTaskGroup = errors.New("couldn't find config task group in job")
|
||||
TaskArgs = []string{"infinity"}
|
||||
)
|
||||
|
||||
func (a *APIClient) RegisterRunnerJob(template *appv1.Deployment) error {
|
||||
evalID, err := a.apiQuerier.RegisterKubernetesDeployment(*template)
|
||||
if err != nil {
|
||||
return fmt.Errorf("couldn't register runner job: %w", err)
|
||||
}
|
||||
|
||||
registerTimeout, cancel := context.WithTimeout(context.Background(), RegisterTimeout)
|
||||
defer cancel()
|
||||
return a.MonitorEvaluation(evalID, registerTimeout)
|
||||
}
|
||||
|
||||
// SetForcePullFlag sets the flag of a job if the image should be pulled again.
|
||||
func SetForcePullFlag(deployment *appv1.Deployment, value bool) {
|
||||
for _, container := range deployment.Spec.Template.Spec.Containers {
|
||||
if container.Name == TaskName {
|
||||
if value {
|
||||
container.ImagePullPolicy = v1.PullAlways
|
||||
} else {
|
||||
container.ImagePullPolicy = v1.PullIfNotPresent
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// IsEnvironmentTemplateID checks if the passed job id belongs to a template job.
|
||||
func IsEnvironmentTemplateID(jobID string) bool {
|
||||
parts := strings.Split(jobID, "-")
|
||||
if len(parts) != TemplateJobNameParts || parts[0] != TemplateJobPrefix {
|
||||
return false
|
||||
}
|
||||
|
||||
_, err := EnvironmentIDFromTemplateJobID(jobID)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// RunnerJobID returns the nomad job id of the runner with the given environmentID and id.
|
||||
func RunnerJobID(environmentID dto.EnvironmentID, id string) string {
|
||||
return fmt.Sprintf("%d-%s", environmentID, id)
|
||||
}
|
||||
|
||||
// TemplateJobID returns the id of the nomad job for the environment with the given id.
|
||||
func TemplateJobID(id dto.EnvironmentID) string {
|
||||
return fmt.Sprintf("%s-%d", TemplateJobPrefix, id)
|
||||
}
|
||||
|
||||
// EnvironmentIDFromRunnerID returns the environment id that is part of the passed runner job id.
|
||||
func EnvironmentIDFromRunnerID(jobID string) (dto.EnvironmentID, error) {
|
||||
return partOfJobID(jobID, 0)
|
||||
}
|
||||
|
||||
// EnvironmentIDFromTemplateJobID returns the environment id that is part of the passed environment job id.
|
||||
func EnvironmentIDFromTemplateJobID(id string) (dto.EnvironmentID, error) {
|
||||
return partOfJobID(id, 1)
|
||||
}
|
||||
|
||||
func partOfJobID(id string, part uint) (dto.EnvironmentID, error) {
|
||||
parts := strings.Split(id, "-")
|
||||
if len(parts) == 0 {
|
||||
return 0, fmt.Errorf("empty job id: %w", ErrorInvalidJobID)
|
||||
}
|
||||
environmentID, err := strconv.Atoi(parts[part])
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("invalid environment id par %v: %w", err, ErrorInvalidJobID)
|
||||
}
|
||||
return dto.EnvironmentID(environmentID), nil
|
||||
}
|
||||
|
||||
func isOOMKilled(alloc *nomadApi.Allocation) bool {
|
||||
state, ok := alloc.TaskStates[TaskName]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
var oomKilledCount uint64
|
||||
for _, event := range state.Events {
|
||||
if oomString, ok := event.Details["oom_killed"]; ok {
|
||||
if oom, err := strconv.ParseBool(oomString); err == nil && oom {
|
||||
oomKilledCount++
|
||||
}
|
||||
}
|
||||
}
|
||||
return oomKilledCount >= state.Restarts
|
||||
}
|
221
internal/kubernetes/kubernetes.go
Normal file
221
internal/kubernetes/kubernetes.go
Normal file
@ -0,0 +1,221 @@
|
||||
package kubernetes
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
nomadApi "github.com/hashicorp/nomad/api"
|
||||
"github.com/hashicorp/nomad/nomad/structs"
|
||||
"github.com/influxdata/influxdb-client-go/v2/api/write"
|
||||
"github.com/openHPI/poseidon/pkg/dto"
|
||||
"github.com/openHPI/poseidon/pkg/logging"
|
||||
"github.com/openHPI/poseidon/pkg/monitoring"
|
||||
"github.com/openHPI/poseidon/pkg/storage"
|
||||
"io"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/client-go/rest"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
log = logging.GetLogger("kubernetes")
|
||||
ErrorExecutorCommunicationFailed = errors.New("communication with executor failed")
|
||||
ErrorEvaluation = errors.New("evaluation could not complete")
|
||||
ErrorPlacingAllocations = errors.New("failed to place all allocations")
|
||||
ErrorLoadingJob = errors.New("failed to load job")
|
||||
ErrorNoAllocatedResourcesFound = errors.New("no allocated resources found")
|
||||
ErrorLocalDestruction RunnerDeletedReason = errors.New("the destruction should not cause external changes")
|
||||
ErrorOOMKilled RunnerDeletedReason = fmt.Errorf("%s: %w", dto.ErrOOMKilled.Error(), ErrorLocalDestruction)
|
||||
ErrorAllocationRescheduled RunnerDeletedReason = fmt.Errorf("the allocation was rescheduled: %w", ErrorLocalDestruction)
|
||||
ErrorAllocationStopped RunnerDeletedReason = errors.New("the allocation was stopped")
|
||||
ErrorAllocationStoppedUnexpectedly RunnerDeletedReason = fmt.Errorf("%w unexpectedly", ErrorAllocationStopped)
|
||||
ErrorAllocationRescheduledUnexpectedly RunnerDeletedReason = fmt.Errorf(
|
||||
"%w correctly but rescheduled", ErrorAllocationStopped)
|
||||
// ErrorAllocationCompleted is for reporting the reason for the stopped allocation.
|
||||
// We do not consider it as an error but add it anyway for a complete reporting.
|
||||
ErrorAllocationCompleted RunnerDeletedReason = errors.New("the allocation completed")
|
||||
)
|
||||
|
||||
type allocationData struct {
|
||||
// allocClientStatus defines the state defined by Nomad.
|
||||
allocClientStatus string
|
||||
// allocDesiredStatus defines if the allocation wants to be running or being stopped.
|
||||
allocDesiredStatus string
|
||||
jobID string
|
||||
start time.Time
|
||||
// stopExpected is used to suppress warnings that could be triggered by a race condition
|
||||
// between the Inactivity timer and an external event leadng to allocation rescheduling.
|
||||
stopExpected bool
|
||||
// Just debugging information
|
||||
allocNomadNode string
|
||||
}
|
||||
|
||||
// resultChannelWriteTimeout is to detect the error when more element are written into a channel than expected.
|
||||
const resultChannelWriteTimeout = 10 * time.Millisecond
|
||||
|
||||
type DeletedAllocationProcessor func(jobID string, RunnerDeletedReason error) (removedByPoseidon bool)
|
||||
type NewAllocationProcessor func(*nomadApi.Allocation, time.Duration)
|
||||
|
||||
// AllocationProcessing includes the callbacks to interact with allocation events.
|
||||
type AllocationProcessing struct {
|
||||
OnNew NewAllocationProcessor
|
||||
OnDeleted DeletedAllocationProcessor
|
||||
}
|
||||
type RunnerDeletedReason error
|
||||
|
||||
// ExecutorAPI provides access to a container orchestration solution.
|
||||
type ExecutorAPI interface {
|
||||
apiQuerier
|
||||
|
||||
// LoadEnvironmentJobs loads all environment jobs.
|
||||
LoadEnvironmentJobs() ([]*appsv1.Deployment, error)
|
||||
|
||||
// LoadRunnerJobs loads all runner jobs specific for the environment.
|
||||
LoadRunnerJobs(environmentID dto.EnvironmentID) ([]*appsv1.Deployment, error)
|
||||
|
||||
// LoadRunnerIDs returns the IDs of all runners with the specified id prefix which are not about to
|
||||
// get stopped.
|
||||
LoadRunnerIDs(prefix string) (runnerIds []string, err error)
|
||||
|
||||
// LoadRunnerPortMappings returns the mapped ports of the runner.
|
||||
LoadRunnerPortMappings(runnerID string) ([]v1.ContainerPort, error)
|
||||
|
||||
// RegisterRunnerJob creates a runner job based on the template job.
|
||||
// It registers the job and waits until the registration completes.
|
||||
RegisterRunnerJob(template *appsv1.Deployment) error
|
||||
|
||||
// MonitorEvaluation monitors the given evaluation ID.
|
||||
// It waits until the evaluation reaches one of the states complete, canceled or failed.
|
||||
// If the evaluation was not successful, an error containing the failures is returned.
|
||||
// See also https://github.com/hashicorp/nomad/blob/7d5a9ecde95c18da94c9b6ace2565afbfdd6a40d/command/monitor.go#L175
|
||||
MonitorEvaluation(evaluationID string, ctx context.Context) error
|
||||
|
||||
// WatchEventStream listens on the Nomad event stream for allocation and evaluation events.
|
||||
// Depending on the incoming event, any of the given function is executed.
|
||||
// Do not run multiple times simultaneously.
|
||||
WatchEventStream(ctx context.Context, callbacks *AllocationProcessing) error
|
||||
|
||||
// ExecuteCommand executes the given command in the job/runner with the given id.
|
||||
// It writes the output of the command to stdout/stderr and reads input from stdin.
|
||||
// If tty is true, the command will run with a tty.
|
||||
// Iff privilegedExecution is true, the command will be executed privileged.
|
||||
// The command is passed in the shell form (not the exec array form) and will be executed in a shell.
|
||||
ExecuteCommand(jobID string, ctx context.Context, command string, tty bool, privilegedExecution bool,
|
||||
stdin io.Reader, stdout, stderr io.Writer) (int, error)
|
||||
|
||||
// MarkRunnerAsUsed marks the runner with the given ID as used. It also stores the timeout duration in the metadata.
|
||||
MarkRunnerAsUsed(runnerID string, duration int) error
|
||||
}
|
||||
|
||||
// APIClient implements the ExecutorAPI interface and can be used to perform different operations on the real
|
||||
// Executor API and its return values.
|
||||
type APIClient struct {
|
||||
apiQuerier
|
||||
evaluations storage.Storage[chan error]
|
||||
// allocations contain management data for all pending and running allocations.
|
||||
allocations storage.Storage[*allocationData]
|
||||
isListening bool
|
||||
}
|
||||
|
||||
func (A APIClient) LoadEnvironmentJobs() ([]*appsv1.Deployment, error) {
|
||||
//TODO implement me
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (a *APIClient) LoadRunnerJobs(environmentID dto.EnvironmentID) ([]*appsv1.Deployment, error) {
|
||||
go a.initializeAllocations(environmentID)
|
||||
|
||||
runnerIDs, err := a.LoadRunnerIDs(RunnerJobID(environmentID, ""))
|
||||
if err != nil {
|
||||
return []*appsv1.Deployment{}, fmt.Errorf("couldn't load jobs: %w", err)
|
||||
}
|
||||
|
||||
var occurredError error
|
||||
jobs := make([]*appsv1.Deployment, 0, len(runnerIDs))
|
||||
for _, id := range runnerIDs {
|
||||
job, err := a.apiQuerier.deployment(id)
|
||||
if err != nil {
|
||||
if occurredError == nil {
|
||||
occurredError = ErrorLoadingJob
|
||||
}
|
||||
occurredError = fmt.Errorf("%w: couldn't load job info for runner %s - %v", occurredError, id, err)
|
||||
continue
|
||||
}
|
||||
jobs = append(jobs, &job)
|
||||
}
|
||||
return jobs, occurredError
|
||||
}
|
||||
|
||||
func (A APIClient) LoadRunnerIDs(prefix string) (runnerIds []string, err error) {
|
||||
//TODO implement me
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (A APIClient) LoadRunnerPortMappings(runnerID string) ([]v1.ContainerPort, error) {
|
||||
//TODO implement me
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (A APIClient) MonitorEvaluation(evaluationID string, ctx context.Context) error {
|
||||
//TODO implement me
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (A APIClient) WatchEventStream(ctx context.Context, callbacks *AllocationProcessing) error {
|
||||
//TODO implement me
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (A APIClient) ExecuteCommand(jobID string, ctx context.Context, command string, tty bool, privilegedExecution bool, stdin io.Reader, stdout, stderr io.Writer) (int, error) {
|
||||
//TODO implement me
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (A APIClient) MarkRunnerAsUsed(runnerID string, duration int) error {
|
||||
//TODO implement me
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
// NewExecutorAPI creates a new api client.
|
||||
// One client is usually sufficient for the complete runtime of the API.
|
||||
func NewExecutorAPI(kubernetesConfig *rest.Config) (ExecutorAPI, error) {
|
||||
client := &APIClient{
|
||||
apiQuerier: &kubernetesAPIClient{},
|
||||
evaluations: storage.NewLocalStorage[chan error](),
|
||||
allocations: storage.NewMonitoredLocalStorage[*allocationData](monitoring.MeasurementNomadAllocations,
|
||||
func(p *write.Point, object *allocationData, _ storage.EventType) {
|
||||
p.AddTag(monitoring.InfluxKeyJobID, object.jobID)
|
||||
p.AddTag(monitoring.InfluxKeyClientStatus, object.allocClientStatus)
|
||||
p.AddTag(monitoring.InfluxKeyNomadNode, object.allocNomadNode)
|
||||
}, 0, nil),
|
||||
}
|
||||
err := client.init(kubernetesConfig)
|
||||
return client, err
|
||||
}
|
||||
|
||||
func (a *APIClient) initializeAllocations(environmentID dto.EnvironmentID) {
|
||||
allocationStubs, err := a.listAllocations()
|
||||
if err != nil {
|
||||
log.WithError(err).Warn("Could not initialize allocations")
|
||||
} else {
|
||||
for _, stub := range allocationStubs {
|
||||
switch {
|
||||
case IsEnvironmentTemplateID(stub.JobID):
|
||||
continue
|
||||
case !strings.HasPrefix(stub.JobID, RunnerJobID(environmentID, "")):
|
||||
continue
|
||||
case stub.ClientStatus == structs.AllocClientStatusPending || stub.ClientStatus == structs.AllocClientStatusRunning:
|
||||
log.WithField("jobID", stub.JobID).WithField("status", stub.ClientStatus).Debug("Recovered Allocation")
|
||||
a.allocations.Add(stub.ID, &allocationData{
|
||||
allocClientStatus: stub.ClientStatus,
|
||||
allocDesiredStatus: stub.DesiredStatus,
|
||||
jobID: stub.JobID,
|
||||
start: time.Unix(0, stub.CreateTime),
|
||||
allocNomadNode: stub.NodeName,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,304 +0,0 @@
|
||||
// Code generated by mockery v2.23.1. DO NOT EDIT.
|
||||
|
||||
package nomad
|
||||
|
||||
import (
|
||||
context "context"
|
||||
|
||||
api "github.com/hashicorp/nomad/api"
|
||||
config "github.com/openHPI/poseidon/internal/config"
|
||||
|
||||
io "io"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// apiQuerierMock is an autogenerated mock type for the apiQuerier type
|
||||
type apiQuerierMock struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// DeleteJob provides a mock function with given fields: jobID
|
||||
func (_m *apiQuerierMock) DeleteJob(jobID string) error {
|
||||
ret := _m.Called(jobID)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string) error); ok {
|
||||
r0 = rf(jobID)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// EventStream provides a mock function with given fields: ctx
|
||||
func (_m *apiQuerierMock) EventStream(ctx context.Context) (<-chan *api.Events, error) {
|
||||
ret := _m.Called(ctx)
|
||||
|
||||
var r0 <-chan *api.Events
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context) (<-chan *api.Events, error)); ok {
|
||||
return rf(ctx)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context) <-chan *api.Events); ok {
|
||||
r0 = rf(ctx)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(<-chan *api.Events)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context) error); ok {
|
||||
r1 = rf(ctx)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Execute provides a mock function with given fields: jobID, ctx, command, tty, stdin, stdout, stderr
|
||||
func (_m *apiQuerierMock) Execute(jobID string, ctx context.Context, command string, tty bool, stdin io.Reader, stdout io.Writer, stderr io.Writer) (int, error) {
|
||||
ret := _m.Called(jobID, ctx, command, tty, stdin, stdout, stderr)
|
||||
|
||||
var r0 int
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(string, context.Context, string, bool, io.Reader, io.Writer, io.Writer) (int, error)); ok {
|
||||
return rf(jobID, ctx, command, tty, stdin, stdout, stderr)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(string, context.Context, string, bool, io.Reader, io.Writer, io.Writer) int); ok {
|
||||
r0 = rf(jobID, ctx, command, tty, stdin, stdout, stderr)
|
||||
} else {
|
||||
r0 = ret.Get(0).(int)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(string, context.Context, string, bool, io.Reader, io.Writer, io.Writer) error); ok {
|
||||
r1 = rf(jobID, ctx, command, tty, stdin, stdout, stderr)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// JobScale provides a mock function with given fields: jobID
|
||||
func (_m *apiQuerierMock) JobScale(jobID string) (uint, error) {
|
||||
ret := _m.Called(jobID)
|
||||
|
||||
var r0 uint
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(string) (uint, error)); ok {
|
||||
return rf(jobID)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(string) uint); ok {
|
||||
r0 = rf(jobID)
|
||||
} else {
|
||||
r0 = ret.Get(0).(uint)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(string) error); ok {
|
||||
r1 = rf(jobID)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// LoadJobList provides a mock function with given fields:
|
||||
func (_m *apiQuerierMock) LoadJobList() ([]*api.JobListStub, error) {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 []*api.JobListStub
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func() ([]*api.JobListStub, error)); ok {
|
||||
return rf()
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func() []*api.JobListStub); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]*api.JobListStub)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func() error); ok {
|
||||
r1 = rf()
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// RegisterNomadJob provides a mock function with given fields: job
|
||||
func (_m *apiQuerierMock) RegisterNomadJob(job *api.Job) (string, error) {
|
||||
ret := _m.Called(job)
|
||||
|
||||
var r0 string
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(*api.Job) (string, error)); ok {
|
||||
return rf(job)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(*api.Job) string); ok {
|
||||
r0 = rf(job)
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(*api.Job) error); ok {
|
||||
r1 = rf(job)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// SetJobScale provides a mock function with given fields: jobID, count, reason
|
||||
func (_m *apiQuerierMock) SetJobScale(jobID string, count uint, reason string) error {
|
||||
ret := _m.Called(jobID, count, reason)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string, uint, string) error); ok {
|
||||
r0 = rf(jobID, count, reason)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// allocation provides a mock function with given fields: jobID
|
||||
func (_m *apiQuerierMock) allocation(jobID string) (*api.Allocation, error) {
|
||||
ret := _m.Called(jobID)
|
||||
|
||||
var r0 *api.Allocation
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(string) (*api.Allocation, error)); ok {
|
||||
return rf(jobID)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(string) *api.Allocation); ok {
|
||||
r0 = rf(jobID)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*api.Allocation)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(string) error); ok {
|
||||
r1 = rf(jobID)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// init provides a mock function with given fields: nomadConfig
|
||||
func (_m *apiQuerierMock) init(nomadConfig *config.Nomad) error {
|
||||
ret := _m.Called(nomadConfig)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(*config.Nomad) error); ok {
|
||||
r0 = rf(nomadConfig)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// job provides a mock function with given fields: jobID
|
||||
func (_m *apiQuerierMock) job(jobID string) (*api.Job, error) {
|
||||
ret := _m.Called(jobID)
|
||||
|
||||
var r0 *api.Job
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(string) (*api.Job, error)); ok {
|
||||
return rf(jobID)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(string) *api.Job); ok {
|
||||
r0 = rf(jobID)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*api.Job)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(string) error); ok {
|
||||
r1 = rf(jobID)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// listAllocations provides a mock function with given fields:
|
||||
func (_m *apiQuerierMock) listAllocations() ([]*api.AllocationListStub, error) {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 []*api.AllocationListStub
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func() ([]*api.AllocationListStub, error)); ok {
|
||||
return rf()
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func() []*api.AllocationListStub); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]*api.AllocationListStub)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func() error); ok {
|
||||
r1 = rf()
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// listJobs provides a mock function with given fields: prefix
|
||||
func (_m *apiQuerierMock) listJobs(prefix string) ([]*api.JobListStub, error) {
|
||||
ret := _m.Called(prefix)
|
||||
|
||||
var r0 []*api.JobListStub
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(string) ([]*api.JobListStub, error)); ok {
|
||||
return rf(prefix)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(string) []*api.JobListStub); ok {
|
||||
r0 = rf(prefix)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]*api.JobListStub)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(string) error); ok {
|
||||
r1 = rf(prefix)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
type mockConstructorTestingTnewApiQuerierMock interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}
|
||||
|
||||
// newApiQuerierMock creates a new instance of apiQuerierMock. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
func newApiQuerierMock(t mockConstructorTestingTnewApiQuerierMock) *apiQuerierMock {
|
||||
mock := &apiQuerierMock{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
package nomad
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/openHPI/poseidon/tests"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type MainTestSuite struct {
|
||||
tests.MemoryLeakTestSuite
|
||||
}
|
||||
|
||||
func TestMainTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(MainTestSuite))
|
||||
}
|
||||
|
||||
func (s *MainTestSuite) TestWebsocketErrorNeedsToBeUnwrapped() {
|
||||
rawError := &websocket.CloseError{Code: websocket.CloseNormalClosure}
|
||||
err := fmt.Errorf("websocket closed before receiving exit code: %w", rawError)
|
||||
|
||||
s.False(websocket.IsCloseError(err, websocket.CloseNormalClosure))
|
||||
rootCause := errors.Unwrap(err)
|
||||
s.True(websocket.IsCloseError(rootCause, websocket.CloseNormalClosure))
|
||||
}
|
@ -1,490 +0,0 @@
|
||||
// Code generated by mockery v2.23.1. DO NOT EDIT.
|
||||
|
||||
package nomad
|
||||
|
||||
import (
|
||||
context "context"
|
||||
|
||||
api "github.com/hashicorp/nomad/api"
|
||||
config "github.com/openHPI/poseidon/internal/config"
|
||||
|
||||
dto "github.com/openHPI/poseidon/pkg/dto"
|
||||
|
||||
io "io"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// ExecutorAPIMock is an autogenerated mock type for the ExecutorAPI type
|
||||
type ExecutorAPIMock struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// DeleteJob provides a mock function with given fields: jobID
|
||||
func (_m *ExecutorAPIMock) DeleteJob(jobID string) error {
|
||||
ret := _m.Called(jobID)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string) error); ok {
|
||||
r0 = rf(jobID)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// EventStream provides a mock function with given fields: ctx
|
||||
func (_m *ExecutorAPIMock) EventStream(ctx context.Context) (<-chan *api.Events, error) {
|
||||
ret := _m.Called(ctx)
|
||||
|
||||
var r0 <-chan *api.Events
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context) (<-chan *api.Events, error)); ok {
|
||||
return rf(ctx)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context) <-chan *api.Events); ok {
|
||||
r0 = rf(ctx)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(<-chan *api.Events)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context) error); ok {
|
||||
r1 = rf(ctx)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Execute provides a mock function with given fields: jobID, ctx, command, tty, stdin, stdout, stderr
|
||||
func (_m *ExecutorAPIMock) Execute(jobID string, ctx context.Context, command string, tty bool, stdin io.Reader, stdout io.Writer, stderr io.Writer) (int, error) {
|
||||
ret := _m.Called(jobID, ctx, command, tty, stdin, stdout, stderr)
|
||||
|
||||
var r0 int
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(string, context.Context, string, bool, io.Reader, io.Writer, io.Writer) (int, error)); ok {
|
||||
return rf(jobID, ctx, command, tty, stdin, stdout, stderr)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(string, context.Context, string, bool, io.Reader, io.Writer, io.Writer) int); ok {
|
||||
r0 = rf(jobID, ctx, command, tty, stdin, stdout, stderr)
|
||||
} else {
|
||||
r0 = ret.Get(0).(int)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(string, context.Context, string, bool, io.Reader, io.Writer, io.Writer) error); ok {
|
||||
r1 = rf(jobID, ctx, command, tty, stdin, stdout, stderr)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// ExecuteCommand provides a mock function with given fields: jobID, ctx, command, tty, privilegedExecution, stdin, stdout, stderr
|
||||
func (_m *ExecutorAPIMock) ExecuteCommand(jobID string, ctx context.Context, command string, tty bool, privilegedExecution bool, stdin io.Reader, stdout io.Writer, stderr io.Writer) (int, error) {
|
||||
ret := _m.Called(jobID, ctx, command, tty, privilegedExecution, stdin, stdout, stderr)
|
||||
|
||||
var r0 int
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(string, context.Context, string, bool, bool, io.Reader, io.Writer, io.Writer) (int, error)); ok {
|
||||
return rf(jobID, ctx, command, tty, privilegedExecution, stdin, stdout, stderr)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(string, context.Context, string, bool, bool, io.Reader, io.Writer, io.Writer) int); ok {
|
||||
r0 = rf(jobID, ctx, command, tty, privilegedExecution, stdin, stdout, stderr)
|
||||
} else {
|
||||
r0 = ret.Get(0).(int)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(string, context.Context, string, bool, bool, io.Reader, io.Writer, io.Writer) error); ok {
|
||||
r1 = rf(jobID, ctx, command, tty, privilegedExecution, stdin, stdout, stderr)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// JobScale provides a mock function with given fields: jobID
|
||||
func (_m *ExecutorAPIMock) JobScale(jobID string) (uint, error) {
|
||||
ret := _m.Called(jobID)
|
||||
|
||||
var r0 uint
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(string) (uint, error)); ok {
|
||||
return rf(jobID)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(string) uint); ok {
|
||||
r0 = rf(jobID)
|
||||
} else {
|
||||
r0 = ret.Get(0).(uint)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(string) error); ok {
|
||||
r1 = rf(jobID)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// LoadEnvironmentJobs provides a mock function with given fields:
|
||||
func (_m *ExecutorAPIMock) LoadEnvironmentJobs() ([]*api.Job, error) {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 []*api.Job
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func() ([]*api.Job, error)); ok {
|
||||
return rf()
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func() []*api.Job); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]*api.Job)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func() error); ok {
|
||||
r1 = rf()
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// LoadJobList provides a mock function with given fields:
|
||||
func (_m *ExecutorAPIMock) LoadJobList() ([]*api.JobListStub, error) {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 []*api.JobListStub
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func() ([]*api.JobListStub, error)); ok {
|
||||
return rf()
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func() []*api.JobListStub); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]*api.JobListStub)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func() error); ok {
|
||||
r1 = rf()
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// LoadRunnerIDs provides a mock function with given fields: prefix
|
||||
func (_m *ExecutorAPIMock) LoadRunnerIDs(prefix string) ([]string, error) {
|
||||
ret := _m.Called(prefix)
|
||||
|
||||
var r0 []string
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(string) ([]string, error)); ok {
|
||||
return rf(prefix)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(string) []string); ok {
|
||||
r0 = rf(prefix)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]string)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(string) error); ok {
|
||||
r1 = rf(prefix)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// LoadRunnerJobs provides a mock function with given fields: environmentID
|
||||
func (_m *ExecutorAPIMock) LoadRunnerJobs(environmentID dto.EnvironmentID) ([]*api.Job, error) {
|
||||
ret := _m.Called(environmentID)
|
||||
|
||||
var r0 []*api.Job
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(dto.EnvironmentID) ([]*api.Job, error)); ok {
|
||||
return rf(environmentID)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(dto.EnvironmentID) []*api.Job); ok {
|
||||
r0 = rf(environmentID)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]*api.Job)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(dto.EnvironmentID) error); ok {
|
||||
r1 = rf(environmentID)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// LoadRunnerPortMappings provides a mock function with given fields: runnerID
|
||||
func (_m *ExecutorAPIMock) LoadRunnerPortMappings(runnerID string) ([]api.PortMapping, error) {
|
||||
ret := _m.Called(runnerID)
|
||||
|
||||
var r0 []api.PortMapping
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(string) ([]api.PortMapping, error)); ok {
|
||||
return rf(runnerID)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(string) []api.PortMapping); ok {
|
||||
r0 = rf(runnerID)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]api.PortMapping)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(string) error); ok {
|
||||
r1 = rf(runnerID)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MarkRunnerAsUsed provides a mock function with given fields: runnerID, duration
|
||||
func (_m *ExecutorAPIMock) MarkRunnerAsUsed(runnerID string, duration int) error {
|
||||
ret := _m.Called(runnerID, duration)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string, int) error); ok {
|
||||
r0 = rf(runnerID, duration)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// MonitorEvaluation provides a mock function with given fields: evaluationID, ctx
|
||||
func (_m *ExecutorAPIMock) MonitorEvaluation(evaluationID string, ctx context.Context) error {
|
||||
ret := _m.Called(evaluationID, ctx)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string, context.Context) error); ok {
|
||||
r0 = rf(evaluationID, ctx)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// RegisterNomadJob provides a mock function with given fields: job
|
||||
func (_m *ExecutorAPIMock) RegisterNomadJob(job *api.Job) (string, error) {
|
||||
ret := _m.Called(job)
|
||||
|
||||
var r0 string
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(*api.Job) (string, error)); ok {
|
||||
return rf(job)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(*api.Job) string); ok {
|
||||
r0 = rf(job)
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(*api.Job) error); ok {
|
||||
r1 = rf(job)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// RegisterRunnerJob provides a mock function with given fields: template
|
||||
func (_m *ExecutorAPIMock) RegisterRunnerJob(template *api.Job) error {
|
||||
ret := _m.Called(template)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(*api.Job) error); ok {
|
||||
r0 = rf(template)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// SetJobScale provides a mock function with given fields: jobID, count, reason
|
||||
func (_m *ExecutorAPIMock) SetJobScale(jobID string, count uint, reason string) error {
|
||||
ret := _m.Called(jobID, count, reason)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string, uint, string) error); ok {
|
||||
r0 = rf(jobID, count, reason)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// WatchEventStream provides a mock function with given fields: ctx, callbacks
|
||||
func (_m *ExecutorAPIMock) WatchEventStream(ctx context.Context, callbacks *AllocationProcessing) error {
|
||||
ret := _m.Called(ctx, callbacks)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *AllocationProcessing) error); ok {
|
||||
r0 = rf(ctx, callbacks)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// allocation provides a mock function with given fields: jobID
|
||||
func (_m *ExecutorAPIMock) allocation(jobID string) (*api.Allocation, error) {
|
||||
ret := _m.Called(jobID)
|
||||
|
||||
var r0 *api.Allocation
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(string) (*api.Allocation, error)); ok {
|
||||
return rf(jobID)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(string) *api.Allocation); ok {
|
||||
r0 = rf(jobID)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*api.Allocation)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(string) error); ok {
|
||||
r1 = rf(jobID)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// init provides a mock function with given fields: nomadConfig
|
||||
func (_m *ExecutorAPIMock) init(nomadConfig *config.Nomad) error {
|
||||
ret := _m.Called(nomadConfig)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(*config.Nomad) error); ok {
|
||||
r0 = rf(nomadConfig)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// job provides a mock function with given fields: jobID
|
||||
func (_m *ExecutorAPIMock) job(jobID string) (*api.Job, error) {
|
||||
ret := _m.Called(jobID)
|
||||
|
||||
var r0 *api.Job
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(string) (*api.Job, error)); ok {
|
||||
return rf(jobID)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(string) *api.Job); ok {
|
||||
r0 = rf(jobID)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*api.Job)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(string) error); ok {
|
||||
r1 = rf(jobID)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// listAllocations provides a mock function with given fields:
|
||||
func (_m *ExecutorAPIMock) listAllocations() ([]*api.AllocationListStub, error) {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 []*api.AllocationListStub
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func() ([]*api.AllocationListStub, error)); ok {
|
||||
return rf()
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func() []*api.AllocationListStub); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]*api.AllocationListStub)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func() error); ok {
|
||||
r1 = rf()
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// listJobs provides a mock function with given fields: prefix
|
||||
func (_m *ExecutorAPIMock) listJobs(prefix string) ([]*api.JobListStub, error) {
|
||||
ret := _m.Called(prefix)
|
||||
|
||||
var r0 []*api.JobListStub
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(string) ([]*api.JobListStub, error)); ok {
|
||||
return rf(prefix)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(string) []*api.JobListStub); ok {
|
||||
r0 = rf(prefix)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]*api.JobListStub)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(string) error); ok {
|
||||
r1 = rf(prefix)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
type mockConstructorTestingTNewExecutorAPIMock interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}
|
||||
|
||||
// NewExecutorAPIMock creates a new instance of ExecutorAPIMock. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
func NewExecutorAPIMock(t mockConstructorTestingTNewExecutorAPIMock) *ExecutorAPIMock {
|
||||
mock := &ExecutorAPIMock{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
@ -1,156 +0,0 @@
|
||||
package nomad
|
||||
|
||||
import (
|
||||
nomadApi "github.com/hashicorp/nomad/api"
|
||||
"github.com/openHPI/poseidon/internal/config"
|
||||
"github.com/openHPI/poseidon/pkg/dto"
|
||||
"github.com/openHPI/poseidon/tests/helpers"
|
||||
)
|
||||
|
||||
func (s *MainTestSuite) TestFindTaskGroup() {
|
||||
s.Run("Returns nil if task group not found", func() {
|
||||
group := FindTaskGroup(&nomadApi.Job{}, TaskGroupName)
|
||||
s.Nil(group)
|
||||
})
|
||||
|
||||
s.Run("Finds task group when existent", func() {
|
||||
_, job := helpers.CreateTemplateJob()
|
||||
group := FindTaskGroup(job, TaskGroupName)
|
||||
s.NotNil(group)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MainTestSuite) TestFindOrCreateDefaultTask() {
|
||||
s.Run("Adds default task group when not set", func() {
|
||||
job := &nomadApi.Job{}
|
||||
group := FindAndValidateDefaultTaskGroup(job)
|
||||
s.NotNil(group)
|
||||
s.Equal(TaskGroupName, *group.Name)
|
||||
s.Equal(1, len(job.TaskGroups))
|
||||
s.Equal(group, job.TaskGroups[0])
|
||||
s.Equal(TaskCount, *group.Count)
|
||||
})
|
||||
|
||||
s.Run("Does not modify task group when already set", func() {
|
||||
job := &nomadApi.Job{}
|
||||
groupName := TaskGroupName
|
||||
expectedGroup := &nomadApi.TaskGroup{Name: &groupName}
|
||||
job.TaskGroups = []*nomadApi.TaskGroup{expectedGroup}
|
||||
|
||||
group := FindAndValidateDefaultTaskGroup(job)
|
||||
s.NotNil(group)
|
||||
s.Equal(1, len(job.TaskGroups))
|
||||
s.Equal(expectedGroup, group)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MainTestSuite) TestFindOrCreateConfigTaskGroup() {
|
||||
s.Run("Adds config task group when not set", func() {
|
||||
job := &nomadApi.Job{}
|
||||
group := FindAndValidateConfigTaskGroup(job)
|
||||
s.NotNil(group)
|
||||
s.Equal(group, job.TaskGroups[0])
|
||||
s.Equal(1, len(job.TaskGroups))
|
||||
|
||||
s.Equal(ConfigTaskGroupName, *group.Name)
|
||||
s.Equal(0, *group.Count)
|
||||
})
|
||||
|
||||
s.Run("Does not modify task group when already set", func() {
|
||||
job := &nomadApi.Job{}
|
||||
groupName := ConfigTaskGroupName
|
||||
expectedGroup := &nomadApi.TaskGroup{Name: &groupName}
|
||||
job.TaskGroups = []*nomadApi.TaskGroup{expectedGroup}
|
||||
|
||||
group := FindAndValidateConfigTaskGroup(job)
|
||||
s.NotNil(group)
|
||||
s.Equal(1, len(job.TaskGroups))
|
||||
s.Equal(expectedGroup, group)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MainTestSuite) TestFindOrCreateTask() {
|
||||
s.Run("Does not modify default task when already set", func() {
|
||||
groupName := TaskGroupName
|
||||
group := &nomadApi.TaskGroup{Name: &groupName}
|
||||
expectedTask := &nomadApi.Task{Name: TaskName}
|
||||
group.Tasks = []*nomadApi.Task{expectedTask}
|
||||
|
||||
task := FindAndValidateDefaultTask(group)
|
||||
s.NotNil(task)
|
||||
s.Equal(1, len(group.Tasks))
|
||||
s.Equal(expectedTask, task)
|
||||
})
|
||||
|
||||
s.Run("Does not modify config task when already set", func() {
|
||||
groupName := ConfigTaskGroupName
|
||||
group := &nomadApi.TaskGroup{Name: &groupName}
|
||||
expectedTask := &nomadApi.Task{Name: ConfigTaskName}
|
||||
group.Tasks = []*nomadApi.Task{expectedTask}
|
||||
|
||||
task := FindAndValidateConfigTask(group)
|
||||
s.NotNil(task)
|
||||
s.Equal(1, len(group.Tasks))
|
||||
s.Equal(expectedTask, task)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MainTestSuite) TestSetForcePullFlag() {
|
||||
_, job := helpers.CreateTemplateJob()
|
||||
taskGroup := FindAndValidateDefaultTaskGroup(job)
|
||||
task := FindAndValidateDefaultTask(taskGroup)
|
||||
|
||||
s.Run("Ignoring passed value if DisableForcePull", func() {
|
||||
config.Config.Nomad.DisableForcePull = true
|
||||
SetForcePullFlag(job, true)
|
||||
s.Equal(false, task.Config["force_pull"])
|
||||
})
|
||||
|
||||
s.Run("Using passed value if not DisableForcePull", func() {
|
||||
config.Config.Nomad.DisableForcePull = false
|
||||
SetForcePullFlag(job, true)
|
||||
s.Equal(true, task.Config["force_pull"])
|
||||
|
||||
SetForcePullFlag(job, false)
|
||||
s.Equal(false, task.Config["force_pull"])
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MainTestSuite) TestIsEnvironmentTemplateID() {
|
||||
s.True(IsEnvironmentTemplateID("template-42"))
|
||||
s.False(IsEnvironmentTemplateID("template-42-100"))
|
||||
s.False(IsEnvironmentTemplateID("job-42"))
|
||||
s.False(IsEnvironmentTemplateID("template-top"))
|
||||
}
|
||||
|
||||
func (s *MainTestSuite) TestRunnerJobID() {
|
||||
s.Equal("0-RANDOM-UUID", RunnerJobID(0, "RANDOM-UUID"))
|
||||
}
|
||||
|
||||
func (s *MainTestSuite) TestTemplateJobID() {
|
||||
s.Equal("template-42", TemplateJobID(42))
|
||||
}
|
||||
|
||||
func (s *MainTestSuite) TestEnvironmentIDFromRunnerID() {
|
||||
id, err := EnvironmentIDFromRunnerID("42-RANDOM-UUID")
|
||||
s.NoError(err)
|
||||
s.Equal(dto.EnvironmentID(42), id)
|
||||
|
||||
_, err = EnvironmentIDFromRunnerID("")
|
||||
s.Error(err)
|
||||
}
|
||||
|
||||
func (s *MainTestSuite) TestOOMKilledAllocation() {
|
||||
event := nomadApi.TaskEvent{Details: map[string]string{"oom_killed": "true"}}
|
||||
state := nomadApi.TaskState{Restarts: 2, Events: []*nomadApi.TaskEvent{&event}}
|
||||
alloc := nomadApi.Allocation{TaskStates: map[string]*nomadApi.TaskState{TaskName: &state}}
|
||||
s.False(isOOMKilled(&alloc))
|
||||
|
||||
event2 := nomadApi.TaskEvent{Details: map[string]string{"oom_killed": "false"}}
|
||||
alloc.TaskStates[TaskName].Events = []*nomadApi.TaskEvent{&event, &event2}
|
||||
s.False(isOOMKilled(&alloc))
|
||||
|
||||
event3 := nomadApi.TaskEvent{Details: map[string]string{"oom_killed": "true"}}
|
||||
alloc.TaskStates[TaskName].Events = []*nomadApi.TaskEvent{&event, &event2, &event3}
|
||||
s.True(isOOMKilled(&alloc))
|
||||
}
|
@ -1,983 +0,0 @@
|
||||
package nomad
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
nomadApi "github.com/hashicorp/nomad/api"
|
||||
"github.com/hashicorp/nomad/nomad/structs"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/openHPI/poseidon/internal/config"
|
||||
"github.com/openHPI/poseidon/pkg/nullio"
|
||||
"github.com/openHPI/poseidon/pkg/storage"
|
||||
"github.com/openHPI/poseidon/tests"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"io"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
noopAllocationProcessing = &AllocationProcessing{
|
||||
OnNew: func(_ *nomadApi.Allocation, _ time.Duration) {},
|
||||
OnDeleted: func(_ string, _ error) bool { return false },
|
||||
}
|
||||
ErrUnexpectedEOF = errors.New("unexpected EOF")
|
||||
)
|
||||
|
||||
func TestLoadRunnersTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(LoadRunnersTestSuite))
|
||||
}
|
||||
|
||||
type LoadRunnersTestSuite struct {
|
||||
tests.MemoryLeakTestSuite
|
||||
jobID string
|
||||
mock *apiQuerierMock
|
||||
nomadAPIClient APIClient
|
||||
availableRunner *nomadApi.JobListStub
|
||||
anotherAvailableRunner *nomadApi.JobListStub
|
||||
pendingRunner *nomadApi.JobListStub
|
||||
deadRunner *nomadApi.JobListStub
|
||||
}
|
||||
|
||||
func (s *LoadRunnersTestSuite) SetupTest() {
|
||||
s.MemoryLeakTestSuite.SetupTest()
|
||||
s.jobID = tests.DefaultRunnerID
|
||||
|
||||
s.mock = &apiQuerierMock{}
|
||||
s.nomadAPIClient = APIClient{apiQuerier: s.mock}
|
||||
|
||||
s.availableRunner = newJobListStub(tests.DefaultRunnerID, structs.JobStatusRunning, 1)
|
||||
s.anotherAvailableRunner = newJobListStub(tests.AnotherRunnerID, structs.JobStatusRunning, 1)
|
||||
s.pendingRunner = newJobListStub(tests.DefaultRunnerID+"-1", structs.JobStatusPending, 0)
|
||||
s.deadRunner = newJobListStub(tests.AnotherRunnerID+"-1", structs.JobStatusDead, 0)
|
||||
}
|
||||
|
||||
func newJobListStub(id, status string, amountRunning int) *nomadApi.JobListStub {
|
||||
return &nomadApi.JobListStub{
|
||||
ID: id,
|
||||
Status: status,
|
||||
JobSummary: &nomadApi.JobSummary{
|
||||
JobID: id,
|
||||
Summary: map[string]nomadApi.TaskGroupSummary{TaskGroupName: {Running: amountRunning}},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *LoadRunnersTestSuite) TestErrorOfUnderlyingApiCallIsPropagated() {
|
||||
s.mock.On("listJobs", mock.AnythingOfType("string")).
|
||||
Return(nil, tests.ErrDefault)
|
||||
|
||||
returnedIds, err := s.nomadAPIClient.LoadRunnerIDs(s.jobID)
|
||||
s.Nil(returnedIds)
|
||||
s.Equal(tests.ErrDefault, err)
|
||||
}
|
||||
|
||||
func (s *LoadRunnersTestSuite) TestReturnsNoErrorWhenUnderlyingApiCallDoesNot() {
|
||||
s.mock.On("listJobs", mock.AnythingOfType("string")).
|
||||
Return([]*nomadApi.JobListStub{}, nil)
|
||||
|
||||
_, err := s.nomadAPIClient.LoadRunnerIDs(s.jobID)
|
||||
s.NoError(err)
|
||||
}
|
||||
|
||||
func (s *LoadRunnersTestSuite) TestAvailableRunnerIsReturned() {
|
||||
s.mock.On("listJobs", mock.AnythingOfType("string")).
|
||||
Return([]*nomadApi.JobListStub{s.availableRunner}, nil)
|
||||
|
||||
returnedIds, err := s.nomadAPIClient.LoadRunnerIDs(s.jobID)
|
||||
s.Require().NoError(err)
|
||||
s.Len(returnedIds, 1)
|
||||
s.Equal(s.availableRunner.ID, returnedIds[0])
|
||||
}
|
||||
|
||||
func (s *LoadRunnersTestSuite) TestPendingRunnerIsReturned() {
|
||||
s.mock.On("listJobs", mock.AnythingOfType("string")).
|
||||
Return([]*nomadApi.JobListStub{s.pendingRunner}, nil)
|
||||
|
||||
returnedIds, err := s.nomadAPIClient.LoadRunnerIDs(s.jobID)
|
||||
s.Require().NoError(err)
|
||||
s.Len(returnedIds, 1)
|
||||
s.Equal(s.pendingRunner.ID, returnedIds[0])
|
||||
}
|
||||
|
||||
func (s *LoadRunnersTestSuite) TestDeadRunnerIsNotReturned() {
|
||||
s.mock.On("listJobs", mock.AnythingOfType("string")).
|
||||
Return([]*nomadApi.JobListStub{s.deadRunner}, nil)
|
||||
|
||||
returnedIds, err := s.nomadAPIClient.LoadRunnerIDs(s.jobID)
|
||||
s.Require().NoError(err)
|
||||
s.Empty(returnedIds)
|
||||
}
|
||||
|
||||
func (s *LoadRunnersTestSuite) TestReturnsAllAvailableRunners() {
|
||||
runnersList := []*nomadApi.JobListStub{
|
||||
s.availableRunner,
|
||||
s.anotherAvailableRunner,
|
||||
s.pendingRunner,
|
||||
s.deadRunner,
|
||||
}
|
||||
s.mock.On("listJobs", mock.AnythingOfType("string")).
|
||||
Return(runnersList, nil)
|
||||
|
||||
returnedIds, err := s.nomadAPIClient.LoadRunnerIDs(s.jobID)
|
||||
s.Require().NoError(err)
|
||||
s.Len(returnedIds, 3)
|
||||
s.Contains(returnedIds, s.availableRunner.ID)
|
||||
s.Contains(returnedIds, s.anotherAvailableRunner.ID)
|
||||
s.Contains(returnedIds, s.pendingRunner.ID)
|
||||
s.NotContains(returnedIds, s.deadRunner.ID)
|
||||
}
|
||||
|
||||
const TestNamespace = "unit-tests"
|
||||
const TestNomadToken = "n0m4d-t0k3n"
|
||||
const TestDefaultAddress = "127.0.0.1"
|
||||
const evaluationID = "evaluation-id"
|
||||
|
||||
func NomadTestConfig(address string) *config.Nomad {
|
||||
return &config.Nomad{
|
||||
Address: address,
|
||||
Port: 4646,
|
||||
Token: TestNomadToken,
|
||||
TLS: config.TLS{
|
||||
Active: false,
|
||||
},
|
||||
Namespace: TestNamespace,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *MainTestSuite) TestApiClient_init() {
|
||||
client := &APIClient{apiQuerier: &nomadAPIClient{}}
|
||||
err := client.init(NomadTestConfig(TestDefaultAddress))
|
||||
s.Require().Nil(err)
|
||||
}
|
||||
|
||||
func (s *MainTestSuite) TestApiClientCanNotBeInitializedWithInvalidUrl() {
|
||||
client := &APIClient{apiQuerier: &nomadAPIClient{}}
|
||||
err := client.init(NomadTestConfig("http://" + TestDefaultAddress))
|
||||
s.NotNil(err)
|
||||
}
|
||||
|
||||
func (s *MainTestSuite) TestNewExecutorApiCanBeCreatedWithoutError() {
|
||||
expectedClient := &APIClient{apiQuerier: &nomadAPIClient{}}
|
||||
err := expectedClient.init(NomadTestConfig(TestDefaultAddress))
|
||||
s.Require().Nil(err)
|
||||
|
||||
_, err = NewExecutorAPI(NomadTestConfig(TestDefaultAddress))
|
||||
s.Require().Nil(err)
|
||||
}
|
||||
|
||||
// asynchronouslyMonitorEvaluation creates an APIClient with mocked Nomad API and
|
||||
// runs the MonitorEvaluation method in a goroutine. The mock returns a read-only
|
||||
// version of the given stream to simulate an event stream gotten from the real
|
||||
// Nomad API.
|
||||
func asynchronouslyMonitorEvaluation(stream <-chan *nomadApi.Events) chan error {
|
||||
ctx := context.Background()
|
||||
// We can only get a read-only channel once we return it from a function.
|
||||
readOnlyStream := func() <-chan *nomadApi.Events { return stream }()
|
||||
|
||||
apiMock := &apiQuerierMock{}
|
||||
apiMock.On("EventStream", mock.AnythingOfType("*context.cancelCtx")).
|
||||
Return(readOnlyStream, nil)
|
||||
apiClient := &APIClient{apiMock, storage.NewLocalStorage[chan error](), storage.NewLocalStorage[*allocationData](), false}
|
||||
|
||||
errChan := make(chan error)
|
||||
go func() {
|
||||
errChan <- apiClient.MonitorEvaluation(evaluationID, ctx)
|
||||
}()
|
||||
return errChan
|
||||
}
|
||||
|
||||
func (s *MainTestSuite) TestApiClient_MonitorEvaluationReturnsNilWhenStreamIsClosed() {
|
||||
stream := make(chan *nomadApi.Events)
|
||||
errChan := asynchronouslyMonitorEvaluation(stream)
|
||||
|
||||
close(stream)
|
||||
var err error
|
||||
// If close doesn't terminate MonitorEvaluation, this test won't complete without a timeout.
|
||||
select {
|
||||
case err = <-errChan:
|
||||
case <-time.After(time.Millisecond * 10):
|
||||
s.T().Fatal("MonitorEvaluation didn't finish as expected")
|
||||
}
|
||||
s.Nil(err)
|
||||
}
|
||||
|
||||
func (s *MainTestSuite) TestApiClient_MonitorEvaluationReturnsErrorWhenStreamReturnsError() {
|
||||
apiMock := &apiQuerierMock{}
|
||||
apiMock.On("EventStream", mock.AnythingOfType("*context.cancelCtx")).
|
||||
Return(nil, tests.ErrDefault)
|
||||
apiClient := &APIClient{apiMock, storage.NewLocalStorage[chan error](), storage.NewLocalStorage[*allocationData](), false}
|
||||
err := apiClient.MonitorEvaluation("id", context.Background())
|
||||
s.ErrorIs(err, tests.ErrDefault)
|
||||
}
|
||||
|
||||
type eventPayload struct {
|
||||
Evaluation *nomadApi.Evaluation
|
||||
Allocation *nomadApi.Allocation
|
||||
}
|
||||
|
||||
// eventForEvaluation takes an evaluation and creates an Event with the given evaluation
|
||||
// as its payload. Nomad uses the mapstructure library to decode the payload, which we
|
||||
// simply reverse here.
|
||||
func eventForEvaluation(t *testing.T, eval *nomadApi.Evaluation) nomadApi.Event {
|
||||
t.Helper()
|
||||
payload := make(map[string]interface{})
|
||||
|
||||
err := mapstructure.Decode(eventPayload{Evaluation: eval}, &payload)
|
||||
if err != nil {
|
||||
t.Fatalf("Couldn't decode evaluation %v into payload map", eval)
|
||||
return nomadApi.Event{}
|
||||
}
|
||||
event := nomadApi.Event{Topic: nomadApi.TopicEvaluation, Payload: payload}
|
||||
return event
|
||||
}
|
||||
|
||||
// simulateNomadEventStream streams the given events sequentially to the stream channel.
|
||||
// It returns how many events have been processed until an error occurred.
|
||||
func simulateNomadEventStream(
|
||||
ctx context.Context,
|
||||
stream chan<- *nomadApi.Events,
|
||||
errChan chan error,
|
||||
events []*nomadApi.Events,
|
||||
) (int, error) {
|
||||
eventsProcessed := 0
|
||||
var e *nomadApi.Events
|
||||
for _, e = range events {
|
||||
select {
|
||||
case err := <-errChan:
|
||||
return eventsProcessed, err
|
||||
case stream <- e:
|
||||
eventsProcessed++
|
||||
}
|
||||
}
|
||||
close(stream)
|
||||
// Wait for last event being processed
|
||||
var err error
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case err = <-errChan:
|
||||
}
|
||||
return eventsProcessed, err
|
||||
}
|
||||
|
||||
// runEvaluationMonitoring simulates events streamed from the Nomad event stream
|
||||
// to the MonitorEvaluation method. It starts the MonitorEvaluation function as a goroutine
|
||||
// and sequentially transfers the events from the given array to a channel simulating the stream.
|
||||
func runEvaluationMonitoring(ctx context.Context, events []*nomadApi.Events) (eventsProcessed int, err error) {
|
||||
stream := make(chan *nomadApi.Events)
|
||||
errChan := asynchronouslyMonitorEvaluation(stream)
|
||||
return simulateNomadEventStream(ctx, stream, errChan, events)
|
||||
}
|
||||
|
||||
func (s *MainTestSuite) TestApiClient_MonitorEvaluationWithSuccessfulEvent() {
|
||||
eval := nomadApi.Evaluation{Status: structs.EvalStatusComplete}
|
||||
pendingEval := nomadApi.Evaluation{Status: structs.EvalStatusPending}
|
||||
|
||||
// make sure that the tested function can complete
|
||||
s.Require().Nil(checkEvaluation(&eval))
|
||||
|
||||
events := nomadApi.Events{Events: []nomadApi.Event{eventForEvaluation(s.T(), &eval)}}
|
||||
pendingEvaluationEvents := nomadApi.Events{Events: []nomadApi.Event{eventForEvaluation(s.T(), &pendingEval)}}
|
||||
multipleEventsWithPending := nomadApi.Events{Events: []nomadApi.Event{
|
||||
eventForEvaluation(s.T(), &pendingEval), eventForEvaluation(s.T(), &eval),
|
||||
}}
|
||||
|
||||
var cases = []struct {
|
||||
streamedEvents []*nomadApi.Events
|
||||
expectedEventsProcessed int
|
||||
name string
|
||||
}{
|
||||
{[]*nomadApi.Events{&events}, 1,
|
||||
"it completes with successful event"},
|
||||
{[]*nomadApi.Events{&events, &events}, 2,
|
||||
"it keeps listening after first successful event"},
|
||||
{[]*nomadApi.Events{{}, &events}, 2,
|
||||
"it skips heartbeat and completes"},
|
||||
{[]*nomadApi.Events{&pendingEvaluationEvents, &events}, 2,
|
||||
"it skips pending evaluation and completes"},
|
||||
{[]*nomadApi.Events{&multipleEventsWithPending}, 1,
|
||||
"it handles multiple events per received event"},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
s.Run(c.name, func() {
|
||||
eventsProcessed, err := runEvaluationMonitoring(s.TestCtx, c.streamedEvents)
|
||||
s.Nil(err)
|
||||
s.Equal(c.expectedEventsProcessed, eventsProcessed)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (s *MainTestSuite) TestApiClient_MonitorEvaluationWithFailingEvent() {
|
||||
eval := nomadApi.Evaluation{ID: evaluationID, Status: structs.EvalStatusFailed}
|
||||
evalErr := checkEvaluation(&eval)
|
||||
s.Require().NotNil(evalErr)
|
||||
|
||||
pendingEval := nomadApi.Evaluation{Status: structs.EvalStatusPending}
|
||||
|
||||
events := nomadApi.Events{Events: []nomadApi.Event{eventForEvaluation(s.T(), &eval)}}
|
||||
pendingEvaluationEvents := nomadApi.Events{Events: []nomadApi.Event{eventForEvaluation(s.T(), &pendingEval)}}
|
||||
multipleEventsWithPending := nomadApi.Events{Events: []nomadApi.Event{
|
||||
eventForEvaluation(s.T(), &pendingEval), eventForEvaluation(s.T(), &eval),
|
||||
}}
|
||||
eventsWithErr := nomadApi.Events{Err: tests.ErrDefault, Events: []nomadApi.Event{{}}}
|
||||
|
||||
var cases = []struct {
|
||||
streamedEvents []*nomadApi.Events
|
||||
expectedEventsProcessed int
|
||||
expectedError error
|
||||
name string
|
||||
}{
|
||||
{[]*nomadApi.Events{&events}, 1, evalErr,
|
||||
"it fails with failing event"},
|
||||
{[]*nomadApi.Events{{}, &events}, 2, evalErr,
|
||||
"it skips heartbeat and fail"},
|
||||
{[]*nomadApi.Events{&pendingEvaluationEvents, &events}, 2, evalErr,
|
||||
"it skips pending evaluation and fail"},
|
||||
{[]*nomadApi.Events{&multipleEventsWithPending}, 1, evalErr,
|
||||
"it handles multiple events per received event and fails"},
|
||||
{[]*nomadApi.Events{&eventsWithErr}, 1, tests.ErrDefault,
|
||||
"it fails with event error when event has error"},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
s.Run(c.name, func() {
|
||||
eventsProcessed, err := runEvaluationMonitoring(s.TestCtx, c.streamedEvents)
|
||||
s.Require().NotNil(err)
|
||||
s.Contains(err.Error(), c.expectedError.Error())
|
||||
s.Equal(c.expectedEventsProcessed, eventsProcessed)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (s *MainTestSuite) TestApiClient_MonitorEvaluationFailsWhenFailingToDecodeEvaluation() {
|
||||
event := nomadApi.Event{
|
||||
Topic: nomadApi.TopicEvaluation,
|
||||
// This should fail decoding, as Evaluation.Status is expected to be a string, not int
|
||||
Payload: map[string]interface{}{"Evaluation": map[string]interface{}{"Status": 1}},
|
||||
}
|
||||
_, err := event.Evaluation()
|
||||
s.Require().NotNil(err)
|
||||
eventsProcessed, err := runEvaluationMonitoring(s.TestCtx, []*nomadApi.Events{{Events: []nomadApi.Event{event}}})
|
||||
s.Error(err)
|
||||
s.Equal(1, eventsProcessed)
|
||||
}
|
||||
|
||||
func (s *MainTestSuite) TestCheckEvaluationWithFailedAllocations() {
|
||||
testKey := "test1"
|
||||
failedAllocs := map[string]*nomadApi.AllocationMetric{
|
||||
testKey: {NodesExhausted: 1},
|
||||
}
|
||||
evaluation := nomadApi.Evaluation{FailedTGAllocs: failedAllocs, Status: structs.EvalStatusFailed}
|
||||
|
||||
assertMessageContainsCorrectStrings := func(msg string) {
|
||||
s.Contains(msg, evaluation.Status, "error should contain the evaluation status")
|
||||
s.Contains(msg, fmt.Sprintf("%s: %#v", testKey, failedAllocs[testKey]),
|
||||
"error should contain the failed allocations metric")
|
||||
}
|
||||
|
||||
var msgWithoutBlockedEval, msgWithBlockedEval string
|
||||
s.Run("without blocked eval", func() {
|
||||
err := checkEvaluation(&evaluation)
|
||||
s.Require().NotNil(err)
|
||||
msgWithoutBlockedEval = err.Error()
|
||||
assertMessageContainsCorrectStrings(msgWithoutBlockedEval)
|
||||
})
|
||||
|
||||
s.Run("with blocked eval", func() {
|
||||
evaluation.BlockedEval = "blocking-eval"
|
||||
err := checkEvaluation(&evaluation)
|
||||
s.Require().NotNil(err)
|
||||
msgWithBlockedEval = err.Error()
|
||||
assertMessageContainsCorrectStrings(msgWithBlockedEval)
|
||||
})
|
||||
|
||||
s.NotEqual(msgWithBlockedEval, msgWithoutBlockedEval)
|
||||
}
|
||||
|
||||
func (s *MainTestSuite) TestCheckEvaluationWithoutFailedAllocations() {
|
||||
evaluation := nomadApi.Evaluation{FailedTGAllocs: make(map[string]*nomadApi.AllocationMetric)}
|
||||
|
||||
s.Run("when evaluation status complete", func() {
|
||||
evaluation.Status = structs.EvalStatusComplete
|
||||
err := checkEvaluation(&evaluation)
|
||||
s.Nil(err)
|
||||
})
|
||||
|
||||
s.Run("when evaluation status not complete", func() {
|
||||
incompleteStates := []string{structs.EvalStatusFailed, structs.EvalStatusCancelled,
|
||||
structs.EvalStatusBlocked, structs.EvalStatusPending}
|
||||
for _, status := range incompleteStates {
|
||||
evaluation.Status = status
|
||||
err := checkEvaluation(&evaluation)
|
||||
s.Require().NotNil(err)
|
||||
s.Contains(err.Error(), status, "error should contain the evaluation status")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MainTestSuite) TestApiClient_WatchAllocationsIgnoresOldAllocations() {
|
||||
oldStoppedAllocation := createOldAllocation(structs.AllocClientStatusRunning, structs.AllocDesiredStatusStop)
|
||||
oldPendingAllocation := createOldAllocation(structs.AllocClientStatusPending, structs.AllocDesiredStatusRun)
|
||||
oldRunningAllocation := createOldAllocation(structs.AllocClientStatusRunning, structs.AllocDesiredStatusRun)
|
||||
oldAllocationEvents := nomadApi.Events{Events: []nomadApi.Event{
|
||||
eventForAllocation(s.T(), oldStoppedAllocation),
|
||||
eventForAllocation(s.T(), oldPendingAllocation),
|
||||
eventForAllocation(s.T(), oldRunningAllocation),
|
||||
}}
|
||||
|
||||
assertWatchAllocation(s, []*nomadApi.Events{&oldAllocationEvents},
|
||||
[]*nomadApi.Allocation(nil), []string(nil))
|
||||
}
|
||||
|
||||
func createOldAllocation(clientStatus, desiredStatus string) *nomadApi.Allocation {
|
||||
return createAllocation(time.Now().Add(-time.Minute).UnixNano(), clientStatus, desiredStatus)
|
||||
}
|
||||
|
||||
func (s *MainTestSuite) TestApiClient_WatchAllocationsIgnoresUnhandledEvents() {
|
||||
nodeEvents := nomadApi.Events{Events: []nomadApi.Event{
|
||||
{
|
||||
Topic: nomadApi.TopicNode,
|
||||
Type: structs.TypeNodeEvent,
|
||||
},
|
||||
}}
|
||||
assertWatchAllocation(s, []*nomadApi.Events{&nodeEvents}, []*nomadApi.Allocation(nil), []string(nil))
|
||||
}
|
||||
|
||||
func (s *MainTestSuite) TestApiClient_WatchAllocationsUsesCallbacksForEvents() {
|
||||
pendingAllocation := createRecentAllocation(structs.AllocClientStatusPending, structs.AllocDesiredStatusRun)
|
||||
pendingEvents := nomadApi.Events{Events: []nomadApi.Event{eventForAllocation(s.T(), pendingAllocation)}}
|
||||
|
||||
s.Run("it does not add allocation when client status is pending", func() {
|
||||
assertWatchAllocation(s, []*nomadApi.Events{&pendingEvents}, []*nomadApi.Allocation(nil), []string(nil))
|
||||
})
|
||||
|
||||
startedAllocation := createRecentAllocation(structs.AllocClientStatusRunning, structs.AllocDesiredStatusRun)
|
||||
startedEvents := nomadApi.Events{Events: []nomadApi.Event{eventForAllocation(s.T(), startedAllocation)}}
|
||||
pendingStartedEvents := nomadApi.Events{Events: []nomadApi.Event{
|
||||
eventForAllocation(s.T(), pendingAllocation), eventForAllocation(s.T(), startedAllocation)}}
|
||||
|
||||
s.Run("it adds allocation with matching events", func() {
|
||||
assertWatchAllocation(s, []*nomadApi.Events{&pendingStartedEvents},
|
||||
[]*nomadApi.Allocation{startedAllocation}, []string(nil))
|
||||
})
|
||||
|
||||
s.Run("it skips heartbeat and adds allocation with matching events", func() {
|
||||
assertWatchAllocation(s, []*nomadApi.Events{&pendingStartedEvents},
|
||||
[]*nomadApi.Allocation{startedAllocation}, []string(nil))
|
||||
})
|
||||
|
||||
stoppedAllocation := createRecentAllocation(structs.AllocClientStatusComplete, structs.AllocDesiredStatusStop)
|
||||
stoppedEvents := nomadApi.Events{Events: []nomadApi.Event{eventForAllocation(s.T(), stoppedAllocation)}}
|
||||
pendingStartStopEvents := nomadApi.Events{Events: []nomadApi.Event{
|
||||
eventForAllocation(s.T(), pendingAllocation),
|
||||
eventForAllocation(s.T(), startedAllocation),
|
||||
eventForAllocation(s.T(), stoppedAllocation),
|
||||
}}
|
||||
|
||||
s.Run("it adds and deletes the allocation", func() {
|
||||
assertWatchAllocation(s, []*nomadApi.Events{&pendingStartStopEvents},
|
||||
[]*nomadApi.Allocation{startedAllocation}, []string{stoppedAllocation.JobID})
|
||||
})
|
||||
|
||||
s.Run("it ignores duplicate events", func() {
|
||||
assertWatchAllocation(s, []*nomadApi.Events{&pendingEvents, &startedEvents, &startedEvents,
|
||||
&stoppedEvents, &stoppedEvents, &stoppedEvents},
|
||||
[]*nomadApi.Allocation{startedAllocation}, []string{startedAllocation.JobID})
|
||||
})
|
||||
|
||||
s.Run("it ignores events of unknown allocations", func() {
|
||||
assertWatchAllocation(s, []*nomadApi.Events{&startedEvents, &startedEvents,
|
||||
&stoppedEvents, &stoppedEvents, &stoppedEvents}, []*nomadApi.Allocation(nil), []string(nil))
|
||||
})
|
||||
|
||||
s.Run("it removes restarted allocations", func() {
|
||||
assertWatchAllocation(s, []*nomadApi.Events{&pendingStartedEvents, &pendingStartedEvents},
|
||||
[]*nomadApi.Allocation{startedAllocation, startedAllocation}, []string{startedAllocation.JobID})
|
||||
})
|
||||
|
||||
rescheduleAllocation := createRecentAllocation(structs.AllocClientStatusPending, structs.AllocDesiredStatusRun)
|
||||
rescheduleAllocation.ID = tests.AnotherUUID
|
||||
rescheduleAllocation.PreviousAllocation = pendingAllocation.ID
|
||||
rescheduleStartedAllocation := createRecentAllocation(structs.AllocClientStatusRunning, structs.AllocDesiredStatusRun)
|
||||
rescheduleStartedAllocation.ID = tests.AnotherUUID
|
||||
rescheduleAllocation.PreviousAllocation = pendingAllocation.ID
|
||||
rescheduleEvents := nomadApi.Events{Events: []nomadApi.Event{
|
||||
eventForAllocation(s.T(), rescheduleAllocation), eventForAllocation(s.T(), rescheduleStartedAllocation)}}
|
||||
|
||||
s.Run("it removes rescheduled allocations", func() {
|
||||
assertWatchAllocation(s, []*nomadApi.Events{&pendingStartedEvents, &rescheduleEvents},
|
||||
[]*nomadApi.Allocation{startedAllocation, rescheduleStartedAllocation}, []string{startedAllocation.JobID})
|
||||
})
|
||||
|
||||
stoppedPendingAllocation := createRecentAllocation(structs.AllocClientStatusPending, structs.AllocDesiredStatusStop)
|
||||
stoppedPendingEvents := nomadApi.Events{Events: []nomadApi.Event{eventForAllocation(s.T(), stoppedPendingAllocation)}}
|
||||
|
||||
s.Run("it does not callback for stopped pending allocations", func() {
|
||||
assertWatchAllocation(s, []*nomadApi.Events{&pendingEvents, &stoppedPendingEvents},
|
||||
[]*nomadApi.Allocation(nil), []string(nil))
|
||||
})
|
||||
|
||||
failedAllocation := createRecentAllocation(structs.AllocClientStatusFailed, structs.AllocDesiredStatusStop)
|
||||
failedEvents := nomadApi.Events{Events: []nomadApi.Event{eventForAllocation(s.T(), failedAllocation)}}
|
||||
|
||||
s.Run("it removes stopped failed allocations", func() {
|
||||
assertWatchAllocation(s, []*nomadApi.Events{&pendingStartedEvents, &failedEvents},
|
||||
[]*nomadApi.Allocation{startedAllocation}, []string{failedAllocation.JobID})
|
||||
})
|
||||
|
||||
lostAllocation := createRecentAllocation(structs.AllocClientStatusLost, structs.AllocDesiredStatusStop)
|
||||
lostEvents := nomadApi.Events{Events: []nomadApi.Event{eventForAllocation(s.T(), lostAllocation)}}
|
||||
|
||||
s.Run("it removes stopped lost allocations", func() {
|
||||
assertWatchAllocation(s, []*nomadApi.Events{&pendingStartedEvents, &lostEvents},
|
||||
[]*nomadApi.Allocation{startedAllocation}, []string{lostAllocation.JobID})
|
||||
})
|
||||
|
||||
rescheduledLostAllocation := createRecentAllocation(structs.AllocClientStatusLost, structs.AllocDesiredStatusStop)
|
||||
rescheduledLostAllocation.NextAllocation = tests.AnotherUUID
|
||||
rescheduledLostEvents := nomadApi.Events{Events: []nomadApi.Event{
|
||||
eventForAllocation(s.T(), rescheduledLostAllocation)}}
|
||||
|
||||
s.Run("it removes lost allocations not before the last restart attempt", func() {
|
||||
assertWatchAllocation(s, []*nomadApi.Events{&pendingStartedEvents, &rescheduledLostEvents},
|
||||
[]*nomadApi.Allocation{startedAllocation}, []string(nil))
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MainTestSuite) TestHandleAllocationEventBuffersPendingAllocation() {
|
||||
s.Run("AllocationUpdated", func() {
|
||||
newPendingAllocation := createRecentAllocation(structs.AllocClientStatusPending, structs.AllocDesiredStatusRun)
|
||||
newPendingEvent := eventForAllocation(s.T(), newPendingAllocation)
|
||||
|
||||
allocations := storage.NewLocalStorage[*allocationData]()
|
||||
err := handleAllocationEvent(
|
||||
time.Now().UnixNano(), allocations, &newPendingEvent, noopAllocationProcessing)
|
||||
s.Require().NoError(err)
|
||||
|
||||
_, ok := allocations.Get(newPendingAllocation.ID)
|
||||
s.True(ok)
|
||||
})
|
||||
s.Run("PlanResult", func() {
|
||||
newPendingAllocation := createRecentAllocation(structs.AllocClientStatusPending, structs.AllocDesiredStatusRun)
|
||||
newPendingEvent := eventForAllocation(s.T(), newPendingAllocation)
|
||||
newPendingEvent.Type = structs.TypePlanResult
|
||||
|
||||
allocations := storage.NewLocalStorage[*allocationData]()
|
||||
err := handleAllocationEvent(
|
||||
time.Now().UnixNano(), allocations, &newPendingEvent, noopAllocationProcessing)
|
||||
s.Require().NoError(err)
|
||||
|
||||
_, ok := allocations.Get(newPendingAllocation.ID)
|
||||
s.True(ok)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MainTestSuite) TestHandleAllocationEvent_RegressionTest_14_09_2023() {
|
||||
jobID := "29-6f04b525-5315-11ee-af32-fa163e079f19"
|
||||
a1ID := "04d86250-550c-62f9-9a21-ecdc3b38773e"
|
||||
a1Starting := createRecentAllocation(structs.AllocClientStatusPending, structs.AllocDesiredStatusRun)
|
||||
a1Starting.ID = a1ID
|
||||
a1Starting.JobID = jobID
|
||||
|
||||
// With this event the job is added to the idle runners
|
||||
a1Running := createRecentAllocation(structs.AllocClientStatusRunning, structs.AllocDesiredStatusRun)
|
||||
a1Running.ID = a1ID
|
||||
a1Running.JobID = jobID
|
||||
|
||||
// With this event the job is removed from the idle runners
|
||||
a2ID := "102f282f-376a-1453-4d3d-7d4e32046acd"
|
||||
a2Starting := createRecentAllocation(structs.AllocClientStatusPending, structs.AllocDesiredStatusRun)
|
||||
a2Starting.ID = a2ID
|
||||
a2Starting.PreviousAllocation = a1ID
|
||||
a2Starting.JobID = jobID
|
||||
|
||||
// Because the runner is neither an idle runner nor an used runner, this event triggered the now removed
|
||||
// race condition handling that led to neither removing a2 from the allocations nor adding a3 to the allocations.
|
||||
a3ID := "0d8a8ece-cf52-2968-5a9f-e972a4150a6e"
|
||||
a3Starting := createRecentAllocation(structs.AllocClientStatusPending, structs.AllocDesiredStatusRun)
|
||||
a3Starting.ID = a3ID
|
||||
a3Starting.PreviousAllocation = a2ID
|
||||
a3Starting.JobID = jobID
|
||||
|
||||
// a2Stopping was not ignored and led to an unexpected allocation stopping.
|
||||
a2Stopping := createRecentAllocation(structs.AllocClientStatusPending, structs.AllocDesiredStatusStop)
|
||||
a2Stopping.ID = a2ID
|
||||
a2Stopping.PreviousAllocation = a1ID
|
||||
a2Stopping.NextAllocation = a3ID
|
||||
a2Stopping.JobID = jobID
|
||||
|
||||
// a2Complete was not ignored (wrong behavior).
|
||||
a2Complete := createRecentAllocation(structs.AllocClientStatusComplete, structs.AllocDesiredStatusStop)
|
||||
a2Complete.ID = a2ID
|
||||
a2Complete.PreviousAllocation = a1ID
|
||||
a2Complete.NextAllocation = a3ID
|
||||
a2Complete.JobID = jobID
|
||||
|
||||
// a3Running was ignored because it was unknown (wrong behavior).
|
||||
a3Running := createRecentAllocation(structs.AllocClientStatusRunning, structs.AllocDesiredStatusRun)
|
||||
a3Running.ID = a3ID
|
||||
a3Running.PreviousAllocation = a2ID
|
||||
a3Running.JobID = jobID
|
||||
|
||||
events := []*nomadApi.Events{{Events: []nomadApi.Event{
|
||||
eventForAllocation(s.T(), a1Starting),
|
||||
eventForAllocation(s.T(), a1Running),
|
||||
eventForAllocation(s.T(), a2Starting),
|
||||
eventForAllocation(s.T(), a3Starting),
|
||||
eventForAllocation(s.T(), a2Stopping),
|
||||
eventForAllocation(s.T(), a2Complete),
|
||||
eventForAllocation(s.T(), a3Running),
|
||||
}}}
|
||||
|
||||
idleRunner := make(map[string]bool)
|
||||
callbacks := &AllocationProcessing{
|
||||
OnNew: func(alloc *nomadApi.Allocation, _ time.Duration) {
|
||||
idleRunner[alloc.JobID] = true
|
||||
},
|
||||
OnDeleted: func(jobID string, _ error) bool {
|
||||
_, ok := idleRunner[jobID]
|
||||
delete(idleRunner, jobID)
|
||||
return !ok
|
||||
},
|
||||
}
|
||||
|
||||
_, err := runAllocationWatching(s, events, callbacks)
|
||||
s.NoError(err)
|
||||
s.True(idleRunner[jobID])
|
||||
}
|
||||
|
||||
func (s *MainTestSuite) TestHandleAllocationEvent_ReportsOOMKilledStatus() {
|
||||
restartedAllocation := createRecentAllocation(structs.AllocClientStatusPending, structs.AllocDesiredStatusRun)
|
||||
event := nomadApi.TaskEvent{Details: map[string]string{"oom_killed": "true"}}
|
||||
state := nomadApi.TaskState{Restarts: 1, Events: []*nomadApi.TaskEvent{&event}}
|
||||
restartedAllocation.TaskStates = map[string]*nomadApi.TaskState{TaskName: &state}
|
||||
restartedEvent := eventForAllocation(s.T(), restartedAllocation)
|
||||
|
||||
allocations := storage.NewLocalStorage[*allocationData]()
|
||||
allocations.Add(restartedAllocation.ID, &allocationData{jobID: restartedAllocation.JobID})
|
||||
|
||||
var reason error
|
||||
err := handleAllocationEvent(time.Now().UnixNano(), allocations, &restartedEvent, &AllocationProcessing{
|
||||
OnNew: func(_ *nomadApi.Allocation, _ time.Duration) {},
|
||||
OnDeleted: func(_ string, r error) bool {
|
||||
reason = r
|
||||
return true
|
||||
},
|
||||
})
|
||||
s.Require().NoError(err)
|
||||
s.ErrorIs(reason, ErrorOOMKilled)
|
||||
}
|
||||
|
||||
func (s *MainTestSuite) TestAPIClient_WatchAllocationsReturnsErrorWhenAllocationStreamCannotBeRetrieved() {
|
||||
apiMock := &apiQuerierMock{}
|
||||
apiMock.On("EventStream", mock.Anything).Return(nil, tests.ErrDefault)
|
||||
apiClient := &APIClient{apiMock, storage.NewLocalStorage[chan error](), storage.NewLocalStorage[*allocationData](), false}
|
||||
|
||||
err := apiClient.WatchEventStream(context.Background(), noopAllocationProcessing)
|
||||
s.ErrorIs(err, tests.ErrDefault)
|
||||
}
|
||||
|
||||
// Test case: WatchAllocations returns an error when an allocation cannot be retrieved without receiving further events.
|
||||
func (s *MainTestSuite) TestAPIClient_WatchAllocations() {
|
||||
event := nomadApi.Event{
|
||||
Type: structs.TypeAllocationUpdated,
|
||||
Topic: nomadApi.TopicAllocation,
|
||||
// This should fail decoding, as Allocation.ID is expected to be a string, not int
|
||||
Payload: map[string]interface{}{"Allocation": map[string]interface{}{"ID": 1}},
|
||||
}
|
||||
_, err := event.Allocation()
|
||||
s.Require().Error(err)
|
||||
|
||||
events := []*nomadApi.Events{{Events: []nomadApi.Event{event}}, {}}
|
||||
eventsProcessed, err := runAllocationWatching(s, events, noopAllocationProcessing)
|
||||
s.Error(err)
|
||||
s.Equal(1, eventsProcessed)
|
||||
}
|
||||
|
||||
func (s *MainTestSuite) TestAPIClient_WatchAllocationsReturnsErrorOnUnexpectedEOF() {
|
||||
events := []*nomadApi.Events{{Err: ErrUnexpectedEOF}, {}}
|
||||
eventsProcessed, err := runAllocationWatching(s, events, noopAllocationProcessing)
|
||||
s.Error(err)
|
||||
s.Equal(1, eventsProcessed)
|
||||
}
|
||||
|
||||
func assertWatchAllocation(s *MainTestSuite, events []*nomadApi.Events,
|
||||
expectedNewAllocations []*nomadApi.Allocation, expectedDeletedAllocations []string) {
|
||||
s.T().Helper()
|
||||
var newAllocations []*nomadApi.Allocation
|
||||
var deletedAllocations []string
|
||||
callbacks := &AllocationProcessing{
|
||||
OnNew: func(alloc *nomadApi.Allocation, _ time.Duration) {
|
||||
newAllocations = append(newAllocations, alloc)
|
||||
},
|
||||
OnDeleted: func(jobID string, _ error) bool {
|
||||
deletedAllocations = append(deletedAllocations, jobID)
|
||||
return false
|
||||
},
|
||||
}
|
||||
|
||||
eventsProcessed, err := runAllocationWatching(s, events, callbacks)
|
||||
s.NoError(err)
|
||||
s.Equal(len(events), eventsProcessed)
|
||||
|
||||
s.Equal(expectedNewAllocations, newAllocations)
|
||||
s.Equal(expectedDeletedAllocations, deletedAllocations)
|
||||
}
|
||||
|
||||
// runAllocationWatching simulates events streamed from the Nomad event stream
|
||||
// to the MonitorEvaluation method. It starts the MonitorEvaluation function as a goroutine
|
||||
// and sequentially transfers the events from the given array to a channel simulating the stream.
|
||||
func runAllocationWatching(s *MainTestSuite, events []*nomadApi.Events, callbacks *AllocationProcessing) (
|
||||
eventsProcessed int, err error) {
|
||||
s.T().Helper()
|
||||
stream := make(chan *nomadApi.Events)
|
||||
errChan := asynchronouslyWatchAllocations(stream, callbacks)
|
||||
return simulateNomadEventStream(s.TestCtx, stream, errChan, events)
|
||||
}
|
||||
|
||||
// asynchronouslyMonitorEvaluation creates an APIClient with mocked Nomad API and
|
||||
// runs the MonitorEvaluation method in a goroutine. The mock returns a read-only
|
||||
// version of the given stream to simulate an event stream gotten from the real
|
||||
// Nomad API.
|
||||
func asynchronouslyWatchAllocations(stream chan *nomadApi.Events, callbacks *AllocationProcessing) chan error {
|
||||
ctx := context.Background()
|
||||
// We can only get a read-only channel once we return it from a function.
|
||||
readOnlyStream := func() <-chan *nomadApi.Events { return stream }()
|
||||
|
||||
apiMock := &apiQuerierMock{}
|
||||
apiMock.On("EventStream", ctx).Return(readOnlyStream, nil)
|
||||
apiClient := &APIClient{apiMock, storage.NewLocalStorage[chan error](), storage.NewLocalStorage[*allocationData](), false}
|
||||
|
||||
errChan := make(chan error)
|
||||
go func() {
|
||||
errChan <- apiClient.WatchEventStream(ctx, callbacks)
|
||||
}()
|
||||
return errChan
|
||||
}
|
||||
|
||||
// eventForEvaluation takes an evaluation and creates an Event with the given evaluation
|
||||
// as its payload. Nomad uses the mapstructure library to decode the payload, which we
|
||||
// simply reverse here.
|
||||
func eventForAllocation(t *testing.T, alloc *nomadApi.Allocation) nomadApi.Event {
|
||||
t.Helper()
|
||||
payload := make(map[string]interface{})
|
||||
|
||||
err := mapstructure.Decode(eventPayload{Allocation: alloc}, &payload)
|
||||
if err != nil {
|
||||
t.Fatalf("Couldn't decode allocation %v into payload map", err)
|
||||
return nomadApi.Event{}
|
||||
}
|
||||
event := nomadApi.Event{
|
||||
Topic: nomadApi.TopicAllocation,
|
||||
Type: structs.TypeAllocationUpdated,
|
||||
Payload: payload,
|
||||
}
|
||||
return event
|
||||
}
|
||||
|
||||
func createAllocation(modifyTime int64, clientStatus, desiredStatus string) *nomadApi.Allocation {
|
||||
return &nomadApi.Allocation{
|
||||
ID: tests.DefaultUUID,
|
||||
JobID: tests.DefaultRunnerID,
|
||||
ModifyTime: modifyTime,
|
||||
ClientStatus: clientStatus,
|
||||
DesiredStatus: desiredStatus,
|
||||
}
|
||||
}
|
||||
|
||||
func createRecentAllocation(clientStatus, desiredStatus string) *nomadApi.Allocation {
|
||||
return createAllocation(time.Now().Add(time.Minute).UnixNano(), clientStatus, desiredStatus)
|
||||
}
|
||||
|
||||
func TestExecuteCommandTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(ExecuteCommandTestSuite))
|
||||
}
|
||||
|
||||
type ExecuteCommandTestSuite struct {
|
||||
tests.MemoryLeakTestSuite
|
||||
allocationID string
|
||||
ctx context.Context
|
||||
testCommand string
|
||||
expectedStdout string
|
||||
expectedStderr string
|
||||
apiMock *apiQuerierMock
|
||||
nomadAPIClient APIClient
|
||||
}
|
||||
|
||||
func (s *ExecuteCommandTestSuite) SetupTest() {
|
||||
s.MemoryLeakTestSuite.SetupTest()
|
||||
s.allocationID = "test-allocation-id"
|
||||
s.ctx = context.Background()
|
||||
s.testCommand = "echo \"do nothing\""
|
||||
s.expectedStdout = "stdout"
|
||||
s.expectedStderr = "stderr"
|
||||
s.apiMock = &apiQuerierMock{}
|
||||
s.nomadAPIClient = APIClient{apiQuerier: s.apiMock}
|
||||
}
|
||||
|
||||
const withTTY = true
|
||||
|
||||
func (s *ExecuteCommandTestSuite) TestWithSeparateStderr() {
|
||||
config.Config.Server.InteractiveStderr = true
|
||||
commandExitCode := 42
|
||||
stderrExitCode := 1
|
||||
|
||||
var stdout, stderr bytes.Buffer
|
||||
var calledStdoutCommand, calledStderrCommand string
|
||||
runFn := func(args mock.Arguments) {
|
||||
var ok bool
|
||||
calledCommand, ok := args.Get(2).(string)
|
||||
s.Require().True(ok)
|
||||
var out string
|
||||
if isStderrCommand := strings.Contains(calledCommand, "mkfifo"); isStderrCommand {
|
||||
calledStderrCommand = calledCommand
|
||||
out = s.expectedStderr
|
||||
} else {
|
||||
calledStdoutCommand = calledCommand
|
||||
out = s.expectedStdout
|
||||
}
|
||||
|
||||
writer, ok := args.Get(5).(io.Writer)
|
||||
s.Require().True(ok)
|
||||
_, err := writer.Write([]byte(out))
|
||||
s.Require().NoError(err)
|
||||
}
|
||||
|
||||
s.apiMock.On("Execute", s.allocationID, mock.Anything, mock.Anything, withTTY,
|
||||
mock.AnythingOfType("nullio.Reader"), mock.Anything, mock.Anything).Run(runFn).Return(stderrExitCode, nil)
|
||||
s.apiMock.On("Execute", s.allocationID, mock.Anything, mock.Anything, withTTY,
|
||||
mock.AnythingOfType("*bytes.Buffer"), mock.Anything, mock.Anything).Run(runFn).Return(commandExitCode, nil)
|
||||
|
||||
exitCode, err := s.nomadAPIClient.ExecuteCommand(s.allocationID, s.ctx, s.testCommand, withTTY,
|
||||
UnprivilegedExecution, &bytes.Buffer{}, &stdout, &stderr)
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.apiMock.AssertNumberOfCalls(s.T(), "Execute", 2)
|
||||
s.Equal(commandExitCode, exitCode)
|
||||
|
||||
s.Run("should wrap command in stderr wrapper", func() {
|
||||
s.Require().NotEmpty(calledStdoutCommand)
|
||||
stderrWrapperCommand := fmt.Sprintf(stderrWrapperCommandFormat, stderrFifoFormat, s.testCommand, stderrFifoFormat)
|
||||
stdoutFifoRegexp := strings.ReplaceAll(regexp.QuoteMeta(stderrWrapperCommand), "%d", "\\d*")
|
||||
stdoutFifoRegexp = strings.Replace(stdoutFifoRegexp, s.testCommand, ".*", 1)
|
||||
s.Regexp(stdoutFifoRegexp, calledStdoutCommand)
|
||||
})
|
||||
|
||||
s.Run("should call correct stderr command", func() {
|
||||
s.Require().NotEmpty(calledStderrCommand)
|
||||
stderrFifoCommand := fmt.Sprintf(stderrFifoCommandFormat, stderrFifoFormat, stderrFifoFormat, stderrFifoFormat)
|
||||
stderrFifoRegexp := strings.ReplaceAll(regexp.QuoteMeta(stderrFifoCommand), "%d", "\\d*")
|
||||
s.Regexp(stderrFifoRegexp, calledStderrCommand)
|
||||
})
|
||||
|
||||
s.Run("should return correct output", func() {
|
||||
s.Equal(s.expectedStdout, stdout.String())
|
||||
s.Equal(s.expectedStderr, stderr.String())
|
||||
})
|
||||
}
|
||||
|
||||
func (s *ExecuteCommandTestSuite) TestWithSeparateStderrReturnsCommandError() {
|
||||
config.Config.Server.InteractiveStderr = true
|
||||
|
||||
call := s.mockExecute(mock.AnythingOfType("string"), 0, nil, func(_ mock.Arguments) {})
|
||||
call.Run(func(args mock.Arguments) {
|
||||
var ok bool
|
||||
calledCommand, ok := args.Get(2).(string)
|
||||
s.Require().True(ok)
|
||||
|
||||
if isStderrCommand := strings.Contains(calledCommand, "mkfifo"); isStderrCommand {
|
||||
// Here we defuse the data race condition of the ReturnArguments being set twice at the same time.
|
||||
<-time.After(tests.ShortTimeout)
|
||||
call.ReturnArguments = mock.Arguments{1, nil}
|
||||
} else {
|
||||
call.ReturnArguments = mock.Arguments{1, tests.ErrDefault}
|
||||
}
|
||||
})
|
||||
|
||||
_, err := s.nomadAPIClient.ExecuteCommand(s.allocationID, s.ctx, s.testCommand, withTTY, UnprivilegedExecution,
|
||||
nullio.Reader{}, io.Discard, io.Discard)
|
||||
s.Equal(tests.ErrDefault, err)
|
||||
}
|
||||
|
||||
func (s *ExecuteCommandTestSuite) TestWithoutSeparateStderr() {
|
||||
config.Config.Server.InteractiveStderr = false
|
||||
var stdout, stderr bytes.Buffer
|
||||
commandExitCode := 42
|
||||
|
||||
// mock regular call
|
||||
expectedCommand := prepareCommandWithoutTTY(s.testCommand, UnprivilegedExecution)
|
||||
s.mockExecute(expectedCommand, commandExitCode, nil, func(args mock.Arguments) {
|
||||
stdout, ok := args.Get(5).(io.Writer)
|
||||
s.Require().True(ok)
|
||||
_, err := stdout.Write([]byte(s.expectedStdout))
|
||||
s.Require().NoError(err)
|
||||
stderr, ok := args.Get(6).(io.Writer)
|
||||
s.Require().True(ok)
|
||||
_, err = stderr.Write([]byte(s.expectedStderr))
|
||||
s.Require().NoError(err)
|
||||
})
|
||||
|
||||
exitCode, err := s.nomadAPIClient.ExecuteCommand(s.allocationID, s.ctx, s.testCommand, withTTY,
|
||||
UnprivilegedExecution, nullio.Reader{}, &stdout, &stderr)
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.apiMock.AssertNumberOfCalls(s.T(), "Execute", 1)
|
||||
s.Equal(commandExitCode, exitCode)
|
||||
s.Equal(s.expectedStdout, stdout.String())
|
||||
s.Equal(s.expectedStderr, stderr.String())
|
||||
}
|
||||
|
||||
func (s *ExecuteCommandTestSuite) TestWithoutSeparateStderrReturnsCommandError() {
|
||||
config.Config.Server.InteractiveStderr = false
|
||||
expectedCommand := prepareCommandWithoutTTY(s.testCommand, UnprivilegedExecution)
|
||||
s.mockExecute(expectedCommand, 1, tests.ErrDefault, func(args mock.Arguments) {})
|
||||
_, err := s.nomadAPIClient.ExecuteCommand(s.allocationID, s.ctx, s.testCommand, withTTY, UnprivilegedExecution,
|
||||
nullio.Reader{}, io.Discard, io.Discard)
|
||||
s.ErrorIs(err, tests.ErrDefault)
|
||||
}
|
||||
|
||||
func (s *ExecuteCommandTestSuite) mockExecute(command interface{}, exitCode int,
|
||||
err error, runFunc func(arguments mock.Arguments)) *mock.Call {
|
||||
return s.apiMock.On("Execute", s.allocationID, mock.Anything, command, withTTY,
|
||||
mock.Anything, mock.Anything, mock.Anything).
|
||||
Run(runFunc).
|
||||
Return(exitCode, err)
|
||||
}
|
||||
|
||||
func (s *MainTestSuite) TestAPIClient_LoadRunnerPortMappings() {
|
||||
apiMock := &apiQuerierMock{}
|
||||
mockedCall := apiMock.On("allocation", tests.DefaultRunnerID)
|
||||
nomadAPIClient := APIClient{apiQuerier: apiMock}
|
||||
|
||||
s.Run("should return error when API query fails", func() {
|
||||
mockedCall.Return(nil, tests.ErrDefault)
|
||||
portMappings, err := nomadAPIClient.LoadRunnerPortMappings(tests.DefaultRunnerID)
|
||||
s.Nil(portMappings)
|
||||
s.ErrorIs(err, tests.ErrDefault)
|
||||
})
|
||||
|
||||
s.Run("should return error when AllocatedResources is nil", func() {
|
||||
mockedCall.Return(&nomadApi.Allocation{AllocatedResources: nil}, nil)
|
||||
portMappings, err := nomadAPIClient.LoadRunnerPortMappings(tests.DefaultRunnerID)
|
||||
s.ErrorIs(err, ErrorNoAllocatedResourcesFound)
|
||||
s.Nil(portMappings)
|
||||
})
|
||||
|
||||
s.Run("should correctly return ports", func() {
|
||||
allocation := &nomadApi.Allocation{
|
||||
AllocatedResources: &nomadApi.AllocatedResources{
|
||||
Shared: nomadApi.AllocatedSharedResources{Ports: tests.DefaultPortMappings},
|
||||
},
|
||||
}
|
||||
mockedCall.Return(allocation, nil)
|
||||
portMappings, err := nomadAPIClient.LoadRunnerPortMappings(tests.DefaultRunnerID)
|
||||
s.NoError(err)
|
||||
s.Equal(tests.DefaultPortMappings, portMappings)
|
||||
})
|
||||
}
|
@ -1,52 +0,0 @@
|
||||
package nomad
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
)
|
||||
|
||||
func (s *MainTestSuite) TestSentryDebugWriter_Write() {
|
||||
buf := &bytes.Buffer{}
|
||||
w := SentryDebugWriter{Target: buf, Ctx: s.TestCtx}
|
||||
|
||||
description := "TestDebugMessageDescription"
|
||||
data := "\x1EPoseidon " + description + " 1676646791482\x1E"
|
||||
count, err := w.Write([]byte(data))
|
||||
|
||||
s.Require().NoError(err)
|
||||
s.Equal(len(data), count)
|
||||
s.NotContains(buf.String(), description)
|
||||
}
|
||||
|
||||
func (s *MainTestSuite) TestSentryDebugWriter_WriteComposed() {
|
||||
buf := &bytes.Buffer{}
|
||||
w := SentryDebugWriter{Target: buf, Ctx: s.TestCtx}
|
||||
|
||||
data := "Hello World!\r\n\x1EPoseidon unset 1678540012404\x1E\x1EPoseidon /sbin/setuser user 1678540012408\x1E"
|
||||
count, err := w.Write([]byte(data))
|
||||
|
||||
s.Require().NoError(err)
|
||||
s.Equal(len(data), count)
|
||||
s.Contains(buf.String(), "Hello World!")
|
||||
}
|
||||
|
||||
func (s *MainTestSuite) TestSentryDebugWriter_Close() {
|
||||
buf := &bytes.Buffer{}
|
||||
w := NewSentryDebugWriter(buf, s.TestCtx)
|
||||
s.Require().Empty(w.lastSpan.Tags)
|
||||
|
||||
w.Close(42)
|
||||
s.Require().Contains(w.lastSpan.Tags, "exit_code")
|
||||
s.Equal("42", w.lastSpan.Tags["exit_code"])
|
||||
}
|
||||
|
||||
func (s *MainTestSuite) TestSentryDebugWriter_handleTimeDebugMessage() {
|
||||
buf := &bytes.Buffer{}
|
||||
w := NewSentryDebugWriter(buf, s.TestCtx)
|
||||
s.Require().Equal("nomad.execute.connect", w.lastSpan.Op)
|
||||
|
||||
description := "TestDebugMessageDescription"
|
||||
match := map[string][]byte{"time": []byte("1676646791482"), "text": []byte(description)}
|
||||
w.handleTimeDebugMessage(match)
|
||||
s.Equal("nomad.execute.bash", w.lastSpan.Op)
|
||||
s.Equal(description, w.lastSpan.Description)
|
||||
}
|
@ -1,48 +0,0 @@
|
||||
package runner
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/openHPI/poseidon/pkg/dto"
|
||||
"time"
|
||||
)
|
||||
|
||||
type AWSRunnerManager struct {
|
||||
*AbstractManager
|
||||
}
|
||||
|
||||
// NewAWSRunnerManager creates a new runner manager that keeps track of all runners at AWS.
|
||||
func NewAWSRunnerManager(ctx context.Context) *AWSRunnerManager {
|
||||
return &AWSRunnerManager{NewAbstractManager(ctx)}
|
||||
}
|
||||
|
||||
func (a AWSRunnerManager) Claim(id dto.EnvironmentID, duration int) (Runner, error) {
|
||||
environment, ok := a.GetEnvironment(id)
|
||||
if !ok {
|
||||
r, err := a.NextHandler().Claim(id, duration)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("aws wrapped: %w", err)
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
runner, ok := environment.Sample()
|
||||
if !ok {
|
||||
log.Warn("no aws runner available")
|
||||
return nil, ErrNoRunnersAvailable
|
||||
}
|
||||
|
||||
a.usedRunners.Add(runner.ID(), runner)
|
||||
runner.SetupTimeout(time.Duration(duration) * time.Second)
|
||||
return runner, nil
|
||||
}
|
||||
|
||||
func (a AWSRunnerManager) Return(r Runner) error {
|
||||
_, isAWSRunner := r.(*AWSFunctionWorkload)
|
||||
if isAWSRunner {
|
||||
a.usedRunners.Delete(r.ID())
|
||||
} else if err := a.NextHandler().Return(r); err != nil {
|
||||
return fmt.Errorf("aws wrapped: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
@ -1,118 +0,0 @@
|
||||
package runner
|
||||
|
||||
import (
|
||||
"github.com/openHPI/poseidon/internal/nomad"
|
||||
"github.com/openHPI/poseidon/pkg/dto"
|
||||
"github.com/openHPI/poseidon/tests"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type MainTestSuite struct {
|
||||
tests.MemoryLeakTestSuite
|
||||
}
|
||||
|
||||
func TestMainTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(MainTestSuite))
|
||||
}
|
||||
|
||||
func (s *MainTestSuite) TestAWSRunnerManager_EnvironmentAccessor() {
|
||||
m := NewAWSRunnerManager(s.TestCtx)
|
||||
|
||||
environments := m.ListEnvironments()
|
||||
s.Empty(environments)
|
||||
|
||||
environment := createBasicEnvironmentMock(defaultEnvironmentID)
|
||||
m.StoreEnvironment(environment)
|
||||
|
||||
environments = m.ListEnvironments()
|
||||
s.Len(environments, 1)
|
||||
s.Equal(environments[0].ID(), dto.EnvironmentID(tests.DefaultEnvironmentIDAsInteger))
|
||||
|
||||
e, ok := m.GetEnvironment(tests.DefaultEnvironmentIDAsInteger)
|
||||
s.True(ok)
|
||||
s.Equal(environment, e)
|
||||
|
||||
_, ok = m.GetEnvironment(tests.AnotherEnvironmentIDAsInteger)
|
||||
s.False(ok)
|
||||
}
|
||||
|
||||
func (s *MainTestSuite) TestAWSRunnerManager_Claim() {
|
||||
m := NewAWSRunnerManager(s.TestCtx)
|
||||
environment := createBasicEnvironmentMock(defaultEnvironmentID)
|
||||
r, err := NewAWSFunctionWorkload(environment, func(_ Runner) error { return nil })
|
||||
s.NoError(err)
|
||||
environment.On("Sample").Return(r, true)
|
||||
m.StoreEnvironment(environment)
|
||||
|
||||
s.Run("returns runner for AWS environment", func() {
|
||||
r, err := m.Claim(tests.DefaultEnvironmentIDAsInteger, 60)
|
||||
s.NoError(err)
|
||||
s.NotNil(r)
|
||||
})
|
||||
|
||||
s.Run("forwards request for non-AWS environments", func() {
|
||||
nextHandler := &ManagerMock{}
|
||||
nextHandler.On("Claim", mock.AnythingOfType("dto.EnvironmentID"), mock.AnythingOfType("int")).
|
||||
Return(nil, nil)
|
||||
m.SetNextHandler(nextHandler)
|
||||
|
||||
_, err := m.Claim(tests.AnotherEnvironmentIDAsInteger, 60)
|
||||
s.Nil(err)
|
||||
nextHandler.AssertCalled(s.T(), "Claim", dto.EnvironmentID(tests.AnotherEnvironmentIDAsInteger), 60)
|
||||
})
|
||||
|
||||
err = r.Destroy(nil)
|
||||
s.NoError(err)
|
||||
}
|
||||
|
||||
func (s *MainTestSuite) TestAWSRunnerManager_Return() {
|
||||
m := NewAWSRunnerManager(s.TestCtx)
|
||||
environment := createBasicEnvironmentMock(defaultEnvironmentID)
|
||||
m.StoreEnvironment(environment)
|
||||
r, err := NewAWSFunctionWorkload(environment, func(_ Runner) error { return nil })
|
||||
s.NoError(err)
|
||||
|
||||
s.Run("removes usedRunner", func() {
|
||||
m.usedRunners.Add(r.ID(), r)
|
||||
s.Contains(m.usedRunners.List(), r)
|
||||
|
||||
err := m.Return(r)
|
||||
s.NoError(err)
|
||||
s.NotContains(m.usedRunners.List(), r)
|
||||
})
|
||||
|
||||
s.Run("calls nextHandler for non-AWS runner", func() {
|
||||
nextHandler := &ManagerMock{}
|
||||
nextHandler.On("Return", mock.AnythingOfType("*runner.NomadJob")).Return(nil)
|
||||
m.SetNextHandler(nextHandler)
|
||||
|
||||
apiMock := &nomad.ExecutorAPIMock{}
|
||||
apiMock.On("DeleteJob", mock.AnythingOfType("string")).Return(nil)
|
||||
nonAWSRunner := NewNomadJob(tests.DefaultRunnerID, nil, apiMock, nil)
|
||||
err := m.Return(nonAWSRunner)
|
||||
s.NoError(err)
|
||||
nextHandler.AssertCalled(s.T(), "Return", nonAWSRunner)
|
||||
|
||||
err = nonAWSRunner.Destroy(nil)
|
||||
s.NoError(err)
|
||||
})
|
||||
|
||||
err = r.Destroy(nil)
|
||||
s.NoError(err)
|
||||
}
|
||||
|
||||
func createBasicEnvironmentMock(id dto.EnvironmentID) *ExecutionEnvironmentMock {
|
||||
environment := &ExecutionEnvironmentMock{}
|
||||
environment.On("ID").Return(id)
|
||||
environment.On("Image").Return("")
|
||||
environment.On("CPULimit").Return(uint(0))
|
||||
environment.On("MemoryLimit").Return(uint(0))
|
||||
environment.On("NetworkAccess").Return(false, nil)
|
||||
environment.On("DeleteRunner", mock.AnythingOfType("string")).Return(nil, false)
|
||||
environment.On("ApplyPrewarmingPoolSize").Return(nil)
|
||||
environment.On("IdleRunnerCount").Return(uint(1)).Maybe()
|
||||
environment.On("PrewarmingPoolSize").Return(uint(1)).Maybe()
|
||||
return environment
|
||||
}
|
@ -1,246 +0,0 @@
|
||||
package runner
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/google/uuid"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/openHPI/poseidon/internal/config"
|
||||
"github.com/openHPI/poseidon/pkg/dto"
|
||||
"github.com/openHPI/poseidon/pkg/monitoring"
|
||||
"github.com/openHPI/poseidon/pkg/storage"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
var ErrWrongMessageType = errors.New("received message that is not a text message")
|
||||
|
||||
type awsFunctionRequest struct {
|
||||
Action string `json:"action"`
|
||||
Cmd []string `json:"cmd"`
|
||||
Files map[dto.FilePath][]byte `json:"files"`
|
||||
}
|
||||
|
||||
// AWSFunctionWorkload is an abstraction to build a request to an AWS Lambda Function.
|
||||
// It is not persisted on a Poseidon restart.
|
||||
// The InactivityTimer is used actively. It stops listening to the Lambda function.
|
||||
// AWS terminates the Lambda Function after the [Globals.Function.Timeout](deploy/aws/template.yaml).
|
||||
type AWSFunctionWorkload struct {
|
||||
InactivityTimer
|
||||
id string
|
||||
fs map[dto.FilePath][]byte
|
||||
executions storage.Storage[*dto.ExecutionRequest]
|
||||
runningExecutions map[string]context.CancelFunc
|
||||
onDestroy DestroyRunnerHandler
|
||||
environment ExecutionEnvironment
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
// NewAWSFunctionWorkload creates a new AWSFunctionWorkload with the provided id.
|
||||
func NewAWSFunctionWorkload(
|
||||
environment ExecutionEnvironment, onDestroy DestroyRunnerHandler) (*AWSFunctionWorkload, error) {
|
||||
newUUID, err := uuid.NewUUID()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed generating runner id: %w", err)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
workload := &AWSFunctionWorkload{
|
||||
id: newUUID.String(),
|
||||
fs: make(map[dto.FilePath][]byte),
|
||||
runningExecutions: make(map[string]context.CancelFunc),
|
||||
onDestroy: onDestroy,
|
||||
environment: environment,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
}
|
||||
workload.executions = storage.NewMonitoredLocalStorage[*dto.ExecutionRequest](
|
||||
monitoring.MeasurementExecutionsAWS, monitorExecutionsRunnerID(environment.ID(), workload.id), time.Minute, ctx)
|
||||
workload.InactivityTimer = NewInactivityTimer(workload, func(_ Runner) error {
|
||||
return workload.Destroy(nil)
|
||||
})
|
||||
return workload, nil
|
||||
}
|
||||
|
||||
func (w *AWSFunctionWorkload) ID() string {
|
||||
return w.id
|
||||
}
|
||||
|
||||
func (w *AWSFunctionWorkload) Environment() dto.EnvironmentID {
|
||||
return w.environment.ID()
|
||||
}
|
||||
|
||||
func (w *AWSFunctionWorkload) MappedPorts() []*dto.MappedPort {
|
||||
return []*dto.MappedPort{}
|
||||
}
|
||||
|
||||
func (w *AWSFunctionWorkload) StoreExecution(id string, request *dto.ExecutionRequest) {
|
||||
w.executions.Add(id, request)
|
||||
}
|
||||
|
||||
func (w *AWSFunctionWorkload) ExecutionExists(id string) bool {
|
||||
_, ok := w.executions.Get(id)
|
||||
return ok
|
||||
}
|
||||
|
||||
// ExecuteInteractively runs the execution request in an AWS function.
|
||||
// It should be further improved by using the passed context to handle lost connections.
|
||||
func (w *AWSFunctionWorkload) ExecuteInteractively(
|
||||
id string, _ io.ReadWriter, stdout, stderr io.Writer, _ context.Context) (
|
||||
<-chan ExitInfo, context.CancelFunc, error) {
|
||||
w.ResetTimeout()
|
||||
request, ok := w.executions.Pop(id)
|
||||
if !ok {
|
||||
return nil, nil, ErrorUnknownExecution
|
||||
}
|
||||
hideEnvironmentVariables(request, "AWS")
|
||||
request.PrivilegedExecution = true // AWS does not support multiple users at this moment.
|
||||
command, ctx, cancel := prepareExecution(request, w.ctx)
|
||||
commands := []string{"/bin/bash", "-c", command}
|
||||
exitInternal := make(chan ExitInfo)
|
||||
exit := make(chan ExitInfo, 1)
|
||||
|
||||
go w.executeCommand(ctx, commands, stdout, stderr, exitInternal)
|
||||
go w.handleRunnerTimeout(ctx, exitInternal, exit, id)
|
||||
|
||||
return exit, cancel, nil
|
||||
}
|
||||
|
||||
// ListFileSystem is currently not supported with this aws serverless function.
|
||||
// This is because the function execution ends with the termination of the workload code.
|
||||
// So an on-demand file system listing after the termination is not possible. Also, we do not want to copy all files.
|
||||
func (w *AWSFunctionWorkload) ListFileSystem(_ string, _ bool, _ io.Writer, _ bool, _ context.Context) error {
|
||||
return dto.ErrNotSupported
|
||||
}
|
||||
|
||||
// UpdateFileSystem copies Files into the executor.
|
||||
// Current limitation: No files can be deleted apart from the previously added files.
|
||||
// Future Work: Deduplication of the file systems, as the largest workload is likely to be used by additional
|
||||
// CSV files or similar, which are the same for many executions.
|
||||
func (w *AWSFunctionWorkload) UpdateFileSystem(request *dto.UpdateFileSystemRequest, _ context.Context) error {
|
||||
for _, path := range request.Delete {
|
||||
delete(w.fs, path)
|
||||
}
|
||||
for _, file := range request.Copy {
|
||||
w.fs[file.Path] = file.Content
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetFileContent is currently not supported with this aws serverless function.
|
||||
// This is because the function execution ends with the termination of the workload code.
|
||||
// So an on-demand file streaming after the termination is not possible. Also, we do not want to copy all files.
|
||||
func (w *AWSFunctionWorkload) GetFileContent(_ string, _ http.ResponseWriter, _ bool, _ context.Context) error {
|
||||
return dto.ErrNotSupported
|
||||
}
|
||||
|
||||
func (w *AWSFunctionWorkload) Destroy(_ DestroyReason) error {
|
||||
w.cancel()
|
||||
if err := w.onDestroy(w); err != nil {
|
||||
return fmt.Errorf("error while destroying aws runner: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *AWSFunctionWorkload) executeCommand(ctx context.Context, command []string,
|
||||
stdout, stderr io.Writer, exit chan<- ExitInfo,
|
||||
) {
|
||||
defer close(exit)
|
||||
data := &awsFunctionRequest{
|
||||
Action: w.environment.Image(),
|
||||
Cmd: command,
|
||||
Files: w.fs,
|
||||
}
|
||||
log.WithContext(ctx).WithField("request", data).Trace("Sending request to AWS")
|
||||
rawData, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
exit <- ExitInfo{uint8(1), fmt.Errorf("cannot stingify aws function request: %w", err)}
|
||||
return
|
||||
}
|
||||
|
||||
wsConn, response, err := websocket.DefaultDialer.Dial(config.Config.AWS.Endpoint, nil)
|
||||
if err != nil {
|
||||
exit <- ExitInfo{uint8(1), fmt.Errorf("failed to establish aws connection: %w", err)}
|
||||
return
|
||||
}
|
||||
_ = response.Body.Close()
|
||||
defer wsConn.Close()
|
||||
err = wsConn.WriteMessage(websocket.TextMessage, rawData)
|
||||
if err != nil {
|
||||
exit <- ExitInfo{uint8(1), fmt.Errorf("cannot send aws request: %w", err)}
|
||||
return
|
||||
}
|
||||
|
||||
// receiveOutput listens for the execution timeout (or the exit code).
|
||||
exitCode, err := w.receiveOutput(wsConn, stdout, stderr, ctx)
|
||||
// TimeoutPassed checks the runner timeout
|
||||
if w.TimeoutPassed() {
|
||||
err = ErrorRunnerInactivityTimeout
|
||||
}
|
||||
exit <- ExitInfo{exitCode, err}
|
||||
}
|
||||
|
||||
func (w *AWSFunctionWorkload) receiveOutput(
|
||||
conn *websocket.Conn, stdout, stderr io.Writer, ctx context.Context) (uint8, error) {
|
||||
for ctx.Err() == nil {
|
||||
messageType, reader, err := conn.NextReader()
|
||||
if err != nil {
|
||||
return 1, fmt.Errorf("cannot read from aws connection: %w", err)
|
||||
}
|
||||
if messageType != websocket.TextMessage {
|
||||
return 1, ErrWrongMessageType
|
||||
}
|
||||
var wsMessage dto.WebSocketMessage
|
||||
err = json.NewDecoder(reader).Decode(&wsMessage)
|
||||
if err != nil {
|
||||
return 1, fmt.Errorf("failed to decode message from aws: %w", err)
|
||||
}
|
||||
|
||||
log.WithField("msg", wsMessage).Info("New Message from AWS function")
|
||||
|
||||
switch wsMessage.Type {
|
||||
default:
|
||||
log.WithContext(ctx).WithField("data", wsMessage).Warn("unexpected message from aws function")
|
||||
case dto.WebSocketExit:
|
||||
return wsMessage.ExitCode, nil
|
||||
case dto.WebSocketOutputStdout:
|
||||
// We do not check the written bytes as the rawToCodeOceanWriter receives everything or nothing.
|
||||
_, err = stdout.Write([]byte(wsMessage.Data))
|
||||
case dto.WebSocketOutputStderr, dto.WebSocketOutputError:
|
||||
_, err = stderr.Write([]byte(wsMessage.Data))
|
||||
}
|
||||
if err != nil {
|
||||
return 1, fmt.Errorf("failed to forward message: %w", err)
|
||||
}
|
||||
}
|
||||
return 1, fmt.Errorf("receiveOutput stpped by context: %w", ctx.Err())
|
||||
}
|
||||
|
||||
// handleRunnerTimeout listens for a runner timeout and aborts the execution in that case.
|
||||
// It listens via a context in runningExecutions that is canceled on the timeout event.
|
||||
func (w *AWSFunctionWorkload) handleRunnerTimeout(ctx context.Context,
|
||||
exitInternal <-chan ExitInfo, exit chan<- ExitInfo, executionID string) {
|
||||
executionCtx, cancelExecution := context.WithCancel(ctx)
|
||||
w.runningExecutions[executionID] = cancelExecution
|
||||
defer delete(w.runningExecutions, executionID)
|
||||
defer close(exit)
|
||||
|
||||
select {
|
||||
case exitInfo := <-exitInternal:
|
||||
exit <- exitInfo
|
||||
case <-executionCtx.Done():
|
||||
exit <- ExitInfo{255, ErrorRunnerInactivityTimeout}
|
||||
}
|
||||
}
|
||||
|
||||
// hideEnvironmentVariables sets the CODEOCEAN variable and unsets all variables starting with the passed prefix.
|
||||
func hideEnvironmentVariables(request *dto.ExecutionRequest, unsetPrefix string) {
|
||||
if request.Environment == nil {
|
||||
request.Environment = make(map[string]string)
|
||||
}
|
||||
request.Command = "unset \"${!" + unsetPrefix + "@}\" && " + request.Command
|
||||
}
|
@ -1,165 +0,0 @@
|
||||
package runner
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/openHPI/poseidon/internal/config"
|
||||
"github.com/openHPI/poseidon/pkg/dto"
|
||||
"github.com/openHPI/poseidon/tests"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (s *MainTestSuite) TestAWSExecutionRequestIsStored() {
|
||||
environment := &ExecutionEnvironmentMock{}
|
||||
environment.On("ID").Return(dto.EnvironmentID(tests.DefaultEnvironmentIDAsInteger))
|
||||
r, err := NewAWSFunctionWorkload(environment, func(_ Runner) error { return nil })
|
||||
s.NoError(err)
|
||||
executionRequest := &dto.ExecutionRequest{
|
||||
Command: "command",
|
||||
TimeLimit: 10,
|
||||
Environment: nil,
|
||||
}
|
||||
r.StoreExecution(tests.DefaultEnvironmentIDAsString, executionRequest)
|
||||
s.True(r.ExecutionExists(tests.DefaultEnvironmentIDAsString))
|
||||
storedExecutionRunner, ok := r.executions.Pop(tests.DefaultEnvironmentIDAsString)
|
||||
s.True(ok, "Getting an execution should not return ok false")
|
||||
s.Equal(executionRequest, storedExecutionRunner)
|
||||
|
||||
err = r.Destroy(nil)
|
||||
s.NoError(err)
|
||||
}
|
||||
|
||||
type awsEndpointMock struct {
|
||||
hasConnected bool
|
||||
ctx context.Context
|
||||
receivedData string
|
||||
}
|
||||
|
||||
func (a *awsEndpointMock) handler(w http.ResponseWriter, r *http.Request) {
|
||||
upgrader := websocket.Upgrader{}
|
||||
c, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer c.Close()
|
||||
|
||||
a.hasConnected = true
|
||||
for a.ctx.Err() == nil {
|
||||
_, message, err := c.ReadMessage()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
a.receivedData = string(message)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *MainTestSuite) TestAWSFunctionWorkload_ExecuteInteractively() {
|
||||
environment := &ExecutionEnvironmentMock{}
|
||||
environment.On("ID").Return(dto.EnvironmentID(tests.DefaultEnvironmentIDAsInteger))
|
||||
environment.On("Image").Return("testImage or AWS endpoint")
|
||||
r, err := NewAWSFunctionWorkload(environment, func(_ Runner) error { return nil })
|
||||
s.Require().NoError(err)
|
||||
|
||||
var cancel context.CancelFunc
|
||||
awsMock := &awsEndpointMock{}
|
||||
sv := httptest.NewServer(http.HandlerFunc(awsMock.handler))
|
||||
defer sv.Close()
|
||||
|
||||
s.Run("establishes WebSocket connection to AWS endpoint", func() {
|
||||
// Convert http://127.0.0.1 to ws://127.0.0.1
|
||||
config.Config.AWS.Endpoint = "ws" + strings.TrimPrefix(sv.URL, "http")
|
||||
awsMock.ctx, cancel = context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
r.StoreExecution(tests.DefaultEnvironmentIDAsString, &dto.ExecutionRequest{})
|
||||
exit, _, err := r.ExecuteInteractively(
|
||||
tests.DefaultEnvironmentIDAsString, nil, io.Discard, io.Discard, s.TestCtx)
|
||||
s.Require().NoError(err)
|
||||
<-exit
|
||||
s.True(awsMock.hasConnected)
|
||||
})
|
||||
|
||||
s.Run("sends execution request", func() {
|
||||
s.T().Skip("The AWS runner ignores its context for executions and waits infinetly for the exit message.") // ToDo
|
||||
awsMock.ctx, cancel = context.WithTimeout(context.Background(), tests.ShortTimeout)
|
||||
defer cancel()
|
||||
command := "sl"
|
||||
request := &dto.ExecutionRequest{Command: command}
|
||||
r.StoreExecution(tests.DefaultEnvironmentIDAsString, request)
|
||||
|
||||
_, cancel, err := r.ExecuteInteractively(
|
||||
tests.DefaultEnvironmentIDAsString, nil, io.Discard, io.Discard, s.TestCtx)
|
||||
s.Require().NoError(err)
|
||||
<-time.After(tests.ShortTimeout)
|
||||
cancel()
|
||||
|
||||
expectedRequestData := `{"action":"` + environment.Image() +
|
||||
`","cmd":["/bin/bash","-c","env CODEOCEAN=true /bin/bash -c \"unset \\\"\\${!AWS@}\\\" \u0026\u0026 ` + command +
|
||||
`\""],"files":{}}`
|
||||
s.Equal(expectedRequestData, awsMock.receivedData)
|
||||
})
|
||||
|
||||
err = r.Destroy(nil)
|
||||
s.NoError(err)
|
||||
}
|
||||
|
||||
func (s *MainTestSuite) TestAWSFunctionWorkload_UpdateFileSystem() {
|
||||
s.T().Skip("The AWS runner ignores its context for executions and waits infinetly for the exit message.") // ToDo
|
||||
|
||||
environment := &ExecutionEnvironmentMock{}
|
||||
environment.On("ID").Return(dto.EnvironmentID(tests.DefaultEnvironmentIDAsInteger))
|
||||
environment.On("Image").Return("testImage or AWS endpoint")
|
||||
r, err := NewAWSFunctionWorkload(environment, nil)
|
||||
s.Require().NoError(err)
|
||||
|
||||
var cancel context.CancelFunc
|
||||
awsMock := &awsEndpointMock{}
|
||||
sv := httptest.NewServer(http.HandlerFunc(awsMock.handler))
|
||||
defer sv.Close()
|
||||
|
||||
// Convert http://127.0.0.1 to ws://127.0.0.1
|
||||
config.Config.AWS.Endpoint = "ws" + strings.TrimPrefix(sv.URL, "http")
|
||||
awsMock.ctx, cancel = context.WithTimeout(context.Background(), tests.ShortTimeout)
|
||||
defer cancel()
|
||||
command := "sl"
|
||||
request := &dto.ExecutionRequest{Command: command}
|
||||
r.StoreExecution(tests.DefaultEnvironmentIDAsString, request)
|
||||
myFile := dto.File{Path: "myPath", Content: []byte("myContent")}
|
||||
|
||||
err = r.UpdateFileSystem(&dto.UpdateFileSystemRequest{Copy: []dto.File{myFile}}, s.TestCtx)
|
||||
s.NoError(err)
|
||||
_, execCancel, err := r.ExecuteInteractively(
|
||||
tests.DefaultEnvironmentIDAsString, nil, io.Discard, io.Discard, s.TestCtx)
|
||||
s.Require().NoError(err)
|
||||
<-time.After(tests.ShortTimeout)
|
||||
execCancel()
|
||||
|
||||
expectedRequestData := `{"action":"` + environment.Image() +
|
||||
`","cmd":["/bin/bash","-c","env CODEOCEAN=true /bin/bash -c \"unset \\\"\\${!AWS@}\\\" \u0026\u0026 ` + command +
|
||||
`\""],"files":{"` + string(myFile.Path) + `":"` + base64.StdEncoding.EncodeToString(myFile.Content) + `"}}`
|
||||
s.Equal(expectedRequestData, awsMock.receivedData)
|
||||
|
||||
err = r.Destroy(nil)
|
||||
s.NoError(err)
|
||||
}
|
||||
|
||||
func (s *MainTestSuite) TestAWSFunctionWorkload_Destroy() {
|
||||
environment := &ExecutionEnvironmentMock{}
|
||||
environment.On("ID").Return(dto.EnvironmentID(tests.DefaultEnvironmentIDAsInteger))
|
||||
hasDestroyBeenCalled := false
|
||||
r, err := NewAWSFunctionWorkload(environment, func(_ Runner) error {
|
||||
hasDestroyBeenCalled = true
|
||||
return nil
|
||||
})
|
||||
s.Require().NoError(err)
|
||||
|
||||
var reason error
|
||||
err = r.Destroy(reason)
|
||||
s.NoError(err)
|
||||
s.True(hasDestroyBeenCalled)
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
package runner
|
||||
|
||||
import (
|
||||
"github.com/openHPI/poseidon/pkg/dto"
|
||||
"github.com/openHPI/poseidon/tests"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultEnvironmentID = dto.EnvironmentID(tests.DefaultEnvironmentIDAsInteger)
|
||||
anotherEnvironmentID = dto.EnvironmentID(tests.AnotherEnvironmentIDAsInteger)
|
||||
defaultInactivityTimeout = 0
|
||||
)
|
@ -1,349 +0,0 @@
|
||||
// Code generated by mockery v2.43.2. DO NOT EDIT.
|
||||
|
||||
package runner
|
||||
|
||||
import (
|
||||
dto "github.com/openHPI/poseidon/pkg/dto"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// ExecutionEnvironmentMock is an autogenerated mock type for the ExecutionEnvironment type
|
||||
type ExecutionEnvironmentMock struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// AddRunner provides a mock function with given fields: r
|
||||
func (_m *ExecutionEnvironmentMock) AddRunner(r Runner) {
|
||||
_m.Called(r)
|
||||
}
|
||||
|
||||
// ApplyPrewarmingPoolSize provides a mock function with given fields:
|
||||
func (_m *ExecutionEnvironmentMock) ApplyPrewarmingPoolSize() error {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for ApplyPrewarmingPoolSize")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func() error); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// CPULimit provides a mock function with given fields:
|
||||
func (_m *ExecutionEnvironmentMock) CPULimit() uint {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for CPULimit")
|
||||
}
|
||||
|
||||
var r0 uint
|
||||
if rf, ok := ret.Get(0).(func() uint); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Get(0).(uint)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Delete provides a mock function with given fields: reason
|
||||
func (_m *ExecutionEnvironmentMock) Delete(reason DestroyReason) error {
|
||||
ret := _m.Called(reason)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Delete")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(DestroyReason) error); ok {
|
||||
r0 = rf(reason)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// DeleteRunner provides a mock function with given fields: id
|
||||
func (_m *ExecutionEnvironmentMock) DeleteRunner(id string) (Runner, bool) {
|
||||
ret := _m.Called(id)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for DeleteRunner")
|
||||
}
|
||||
|
||||
var r0 Runner
|
||||
var r1 bool
|
||||
if rf, ok := ret.Get(0).(func(string) (Runner, bool)); ok {
|
||||
return rf(id)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(string) Runner); ok {
|
||||
r0 = rf(id)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(Runner)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(string) bool); ok {
|
||||
r1 = rf(id)
|
||||
} else {
|
||||
r1 = ret.Get(1).(bool)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// ID provides a mock function with given fields:
|
||||
func (_m *ExecutionEnvironmentMock) ID() dto.EnvironmentID {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for ID")
|
||||
}
|
||||
|
||||
var r0 dto.EnvironmentID
|
||||
if rf, ok := ret.Get(0).(func() dto.EnvironmentID); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Get(0).(dto.EnvironmentID)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// IdleRunnerCount provides a mock function with given fields:
|
||||
func (_m *ExecutionEnvironmentMock) IdleRunnerCount() uint {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for IdleRunnerCount")
|
||||
}
|
||||
|
||||
var r0 uint
|
||||
if rf, ok := ret.Get(0).(func() uint); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Get(0).(uint)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Image provides a mock function with given fields:
|
||||
func (_m *ExecutionEnvironmentMock) Image() string {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Image")
|
||||
}
|
||||
|
||||
var r0 string
|
||||
if rf, ok := ret.Get(0).(func() string); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// MarshalJSON provides a mock function with given fields:
|
||||
func (_m *ExecutionEnvironmentMock) MarshalJSON() ([]byte, error) {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for MarshalJSON")
|
||||
}
|
||||
|
||||
var r0 []byte
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func() ([]byte, error)); ok {
|
||||
return rf()
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func() []byte); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]byte)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func() error); ok {
|
||||
r1 = rf()
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MemoryLimit provides a mock function with given fields:
|
||||
func (_m *ExecutionEnvironmentMock) MemoryLimit() uint {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for MemoryLimit")
|
||||
}
|
||||
|
||||
var r0 uint
|
||||
if rf, ok := ret.Get(0).(func() uint); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Get(0).(uint)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// NetworkAccess provides a mock function with given fields:
|
||||
func (_m *ExecutionEnvironmentMock) NetworkAccess() (bool, []uint16) {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for NetworkAccess")
|
||||
}
|
||||
|
||||
var r0 bool
|
||||
var r1 []uint16
|
||||
if rf, ok := ret.Get(0).(func() (bool, []uint16)); ok {
|
||||
return rf()
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func() bool); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Get(0).(bool)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func() []uint16); ok {
|
||||
r1 = rf()
|
||||
} else {
|
||||
if ret.Get(1) != nil {
|
||||
r1 = ret.Get(1).([]uint16)
|
||||
}
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// PrewarmingPoolSize provides a mock function with given fields:
|
||||
func (_m *ExecutionEnvironmentMock) PrewarmingPoolSize() uint {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for PrewarmingPoolSize")
|
||||
}
|
||||
|
||||
var r0 uint
|
||||
if rf, ok := ret.Get(0).(func() uint); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Get(0).(uint)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Register provides a mock function with given fields:
|
||||
func (_m *ExecutionEnvironmentMock) Register() error {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Register")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func() error); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Sample provides a mock function with given fields:
|
||||
func (_m *ExecutionEnvironmentMock) Sample() (Runner, bool) {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Sample")
|
||||
}
|
||||
|
||||
var r0 Runner
|
||||
var r1 bool
|
||||
if rf, ok := ret.Get(0).(func() (Runner, bool)); ok {
|
||||
return rf()
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func() Runner); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(Runner)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func() bool); ok {
|
||||
r1 = rf()
|
||||
} else {
|
||||
r1 = ret.Get(1).(bool)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// SetCPULimit provides a mock function with given fields: limit
|
||||
func (_m *ExecutionEnvironmentMock) SetCPULimit(limit uint) {
|
||||
_m.Called(limit)
|
||||
}
|
||||
|
||||
// SetConfigFrom provides a mock function with given fields: environment
|
||||
func (_m *ExecutionEnvironmentMock) SetConfigFrom(environment ExecutionEnvironment) {
|
||||
_m.Called(environment)
|
||||
}
|
||||
|
||||
// SetID provides a mock function with given fields: id
|
||||
func (_m *ExecutionEnvironmentMock) SetID(id dto.EnvironmentID) {
|
||||
_m.Called(id)
|
||||
}
|
||||
|
||||
// SetImage provides a mock function with given fields: image
|
||||
func (_m *ExecutionEnvironmentMock) SetImage(image string) {
|
||||
_m.Called(image)
|
||||
}
|
||||
|
||||
// SetMemoryLimit provides a mock function with given fields: limit
|
||||
func (_m *ExecutionEnvironmentMock) SetMemoryLimit(limit uint) {
|
||||
_m.Called(limit)
|
||||
}
|
||||
|
||||
// SetNetworkAccess provides a mock function with given fields: allow, ports
|
||||
func (_m *ExecutionEnvironmentMock) SetNetworkAccess(allow bool, ports []uint16) {
|
||||
_m.Called(allow, ports)
|
||||
}
|
||||
|
||||
// SetPrewarmingPoolSize provides a mock function with given fields: count
|
||||
func (_m *ExecutionEnvironmentMock) SetPrewarmingPoolSize(count uint) {
|
||||
_m.Called(count)
|
||||
}
|
||||
|
||||
// NewExecutionEnvironmentMock creates a new instance of ExecutionEnvironmentMock. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewExecutionEnvironmentMock(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *ExecutionEnvironmentMock {
|
||||
mock := &ExecutionEnvironmentMock{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
// Code generated by mockery v0.0.0-dev. DO NOT EDIT.
|
||||
|
||||
package runner
|
||||
|
||||
import (
|
||||
time "time"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// InactivityTimerMock is an autogenerated mock type for the InactivityTimer type
|
||||
type InactivityTimerMock struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// ResetTimeout provides a mock function with given fields:
|
||||
func (_m *InactivityTimerMock) ResetTimeout() {
|
||||
_m.Called()
|
||||
}
|
||||
|
||||
// SetupTimeout provides a mock function with given fields: duration
|
||||
func (_m *InactivityTimerMock) SetupTimeout(duration time.Duration) {
|
||||
_m.Called(duration)
|
||||
}
|
||||
|
||||
// StopTimeout provides a mock function with given fields:
|
||||
func (_m *InactivityTimerMock) StopTimeout() {
|
||||
_m.Called()
|
||||
}
|
||||
|
||||
// TimeoutPassed provides a mock function with given fields:
|
||||
func (_m *InactivityTimerMock) TimeoutPassed() bool {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 bool
|
||||
if rf, ok := ret.Get(0).(func() bool); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Get(0).(bool)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
@ -1,95 +0,0 @@
|
||||
package runner
|
||||
|
||||
import (
|
||||
"github.com/openHPI/poseidon/internal/nomad"
|
||||
"github.com/openHPI/poseidon/tests"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestInactivityTimerTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(InactivityTimerTestSuite))
|
||||
}
|
||||
|
||||
type InactivityTimerTestSuite struct {
|
||||
tests.MemoryLeakTestSuite
|
||||
runner Runner
|
||||
returned chan bool
|
||||
}
|
||||
|
||||
func (s *InactivityTimerTestSuite) SetupTest() {
|
||||
s.MemoryLeakTestSuite.SetupTest()
|
||||
s.returned = make(chan bool, 1)
|
||||
apiMock := &nomad.ExecutorAPIMock{}
|
||||
apiMock.On("DeleteJob", tests.DefaultRunnerID).Return(nil)
|
||||
s.runner = NewNomadJob(tests.DefaultRunnerID, nil, apiMock, func(_ Runner) error {
|
||||
s.returned <- true
|
||||
return nil
|
||||
})
|
||||
|
||||
s.runner.SetupTimeout(tests.ShortTimeout)
|
||||
}
|
||||
|
||||
func (s *InactivityTimerTestSuite) TearDownTest() {
|
||||
defer s.MemoryLeakTestSuite.TearDownTest()
|
||||
go func() {
|
||||
select {
|
||||
case <-s.returned:
|
||||
case <-time.After(tests.ShortTimeout):
|
||||
}
|
||||
}()
|
||||
|
||||
err := s.runner.Destroy(nil)
|
||||
s.Require().NoError(err)
|
||||
}
|
||||
|
||||
func (s *InactivityTimerTestSuite) TestRunnerIsReturnedAfterTimeout() {
|
||||
s.True(tests.ChannelReceivesSomething(s.returned, 2*tests.ShortTimeout))
|
||||
}
|
||||
|
||||
func (s *InactivityTimerTestSuite) TestRunnerIsNotReturnedBeforeTimeout() {
|
||||
s.False(tests.ChannelReceivesSomething(s.returned, tests.ShortTimeout/2))
|
||||
}
|
||||
|
||||
func (s *InactivityTimerTestSuite) TestResetTimeoutExtendsTheDeadline() {
|
||||
time.Sleep(3 * tests.ShortTimeout / 4)
|
||||
s.runner.ResetTimeout()
|
||||
s.False(tests.ChannelReceivesSomething(s.returned, 3*tests.ShortTimeout/4),
|
||||
"Because of the reset, the timeout should not be reached by now.")
|
||||
s.True(tests.ChannelReceivesSomething(s.returned, 5*tests.ShortTimeout/4),
|
||||
"After reset, the timout should be reached by now.")
|
||||
}
|
||||
|
||||
func (s *InactivityTimerTestSuite) TestStopTimeoutStopsTimeout() {
|
||||
s.runner.StopTimeout()
|
||||
s.False(tests.ChannelReceivesSomething(s.returned, 2*tests.ShortTimeout))
|
||||
}
|
||||
|
||||
func (s *InactivityTimerTestSuite) TestTimeoutPassedReturnsFalseBeforeDeadline() {
|
||||
s.False(s.runner.TimeoutPassed())
|
||||
}
|
||||
|
||||
func (s *InactivityTimerTestSuite) TestTimeoutPassedReturnsTrueAfterDeadline() {
|
||||
<-time.After(2 * tests.ShortTimeout)
|
||||
s.True(s.runner.TimeoutPassed())
|
||||
}
|
||||
|
||||
func (s *InactivityTimerTestSuite) TestTimerIsNotResetAfterDeadline() {
|
||||
time.Sleep(2 * tests.ShortTimeout)
|
||||
// We need to empty the returned channel so Return can send to it again.
|
||||
tests.ChannelReceivesSomething(s.returned, 0)
|
||||
s.runner.ResetTimeout()
|
||||
s.False(tests.ChannelReceivesSomething(s.returned, 2*tests.ShortTimeout))
|
||||
}
|
||||
|
||||
func (s *InactivityTimerTestSuite) TestSetupTimeoutStopsOldTimeout() {
|
||||
s.runner.SetupTimeout(3 * tests.ShortTimeout)
|
||||
s.False(tests.ChannelReceivesSomething(s.returned, 2*tests.ShortTimeout))
|
||||
s.True(tests.ChannelReceivesSomething(s.returned, 2*tests.ShortTimeout))
|
||||
}
|
||||
|
||||
func (s *InactivityTimerTestSuite) TestTimerIsInactiveWhenDurationIsZero() {
|
||||
s.runner.SetupTimeout(0)
|
||||
s.False(tests.ChannelReceivesSomething(s.returned, tests.ShortTimeout))
|
||||
}
|
125
internal/runner/kubernetes_manager.go
Normal file
125
internal/runner/kubernetes_manager.go
Normal file
@ -0,0 +1,125 @@
|
||||
package runner
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/openHPI/poseidon/internal/kubernetes"
|
||||
"github.com/openHPI/poseidon/internal/nomad"
|
||||
"github.com/openHPI/poseidon/pkg/dto"
|
||||
"github.com/openHPI/poseidon/pkg/storage"
|
||||
"github.com/openHPI/poseidon/pkg/util"
|
||||
appv1 "k8s.io/api/apps/v1"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type KubernetesRunnerManager struct {
|
||||
*AbstractManager
|
||||
apiClient kubernetes.ExecutorAPI
|
||||
reloadingEnvironment storage.Storage[*alertData]
|
||||
}
|
||||
|
||||
func NewKubernetesRunnerManager(apiClient *kubernetes.ExecutorAPI, ctx context.Context) *KubernetesRunnerManager {
|
||||
return &KubernetesRunnerManager{
|
||||
AbstractManager: NewAbstractManager(ctx),
|
||||
apiClient: *apiClient,
|
||||
reloadingEnvironment: storage.NewLocalStorage[*alertData](),
|
||||
}
|
||||
}
|
||||
|
||||
// Load recovers all runners for all existing environments.
|
||||
func (k *KubernetesRunnerManager) Load() {
|
||||
log.Info("Loading runners")
|
||||
newUsedRunners := storage.NewLocalStorage[Runner]()
|
||||
for _, environment := range k.ListEnvironments() {
|
||||
usedRunners, err := k.loadEnvironment(environment)
|
||||
if err != nil {
|
||||
log.WithError(err).WithField(dto.KeyEnvironmentID, environment.ID().ToString()).
|
||||
Warn("Failed loading environment. Skipping...")
|
||||
continue
|
||||
}
|
||||
for _, r := range usedRunners.List() {
|
||||
newUsedRunners.Add(r.ID(), r)
|
||||
}
|
||||
}
|
||||
// TODO MISSING IMPLEMENTATION
|
||||
//k.updateUsedRunners(newUsedRunners, true)
|
||||
}
|
||||
|
||||
func (k *KubernetesRunnerManager) loadEnvironment(environment ExecutionEnvironment) (used storage.Storage[Runner], err error) {
|
||||
used = storage.NewLocalStorage[Runner]()
|
||||
|
||||
runnerJobs, err := k.apiClient.LoadRunnerJobs(environment.ID())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed fetching the runner jobs: %w", err)
|
||||
}
|
||||
for _, job := range runnerJobs {
|
||||
r, isUsed, err := k.loadSingleJob(job, environment)
|
||||
if err != nil {
|
||||
log.WithError(err).WithField(dto.KeyEnvironmentID, environment.ID().ToString()).
|
||||
WithField("used", isUsed).Warn("Failed loading job. Skipping...")
|
||||
continue
|
||||
} else if isUsed {
|
||||
used.Add(r.ID(), r)
|
||||
}
|
||||
}
|
||||
err = environment.ApplyPrewarmingPoolSize()
|
||||
if err != nil {
|
||||
return used, fmt.Errorf("couldn't scale environment: %w", err)
|
||||
}
|
||||
return used, nil
|
||||
}
|
||||
|
||||
func (k *KubernetesRunnerManager) loadSingleJob(deployment *appv1.Deployment, environment ExecutionEnvironment) (r Runner, isUsed bool, err error) {
|
||||
configTaskGroup := deployment.Spec.Template
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("%w, %s", nomad.ErrorMissingTaskGroup, deployment.Name)
|
||||
}
|
||||
|
||||
isUsed = configTaskGroup.Annotations[nomad.ConfigMetaUsedKey] == nomad.ConfigMetaUsedValue
|
||||
portMappings, err := k.apiClient.LoadRunnerPortMappings(deployment.Name)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("error loading runner portMappings: %w", err)
|
||||
}
|
||||
|
||||
newJob := NewKubernetesDeployment(deployment.Name, portMappings, k.apiClient, k.onRunnerDestroyed)
|
||||
log.WithField("isUsed", isUsed).WithField(dto.KeyRunnerID, newJob.ID()).Debug("Recovered Runner")
|
||||
if isUsed {
|
||||
timeout, err := strconv.Atoi(configTaskGroup.ObjectMeta.Annotations[nomad.ConfigMetaTimeoutKey])
|
||||
if err != nil {
|
||||
log.WithField(dto.KeyRunnerID, newJob.ID()).WithError(err).Warn("failed loading timeout from meta values")
|
||||
timeout = int(nomad.RunnerTimeoutFallback.Seconds())
|
||||
go k.markRunnerAsUsed(newJob, timeout)
|
||||
}
|
||||
newJob.SetupTimeout(time.Duration(timeout) * time.Second)
|
||||
} else {
|
||||
environment.AddRunner(newJob)
|
||||
}
|
||||
return newJob, isUsed, nil
|
||||
}
|
||||
|
||||
func (k *KubernetesRunnerManager) markRunnerAsUsed(runner Runner, timeoutDuration int) {
|
||||
err := util.RetryExponential(func() (err error) {
|
||||
if err = k.apiClient.MarkRunnerAsUsed(runner.ID(), timeoutDuration); err != nil {
|
||||
err = fmt.Errorf("cannot mark runner as used: %w", err)
|
||||
}
|
||||
return
|
||||
})
|
||||
if err != nil {
|
||||
log.WithError(err).WithField(dto.KeyRunnerID, runner.ID()).Error("cannot mark runner as used")
|
||||
err := k.Return(runner)
|
||||
if err != nil {
|
||||
log.WithError(err).WithField(dto.KeyRunnerID, runner.ID()).Error("can't mark runner as used and can't return runner")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (k *KubernetesRunnerManager) onRunnerDestroyed(r Runner) error {
|
||||
k.usedRunners.Delete(r.ID())
|
||||
|
||||
environment, ok := k.GetEnvironment(r.Environment())
|
||||
if ok {
|
||||
environment.DeleteRunner(r.ID())
|
||||
}
|
||||
return nil
|
||||
}
|
107
internal/runner/kubernetes_runner.go
Normal file
107
internal/runner/kubernetes_runner.go
Normal file
@ -0,0 +1,107 @@
|
||||
package runner
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/openHPI/poseidon/internal/kubernetes"
|
||||
"github.com/openHPI/poseidon/internal/nomad"
|
||||
"github.com/openHPI/poseidon/pkg/dto"
|
||||
"github.com/openHPI/poseidon/pkg/monitoring"
|
||||
"github.com/openHPI/poseidon/pkg/storage"
|
||||
"io"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// NomadJob is an abstraction to communicate with Nomad environments.
|
||||
type KubernetesDeployment struct {
|
||||
InactivityTimer
|
||||
executions storage.Storage[*dto.ExecutionRequest]
|
||||
id string
|
||||
portMappings []v1.ContainerPort
|
||||
api kubernetes.ExecutorAPI
|
||||
onDestroy DestroyRunnerHandler
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
func (r *KubernetesDeployment) MappedPorts() []*dto.MappedPort {
|
||||
//TODO implement me
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (r *KubernetesDeployment) StoreExecution(id string, executionRequest *dto.ExecutionRequest) {
|
||||
//TODO implement me
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (r *KubernetesDeployment) ExecutionExists(id string) bool {
|
||||
//TODO implement me
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (r *KubernetesDeployment) ExecuteInteractively(id string, stdin io.ReadWriter, stdout, stderr io.Writer, ctx context.Context) (exit <-chan ExitInfo, cancel context.CancelFunc, err error) {
|
||||
//TODO implement me
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (r *KubernetesDeployment) ListFileSystem(path string, recursive bool, result io.Writer, privilegedExecution bool, ctx context.Context) error {
|
||||
//TODO implement me
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (r *KubernetesDeployment) UpdateFileSystem(request *dto.UpdateFileSystemRequest, ctx context.Context) error {
|
||||
//TODO implement me
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (r *KubernetesDeployment) GetFileContent(path string, content http.ResponseWriter, privilegedExecution bool, ctx context.Context) error {
|
||||
//TODO implement me
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (r *KubernetesDeployment) Destroy(reason DestroyReason) error {
|
||||
//TODO implement me
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (r *KubernetesDeployment) ID() string {
|
||||
return r.id
|
||||
}
|
||||
|
||||
func (r *KubernetesDeployment) Environment() dto.EnvironmentID {
|
||||
id, err := nomad.EnvironmentIDFromRunnerID(r.ID())
|
||||
if err != nil {
|
||||
log.WithError(err).Error("Runners must have correct IDs")
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
// NewNomadJob creates a new NomadJob with the provided id.
|
||||
// The InactivityTimer is used actively. It executes onDestroy when it has expired.
|
||||
// The InactivityTimer is persisted in Nomad by the runner manager's Claim Function.
|
||||
func NewKubernetesDeployment(id string, portMappings []v1.ContainerPort,
|
||||
apiClient kubernetes.ExecutorAPI, onDestroy DestroyRunnerHandler,
|
||||
) *KubernetesDeployment {
|
||||
ctx := context.WithValue(context.Background(), dto.ContextKey(dto.KeyRunnerID), id)
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
job := &KubernetesDeployment{
|
||||
id: id,
|
||||
portMappings: portMappings,
|
||||
api: apiClient,
|
||||
onDestroy: onDestroy,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
}
|
||||
job.executions = storage.NewMonitoredLocalStorage[*dto.ExecutionRequest](
|
||||
monitoring.MeasurementExecutionsNomad, monitorExecutionsRunnerID(job.Environment(), id), time.Minute, ctx)
|
||||
job.InactivityTimer = NewInactivityTimer(job, func(r Runner) error {
|
||||
err := r.Destroy(ErrorRunnerInactivityTimeout)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("NomadJob: %w", err)
|
||||
}
|
||||
return err
|
||||
})
|
||||
return job
|
||||
}
|
@ -1,178 +0,0 @@
|
||||
// Code generated by mockery v2.10.0. DO NOT EDIT.
|
||||
|
||||
package runner
|
||||
|
||||
import (
|
||||
dto "github.com/openHPI/poseidon/pkg/dto"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// ManagerMock is an autogenerated mock type for the Manager type
|
||||
type ManagerMock struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// Claim provides a mock function with given fields: id, duration
|
||||
func (_m *ManagerMock) Claim(id dto.EnvironmentID, duration int) (Runner, error) {
|
||||
ret := _m.Called(id, duration)
|
||||
|
||||
var r0 Runner
|
||||
if rf, ok := ret.Get(0).(func(dto.EnvironmentID, int) Runner); ok {
|
||||
r0 = rf(id, duration)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(Runner)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(dto.EnvironmentID, int) error); ok {
|
||||
r1 = rf(id, duration)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// DeleteEnvironment provides a mock function with given fields: id
|
||||
func (_m *ManagerMock) DeleteEnvironment(id dto.EnvironmentID) {
|
||||
_m.Called(id)
|
||||
}
|
||||
|
||||
// EnvironmentStatistics provides a mock function with given fields:
|
||||
func (_m *ManagerMock) EnvironmentStatistics() map[dto.EnvironmentID]*dto.StatisticalExecutionEnvironmentData {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 map[dto.EnvironmentID]*dto.StatisticalExecutionEnvironmentData
|
||||
if rf, ok := ret.Get(0).(func() map[dto.EnvironmentID]*dto.StatisticalExecutionEnvironmentData); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(map[dto.EnvironmentID]*dto.StatisticalExecutionEnvironmentData)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Get provides a mock function with given fields: runnerID
|
||||
func (_m *ManagerMock) Get(runnerID string) (Runner, error) {
|
||||
ret := _m.Called(runnerID)
|
||||
|
||||
var r0 Runner
|
||||
if rf, ok := ret.Get(0).(func(string) Runner); ok {
|
||||
r0 = rf(runnerID)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(Runner)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(string) error); ok {
|
||||
r1 = rf(runnerID)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetEnvironment provides a mock function with given fields: id
|
||||
func (_m *ManagerMock) GetEnvironment(id dto.EnvironmentID) (ExecutionEnvironment, bool) {
|
||||
ret := _m.Called(id)
|
||||
|
||||
var r0 ExecutionEnvironment
|
||||
if rf, ok := ret.Get(0).(func(dto.EnvironmentID) ExecutionEnvironment); ok {
|
||||
r0 = rf(id)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(ExecutionEnvironment)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 bool
|
||||
if rf, ok := ret.Get(1).(func(dto.EnvironmentID) bool); ok {
|
||||
r1 = rf(id)
|
||||
} else {
|
||||
r1 = ret.Get(1).(bool)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// HasNextHandler provides a mock function with given fields:
|
||||
func (_m *ManagerMock) HasNextHandler() bool {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 bool
|
||||
if rf, ok := ret.Get(0).(func() bool); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Get(0).(bool)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// ListEnvironments provides a mock function with given fields:
|
||||
func (_m *ManagerMock) ListEnvironments() []ExecutionEnvironment {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 []ExecutionEnvironment
|
||||
if rf, ok := ret.Get(0).(func() []ExecutionEnvironment); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]ExecutionEnvironment)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Load provides a mock function with given fields:
|
||||
func (_m *ManagerMock) Load() {
|
||||
_m.Called()
|
||||
}
|
||||
|
||||
// NextHandler provides a mock function with given fields:
|
||||
func (_m *ManagerMock) NextHandler() AccessorHandler {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 AccessorHandler
|
||||
if rf, ok := ret.Get(0).(func() AccessorHandler); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(AccessorHandler)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Return provides a mock function with given fields: r
|
||||
func (_m *ManagerMock) Return(r Runner) error {
|
||||
ret := _m.Called(r)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(Runner) error); ok {
|
||||
r0 = rf(r)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// SetNextHandler provides a mock function with given fields: m
|
||||
func (_m *ManagerMock) SetNextHandler(m AccessorHandler) {
|
||||
_m.Called(m)
|
||||
}
|
||||
|
||||
// StoreEnvironment provides a mock function with given fields: environment
|
||||
func (_m *ManagerMock) StoreEnvironment(environment ExecutionEnvironment) {
|
||||
_m.Called(environment)
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user