Skip to content

Commit e7d5669

Browse files
authored
feat: node patch improvements (#33)
1 parent 0d31d5c commit e7d5669

File tree

5 files changed

+156
-30
lines changed

5 files changed

+156
-30
lines changed

actions/actions.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"errors"
66
"fmt"
77
"reflect"
8+
"runtime/debug"
89
"sync"
910
"time"
1011

@@ -176,6 +177,13 @@ func (s *service) startProcessing(actionID string) bool {
176177
func (s *service) handleAction(ctx context.Context, action *castai.ClusterAction) (err error) {
177178
data := action.Data()
178179
actionType := reflect.TypeOf(data)
180+
181+
defer func() {
182+
if rerr := recover(); rerr != nil {
183+
err = fmt.Errorf("panic: handling action %s: %s: %s", actionType, rerr, string(debug.Stack()))
184+
}
185+
}()
186+
179187
s.log.Infof("handling action, id=%s, type=%s", action.ID, actionType)
180188
handler, ok := s.actionHandlers[actionType]
181189
if !ok {

actions/actions_test.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,14 +140,46 @@ func TestActions(t *testing.T) {
140140
}()
141141
svc.Run(ctx)
142142
})
143+
144+
t.Run("ack with error when action handler panic occurred", func(t *testing.T) {
145+
r := require.New(t)
146+
147+
apiActions := []*castai.ClusterAction{
148+
{
149+
ID: "a1",
150+
CreatedAt: time.Now(),
151+
ActionPatchNode: &castai.ActionPatchNode{
152+
NodeName: "n1",
153+
},
154+
},
155+
}
156+
client := mock.NewMockAPIClient(apiActions)
157+
handler := &mockAgentActionHandler{panicErr: errors.New("ups")}
158+
svc := newTestService(handler, client)
159+
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Millisecond)
160+
defer func() {
161+
cancel()
162+
svc.startedActionsWg.Wait()
163+
164+
r.Empty(client.Actions)
165+
r.Len(client.Acks, 1)
166+
r.Equal("a1", client.Acks[0].ActionID)
167+
r.Contains(*client.Acks[0].Err, "panic: handling action *castai.ActionPatchNode: ups: goroutine")
168+
}()
169+
svc.Run(ctx)
170+
})
143171
}
144172

145173
type mockAgentActionHandler struct {
146174
err error
175+
panicErr error
147176
handleDelay time.Duration
148177
}
149178

150179
func (m *mockAgentActionHandler) Handle(ctx context.Context, data interface{}) error {
151180
time.Sleep(m.handleDelay)
181+
if m.panicErr != nil {
182+
panic(m.panicErr)
183+
}
152184
return m.err
153185
}

actions/patch_node_handler.go

Lines changed: 62 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package actions
22

