Skip to content

Commit

Permalink
Notebooks 2.0 // Backend // List Workspaces API (#60)
Browse files Browse the repository at this point in the history
Signed-off-by: Eder Ignatowicz <ignatowicz@gmail.com>
  • Loading branch information
ederign authored Sep 23, 2024
1 parent b0af8ae commit f852c16
Show file tree
Hide file tree
Showing 20 changed files with 1,053 additions and 96 deletions.
3 changes: 2 additions & 1 deletion workspaces/backend/.gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
/bin
/bin
cover.out
100 changes: 91 additions & 9 deletions workspaces/backend/Makefile
Original file line number Diff line number Diff line change
@@ -1,33 +1,115 @@
# CONTAINER_TOOL defines the container tool to be used for building images.
# Be aware that the target commands are only tested with Docker which is
# scaffolded by default. However, you might want to replace it to use other
# tools. (i.e. podman)
CONTAINER_TOOL ?= docker
# Image URL to use all building/pushing image targets
IMG ?= nbv2-backend:latest
# Backend default port
PORT ?= 4000
# ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary.
ENVTEST_K8S_VERSION = 1.29.0

# Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set)
ifeq (,$(shell go env GOBIN))
GOBIN=$(shell go env GOPATH)/bin
else
GOBIN=$(shell go env GOBIN)
endif

# Setting SHELL to bash allows bash commands to be executed by recipes.
# Options are set to exit when a recipe line exits non-zero or a piped command fails.
SHELL = /usr/bin/env bash -o pipefail
.SHELLFLAGS = -ec

.PHONY: all
all: build

##@ General

.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)

##@ Development

.PHONY: fmt
fmt:
fmt: ## Run go fmt against code.
go fmt ./...

.PHONY: vet
vet: .
vet: ## Run go vet against code.
go vet ./...

.PHONY: test
test:
go test ./...
test: fmt vet envtest ## Run tests.
KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" \
go test ./... -coverprofile cover.out

.PHONY: lint
lint: golangci-lint ## Run golangci-lint linter & yamllint
$(GOLANGCI_LINT) run

.PHONY: lint-fix
lint-fix: golangci-lint ## Run golangci-lint linter and perform fixes
$(GOLANGCI_LINT) run --fix

##@ Build

.PHONY: build
build: fmt vet test
build: fmt vet envtest ## Build backend binary.
go build -o bin/backend cmd/main.go

.PHONY: run
run: fmt vet
PORT=4000 go run ./cmd/main.go
run: fmt vet ## Run a backend from your host.
go run ./cmd/main.go --port=$(PORT)

.PHONY: docker-build
docker-build:
$(CONTAINER_TOOL) build -t ${IMG} .
docker-build: ## Build docker image with the backend.
$(CONTAINER_TOOL) build -t ${IMG} .

.PHONY: docker-push
docker-push: ## Push docker image with the backend.
$(CONTAINER_TOOL) push ${IMG}

##@ Dependencies

## Location to install dependencies to
LOCALBIN ?= $(shell pwd)/bin
$(LOCALBIN):
mkdir -p $(LOCALBIN)

## Tool Binaries
KUBECTL ?= kubectl
ENVTEST ?= $(LOCALBIN)/setup-envtest-$(ENVTEST_VERSION)
GOLANGCI_LINT ?= $(LOCALBIN)/golangci-lint-$(GOLANGCI_LINT_VERSION)

## Tool Versions
KUSTOMIZE_VERSION ?= v5.3.0
ENVTEST_VERSION ?= release-0.17
GOLANGCI_LINT_VERSION ?= v1.57.2

.PHONY: envtest
envtest: $(ENVTEST) ## Download setup-envtest locally if necessary.
$(ENVTEST): $(LOCALBIN)
$(call go-install-tool,$(ENVTEST),sigs.k8s.io/controller-runtime/tools/setup-envtest,$(ENVTEST_VERSION))

