From 44168cbcb3b7fd945f4548a07d27dd98d8755c95 Mon Sep 17 00:00:00 2001 From: Web7Agent Date: Sun, 19 Jul 2026 11:51:53 +0000 Subject: [PATCH] feat: add environment install command for existing clusters --- cmd/overlock/environment/environment.go | 5 +- cmd/overlock/environment/install.go | 84 +++++++++++++++++++++++++ pkg/environment/environment.go | 42 +++++++++++-- 3 files changed, 124 insertions(+), 7 deletions(-) create mode 100644 cmd/overlock/environment/install.go diff --git a/cmd/overlock/environment/environment.go b/cmd/overlock/environment/environment.go index 719ee50..2ac9df8 100644 --- a/cmd/overlock/environment/environment.go +++ b/cmd/overlock/environment/environment.go @@ -1,8 +1,9 @@ package environment type Cmd struct { - Create createCmd `cmd:"" help:"Create an Environment"` - Delete deleteCmd `cmd:"" help:"Delete an Environment"` + Create createCmd `cmd:"" help:"Create an Environment"` + Install installCmd `cmd:"" help:"Install engine into an existing cluster without creating one"` + Delete deleteCmd `cmd:"" help:"Delete an Environment"` // Copy copyCmd `cmd:"" help:"Copy an Environment to another destination context"` // List listCmd `cmd:"" help:"List of Environments"` Stop stopCmd `cmd:"" help:"Stop an Environment"` diff --git a/cmd/overlock/environment/install.go b/cmd/overlock/environment/install.go new file mode 100644 index 0000000..8998df6 --- /dev/null +++ b/cmd/overlock/environment/install.go @@ -0,0 +1,84 @@ +package environment + +import ( + "context" + "fmt" + "strings" + + "go.uber.org/zap" + + "github.com/web-seven/overlock/pkg/environment" +) + +type installCmd struct { + Name string `arg:"" required:"" help:"Name of environment."` + Context string `required:"" short:"c" help:"Kubernetes context to install the engine into."` + NodeLabel []string `optional:"" help:"Node label in key=value format used as a nodeSelector to schedule the engine onto selected node(s). Can be specified multiple times."` + NodeTaint []string `optional:"" help:"Node taint in key=value:Effect format; a matching toleration is added so the engine can be placed on tainted node(s). Can be specified multiple times."` + Providers []string `optional:"" help:"List of providers to apply to the environment."` + Configurations []string `optional:"" help:"List of configurations to apply to the environment."` + Functions []string `optional:"" help:"List of functions to apply to the environment."` +} + +func (c *installCmd) Run(ctx context.Context, logger *zap.SugaredLogger) error { + nodeSelector, err := parseNodeLabels(c.NodeLabel) + if err != nil { + return err + } + + tolerations, err := parseNodeTaints(c.NodeTaint) + if err != nil { + return err + } + + return environment. + New("", c.Name). + WithContext(c.Context). + WithProviders(c.Providers). + WithConfigurations(c.Configurations). + WithFunctions(c.Functions). + WithNodeSelector(nodeSelector). + WithTolerations(tolerations). + Install(ctx, logger) +} + +// parseNodeLabels parses "key=value" strings into a nodeSelector map. +func parseNodeLabels(labels []string) (map[string]interface{}, error) { + if len(labels) == 0 { + return nil, nil + } + nodeSelector := make(map[string]interface{}, len(labels)) + for _, label := range labels { + key, value, ok := strings.Cut(label, "=") + if !ok || key == "" || value == "" { + return nil, fmt.Errorf("invalid --node-label %q, expected key=value", label) + } + nodeSelector[key] = value + } + return nodeSelector, nil +} + +// parseNodeTaints parses "key=value:Effect" strings into tolerations matching those taints. +func parseNodeTaints(taints []string) ([]interface{}, error) { + if len(taints) == 0 { + return nil, nil + } + tolerations := make([]interface{}, 0, len(taints)) + for _, taint := range taints { + keyValue, effect, ok := strings.Cut(taint, ":") + if !ok || effect == "" { + return nil, fmt.Errorf("invalid --node-taint %q, expected key=value:Effect", taint) + } + key, value, ok := strings.Cut(keyValue, "=") + if !ok || key == "" || value == "" { + return nil, fmt.Errorf("invalid --node-taint %q, expected key=value:Effect", taint) + } + tolerations = append(tolerations, map[string]interface{}{ + "key": key, + "operator": "Equal", + "value": value, + "effect": effect, + }) + } + return tolerations, nil +} diff --git a/pkg/environment/environment.go b/pkg/environment/environment.go index 2ed8c61..8cc213c 100644 --- a/pkg/environment/environment.go +++ b/pkg/environment/environment.go @@ -46,6 +46,8 @@ type Environment struct { adminServiceAccountName string skipNodeSetup bool maxReconcileRate int + nodeSelector map[string]interface{} + tolerations []interface{} } // New Environment entity @@ -99,6 +101,20 @@ func (e *Environment) Create(ctx context.Context, logger *zap.SugaredLogger) err return nil } +// Install installs the engine into an existing cluster, selected via context, +// without provisioning a new cluster. +func (e *Environment) Install(ctx context.Context, logger *zap.SugaredLogger) error { + if e.context == "" { + return fmt.Errorf("--context is required to install the engine into an existing cluster") + } + + if err := e.Setup(ctx, logger); err != nil { + return err + } + logger.Info("Engine installed successfully.") + return nil +} + // Upgrade environment with options or new features func (e *Environment) Upgrade(ctx context.Context, logger *zap.SugaredLogger) error { var err error @@ -181,9 +197,15 @@ func (e *Environment) Setup(ctx context.Context, logger *zap.SugaredLogger) erro } } - // Build engine scope params for chart installation (nil for non-k3s-docker). + // Build engine scope params for chart installation. A user-supplied + // nodeSelector (e.g. via `environment install --node-label/--node-taint`) + // takes precedence over the automatic k3s-docker engine scope. var nodeSelector map[string]interface{} - if e.engine == "k3s-docker" { + var tolerations []interface{} + if len(e.nodeSelector) > 0 { + nodeSelector = e.nodeSelector + tolerations = e.tolerations + } else if e.engine == "k3s-docker" { nodeSelector, _ = chart.EngineScopeSelector() } @@ -205,7 +227,7 @@ func (e *Environment) Setup(ctx context.Context, logger *zap.SugaredLogger) erro for _, ch := range charts { var scopeParams map[string]any if nodeSelector != nil { - scopeParams = ch.ScopeParams(nodeSelector, []interface{}{}) + scopeParams = ch.ScopeParams(nodeSelector, tolerations) } if err := ch.Install(ctx, configClient, scopeParams, logger); err != nil { return fmt.Errorf("failed to install chart: %w", err) @@ -213,8 +235,8 @@ func (e *Environment) Setup(ctx context.Context, logger *zap.SugaredLogger) erro } // Patch DeploymentRuntimeConfig for provider/function scheduling. - if e.engine == "k3s-docker" { - if err := chart.PatchDefaultRuntimeConfig(configClient, nodeSelector, []interface{}{}, logger); err != nil { + if nodeSelector != nil { + if err := chart.PatchDefaultRuntimeConfig(configClient, nodeSelector, tolerations, logger); err != nil { logger.Warnf("Failed to patch DeploymentRuntimeConfig: %v", err) } } @@ -465,6 +487,16 @@ func (e *Environment) WithMaxReconcileRate(rate int) *Environment { return e } +func (e *Environment) WithNodeSelector(nodeSelector map[string]interface{}) *Environment { + e.nodeSelector = nodeSelector + return e +} + +func (e *Environment) WithTolerations(tolerations []interface{}) *Environment { + e.tolerations = tolerations + return e +} + func SwitchContext(name string) (err error) { newConfig := clientcmd.GetConfigFromFileOrDie(clientcmd.RecommendedHomeFile) newConfig.CurrentContext = name