Skip to content

Commit

Permalink
feat(ws): initial commit for 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 29, 2024
1 parent 04d2571 commit 91bc821
Show file tree
Hide file tree
Showing 17 changed files with 732 additions and 1 deletion.
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,7 @@ Sessionx.vim
.DS_Store
.AppleDouble
.LSOverride
._*
._*

## backend
workspaces/backend/bin
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"]
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 91bc821

Please sign in to comment.