Implement routes to list, get and delete execution environments
* #9 Implement routes to list, get and delete execution environments. A refactoring was required to introduce the ExecutionEnvironment interface. * Fix MR comments, linting issues and bug that lead to e2e test failure * Add e2e tests * Add unit tests
This commit is contained in:
352
internal/environment/environment.go
Normal file
352
internal/environment/environment.go
Normal file
@@ -0,0 +1,352 @@
|
||||
package environment
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/google/uuid"
|
||||
nomadApi "github.com/hashicorp/nomad/api"
|
||||
"github.com/hashicorp/nomad/jobspec2"
|
||||
"github.com/openHPI/poseidon/internal/nomad"
|
||||
"github.com/openHPI/poseidon/internal/runner"
|
||||
"github.com/openHPI/poseidon/pkg/dto"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
const (
|
||||
portNumberBase = 10
|
||||
)
|
||||
|
||||
var (
|
||||
ErrorUpdatingExecutionEnvironment = errors.New("errors occurred when updating environment")
|
||||
)
|
||||
|
||||
type NomadEnvironment struct {
|
||||
jobHCL string
|
||||
job *nomadApi.Job
|
||||
idleRunners runner.Storage
|
||||
}
|
||||
|
||||
func NewNomadEnvironment(jobHCL string) (*NomadEnvironment, error) {
|
||||
job, err := parseJob(jobHCL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing Nomad job: %w", err)
|
||||
}
|
||||
|
||||
return &NomadEnvironment{jobHCL, job, runner.NewLocalRunnerStorage()}, nil
|
||||
}
|
||||
|
||||
func (n *NomadEnvironment) ID() dto.EnvironmentID {
|
||||
id, err := nomad.EnvironmentIDFromTemplateJobID(*n.job.ID)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("Environment ID can not be parsed from Job")
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
func (n *NomadEnvironment) SetID(id dto.EnvironmentID) {
|
||||
name := nomad.TemplateJobID(id)
|
||||
n.job.ID = &name
|
||||
n.job.Name = &name
|
||||
}
|
||||
|
||||
func (n *NomadEnvironment) PrewarmingPoolSize() uint {
|
||||
configTaskGroup := nomad.FindOrCreateConfigTaskGroup(n.job)
|
||||
count, err := strconv.Atoi(configTaskGroup.Meta[nomad.ConfigMetaPoolSizeKey])
|
||||
if err != nil {
|
||||
log.WithError(err).Error("Prewarming pool size can not be parsed from Job")
|
||||
}
|
||||
return uint(count)
|
||||
}
|
||||
|
||||
func (n *NomadEnvironment) SetPrewarmingPoolSize(count uint) {
|
||||
taskGroup := nomad.FindOrCreateConfigTaskGroup(n.job)
|
||||
|
||||
if taskGroup.Meta == nil {
|
||||
taskGroup.Meta = make(map[string]string)
|
||||
}
|
||||
taskGroup.Meta[nomad.ConfigMetaPoolSizeKey] = strconv.Itoa(int(count))
|
||||
}
|
||||
|
||||
func (n *NomadEnvironment) CPULimit() uint {
|
||||
defaultTaskGroup := nomad.FindOrCreateDefaultTaskGroup(n.job)
|
||||
defaultTask := nomad.FindOrCreateDefaultTask(defaultTaskGroup)
|
||||
return uint(*defaultTask.Resources.CPU)
|
||||
}
|
||||
|
||||
func (n *NomadEnvironment) SetCPULimit(limit uint) {
|
||||
defaultTaskGroup := nomad.FindOrCreateDefaultTaskGroup(n.job)
|
||||
defaultTask := nomad.FindOrCreateDefaultTask(defaultTaskGroup)
|
||||
|
||||
integerCPULimit := int(limit)
|
||||
defaultTask.Resources.CPU = &integerCPULimit
|
||||
}
|
||||
|
||||
func (n *NomadEnvironment) MemoryLimit() uint {
|
||||
defaultTaskGroup := nomad.FindOrCreateDefaultTaskGroup(n.job)
|
||||
defaultTask := nomad.FindOrCreateDefaultTask(defaultTaskGroup)
|
||||
return uint(*defaultTask.Resources.MemoryMB)
|
||||
}
|
||||
|
||||
func (n *NomadEnvironment) SetMemoryLimit(limit uint) {
|
||||
defaultTaskGroup := nomad.FindOrCreateDefaultTaskGroup(n.job)
|
||||
defaultTask := nomad.FindOrCreateDefaultTask(defaultTaskGroup)
|
||||
|
||||
integerMemoryLimit := int(limit)
|
||||
defaultTask.Resources.MemoryMB = &integerMemoryLimit
|
||||
}
|
||||
|
||||
func (n *NomadEnvironment) Image() string {
|
||||
defaultTaskGroup := nomad.FindOrCreateDefaultTaskGroup(n.job)
|
||||
defaultTask := nomad.FindOrCreateDefaultTask(defaultTaskGroup)
|
||||
image, ok := defaultTask.Config["image"].(string)
|
||||
if !ok {
|
||||
image = ""
|
||||
}
|
||||
return image
|
||||
}
|
||||
|
||||
func (n *NomadEnvironment) SetImage(image string) {
|
||||
defaultTaskGroup := nomad.FindOrCreateDefaultTaskGroup(n.job)
|
||||
defaultTask := nomad.FindOrCreateDefaultTask(defaultTaskGroup)
|
||||
defaultTask.Config["image"] = image
|
||||
}
|
||||
|
||||
func (n *NomadEnvironment) NetworkAccess() (allowed bool, ports []uint16) {
|
||||
defaultTaskGroup := nomad.FindOrCreateDefaultTaskGroup(n.job)
|
||||
defaultTask := nomad.FindOrCreateDefaultTask(defaultTaskGroup)
|
||||
|
||||
allowed = defaultTask.Config["network_mode"] != "none"
|
||||
if len(defaultTaskGroup.Networks) > 0 {
|
||||
networkResource := defaultTaskGroup.Networks[0]
|
||||
for _, port := range networkResource.DynamicPorts {
|
||||
ports = append(ports, uint16(port.To))
|
||||
}
|
||||
}
|
||||
return allowed, ports
|
||||
}
|
||||
|
||||
func (n *NomadEnvironment) SetNetworkAccess(allow bool, exposedPorts []uint16) {
|
||||
defaultTaskGroup := nomad.FindOrCreateDefaultTaskGroup(n.job)
|
||||
defaultTask := nomad.FindOrCreateDefaultTask(defaultTaskGroup)
|
||||
|
||||
if len(defaultTaskGroup.Tasks) == 0 {
|
||||
// This function is only used internally and must be called as last step when configuring the task.
|
||||
// This error is not recoverable.
|
||||
log.Fatal("Can't configure network before task has been configured!")
|
||||
}
|
||||
|
||||
if allow {
|
||||
var networkResource *nomadApi.NetworkResource
|
||||
if len(defaultTaskGroup.Networks) == 0 {
|
||||
networkResource = &nomadApi.NetworkResource{}
|
||||
defaultTaskGroup.Networks = []*nomadApi.NetworkResource{networkResource}
|
||||
} else {
|
||||
networkResource = defaultTaskGroup.Networks[0]
|
||||
}
|
||||
// Prefer "bridge" network over "host" to have an isolated network namespace with bridged interface
|
||||
// instead of joining the host network namespace.
|
||||
networkResource.Mode = "bridge"
|
||||
for _, portNumber := range exposedPorts {
|
||||
port := nomadApi.Port{
|
||||
Label: strconv.FormatUint(uint64(portNumber), portNumberBase),
|
||||
To: int(portNumber),
|
||||
}
|
||||
networkResource.DynamicPorts = append(networkResource.DynamicPorts, port)
|
||||
}
|
||||
|
||||
// Explicitly set mode to override existing settings when updating job from without to with network.
|
||||
// Don't use bridge as it collides with the bridge mode above. This results in Docker using 'bridge'
|
||||
// mode, meaning all allocations will be attached to the `docker0` adapter and could reach other
|
||||
// non-Nomad containers attached to it. This is avoided when using Nomads bridge network mode.
|
||||
defaultTask.Config["network_mode"] = ""
|
||||
} else {
|
||||
// Somehow, we can't set the network mode to none in the NetworkResource on task group level.
|
||||
// See https://github.com/hashicorp/nomad/issues/10540
|
||||
defaultTask.Config["network_mode"] = "none"
|
||||
// Explicitly set Networks to signal Nomad to remove the possibly existing networkResource
|
||||
defaultTaskGroup.Networks = []*nomadApi.NetworkResource{}
|
||||
}
|
||||
}
|
||||
|
||||
// Register creates a Nomad job based on the default job configuration and the given parameters.
|
||||
// It registers the job with Nomad and waits until the registration completes.
|
||||
func (n *NomadEnvironment) Register(apiClient nomad.ExecutorAPI) error {
|
||||
evalID, err := apiClient.RegisterNomadJob(n.job)
|
||||
if err != nil {
|
||||
return fmt.Errorf("couldn't register job: %w", err)
|
||||
}
|
||||
err = apiClient.MonitorEvaluation(evalID, context.Background())
|
||||
if err != nil {
|
||||
return fmt.Errorf("error during the monitoring of the environment job: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *NomadEnvironment) Delete(apiClient nomad.ExecutorAPI) error {
|
||||
err := n.removeRunners(apiClient, uint(n.idleRunners.Length()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = apiClient.DeleteJob(*n.job.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("couldn't delete environment job: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *NomadEnvironment) Scale(apiClient nomad.ExecutorAPI) error {
|
||||
required := int(n.PrewarmingPoolSize()) - n.idleRunners.Length()
|
||||
|
||||
if required > 0 {
|
||||
return n.createRunners(apiClient, uint(required))
|
||||
} else {
|
||||
return n.removeRunners(apiClient, uint(-required))
|
||||
}
|
||||
}
|
||||
|
||||
func (n *NomadEnvironment) UpdateRunnerSpecs(apiClient nomad.ExecutorAPI) error {
|
||||
runners, err := apiClient.LoadRunnerIDs(n.ID().ToString())
|
||||
if err != nil {
|
||||
return fmt.Errorf("update environment couldn't load runners: %w", err)
|
||||
}
|
||||
|
||||
var occurredError error
|
||||
for _, id := range runners {
|
||||
// avoid taking the address of the loop variable
|
||||
runnerID := id
|
||||
updatedRunnerJob := n.DeepCopyJob()
|
||||
updatedRunnerJob.ID = &runnerID
|
||||
updatedRunnerJob.Name = &runnerID
|
||||
|
||||
err := apiClient.RegisterRunnerJob(updatedRunnerJob)
|
||||
if err != nil {
|
||||
if occurredError == nil {
|
||||
occurredError = ErrorUpdatingExecutionEnvironment
|
||||
}
|
||||
occurredError = fmt.Errorf("%w; new api error for runner %s - %v", occurredError, id, err)
|
||||
}
|
||||
}
|
||||
return occurredError
|
||||
}
|
||||
|
||||
func (n *NomadEnvironment) Sample(apiClient nomad.ExecutorAPI) (runner.Runner, bool) {
|
||||
r, ok := n.idleRunners.Sample()
|
||||
if ok {
|
||||
err := n.createRunner(apiClient)
|
||||
if err != nil {
|
||||
log.WithError(err).WithField("environmentID", n.ID()).Error("Couldn't create new runner for claimed one")
|
||||
}
|
||||
}
|
||||
return r, ok
|
||||
}
|
||||
|
||||
func (n *NomadEnvironment) AddRunner(r runner.Runner) {
|
||||
n.idleRunners.Add(r)
|
||||
}
|
||||
|
||||
func (n *NomadEnvironment) DeleteRunner(id string) {
|
||||
n.idleRunners.Delete(id)
|
||||
}
|
||||
|
||||
// MarshalJSON implements the json.Marshaler interface.
|
||||
// This converts the NomadEnvironment into the expected schema for dto.ExecutionEnvironmentData.
|
||||
func (n *NomadEnvironment) MarshalJSON() (res []byte, err error) {
|
||||
networkAccess, exposedPorts := n.NetworkAccess()
|
||||
|
||||
res, err = json.Marshal(dto.ExecutionEnvironmentData{
|
||||
ID: int(n.ID()),
|
||||
ExecutionEnvironmentRequest: dto.ExecutionEnvironmentRequest{
|
||||
PrewarmingPoolSize: n.PrewarmingPoolSize(),
|
||||
CPULimit: n.CPULimit(),
|
||||
MemoryLimit: n.MemoryLimit(),
|
||||
Image: n.Image(),
|
||||
NetworkAccess: networkAccess,
|
||||
ExposedPorts: exposedPorts,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return res, fmt.Errorf("couldn't marshal execution environment: %w", err)
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// DeepCopyJob clones the native Nomad job in a way that it can be used as Runner job.
|
||||
func (n *NomadEnvironment) DeepCopyJob() *nomadApi.Job {
|
||||
copyJob, err := parseJob(n.jobHCL)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("The HCL of an existing environment should throw no error!")
|
||||
return nil
|
||||
}
|
||||
copyEnvironment := &NomadEnvironment{job: copyJob}
|
||||
|
||||
copyEnvironment.SetConfigFrom(n)
|
||||
return copyEnvironment.job
|
||||
}
|
||||
|
||||
func (n *NomadEnvironment) SetConfigFrom(environment runner.ExecutionEnvironment) {
|
||||
n.SetID(environment.ID())
|
||||
n.SetPrewarmingPoolSize(environment.PrewarmingPoolSize())
|
||||
n.SetCPULimit(environment.CPULimit())
|
||||
n.SetMemoryLimit(environment.MemoryLimit())
|
||||
n.SetImage(environment.Image())
|
||||
n.SetNetworkAccess(environment.NetworkAccess())
|
||||
}
|
||||
|
||||
func parseJob(jobHCL string) (*nomadApi.Job, error) {
|
||||
config := jobspec2.ParseConfig{
|
||||
Body: []byte(jobHCL),
|
||||
AllowFS: false,
|
||||
Strict: true,
|
||||
}
|
||||
job, err := jobspec2.ParseWithConfig(&config)
|
||||
if err != nil {
|
||||
return job, fmt.Errorf("couldn't parse job HCL: %w", err)
|
||||
}
|
||||
return job, nil
|
||||
}
|
||||
|
||||
func (n *NomadEnvironment) createRunners(apiClient nomad.ExecutorAPI, count uint) error {
|
||||
log.WithField("runnersRequired", count).WithField("id", n.ID()).Debug("Creating new runners")
|
||||
for i := 0; i < int(count); i++ {
|
||||
err := n.createRunner(apiClient)
|
||||
if err != nil {
|
||||
return fmt.Errorf("couldn't create new runner: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *NomadEnvironment) createRunner(apiClient nomad.ExecutorAPI) error {
|
||||
newUUID, err := uuid.NewUUID()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed generating runner id: %w", err)
|
||||
}
|
||||
|
||||
newRunnerID := nomad.RunnerJobID(n.ID(), newUUID.String())
|
||||
template := n.DeepCopyJob()
|
||||
template.ID = &newRunnerID
|
||||
template.Name = &newRunnerID
|
||||
|
||||
err = apiClient.RegisterRunnerJob(template)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error registering new runner job: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *NomadEnvironment) removeRunners(apiClient nomad.ExecutorAPI, count uint) error {
|
||||
log.WithField("runnersToDelete", count).WithField("id", n.ID()).Debug("Removing idle runners")
|
||||
for i := 0; i < int(count); i++ {
|
||||
r, ok := n.idleRunners.Sample()
|
||||
if !ok {
|
||||
return fmt.Errorf("could not delete expected idle runner: %w", runner.ErrRunnerNotFound)
|
||||
}
|
||||
err := apiClient.DeleteJob(r.ID())
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not delete expected Nomad idle runner: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
192
internal/environment/environment_test.go
Normal file
192
internal/environment/environment_test.go
Normal file
@@ -0,0 +1,192 @@
|
||||
package environment
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
nomadApi "github.com/hashicorp/nomad/api"
|
||||
"github.com/openHPI/poseidon/internal/nomad"
|
||||
"github.com/openHPI/poseidon/internal/runner"
|
||||
"github.com/openHPI/poseidon/tests"
|
||||
"github.com/openHPI/poseidon/tests/helpers"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestConfigureNetworkCreatesNewNetworkWhenNoNetworkExists(t *testing.T) {
|
||||
_, job := helpers.CreateTemplateJob()
|
||||
defaultTaskGroup := nomad.FindOrCreateDefaultTaskGroup(job)
|
||||
environment := &NomadEnvironment{"", job, nil}
|
||||
|
||||
if assert.Equal(t, 0, len(defaultTaskGroup.Networks)) {
|
||||
environment.SetNetworkAccess(true, []uint16{})
|
||||
|
||||
assert.Equal(t, 1, len(defaultTaskGroup.Networks))
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigureNetworkDoesNotCreateNewNetworkWhenNetworkExists(t *testing.T) {
|
||||
_, job := helpers.CreateTemplateJob()
|
||||
defaultTaskGroup := nomad.FindOrCreateDefaultTaskGroup(job)
|
||||
environment := &NomadEnvironment{"", job, nil}
|
||||
|
||||
networkResource := &nomadApi.NetworkResource{Mode: "bridge"}
|
||||
defaultTaskGroup.Networks = []*nomadApi.NetworkResource{networkResource}
|
||||
|
||||
if assert.Equal(t, 1, len(defaultTaskGroup.Networks)) {
|
||||
environment.SetNetworkAccess(true, []uint16{})
|
||||
|
||||
assert.Equal(t, 1, len(defaultTaskGroup.Networks))
|
||||
assert.Equal(t, networkResource, defaultTaskGroup.Networks[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigureNetworkSetsCorrectValues(t *testing.T) {
|
||||
_, job := helpers.CreateTemplateJob()
|
||||
defaultTaskGroup := nomad.FindOrCreateDefaultTaskGroup(job)
|
||||
defaultTask := nomad.FindOrCreateDefaultTask(defaultTaskGroup)
|
||||
|
||||
mode, ok := defaultTask.Config["network_mode"]
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "none", mode)
|
||||
assert.Equal(t, 0, len(defaultTaskGroup.Networks))
|
||||
|
||||
exposedPortsTests := [][]uint16{{}, {1337}, {42, 1337}}
|
||||
t.Run("with no network access", func(t *testing.T) {
|
||||
for _, ports := range exposedPortsTests {
|
||||
_, testJob := helpers.CreateTemplateJob()
|
||||
testTaskGroup := nomad.FindOrCreateDefaultTaskGroup(testJob)
|
||||
testTask := nomad.FindOrCreateDefaultTask(testTaskGroup)
|
||||
testEnvironment := &NomadEnvironment{"", job, nil}
|
||||
|
||||
testEnvironment.SetNetworkAccess(false, ports)
|
||||
mode, ok := testTask.Config["network_mode"]
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "none", mode)
|
||||
assert.Equal(t, 0, len(testTaskGroup.Networks))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("with network access", func(t *testing.T) {
|
||||
for _, ports := range exposedPortsTests {
|
||||
_, testJob := helpers.CreateTemplateJob()
|
||||
testTaskGroup := nomad.FindOrCreateDefaultTaskGroup(testJob)
|
||||
testTask := nomad.FindOrCreateDefaultTask(testTaskGroup)
|
||||
testEnvironment := &NomadEnvironment{"", testJob, nil}
|
||||
|
||||
testEnvironment.SetNetworkAccess(true, ports)
|
||||
require.Equal(t, 1, len(testTaskGroup.Networks))
|
||||
|
||||
networkResource := testTaskGroup.Networks[0]
|
||||
assert.Equal(t, "bridge", networkResource.Mode)
|
||||
require.Equal(t, len(ports), len(networkResource.DynamicPorts))
|
||||
|
||||
assertExpectedPorts(t, ports, networkResource)
|
||||
|
||||
mode, ok := testTask.Config["network_mode"]
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, 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 TestRegisterFailsWhenNomadJobRegistrationFails(t *testing.T) {
|
||||
apiClientMock := &nomad.ExecutorAPIMock{}
|
||||
expectedErr := tests.ErrDefault
|
||||
|
||||
apiClientMock.On("RegisterNomadJob", mock.AnythingOfType("*api.Job")).Return("", expectedErr)
|
||||
|
||||
environment := &NomadEnvironment{"", &nomadApi.Job{}, nil}
|
||||
environment.SetID(tests.DefaultEnvironmentIDAsInteger)
|
||||
err := environment.Register(apiClientMock)
|
||||
|
||||
assert.ErrorIs(t, err, expectedErr)
|
||||
apiClientMock.AssertNotCalled(t, "EvaluationStream")
|
||||
}
|
||||
|
||||
func TestRegisterTemplateJobSucceedsWhenMonitoringEvaluationSucceeds(t *testing.T) {
|
||||
apiClientMock := &nomad.ExecutorAPIMock{}
|
||||
evaluationID := "id"
|
||||
|
||||
stream := make(chan *nomadApi.Events)
|
||||
readonlyStream := func() <-chan *nomadApi.Events {
|
||||
return stream
|
||||
}()
|
||||
// Immediately close stream to avoid any reading from it resulting in endless wait
|
||||
close(stream)
|
||||
|
||||
apiClientMock.On("RegisterNomadJob", mock.AnythingOfType("*api.Job")).Return(evaluationID, nil)
|
||||
apiClientMock.On("MonitorEvaluation", mock.AnythingOfType("string"), mock.Anything).Return(nil)
|
||||
apiClientMock.On("EvaluationStream", evaluationID, mock.AnythingOfType("*context.emptyCtx")).
|
||||
Return(readonlyStream, nil)
|
||||
|
||||
environment := &NomadEnvironment{"", &nomadApi.Job{}, nil}
|
||||
environment.SetID(tests.DefaultEnvironmentIDAsInteger)
|
||||
err := environment.Register(apiClientMock)
|
||||
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestRegisterTemplateJobReturnsErrorWhenMonitoringEvaluationFails(t *testing.T) {
|
||||
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)
|
||||
|
||||
environment := &NomadEnvironment{"", &nomadApi.Job{}, nil}
|
||||
environment.SetID(tests.DefaultEnvironmentIDAsInteger)
|
||||
err := environment.Register(apiClientMock)
|
||||
|
||||
assert.ErrorIs(t, err, tests.ErrDefault)
|
||||
}
|
||||
|
||||
func TestParseJob(t *testing.T) {
|
||||
t.Run("parses the given default job", func(t *testing.T) {
|
||||
environment, err := NewNomadEnvironment(templateEnvironmentJobHCL)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, environment.job)
|
||||
})
|
||||
|
||||
t.Run("returns error when given wrong job", func(t *testing.T) {
|
||||
environment, err := NewNomadEnvironment("")
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, environment)
|
||||
})
|
||||
}
|
||||
|
||||
func TestTwoSampleAddExactlyTwoRunners(t *testing.T) {
|
||||
apiMock := &nomad.ExecutorAPIMock{}
|
||||
apiMock.On("RegisterRunnerJob", mock.AnythingOfType("*api.Job")).Return(nil)
|
||||
|
||||
_, job := helpers.CreateTemplateJob()
|
||||
environment := &NomadEnvironment{templateEnvironmentJobHCL, job, runner.NewLocalRunnerStorage()}
|
||||
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(apiMock)
|
||||
require.True(t, ok)
|
||||
_, ok = environment.Sample(apiMock)
|
||||
require.True(t, ok)
|
||||
|
||||
apiMock.AssertNumberOfCalls(t, "RegisterRunnerJob", 2)
|
||||
}
|
@@ -3,15 +3,12 @@ package environment
|
||||
import (
|
||||
_ "embed"
|
||||
"fmt"
|
||||
nomadApi "github.com/hashicorp/nomad/api"
|
||||
"github.com/hashicorp/nomad/jobspec2"
|
||||
"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/pkg/logging"
|
||||
"os"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// templateEnvironmentJobHCL holds our default job in HCL format.
|
||||
@@ -28,13 +25,31 @@ type Manager interface {
|
||||
// It should be called during the startup process (e.g. on creation of the Manager).
|
||||
Load() error
|
||||
|
||||
// List returns all environments known by Poseidon.
|
||||
// When `fetch` is set the environments are fetched from the executor before returning.
|
||||
List(fetch bool) ([]runner.ExecutionEnvironment, error)
|
||||
|
||||
// Get returns the details of the requested environment.
|
||||
// When `fetch` is set the requested environment is fetched from the executor before returning.
|
||||
Get(id dto.EnvironmentID, fetch bool) (runner.ExecutionEnvironment, error)
|
||||
|
||||
// CreateOrUpdate creates/updates an execution environment on the executor.
|
||||
// If the job was created, the returned boolean is true, if it was updated, it is false.
|
||||
// If err is not nil, that means the environment was neither created nor updated.
|
||||
CreateOrUpdate(
|
||||
id runner.EnvironmentID,
|
||||
id dto.EnvironmentID,
|
||||
request dto.ExecutionEnvironmentRequest,
|
||||
) (bool, error)
|
||||
|
||||
// Delete removes the specified execution environment.
|
||||
// Iff the specified environment could not be found Delete returns false.
|
||||
Delete(id dto.EnvironmentID) (bool, error)
|
||||
}
|
||||
|
||||
type NomadEnvironmentManager struct {
|
||||
runnerManager runner.Manager
|
||||
api nomad.ExecutorAPI
|
||||
templateEnvironmentHCL string
|
||||
}
|
||||
|
||||
func NewNomadEnvironmentManager(
|
||||
@@ -45,11 +60,8 @@ func NewNomadEnvironmentManager(
|
||||
if err := loadTemplateEnvironmentJobHCL(templateJobFile); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
templateEnvironmentJob, err := parseJob(templateEnvironmentJobHCL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m := &NomadEnvironmentManager{runnerManager, apiClient, *templateEnvironmentJob}
|
||||
|
||||
m := &NomadEnvironmentManager{runnerManager, apiClient, templateEnvironmentJobHCL}
|
||||
if err := m.Load(); err != nil {
|
||||
log.WithError(err).Error("Error recovering the execution environments")
|
||||
}
|
||||
@@ -57,6 +69,121 @@ func NewNomadEnvironmentManager(
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m *NomadEnvironmentManager) Get(id dto.EnvironmentID, fetch bool) (
|
||||
executionEnvironment runner.ExecutionEnvironment, err error) {
|
||||
executionEnvironment, ok := m.runnerManager.GetEnvironment(id)
|
||||
|
||||
if fetch {
|
||||
fetchedEnvironment, err := fetchEnvironment(id, m.api)
|
||||
switch {
|
||||
case err != nil:
|
||||
return nil, err
|
||||
case fetchedEnvironment == nil:
|
||||
_, err = m.Delete(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ok = false
|
||||
case !ok:
|
||||
m.runnerManager.SetEnvironment(fetchedEnvironment)
|
||||
executionEnvironment = fetchedEnvironment
|
||||
ok = true
|
||||
default:
|
||||
executionEnvironment.SetConfigFrom(fetchedEnvironment)
|
||||
}
|
||||
}
|
||||
|
||||
if !ok {
|
||||
err = runner.ErrUnknownExecutionEnvironment
|
||||
}
|
||||
return executionEnvironment, err
|
||||
}
|
||||
|
||||
func (m *NomadEnvironmentManager) List(fetch bool) ([]runner.ExecutionEnvironment, error) {
|
||||
if fetch {
|
||||
err := m.Load()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return m.runnerManager.ListEnvironments(), nil
|
||||
}
|
||||
|
||||
func (m *NomadEnvironmentManager) CreateOrUpdate(id dto.EnvironmentID, request dto.ExecutionEnvironmentRequest) (
|
||||
created bool, err error) {
|
||||
environment, ok := m.runnerManager.GetEnvironment(id)
|
||||
if !ok {
|
||||
environment, err = NewNomadEnvironment(m.templateEnvironmentHCL)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("error creating Nomad environment: %w", err)
|
||||
}
|
||||
environment.SetID(id)
|
||||
}
|
||||
|
||||
environment.SetPrewarmingPoolSize(request.PrewarmingPoolSize)
|
||||
environment.SetCPULimit(request.CPULimit)
|
||||
environment.SetMemoryLimit(request.MemoryLimit)
|
||||
environment.SetImage(request.Image)
|
||||
environment.SetNetworkAccess(request.NetworkAccess, request.ExposedPorts)
|
||||
created = m.runnerManager.SetEnvironment(environment)
|
||||
|
||||
err = environment.Register(m.api)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("error registering template job in API: %w", err)
|
||||
}
|
||||
err = environment.UpdateRunnerSpecs(m.api)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("error updating runner jobs in API: %w", err)
|
||||
}
|
||||
err = environment.Scale(m.api)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("error scaling template job in API: %w", err)
|
||||
}
|
||||
|
||||
return created, nil
|
||||
}
|
||||
|
||||
func (m *NomadEnvironmentManager) Delete(id dto.EnvironmentID) (bool, error) {
|
||||
executionEnvironment, ok := m.runnerManager.GetEnvironment(id)
|
||||
if !ok {
|
||||
return false, nil
|
||||
}
|
||||
m.runnerManager.DeleteEnvironment(id)
|
||||
err := executionEnvironment.Delete(m.api)
|
||||
if err != nil {
|
||||
return true, fmt.Errorf("could not delete environment: %w", err)
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (m *NomadEnvironmentManager) Load() error {
|
||||
templateJobs, err := m.api.LoadEnvironmentJobs()
|
||||
if err != nil {
|
||||
return fmt.Errorf("couldn't load template jobs: %w", err)
|
||||
}
|
||||
|
||||
for _, job := range templateJobs {
|
||||
jobLogger := log.WithField("jobID", *job.ID)
|
||||
if *job.Status != structs.JobStatusRunning {
|
||||
jobLogger.Info("Job not running, skipping ...")
|
||||
continue
|
||||
}
|
||||
configTaskGroup := nomad.FindOrCreateConfigTaskGroup(job)
|
||||
if configTaskGroup == nil {
|
||||
jobLogger.Info("Couldn't find config task group in job, skipping ...")
|
||||
continue
|
||||
}
|
||||
environment := &NomadEnvironment{
|
||||
jobHCL: templateEnvironmentJobHCL,
|
||||
job: job,
|
||||
idleRunners: runner.NewLocalRunnerStorage(),
|
||||
}
|
||||
m.runnerManager.SetEnvironment(environment)
|
||||
jobLogger.Info("Successfully recovered environment")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadTemplateEnvironmentJobHCL loads the template environment job HCL from the given path.
|
||||
// If the path is empty, the embedded default file is used.
|
||||
func loadTemplateEnvironmentJobHCL(path string) error {
|
||||
@@ -71,84 +198,25 @@ func loadTemplateEnvironmentJobHCL(path string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type NomadEnvironmentManager struct {
|
||||
runnerManager runner.Manager
|
||||
api nomad.ExecutorAPI
|
||||
templateEnvironmentJob nomadApi.Job
|
||||
}
|
||||
|
||||
func (m *NomadEnvironmentManager) CreateOrUpdate(
|
||||
id runner.EnvironmentID,
|
||||
request dto.ExecutionEnvironmentRequest,
|
||||
) (bool, error) {
|
||||
templateJob, err := m.api.RegisterTemplateJob(&m.templateEnvironmentJob, runner.TemplateJobID(id),
|
||||
request.PrewarmingPoolSize, request.CPULimit, request.MemoryLimit,
|
||||
request.Image, request.NetworkAccess, request.ExposedPorts)
|
||||
|
||||
func fetchEnvironment(id dto.EnvironmentID, apiClient nomad.ExecutorAPI) (runner.ExecutionEnvironment, error) {
|
||||
environments, err := apiClient.LoadEnvironmentJobs()
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("error registering template job in API: %w", err)
|
||||
return nil, fmt.Errorf("error fetching the environment jobs: %w", err)
|
||||
}
|
||||
|
||||
created, err := m.runnerManager.CreateOrUpdateEnvironment(id, request.PrewarmingPoolSize, templateJob, true)
|
||||
if err != nil {
|
||||
return created, fmt.Errorf("error updating environment in runner manager: %w", err)
|
||||
var fetchedEnvironment runner.ExecutionEnvironment
|
||||
for _, job := range environments {
|
||||
environmentID, err := nomad.EnvironmentIDFromTemplateJobID(*job.ID)
|
||||
if err != nil {
|
||||
log.WithError(err).Warn("Cannot parse environment id of loaded environment")
|
||||
continue
|
||||
}
|
||||
if id == environmentID {
|
||||
fetchedEnvironment = &NomadEnvironment{
|
||||
jobHCL: templateEnvironmentJobHCL,
|
||||
job: job,
|
||||
idleRunners: runner.NewLocalRunnerStorage(),
|
||||
}
|
||||
}
|
||||
}
|
||||
return created, nil
|
||||
}
|
||||
|
||||
func (m *NomadEnvironmentManager) Load() error {
|
||||
templateJobs, err := m.api.LoadEnvironmentJobs()
|
||||
if err != nil {
|
||||
return fmt.Errorf("couldn't load template jobs: %w", err)
|
||||
}
|
||||
|
||||
for _, job := range templateJobs {
|
||||
jobLogger := log.WithField("jobID", *job.ID)
|
||||
if *job.Status != structs.JobStatusRunning {
|
||||
jobLogger.Info("Job not running, skipping ...")
|
||||
continue
|
||||
}
|
||||
configTaskGroup := nomad.FindConfigTaskGroup(job)
|
||||
if configTaskGroup == nil {
|
||||
jobLogger.Info("Couldn't find config task group in job, skipping ...")
|
||||
continue
|
||||
}
|
||||
desiredIdleRunnersCount, err := strconv.Atoi(configTaskGroup.Meta[nomad.ConfigMetaPoolSizeKey])
|
||||
if err != nil {
|
||||
jobLogger.Infof("Couldn't convert pool size to int: %v, skipping ...", err)
|
||||
continue
|
||||
}
|
||||
environmentIDString, err := runner.EnvironmentIDFromTemplateJobID(*job.ID)
|
||||
if err != nil {
|
||||
jobLogger.WithError(err).Error("Couldn't retrieve environment id from template job")
|
||||
}
|
||||
environmentID, err := runner.NewEnvironmentID(environmentIDString)
|
||||
if err != nil {
|
||||
jobLogger.WithField("environmentID", environmentIDString).
|
||||
WithError(err).
|
||||
Error("Couldn't retrieve environmentID from string")
|
||||
continue
|
||||
}
|
||||
_, err = m.runnerManager.CreateOrUpdateEnvironment(environmentID, uint(desiredIdleRunnersCount), job, false)
|
||||
if err != nil {
|
||||
jobLogger.WithError(err).Info("Could not recover job.")
|
||||
continue
|
||||
}
|
||||
jobLogger.Info("Successfully recovered environment")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseJob(jobHCL string) (*nomadApi.Job, error) {
|
||||
config := jobspec2.ParseConfig{
|
||||
Body: []byte(jobHCL),
|
||||
AllowFS: false,
|
||||
Strict: true,
|
||||
}
|
||||
job, err := jobspec2.ParseWithConfig(&config)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing Nomad job: %w", err)
|
||||
}
|
||||
|
||||
return job, nil
|
||||
return fetchedEnvironment, nil
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
// Code generated by mockery v2.8.0. DO NOT EDIT.
|
||||
// Code generated by mockery v2.9.4. DO NOT EDIT.
|
||||
|
||||
package environment
|
||||
|
||||
@@ -15,18 +15,18 @@ type ManagerMock struct {
|
||||
}
|
||||
|
||||
// CreateOrUpdate provides a mock function with given fields: id, request
|
||||
func (_m *ManagerMock) CreateOrUpdate(id runner.EnvironmentID, request dto.ExecutionEnvironmentRequest) (bool, error) {
|
||||
func (_m *ManagerMock) CreateOrUpdate(id dto.EnvironmentID, request dto.ExecutionEnvironmentRequest) (bool, error) {
|
||||
ret := _m.Called(id, request)
|
||||
|
||||
var r0 bool
|
||||
if rf, ok := ret.Get(0).(func(runner.EnvironmentID, dto.ExecutionEnvironmentRequest) bool); ok {
|
||||
if rf, ok := ret.Get(0).(func(dto.EnvironmentID, dto.ExecutionEnvironmentRequest) bool); ok {
|
||||
r0 = rf(id, request)
|
||||
} else {
|
||||
r0 = ret.Get(0).(bool)
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(runner.EnvironmentID, dto.ExecutionEnvironmentRequest) error); ok {
|
||||
if rf, ok := ret.Get(1).(func(dto.EnvironmentID, dto.ExecutionEnvironmentRequest) error); ok {
|
||||
r1 = rf(id, request)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
@@ -36,8 +36,70 @@ func (_m *ManagerMock) CreateOrUpdate(id runner.EnvironmentID, request dto.Execu
|
||||
}
|
||||
|
||||
// Delete provides a mock function with given fields: id
|
||||
func (_m *ManagerMock) Delete(id string) {
|
||||
_m.Called(id)
|
||||
func (_m *ManagerMock) 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 *ManagerMock) 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
|
||||
}
|
||||
|
||||
// List provides a mock function with given fields: fetch
|
||||
func (_m *ManagerMock) 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
|
||||
}
|
||||
|
||||
// Load provides a mock function with given fields:
|
||||
|
@@ -1,7 +1,9 @@
|
||||
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"
|
||||
@@ -12,6 +14,7 @@ import (
|
||||
"github.com/stretchr/testify/suite"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type CreateOrUpdateTestSuite struct {
|
||||
@@ -20,7 +23,7 @@ type CreateOrUpdateTestSuite struct {
|
||||
apiMock nomad.ExecutorAPIMock
|
||||
request dto.ExecutionEnvironmentRequest
|
||||
manager *NomadEnvironmentManager
|
||||
environmentID runner.EnvironmentID
|
||||
environmentID dto.EnvironmentID
|
||||
}
|
||||
|
||||
func TestCreateOrUpdateTestSuite(t *testing.T) {
|
||||
@@ -41,97 +44,22 @@ func (s *CreateOrUpdateTestSuite) SetupTest() {
|
||||
}
|
||||
|
||||
s.manager = &NomadEnvironmentManager{
|
||||
runnerManager: &s.runnerManagerMock,
|
||||
api: &s.apiMock,
|
||||
runnerManager: &s.runnerManagerMock,
|
||||
api: &s.apiMock,
|
||||
templateEnvironmentHCL: templateEnvironmentJobHCL,
|
||||
}
|
||||
|
||||
s.environmentID = runner.EnvironmentID(tests.DefaultEnvironmentIDAsInteger)
|
||||
}
|
||||
|
||||
func (s *CreateOrUpdateTestSuite) mockRegisterTemplateJob(job *nomadApi.Job, err error) {
|
||||
s.apiMock.On("RegisterTemplateJob",
|
||||
mock.AnythingOfType("*api.Job"), mock.AnythingOfType("string"),
|
||||
mock.AnythingOfType("uint"), mock.AnythingOfType("uint"), mock.AnythingOfType("uint"),
|
||||
mock.AnythingOfType("string"), mock.AnythingOfType("bool"), mock.AnythingOfType("[]uint16")).
|
||||
Return(job, err)
|
||||
}
|
||||
|
||||
func (s *CreateOrUpdateTestSuite) mockCreateOrUpdateEnvironment(created bool, err error) {
|
||||
s.runnerManagerMock.On("CreateOrUpdateEnvironment", mock.AnythingOfType("EnvironmentID"),
|
||||
mock.AnythingOfType("uint"), mock.AnythingOfType("*api.Job"), mock.AnythingOfType("bool")).
|
||||
Return(created, err)
|
||||
}
|
||||
|
||||
func (s *CreateOrUpdateTestSuite) TestRegistersCorrectTemplateJob() {
|
||||
s.mockRegisterTemplateJob(&nomadApi.Job{}, nil)
|
||||
s.mockCreateOrUpdateEnvironment(true, nil)
|
||||
|
||||
_, err := s.manager.CreateOrUpdate(s.environmentID, s.request)
|
||||
s.NoError(err)
|
||||
|
||||
s.apiMock.AssertCalled(s.T(), "RegisterTemplateJob",
|
||||
&s.manager.templateEnvironmentJob, runner.TemplateJobID(s.environmentID),
|
||||
s.request.PrewarmingPoolSize, s.request.CPULimit, s.request.MemoryLimit,
|
||||
s.request.Image, s.request.NetworkAccess, s.request.ExposedPorts)
|
||||
}
|
||||
|
||||
func (s *CreateOrUpdateTestSuite) TestReturnsErrorWhenRegisterTemplateJobReturnsError() {
|
||||
s.mockRegisterTemplateJob(nil, tests.ErrDefault)
|
||||
|
||||
created, err := s.manager.CreateOrUpdate(s.environmentID, s.request)
|
||||
s.ErrorIs(err, tests.ErrDefault)
|
||||
s.False(created)
|
||||
}
|
||||
|
||||
func (s *CreateOrUpdateTestSuite) TestCreatesOrUpdatesCorrectEnvironment() {
|
||||
templateJobID := tests.DefaultJobID
|
||||
templateJob := &nomadApi.Job{ID: &templateJobID}
|
||||
s.mockRegisterTemplateJob(templateJob, nil)
|
||||
s.mockCreateOrUpdateEnvironment(true, nil)
|
||||
|
||||
_, err := s.manager.CreateOrUpdate(s.environmentID, s.request)
|
||||
s.NoError(err)
|
||||
s.runnerManagerMock.AssertCalled(s.T(), "CreateOrUpdateEnvironment",
|
||||
s.environmentID, s.request.PrewarmingPoolSize, templateJob, true)
|
||||
s.environmentID = dto.EnvironmentID(tests.DefaultEnvironmentIDAsInteger)
|
||||
}
|
||||
|
||||
func (s *CreateOrUpdateTestSuite) TestReturnsErrorIfCreatesOrUpdateEnvironmentReturnsError() {
|
||||
s.mockRegisterTemplateJob(&nomadApi.Job{}, nil)
|
||||
s.mockCreateOrUpdateEnvironment(false, tests.ErrDefault)
|
||||
_, err := s.manager.CreateOrUpdate(runner.EnvironmentID(tests.DefaultEnvironmentIDAsInteger), s.request)
|
||||
s.apiMock.On("RegisterNomadJob", mock.AnythingOfType("*api.Job")).Return("", tests.ErrDefault)
|
||||
s.runnerManagerMock.On("GetEnvironment", mock.AnythingOfType("dto.EnvironmentID")).Return(nil, false)
|
||||
s.runnerManagerMock.On("SetEnvironment", mock.AnythingOfType("*environment.NomadEnvironment")).Return(true)
|
||||
_, err := s.manager.CreateOrUpdate(dto.EnvironmentID(tests.DefaultEnvironmentIDAsInteger), s.request)
|
||||
s.ErrorIs(err, tests.ErrDefault)
|
||||
}
|
||||
|
||||
func (s *CreateOrUpdateTestSuite) TestReturnsTrueIfCreatesOrUpdateEnvironmentReturnsTrue() {
|
||||
s.mockRegisterTemplateJob(&nomadApi.Job{}, nil)
|
||||
s.mockCreateOrUpdateEnvironment(true, nil)
|
||||
created, err := s.manager.CreateOrUpdate(runner.EnvironmentID(tests.DefaultEnvironmentIDAsInteger), s.request)
|
||||
s.Require().NoError(err)
|
||||
s.True(created)
|
||||
}
|
||||
|
||||
func (s *CreateOrUpdateTestSuite) TestReturnsFalseIfCreatesOrUpdateEnvironmentReturnsFalse() {
|
||||
s.mockRegisterTemplateJob(&nomadApi.Job{}, nil)
|
||||
s.mockCreateOrUpdateEnvironment(false, nil)
|
||||
created, err := s.manager.CreateOrUpdate(runner.EnvironmentID(tests.DefaultEnvironmentIDAsInteger), s.request)
|
||||
s.Require().NoError(err)
|
||||
s.False(created)
|
||||
}
|
||||
|
||||
func TestParseJob(t *testing.T) {
|
||||
t.Run("parses the given default job", func(t *testing.T) {
|
||||
job, err := parseJob(templateEnvironmentJobHCL)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, job)
|
||||
})
|
||||
|
||||
t.Run("returns error when given wrong job", func(t *testing.T) {
|
||||
job, err := parseJob("")
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, job)
|
||||
})
|
||||
}
|
||||
|
||||
func TestNewNomadEnvironmentManager(t *testing.T) {
|
||||
executorAPIMock := &nomad.ExecutorAPIMock{}
|
||||
executorAPIMock.On("LoadEnvironmentJobs").Return([]*nomadApi.Job{}, nil)
|
||||
@@ -148,7 +76,7 @@ func TestNewNomadEnvironmentManager(t *testing.T) {
|
||||
|
||||
t.Run("loads template environment job from file", func(t *testing.T) {
|
||||
templateJobHCL := "job \"test\" {}"
|
||||
expectedJob, err := parseJob(templateJobHCL)
|
||||
_, err := NewNomadEnvironment(templateJobHCL)
|
||||
require.NoError(t, err)
|
||||
f := createTempFile(t, templateJobHCL)
|
||||
defer os.Remove(f.Name())
|
||||
@@ -156,8 +84,7 @@ func TestNewNomadEnvironmentManager(t *testing.T) {
|
||||
m, err := NewNomadEnvironmentManager(runnerManagerMock, executorAPIMock, f.Name())
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, m)
|
||||
assert.Equal(t, templateJobHCL, templateEnvironmentJobHCL)
|
||||
assert.Equal(t, *expectedJob, m.templateEnvironmentJob)
|
||||
assert.Equal(t, templateJobHCL, m.templateEnvironmentHCL)
|
||||
})
|
||||
|
||||
t.Run("returns error if template file is invalid", func(t *testing.T) {
|
||||
@@ -165,13 +92,153 @@ func TestNewNomadEnvironmentManager(t *testing.T) {
|
||||
f := createTempFile(t, templateJobHCL)
|
||||
defer os.Remove(f.Name())
|
||||
|
||||
_, err := NewNomadEnvironmentManager(runnerManagerMock, executorAPIMock, f.Name())
|
||||
m, err := NewNomadEnvironmentManager(runnerManagerMock, executorAPIMock, f.Name())
|
||||
require.NoError(t, err)
|
||||
_, err = NewNomadEnvironment(m.templateEnvironmentHCL)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
templateEnvironmentJobHCL = previousTemplateEnvironmentJobHCL
|
||||
}
|
||||
|
||||
func TestNomadEnvironmentManager_Get(t *testing.T) {
|
||||
apiMock := &nomad.ExecutorAPIMock{}
|
||||
mockWatchAllocations(apiMock)
|
||||
call := apiMock.On("LoadEnvironmentJobs")
|
||||
call.Run(func(args mock.Arguments) {
|
||||
call.ReturnArguments = mock.Arguments{[]*nomadApi.Job{}, nil}
|
||||
})
|
||||
|
||||
runnerManager := runner.NewNomadRunnerManager(apiMock, context.Background())
|
||||
m, err := NewNomadEnvironmentManager(runnerManager, apiMock, "")
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("Returns error when not found", func(t *testing.T) {
|
||||
_, err := m.Get(tests.DefaultEnvironmentIDAsInteger, false)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("Returns environment when it was added before", func(t *testing.T) {
|
||||
expectedEnvironment, err := NewNomadEnvironment(templateEnvironmentJobHCL)
|
||||
expectedEnvironment.SetID(tests.DefaultEnvironmentIDAsInteger)
|
||||
require.NoError(t, err)
|
||||
runnerManager.SetEnvironment(expectedEnvironment)
|
||||
|
||||
environment, err := m.Get(tests.DefaultEnvironmentIDAsInteger, false)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expectedEnvironment, environment)
|
||||
})
|
||||
|
||||
t.Run("Fetch", func(t *testing.T) {
|
||||
apiMock.On("DeleteJob", mock.AnythingOfType("string")).Return(nil)
|
||||
t.Run("Returns error when not found", func(t *testing.T) {
|
||||
_, err := m.Get(tests.DefaultEnvironmentIDAsInteger, true)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("Updates values when environment already known by Poseidon", func(t *testing.T) {
|
||||
fetchedEnvironment, err := NewNomadEnvironment(templateEnvironmentJobHCL)
|
||||
require.NoError(t, 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(templateEnvironmentJobHCL)
|
||||
require.NoError(t, err)
|
||||
localEnvironment.SetID(tests.DefaultEnvironmentIDAsInteger)
|
||||
runnerManager.SetEnvironment(localEnvironment)
|
||||
|
||||
environment, err := m.Get(tests.DefaultEnvironmentIDAsInteger, false)
|
||||
assert.NoError(t, err)
|
||||
assert.NotEqual(t, fetchedEnvironment.Image(), environment.Image())
|
||||
|
||||
environment, err = m.Get(tests.DefaultEnvironmentIDAsInteger, true)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, fetchedEnvironment.Image(), environment.Image())
|
||||
})
|
||||
runnerManager.DeleteEnvironment(tests.DefaultEnvironmentIDAsInteger)
|
||||
|
||||
t.Run("Adds environment when not already known by Poseidon", func(t *testing.T) {
|
||||
fetchedEnvironment, err := NewNomadEnvironment(templateEnvironmentJobHCL)
|
||||
require.NoError(t, 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)
|
||||
assert.Error(t, err)
|
||||
|
||||
environment, err := m.Get(tests.DefaultEnvironmentIDAsInteger, true)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, fetchedEnvironment.Image(), environment.Image())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestNomadEnvironmentManager_List(t *testing.T) {
|
||||
apiMock := &nomad.ExecutorAPIMock{}
|
||||
mockWatchAllocations(apiMock)
|
||||
call := apiMock.On("LoadEnvironmentJobs")
|
||||
call.Run(func(args mock.Arguments) {
|
||||
call.ReturnArguments = mock.Arguments{[]*nomadApi.Job{}, nil}
|
||||
})
|
||||
|
||||
runnerManager := runner.NewNomadRunnerManager(apiMock, context.Background())
|
||||
m, err := NewNomadEnvironmentManager(runnerManager, apiMock, "")
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("with no environments", func(t *testing.T) {
|
||||
environments, err := m.List(true)
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, environments)
|
||||
})
|
||||
|
||||
t.Run("Returns added environment", func(t *testing.T) {
|
||||
localEnvironment, err := NewNomadEnvironment(templateEnvironmentJobHCL)
|
||||
require.NoError(t, err)
|
||||
localEnvironment.SetID(tests.DefaultEnvironmentIDAsInteger)
|
||||
runnerManager.SetEnvironment(localEnvironment)
|
||||
|
||||
environments, err := m.List(false)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, len(environments))
|
||||
assert.Equal(t, localEnvironment, environments[0])
|
||||
})
|
||||
runnerManager.DeleteEnvironment(tests.DefaultEnvironmentIDAsInteger)
|
||||
|
||||
t.Run("Fetches new Runners via the api client", func(t *testing.T) {
|
||||
fetchedEnvironment, err := NewNomadEnvironment(templateEnvironmentJobHCL)
|
||||
require.NoError(t, 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)
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, environments)
|
||||
|
||||
environments, err = m.List(true)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, len(environments))
|
||||
assert.Equal(t, fetchedEnvironment, environments[0])
|
||||
})
|
||||
}
|
||||
|
||||
func mockWatchAllocations(apiMock *nomad.ExecutorAPIMock) {
|
||||
call := apiMock.On("WatchAllocations", mock.Anything, mock.Anything, mock.Anything)
|
||||
call.Run(func(args mock.Arguments) {
|
||||
<-time.After(10 * time.Minute) // 10 minutes is the default test timeout
|
||||
call.ReturnArguments = mock.Arguments{nil}
|
||||
})
|
||||
}
|
||||
|
||||
func createTempFile(t *testing.T, content string) *os.File {
|
||||
t.Helper()
|
||||
f, err := os.CreateTemp("", "test")
|
||||
|
Reference in New Issue
Block a user