controllers/manager/staticgatewayconfiguration_controller.go (325 lines of code) (raw):

// Copyright (c) Microsoft Corporation. // Licensed under the MIT license. package manager import ( "context" "fmt" "os" "strings" "golang.zx2c4.com/wireguard/wgctrl/wgtypes" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/client-go/tools/record" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/builder" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/event" "sigs.k8s.io/controller-runtime/pkg/handler" "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/predicate" "sigs.k8s.io/controller-runtime/pkg/reconcile" egressgatewayv1alpha1 "github.com/Azure/kube-egress-gateway/api/v1alpha1" "github.com/Azure/kube-egress-gateway/pkg/consts" "github.com/Azure/kube-egress-gateway/pkg/metrics" ) var _ reconcile.Reconciler = &StaticGatewayConfigurationReconciler{} // StaticGatewayConfigurationReconciler reconciles gateway loadBalancer according to a StaticGatewayConfiguration object type StaticGatewayConfigurationReconciler struct { client.Client SecretNamespace string Recorder record.EventRecorder } //+kubebuilder:rbac:groups=core,resources=events,verbs=create;patch //+kubebuilder:rbac:groups=core,resources=secrets,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups=egressgateway.kubernetes.azure.com,resources=staticgatewayconfigurations,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups=egressgateway.kubernetes.azure.com,resources=staticgatewayconfigurations/status,verbs=get;update;patch //+kubebuilder:rbac:groups=egressgateway.kubernetes.azure.com,resources=staticgatewayconfigurations/finalizers,verbs=update //+kubebuilder:rbac:groups=egressgateway.kubernetes.azure.com,resources=gatewaylbconfigurations,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups=egressgateway.kubernetes.azure.com,resources=gatewaylbconfigurations/status,verbs=get;update;patch // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. func (r *StaticGatewayConfigurationReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { log := log.FromContext(ctx) // Fetch the StaticGatewayConfiguration instance. gwConfig := &egressgatewayv1alpha1.StaticGatewayConfiguration{} if err := r.Get(ctx, req.NamespacedName, gwConfig); err != nil { if apierrors.IsNotFound(err) { // Object not found, return. return ctrl.Result{}, nil } log.Error(err, "unable to fetch StaticGatewayConfiguration instance") return ctrl.Result{}, err } if !gwConfig.ObjectMeta.DeletionTimestamp.IsZero() { // Clean up staticGatewayConfiguration return ctrl.Result{}, r.ensureDeleted(ctx, gwConfig) } return ctrl.Result{}, r.reconcile(ctx, gwConfig) } // SetupWithManager sets up the controller with the Manager. func (r *StaticGatewayConfigurationReconciler) SetupWithManager(mgr ctrl.Manager) error { secretPredicate := predicate.Funcs{ CreateFunc: func(e event.CreateEvent) bool { // no need to trigger reconcile for secrets creation return false }, UpdateFunc: func(e event.UpdateEvent) bool { return strings.EqualFold(e.ObjectOld.GetNamespace(), r.SecretNamespace) }, DeleteFunc: func(e event.DeleteEvent) bool { return strings.EqualFold(e.Object.GetNamespace(), r.SecretNamespace) }, GenericFunc: func(e event.GenericEvent) bool { return false }, } return ctrl.NewControllerManagedBy(mgr). For(&egressgatewayv1alpha1.StaticGatewayConfiguration{}). Owns(&egressgatewayv1alpha1.GatewayLBConfiguration{}). // generated secrets created in the dedicated namespace Watches(&corev1.Secret{}, enqueueOwningSGCFromLabels(), builder.WithPredicates(secretPredicate)). Complete(r) } func enqueueOwningSGCFromLabels() handler.EventHandler { return handler.EnqueueRequestsFromMapFunc(func(_ context.Context, o client.Object) []reconcile.Request { labels := o.GetLabels() if labels == nil { return nil } owningSGCNamespace, foundNamespace := labels[consts.OwningSGCNamespaceLabel] owningSGCName, foundName := labels[consts.OwningSGCNameLabel] if !foundNamespace || !foundName { return nil } return []reconcile.Request{ { NamespacedName: client.ObjectKey{ Name: owningSGCName, Namespace: owningSGCNamespace, }, }, } }) } func (r *StaticGatewayConfigurationReconciler) reconcile( ctx context.Context, gwConfig *egressgatewayv1alpha1.StaticGatewayConfiguration, ) error { log := log.FromContext(ctx) log.Info(fmt.Sprintf("Reconciling staticGatewayConfiguration %s/%s", gwConfig.Namespace, gwConfig.Name)) mc := metrics.NewMetricsContext( os.Getenv(consts.PodNamespaceEnvKey), "reconcile_static_gateway_configuration", "n/a", "n/a", strings.ToLower(fmt.Sprintf("%s/%s", gwConfig.Namespace, gwConfig.Name)), ) // no subscription_id/resource_group for SGC reconciler succeeded := false defer func() { mc.ObserveControllerReconcileMetrics(succeeded) }() if err := validate(gwConfig); err != nil { r.Recorder.Event(gwConfig, corev1.EventTypeWarning, "InvalidSpec", err.Error()) return err } if !controllerutil.ContainsFinalizer(gwConfig, consts.SGCFinalizerName) { log.Info("Adding finalizer") controllerutil.AddFinalizer(gwConfig, consts.SGCFinalizerName) err := r.Update(ctx, gwConfig) if err != nil { log.Error(err, "failed to add finalizer") return err } } _, err := controllerutil.CreateOrPatch(ctx, r, gwConfig, func() error { // reconcile wireguard keypair if err := r.reconcileWireguardKey(ctx, gwConfig); err != nil { log.Error(err, "failed to reconcile wireguard key") r.Recorder.Event(gwConfig, corev1.EventTypeWarning, "ReconcileError", err.Error()) return err } // reconcile lbconfig if err := r.reconcileGatewayLBConfig(ctx, gwConfig); err != nil { log.Error(err, "failed to reconcile gateway LB configuration") r.Recorder.Event(gwConfig, corev1.EventTypeWarning, "ReconcileError", err.Error()) return err } return nil }) prefix, reconcileStatus := "<pending>", "Reconciling" if gwConfig.Status.EgressIpPrefix != "" { prefix, reconcileStatus = gwConfig.Status.EgressIpPrefix, "Reconciled" } r.Recorder.Eventf(gwConfig, corev1.EventTypeNormal, reconcileStatus, "StaticGatewayConfiguration provisioned with egress prefix %s", prefix) log.Info("staticGatewayConfiguration reconciled") succeeded = true return err } func (r *StaticGatewayConfigurationReconciler) ensureDeleted( ctx context.Context, gwConfig *egressgatewayv1alpha1.StaticGatewayConfiguration, ) error { log := log.FromContext(ctx) log.Info(fmt.Sprintf("Reconciling staticGatewayConfiguration deletion %s/%s", gwConfig.Namespace, gwConfig.Name)) if !controllerutil.ContainsFinalizer(gwConfig, consts.SGCFinalizerName) { log.Info("gwConfig does not have finalizer, no additional cleanup needed") return nil } mc := metrics.NewMetricsContext( os.Getenv(consts.PodNamespaceEnvKey), "delete_static_gateway_configuration", "n/a", "n/a", strings.ToLower(fmt.Sprintf("%s/%s", gwConfig.Namespace, gwConfig.Name)), ) // no subscription_id/resource_group for SGC reconciler succeeded := false defer func() { mc.ObserveControllerReconcileMetrics(succeeded) }() secretDeleted := false log.Info("Deleting wireguard key") secret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: fmt.Sprintf("sgw-%s", string(gwConfig.UID)), Namespace: r.SecretNamespace, }, } if err := r.Delete(ctx, secret); err != nil { if !apierrors.IsNotFound(err) { log.Error(err, "failed to delete existing gateway LB configuration") return err } else { secretDeleted = true } } lbConfigDeleted := false log.Info("Deleting gateway LB configuration") lbConfig := &egressgatewayv1alpha1.GatewayLBConfiguration{ ObjectMeta: metav1.ObjectMeta{ Name: gwConfig.Name, Namespace: gwConfig.Namespace, }, } if err := r.Delete(ctx, lbConfig); err != nil { if !apierrors.IsNotFound(err) { log.Error(err, "failed to delete existing gateway LB configuration") return err } else { lbConfigDeleted = true } } if secretDeleted && lbConfigDeleted { log.Info("Secret and LBConfig are deleted, removing finalizer") controllerutil.RemoveFinalizer(gwConfig, consts.SGCFinalizerName) if err := r.Update(ctx, gwConfig); err != nil { log.Error(err, "failed to remove finalizer") return err } } log.Info("staticGatewayConfiguration deletion reconciled") succeeded = true return nil } func validate(gwConfig *egressgatewayv1alpha1.StaticGatewayConfiguration) error { // need to validate either GatewayNodepoolName or GatewayVmssProfile is provided, but not both var allErrs field.ErrorList if gwConfig.Spec.GatewayNodepoolName == "" && vmssProfileIsEmpty(gwConfig) { allErrs = append(allErrs, field.Invalid(field.NewPath("spec").Child("gatewaynodepoolname"), fmt.Sprintf("GatewayNodepoolName: %s, GatewayVmssProfile: %#v", gwConfig.Spec.GatewayNodepoolName, gwConfig.Spec.GatewayVmssProfile), "Either GatewayNodepoolName or GatewayVmssProfile must be provided")) } if gwConfig.Spec.GatewayNodepoolName != "" && !vmssProfileIsEmpty(gwConfig) { allErrs = append(allErrs, field.Invalid(field.NewPath("spec").Child("gatewaynodepoolname"), fmt.Sprintf("GatewayNodepoolName: %s, GatewayVmssProfile: %#v", gwConfig.Spec.GatewayNodepoolName, gwConfig.Spec.GatewayVmssProfile), "Only one of GatewayNodepoolName and GatewayVmssProfile should be provided")) } if !vmssProfileIsEmpty(gwConfig) { if gwConfig.Spec.GatewayVmssProfile.VmssResourceGroup == "" { allErrs = append(allErrs, field.Invalid(field.NewPath("spec").Child("gatewayvmssprofile").Child("vmssresourcegroup"), gwConfig.Spec.GatewayVmssProfile.VmssResourceGroup, "Gateway vmss resource group is empty")) } if gwConfig.Spec.GatewayVmssProfile.VmssName == "" { allErrs = append(allErrs, field.Invalid(field.NewPath("spec").Child("gatewayvmssprofile").Child("vmssname"), gwConfig.Spec.GatewayVmssProfile.VmssName, "Gateway vmss name is empty")) } if gwConfig.Spec.GatewayVmssProfile.PublicIpPrefixSize < 0 || gwConfig.Spec.GatewayVmssProfile.PublicIpPrefixSize > 31 { allErrs = append(allErrs, field.Invalid(field.NewPath("spec").Child("gatewayvmssprofile").Child("publicipprefixsize"), gwConfig.Spec.GatewayVmssProfile.PublicIpPrefixSize, "Gateway vmss public ip prefix size should be between 0 and 31 inclusively")) } } if !gwConfig.Spec.ProvisionPublicIps && gwConfig.Spec.PublicIpPrefixId != "" { allErrs = append(allErrs, field.Invalid(field.NewPath("spec").Child("publicipprefixid"), gwConfig.Spec.PublicIpPrefixId, "PublicIpPrefixId should be empty when ProvisionPublicIps is false")) } if len(allErrs) == 0 { return nil } return apierrors.NewInvalid( schema.GroupKind{Group: "egressgateway.kubernetes.azure.com", Kind: "StaticGatewayConfiguration"}, gwConfig.Name, allErrs) } func vmssProfileIsEmpty(gwConfig *egressgatewayv1alpha1.StaticGatewayConfiguration) bool { return gwConfig.Spec.GatewayVmssProfile.VmssResourceGroup == "" && gwConfig.Spec.GatewayVmssProfile.VmssName == "" && gwConfig.Spec.GatewayVmssProfile.PublicIpPrefixSize == 0 } func (r *StaticGatewayConfigurationReconciler) reconcileWireguardKey( ctx context.Context, gwConfig *egressgatewayv1alpha1.StaticGatewayConfiguration, ) error { log := log.FromContext(ctx) secret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: fmt.Sprintf("sgw-%s", string(gwConfig.UID)), Namespace: r.SecretNamespace, }, } if _, err := controllerutil.CreateOrUpdate(ctx, r, secret, func() error { if secret.Labels == nil { secret.Labels = make(map[string]string) } if sgcNS, ok := secret.Labels[consts.OwningSGCNamespaceLabel]; !ok || sgcNS != gwConfig.Namespace { secret.Labels[consts.OwningSGCNamespaceLabel] = gwConfig.Namespace } if sgcName, ok := secret.Labels[consts.OwningSGCNameLabel]; !ok || sgcName != gwConfig.Name { secret.Labels[consts.OwningSGCNameLabel] = gwConfig.Name } if secret.Data == nil { secret.Data = make(map[string][]byte) } if _, ok := secret.Data[consts.WireguardPrivateKeyName]; !ok { // create new private key wgPrivateKey, err := wgtypes.GeneratePrivateKey() if err != nil { log.Error(err, "failed to generate wireguard private key") return err } secret.Data[consts.WireguardPrivateKeyName] = []byte(wgPrivateKey.String()) secret.Data[consts.WireguardPublicKeyName] = []byte(wgPrivateKey.PublicKey().String()) } return nil }); err != nil { log.Error(err, "failed to reconcile wireguard keypair secret") return err } if secret.DeletionTimestamp.IsZero() { // Update secret reference gwConfig.Status.PrivateKeySecretRef = &corev1.ObjectReference{ APIVersion: "v1", Kind: "Secret", Name: secret.Name, Namespace: secret.Namespace, } // Update public key gwConfig.Status.PublicKey = string(secret.Data[consts.WireguardPublicKeyName]) } return nil } func (r *StaticGatewayConfigurationReconciler) reconcileGatewayLBConfig( ctx context.Context, gwConfig *egressgatewayv1alpha1.StaticGatewayConfiguration, ) error { log := log.FromContext(ctx) // check existence of the gatewayLBConfig lbConfig := &egressgatewayv1alpha1.GatewayLBConfiguration{ ObjectMeta: metav1.ObjectMeta{ Name: gwConfig.Name, Namespace: gwConfig.Namespace, }, } if _, err := controllerutil.CreateOrPatch(ctx, r, lbConfig, func() error { lbConfig.Spec.GatewayNodepoolName = gwConfig.Spec.GatewayNodepoolName lbConfig.Spec.GatewayVmssProfile = gwConfig.Spec.GatewayVmssProfile lbConfig.Spec.ProvisionPublicIps = gwConfig.Spec.ProvisionPublicIps lbConfig.Spec.PublicIpPrefixId = gwConfig.Spec.PublicIpPrefixId return controllerutil.SetControllerReference(gwConfig, lbConfig, r.Client.Scheme()) }); err != nil { log.Error(err, "failed to reconcile gateway lb configuration") return err } if lbConfig.DeletionTimestamp.IsZero() && lbConfig.Status != nil { gwConfig.Status.Ip = lbConfig.Status.FrontendIp gwConfig.Status.Port = lbConfig.Status.ServerPort gwConfig.Status.EgressIpPrefix = lbConfig.Status.EgressIpPrefix } return nil }