/* Copyright 2026. 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. */ // Command migration-controller reconciles the forklift.cozystack.io API group: // VMImportSource connections and the VMImportTask operations that run over them. package main import ( "crypto/tls" "flag" "fmt" "os" corev1 "k8s.io/api/core/v1" storagev1 "k8s.io/api/storage/v1" "k8s.io/apimachinery/pkg/runtime" 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/client" "sigs.k8s.io/controller-runtime/pkg/healthz" "sigs.k8s.io/controller-runtime/pkg/log/zap" metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" "sigs.k8s.io/controller-runtime/pkg/webhook" migrationv1alpha1 "github.com/cozystack/cozystack/api/migration/v1alpha1" "github.com/cozystack/cozystack/internal/migrationcontroller" ) var ( scheme = runtime.NewScheme() setupLog = ctrl.Log.WithName("setup") ) func init() { utilruntime.Must(clientgoscheme.AddToScheme(scheme)) utilruntime.Must(migrationv1alpha1.AddToScheme(scheme)) // Forklift, KubeVirt and CDI objects are handled as unstructured, so their // types are deliberately absent from the scheme: Forklift is an optional // package and this controller must start cleanly on a cluster where its // CRDs are not installed at all. } // envOr returns the environment variable's value, or a fallback when unset. func envOr(key, fallback string) string { if v := os.Getenv(key); v != "" { return v } return fallback } func main() { var metricsAddr string var probeAddr string var enableLeaderElection bool var secureMetrics bool var enableHTTP2 bool var vddkImage string var forkliftNamespace string var inventoryURL string var tlsOpts []func(*tls.Config) flag.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. "+ "Use :8443 for HTTPS or :8080 for HTTP, or leave as 0 to disable the metrics service.") flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") flag.BoolVar(&enableLeaderElection, "leader-elect", false, "Enable leader election for controller manager. "+ "Enabling this will ensure there is only one active controller manager.") flag.BoolVar(&secureMetrics, "metrics-secure", true, "If set, the metrics endpoint is served securely via HTTPS. Use --metrics-secure=false to use HTTP instead.") flag.BoolVar(&enableHTTP2, "enable-http2", false, "If set, HTTP/2 will be enabled for the metrics and webhook servers") flag.StringVar(&vddkImage, "vddk-image", os.Getenv("MIGRATION_VDDK_IMAGE"), "Container image carrying the VMware Virtual Disk Development Kit. Platform "+ "configuration, never a tenant value: the image is built from a proprietary "+ "SDK that Cozystack cannot ship, so an operator who holds a licence builds it "+ "and names it here. Empty means the vSphere import path is unavailable, which "+ "vSphere sources report on their own status.") flag.StringVar(&forkliftNamespace, "forklift-namespace", envOr("MIGRATION_FORKLIFT_NAMESPACE", "cozy-forklift"), "Namespace Forklift runs in. The controller reads the inventory service's CA "+ "from its serving-certificate secret there.") flag.StringVar(&inventoryURL, "inventory-url", os.Getenv("MIGRATION_INVENTORY_URL"), "Base URL of Forklift's inventory REST service. Defaults to "+ "https://forklift-inventory..svc:8443. The controller "+ "reads the source's networks and datastores from it: those identifiers "+ "appear on no Forklift custom resource, so the Plan's maps cannot be built "+ "without it.") opts := zap.Options{Development: false} opts.BindFlags(flag.CommandLine) flag.Parse() ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) // HTTP/2 stays off by default: it carries the Stream Cancellation and Rapid // Reset CVEs and nothing here needs it. if !enableHTTP2 { tlsOpts = append(tlsOpts, func(c *tls.Config) { setupLog.Info("disabling http/2") c.NextProtos = []string{"http/1.1"} }) } metricsServerOptions := metricsserver.Options{ BindAddress: metricsAddr, SecureServing: secureMetrics, TLSOpts: tlsOpts, } config := ctrl.GetConfigOrDie() config.QPS = 50.0 config.Burst = 100 mgr, err := ctrl.NewManager(config, ctrl.Options{ Scheme: scheme, Metrics: metricsServerOptions, WebhookServer: webhook.NewServer(webhook.Options{TLSOpts: tlsOpts}), HealthProbeBindAddress: probeAddr, LeaderElection: enableLeaderElection, LeaderElectionID: "forklift.cozystack.io", Client: client.Options{ // Secrets and volumes are read and written point-wise across // arbitrary tenant namespaces. A cluster-wide informer for them // would need list/watch RBAC this package deliberately does not // grant, and would hold every tenant's Secret in memory. Direct // apiserver calls keep the blast radius to the namespaces actually // being reconciled. Cache: &client.CacheOptions{ DisableFor: []client.Object{ &corev1.Secret{}, &corev1.PersistentVolume{}, &corev1.PersistentVolumeClaim{}, &storagev1.StorageClass{}, }, }, }, }) if err != nil { setupLog.Error(err, "unable to start manager") os.Exit(1) } if vddkImage == "" { setupLog.Info("no VDDK image configured; the vSphere import path will report itself unavailable") } if inventoryURL == "" { inventoryURL = fmt.Sprintf("https://forklift-inventory.%s.svc:8443", forkliftNamespace) } // One client for both reconcilers: the task reads a VM's topology, the // source publishes the machine list the console's picker offers. inventory := migrationcontroller.NewInventoryClient( inventoryURL, forkliftNamespace, mgr.GetAPIReader()) if err = (&migrationcontroller.VMImportSourceReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), Recorder: mgr.GetEventRecorderFor("migration-controller"), VDDKImage: vddkImage, Inventory: inventory, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "VMImportSource") os.Exit(1) } if err = (&migrationcontroller.VMImportTaskReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), Recorder: mgr.GetEventRecorderFor("migration-controller"), Inventory: inventory, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "VMImportTask") os.Exit(1) } if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { setupLog.Error(err, "unable to set up health check") os.Exit(1) } if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil { setupLog.Error(err, "unable to set up ready check") os.Exit(1) } setupLog.Info("starting manager") if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { setupLog.Error(err, "problem running manager") os.Exit(1) } }