Skip to content

Commit

Permalink
feat(nb): Scaffolding of backend(/workspaces/backend)
Browse files Browse the repository at this point in the history
Signed-off-by: Eder Ignatowicz <ignatowicz@gmail.com>
  • Loading branch information
ederign committed May 22, 2024
1 parent 04d2571 commit f5d5e26
Show file tree
Hide file tree
Showing 19 changed files with 1,052 additions and 0 deletions.
27 changes: 27 additions & 0 deletions workspaces/backend/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
bin/*
Dockerfile.cross

# Test binary, built with `go test -c`
*.test

# Output of the go coverage tool, specifically when used with LiteIDE
*.out

# Go workspace file
go.work

# Kubernetes Generated files - skip generated files, except for vendored files
!vendor/**/zz_generated.*

# editor and IDE paraphernalia
.idea
.vscode
*.swp
*.swo
*~
40 changes: 40 additions & 0 deletions workspaces/backend/.golangci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
run:
timeout: 5m
allow-parallel-runners: true

issues:
# don't skip warning about doc comments
# don't exclude the default set of lint
exclude-use-default: false
# restore some of the defaults
# (fill in the rest as needed)
exclude-rules:
- path: "api/*"
linters:
- lll
- path: "internal/*"
linters:
- dupl
- lll
linters:
disable-all: true
enable:
- dupl
- errcheck
- exportloopref
- goconst
- gocyclo
- gofmt
- goimports
- gosimple
- govet
- ineffassign
- lll
- misspell
- nakedret
- prealloc
- staticcheck
- typecheck
- unconvert
- unparam
- unused
37 changes: 37 additions & 0 deletions workspaces/backend/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Use the golang image to build the application
FROM golang:1.22.2 AS builder
ARG TARGETOS
ARG TARGETARCH

WORKDIR /workspace

# Copy the Go Modules manifests
COPY go.mod go.sum ./

# Download dependencies
RUN go mod download

# Copy the go source files
COPY cmd/ cmd/
COPY api/ api/
COPY config/ config/
COPY data/ data/
COPY integrations/ integrations/

# Build the Go application
RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o backend ./cmd/main.go

# Use distroless as minimal base image to package the application binary
FROM gcr.io/distroless/static:nonroot
WORKDIR /
COPY --from=builder /workspace/backend ./
USER 65532:65532

# Expose port 4000
EXPOSE 4000

# Define environment variables
ENV PORT 4001
ENV ENV development

ENTRYPOINT ["/backend"]
29 changes: 29 additions & 0 deletions workspaces/backend/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
CONTAINER_TOOL ?= docker
IMG ?= nbv2-backend:latest

.PHONY: all
all: build

.PHONY: help
help: ## Display this help.
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<target>\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)

.PHONY: fmt
fmt:
go fmt ./...

.PHONY: vet
vet: .
go vet ./...

.PHONY: build
build: fmt vet
go build -o bin/backend cmd/main.go

.PHONY: run
run: fmt vet
PORT=4000 ENV=development go run ./cmd/main.go

.PHONY: docker-build
docker-build:
$(CONTAINER_TOOL) build -t ${IMG} .
21 changes: 21 additions & 0 deletions workspaces/backend/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Workspaces backend
This component serves as a BFF layer for Workspaces Frontend.

# Building and Deploying
TBD

# Development
## Getting started

### Endpoints

| URL Pattern | Handler | Action |
|---------------------|--------------------|-------------------------------|
| GET /v1/healthcheck | HealthcheckHandler | Show application information. |


### Sample local calls
```
# GET /v1/healthcheck
curl -i localhost:4000/api/v1/healthcheck/
```
56 changes: 56 additions & 0 deletions workspaces/backend/api/app.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
Copyright 2024.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package api

import (
"github.com/kubeflow/notebooks/workspaces/backend/config"
"github.com/kubeflow/notebooks/workspaces/backend/data"
"log/slog"
"net/http"

"github.com/julienschmidt/httprouter"
)

const (
Version = "1.0.0"
HealthCheckPath = "/api/v1/healthcheck/"
)

type App struct {
config config.EnvConfig
logger *slog.Logger
models data.Models
}

func NewApp(cfg config.EnvConfig, logger *slog.Logger) *App {
app := &App{
config: cfg,
logger: logger,
}
return app
}

