1
16
17 package taint
18
19 import (
20 "encoding/json"
21 "fmt"
22 "strings"
23
24 "github.com/spf13/cobra"
25 "k8s.io/klog/v2"
26 "k8s.io/kubectl/pkg/explain"
27
28 v1 "k8s.io/api/core/v1"
29 "k8s.io/apimachinery/pkg/api/meta"
30 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
31 "k8s.io/apimachinery/pkg/runtime"
32 "k8s.io/apimachinery/pkg/types"
33 "k8s.io/apimachinery/pkg/util/strategicpatch"
34 "k8s.io/apimachinery/pkg/util/validation"
35 "k8s.io/cli-runtime/pkg/genericclioptions"
36 "k8s.io/cli-runtime/pkg/genericiooptions"
37 "k8s.io/cli-runtime/pkg/printers"
38 "k8s.io/cli-runtime/pkg/resource"
39 cmdutil "k8s.io/kubectl/pkg/cmd/util"
40 "k8s.io/kubectl/pkg/scheme"
41 "k8s.io/kubectl/pkg/util/completion"
42 "k8s.io/kubectl/pkg/util/i18n"
43 "k8s.io/kubectl/pkg/util/templates"
44 )
45
46
47 type TaintOptions struct {
48 PrintFlags *genericclioptions.PrintFlags
49 ToPrinter func(string) (printers.ResourcePrinter, error)
50
51 DryRunStrategy cmdutil.DryRunStrategy
52 ValidationDirective string
53
54 resources []string
55 taintsToAdd []v1.Taint
56 taintsToRemove []v1.Taint
57 builder *resource.Builder
58 selector string
59 overwrite bool
60 all bool
61 fieldManager string
62
63 ClientForMapping func(*meta.RESTMapping) (resource.RESTClient, error)
64
65 genericiooptions.IOStreams
66
67 Mapper meta.RESTMapper
68 }
69
70 var (
71 taintLong = templates.LongDesc(i18n.T(`
72 Update the taints on one or more nodes.
73
74 * A taint consists of a key, value, and effect. As an argument here, it is expressed as key=value:effect.
75 * The key must begin with a letter or number, and may contain letters, numbers, hyphens, dots, and underscores, up to %[1]d characters.
76 * Optionally, the key can begin with a DNS subdomain prefix and a single '/', like example.com/my-app.
77 * The value is optional. If given, it must begin with a letter or number, and may contain letters, numbers, hyphens, dots, and underscores, up to %[2]d characters.
78 * The effect must be NoSchedule, PreferNoSchedule or NoExecute.
79 * Currently taint can only apply to node.`))
80
81 taintExample = templates.Examples(i18n.T(`
82 # Update node 'foo' with a taint with key 'dedicated' and value 'special-user' and effect 'NoSchedule'
83 # If a taint with that key and effect already exists, its value is replaced as specified
84 kubectl taint nodes foo dedicated=special-user:NoSchedule
85
86 # Remove from node 'foo' the taint with key 'dedicated' and effect 'NoSchedule' if one exists
87 kubectl taint nodes foo dedicated:NoSchedule-
88
89 # Remove from node 'foo' all the taints with key 'dedicated'
90 kubectl taint nodes foo dedicated-
91
92 # Add a taint with key 'dedicated' on nodes having label myLabel=X
93 kubectl taint node -l myLabel=X dedicated=foo:PreferNoSchedule
94
95 # Add to node 'foo' a taint with key 'bar' and no value
96 kubectl taint nodes foo bar:NoSchedule`))
97 )
98
99 func NewCmdTaint(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command {
100 options := &TaintOptions{
101 PrintFlags: genericclioptions.NewPrintFlags("tainted").WithTypeSetter(scheme.Scheme),
102 IOStreams: streams,
103 }
104
105 validArgs := []string{"node"}
106
107 cmd := &cobra.Command{
108 Use: "taint NODE NAME KEY_1=VAL_1:TAINT_EFFECT_1 ... KEY_N=VAL_N:TAINT_EFFECT_N",
109 DisableFlagsInUseLine: true,
110 Short: i18n.T("Update the taints on one or more nodes"),
111 Long: fmt.Sprintf(taintLong, validation.DNS1123SubdomainMaxLength, validation.LabelValueMaxLength),
112 Example: taintExample,
113 ValidArgsFunction: completion.SpecifiedResourceTypeAndNameCompletionFunc(f, validArgs),
114 Run: func(cmd *cobra.Command, args []string) {
115 cmdutil.CheckErr(options.Complete(f, cmd, args))
116 cmdutil.CheckErr(options.Validate())
117 cmdutil.CheckErr(options.RunTaint())
118 },
119 }
120
121 options.PrintFlags.AddFlags(cmd)
122 cmdutil.AddDryRunFlag(cmd)
123 cmdutil.AddValidateFlags(cmd)
124 cmdutil.AddLabelSelectorFlagVar(cmd, &options.selector)
125 cmd.Flags().BoolVar(&options.overwrite, "overwrite", options.overwrite, "If true, allow taints to be overwritten, otherwise reject taint updates that overwrite existing taints.")
126 cmd.Flags().BoolVar(&options.all, "all", options.all, "Select all nodes in the cluster")
127 cmdutil.AddFieldManagerFlagVar(cmd, &options.fieldManager, "kubectl-taint")
128 return cmd
129 }
130
131
132 func (o *TaintOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) (err error) {
133 namespace, _, err := f.ToRawKubeConfigLoader().Namespace()
134 if err != nil {
135 return err
136 }
137
138 o.Mapper, err = f.ToRESTMapper()
139 if err != nil {
140 return err
141 }
142
143 o.DryRunStrategy, err = cmdutil.GetDryRunStrategy(cmd)
144 if err != nil {
145 return err
146 }
147 cmdutil.PrintFlagsWithDryRunStrategy(o.PrintFlags, o.DryRunStrategy)
148
149 o.ValidationDirective, err = cmdutil.GetValidationDirective(cmd)
150 if err != nil {
151 return err
152 }
153
154
155
156 taintArgs := []string{}
157 metTaintArg := false
158 for _, s := range args {
159 isTaint := strings.Contains(s, "=") || strings.Contains(s, ":") || strings.HasSuffix(s, "-")
160 switch {
161 case !metTaintArg && isTaint:
162 metTaintArg = true
163 fallthrough
164 case metTaintArg && isTaint:
165 taintArgs = append(taintArgs, s)
166 case !metTaintArg && !isTaint:
167 o.resources = append(o.resources, s)
168 case metTaintArg && !isTaint:
169 return fmt.Errorf("all resources must be specified before taint changes: %s", s)
170 }
171 }
172
173 o.ToPrinter = func(operation string) (printers.ResourcePrinter, error) {
174 o.PrintFlags.NamePrintFlags.Operation = operation
175 return o.PrintFlags.ToPrinter()
176 }
177
178 if len(o.resources) < 1 {
179 return fmt.Errorf("one or more resources must be specified as <resource> <name>")
180 }
181 if len(taintArgs) < 1 {
182 return fmt.Errorf("at least one taint update is required")
183 }
184
185 if o.taintsToAdd, o.taintsToRemove, err = parseTaints(taintArgs); err != nil {
186 return cmdutil.UsageErrorf(cmd, err.Error())
187 }
188 o.builder = f.NewBuilder().
189 WithScheme(scheme.Scheme, scheme.Scheme.PrioritizedVersionsAllGroups()...).
190 ContinueOnError().
191 NamespaceParam(namespace).DefaultNamespace()
192 if o.selector != "" {
193 o.builder = o.builder.LabelSelectorParam(o.selector).ResourceTypes("node")
194 }
195 if o.all {
196 o.builder = o.builder.SelectAllParam(o.all).ResourceTypes("node").Flatten().Latest()
197 }
198 if !o.all && o.selector == "" && len(o.resources) >= 2 {
199 o.builder = o.builder.ResourceNames("node", o.resources[1:]...)
200 }
201 o.builder = o.builder.LabelSelectorParam(o.selector).
202 Flatten().
203 Latest()
204
205 o.ClientForMapping = f.ClientForMapping
206 return nil
207 }
208
209
210 func (o TaintOptions) validateFlags() error {
211
212 if o.all && o.selector != "" {
213 return fmt.Errorf("setting 'all' parameter with a non empty selector is prohibited")
214 }
215
216 if !o.all && o.selector == "" {
217 if len(o.resources) < 2 {
218 return fmt.Errorf("at least one resource name must be specified since 'all' parameter is not set")
219 } else {
220 return nil
221 }
222 }
223 return nil
224 }
225
226
227 func (o TaintOptions) Validate() error {
228 resourceType := strings.ToLower(o.resources[0])
229 fullySpecifiedGVR, _, err := explain.SplitAndParseResourceRequest(resourceType, o.Mapper)
230 if err != nil {
231 return err
232 }
233
234 gvk, err := o.Mapper.KindFor(fullySpecifiedGVR)
235 if err != nil {
236 return err
237 }
238
239 if gvk.Kind != "Node" {
240 return fmt.Errorf("invalid resource type %s, only node types are supported", resourceType)
241 }
242
243
244 var conflictTaints []string
245 for _, taintAdd := range o.taintsToAdd {
246 for _, taintRemove := range o.taintsToRemove {
247 if taintAdd.Key != taintRemove.Key {
248 continue
249 }
250 if len(taintRemove.Effect) == 0 || taintAdd.Effect == taintRemove.Effect {
251 conflictTaint := fmt.Sprintf("%s=%s", taintRemove.Key, taintRemove.Effect)
252 conflictTaints = append(conflictTaints, conflictTaint)
253 }
254 }
255 }
256 if len(conflictTaints) > 0 {
257 return fmt.Errorf("can not both modify and remove the following taint(s) in the same command: %s", strings.Join(conflictTaints, ", "))
258 }
259 return o.validateFlags()
260 }
261
262
263 func (o TaintOptions) RunTaint() error {
264 r := o.builder.Do()
265 if err := r.Err(); err != nil {
266 return err
267 }
268
269 return r.Visit(func(info *resource.Info, err error) error {
270 if err != nil {
271 return err
272 }
273
274 obj := info.Object
275 name, namespace := info.Name, info.Namespace
276 oldData, err := json.Marshal(obj)
277 if err != nil {
278 return err
279 }
280 operation, err := o.updateTaints(obj)
281 if err != nil {
282 return err
283 }
284 newData, err := json.Marshal(obj)
285 if err != nil {
286 return err
287 }
288 patchBytes, err := strategicpatch.CreateTwoWayMergePatch(oldData, newData, obj)
289 createdPatch := err == nil
290 if err != nil {
291 klog.V(2).Infof("couldn't compute patch: %v", err)
292 }
293
294 printer, err := o.ToPrinter(operation)
295 if err != nil {
296 return err
297 }
298 if o.DryRunStrategy == cmdutil.DryRunClient {
299 if createdPatch {
300 typedObj, err := scheme.Scheme.ConvertToVersion(info.Object, info.Mapping.GroupVersionKind.GroupVersion())
301 if err != nil {
302 return err
303 }
304
305 nodeObj, ok := typedObj.(*v1.Node)
306 if !ok {
307 return fmt.Errorf("unexpected type %T", typedObj)
308 }
309
310 originalObjJS, err := json.Marshal(nodeObj)
311 if err != nil {
312 return err
313 }
314
315 originalPatchedObjJS, err := strategicpatch.StrategicMergePatch(originalObjJS, patchBytes, nodeObj)
316 if err != nil {
317 return err
318 }
319
320 targetObj, err := runtime.Decode(unstructured.UnstructuredJSONScheme, originalPatchedObjJS)
321 if err != nil {
322 return err
323 }
324 return printer.PrintObj(targetObj, o.Out)
325 }
326 return printer.PrintObj(obj, o.Out)
327 }
328
329 mapping := info.ResourceMapping()
330 client, err := o.ClientForMapping(mapping)
331 if err != nil {
332 return err
333 }
334 helper := resource.
335 NewHelper(client, mapping).
336 DryRun(o.DryRunStrategy == cmdutil.DryRunServer).
337 WithFieldManager(o.fieldManager).
338 WithFieldValidation(o.ValidationDirective)
339
340 var outputObj runtime.Object
341 if createdPatch {
342 outputObj, err = helper.Patch(namespace, name, types.StrategicMergePatchType, patchBytes, nil)
343 } else {
344 outputObj, err = helper.Replace(namespace, name, false, obj)
345 }
346 if err != nil {
347 return err
348 }
349
350 return printer.PrintObj(outputObj, o.Out)
351 })
352 }
353
354
355 func (o TaintOptions) updateTaints(obj runtime.Object) (string, error) {
356 node, ok := obj.(*v1.Node)
357 if !ok {
358 return "", fmt.Errorf("unexpected type %T, expected Node", obj)
359 }
360 if !o.overwrite {
361 if exists := checkIfTaintsAlreadyExists(node.Spec.Taints, o.taintsToAdd); len(exists) != 0 {
362 return "", fmt.Errorf("node %s already has %v taint(s) with same effect(s) and --overwrite is false", node.Name, exists)
363 }
364 }
365 operation, newTaints, err := reorganizeTaints(node, o.overwrite, o.taintsToAdd, o.taintsToRemove)
366 if err != nil {
367 return "", err
368 }
369 node.Spec.Taints = newTaints
370 return operation, nil
371 }
372
View as plain text