-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathmain.go
421 lines (370 loc) · 12.5 KB
/
main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
package privilegeddaemonset
import (
"context"
"fmt"
"time"
appsv1 "k8s.io/api/apps/v1"
v1core "k8s.io/api/core/v1"
rbacv1 "k8s.io/api/rbac/v1"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/kubernetes"
pointer "k8s.io/utils/ptr"
)
const (
tolerationsPeriodSecs = 300
)
type DaemonSetClient struct {
K8sClient kubernetes.Interface
}
var daemonsetClient = DaemonSetClient{}
func SetDaemonSetClient(aK8sClient kubernetes.Interface) {
daemonsetClient.K8sClient = aK8sClient
}
const (
roleSaName = "privileged-ds"
waitingTime = 5 * time.Second
namespaceDeleteTimeout = time.Minute * 2
)
//nolint:funlen
func createDaemonSetsTemplate(dsName, namespace, containerName, imageWithVersion string, labelsMap map[string]string, env []v1core.EnvVar, cpuReq, cpuLim, memReq, memLim string) *appsv1.DaemonSet {
dsAnnotations := make(map[string]string)
dsAnnotations["debug.openshift.io/source-container"] = containerName
dsAnnotations["openshift.io/scc"] = "node-exporter"
matchLabels := make(map[string]string)
matchLabels["name"] = dsName
for key, value := range labelsMap {
matchLabels[key] = value
}
rootUser := pointer.To(int64(0))
container := v1core.Container{
Name: containerName,
Image: imageWithVersion,
ImagePullPolicy: "IfNotPresent",
Env: env,
SecurityContext: &v1core.SecurityContext{
Privileged: pointer.To(true),
RunAsUser: rootUser,
},
Stdin: true,
StdinOnce: true,
TerminationMessagePath: "/dev/termination-log",
TTY: true,
VolumeMounts: []v1core.VolumeMount{
{
MountPath: "/host",
Name: "host",
},
},
}
// setting CPU and memory request/limits
container.Resources.Requests = v1core.ResourceList{}
container.Resources.Limits = v1core.ResourceList{}
container.Resources.Requests[v1core.ResourceCPU] = resource.MustParse(cpuReq)
container.Resources.Limits[v1core.ResourceCPU] = resource.MustParse(cpuLim)
container.Resources.Requests[v1core.ResourceMemory] = resource.MustParse(memReq)
container.Resources.Limits[v1core.ResourceMemory] = resource.MustParse(memLim)
preemptPolicyLowPrio := v1core.PreemptLowerPriority
hostPathTypeDir := v1core.HostPathDirectory
tolerationsSeconds := pointer.To(int64(tolerationsPeriodSecs))
return &appsv1.DaemonSet{
ObjectMeta: metav1.ObjectMeta{
Name: dsName,
Namespace: namespace,
Annotations: dsAnnotations,
},
Spec: appsv1.DaemonSetSpec{
Selector: &metav1.LabelSelector{
MatchLabels: matchLabels,
},
Template: v1core.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: matchLabels,
},
Spec: v1core.PodSpec{
ServiceAccountName: roleSaName,
Containers: []v1core.Container{container},
PreemptionPolicy: &preemptPolicyLowPrio,
Priority: pointer.To(int32(0)),
HostNetwork: true,
HostIPC: true,
HostPID: true,
Tolerations: []v1core.Toleration{
{
Effect: "NoExecute",
Key: "node.kubernetes.io/not-ready",
Operator: "Exists",
TolerationSeconds: tolerationsSeconds,
},
{
Effect: "NoExecute",
Key: "node.kubernetes.io/unreachable",
Operator: "Exists",
TolerationSeconds: tolerationsSeconds,
},
{
Effect: "NoSchedule",
Key: "node-role.kubernetes.io/master",
},
{
Effect: "NoSchedule",
Key: "node-role.kubernetes.io/control-plane",
},
},
Volumes: []v1core.Volume{
{
Name: "host",
VolumeSource: v1core.VolumeSource{
HostPath: &v1core.HostPathVolumeSource{
Path: "/",
Type: &hostPathTypeDir,
},
},
},
},
},
},
},
}
}
// This method is used to delete a daemonset specified by the name at a specified namespace
func DeleteDaemonSet(daemonSetName, namespace string) error {
const (
Timeout = 5 * time.Minute
)
deletePolicy := metav1.DeletePropagationForeground
err := daemonsetClient.K8sClient.AppsV1().DaemonSets(namespace).Delete(context.TODO(), daemonSetName, metav1.DeleteOptions{PropagationPolicy: &deletePolicy})
if err != nil {
return fmt.Errorf("daemonset %q deletion failed, err: %v", daemonSetName, err)
}
dsDeleted := false
start := time.Now()
for time.Since(start) < Timeout {
if !doesDaemonSetExist(daemonSetName, namespace) {
dsDeleted = true
break
}
time.Sleep(waitingTime)
}
if !dsDeleted {
return fmt.Errorf("timeout waiting for daemonset %q to be deleted", daemonSetName)
}
return nil
}
// Check if the daemonset exists
func doesDaemonSetExist(daemonSetName, namespace string) bool {
_, err := daemonsetClient.K8sClient.AppsV1().DaemonSets(namespace).Get(context.TODO(), daemonSetName, metav1.GetOptions{})
return err == nil
}
func IsDaemonSetReady(daemonSetName, namespace, image string) bool {
const hoursPerWeek = 168 // 7 days
// The daemonset will be considered not ready if it does not exist
ds, err := daemonsetClient.K8sClient.AppsV1().DaemonSets(namespace).Get(context.TODO(), daemonSetName, metav1.GetOptions{})
if err != nil {
return false
}
// Or if it's been running for more than a week
if time.Since(ds.CreationTimestamp.Time).Hours() > hoursPerWeek {
return false
}
// Or if the container image do not match the desired one
if ds.Spec.Template.Spec.Containers[0].Image != image {
return false
}
// Or if it's not healthy
return isDaemonSetReady(&ds.Status)
}
// This function is used to create a daemonset with the specified name, namespace, container name and image with the timeout to check
// if the deployment is ready and all daemonset pods are running fine
func CreateDaemonSet(daemonSetName, namespace, containerName, imageWithVersion string, labels map[string]string, env []v1core.EnvVar, timeout time.Duration, cpuReq, cpuLim, memReq, memLim string) (aPodList *v1core.PodList, err error) {
// first, initialize the namespace
err = initNamespace(namespace)
if err != nil {
return aPodList, fmt.Errorf("failed to initialize the privileged daemonset namespace, err: %v", err)
}
daemonSet := createDaemonSetsTemplate(daemonSetName, namespace, containerName, imageWithVersion, labels, env, cpuReq, cpuLim, memReq, memLim)
if doesDaemonSetExist(daemonSetName, namespace) {
err = DeleteDaemonSet(daemonSetName, namespace)
if err != nil {
return aPodList, fmt.Errorf("failed to delete daemonset %q, err: %v", daemonSetName, err)
}
}
_, err = daemonsetClient.K8sClient.AppsV1().DaemonSets(namespace).Create(context.TODO(), daemonSet, metav1.CreateOptions{})
if err != nil {
return aPodList, err
}
err = WaitDaemonsetReady(namespace, daemonSetName, timeout)
if err != nil {
return aPodList, err
}
aPodList, err = daemonsetClient.K8sClient.CoreV1().Pods(namespace).List(context.TODO(), metav1.ListOptions{LabelSelector: "name=" + daemonSetName})
if err != nil {
return aPodList, err
}
return aPodList, nil
}
// This function is used to wait until daemonset is ready
func WaitDaemonsetReady(namespace, name string, timeout time.Duration) error {
nodes, err := daemonsetClient.K8sClient.CoreV1().Nodes().List(context.TODO(), metav1.ListOptions{})
if err != nil {
return fmt.Errorf("failed to get node list, err:%s", err)
}
nodesCount := int32(len(nodes.Items))
isReady := false
for start := time.Now(); !isReady && time.Since(start) < timeout; {
daemonSet, err := daemonsetClient.K8sClient.AppsV1().DaemonSets(namespace).Get(context.Background(), name, metav1.GetOptions{})
if err != nil {
return fmt.Errorf("failed to get daemonset %q (ns %q), err: %v", name, namespace, err)
}
if daemonSet.Status.DesiredNumberScheduled == nodesCount {
if isDaemonSetReady(&daemonSet.Status) {
isReady = true
break
}
}
time.Sleep(waitingTime)
}
if !isReady {
return fmt.Errorf("daemonset %q (ns %q) could not be deployed (timed out)", name, namespace)
}
return nil
}
func isDaemonSetReady(status *appsv1.DaemonSetStatus) bool {
//nolint:gocritic
return status.DesiredNumberScheduled == status.CurrentNumberScheduled &&
status.DesiredNumberScheduled == status.NumberAvailable &&
status.DesiredNumberScheduled == status.NumberReady &&
status.NumberMisscheduled == 0
}
//nolint:funlen
func ConfigurePrivilegedServiceAccount(namespace string) error {
aRole := rbacv1.Role{
TypeMeta: metav1.TypeMeta{
Kind: "Role",
APIVersion: "v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: roleSaName,
Namespace: namespace,
},
Rules: []rbacv1.PolicyRule{{
APIGroups: []string{"security.openshift.io"},
Resources: []string{"securitycontextconstraints"},
ResourceNames: []string{"privileged"},
Verbs: []string{"use"},
},
},
}
aRoleBinding := rbacv1.RoleBinding{
TypeMeta: metav1.TypeMeta{
Kind: "RoleBinding",
APIVersion: "v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: roleSaName,
Namespace: namespace,
},
Subjects: []rbacv1.Subject{{
Kind: "ServiceAccount",
Name: roleSaName,
Namespace: namespace,
}},
RoleRef: rbacv1.RoleRef{
Kind: "Role",
Name: roleSaName,
APIGroup: "rbac.authorization.k8s.io",
},
}
aServiceAccount := v1core.ServiceAccount{
TypeMeta: metav1.TypeMeta{
Kind: "ServiceAccount",
APIVersion: "v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: roleSaName,
Namespace: namespace,
},
}
// create role
_, err := daemonsetClient.K8sClient.RbacV1().Roles(namespace).Create(context.TODO(), &aRole, metav1.CreateOptions{})
if err != nil {
return fmt.Errorf("error creating role, err=%s", err)
}
// create rolebinding
_, err = daemonsetClient.K8sClient.RbacV1().RoleBindings(namespace).Create(context.TODO(), &aRoleBinding, metav1.CreateOptions{})
if err != nil {
return fmt.Errorf("error creating role bindings, err=%s", err)
}
// create service account
_, err = daemonsetClient.K8sClient.CoreV1().ServiceAccounts(namespace).Create(context.TODO(), &aServiceAccount, metav1.CreateOptions{})
if err != nil {
return fmt.Errorf("error creating service account, err=%s", err)
}
return nil
}
func initNamespace(namespace string) (err error) {
err =
DeleteNamespaceIfPresent(namespace)
if err != nil {
return fmt.Errorf("could not delete (if present) namespace=%s, err=%s", namespace, err)
}
// create namespace
err = namespaceCreate(namespace)
if err != nil {
return fmt.Errorf("could not create namespace=%s, err=%s", namespace, err)
}
// create service account
err = ConfigurePrivilegedServiceAccount(namespace)
if err != nil {
return fmt.Errorf("could not configure privileged rights, err=%s", err)
}
return nil
}
// WaitForCondition waits until the pod will have specified condition type with the expected status
func namespaceIsPresent(namespace string) bool {
_, err := daemonsetClient.K8sClient.CoreV1().Namespaces().Get(context.Background(), namespace, metav1.GetOptions{})
return err == nil
}
// WaitForDeletion waits until the namespace will be removed from the cluster
func namespaceWaitForDeletion(nsName string, timeout time.Duration) error {
//nolint:revive
return wait.PollUntilContextTimeout(context.TODO(), time.Second, timeout, true, func(ctx context.Context) (bool, error) {
_, err := daemonsetClient.K8sClient.CoreV1().Namespaces().Get(context.Background(), nsName, metav1.GetOptions{})
if k8serrors.IsNotFound(err) {
return true, nil
}
return false, nil
})
}
// Create creates a new namespace with the given name.
// If the namespace exists, it returns.
func namespaceCreate(namespace string) error {
_, err := daemonsetClient.K8sClient.CoreV1().Namespaces().Create(context.Background(), &v1core.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: namespace,
}},
metav1.CreateOptions{},
)
if k8serrors.IsAlreadyExists(err) {
return nil
}
return err
}
func DeleteNamespaceIfPresent(namespace string) (err error) {
// delete namespace if present
if !namespaceIsPresent(namespace) {
return nil
}
err = daemonsetClient.K8sClient.CoreV1().Namespaces().Delete(context.Background(), namespace, metav1.DeleteOptions{})
if err != nil {
return fmt.Errorf("could not delete namespace %q, err: %v", namespace, err)
}
// wait for the namespace to be deleted
err = namespaceWaitForDeletion(namespace, namespaceDeleteTimeout)
if err != nil {
return fmt.Errorf("failed waiting for namespace %q to be deleted, err: %v", namespace, err)
}
return nil
}