Restructure project

We previously didn't really had any structure in our project apart
from creating a new folder for each package in our project root.
Now that we have accumulated some packages, we use the well-known
Golang project layout in order to clearly communicate our intent
with packages. See https://github.com/golang-standards/project-layout
This commit is contained in:
sirkrypt0
2021-07-16 09:19:42 +02:00
parent 2f1383b743
commit 8b26ecbe5f
66 changed files with 95 additions and 95 deletions

View File

@@ -0,0 +1,132 @@
package environment
import (
_ "embed"
"fmt"
nomadApi "github.com/hashicorp/nomad/api"
"github.com/hashicorp/nomad/jobspec2"
"github.com/hashicorp/nomad/nomad/structs"
"gitlab.hpi.de/codeocean/codemoon/poseidon/internal/nomad"
"gitlab.hpi.de/codeocean/codemoon/poseidon/internal/runner"
"gitlab.hpi.de/codeocean/codemoon/poseidon/pkg/dto"
"gitlab.hpi.de/codeocean/codemoon/poseidon/pkg/logging"
"strconv"
)
// templateEnvironmentJobHCL holds our default job in HCL format.
// The default job is used when creating new job and provides
// common settings that all the jobs share.
//go:embed template-environment-job.hcl
var templateEnvironmentJobHCL string
var log = logging.GetLogger("environment")
// Manager encapsulates API calls to the executor API for creation and deletion of execution environments.
type Manager interface {
// Load fetches all already created execution environments from the executor and registers them at the runner manager.
// It should be called during the startup process (e.g. on creation of the Manager).
Load() 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,
request dto.ExecutionEnvironmentRequest,
) (bool, error)
}
func NewNomadEnvironmentManager(
runnerManager runner.Manager,
apiClient nomad.ExecutorAPI,
) *NomadEnvironmentManager {
m := &NomadEnvironmentManager{runnerManager, apiClient, *parseJob(templateEnvironmentJobHCL)}
if err := m.Load(); err != nil {
log.WithError(err).Error("Error recovering the execution environments")
}
runnerManager.Load()
return m
}
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)
if err != nil {
return false, fmt.Errorf("error registering template job in API: %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)
}
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 {
config := jobspec2.ParseConfig{
Body: []byte(jobHCL),
AllowFS: false,
Strict: true,
}
job, err := jobspec2.ParseWithConfig(&config)
if err != nil {
log.WithError(err).Fatal("Error parsing Nomad job")
return nil
}
return job
}

View File

@@ -0,0 +1,55 @@
// Code generated by mockery v2.8.0. DO NOT EDIT.
package environment
import (
mock "github.com/stretchr/testify/mock"
dto "gitlab.hpi.de/codeocean/codemoon/poseidon/pkg/dto"
runner "gitlab.hpi.de/codeocean/codemoon/poseidon/internal/runner"
)
// ManagerMock is an autogenerated mock type for the Manager type
type ManagerMock struct {
mock.Mock
}
// CreateOrUpdate provides a mock function with given fields: id, request
func (_m *ManagerMock) CreateOrUpdate(id runner.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 {
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 {
r1 = rf(id, request)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Delete provides a mock function with given fields: id
func (_m *ManagerMock) Delete(id string) {
_m.Called(id)
}
// Load provides a mock function with given fields:
func (_m *ManagerMock) Load() 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
}

View File

@@ -0,0 +1,142 @@
package environment
import (
nomadApi "github.com/hashicorp/nomad/api"
"github.com/sirupsen/logrus"
"github.com/sirupsen/logrus/hooks/test"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/suite"
"gitlab.hpi.de/codeocean/codemoon/poseidon/internal/nomad"
"gitlab.hpi.de/codeocean/codemoon/poseidon/internal/runner"
"gitlab.hpi.de/codeocean/codemoon/poseidon/pkg/dto"
"gitlab.hpi.de/codeocean/codemoon/poseidon/tests"
"testing"
)
type CreateOrUpdateTestSuite struct {
suite.Suite
runnerManagerMock runner.ManagerMock
apiMock nomad.ExecutorAPIMock
request dto.ExecutionEnvironmentRequest
manager *NomadEnvironmentManager
environmentID runner.EnvironmentID
}
func TestCreateOrUpdateTestSuite(t *testing.T) {
suite.Run(t, new(CreateOrUpdateTestSuite))
}
func (s *CreateOrUpdateTestSuite) 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{
runnerManager: &s.runnerManagerMock,
api: &s.apiMock,
}
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)
}
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.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) {
exited := false
logger, hook := test.NewNullLogger()
logger.ExitFunc = func(i int) {
exited = true
}
log = logger.WithField("pkg", "nomad")
t.Run("parses the given default job", func(t *testing.T) {
job := parseJob(templateEnvironmentJobHCL)
assert.False(t, exited)
assert.NotNil(t, job)
})
t.Run("fatals when given wrong job", func(t *testing.T) {
job := parseJob("")
assert.True(t, exited)
assert.Nil(t, job)
assert.Equal(t, logrus.FatalLevel, hook.LastEntry().Level)
})
}

View File

@@ -0,0 +1,79 @@
// This is the default job configuration that is used when no path to another default configuration is given
job "template-0" {
datacenters = ["dc1"]
type = "batch"
group "default-group" {
ephemeral_disk {
migrate = false
size = 10
sticky = false
}
count = 1
scaling {
enabled = true
min = 0
max = 300
}
spread {
// see https://www.nomadproject.io/docs/job-specification/spread#even-spread-across-data-center
// This spreads the load evenly amongst our nodes
attribute = "${node.unique.name}"
weight = 100
}
task "default-task" {
driver = "docker"
kill_timeout = "0s"
kill_signal = "SIGKILL"
config {
image = "drp.codemoon.xopic.de/openhpi/co_execenv_python:3.8"
command = "sleep"
args = ["infinity"]
network_mode = "none"
}
logs {
max_files = 1
max_file_size = 1
}
resources {
cpu = 40
memory = 40
}
restart {
delay = "0s"
}
}
}
group "config" {
// We want to store whether a task is in use in order to recover from a downtime.
// Without a separate config task, marking a task as used would result in a restart of that task,
// as the meta information is passed to the container as environment variables.
count = 0
task "config" {
driver = "exec"
config {
command = "true"
}
logs {
max_files = 1
max_file_size = 1
}
resources {
// minimum values
cpu = 1
memory = 10
}
}
meta {
used = "false"
prewarmingPoolSize = "0"
}
}
}