├── .gitignore ├── LICENSE ├── README.md ├── chart ├── .helmignore ├── Chart.lock ├── Chart.yaml ├── templates │ ├── auth.yaml │ ├── executor.yaml │ ├── frontend.yaml │ ├── problems.yaml │ └── submissions.yaml └── values.yaml ├── cmd ├── auth │ ├── Dockerfile │ └── main.go ├── executor │ ├── Dockerfile │ ├── image.tar │ └── main.go ├── frontend │ ├── Dockerfile │ ├── auth.go │ ├── main.go │ ├── problems.go │ ├── static │ │ ├── css │ │ │ └── common.css │ │ └── js │ │ │ ├── edit.js │ │ │ ├── problem.js │ │ │ └── problems.js │ └── templates │ │ ├── create.tmpl │ │ ├── edit.tmpl │ │ ├── index.tmpl │ │ ├── login.tmpl │ │ ├── problem.tmpl │ │ ├── problems.tmpl │ │ └── register.tmpl ├── problems │ ├── Dockerfile │ ├── admin.go │ └── main.go └── submissions │ ├── Dockerfile │ └── main.go ├── docker-compose.yml ├── go.mod ├── go.sum ├── images └── Noob Architecture.svg └── pkg ├── database ├── database.go ├── errors.go ├── problems.go └── users.go ├── message ├── pubsub.go ├── queue.go └── rabbit.go ├── model ├── problem.go ├── submission.go └── user.go ├── sessions ├── middleware.go └── sessions.go └── tracing └── tracing.go /.gitignore: -------------------------------------------------------------------------------- 1 | *.tgz 2 | *.swp 3 | .idea 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Kevin Zheng 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Noob 2 | A scalable distributed microservice architecture platform running on Kubernetes capable of judging and orchestrating the execution of user submitted code. 3 |

WIP Demo Video