.PHONY: golangci-lint
golangci-lint: $(GOLANGCI_LINT) ## Download golangci-lint locally if necessary.
$(GOLANGCI_LINT): $(LOCALBIN)
$(call go-install-tool,$(GOLANGCI_LINT),github.com/golangci/golangci-lint/cmd/golangci-lint,${GOLANGCI_LINT_VERSION})


# go-install-tool will 'go install' any package with custom target and name of binary, if it doesn't exist
# $1 - target path with name of binary (ideally with version)
# $2 - package url which can be installed
# $3 - specific version of package
define go-install-tool
@[ -f $(1) ] || { \
set -e; \
package=$(2)@$(3) ;\
echo "Downloading $${package}" ;\
GOBIN=$(LOCALBIN) go install $${package} ;\
mv "$$(echo "$(1)" | sed "s/-$(3)$$//")" $(1) ;\
}
endef
39 changes: 32 additions & 7 deletions workspaces/backend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,42 @@ The Kubeflow Workspaces Backend is the _backend for frontend_ (BFF) used by the
TBD

# Development
## Getting started

Run the following command to build the BFF:
```shell
make build
```
After building it, you can run our app with:
```shell
make run
```
If you want to use a different port:
```shell
make run PORT=8000
```
### Endpoints

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

| URL Pattern | Handler | Action |
|------------------------------------------------------|----------------------|-------------------------------|
| GET /v1/healthcheck | HealthcheckHandler | Show application information. |
| GET /v1/spawner/{namespace}/workspaces | GetWorkspacesHandler | Get all Workspaces |
| POST /v1/spawner/{namespace}/workspaces | TBD | Create a Workspace |
| GET /v1/spawner/{namespace}/workspaces/{name} | TBD | Get a Workspace entity |
| PATCH /v1/spawner/{namespace}/workspaces/{name} | TBD | Patch a Workspace entity |
| PUT /v1/spawner/{namespace}/workspaces/{name} | TBD | Update a Workspace entity |
| DELETE /v1/spawner/{namespace}/workspaces/{name} | TBD | Delete a Workspace entity |
| GET /v1/spawner/{namespace}/workspacekinds | TDB | Get all WorkspaceKind |
| POST /v1/spawner/{namespace}/workspacekinds | TDB | Create a WorkspaceKind |
| GET /v1/spawner/{namespace}/workspacekinds/{name} | TBD | Get a WorkspaceKind entity |
| PATCH /v1/spawner/{namespace}/workspacekinds/{name} | TBD | Patch a WorkspaceKind entity |
| PUT /v1/spawner/{namespace}/workspacekinds/{name} | TBD | Update a WorkspaceKind entity |
| DELETE /v1/spawner/{namespace}/workspacekinds/{name} | TBD | Delete a WorkspaceKind entity |

### Sample local calls
```
# GET /v1/healthcheck
curl -i localhost:4000/api/v1/healthcheck/
```
```
``````
# GET /v1/spawner/{namespace}/workspace
curl -i localhost:4000/api/v1/spawner/{namespace}/workspaces
```
31 changes: 16 additions & 15 deletions workspaces/backend/api/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ limitations under the License.
package api

import (
"github.com/kubeflow/notebooks/workspaces/backend/internal/repositories"
"log/slog"
"net/http"

Expand All @@ -25,32 +26,31 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client"

"github.com/kubeflow/notebooks/workspaces/backend/internal/config"
"github.com/kubeflow/notebooks/workspaces/backend/internal/data"
)

const (
Version = "1.0.0"
HealthCheckPath = "/api/v1/healthcheck/"
Version = "1.0.0"
HealthCheckPath = "/api/v1/healthcheck/"
NamespacePathParam = "namespace"
PathPrefix = "/api/v1/spawner/:namespace"
WorkspacesPath = PathPrefix + "/workspaces"
)

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

client.Client
Scheme *runtime.Scheme
Config config.EnvConfig
logger *slog.Logger
repositories *repositories.Repositories
Scheme *runtime.Scheme
}

