Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
"""Patches for the api proxy container."""

from typing import TYPE_CHECKING

from kubernetes import client

from renku_data_services.base_models.core import AnonymousAPIUser, AuthenticatedAPIUser
from renku_data_services.notebooks.api.amalthea_patches.utils import get_certificates_volume_mounts
from renku_data_services.notebooks.config import NotebooksConfig

if TYPE_CHECKING:
# NOTE: If these are directly imported then you get circular imports.
pass


def main_container(
session_id: str,
user: AnonymousAPIUser | AuthenticatedAPIUser,
config: NotebooksConfig,
) -> client.V1Container | None:
"""The patch that adds the api proxy container to a session statefulset."""
if not user.is_authenticated or user.access_token is None or user.refresh_token is None:
return None

etc_cert_volume_mount = get_certificates_volume_mounts(
config,
custom_certs=False,
etc_certs=True,
read_only_etc_certs=True,
)

prefix = "API_PROXY_"
env = [
client.V1EnvVar(name=f"{prefix}HOST", value=""),
client.V1EnvVar(name=f"{prefix}PORT", value="58080"),
client.V1EnvVar(name=f"{prefix}SESSION_ID", value=session_id),
client.V1EnvVar(name=f"{prefix}RENKU_ACCESS_TOKEN", value=str(user.access_token)),
client.V1EnvVar(name=f"{prefix}RENKU_REFRESH_TOKEN", value=str(user.refresh_token)),
client.V1EnvVar(name=f"{prefix}RENKU_REALM", value=config.keycloak_realm),
client.V1EnvVar(
name=f"{prefix}RENKU_CLIENT_ID",
value=str(config.sessions.git_proxy.renku_client_id),
),
client.V1EnvVar(
name=f"{prefix}RENKU_CLIENT_SECRET",
value=str(config.sessions.git_proxy.renku_client_secret),
),
client.V1EnvVar(name=f"{prefix}RENKU_URL", value="https://" + config.sessions.ingress.host),
]
container = client.V1Container(
image="leafty/test:api-proxy-8b05fcca",
security_context={
"runAsGroup": 1000,
"runAsUser": 1000,
"allowPrivilegeEscalation": False,
"runAsNonRoot": True,
},
name="api-proxy",
env=env,
liveness_probe={
"httpGet": {
"path": "/health",
"port": 58080,
},
"initialDelaySeconds": 3,
},
readiness_probe={
"httpGet": {
"path": "/health",
"port": 58080,
},
"initialDelaySeconds": 3,
},
volume_mounts=etc_cert_volume_mount,
resources={
"requests": {"memory": "16Mi", "cpu": "50m"},
},
)
return container
4 changes: 3 additions & 1 deletion components/renku_data_services/notebooks/blueprints.py
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,9 @@ async def _handler(
uid=environment.uid,
gid=environment.gid,
)
extra_containers = await get_extra_containers(self.nb_config, user, repositories, git_providers)
extra_containers = await get_extra_containers(
self.nb_config, server_name, user, repositories, git_providers
)
extra_volumes.extend(extra_init_volumes_dc)
extra_init_containers.extend(extra_init_containers_dc)