4 | 5 | 6 | Screen Shot 2020-10-08 at 4 51 09 AM 7 | 8 | 9 | #### Roadmap 10 | - Set up Kubernetes Cluster. 11 | * Set up rook helm chart for persistent volumes. (✅ 8/5/18) 12 | * ^ decided to do this w/o Helm. 13 | * Set up redis helm chart for sessions. (✅ 8/5/18) 14 | * Set up mongodb helm chart. (✅ 8/5/18) 15 | * Se up rabbitmq helm chart for code execution queue. 16 | - Set up auth microservice. 17 | * Add Dockerfile. (✅ 8/10/18) 18 | * Integrate into Kubernetes/Helm: 19 | * Create deployment. (✅ 8/11/18) 20 | * Create service. (✅ 8/11/18) 21 | * Set up /auth/ with nginx ingress. (✅ 8/19/18) 22 | * Connect to redis. (✅ 8/12/18) 23 | * Connect to mongodb. (✅ 8/13/18) 24 | * Endpoints: 25 | * Login - authenticate username + password with mongodb,
 create session in redis. 
 (✅ 8/17/18) 26 | * Logout - destroy session in redis. (✅ 8/17/18) 27 | * Register - store username + hashed password in mongodb. (✅ 8/17/18) 28 | - Set up frontend microservice. 29 | * Add Dockerfile. (✅ 8/28/18) 30 | * Integrate into Kubernetes/Helm: 31 | * Create deployment. (✅ 8/28/18) 32 | * Create service. (✅ 8/28/18) 33 | * Set up / with nginx ingress. (✅ 8/28/18) 34 | * Create mock authentication page for signing up and logging in. (✅ 8/28/18) 35 | - Set up admin microservice. 36 | * Add Dockerfile. (✅ 9/5/18) 37 | * Integrate into Kubernetes/Helm: 38 | * Create deployment. (✅ 9/5/18) 39 | * Create service. (✅ 9/5/18) 40 | * Set up /admin/ nginx ingress. (✅ 9/5/18) 41 | * Endpoints: 42 | * Create Problem - store problem in mongodb (✅ 9/5/18) 43 | * Update Problem - update problem in mongodb (✅ 9/5/18) 44 | * Delete Problem - delete problem in mongodb (✅ 9/5/18) 45 | * Frontend UI 46 | - next: tbd… 47 | 48 | ### Development 49 | Some commands to know :P . 50 | **Accessing MongoDB**: 51 | ``` 52 | $ kubectl get secret noob-mongodb -o jsonpath="{.data.mongodb-root-password}" | base64 --decode 53 | $ kubectl run -i -t --rm debug --image=ubuntu --restart=Never 54 | $ apt-get update && apt-get install mongodb 55 | $ mongo --host noob-mongodb --port 27017 -u root -p admin 56 | ``` 57 | * Changing user’s role: 58 | ``` 59 | db.users.findAndModify({ 60 | query: {username: }, 61 | update: {$set: {role: }}, 62 | new: true, 63 | }) 64 | ``` 65 | 66 | **Updating Microservices**: 67 | * Entire System: 68 | ``` 69 | $ docker-compose build && docker-compose push 70 | $ helm delete --purge noob 71 | $ helm install --namespace noob --name noob ./chart/ 72 | ``` 73 | * Single Microservice: 74 | ``` 75 | $ docker-compose build && docker-compose push 76 | $ kubectl get pods 77 | $ kubectl delete pod 78 | ``` 79 | 80 | #### Personal Notes 81 | - Helm update will mess up the redis k8s secret since the secret does not update while the redis password will. The solution to this is just do a hard delete and install when updating the entire chart. (Temp fixed) 82 | 83 | #### Sidetrack 84 | - Set up continuous integration and deployment. 85 | * Possibly with Google Cloud Build or Concourse? 86 | - Make sure to have high quality documentation! 87 | - Also test, test, test! 88 | - Deploy to Google Cloud Platform or Amazon Web Services. Currently running my own Kubernetes cluster on OVH vps. 89 | -------------------------------------------------------------------------------- /chart/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /chart/Chart.lock: -------------------------------------------------------------------------------- 1 | dependencies: 2 | - name: redis 3 | repository: https://charts.bitnami.com/bitnami 4 | version: 10.7.11 5 | - name: mongodb 6 | repository: https://charts.bitnami.com/bitnami 7 | version: 8.2.1 8 | - name: postgresql 9 | repository: https://charts.bitnami.com/bitnami 10 | version: 9.1.1 11 | - name: rabbitmq 12 | repository: https://charts.bitnami.com/bitnami 13 | version: 7.5.6 14 | digest: sha256:27c8d9c1c98b25f206f478c037c8b95eb9ce9c961c9f6aad8a777f175477a7b6 15 | generated: "2020-07-25T19:43:04.922900582Z" 16 | -------------------------------------------------------------------------------- /chart/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: noob 3 | description: Code judge platform on Kubernetes 4 | type: application 5 | home: https://github.com/kzh/noob 6 | dependencies: 7 | - name: redis 8 | version: 10.7.11 9 | repository: https://charts.bitnami.com/bitnami 10 | alias: sessions 11 | - name: mongodb 12 | version: 8.2.1 13 | repository: https://charts.bitnami.com/bitnami 14 | - name: postgresql 15 | version: 9.1.1 16 | repository: https://charts.bitnami.com/bitnami 17 | - name: rabbitmq 18 | version: 7.5.6 19 | repository: https://charts.bitnami.com/bitnami 20 | version: 0.1.0 21 | appVersion: 0.1.0 22 | -------------------------------------------------------------------------------- /chart/templates/auth.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ .Release.Name }}-auth 5 | labels: 6 | app: {{ .Release.Name }}-auth 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: {{ .Release.Name }}-auth 12 | template: 13 | metadata: 14 | labels: 15 | app: {{ .Release.Name }}-auth 16 | spec: 17 | containers: 18 | - name: {{ .Release.Name }}-auth 19 | image: uhkevin/noob-auth 20 | env: 21 | - name: SESSIONS_PASSWORD 22 | valueFrom: 23 | secretKeyRef: 24 | name: noob-sessions 25 | key: redis-password 26 | - name: MONGODB_PASSWORD 27 | valueFrom: 28 | secretKeyRef: 29 | name: noob-mongodb 30 | key: mongodb-root-password 31 | ports: 32 | - containerPort: 8080 33 | --- 34 | apiVersion: v1 35 | kind: Service 36 | metadata: 37 | name: {{ .Release.Name }}-auth 38 | spec: 39 | selector: 40 | app: {{ .Release.Name }}-auth 41 | ports: 42 | - protocol: TCP 43 | port: 8080 44 | targetPort: 8080 45 | -------------------------------------------------------------------------------- /chart/templates/executor.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ .Release.Name }}-executor 5 | labels: 6 | app: {{ .Release.Name }}-executor 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: {{ .Release.Name }}-executor 12 | template: 13 | metadata: 14 | labels: 15 | app: {{ .Release.Name }}-executor 16 | spec: 17 | containers: 18 | - name: {{ .Release.Name }}-executor 19 | image: uhkevin/noob-executor 20 | env: 21 | - name: RABBITMQ_PASSWORD 22 | valueFrom: 23 | secretKeyRef: 24 | name: noob-rabbitmq 25 | key: rabbitmq-password 26 | - name: MONGODB_PASSWORD 27 | valueFrom: 28 | secretKeyRef: 29 | name: noob-mongodb 30 | key: mongodb-root-password 31 | - name: DOCKER_HOST 32 | value: "tcp://localhost:2375" 33 | - name: DOCKER_API_VERSION 34 | value: "1.38" 35 | - name: JAEGER_SERVICE_NAME 36 | value: "noob-executor" 37 | - name: JAEGER_ENDPOINT 38 | value: "http://jaeger-collector.observability.svc.cluster.local:14268/api/traces" 39 | - name: JAEGER_SAMPLER_TYPE 40 | value: "const" 41 | - name: JAEGER_SAMPLER_PARAM 42 | value: "1" 43 | - name: JAEGER_REPORTER_LOG_SPANS 44 | value: "true" 45 | - name: {{ .Release.Name }}-dockerd 46 | image: docker:18.06-dind 47 | securityContext: 48 | privileged: true 49 | -------------------------------------------------------------------------------- /chart/templates/frontend.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ .Release.Name }}-frontend 5 | labels: 6 | app: {{ .Release.Name }}-frontend 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: {{ .Release.Name }}-frontend 12 | template: 13 | metadata: 14 | labels: 15 | app: {{ .Release.Name }}-frontend 16 | spec: 17 | containers: 18 | - name: {{ .Release.Name }}-frontend 19 | image: uhkevin/noob-frontend 20 | env: 21 | - name: SESSIONS_PASSWORD 22 | valueFrom: 23 | secretKeyRef: 24 | name: noob-sessions 25 | key: redis-password 26 | ports: 27 | - containerPort: 8080 28 | --- 29 | apiVersion: v1 30 | kind: Service 31 | metadata: 32 | name: {{ .Release.Name }}-frontend 33 | spec: 34 | selector: 35 | app: {{ .Release.Name }}-frontend 36 | ports: 37 | - protocol: TCP 38 | port: 8080 39 | targetPort: 8080 40 | -------------------------------------------------------------------------------- /chart/templates/problems.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ .Release.Name }}-problems 5 | labels: 6 | app: {{ .Release.Name }}-problems 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: {{ .Release.Name }}-problems 12 | template: 13 | metadata: 14 | labels: 15 | app: {{ .Release.Name }}-problems 16 | spec: 17 | containers: 18 | - name: {{ .Release.Name }}-problems 19 | image: uhkevin/noob-problems 20 | env: 21 | - name: SESSIONS_PASSWORD 22 | valueFrom: 23 | secretKeyRef: 24 | name: noob-sessions 25 | key: redis-password 26 | - name: MONGODB_PASSWORD 27 | valueFrom: 28 | secretKeyRef: 29 | name: noob-mongodb 30 | key: mongodb-root-password 31 | ports: 32 | - containerPort: 8080 33 | --- 34 | apiVersion: v1 35 | kind: Service 36 | metadata: 37 | name: {{ .Release.Name }}-problems 38 | spec: 39 | selector: 40 | app: {{ .Release.Name }}-problems 41 | ports: 42 | - protocol: TCP 43 | port: 8080 44 | targetPort: 8080 45 | -------------------------------------------------------------------------------- /chart/templates/submissions.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ .Release.Name }}-submissions 5 | labels: 6 | app: {{ .Release.Name }}-submissions 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: {{ .Release.Name }}-submissions 12 | template: 13 | metadata: 14 | labels: 15 | app: {{ .Release.Name }}-submissions 16 | spec: 17 | containers: 18 | - name: {{ .Release.Name }}-submissions 19 | image: uhkevin/noob-submissions 20 | env: 21 | - name: SESSIONS_PASSWORD 22 | valueFrom: 23 | secretKeyRef: 24 | name: noob-sessions 25 | key: redis-password 26 | - name: MONGODB_PASSWORD 27 | valueFrom: 28 | secretKeyRef: 29 | name: noob-mongodb 30 | key: mongodb-root-password 31 | - name: RABBITMQ_PASSWORD 32 | valueFrom: 33 | secretKeyRef: 34 | name: noob-rabbitmq 35 | key: rabbitmq-password 36 | ports: 37 | - containerPort: 8080 38 | --- 39 | apiVersion: v1 40 | kind: Service 41 | metadata: 42 | name: {{ .Release.Name }}-submissions 43 | spec: 44 | selector: 45 | app: {{ .Release.Name }}-submissions 46 | ports: 47 | - protocol: TCP 48 | port: 8080 49 | targetPort: 8080 50 | -------------------------------------------------------------------------------- /chart/values.yaml: -------------------------------------------------------------------------------- 1 | mongodb: 2 | auth: 3 | rootPassword: "MONGODB_TEMP" 4 | 5 | postgresql: 6 | postgresqlPassword: "POSTGRESQL_TEMP" 7 | 8 | sessions: 9 | password: "SESSIONS_TEMP" 10 | cluster: 11 | enabled: false 12 | 13 | rabbitmq: 14 | auth: 15 | password: "RABBITMQ_TEMP" 16 | erlangCookie: "ERLANG_COOKIE" 17 | livenessProbe: 18 | enabled: false 19 | readinessProbe: 20 | enabled: false 21 | -------------------------------------------------------------------------------- /cmd/auth/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:alpine AS build 2 | 3 | WORKDIR /go/src/github.com/kzh/noob/ 4 | ADD . /go/src/github.com/kzh/noob/ 5 | 6 | RUN go build -o main ./cmd/auth/ 7 | 8 | FROM alpine 9 | 10 | WORKDIR /app/ 11 | 12 | COPY --from=build /go/src/github.com/kzh/noob/main ./auth 13 | 14 | CMD ["./auth"] 15 | -------------------------------------------------------------------------------- /cmd/auth/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | 7 | "github.com/gin-gonic/gin" 8 | "github.com/globalsign/mgo/bson" 9 | 10 | noobdb "github.com/kzh/noob/pkg/database" 11 | "github.com/kzh/noob/pkg/model" 12 | noobsess "github.com/kzh/noob/pkg/sessions" 13 | ) 14 | 15 | func handleLogin(ctx *gin.Context) { 16 | var redirect string 17 | session := noobsess.Default(ctx) 18 | 19 | defer func() { 20 | session.Save() 21 | ctx.Redirect(http.StatusSeeOther, redirect) 22 | }() 23 | 24 | if session.IsLoggedIn() { 25 | session.AddFlash("Already logged in.") 26 | redirect = "/" 27 | return 28 | } 29 | 30 | var cred model.Credential 31 | if err := ctx.ShouldBind(&cred); err != nil { 32 | session.AddFlash("Invalid username or password.") 33 | redirect = "/login/" 34 | return 35 | } 36 | 37 | rec, err := noobdb.Authenticate(cred) 38 | if err != nil { 39 | session.AddFlash(err.Error()) 40 | redirect = "/login/" 41 | return 42 | } 43 | 44 | session.SetM(gin.H{ 45 | "username": cred.Username, 46 | "role": rec["role"], 47 | }) 48 | 49 | session.AddFlash("Success!") 50 | redirect = "/" 51 | } 52 | 53 | func handleRegister(ctx *gin.Context) { 54 | var redirect string 55 | session := noobsess.Default(ctx) 56 | 57 | defer func() { 58 | session.Save() 59 | ctx.Redirect(http.StatusSeeOther, redirect) 60 | }() 61 | 62 | if session.IsLoggedIn() { 63 | session.AddFlash("Already logged in.") 64 | redirect = "/" 65 | return 66 | } 67 | 68 | var cred model.Credential 69 | if err := ctx.ShouldBind(&cred); err != nil { 70 | session.AddFlash("Invalid username or password.") 71 | redirect = "/register/" 72 | return 73 | } 74 | 75 | rec := bson.M{"role": "user"} 76 | if err := noobdb.Register(cred, rec); err != nil { 77 | session.AddFlash(err.Error()) 78 | redirect = "/register/" 79 | } 80 | 81 | session.AddFlash("Success!") 82 | redirect = "/" 83 | } 84 | 85 | func handleLogout(ctx *gin.Context) { 86 | session := noobsess.Default(ctx) 87 | 88 | defer func() { 89 | session.Save() 90 | ctx.Redirect(http.StatusSeeOther, "/") 91 | }() 92 | 93 | if !session.IsLoggedIn() { 94 | session.AddFlash("Not logged in.") 95 | return 96 | } 97 | 98 | session.Clear() 99 | session.AddFlash("Success!") 100 | } 101 | 102 | func main() { 103 | log.Println("Noob: Auth MS is starting...") 104 | 105 | // Create gin router 106 | r := gin.Default() 107 | 108 | r.Use(noobsess.Sessions()) 109 | 110 | r.POST("/login", handleLogin) 111 | r.POST("/register", handleRegister) 112 | r.POST("/logout", handleLogout) 113 | 114 | // Serve gin router 115 | if err := r.Run(); err != nil { 116 | log.Println(err) 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /cmd/executor/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:alpine AS build 2 | 3 | WORKDIR /go/src/github.com/kzh/noob/ 4 | ADD . /go/src/github.com/kzh/noob/ 5 | 6 | RUN go build -o main ./cmd/executor/ 7 | 8 | FROM alpine 9 | 10 | WORKDIR /app/ 11 | 12 | COPY --from=build /go/src/github.com/kzh/noob/main ./executor 13 | COPY ./cmd/executor/image.tar . 14 | 15 | CMD ["./executor"] 16 | -------------------------------------------------------------------------------- /cmd/executor/image.tar: -------------------------------------------------------------------------------- 1 | Dockerfile000644 000770 000024 00000000233 13357000517 013322 0ustar00kevinstaff000000 000000 FROM golang:alpine AS build 2 | 3 | WORKDIR /go/src/build/ 4 | ADD main.go /go/src/build/ 5 | 6 | RUN go build -o ex . 7 | RUN rm main.go 8 | 9 | CMD ["sh", "-c", "tail -f /dev/null"] 10 | -------------------------------------------------------------------------------- /cmd/executor/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "archive/tar" 5 | "bytes" 6 | "context" 7 | "encoding/json" 8 | "fmt" 9 | "io" 10 | "io/ioutil" 11 | "log" 12 | "strconv" 13 | "strings" 14 | "sync" 15 | 16 | "github.com/docker/docker/api/types" 17 | "github.com/docker/docker/api/types/container" 18 | "github.com/docker/docker/api/types/filters" 19 | "github.com/docker/docker/client" 20 | opentracing "github.com/opentracing/opentracing-go" 21 | otlog "github.com/opentracing/opentracing-go/log" 22 | "github.com/streadway/amqp" 23 | 24 | noobdb "github.com/kzh/noob/pkg/database" 25 | "github.com/kzh/noob/pkg/message" 26 | "github.com/kzh/noob/pkg/model" 27 | "github.com/kzh/noob/pkg/tracing" 28 | ) 29 | 30 | var dock *client.Client 31 | 32 | func buildImageContext(parent opentracing.Span, code string) (io.Reader, error) { 33 | span := opentracing.StartSpan( 34 | "buildImageContext", 35 | opentracing.ChildOf(parent.Context()), 36 | ) 37 | defer span.Finish() 38 | 39 | raw, err := ioutil.ReadFile("image.tar") 40 | if err != nil { 41 | return nil, err 42 | } 43 | 44 | trim := len(raw) - 1024 45 | buf := bytes.NewBuffer(raw[:trim]) 46 | 47 | w := tar.NewWriter(buf) 48 | header := &tar.Header{ 49 | Name: "main.go", 50 | Size: int64(len(code)), 51 | } 52 | err = w.WriteHeader(header) 53 | if err != nil { 54 | return nil, err 55 | } 56 | _, err = w.Write([]byte(code)) 57 | if err != nil { 58 | return nil, err 59 | } 60 | if err := w.Close(); err != nil { 61 | return nil, err 62 | } 63 | 64 | return buf, nil 65 | } 66 | 67 | func buildImage(parent opentracing.Span, id string, buildContext io.Reader) error { 68 | span := opentracing.StartSpan( 69 | "buildImage", 70 | opentracing.ChildOf(parent.Context()), 71 | ) 72 | defer span.Finish() 73 | 74 | ctx := context.Background() 75 | res, err := dock.ImageBuild( 76 | ctx, 77 | buildContext, 78 | types.ImageBuildOptions{ 79 | Remove: true, 80 | ForceRemove: true, 81 | Tags: []string{id}, 82 | Context: buildContext, 83 | Dockerfile: "Dockerfile", 84 | }, 85 | ) 86 | if err != nil { 87 | return err 88 | } 89 | defer res.Body.Close() 90 | 91 | b, _ := ioutil.ReadAll(res.Body) 92 | log.Println(string(b)) 93 | 94 | return nil 95 | } 96 | 97 | func prepareContainer(parent opentracing.Span, uid string) (string, error) { 98 | span := opentracing.StartSpan( 99 | "prepareContainer", 100 | opentracing.ChildOf(parent.Context()), 101 | ) 102 | defer span.Finish() 103 | 104 | ctx := context.Background() 105 | resp, err := dock.ContainerCreate( 106 | ctx, 107 | &container.Config{ 108 | Image: uid, 109 | NetworkDisabled: true, 110 | }, 111 | &container.HostConfig{ 112 | Resources: container.Resources{ 113 | MemoryReservation: 4*1024*1024 + 1, 114 | }, 115 | }, 116 | nil, uid, 117 | ) 118 | if err != nil { 119 | return "", err 120 | } 121 | 122 | err = dock.ContainerStart( 123 | ctx, resp.ID, 124 | types.ContainerStartOptions{}, 125 | ) 126 | if err != nil { 127 | return "", err 128 | } 129 | 130 | return resp.ID, nil 131 | } 132 | 133 | func test(parent opentracing.Span, container, problem, submission string) { 134 | span := opentracing.StartSpan( 135 | "test", 136 | opentracing.ChildOf(parent.Context()), 137 | ) 138 | defer span.Finish() 139 | 140 | probio, err := noobdb.IOProblem(problem) 141 | if err != nil { 142 | var result model.SubmissionResult 143 | result.Stage = "Internal" 144 | result.Status = "FAILED" 145 | message.Publish(submission, result) 146 | return 147 | } 148 | 149 | inputs := strings.Split(probio.In, "---") 150 | outputs := strings.Split(probio.Out, "---") 151 | 152 | var wg sync.WaitGroup 153 | 154 | for i, input := range inputs { 155 | wg.Add(1) 156 | 157 | i := i 158 | in, out := sanitize(input)+"\n", sanitize(outputs[i]) 159 | go func() { 160 | defer wg.Done() 161 | 162 | resp, err := exec(span, container, in, out) 163 | 164 | var result model.SubmissionResult 165 | result.Stage = strconv.Itoa(i + 1) 166 | result.Status = "PASSED" 167 | if resp != "" || err != nil { 168 | result.Status = "FAILED" 169 | log.Printf("%s %#v\n", resp, err) 170 | } 171 | 172 | message.Publish(submission, result) 173 | }() 174 | } 175 | 176 | wg.Wait() 177 | } 178 | 179 | func exec(parent opentracing.Span, uid, in, out string) (string, error) { 180 | span := opentracing.StartSpan( 181 | "exec", 182 | opentracing.ChildOf(parent.Context()), 183 | ) 184 | defer span.Finish() 185 | 186 | span.LogFields( 187 | otlog.String("input", in), 188 | ) 189 | 190 | ctx := context.Background() 191 | resp, err := dock.ContainerExecCreate( 192 | ctx, uid, 193 | types.ExecConfig{ 194 | Cmd: []string{"./ex"}, 195 | Tty: true, 196 | AttachStdin: true, 197 | AttachStderr: true, 198 | AttachStdout: true, 199 | }, 200 | ) 201 | if err != nil { 202 | return "", err 203 | } 204 | 205 | exec, err := dock.ContainerExecAttach( 206 | ctx, resp.ID, 207 | types.ExecConfig{Tty: true}, 208 | ) 209 | if err != nil { 210 | return "", err 211 | } 212 | defer exec.Close() 213 | 214 | _, err = exec.Conn.Write([]byte(in)) 215 | if err != nil { 216 | return "", err 217 | } 218 | 219 | _, err = fmt.Fscanf(exec.Reader, in+out) 220 | if err != nil { 221 | line, _ := ioutil.ReadAll(exec.Reader) 222 | log.Println("FAILED") 223 | fmt.Printf("Error:\n%s\n", string(line)) 224 | return string(line), nil 225 | } 226 | 227 | log.Println("PASSED") 228 | return "", err 229 | } 230 | 231 | func clean(parent opentracing.Span, uid string) error { 232 | span := opentracing.StartSpan( 233 | "clean", 234 | opentracing.ChildOf(parent.Context()), 235 | ) 236 | defer span.Finish() 237 | 238 | ctx := context.Background() 239 | err := dock.ContainerRemove( 240 | ctx, uid, 241 | types.ContainerRemoveOptions{ 242 | RemoveVolumes: true, 243 | Force: true, 244 | }, 245 | ) 246 | if err != nil { 247 | return err 248 | } 249 | 250 | _, err = dock.ImageRemove( 251 | ctx, uid, 252 | types.ImageRemoveOptions{ 253 | Force: true, 254 | PruneChildren: true, 255 | }, 256 | ) 257 | 258 | return err 259 | } 260 | 261 | func sanitize(in string) string { 262 | return strings.Map(func(r rune) rune { 263 | if r == '\n' || r == '\r' { 264 | return -1 265 | } 266 | 267 | return r 268 | }, in) 269 | } 270 | 271 | func handle(msg amqp.Delivery) { 272 | span := opentracing.StartSpan("handleSubmission") 273 | defer span.Finish() 274 | 275 | var submission model.Submission 276 | err := json.Unmarshal(msg.Body, &submission) 277 | if err != nil { 278 | log.Println(err) 279 | return 280 | } 281 | 282 | span.LogFields( 283 | otlog.String("id", submission.ID), 284 | otlog.String("problem", submission.ProblemID), 285 | otlog.String("code", submission.Code), 286 | ) 287 | 288 | log.Println("Incoming submission.") 289 | log.Println("ID: " + submission.ID) 290 | 291 | ctx, err := buildImageContext(span, submission.Code) 292 | if err != nil { 293 | log.Println(err) 294 | return 295 | } 296 | 297 | err = buildImage(span, submission.ID, ctx) 298 | if err != nil { 299 | log.Println(err) 300 | return 301 | } 302 | 303 | cid, err := prepareContainer(span, submission.ID) 304 | if err != nil { 305 | log.Println(err) 306 | 307 | _, err = dock.ImagesPrune( 308 | context.Background(), 309 | filters.Args{}, 310 | ) 311 | 312 | var result model.SubmissionResult 313 | result.Stage = "Compile" 314 | result.Status = "FAILED" 315 | message.Publish(submission.ID, result) 316 | return 317 | } 318 | 319 | test(span, cid, submission.ProblemID, submission.ID) 320 | 321 | err = clean(span, submission.ID) 322 | if err != nil { 323 | log.Println(err) 324 | return 325 | } 326 | 327 | log.Println("Finishing handling submission.") 328 | } 329 | 330 | func main() { 331 | log.Println("Noob: Executor Worker is starting...") 332 | 333 | closer, err := tracing.InitJaeger() 334 | if err != nil { 335 | panic(err) 336 | } 337 | defer closer.Close() 338 | 339 | dock, err = client.NewEnvClient() 340 | if err != nil { 341 | panic(err) 342 | } 343 | 344 | msgs, err := message.Poll() 345 | if err != nil { 346 | panic(err) 347 | } 348 | 349 | for msg := range msgs { 350 | go handle(msg) 351 | } 352 | } 353 | -------------------------------------------------------------------------------- /cmd/frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:alpine AS build 2 | 3 | WORKDIR /go/src/github.com/kzh/noob/ 4 | ADD . /go/src/github.com/kzh/noob/ 5 | 6 | RUN go build -o main ./cmd/frontend/ 7 | 8 | FROM alpine 9 | 10 | WORKDIR /app/ 11 | 12 | COPY --from=build /go/src/github.com/kzh/noob/main ./frontend 13 | ADD ./cmd/frontend/templates/ ./templates/ 14 | ADD ./cmd/frontend/static/ ./static/ 15 | 16 | CMD ["./frontend"] 17 | -------------------------------------------------------------------------------- /cmd/frontend/auth.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | 8 | noobsess "github.com/kzh/noob/pkg/sessions" 9 | ) 10 | 11 | func handleLogin(c *gin.Context) { 12 | session := noobsess.Default(c) 13 | 14 | data := struct { 15 | Message string 16 | }{} 17 | messages := session.Flashes() 18 | if len(messages) > 0 { 19 | data.Message = messages[0].(string) 20 | } 21 | 22 | session.Save() 23 | c.HTML(http.StatusOK, "login.tmpl", data) 24 | } 25 | 26 | func handleRegister(c *gin.Context) { 27 | session := noobsess.Default(c) 28 | 29 | data := struct { 30 | Message string 31 | }{} 32 | messages := session.Flashes() 33 | if len(messages) > 0 { 34 | data.Message = messages[0].(string) 35 | } 36 | 37 | session.Save() 38 | c.HTML(http.StatusOK, "register.tmpl", data) 39 | } 40 | -------------------------------------------------------------------------------- /cmd/frontend/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | 7 | "github.com/gin-gonic/gin" 8 | 9 | noobsess "github.com/kzh/noob/pkg/sessions" 10 | ) 11 | 12 | func handleHome(c *gin.Context) { 13 | session := noobsess.Default(c) 14 | 15 | data := struct { 16 | User string 17 | Admin bool 18 | Message string 19 | }{} 20 | if session.IsLoggedIn() { 21 | data.User = session.Username() 22 | } 23 | messages := session.Flashes() 24 | if len(messages) > 0 { 25 | data.Message = messages[0].(string) 26 | } 27 | data.Admin = session.IsAdmin() 28 | 29 | session.Save() 30 | c.HTML(http.StatusOK, "index.tmpl", data) 31 | } 32 | 33 | func main() { 34 | log.Println("Noob: Frontend MS is starting...") 35 | 36 | // Create gin router 37 | r := gin.Default() 38 | 39 | // Use redis sessions middleware 40 | r.Use(noobsess.Sessions()) 41 | 42 | r.Static("/static", "./static/") 43 | r.LoadHTMLGlob("templates/*.tmpl") 44 | 45 | r.GET("/", handleHome) 46 | 47 | problems := r.Group("/") 48 | problems.GET("/problems/", handleProblems) 49 | problems.GET("/problem/:id/", handleProblem) 50 | 51 | // Auth 52 | auth := r.Group("/") 53 | auth.GET("/login/", handleLogin) 54 | auth.GET("/register/", handleRegister) 55 | 56 | // Admin 57 | admin := r.Group("/") 58 | admin.Use(noobsess.LoggedIn(true)) 59 | admin.Use(noobsess.Admin(true)) 60 | admin.GET("/create/", handleCreate) 61 | admin.GET("/problem/:id/edit/", handleEdit) 62 | 63 | if err := r.Run(); err != nil { 64 | log.Println(err) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /cmd/frontend/problems.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | 8 | noobsess "github.com/kzh/noob/pkg/sessions" 9 | ) 10 | 11 | func handleCreate(c *gin.Context) { 12 | session := noobsess.Default(c) 13 | 14 | data := struct { 15 | Message string 16 | }{} 17 | messages := session.Flashes() 18 | if len(messages) > 0 { 19 | data.Message = messages[0].(string) 20 | } 21 | 22 | session.Save() 23 | c.HTML(http.StatusOK, "create.tmpl", data) 24 | } 25 | 26 | func handleEdit(c *gin.Context) { 27 | c.HTML(http.StatusOK, "edit.tmpl", nil) 28 | } 29 | 30 | func handleProblems(c *gin.Context) { 31 | c.HTML(http.StatusOK, "problems.tmpl", nil) 32 | } 33 | 34 | func handleProblem(c *gin.Context) { 35 | session := noobsess.Default(c) 36 | data := struct { 37 | Admin bool 38 | }{ 39 | session.IsAdmin(), 40 | } 41 | session.Save() 42 | 43 | c.HTML(http.StatusOK, "problem.tmpl", data) 44 | } 45 | -------------------------------------------------------------------------------- /cmd/frontend/static/css/common.css: -------------------------------------------------------------------------------- 1 | .hidden { 2 | display: none; 3 | } 4 | -------------------------------------------------------------------------------- /cmd/frontend/static/js/edit.js: -------------------------------------------------------------------------------- 1 | window.onload = function() { 2 | const path = window.location.pathname; 3 | const split = path.split("/"); 4 | const id = split[split.length - 3]; 5 | 6 | const idEl = document.querySelector("input[name=\"id\"]"); 7 | idEl.value = id; 8 | 9 | function handleError(res) { 10 | if (res.error == undefined) { 11 | return res; 12 | } 13 | 14 | throw new Error(res.error); 15 | } 16 | 17 | const msg = document.querySelector(".message"); 18 | function error(err) { 19 | if (err instanceof SyntaxError) { 20 | err = new Error("Service unavailable."); 21 | } 22 | 23 | msg.innerText = err; 24 | } 25 | 26 | let ready1 = false; 27 | let ready2 = false; 28 | 29 | function reveal() { 30 | if (!ready1 || !ready2) { 31 | return; 32 | } 33 | 34 | const content = document.querySelector(".content"); 35 | content.classList.remove("hidden"); 36 | 37 | msg.classList.add("hidden"); 38 | } 39 | 40 | function populateProblem(data) { 41 | const name = document.querySelector("input[name=\"name\"]"); 42 | const description = document.querySelector("textarea[name=\"description\"]"); 43 | name.value = data.name; 44 | description.value = data.description; 45 | 46 | ready1 = true; 47 | reveal(); 48 | } 49 | 50 | function populateIO(data) { 51 | const inputs = document.querySelector("textarea[name=\"inputs\"]"); 52 | const outputs = document.querySelector("textarea[name=\"outputs\"]"); 53 | inputs.value = data.inputs; 54 | outputs.value = data.outputs; 55 | 56 | ready2 = true; 57 | reveal(); 58 | } 59 | 60 | fetch("/api/problems/get/" + id) 61 | .then(res => res.json()) 62 | .then(handleError) 63 | .then(populateProblem) 64 | .catch(error); 65 | 66 | fetch("/api/problems/get/" + id + "/io") 67 | .then(res => res.json()) 68 | .then(handleError) 69 | .then(populateIO) 70 | .catch(error); 71 | } 72 | -------------------------------------------------------------------------------- /cmd/frontend/static/js/problem.js: -------------------------------------------------------------------------------- 1 | window.onload = function() { 2 | const path = window.location.pathname; 3 | const split = path.split("/"); 4 | const id = split[split.length - 2]; 5 | 6 | function handleError(res) { 7 | if (res.error == undefined) { 8 | return res; 9 | } 10 | 11 | throw new Error(res.error); 12 | } 13 | 14 | const title = document.getElementsByTagName("title")[0]; 15 | const msg = document.querySelector(".message"); 16 | function error(err) { 17 | if (err instanceof SyntaxError) { 18 | err = new Error("Service unavailable."); 19 | } 20 | 21 | msg.classList.remove("hidden"); 22 | msg.innerText = err; 23 | title.innerText = "Error"; 24 | } 25 | 26 | function populateData(data) { 27 | const name = document.getElementById("name"); 28 | const description = document.getElementById("description"); 29 | 30 | title.innerText = "Problem: " + data.name; 31 | name.innerText = data.name; 32 | description.textContent = data.description; 33 | 34 | const content = document.querySelector(".content"); 35 | content.classList.remove("hidden"); 36 | 37 | msg.classList.add("hidden"); 38 | } 39 | 40 | fetch("/api/problems/get/" + id) 41 | .then(res => res.json()) 42 | .then(handleError) 43 | .then(populateData) 44 | .catch(error); 45 | 46 | const editBtn = document.getElementById("edit"); 47 | if (editBtn) { 48 | editBtn.addEventListener("click", function() { 49 | window.location.href += "edit/"; 50 | }); 51 | } 52 | 53 | const idEl = document.querySelector("input[name=\"id\"]"); 54 | if (idEl) { 55 | idEl.value = id; 56 | } 57 | 58 | function submitCallback(res) { 59 | if (res.message != undefined) { 60 | msg.classList.remove("hidden"); 61 | msg.innerText = res.message; 62 | } 63 | } 64 | 65 | const resultsEl = document.getElementById("results"); 66 | function displayResult(res) { 67 | const resEl = document.createElement("p"); 68 | resEl.innerText = "Stage: " + res.stage + " - " + res.status; 69 | resultsEl.appendChild(resEl); 70 | } 71 | 72 | const submitBtn = document.getElementById("submit"); 73 | submitBtn.addEventListener("click", function() { 74 | resultsEl.innerText = ""; 75 | const code = document.getElementById("code"); 76 | 77 | const submission = { 78 | problem: id, 79 | code: code.value, 80 | }; 81 | 82 | const addr = "wss://" + window.location.hostname + "/api/submissions/submit"; 83 | const ws = new WebSocket(addr); 84 | ws.onmessage = function(event) { 85 | console.log(event.data); 86 | displayResult(JSON.parse(event.data)); 87 | } 88 | ws.onopen = function() { 89 | ws.send(JSON.stringify(submission)); 90 | } 91 | }); 92 | } 93 | -------------------------------------------------------------------------------- /cmd/frontend/static/js/problems.js: -------------------------------------------------------------------------------- 1 | window.onload = function() { 2 | const problemsContainer = document.querySelector(".problems"); 3 | 4 | function createProblem(id, name) { 5 | const problem = document.createElement("a"); 6 | problem.href = "/problem/" + id + "/"; 7 | 8 | const text = document.createTextNode(name); 9 | problem.appendChild(text); 10 | 11 | return problem; 12 | } 13 | 14 | function handleError(res) { 15 | if (res.error == undefined) { 16 | return res; 17 | } 18 | 19 | throw new Error(res.error); 20 | } 21 | 22 | const msg = document.querySelector(".message"); 23 | function error(err) { 24 | if (err instanceof SyntaxError) { 25 | err = new Error("Service unavailable."); 26 | } 27 | 28 | msg.innerText = err; 29 | } 30 | 31 | function populateProblems(problems) { 32 | for (const p of problems) { 33 | const el = createProblem(p.id, p.name); 34 | problemsContainer.appendChild(el); 35 | 36 | const br = document.createElement("br"); 37 | problemsContainer.appendChild(br); 38 | } 39 | 40 | const content = document.querySelector(".content"); 41 | content.classList.remove("hidden"); 42 | 43 | msg.classList.add("hidden"); 44 | } 45 | 46 | fetch("/api/problems/list") 47 | .then(res => res.json()) 48 | .then(handleError) 49 | .then(populateProblems) 50 | .catch(error); 51 | } 52 | -------------------------------------------------------------------------------- /cmd/frontend/templates/create.tmpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | Create 4 | 5 | 6 | {{if .Message}} 7 |