func (app *App) Routes() http.Handler {
router := httprouter.New()

router.NotFound = http.HandlerFunc(app.notFoundResponse)
router.MethodNotAllowed = http.HandlerFunc(app.methodNotAllowedResponse)

router.GET(HealthCheckPath, app.AttachKubernetesClient(app.HealthcheckHandler))

return app.RecoverPanic(app.enableCORS(router))
}
119 changes: 119 additions & 0 deletions workspaces/backend/api/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/*
Copyright 2024.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package api

import (
"encoding/json"
"fmt"
"net/http"
"strconv"
)

type HTTPError struct {
StatusCode int `json:"-"`
ErrorResponse
}

type ErrorResponse struct {
Code string `json:"code"`
Message string `json:"message"`
}

func (app *App) LogError(r *http.Request, err error) {
var (
method = r.Method
uri = r.URL.RequestURI()
)

app.logger.Error(err.Error(), "method", method, "uri", uri)
}

func (app *App) badRequestResponse(w http.ResponseWriter, r *http.Request, err error) {
httpError := &HTTPError{
StatusCode: http.StatusBadRequest,
ErrorResponse: ErrorResponse{
Code: strconv.Itoa(http.StatusBadRequest),
Message: err.Error(),
},
}
app.errorResponse(w, r, httpError)
}

func (app *App) errorResponse(w http.ResponseWriter, r *http.Request, error *HTTPError) {

env := Envelope{"error": error}

err := app.WriteJSON(w, error.StatusCode, env, nil)

if err != nil {
app.LogError(r, err)
w.WriteHeader(error.StatusCode)
}
}

func (app *App) serverErrorResponse(w http.ResponseWriter, r *http.Request, err error) {
app.LogError(r, err)

httpError := &HTTPError{
StatusCode: http.StatusInternalServerError,
ErrorResponse: ErrorResponse{
Code: strconv.Itoa(http.StatusInternalServerError),
Message: "the server encountered a problem and could not process your request",
},
}
app.errorResponse(w, r, httpError)
}

func (app *App) notFoundResponse(w http.ResponseWriter, r *http.Request) {

httpError := &HTTPError{
StatusCode: http.StatusNotFound,
ErrorResponse: ErrorResponse{
Code: strconv.Itoa(http.StatusNotFound),
Message: "the requested resource could not be found",
},
}
app.errorResponse(w, r, httpError)
}

func (app *App) methodNotAllowedResponse(w http.ResponseWriter, r *http.Request) {

httpError := &HTTPError{
StatusCode: http.StatusMethodNotAllowed,
ErrorResponse: ErrorResponse{
Code: strconv.Itoa(http.StatusMethodNotAllowed),
Message: fmt.Sprintf("the %s method is not supported for this resource", r.Method),
},
}
app.errorResponse(w, r, httpError)
}

func (app *App) failedValidationResponse(w http.ResponseWriter, r *http.Request, errors map[string]string) {

message, err := json.Marshal(errors)
if err != nil {
message = []byte("{}")
}
httpError := &HTTPError{
StatusCode: http.StatusUnprocessableEntity,
ErrorResponse: ErrorResponse{
Code: strconv.Itoa(http.StatusUnprocessableEntity),
Message: string(message),
},
}
app.errorResponse(w, r, httpError)
}
56 changes: 56 additions & 0 deletions workspaces/backend/api/healthcheck_handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
Copyright 2024.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package api

import (
"github.com/julienschmidt/httprouter"
"github.com/kubeflow/notebooks/workspaces/backend/data"
"github.com/kubeflow/notebooks/workspaces/backend/integrations"
"net/http"
)

func (app *App) HealthcheckHandler(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {

client, ok := r.Context().Value(k8sClientKey).(*integrations.KubernetesClient)
if !ok {
healthCheck := data.HealthCheckModel{
Status: "unavailable",
SystemInfo: data.SystemInfo{
Environment: app.config.Env.String(),
Version: Version,
},
Dependencies: data.Dependencies{
Kubernetes: "unavailable",
},
}
app.WriteJSON(w, http.StatusServiceUnavailable, healthCheck, nil)
return
}

healthCheck, err := app.models.HealthCheck.HealthCheck(client, app.config.Env, Version)
if err != nil {
app.serverErrorResponse(w, r, err)
return
}

err = app.WriteJSON(w, http.StatusOK, healthCheck, nil)

if err != nil {
app.serverErrorResponse(w, r, err)
}

}
Loading

0 comments on commit f5d5e26

Please sign in to comment.