Skip to content

Commit fc518cb

Browse files
committed
Merge branch '3810-cordon-control'
2 parents 51da22c + f800f64 commit fc518cb

File tree

27 files changed

+4456
-3
lines changed

27 files changed

+4456
-3
lines changed

examples/k8s/cluster-role.yaml

+10-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ rules:
1515
- pods/log
1616
- replicationcontrollers
1717
- services
18-
- nodes
1918
- namespaces
2019
- persistentvolumes
2120
- persistentvolumeclaims
@@ -95,3 +94,13 @@ rules:
9594
verbs:
9695
- list
9796
- watch
97+
- apiGroups:
98+
- ""
99+
resources:
100+
- nodes
101+
verbs:
102+
- get
103+
- list
104+
- watch
105+
- update
106+
- patch

go.sum

+7
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
camlistore.org v0.0.0-20171230002226-a5a65f0d8b22 h1:VP9VuyosMHmS9zdzd5Co9TJKWPbMTfmtKc/XWctszyQ=
22
camlistore.org v0.0.0-20171230002226-a5a65f0d8b22/go.mod h1:mzAP6ICVzPdfO0f3N9hAVWhO7qplHF7mbFhGsGdErTI=
3+
cloud.google.com/go v0.26.0 h1:e0WKqKTd5BnrG8aKH3J3h+QvEIQtSUcf2n5UZ5ZgLtQ=
34
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
45
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8=
56
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
67
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
8+
github.com/DataDog/datadog-go v2.2.0+incompatible h1:V5BKkxACZLjzHjSgBbr2gvLA2Ae49yhc6CSY7MLy5k4=
79
github.com/DataDog/datadog-go v2.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
810
github.com/Microsoft/go-winio v0.4.11 h1:zoIOcVf0xPN1tnMVbTtEdI+P8OofVk3NObnwOQ6nK2Q=
911
github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=
@@ -39,7 +41,9 @@ github.com/certifi/gocertifi v0.0.0-20150906030631-84c0a38a18fc h1:zSPFItDTOJZPd
3941
github.com/certifi/gocertifi v0.0.0-20150906030631-84c0a38a18fc/go.mod h1:GJKEexRPVJrBSOjoqN5VNOIKJ5Q3RViH6eu3puDRwx4=
4042
github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
4143
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
44+
github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible h1:C29Ae4G5GtYyYMm1aztcyj/J5ckgJm2zwdDajFbx1NY=
4245
github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
46+
github.com/circonus-labs/circonusllhist v0.1.3 h1:TJH+oke8D16535+jHExHj4nQvzlZrj7ug5D7I/orNUA=
4347
github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
4448
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
4549
github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd h1:qMd81Ts1T2OTKmB4acZcyKaMtRnY5Y44NuXGX2GFJ1w=
@@ -97,6 +101,7 @@ github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfU
97101
github.com/golang/groupcache v0.0.0-20171101203131-84a468cf14b4 h1:6o8aP0LGMKzo3NzwhhX6EJsiJ3ejmj+9yA/3p8Fjjlw=
98102
github.com/golang/groupcache v0.0.0-20171101203131-84a468cf14b4/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
99103
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
104+
github.com/golang/mock v1.1.1 h1:G5FRp8JnTd7RQH5kemVNlMeyXQAztQ3mOWV95KxsXH8=
100105
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
101106
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
102107
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@@ -134,6 +139,7 @@ github.com/hashicorp/go-immutable-radix v1.0.0 h1:AKDB1HM5PWEA7i4nhcpwOrO2byshxB
134139
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
135140
github.com/hashicorp/go-msgpack v1.1.5 h1:9byZdVjKTe5mce63pRVNP1L7UAmdHOTEMGehn6KvJWs=
136141
github.com/hashicorp/go-msgpack v1.1.5/go.mod h1:gWVc3sv/wbDmR3rQsj1CAktEZzoz1YNK9NfGLXJ69/4=
142+
github.com/hashicorp/go-retryablehttp v0.5.3 h1:QlWt0KvWT0lq8MFppF9tsJGF+ynG7ztc2KIPhzRGk7s=
137143
github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
138144
github.com/hashicorp/go-uuid v1.0.0 h1:RS8zrF7PhGwyNPOtxSClXXj9HA8feRnJzgnI1RJCSnM=
139145
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
@@ -274,6 +280,7 @@ github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJy
274280
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
275281
github.com/tinylib/msgp v1.1.2 h1:gWmO7n0Ys2RBEb7GPYB9Ujq8Mk5p2U08lRnmMcGy6BQ=
276282
github.com/tinylib/msgp v1.1.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
283+
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926 h1:G3dpKMzFDjgEh2q1Z7zUUtKa8ViPtH+ocF0bE0g00O8=
277284
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
278285
github.com/tylerb/graceful v1.2.13 h1:yKdTh6eHcWdD8Jm3wxgJ6pNf8Lb3wwbV4Ip8fHbeMLE=
279286
github.com/tylerb/graceful v1.2.13/go.mod h1:LPYTbOYmUTdabwRt0TGhLllQ0MUNbs0Y5q1WXJOI9II=