Alert: {{.Message}}

8 | {{end}} 9 |
10 | 11 |
12 | 13 |
14 | 15 |
16 | 17 |
18 | 19 |
20 | 21 | 22 | -------------------------------------------------------------------------------- /cmd/frontend/templates/edit.tmpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | Edit 4 | 5 | 6 | 7 | 8 |

Loading...

9 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /cmd/frontend/templates/index.tmpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | Noob 4 | 5 | 6 | {{if .Message}} 7 |

Alert: {{.Message}}

8 | {{end}} 9 | {{if .User}} 10 |

Hello {{.User}}.

11 | View Problems 12 |
13 |
14 | {{if .Admin}} 15 | Admin Privileges: 16 |
17 | Create Problem 18 |
19 |
20 | {{end}} 21 |
22 | 23 |
24 | {{else}} 25 | Login 26 |
27 | Register 28 | {{end}} 29 | 30 | 31 | -------------------------------------------------------------------------------- /cmd/frontend/templates/login.tmpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | Login 4 | 5 | 6 | {{if .Message}} 7 |

Alert: {{.Message}}

8 | {{end}} 9 |
10 | 11 |
12 | 13 |
14 | 15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /cmd/frontend/templates/problem.tmpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |

Loading...

9 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /cmd/frontend/templates/problems.tmpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | Problems 4 | 5 | 6 | 7 | 8 |

