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.
|
run: fmt vet swag ## Run a backend from your host.
|
||||||
go run ./cmd/main.go --port=$(PORT)
|
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.
|
# 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.
|
# (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/
|
# 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
|
GOLANGCI_LINT_VERSION ?= v1.61.0
|
||||||
SWAGGER_VERSION ?= v1.16.6
|
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
|
.PHONY: SWAGGER
|
||||||
SWAGGER: $(SWAGGER)
|
SWAGGER: $(SWAGGER)
|
||||||
$(SWAGGER): $(LOCALBIN)
|
$(SWAGGER): $(LOCALBIN)
|
||||||
|
|
|
||||||
|
|
@ -18,17 +18,22 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"flag"
|
"flag"
|
||||||
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
stdruntime "runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/go-logr/logr"
|
||||||
|
|
||||||
ctrl "sigs.k8s.io/controller-runtime"
|
ctrl "sigs.k8s.io/controller-runtime"
|
||||||
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
|
|
||||||
|
|
||||||
application "github.com/kubeflow/notebooks/workspaces/backend/api"
|
application "github.com/kubeflow/notebooks/workspaces/backend/api"
|
||||||
"github.com/kubeflow/notebooks/workspaces/backend/internal/auth"
|
"github.com/kubeflow/notebooks/workspaces/backend/internal/auth"
|
||||||
"github.com/kubeflow/notebooks/workspaces/backend/internal/config"
|
"github.com/kubeflow/notebooks/workspaces/backend/internal/config"
|
||||||
"github.com/kubeflow/notebooks/workspaces/backend/internal/helper"
|
"github.com/kubeflow/notebooks/workspaces/backend/internal/helper"
|
||||||
|
"github.com/kubeflow/notebooks/workspaces/backend/internal/k8sclientfactory"
|
||||||
"github.com/kubeflow/notebooks/workspaces/backend/internal/server"
|
"github.com/kubeflow/notebooks/workspaces/backend/internal/server"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -47,7 +52,7 @@ import (
|
||||||
// @consumes application/json
|
// @consumes application/json
|
||||||
// @produces application/json
|
// @produces application/json
|
||||||
|
|
||||||
func main() {
|
func run() error {
|
||||||
// Define command line flags
|
// Define command line flags
|
||||||
cfg := &config.EnvConfig{}
|
cfg := &config.EnvConfig{}
|
||||||
flag.IntVar(&cfg.Port,
|
flag.IntVar(&cfg.Port,
|
||||||
|
|
@ -93,44 +98,59 @@ func main() {
|
||||||
"Key of request header containing user groups",
|
"Key of request header containing user groups",
|
||||||
)
|
)
|
||||||
|
|
||||||
// Initialize the logger
|
var enableEnvTest bool
|
||||||
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
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
|
// Initialize the logger
|
||||||
kubeconfig, err := ctrl.GetConfig()
|
slogTextHandler := slog.NewTextHandler(os.Stdout, nil)
|
||||||
if err != nil {
|
logger := slog.New(slogTextHandler)
|
||||||
logger.Error("failed to get Kubernetes config", "error", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
kubeconfig.QPS = float32(cfg.ClientQPS)
|
|
||||||
kubeconfig.Burst = cfg.ClientBurst
|
|
||||||
|
|
||||||
// Build the Kubernetes scheme
|
// Build the Kubernetes scheme
|
||||||
scheme, err := helper.BuildScheme()
|
scheme, err := helper.BuildScheme()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("failed to build Kubernetes scheme", "error", err)
|
logger.Error("failed to build Kubernetes scheme", "error", err)
|
||||||
os.Exit(1)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the controller manager
|
// Defining CRD's path
|
||||||
mgr, err := ctrl.NewManager(kubeconfig, ctrl.Options{
|
crdPath := os.Getenv("CRD_PATH")
|
||||||
Scheme: scheme,
|
if crdPath == "" {
|
||||||
Metrics: metricsserver.Options{
|
_, currentFile, _, ok := stdruntime.Caller(0)
|
||||||
BindAddress: "0", // disable metrics serving
|
if !ok {
|
||||||
},
|
logger.Info("Failed to get current file path using stdruntime.Caller")
|
||||||
HealthProbeBindAddress: "0", // disable health probe serving
|
}
|
||||||
LeaderElection: false,
|
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 {
|
if err != nil {
|
||||||
logger.Error("unable to create manager", "error", err)
|
logger.Error("Failed to get Kubernetes manager/config from factory", "error", err)
|
||||||
os.Exit(1)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the request authenticator
|
// Create the request authenticator
|
||||||
reqAuthN, err := auth.NewRequestAuthenticator(cfg.UserIdHeader, cfg.UserIdPrefix, cfg.GroupsHeader)
|
reqAuthN, err := auth.NewRequestAuthenticator(cfg.UserIdHeader, cfg.UserIdPrefix, cfg.GroupsHeader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("failed to create request authenticator", "error", err)
|
logger.Error("failed to create request authenticator", "error", err)
|
||||||
os.Exit(1)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the request authorizer
|
// Create the request authorizer
|
||||||
|
|
@ -143,22 +163,30 @@ func main() {
|
||||||
app, err := application.NewApp(cfg, logger, mgr.GetClient(), mgr.GetScheme(), reqAuthN, reqAuthZ)
|
app, err := application.NewApp(cfg, logger, mgr.GetClient(), mgr.GetScheme(), reqAuthN, reqAuthZ)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("failed to create app", "error", err)
|
logger.Error("failed to create app", "error", err)
|
||||||
os.Exit(1)
|
return err
|
||||||
}
|
}
|
||||||
svr, err := server.NewServer(app, logger)
|
svr, err := server.NewServer(app, logger)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("failed to create server", "error", err)
|
logger.Error("failed to create server", "error", err)
|
||||||
os.Exit(1)
|
return err
|
||||||
}
|
}
|
||||||
if err := svr.SetupWithManager(mgr); err != nil {
|
if err := svr.SetupWithManager(mgr); err != nil {
|
||||||
logger.Error("failed to setup server with manager", "error", err)
|
logger.Error("failed to setup server with manager", "error", err)
|
||||||
os.Exit(1)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start the controller manager
|
logger.Info("Starting manager...")
|
||||||
logger.Info("starting manager")
|
if err := mgr.Start(ctx); err != nil {
|
||||||
if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
|
logger.Error("Problem running manager", "error", err)
|
||||||
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)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,12 +33,10 @@ require (
|
||||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||||
github.com/fxamacker/cbor/v2 v2.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/stdr v1.2.2 // indirect
|
||||||
github.com/go-logr/zapr v1.3.0 // indirect
|
github.com/go-logr/zapr v1.3.0 // indirect
|
||||||
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
||||||
github.com/go-openapi/jsonreference 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-openapi/swag v0.23.0 // indirect
|
||||||
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
||||||
github.com/gogo/protobuf v1.3.2 // indirect
|
github.com/gogo/protobuf v1.3.2 // indirect
|
||||||
|
|
@ -55,7 +53,6 @@ require (
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/josharian/intern v1.0.0 // indirect
|
github.com/josharian/intern v1.0.0 // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // 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/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // 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/cobra v1.8.1 // indirect
|
||||||
github.com/spf13/pflag v1.0.5 // indirect
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
github.com/stoewer/go-strcase v1.2.0 // 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
|
github.com/x448/float16 v0.8.4 // indirect
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect
|
||||||
go.opentelemetry.io/otel v1.28.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/term v0.29.0 // indirect
|
||||||
golang.org/x/text v0.22.0 // indirect
|
golang.org/x/text v0.22.0 // indirect
|
||||||
golang.org/x/time v0.3.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
|
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/api v0.0.0-20240528184218-531527333157 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // 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/apiserver-network-proxy/konnectivity-client v0.30.3 // indirect
|
||||||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // 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/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