diff --git a/.gitignore b/.gitignore index 98e03cd..cb5cb5f 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,7 @@ *.dll *.so *.dylib - +*.sock # Test binary, built with `go test -c` *.test diff --git a/Dockerfile b/Dockerfile index ee42953..1ed1f0a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,11 +1,5 @@ FROM golang:1.24-alpine AS builder -# Build arguments -ARG VERSION=dev -ARG COMMIT_INFO=unknown -ARG BUILD_DATE=unknown -ARG BRANCH=unknown - # for sqlite RUN apk update && apk add --no-cache gcc musl-dev ENV CGO_ENABLED=1 @@ -19,6 +13,12 @@ RUN go mod download COPY ./src . +# Build arguments +ARG VERSION=dev +ARG COMMIT_INFO=unknown +ARG BUILD_DATE=unknown +ARG BRANCH=unknown + # arg substitution, do not put it higher than this for caching # https://stackoverflow.com/questions/44438637/arg-substitution-in-run-command-not-working-for-dockerfile ENV VERSION=${VERSION} @@ -29,12 +29,13 @@ ENV BRANCH=${BRANCH} # build optimized binary without debugging symbols RUN SOURCE_HASH=$(find . -type f -name "*.go" -print0 | sort -z | xargs -0 cat | sha256sum | cut -d ' ' -f1) && \ go build -ldflags "-s -w \ - -X github.com/makeopensource/leviathan/common.Version=${VERSION} \ - -X github.com/makeopensource/leviathan/common.CommitInfo=${COMMIT_INFO} \ - -X github.com/makeopensource/leviathan/common.BuildDate=${BUILD_DATE} \ - -X github.com/makeopensource/leviathan/common.Branch=${BRANCH} \ - -X github.com/makeopensource/leviathan/common.SourceHash=${SOURCE_HASH}" \ - -o leviathan + -X github.com/makeopensource/leviathan/internal/info.Version=${VERSION} \ + -X github.com/makeopensource/leviathan/internal/info.CommitInfo=${COMMIT_INFO} \ + -X github.com/makeopensource/leviathan/internal/info.BuildDate=${BUILD_DATE} \ + -X github.com/makeopensource/leviathan/internal/info.Branch=${BRANCH} \ + -X github.com/makeopensource/leviathan/internal/info.SourceHash=${SOURCE_HASH}" \ + -o leviathan \ + ./cmd/server/main.go FROM alpine:latest diff --git a/src/api/api.go b/src/api/api.go deleted file mode 100644 index 7b3b63b..0000000 --- a/src/api/api.go +++ /dev/null @@ -1,73 +0,0 @@ -package api - -import ( - "connectrpc.com/connect" - "fmt" - v1 "github.com/makeopensource/leviathan/api/v1" - "github.com/makeopensource/leviathan/common" - dkclient "github.com/makeopensource/leviathan/generated/docker_rpc/v1/v1connect" - jobClient "github.com/makeopensource/leviathan/generated/jobs/v1/v1connect" - labClient "github.com/makeopensource/leviathan/generated/labs/v1/v1connect" - "github.com/makeopensource/leviathan/service" - "github.com/rs/zerolog/log" - "golang.org/x/net/http2" - "golang.org/x/net/http2/h2c" - "net/http" -) - -func StartGrpcServer() { - mux := setupEndpoints() - - log.Info().Msg("Leviathan initialized successfully") - - srvAddr := fmt.Sprintf(":%s", common.ServerPort.GetStr()) - log.Info().Msgf("starting server on %s", srvAddr) - err := http.ListenAndServe( - srvAddr, - // Use h2c so we can serve HTTP/2 without TLS. - h2c.NewHandler(mux, &http2.Server{}), - ) - if err != nil { - log.Fatal().Err(err).Msgf("Failed to start server on %s", srvAddr) - return - } -} - -func setupEndpoints() *http.ServeMux { - docker, job, lab := service.InitServices() - - interceptor := connect.WithInterceptors() - if common.ApiKey.GetStr() != "" { - log.Info().Msg("ApiKey is set, endpoints now require authentication") - interceptor = connect.WithInterceptors(&authInterceptor{common.ApiKey.GetStr()}) - } - - v1Endpoints := []func() (string, http.Handler){ - // jobs endpoints - func() (string, http.Handler) { - jobSrv := v1.NewJobServer(job) - return jobClient.NewJobServiceHandler(jobSrv, interceptor) - }, - // docker endpoints - func() (string, http.Handler) { - dkSrv := &v1.DockerServer{Service: docker} - return dkclient.NewDockerServiceHandler(dkSrv, interceptor) - }, - func() (string, http.Handler) { - labSrv := v1.LabServer{Srv: lab} - return labClient.NewLabServiceHandler(labSrv, interceptor) - }, - func() (string, http.Handler) { - fileHandler := v1.NewFileManagerHandler("/files.v1") - return fileHandler.BasePath + "/", fileHandler - }, - } - - mux := http.NewServeMux() - for _, svc := range v1Endpoints { - path, handler := svc() - mux.Handle(path, handler) - } - - return mux -} diff --git a/src/api/v1/docker_impl.go b/src/api/v1/docker_impl.go deleted file mode 100644 index fa8b83a..0000000 --- a/src/api/v1/docker_impl.go +++ /dev/null @@ -1,50 +0,0 @@ -package v1 - -import ( - "connectrpc.com/connect" - "context" - dkrpc "github.com/makeopensource/leviathan/generated/docker_rpc/v1" - "github.com/makeopensource/leviathan/service/docker" -) - -type DockerServer struct { - Service *docker.DkService -} - -func (dk *DockerServer) CreateContainer(_ context.Context, req *connect.Request[dkrpc.CreateContainerRequest]) (*connect.Response[dkrpc.CreateContainerResponse], error) { - res := connect.NewResponse(&dkrpc.CreateContainerResponse{}) - return res, nil -} - -func (dk *DockerServer) StartContainer(_ context.Context, req *connect.Request[dkrpc.StartContainerRequest]) (*connect.Response[dkrpc.StartContainerResponse], error) { - res := connect.NewResponse(&dkrpc.StartContainerResponse{}) - return res, nil -} - -func (dk *DockerServer) DeleteContainer(_ context.Context, req *connect.Request[dkrpc.DeleteContainerRequest]) (*connect.Response[dkrpc.DeleteContainerResponse], error) { - res := connect.NewResponse(&dkrpc.DeleteContainerResponse{}) - return res, nil -} - -func (dk *DockerServer) StopContainer(_ context.Context, req *connect.Request[dkrpc.StopContainerRequest]) (*connect.Response[dkrpc.StopContainerResponse], error) { - res := connect.NewResponse(&dkrpc.StopContainerResponse{}) - return res, nil -} - -func (dk *DockerServer) GetContainerLogs(_ context.Context, req *connect.Request[dkrpc.GetContainerLogRequest], responseStream *connect.ServerStream[dkrpc.GetContainerLogResponse]) error { - return nil -} - -func (dk *DockerServer) CreateNewImage(_ context.Context, req *connect.Request[dkrpc.NewImageRequest]) (*connect.Response[dkrpc.NewImageResponse], error) { - res := connect.NewResponse(&dkrpc.NewImageResponse{}) - return res, nil -} -func (dk *DockerServer) ListImages(_ context.Context, _ *connect.Request[dkrpc.ListImageRequest]) (*connect.Response[dkrpc.ListImageResponse], error) { - res := connect.NewResponse(&dkrpc.ListImageResponse{}) - return res, nil -} - -func (dk *DockerServer) ListContainers(_ context.Context, _ *connect.Request[dkrpc.ListContainersRequest]) (*connect.Response[dkrpc.ListContainersResponse], error) { - res := connect.NewResponse(&dkrpc.ListContainersResponse{}) - return res, nil -} diff --git a/src/cmd/api.go b/src/cmd/api.go new file mode 100644 index 0000000..a222908 --- /dev/null +++ b/src/cmd/api.go @@ -0,0 +1,82 @@ +package cmd + +import ( + "connectrpc.com/connect" + "fmt" + dockerrpc "github.com/makeopensource/leviathan/generated/docker_rpc/v1/v1connect" + jobrpc "github.com/makeopensource/leviathan/generated/jobs/v1/v1connect" + labrpc "github.com/makeopensource/leviathan/generated/labs/v1/v1connect" + "github.com/makeopensource/leviathan/internal/config" + "github.com/makeopensource/leviathan/internal/docker" + fm "github.com/makeopensource/leviathan/internal/file_manager" + "github.com/makeopensource/leviathan/internal/jobs" + "github.com/makeopensource/leviathan/internal/labs" + "github.com/rs/zerolog/log" + "golang.org/x/net/http2" + "golang.org/x/net/http2/h2c" + "net/http" +) + +// StartServerWithAddr starts a server using the port specified in the config +func StartServerWithAddr() { + srvAddr := fmt.Sprintf(":%s", config.ServerPort.GetStr()) + StartServer(srvAddr) +} + +// StartServer starts the grpc server at "addr:port" +// +// e.g. StartServer(":8080") or StartServer("127.0.0.1:8080") +func StartServer(srvAddr string) { + mux := setupEndpoints() + + log.Info().Msg("Leviathan initialized successfully") + log.Info().Msgf("starting server on %s", srvAddr) + err := http.ListenAndServe( + srvAddr, + // Use h2c so we can serve HTTP/2 without TLS. + h2c.NewHandler(mux, &http2.Server{}), + ) + if err != nil { + log.Fatal().Err(err).Msgf("Failed to start server on %s", srvAddr) + return + } +} + +func setupEndpoints() *http.ServeMux { + dk, job, lab := InitServices() + + interceptor := connect.WithInterceptors() + if config.ApiKey.GetStr() != "" { + log.Info().Msg("ApiKey is set, endpoints now require authentication") + interceptor = connect.WithInterceptors(&authInterceptor{config.ApiKey.GetStr()}) + } + + v1Endpoints := []func() (string, http.Handler){ + // jobs endpoints + func() (string, http.Handler) { + jobSrv := jobs.NewJobServer(job) + return jobrpc.NewJobServiceHandler(jobSrv, interceptor) + }, + // docker endpoints + func() (string, http.Handler) { + dkSrv := &docker.Server{Service: dk} + return dockerrpc.NewDockerServiceHandler(dkSrv, interceptor) + }, + func() (string, http.Handler) { + labSrv := labs.LabServer{Srv: lab} + return labrpc.NewLabServiceHandler(labSrv, interceptor) + }, + func() (string, http.Handler) { + fileHandler := fm.NewFileManagerHandler("/files.v1") + return fileHandler.BasePath + "/", fileHandler + }, + } + + mux := http.NewServeMux() + for _, svc := range v1Endpoints { + path, handler := svc() + mux.Handle(path, handler) + } + + return mux +} diff --git a/src/api/auth_middleware.go b/src/cmd/auth_middleware.go similarity index 99% rename from src/api/auth_middleware.go rename to src/cmd/auth_middleware.go index 090fc57..b1f4e35 100644 --- a/src/api/auth_middleware.go +++ b/src/cmd/auth_middleware.go @@ -1,4 +1,4 @@ -package api +package cmd import ( "connectrpc.com/connect" diff --git a/src/cmd/server/main.go b/src/cmd/server/main.go new file mode 100644 index 0000000..f99e629 --- /dev/null +++ b/src/cmd/server/main.go @@ -0,0 +1,12 @@ +package main + +import ( + "github.com/makeopensource/leviathan/cmd" + "github.com/makeopensource/leviathan/internal/info" +) + +func main() { + info.PrintInfo() + cmd.Setup() + cmd.StartServerWithAddr() +} diff --git a/src/cmd/setup.go b/src/cmd/setup.go new file mode 100644 index 0000000..bfd5fbe --- /dev/null +++ b/src/cmd/setup.go @@ -0,0 +1,31 @@ +package cmd + +import ( + "github.com/makeopensource/leviathan/internal/config" + "github.com/makeopensource/leviathan/internal/database" + "github.com/makeopensource/leviathan/internal/docker" + fu "github.com/makeopensource/leviathan/internal/file_manager" + "github.com/makeopensource/leviathan/internal/jobs" + "github.com/makeopensource/leviathan/internal/labs" + "github.com/makeopensource/leviathan/pkg/logger" + "github.com/rs/zerolog/log" +) + +func Setup() { + log.Logger = logger.ConsoleLogger() // logs here are not saved to the log file + config.LoadConfig() + // once the log dir and level is set by config, + // we start a file logger along with the console logger + log.Logger = logger.FileConsoleLogger(config.LogDir.GetStr(), config.LogLevel.GetStr()) +} + +func InitServices() (*docker.DkService, *jobs.JobService, *labs.LabService) { + db, bc := database.NewDatabaseWithGorm() + + dkService := docker.NewDockerServiceWithClients() + fileManService := fu.NewFileManagerService() + labService := labs.NewLabService(db, dkService, fileManService) + jobService := jobs.NewJobService(db, bc, dkService, labService, fileManService) + + return dkService, jobService, labService +} diff --git a/src/go.mod b/src/go.mod index a4c18cd..f8dfa30 100644 --- a/src/go.mod +++ b/src/go.mod @@ -4,22 +4,22 @@ go 1.24 require ( connectrpc.com/connect v1.18.1 - github.com/docker/cli v28.0.4+incompatible - github.com/docker/docker v28.0.4+incompatible + github.com/docker/cli v28.2.1+incompatible + github.com/docker/docker v28.2.1+incompatible github.com/google/uuid v1.6.0 github.com/joho/godotenv v1.5.1 github.com/opencontainers/image-spec v1.1.1 github.com/rs/zerolog v1.34.0 github.com/spf13/viper v1.20.1 github.com/stretchr/testify v1.10.0 - golang.org/x/crypto v0.36.0 - golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 - golang.org/x/net v0.38.0 + golang.org/x/crypto v0.38.0 + golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 + golang.org/x/net v0.40.0 google.golang.org/protobuf v1.36.6 gopkg.in/natefinch/lumberjack.v2 v2.2.1 - gorm.io/driver/postgres v1.5.11 + gorm.io/driver/postgres v1.6.0 gorm.io/driver/sqlite v1.5.7 - gorm.io/gorm v1.25.12 + gorm.io/gorm v1.30.0 ) require ( @@ -31,32 +31,32 @@ require ( github.com/docker/go-connections v0.5.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/fsnotify/fsnotify v1.8.0 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-viper/mapstructure/v2 v2.2.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect - github.com/jackc/pgx/v5 v5.7.4 // indirect + github.com/jackc/pgx/v5 v5.7.5 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-sqlite3 v1.14.24 // indirect + github.com/mattn/go-sqlite3 v1.14.28 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/term v0.5.0 // indirect github.com/morikuni/aec v1.0.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect - github.com/pelletier/go-toml/v2 v2.2.3 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/sagikazarmark/locafero v0.9.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.14.0 // indirect - github.com/spf13/cast v1.7.1 // indirect + github.com/spf13/cast v1.8.0 // indirect github.com/spf13/pflag v1.0.6 // indirect github.com/subosito/gotenv v1.6.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect @@ -66,9 +66,9 @@ require ( go.opentelemetry.io/otel/metric v1.35.0 // indirect go.opentelemetry.io/otel/trace v1.35.0 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/sync v0.12.0 // indirect - golang.org/x/sys v0.31.0 // indirect - golang.org/x/text v0.23.0 // indirect + golang.org/x/sync v0.14.0 // indirect + golang.org/x/sys v0.33.0 // indirect + golang.org/x/text v0.25.0 // indirect golang.org/x/time v0.11.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect gotest.tools/v3 v3.5.1 // indirect diff --git a/src/go.sum b/src/go.sum index 59bc72b..24d7bbf 100644 --- a/src/go.sum +++ b/src/go.sum @@ -33,10 +33,18 @@ github.com/docker/cli v28.0.2+incompatible h1:cRPZ77FK3/IXTAIQQj1vmhlxiLS5m+MIUD github.com/docker/cli v28.0.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/cli v28.0.4+incompatible h1:pBJSJeNd9QeIWPjRcV91RVJihd/TXB77q1ef64XEu4A= github.com/docker/cli v28.0.4+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/cli v28.1.1+incompatible h1:eyUemzeI45DY7eDPuwUcmDyDj1pM98oD5MdSpiItp8k= +github.com/docker/cli v28.1.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/cli v28.2.1+incompatible h1:AYyTcuwvhl9dXdyCiXlOGXiIqSNYzTmaDNpxIISPGsM= +github.com/docker/cli v28.2.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/docker v28.0.2+incompatible h1:9BILleFwug5FSSqWBgVevgL3ewDJfWWWyZVqlDMttE8= github.com/docker/docker v28.0.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker v28.0.4+incompatible h1:JNNkBctYKurkw6FrHfKqY0nKIDf5nrbxjVBtS+cdcok= github.com/docker/docker v28.0.4+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v28.1.1+incompatible h1:49M11BFLsVO1gxY9UX9p/zwkE/rswggs8AdFmXQw51I= +github.com/docker/docker v28.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v28.2.1+incompatible h1:aTSWVTDStpHbnRu0xBcGoJEjRf5EQKt6nik6Vif8sWw= +github.com/docker/docker v28.2.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= @@ -53,6 +61,8 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.1/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -105,6 +115,8 @@ github.com/jackc/pgx/v5 v5.7.3 h1:PO1wNKj/bTAwxSJnO1Z4Ai8j4magtqg2SLNjEDzcXQo= github.com/jackc/pgx/v5 v5.7.3/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ= github.com/jackc/pgx/v5 v5.7.4 h1:9wKznZrhWa2QiHL+NjTSPP6yjl3451BX3imWDnokYlg= github.com/jackc/pgx/v5 v5.7.4/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ= +github.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs= +github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= @@ -128,6 +140,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A= +github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= @@ -140,6 +154,8 @@ github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJw github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -164,6 +180,8 @@ github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA= github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo= github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cast v1.8.0 h1:gEN9K4b8Xws4EX0+a0reLmhq8moKn7ntRlQYgjPeCDk= +github.com/spf13/cast v1.8.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.20.0 h1:zrxIyR3RQIOsarIrgL8+sAvALXul9jeEPa06Y0Ph6vY= @@ -213,9 +231,17 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= +golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= +golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= +golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw= golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM= +golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM= +golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8= +golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI= +golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= @@ -235,6 +261,10 @@ golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= +golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= +golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= +golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -245,6 +275,10 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= +golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= +golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -257,12 +291,21 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= +golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= +golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= +golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= +golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= +golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -318,10 +361,16 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gorm.io/driver/postgres v1.5.11 h1:ubBVAfbKEUld/twyKZ0IYn9rSQh448EdelLYk9Mv314= gorm.io/driver/postgres v1.5.11/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI= +gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4= +gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo= gorm.io/driver/sqlite v1.5.7 h1:8NvsrhP0ifM7LX9G4zPB97NwovUakUxc+2V2uuf3Z1I= gorm.io/driver/sqlite v1.5.7/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4= gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= +gorm.io/gorm v1.26.0 h1:9lqQVPG5aNNS6AyHdRiwScAVnXHg/L/Srzx55G5fOgs= +gorm.io/gorm v1.26.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= +gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs= +gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/src/common/config.go b/src/internal/config/config.go similarity index 91% rename from src/common/config.go rename to src/internal/config/config.go index b6145ee..ffc0e41 100644 --- a/src/common/config.go +++ b/src/internal/config/config.go @@ -1,10 +1,9 @@ -package common +package config import ( "errors" "fmt" "github.com/joho/godotenv" - "github.com/makeopensource/leviathan/models" "github.com/rs/zerolog/log" "github.com/spf13/viper" "os" @@ -14,7 +13,7 @@ import ( const DefaultFilePerm = 0o775 -func InitConfig() { +func LoadConfig() { _, ok := os.LookupEnv("LEVIATHAN_IS_DOCKER") if !ok { err := godotenv.Load() // load .env file for non docker env @@ -23,10 +22,6 @@ func InitConfig() { } } - defer func() { - log.Logger = FileConsoleLogger() - }() - baseDir, err := getBaseDir() if err != nil { log.Fatal().Err(err).Msg("unable to get base dir") @@ -176,17 +171,9 @@ func setupDefaultOptions(configDir string) { viper.SetDefault(serverPortKey, "9221") viper.SetDefault(enableLocalDockerKey, true) viper.SetDefault(concurrentJobsKey, 50) - viper.SetDefault(clientSSHKey, map[string]models.MachineOptions{ - "example": { - Enable: false, - Host: "192.168.1.69", - Port: 22, - User: "test", - Password: "", - RemotePublickey: "", - UsePublicKeyAuth: false, - }, - }) + //viper.SetDefault(clientSSHKey, map[string]docker.MachineOptions{ + // "example": DefaultMachine, + //}) } func getBaseDir() (string, error) { diff --git a/src/common/config_keys.go b/src/internal/config/config_keys.go similarity index 89% rename from src/common/config_keys.go rename to src/internal/config/config_keys.go index 14a7fe2..5afe4b7 100644 --- a/src/common/config_keys.go +++ b/src/internal/config/config_keys.go @@ -1,4 +1,4 @@ -package common +package config import ( "github.com/spf13/viper" @@ -58,12 +58,12 @@ var ( // postgres EnablePostgres = Config{enablePostgresKey} - postgresHost = Config{postgresHostKey} - postgresPort = Config{postgresPortKey} - postgresUser = Config{postgresUserKey} - postgresPass = Config{postgresPassKey} - postgresDB = Config{postgresDBKey} - postgresSsl = Config{postgresSslKey} + PostgresHost = Config{postgresHostKey} + PostgresPort = Config{postgresPortKey} + PostgresUser = Config{postgresUserKey} + PostgresPass = Config{postgresPassKey} + PostgresDB = Config{postgresDBKey} + PostgresSsl = Config{postgresSslKey} // sqlite SqliteDbPath = Config{sqliteDbPathKey} diff --git a/src/common/database.go b/src/internal/database/connect.go similarity index 64% rename from src/common/database.go rename to src/internal/database/connect.go index d8ea5e9..5a52cb4 100644 --- a/src/common/database.go +++ b/src/internal/database/connect.go @@ -1,8 +1,10 @@ -package common +package database import ( "fmt" - "github.com/makeopensource/leviathan/models" + "github.com/makeopensource/leviathan/internal/config" + "github.com/makeopensource/leviathan/internal/jobs" + "github.com/makeopensource/leviathan/internal/labs" "github.com/rs/zerolog/log" "gorm.io/driver/postgres" "gorm.io/driver/sqlite" @@ -11,22 +13,22 @@ import ( "time" ) -func InitDB() (*gorm.DB, *models.BroadcastChannel) { +func initDB() (*gorm.DB, *jobs.BroadcastChannel) { var connection gorm.Dialector - var config *gorm.Config + var dbConfig *gorm.Config - if EnablePostgres.GetBool() { - connection, config = usePostgres() + if config.EnablePostgres.GetBool() { + connection, dbConfig = usePostgres() } else { - connection, config = useSqlite() + connection, dbConfig = useSqlite() } - db, err := gorm.Open(connection, config) + db, err := gorm.Open(connection, dbConfig) if err != nil { log.Fatal().Err(err).Msg("failed to connect to database") } - if EnablePostgres.GetBool() { + if config.EnablePostgres.GetBool() { sqlDB, err := db.DB() if err != nil { log.Fatal().Err(err).Msg("failed to connect to database") @@ -36,25 +38,25 @@ func InitDB() (*gorm.DB, *models.BroadcastChannel) { sqlDB.SetConnMaxLifetime(time.Hour) // SetConnMaxLifetime sets the maximum amount of time a connection may be reused. } - err = db.AutoMigrate(&models.Lab{}, &models.Job{}) + err = db.AutoMigrate(&labs.Lab{}, &jobs.Job{}) if err != nil { log.Fatal().Err(err).Msgf("failed to migrate database") } - bc, ctx := models.NewBroadcastChannel() + bc, ctx := jobs.NewBroadcastChannel() db = db.WithContext(ctx) // inject broadcast channel to database return db, bc } func useSqlite() (gorm.Dialector, *gorm.Config) { - dbPath := SqliteDbPath.GetStr() + dbPath := config.SqliteDbPath.GetStr() if dbPath == "" { log.Fatal().Msgf("db_path is empty") } connectionStr := sqlite.Open(dbPath + "?_journal_mode=WAL&_busy_timeout=5000") - config := &gorm.Config{ + dbConfig := &gorm.Config{ PrepareStmt: true, } @@ -66,24 +68,24 @@ func useSqlite() (gorm.Dialector, *gorm.Config) { log.Info().Msgf("using sqlite at: %s", abs) } - return connectionStr, config + return connectionStr, dbConfig } func usePostgres() (gorm.Dialector, *gorm.Config) { - host := postgresHost.GetStr() - user := postgresUser.GetStr() - password := postgresPass.GetStr() - database := postgresDB.GetStr() - port := postgresPort.GetStr() - sslmode := postgresSsl.GetStr() - - dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%s sslmode=%s", + host := config.PostgresHost.GetStr() + user := config.PostgresUser.GetStr() + password := config.PostgresPass.GetStr() + database := config.PostgresDB.GetStr() + port := config.PostgresPort.GetStr() + sslMode := config.PostgresSsl.GetStr() + + dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%s sslMode=%s", host, user, password, database, port, - sslmode, + sslMode, ) log.Info().Msgf("using postgres at: %s", dsn) diff --git a/src/internal/database/impl_jobs.go b/src/internal/database/impl_jobs.go new file mode 100644 index 0000000..fcefbef --- /dev/null +++ b/src/internal/database/impl_jobs.go @@ -0,0 +1,49 @@ +package database + +import ( + "github.com/makeopensource/leviathan/internal/jobs" + "gorm.io/gorm" +) + +// JobDatabase implements jobs.JobStore interface +type JobDatabase struct { + db *gorm.DB +} + +func (j *JobDatabase) CreateJob(job *jobs.Job) error { + res := j.db.Create(job) + if res.Error != nil { + return res.Error + } + return nil +} + +func (j *JobDatabase) GetJobByUuid(jobUuid string) (*jobs.Job, error) { + var job jobs.Job + res := j.db.First(&job, "job_id = ?", jobUuid) + if res.Error != nil { + return nil, res.Error + } + return &job, nil +} + +func (j *JobDatabase) UpdateJob(job *jobs.Job) error { + res := j.db.Save(job) + if res.Error != nil { + return res.Error + } + return nil +} + +func (j *JobDatabase) FetchInProgressJobs(result *[]jobs.Job) error { + res := j.db.Preload("LabData"). + Where("status = ?", string(jobs.Queued)). + Or("status = ?", string(jobs.Running)). + Or("status = ?", string(jobs.Preparing)). + Find(result) + + if res.Error != nil { + return res.Error + } + return nil +} diff --git a/src/internal/database/impl_labs.go b/src/internal/database/impl_labs.go new file mode 100644 index 0000000..eb7347d --- /dev/null +++ b/src/internal/database/impl_labs.go @@ -0,0 +1,36 @@ +package database + +import ( + "github.com/makeopensource/leviathan/internal/labs" + "gorm.io/gorm" +) + +type LabDatabase struct { + db *gorm.DB +} + +func (l *LabDatabase) CreateLab(lab *labs.Lab) error { + res := l.db.Save(lab) + if res.Error != nil { + return res.Error + } + + return nil +} + +func (l *LabDatabase) DeleteLab(id uint) error { + if res := l.db.Delete(&labs.Lab{}, id); res.Error != nil { + return res.Error + } + + return nil +} + +func (l *LabDatabase) GetLab(id uint) (*labs.Lab, error) { + var lab labs.Lab + if res := l.db.Where("ID = ?", id).First(&lab); res.Error != nil { + return nil, res.Error + } + + return &lab, nil +} diff --git a/src/internal/database/service.go b/src/internal/database/service.go new file mode 100644 index 0000000..6926d9b --- /dev/null +++ b/src/internal/database/service.go @@ -0,0 +1,26 @@ +package database + +import ( + "github.com/makeopensource/leviathan/internal/jobs" + "gorm.io/gorm" +) + +type Service struct { + *gorm.DB + *JobDatabase + *LabDatabase +} + +// NewDatabaseWithGorm calls initDB implicitly +func NewDatabaseWithGorm() (*Service, *jobs.BroadcastChannel) { + db, bc := initDB() + return NewDatabase(db), bc +} + +func NewDatabase(db *gorm.DB) *Service { + return &Service{ + DB: db, + JobDatabase: &JobDatabase{db: db}, + LabDatabase: &LabDatabase{db: db}, + } +} diff --git a/src/service/docker/docker_client.go b/src/internal/docker/docker_client.go similarity index 81% rename from src/service/docker/docker_client.go rename to src/internal/docker/docker_client.go index 382d8e2..b0cd8e5 100644 --- a/src/service/docker/docker_client.go +++ b/src/internal/docker/docker_client.go @@ -5,12 +5,13 @@ import ( "fmt" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" + dk "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/image" "github.com/docker/docker/api/types/network" "github.com/docker/docker/client" dktypes "github.com/makeopensource/leviathan/generated/docker_rpc/v1" - "github.com/makeopensource/leviathan/models" + su "github.com/makeopensource/leviathan/pkg/sync_utils" v1 "github.com/opencontainers/image-spec/specs-go/v1" "github.com/rs/zerolog/log" "io" @@ -18,17 +19,19 @@ import ( "time" ) -type ImageMap = models.Map[string, *models.CountingMutex] +type ImageMap = su.Map[string, *su.CountingMutex] + +const JobIdLabel = "jobIdLabel" // DkClient a wrapper for the docker client struct, that exposes the commands leviathan needs type DkClient struct { - Client *client.Client + client *client.Client imgMap *ImageMap } func NewDkClient(client *client.Client) *DkClient { cli := &DkClient{ - Client: client, + client: client, imgMap: &ImageMap{}, } go cleanupImageTagLocks(cli.imgMap) @@ -38,7 +41,7 @@ func NewDkClient(client *client.Client) *DkClient { // BuildImageFromDockerfile Build image func (c *DkClient) BuildImageFromDockerfile(dockerfilePath string, tagName string) error { // prevent concurrent duplicate image builds - tagLock := c.imgMap.LoadOrStore(tagName, models.NewCountMutex()) + tagLock := c.imgMap.LoadOrStore(tagName, su.NewCountMutex()) tagLock.Lock() defer tagLock.Unlock() @@ -47,7 +50,7 @@ func (c *DkClient) BuildImageFromDockerfile(dockerfilePath string, tagName strin return fmt.Errorf("failed to tar file %s", dockerfilePath) } // Build the Docker image - resp, err := c.Client.ImageBuild( + resp, err := c.client.ImageBuild( context.Background(), dockerfileTar, types.ImageBuildOptions{ @@ -83,7 +86,7 @@ func (c *DkClient) BuildImageFromDockerfile(dockerfilePath string, tagName strin // ListImages lists all images on the docker web_gen func (c *DkClient) ListImages() ([]*dktypes.ImageMetaData, error) { - imageInfos, err := c.Client.ImageList(context.Background(), image.ListOptions{All: true}) + imageInfos, err := c.client.ImageList(context.Background(), image.ListOptions{All: true}) if err != nil { log.Error().Err(err).Msgf("failed to list Docker images") return nil, err @@ -103,9 +106,13 @@ func (c *DkClient) ListImages() ([]*dktypes.ImageMetaData, error) { return imageInfoList, nil } +func (c *DkClient) WaitForContainerStatusChange(contId string) (<-chan dk.WaitResponse, <-chan error) { + return c.client.ContainerWait(context.Background(), contId, dk.WaitConditionNotRunning) +} + // ListContainers lists containers -func (c *DkClient) ListContainers(machineId string) ([]*dktypes.ContainerMetaData, error) { - containerInfos, err := c.Client.ContainerList(context.Background(), container.ListOptions{All: true}) +func (c *DkClient) ListContainers() ([]*dktypes.ContainerMetaData, error) { + containerInfos, err := c.client.ContainerList(context.Background(), container.ListOptions{All: true}) if err != nil { log.Error().Err(err).Msgf("failed to list Docker images") return nil, err @@ -118,13 +125,12 @@ func (c *DkClient) ListContainers(machineId string) ([]*dktypes.ContainerMetaDat } // CreateNewContainer creates a new container from given image -func (c *DkClient) CreateNewContainer(jobUuid, image, jobFolder, entryCmd string, machineLimits container.Resources) (string, error) { +func (c *DkClient) CreateNewContainer(jobUuid, image, entryCmd string, machineLimits dk.Resources) (string, error) { + config := &container.Config{ - Image: image, - Labels: map[string]string{ - "con": jobUuid, - }, - Cmd: []string{"sh", "-c", entryCmd}, + Image: image, + Labels: map[string]string{JobIdLabel: jobUuid}, + Cmd: []string{"sh", "-c", entryCmd}, } hostConfig := &container.HostConfig{ @@ -138,7 +144,7 @@ func (c *DkClient) CreateNewContainer(jobUuid, image, jobFolder, entryCmd string var platform *v1.Platform = nil - cont, err := c.Client.ContainerCreate( + cont, err := c.client.ContainerCreate( context.Background(), config, hostConfig, @@ -160,7 +166,7 @@ func (c *DkClient) CreateNewContainer(jobUuid, image, jobFolder, entryCmd string // StartContainer starts the container of a given ID func (c *DkClient) StartContainer(containerID string) error { - err := c.Client.ContainerStart(context.Background(), containerID, container.StartOptions{}) + err := c.client.ContainerStart(context.Background(), containerID, container.StartOptions{}) if err != nil { log.Error().Err(err).Msgf("failed to start Docker container") return err @@ -170,7 +176,7 @@ func (c *DkClient) StartContainer(containerID string) error { // StopContainer stops the container of a given ID func (c *DkClient) StopContainer(containerID string) error { - err := c.Client.ContainerStop(context.Background(), containerID, container.StopOptions{}) + err := c.client.ContainerStop(context.Background(), containerID, container.StopOptions{}) if err != nil { log.Error().Err(err).Msgf("failed to stop Docker container") return err @@ -180,7 +186,7 @@ func (c *DkClient) StopContainer(containerID string) error { // RemoveContainer deletes the container of a given ID func (c *DkClient) RemoveContainer(containerID string, force bool, removeVolumes bool) error { - err := c.Client.ContainerRemove( + err := c.client.ContainerRemove( context.Background(), containerID, container.RemoveOptions{ Force: force, RemoveVolumes: removeVolumes, @@ -206,7 +212,7 @@ func (c *DkClient) CopyToContainer(containerID string, submissionDirPath string) // create the directory under its parent parent := filepath.Dir("/home/") - err = c.Client.CopyToContainer(context.Background(), containerID, parent, jobBytes, container.CopyToContainerOptions{}) + err = c.client.CopyToContainer(context.Background(), containerID, parent, jobBytes, container.CopyToContainerOptions{}) if err != nil { log.Error().Err(err).Msgf("failed to copy to container") return fmt.Errorf("failed to copy submission to container") @@ -216,7 +222,7 @@ func (c *DkClient) CopyToContainer(containerID string, submissionDirPath string) } func (c *DkClient) TailContainerLogs(ctx context.Context, containerID string) (io.ReadCloser, error) { - reader, err := c.Client.ContainerLogs(ctx, containerID, container.LogsOptions{ + reader, err := c.client.ContainerLogs(ctx, containerID, container.LogsOptions{ ShowStdout: true, ShowStderr: true, Follow: true, @@ -234,7 +240,7 @@ func (c *DkClient) TailContainerLogs(ctx context.Context, containerID string) (i // PruneContainers clears all containers that are not running func (c *DkClient) PruneContainers() error { - report, err := c.Client.ContainersPrune(context.Background(), filters.Args{}) + report, err := c.client.ContainersPrune(context.Background(), filters.Args{}) if err != nil { log.Error().Err(err).Msgf("failed to prune Docker container") return err @@ -245,7 +251,7 @@ func (c *DkClient) PruneContainers() error { } func (c *DkClient) GetContainerStatus(ctx context.Context, contId string) (*container.InspectResponse, error) { - inspect, err := c.Client.ContainerInspect(ctx, contId) + inspect, err := c.client.ContainerInspect(ctx, contId) if err != nil { return nil, err } @@ -259,7 +265,7 @@ func cleanupImageTagLocks(cli *ImageMap) { defer ticker.Stop() for { <-ticker.C - cli.Range(func(key string, value *models.CountingMutex) bool { + cli.Range(func(key string, value *su.CountingMutex) bool { checkImageLockStatus(key, value, cli) // always return true to continue iterating return true @@ -267,7 +273,7 @@ func cleanupImageTagLocks(cli *ImageMap) { } } -func checkImageLockStatus(tagName string, mut *models.CountingMutex, imageMap *ImageMap) { +func checkImageLockStatus(tagName string, mut *su.CountingMutex, imageMap *ImageMap) { c := mut.WaitingCount() if c == 0 { log.Debug().Msgf("removing unused tag: %s from image queue", tagName) diff --git a/src/internal/docker/docker_handler.go b/src/internal/docker/docker_handler.go new file mode 100644 index 0000000..28605b3 --- /dev/null +++ b/src/internal/docker/docker_handler.go @@ -0,0 +1,49 @@ +package docker + +import ( + "connectrpc.com/connect" + "context" + dkrpc "github.com/makeopensource/leviathan/generated/docker_rpc/v1" +) + +type Server struct { + Service *DkService +} + +func (dk *Server) CreateContainer(_ context.Context, req *connect.Request[dkrpc.CreateContainerRequest]) (*connect.Response[dkrpc.CreateContainerResponse], error) { + res := connect.NewResponse(&dkrpc.CreateContainerResponse{}) + return res, nil +} + +func (dk *Server) StartContainer(_ context.Context, req *connect.Request[dkrpc.StartContainerRequest]) (*connect.Response[dkrpc.StartContainerResponse], error) { + res := connect.NewResponse(&dkrpc.StartContainerResponse{}) + return res, nil +} + +func (dk *Server) DeleteContainer(_ context.Context, req *connect.Request[dkrpc.DeleteContainerRequest]) (*connect.Response[dkrpc.DeleteContainerResponse], error) { + res := connect.NewResponse(&dkrpc.DeleteContainerResponse{}) + return res, nil +} + +func (dk *Server) StopContainer(_ context.Context, req *connect.Request[dkrpc.StopContainerRequest]) (*connect.Response[dkrpc.StopContainerResponse], error) { + res := connect.NewResponse(&dkrpc.StopContainerResponse{}) + return res, nil +} + +func (dk *Server) GetContainerLogs(_ context.Context, req *connect.Request[dkrpc.GetContainerLogRequest], responseStream *connect.ServerStream[dkrpc.GetContainerLogResponse]) error { + return nil +} + +func (dk *Server) CreateNewImage(_ context.Context, req *connect.Request[dkrpc.NewImageRequest]) (*connect.Response[dkrpc.NewImageResponse], error) { + res := connect.NewResponse(&dkrpc.NewImageResponse{}) + return res, nil +} +func (dk *Server) ListImages(_ context.Context, _ *connect.Request[dkrpc.ListImageRequest]) (*connect.Response[dkrpc.ListImageResponse], error) { + res := connect.NewResponse(&dkrpc.ListImageResponse{}) + return res, nil +} + +func (dk *Server) ListContainers(_ context.Context, _ *connect.Request[dkrpc.ListContainersRequest]) (*connect.Response[dkrpc.ListContainersResponse], error) { + res := connect.NewResponse(&dkrpc.ListContainersResponse{}) + return res, nil +} diff --git a/src/service/docker/docker_manager.go b/src/internal/docker/docker_manager.go similarity index 88% rename from src/service/docker/docker_manager.go rename to src/internal/docker/docker_manager.go index 45c7402..c08fbac 100644 --- a/src/service/docker/docker_manager.go +++ b/src/internal/docker/docker_manager.go @@ -6,8 +6,7 @@ import ( "github.com/docker/cli/cli/connhelper" "github.com/docker/docker/api/types/system" "github.com/docker/docker/client" - "github.com/makeopensource/leviathan/common" - "github.com/makeopensource/leviathan/models" + "github.com/makeopensource/leviathan/internal/config" "github.com/rs/zerolog/log" "github.com/spf13/viper" "golang.org/x/crypto/ssh" @@ -50,7 +49,7 @@ func NewRemoteClientManager() *RemoteClientManager { continue } - info, err := testClientConn(remoteClient.Client) + info, err := testClientConn(remoteClient.client) if err != nil { log.Warn().Err(err).Msgf("Remote docker client failed to connect: %s", machine.Name()) continue @@ -62,13 +61,13 @@ func NewRemoteClientManager() *RemoteClientManager { } } - if common.EnableLocalDocker.GetBool() { + if config.EnableLocalDocker.GetBool() { localClient, err := NewLocalClient() if err != nil { log.Error().Err(err).Msg("Failed to setup local docker client") } - info, err := testClientConn(localClient.Client) + info, err := testClientConn(localClient.client) if err != nil { log.Warn().Err(err).Msgf("Client failed to connect: localdocker") } else { @@ -98,7 +97,7 @@ func NewRemoteClientManager() *RemoteClientManager { // // This function assumes the user has already configured SSH access to the remote host. // It does not handle key generation or SSH configuration. -func NewHostSSHClient(machine models.MachineOptions) (*DkClient, error) { +func NewHostSSHClient(machine MachineOptions) (*DkClient, error) { connectionString := fmt.Sprintf("%s@%s:%d", machine.User, machine.Host, machine.Port) helper, err := connhelper.GetConnectionHelper(fmt.Sprintf("ssh://%s", connectionString)) if err != nil { @@ -134,7 +133,7 @@ func NewHostSSHClient(machine models.MachineOptions) (*DkClient, error) { // // This function assumes the user has already transferred the public key generated by initKeyPairFile // and configured SSH access to the remote host. -func NewSSHClientWithPublicKeyAuth(machine models.MachineOptions) (*DkClient, error) { +func NewSSHClientWithPublicKeyAuth(machine MachineOptions) (*DkClient, error) { privateKey, err := LoadPrivateKey() if err != nil { return nil, fmt.Errorf("failed to load private key: %s", err.Error()) @@ -150,19 +149,19 @@ func NewSSHClientWithPublicKeyAuth(machine models.MachineOptions) (*DkClient, er // NewSSHClientWithPasswordAuth connects to a remote docker host using a password. // -// It is assumed machine models.MachineOptions has the correct password set. -func NewSSHClientWithPasswordAuth(machine models.MachineOptions) (*DkClient, error) { +// It is assumed machine config.MachineOptions has the correct password set. +func NewSSHClientWithPasswordAuth(machine MachineOptions) (*DkClient, error) { auth := ssh.Password(machine.Password) return createSshDockerConnection(machine, auth) } // createSshDockerConnection establishes an SSH connection to a Docker host based on the provided authentication method. // -// If models.MachineOptions contains an empty public key, the key is saved on connect; +// If MachineOptions contains an empty public key, the key is saved on connect; // otherwise, the provided key is verified. -func createSshDockerConnection(machine models.MachineOptions, auth ...ssh.AuthMethod) (*DkClient, error) { +func createSshDockerConnection(machine MachineOptions, auth ...ssh.AuthMethod) (*DkClient, error) { sshHost := fmt.Sprintf("%s:%d", machine.Host, machine.Port) - config := &ssh.ClientConfig{ + conf := &ssh.ClientConfig{ User: machine.User, Auth: auth, HostKeyCallback: saveHostKey(machine), @@ -176,10 +175,10 @@ func createSshDockerConnection(machine models.MachineOptions, auth ...ssh.AuthMe return nil, err } - config.HostKeyCallback = ssh.FixedHostKey(pubkey) + conf.HostKeyCallback = ssh.FixedHostKey(pubkey) } - sshClient, err := ssh.Dial("tcp", sshHost, config) + sshClient, err := ssh.Dial("tcp", sshHost, conf) if err != nil { return nil, fmt.Errorf("failed to create ssh client: %v", err) } @@ -250,7 +249,6 @@ func (man *RemoteClientManager) GetClientById(id string) (*DkClient, error) { if !exists { return nil, fmt.Errorf("invalid machine id: %s", id) } - return status.Client, nil } @@ -283,11 +281,11 @@ func (man *RemoteClientManager) DecreaseJobCount(id string) { } // getClientList loads clients from config, if client has 'enable: false' it will be skipped -func getClientList() map[string]models.MachineOptions { - var machineMap = map[string]models.MachineOptions{} +func getClientList() map[string]MachineOptions { + var machineMap = map[string]MachineOptions{} // Get all settings - allSettings := common.ClientsSSH.GetAny() + allSettings := config.ClientsSSH.GetAny() clients, ok := allSettings.(map[string]interface{}) if !ok { log.Warn().Msg("clients.ssh not configured, ssh docker clients will not be used") @@ -295,7 +293,7 @@ func getClientList() map[string]models.MachineOptions { } for name := range clients { - var options models.MachineOptions + var options MachineOptions key := fmt.Sprintf("clients.ssh.%s", name) if err := viper.UnmarshalKey(key, &options); err != nil { diff --git a/src/service/docker/docker_manager_test.go b/src/internal/docker/docker_manager_test.go similarity index 97% rename from src/service/docker/docker_manager_test.go rename to src/internal/docker/docker_manager_test.go index 47c290f..3c3c73b 100644 --- a/src/service/docker/docker_manager_test.go +++ b/src/internal/docker/docker_manager_test.go @@ -2,7 +2,7 @@ package docker import ( "fmt" - "github.com/makeopensource/leviathan/common" + "github.com/makeopensource/leviathan/internal/config" "github.com/stretchr/testify/assert" "sync" "testing" @@ -98,7 +98,7 @@ func TestRemoteClientManager_GetLeastJobCountMachineId(t *testing.T) { } func TestNewSSHClientWithPasswordAuth(t *testing.T) { - common.InitConfig() + config.LoadConfig() initKeyPairFile() // when running this test update the config.yaml with the test machine info @@ -128,7 +128,7 @@ func TestNewSSHClientWithPasswordAuth(t *testing.T) { } func TestNewSSHClientWithPublicKeyAuth(t *testing.T) { - common.InitConfig() + config.LoadConfig() initKeyPairFile() // when running this test update the config.yaml with the test machine info diff --git a/src/internal/docker/docker_service.go b/src/internal/docker/docker_service.go new file mode 100644 index 0000000..007beb0 --- /dev/null +++ b/src/internal/docker/docker_service.go @@ -0,0 +1,37 @@ +package docker + +import ( + "github.com/rs/zerolog/log" + "sync" +) + +type DkService struct { + ClientManager *RemoteClientManager +} + +func NewDockerService(clientList *RemoteClientManager) *DkService { + return &DkService{ClientManager: clientList} +} + +func NewDockerServiceWithClients() *DkService { + return &DkService{ClientManager: NewRemoteClientManager()} +} + +func (dk *DkService) BuildNewImageOnAllClients(dockerfilePath string, imageTag string) { + var wg sync.WaitGroup + + for name, item := range dk.ClientManager.Clients { + wg.Add(1) + go func() { + defer wg.Done() + err := item.Client.BuildImageFromDockerfile(dockerfilePath, imageTag) + if err != nil { + log.Error().Err(err).Msgf("unable to build image: %s for %s", imageTag, name) + return + } + log.Debug().Msgf("image: %s built successfully for machine: %s", imageTag, name) + }() + } + + wg.Wait() +} diff --git a/src/service/docker/docker_service_test.go b/src/internal/docker/docker_service_test.go similarity index 82% rename from src/service/docker/docker_service_test.go rename to src/internal/docker/docker_service_test.go index 08c5989..f63d370 100644 --- a/src/service/docker/docker_service_test.go +++ b/src/internal/docker/docker_service_test.go @@ -4,7 +4,7 @@ import ( "fmt" "github.com/docker/docker/api/types/container" "github.com/google/uuid" - "github.com/makeopensource/leviathan/common" + "github.com/makeopensource/leviathan/internal/config" "sync" "testing" ) @@ -25,10 +25,7 @@ func TestConcurrentImageBuilds(t *testing.T) { for i := 0; i < numTimes; i++ { t.Run(fmt.Sprintf("image_%d", i), func(t *testing.T) { t.Parallel() - err := DkTestService.BuildNewImageOnAllClients(DockerFilePath, "test") - if err != nil { - t.Fatal(err) - } + DkTestService.BuildNewImageOnAllClients(DockerFilePath, "test") }) } } @@ -43,7 +40,7 @@ func TestCopyToContainer(t *testing.T) { ifg := uuid.New() - contId, err := machine.CreateNewContainer(ifg.String(), ImageName, "", "echo hello", container.Resources{}) + contId, err := machine.CreateNewContainer(ifg.String(), ImageName, "echo hello", container.Resources{}) if err != nil { t.Fatalf("%v", err) } @@ -56,7 +53,7 @@ func TestCopyToContainer(t *testing.T) { func setupTest() { setup.Do(func() { - common.InitConfig() + config.LoadConfig() initServices() }) } diff --git a/src/models/machine.go b/src/internal/docker/machine.go similarity index 87% rename from src/models/machine.go rename to src/internal/docker/machine.go index 9f5484c..cfedd48 100644 --- a/src/models/machine.go +++ b/src/internal/docker/machine.go @@ -1,4 +1,4 @@ -package models +package docker import ( "fmt" @@ -68,3 +68,15 @@ func (opts *MachineOptions) Name() string { func (opts *MachineOptions) SetName(name string) { opts.name = name } + +func DefaultMachine() *MachineOptions { + return &MachineOptions{ + Enable: false, + Host: "192.168.1.69", + Port: 22, + User: "test", + Password: "", + RemotePublickey: "", + UsePublicKeyAuth: false, + } +} diff --git a/src/service/docker/docker_utils_file.go b/src/internal/docker/utils_file.go similarity index 100% rename from src/service/docker/docker_utils_file.go rename to src/internal/docker/utils_file.go diff --git a/src/service/docker/docker_utils_ssh.go b/src/internal/docker/utils_ssh.go similarity index 88% rename from src/service/docker/docker_utils_ssh.go rename to src/internal/docker/utils_ssh.go index d585dbb..85f74a1 100644 --- a/src/service/docker/docker_utils_ssh.go +++ b/src/internal/docker/utils_ssh.go @@ -9,8 +9,8 @@ import ( "crypto/x509" "encoding/pem" "fmt" - com "github.com/makeopensource/leviathan/common" - "github.com/makeopensource/leviathan/models" + "github.com/makeopensource/leviathan/internal/config" + fu "github.com/makeopensource/leviathan/pkg/file_utils" "github.com/rs/zerolog/log" "github.com/spf13/viper" "golang.org/x/crypto/ssh" @@ -53,7 +53,7 @@ func sshDialer(sshClient *ssh.Client) func(ctx context.Context, network string, } } -func saveHostKey(machine models.MachineOptions) func(hostname string, remote net.Addr, key ssh.PublicKey) error { +func saveHostKey(machine MachineOptions) func(hostname string, remote net.Addr, key ssh.PublicKey) error { return func(hostname string, remote net.Addr, key ssh.PublicKey) error { log.Debug().Msgf("Empty public key for %s, public key will be saved on connect", machine.Name()) @@ -69,8 +69,8 @@ func saveHostKey(machine models.MachineOptions) func(hostname string, remote net } } -func writeMachineToConfigFile(machine models.MachineOptions) { - machineKey := fmt.Sprintf("%s.%s", com.ClientsSSH.ConfigKey, machine.Name()) +func writeMachineToConfigFile(machine MachineOptions) { + machineKey := fmt.Sprintf("%s.%s", config.ClientsSSH.ConfigKey, machine.Name()) viper.Set(machineKey, machine) err := viper.WriteConfig() if err != nil { @@ -109,7 +109,7 @@ func GenerateKeyPair() (privateKey []byte, publicKey []byte, err error) { // // the generated keys can be found in common.SSHConfigFolder func initKeyPairFile() { - basePath := com.SSHConfigFolder.GetStr() + basePath := config.SSHConfigFolder.GetStr() privateKeyPath := fmt.Sprintf("%s/%s", basePath, "id_rsa") publicKeyPath := fmt.Sprintf("%s/%s", basePath, "id_rsa.pub") @@ -120,7 +120,7 @@ func initKeyPairFile() { Str("private_key_file", privateKeyPath). Str("public_key_file", publicKeyPath) - if com.FileExists(privateKeyPath) && com.FileExists(publicKeyPath) { + if fu.FileExists(privateKeyPath) && fu.FileExists(publicKeyPath) { logF.Msg("found existing keys... skipping generation") return } @@ -143,7 +143,7 @@ func initKeyPairFile() { func LoadPrivateKey() ([]byte, error) { return os.ReadFile(fmt.Sprintf( "%s/%s", - com.SSHConfigFolder.GetStr(), + config.SSHConfigFolder.GetStr(), "id_rsa", )) } diff --git a/src/api/v1/file_manager_impl.go b/src/internal/file_manager/handler.go similarity index 80% rename from src/api/v1/file_manager_impl.go rename to src/internal/file_manager/handler.go index cd83561..c398d4c 100644 --- a/src/api/v1/file_manager_impl.go +++ b/src/internal/file_manager/handler.go @@ -1,9 +1,8 @@ -package v1 +package file_manager import ( "encoding/json" - com "github.com/makeopensource/leviathan/common" - fm "github.com/makeopensource/leviathan/service/file_manager" + "github.com/makeopensource/leviathan/pkg/logger" "github.com/rs/zerolog/log" "mime/multipart" "net/http" @@ -20,7 +19,7 @@ type FileManagerHandler struct { BasePath string UploadLabPath string UploadSubmissionPath string - service fm.FileManagerService + service FileManagerService } func NewFileManagerHandler(basePath string) *FileManagerHandler { @@ -28,7 +27,7 @@ func NewFileManagerHandler(basePath string) *FileManagerHandler { BasePath: basePath, UploadLabPath: basePath + "/upload/lab", UploadSubmissionPath: basePath + "/upload/submission", - service: fm.FileManagerService{}, + service: FileManagerService{}, } } @@ -57,12 +56,12 @@ func (f *FileManagerHandler) UploadLabData(w http.ResponseWriter, r *http.Reques if err != nil { http.Error( w, - com.ErrLog("Failed to get dockerfile in form", err, log.Error()).Error(), + logger.ErrLog("Failed to get dockerfile in form", err, log.Error()).Error(), http.StatusBadRequest, ) return } - defer com.CloseFile(dockerFile) + defer closeWithLog(dockerFile) jobFiles, ok := r.MultipartForm.File[LabFilesKey] if !ok || len(jobFiles) == 0 { @@ -75,9 +74,9 @@ func (f *FileManagerHandler) UploadLabData(w http.ResponseWriter, r *http.Reques http.Error(w, err.Error(), http.StatusBadRequest) return } - defer func(files []*fm.FileInfo) { + defer func(files []*FileInfo) { for _, file := range files { - com.CloseFile(file.Reader) + closeWithLog(file.Reader) } }(fileInfos) @@ -108,9 +107,9 @@ func (f *FileManagerHandler) UploadSubmissionData(w http.ResponseWriter, r *http http.Error(w, err.Error(), http.StatusBadRequest) return } - defer func(files []*fm.FileInfo) { + defer func(files []*FileInfo) { for _, file := range files { - com.CloseFile(file.Reader) + closeWithLog(file.Reader) } }(fileInfos) @@ -134,7 +133,7 @@ func sendResponse(w http.ResponseWriter, folderID string) { if err != nil { http.Error( w, - com.ErrLog("Failed to write response", err, log.Error()).Error(), + logger.ErrLog("Failed to write response", err, log.Error()).Error(), http.StatusInternalServerError, ) return @@ -147,19 +146,19 @@ func toJson(folderID string) ([]byte, error) { } jsonData, err := json.Marshal(resultMap) if err != nil { - return nil, com.ErrLog("Failed to marshal json", err, log.Error()) + return nil, logger.ErrLog("Failed to marshal json", err, log.Error()) } return jsonData, nil } -func mapToFileInfo(jobFiles []*multipart.FileHeader) ([]*fm.FileInfo, error) { - var fileInfos []*fm.FileInfo +func mapToFileInfo(jobFiles []*multipart.FileHeader) ([]*FileInfo, error) { + var fileInfos []*FileInfo for _, jobFile := range jobFiles { file, err := jobFile.Open() if err != nil { - return fileInfos, com.ErrLog("unable to open file: "+err.Error(), err, log.Error()) + return fileInfos, logger.ErrLog("unable to open file: "+err.Error(), err, log.Error()) } - fileInfos = append(fileInfos, &fm.FileInfo{ + fileInfos = append(fileInfos, &FileInfo{ Reader: file, Filename: jobFile.Filename, }) diff --git a/src/service/file_manager/file_manager_service.go b/src/internal/file_manager/service.go similarity index 79% rename from src/service/file_manager/file_manager_service.go rename to src/internal/file_manager/service.go index 5c5f55c..fbd830b 100644 --- a/src/service/file_manager/file_manager_service.go +++ b/src/internal/file_manager/service.go @@ -3,7 +3,9 @@ package file_manager import ( "fmt" "github.com/google/uuid" - com "github.com/makeopensource/leviathan/common" + "github.com/makeopensource/leviathan/internal/config" + fu "github.com/makeopensource/leviathan/pkg/file_utils" + "github.com/makeopensource/leviathan/pkg/logger" "github.com/rs/zerolog/log" "io" "os" @@ -36,7 +38,7 @@ func (f *FileManagerService) CreateTmpLabFolder(dockerfile io.Reader, jobFiles . jobDataDir := filepath.Join(basePath, JobDataFolderName) err = os.MkdirAll(jobDataDir, os.ModePerm) if err != nil { - return "", com.ErrLog("unable to create job data folder", err, log.Error()) + return "", logger.ErrLog("unable to create job data folder", err, log.Error()) } if err = f.SaveFile(basePath, DockerfileName, dockerfile); err != nil { @@ -71,14 +73,14 @@ func (f *FileManagerService) CreateSubmissionFolder(jobFiles ...*FileInfo) (stri func (f *FileManagerService) createBaseFolder() (string, string, error) { folderUUID, err := uuid.NewUUID() if err != nil { - return "", "", com.ErrLog("Unable to generate uuid", err, log.Error()) + return "", "", logger.ErrLog("Unable to generate uuid", err, log.Error()) } stringUuid := folderUUID.String() - basePath := filepath.Join(com.TmpUploadFolder.GetStr(), stringUuid) + basePath := filepath.Join(config.TmpUploadFolder.GetStr(), stringUuid) - err = os.Mkdir(basePath, com.DefaultFilePerm) + err = os.Mkdir(basePath, config.DefaultFilePerm) if err != nil { - return "", "", com.ErrLog("Unable to create tmp folder", err, log.Error()) + return "", "", logger.ErrLog("Unable to create tmp folder", err, log.Error()) } return folderUUID.String(), basePath, nil @@ -90,7 +92,7 @@ func (f *FileManagerService) SaveFile(basePath string, filename string, file io. dst, err := os.Create(fPath) if err != nil { - return com.ErrLog( + return logger.ErrLog( "Failed to create destination file", err, log.Error(), @@ -106,7 +108,7 @@ func (f *FileManagerService) SaveFile(basePath string, filename string, file io. // Copy the file contents written, err := io.Copy(dst, file) if err != nil { - return com.ErrLog( + return logger.ErrLog( "Failed to write file", err, log.Error(), @@ -122,14 +124,14 @@ func (f *FileManagerService) SaveFile(basePath string, filename string, file io. } func (f *FileManagerService) DeleteFolder(folderUuid string) { - basePath := filepath.Join(com.TmpUploadFolder.GetStr(), folderUuid) + basePath := filepath.Join(config.TmpUploadFolder.GetStr(), folderUuid) if err := os.RemoveAll(basePath); err != nil { log.Warn().Err(err).Msgf("failed to delete tmp folder %s", folderUuid) } } func (f *FileManagerService) GetLabFilePaths(folderUuid string) (basePath string, err error) { - basePath = filepath.Join(com.TmpUploadFolder.GetStr(), folderUuid) + basePath = filepath.Join(config.TmpUploadFolder.GetStr(), folderUuid) jobData := filepath.Join(basePath, JobDataFolderName) dockerFile := filepath.Join(basePath, DockerfileName) @@ -144,12 +146,12 @@ func (f *FileManagerService) GetLabFilePaths(folderUuid string) (basePath string } func (f *FileManagerService) GetSubmissionPath(uuid string) (string, error) { - path := filepath.Join(com.TmpUploadFolder.GetStr(), uuid) + path := filepath.Join(config.TmpUploadFolder.GetStr(), uuid) return f.checkFolder(path) } func (f *FileManagerService) checkFolder(path string) (jobData string, err error) { - if !com.FileExists(path) { + if !fu.FileExists(path) { return "", fmt.Errorf("could not find path") } return path, err diff --git a/src/internal/file_manager/utils.go b/src/internal/file_manager/utils.go new file mode 100644 index 0000000..6ad46d9 --- /dev/null +++ b/src/internal/file_manager/utils.go @@ -0,0 +1,15 @@ +package file_manager + +import ( + "github.com/rs/zerolog/log" + "io" +) + +// closeWithLog closes an io.closer +// prints a warning log if an error occurs +func closeWithLog(closer io.Closer) { + err := closer.Close() + if err != nil { + log.Warn().Err(err).Msg("Error occurred while closing interface") + } +} diff --git a/src/common/info.go b/src/internal/info/info.go similarity index 76% rename from src/common/info.go rename to src/internal/info/info.go index 021dfc9..22fc364 100644 --- a/src/common/info.go +++ b/src/internal/info/info.go @@ -1,13 +1,10 @@ -package common +package info import ( "fmt" - "math" "math/rand/v2" "runtime" - "sort" "strings" - "time" ) // build args to modify these vars @@ -31,7 +28,7 @@ var GoVersion = runtime.Version() func PrintInfo() { // generated from https://patorjk.com/software/taag/#p=testall&t=leviathan var headers = []string{ - // contains some characters that mess with multiline strings leave this alone + // contains some characters that mess with go multiline strings leave this alone "\n (`-') _ (`-') _ (`-') _ (`-') (`-').-> (`-') _ <-. (`-')_ \n <-. ( OO).-/ _(OO ) (_) (OO ).-/ ( OO).-> (OO )__ (OO ).-/ \\( OO) )\n ,--. ) (,------.,--.(_/,-.\\ ,-(`-')/ ,---. / '._ ,--. ,'-' / ,---. ,--./ ,--/ \n | (`-') | .---'\\ \\ / (_/ | ( OO)| \\ /`.\\ |'--...__)| | | | | \\ /`.\\ | \\ | | \n | |OO )(| '--. \\ / / | | )'-'|_.' |`--. .--'| `-' | '-'|_.' || . '| |)\n(| '__ | | .--' _ \\ /_)(| |_/(| .-. | | | | .-. |(| .-. || |\\ | \n | |' | `---.\\-'\\ / | |'->| | | | | | | | | | | | | || | \\ | \n `-----' `------' `-' `--' `--' `--' `--' `--' `--' `--' `--'`--' `--' \n", "\n __ ______ __ __ ________ ________ _________ ___ ___ ________ ___ __ \n/_/\\ /_____/\\ /_/\\ /_/\\ /_______/\\/_______/\\ /________/\\/__/\\ /__/\\ /_______/\\ /__/\\ /__/\\ \n\\:\\ \\ \\::::_\\/_\\:\\ \\\\ \\ \\\\__.::._\\/\\::: _ \\ \\\\__.::.__\\/\\::\\ \\\\ \\ \\\\::: _ \\ \\\\::\\_\\\\ \\ \\ \n \\:\\ \\ \\:\\/___/\\\\:\\ \\\\ \\ \\ \\::\\ \\ \\::(_) \\ \\ \\::\\ \\ \\::\\/_\\ .\\ \\\\::(_) \\ \\\\:. `-\\ \\ \\ \n \\:\\ \\____\\::___\\/_\\:\\_/.:\\ \\ _\\::\\ \\__\\:: __ \\ \\ \\::\\ \\ \\:: ___::\\ \\\\:: __ \\ \\\\:. _ \\ \\ \n \\:\\/___/\\\\:\\____/\\\\ ..::/ //__\\::\\__/\\\\:.\\ \\ \\ \\ \\::\\ \\ \\: \\ \\\\::\\ \\\\:.\\ \\ \\ \\\\. \\`-\\ \\ \\\n \\_____\\/ \\_____\\/ \\___/_( \\________\\/ \\__\\/\\__\\/ \\__\\/ \\__\\/ \\::\\/ \\__\\/\\__\\/ \\__\\/ \\__\\/\n \n", ` @@ -107,7 +104,6 @@ func PrintInfo() { printField("Source Hash", SourceHash) printField("GoVersion", GoVersion) - //nolint if Branch != "unknown" && CommitInfo != "unknown" { fmt.Println(nord10 + strings.Repeat("-", width) + colorReset) var baserepo = fmt.Sprintf("https://github.com/makeopensource/leviathan") @@ -121,98 +117,3 @@ func PrintInfo() { fmt.Println(divider) } - -func formatTime(input string) string { - buildTime, err := time.Parse(time.RFC3339, input) - if err != nil { - //fmt.Printf("Error parsing build time: %v\n", err) - return input - } - // Get the local timezone - localLocation, err := time.LoadLocation("Local") - if err != nil { - return input - } - // Convert the time to the local timezone - localBuildTime := buildTime.In(localLocation) - return fmt.Sprintf("%s (%s)", localBuildTime.Format("2006-01-02 3:04 PM MST"), timeago(localBuildTime)) -} - -// Seconds-based time units -const ( - Day = 24 * time.Hour - Week = 7 * Day - Month = 30 * Day - Year = 12 * Month - LongTime = 37 * Year -) - -// Time formats a time into a relative string. -// -// Time(someT) -> "3 weeks ago" -// -// stolen from -> https://github.com/dustin/go-humanize/blob/master/times.go -func timeago(then time.Time) string { - return CustomRelTime(then, time.Now(), "ago", "from now", defaultMagnitudes) -} - -type RelTimeMagnitude struct { - D time.Duration - Format string - DivBy time.Duration -} - -var defaultMagnitudes = []RelTimeMagnitude{ - {time.Second, "now", time.Second}, - {2 * time.Second, "1 second %s", 1}, - {time.Minute, "%d seconds %s", time.Second}, - {2 * time.Minute, "1 minute %s", 1}, - {time.Hour, "%d minutes %s", time.Minute}, - {2 * time.Hour, "1 hour %s", 1}, - {Day, "%d hours %s", time.Hour}, - {2 * Day, "1 day %s", 1}, - {Week, "%d days %s", Day}, - {2 * Week, "1 week %s", 1}, - {Month, "%d weeks %s", Week}, - {2 * Month, "1 month %s", 1}, - {Year, "%d months %s", Month}, - {18 * Month, "1 year %s", 1}, - {2 * Year, "2 years %s", 1}, - {LongTime, "%d years %s", Year}, - {math.MaxInt64, "a long while %s", 1}, -} - -func CustomRelTime(a, b time.Time, albl, blbl string, magnitudes []RelTimeMagnitude) string { - lbl := albl - diff := b.Sub(a) - - if a.After(b) { - lbl = blbl - diff = a.Sub(b) - } - - n := sort.Search(len(magnitudes), func(i int) bool { - return magnitudes[i].D > diff - }) - - if n >= len(magnitudes) { - n = len(magnitudes) - 1 - } - mag := magnitudes[n] - var args []interface{} - escaped := false - for _, ch := range mag.Format { - if escaped { - switch ch { - case 's': - args = append(args, lbl) - case 'd': - args = append(args, diff/mag.DivBy) - } - escaped = false - } else { - escaped = ch == '%' - } - } - return fmt.Sprintf(mag.Format, args...) -} diff --git a/src/internal/info/timeago.go b/src/internal/info/timeago.go new file mode 100644 index 0000000..3491893 --- /dev/null +++ b/src/internal/info/timeago.go @@ -0,0 +1,103 @@ +package info + +import ( + "fmt" + "math" + "sort" + "time" +) + +func formatTime(input string) string { + buildTime, err := time.Parse(time.RFC3339, input) + if err != nil { + //fmt.Printf("Error parsing build time: %v\n", err) + return input + } + // Get the local timezone + localLocation, err := time.LoadLocation("Local") + if err != nil { + return input + } + // Convert the time to the local timezone + localBuildTime := buildTime.In(localLocation) + return fmt.Sprintf("%s (%s)", localBuildTime.Format("2006-01-02 3:04 PM MST"), timeago(localBuildTime)) +} + +// Seconds-based time units +const ( + Day = 24 * time.Hour + Week = 7 * Day + Month = 30 * Day + Year = 12 * Month + LongTime = 37 * Year +) + +// Time formats a time into a relative string. +// +// Time(someT) -> "3 weeks ago" +// +// stolen from -> https://github.com/dustin/go-humanize/blob/master/times.go +func timeago(then time.Time) string { + return CustomRelTime(then, time.Now(), "ago", "from now", defaultMagnitudes) +} + +type RelTimeMagnitude struct { + D time.Duration + Format string + DivBy time.Duration +} + +var defaultMagnitudes = []RelTimeMagnitude{ + {time.Second, "now", time.Second}, + {2 * time.Second, "1 second %s", 1}, + {time.Minute, "%d seconds %s", time.Second}, + {2 * time.Minute, "1 minute %s", 1}, + {time.Hour, "%d minutes %s", time.Minute}, + {2 * time.Hour, "1 hour %s", 1}, + {Day, "%d hours %s", time.Hour}, + {2 * Day, "1 day %s", 1}, + {Week, "%d days %s", Day}, + {2 * Week, "1 week %s", 1}, + {Month, "%d weeks %s", Week}, + {2 * Month, "1 month %s", 1}, + {Year, "%d months %s", Month}, + {18 * Month, "1 year %s", 1}, + {2 * Year, "2 years %s", 1}, + {LongTime, "%d years %s", Year}, + {math.MaxInt64, "a long while %s", 1}, +} + +func CustomRelTime(a, b time.Time, albl, blbl string, magnitudes []RelTimeMagnitude) string { + lbl := albl + diff := b.Sub(a) + + if a.After(b) { + lbl = blbl + diff = a.Sub(b) + } + + n := sort.Search(len(magnitudes), func(i int) bool { + return magnitudes[i].D > diff + }) + + if n >= len(magnitudes) { + n = len(magnitudes) - 1 + } + mag := magnitudes[n] + var args []interface{} + escaped := false + for _, ch := range mag.Format { + if escaped { + switch ch { + case 's': + args = append(args, lbl) + case 'd': + args = append(args, diff/mag.DivBy) + } + escaped = false + } else { + escaped = ch == '%' + } + } + return fmt.Sprintf(mag.Format, args...) +} diff --git a/src/models/broadcast_channel.go b/src/internal/jobs/broadcast_channel.go similarity index 86% rename from src/models/broadcast_channel.go rename to src/internal/jobs/broadcast_channel.go index 614a729..3c96eb0 100644 --- a/src/models/broadcast_channel.go +++ b/src/internal/jobs/broadcast_channel.go @@ -1,7 +1,8 @@ -package models +package jobs import ( "context" + su "github.com/makeopensource/leviathan/pkg/sync_utils" "github.com/rs/zerolog/log" ) @@ -10,12 +11,12 @@ type BroadcastChannelKey string const BroadcastKey BroadcastChannelKey = "broadcast" type BroadcastChannel struct { - subscribers Map[string, chan *Job] + subscribers su.Map[string, chan *Job] } func NewBroadcastChannel() (*BroadcastChannel, context.Context) { bc := &BroadcastChannel{ - subscribers: Map[string, chan *Job]{}, + subscribers: su.Map[string, chan *Job]{}, } return bc, context.WithValue(context.Background(), BroadcastKey, bc) } diff --git a/src/models/broadcast_channel_test.go b/src/internal/jobs/broadcast_channel_test.go similarity index 99% rename from src/models/broadcast_channel_test.go rename to src/internal/jobs/broadcast_channel_test.go index 8ac816f..8e3e3df 100644 --- a/src/models/broadcast_channel_test.go +++ b/src/internal/jobs/broadcast_channel_test.go @@ -1,4 +1,4 @@ -package models +package jobs import ( "github.com/stretchr/testify/assert" diff --git a/src/api/v1/job_impl.go b/src/internal/jobs/handler.go similarity index 85% rename from src/api/v1/job_impl.go rename to src/internal/jobs/handler.go index ca44db4..7200654 100644 --- a/src/api/v1/job_impl.go +++ b/src/internal/jobs/handler.go @@ -1,19 +1,17 @@ -package v1 +package jobs import ( "connectrpc.com/connect" "context" "fmt" v1 "github.com/makeopensource/leviathan/generated/jobs/v1" - "github.com/makeopensource/leviathan/models" - "github.com/makeopensource/leviathan/service/jobs" ) type JobServer struct { - srv *jobs.JobService + srv *JobService } -func NewJobServer(srv *jobs.JobService) *JobServer { +func NewJobServer(srv *JobService) *JobServer { return &JobServer{srv: srv} } @@ -24,7 +22,7 @@ func (job *JobServer) NewJob(_ context.Context, req *connect.Request[v1.NewJobRe return nil, fmt.Errorf("submission folder id is empty") } - newJob := &models.Job{LabID: uint(labId)} + newJob := &Job{LabID: uint(labId)} jobId, err := job.srv.NewJob(newJob, submissionTmpFolder) if err != nil { return nil, err @@ -48,7 +46,7 @@ func (job *JobServer) GetStatus(_ context.Context, req *connect.Request[v1.JobLo } func (job *JobServer) StreamStatus(ctx context.Context, req *connect.Request[v1.JobLogRequest], stream *connect.ServerStream[v1.JobLogsResponse]) error { - streamFunc := func(jobInfo *models.Job, logs string) error { + streamFunc := func(jobInfo *Job, logs string) error { return stream.Send(&v1.JobLogsResponse{ JobInfo: jobInfo.ToProto(), Logs: logs, diff --git a/src/models/job.go b/src/internal/jobs/models.go similarity index 80% rename from src/models/job.go rename to src/internal/jobs/models.go index e6c8406..d972e82 100644 --- a/src/models/job.go +++ b/src/internal/jobs/models.go @@ -1,9 +1,10 @@ -package models +package jobs import ( "context" "fmt" v1 "github.com/makeopensource/leviathan/generated/jobs/v1" + "github.com/makeopensource/leviathan/internal/labs" "github.com/rs/zerolog/log" "gorm.io/gorm" ) @@ -56,7 +57,7 @@ type Job struct { // to store if an error occurred, otherwise empty, Error string LabID uint - LabData *Lab `gorm:"foreignKey:LabID;references:ID"` + LabData *labs.Lab `gorm:"foreignKey:LabID;references:ID"` // OutputLogFilePath text file contain the container std out OutputLogFilePath string // TmpJobFolderPath holds the path to the tmp dir all files related to the job except the final output @@ -118,3 +119,37 @@ func (j *Job) AfterUpdate(tx *gorm.DB) (err error) { go ch.(*BroadcastChannel).Broadcast(j) return } + +type JobError interface { + // Reason will be displayed to the end user, providing a user-friendly message. + Reason() string + // Err err parameter holds the underlying error, used for debugging purposes. + Err() error + // ErrStr returns string from the error, if nil return empty string + ErrStr() string +} + +// JErr implements JobError +type JErr struct { + reason string + err error +} + +func JError(reason string, err error) JErr { + return JErr{reason: reason, err: err} +} + +func (err JErr) Reason() string { + return err.reason +} + +func (err JErr) Err() error { + return err.err +} + +func (err JErr) ErrStr() string { + if err.err != nil { + return err.err.Error() + } + return "" +} diff --git a/src/service/jobs/job_queue.go b/src/internal/jobs/queue.go similarity index 61% rename from src/service/jobs/job_queue.go rename to src/internal/jobs/queue.go index f405f3d..3dabce3 100644 --- a/src/service/jobs/job_queue.go +++ b/src/internal/jobs/queue.go @@ -5,39 +5,41 @@ import ( "fmt" dk "github.com/docker/docker/api/types/container" "github.com/docker/docker/pkg/stdcopy" - "github.com/makeopensource/leviathan/common" - md "github.com/makeopensource/leviathan/models" - "github.com/makeopensource/leviathan/service/docker" + "github.com/makeopensource/leviathan/internal/docker" + "github.com/makeopensource/leviathan/internal/labs" + "github.com/makeopensource/leviathan/pkg/logger" + su "github.com/makeopensource/leviathan/pkg/sync_utils" "github.com/rs/zerolog/log" - "gorm.io/gorm" "os" "path/filepath" "time" ) type JobQueue struct { - jobSemaphore *md.WorkerSemaphore - db *gorm.DB + jobSemaphore *su.WorkerSemaphore + db JobStore dkSrv *docker.DkService - contextMap *md.Map[string, func()] + contextMap *su.Map[string, func()] + labSrv *labs.LabService } -func NewJobQueue(totalJobs uint, db *gorm.DB, dk *docker.DkService) *JobQueue { +func NewJobQueue(totalJobs uint, db JobStore, dk *docker.DkService, lab *labs.LabService) *JobQueue { queue := &JobQueue{ - contextMap: &md.Map[string, func()]{}, + contextMap: &su.Map[string, func()]{}, db: db, dkSrv: dk, - jobSemaphore: md.NewWorkerSemaphore(int(totalJobs)), + jobSemaphore: su.NewWorkerSemaphore(int(totalJobs)), + labSrv: lab, } return queue } -func (q *JobQueue) AddJob(mes *md.Job) error { +func (q *JobQueue) AddJob(mes *Job) error { jog(mes.JobCtx).Info().Msg("sending job to queue") err := mes.ValidateForQueue() if err != nil { - return common.ErrLog("job validation failed: "+err.Error(), err, jog(mes.JobCtx).Error()) + return logger.ErrLog("job validation failed: "+err.Error(), err, jog(mes.JobCtx).Error()) } go q.worker(mes) @@ -52,19 +54,19 @@ func (q *JobQueue) NewJobContext(jobID string) context.Context { } q.contextMap.Store(jobID, wrapCancelFunc) - return common.CreateJobSubLoggerCtx(ctx, jobID) + return logger.CreateJobSubLoggerCtx(ctx, jobID) } func (q *JobQueue) CancelJob(messageId string) { cancel, ok := q.contextMap.Load(messageId) if !ok { - log.Warn().Str(common.JobLogKey, messageId).Msg("job context was nil") + log.Warn().Str(logger.JobLogKey, messageId).Msg("job context was nil") return } cancel() } -func (q *JobQueue) worker(msg *md.Job) { +func (q *JobQueue) worker(msg *Job) { if msg == nil { log.Error().Msg("job received was nil, THIS SHOULD NEVER HAPPEN") return @@ -85,7 +87,7 @@ func (q *JobQueue) worker(msg *md.Job) { } // runJob should ALWAYS BE BLOCKING, as it prevents the worker from moving on to a new job -func (q *JobQueue) runJob(job *md.Job) { +func (q *JobQueue) runJob(job *Job) { client, contId, err := q.setupJob(job) defer q.cleanupJob(job, client) if err != nil { @@ -93,22 +95,22 @@ func (q *JobQueue) runJob(job *md.Job) { return } - logStatusCh := make(chan md.JobError, 1) + logStatusCh := make(chan JobError) q.setJobInProgress(job) err2 := client.StartContainer(contId) if err2 != nil { - q.bigProblem(job, md.JError("unable to start job container", err2)) + q.bigProblem(job, JError("unable to start job container", err2)) return } - // start writing to log file so that + // start writing to log file so that, // we can stream changes to the log file to the user go func() { logStatusCh <- writeLogs(client, job) }() - statusCh, errCh := client.Client.ContainerWait(context.Background(), contId, dk.WaitConditionNotRunning) + statusCh, errCh := client.WaitForContainerStatusChange(contId) select { case <-statusCh: if mes := <-logStatusCh; mes != nil { @@ -123,10 +125,10 @@ func (q *JobQueue) runJob(job *md.Job) { q.greatSuccess(job, logLine) return case err := <-errCh: - q.bigProblem(job, md.JError("error occurred while waiting for job process", err)) + q.bigProblem(job, JError("error occurred while waiting for job process", err)) return case <-time.After(job.LabData.JobTimeout): - q.bigProblem(job, md.JError(timeoutErrLog(job.LabData.JobTimeout), nil)) + q.bigProblem(job, JError(timeoutErrLog(job.LabData.JobTimeout), nil)) return case <-job.JobCtx.Done(): q.setJobAsCancelled(job) @@ -141,50 +143,46 @@ func timeoutErrLog(timeout time.Duration) string { // setupJob Set up job like king, yes! // returns nil client if an error occurred while setup, // make sure to handle null ptr dereference -func (q *JobQueue) setupJob(msg *md.Job) (*docker.DkClient, string, md.JobError) { +func (q *JobQueue) setupJob(msg *Job) (*docker.DkClient, string, JobError) { q.setJobInSetup(msg) machine, err := q.dkSrv.ClientManager.GetClientById(msg.MachineId) if err != nil { - return nil, "", md.JError("Failed to get machine info", err) + return nil, "", JError("Failed to get machine info", err) } // incase dockerfile is not passed and referenced via tag name if msg.LabData.DockerFilePath != "" { err = machine.BuildImageFromDockerfile(msg.LabData.DockerFilePath, msg.LabData.ImageTag) if err != nil { - return nil, "", md.JError("Failed to create image", err) - } - // folder structure is '//autolab/Dockerfile, get the folder path - parent := filepath.Base(filepath.Dir(filepath.Dir(msg.LabData.DockerFilePath))) - - if parent != "labs" { // do not delete if job is from a saved lab - if err = os.RemoveAll(parent); err != nil { - return nil, "", md.JError("failed to delete dockerfile", err) - } + return nil, "", JError("Failed to create image", err) } } resources := dk.Resources{ - NanoCPUs: msg.LabData.JobLimits.NanoCPU * md.CPUQuota, - Memory: msg.LabData.JobLimits.Memory * md.MB, + NanoCPUs: msg.LabData.JobLimits.NanoCPU * CPUQuota, + Memory: msg.LabData.JobLimits.Memory * MB, PidsLimit: &msg.LabData.JobLimits.PidsLimit, } - contId, err := machine.CreateNewContainer(msg.JobId, msg.LabData.ImageTag, filepath.Base(msg.TmpJobFolderPath), msg.LabData.JobEntryCmd, resources) + contId, err := machine.CreateNewContainer( + msg.JobId, + msg.LabData.ImageTag, + msg.LabData.JobEntryCmd, + resources, + ) if err != nil { - return nil, "", md.JError("unable to create job container", err) + return nil, "", JError("unable to create job container", err) } err = machine.CopyToContainer(contId, msg.TmpJobFolderPath) if err != nil { - return nil, "", md.JError("unable to copy files to job container", err) + return nil, "", JError("unable to copy files to job container", err) } msg.ContainerId = contId - res := q.db.Save(msg) - if res.Error != nil { - return nil, "", md.JError("unable to update job in db", err) + if err = q.db.UpdateJob(msg); err != nil { + return nil, "", JError("unable to update job in db", err) } return machine, contId, nil @@ -192,7 +190,7 @@ func (q *JobQueue) setupJob(msg *md.Job) (*docker.DkClient, string, md.JobError) // cleanupJob clean up job, // updates job in DB, removes the container and associated tmp job data -func (q *JobQueue) cleanupJob(msg *md.Job, client *docker.DkClient) { +func (q *JobQueue) cleanupJob(msg *Job, client *docker.DkClient) { jog(msg.JobCtx).Info().Msg("cleaning up job") q.updateJobVeryNice(msg) @@ -217,56 +215,55 @@ func (q *JobQueue) cleanupJob(msg *md.Job, client *docker.DkClient) { // Very nice! // // jobResult is the last line expected to be valid json string, returned to the job caller -func (q *JobQueue) greatSuccess(job *md.Job, jobResult string) { +func (q *JobQueue) greatSuccess(job *Job, jobResult string) { jog(job.JobCtx).Info().Msg("job completed successfully") - job.Status = md.Complete + job.Status = Complete job.StatusMessage = jobResult } // bigProblem set job status to models.Failed // // job failed, Not good! -func (q *JobQueue) bigProblem(job *md.Job, jErr md.JobError) { +func (q *JobQueue) bigProblem(job *Job, jErr JobError) { jog(job.JobCtx).Error().Err(jErr.Err()).Str("reason", jErr.Reason()).Msg("job failed") - job.Status = md.Failed + job.Status = Failed job.StatusMessage = jErr.Reason() job.Error = jErr.ErrStr() } -func (q *JobQueue) setJobAsCancelled(job *md.Job) { +func (q *JobQueue) setJobAsCancelled(job *Job) { jog(job.JobCtx).Info().Msg("job was cancelled") - job.Status = md.Canceled + job.Status = Canceled job.StatusMessage = "Job was cancelled" } // setJobInProgress set job status as models.Running // // Job is in progress, success soon! -func (q *JobQueue) setJobInProgress(msg *md.Job) { - msg.Status = md.Running +func (q *JobQueue) setJobInProgress(msg *Job) { + msg.Status = Running q.updateJobVeryNice(msg) } // setJobInSetup set job status as models.Preparing // // job is being setup standby -func (q *JobQueue) setJobInSetup(msg *md.Job) { - msg.Status = md.Preparing +func (q *JobQueue) setJobInSetup(msg *Job) { + msg.Status = Preparing q.updateJobVeryNice(msg) } // updateJobVeryNice Database updated, fresh like new wife! -func (q *JobQueue) updateJobVeryNice(msg *md.Job) { - res := q.db.Save(msg) - if res.Error != nil { - jog(msg.JobCtx).Error().Err(res.Error).Msg("error occurred while saving job to db") +func (q *JobQueue) updateJobVeryNice(msg *Job) { + if err := q.db.UpdateJob(msg); err != nil { + jog(msg.JobCtx).Error().Err(err).Msg("error occurred while saving job to db") } } -func writeLogs(client *docker.DkClient, msg *md.Job) md.JobError { +func writeLogs(client *docker.DkClient, msg *Job) JobError { outputFile, err := os.OpenFile(msg.OutputLogFilePath, os.O_RDWR|os.O_CREATE, 0660) if err != nil { - return md.JError("unable to open log file", err) + return JError("unable to open log file", err) } defer func() { @@ -278,24 +275,24 @@ func writeLogs(client *docker.DkClient, msg *md.Job) md.JobError { logs, err := client.TailContainerLogs(context.Background(), msg.ContainerId) if err != nil { - return md.JError("unable to tail job container", err) + return JError("unable to tail job container", err) } _, err = stdcopy.StdCopy(outputFile, outputFile, logs) if err != nil { - return md.JError("unable to write to log file", err) + return JError("unable to write to log file", err) } return nil } -func verifyLogs(msg *md.Job) (string, md.JobError) { - if msg.Status == md.Failed { - return "", md.JError("Job failed, skipping parsing log file", nil) +func verifyLogs(msg *Job) (string, JobError) { + if msg.Status == Failed { + return "", JError("Job failed, skipping parsing log file", nil) } outputFile, err := os.Open(msg.OutputLogFilePath) if err != nil { - return "", md.JError("unable to open log file", err) + return "", JError("unable to open log file", err) } defer func(open *os.File) { err := open.Close() @@ -306,10 +303,10 @@ func verifyLogs(msg *md.Job) (string, md.JobError) { line, err := GetLastLine(outputFile) if err != nil { - return "", md.JError("unable to get logs", err) + return "", JError("unable to get logs", err) } if !IsValidJSON(line) { - return "", md.JError("unable to parse log output", err) + return "", JError("unable to parse log output", err) } return line, nil diff --git a/src/service/jobs/job_service.go b/src/internal/jobs/service.go similarity index 68% rename from src/service/jobs/job_service.go rename to src/internal/jobs/service.go index 4616a83..fb7bea6 100644 --- a/src/service/jobs/job_service.go +++ b/src/internal/jobs/service.go @@ -4,40 +4,47 @@ import ( "context" "fmt" "github.com/google/uuid" - com "github.com/makeopensource/leviathan/common" - "github.com/makeopensource/leviathan/models" - "github.com/makeopensource/leviathan/service/docker" - "github.com/makeopensource/leviathan/service/file_manager" - "github.com/makeopensource/leviathan/service/labs" + "github.com/makeopensource/leviathan/internal/config" + "github.com/makeopensource/leviathan/internal/docker" + fm "github.com/makeopensource/leviathan/internal/file_manager" + "github.com/makeopensource/leviathan/internal/labs" + fu "github.com/makeopensource/leviathan/pkg/file_utils" + "github.com/makeopensource/leviathan/pkg/logger" "github.com/rs/zerolog" "github.com/rs/zerolog/log" - "gorm.io/gorm" "os" "path/filepath" "time" ) type JobService struct { - db *gorm.DB dockerSrv *docker.DkService queue *JobQueue - broadcastCh *models.BroadcastChannel + broadcastCh *BroadcastChannel labSrv *labs.LabService - fileManSrv *file_manager.FileManagerService + fileManSrv *fm.FileManagerService + db JobStore } func NewJobService( - db *gorm.DB, - bc *models.BroadcastChannel, + db JobStore, + bc *BroadcastChannel, dockerService *docker.DkService, labService *labs.LabService, - tmpFileService *file_manager.FileManagerService, + tmpFileService *fm.FileManagerService, ) *JobService { + queue := NewJobQueue( + uint(config.ConcurrentJobs.GetUint64()), + db, + dockerService, + labService, + ) + srv := &JobService{ db: db, broadcastCh: bc, dockerSrv: dockerService, - queue: NewJobQueue(uint(com.ConcurrentJobs.GetUint64()), db, dockerService), + queue: queue, labSrv: labService, fileManSrv: tmpFileService, } @@ -45,7 +52,7 @@ func NewJobService( return srv } -func (job *JobService) NewJob(newJob *models.Job, submissionFolderId string) (string, error) { +func (job *JobService) NewJob(newJob *Job, submissionFolderId string) (string, error) { if newJob.LabData == nil { labData, err := job.getLab(newJob.LabID) if err != nil { @@ -56,7 +63,7 @@ func (job *JobService) NewJob(newJob *models.Job, submissionFolderId string) (st jobId, err := uuid.NewUUID() if err != nil { - return "", com.ErrLog("failed to generate job ID", err, log.Error()) + return "", logger.ErrLog("failed to generate job ID", err, log.Error()) } newJob.JobId = jobId.String() @@ -65,9 +72,9 @@ func (job *JobService) NewJob(newJob *models.Job, submissionFolderId string) (st // job context, so that it can be cancelled, and store sub logger ctx := job.queue.NewJobContext(newJob.JobId) - jobDir, err := CreateTmpJobDir(newJob.JobId, com.SubmissionFolder.GetStr()) + jobDir, err := CreateTmpJobDir(newJob.JobId, config.SubmissionFolder.GetStr()) if err != nil { - return "", com.ErrLog("failed to create job dir", err, jog(ctx).Error()) + return "", logger.ErrLog("failed to create job dir", err, jog(ctx).Error()) } submissionFolder, err := job.fileManSrv.GetSubmissionPath(submissionFolderId) @@ -76,16 +83,16 @@ func (job *JobService) NewJob(newJob *models.Job, submissionFolderId string) (st } defer job.fileManSrv.DeleteFolder(submissionFolderId) - if err = com.HardLinkFolder(submissionFolder, jobDir); err != nil { - return "", com.ErrLog("unable to copy files to job dir", err, log.Error()) + if err = fu.HardLinkFolder(submissionFolder, jobDir); err != nil { + return "", logger.ErrLog("unable to copy files to job dir", err, log.Error()) } - if err = com.HardLinkFolder(newJob.LabData.JobFilesDirPath, jobDir); err != nil { - return "", com.ErrLog("unable to copy files to job dir", err, log.Error()) + if err = fu.HardLinkFolder(newJob.LabData.JobFilesDirPath, jobDir); err != nil { + return "", logger.ErrLog("unable to copy files to job dir", err, log.Error()) } logPath, err2 := setupLogFile(newJob.JobId) if err2 != nil { - return "", com.ErrLog( + return "", logger.ErrLog( "failed to setup log file: "+err2.Reason(), err, jog(ctx).Error().Str("reason", err2.Reason()), @@ -94,16 +101,16 @@ func (job *JobService) NewJob(newJob *models.Job, submissionFolderId string) (st // setup job metadata newJob.MachineId = mId - newJob.Status = models.Queued + newJob.Status = Queued newJob.OutputLogFilePath = logPath newJob.TmpJobFolderPath = jobDir newJob.JobCtx = ctx newJob.LabData.VerifyJobLimits() jog(newJob.JobCtx).Debug().Any("limits", newJob.LabData.JobLimits).Msg("job limits") - res := job.db.Create(newJob) - if res.Error != nil { - return "", com.ErrLog("failed to save job to db", res.Error, jog(ctx).Error()) + err = job.db.CreateJob(newJob) + if err != nil { + return "", logger.ErrLog("failed to save job to db", err, jog(ctx).Error()) } err = job.queue.AddJob(newJob) @@ -119,14 +126,14 @@ func (job *JobService) CancelJob(jobUuid string) { } // WaitForJobAndLogs blocks until job ends -func (job *JobService) WaitForJobAndLogs(jobUuid string) (*models.Job, string, error) { - var jobInfo *models.Job +func (job *JobService) WaitForJobAndLogs(jobUuid string) (*Job, string, error) { + var jobInfo *Job var outerLogs string err := job.StreamJobAndLogs( context.Background(), jobUuid, - func(jobInf *models.Job, logs string) error { + func(jobInf *Job, logs string) error { jobInfo = jobInf outerLogs = logs return nil @@ -139,7 +146,7 @@ func (job *JobService) WaitForJobAndLogs(jobUuid string) (*models.Job, string, e func (job *JobService) StreamJobAndLogs( ctx context.Context, jobUuid string, - streamFunc func(jobInf *models.Job, logs string) error, + streamFunc func(jobInf *Job, logs string) error, ) error { jobInfo, complete, flogs, err := job.checkJob(jobUuid) if err != nil { @@ -203,7 +210,7 @@ func (job *JobService) StreamJobAndLogs( } // GetJobStatusAndLogs gets the status once whatever it may be and current logs -func (job *JobService) GetJobStatusAndLogs(jobUuid string) (*models.Job, string, error) { +func (job *JobService) GetJobStatusAndLogs(jobUuid string) (*Job, string, error) { jobInfo, _, logs, err := job.checkJob(jobUuid) if err != nil { return nil, "", err @@ -211,7 +218,7 @@ func (job *JobService) GetJobStatusAndLogs(jobUuid string) (*models.Job, string, return jobInfo, logs, nil } -func (job *JobService) ListenToJobLogs(ctx context.Context, jobInfo *models.Job) chan string { +func (job *JobService) ListenToJobLogs(ctx context.Context, jobInfo *Job) chan string { logChannel := make(chan string, 2) go func(ctx context.Context) { prevLength := 0 @@ -224,13 +231,13 @@ func (job *JobService) ListenToJobLogs(ctx context.Context, jobInfo *models.Job) content := ReadLogFile(jobInfo.OutputLogFilePath) contLen := len(content) if contLen > prevLength { // send if content changed - log.Debug().Str(com.JobLogKey, jobInfo.JobId).Msgf("sending log, length changed from %d to %d", prevLength, contLen) + log.Debug().Str(logger.JobLogKey, jobInfo.JobId).Msgf("sending log, length changed from %d to %d", prevLength, contLen) prevLength = contLen logChannel <- content } case <-ctx.Done(): close(logChannel) - log.Debug().Str(com.JobLogKey, jobInfo.JobId).Msg("stopping listening for logs") + log.Debug().Str(logger.JobLogKey, jobInfo.JobId).Msg("stopping listening for logs") return } } @@ -239,7 +246,7 @@ func (job *JobService) ListenToJobLogs(ctx context.Context, jobInfo *models.Job) return logChannel } -func (job *JobService) SubToJob(jobUuid string) chan *models.Job { +func (job *JobService) SubToJob(jobUuid string) chan *Job { return job.broadcastCh.Subscribe(jobUuid) } @@ -247,14 +254,14 @@ func (job *JobService) UnsubToJob(jobUuid string) { job.broadcastCh.Unsubscribe(jobUuid) } -func (job *JobService) checkJob(jobUuid string) (*models.Job, bool, string, error) { - jobInf, err := job.getJobFromDB(jobUuid) +func (job *JobService) checkJob(jobUuid string) (*Job, bool, string, error) { + jobInf, err := job.GetJobFromDB(jobUuid) if err != nil { return nil, false, "", fmt.Errorf("failed to get job info") } if jobInf.Status.Done() { - log.Debug().Str(com.JobLogKey, jobUuid).Msg("job is already done") + log.Debug().Str(logger.JobLogKey, jobUuid).Msg("job is already done") content := ReadLogFile(jobInf.OutputLogFilePath) return jobInf, true, content, nil @@ -263,11 +270,10 @@ func (job *JobService) checkJob(jobUuid string) (*models.Job, bool, string, erro } } -func (job *JobService) getJobFromDB(jobUuid string) (*models.Job, error) { - var jobInfo *models.Job - res := job.db.First(&jobInfo, "job_id = ?", jobUuid) - if res.Error != nil { - return nil, fmt.Errorf("failed to get job info from db") +func (job *JobService) GetJobFromDB(jobUuid string) (*Job, error) { + jobInfo, err := job.db.GetJobByUuid(jobUuid) + if err != nil { + return nil, fmt.Errorf("failed to get job info from db: %v", err) } return jobInfo, nil } @@ -277,14 +283,10 @@ func (job *JobService) getJobFromDB(jobUuid string) (*models.Job, error) { // // for example machine running leviathan shutdown unexpectedly or leviathan had an unrecoverable error func (job *JobService) cleanupOrphanJobs() { - var orphanJobs []*models.Job - res := job.db.Preload("LabData"). - Where("status = ?", string(models.Queued)). - Or("status = ?", string(models.Running)). - Or("status = ?", string(models.Preparing)). - Find(&orphanJobs) - if res.Error != nil { - log.Warn().Err(res.Error).Msgf("Failed to query database for orphan jobs") + var orphanJobs []Job + err := job.db.FetchInProgressJobs(&orphanJobs) + if err != nil { + log.Warn().Err(err).Msgf("Failed to query database for orphan jobs") return } @@ -318,11 +320,11 @@ func (job *JobService) cleanupOrphanJobs() { } } - orphan.Status = models.Failed + orphan.Status = Failed orphan.StatusMessage = "job was unable to be processed due to an internal server error" - res = job.db.Save(orphan) - if res.Error != nil { - log.Warn().Err(res.Error).Msg("unable to update orphan job status") + err = job.db.UpdateJob(&orphan) + if err != nil { + log.Warn().Err(err).Msg("unable to update orphan job status") } } @@ -332,11 +334,11 @@ func (job *JobService) cleanupOrphanJobs() { } // setupLogFile store grader output -func setupLogFile(jobId string) (string, models.JobError) { - outputFile := fmt.Sprintf("%s/%s.txt", com.OutputFolder.GetStr(), jobId) +func setupLogFile(jobId string) (string, JobError) { + outputFile := fmt.Sprintf("%s/%s.txt", config.OutputFolder.GetStr(), jobId) outFile, err := os.Create(outputFile) if err != nil { - return "", models.JError(fmt.Sprintf("error while creating log file at %s", outputFile), err) + return "", JError(fmt.Sprintf("error while creating log file at %s", outputFile), err) } defer func() { err := outFile.Close() @@ -347,13 +349,13 @@ func setupLogFile(jobId string) (string, models.JobError) { full, err := filepath.Abs(outputFile) if err != nil { - return "", models.JError("error while getting absolute path", err) + return "", JError("error while getting absolute path", err) } return full, nil } -func (job *JobService) getLab(labId uint) (*models.Lab, error) { +func (job *JobService) getLab(labId uint) (*labs.Lab, error) { if labId == 0 { return nil, fmt.Errorf("invalid lab ID %d", labId) } diff --git a/src/service/jobs/job_service_batch_test.go b/src/internal/jobs/service_batch_test.go similarity index 98% rename from src/service/jobs/job_service_batch_test.go rename to src/internal/jobs/service_batch_test.go index 6a23c82..ec394ef 100644 --- a/src/service/jobs/job_service_batch_test.go +++ b/src/internal/jobs/service_batch_test.go @@ -1,4 +1,4 @@ -package jobs +package jobs_test import ( "fmt" diff --git a/src/service/jobs/job_service_tango_test.go b/src/internal/jobs/service_tango_test.go similarity index 86% rename from src/service/jobs/job_service_tango_test.go rename to src/internal/jobs/service_tango_test.go index 0ebd7d9..ddaa8f5 100644 --- a/src/service/jobs/job_service_tango_test.go +++ b/src/internal/jobs/service_tango_test.go @@ -1,8 +1,9 @@ -package jobs +package jobs_test import ( - "github.com/makeopensource/leviathan/models" - . "github.com/makeopensource/leviathan/service/file_manager" + "github.com/makeopensource/leviathan/internal/file_manager" + "github.com/makeopensource/leviathan/internal/jobs" + "github.com/makeopensource/leviathan/internal/labs" "os" "path/filepath" "testing" @@ -28,73 +29,73 @@ var ( "correct": { studentFile: autolab0 + "/handin.py", expectedOutput: `{"scores": {"q1": 10, "q2": 10, "q3": 10}}`, - correctStatus: models.Complete, + correctStatus: jobs.Complete, }, "cheating1": testCase{ studentFile: autolab0 + "/handin_cheating1.py", expectedOutput: `{"scores": {"q1": 0, "q2": 0, "q3": 0}}`, - correctStatus: models.Complete, + correctStatus: jobs.Complete, }, "cheating2": testCase{ studentFile: autolab0 + "/handin_cheating2.py", expectedOutput: `Maximum timeout reached for job, job ran for 10s`, - correctStatus: models.Failed, + correctStatus: jobs.Failed, }, "incorrect1": testCase{ studentFile: autolab0 + "/handin_incorrect1.py", expectedOutput: `{"scores": {"q1": 0, "q2": 0, "q3": 0}}`, - correctStatus: models.Complete, + correctStatus: jobs.Complete, }, "incorrect2": testCase{ studentFile: autolab0 + "/handin_incorrect2.py", expectedOutput: `unable to parse log output`, - correctStatus: models.Failed, + correctStatus: jobs.Failed, }, }, "tango1": { "correct": { studentFile: autolab1 + "/handin.py", expectedOutput: `{"scores": {"q1": 10, "q2": 10, "q3": 10}}`, - correctStatus: models.Complete, + correctStatus: jobs.Complete, }, "incorrect1": testCase{ studentFile: autolab1 + "/handin_incorrect1.py", expectedOutput: `{"scores": {"q1": 10, "q2": 0, "q3": 0}}`, - correctStatus: models.Complete, + correctStatus: jobs.Complete, }, "incorrect2": testCase{ studentFile: autolab1 + "/handin_incorrect2.py", expectedOutput: `{"scores": {"q1": 9, "q2": 3, "q3": 3}}`, - correctStatus: models.Complete, + correctStatus: jobs.Complete, }, "incorrect3": testCase{ studentFile: autolab1 + "/handin_incorrect3.py", expectedOutput: `{"scores": {"q1": 1, "q2": 0, "q3": 0}}`, - correctStatus: models.Complete, + correctStatus: jobs.Complete, }, "incorrect4": testCase{ studentFile: autolab1 + "/handin_incorrect4.py", expectedOutput: `{"scores": {"q1": 0, "q2": 0, "q3": 0}}`, - correctStatus: models.Complete, + correctStatus: jobs.Complete, }, }, "tango3": { "correct": { studentFile: autolab3 + "/handin.json", expectedOutput: `{"scores": {"q1": 10, "q2": 10, "q3": 99}}`, - correctStatus: models.Complete, + correctStatus: jobs.Complete, }, }, "tango4": { "correct": { studentFile: autolab4 + "/handin.py", expectedOutput: `{"scores": {"q1": 10}}`, - correctStatus: models.Complete, + correctStatus: jobs.Complete, }, "incorrect": { studentFile: autolab4 + "/handin_incorrect.py", expectedOutput: `{"scores": {"q1": 0}}`, - correctStatus: models.Complete, + correctStatus: jobs.Complete, }, }, } @@ -183,7 +184,7 @@ func TestTango4(t *testing.T) { func newLab(t *testing.T, folderName string) uint { tarPath := folderName + "/autograde.tar" tangoMakeFilePath := folderName + "/Makefile" - labId := createLab(t, &models.Lab{ + labId := createLab(t, &labs.Lab{ Name: "tango-test-lab", JobTimeout: tangoTimeout, AutolabCompatible: true, @@ -195,7 +196,7 @@ func newLab(t *testing.T, folderName string) uint { } func setupJobProcessTango(t *testing.T, labId uint, studentCodePath string) string { - newJob := &models.Job{LabID: labId} + newJob := &jobs.Job{LabID: labId} studentFileName := filepath.Base(studentCodePath) if filepath.Ext(studentFileName) == ".json" { @@ -209,7 +210,7 @@ func setupJobProcessTango(t *testing.T, labId uint, studentCodePath string) stri t.Fatal(err) } - folderId, err := fileManTestService.CreateSubmissionFolder(&FileInfo{ + folderId, err := fileManTestService.CreateSubmissionFolder(&file_manager.FileInfo{ Reader: studentCode, Filename: studentFileName, }) diff --git a/src/service/jobs/job_service_test.go b/src/internal/jobs/service_test.go similarity index 75% rename from src/service/jobs/job_service_test.go rename to src/internal/jobs/service_test.go index 0142ac1..a8ff037 100644 --- a/src/service/jobs/job_service_test.go +++ b/src/internal/jobs/service_test.go @@ -1,13 +1,12 @@ -package jobs +package jobs_test import ( "fmt" - "github.com/makeopensource/leviathan/common" - "github.com/makeopensource/leviathan/models" - "github.com/makeopensource/leviathan/service/docker" - . "github.com/makeopensource/leviathan/service/file_manager" - "github.com/makeopensource/leviathan/service/labs" - "github.com/rs/zerolog" + "github.com/makeopensource/leviathan/cmd" + "github.com/makeopensource/leviathan/internal/docker" + "github.com/makeopensource/leviathan/internal/file_manager" + "github.com/makeopensource/leviathan/internal/jobs" + "github.com/makeopensource/leviathan/internal/labs" "github.com/rs/zerolog/log" "github.com/stretchr/testify/assert" "os" @@ -20,9 +19,9 @@ import ( var ( dkTestService *docker.DkService - jobTestService *JobService + jobTestService *jobs.JobService labTestService *labs.LabService - fileManTestService *FileManagerService + fileManTestService *file_manager.FileManagerService setupOnce sync.Once labCreateOnce sync.Once createLabId uint @@ -37,7 +36,7 @@ const ( type testCase struct { studentFile string expectedOutput string - correctStatus models.JobStatus + correctStatus jobs.JobStatus } var ( @@ -46,32 +45,32 @@ var ( "correct": { studentFile: "../../../example/simple-addition/student_correct.py", expectedOutput: `{"addition": {"passed": true, "message": ""}, "subtraction": {"passed": true, "message": ""}, "multiplication": {"passed": true, "message": ""}, "division": {"passed": true, "message": ""}}`, - correctStatus: models.Complete, + correctStatus: jobs.Complete, }, "incorrect": { studentFile: "../../../example/simple-addition/student_incorrect.py", expectedOutput: `{"addition": {"passed": true, "message": ""}, "subtraction": {"passed": true, "message": ""}, "multiplication": {"passed": false, "message": "Multiplication failed. Expected 42, got 48"}, "division": {"passed": false, "message": "Division failed. Expected 4, got 3.3333333333333335"}}`, - correctStatus: models.Complete, + correctStatus: jobs.Complete, }, "timeout": { studentFile: "../../../example/simple-addition/student_timeout.py", expectedOutput: "Maximum timeout reached for job, job ran for 10s", - correctStatus: models.Failed, + correctStatus: jobs.Failed, }, "timeout_edge": { studentFile: "../../../example/simple-addition/student_timeout_edge.py", expectedOutput: "Maximum timeout reached for job, job ran for 10s", - correctStatus: models.Failed, + correctStatus: jobs.Failed, }, "oom": { studentFile: "../../../example/simple-addition/student_oom.py", expectedOutput: "unable to parse log output", - correctStatus: models.Failed, + correctStatus: jobs.Failed, }, "forkb": { studentFile: "../../../example/simple-addition/student_fork_bomb.py", expectedOutput: `{"addition": {"passed": false, "message": "Addition test caused an error: [Errno 11] Resource temporarily unavailable"}, "subtraction": {"passed": true, "message": ""}, "multiplication": {"passed": false, "message": "Multiplication failed. Expected 42, got 48"}, "division": {"passed": false, "message": "Division failed. Expected 4, got 3.3333333333333335"}}`, - correctStatus: models.Complete, // job completes since we can parse the last line + correctStatus: jobs.Complete, // job completes since we can parse the last line }, } testFuncs = map[string]func(*testing.T){ @@ -112,7 +111,6 @@ func TestIncorrect(t *testing.T) { testJobProcessor(t, incorrect.studentFile, incorrect.expectedOutput, defaultTimeout, incorrect.correctStatus) } -// TODO func TestForkBomb(t *testing.T) { setupTest() forkBomb := testCases["forkb"] @@ -151,22 +149,22 @@ func TestCancel(t *testing.T) { jobTestService.CancelJob(jobId) }) - testJob(t, jobId, timeout.expectedOutput, models.Canceled) + testJob(t, jobId, timeout.expectedOutput, jobs.Canceled) // verify cancel function was removed from context map - _, ok := jobTestService.queue.contextMap.Load(jobId) - if ok { - t.Fatalf("Job was cancelled, but the cancel func was not nil") - } + //_, ok := jobTestService.queue.contextMap.Load(jobId) + //if ok { + // t.Fatalf("Job was cancelled, but the cancel func was not nil") + //} } -func testJobProcessor(t *testing.T, studentCodePath string, correctOutput string, timeout time.Duration, status models.JobStatus) { +func testJobProcessor(t *testing.T, studentCodePath string, correctOutput string, timeout time.Duration, status jobs.JobStatus) { jobId := setupJobProcess(t, studentCodePath, timeout) testJob(t, jobId, correctOutput, status) } func setupJobProcess(t *testing.T, studentCodePath string, timeout time.Duration) string { - labId := setupLab(t, &models.Lab{ + labId := setupLab(t, &labs.Lab{ Name: "test-lab", JobTimeout: timeout, JobEntryCmd: "make grade", @@ -180,7 +178,7 @@ func setupJobProcess(t *testing.T, studentCodePath string, timeout time.Duration if err != nil { t.Fatal("Error reading student", err) } - studentFileInfo := &FileInfo{ + studentFileInfo := &file_manager.FileInfo{ Reader: studentBytes, Filename: "student.py", } @@ -190,7 +188,7 @@ func setupJobProcess(t *testing.T, studentCodePath string, timeout time.Duration return "" } - newJob := &models.Job{LabID: labId} + newJob := &jobs.Job{LabID: labId} jobId, err := jobTestService.NewJob( newJob, tmpSubmissionFolder, @@ -204,8 +202,8 @@ func setupJobProcess(t *testing.T, studentCodePath string, timeout time.Duration return jobId } -func createLab(t *testing.T, labData *models.Lab, dockerfilePath string, files ...string) uint { - var labfiles []*FileInfo +func createLab(t *testing.T, labData *labs.Lab, dockerfilePath string, files ...string) uint { + var labfiles []*file_manager.FileInfo for _, file := range files { filename := filepath.Base(file) @@ -214,7 +212,7 @@ func createLab(t *testing.T, labData *models.Lab, dockerfilePath string, files . t.Fatal("Error reading ", filename, " ", err) } - labfiles = append(labfiles, &FileInfo{ + labfiles = append(labfiles, &file_manager.FileInfo{ Reader: fileBytes, Filename: filename, }) @@ -237,27 +235,36 @@ func createLab(t *testing.T, labData *models.Lab, dockerfilePath string, files . return labId } -func setupLab(t *testing.T, labData *models.Lab, dockerfilePath string, files ...string) uint { +func setupLab(t *testing.T, labData *labs.Lab, dockerfilePath string, files ...string) uint { labCreateOnce.Do(func() { createLabId = createLab(t, labData, dockerfilePath, files...) }) return createLabId } -func testJob(t *testing.T, jobId string, correctOutput string, correctStatus models.JobStatus) { - jobInfo, logs, err := jobTestService.WaitForJobAndLogs(jobId) +func testJob(t *testing.T, jobId string, correctOutput string, correctStatus jobs.JobStatus) { + jobInfo, returnedLogs, err := jobTestService.WaitForJobAndLogs(jobId) if err != nil { t.Fatalf("Error waiting for job: %v", err) return } - t.Log("Job ID: ", jobId, " Logs:\n", logs) + t.Log("Job ID: ", jobId, " Logs:\n", returnedLogs) returned := strings.TrimSpace(jobInfo.StatusMessage) expected := strings.TrimSpace(correctOutput) assert.Equal(t, expected, returned) assert.Equal(t, correctStatus, jobInfo.Status) + + db, err := jobTestService.GetJobFromDB(jobId) + if err != nil { + t.Fatal("Error getting job", err) + return + } + + expectedLogs := jobs.ReadLogFile(db.OutputLogFilePath) + assert.Equal(t, expectedLogs, returnedLogs) } func setupTest() { @@ -267,14 +274,19 @@ func setupTest() { } func initServices() { - common.InitConfig() - db, bc := common.InitDB() + cmd.Setup() + _, jobTestService, labTestService = cmd.InitServices() - dkTestService = docker.NewDockerServiceWithClients() - fileManTestService = NewFileManagerService() - labTestService = labs.NewLabService(db, dkTestService, fileManTestService) - jobTestService = NewJobService(db, bc, dkTestService, labTestService, fileManTestService) + //config.LoadConfig() + //db, bc := database.NewDatabaseWithGorm() + // + //dkTestService = docker.NewDockerServiceWithClients() + //fileManTestService = file_manager.NewFileManagerService() + //labTestService = labs.NewLabService(db, dkTestService, fileManTestService) + //jobTestService = jobs.NewJobService(db, bc, dkTestService, labTestService, fileManTestService) // no logs on tests - log.Logger = log.Logger.Level(zerolog.Disabled) + log.Logger = log.Logger.With().Logger() + + //log.Logger = log.Logger.Level(zerolog.Disabled) } diff --git a/src/internal/jobs/store.go b/src/internal/jobs/store.go new file mode 100644 index 0000000..10c8d8e --- /dev/null +++ b/src/internal/jobs/store.go @@ -0,0 +1,9 @@ +package jobs + +// JobStore contains the database interface needed by jobs +type JobStore interface { + CreateJob(job *Job) error + GetJobByUuid(jobUuid string) (*Job, error) + UpdateJob(job *Job) error + FetchInProgressJobs(result *[]Job) error +} diff --git a/src/service/jobs/job_utils.go b/src/internal/jobs/utils.go similarity index 100% rename from src/service/jobs/job_utils.go rename to src/internal/jobs/utils.go diff --git a/src/api/v1/lab_impl.go b/src/internal/labs/handler.go similarity index 61% rename from src/api/v1/lab_impl.go rename to src/internal/labs/handler.go index d6f262f..8998f6d 100644 --- a/src/api/v1/lab_impl.go +++ b/src/internal/labs/handler.go @@ -1,21 +1,19 @@ -package v1 +package labs import ( "connectrpc.com/connect" "context" "fmt" - v1 "github.com/makeopensource/leviathan/generated/labs/v1" - typesv1 "github.com/makeopensource/leviathan/generated/types/v1" - "github.com/makeopensource/leviathan/models" - "github.com/makeopensource/leviathan/service/labs" + labrpc "github.com/makeopensource/leviathan/generated/labs/v1" + rpctypes "github.com/makeopensource/leviathan/generated/types/v1" "time" ) type LabServer struct { - Srv *labs.LabService + Srv *LabService } -func (l LabServer) NewLab(ctx context.Context, req *connect.Request[v1.NewLabRequest]) (*connect.Response[v1.NewLabResponse], error) { +func (l LabServer) NewLab(ctx context.Context, req *connect.Request[labrpc.NewLabRequest]) (*connect.Response[labrpc.NewLabResponse], error) { if req.Msg.LabData.Labname == "" { return nil, fmt.Errorf("lab name is required") } else if req.Msg.LabData.JobTimeoutInSeconds == 0 { @@ -24,10 +22,10 @@ func (l LabServer) NewLab(ctx context.Context, req *connect.Request[v1.NewLabReq return nil, fmt.Errorf("tmp folder id is required") } - lab := &models.Lab{ + lab := &Lab{ Name: req.Msg.LabData.Labname, JobTimeout: time.Duration(req.Msg.LabData.JobTimeoutInSeconds) * time.Second, - JobLimits: models.MachineLimits{ + JobLimits: MachineLimits{ PidsLimit: int64(req.Msg.LabData.Limits.PidLimit), NanoCPU: int64(req.Msg.LabData.Limits.CPUCores), Memory: int64(req.Msg.LabData.Limits.MemoryInMb), @@ -41,16 +39,16 @@ func (l LabServer) NewLab(ctx context.Context, req *connect.Request[v1.NewLabReq return nil, err } - res := connect.NewResponse(&v1.NewLabResponse{LabId: int64(labID)}) + res := connect.NewResponse(&labrpc.NewLabResponse{LabId: int64(labID)}) return res, nil } -func (l LabServer) EditLab(ctx context.Context, c *connect.Request[typesv1.LabData]) (*connect.Response[v1.EditLabResponse], error) { - //TODO implement me - panic("implement me") +func (l LabServer) EditLab(ctx context.Context, c *connect.Request[rpctypes.LabData]) (*connect.Response[labrpc.EditLabResponse], error) { + // todo + return nil, fmt.Errorf("unimplmented") } -func (l LabServer) DeleteLab(ctx context.Context, req *connect.Request[v1.DeleteLabRequest]) (*connect.Response[v1.DeleteLabResponse], error) { - - return nil, fmt.Errorf("unimplmented methods") +func (l LabServer) DeleteLab(ctx context.Context, req *connect.Request[labrpc.DeleteLabRequest]) (*connect.Response[labrpc.DeleteLabResponse], error) { + // todo + return nil, fmt.Errorf("unimplmented") } diff --git a/src/models/lab.go b/src/internal/labs/models.go similarity index 98% rename from src/models/lab.go rename to src/internal/labs/models.go index 6acc669..94b67a4 100644 --- a/src/models/lab.go +++ b/src/internal/labs/models.go @@ -1,4 +1,4 @@ -package models +package labs import ( "gorm.io/gorm" diff --git a/src/service/labs/lab_service.go b/src/internal/labs/service.go similarity index 50% rename from src/service/labs/lab_service.go rename to src/internal/labs/service.go index 90ddb45..89ed251 100644 --- a/src/service/labs/lab_service.go +++ b/src/internal/labs/service.go @@ -2,24 +2,24 @@ package labs import ( "fmt" - com "github.com/makeopensource/leviathan/common" - "github.com/makeopensource/leviathan/models" - "github.com/makeopensource/leviathan/service/docker" - fm "github.com/makeopensource/leviathan/service/file_manager" + "github.com/makeopensource/leviathan/internal/config" + "github.com/makeopensource/leviathan/internal/docker" + fm "github.com/makeopensource/leviathan/internal/file_manager" + "github.com/makeopensource/leviathan/pkg/file_utils" + "github.com/makeopensource/leviathan/pkg/logger" "github.com/rs/zerolog/log" - "gorm.io/gorm" "os" "path/filepath" "strings" ) type LabService struct { - db *gorm.DB + db LabStore dk *docker.DkService fileMan *fm.FileManagerService } -func NewLabService(db *gorm.DB, dk *docker.DkService, service *fm.FileManagerService) *LabService { +func NewLabService(db LabStore, dk *docker.DkService, service *fm.FileManagerService) *LabService { return &LabService{ db: db, dk: dk, @@ -27,7 +27,7 @@ func NewLabService(db *gorm.DB, dk *docker.DkService, service *fm.FileManagerSer } } -func (service *LabService) CreateLab(lab *models.Lab, jobDirId string) (uint, error) { +func (service *LabService) CreateLab(lab *Lab, jobDirId string) (uint, error) { tmpDir, err := service.fileMan.GetLabFilePaths(jobDirId) if err != nil { return 0, err @@ -35,17 +35,17 @@ func (service *LabService) CreateLab(lab *models.Lab, jobDirId string) (uint, er defer service.fileMan.DeleteFolder(jobDirId) jobFolderName := fmt.Sprintf("%s_%s", lab.Name, jobDirId) - jobDataDirPath := fmt.Sprintf("%s/%s", com.LabsFolder.GetStr(), jobFolderName) - if err = os.MkdirAll(jobDataDirPath, com.DefaultFilePerm); err != nil { - return 0, com.ErrLog( + jobDataDirPath := fmt.Sprintf("%s/%s", config.LabsFolder.GetStr(), jobFolderName) + if err = os.MkdirAll(jobDataDirPath, config.DefaultFilePerm); err != nil { + return 0, logger.ErrLog( "unable to create directories for lab: "+lab.Name, err, log.Error(), ) } - if err = com.HardLinkFolder(tmpDir, jobDataDirPath); err != nil { - return 0, com.ErrLog("unable to copy files to job dir", err, log.Error()) + if err = file_utils.HardLinkFolder(tmpDir, jobDataDirPath); err != nil { + return 0, logger.ErrLog("unable to copy files to job dir", err, log.Error()) } lab.DockerFilePath = filepath.Join(jobDataDirPath, fm.DockerfileName) @@ -55,24 +55,23 @@ func (service *LabService) CreateLab(lab *models.Lab, jobDirId string) (uint, er lab.ImageTag = strings.ToLower(strings.Trim(strings.TrimSpace(lab.ImageTag), " ")) if lab.AutolabCompatible { - lab.JobEntryCmd = CreateTangoEntryCommand( + lab.JobEntryCmd = createTangoEntryCommand( WithTimeout(int(lab.JobTimeout.Seconds())), ) } else { - lab.JobEntryCmd = CreateLeviathanEntryCommand(lab.JobEntryCmd) + lab.JobEntryCmd = createLeviathanEntryCommand(lab.JobEntryCmd) } // final save to update paths - db, err := service.SaveLabToDB(lab) - if err != nil { + if err = service.db.CreateLab(lab); err != nil { return 0, err } - return db.ID, nil + return lab.ID, nil } -func (service *LabService) EditLab(id uint, lab *models.Lab, jobFiles string) (uint, error) { - labData, err := service.GetLabFromDB(id) +func (service *LabService) EditLab(id uint, lab *Lab, jobFiles string) (uint, error) { + labData, err := service.db.GetLab(id) if err != nil { return 0, err } @@ -91,7 +90,7 @@ func (service *LabService) EditLab(id uint, lab *models.Lab, jobFiles string) (u } func (service *LabService) DeleteLab(id uint) error { - labData, err := service.GetLabFromDB(id) + labData, err := service.db.GetLab(id) if err != nil { return err } @@ -101,17 +100,17 @@ func (service *LabService) DeleteLab(id uint) error { return err } - if res := service.db.Delete(&models.Lab{}, id); res.Error != nil { - return res.Error + if err = service.db.DeleteLab(id); err != nil { + return err } return nil } -func (service *LabService) deleteLabFiles(labData *models.Lab) error { +func (service *LabService) deleteLabFiles(labData *Lab) error { err := os.RemoveAll(filepath.Base(labData.DockerFilePath)) if err != nil { - return com.ErrLog( + return logger.ErrLog( "unable to delete directories for lab: "+labData.Name, err, log.Error(), @@ -120,19 +119,6 @@ func (service *LabService) deleteLabFiles(labData *models.Lab) error { return nil } -func (service *LabService) GetLabFromDB(id uint) (*models.Lab, error) { - var lab models.Lab - if res := service.db.Where("ID = ?", id).First(&lab); res.Error != nil { - return nil, res.Error - } - return &lab, nil -} - -func (service *LabService) SaveLabToDB(lab *models.Lab) (*models.Lab, error) { - res := service.db.Save(lab) - if res.Error != nil { - return nil, res.Error - } - - return lab, nil +func (service *LabService) GetLabFromDB(id uint) (*Lab, error) { + return service.db.GetLab(id) } diff --git a/src/service/labs/lab_service_test.go b/src/internal/labs/service_test.go similarity index 74% rename from src/service/labs/lab_service_test.go rename to src/internal/labs/service_test.go index cf5d302..bf1b522 100644 --- a/src/service/labs/lab_service_test.go +++ b/src/internal/labs/service_test.go @@ -1,10 +1,9 @@ package labs import ( - "github.com/makeopensource/leviathan/common" - "github.com/makeopensource/leviathan/models" - "github.com/makeopensource/leviathan/service/docker" - . "github.com/makeopensource/leviathan/service/file_manager" + "github.com/makeopensource/leviathan/cmd" + fm "github.com/makeopensource/leviathan/internal/file_manager" + "github.com/makeopensource/leviathan/pkg/file_utils" "github.com/rs/zerolog" "github.com/rs/zerolog/log" "os" @@ -15,8 +14,7 @@ import ( ) var ( - dkTestService *docker.DkService - fileMan *FileManagerService + fileMan *fm.FileManagerService labTestService *LabService setupOnce sync.Once ) @@ -46,7 +44,7 @@ func TestLabService_CreateLab(t *testing.T) { return } - files := []*FileInfo{ + files := []*fm.FileInfo{ { Reader: makefileBytes, Filename: filepath.Base(makeFilePath), @@ -63,7 +61,7 @@ func TestLabService_CreateLab(t *testing.T) { return } - lab := models.Lab{ + lab := Lab{ Name: "test-lab", JobTimeout: time.Second * 10, ImageTag: "test-lab:v1", @@ -84,11 +82,11 @@ func TestLabService_CreateLab(t *testing.T) { return } - if !common.FileExists(labDta.JobFilesDirPath) { + if !file_utils.FileExists(labDta.JobFilesDirPath) { t.Fatalf("Job files dir does not exist") return } - if !common.FileExists(labDta.DockerFilePath) { + if !file_utils.FileExists(labDta.DockerFilePath) { t.Fatalf("Dockerfile does not exist") return } @@ -96,12 +94,9 @@ func TestLabService_CreateLab(t *testing.T) { func initDeps() { setupOnce.Do(func() { - common.InitConfig() - db, _ := common.InitDB() - - dkTestService = docker.NewDockerServiceWithClients() - fileMan = NewFileManagerService() - labTestService = NewLabService(db, dkTestService, fileMan) + cmd.Setup() + _, _, labTestService = cmd.InitServices() + fileMan = fm.NewFileManagerService() // no logs on tests log.Logger = log.Logger.Level(zerolog.Disabled) diff --git a/src/internal/labs/store.go b/src/internal/labs/store.go new file mode 100644 index 0000000..1df086e --- /dev/null +++ b/src/internal/labs/store.go @@ -0,0 +1,7 @@ +package labs + +type LabStore interface { + CreateLab(lab *Lab) error + DeleteLab(id uint) error + GetLab(id uint) (*Lab, error) +} diff --git a/src/service/labs/lab_utils.go b/src/internal/labs/utils.go similarity index 88% rename from src/service/labs/lab_utils.go rename to src/internal/labs/utils.go index da71814..c096c1f 100644 --- a/src/service/labs/lab_utils.go +++ b/src/internal/labs/utils.go @@ -30,14 +30,14 @@ var ( } ) -// CreateLeviathanEntryCommand command for normal graders made for leviathan -func CreateLeviathanEntryCommand(cmd string) string { +// createLeviathanEntryCommand command for normal graders made for leviathan +func createLeviathanEntryCommand(cmd string) string { return "cd autolab;" + cmd } -// CreateTangoEntryCommand builds the autodriver command for tango compatibility +// createTangoEntryCommand builds the autodriver command for tango compatibility // defaults to DefaultAutoDriverLimits, if no limits is passed -func CreateTangoEntryCommand(opts ...LimitOption) string { +func createTangoEntryCommand(opts ...LimitOption) string { limits := &DefaultAutoDriverLimits // Start with default limits for _, opt := range opts { // Apply all options opt(limits) diff --git a/src/main.go b/src/main.go deleted file mode 100644 index 2ae237f..0000000 --- a/src/main.go +++ /dev/null @@ -1,14 +0,0 @@ -package main - -import ( - "github.com/makeopensource/leviathan/api" - "github.com/makeopensource/leviathan/common" - "github.com/rs/zerolog/log" -) - -func main() { - common.PrintInfo() - log.Logger = common.ConsoleLogger() - common.InitConfig() - api.StartGrpcServer() -} diff --git a/src/models/job_error.go b/src/models/job_error.go deleted file mode 100644 index a5a967e..0000000 --- a/src/models/job_error.go +++ /dev/null @@ -1,35 +0,0 @@ -package models - -type JobError interface { - // Reason will be displayed to the end user, providing a user-friendly message. - Reason() string - // Err err parameter holds the underlying error, used for debugging purposes. - Err() error - // ErrStr returns string from the error, if nil return empty string - ErrStr() string -} - -// JErr implements JobError -type JErr struct { - reason string - err error -} - -func JError(reason string, err error) JErr { - return JErr{reason: reason, err: err} -} - -func (err JErr) Reason() string { - return err.reason -} - -func (err JErr) Err() error { - return err.err -} - -func (err JErr) ErrStr() string { - if err.err != nil { - return err.err.Error() - } - return "" -} diff --git a/src/pkg/file_utils/file.go b/src/pkg/file_utils/file.go new file mode 100644 index 0000000..96d9a8b --- /dev/null +++ b/src/pkg/file_utils/file.go @@ -0,0 +1,8 @@ +package file_utils + +import "os" + +func FileExists(filename string) bool { + _, err := os.Stat(filename) + return !os.IsNotExist(err) +} diff --git a/src/common/utils.go b/src/pkg/file_utils/hardlink.go similarity index 52% rename from src/common/utils.go rename to src/pkg/file_utils/hardlink.go index b90dfa1..3b6b0df 100644 --- a/src/common/utils.go +++ b/src/pkg/file_utils/hardlink.go @@ -1,41 +1,53 @@ -package common +package file_utils import ( "fmt" - "github.com/rs/zerolog/log" "io" "io/fs" + "mime" + "net/http" "os" + "path" "path/filepath" + "runtime" ) -func FileExists(filename string) bool { - _, err := os.Stat(filename) - return !os.IsNotExist(err) -} +var DefaultFilePerm = 0o775 -func CloseFile(file io.ReadCloser) { - err := file.Close() - if err != nil { - log.Warn().Err(err).Msg("Error occurred while closing file") +// RecursiveChown chowns the file/folder recursively at path +// +// Chowning is not supported on windows, no action will be taken if called on windows +func RecursiveChown(path string, uid, gid int) error { + if runtime.GOOS == "windows" { + return nil } + // Walk the directory tree + return filepath.Walk(path, func(name string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + // Change ownership of the current file/directory + err = os.Chown(name, uid, gid) + if err != nil { + return fmt.Errorf("failed to chown %s: %v", name, err) + } + + return nil + }) } // HardLinkFolder creates hard links of all files from source folder to target folder // and maintains the original UID/GID func HardLinkFolder(sourceDir, targetDir string) error { - if sourceDir == "" || targetDir == "" { - log.Warn().Msg("Source/target directory is empty") - return nil - } - - sourceStat, err := os.Stat(sourceDir) // Check if source directory exists + // Check if source directory exists + sourceStat, err := os.Stat(sourceDir) if err != nil { return fmt.Errorf("source directory error: %w", err) } - // Create target directory if it doesn't exist - err = os.MkdirAll(targetDir, DefaultFilePerm) + // CreateJob target directory if it doesn't exist + err = os.MkdirAll(targetDir, os.FileMode(DefaultFilePerm)) if err != nil { return fmt.Errorf("failed to create target directory: %w", err) } @@ -44,12 +56,9 @@ func HardLinkFolder(sourceDir, targetDir string) error { if !sourceStat.IsDir() { // For single file, create the parent directory if needed sourceFile := filepath.Base(sourceDir) - log.Debug().Msgf("Target path is: %s", sourceFile) - targetDir = fmt.Sprintf("%s/%s", targetDir, sourceFile) - log.Info().Err(err).Msgf("Source is a file, final dest path will be %s", targetDir) - // Create hard link for the file + // CreateJob hard link for the file if err := createHardLink(sourceDir, targetDir, sourceStat.Mode()); err != nil { return fmt.Errorf("failed to create hard link from %s to %s: %w", sourceDir, targetDir, err) } @@ -80,14 +89,14 @@ func HardLinkFolder(sourceDir, targetDir string) error { // If it's a directory, create it in target with proper ownership if d.IsDir() { - if err := os.MkdirAll(targetPath, DefaultFilePerm); err != nil { + if err := os.MkdirAll(targetPath, os.FileMode(DefaultFilePerm)); err != nil { return fmt.Errorf("failed to create directory %s: %w", targetPath, err) } return nil } - // Create hard link for files + // CreateJob hard link for files err = createHardLink(path, targetPath, info.Mode()) if err != nil { return fmt.Errorf("failed to create hard link from %s to %s: %w", path, targetPath, err) @@ -97,6 +106,50 @@ func HardLinkFolder(sourceDir, targetDir string) error { }) } +func DownloadTorrentFile(downloadLink, downloadPath string) (string, error) { + resp, err := http.Get(downloadLink) + if err != nil { + return "", fmt.Errorf("failed to make request: %v", err) + } + if resp.StatusCode != 200 { + return "", fmt.Errorf("invalid http code: %d, reason: %s", resp.StatusCode, resp.Status) + } + + defer func(Body io.ReadCloser) { + _ = Body.Close() + }(resp.Body) + + // Get filename from Content-Disposition header + _, params, err := mime.ParseMediaType(resp.Header.Get("Content-Disposition")) + filename := "" + if err == nil && params["filename"] != "" { + filename = params["filename"] + } else { + // Fallback to URL path if header not available + filename = path.Base(downloadLink) + } + + if err := os.MkdirAll(downloadPath, 0775); err != nil { + return "", fmt.Errorf("failed to create directories: %v", err) + } + + destPath := fmt.Sprintf("%s/%s", downloadPath, filename) + file, err := os.Create(destPath) + if err != nil { + return "", fmt.Errorf("failed to create file: %v", err) + } + defer func(file *os.File) { + _ = file.Close() + }(file) + + _, err = io.Copy(file, resp.Body) + if err != nil { + return "", fmt.Errorf("failed to copy response body: %v", err) + } + + return destPath, nil +} + // createHardLink creates a hard link with proper ownership and permissions func createHardLink(oldPath, newPath string, mode fs.FileMode) error { // Ensure the target directory exists @@ -110,7 +163,7 @@ func createHardLink(oldPath, newPath string, mode fs.FileMode) error { return err } - // Create the hard link + // CreateJob the hard link if err := os.Link(oldPath, newPath); err != nil { return err } diff --git a/src/common/logger.go b/src/pkg/logger/logger.go similarity index 91% rename from src/common/logger.go rename to src/pkg/logger/logger.go index 441542d..176cd8d 100644 --- a/src/common/logger.go +++ b/src/pkg/logger/logger.go @@ -1,4 +1,4 @@ -package common +package logger import ( "context" @@ -33,8 +33,8 @@ func CreateJobSubLoggerCtx(ctx context.Context, jobID string) context.Context { return log.Logger.With().Str(JobLogKey, jobID).Logger().WithContext(ctx) } -func FileConsoleLogger() zerolog.Logger { - level, err := zerolog.ParseLevel(LogLevel.GetStr()) +func FileConsoleLogger(logDir, logLevel string) zerolog.Logger { + level, err := zerolog.ParseLevel(logLevel) if err != nil { log.Fatal().Err(err).Msg("unable to parse log level") } @@ -42,7 +42,7 @@ func FileConsoleLogger() zerolog.Logger { return getBaseLogger().Output( zerolog.MultiLevelWriter( - GetFileLogger(LogDir.GetStr()), + GetFileLogger(logDir), getConsoleWriter(), ), ).Level(level) diff --git a/src/models/counting_mutex.go b/src/pkg/sync_utils/counting_mutex.go similarity index 97% rename from src/models/counting_mutex.go rename to src/pkg/sync_utils/counting_mutex.go index 96f1bac..1644ae3 100644 --- a/src/models/counting_mutex.go +++ b/src/pkg/sync_utils/counting_mutex.go @@ -1,4 +1,4 @@ -package models +package sync_utils import ( "sync" diff --git a/src/models/sync_map.go b/src/pkg/sync_utils/sync_map.go similarity index 97% rename from src/models/sync_map.go rename to src/pkg/sync_utils/sync_map.go index 6eb8415..58d647e 100644 --- a/src/models/sync_map.go +++ b/src/pkg/sync_utils/sync_map.go @@ -1,4 +1,4 @@ -package models +package sync_utils import "sync" diff --git a/src/models/worker_semaphore.go b/src/pkg/sync_utils/worker_semaphore.go similarity index 96% rename from src/models/worker_semaphore.go rename to src/pkg/sync_utils/worker_semaphore.go index 5b42dea..a2cb0d3 100644 --- a/src/models/worker_semaphore.go +++ b/src/pkg/sync_utils/worker_semaphore.go @@ -1,4 +1,4 @@ -package models +package sync_utils type WorkerSemaphore struct { semaphore chan struct{} diff --git a/src/service/docker/docker_service.go b/src/service/docker/docker_service.go deleted file mode 100644 index 8e3ab5f..0000000 --- a/src/service/docker/docker_service.go +++ /dev/null @@ -1,36 +0,0 @@ -package docker - -import ( - "context" - "fmt" - "github.com/rs/zerolog/log" -) - -type DkService struct { - ClientManager *RemoteClientManager -} - -func NewDockerService(clientList *RemoteClientManager) *DkService { - return &DkService{ClientManager: clientList} -} - -func NewDockerServiceWithClients() *DkService { - return &DkService{ClientManager: NewRemoteClientManager()} -} - -func (service *DkService) BuildNewImageOnAllClients(dockerfilePath string, imageTag string) error { - for _, item := range service.ClientManager.Clients { - err := item.Client.BuildImageFromDockerfile(dockerfilePath, imageTag) - if err != nil { - info, err := item.Client.Client.Info(context.Background()) - if err != nil { - log.Error().Err(err).Msg("failed to get server info") - return fmt.Errorf("failed to get server info") - } - log.Error().Err(err).Msgf("Error building image for %s", info.Name) - return fmt.Errorf("failed to create image for client") - } - } - - return nil -} diff --git a/src/service/jobs/job_service_stream_test.go b/src/service/jobs/job_service_stream_test.go deleted file mode 100644 index ac3f9f7..0000000 --- a/src/service/jobs/job_service_stream_test.go +++ /dev/null @@ -1,66 +0,0 @@ -package jobs - -import ( - "github.com/google/uuid" - "github.com/makeopensource/leviathan/models" - "github.com/stretchr/testify/assert" - "math/rand/v2" - "testing" - "time" -) - -var statusList = []models.JobStatus{models.Complete, models.Failed, models.Canceled} - -// TODO -func TestBroadcastJobs(t *testing.T) { - setupTest() - - numJobs := 5 - var jobList []*models.Job - - for i := 0; i < numJobs; i++ { - jobList = append(jobList, &models.Job{JobId: uuid.New().String()}) - } - - // run multiple listeners for the same job - for _, job := range jobList { - t.Run(job.JobId, func(t *testing.T) { - t.Parallel() - - time.AfterFunc(1*time.Second, func() { - job.StatusMessage = "changing job status" - job.Status = models.Queued - jobTestService.db.Model(job).Save(job) - }) - - time.AfterFunc(2*time.Second, func() { - job.StatusMessage = "changing job status" - job.Status = models.Running - jobTestService.db.Model(job).Save(job) - }) - - time.AfterFunc(4*time.Second, func() { - job.StatusMessage = "changing job" - // random 'finished' status - job.Status = statusList[rand.IntN(len(statusList))] - jobTestService.db.Model(job).Save(job) - }) - - jobCh := jobTestService.SubToJob(job.JobId) - - for { - select { - case jobFromCh, ok := <-jobCh: - if ok { - assert.Equal(t, job.Status, jobFromCh.Status) - assert.Equal(t, job.StatusMessage, jobFromCh.StatusMessage) - } else { - continue - } - case <-time.After(15 * time.Second): - t.Fatal("timed out waiting for job status to change") - } - } - }) - } -} diff --git a/src/service/labs/lab_service_utils_test.go b/src/service/labs/lab_service_utils_test.go deleted file mode 100644 index 286c6db..0000000 --- a/src/service/labs/lab_service_utils_test.go +++ /dev/null @@ -1 +0,0 @@ -package labs diff --git a/src/service/service.go b/src/service/service.go deleted file mode 100644 index aab2f27..0000000 --- a/src/service/service.go +++ /dev/null @@ -1,20 +0,0 @@ -package service - -import ( - "github.com/makeopensource/leviathan/common" - "github.com/makeopensource/leviathan/service/docker" - "github.com/makeopensource/leviathan/service/file_manager" - "github.com/makeopensource/leviathan/service/jobs" - "github.com/makeopensource/leviathan/service/labs" -) - -func InitServices() (*docker.DkService, *jobs.JobService, *labs.LabService) { - db, bc := common.InitDB() - - dkService := docker.NewDockerServiceWithClients() - fileManService := file_manager.NewFileManagerService() - labService := labs.NewLabService(db, dkService, fileManService) - jobService := jobs.NewJobService(db, bc, dkService, labService, fileManService) - - return dkService, jobService, labService -}