@@ -26,6 +26,8 @@ import (
26
26
corev1 "k8s.io/api/core/v1"
27
27
"k8s.io/apimachinery/pkg/api/errors"
28
28
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
29
+ "k8s.io/apimachinery/pkg/fields"
30
+ "k8s.io/apimachinery/pkg/labels"
29
31
"k8s.io/apimachinery/pkg/types"
30
32
"k8s.io/apimachinery/pkg/util/wait"
31
33
"k8s.io/client-go/tools/record"
@@ -81,6 +83,41 @@ func NewWorkspaceController(c client.Client, recorder record.EventRecorder, node
81
83
}, nil
82
84
}
83
85
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
+
84
121
// SetupWithManager sets up the controller with the Manager.
85
122
func (wsc * WorkspaceController ) SetupWithManager (mgr ctrl.Manager ) error {
86
123
return ctrl .NewControllerManagedBy (mgr ).
@@ -146,6 +183,45 @@ func (wsc *WorkspaceController) Reconcile(ctx context.Context, req ctrl.Request)
146
183
return ctrl.Result {}, nil
147
184
}
148
185
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
+
149
225
// latestWorkspace checks if the we have the latest generation of the workspace CR. We do this because
150
226
// the cache could be stale and we retrieve a workspace CR that does not have the content init/backup
151
227
// conditions even though we have set them previously. This will lead to us performing these operations
0 commit comments