// NewApp creates a new instance of the app
func NewApp(cfg config.EnvConfig, logger *slog.Logger, client client.Client, scheme *runtime.Scheme) (*App, error) {
app := &App{
Config: cfg,
logger: logger,
models: data.NewModels(),

Client: client,
Scheme: scheme,
app := &App{
Config: cfg,
logger: logger,
repositories: repositories.NewRepositories(client),
Scheme: scheme,
}
return app, nil
}
Expand All @@ -63,6 +63,7 @@ func (a *App) Routes() http.Handler {
router.MethodNotAllowed = http.HandlerFunc(a.methodNotAllowedResponse)

router.GET(HealthCheckPath, a.HealthcheckHandler)
router.GET(WorkspacesPath, a.GetWorkspacesHandler)

return a.RecoverPanic(a.enableCORS(router))
}
2 changes: 2 additions & 0 deletions workspaces/backend/api/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ func (a *App) LogError(r *http.Request, err error) {
a.logger.Error(err.Error(), "method", method, "uri", uri)
}

// nolint:unused
func (a *App) badRequestResponse(w http.ResponseWriter, r *http.Request, err error) {
httpError := &HTTPError{
StatusCode: http.StatusBadRequest,
Expand Down Expand Up @@ -102,6 +103,7 @@ func (a *App) methodNotAllowedResponse(w http.ResponseWriter, r *http.Request) {
a.errorResponse(w, r, httpError)
}

// nolint:unused
func (a *App) failedValidationResponse(w http.ResponseWriter, r *http.Request, errors map[string]string) {

message, err := json.Marshal(errors)
Expand Down
19 changes: 11 additions & 8 deletions workspaces/backend/api/healthcheck__handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ package api

import (
"encoding/json"
"github.com/kubeflow/notebooks/workspaces/backend/internal/models"
"github.com/kubeflow/notebooks/workspaces/backend/internal/repositories"
"io"
"net/http"
"net/http/httptest"
Expand All @@ -26,14 +28,15 @@ import (
"github.com/stretchr/testify/assert"

"github.com/kubeflow/notebooks/workspaces/backend/internal/config"
"github.com/kubeflow/notebooks/workspaces/backend/internal/data"
)

func TestHealthCheckHandler(t *testing.T) {

app := App{Config: config.EnvConfig{
Port: 4000,
}}
app := App{
Config: config.EnvConfig{
Port: 4000,
},
repositories: repositories.NewRepositories(k8sClient),
}

rr := httptest.NewRecorder()
req, err := http.NewRequest(http.MethodGet, HealthCheckPath, nil)
Expand All @@ -51,15 +54,15 @@ func TestHealthCheckHandler(t *testing.T) {
t.Fatal("Failed to read response body")
}

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

expected := data.HealthCheckModel{
expected := models.HealthCheckModel{
Status: "available",
SystemInfo: data.SystemInfo{
SystemInfo: models.SystemInfo{
Version: Version,
},
}
Expand Down
5 changes: 3 additions & 2 deletions workspaces/backend/api/healthcheck_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,14 @@ limitations under the License.
package api

import (
"github.com/julienschmidt/httprouter"
"net/http"

"github.com/julienschmidt/httprouter"
)

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

healthCheck, err := a.models.HealthCheck.HealthCheck(Version)
healthCheck, err := a.repositories.HealthCheck.HealthCheck(Version)
if err != nil {
a.serverErrorResponse(w, r, err)
return
Expand Down
7 changes: 5 additions & 2 deletions workspaces/backend/api/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import (
"strings"
)

type Envelope map[string]any
type Envelope map[string]interface{}

func (a *App) WriteJSON(w http.ResponseWriter, status int, data any, headers http.Header) error {

Expand All @@ -43,7 +43,10 @@ func (a *App) WriteJSON(w http.ResponseWriter, status int, data any, headers htt

w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
w.Write(js)
_, err = w.Write(js)
if err != nil {
return err
}

return nil
}
Expand Down
Loading

0 comments on commit f852c16

Please sign in to comment.