This commit is contained in:
yehudit1987 2025-09-14 15:57:32 +03:00 committed by GitHub
commit 05e55caa77
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 1395 additions and 38 deletions

View File

@ -86,6 +86,10 @@ build: fmt vet swag ## Build backend binary.
run: fmt vet swag ## Run a backend from your host.
go run ./cmd/main.go --port=$(PORT)
.PHONY: run-envtest
run-envtest: fmt vet prepare-envtest-assets ## Run envtest.
go run ./cmd/main.go --enable-envtest --port=$(PORT)
# If you wish to build the manager image targeting other platforms you can use the --platform flag.
# (i.e. docker build --platform linux/arm64). However, you must enable docker buildKit for it.
# More info: https://docs.docker.com/develop/develop-images/build_enhancements/
@ -135,6 +139,11 @@ ENVTEST_VERSION ?= release-0.19
GOLANGCI_LINT_VERSION ?= v1.61.0
SWAGGER_VERSION ?= v1.16.6
.PHONY: prepare-envtest-assets
prepare-envtest-assets: envtest ## Download K8s control plane binaries directly into ./bin/k8s/
@echo ">>>> Downloading envtest Kubernetes control plane binaries to ./bin/k8s/..."
$(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir=$(LOCALBIN)
.PHONY: SWAGGER
SWAGGER: $(SWAGGER)
$(SWAGGER): $(LOCALBIN)

View File

@ -18,17 +18,22 @@ package main
import (
"flag"
"fmt"
"log/slog"
"os"
"path/filepath"
stdruntime "runtime"
"strconv"
"github.com/go-logr/logr"
ctrl "sigs.k8s.io/controller-runtime"
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
application "github.com/kubeflow/notebooks/workspaces/backend/api"
"github.com/kubeflow/notebooks/workspaces/backend/internal/auth"
"github.com/kubeflow/notebooks/workspaces/backend/internal/config"
"github.com/kubeflow/notebooks/workspaces/backend/internal/helper"
"github.com/kubeflow/notebooks/workspaces/backend/internal/k8sclientfactory"
"github.com/kubeflow/notebooks/workspaces/backend/internal/server"
)
@ -47,7 +52,7 @@ import (
// @consumes application/json
// @produces application/json
func main() {
func run() error {
// Define command line flags
cfg := &config.EnvConfig{}
flag.IntVar(&cfg.Port,
@ -93,44 +98,59 @@ func main() {
"Key of request header containing user groups",
)
// Initialize the logger
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
var enableEnvTest bool
flag.BoolVar(&enableEnvTest,
"enable-envtest",
getEnvAsBool("ENABLE_ENVTEST", false),
"Enable envtest for local development without a real k8s cluster",
)
flag.Parse()
// Build the Kubernetes client configuration
kubeconfig, err := ctrl.GetConfig()
if err != nil {
logger.Error("failed to get Kubernetes config", "error", err)
os.Exit(1)
}
kubeconfig.QPS = float32(cfg.ClientQPS)
kubeconfig.Burst = cfg.ClientBurst
// Initialize the logger
slogTextHandler := slog.NewTextHandler(os.Stdout, nil)
logger := slog.New(slogTextHandler)
// Build the Kubernetes scheme
scheme, err := helper.BuildScheme()
if err != nil {
logger.Error("failed to build Kubernetes scheme", "error", err)
os.Exit(1)
return err
}
// Create the controller manager
mgr, err := ctrl.NewManager(kubeconfig, ctrl.Options{
Scheme: scheme,
Metrics: metricsserver.Options{
BindAddress: "0", // disable metrics serving
},
HealthProbeBindAddress: "0", // disable health probe serving
LeaderElection: false,
})
// Defining CRD's path
crdPath := os.Getenv("CRD_PATH")
if crdPath == "" {
_, currentFile, _, ok := stdruntime.Caller(0)
if !ok {
logger.Info("Failed to get current file path using stdruntime.Caller")
}
testFileDir := filepath.Dir(currentFile)
crdPath = filepath.Join(testFileDir, "..", "..", "controller", "config", "crd", "bases")
logger.Info("CRD_PATH not set, using guessed default", "path", crdPath)
}
// ctx creates a context that listens for OS signals (e.g., SIGINT, SIGTERM) for graceful shutdown.
ctx := ctrl.SetupSignalHandler()
logrlogger := logr.FromSlogHandler(slogTextHandler)
// factory creates a new Kubernetes client factory, configured for envtest if enabled.
factory := k8sclientfactory.NewClientFactory(logrlogger, scheme, enableEnvTest, []string{crdPath}, cfg)
// Create the controller manager, build Kubernetes client configuration
// envtestCleanupFunc is a function to clean envtest if it was created, otherwise it's an empty function.
mgr, _, envtestCleanupFunc, err := factory.GetManagerAndConfig(ctx)
defer envtestCleanupFunc()
if err != nil {
logger.Error("unable to create manager", "error", err)
os.Exit(1)
logger.Error("Failed to get Kubernetes manager/config from factory", "error", err)
return err
}
// Create the request authenticator
reqAuthN, err := auth.NewRequestAuthenticator(cfg.UserIdHeader, cfg.UserIdPrefix, cfg.GroupsHeader)
if err != nil {
logger.Error("failed to create request authenticator", "error", err)
os.Exit(1)
return err
}
// Create the request authorizer
@ -143,22 +163,30 @@ func main() {
app, err := application.NewApp(cfg, logger, mgr.GetClient(), mgr.GetScheme(), reqAuthN, reqAuthZ)
if err != nil {
logger.Error("failed to create app", "error", err)
os.Exit(1)
return err
}
svr, err := server.NewServer(app, logger)
if err != nil {
logger.Error("failed to create server", "error", err)
os.Exit(1)
return err
}
if err := svr.SetupWithManager(mgr); err != nil {
logger.Error("failed to setup server with manager", "error", err)
os.Exit(1)
return err
}
// Start the controller manager
logger.Info("starting manager")
if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
logger.Error("problem running manager", "error", err)
logger.Info("Starting manager...")
if err := mgr.Start(ctx); err != nil {
logger.Error("Problem running manager", "error", err)
return err
}
return nil
}
func main() {
if err := run(); err != nil {
fmt.Fprintf(os.Stderr, "Application run failed: %v\n", err)
os.Exit(1)
}
}

View File

@ -33,12 +33,10 @@ require (
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-logr/zapr v1.3.0 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/jsonreference v0.21.0 // indirect
github.com/go-openapi/spec v0.21.0 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
@ -55,7 +53,6 @@ require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/mailru/easyjson v0.9.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
@ -67,7 +64,6 @@ require (
github.com/spf13/cobra v1.8.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/stoewer/go-strcase v1.2.0 // indirect
github.com/swaggo/files/v2 v2.0.2 // indirect
github.com/x448/float16 v0.8.4 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect
go.opentelemetry.io/otel v1.28.0 // indirect
@ -88,7 +84,6 @@ require (
golang.org/x/term v0.29.0 // indirect
golang.org/x/text v0.22.0 // indirect
golang.org/x/time v0.3.0 // indirect
golang.org/x/tools v0.30.0 // indirect
gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect
@ -104,5 +99,13 @@ require (
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.30.3 // indirect
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
sigs.k8s.io/yaml v1.4.0 // indirect
)
require (
github.com/go-logr/logr v1.4.2
github.com/go-openapi/spec v0.21.0 // indirect
github.com/mailru/easyjson v0.9.0 // indirect
github.com/swaggo/files/v2 v2.0.2 // indirect
golang.org/x/tools v0.30.0 // indirect
sigs.k8s.io/yaml v1.4.0
)

View File

@ -0,0 +1,133 @@
/*
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 k8sclientfactory
import (
"context"
"errors"
"fmt"
"github.com/go-logr/logr"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/envtest"
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
"github.com/kubeflow/notebooks/workspaces/backend/internal/config"
"k8s.io/client-go/rest"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/kubeflow/notebooks/workspaces/backend/localdev"
)
// ClientFactory responsible for providing a Kubernetes client and manager
type ClientFactory struct {
useEnvtest bool
crdPaths []string
logger logr.Logger
scheme *runtime.Scheme
clientQPS float64
clientBurst int
}
// NewClientFactory creates a new factory
func NewClientFactory(
logger logr.Logger,
scheme *runtime.Scheme,
useEnvtest bool,
crdPaths []string,
appCfg *config.EnvConfig,
) *ClientFactory {
return &ClientFactory{
useEnvtest: useEnvtest,
crdPaths: crdPaths,
logger: logger.WithName("k8s-client-factory"),
scheme: scheme,
clientQPS: appCfg.ClientQPS,
clientBurst: appCfg.ClientBurst,
}
}
// GetManagerAndConfig returns a configured Kubernetes manager and its rest.Config
// It also returns a cleanup function for envtest if it was started.
func (f *ClientFactory) GetManagerAndConfig(ctx context.Context) (ctrl.Manager, *rest.Config, func(), error) {
var mgr ctrl.Manager
var cfg *rest.Config
var err error
var cleanupFunc func() = func() {} // No-op cleanup by default
if f.useEnvtest {
f.logger.Info("Using envtest mode: setting up local Kubernetes environment...")
var testEnvInstance *envtest.Environment
cfg, mgr, testEnvInstance, err = localdev.StartLocalDevEnvironment(ctx, f.crdPaths, f.scheme)
if err != nil {
return nil, nil, nil, fmt.Errorf("could not start local dev environment: %w", err)
}
f.logger.Info("Local dev K8s API (envtest) is ready.", "host", cfg.Host)
if testEnvInstance != nil {
cleanupFunc = func() {
f.logger.Info("Stopping envtest environment...")
if err := testEnvInstance.Stop(); err != nil {
f.logger.Error(err, "Failed to stop envtest environment")
}
}
} else {
err = errors.New("StartLocalDevEnvironment returned successfully but with a nil testEnv instance, cleanup is not possible")
f.logger.Error(err, "invalid return state from localdev setup")
return nil, nil, nil, err
}
} else {
f.logger.Info("Using real cluster mode: connecting to existing Kubernetes cluster...")
cfg, err = ctrl.GetConfig()
if err != nil {
return nil, nil, nil, fmt.Errorf("failed to get Kubernetes config: %w", err)
}
f.logger.Info("Successfully connected to existing Kubernetes cluster.")
cfg.QPS = float32(f.clientQPS)
cfg.Burst = f.clientBurst
mgr, err = ctrl.NewManager(cfg, ctrl.Options{
Scheme: f.scheme,
Metrics: metricsserver.Options{
BindAddress: "0", // disable metrics serving
},
HealthProbeBindAddress: "0", // disable health probe serving
LeaderElection: false,
})
if err != nil {
return nil, nil, nil, fmt.Errorf("unable to create manager for real cluster: %w", err)
}
f.logger.Info("Successfully configured manager for existing Kubernetes cluster.")
}
return mgr, cfg, cleanupFunc, nil
}
// GetClient returns just the client.Client (useful if manager lifecycle is handled elsewhere or already started)
func (f *ClientFactory) GetClient(ctx context.Context) (client.Client, func(), error) {
mgr, _, cleanup, err := f.GetManagerAndConfig(ctx)
if err != nil {
if cleanup != nil {
f.logger.Info("Calling cleanup function due to error during manager/config retrieval", "error", err)
cleanup()
}
return nil, cleanup, err
}
return mgr.GetClient(), cleanup, nil
}

View File

@ -0,0 +1,131 @@
/*
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 localdev
import (
"context"
"errors"
"fmt"
"os"
"path/filepath"
stdruntime "runtime"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/rest"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/envtest"
"sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
)
var (
testEnv *envtest.Environment
)
// StartLocalDevEnvironment starts the envtest and the controllers
func StartLocalDevEnvironment(ctx context.Context, crdPaths []string,
localScheme *runtime.Scheme) (*rest.Config, ctrl.Manager, *envtest.Environment, error) {
setupLog := ctrl.Log.WithName("setup-localdev")
projectRoot, err := getProjectRoot()
if err != nil {
setupLog.Error(err, "Failed to get project root")
return nil, nil, nil, err
}
log.SetLogger(zap.New(zap.WriteTo(os.Stderr), zap.UseDevMode(true)))
setupLog.Info("Setting up envtest environment...")
testEnv = &envtest.Environment{
CRDDirectoryPaths: crdPaths,
ErrorIfCRDPathMissing: true,
BinaryAssetsDirectory: filepath.Join(projectRoot, "bin", "k8s",
fmt.Sprintf("1.31.0-%s-%s", stdruntime.GOOS, stdruntime.GOARCH)),
}
// --- turning envtest on ---
cfg, err := testEnv.Start()
if err != nil {
setupLog.Error(err, "Failed to start envtest")
return nil, nil, testEnv, err
}
setupLog.Info("envtest started successfully")
// --- Manager creation ---
// The Manager is the "brain" of controller-runtime.
setupLog.Info("Creating controller-runtime manager")
mgr, err := ctrl.NewManager(cfg, ctrl.Options{
Scheme: localScheme,
LeaderElection: false,
})
if err != nil {
setupLog.Error(err, "Failed to create manager")
CleanUpEnvTest()
return nil, nil, testEnv, err
}
// --- Creating resources (Namespace, WorkspaceKind, Workspace) ---
if err := createInitialResources(ctx, mgr.GetClient()); err != nil {
setupLog.Error(err, "Failed to create initial resources")
} else {
setupLog.Info("Initial resources created successfully")
}
setupLog.Info("Local development environment is ready!")
return cfg, mgr, testEnv, nil
}
// CleanUpEnvTest stops the envtest.
func CleanUpEnvTest() {
cleanupLog := ctrl.Log.WithName("envtest-cleanup") // Or pass logger from factory
if testEnv != nil {
cleanupLog.Info("Attempting to stop envtest control plane...")
if err := testEnv.Stop(); err != nil {
cleanupLog.Error(err, "Failed to stop envtest control plane")
} else {
cleanupLog.Info("Envtest control plane stopped successfully.")
}
} else {
cleanupLog.Info("testEnv was nil, nothing to stop.")
}
ctrl.Log.Info("Local dev environment stopped.")
}
// getProjectRoot finds the project root directory by searching upwards from the currently
func getProjectRoot() (string, error) {
_, currentFile, _, ok := stdruntime.Caller(0)
if !ok {
return "", errors.New("cannot get current file's path via runtime.Caller")
}
// Start searching from the directory containing this Go file.
currentDir := filepath.Dir(currentFile)
for {
goModPath := filepath.Join(currentDir, "go.mod")
if _, err := os.Stat(goModPath); err == nil {
return currentDir, nil
}
parentDir := filepath.Dir(currentDir)
if parentDir == currentDir {
return "", errors.New("could not find project root containing go.mod")
}
currentDir = parentDir
}
}

View File

@ -0,0 +1,373 @@
/*
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 localdev
import (
"context"
"errors"
"fmt"
"os"
"path/filepath"
stdruntime "runtime"
"strings"
"k8s.io/apimachinery/pkg/api/resource"
kubefloworgv1beta1 "github.com/kubeflow/notebooks/workspaces/controller/api/v1beta1"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/yaml"
)
// --- Helper Functions for Pointers ---
func stringPtr(s string) *string { return &s }
func boolPtr(b bool) *bool { return &b }
// --- Specialized Functions for Resource Creation ---
func createNamespace(ctx context.Context, cl client.Client, namespaceName string) error {
logger := log.FromContext(ctx).WithName("create-namespace")
ns := &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{Name: namespaceName},
}
logger.Info("Creating namespace", "name", namespaceName)
if err := cl.Create(ctx, ns); err != nil {
if !apierrors.IsAlreadyExists(err) {
logger.Error(err, "Failed to create namespace", "name", namespaceName)
return fmt.Errorf("failed to create namespace %s: %w", namespaceName, err)
}
logger.Info("Namespace already exists", "name", namespaceName)
}
return nil
}
func loadAndCreateWorkspaceKindsFromDir(ctx context.Context, cl client.Client,
dirPath string) ([]kubefloworgv1beta1.WorkspaceKind, error) {
logger := log.FromContext(ctx).WithName("load-create-workspacekinds")
logger.Info("Loading WorkspaceKind YAMLs from", "path", dirPath)
absDirPath, err := filepath.Abs(dirPath)
if err != nil {
logger.Error(err, "Failed to get absolute path for dirPath", "path", dirPath)
return nil, fmt.Errorf("failed to get absolute path for dirPath %s: %w", dirPath, err)
}
absDirPath = filepath.Clean(absDirPath)
yamlFiles, err := filepath.Glob(filepath.Join(absDirPath, "*.yaml")) // Use *.yaml to get all YAML files
if err != nil {
logger.Error(err, "Failed to glob WorkspaceKind YAML files", "path", dirPath)
return nil, fmt.Errorf("failed to glob WorkspaceKind YAML files in %s: %w", dirPath, err)
}
if len(yamlFiles) == 0 {
logger.Info("No WorkspaceKind YAML files found in", "path", dirPath)
return []kubefloworgv1beta1.WorkspaceKind{}, nil // Return empty slice, not an error
}
var successfullyCreatedWKs []kubefloworgv1beta1.WorkspaceKind
for _, yamlFile := range yamlFiles {
logger.Info("Processing WorkspaceKind from file", "file", yamlFile)
absYamlFile, err := filepath.Abs(yamlFile)
if err != nil {
logger.Error(err, "Failed to get absolute path for yaml file", "file", yamlFile)
continue
}
absYamlFile = filepath.Clean(absYamlFile)
if !strings.HasPrefix(absYamlFile, absDirPath) {
errUnsafePath := fmt.Errorf("unsafe file path: resolved file '%s' is outside allowed directory '%s'",
absYamlFile, absDirPath)
logger.Error(errUnsafePath, "Skipping potentially unsafe file", "original_file",
yamlFile)
continue
}
yamlContent, errReadFile := os.ReadFile(absYamlFile)
if errReadFile != nil {
logger.Error(errReadFile, "Failed to read WorkspaceKind YAML file", "file", yamlFile)
continue // Skip this file
}
var wk kubefloworgv1beta1.WorkspaceKind
errUnmarshal := yaml.UnmarshalStrict(yamlContent, &wk)
if errUnmarshal != nil {
logger.Error(errUnmarshal, "Failed to unmarshal YAML to WorkspaceKind", "file", yamlFile)
continue // Skip this file
}
if wk.Name == "" {
logger.Error(errors.New("WorkspaceKind has no name"), "Skipping creation for file",
"file", yamlFile)
continue
}
logger.Info("Attempting to create/verify WorkspaceKind in API server", "name", wk.GetName())
errCreate := cl.Create(ctx, &wk)
if errCreate != nil {
if apierrors.IsAlreadyExists(errCreate) {
logger.Info("WorkspaceKind already exists in API server. Fetching it.", "name",
wk.GetName())
var existingWk kubefloworgv1beta1.WorkspaceKind
if errGet := cl.Get(ctx, client.ObjectKey{Name: wk.Name}, &existingWk); errGet == nil {
successfullyCreatedWKs = append(successfullyCreatedWKs, existingWk)
} else {
logger.Error(errGet, "WorkspaceKind already exists but failed to GET it", "name",
wk.GetName())
}
} else {
logger.Error(errCreate, "Failed to create WorkspaceKind in API server", "name",
wk.GetName(), "file", yamlFile)
}
} else {
logger.Info("Successfully created WorkspaceKind in API server", "name", wk.GetName())
successfullyCreatedWKs = append(successfullyCreatedWKs, wk)
}
}
logger.Info("Finished processing WorkspaceKind YAML files.", "successfully_processed_count",
len(successfullyCreatedWKs))
return successfullyCreatedWKs, nil
}
func extractConfigIDsFromWorkspaceKind(ctx context.Context,
wkCR *kubefloworgv1beta1.WorkspaceKind) (imageConfigID string, podConfigID string, err error) {
logger := log.FromContext(ctx).WithName("extract-config-ids").WithValues("workspaceKindName",
wkCR.Name)
// --- Handle ImageConfig ---
imageConf := wkCR.Spec.PodTemplate.Options.ImageConfig
if imageConf.Spawner.Default != "" {
imageConfigID = imageConf.Spawner.Default
} else {
logger.V(1).Info("No default imageConfig found in Spawner. Trying first available from 'Values'.")
if len(imageConf.Values) > 0 {
imageConfigID = imageConf.Values[0].Id // Ensure .ID matches your struct field name
} else {
err = fmt.Errorf("WorkspaceKind '%s' has no suitable imageConfig options "+
"(no Spawner.Default and no Values)", wkCR.Name)
logger.Error(err, "Cannot determine imageConfigID.")
return "", "", err // Return error if no ID could be found
}
}
// --- Handle PodConfig ---
podConf := wkCR.Spec.PodTemplate.Options.PodConfig
if podConf.Spawner.Default != "" {
podConfigID = podConf.Spawner.Default
} else {
logger.V(1).Info("No default podConfig found in Spawner. Trying first available from 'Values'.")
if len(podConf.Values) > 0 {
podConfigID = podConf.Values[0].Id // Ensure .ID matches your struct field name
} else {
err = fmt.Errorf("WorkspaceKind '%s' has no suitable podConfig options "+
"(no Spawner.Default and no Values)", wkCR.Name)
logger.Error(err, "Cannot determine podConfigID.")
return imageConfigID, "", err
}
}
logger.V(1).Info("Determined config IDs", "imageConfigID", imageConfigID, "podConfigID",
podConfigID)
return imageConfigID, podConfigID, nil
}
// createPVC creates a PersistentVolumeClaim with a default size and access mode.
func createPVC(ctx context.Context, cl client.Client, namespace, pvcName string) error {
logger := log.FromContext(ctx).WithName("create-pvc")
pvc := &corev1.PersistentVolumeClaim{
ObjectMeta: metav1.ObjectMeta{
Name: pvcName,
Namespace: namespace,
},
Spec: corev1.PersistentVolumeClaimSpec{
AccessModes: []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce},
Resources: corev1.VolumeResourceRequirements{
Requests: corev1.ResourceList{
// Defaulting storage size. This can be parameterized if needed.
corev1.ResourceStorage: resource.MustParse("10Gi"),
},
},
},
}
logger.Info("Creating PersistentVolumeClaim", "name", pvcName, "namespace", namespace)
if err := cl.Create(ctx, pvc); err != nil {
if !apierrors.IsAlreadyExists(err) {
logger.Error(err, "Failed to create PersistentVolumeClaim", "name", pvcName, "namespace", namespace)
return fmt.Errorf("failed to create PVC %s in namespace %s: %w", pvcName, namespace, err)
}
logger.Info("PersistentVolumeClaim already exists", "name", pvcName, "namespace", namespace)
}
return nil
}
func createWorkspacesForKind(ctx context.Context, cl client.Client, namespaceName string,
wkCR *kubefloworgv1beta1.WorkspaceKind, instancesPerKind int) error {
logger := log.FromContext(ctx).WithName("create-workspaces").WithValues("workspaceKindName",
wkCR.Name)
logger.Info("Preparing to create Workspaces")
imageConfigID, podConfigID, err := extractConfigIDsFromWorkspaceKind(ctx, wkCR)
if err != nil {
return fmt.Errorf("skipping workspace creation for %s due to config ID extraction error: %w",
wkCR.Name, err)
}
for i := 1; i <= instancesPerKind; i++ {
workspaceName := fmt.Sprintf("%s-ws-%d", wkCR.Name, i)
homePVCName := fmt.Sprintf("%s-homevol", workspaceName)
dataPVCName := fmt.Sprintf("%s-datavol", workspaceName)
// Create the required PVCs before creating the Workspace
if err := createPVC(ctx, cl, namespaceName, homePVCName); err != nil {
logger.Error(err, "Failed to create home PVC for workspace, skipping workspace creation",
"workspaceName", workspaceName, "pvcName", homePVCName)
continue // Skip this workspace instance
}
if err := createPVC(ctx, cl, namespaceName, dataPVCName); err != nil {
logger.Error(err, "Failed to create data PVC for workspace, skipping workspace creation",
"workspaceName", workspaceName, "pvcName", dataPVCName)
continue // Skip this workspace instance
}
ws := newWorkspace(workspaceName, namespaceName, wkCR.Name, imageConfigID, podConfigID, i)
logger.Info("Attempting to create Workspace in API server", "name", ws.Name, "namespace",
ws.Namespace)
if errCreateWS := cl.Create(ctx, ws); errCreateWS != nil {
if apierrors.IsAlreadyExists(errCreateWS) {
logger.Info("Workspace already exists", "name", ws.Name, "namespace", ws.Namespace)
} else {
logger.Error(errCreateWS, "Failed to create Workspace in API server", "name",
ws.Name, "namespace", ws.Namespace)
// Optionally, collect errors and return them at the end, or return on first error
}
} else {
logger.Info("Successfully created Workspace in API server", "name",
ws.Name, "namespace", ws.Namespace)
}
}
return nil
}
// newWorkspace is a helper function to construct a Workspace object
func newWorkspace(name, namespace, workspaceKindName, imageConfigID string, podConfigID string,
instanceNumber int) *kubefloworgv1beta1.Workspace {
// PVC names will be unique based on the workspace name
homePVCName := fmt.Sprintf("%s-homevol", name) // Example naming for home PVC
dataPVCName := fmt.Sprintf("%s-datavol", name) // Example naming for data PVC
return &kubefloworgv1beta1.Workspace{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
Labels: map[string]string{
"app.kubernetes.io/name": name,
"app.kubernetes.io/instance": fmt.Sprintf("%s-%d", workspaceKindName, instanceNumber),
"app.kubernetes.io/created-by": "envtest-initial-resources",
},
Annotations: map[string]string{
"description": fmt.Sprintf("Workspace instance #%d for %s", instanceNumber, workspaceKindName),
},
},
Spec: kubefloworgv1beta1.WorkspaceSpec{
Paused: boolPtr(true), // Workspace starts in a paused state
DeferUpdates: boolPtr(false), // Default value
Kind: workspaceKindName, // Link to the WorkspaceKind CR
PodTemplate: kubefloworgv1beta1.WorkspacePodTemplate{ // Assuming PodTemplate is a pointer
PodMetadata: &kubefloworgv1beta1.WorkspacePodMetadata{ // Assuming PodMetadata is a pointer
Labels: map[string]string{"user-label": "example-value"},
Annotations: map[string]string{"user-annotation": "example-value"},
},
Volumes: kubefloworgv1beta1.WorkspacePodVolumes{ // Assuming Volumes is a pointer
Home: stringPtr(homePVCName), // Assuming Home is *string
Data: []kubefloworgv1beta1.PodVolumeMount{ // Data is likely []DataVolume
{
PVCName: dataPVCName, // Assuming PVCName is string
MountPath: "/data/user-data",
ReadOnly: boolPtr(false),
},
},
},
Options: kubefloworgv1beta1.WorkspacePodOptions{ // Assuming Options is a pointer
ImageConfig: imageConfigID,
PodConfig: podConfigID,
},
},
},
}
}
// createInitialResources creates namespaces, WorkspaceKinds, and Workspaces.
func createInitialResources(ctx context.Context, cl client.Client) error {
logger := log.FromContext(ctx).WithName("create-initial-resources")
// Configurations
namespaceName := "envtest-ns"
_, currentFile, _, ok := stdruntime.Caller(0)
if !ok {
err := errors.New("failed to get current file path using stdruntime.Caller")
logger.Error(err, "Cannot determine testdata directory path")
return err
}
testFileDir := filepath.Dir(currentFile)
workspaceKindsTestDataDir := filepath.Join(testFileDir, "testdata")
numWorkspacesPerKind := 3
// 1. Create Namespace
logger.Info("Creating namespace", "name", namespaceName)
if err := createNamespace(ctx, cl, namespaceName); err != nil {
logger.Error(err, "Failed during namespace creation step")
return err // Assuming namespace is critical
}
logger.Info("Namespace step completed.")
// 2. Create WorkspaceKinds
logger.Info("Loading and Creating WorkspaceKinds from", "directory", workspaceKindsTestDataDir)
successfullyCreatedWKs, err := loadAndCreateWorkspaceKindsFromDir(ctx, cl, workspaceKindsTestDataDir)
if err != nil {
logger.Error(err, "Failed during WorkspaceKind processing step")
return err // Assuming WorkspaceKinds are critical
}
if len(successfullyCreatedWKs) == 0 {
logger.Info("No WorkspaceKinds were loaded or created. Will not proceed")
return errors.New("no WorkspaceKinds were loaded or created")
} else {
logger.Info("WorkspaceKind processing step completed.",
"successfully_processed_count", len(successfullyCreatedWKs))
}
// Step 3: Create Workspaces for each successfully processed Kind
logger.Info("Step 3: Creating Workspaces")
if len(successfullyCreatedWKs) > 0 {
for _, wkCR := range successfullyCreatedWKs {
kindSpecificLogger := logger.WithValues("workspaceKind", wkCR.Name)
kindSpecificCtx := log.IntoContext(ctx, kindSpecificLogger)
if err := createWorkspacesForKind(kindSpecificCtx, cl, namespaceName, &wkCR, numWorkspacesPerKind); err != nil {
kindSpecificLogger.Error(err,
"Failed to create all workspaces for this kind. Continuing with other kinds if any.")
}
}
} else {
logger.Info("Skipping Workspace creation as no WorkspaceKinds are available.")
}
logger.Info("Initial resources setup process completed.")
return nil
}

View File

@ -0,0 +1,227 @@
# rstudio_v1beta1_workspacekind.yaml
apiVersion: kubeflow.org/v1beta1
kind: WorkspaceKind
metadata:
name: rstudio-envtest
spec:
## ================================================================
## SPAWNER CONFIGS
## - how the WorkspaceKind is displayed in the Workspace Spawner UI
## ================================================================
spawner:
## the display name of the WorkspaceKind
displayName: "RStudio IDE"
## the description of the WorkspaceKind
description: "A Workspace which runs the RStudio IDE in a Pod"
## if this WorkspaceKind should be hidden from the Workspace Spawner UI
hidden: false
## if this WorkspaceKind is deprecated
deprecated: false
## a message to show in Workspace Spawner UI when the WorkspaceKind is deprecated
deprecationMessage: "This WorkspaceKind will be removed on 20XX-XX-XX, please use another WorkspaceKind."
## the icon of the WorkspaceKind
## - a small (favicon-sized) icon used in the Workspace Spawner UI
##
icon:
url: "https://avatars.githubusercontent.com/u/513560?s=48&v=4"
#configMap:
# name: "my-logos"
# key: "apple-touch-icon-152x152.png"
## the logo of the WorkspaceKind
## - a 1:1 (card size) logo used in the Workspace Spawner UI
##
logo:
url: "https://avatars.githubusercontent.com/u/513560?s=48&v=4"
## ================================================================
## DEFINITION CONFIGS
## ================================================================
podTemplate:
## metadata for Workspace Pods (MUTABLE)
podMetadata:
labels:
my-workspace-kind-label: "my-value"
annotations:
my-workspace-kind-annotation: "my-value"
## service account configs for Workspace Pods
serviceAccount:
## the name of the ServiceAccount (NOT MUTABLE)
name: "default-editor"
## volume mount paths
volumeMounts:
## the path to mount the home PVC (NOT MUTABLE)
home: "/home/rstudio"
## http proxy configs (MUTABLE)
httpProxy:
## if the path prefix is stripped from incoming HTTP requests
## - if true, the '/workspace/{profile_name}/{workspace_name}/' path prefix
## is stripped from incoming requests, the application sees the request
## as if it was made to '/...'
## - this only works if the application serves RELATIVE URLs for its assets
removePathPrefix: false
requestHeaders:
set:
X-RStudio-Root-Path: "{{ .PathPrefix }}"
## ==============================================================
## WORKSPACE OPTIONS
## - options are the user-selectable fields,
## they determine the PodSpec of the Workspace
## ==============================================================
options:
##
## About the `values` fields:
## - the `values` field is a list of options that the user can select
## - elements of `values` can NOT be removed, only HIDDEN or REDIRECTED
## - this prevents options being removed that are still in use by existing Workspaces
## - this limitation may be removed in the future
## - options may be "hidden" by setting `spawner.hidden` to `true`
## - hidden options are NOT selectable in the Spawner UI
## - hidden options are still available to the controller and manually created Workspace resources
## - options may be "redirected" by setting `redirect.to` to another option:
## - redirected options are NOT shown in the Spawner UI
## - redirected options are like an HTTP 302 redirect, the controller will use the target option
## without actually changing the `spec.podTemplate.options` field of the Workspace
## - the Spawner UI will warn users about Workspaces with pending restarts
##
## ============================================================
## IMAGE CONFIG OPTIONS
## - SETS: image, imagePullPolicy, ports
## ============================================================
imageConfig:
## spawner ui configs
spawner:
## the id of the default option
default: "rstudio_latest"
## the list of image configs that are available
values:
## ================================
## EXAMPLE 1: a basic RStudio image
## ================================
- id: "rstudio_latest"
spawner:
displayName: "RStudio (Latest)"
description: "Latest stable release of RStudio"
spec:
## the container image to use
image: "ghcr.io/kubeflow/kubeflow/notebook-servers/rstudio:latest"
## the pull policy for the container image
imagePullPolicy: "IfNotPresent"
## ports that the container listens on
ports:
- id: "rstudio"
displayName: "RStudio"
port: 8787
protocol: "HTTP"
## ================================
## EXAMPLE 2: an RStudio image with specific R version
## ================================
- id: "rstudio_v1.9.1"
spawner:
displayName: "RStudio (V 1.9.1)"
description: "RStudio with R version 1.9.1"
spec:
image: "ghcr.io/kubeflow/kubeflow/notebook-servers/rstudio:v1.9.1"
imagePullPolicy: "IfNotPresent"
ports:
- id: "rstudio"
displayName: "RStudio"
port: 8787
protocol: "HTTP"
## ============================================================
## POD CONFIG OPTIONS
## - SETS: affinity, nodeSelector, tolerations, resources
## ============================================================
podConfig:
## spawner ui configs
spawner:
## the id of the default option
default: "tiny_cpu"
## the list of pod configs that are available
values:
## ================================
## EXAMPLE 1: a tiny CPU pod
## ================================
- id: "tiny_cpu"
spawner:
displayName: "Tiny CPU"
description: "Pod with 0.1 CPU, 128 Mb RAM"
labels:
- key: "cpu"
value: "100m"
- key: "memory"
value: "128Mi"
spec:
resources:
requests:
cpu: 100m
memory: 128Mi
## ================================
## EXAMPLE 2: a small CPU pod
## ================================
- id: "small_cpu"
spawner:
displayName: "Small CPU"
description: "Pod with 1 CPU, 2 GB RAM"
labels:
- key: "cpu"
value: "1000m"
- key: "memory"
value: "2Gi"
hidden: false
spec:
resources:
requests:
cpu: 1000m
memory: 2Gi
## ================================
## EXAMPLE 3: a big GPU pod
## ================================
- id: "big_gpu"
spawner:
displayName: "Big GPU"
description: "Pod with 4 CPU, 16 GB RAM, and 1 GPU"
labels:
- key: "cpu"
value: "4000m"
- key: "memory"
value: "16Gi"
- key: "gpu"
value: "1"
hidden: false
spec:
resources:
requests:
cpu: 4000m
memory: 16Gi
limits:
nvidia.com/gpu: 1

View File

@ -0,0 +1,217 @@
# codeserver_v1beta1_workspacekind.yaml
apiVersion: kubeflow.org/v1beta1
kind: WorkspaceKind
metadata:
name: codeserver-envtest
spec:
## ================================================================
## SPAWNER CONFIGS
## - how the WorkspaceKind is displayed in the Workspace Spawner UI
## ================================================================
spawner:
## the display name of the WorkspaceKind
displayName: "Code-Server IDE"
## the description of the WorkspaceKind
description: "A Workspace which runs Code-Server (VS Code in a browser) in a Pod"
## if this WorkspaceKind should be hidden from the Workspace Spawner UI
hidden: false
## if this WorkspaceKind is deprecated
deprecated: false
## a message to show in Workspace Spawner UI when the WorkspaceKind is deprecated
deprecationMessage: "This WorkspaceKind will be removed on 20XX-XX-XX, please use another WorkspaceKind."
## the icon of the WorkspaceKind
icon:
url: "https://avatars.githubusercontent.com/u/95932066?s=48&v=4"
## the logo of the WorkspaceKind
logo:
url: "https://avatars.githubusercontent.com/u/95932066?s=48&v=4"
## ================================================================
## DEFINITION CONFIGS
## ================================================================
podTemplate:
## metadata for Workspace Pods (MUTABLE)
podMetadata:
labels:
my-workspace-kind-label: "my-value"
annotations:
my-workspace-kind-annotation: "my-value"
## service account configs for Workspace Pods
serviceAccount:
## the name of the ServiceAccount (NOT MUTABLE)
name: "default-editor"
## volume mount paths
volumeMounts:
## the path to mount the home PVC (NOT MUTABLE)
home: "/home/coder"
## http proxy configs (MUTABLE)
httpProxy:
## if the path prefix is stripped from incoming HTTP requests
## - if true, the '/workspace/{profile_name}/{workspace_name}/' path prefix
## is stripped from incoming requests, the application sees the request
## as if it was made to '/...'
## - this only works if the application serves RELATIVE URLs for its assets
removePathPrefix: false
## ==============================================================
## WORKSPACE OPTIONS
## - options are the user-selectable fields,
## they determine the PodSpec of the Workspace
## ==============================================================
options:
##
## About the `values` fields:
## - the `values` field is a list of options that the user can select
## - elements of `values` can NOT be removed, only HIDDEN or REDIRECTED
## - this prevents options being removed that are still in use by existing Workspaces
## - this limitation may be removed in the future
## - options may be "hidden" by setting `spawner.hidden` to `true`
## - hidden options are NOT selectable in the Spawner UI
## - hidden options are still available to the controller and manually created Workspace resources
## - options may be "redirected" by setting `redirect.to` to another option:
## - redirected options are NOT shown in the Spawner UI
## - redirected options are like an HTTP 302 redirect, the controller will use the target option
## without actually changing the `spec.podTemplate.options` field of the Workspace
## - the Spawner UI will warn users about Workspaces with pending restarts
##
## ============================================================
## IMAGE CONFIG OPTIONS
## - SETS: image, imagePullPolicy, ports
## ============================================================
imageConfig:
## spawner ui configs
spawner:
## the id of the default option
default: "codeserver_latest"
## the list of image configs that are available
values:
## ================================
## EXAMPLE 1: a hidden option
## ================================
- id: "codeserver_latest"
spawner:
displayName: "Code-Server (Stable)"
description: "Latest stable release of Code-Server"
spec:
## the container image to use
image: "ghcr.io/kubeflow/kubeflow/notebook-servers/codeserver:latest"
## the pull policy for the container image
imagePullPolicy: "IfNotPresent"
## ports that the container listens on
ports:
- id: "codeserver"
displayName: "Code-Server"
port: 8080
protocol: "HTTP"
## ================================
## EXAMPLE 2: a previous version Code-Server option
## ================================
- id: "codeserver_v1.9.0"
spawner:
displayName: "Code-Server (V 1.9.0)"
description: "V 1.9.0 build of Code-Server (may be unstable)"
spec:
image: "ghcr.io/kubeflow/kubeflow/notebook-servers/codeserver:v1.9.0"
imagePullPolicy: "IfNotPresent"
ports:
- id: "codeserver"
displayName: "Code-Server"
port: 8080
protocol: "HTTP"
## ============================================================
## POD CONFIG OPTIONS
## - SETS: affinity, nodeSelector, tolerations, resources
## ============================================================
podConfig:
## spawner ui configs
spawner:
## the id of the default option
default: "tiny_cpu"
## the list of pod configs that are available
values:
## ================================
## EXAMPLE 1: a tiny CPU pod
## ================================
- id: "tiny_cpu"
spawner:
displayName: "Tiny CPU"
description: "Pod with 0.1 CPU, 128 Mb RAM"
labels:
- key: "cpu"
value: "100m"
- key: "memory"
value: "128Mi"
spec:
resources:
requests:
cpu: 100m
memory: 128Mi
## ================================
## EXAMPLE 2: a small CPU pod
## ================================
- id: "small_cpu"
spawner:
displayName: "Small CPU"
description: "Pod with 1 CPU, 2 GB RAM"
labels:
- key: "cpu"
value: "1000m"
- key: "memory"
value: "2Gi"
hidden: false
spec:
resources:
requests:
cpu: 1000m
memory: 2Gi
## ================================
## EXAMPLE 3: a big GPU pod
## ================================
- id: "big_gpu"
spawner:
displayName: "Big GPU"
description: "Pod with 4 CPU, 16 GB RAM, and 1 GPU"
labels:
- key: "cpu"
value: "4000m"
- key: "memory"
value: "16Gi"
- key: "gpu"
value: "1"
hidden: false
spec:
resources:
requests:
cpu: 4000m
memory: 16Gi
limits:
nvidia.com/gpu: 1

View File

@ -0,0 +1,236 @@
# jupyterlab_v1beta1_workspacekind.yaml
apiVersion: kubeflow.org/v1beta1
kind: WorkspaceKind
metadata:
name: jupyterlab-envtest
spec:
## ================================================================
## SPAWNER CONFIGS
## - how the WorkspaceKind is displayed in the Workspace Spawner UI
## ================================================================
spawner:
## the display name of the WorkspaceKind
displayName: "JupyterLab Notebook"
## the description of the WorkspaceKind
description: "A Workspace which runs JupyterLab in a Pod"
## if this WorkspaceKind should be hidden from the Workspace Spawner UI
hidden: false
## if this WorkspaceKind is deprecated
deprecated: false
## a message to show in Workspace Spawner UI when the WorkspaceKind is deprecated
deprecationMessage: "This WorkspaceKind will be removed on 20XX-XX-XX, please use another WorkspaceKind."
## the icon of the WorkspaceKind
## - a small (favicon-sized) icon used in the Workspace Spawner UI
##
icon:
url: "https://jupyter.org/assets/favicons/apple-touch-icon-152x152.png"
#configMap:
# name: "my-logos"
# key: "apple-touch-icon-152x152.png"
## the logo of the WorkspaceKind
## - a 1:1 (card size) logo used in the Workspace Spawner UI
##
logo:
url: "https://upload.wikimedia.org/wikipedia/commons/3/38/Jupyter_logo.svg"
## ================================================================
## DEFINITION CONFIGS
## ================================================================
podTemplate:
## metadata for Workspace Pods (MUTABLE)
podMetadata:
labels:
my-workspace-kind-label: "my-value"
annotations:
my-workspace-kind-annotation: "my-value"
## service account configs for Workspace Pods
serviceAccount:
## the name of the ServiceAccount (NOT MUTABLE)
name: "default-editor"
## volume mount paths
volumeMounts:
## the path to mount the home PVC (NOT MUTABLE)
home: "/home/jovyan"
## http proxy configs (MUTABLE)
httpProxy:
## if the path prefix is stripped from incoming HTTP requests
## - if true, the '/workspace/{profile_name}/{workspace_name}/' path prefix
## is stripped from incoming requests, the application sees the request
## as if it was made to '/...'
## - this only works if the application serves RELATIVE URLs for its assets
removePathPrefix: false
## ==============================================================
## WORKSPACE OPTIONS
## - options are the user-selectable fields,
## they determine the PodSpec of the Workspace
## ==============================================================
options:
##
## About the `values` fields:
## - the `values` field is a list of options that the user can select
## - elements of `values` can NOT be removed, only HIDDEN or REDIRECTED
## - this prevents options being removed that are still in use by existing Workspaces
## - this limitation may be removed in the future
## - options may be "hidden" by setting `spawner.hidden` to `true`
## - hidden options are NOT selectable in the Spawner UI
## - hidden options are still available to the controller and manually created Workspace resources
## - options may be "redirected" by setting `redirect.to` to another option:
## - redirected options are NOT shown in the Spawner UI
## - redirected options are like an HTTP 302 redirect, the controller will use the target option
## without actually changing the `spec.podTemplate.options` field of the Workspace
## - the Spawner UI will warn users about Workspaces with pending restarts
##
## ============================================================
## IMAGE CONFIG OPTIONS
## - SETS: image, imagePullPolicy, ports
## ============================================================
imageConfig:
## spawner ui configs
spawner:
## the id of the default option
default: "jupyterlab_scipy_190"
## the list of image configs that are available
values:
## ================================
## EXAMPLE 1: a hidden option
## ================================
- id: "jupyterlab_scipy_180"
spawner:
displayName: "jupyter-scipy:v1.8.0"
description: "JupyterLab, with SciPy Packages"
labels:
- key: "python_version"
value: "3.11"
hidden: true
redirect:
to: "jupyterlab_scipy_190"
message:
level: "Info" # "Info" | "Warning" | "Danger"
text: "This update will change..."
spec:
## the container image to use
image: "docker.io/kubeflownotebookswg/jupyter-scipy:v1.8.0"
## the pull policy for the container image
imagePullPolicy: "IfNotPresent"
## ports that the container listens on
ports:
- id: "jupyterlab"
displayName: "JupyterLab"
port: 8888
protocol: "HTTP"
## ================================
## EXAMPLE 2: a visible option
## ================================
- id: "jupyterlab_scipy_190"
spawner:
displayName: "jupyter-scipy:v1.9.0"
description: "JupyterLab, with SciPy Packages"
labels:
- key: "python_version"
value: "3.11"
spec:
image: "docker.io/kubeflownotebookswg/jupyter-scipy:v1.9.0"
imagePullPolicy: "IfNotPresent"
ports:
- id: "jupyterlab"
displayName: "JupyterLab"
port: 8888
protocol: "HTTP"
## ============================================================
## POD CONFIG OPTIONS
## - SETS: affinity, nodeSelector, tolerations, resources
## ============================================================
podConfig:
## spawner ui configs
spawner:
## the id of the default option
default: "tiny_cpu"
## the list of pod configs that are available
values:
## ================================
## EXAMPLE 1: a tiny CPU pod
## ================================
- id: "tiny_cpu"
spawner:
displayName: "Tiny CPU"
description: "Pod with 0.1 CPU, 128 Mb RAM"
labels:
- key: "cpu"
value: "100m"
- key: "memory"
value: "128Mi"
spec:
resources:
requests:
cpu: 100m
memory: 128Mi
## ================================
## EXAMPLE 2: a small CPU pod
## ================================
- id: "small_cpu"
spawner:
displayName: "Small CPU"
description: "Pod with 1 CPU, 2 GB RAM"
labels:
- key: "cpu"
value: "1000m"
- key: "memory"
value: "2Gi"
hidden: false
spec:
resources:
requests:
cpu: 1000m
memory: 2Gi
## ================================
## EXAMPLE 3: a big GPU pod
## ================================
- id: "big_gpu"
spawner:
displayName: "Big GPU"
description: "Pod with 4 CPU, 16 GB RAM, and 1 GPU"
labels:
- key: "cpu"
value: "4000m"
- key: "memory"
value: "16Gi"
- key: "gpu"
value: "1"
hidden: false
spec:
resources:
requests:
cpu: 4000m
memory: 16Gi
limits:
nvidia.com/gpu: 1