Skip to content

Commit

Permalink
feat(ws): initial commit for backend (#7)
Browse files Browse the repository at this point in the history
* feat(ws): initial commit for backend

Signed-off-by: Eder Ignatowicz <[email protected]>

* Fixing docker build

Signed-off-by: Eder Ignatowicz <[email protected]>

* Fixing git ignore

Signed-off-by: Eder Ignatowicz <[email protected]>

---------

Signed-off-by: Eder Ignatowicz <[email protected]>
  • Loading branch information
ederign authored May 31, 2024
1 parent 20a3de3 commit 9a945fb
Show file tree
Hide file tree
Showing 16 changed files with 705 additions and 0 deletions.
1 change: 1 addition & 0 deletions workspaces/backend/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/bin
36 changes: 36 additions & 0 deletions workspaces/backend/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# 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/

# 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"]
33 changes: 33 additions & 0 deletions workspaces/backend/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
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: test
test:
go test ./...

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

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

.PHONY: docker-build
docker-build:
$(CONTAINER_TOOL) build -t ${IMG} .
26 changes: 26 additions & 0 deletions workspaces/backend/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Kubeflow Workspaces Backend
The Kubeflow Workspaces Backend is the _backend for frontend_ (BFF) used by the Kubeflow Workspaces UI as part of [Kubeflow Notebooks 2.0](https://github.com/kubeflow/kubeflow/issues/7156).

> ⚠️ __Warning__ ⚠️
>
> The Kubeflow Workspaces Backend is a work in progress and is __NOT__ currently ready for use.
> We greatly appreciate any contributions.
# 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.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)
}
66 changes: 66 additions & 0 deletions workspaces/backend/api/healthcheck__handler_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
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"
"github.com/kubeflow/notebooks/workspaces/backend/config"
"github.com/kubeflow/notebooks/workspaces/backend/data"
"github.com/stretchr/testify/assert"
"io"
"net/http"
"net/http/httptest"
"testing"
)

func TestHealthCheckHandler(t *testing.T) {

app := App{config: config.EnvConfig{
Port: 4000,
}}

rr := httptest.NewRecorder()
req, err := http.NewRequest(http.MethodGet, HealthCheckPath, nil)
if err != nil {
t.Fatal(err)
}

app.HealthcheckHandler(rr, req, nil)
rs := rr.Result()

defer rs.Body.Close()

body, err := io.ReadAll(rs.Body)
if err != nil {
t.Fatal("Failed to read response body")
}

var healthCheckRes data.HealthCheckModel
err = json.Unmarshal(body, &healthCheckRes)
if err != nil {
t.Fatalf("Error unmarshalling response JSON: %v", err)
}

expected := data.HealthCheckModel{
Status: "available",
SystemInfo: data.SystemInfo{
Version: Version,
},
}

assert.Equal(t, expected, healthCheckRes)
}
38 changes: 38 additions & 0 deletions workspaces/backend/api/healthcheck_handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
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"
"net/http"
)

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

healthCheck, err := app.models.HealthCheck.HealthCheck(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 9a945fb

Please sign in to comment.