Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
gitpod-io
GitHub Repository: gitpod-io/gitpod
Path: blob/main/components/node-labeler/cmd/run_test.go
2498 views
1
// Copyright (c) 2025 Gitpod GmbH. All rights reserved.
2
// Licensed under the GNU Affero General Public License (AGPL).
3
// See License.AGPL.txt in the project root for license information.
4
5
package cmd
6
7
import (
8
"context"
9
"path/filepath"
10
"testing"
11
"time"
12
13
"github.com/aws/smithy-go/ptr"
14
"github.com/golang/mock/gomock"
15
"github.com/google/uuid"
16
. "github.com/onsi/ginkgo/v2"
17
. "github.com/onsi/gomega"
18
"google.golang.org/protobuf/proto"
19
corev1 "k8s.io/api/core/v1"
20
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
21
"k8s.io/apimachinery/pkg/types"
22
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
23
ctrl "sigs.k8s.io/controller-runtime"
24
"sigs.k8s.io/controller-runtime/pkg/client"
25
"sigs.k8s.io/controller-runtime/pkg/envtest"
26
logf "sigs.k8s.io/controller-runtime/pkg/log"
27
"sigs.k8s.io/controller-runtime/pkg/log/zap"
28
29
"github.com/gitpod-io/gitpod/common-go/util"
30
csapi "github.com/gitpod-io/gitpod/content-service/api"
31
workspacev1 "github.com/gitpod-io/gitpod/ws-manager/api/crd/v1"
32
)
33
34
const (
35
timeout = time.Second * 10
36
duration = time.Second * 2
37
interval = time.Millisecond * 250
38
workspaceNamespace = "default"
39
secretsNamespace = "workspace-secrets"
40
)
41
42
var (
43
k8sClient client.Client
44
testEnv *envtest.Environment
45
mock_ctrl *gomock.Controller
46
ctx context.Context
47
cancel context.CancelFunc
48
nodeScaledownCtrl *NodeScaledownAnnotationController
49
NodeName = "cool-ws-node"
50
)
51
52
func TestAPIs(t *testing.T) {
53
mock_ctrl = gomock.NewController(t)
54
RegisterFailHandler(Fail)
55
RunSpecs(t, "Controller Suite")
56
}
57
58
var _ = Describe("NodeScaledownAnnotationController", func() {
59
It("should remove scale-down-disabled when last workspace is removed", func() {
60
ws1 := newWorkspace(uuid.NewString(), workspaceNamespace, NodeName, workspacev1.WorkspacePhaseRunning)
61
ws2 := newWorkspace(uuid.NewString(), workspaceNamespace, NodeName, workspacev1.WorkspacePhaseRunning)
62
ws1.Status.Runtime = nil
63
ws2.Status.Runtime = nil
64
createWorkspace(ws1)
65
createWorkspace(ws2)
66
67
By("Assigning nodes to workspaces")
68
updateObjWithRetries(k8sClient, ws1, true, func(ws *workspacev1.Workspace) {
69
ws.Status.Conditions = []metav1.Condition{}
70
ws.Status.Runtime = &workspacev1.WorkspaceRuntimeStatus{
71
NodeName: NodeName,
72
}
73
})
74
75
updateObjWithRetries(k8sClient, ws2, true, func(ws *workspacev1.Workspace) {
76
ws.Status.Conditions = []metav1.Condition{}
77
ws.Status.Runtime = &workspacev1.WorkspaceRuntimeStatus{
78
NodeName: NodeName,
79
}
80
})
81
82
By("Verifying node annotation")
83
Eventually(func(g Gomega) {
84
var node corev1.Node
85
g.Expect(k8sClient.Get(ctx, types.NamespacedName{Name: NodeName}, &node)).To(Succeed())
86
g.Expect(node.Annotations).To(HaveKeyWithValue("cluster-autoscaler.kubernetes.io/scale-down-disabled", "true"))
87
}, timeout, interval).Should(Succeed())
88
89
By("Deleting workspaces")
90
Expect(k8sClient.Delete(ctx, ws1)).To(Succeed())
91
Expect(k8sClient.Delete(ctx, ws2)).To(Succeed())
92
93
By("Verifying final state")
94
Eventually(func(g Gomega) {
95
var node corev1.Node
96
g.Expect(k8sClient.Get(ctx, types.NamespacedName{Name: NodeName}, &node)).To(Succeed())
97
g.Expect(node.Annotations).ToNot(HaveKey("cluster-autoscaler.kubernetes.io/scale-down-disabled"))
98
}, timeout, interval).Should(Succeed())
99
})
100
})
101
102
var _ = BeforeSuite(func() {
103
logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))
104
105
crdPath := filepath.Join("..", "crd")
106
if !util.InLeewayBuild() {
107
crdPath = filepath.Join("..", "..", "ws-manager-mk2", "config", "crd", "bases")
108
}
109
110
By("bootstrapping test environment")
111
testEnv = &envtest.Environment{
112
ControlPlaneStartTimeout: 1 * time.Minute,
113
ControlPlaneStopTimeout: 1 * time.Minute,
114
CRDDirectoryPaths: []string{crdPath},
115
ErrorIfCRDPathMissing: true,
116
}
117
118
cfg, err := testEnv.Start()
119
Expect(err).NotTo(HaveOccurred())
120
Expect(cfg).NotTo(BeNil())
121
122
err = workspacev1.AddToScheme(clientgoscheme.Scheme)
123
Expect(err).NotTo(HaveOccurred())
124
125
//+kubebuilder:scaffold:scheme
126
127
k8sClient, err = client.New(cfg, client.Options{Scheme: clientgoscheme.Scheme})
128
Expect(err).NotTo(HaveOccurred())
129
Expect(k8sClient).NotTo(BeNil())
130
131
k8sManager, err := ctrl.NewManager(cfg, ctrl.Options{
132
Scheme: clientgoscheme.Scheme,
133
})
134
Expect(err).ToNot(HaveOccurred())
135
ctx, cancel = context.WithCancel(context.Background())
136
137
By("Creating default ws node")
138
node := &corev1.Node{
139
ObjectMeta: metav1.ObjectMeta{
140
Name: NodeName,
141
},
142
}
143
Expect(k8sClient.Create(ctx, node)).To(Succeed())
144
145
err = k8sManager.GetFieldIndexer().IndexField(context.Background(),
146
&workspacev1.Workspace{},
147
"status.runtime.nodeName",
148
func(o client.Object) []string {
149
ws := o.(*workspacev1.Workspace)
150
if ws.Status.Runtime == nil {
151
return nil
152
}
153
return []string{ws.Status.Runtime.NodeName}
154
})
155
Expect(err).ToNot(HaveOccurred())
156
157
By("Setting up controllers")
158
nodeScaledownCtrl, err = NewNodeScaledownAnnotationController(k8sManager.GetClient())
159
Expect(err).NotTo(HaveOccurred())
160
Expect(nodeScaledownCtrl.SetupWithManager(k8sManager)).To(Succeed())
161
162
_ = createNamespace(secretsNamespace)
163
164
By("Starting the manager")
165
go func() {
166
defer GinkgoRecover()
167
err = k8sManager.Start(ctx)
168
Expect(err).ToNot(HaveOccurred(), "failed to run manager")
169
}()
170
171
By("Waiting for controllers to be ready")
172
DeferCleanup(cancel)
173
174
// Wait for controllers to be ready
175
Eventually(func() bool {
176
return k8sManager.GetCache().WaitForCacheSync(ctx)
177
}, time.Second*10, time.Millisecond*100).Should(BeTrue())
178
})
179
180
var _ = AfterSuite(func() {
181
if cancel != nil {
182
cancel()
183
}
184
By("tearing down the test environment")
185
err := testEnv.Stop()
186
Expect(err).NotTo(HaveOccurred())
187
})
188
189
func newWorkspace(name, namespace, nodeName string, phase workspacev1.WorkspacePhase) *workspacev1.Workspace {
190
GinkgoHelper()
191
initializer := &csapi.WorkspaceInitializer{
192
Spec: &csapi.WorkspaceInitializer_Empty{Empty: &csapi.EmptyInitializer{}},
193
}
194
initializerBytes, err := proto.Marshal(initializer)
195
Expect(err).ToNot(HaveOccurred())
196
197
return &workspacev1.Workspace{
198
Status: workspacev1.WorkspaceStatus{
199
Phase: phase,
200
Runtime: &workspacev1.WorkspaceRuntimeStatus{
201
NodeName: nodeName,
202
},
203
Conditions: []metav1.Condition{},
204
},
205
TypeMeta: metav1.TypeMeta{
206
APIVersion: "workspace.gitpod.io/v1",
207
Kind: "Workspace",
208
},
209
ObjectMeta: metav1.ObjectMeta{
210
Name: name,
211
Namespace: namespace,
212
},
213
Spec: workspacev1.WorkspaceSpec{
214
Ownership: workspacev1.Ownership{
215
Owner: "foobar",
216
WorkspaceID: "cool-workspace",
217
},
218
Type: workspacev1.WorkspaceTypeRegular,
219
Class: "default",
220
Image: workspacev1.WorkspaceImages{
221
Workspace: workspacev1.WorkspaceImage{
222
Ref: ptr.String("alpine:latest"),
223
},
224
IDE: workspacev1.IDEImages{
225
Refs: []string{},
226
},
227
},
228
Ports: []workspacev1.PortSpec{},
229
Initializer: initializerBytes,
230
Admission: workspacev1.AdmissionSpec{
231
Level: workspacev1.AdmissionLevelEveryone,
232
},
233
},
234
}
235
}
236
237
func createWorkspace(ws *workspacev1.Workspace) {
238
GinkgoHelper()
239
By("creating workspace")
240
Expect(k8sClient.Create(ctx, ws)).To(Succeed())
241
}
242
243
func updateObjWithRetries[O client.Object](c client.Client, obj O, updateStatus bool, update func(obj O)) {
244
GinkgoHelper()
245
Eventually(func() error {
246
var err error
247
if err = c.Get(ctx, types.NamespacedName{Name: obj.GetName(), Namespace: obj.GetNamespace()}, obj); err != nil {
248
return err
249
}
250
// Apply update.
251
update(obj)
252
if updateStatus {
253
err = c.Status().Update(ctx, obj)
254
} else {
255
err = c.Update(ctx, obj)
256
}
257
return err
258
}, timeout, interval).Should(Succeed())
259
}
260
261
func createNamespace(name string) *corev1.Namespace {
262
GinkgoHelper()
263
264
namespace := &corev1.Namespace{
265
ObjectMeta: metav1.ObjectMeta{
266
Name: name,
267
},
268
}
269
270
Expect(k8sClient.Create(ctx, namespace)).To(Succeed())
271
return namespace
272
}
273
274