Loading...

9 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /cmd/frontend/templates/register.tmpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | Register 4 | 5 | 6 | {{if .Message}} 7 |

Alert: {{.Message}}

8 | {{end}} 9 |
10 | 11 |
12 | 13 |
14 | 15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /cmd/problems/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:alpine AS build 2 | 3 | WORKDIR /go/src/github.com/kzh/noob/ 4 | ADD . /go/src/github.com/kzh/noob/ 5 | 6 | RUN go build -o main ./cmd/problems/ 7 | 8 | FROM alpine 9 | 10 | WORKDIR /app/ 11 | 12 | COPY --from=build /go/src/github.com/kzh/noob/main ./problems 13 | 14 | CMD ["./problems"] 15 | -------------------------------------------------------------------------------- /cmd/problems/admin.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | 7 | "github.com/gin-gonic/gin" 8 | 9 | noobdb "github.com/kzh/noob/pkg/database" 10 | "github.com/kzh/noob/pkg/model" 11 | noobsess "github.com/kzh/noob/pkg/sessions" 12 | ) 13 | 14 | func handleCreate(ctx *gin.Context) { 15 | var redirect string 16 | session := noobsess.Default(ctx) 17 | 18 | defer func() { 19 | session.Save() 20 | ctx.Redirect(http.StatusSeeOther, redirect) 21 | }() 22 | 23 | var prob model.ProblemData 24 | if err := ctx.ShouldBind(&prob); err != nil { 25 | redirect = "/" 26 | session.AddFlash("Invalid form data format") 27 | log.Println(err) 28 | return 29 | } 30 | 31 | problem, err := noobdb.CreateProblem(prob) 32 | if err != nil { 33 | session.AddFlash(err.Error()) 34 | redirect = "/" 35 | } 36 | 37 | redirect = "/problem/" + problem + "/" 38 | } 39 | 40 | func handleSelectIO(ctx *gin.Context) { 41 | id := ctx.Param("id") 42 | io, err := noobdb.IOProblem(id) 43 | if err == nil { 44 | ctx.JSON(http.StatusOK, io) 45 | return 46 | } 47 | 48 | status := http.StatusInternalServerError 49 | if err == noobdb.ErrNoSuchProblem { 50 | status = http.StatusNotFound 51 | } 52 | 53 | ctx.JSON(status, gin.H{ 54 | "error": err.Error(), 55 | }) 56 | } 57 | 58 | func handleEdit(ctx *gin.Context) { 59 | var redirect string 60 | session := noobsess.Default(ctx) 61 | 62 | defer func() { 63 | session.Save() 64 | ctx.Redirect(http.StatusSeeOther, redirect) 65 | }() 66 | 67 | var prob model.Problem 68 | if err := ctx.ShouldBind(&prob); err != nil { 69 | redirect = "/" 70 | session.AddFlash("Invalid form data format") 71 | log.Println(err) 72 | return 73 | } 74 | 75 | err := noobdb.EditProblem(prob) 76 | if err != nil { 77 | session.AddFlash(err.Error()) 78 | redirect = "/" 79 | } 80 | 81 | session.AddFlash("Success!") 82 | redirect = "/problem/" + prob.ID + "/edit/" 83 | } 84 | 85 | func handleDelete(ctx *gin.Context) { 86 | var redirect string 87 | session := noobsess.Default(ctx) 88 | 89 | defer func() { 90 | session.Save() 91 | ctx.Redirect(http.StatusSeeOther, redirect) 92 | }() 93 | 94 | var prob model.ProblemID 95 | if err := ctx.ShouldBind(&prob); err != nil { 96 | redirect = "/" 97 | session.AddFlash("Invalid form data format") 98 | log.Println(err) 99 | return 100 | } 101 | 102 | err := noobdb.DeleteProblem(prob) 103 | if err != nil { 104 | redirect = "/" 105 | } 106 | log.Println(prob) 107 | 108 | session.AddFlash("Success!") 109 | redirect = "/" 110 | } 111 | -------------------------------------------------------------------------------- /cmd/problems/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | 7 | "github.com/gin-gonic/gin" 8 | 9 | noobdb "github.com/kzh/noob/pkg/database" 10 | noobsess "github.com/kzh/noob/pkg/sessions" 11 | ) 12 | 13 | func handleList(ctx *gin.Context) { 14 | problems, err := noobdb.Problems() 15 | if err == nil { 16 | ctx.JSON(http.StatusOK, problems) 17 | return 18 | } 19 | 20 | status := http.StatusInternalServerError 21 | ctx.JSON(status, gin.H{ 22 | "error": err.Error(), 23 | }) 24 | } 25 | 26 | func handleSelect(ctx *gin.Context) { 27 | id := ctx.Param("id") 28 | problem, err := noobdb.SnapProblem(id) 29 | if err == nil { 30 | ctx.JSON(http.StatusOK, problem) 31 | return 32 | } 33 | 34 | status := http.StatusInternalServerError 35 | if err == noobdb.ErrNoSuchProblem { 36 | status = http.StatusNotFound 37 | } 38 | 39 | ctx.JSON(status, gin.H{ 40 | "error": err.Error(), 41 | }) 42 | } 43 | 44 | func main() { 45 | log.Println("Noob: Problems MS is starting...") 46 | 47 | r := gin.Default() 48 | 49 | r.Use(noobsess.Sessions()) 50 | 51 | r.GET("/list", handleList) 52 | r.GET("/get/:id", handleSelect) 53 | 54 | admin := r.Group("/") 55 | admin.Use(noobsess.LoggedIn(true)) 56 | admin.Use(noobsess.Admin(true)) 57 | 58 | admin.POST("/create", handleCreate) 59 | admin.POST("/edit", handleEdit) 60 | admin.POST("/delete", handleDelete) 61 | 62 | adminNR := r.Group("/") 63 | adminNR.Use(noobsess.LoggedIn(false)) 64 | adminNR.Use(noobsess.Admin(false)) 65 | 66 | adminNR.GET("/get/:id/io", handleSelectIO) 67 | 68 | if err := r.Run(); err != nil { 69 | log.Println(err) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /cmd/submissions/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:alpine AS build 2 | 3 | WORKDIR /go/src/github.com/kzh/noob/ 4 | ADD . /go/src/github.com/kzh/noob/ 5 | 6 | RUN go build -o main ./cmd/submissions/ 7 | 8 | FROM alpine 9 | 10 | WORKDIR /app/ 11 | 12 | COPY --from=build /go/src/github.com/kzh/noob/main ./submissions 13 | 14 | CMD ["./submissions"] 15 | -------------------------------------------------------------------------------- /cmd/submissions/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/gin-gonic/gin" 7 | "github.com/google/uuid" 8 | "github.com/gorilla/websocket" 9 | 10 | "github.com/kzh/noob/pkg/message" 11 | "github.com/kzh/noob/pkg/model" 12 | noobsess "github.com/kzh/noob/pkg/sessions" 13 | ) 14 | 15 | var upgrader = websocket.Upgrader{} 16 | 17 | func handleSubmit(ctx *gin.Context) { 18 | c, err := upgrader.Upgrade(ctx.Writer, ctx.Request, nil) 19 | if err != nil { 20 | return 21 | } 22 | defer c.Close() 23 | 24 | var submission model.Submission 25 | if err := c.ReadJSON(&submission); err != nil { 26 | return 27 | } 28 | submission.ID = uuid.New().String() 29 | 30 | if err := message.Schedule(submission); err != nil { 31 | return 32 | } 33 | 34 | results, err := message.Subscribe(submission.ID) 35 | if err != nil { 36 | return 37 | } 38 | 39 | log.Println("Results:") 40 | for result := range results { 41 | log.Println(string(result.Body)) 42 | err := c.WriteMessage(websocket.TextMessage, result.Body) 43 | if err != nil { 44 | return 45 | } 46 | } 47 | } 48 | 49 | func main() { 50 | log.Println("Noob: Submissions MS is starting...") 51 | 52 | r := gin.Default() 53 | r.Use(noobsess.Sessions()) 54 | r.Use(noobsess.LoggedIn(false)) 55 | 56 | r.GET("/submit", handleSubmit) 57 | 58 | if err := r.Run(); err != nil { 59 | log.Println(err) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | auth: 4 | build: 5 | context: . 6 | dockerfile: ./cmd/auth/Dockerfile 7 | container_name: uhkevin/noob-auth 8 | image: uhkevin/noob-auth 9 | ports: 10 | - "8080:8080" 11 | frontend: 12 | build: 13 | context: . 14 | dockerfile: ./cmd/frontend/Dockerfile 15 | container_name: uhkevin/noob-frontend 16 | image: uhkevin/noob-frontend 17 | ports: 18 | - "8080:8080" 19 | problems: 20 | build: 21 | context: . 22 | dockerfile: ./cmd/problems/Dockerfile 23 | container_name: uhkevin/noob-problems 24 | image: uhkevin/noob-problems 25 | ports: 26 | - "8080:8080" 27 | submissions: 28 | build: 29 | context: . 30 | dockerfile: ./cmd/submissions/Dockerfile 31 | container_name: uhkevin/noob-submissions 32 | image: uhkevin/noob-submissions 33 | ports: 34 | - "8080:8080" 35 | executor: 36 | build: 37 | context: . 38 | dockerfile: ./cmd/executor/Dockerfile 39 | container_name: uhkevin/noob-executor 40 | image: uhkevin/noob-executor 41 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/kzh/noob 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/docker/distribution v2.7.1+incompatible // indirect 7 | github.com/docker/docker v1.13.1 8 | github.com/docker/go-connections v0.4.0 // indirect 9 | github.com/docker/go-units v0.4.0 // indirect 10 | github.com/gin-contrib/sessions v0.0.3 11 | github.com/gin-gonic/gin v1.6.2 12 | github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8 13 | github.com/google/uuid v1.1.1 14 | github.com/gorilla/websocket v1.4.2 15 | github.com/opencontainers/go-digest v1.0.0-rc1 // indirect 16 | github.com/opentracing/opentracing-go v1.2.0 17 | github.com/pkg/errors v0.9.1 // indirect 18 | github.com/streadway/amqp v0.0.0-20200108173154-1c71cc93ed71 19 | github.com/uber/jaeger-client-go v2.25.0+incompatible 20 | github.com/uber/jaeger-lib v2.2.0+incompatible // indirect 21 | go.uber.org/atomic v1.6.0 // indirect 22 | golang.org/x/crypto v0.0.0-20200406173513-056763e48d71 23 | ) 24 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff h1:RmdPFa+slIr4SCBg4st/l/vZWVe9QJKMXGO60Bxbe04= 2 | github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff/go.mod h1:+RTT1BOk5P97fT2CiHkbFQwkK3mjsFAP6zCYV2aXtjw= 3 | github.com/bradfitz/gomemcache v0.0.0-20190329173943-551aad21a668/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA= 4 | github.com/bradleypeabody/gorilla-sessions-memcache v0.0.0-20181103040241-659414f458e1/go.mod h1:dkChI7Tbtx7H1Tj7TqGSZMOeGpMP5gLHtjroHd4agiI= 5 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 7 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug= 9 | github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= 10 | github.com/docker/docker v1.13.1 h1:IkZjBSIc8hBjLpqeAbeE5mca5mNgeatLHBy3GO78BWo= 11 | github.com/docker/docker v1.13.1/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= 12 | github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= 13 | github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= 14 | github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= 15 | github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= 16 | github.com/gin-contrib/sessions v0.0.3 h1:PoBXki+44XdJdlgDqDrY5nDVe3Wk7wDV/UCOuLP6fBI= 17 | github.com/gin-contrib/sessions v0.0.3/go.mod h1:8C/J6cad3Il1mWYYgtw0w+hqasmpvy25mPkXdOgeB9I= 18 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 19 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 20 | github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do= 21 | github.com/gin-gonic/gin v1.6.2 h1:88crIK23zO6TqlQBt+f9FrPJNKm9ZEr7qjp9vl/d5TM= 22 | github.com/gin-gonic/gin v1.6.2/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= 23 | github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8 h1:DujepqpGd1hyOd7aW59XpK7Qymp8iy83xq74fLr21is= 24 | github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= 25 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 26 | github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM= 27 | github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= 28 | github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= 29 | github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY= 30 | github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= 31 | github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= 32 | github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= 33 | github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= 34 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 35 | github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= 36 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 37 | github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0= 38 | github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= 39 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 40 | github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= 41 | github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 42 | github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8= 43 | github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= 44 | github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= 45 | github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= 46 | github.com/gorilla/sessions v1.1.1/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w= 47 | github.com/gorilla/sessions v1.1.3 h1:uXoZdcdA5XdXF3QzuSlheVRUvjl+1rKY7zBXL68L9RU= 48 | github.com/gorilla/sessions v1.1.3/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w= 49 | github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= 50 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 51 | github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 52 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 53 | github.com/kidstuff/mongostore v0.0.0-20181113001930-e650cd85ee4b/go.mod h1:g2nVr8KZVXJSS97Jo8pJ0jgq29P6H7dG0oplUA86MQw= 54 | github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw= 55 | github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= 56 | github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= 57 | github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= 58 | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= 59 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 60 | github.com/memcachier/mc v2.0.1+incompatible/go.mod h1:7bkvFE61leUBvXz+yxsOnGBQSZpBSPIMUQSmmSHvuXc= 61 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 62 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 63 | github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ= 64 | github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= 65 | github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= 66 | github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= 67 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 68 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 69 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 70 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 71 | github.com/quasoft/memstore v0.0.0-20180925164028-84a050167438/go.mod h1:wTPjTepVu7uJBYgZ0SdWHQlIas582j6cn2jgk4DDdlg= 72 | github.com/streadway/amqp v0.0.0-20200108173154-1c71cc93ed71 h1:2MR0pKUzlP3SGgj5NYJe/zRYDwOu9ku6YHy+Iw7l5DM= 73 | github.com/streadway/amqp v0.0.0-20200108173154-1c71cc93ed71/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= 74 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 75 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 76 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 77 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 78 | github.com/uber/jaeger-client-go v1.6.0 h1:3+zLlq+4npI5fg8IsgAje3YsP7TcEdNzJScyqFIzxEQ= 79 | github.com/uber/jaeger-client-go v2.25.0+incompatible h1:IxcNZ7WRY1Y3G4poYlx24szfsn/3LvK9QHCq9oQw8+U= 80 | github.com/uber/jaeger-client-go v2.25.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= 81 | github.com/uber/jaeger-lib v1.5.0 h1:OHbgr8l656Ub3Fw5k9SWnBfIEwvoHQ+W2y+Aa9D1Uyo= 82 | github.com/uber/jaeger-lib v2.2.0+incompatible h1:MxZXOiR2JuoANZ3J6DE/U0kSFv/eJ/GfSYVCjK7dyaw= 83 | github.com/uber/jaeger-lib v2.2.0+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= 84 | github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= 85 | github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= 86 | github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= 87 | github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= 88 | go.uber.org/atomic v1.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk= 89 | go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 90 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 91 | golang.org/x/crypto v0.0.0-20200406173513-056763e48d71 h1:DOmugCavvUtnUD114C1Wh+UgTgQZ4pMLzXxi1pSt+/Y= 92 | golang.org/x/crypto v0.0.0-20200406173513-056763e48d71/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 93 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 94 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 95 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ= 96 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 97 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI= 98 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 99 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 100 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 101 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 102 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 103 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg= 104 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 105 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 106 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 107 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 108 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 109 | golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 110 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 111 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 112 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 113 | gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= 114 | gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= 115 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 116 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 117 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 118 | -------------------------------------------------------------------------------- /pkg/database/database.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "strconv" 7 | 8 | "github.com/globalsign/mgo" 9 | "github.com/globalsign/mgo/bson" 10 | ) 11 | 12 | var db *mgo.Database 13 | 14 | func init() { 15 | log.Println("Connecting to MongoDB...") 16 | 17 | // Connect to mongodb 18 | dbInfo := &mgo.DialInfo{ 19 | Addrs: []string{"noob-mongodb:27017"}, 20 | Database: "admin", 21 | Username: "root", 22 | Password: os.Getenv("MONGODB_PASSWORD"), 23 | } 24 | 25 | session, err := mgo.DialWithInfo(dbInfo) 26 | if err != nil { 27 | panic(err) 28 | } 29 | 30 | db = session.DB("noob") 31 | log.Println("Connected to MongoDB.") 32 | } 33 | 34 | func count(name string) (string, error) { 35 | counters := db.C("counters") 36 | 37 | query := bson.M{"_id": name} 38 | update := mgo.Change{ 39 | Update: bson.M{"$inc": bson.M{"count": 1}}, 40 | Upsert: true, 41 | ReturnNew: true, 42 | } 43 | 44 | var doc bson.M 45 | _, err := counters.Find(query).Apply(update, &doc) 46 | if err != nil { 47 | return "", err 48 | } 49 | 50 | id := strconv.Itoa(doc["count"].(int)) 51 | return id, nil 52 | } 53 | -------------------------------------------------------------------------------- /pkg/database/errors.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | var ( 8 | ErrInvalidCredential = errors.New("Invalid username or password.") 9 | ErrUnavailableUsername = errors.New("Username taken.") 10 | ErrInternalServer = errors.New("Internal server error.") 11 | ErrNoSuchProblem = errors.New("No such problem.") 12 | ) 13 | -------------------------------------------------------------------------------- /pkg/database/problems.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/globalsign/mgo" 7 | "github.com/globalsign/mgo/bson" 8 | 9 | . "github.com/kzh/noob/pkg/model" 10 | ) 11 | 12 | func CreateProblem(p ProblemData) (string, error) { 13 | problems := db.C("problems") 14 | 15 | id, err := count("problems") 16 | if err != nil { 17 | log.Println(err) 18 | return "", ErrInternalServer 19 | } 20 | 21 | rec := bson.M{ 22 | "_id": id, 23 | "name": p.Name, 24 | "description": p.Description, 25 | "inputs": p.In, 26 | "outputs": p.Out, 27 | } 28 | if err := problems.Insert(rec); err != nil { 29 | log.Println(err) 30 | return "", ErrInternalServer 31 | } 32 | 33 | return id, nil 34 | } 35 | 36 | func EditProblem(p Problem) error { 37 | problems := db.C("problems") 38 | 39 | query := bson.M{"_id": p.ID} 40 | update := bson.M{ 41 | "_id": p.ID, 42 | "name": p.Name, 43 | "description": p.Description, 44 | "inputs": p.In, 45 | "outputs": p.Out, 46 | } 47 | 48 | _, err := problems.Upsert( 49 | query, 50 | bson.M{ 51 | "$set": update, 52 | }, 53 | ) 54 | if err != nil { 55 | log.Println(err) 56 | return ErrInternalServer 57 | } 58 | 59 | return nil 60 | } 61 | 62 | func DeleteProblem(pid ProblemID) error { 63 | problems := db.C("problems") 64 | 65 | query := bson.M{"_id": pid.ID} 66 | err := problems.Remove(query) 67 | if err == nil { 68 | return nil 69 | } else if err == mgo.ErrNotFound { 70 | return ErrNoSuchProblem 71 | } else { 72 | log.Println(err) 73 | return ErrInternalServer 74 | } 75 | } 76 | 77 | func Problems() ([]ProblemSnap, error) { 78 | problems := db.C("problems") 79 | 80 | query := problems.Find(bson.M{}) 81 | count, err := query.Count() 82 | if err != nil { 83 | log.Println(err) 84 | return nil, ErrInternalServer 85 | } 86 | 87 | res := make([]ProblemSnap, count) 88 | 89 | var ( 90 | problem ProblemSnap 91 | i int 92 | ) 93 | 94 | iter := query.Iter() 95 | for iter.Next(&problem) { 96 | res[i] = problem 97 | i++ 98 | } 99 | 100 | err = iter.Close() 101 | if err != nil { 102 | log.Println(err) 103 | } 104 | 105 | return res, nil 106 | } 107 | 108 | func problem(id string, format interface{}) error { 109 | problems := db.C("problems") 110 | 111 | err := problems.Find(bson.M{ 112 | "_id": id, 113 | }).One(format) 114 | 115 | if err == mgo.ErrNotFound { 116 | return ErrNoSuchProblem 117 | } else if err != nil { 118 | log.Println(err) 119 | return ErrInternalServer 120 | } 121 | 122 | return err 123 | } 124 | 125 | func FullProblem(id string) (Problem, error) { 126 | var p Problem 127 | err := problem(id, &p) 128 | return p, err 129 | } 130 | 131 | func SnapProblem(id string) (ProblemSnap, error) { 132 | var p ProblemSnap 133 | err := problem(id, &p) 134 | return p, err 135 | } 136 | 137 | func IOProblem(id string) (ProblemIO, error) { 138 | var io ProblemIO 139 | err := problem(id, &io) 140 | return io, err 141 | } 142 | -------------------------------------------------------------------------------- /pkg/database/users.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/globalsign/mgo/bson" 7 | "golang.org/x/crypto/bcrypt" 8 | 9 | . "github.com/kzh/noob/pkg/model" 10 | ) 11 | 12 | func Authenticate(cred Credential) (bson.M, error) { 13 | users := db.C("users") 14 | 15 | var rec bson.M 16 | query := bson.M{"username": cred.Username} 17 | if err := users.Find(query).One(&rec); err != nil { 18 | return nil, ErrInvalidCredential 19 | } 20 | 21 | hash := rec["password"].(string) 22 | if err := bcrypt.CompareHashAndPassword( 23 | []byte(hash), 24 | []byte(cred.Password), 25 | ); err != nil { 26 | return nil, ErrInvalidCredential 27 | } 28 | 29 | delete(rec, "password") 30 | return rec, nil 31 | } 32 | 33 | func Register(cred Credential, data bson.M) error { 34 | users := db.C("users") 35 | 36 | var rec bson.M 37 | query := bson.M{"username": cred.Username} 38 | if err := users.Find(query).One(&rec); err == nil { 39 | return ErrUnavailableUsername 40 | } 41 | 42 | hash, err := bcrypt.GenerateFromPassword( 43 | []byte(cred.Password), 44 | bcrypt.DefaultCost, 45 | ) 46 | if err != nil { 47 | log.Println(err) 48 | return ErrInternalServer 49 | } 50 | 51 | rec = bson.M{ 52 | "username": cred.Username, 53 | "password": string(hash), 54 | } 55 | 56 | for k, v := range data { 57 | rec[k] = v 58 | } 59 | 60 | if err := users.Insert(rec); err != nil { 61 | log.Println(err) 62 | return ErrInternalServer 63 | } 64 | 65 | return nil 66 | } 67 | -------------------------------------------------------------------------------- /pkg/message/pubsub.go: -------------------------------------------------------------------------------- 1 | package message 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/streadway/amqp" 7 | 8 | "github.com/kzh/noob/pkg/model" 9 | ) 10 | 11 | func Subscribe(pubsub string) (<-chan amqp.Delivery, error) { 12 | ch, err := rabbitmq.Channel() 13 | if err != nil { 14 | return nil, err 15 | } 16 | 17 | err = ch.ExchangeDeclare( 18 | pubsub, "fanout", 19 | true, false, false, false, nil, 20 | ) 21 | if err != nil { 22 | return nil, err 23 | } 24 | 25 | q, err := ch.QueueDeclare( 26 | "", 27 | false, false, true, false, nil, 28 | ) 29 | if err != nil { 30 | return nil, err 31 | } 32 | 33 | err = ch.QueueBind( 34 | q.Name, 35 | "", pubsub, false, nil, 36 | ) 37 | if err != nil { 38 | return nil, err 39 | } 40 | 41 | msgs, err := ch.Consume( 42 | q.Name, "", 43 | true, false, false, false, nil, 44 | ) 45 | return msgs, err 46 | } 47 | 48 | func Publish(pubsub string, res model.SubmissionResult) error { 49 | ch, err := rabbitmq.Channel() 50 | if err != nil { 51 | return err 52 | } 53 | 54 | err = ch.ExchangeDeclare( 55 | pubsub, "fanout", 56 | true, false, false, false, nil, 57 | ) 58 | if err != nil { 59 | return err 60 | } 61 | 62 | body, err := json.Marshal(res) 63 | if err != nil { 64 | return err 65 | } 66 | 67 | publishing := amqp.Publishing{ 68 | DeliveryMode: amqp.Persistent, 69 | ContentType: "text/plain", 70 | Body: body, 71 | } 72 | return ch.Publish(pubsub, "", false, false, publishing) 73 | } 74 | -------------------------------------------------------------------------------- /pkg/message/queue.go: -------------------------------------------------------------------------------- 1 | package message 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | 7 | "github.com/streadway/amqp" 8 | 9 | "github.com/kzh/noob/pkg/model" 10 | ) 11 | 12 | func Schedule(s model.Submission) error { 13 | ch, err := rabbitmq.Channel() 14 | if err != nil { 15 | return err 16 | } 17 | defer ch.Close() 18 | 19 | _, err = ch.QueueDeclare( 20 | "submissions", 21 | true, false, false, false, nil, 22 | ) 23 | if err != nil { 24 | return err 25 | } 26 | 27 | body, err := json.Marshal(s) 28 | if err != nil { 29 | return err 30 | } 31 | log.Println(string(body)) 32 | 33 | publishing := amqp.Publishing{ 34 | DeliveryMode: amqp.Persistent, 35 | ContentType: "text/plain", 36 | Body: body, 37 | } 38 | return ch.Publish("", "submissions", false, false, publishing) 39 | } 40 | 41 | func Poll() (<-chan amqp.Delivery, error) { 42 | ch, err := rabbitmq.Channel() 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | _, err = ch.QueueDeclare( 48 | "submissions", 49 | true, false, false, false, nil, 50 | ) 51 | if err != nil { 52 | return nil, err 53 | } 54 | 55 | msgs, err := ch.Consume( 56 | "submissions", 57 | "", true, false, false, false, nil, 58 | ) 59 | return msgs, err 60 | } 61 | -------------------------------------------------------------------------------- /pkg/message/rabbit.go: -------------------------------------------------------------------------------- 1 | package message 2 | 3 | import ( 4 | "log" 5 | "net/url" 6 | "os" 7 | 8 | "github.com/streadway/amqp" 9 | ) 10 | 11 | var rabbitmq *amqp.Connection 12 | 13 | func init() { 14 | log.Println("Connecting to RabbitMQ...") 15 | 16 | var addr url.URL 17 | addr.Scheme = "amqp" 18 | addr.User = url.UserPassword( 19 | "user", 20 | os.Getenv("RABBITMQ_PASSWORD"), 21 | ) 22 | addr.Host = "noob-rabbitmq:5672" 23 | addr.Path = "/" 24 | 25 | u := addr.String() 26 | 27 | var err error 28 | rabbitmq, err = amqp.Dial(u) 29 | if err != nil { 30 | panic(err) 31 | } 32 | 33 | log.Println("Connected to RabbitMQ.") 34 | } 35 | -------------------------------------------------------------------------------- /pkg/model/problem.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type ProblemID struct { 4 | ID string `form:"id" binding:"required"` 5 | } 6 | 7 | type ProblemData struct { 8 | Name string `form:"name" binding:"required"` 9 | Description string `form:"description" binding:"required"` 10 | In string `form:"inputs" binding:"required"` 11 | Out string `form:"outputs" binding:"required"` 12 | } 13 | 14 | type Problem struct { 15 | ID string `form:"id" json:"id" bson:"_id"` 16 | Name string `form:"name" json:"name" bson:"name" binding:"required"` 17 | Description string `form:"description" json:"description" bson:"description" binding:"required"` 18 | In string `form:"inputs" json:"inputs" bson:"inputs" binding:"required"` 19 | Out string `form:"outputs" json:"outputs" bson:"outputs" binding:"required"` 20 | } 21 | 22 | type ProblemSnap struct { 23 | ID string `json:"id" bson:"_id"` 24 | Name string `json:"name" bson:"name"` 25 | Description string `json:"description" bson:"description"` 26 | } 27 | 28 | type ProblemIO struct { 29 | In string `json:"inputs" bson:"inputs"` 30 | Out string `json:"outputs" bson:"outputs"` 31 | } 32 | -------------------------------------------------------------------------------- /pkg/model/submission.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type Submission struct { 4 | ID string 5 | ProblemID string `json:"problem"` 6 | Code string `json:"code"` 7 | } 8 | 9 | type SubmissionResult struct { 10 | Stage string `json:"stage"` 11 | Status string `json:"status"` 12 | } 13 | -------------------------------------------------------------------------------- /pkg/model/user.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type Credential struct { 4 | Username string `form:"username" binding:"required"` 5 | Password string `form:"password" binding:"required"` 6 | } 7 | -------------------------------------------------------------------------------- /pkg/sessions/middleware.go: -------------------------------------------------------------------------------- 1 | package sessions 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "net/http" 6 | ) 7 | 8 | type guardian func(sess NoobSession) bool 9 | 10 | func guard(g guardian, message string, redirect bool) gin.HandlerFunc { 11 | return func(c *gin.Context) { 12 | session := Default(c) 13 | defer session.Save() 14 | 15 | if g(session) { 16 | c.Next() 17 | return 18 | } 19 | 20 | if redirect { 21 | session.AddFlash(message) 22 | c.Redirect(http.StatusSeeOther, "/") 23 | } else { 24 | c.JSON( 25 | http.StatusUnauthorized, 26 | gin.H{"error": message}, 27 | ) 28 | } 29 | 30 | c.Abort() 31 | } 32 | } 33 | 34 | func LoggedIn(redirect bool) gin.HandlerFunc { 35 | return guard( 36 | NoobSession.IsLoggedIn, 37 | "Not logged in.", 38 | redirect, 39 | ) 40 | } 41 | 42 | func Admin(redirect bool) gin.HandlerFunc { 43 | return guard( 44 | NoobSession.IsAdmin, 45 | "Insufficient permissions.", 46 | redirect, 47 | ) 48 | } 49 | -------------------------------------------------------------------------------- /pkg/sessions/sessions.go: -------------------------------------------------------------------------------- 1 | package sessions 2 | 3 | import ( 4 | "log" 5 | "os" 6 | 7 | "github.com/gin-contrib/sessions" 8 | "github.com/gin-contrib/sessions/redis" 9 | "github.com/gin-gonic/gin" 10 | ) 11 | 12 | func Sessions() gin.HandlerFunc { 13 | log.Println("Connecting to Redis Sessions...") 14 | 15 | store, err := redis.NewStore( 16 | 10, 17 | "tcp", 18 | "noob-sessions-master:6379", 19 | os.Getenv("SESSIONS_PASSWORD"), 20 | []byte("NOOB_SESSION_SECRET"), 21 | //[]byte(os.Getenv("SESSION_SECRET")), 22 | ) 23 | if err != nil { 24 | panic(err) 25 | } 26 | 27 | log.Println("Connected to Redis Sessions.") 28 | return sessions.Sessions("noob", store) 29 | } 30 | 31 | type NoobSession struct { 32 | sessions.Session 33 | } 34 | 35 | func Default(ctx *gin.Context) NoobSession { 36 | return NoobSession{sessions.Default(ctx)} 37 | } 38 | 39 | func (s NoobSession) SetM(data map[string]interface{}) { 40 | for k, v := range data { 41 | s.Set(k, v) 42 | } 43 | } 44 | 45 | func (s NoobSession) IsLoggedIn() bool { 46 | _, ok := s.Get("username").(string) 47 | return ok 48 | } 49 | 50 | func (s NoobSession) IsAdmin() bool { 51 | role, ok := s.Get("role").(string) 52 | return ok && role == "admin" 53 | } 54 | 55 | func (s NoobSession) Username() string { 56 | usr, ok := s.Get("username").(string) 57 | if !ok { 58 | return "" 59 | } 60 | 61 | return usr 62 | } 63 | -------------------------------------------------------------------------------- /pkg/tracing/tracing.go: -------------------------------------------------------------------------------- 1 | package tracing 2 | 3 | import ( 4 | "io" 5 | "log" 6 | 7 | opentracing "github.com/opentracing/opentracing-go" 8 | config "github.com/uber/jaeger-client-go/config" 9 | jaegerlog "github.com/uber/jaeger-client-go/log" 10 | ) 11 | 12 | func InitJaeger() (io.Closer, error) { 13 | log.Println("Connecting to Jaeger...") 14 | 15 | cfg, err := config.FromEnv() 16 | if err != nil { 17 | log.Printf("Could not parse Jaeger env vars: %s", err.Error()) 18 | return nil, err 19 | } 20 | 21 | tracer, closer, err := cfg.NewTracer( 22 | config.Logger(jaegerlog.StdLogger), 23 | ) 24 | if err != nil { 25 | log.Printf("Could not initialize jaeger tracer: %s", err.Error()) 26 | return nil, err 27 | } 28 | 29 | opentracing.SetGlobalTracer(tracer) 30 | 31 | log.Println("Connected to Jaeger.") 32 | return closer, nil 33 | } 34 | --------------------------------------------------------------------------------