/* 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. */ // Package main is the composition root: it loads the provider config (fail // fast), assembles the provider registry, and wires the reconciler, health // engine, lease store, discovery API, orphan GC, and metrics onto one // controller-runtime manager. package main import ( "context" "crypto/tls" "flag" "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. _ "k8s.io/client-go/plugin/pkg/client/auth" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/labels" "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/cache" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/healthz" "sigs.k8s.io/controller-runtime/pkg/log/zap" ctrlmetrics "sigs.k8s.io/controller-runtime/pkg/metrics" "sigs.k8s.io/controller-runtime/pkg/metrics/filters" metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" crawlv1alpha1 "gitea.home.hrajfrisbee.cz/kacerr/egress-proxies-operator/api/v1alpha1" "gitea.home.hrajfrisbee.cz/kacerr/egress-proxies-operator/internal/controller" "gitea.home.hrajfrisbee.cz/kacerr/egress-proxies-operator/internal/discovery" "gitea.home.hrajfrisbee.cz/kacerr/egress-proxies-operator/internal/gc" "gitea.home.hrajfrisbee.cz/kacerr/egress-proxies-operator/internal/health" "gitea.home.hrajfrisbee.cz/kacerr/egress-proxies-operator/internal/lease" "gitea.home.hrajfrisbee.cz/kacerr/egress-proxies-operator/internal/metrics" "gitea.home.hrajfrisbee.cz/kacerr/egress-proxies-operator/internal/provider" "gitea.home.hrajfrisbee.cz/kacerr/egress-proxies-operator/internal/provider/gcp" "gitea.home.hrajfrisbee.cz/kacerr/egress-proxies-operator/internal/provider/kubernetes" "gitea.home.hrajfrisbee.cz/kacerr/egress-proxies-operator/internal/provider/registry" // +kubebuilder:scaffold:imports ) var ( scheme = runtime.NewScheme() setupLog = ctrl.Log.WithName("setup") ) func init() { utilruntime.Must(clientgoscheme.AddToScheme(scheme)) utilruntime.Must(crawlv1alpha1.AddToScheme(scheme)) // +kubebuilder:scaffold:scheme } // nolint:gocyclo func main() { var metricsAddr string var metricsCertPath, metricsCertName, metricsCertKey string var enableLeaderElection bool var probeAddr string var secureMetrics bool var enableHTTP2 bool var tlsOpts []func(*tls.Config) var providersConfig string var discoveryAddr string var proxyNamespace string var healthWorkers int var gcInterval, gcMinAge time.Duration var gcAllowNamespaced bool var leaseCooldown, maxLeaseTTL time.Duration 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.StringVar(&metricsCertPath, "metrics-cert-path", "", "The directory that contains the metrics server certificate.") flag.StringVar(&metricsCertName, "metrics-cert-name", "tls.crt", "The name of the metrics server certificate file.") flag.StringVar(&metricsCertKey, "metrics-cert-key", "tls.key", "The name of the metrics server key file.") flag.BoolVar(&enableHTTP2, "enable-http2", false, "If set, HTTP/2 will be enabled for the metrics server") flag.StringVar(&providersConfig, "providers-config", "", "Path to the providers config YAML. Required.") flag.StringVar(&discoveryAddr, "discovery-addr", ":8090", "Listen address of the discovery/lease HTTP API.") flag.StringVar(&proxyNamespace, "proxy-namespace", "", "Restrict the manager's cache to one namespace. Empty watches all namespaces. "+ "Restricting also disables orphan GC unless --gc-allow-namespaced is set.") flag.IntVar(&healthWorkers, "health-workers", 8, "Number of concurrent health-probe workers.") flag.DurationVar(&gcInterval, "gc-interval", 10*time.Minute, "Interval between orphan GC sweeps.") flag.DurationVar(&gcMinAge, "gc-min-age", 10*time.Minute, "Minimum instance age before orphan GC may delete it.") flag.BoolVar(&gcAllowNamespaced, "gc-allow-namespaced", false, "Allow orphan GC to run although the cache is namespace-restricted. Dangerous: proxies "+ "outside the namespace count as orphans and their instances get deleted.") flag.DurationVar(&leaseCooldown, "lease-cooldown", 15*time.Minute, "How long a reported proxy/target pair is excluded from lease selection.") flag.DurationVar(&maxLeaseTTL, "max-lease-ttl", time.Hour, "Maximum lease TTL a client may request.") opts := zap.Options{ Development: true, } opts.BindFlags(flag.CommandLine) flag.Parse() ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) ctx := ctrl.SetupSignalHandler() // Providers load first and fail fast: a manager that comes up without // its backends would just convert every Proxy into an error loop. if providersConfig == "" { setupLog.Error(nil, "--providers-config is required") os.Exit(1) } cfg, err := provider.LoadConfigFile(providersConfig) if err != nil { setupLog.Error(err, "Failed to load providers config", "path", providersConfig) os.Exit(1) } providers, err := registry.Build(ctx, cfg, map[string]registry.Constructor{ "kubernetes": kubernetes.New, "gcp": gcp.New, }) if err != nil { setupLog.Error(err, "Failed to build providers") os.Exit(1) } m := metrics.New() for name, p := range providers { providers[name] = provider.WithMetrics(name, p, m) } // 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"} } if !enableHTTP2 { tlsOpts = append(tlsOpts, disableHTTP2) } // Metrics endpoint is enabled in 'config/default/kustomization.yaml'. The Metrics options configure the server. // More info: // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.24.1/pkg/metrics/server // - https://book.kubebuilder.io/reference/metrics.html metricsServerOptions := metricsserver.Options{ BindAddress: metricsAddr, SecureServing: secureMetrics, TLSOpts: tlsOpts, } if secureMetrics { // FilterProvider is used to protect the metrics endpoint with authn/authz. // These configurations ensure that only authorized users and service accounts // can access the metrics endpoint. The RBAC are configured in 'config/rbac/kustomization.yaml'. More info: // https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.24.1/pkg/metrics/filters#WithAuthenticationAndAuthorization metricsServerOptions.FilterProvider = filters.WithAuthenticationAndAuthorization } // If the certificate is not specified, controller-runtime will automatically // generate self-signed certificates for the metrics server. While convenient for development and testing, // this setup is not recommended for production. This project doesn't use cert-manager (no admission // webhooks, no other consumer of managed certs) -- pass real certs via the flags below if needed. if len(metricsCertPath) > 0 { setupLog.Info("Initializing metrics certificate watcher using provided certificates", "metrics-cert-path", metricsCertPath, "metrics-cert-name", metricsCertName, "metrics-cert-key", metricsCertKey) metricsServerOptions.CertDir = metricsCertPath metricsServerOptions.CertName = metricsCertName metricsServerOptions.KeyName = metricsCertKey } // The Secret cache is restricted to labelled cloud-init Secrets: the // operator has cluster-wide Secret read RBAC, and without the label // selector it would cache every Secret in scope. cacheOpts := cache.Options{ ByObject: map[client.Object]cache.ByObject{ &corev1.Secret{}: { Label: labels.SelectorFromSet(labels.Set{crawlv1alpha1.LabelCloudInit: "true"}), }, }, } if proxyNamespace != "" { cacheOpts.DefaultNamespaces = map[string]cache.Config{proxyNamespace: {}} } mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ Scheme: scheme, Metrics: metricsServerOptions, HealthProbeBindAddress: probeAddr, Cache: cacheOpts, LeaderElection: enableLeaderElection, LeaderElectionID: "b47711d1.example.com", }) if err != nil { setupLog.Error(err, "Failed to start manager") os.Exit(1) } store := lease.NewStore(leaseCooldown) if err := mgr.Add(store); err != nil { setupLog.Error(err, "Failed to add lease store") os.Exit(1) } engine := health.NewEngine(mgr.GetClient()) engine.Workers = healthWorkers engine.Metrics = m if err := mgr.Add(engine); err != nil { setupLog.Error(err, "Failed to add health engine") os.Exit(1) } if err := mgr.Add(&discovery.Server{ Reader: mgr.GetClient(), Store: store, Addr: discoveryAddr, Token: os.Getenv("DISCOVERY_TOKEN"), MaxLeaseTTL: maxLeaseTTL, Metrics: m, }); err != nil { setupLog.Error(err, "Failed to add discovery server") os.Exit(1) } if err := mgr.Add(&gc.Sweeper{ Reader: mgr.GetClient(), Providers: providers, Interval: gcInterval, MinAge: gcMinAge, NamespaceRestricted: proxyNamespace != "", AllowNamespaced: gcAllowNamespaced, }); err != nil { setupLog.Error(err, "Failed to add orphan GC") os.Exit(1) } if err := m.Register(ctrlmetrics.Registry, proxyPhaseCounts(mgr.GetClient()), func() int { total := 0 for _, n := range store.Counts() { total += n } return total }, ); err != nil { setupLog.Error(err, "Failed to register metrics") os.Exit(1) } if err := (&controller.ProxyReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), Providers: providers, Health: engine, HealthEvents: engine.Events, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "Failed to create controller", "controller", "proxy") os.Exit(1) } // +kubebuilder:scaffold:builder if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { setupLog.Error(err, "Failed to set up health check") os.Exit(1) } if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil { setupLog.Error(err, "Failed to set up ready check") os.Exit(1) } setupLog.Info("Starting manager") if err := mgr.Start(ctx); err != nil { setupLog.Error(err, "Failed to run manager") os.Exit(1) } } // proxyPhaseCounts reads phase counts from the cache at scrape time. Before // the cache has synced (or on any list error) it reports nothing rather // than something wrong. func proxyPhaseCounts(c client.Reader) func() map[string]int { return func() map[string]int { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() var list crawlv1alpha1.ProxyList if err := c.List(ctx, &list); err != nil { return nil } counts := map[string]int{} for i := range list.Items { phase := string(list.Items[i].Status.Phase) if phase == "" { phase = string(crawlv1alpha1.PhasePending) } counts[phase]++ } return counts } }