Merge 670b172813 into 5d91ee05b2
This commit is contained in:
commit
05e55caa77
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
Loading…
Reference in New Issue