|
| 1 | +/* |
| 2 | +Copyright 2025. |
| 3 | +
|
| 4 | +Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | +you may not use this file except in compliance with the License. |
| 6 | +You may obtain a copy of the License at |
| 7 | +
|
| 8 | + http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | +
|
| 10 | +Unless required by applicable law or agreed to in writing, software |
| 11 | +distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | +See the License for the specific language governing permissions and |
| 14 | +limitations under the License. |
| 15 | +*/ |
| 16 | + |
| 17 | +package main |
| 18 | + |
| 19 | +import ( |
| 20 | + "crypto/tls" |
| 21 | + "flag" |
| 22 | + "fmt" |
| 23 | + "os" |
| 24 | + "path/filepath" |
| 25 | + |
| 26 | + // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) |
| 27 | + // to ensure that exec-entrypoint and run can make use of them. |
| 28 | + _ "k8s.io/client-go/plugin/pkg/client/auth" |
| 29 | + |
| 30 | + sdkconfig "github.com/stackitcloud/stackit-sdk-go/core/config" |
| 31 | + "k8s.io/apimachinery/pkg/runtime" |
| 32 | + utilruntime "k8s.io/apimachinery/pkg/util/runtime" |
| 33 | + clientgoscheme "k8s.io/client-go/kubernetes/scheme" |
| 34 | + ctrl "sigs.k8s.io/controller-runtime" |
| 35 | + "sigs.k8s.io/controller-runtime/pkg/certwatcher" |
| 36 | + "sigs.k8s.io/controller-runtime/pkg/healthz" |
| 37 | + "sigs.k8s.io/controller-runtime/pkg/log/zap" |
| 38 | + "sigs.k8s.io/controller-runtime/pkg/metrics/filters" |
| 39 | + metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" |
| 40 | + "sigs.k8s.io/controller-runtime/pkg/webhook" |
| 41 | + |
| 42 | + albclient "github.com/stackitcloud/cloud-provider-stackit/pkg/stackit" |
| 43 | + albsdk "github.com/stackitcloud/stackit-sdk-go/services/alb" |
| 44 | + certsdk "github.com/stackitcloud/stackit-sdk-go/services/certificates" |
| 45 | + // +kubebuilder:scaffold:imports |
| 46 | +) |
| 47 | + |
| 48 | +var ( |
| 49 | + scheme = runtime.NewScheme() |
| 50 | + setupLog = ctrl.Log.WithName("setup") |
| 51 | +) |
| 52 | + |
| 53 | +func init() { |
| 54 | + utilruntime.Must(clientgoscheme.AddToScheme(scheme)) |
| 55 | + |
| 56 | + // +kubebuilder:scaffold:scheme |
| 57 | +} |
| 58 | + |
| 59 | +// nolint:gocyclo |
| 60 | +func main() { |
| 61 | + var metricsAddr string |
| 62 | + var metricsCertPath, metricsCertName, metricsCertKey string |
| 63 | + var webhookCertPath, webhookCertName, webhookCertKey string |
| 64 | + var enableLeaderElection bool |
| 65 | + var leaderElectionNamespace string |
| 66 | + var leaderElectionID string |
| 67 | + var probeAddr string |
| 68 | + var secureMetrics bool |
| 69 | + var enableHTTP2 bool |
| 70 | + var tlsOpts []func(*tls.Config) |
| 71 | + flag.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. "+ |
| 72 | + "Use :8443 for HTTPS or :8080 for HTTP, or leave as 0 to disable the metrics service.") |
| 73 | + flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") |
| 74 | + flag.BoolVar(&enableLeaderElection, "leader-elect", false, |
| 75 | + "Enable leader election for controller manager. "+ |
| 76 | + "Enabling this will ensure there is only one active controller manager.") |
| 77 | + flag.StringVar(&leaderElectionNamespace, "leader-election-namespace", "default", "The namespace in which the leader "+ |
| 78 | + "election resource will be created.") |
| 79 | + flag.StringVar(&leaderElectionID, "leader-election-id", "d0fbe9c4.stackit.cloud", "The name of the resource that "+ |
| 80 | + "leader election will use for holding the leader lock.") |
| 81 | + flag.BoolVar(&secureMetrics, "metrics-secure", true, |
| 82 | + "If set, the metrics endpoint is served securely via HTTPS. Use --metrics-secure=false to use HTTP instead.") |
| 83 | + flag.StringVar(&webhookCertPath, "webhook-cert-path", "", "The directory that contains the webhook certificate.") |
| 84 | + flag.StringVar(&webhookCertName, "webhook-cert-name", "tls.crt", "The name of the webhook certificate file.") |
| 85 | + flag.StringVar(&webhookCertKey, "webhook-cert-key", "tls.key", "The name of the webhook key file.") |
| 86 | + flag.StringVar(&metricsCertPath, "metrics-cert-path", "", |
| 87 | + "The directory that contains the metrics server certificate.") |
| 88 | + flag.StringVar(&metricsCertName, "metrics-cert-name", "tls.crt", "The name of the metrics server certificate file.") |
| 89 | + flag.StringVar(&metricsCertKey, "metrics-cert-key", "tls.key", "The name of the metrics server key file.") |
| 90 | + flag.BoolVar(&enableHTTP2, "enable-http2", false, |
| 91 | + "If set, HTTP/2 will be enabled for the metrics and webhook servers") |
| 92 | + opts := zap.Options{ |
| 93 | + Development: true, |
| 94 | + } |
| 95 | + opts.BindFlags(flag.CommandLine) |
| 96 | + flag.Parse() |
| 97 | + |
| 98 | + ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) |
| 99 | + |
| 100 | + // if the enable-http2 flag is false (the default), http/2 should be disabled |
| 101 | + // due to its vulnerabilities. More specifically, disabling http/2 will |
| 102 | + // prevent from being vulnerable to the HTTP/2 Stream Cancellation and |
| 103 | + // Rapid Reset CVEs. For more information see: |
| 104 | + // - https://github.com/advisories/GHSA-qppj-fm5r-hxr3 |
| 105 | + // - https://github.com/advisories/GHSA-4374-p667-p6c8 |
| 106 | + disableHTTP2 := func(c *tls.Config) { |
| 107 | + setupLog.Info("disabling http/2") |
| 108 | + c.NextProtos = []string{"http/1.1"} |
| 109 | + } |
| 110 | + |
| 111 | + if !enableHTTP2 { |
| 112 | + tlsOpts = append(tlsOpts, disableHTTP2) |
| 113 | + } |
| 114 | + |
| 115 | + // Create watchers for metrics and webhooks certificates |
| 116 | + var metricsCertWatcher, webhookCertWatcher *certwatcher.CertWatcher |
| 117 | + |
| 118 | + // Initial webhook TLS options |
| 119 | + webhookTLSOpts := tlsOpts |
| 120 | + |
| 121 | + if len(webhookCertPath) > 0 { |
| 122 | + setupLog.Info("Initializing webhook certificate watcher using provided certificates", |
| 123 | + "webhook-cert-path", webhookCertPath, "webhook-cert-name", webhookCertName, "webhook-cert-key", webhookCertKey) |
| 124 | + |
| 125 | + var err error |
| 126 | + webhookCertWatcher, err = certwatcher.New( |
| 127 | + filepath.Join(webhookCertPath, webhookCertName), |
| 128 | + filepath.Join(webhookCertPath, webhookCertKey), |
| 129 | + ) |
| 130 | + if err != nil { |
| 131 | + setupLog.Error(err, "Failed to initialize webhook certificate watcher") |
| 132 | + os.Exit(1) |
| 133 | + } |
| 134 | + |
| 135 | + webhookTLSOpts = append(webhookTLSOpts, func(config *tls.Config) { |
| 136 | + config.GetCertificate = webhookCertWatcher.GetCertificate |
| 137 | + }) |
| 138 | + } |
| 139 | + |
| 140 | + webhookServer := webhook.NewServer(webhook.Options{ |
| 141 | + TLSOpts: webhookTLSOpts, |
| 142 | + }) |
| 143 | + |
| 144 | + // Metrics endpoint is enabled in 'config/default/kustomization.yaml'. The Metrics options configure the server. |
| 145 | + // More info: |
| 146 | + // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.19.4/pkg/metrics/server |
| 147 | + // - https://book.kubebuilder.io/reference/metrics.html |
| 148 | + metricsServerOptions := metricsserver.Options{ |
| 149 | + BindAddress: metricsAddr, |
| 150 | + SecureServing: secureMetrics, |
| 151 | + TLSOpts: tlsOpts, |
| 152 | + } |
| 153 | + |
| 154 | + if secureMetrics { |
| 155 | + // FilterProvider is used to protect the metrics endpoint with authn/authz. |
| 156 | + // These configurations ensure that only authorized users and service accounts |
| 157 | + // can access the metrics endpoint. The RBAC are configured in 'config/rbac/kustomization.yaml'. More info: |
| 158 | + // https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.19.4/pkg/metrics/filters#WithAuthenticationAndAuthorization |
| 159 | + metricsServerOptions.FilterProvider = filters.WithAuthenticationAndAuthorization |
| 160 | + } |
| 161 | + |
| 162 | + // If the certificate is not specified, controller-runtime will automatically |
| 163 | + // generate self-signed certificates for the metrics server. While convenient for development and testing, |
| 164 | + // this setup is not recommended for production. |
| 165 | + // |
| 166 | + // TODO(user): If you enable certManager, uncomment the following lines: |
| 167 | + // - [METRICS-WITH-CERTS] at config/default/kustomization.yaml to generate and use certificates |
| 168 | + // managed by cert-manager for the metrics server. |
| 169 | + // - [PROMETHEUS-WITH-CERTS] at config/prometheus/kustomization.yaml for TLS certification. |
| 170 | + if len(metricsCertPath) > 0 { |
| 171 | + setupLog.Info("Initializing metrics certificate watcher using provided certificates", |
| 172 | + "metrics-cert-path", metricsCertPath, "metrics-cert-name", metricsCertName, "metrics-cert-key", metricsCertKey) |
| 173 | + |
| 174 | + var err error |
| 175 | + metricsCertWatcher, err = certwatcher.New( |
| 176 | + filepath.Join(metricsCertPath, metricsCertName), |
| 177 | + filepath.Join(metricsCertPath, metricsCertKey), |
| 178 | + ) |
| 179 | + if err != nil { |
| 180 | + setupLog.Error(err, "to initialize metrics certificate watcher", "error", err) |
| 181 | + os.Exit(1) |
| 182 | + } |
| 183 | + |
| 184 | + metricsServerOptions.TLSOpts = append(metricsServerOptions.TLSOpts, func(config *tls.Config) { |
| 185 | + config.GetCertificate = metricsCertWatcher.GetCertificate |
| 186 | + }) |
| 187 | + } |
| 188 | + |
| 189 | + mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ |
| 190 | + Scheme: scheme, |
| 191 | + Metrics: metricsServerOptions, |
| 192 | + WebhookServer: webhookServer, |
| 193 | + HealthProbeBindAddress: probeAddr, |
| 194 | + LeaderElection: enableLeaderElection, |
| 195 | + LeaderElectionID: leaderElectionID, |
| 196 | + LeaderElectionNamespace: leaderElectionNamespace, |
| 197 | + // LeaderElectionReleaseOnCancel defines if the leader should step down voluntarily |
| 198 | + // when the Manager ends. This requires the binary to immediately end when the |
| 199 | + // Manager is stopped, otherwise, this setting is unsafe. Setting this significantly |
| 200 | + // speeds up voluntary leader transitions as the new leader don't have to wait |
| 201 | + // LeaseDuration time first. |
| 202 | + // |
| 203 | + // In the default scaffold provided, the program ends immediately after |
| 204 | + // the manager stops, so would be fine to enable this option. However, |
| 205 | + // if you are doing or is intended to do any operation such as perform cleanups |
| 206 | + // after the manager stops then its usage might be unsafe. |
| 207 | + // LeaderElectionReleaseOnCancel: true, |
| 208 | + }) |
| 209 | + if err != nil { |
| 210 | + setupLog.Error(err, "unable to start manager") |
| 211 | + os.Exit(1) |
| 212 | + } |
| 213 | + |
| 214 | + albURL, _ := os.LookupEnv("STACKIT_LOAD_BALANCER_API_ALB_URL") |
| 215 | + |
| 216 | + certURL, _ := os.LookupEnv("STACKIT_LOAD_BALANCER_API_CERT_URL") |
| 217 | + |
| 218 | + region, set := os.LookupEnv("STACKIT_REGION") |
| 219 | + if !set { |
| 220 | + setupLog.Error(err, "STACKIT_REGION not set", "controller", "IngressClass") |
| 221 | + os.Exit(1) |
| 222 | + } |
| 223 | + projectID, set := os.LookupEnv("PROJECT_ID") |
| 224 | + if !set { |
| 225 | + setupLog.Error(err, "PROJECT_ID not set", "controller", "IngressClass") |
| 226 | + os.Exit(1) |
| 227 | + } |
| 228 | + networkID, set := os.LookupEnv("NETWORK_ID") |
| 229 | + if !set { |
| 230 | + setupLog.Error(err, "NETWORK_ID not set", "controller", "IngressClass") |
| 231 | + os.Exit(1) |
| 232 | + } |
| 233 | + |
| 234 | + // Create an ALB SDK client |
| 235 | + albOpts := []sdkconfig.ConfigurationOption{} |
| 236 | + if albURL != "" { |
| 237 | + albOpts = append(albOpts, sdkconfig.WithEndpoint(albURL)) |
| 238 | + } |
| 239 | + |
| 240 | + certOpts := []sdkconfig.ConfigurationOption{} |
| 241 | + if certURL != "" { |
| 242 | + certOpts = append(certOpts, sdkconfig.WithEndpoint(certURL)) |
| 243 | + } |
| 244 | + |
| 245 | + fmt.Printf("Create ALB SDK client\n") |
| 246 | + sdkClient, err := albsdk.NewAPIClient(albOpts...) |
| 247 | + if err != nil { |
| 248 | + setupLog.Error(err, "unable to create ALB SDK client", "controller", "IngressClass") |
| 249 | + os.Exit(1) |
| 250 | + } |
| 251 | + // Create an ALB client |
| 252 | + fmt.Printf("Create ALB client\n") |
| 253 | + albClient, err := albclient.NewClient(sdkClient) |
| 254 | + if err != nil { |
| 255 | + setupLog.Error(err, "unable to create ALB client", "controller", "IngressClass") |
| 256 | + os.Exit(1) |
| 257 | + } |
| 258 | + |
| 259 | + // Create an Certificates SDK client |
| 260 | + certificateAPI, err := certsdk.NewAPIClient(certOpts...) |
| 261 | + if err != nil { |
| 262 | + setupLog.Error(err, "unable to create certificate SDK client", "controller", "IngressClass") |
| 263 | + os.Exit(1) |
| 264 | + } |
| 265 | + // Create an Certificates API client |
| 266 | + certificateClient, err := certificateclient.NewCertClient(certificateAPI) |
| 267 | + if err != nil { |
| 268 | + setupLog.Error(err, "unable to create Certificates client", "controller", "IngressClass") |
| 269 | + os.Exit(1) |
| 270 | + } |
| 271 | + |
| 272 | + if err = (&controller.IngressClassReconciler{ |
| 273 | + Client: mgr.GetClient(), |
| 274 | + ALBClient: albClient, |
| 275 | + CertificateClient: certificateClient, |
| 276 | + Scheme: mgr.GetScheme(), |
| 277 | + ProjectID: projectID, |
| 278 | + NetworkID: networkID, |
| 279 | + Region: region, |
| 280 | + }).SetupWithManager(mgr); err != nil { |
| 281 | + setupLog.Error(err, "unable to create controller", "controller", "IngressClass") |
| 282 | + os.Exit(1) |
| 283 | + } |
| 284 | + // +kubebuilder:scaffold:builder |
| 285 | + |
| 286 | + if metricsCertWatcher != nil { |
| 287 | + setupLog.Info("Adding metrics certificate watcher to manager") |
| 288 | + if err := mgr.Add(metricsCertWatcher); err != nil { |
| 289 | + setupLog.Error(err, "unable to add metrics certificate watcher to manager") |
| 290 | + os.Exit(1) |
| 291 | + } |
| 292 | + } |
| 293 | + |
| 294 | + if webhookCertWatcher != nil { |
| 295 | + setupLog.Info("Adding webhook certificate watcher to manager") |
| 296 | + if err := mgr.Add(webhookCertWatcher); err != nil { |
| 297 | + setupLog.Error(err, "unable to add webhook certificate watcher to manager") |
| 298 | + os.Exit(1) |
| 299 | + } |
| 300 | + } |
| 301 | + |
| 302 | + if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { |
| 303 | + setupLog.Error(err, "unable to set up health check") |
| 304 | + os.Exit(1) |
| 305 | + } |
| 306 | + if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil { |
| 307 | + setupLog.Error(err, "unable to set up ready check") |
| 308 | + os.Exit(1) |
| 309 | + } |
| 310 | + |
| 311 | + setupLog.Info("starting manager") |
| 312 | + if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { |
| 313 | + setupLog.Error(err, "problem running manager") |
| 314 | + os.Exit(1) |
| 315 | + } |
| 316 | +} |
0 commit comments