33
import (
44
"context"
5+
"errors"
56
"fmt"
67

78
"github.com/sirupsen/logrus"
@@ -30,6 +31,21 @@ func (h *patchNodeHandler) Handle(ctx context.Context, data interface{}) error {
3031
if !ok {
3132
return fmt.Errorf("unexpected type %T for delete patch handler", data)
3233
}
34+
for k := range req.Labels {
35+
if k == "" {
36+
return errors.New("labels contain entry with empty key")
37+
}
38+
}
39+
for k := range req.Annotations {
40+
if k == "" {
41+
return errors.New("annotations contain entry with empty key")
42+
}
43+
}
44+
for _, t := range req.Taints {
45+
if t.Key == "" {
46+
return errors.New("taint contain entry with empty key")
47+
}
48+
}
3349

3450
log := h.log.WithField("node_name", req.NodeName)
3551

@@ -42,34 +58,59 @@ func (h *patchNodeHandler) Handle(ctx context.Context, data interface{}) error {
4258
return err
4359
}
4460

45-
log.Infof("patching node, labels=%v, taints=%v", req.Labels, req.Taints)
61+
log.Infof("patching node, labels=%v, taints=%v, annotations=%v", req.Labels, req.Taints, req.Annotations)
4662

4763
return patchNode(ctx, h.clientset, node, func(n *v1.Node) error {
48-
if n.Labels == nil {
49-
n.Labels = map[string]string{}
50-
}
64+
node.Labels = patchNodeMapField(node.Labels, req.Labels)
65+
node.Annotations = patchNodeMapField(node.Annotations, req.Annotations)
66+
node.Spec.Taints = patchTaints(node.Spec.Taints, req.Taints)
67+
return nil
68+
})
69+
}
70+
71+
func patchNodeMapField(values map[string]string, patch map[string]string) map[string]string {
72+
if values == nil {
73+
values = map[string]string{}
74+
}
5175

52-
for key, val := range req.Labels {
53-
n.Labels[key] = val
76+
for k, v := range patch {
77+
if k[0] == '-' {
78+
delete(values, k[1:])
79+
} else {
80+
values[k] = v
5481
}
82+
}
83+
return values
84+
}
5585

56-
seen := make(map[string]struct{}, len(n.Spec.Taints))
57-
for _, taint := range n.Spec.Taints {
58-
seen[taint.Key] = struct{}{}
86+
func patchTaints(taints []v1.Taint, patch []castai.NodeTaint) []v1.Taint {
87+
for _, v := range patch {
88+
taint := &v1.Taint{Key: v.Key, Value: v.Value, Effect: v1.TaintEffect(v.Effect)}
89+
if v.Key[0] == '-' {
90+
taint.Key = taint.Key[1:]
91+
taints = deleteTaint(taints, taint)
92+
} else if _, found := findTaint(taints, taint); !found {
93+
taints = append(taints, *taint)
5994
}
95+
}
96+
return taints
97+
}
6098

61-
for _, newTaint := range req.Taints {
62-
if _, ok := seen[newTaint.Key]; ok {
63-
continue
64-
}
65-
taint := v1.Taint{
66-
Key: newTaint.Key,
67-
Value: newTaint.Value,
68-
Effect: v1.TaintEffect(newTaint.Effect),
69-
}
70-
n.Spec.Taints = append(n.Spec.Taints, taint)
99+
func findTaint(taints []v1.Taint, t *v1.Taint) (v1.Taint, bool) {
100+
for _, taint := range taints {
101+
if taint.MatchTaint(t) {
102+
return taint, true
71103
}
104+
}
105+
return v1.Taint{}, false
106+
}
72107

73-
return nil
74-
})
108+
func deleteTaint(taints []v1.Taint, t *v1.Taint) []v1.Taint {
109+
var res []v1.Taint
110+
for _, taint := range taints {
111+
if !taint.MatchTaint(t) {
112+
res = append(res, taint)
113+
}
114+
}
115+
return res
75116
}

actions/patch_node_handler_test.go

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,26 @@ func TestPatchNodeHandler(t *testing.T) {
2424
node := &v1.Node{
2525
ObjectMeta: metav1.ObjectMeta{
2626
Name: nodeName,
27+
Labels: map[string]string{
28+
"l1": "v1",
29+
},
30+
Annotations: map[string]string{
31+
"a1": "v1",
32+
},
33+
},
34+
Spec: v1.NodeSpec{
35+
Taints: []v1.Taint{
36+
{
37+
Key: "t1",
38+
Value: "v1",
39+
Effect: v1.TaintEffectNoSchedule,
40+
},
41+
{
42+
Key: "t2",
43+
Value: "v2",
44+
Effect: v1.TaintEffectNoSchedule,
45+
},
46+
},
2747
},
2848
}
2949
clientset := fake.NewSimpleClientset(node)
@@ -36,13 +56,23 @@ func TestPatchNodeHandler(t *testing.T) {
3656
req := &castai.ActionPatchNode{
3757
NodeName: "node1",
3858
Labels: map[string]string{
39-
"label": "ok",
59+
"-l1": "",
60+
"l2": "v2",
61+
},
62+
Annotations: map[string]string{
63+
"-a1": "",
64+
"a2": "",
4065
},
4166
Taints: []castai.NodeTaint{
4267
{
43-
Key: "taint",
44-
Value: "ok2",
45-
Effect: "NoSchedule",
68+
Key: "t3",
69+
Value: "t3",
70+
Effect: string(v1.TaintEffectNoSchedule),
71+
},
72+
{
73+
Key: "-t2",
74+
Value: "",
75+
Effect: string(v1.TaintEffectNoSchedule),
4676
},
4777
},
4878
}
@@ -52,8 +82,22 @@ func TestPatchNodeHandler(t *testing.T) {
5282

5383
n, err := clientset.CoreV1().Nodes().Get(context.Background(), nodeName, metav1.GetOptions{})
5484
r.NoError(err)
55-
r.Equal("ok", n.Labels["label"])
56-
r.Equal([]v1.Taint{{Key: "taint", Value: "ok2", Effect: "NoSchedule", TimeAdded: (*metav1.Time)(nil)}}, n.Spec.Taints)
85+
86+
expectedLabels := map[string]string{
87+
"l2": "v2",
88+
}
89+
r.Equal(expectedLabels, n.Labels)
90+
91+
expectedAnnotations := map[string]string{
92+
"a2": "",
93+
}
94+
r.Equal(expectedAnnotations, n.Annotations)
95+
96+
expectedTaints := []v1.Taint{
97+
{Key: "t1", Value: "v1", Effect: "NoSchedule", TimeAdded: (*metav1.Time)(nil)},
98+
{Key: "t3", Value: "t3", Effect: "NoSchedule", TimeAdded: (*metav1.Time)(nil)},
99+
}
100+
r.Equal(expectedTaints, n.Spec.Taints)
57101
})
58102

59103
t.Run("skip patch when node not found", func(t *testing.T) {

castai/types.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,10 @@ type ActionApproveCSR struct {
8989
}
9090

9191
type ActionPatchNode struct {
92-
NodeName string `json:"nodeName"`
93-
Labels map[string]string `json:"labels"`
94-
Taints []NodeTaint `json:"taints"`
92+
NodeName string `json:"nodeName"`
93+
Labels map[string]string `json:"labels"`
94+
Taints []NodeTaint `json:"taints"`
95+
Annotations map[string]string `json:"annotations"`
9596
}
9697

9798
type NodeTaint struct {

0 commit comments

Comments
 (0)