@@ -13,7 +13,8 @@ import (
1313 "sync"
1414 "time"
1515
16- "github.com/pkg/errors"
16+ "errors"
17+
1718 "go.uber.org/zap"
1819 xslices "golang.org/x/exp/slices"
1920 corev1 "k8s.io/api/core/v1"
@@ -58,6 +59,7 @@ type ConnectorReconciler struct {
5859
5960 subnetRouters set.Slice [types.UID ] // for subnet routers gauge
6061 exitNodes set.Slice [types.UID ] // for exit nodes gauge
62+ appConnectors set.Slice [types.UID ] // for app connectors gauge
6163}
6264
6365var (
6769 gaugeConnectorSubnetRouterResources = clientmetric .NewGauge (kubetypes .MetricConnectorWithSubnetRouterCount )
6870 // gaugeConnectorExitNodeResources tracks the number of Connectors currently managed by this operator instance that are exit nodes.
6971 gaugeConnectorExitNodeResources = clientmetric .NewGauge (kubetypes .MetricConnectorWithExitNodeCount )
72+ // gaugeConnectorAppConnectorResources tracks the number of Connectors currently managed by this operator instance that are app connectors.
73+ gaugeConnectorAppConnectorResources = clientmetric .NewGauge (kubetypes .MetricConnectorWithAppConnectorCount )
7074)
7175
7276func (a * ConnectorReconciler ) Reconcile (ctx context.Context , req reconcile.Request ) (res reconcile.Result , err error ) {
@@ -108,13 +112,12 @@ func (a *ConnectorReconciler) Reconcile(ctx context.Context, req reconcile.Reque
108112 oldCnStatus := cn .Status .DeepCopy ()
109113 setStatus := func (cn * tsapi.Connector , _ tsapi.ConditionType , status metav1.ConditionStatus , reason , message string ) (reconcile.Result , error ) {
110114 tsoperator .SetConnectorCondition (cn , tsapi .ConnectorReady , status , reason , message , cn .Generation , a .clock , logger )
115+ var updateErr error
111116 if ! apiequality .Semantic .DeepEqual (oldCnStatus , cn .Status ) {
112117 // An error encountered here should get returned by the Reconcile function.
113- if updateErr := a .Client .Status ().Update (ctx , cn ); updateErr != nil {
114- err = errors .Wrap (err , updateErr .Error ())
115- }
118+ updateErr = a .Client .Status ().Update (ctx , cn )
116119 }
117- return res , err
120+ return res , errors . Join ( err , updateErr )
118121 }
119122
120123 if ! slices .Contains (cn .Finalizers , FinalizerName ) {
@@ -150,6 +153,9 @@ func (a *ConnectorReconciler) Reconcile(ctx context.Context, req reconcile.Reque
150153 cn .Status .SubnetRoutes = cn .Spec .SubnetRouter .AdvertiseRoutes .Stringify ()
151154 return setStatus (cn , tsapi .ConnectorReady , metav1 .ConditionTrue , reasonConnectorCreated , reasonConnectorCreated )
152155 }
156+ if cn .Spec .AppConnector != nil {
157+ cn .Status .IsAppConnector = true
158+ }
153159 cn .Status .SubnetRoutes = ""
154160 return setStatus (cn , tsapi .ConnectorReady , metav1 .ConditionTrue , reasonConnectorCreated , reasonConnectorCreated )
155161}
@@ -189,23 +195,37 @@ func (a *ConnectorReconciler) maybeProvisionConnector(ctx context.Context, logge
189195 sts .Connector .routes = cn .Spec .SubnetRouter .AdvertiseRoutes .Stringify ()
190196 }
191197
198+ if cn .Spec .AppConnector != nil {
199+ sts .Connector .isAppConnector = true
200+ if len (cn .Spec .AppConnector .Routes ) != 0 {
201+ sts .Connector .routes = cn .Spec .AppConnector .Routes .Stringify ()
202+ }
203+ }
204+
192205 a .mu .Lock ()
193- if sts . Connector . isExitNode {
206+ if cn . Spec . ExitNode {
194207 a .exitNodes .Add (cn .UID )
195208 } else {
196209 a .exitNodes .Remove (cn .UID )
197210 }
198- if sts . Connector . routes != "" {
211+ if cn . Spec . SubnetRouter != nil {
199212 a .subnetRouters .Add (cn .GetUID ())
200213 } else {
201214 a .subnetRouters .Remove (cn .GetUID ())
202215 }
216+ if cn .Spec .AppConnector != nil {
217+ a .appConnectors .Add (cn .GetUID ())
218+ } else {
219+ a .appConnectors .Remove (cn .GetUID ())
220+ }
203221 a .mu .Unlock ()
204222 gaugeConnectorSubnetRouterResources .Set (int64 (a .subnetRouters .Len ()))
205223 gaugeConnectorExitNodeResources .Set (int64 (a .exitNodes .Len ()))
224+ gaugeConnectorAppConnectorResources .Set (int64 (a .appConnectors .Len ()))
206225 var connectors set.Slice [types.UID ]
207226 connectors .AddSlice (a .exitNodes .Slice ())
208227 connectors .AddSlice (a .subnetRouters .Slice ())
228+ connectors .AddSlice (a .appConnectors .Slice ())
209229 gaugeConnectorResources .Set (int64 (connectors .Len ()))
210230
211231 _ , err := a .ssr .Provision (ctx , logger , sts )
@@ -248,12 +268,15 @@ func (a *ConnectorReconciler) maybeCleanupConnector(ctx context.Context, logger
248268 a .mu .Lock ()
249269 a .subnetRouters .Remove (cn .UID )
250270 a .exitNodes .Remove (cn .UID )
271+ a .appConnectors .Remove (cn .UID )
251272 a .mu .Unlock ()
252273 gaugeConnectorExitNodeResources .Set (int64 (a .exitNodes .Len ()))
253274 gaugeConnectorSubnetRouterResources .Set (int64 (a .subnetRouters .Len ()))
275+ gaugeConnectorAppConnectorResources .Set (int64 (a .appConnectors .Len ()))
254276 var connectors set.Slice [types.UID ]
255277 connectors .AddSlice (a .exitNodes .Slice ())
256278 connectors .AddSlice (a .subnetRouters .Slice ())
279+ connectors .AddSlice (a .appConnectors .Slice ())
257280 gaugeConnectorResources .Set (int64 (connectors .Len ()))
258281 return true , nil
259282}
@@ -262,8 +285,14 @@ func (a *ConnectorReconciler) validate(cn *tsapi.Connector) error {
262285 // Connector fields are already validated at apply time with CEL validation
263286 // on custom resource fields. The checks here are a backup in case the
264287 // CEL validation breaks without us noticing.
265- if ! (cn .Spec .SubnetRouter != nil || cn .Spec .ExitNode ) {
266- return errors .New ("invalid spec: a Connector must expose subnet routes or act as an exit node (or both)" )
288+ if cn .Spec .SubnetRouter == nil && ! cn .Spec .ExitNode && cn .Spec .AppConnector == nil {
289+ return errors .New ("invalid spec: a Connector must be configured as at least one of subnet router, exit node or app connector" )
290+ }
291+ if (cn .Spec .SubnetRouter != nil || cn .Spec .ExitNode ) && cn .Spec .AppConnector != nil {
292+ return errors .New ("invalid spec: a Connector that is configured as an app connector must not be also configured as a subnet router or exit node" )
293+ }
294+ if cn .Spec .AppConnector != nil {
295+ return validateAppConnector (cn .Spec .AppConnector )
267296 }
268297 if cn .Spec .SubnetRouter == nil {
269298 return nil
@@ -272,19 +301,27 @@ func (a *ConnectorReconciler) validate(cn *tsapi.Connector) error {
272301}
273302
274303func validateSubnetRouter (sb * tsapi.SubnetRouter ) error {
275- if len (sb .AdvertiseRoutes ) < 1 {
304+ if len (sb .AdvertiseRoutes ) == 0 {
276305 return errors .New ("invalid subnet router spec: no routes defined" )
277306 }
278- var err error
279- for _ , route := range sb .AdvertiseRoutes {
307+ return validateRoutes (sb .AdvertiseRoutes )
308+ }
309+
310+ func validateAppConnector (ac * tsapi.AppConnector ) error {
311+ return validateRoutes (ac .Routes )
312+ }
313+
314+ func validateRoutes (routes tsapi.Routes ) error {
315+ var errs []error
316+ for _ , route := range routes {
280317 pfx , e := netip .ParsePrefix (string (route ))
281318 if e != nil {
282- err = errors . Wrap ( err , fmt .Sprintf ("route %s is invalid: %v" , route , err ))
319+ errs = append ( errs , fmt .Errorf ("route %v is invalid: %v" , route , e ))
283320 continue
284321 }
285322 if pfx .Masked () != pfx {
286- err = errors . Wrap ( err , fmt .Sprintf ("route %s has non-address bits set; expected %s" , pfx , pfx .Masked ()))
323+ errs = append ( errs , fmt .Errorf ("route %s has non-address bits set; expected %s" , pfx , pfx .Masked ()))
287324 }
288325 }
289- return err
326+ return errors . Join ( errs ... )
290327}
0 commit comments