mirror of
https://github.com/cozystack/cozystack.git
synced 2026-10-24 20:16:50 +09:00
Hidora's customers are leaving VMware now, and a tenant has to be able to import their own machines without a platform administrator. The chart approach could not express that: a tenant can neither create the Secret holding vCenter credentials nor name the VDDK image, so both had to come from somewhere they cannot reach. Two CRDs in a new `forklift.cozystack.io` group, split by lifecycle. A VMImportSource is a long-lived connection -- type, endpoint, credentials, readiness -- and a VMImportTask is the one-shot operation that names machines and produces VMDisks and VMInstances. The outputs carry no owner reference back to the task, so deleting a finished import leaves the machines alone. Konveyor Forklift does the transfer underneath; the controller renders its objects, mirrors its verdicts, and hands each finished volume into a VMDisk without copying it. What a live vCenter migration forced out, and no amount of source reading would have: - A guest copied verbatim keeps the drivers it had under VMware and no virtio, so on a virtio disk it does not fail the import -- it succeeds and then does not boot. Imported disks go on SATA. - VDDK connects straight to the ESXi host at the address vCenter advertises, which is routinely unreachable, and on the first cluster collided with the Service CIDR. `spec.hosts` redirects per host. - Firmware must be carried across or a UEFI guest imports as BIOS and never boots (stacked on #3002). - Deleting a task mid-transfer orphaned its DataVolume; a finalizer now removes the engine's volumes and leaves the outputs. The console gains a Migration section: connections are editable, and the VM field is a picker built from a machine list the controller publishes, since the aggregated API cannot reach the inventory and should not hold a source's credentials to try. Two general mechanisms fall out of that -- an option source may take an argument, and one named in a CRD annotation may reference a sibling field. Design: cozystack/community#62. Docs: cozystack/website#673. Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
204 lines
7.3 KiB
Go
204 lines
7.3 KiB
Go
/*
|
|
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.<forklift-namespace>.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)
|
|
}
|
|
}
|