probe/kubernetes/client.go

+34
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,10 @@ type Client interface {
6565
DeleteVolumeSnapshot(namespaceID, volumeSnapshotID string) error
6666
ScaleUp(namespaceID, id string) error
6767
ScaleDown(namespaceID, id string) error
68+
// Cordon or Uncordon a node based on whether `desired` is true or false respectively.
69+
CordonNode(name string, desired bool) error
70+
// Returns a list of kubernetes nodes.
71+
GetNodes() ([]apiv1.Node, error)
6872
}
6973

7074
// ResourceMap is the mapping of resource and their GroupKind
@@ -624,3 +628,33 @@ func (c *client) modifyScale(namespaceID, id string, f func(*autoscalingv1.Scale
624628
func (c *client) Stop() {
625629
close(c.quit)
626630
}
631+
632+
func (c *client) CordonNode(name string, desired bool) error {
633+
node, err := c.client.CoreV1().Nodes().Get(name, metav1.GetOptions{})
634+
if err != nil {
635+
return err
636+
}
637+
638+
helper := newCordonHelper(node)
639+
if updateRequired := helper.updateIfRequired(desired); !updateRequired {
640+
return nil
641+
}
642+
643+
err, patchErr := helper.patchOrReplace(c.client, false)
644+
if patchErr != nil {
645+
return patchErr
646+
}
647+
if err != nil {
648+
return err
649+
}
650+
return nil
651+
}
652+
653+
func (c *client) GetNodes() ([]apiv1.Node, error) {
654+
l, err := c.client.CoreV1().Nodes().List(metav1.ListOptions{})
655+
if err != nil {
656+
return nil, err
657+
}
658+
659+
return l.Items, nil
660+
}

probe/kubernetes/controls.go

+27
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ const (
2121
DeleteVolumeSnapshot = report.KubernetesDeleteVolumeSnapshot
2222
ScaleUp = report.KubernetesScaleUp
2323
ScaleDown = report.KubernetesScaleDown
24+
CordonNode = report.KubernetesCordonNode
25+
UncordonNode = report.KubernetesUncordonNode
2426
)
2527

2628
// GroupName and version used by CRDs
@@ -474,6 +476,17 @@ func (r *Reporter) CaptureJob(f func(xfer.Request, string, string) xfer.Response
474476
}
475477
}
476478

479+
// CaptureNode is exported for testing
480+
func (r *Reporter) CaptureNode(f func(xfer.Request, string) xfer.Response) func(xfer.Request) xfer.Response {
481+
return func(req xfer.Request) xfer.Response {
482+
nodeID, ok := report.ParseHostNodeID(req.NodeID)
483+
if !ok {
484+
return xfer.ResponseErrorf("Invalid ID: %s", req.NodeID)
485+
}
486+
return f(req, nodeID)
487+
}
488+
}
489+
477490
// ScaleUp is the control to scale up a deployment
478491
func (r *Reporter) ScaleUp(req xfer.Request, namespace, id string) xfer.Response {
479492
return xfer.ResponseError(r.client.ScaleUp(namespace, id))
@@ -484,6 +497,16 @@ func (r *Reporter) ScaleDown(req xfer.Request, namespace, id string) xfer.Respon
484497
return xfer.ResponseError(r.client.ScaleDown(namespace, id))
485498
}
486499

500+
// CordonNode is the control to cordon a node.
501+
func (r *Reporter) CordonNode(req xfer.Request, name string) xfer.Response {
502+
return xfer.ResponseError(r.client.CordonNode(name, true))
503+
}
504+
505+
// UncordonNode is the control to un-cordon a node.
506+
func (r *Reporter) UncordonNode(req xfer.Request, name string) xfer.Response {
507+
return xfer.ResponseError(r.client.CordonNode(name, false))
508+
}
509+
487510
func (r *Reporter) registerControls() {
488511
controls := map[string]xfer.ControlHandlerFunc{
489512
CloneVolumeSnapshot: r.CaptureVolumeSnapshot(r.cloneVolumeSnapshot),
@@ -494,6 +517,8 @@ func (r *Reporter) registerControls() {
494517
DeleteVolumeSnapshot: r.CaptureVolumeSnapshot(r.deleteVolumeSnapshot),
495518
ScaleUp: r.CaptureDeployment(r.ScaleUp),
496519
ScaleDown: r.CaptureDeployment(r.ScaleDown),
520+
CordonNode: r.CaptureNode(r.CordonNode),
521+
UncordonNode: r.CaptureNode(r.UncordonNode),
497522
}
498523
r.handlerRegistry.Batch(nil, controls)
499524
}
@@ -508,6 +533,8 @@ func (r *Reporter) deregisterControls() {
508533
DeleteVolumeSnapshot,
509534
ScaleUp,
510535
ScaleDown,
536+
CordonNode,
537+
UncordonNode,
511538
}
512539
r.handlerRegistry.Batch(controls, nil)
513540
}

probe/kubernetes/cordon.go

+87
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/*
2+
Copied from
3+
4+
https://github.com/kubernetes/kubectl/blob/master/pkg/drain/cordon.go
5+
6+
at commit f9460c53339c4bb60b20031e3c6125e8bac679e2 to add node cordon feature.
7+
*/
8+
/*
9+
Copyright 2019 The Kubernetes Authors.
10+
11+
Licensed under the Apache License, Version 2.0 (the "License");
12+
you may not use this file except in compliance with the License.
13+
You may obtain a copy of the License at
14+
15+
http://www.apache.org/licenses/LICENSE-2.0
16+
17+
Unless required by applicable law or agreed to in writing, software
18+
distributed under the License is distributed on an "AS IS" BASIS,
19+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
20+
See the License for the specific language governing permissions and
21+
limitations under the License.
22+
*/
23+
24+
package kubernetes
25+
26+
import (
27+
"encoding/json"
28+
29+
apiv1 "k8s.io/api/core/v1"
30+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
31+
"k8s.io/apimachinery/pkg/types"
32+
"k8s.io/apimachinery/pkg/util/strategicpatch"
33+
"k8s.io/client-go/kubernetes"
34+
)
35+
36+
// CordonHelper wraps functionality to cordon/uncordon nodes
37+
type cordonHelper struct {
38+
node *apiv1.Node
39+
desired bool
40+
}
41+
42+
// NewCordonHelper returns a new CordonHelper
43+
func newCordonHelper(node *apiv1.Node) *cordonHelper {
44+
return &cordonHelper{
45+
node: node,
46+
}
47+
}
48+
49+
// UpdateIfRequired returns true if c.node.Spec.Unschedulable isn't already set,
50+
// or false when no change is needed
51+
func (c *cordonHelper) updateIfRequired(desired bool) bool {
52+
c.desired = desired
53+
54+
return c.node.Spec.Unschedulable != c.desired
55+
}
56+
57+
// PatchOrReplace uses given clientset to update the node status, either by patching or
58+
// updating the given node object; it may return error if the object cannot be encoded as
59+
// JSON, or if either patch or update calls fail; it will also return a second error
60+
// whenever creating a patch has failed
61+
func (c *cordonHelper) patchOrReplace(clientset kubernetes.Interface, serverDryRun bool) (error, error) {
62+
client := clientset.CoreV1().Nodes()
63+
64+
oldData, err := json.Marshal(c.node)
65+
if err != nil {
66+
return err, nil
67+
}
68+
69+
c.node.Spec.Unschedulable = c.desired
70+
71+
newData, err := json.Marshal(c.node)
72+
if err != nil {
73+
return err, nil
74+
}
75+
76+
patchBytes, patchErr := strategicpatch.CreateTwoWayMergePatch(oldData, newData, c.node)
77+
if patchErr == nil {
78+
_, err = client.Patch(c.node.Name, types.StrategicMergePatchType, patchBytes)
79+
} else {
80+
updateOptions := metav1.UpdateOptions{}
81+
if serverDryRun {
82+
updateOptions.DryRun = []string{metav1.DryRunAll}
83+
}
84+
_, err = client.Update(c.node)
85+
}
86+
return err, patchErr
87+
}

probe/kubernetes/reporter.go

+50
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,21 @@ var (
181181
Icon: "fa fa-file-text",
182182
Rank: 2,
183183
}
184+
185+
CordonControl = []report.Control{
186+
{
187+
ID: CordonNode,
188+
Human: "Cordon",
189+
Icon: "fa fa-toggle-off",
190+
Rank: 1,
191+
},
192+
{
193+
ID: UncordonNode,
194+
Human: "Uncordon",
195+
Icon: "fa fa-toggle-on",
196+
Rank: 0,
197+
},
198+
}
184199
)
185200