Expand Down
7 changes: 6 additions & 1 deletion components/renku_data_services/notebooks/core_sessions.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
from renku_data_services.data_connectors.models import DataConnectorSecret, DataConnectorWithSecrets
from renku_data_services.errors import errors
from renku_data_services.notebooks import apispec
from renku_data_services.notebooks.api.amalthea_patches import git_proxy, init_containers
from renku_data_services.notebooks.api.amalthea_patches import api_proxy, git_proxy, init_containers
from renku_data_services.notebooks.api.classes.image import Image
from renku_data_services.notebooks.api.classes.k8s_client import sanitizer
from renku_data_services.notebooks.api.classes.repository import GitProvider, Repository
Expand Down Expand Up @@ -96,6 +96,7 @@ async def get_extra_init_containers(

async def get_extra_containers(
nb_config: NotebooksConfig,
session_id: str,
user: AnonymousAPIUser | AuthenticatedAPIUser,
repositories: list[Repository],
git_providers: list[GitProvider],
Expand All @@ -107,6 +108,9 @@ async def get_extra_containers(
)
if git_proxy_container:
conts.append(ExtraContainer.model_validate(sanitizer(git_proxy_container)))
api_proxy_container = api_proxy.main_container(session_id=session_id, user=user, config=nb_config)
if api_proxy_container:
conts.append(ExtraContainer.model_validate(sanitizer(api_proxy_container)))
return conts


Expand Down Expand Up @@ -549,6 +553,7 @@ async def patch_session(
repositories = await repositories_from_session(user, session, project_repo, git_providers)
extra_containers = await get_extra_containers(
nb_config,
session_id,
user,
repositories,
git_providers,
Expand Down
1 change: 1 addition & 0 deletions components/renku_session_services/api_proxy/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
api-proxy
12 changes: 12 additions & 0 deletions components/renku_session_services/api_proxy/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
FROM golang:1.24.5-alpine3.22 AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY api_proxy.go api_proxy.go
COPY pkg pkg
RUN go build -o /api-proxy github.com/SwissDataScienceCenter/renku-data-services/components/renku_session_services/api_proxy

FROM alpine:3.22
USER 1000:1000
COPY --from=builder --chmod=755 /api-proxy /usr/bin/api-proxy
ENTRYPOINT [ "/usr/bin/api-proxy" ]
18 changes: 18 additions & 0 deletions components/renku_session_services/api_proxy/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
SHELL = bash
LDFLAGS=--ldflags "-s"

.PHONY: api-proxy
api-proxy:
go build -v -o api-proxy $(LDFLAGS)

.PHONY: format
format: ## Format source files
gofmt -l -w .

.PHONY: check-format
check-format: ## Check that sources are correctly formatted
gofmt -d -s . && git diff --exit-code

.PHONY: check-vet
check-vet: ## Check source files with `go vet`
go vet ./...
7 changes: 7 additions & 0 deletions components/renku_session_services/api_proxy/api_proxy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package main

import "github.com/SwissDataScienceCenter/renku-data-services/components/renku_session_services/api_proxy/pkg/cmd"

func main() {
cmd.Main()
}
34 changes: 34 additions & 0 deletions components/renku_session_services/api_proxy/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
module github.com/SwissDataScienceCenter/renku-data-services/components/renku_session_services/api_proxy

go 1.24.5

require (
github.com/golang-jwt/jwt/v5 v5.2.3
github.com/labstack/echo/v4 v4.13.4
github.com/mitchellh/mapstructure v1.5.0
github.com/spf13/viper v1.20.1
)

require (
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/labstack/gommon v0.4.2 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/sagikazarmark/locafero v0.9.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.14.0 // indirect
github.com/spf13/cast v1.9.2 // indirect
github.com/spf13/pflag v1.0.7 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/crypto v0.40.0 // indirect
golang.org/x/net v0.42.0 // indirect
golang.org/x/sys v0.34.0 // indirect
golang.org/x/text v0.27.0 // indirect
golang.org/x/time v0.12.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
70 changes: 70 additions & 0 deletions components/renku_session_services/api_proxy/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/golang-jwt/jwt/v5 v5.2.3 h1:kkGXqQOBSDDWRhWNXTFpqGSCMyh/PLnqUvMGJPDJDs0=
github.com/golang-jwt/jwt/v5 v5.2.3/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/labstack/echo/v4 v4.13.4 h1:oTZZW+T3s9gAu5L8vmzihV7/lkXGZuITzTQkTEhcXEA=
github.com/labstack/echo/v4 v4.13.4/go.mod h1:g63b33BZ5vZzcIUF8AtRH40DrTlXnx4UMC8rBdndmjQ=
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/sagikazarmark/locafero v0.9.0 h1:GbgQGNtTrEmddYDSAH9QLRyfAHY12md+8YFTqyMTC9k=
github.com/sagikazarmark/locafero v0.9.0/go.mod h1:UBUyz37V+EdMS3hDF3QWIiVr/2dPrx49OMO0Bn0hJqk=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA=
github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo=
github.com/spf13/cast v1.9.2 h1:SsGfm7M8QOFtEzumm7UZrZdLLquNdzFYfIbEXntcFbE=
github.com/spf13/cast v1.9.2/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M=
github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=
github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
104 changes: 104 additions & 0 deletions components/renku_session_services/api_proxy/pkg/apiproxy/apiproxy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package apiproxy

import (
"fmt"
"log/slog"
"net/url"
"os"
"strings"

"github.com/SwissDataScienceCenter/renku-data-services/components/renku_session_services/api_proxy/pkg/config"
"github.com/SwissDataScienceCenter/renku-data-services/components/renku_session_services/api_proxy/pkg/tokenstore"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)

type ApiProxy struct {
config *config.Config
store *tokenstore.TokenStore
}

func (ap *ApiProxy) RegisterHandlers(e *echo.Echo, commonMiddlewares ...echo.MiddlewareFunc) {
dataApiURL := ap.config.RenkuURL.JoinPath("api/data")
sessionURL := dataApiURL.JoinPath("sessions", ap.config.SessionID)
sessionPath := sessionURL.EscapedPath()
if !strings.HasPrefix(sessionPath, "/") {
sessionPath = "/" + sessionPath
}

tokenMiddleware := ap.getTokenMiddleware()
dataServiceProxy := proxyFromURL(ap.config.RenkuURL)

slog.Info("setting up reverse proxy for session", "path", sessionPath)
e.Group(sessionPath, append(commonMiddlewares, tokenMiddleware, setHost(ap.config.RenkuURL.Host), dataServiceProxy)...)
}

func (ap *ApiProxy) getTokenMiddleware() echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
existingToken := c.Request().Header.Get(echo.HeaderAuthorization)
if existingToken != "" {
return next(c)
}
token, err := ap.store.GetValidRenkuAccessToken()
if err != nil {
slog.Info("token could not be loaded", "error", err)
return next(c)
}
c.Request().Header.Set(echo.HeaderAuthorization, fmt.Sprintf("Bearer %s", token))
return next(c)
}
}
}

func proxyFromURL(url *url.URL) echo.MiddlewareFunc {
if url == nil {
slog.Error("cannot create a proxy from a nil URL")
os.Exit(1)
}
config := middleware.ProxyConfig{
// the skipper is used to log only
Skipper: func(c echo.Context) bool {
slog.Info("proxy", "destination", url.String(), "request", c.Request().URL.String())
// slog.Info("PROXY", "requestID", utils.GetRequestID(c), "destination", url.String())
return false
},
Balancer: middleware.NewRoundRobinBalancer([]*middleware.ProxyTarget{
{
Name: url.String(),
URL: url,
}}),
}
return middleware.ProxyWithConfig(config)
}

func setHost(host string) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
c.Request().Host = host
return next(c)
}
}
}

type ApiProxyOption func(*ApiProxy)

func WithConfig(config config.Config) ApiProxyOption {
return func(ap *ApiProxy) {
ap.config = &config
}
}

func WithTokenStore(store *tokenstore.TokenStore) ApiProxyOption {
return func(ap *ApiProxy) {
ap.store = store
}
}

func NewApiProxy(options ...ApiProxyOption) (apiProxy *ApiProxy, err error) {
server := ApiProxy{}
for _, opt := range options {
opt(&server)
}
return &server, nil
}
Loading