@@ -26,6 +26,8 @@ import (
2626 corev1 "k8s.io/api/core/v1"
2727 "k8s.io/apimachinery/pkg/api/errors"
2828 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
29+ "k8s.io/apimachinery/pkg/fields"
30+ "k8s.io/apimachinery/pkg/labels"
2931 "k8s.io/apimachinery/pkg/types"
3032 "k8s.io/apimachinery/pkg/util/wait"
3133 "k8s.io/client-go/tools/record"
@@ -81,6 +83,41 @@ func NewWorkspaceController(c client.Client, recorder record.EventRecorder, node
8183 }, nil
8284}
8385
86+ type PodCountController struct {
87+ client.Client
88+ NodeName string
89+ }
90+
91+ // NewPodCountController creates a controller that tracks workspace pod counts and updates node annotations
92+ func NewPodCountController (client client.Client , nodeName string ) (* PodCountController , error ) {
93+ return & PodCountController {
94+ Client : client ,
95+ NodeName : nodeName ,
96+ }, nil
97+ }
98+
99+ func (pc * PodCountController ) SetupWithManager (mgr ctrl.Manager ) error {
100+ return ctrl .NewControllerManagedBy (mgr ).
101+ Named ("pod-count" ).
102+ For (& workspacev1.Workspace {}).
103+ WithEventFilter (podEventFilter (pc .NodeName )).
104+ Complete (pc )
105+ }
106+
107+ func podEventFilter (nodeName string ) predicate.Predicate {
108+ return predicate.Funcs {
109+ CreateFunc : func (e event.CreateEvent ) bool {
110+ return workspaceFilter (e .Object , nodeName )
111+ },
112+ UpdateFunc : func (e event.UpdateEvent ) bool {
113+ return workspaceFilter (e .ObjectNew , nodeName )
114+ },
115+ DeleteFunc : func (e event.DeleteEvent ) bool {
116+ return workspaceFilter (e .Object , nodeName )
117+ },
118+ }
119+ }
120+
84121// SetupWithManager sets up the controller with the Manager.
85122func (wsc * WorkspaceController ) SetupWithManager (mgr ctrl.Manager ) error {
86123 return ctrl .NewControllerManagedBy (mgr ).
@@ -146,6 +183,45 @@ func (wsc *WorkspaceController) Reconcile(ctx context.Context, req ctrl.Request)
146183 return ctrl.Result {}, nil
147184}
148185
186+ func (pc * PodCountController ) Reconcile (ctx context.Context , req ctrl.Request ) (ctrl.Result , error ) {
187+ var podList corev1.PodList
188+ err := pc .List (ctx , & podList , & client.ListOptions {
189+ FieldSelector : fields .SelectorFromSet (fields.Set {"spec.nodeName" : pc .NodeName }),
190+ LabelSelector : labels .SelectorFromSet (labels.Set {"component" : "workspace" }),
191+ })
192+ if err != nil {
193+ glog .WithError (err ).WithField ("nodeName" , pc .NodeName ).Error ("failed to list pods" )
194+ return ctrl.Result {}, err
195+ }
196+ workspaceCount := len (podList .Items )
197+
198+ err = retry .RetryOnConflict (retry .DefaultBackoff , func () error {
199+ var node corev1.Node
200+ err := pc .Get (ctx , types.NamespacedName {Name : pc .NodeName }, & node )
201+ if err != nil {
202+ return fmt .Errorf ("obtaining node %s: %w" , pc .NodeName , err )
203+ }
204+
205+ if node .Annotations == nil {
206+ node .Annotations = make (map [string ]string )
207+ }
208+
209+ if workspaceCount > 0 {
210+ node .Annotations ["cluster-autoscaler.kubernetes.io/scale-down-disabled" ] = "true"
211+ } else {
212+ delete (node .Annotations , "cluster-autoscaler.kubernetes.io/scale-down-disabled" )
213+ }
214+
215+ return pc .Update (ctx , & node )
216+ })
217+ if err != nil {
218+ glog .WithError (err ).WithField ("nodeName" , pc .NodeName ).Error ("[failed to update node" )
219+ return ctrl.Result {}, err
220+ }
221+
222+ return ctrl.Result {}, nil
223+ }
224+
149225// latestWorkspace checks if the we have the latest generation of the workspace CR. We do this because
150226// the cache could be stale and we retrieve a workspace CR that does not have the content init/backup
151227// conditions even though we have set them previously. This will lead to us performing these operations
0 commit comments