186201
// Reporter generate Reports containing Container and ContainerImage topologies
@@ -345,6 +360,10 @@ func (r *Reporter) Report() (report.Report, error) {
345360
if err != nil {
346361
return result, err
347362
}
363+
hostTopology, err := r.hostTopology()
364+
if err != nil {
365+
return result, err
366+
}
348367

349368
result.Pod = result.Pod.Merge(podTopology)
350369
result.Service = result.Service.Merge(serviceTopology)
@@ -359,6 +378,8 @@ func (r *Reporter) Report() (report.Report, error) {
359378
result.VolumeSnapshot = result.VolumeSnapshot.Merge(volumeSnapshotTopology)
360379
result.VolumeSnapshotData = result.VolumeSnapshotData.Merge(volumeSnapshotDataTopology)
361380
result.Job = result.Job.Merge(jobTopology)
381+
result.Host = result.Host.Merge(hostTopology)
382+
362383
return result, nil
363384
}
364385

@@ -678,3 +699,32 @@ func (r *Reporter) namespaceTopology() (report.Topology, error) {
678699
})
679700
return result, err
680701
}
702+
703+
func (r *Reporter) hostTopology() (report.Topology, error) {
704+
result := report.MakeTopology()
705+
// Add buttons for Host view, with the ID of the Kubernetes probe
706+
for _, control := range CordonControl {
707+
control.ProbeID = r.probeID
708+
result.Controls.AddControl(control)
709+
}
710+
711+
nodes, err := r.client.GetNodes()
712+
if err != nil {
713+
return result, err
714+
}
715+
716+
for _, n := range nodes {
717+
var activeControl string
718+
if n.Spec.Unschedulable {
719+
activeControl = UncordonNode
720+
} else {
721+
activeControl = CordonNode
722+
}
723+
result.AddNode(
724+
report.MakeNode(report.MakeHostNodeID(n.Name)).
725+
WithTopology(report.Host).
726+
WithLatestActiveControls(activeControl),
727+
)
728+
}
729+
return result, nil
730+
}

