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
}