734 lines
26 KiB
Go
734 lines
26 KiB
Go
/*
|
|
* SPDX-FileCopyrightText: Copyright (c) 2022 Atalaya Tech. Inc
|
|
* SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*
|
|
* 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.
|
|
* Modifications Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES
|
|
*/
|
|
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"flag"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"time"
|
|
|
|
// Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.)
|
|
// to ensure that exec-entrypoint and run can make use of them.
|
|
|
|
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
|
|
corev1 "k8s.io/api/core/v1"
|
|
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
|
"k8s.io/client-go/discovery/cached/memory"
|
|
"k8s.io/client-go/dynamic"
|
|
"k8s.io/client-go/informers"
|
|
"k8s.io/client-go/kubernetes"
|
|
_ "k8s.io/client-go/plugin/pkg/client/auth"
|
|
"k8s.io/client-go/restmapper"
|
|
"k8s.io/client-go/scale"
|
|
k8sCache "k8s.io/client-go/tools/cache"
|
|
"k8s.io/utils/ptr"
|
|
"sigs.k8s.io/controller-runtime/pkg/cache"
|
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
|
|
|
k8sruntime "k8s.io/apimachinery/pkg/runtime"
|
|
"k8s.io/apimachinery/pkg/runtime/serializer"
|
|
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
|
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
|
|
ctrl "sigs.k8s.io/controller-runtime"
|
|
"sigs.k8s.io/controller-runtime/pkg/healthz"
|
|
"sigs.k8s.io/controller-runtime/pkg/log/zap"
|
|
metricsfilters "sigs.k8s.io/controller-runtime/pkg/metrics/filters"
|
|
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
|
|
"sigs.k8s.io/controller-runtime/pkg/webhook"
|
|
|
|
lwsscheme "sigs.k8s.io/lws/client-go/clientset/versioned/scheme"
|
|
volcanoscheme "volcano.sh/apis/pkg/client/clientset/versioned/scheme"
|
|
|
|
semver "github.com/Masterminds/semver/v3"
|
|
configv1alpha1 "github.com/ai-dynamo/dynamo/deploy/operator/api/config/v1alpha1"
|
|
configvalidation "github.com/ai-dynamo/dynamo/deploy/operator/api/config/validation"
|
|
nvidiacomv1alpha1 "github.com/ai-dynamo/dynamo/deploy/operator/api/v1alpha1"
|
|
nvidiacomv1beta1 "github.com/ai-dynamo/dynamo/deploy/operator/api/v1beta1"
|
|
internalcert "github.com/ai-dynamo/dynamo/deploy/operator/internal/cert"
|
|
"github.com/ai-dynamo/dynamo/deploy/operator/internal/controller"
|
|
commonController "github.com/ai-dynamo/dynamo/deploy/operator/internal/controller_common"
|
|
"github.com/ai-dynamo/dynamo/deploy/operator/internal/gpu"
|
|
"github.com/ai-dynamo/dynamo/deploy/operator/internal/modelendpoint"
|
|
"github.com/ai-dynamo/dynamo/deploy/operator/internal/namespace_scope"
|
|
"github.com/ai-dynamo/dynamo/deploy/operator/internal/observability"
|
|
"github.com/ai-dynamo/dynamo/deploy/operator/internal/rbac"
|
|
"github.com/ai-dynamo/dynamo/deploy/operator/internal/secret"
|
|
"github.com/ai-dynamo/dynamo/deploy/operator/internal/secrets"
|
|
internalwebhook "github.com/ai-dynamo/dynamo/deploy/operator/internal/webhook"
|
|
webhookdefaulting "github.com/ai-dynamo/dynamo/deploy/operator/internal/webhook/defaulting"
|
|
webhookvalidation "github.com/ai-dynamo/dynamo/deploy/operator/internal/webhook/validation"
|
|
grovev1alpha1 "github.com/ai-dynamo/grove/operator/api/core/v1alpha1"
|
|
istioclientsetscheme "istio.io/client-go/pkg/clientset/versioned/scheme"
|
|
gaiev1 "sigs.k8s.io/gateway-api-inference-extension/api/v1"
|
|
//+kubebuilder:scaffold:imports
|
|
)
|
|
|
|
var (
|
|
crdScheme = k8sruntime.NewScheme()
|
|
setupLog = ctrl.Log.WithName("setup")
|
|
configScheme = k8sruntime.NewScheme()
|
|
)
|
|
|
|
// LoadAndValidateOperatorConfig loads the operator configuration from a file,
|
|
// applies defaults via the scheme, and validates it.
|
|
func LoadAndValidateOperatorConfig(path string) (*configv1alpha1.OperatorConfiguration, error) {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read config file %s: %w", path, err)
|
|
}
|
|
|
|
codecFactory := serializer.NewCodecFactory(configScheme)
|
|
cfg := &configv1alpha1.OperatorConfiguration{}
|
|
if err := k8sruntime.DecodeInto(codecFactory.UniversalDecoder(), data, cfg); err != nil {
|
|
return nil, fmt.Errorf("failed to decode config file %s: %w", path, err)
|
|
}
|
|
|
|
// Validate the configuration
|
|
if errs := configvalidation.ValidateOperatorConfiguration(cfg); len(errs) > 0 {
|
|
return nil, fmt.Errorf("config validation failed: %s", errs.ToAggregate().Error())
|
|
}
|
|
|
|
return cfg, nil
|
|
}
|
|
|
|
func createScalesGetter(mgr ctrl.Manager) (scale.ScalesGetter, error) {
|
|
config := mgr.GetConfig()
|
|
|
|
// Create kubernetes client for discovery
|
|
kubeClient, err := kubernetes.NewForConfig(config)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Create cached discovery client
|
|
cachedDiscovery := memory.NewMemCacheClient(kubeClient.Discovery())
|
|
|
|
// Create REST mapper
|
|
restMapper := restmapper.NewDeferredDiscoveryRESTMapper(cachedDiscovery)
|
|
|
|
scalesGetter, err := scale.NewForConfig(
|
|
config,
|
|
restMapper,
|
|
dynamic.LegacyAPIPathResolverFunc,
|
|
scale.NewDiscoveryScaleKindResolver(cachedDiscovery),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return scalesGetter, nil
|
|
}
|
|
|
|
func initCRDSchemes() {
|
|
utilruntime.Must(clientgoscheme.AddToScheme(crdScheme))
|
|
|
|
utilruntime.Must(nvidiacomv1alpha1.AddToScheme(crdScheme))
|
|
|
|
utilruntime.Must(nvidiacomv1beta1.AddToScheme(crdScheme))
|
|
|
|
utilruntime.Must(lwsscheme.AddToScheme(crdScheme))
|
|
|
|
utilruntime.Must(volcanoscheme.AddToScheme(crdScheme))
|
|
|
|
utilruntime.Must(grovev1alpha1.AddToScheme(crdScheme))
|
|
|
|
utilruntime.Must(apiextensionsv1.AddToScheme(crdScheme))
|
|
|
|
utilruntime.Must(admissionregistrationv1.AddToScheme(crdScheme))
|
|
|
|
utilruntime.Must(istioclientsetscheme.AddToScheme(crdScheme))
|
|
|
|
utilruntime.Must(gaiev1.Install(crdScheme))
|
|
//+kubebuilder:scaffold:scheme
|
|
}
|
|
|
|
func initConfigScheme() {
|
|
utilruntime.Must(configv1alpha1.AddToScheme(configScheme))
|
|
}
|
|
|
|
// +kubebuilder:rbac:groups=authentication.k8s.io,resources=tokenreviews,verbs=create
|
|
// +kubebuilder:rbac:groups=authorization.k8s.io,resources=subjectaccessreviews,verbs=create
|
|
|
|
//nolint:gocyclo
|
|
func main() {
|
|
initCRDSchemes()
|
|
initConfigScheme()
|
|
|
|
var configFile string
|
|
var operatorVersion string
|
|
flag.StringVar(&configFile, "config", "", "Path to operator configuration file (required)")
|
|
flag.StringVar(&operatorVersion, "operator-version", "unknown",
|
|
"Version of the operator (used in lease holder identity)")
|
|
opts := zap.Options{
|
|
Development: true,
|
|
}
|
|
opts.BindFlags(flag.CommandLine)
|
|
flag.Parse()
|
|
ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))
|
|
|
|
if configFile == "" {
|
|
setupLog.Error(nil, "--config flag is required")
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Load, default, and validate operator configuration
|
|
operatorCfg, err := LoadAndValidateOperatorConfig(configFile)
|
|
if err != nil {
|
|
setupLog.Error(err, "failed to load operator configuration", "configFile", configFile)
|
|
os.Exit(1)
|
|
}
|
|
setupLog.Info("Operator configuration loaded successfully", "configFile", configFile)
|
|
|
|
// Validate and normalize operator version to semver
|
|
if _, err := semver.NewVersion(operatorVersion); err != nil {
|
|
setupLog.Error(err, "operator-version is not valid semver",
|
|
"provided", operatorVersion, "error", err.Error())
|
|
os.Exit(1)
|
|
}
|
|
setupLog.Info("Operator version configured", "version", operatorVersion)
|
|
|
|
// Initialize runtime config (will be populated after detection)
|
|
runtimeConfig := &commonController.RuntimeConfig{}
|
|
|
|
mainCtx := ctrl.SetupSignalHandler()
|
|
|
|
// if the enable-http2 flag is false (the default), http/2 should be disabled
|
|
// due to its vulnerabilities. More specifically, disabling http/2 will
|
|
// prevent from being vulnerable to the HTTP/2 Stream Cancellation and
|
|
// Rapid Reset CVEs. For more information see:
|
|
// - https://github.com/advisories/GHSA-qppj-fm5r-hxr3
|
|
// - https://github.com/advisories/GHSA-4374-p667-p6c8
|
|
disableHTTP2 := func(c *tls.Config) {
|
|
setupLog.Info("disabling http/2")
|
|
c.NextProtos = []string{"http/1.1"}
|
|
}
|
|
|
|
tlsOpts := []func(*tls.Config){}
|
|
if !operatorCfg.Security.EnableHTTP2 {
|
|
tlsOpts = append(tlsOpts, disableHTTP2)
|
|
}
|
|
|
|
webhookServer := webhook.NewServer(webhook.Options{
|
|
Host: operatorCfg.Server.Webhook.Host,
|
|
Port: operatorCfg.Server.Webhook.Port,
|
|
CertDir: operatorCfg.Server.Webhook.CertDir,
|
|
TLSOpts: tlsOpts,
|
|
})
|
|
|
|
metricsBindAddr := fmt.Sprintf("%s:%d", operatorCfg.Server.Metrics.BindAddress, operatorCfg.Server.Metrics.Port)
|
|
healthProbeAddr := fmt.Sprintf(
|
|
"%s:%d", operatorCfg.Server.HealthProbe.BindAddress, operatorCfg.Server.HealthProbe.Port,
|
|
)
|
|
|
|
mgrOpts := ctrl.Options{
|
|
Scheme: crdScheme,
|
|
Metrics: metricsserver.Options{
|
|
BindAddress: metricsBindAddr,
|
|
SecureServing: ptr.Deref(operatorCfg.Server.Metrics.Secure, true),
|
|
FilterProvider: metricsfilters.WithAuthenticationAndAuthorization,
|
|
TLSOpts: tlsOpts,
|
|
},
|
|
WebhookServer: webhookServer,
|
|
HealthProbeBindAddress: healthProbeAddr,
|
|
LeaderElection: operatorCfg.LeaderElection.Enabled,
|
|
LeaderElectionID: operatorCfg.LeaderElection.ID,
|
|
LeaderElectionNamespace: operatorCfg.LeaderElection.Namespace,
|
|
}
|
|
|
|
restrictedNamespace := operatorCfg.Namespace.Restricted
|
|
if restrictedNamespace != "" {
|
|
mgrOpts.Cache.DefaultNamespaces = map[string]cache.Config{
|
|
restrictedNamespace: {},
|
|
}
|
|
setupLog.Info("Restricted namespace configured, launching in restricted mode", "namespace", restrictedNamespace)
|
|
} else {
|
|
setupLog.Info("No restricted namespace configured, launching in cluster-wide mode")
|
|
}
|
|
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), mgrOpts)
|
|
if err != nil {
|
|
setupLog.Error(err, "unable to start manager")
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Initialize observability metrics
|
|
setupLog.Info("Initializing observability metrics")
|
|
observability.InitMetrics()
|
|
|
|
// Set up webhook certificate management.
|
|
// A direct (non-cached) client is needed because the manager's cache isn't started yet.
|
|
directClient, err := client.New(mgr.GetConfig(), client.Options{Scheme: crdScheme})
|
|
if err != nil {
|
|
setupLog.Error(err, "unable to create direct client for cert management")
|
|
os.Exit(1)
|
|
}
|
|
certMgr, err := internalcert.NewCertManager(directClient, &operatorCfg.Server.Webhook)
|
|
if err != nil {
|
|
setupLog.Error(err, "unable to create cert manager")
|
|
os.Exit(1)
|
|
}
|
|
if err = certMgr.Setup(mainCtx, mgr); err != nil {
|
|
setupLog.Error(err, "failed to setup webhook certificate management")
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Initialize namespace scope mechanism
|
|
var leaseManager *namespace_scope.LeaseManager
|
|
var leaseWatcher *namespace_scope.LeaseWatcher
|
|
|
|
if restrictedNamespace != "" {
|
|
// Namespace-restricted mode: Create and maintain namespace scope marker lease
|
|
setupLog.Info("Creating namespace scope marker lease manager",
|
|
"namespace", restrictedNamespace,
|
|
"leaseDuration", operatorCfg.Namespace.Scope.LeaseDuration.Duration,
|
|
"renewInterval", operatorCfg.Namespace.Scope.LeaseRenewInterval.Duration)
|
|
|
|
leaseManager, err = namespace_scope.NewLeaseManager(
|
|
mgr.GetConfig(),
|
|
restrictedNamespace,
|
|
operatorVersion,
|
|
operatorCfg.Namespace.Scope.LeaseDuration.Duration,
|
|
operatorCfg.Namespace.Scope.LeaseRenewInterval.Duration,
|
|
)
|
|
if err != nil {
|
|
setupLog.Error(err, "unable to create namespace scope marker lease manager")
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Start the lease manager
|
|
if err = leaseManager.Start(mainCtx); err != nil {
|
|
setupLog.Error(err, "unable to start namespace scope marker lease manager")
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Monitor for fatal lease errors
|
|
// If lease renewal fails repeatedly, we must exit to prevent split-brain
|
|
go func() {
|
|
select {
|
|
case err := <-leaseManager.Errors():
|
|
setupLog.Error(err, "FATAL: Lease manager encountered unrecoverable error, shutting down to prevent split-brain")
|
|
os.Exit(1)
|
|
case <-mainCtx.Done():
|
|
// Normal shutdown, error channel monitoring no longer needed
|
|
return
|
|
}
|
|
}()
|
|
|
|
// Ensure lease is released on shutdown
|
|
defer func() {
|
|
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
if err := leaseManager.Stop(shutdownCtx); err != nil {
|
|
setupLog.Error(err, "failed to stop lease manager cleanly")
|
|
}
|
|
}()
|
|
|
|
setupLog.Info("Namespace scope marker lease manager started successfully")
|
|
} else {
|
|
// Cluster-wide mode: Watch for namespace scope marker leases
|
|
setupLog.Info("Setting up namespace scope marker lease watcher for cluster-wide mode")
|
|
|
|
leaseWatcher, err = namespace_scope.NewLeaseWatcher(mgr.GetConfig())
|
|
if err != nil {
|
|
setupLog.Error(err, "unable to create namespace scope marker lease watcher")
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Start the lease watcher
|
|
if err = leaseWatcher.Start(mainCtx); err != nil {
|
|
setupLog.Error(err, "unable to start namespace scope marker lease watcher")
|
|
os.Exit(1)
|
|
}
|
|
|
|
setupLog.Info("Namespace scope marker lease watcher started successfully")
|
|
|
|
// Pass leaseWatcher to runtime config for namespace exclusion filtering
|
|
runtimeConfig.ExcludedNamespaces = leaseWatcher
|
|
}
|
|
|
|
// Start resource counter background goroutine (after ExcludedNamespaces is set)
|
|
setupLog.Info("Starting resource counter")
|
|
go observability.StartResourceCounter(mainCtx, mgr.GetClient(), runtimeConfig.ExcludedNamespaces)
|
|
|
|
// Detect orchestrators availability using discovery client.
|
|
// Config overrides (*bool) take precedence over auto-detection:
|
|
// nil = auto-detect (backward compatible default)
|
|
// false = forcibly disabled regardless of API availability
|
|
// true = forcibly enabled; hard exit if API is not available (misconfiguration)
|
|
setupLog.Info("Detecting Grove availability...")
|
|
groveDetected := commonController.DetectGroveAvailability(mainCtx, mgr)
|
|
switch {
|
|
case operatorCfg.Orchestrators.Grove.Enabled == nil:
|
|
runtimeConfig.GroveEnabled = groveDetected
|
|
case *operatorCfg.Orchestrators.Grove.Enabled:
|
|
if !groveDetected {
|
|
setupLog.Error(nil, "Grove is explicitly enabled in config but the Grove API group was not detected in the cluster")
|
|
os.Exit(1)
|
|
}
|
|
runtimeConfig.GroveEnabled = true
|
|
default:
|
|
setupLog.Info("Grove is explicitly disabled via config override")
|
|
runtimeConfig.GroveEnabled = false
|
|
}
|
|
|
|
setupLog.Info("Detecting LWS availability...")
|
|
lwsDetected := commonController.DetectLWSAvailability(mainCtx, mgr)
|
|
setupLog.Info("Detecting Volcano availability...")
|
|
volcanoDetected := commonController.DetectVolcanoAvailability(mainCtx, mgr)
|
|
// LWS for multinode deployment usage depends on both LWS and Volcano availability
|
|
switch {
|
|
case operatorCfg.Orchestrators.LWS.Enabled == nil:
|
|
runtimeConfig.LWSEnabled = lwsDetected && volcanoDetected
|
|
case *operatorCfg.Orchestrators.LWS.Enabled:
|
|
if !lwsDetected {
|
|
setupLog.Error(nil, "LWS is explicitly enabled in config but the LWS API group was not detected in the cluster")
|
|
os.Exit(1)
|
|
}
|
|
if !volcanoDetected {
|
|
setupLog.Error(nil, "LWS is explicitly enabled in config but the Volcano API group was not detected in the cluster")
|
|
os.Exit(1)
|
|
}
|
|
runtimeConfig.LWSEnabled = true
|
|
default:
|
|
setupLog.Info("LWS is explicitly disabled via config override")
|
|
runtimeConfig.LWSEnabled = false
|
|
}
|
|
|
|
// Detect Kai-scheduler availability using discovery client
|
|
setupLog.Info("Detecting Kai-scheduler availability...")
|
|
kaiSchedulerDetected := commonController.DetectKaiSchedulerAvailability(mainCtx, mgr)
|
|
switch {
|
|
case operatorCfg.Orchestrators.KaiScheduler.Enabled == nil:
|
|
runtimeConfig.KaiSchedulerEnabled = kaiSchedulerDetected
|
|
case *operatorCfg.Orchestrators.KaiScheduler.Enabled:
|
|
if !kaiSchedulerDetected {
|
|
setupLog.Error(nil,
|
|
"Kai-scheduler is explicitly enabled in config but the scheduling.run.ai API group was not detected in the cluster",
|
|
)
|
|
os.Exit(1)
|
|
}
|
|
runtimeConfig.KaiSchedulerEnabled = true
|
|
default:
|
|
setupLog.Info("Kai-scheduler is explicitly disabled via config override")
|
|
runtimeConfig.KaiSchedulerEnabled = false
|
|
}
|
|
|
|
setupLog.Info("Detected orchestrators availability",
|
|
"grove", runtimeConfig.GroveEnabled,
|
|
"lws", runtimeConfig.LWSEnabled,
|
|
"volcano", volcanoDetected,
|
|
"kai-scheduler", runtimeConfig.KaiSchedulerEnabled,
|
|
)
|
|
|
|
dockerSecretRetriever := secrets.NewDockerSecretIndexer(mgr.GetClient())
|
|
// refresh whenever a secret is created/deleted/updated
|
|
// Set up informer
|
|
var factory informers.SharedInformerFactory
|
|
if restrictedNamespace == "" {
|
|
factory = informers.NewSharedInformerFactory(kubernetes.NewForConfigOrDie(mgr.GetConfig()), time.Hour*24)
|
|
} else {
|
|
factory = informers.NewFilteredSharedInformerFactory(
|
|
kubernetes.NewForConfigOrDie(mgr.GetConfig()),
|
|
time.Hour*24,
|
|
restrictedNamespace,
|
|
nil,
|
|
)
|
|
}
|
|
secretInformer := factory.Core().V1().Secrets().Informer()
|
|
// Start the informer factory
|
|
go factory.Start(mainCtx.Done())
|
|
// Wait for the initial sync
|
|
if !k8sCache.WaitForCacheSync(mainCtx.Done(), secretInformer.HasSynced) {
|
|
setupLog.Error(nil, "Failed to sync informer cache")
|
|
os.Exit(1)
|
|
}
|
|
setupLog.Info("Secret informer cache synced and ready")
|
|
_, err = secretInformer.AddEventHandler(k8sCache.ResourceEventHandlerFuncs{
|
|
AddFunc: func(obj interface{}) {
|
|
secret := obj.(*corev1.Secret)
|
|
if secret.Type == corev1.SecretTypeDockerConfigJson {
|
|
setupLog.Info("refreshing docker secrets index after secret creation...")
|
|
err := dockerSecretRetriever.RefreshIndex(context.Background())
|
|
if err != nil {
|
|
setupLog.Error(err, "unable to refresh docker secrets index after secret creation")
|
|
} else {
|
|
setupLog.Info("docker secrets index refreshed after secret creation")
|
|
}
|
|
}
|
|
},
|
|
UpdateFunc: func(old, new interface{}) {
|
|
newSecret := new.(*corev1.Secret)
|
|
if newSecret.Type == corev1.SecretTypeDockerConfigJson {
|
|
setupLog.Info("refreshing docker secrets index after secret update...")
|
|
err := dockerSecretRetriever.RefreshIndex(context.Background())
|
|
if err != nil {
|
|
setupLog.Error(err, "unable to refresh docker secrets index after secret update")
|
|
} else {
|
|
setupLog.Info("docker secrets index refreshed after secret update")
|
|
}
|
|
}
|
|
},
|
|
DeleteFunc: func(obj interface{}) {
|
|
secret := obj.(*corev1.Secret)
|
|
if secret.Type == corev1.SecretTypeDockerConfigJson {
|
|
setupLog.Info("refreshing docker secrets index after secret deletion...")
|
|
err := dockerSecretRetriever.RefreshIndex(context.Background())
|
|
if err != nil {
|
|
setupLog.Error(err, "unable to refresh docker secrets index after secret deletion")
|
|
} else {
|
|
setupLog.Info("docker secrets index refreshed after secret deletion")
|
|
}
|
|
}
|
|
},
|
|
})
|
|
if err != nil {
|
|
setupLog.Error(err, "unable to add event handler to secret informer")
|
|
os.Exit(1)
|
|
}
|
|
// launch a goroutine to refresh the docker secret indexer in any case every minute
|
|
go func() {
|
|
// Initial refresh
|
|
if err := dockerSecretRetriever.RefreshIndex(context.Background()); err != nil {
|
|
setupLog.Error(err, "initial docker secrets index refresh failed")
|
|
}
|
|
ticker := time.NewTicker(60 * time.Second)
|
|
defer ticker.Stop()
|
|
for {
|
|
select {
|
|
case <-mainCtx.Done():
|
|
return
|
|
case <-ticker.C:
|
|
setupLog.Info("refreshing docker secrets index...")
|
|
if err := dockerSecretRetriever.RefreshIndex(mainCtx); err != nil {
|
|
setupLog.Error(err, "unable to refresh docker secrets index")
|
|
}
|
|
setupLog.Info("docker secrets index refreshed")
|
|
}
|
|
}
|
|
}()
|
|
|
|
sshKeyManager := secret.NewSSHKeyManager(mgr.GetClient(), operatorCfg.MPI)
|
|
|
|
if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
|
|
setupLog.Error(err, "unable to set up health check")
|
|
os.Exit(1)
|
|
}
|
|
webhooksReady := make(chan struct{})
|
|
if err := mgr.AddReadyzCheck("readyz", func(req *http.Request) error {
|
|
select {
|
|
case <-webhooksReady:
|
|
return nil
|
|
default:
|
|
return fmt.Errorf("webhook handlers not yet registered")
|
|
}
|
|
}); err != nil {
|
|
setupLog.Error(err, "unable to set up ready check")
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Register controllers synchronously before mgr.Start().
|
|
// Controllers don't depend on TLS certificates.
|
|
if err := registerControllers(
|
|
mgr, operatorCfg, runtimeConfig,
|
|
dockerSecretRetriever, sshKeyManager,
|
|
); err != nil {
|
|
setupLog.Error(err, "failed to register controllers")
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Webhooks require TLS certificates to serve HTTPS. Register them in a
|
|
// goroutine that blocks until the cert-controller has written the certs.
|
|
go func() {
|
|
certMgr.WaitReady()
|
|
|
|
if operatorCfg.Server.Webhook.CertProvisionMode == configv1alpha1.CertProvisionModeAuto {
|
|
injector, err := internalcert.NewCABundleInjector(mgr.GetClient(), operatorCfg)
|
|
if err != nil {
|
|
setupLog.Error(err, "unable to create CA bundle injector")
|
|
os.Exit(1)
|
|
}
|
|
if err := injector.InjectAll(mainCtx); err != nil {
|
|
setupLog.Error(err, "failed to inject CA bundles into webhook configurations")
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
if err := registerWebhooks(mgr, operatorCfg, runtimeConfig, operatorVersion); err != nil {
|
|
setupLog.Error(err, "failed to register webhooks")
|
|
os.Exit(1)
|
|
}
|
|
close(webhooksReady)
|
|
}()
|
|
|
|
setupLog.Info("starting manager")
|
|
if err := mgr.Start(mainCtx); err != nil {
|
|
setupLog.Error(err, "problem running manager")
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
func registerControllers(
|
|
mgr ctrl.Manager,
|
|
operatorCfg *configv1alpha1.OperatorConfiguration,
|
|
runtimeConfig *commonController.RuntimeConfig,
|
|
dockerSecretRetriever *secrets.DockerSecretIndexer,
|
|
sshKeyManager *secret.SSHKeyManager,
|
|
) error {
|
|
if err := (&controller.DynamoComponentDeploymentReconciler{
|
|
Client: mgr.GetClient(),
|
|
Recorder: mgr.GetEventRecorderFor("dynamocomponentdeployment"),
|
|
Config: operatorCfg,
|
|
RuntimeConfig: runtimeConfig,
|
|
DockerSecretRetriever: dockerSecretRetriever,
|
|
}).SetupWithManager(mgr); err != nil {
|
|
return fmt.Errorf("unable to create DynamoComponentDeployment controller: %w", err)
|
|
}
|
|
|
|
scaleClient, err := createScalesGetter(mgr)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to create scale client: %w", err)
|
|
}
|
|
|
|
rbacManager := rbac.NewManager(mgr.GetClient())
|
|
|
|
if err = (&controller.DynamoGraphDeploymentReconciler{
|
|
Client: mgr.GetClient(),
|
|
Recorder: mgr.GetEventRecorderFor("dynamographdeployment"),
|
|
Config: operatorCfg,
|
|
RuntimeConfig: runtimeConfig,
|
|
DockerSecretRetriever: dockerSecretRetriever,
|
|
ScaleClient: scaleClient,
|
|
SSHKeyManager: sshKeyManager,
|
|
RBACManager: rbacManager,
|
|
}).SetupWithManager(mgr); err != nil {
|
|
return fmt.Errorf("unable to create DynamoGraphDeployment controller: %w", err)
|
|
}
|
|
|
|
if err = (&controller.DynamoGraphDeploymentScalingAdapterReconciler{
|
|
Client: mgr.GetClient(),
|
|
Scheme: mgr.GetScheme(),
|
|
Recorder: mgr.GetEventRecorderFor("dgdscalingadapter"),
|
|
Config: operatorCfg,
|
|
RuntimeConfig: runtimeConfig,
|
|
}).SetupWithManager(mgr); err != nil {
|
|
return fmt.Errorf("unable to create DGDScalingAdapter controller: %w", err)
|
|
}
|
|
|
|
if err = (&controller.DynamoGraphDeploymentRequestReconciler{
|
|
Client: mgr.GetClient(),
|
|
APIReader: mgr.GetAPIReader(),
|
|
Recorder: mgr.GetEventRecorderFor("dynamographdeploymentrequest"),
|
|
Config: operatorCfg,
|
|
RuntimeConfig: runtimeConfig,
|
|
GPUDiscoveryCache: gpu.NewGPUDiscoveryCache(),
|
|
GPUDiscovery: gpu.NewGPUDiscovery(gpu.ScrapeMetricsEndpoint),
|
|
RBACManager: rbacManager,
|
|
}).SetupWithManager(mgr); err != nil {
|
|
return fmt.Errorf("unable to create DynamoGraphDeploymentRequest controller: %w", err)
|
|
}
|
|
|
|
if err = (&controller.DynamoModelReconciler{
|
|
Client: mgr.GetClient(),
|
|
Recorder: mgr.GetEventRecorderFor("dynamomodel"),
|
|
EndpointClient: modelendpoint.NewClient(),
|
|
Config: operatorCfg,
|
|
RuntimeConfig: runtimeConfig,
|
|
}).SetupWithManager(mgr); err != nil {
|
|
return fmt.Errorf("unable to create DynamoModel controller: %w", err)
|
|
}
|
|
|
|
if err = (&controller.CheckpointReconciler{
|
|
Client: mgr.GetClient(),
|
|
Config: operatorCfg,
|
|
RuntimeConfig: runtimeConfig,
|
|
Recorder: mgr.GetEventRecorderFor("checkpoint"),
|
|
}).SetupWithManager(mgr); err != nil {
|
|
return fmt.Errorf("unable to create DynamoCheckpoint controller: %w", err)
|
|
}
|
|
|
|
setupLog.Info("Controllers registered successfully")
|
|
return nil
|
|
}
|
|
|
|
func registerWebhooks(
|
|
mgr ctrl.Manager,
|
|
operatorCfg *configv1alpha1.OperatorConfiguration,
|
|
runtimeConfig *commonController.RuntimeConfig,
|
|
operatorVersion string,
|
|
) error {
|
|
isClusterWide := operatorCfg.Namespace.Restricted == ""
|
|
if isClusterWide {
|
|
setupLog.Info("Configuring webhooks with lease-based namespace exclusion for cluster-wide mode")
|
|
internalwebhook.SetExcludedNamespaces(runtimeConfig.ExcludedNamespaces)
|
|
} else {
|
|
setupLog.Info("Configuring webhooks for namespace-restricted mode (no lease checking)",
|
|
"restrictedNamespace", operatorCfg.Namespace.Restricted)
|
|
internalwebhook.SetExcludedNamespaces(nil)
|
|
}
|
|
|
|
setupLog.Info("Registering validation webhooks")
|
|
|
|
dcdHandler := webhookvalidation.NewDynamoComponentDeploymentHandler()
|
|
if err := dcdHandler.RegisterWithManager(mgr); err != nil {
|
|
return fmt.Errorf("unable to register DynamoComponentDeployment webhook: %w", err)
|
|
}
|
|
|
|
dgdHandler := webhookvalidation.NewDynamoGraphDeploymentHandler(mgr)
|
|
if err := dgdHandler.RegisterWithManager(mgr); err != nil {
|
|
return fmt.Errorf("unable to register DynamoGraphDeployment webhook: %w", err)
|
|
}
|
|
|
|
dmHandler := webhookvalidation.NewDynamoModelHandler()
|
|
if err := dmHandler.RegisterWithManager(mgr); err != nil {
|
|
return fmt.Errorf("unable to register DynamoModel webhook: %w", err)
|
|
}
|
|
|
|
dgdrHandler := webhookvalidation.NewDynamoGraphDeploymentRequestHandler(
|
|
isClusterWide, ptr.Deref(operatorCfg.GPU.DiscoveryEnabled, true),
|
|
)
|
|
if err := dgdrHandler.RegisterWithManager(mgr); err != nil {
|
|
return fmt.Errorf("unable to register DynamoGraphDeploymentRequest webhook: %w", err)
|
|
}
|
|
|
|
if err := ctrl.NewWebhookManagedBy(mgr).
|
|
For(&nvidiacomv1beta1.DynamoGraphDeploymentRequest{}).
|
|
Complete(); err != nil {
|
|
return fmt.Errorf("unable to register DynamoGraphDeploymentRequest conversion webhook: %w", err)
|
|
}
|
|
|
|
setupLog.Info("Registering defaulting webhooks")
|
|
|
|
dgdDefaulter := webhookdefaulting.NewDGDDefaulter(operatorVersion)
|
|
if err := dgdDefaulter.RegisterWithManager(mgr); err != nil {
|
|
return fmt.Errorf("unable to register DynamoGraphDeployment defaulting webhook: %w", err)
|
|
}
|
|
|
|
dgdrDefaulter := webhookdefaulting.NewDGDRDefaulter(operatorVersion)
|
|
if err := dgdrDefaulter.RegisterWithManager(mgr); err != nil {
|
|
return fmt.Errorf("unable to register DynamoGraphDeploymentRequest defaulting webhook: %w", err)
|
|
}
|
|
|
|
setupLog.Info("Webhooks registered successfully")
|
|
return nil
|
|
}
|