probe/kubernetes/reporter_test.go

+8
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,14 @@ func (c *mockClient) Describe(namespaceID, resourceID string, groupKind schema.G
205205
return nil, nil
206206
}
207207

208+
func (c *mockClient) CordonNode(name string, desired bool) error {
209+
return nil
210+
}
211+
212+
func (c *mockClient) GetNodes() ([]apiv1.Node, error) {
213+
return nil, nil
214+
}
215+
208216
type mockPipeClient map[string]xfer.Pipe
209217

210218
func (c mockPipeClient) PipeConnection(appID, id string, pipe xfer.Pipe) error {

render/detailed/node.go

+3
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,9 @@ func controlsFor(topology report.Topology, nodeID string) []ControlInstance {
113113
}
114114
for _, controlID := range node.ActiveControls() {
115115
if control, ok := topology.Controls[controlID]; ok {
116+
if control.ProbeID != "" { // does this Control have an override for the node probe?
117+
probeID = control.ProbeID
118+
}
116119
result = append(result, ControlInstance{
117120
ProbeID: probeID,
118121
NodeID: nodeID,

report/controls.go

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ type Control struct {
1010
Icon string `json:"icon"` // from https://fortawesome.github.io/Font-Awesome/cheatsheet/ please
1111
Confirmation string `json:"confirmation,omitempty"`
1212
Rank int `json:"rank"`
13+
ProbeID string `json:"probeId,omitempty"`
1314
}
1415

1516
// Merge merges other with cs, returning a fresh Controls.

report/map_keys.go

+2
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ const (
9595
KubernetesCloneVolumeSnapshot = "kubernetes_clone_volume_snapshot"
9696
KubernetesDeleteVolumeSnapshot = "kubernetes_delete_volume_snapshot"
9797
KubernetesDescribe = "kubernetes_describe"
98+
KubernetesCordonNode = "kubernetes_cordon_node"
99+
KubernetesUncordonNode = "kubernetes_uncordon_node"
98100
// probe/awsecs
99101
ECSCluster = "ecs_cluster"
100102
ECSCreatedAt = "ecs_created_at"

0 commit comments

Comments
 (0)