From 670b17281326409c1597b30072a1b9cf40900c6e Mon Sep 17 00:00:00 2001 From: Yehudit Kerido Date: Thu, 29 May 2025 16:09:01 +0300 Subject: [PATCH] feat(ws): Notebooks 2.0 // Backend // Backend can read data served by envtest Signed-off-by: Yehudit Kerido --- workspaces/backend/Makefile | 9 + workspaces/backend/cmd/main.go | 92 +++-- workspaces/backend/go.mod | 15 +- .../k8sclientfactory/client_factory.go | 133 +++++++ workspaces/backend/localdev/envsetup.go | 131 ++++++ workspaces/backend/localdev/envtest_helper.go | 373 ++++++++++++++++++ .../localdev/testdata/Rstudio-wsk.yaml | 227 +++++++++++ .../localdev/testdata/code-server-wsk.yaml | 217 ++++++++++ .../localdev/testdata/jupyter-wsk.yaml | 236 +++++++++++ 9 files changed, 1395 insertions(+), 38 deletions(-) create mode 100644 workspaces/backend/internal/k8sclientfactory/client_factory.go create mode 100644 workspaces/backend/localdev/envsetup.go create mode 100644 workspaces/backend/localdev/envtest_helper.go create mode 100644 workspaces/backend/localdev/testdata/Rstudio-wsk.yaml create mode 100644 workspaces/backend/localdev/testdata/code-server-wsk.yaml create mode 100644 workspaces/backend/localdev/testdata/jupyter-wsk.yaml diff --git a/workspaces/backend/Makefile b/workspaces/backend/Makefile index 959dbe3d3..de5bee95a 100644 --- a/workspaces/backend/Makefile +++ b/workspaces/backend/Makefile @@ -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/ @@ -132,6 +136,11 @@ ENVTEST_VERSION ?= release-0.19 GOLANGCI_LINT_VERSION ?= v1.61.0 SWAGGER_VERSION ?= v1.16.4 +.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) diff --git a/workspaces/backend/cmd/main.go b/workspaces/backend/cmd/main.go index cbce9f639..f215d45d2 100644 --- a/workspaces/backend/cmd/main.go +++ b/workspaces/backend/cmd/main.go @@ -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 + } + + // 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) } - // 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, - }) + // 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 + } + + logger.Info("Starting manager...") + if err := mgr.Start(ctx); err != nil { + logger.Error("Problem running manager", "error", err) + 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) + return nil +} + +func main() { + if err := run(); err != nil { + fmt.Fprintf(os.Stderr, "Application run failed: %v\n", err) os.Exit(1) } } diff --git a/workspaces/backend/go.mod b/workspaces/backend/go.mod index ab546a803..e48cdf1cb 100644 --- a/workspaces/backend/go.mod +++ b/workspaces/backend/go.mod @@ -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 @@ -87,7 +83,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 @@ -103,5 +98,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 ) diff --git a/workspaces/backend/internal/k8sclientfactory/client_factory.go b/workspaces/backend/internal/k8sclientfactory/client_factory.go new file mode 100644 index 000000000..083af2694 --- /dev/null +++ b/workspaces/backend/internal/k8sclientfactory/client_factory.go @@ -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 +} diff --git a/workspaces/backend/localdev/envsetup.go b/workspaces/backend/localdev/envsetup.go new file mode 100644 index 000000000..dff715b78 --- /dev/null +++ b/workspaces/backend/localdev/envsetup.go @@ -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 + } +} diff --git a/workspaces/backend/localdev/envtest_helper.go b/workspaces/backend/localdev/envtest_helper.go new file mode 100644 index 000000000..957ed8cae --- /dev/null +++ b/workspaces/backend/localdev/envtest_helper.go @@ -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 +} diff --git a/workspaces/backend/localdev/testdata/Rstudio-wsk.yaml b/workspaces/backend/localdev/testdata/Rstudio-wsk.yaml new file mode 100644 index 000000000..c6f1b6539 --- /dev/null +++ b/workspaces/backend/localdev/testdata/Rstudio-wsk.yaml @@ -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 \ No newline at end of file diff --git a/workspaces/backend/localdev/testdata/code-server-wsk.yaml b/workspaces/backend/localdev/testdata/code-server-wsk.yaml new file mode 100644 index 000000000..092fd99ae --- /dev/null +++ b/workspaces/backend/localdev/testdata/code-server-wsk.yaml @@ -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 \ No newline at end of file diff --git a/workspaces/backend/localdev/testdata/jupyter-wsk.yaml b/workspaces/backend/localdev/testdata/jupyter-wsk.yaml new file mode 100644 index 000000000..b1c940947 --- /dev/null +++ b/workspaces/backend/localdev/testdata/jupyter-wsk.yaml @@ -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 \ No newline at end of file