package wireguardctl import ( "context" "time" corev1 "k8s.io/api/core/v1" 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/predicate" "sigs.k8s.io/controller-runtime/pkg/reconcile" "edge-infra.dev/pkg/edge/api/types" v1cluster "edge-infra.dev/pkg/edge/apis/cluster/v1alpha1" "edge-infra.dev/pkg/k8s/meta/status" "edge-infra.dev/pkg/k8s/runtime/conditions" ctrlreconcile "edge-infra.dev/pkg/k8s/runtime/controller/reconcile" "edge-infra.dev/pkg/k8s/runtime/patch" "edge-infra.dev/pkg/lib/gcp/secretmanager" "edge-infra.dev/pkg/sds/remoteaccess/constants" v1vpnconfig "edge-infra.dev/pkg/sds/remoteaccess/k8s/apis/vpnconfigs/v1" "edge-infra.dev/pkg/sds/remoteaccess/k8s/controllers/wireguardctl/vpnconfig" "edge-infra.dev/pkg/sds/remoteaccess/wireguard/vpn" ) type VPNController struct { VPN *vpn.VPN Client client.Client Name string RequeueTime time.Duration } func NewVPNController(mgr ctrl.Manager, vpn *vpn.VPN) VPNController { return VPNController{ VPN: vpn, Client: mgr.GetClient(), Name: constants.WireguardControllerName, RequeueTime: requeueTime, } } // VPNConfigConditions defines the VPNConfig status conditions that // should be considered when determining overall readiness via the Ready condition. var VPNConfigConditions = ctrlreconcile.Conditions{ Target: status.ReadyCondition, Owned: []string{ v1vpnconfig.VPNConfigured, status.ReconcilingCondition, }, Summarize: []string{ v1vpnconfig.VPNConfigured, status.ReconcilingCondition, }, NegativePolarity: []string{ status.ReconcilingCondition, }, } func (c *VPNController) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&v1vpnconfig.VPNConfig{}, builder.WithPredicates(predicate.GenerationChangedPredicate{})). // only reconcile changes to spec WithEventFilter(createEventFilter()). Watches( // reconcile all VPNConfig objects on changes to the vpn/wireguard-relay service &corev1.Service{}, handler.EnqueueRequestsFromMapFunc(c.createVPNConfigReconcileRequests), builder.WithPredicates(isRelayServicePredicate()), ). Watches( // reconcile all VPNConfig objects on changes to the vpn/vpn-cidr configmap &corev1.ConfigMap{}, handler.EnqueueRequestsFromMapFunc(c.createVPNConfigReconcileRequests), builder.WithPredicates(isVPNConfigMapPredicate()), ). Complete(c) } func createEventFilter() predicate.Predicate { return predicate.Funcs{ CreateFunc: func(_ event.CreateEvent) bool { return true }, UpdateFunc: func(_ event.UpdateEvent) bool { return true }, DeleteFunc: func(e event.DeleteEvent) bool { // for the relay service we reconcile deletes so we can update VPNConfig status if _, ok := e.Object.(*corev1.Service); ok { return true } // we do not reconcile delete events fo the VPNConfig as the finalizer results in deletions becoming updates to DeletionTimestamp return false }, } } func (c *VPNController) createVPNConfigReconcileRequests(ctx context.Context, _ client.Object) []reconcile.Request { vpnConfigs := &v1vpnconfig.VPNConfigList{} if err := c.Client.List(ctx, vpnConfigs, &client.ListOptions{Namespace: constants.VPNNamespace}); err != nil { return nil } reconcileRequests := []reconcile.Request{} for _, vpnConfig := range vpnConfigs.Items { reconcileRequests = append(reconcileRequests, reconcile.Request{ NamespacedName: client.ObjectKey{ Namespace: vpnConfig.GetNamespace(), Name: vpnConfig.GetName(), }, }) } return reconcileRequests } func isRelayServicePredicate() predicate.Funcs { return predicate.NewPredicateFuncs(func(object client.Object) bool { return object.GetNamespace() == constants.VPNNamespace && object.GetName() == constants.RelayName }) } func isVPNConfigMapPredicate() predicate.Funcs { return predicate.NewPredicateFuncs(func(object client.Object) bool { return object.GetNamespace() == constants.VPNNamespace && object.GetName() == constants.VPNConfigMapName }) } func (c *VPNController) Reconcile(ctx context.Context, req ctrl.Request) (recResult ctrl.Result, recErr error) { log := ctrl.LoggerFrom(ctx).WithName(c.Name) var ( result = ctrlreconcile.ResultEmpty err error ) // get the VPNConfig object to be reconciled vpnConfig := &v1vpnconfig.VPNConfig{} if err := c.Client.Get(ctx, req.NamespacedName, vpnConfig); err != nil { log.Error(err, "unable to fetch VPNConfig") return ctrl.Result{RequeueAfter: c.RequeueTime}, err } serialPatcher := patch.NewSerialPatcher(vpnConfig, c.Client) defer func() { recResult, recErr = c.summarizeAndPatch(ctx, vpnConfig, serialPatcher, result, err) if result == ctrlreconcile.ResultRequeue { recResult = ctrl.Result{RequeueAfter: c.RequeueTime} } }() if err = ctrlreconcile.Progressing(ctx, vpnConfig, serialPatcher); err != nil { return } // get the cluster CR cluster := &v1cluster.Cluster{} if err = c.Client.Get(ctx, client.ObjectKey{Name: vpnConfig.GetName()}, cluster); err != nil { log.Error(err, "unable to fetch cluster") return } // ensure finalizer is present so controller can handle clean-up on object deletion if controllerutil.AddFinalizer(vpnConfig, v1vpnconfig.Finalizer) { result = ctrlreconcile.ResultRequeue return } // add cluster as ownerReference on VPNConfig vpnconfig.AddOwnerReference(vpnConfig, cluster) // create secret manager for project sm, err := secretmanager.NewWithOptions(ctx, cluster.Spec.ProjectID) if err != nil { log.Error(err, "unable to create secret manager", "projectID", cluster.Spec.ProjectID) return } // check if object is being updated or removed if vpnConfig.ObjectMeta.DeletionTimestamp.IsZero() { err = vpnconfig.Update(ctx, c.Client, sm, c.VPN, vpnConfig, cluster) } else { err = c.remove(ctx, sm, vpnConfig, cluster) } if err != nil { log.Error(err, "unable to update VPNConfig") conditions.MarkFalse(vpnConfig, v1vpnconfig.VPNConfigured, v1vpnconfig.VPNConfigFailed, "%v", err) return } conditions.Delete(vpnConfig, status.ReconcilingCondition) conditions.MarkTrue(vpnConfig, v1vpnconfig.VPNConfigured, v1vpnconfig.VPNConfigSuccessful, "successfully configured vpn") result = ctrlreconcile.ResultSuccess return } func (c *VPNController) remove(ctx context.Context, sm types.SecretManagerService, vpnConfig *v1vpnconfig.VPNConfig, cluster *v1cluster.Cluster) error { if !controllerutil.ContainsFinalizer(vpnConfig, v1vpnconfig.Finalizer) { return nil } controllerutil.RemoveFinalizer(vpnConfig, v1vpnconfig.Finalizer) return vpnconfig.Remove(ctx, c.Client, sm, c.VPN, vpnConfig, cluster) } func (c *VPNController) summarizeAndPatch(ctx context.Context, vpnConfig *v1vpnconfig.VPNConfig, serialPatcher *patch.SerialPatcher, result ctrlreconcile.Result, err error) (ctrl.Result, error) { s := ctrlreconcile.NewSummarizer(serialPatcher) return s.SummarizeAndPatch( ctx, vpnConfig, ctrlreconcile.WithResult(result), ctrlreconcile.WithError(err), ctrlreconcile.WithIgnoreNotFound(), ctrlreconcile.WithFieldOwner(c.Name), ctrlreconcile.WithConditions(VPNConfigConditions), ctrlreconcile.WithProcessors( ctrlreconcile.RecordReconcileReq, ctrlreconcile.RecordResult, ), ) }